Greasy Fork is available in English.

Youtube/Holotools, Display Hololive live streaming on youtubve

You can check schedule of recently hololive stream on youtube

Fra og med 01.05.2021. Se den nyeste version.

// ==UserScript==
// @name         Youtube/Holotools, Display Hololive live streaming on youtubve
// @name:ja      Youtube/Holotools, ホロライブ配信一覧をYoutubeで表示
// @namespace    http://tampermonkey.net/
// @version      0.1.6
// @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 VERSION = "0.1.6";
    const REFRESH_INTERVAL = 5 * 60 * 1000; // 5 mins
    const FREECHAT_SCHEDULE = 7 * 24 * 60 * 60 * 1000; // 7 days
    const ARCHIVE_ENDHOURS = 36 * 60 * 60 * 1000; // 36 hours
    const ERROR_TIMEOUT = 3000; // 3 secs
    const holotoolsHideChannels = "holotoolsHideChannels";
    var scheduleCache, archiveCache,
        isRefreshing = false,
        isDisplaySchedule = null;

    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 contents = document.querySelector("#hololive-schedule #contents"),
              lp = parseFloat(contents.style.left),
              dw = document.documentElement.clientWidth,
              cw = contents.clientWidth;

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

        return false;
    }

    function buildScheduleBase() {
        var style = document.createElement("style");
        style.id = "hololive-schedule-style";
        style.innerHTML = `
#hololive-schedule {
    --background-color: #ffffff;
    --icon-hover-background-color: #d9d9d9;
    --icon-fill: #212121;
}
[dark=true] #hololive-schedule {
    --background-color: #212121;
    --icon-hover-background-color: #4c4c4c;
    --icon-fill: #ffffff;
}

#hololive-schedule {
    position: fixed;
    bottom: 0;
    left: 0;
    min-width: 100%;
    opacity: 0;
    scrollbar-width: none;
    box-sizing: border-box;
    z-index: 20000;
    transition: opacity .1s linear;
}
#hololive-schedule:hover {
    opacity: 1;
}
#hololive-schedule #contents {
    position: absolute;
    bottom: 0;
    left: 0;
    min-width: 100%;
    max-height: 20px;
    padding: 8px 32px 8px 0;
    white-space: nowrap;
    scrollbar-width: none;
    box-sizing: border-box;
    transition: left .2s ease, max-height .5s linear;
    overflow-y: hidden;
    overflow-x: scroll;
    background-color: var(--background-color);
}
#hololive-schedule:hover #contents {
    max-height: 200px;
    scrollbar-width: none;
}
#hololive-schedule #contents::-webkit-scrollbar {
    display: none;
}
.no-scroll #hololive-schedule {
    pointer-events: none;
}
.no-scroll #hololive-schedule[visible_] {
    pointer-events: unset;
}
.hololive-stream {
    display: inline-block;
    position: relative;
    border: solid 3px;
    border-raduis: 3px;
    font-size: 10px;
    margin: 0 0 0 8px;
}
.hololive-stream.hide-channel {
    display: none;
}
.hololive-stream .thumbnail {
    vertical-align: middle;
    width: 128px;
    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: #202020;
    background: #fffd;
    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;
    background-color: var(--background-color);
    background-size: cover;
}
.hololive-stream.live, .hololive-stream.live .photo {
    border-color: crimson;
}
.hololive-stream.upcoming, .hololive-stream.upcoming .photo {
    border-color: deepskyblue;
}
.hololive-stream.ended, .hololive-stream.ended .photo {
    border-color: gray;
}
.hololive-stream.dummy {
    pointer-events: none;
}

#hololive-schedule #tools {
    display: flex;
    flex-direction: column;
    position: absolute;
    right: 0;
    height: calc(72px + 16px + 6px);
    bottom: 0;
    z-index: 2;
    max-height: 20px;
    width: 32px;
    transition: max-height .5s linear, opacity .2s linear, width .1s linear;
}
#hololive-schedule:hover #tools {
    max-height: 200px;
    opacity: 1;
}
#hololive-schedule #tools:hover {
    width: 60px;
}
#hololive-schedule #tools a {
    position: relative;
    display: block;
    flex-grow: 1;
    cursor: pointer;
    vertical-align: middle;
    text-align: center;
    background: linear-gradient(to right, #fff0 0%, var(--background-color) 30%, var(--background-color) 100%);
    transition: background .1s linear, padding .1s linear;
}
#hololive-schedule #tools a:hover {
    background: linear-gradient(to right, #fff0 0%, var(--icon-hover-background-color) 30%, var(--icon-hover-background-color) 100%);
}
#hololive-schedule #tools a svg {
    display: inlnie-block;
    height: 100%;
}
#hololive-schedule #tools a svg .shape {
    fill: var(--icon-fill);
    transition: fill .1s linear;
}
#hololive-schedule #tools a:hover svg .shape {
}
`;
        document.body.appendChild(style);

        var localize = {
            ja: {
                schedule: "スケジュールを見る",
                archive: "終了した放送を見る",
            },
            en: {
                schedule: "View scheulde",
                archive: "View finished streaming",
            }
        }
        var t = localize[window.navigator.language] || localize.en;

        var container = document.createElement("div");
        container.id = "hololive-schedule";
        container.onmouseenter = onMouseEnter;
        container.innerHTML = `
<div id="tools">
<a id="schedule" title="${t.schedule}">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 512 512" style="padding: 2px">
	<path class="shape" d="M256,0C114.625,0,0,114.625,0,256c0,141.374,114.625,256,256,256c141.374,0,256-114.626,256-256
		C512,114.625,397.374,0,256,0z M351.062,258.898l-144,85.945c-1.031,0.626-2.344,0.657-3.406,0.031
		c-1.031-0.594-1.687-1.702-1.687-2.937v-85.946v-85.946c0-1.218,0.656-2.343,1.687-2.938c1.062-0.609,2.375-0.578,3.406,0.031
		l144,85.962c1.031,0.586,1.641,1.718,1.641,2.89C352.703,257.187,352.094,258.297,351.062,258.898z"></path>
</svg>
</a>
<a id="archive" title="${t.archive}">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="28px" height="28px" viewBox="0 0 512 512">
	<path class="shape" d="M464,56v40h-40V56H88v40H48V56H0v400h48v-40h40v40h336v-40h40v40h48V56H464z M88,354.672H48v-64h40V354.672z
		 M88,221.328H48v-64h40V221.328z M308.094,266.047l-101.734,60.734c-0.75,0.438-1.656,0.469-2.406,0.031
		c-0.734-0.422-1.203-1.219-1.203-2.094V264v-60.719c0-0.859,0.469-1.656,1.203-2.094c0.75-0.406,1.656-0.391,2.406,0.031
		l101.734,60.75c0.719,0.406,1.156,1.203,1.156,2.031C309.25,264.844,308.813,265.625,308.094,266.047z M464,354.672h-40v-64h40
		V354.672z M464,221.328h-40v-64h40V221.328z"></path>
</svg>
</a>
</div>
<div id="contents" style="left: 0px;"></div>
`;
        document.body.appendChild(container);

        container.addEventListener("wheel", onScrollContainer, true);
        const contents = container.querySelector("#contents");

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

        container.querySelector("#tools #schedule").addEventListener("click", refreshSchedule);
        container.querySelector("#tools #archive").addEventListener("click", refreshArchive);
        refreshSchedule();
    }

    function onMouseEnter() {
        if (isRefreshing) return;

        if (isDisplaySchedule) {
            refreshSchedule();
        } else {
            refreshArchive();
        }
    }

    function createStream(details) {
        var stream = document.createElement("a");
        stream.data = details;
        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, details.published_at)}</div>
