Youtube/Holotools, Display Hololive live streaming on youtubve

You can check schedule of recently hololive stream on youtube

As of 2020-12-06. See the latest version.

// ==UserScript==
// @name         Youtube/Holotools, Display Hololive live streaming on youtubve
// @name:ja      Youtube/Holotools, ホロライブ配信一覧をYoutubeで表示
// @namespace    http://tampermonkey.net/
// @version      0.1.2
// @description  You can check schedule of recently hololive stream on youtube
// @description:ja ホロライブの直近のスケジュールをYoutubeで確認できます
// @author       You
// @match        https://www.youtube.com*
// @match        https://www.youtube.com/*
// @match        https://hololive.jetri.co/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-end
// @noframes
// @unwrap
// ==/UserScript==

(function() {
    'use strict';
    const REFRESH_INTERVAL = 10 * 60 * 1000; // 10 mins
    const FREECHAT_SCHEDULE = 7 * 24 * 60 * 60 * 1000; // 7 days
    const ERROR_TIMEOUT = 3000; // 3 secs
    const holotoolsHideChannels = "holotoolsHideChannels";

    if (location.href.indexOf("hololive.jetri.co") > 0) {
        GM_setValue(holotoolsHideChannels, localStorage.hideChannels);
        return;
    }

    var hideChannels;
    try {
        hideChannels = JSON.parse(GM_getValue(holotoolsHideChannels) || "[]") || [];
    } catch (ex) {
        hideChannels = [];
    }

    // console.log(hideChannels);

    initilize();

    return;

    function initilize() {
        if (document.enabledHololiveSchedule) return;
        document.enabledHololiveSchedule = true;

        buildScheduleBase();
    }

    function onScrollContainer(ev) {
        // console.log(ev);

        ev.preventDefault();
        ev.stopPropagation();
        ev.stopImmediatePropagation();

        // console.log("on wheel", ev, document.querySelector("#hololive-schedule").clientWidth, document.documentElement.clientWidth, container.style.left);

        const container = document.querySelector("#hololive-schedule"),
              lp = parseFloat(container.style.left),
              dw = document.documentElement.clientWidth,
              cw = container.clientWidth;

        if (ev.deltaY > 0) {
            container.style.left = Math.max(lp - dw / 5, - cw + dw) + "px";
        } else if (ev.deltaY < 0) {
            container.style.left = Math.min(lp + dw / 5, 0) + "px";
        }

        return false;
    }

    function buildScheduleBase() {
        var container = document.createElement("div");
        container.id = "hololive-schedule";
        container.style.left = "0px";
        container.onmouseenter = refreshSchedule;
        document.body.appendChild(container);

        container.addEventListener("wheel", onScrollContainer, true);

        var style = document.createElement("style");
        style.id = "hololive-schedule-style";
        style.innerHTML = `
#hololive-schedule {
    position: fixed;
    bottom: 0;
    left: 0;
    overflow-y: hidden;
    overflow-x: scroll;
    opacity: 0;
    padding: 8px;
    max-height: 20px;
    white-space: nowrap;
    scrollbar-width: none;
    box-sizing: border-box;
    z-index: 10000;
    transition: max-height 0.5s linear, opacity .1s linear, left 0.2s ease;
    background: #fff;
}
#hololive-schedule:hover {
    max-height: 200px;
    opacity: 1;
}
.no-scroll #hololive-schedule {
    pointer-events: none;
}
.no-scroll #hololive-schedule[visible_] {
    pointer-events: initial;
}
.hololive-stream {
    display: inline-block;
    position: relative;
    border: solid 3px;
    border-raduis: 3px;
    font-size: 10px;
}
.hololive-stream.hide-channel {
    display: none;
}
.hololive-stream + .hololive-stream {
    margin-left: 8px;
}
.hololive-stream .thumbnail {
    vertical-align: middle;
    width: 128px;
    max-height: 72px;
}
.hololive-stream .title-wrapper {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    overflow: hidden;
    color: #202020;
    background: #fffa;
    z-index: 1;
}
.hololive-stream:hover .title {
    animation: textAnimation 4s linear infinite;
}
@keyframes textAnimation {
    0% { margin-left: 0; }
    100% { margin-left: -350%; }
}
.hololive-stream .date {
    position: absolute;
    padding: 1px 3px;
    color: #333;
    background: #fff;
    font-weight: bold;
    z-index: 1;
}
.hololive-stream .photo {
    position: absolute;
    right: -8px;
    top: -8px;
    width: 32px;
    height: 32px;
    border: solid 2px #fff;
    border-radius: 18px;
    z-index: 1;
}
.hololive-stream.live .photo {
    border-color: crimson;
}
.hololive-stream.upcoming .photo {
    border-color: deepskyblue;
}
.hololive-stream.ended .photo {
    border-color: gray;
}
.hololive-stream.live {
    border-color: crimson;
}
.hololive-stream.upcoming {
    border-color: deepskyblue;
}
.hololive-stream.ended {
    border-color: gray;
}
`;
        document.body.appendChild(style);

        document.querySelector("ytd-app").addEventListener("scroll", (e) => {
            if (e.target.scrollTop > 100) {
                container.setAttribute("visible_", "");
            } else {
                container.removeAttribute("visible_");
            }
        });

        refreshSchedule();
    }

    function createStream(details) {
        var stream = document.createElement("a");
        stream.classList.add("hololive-stream");
        if (hideChannels.find((x) => x == details.channel.yt_channel_id)) {
            stream.classList.add("hide-channel");
        }
        stream.href = "/watch/" + details.yt_video_key;
        stream.innerHTML = `
<div class="date">${toLiveDate(details.live_schedule, details.live_start, details.live_end)}</div>
<img class="thumbnail" src="https://i.ytimg.com/vi/${details.yt_video_key}/mqdefault.jpg?${new Date(details.live_start || details.live_schedule).getTime()}">
<div class="title-wrapper"><div class="title">${details.title}</div></div>
<img class="photo" src="${details.channel.photo}" />
`;

        return stream;
    }

    function updateSchedule(streams) {
        const contents = document.querySelector("#hololive-schedule");
        contents.innerHTML = ``;
        Array.prototype.map.call(contents.querySelectorAll(".hololive-stream"), (e) => e.remove());
        contents.style.left = "0px";

        streams.live.sort((a, b) => (a.live_start || a.live_schedule) < (b.live_start || b.live_schedule))
            .forEach((details) => {
                var stream = createStream(details);
                stream.classList.add("live");
                contents.appendChild(stream);
        });

        streams.upcoming.sort((a, b) => a.live_schedule > b.live_schedule)
            .filter((a) => new Date(a.live_schedule).getTime() - new Date().getTime() < FREECHAT_SCHEDULE)
            .forEach((details) => {
                var stream = createStream(details);
                stream.classList.add("upcoming");
                contents.appendChild(stream);
        });

        streams.ended.sort((a, b) => a.live_start > b.live_start)
            .forEach((details) => {
                var stream = createStream(details);
                stream.classList.add("ended");
                contents.appendChild(stream);
        });
    }

    var currentData, isRefreshing = false;

    function refreshSchedule() {
        // console.log("refreshSchedule()");
        if (isRefreshing) {
            return;
        }
        isRefreshing = true;

        if (currentData && new Date().getTime() - currentData.updateTime < REFRESH_INTERVAL) {
            // console.log("schedule is enough to new");
            isRefreshing = false;
            return;
        }

        try {
            var cache = currentData = JSON.parse(localStorage.holotoolsyoutube);
            if (new Date().getTime() - cache.updateTime < REFRESH_INTERVAL) {
                // console.log("update schedule with cache");
                updateSchedule(cache);

                isRefreshing = false;
                return;
            }
        } catch (ex) {
            // blank
        }

        GM_xmlhttpRequest({
            method: "GET",
            url: "https://api.holotools.app/v1/live?max_upcoming_hours=2190&hide_channel_desc=1",
            headers: { "user-agent": "holotoolsyoutube" },
            onload: (context) => {
                var data = currentData = JSON.parse(context.responseText);
                data.updateTime = new Date().getTime();
                localStorage.holotoolsyoutube = JSON.stringify(data);

                updateSchedule(data);

                isRefreshing = false;
            },
            onerror: setTimeoutToRefresh,
            onabort: setTimeoutToRefresh,
            ontimeout: setTimeoutToRefresh,
        });
    }

    function setTimeoutToRefresh() {
        setTimeout(() => {
            isRefreshing = false;
        }, ERROR_TIMEOUT);
    }

    function toLiveDate(schedule, start, end) {
        if (end) {
            start = new Date(start);
            end = new Date(end);

            var diff = end.getTime() - start.getTime();
            var dm = Math.floor(diff / 1000 / 60 % 60);
            var dh = Math.floor(diff / 1000 / 60 / 60);

            var span = "";
            if (dh > 0) {
                span += dh + "h ";
            }
            span += dm + "m";

            return span;
        }

        start = new Date(start || schedule);
        var h = ("0" + start.getHours()).slice(-2);
        var m = ("0" + start.getMinutes()).slice(-2);
        if (start.getDate() != new Date().getDate()) {
            return `${start.getDate()}-${h}:${m}`;
        } else {
            return `${h}:${m}`;
        }
    }
})();