您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
You can check schedule of recently hololive stream on youtube
当前为
// ==UserScript== // @name Youtube/Holotools, Display Hololive live streaming on youtubve // @name:ja Youtube/Holotools, ホロライブ配信一覧をYoutubeで表示 // @namespace http://tampermonkey.net/ // @version 0.1.7 // @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.7"; 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"; const storage = { schedule: { get updated() { return catchParseError(() => parseInt(localStorage.ht_s_updated), 0); }, set updated(value) { localStorage.ht_s_updated = new String(value); }, get cache() { return catchParseError(() => JSON.parse(localStorage.ht_s_cache), null); }, set cache(value) { localStorage.ht_s_cache = JSON.stringify(value); }, clear: () => { localStorage.ht_s_updated = 0; delete localStorage.ht_s_cache; } }, archive: { get updated() { return catchParseError(() => parseInt(localStorage.ht_a_updated), 0); }, set updated(value) { localStorage.ht_a_updated = new String(value); }, get cache() { return catchParseError(() => JSON.parse(localStorage.ht_a_cache), null); }, set cache(value) { localStorage.ht_a_cache = JSON.stringify(value); }, clear: () => { localStorage.ht_a_updated = 0; delete localStorage.ht_a_cache; } }, } var isRefreshing = false, scheduleUpdated = 0, archiveUpdated = 0; if (location.href.indexOf("hololive.jetri.co") > 0) { GM_setValue(holotoolsHideChannels, localStorage.hideChannels); return; } var hideChannels; try { hideChannels = JSON.parse(GM_getValue(holotoolsHideChannels) || "[]") || []; } catch { hideChannels = []; } // console.log(hideChannels); initilize(); return; function catchParseError(func, defaultValue) { try { return func(); } catch { return defaultValue; } } 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:not(.hidden)"), 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 { } #hololive-schedule .contents.hidden { display: none; } `; 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"> <div id="schedule" class="contents hidden" style="left: 0px;"></div> <div id="archive" class="contents hidden" style="left: 0px;"></div> <div id="dummy" class="contents" style="left: 0px;"></div> </div> `; document.body.appendChild(container); container.addEventListener("wheel", onScrollContainer, true); 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); drawLoadingDummy(); } function onMouseEnter() { if (isRefreshing) return; if (document.querySelector("#hololive-schedule #contents #archive.hidden")) { 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 drawSchedule(streams) { // console.log(streams); try { const contents = document.querySelector("#hololive-schedule #contents #schedule"); 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 { toggleDisplay("schedule"); } } function drawLoadingDummy() { const contents = document.querySelector("#hololive-schedule #contents #dummy"); contents.innerHTML = ""; contents.style.left = "0px"; const 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: "", } const dummy = createStream(data); dummy.classList.add("dummy"); for (var i = 0; i < 20; i++) { contents.appendChild(dummy.cloneNode(true)); } } function drawArchive(streams) { // console.log(streams); try { const contents = document.querySelector("#hololive-schedule #contents #archive"); 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 || b.published_at) ? 1 : -1); for (var key in streams.videos) { var stream = createStream(streams.videos[key]); stream.classList.add("ended"); contents.appendChild(stream); } } } finally { toggleDisplay("archive"); } } function toggleDisplay(target) { document.querySelectorAll(`#hololive-schedule .contents`).forEach((element, index) => { element.classList.toggle("hidden", element.id != target); }); } function refreshSchedule() { // console.log("refreshSchedule()"); if (isRefreshing) return; isRefreshing = true; try { const cacheUpdated = storage.schedule.updated; const newestUpdated = Math.max(cacheUpdated, scheduleUpdated); // console.log(cacheUpdated, scheduleUpdated, storage.schedule.cache); // 十分に新しい if (new Date().getTime() - newestUpdated < REFRESH_INTERVAL) { // キャッシュのが新しい if (cacheUpdated > scheduleUpdated) { const cache = storage.schedule.cache; if (cache) { drawSchedule(cache); isRefreshing = false; return; } } else { isRefreshing = false; toggleDisplay("schedule"); return; } } } catch (ex) { console.log(ex); storage.schedule.clear(); } toggleDisplay("dummy"); 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) { storage.schedule.updated = scheduleUpdated = new Date().getTime(); storage.schedule.cache = response; drawSchedule(response); } } catch (ex) { console.log(ex, context); } finally { isRefreshing = false; } }, onerror: setTimeoutToRefresh, onabort: setTimeoutToRefresh, ontimeout: setTimeoutToRefresh, }); } function refreshArchive() { // console.log("refreshArchive()"); if (isRefreshing) return; isRefreshing = true; try { const cacheUpdated = storage.archive.updated; const newestUpdated = Math.max(cacheUpdated, archiveUpdated); // 十分に新しい if (new Date().getTime() - newestUpdated < REFRESH_INTERVAL) { // キャッシュのが新しい if (cacheUpdated > archiveUpdated) { const cache = storage.archive.cache; if (cache) { drawArchive(cache); isRefreshing = false; return; } } else { isRefreshing = false; toggleDisplay("archive"); return; } } } catch (ex) { console.log(ex); storage.archive.clear(); } 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()}`); toggleDisplay("dummy"); 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) { storage.archive.updated = archiveUpdated = new Date().getTime(); storage.archive.cache = response; drawArchive(response); } } catch (ex) { console.log(ex, context); } finally { 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}`; } } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址