<img class="thumbnail" src="https://i.ytimg.com/vi/${details.yt_video_key}/mqdefault.jpg?${new Date(details.live_start || details.live_schedule || details.published_at).getTime()}">
<div class="title-wrapper"><div class="title">${details.title}</div></div>
<div class="photo" style="background-image: url('${details.channel.photo}');" />
`;

        return stream;
    }

    function updateSchedule(streams) {
        // console.log(streams);
        try {
            const contents = document.querySelector("#hololive-schedule #contents");
            contents.innerHTML = "";
            contents.style.left = "0px";

            var key, stream;

            if (streams) {
                if (streams.live) {
                    streams.live = streams.live
                        .filter(x => x.status != "missing");
                    streams.live
                        .sort((a, b) => (a.live_start || a.live_schedule) < (b.live_start || b.live_schedule) ? 1 : -1);
                    for (key in streams.live) {
                        stream = createStream(streams.live[key]);
                        stream.classList.add("live");
                        contents.appendChild(stream);
                    }
                }

                if (streams.upcoming) {
                    streams.upcoming = streams.upcoming
                        .filter(x => x.status != "missing")
                        .filter(x => new Date(x.live_schedule).getTime() - new Date().getTime() < FREECHAT_SCHEDULE);
                    streams.upcoming
                        .sort((a, b) => a.live_schedule > b.live_schedule ? 1 : -1);
                    for (key in streams.upcoming) {
                        stream = createStream(streams.upcoming[key]);
                        stream.classList.add("upcoming");
                        contents.appendChild(stream);
                    }
                }

                if (streams.ended) {
                    streams.ended = streams.ended
                        .filter(x => x.status != "missing");
                    streams.ended.sort((a, b) => a.live_start > b.live_start ? 1 : -1);
                    for (key in streams.ended) {
                        stream = createStream(streams.ended[key]);
                        stream.classList.add("ended");
                        contents.appendChild(stream);
                    }
                }
            }
        } finally {
            isDisplaySchedule = true;
        }
    }

    function updateLoadingDummy() {
        const contents = document.querySelector("#hololive-schedule #contents");
        contents.innerHTML = "";
        contents.style.left = "0px";
        var data = {
            bb_video_id: null,
            channel:
            {
                bb_space_id: null,
                id: 0,
                name: "Loading",
                photo: "https://www.youtube.com/s/desktop/fe7279a7/img/favicon_144.png",
                published_at: "2000-01-01T00:00:00.000Z",
                subscriber_count: 0,
                twitter_link: "",
                video_count: 0,
                view_count: 0,
                yt_channel_id: "",
            },
            id: 0,
            live_end: null,
            live_schedule: "2222-22-22T22:22:22.222Z",
            live_start: null,
            live_viewers: null,
            status: "upcoming",
            thumbnail: null,
            title: "Comming soon ...",
            yt_video_key: "",
        }
        var dummy = createStream(data);
        dummy.classList.add("dummy");

        for (var i = 0; i < 20; i++) {
            contents.appendChild(dummy.cloneNode(true));
        }
    }

    function updateArchive(streams) {
        // console.log(streams);

        try {
            const contents = document.querySelector("#hololive-schedule #contents");
            contents.style.left = "0px";
            contents.innerHTML = "";

            if (streams && streams.videos) {
                streams.videos = streams.videos
                    .filter(x => x.status == "past");
                streams.videos
                    .sort((a, b) => (a.live_end || a.published_at) < (b.live_end || a.published_at) ? 1 : -1);
                for (var key in streams.videos) {
                    var stream = createStream(streams.videos[key]);
                    stream.classList.add("ended");
                    contents.appendChild(stream);
                }
            }
        } finally {
            isDisplaySchedule = false;
        }
    }

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

        try {
            var latestUpdated = parseInt(localStorage.holotoolsLatestUpdated);
            scheduleCache = JSON.parse(localStorage.holotoolsScheduleCache);

            if (new Date().getTime() - latestUpdated < REFRESH_INTERVAL && scheduleCache) {
                // console.log("update schedule with cache");
                if (isDisplaySchedule != true) {
                    updateSchedule(scheduleCache);
                }
                isRefreshing = false;
                return;
            }
        } catch (ex) {
            console.log(ex, localStorage.holotoolsScheduleCache);

            scheduleCache = null;
            delete localStorage.holotoolsScheduleCache;
            delete localStorage.holotoolsLatestUpdated;
        }

        // console.log("refresh");
        updateLoadingDummy();

        GM_xmlhttpRequest({
            method: "GET",
            url: "https://api.holotools.app/v1/live?max_upcoming_hours=2190&hide_channel_desc=1",
            headers: { "user-agent": `holotoolsyoutube/${VERSION}` },
            onload: (context) => {
                // console.log(context);
                try {
                    var response = JSON.parse(context.responseText);
                    if (!response.message) {
                        scheduleCache = response;
                        localStorage.holotoolsScheduleCache = JSON.stringify(scheduleCache);
                        localStorage.holotoolsLatestUpdated = new Date().getTime();

                        updateSchedule(scheduleCache);
                    }
                } catch (ex) {
                    console.log(ex, context);
                }

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

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

        try {
            var latestUpdate = parseInt(localStorage.holotoolsArchiveLatestUpdated);
            archiveCache = JSON.parse(localStorage.holotoolsArchiveCache);

            if (new Date().getTime() - latestUpdate < REFRESH_INTERVAL && archiveCache) {
                // console.log("update schedule with cache");
                if (isDisplaySchedule != false) {
                    updateArchive(archiveCache);
                }
                isRefreshing = false;
                return;
            }
        } catch (ex) {
            console.log(ex, localStorage.holotoolsArchiveCache);

            archiveCache = null;
            delete localStorage.holotoolsArchiveCache;
            delete localStorage.holotoolsArchiveLatestUpdated;
        }

        var now = new Date().getTime();
        var offset = new Date().getTimezoneOffset() * 60 * 1000;

        var start = new Date();
        start.setTime(now - ARCHIVE_ENDHOURS - offset);
        var end = new Date();
        end.setTime(now - offset);

        // console.log(`${start.toISOString()}${end.toISOString()}`);
        updateLoadingDummy();

        GM_xmlhttpRequest({
            method: "GET",
            url: `https://api.holotools.app/v1/videos?start_date=${start.toISOString()}&end_date=${end.toISOString()}&limit=50&sort=live_schedule&order=desc`,
            headers: { "user-agent": `holotoolsyoutube/${VERSION}` },
            onload: (context) => {
                // console.log(context);
                try {
                    var response = JSON.parse(context.responseText);
                    if (!response.message) {
                        archiveCache = response;
                        localStorage.holotoolsArchiveCache = JSON.stringify(archiveCache);
                        localStorage.holotoolsArchiveLatestUpdated = new Date().getTime();

                        updateArchive(archiveCache);
                    }
                } catch (ex) {
                    console.log(ex, context);
                }

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

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

    function toLiveDate(schedule, start, end, publish) {
        // console.log(schedule, start, end, publish);

        if (end) {
            var diff = new Date(end).getTime() - new Date(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 || publish);
        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}`;
        }
    }
})();