您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
You can check schedule of recently hololive stream on youtube
当前为
// ==UserScript== // @name Youtube/Holodex, Hololive streaming schedule // @name:ja Youtube/Holodex, ホロライブ配信スケジュール // @namespace http://tampermonkey.net/ // @version 1.0.1 // @description You can check schedule of recently hololive stream on youtube // @description:ja ホロライブの直近のスケジュールをYoutubeで確認できます // @author You // @icon https://holo.poi.cat/youtube-stream/assets/icons/icon-72x72.png // @match https://www.youtube.com* // @match https://www.youtube.com/* // @match https://hololive.jetri.co/* // @match https://archive.net/* // @grant GM.xmlHttpRequest // @grant GM.getValue // @grant GM.setValue // @run-at document-end // @noframes // ==/UserScript== (function() { 'use strict'; const VERSION = "1.0.1", APPNAME = "DHLSOY"; const REFRESH_INTERVAL = 5 * 60 * 1000; // 5 mins const ERROR_TIMEOUT = 3000; // 3 secs const storage = { schedule: { get options() { return catchParseError(() => JSON.parse(localStorage.ht_s_options), null); }, set options(value) { localStorage.ht_s_options = JSON.stringify(value); }, }, archive: { get options() { return catchParseError(() => JSON.parse(localStorage.ht_a_options), null); }, set options(value) { localStorage.ht_a_options = JSON.stringify(value); }, } } var holodex = new Holodex(), quickBar = new QuickBar(), customView = new CustomViewer(), customFilters; // console.log(filterTree, filters); initilize(); function initilize() { if (document.enabledHololiveSchedule) return; document.enabledHololiveSchedule = true; try { customFilters = initilizeFilters(); quickBar.render(); customView.render(); } catch(ex) { console.log(ex); } } function catchParseError(func, defaultValue) { try { return func(); } catch { return defaultValue; } } function QuickBar() { const self = this; const localize = { ja: { archive: "終了した放送を見る", powered: "参照元: Holodex", }, en: { archive: "View archives", powered: "Source: Holotools", } } const t = localize[window.navigator.language] || localize.en; this.$preview = null; this.$previewThumbnail = null; this.$previewTitle = null; // ui this.render = async function () { this.filter = customFilters.all; this.$style = document.createElement("style"); this.$style.id = "hololive-schedule-style"; this.$style.innerHTML = ` #hololive-schedule { --color: #212121; --background-color: #ffffff; --hover-background-color: #ececec; --icon-hover-background-color: #d9d9d9; --icon-fill: #212121; } [dark=true] #hololive-schedule { --color: #ffffff; --background-color: #212121; --hover-background-color: #404040; --icon-hover-background-color: #4c4c4c; --icon-fill: #ffffff; } #hololive-schedule { position: fixed; bottom: 0; left: 0; min-width: 100%; min-height: 97px; opacity: 0; scrollbar-width: none; box-sizing: border-box; background-color: var(--background-color); z-index: 2031; /* drwaer is 2030 */ transition: opacity .1s linear, height .3s linear; pointer-events: none; } #hololive-schedule:hover { opacity: 1; scrollbar-width: none; } #hololive-schedule.visible { pointer-events: unset; } #hololive-schedule #schedule { position: relative; bottom: 0; left: 0; min-width: 100%; padding: 8px; white-space: nowrap; scrollbar-width: none; box-sizing: border-box; overflow-y: hidden; overflow-x: scroll; background-color: var(--background-color); transition: left .2s ease, max-height .5s linear; } #hololive-schedule:hover #schedule { scrollbar-width: none; } #hololive-schedule #schedule::-webkit-scrollbar { display: none; } #hololive-schedule .hololive-stream { display: inline-block; position: relative; border: solid 3px; border-radius: 3px; font-size: 10px; margin: 0 0 0 8px; } #hololive-schedule .hololive-stream.hidden { display: none; } #hololive-schedule .hololive-stream .thumbnail { vertical-align: middle; width: 128px; height: 72px; border-radius: 2px; background-size: contain; background-repeat: 1; background-position: center center; opacity: 0; transition: opacity 0.1s ease; } #hololive-schedule .hololive-stream .thumbnail.loaded { opacity: 1; } #hololive-schedule .hololive-stream .title-wrapper { position: absolute; bottom: 0; left: 0; right: 0; overflow: hidden; color: #202020; background: #fffa; z-index: 1; } #hololive-schedule .hololive-stream:hover .title { /*animation: textAnimation 4s linear infinite;*/ } #hololive-schedule .hololive-stream .date { position: absolute; padding: 1px 3px; color: #202020; background: #fffd; font-weight: bold; z-index: 1; } #hololive-schedule .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; opacity: 0; transition: opacity 0.1s ease; } #hololive-schedule .hololive-stream .photo.loaded { opacity: 1; } #hololive-schedule .hololive-stream[stream-status="live"], #hololive-schedule .hololive-stream[stream-status="live"] .photo { border-color: crimson; } #hololive-schedule .hololive-stream[stream-status="upcoming"], #hololive-schedule .hololive-stream[stream-status="upcoming"] .photo { border-color: deepskyblue; } #hololive-schedule .hololive-stream[stream-status="ended"], #hololive-schedule .hololive-stream[stream-status="ended"] .photo { border-color: gray; } #hololive-schedule .tools { display: block; position: absolute; height: fit-content; top: 0; transform: translateY(-100%); background-color: var(--background-color); color: var(--icon-fill); vertical-align: middle; transition: opacity .2s linear, width .1s linear; } #hololive-schedule .tools.left { left: 0; padding-left: 8px; border-radius: 0 5px 0 0; } #hololive-schedule .tools a { display: inline-block; padding: 6px 8px; cursor: pointer; vertical-align: middle; color: var(--icon-fill); text-decoration: none; text-align: center; transition: background .1s linear, padding .1s linear; } #hololive-schedule .tools a:last-child { border-radius: 0 5px 0 0; } #hololive-schedule .tools a:hover { background: var(--hover-background-color); } #hololive-schedule .tools a svg { display: inlnie-block; height: 14px; } #hololive-schedule .tools a svg .shape { fill: var(--icon-fill); transition: fill .1s linear; } #hololive-schedule #contents .contents.hidden { display: none; width: 0; } #hololive-schedule #preview { position: fixed; bottom: 100px; left: 0; right: 0; text-align: center; z-index: 2031; pointer-events: none; transition: opacity 0.1s ease; } #hololive-schedule #preview.hidden { opacity: 0; } #hololive-schedule #thumbnail-preview { display: block; margin: 0 auto; width: 512px; height: 288px; background-size: 100%; background-position: center; border-radius: 4px; z-index: 2031; pointer-events: none; box-shadow: #000 0px 0px 12px; border: solid 5px; } #hololive-schedule #title-preview { display: inline-block; font-size: 20px; padding: 6px 12px; border-radius: 4px; color: #303030; background: #efefef; white-space: nowrap; margin-top: 8px; } #hololive-schedule #thumbnail-preview[stream-status="live"] { border-color: crimson; } #hololive-schedule #thumbnail-preview[stream-status="upcoming"] { border-color: deepskyblue; } #hololive-schedule #thumbnail-preview[stream-status="ended"] { border-color: gray; } `; document.body.appendChild(this.$style); this.$container = document.createElement("div"); this.$container.id = "hololive-schedule"; this.$container.classList.add("visible"); this.$container.innerHTML = ` <div class="tools left"> <a class="filter" filter="all"><span>All</span></a> <a class="filter" filter="jp"><span>JP</span></a> <a class="filter" filter="en"><span>EN</span></a> <a class="filter" filter="id"><span>ID</span></a> <a class="filter" filter="stars">STARS</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> <a id="powered" href="https://holodex.net/" target="blank_">${t.powered}</a> </div> <div id="schedule" style="left: 0px;"> </div> <div id="preview" class="hidden"> <div id="thumbnail-preview"></div> <div id="title-preview"></div> </div> `; document.body.appendChild(this.$container); this.$tools = this.$container.querySelector(".tools"); this.$schedule = this.$container.querySelector("#schedule"); this.$preview = this.$container.querySelector("#preview"); this.$previewTitle = this.$preview.querySelector("#title-preview"); this.$previewThumbnail = this.$preview.querySelector("#thumbnail-preview"); // events window.addEventListener("wheel", this.updateVisibility); this.$container.addEventListener("wheel", this.onScrollContainer, true); this.$container.addEventListener("mouseenter", async (ev) => { // console.log("mouseenter", ev.target.classList.toString()); this.updateVisibility(); if (await this.updateSchedule()) { this.drawSchedule(); } }, false); this.$tools.addEventListener("mouseenter", (ev) => { self.$preview.classList.add("hidden"); }, false); this.$container.addEventListener("mouseleave", (ev) => { this.$preview.classList.add("hidden"); }, false); this.updateVisibility(); this.$container.querySelectorAll(".filter").forEach($filter => { var filter = customFilters[$filter.getAttribute("filter")]; if (filter) { $filter.filter = filter; $filter.addEventListener("click", ev => this.filterByCategory(filter), false); } }); this.$container.querySelector("#archive").addEventListener("click", this.onOpenCustomView); } this.drawSchedule = function () { // console.log(this.schedule); var streams = this.schedule; this.$schedule.innerHTML = ""; this.$schedule.style.left = "0px"; streams.forEach(streamData => { var stream = this.createStream(streamData); this.$schedule.appendChild(stream); }); this.$schedule.querySelectorAll(".thumbnail").forEach((element, index) => { setTimeout(() => element.classList.add("loaded"), index * 20); }); this.$schedule.querySelectorAll(".photo").forEach((element, index) => { setTimeout(() => element.classList.add("loaded"), index * 20); }); this.filterByCategory(); } // 枠をつくる this.createStream = function (data) { // console.log(details); var $stream = document.createElement("a"); $stream.data = data; $stream.classList.add("hololive-stream"); $stream.setAttribute("stream-status", data.status); var start = new Date(data.start_actual || data.start_scheduled); var thumbnail, href, site = ""; if (data.type == "placeholder") { data.thumbnail = data.thumbnail // twitch if (data.link.indexOf("https://twitch.tv/") == 0) { // thumbnail = thumbnail.replace("1920x1080", "426x240"); site = "twitch"; } // twitter if (data.thumbnail.indexOf("https://pbs.twimg.com/") == 0) { data.thumbnail = data.thumbnail.replace("name=large", "name=small"); site = "witter"; } if (data.link && data.link.indexOf("agqr") >= 0) { site = "radio"; } href = data.link; $stream.target = "_blank"; } else if (data.type == "stream") { data.thumbnail = `https://i.ytimg.com/vi/${data.id}/mqdefault.jpg?${start.getTime()}`; href = "/watch/" + data.id; } var time = this.toLiveDate(data.start_actual, data.start_scheduled, data.ended, data.published_at); var icon = data.channel.photo.replace("=s800", "=s144"); $stream.href = href; $stream.innerHTML = ` <div class="date">${time}</div> <div class="thumbnail" style="background-image: url('${data.thumbnail}')"></div> <div class="title-wrapper"><div class="title">${data.title}</div></div> <div class="photo" style="background-image: url('${icon}');" /> `; $stream.addEventListener("mouseenter", this.previewStream, false); return $stream; } this.previewStream = function (ev) { var data = ev.target.data; if (data) { self.$previewTitle.innerText = data.title; self.$previewThumbnail.style.backgroundImage = `url('${data.thumbnail}')`; self.$previewThumbnail.setAttribute("stream-status", data.status); } self.$preview.classList.toggle("hidden", !data); } // フルスクリーンのときは表示しない this.updateVisibility = function () { var isFullscreen = document.querySelector("ytd-watch-flexy[fullscreen]"); var isScrolled = (window.scrollY || window.scrollTop || document.body.scrollY || document.body.scrollTop) > 100; self.$container.classList.toggle("visible", (!isFullscreen) || (isFullscreen && isScrolled)); } this.filterByCategory = function (filter) { this.filter = filter || this.filter; if (this.filter) { this.$container.querySelectorAll(".hololive-stream").forEach(e => e.classList.toggle("hidden", !filter.match(e.data))); } } // data this.isRefresing = false; this.schedule = []; this.lastUpdated = 0; this.updateSchedule = async function (force) { // console.log(`refreshList(${force})`); try { if (this.isRefreshing) return false; this.isRefreshing = true; // console.log(`cacheUpdated:${cacheUpdated}, lastUpdated:${this.lastUpdated}, ${new Date().getTime()}`); // 十分に新しい、HoloDexから更新の必要はない if (!force && Date.now() - this.lastUpdated < REFRESH_INTERVAL) { return false; } var response = await holodex.getLive().catch(ex => { console.log("cache", ex); }); if (!response) return false; if (Array.isArray(response)) { this.lastUpdated = Date.now(); this.schedule = response; return response; } return false; } catch (ex) { console.log(ex); } finally { this.setTimeoutToRefresh(); } } this.setTimeoutToRefresh = function () { setTimeout(() => { this.isRefreshing = false; }, ERROR_TIMEOUT); } // utils this.toLiveDate = function(schedule, start, end, publish) { // console.log(schedule, start, end, publish); if (end) { const diff = new Date(end).getTime() - new Date(start).getTime(); const dm = Math.floor(diff / 1000 / 60 % 60); const 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); const h = ("0" + start.getHours()).slice(-2); const m = ("0" + start.getMinutes()).slice(-2); if (start.getDate() != new Date().getDate()) { return `${start.getDate()}-${h}:${m}`; } else { return `${h}:${m}`; } } // handler this.onScrollContainer = function (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 lp = parseFloat(self.$schedule.style.left) || 0, dw = document.documentElement.clientWidth, cw = self.$schedule.clientWidth; // console.log (contents, lp, dw, cw); if (ev.deltaY > 0) { self.$schedule.style.left = Math.max(lp - dw / 5, - cw + dw) + "px"; } else if (ev.deltaY < 0) { self.$schedule.style.left = Math.min(lp + dw / 5, 0) + "px"; } else { self.$schedule.style.left = - cw + dw; } return false; } this.onOpenCustomView = function (ev) { customView.open(); } } function CustomViewer() { const localize = { ja: { next: "NEXT", filter: { favorite: "お気に入り", lives: "配信中を含める", all: "ALL", jp: "ホロライブ", en: "ホロライブ EN", id: "ホロライブ ID", stars: "ホロスターズ", outside: "外部コラボ", video: "動画", singing: "歌枠", talk: "雑談", recommend: "注目", pvp: "対戦ゲーム", rpg: "RPG", coop: "ローカル対戦/協力", horror: "ホラー", membersonly: "メンバー限定", }, topic: { membersonly: "メン限", totsu: "凸待ち", talk: "雑談", original_song: "オリジナルソング", music_cover: "歌ってみた", dancing: "踊ってみた", drawing: "お絵かき", morning: "朝活", singing: "歌枠", celebration: "記念枠", marshmallow: "マシュマロ", shorts: "ショート動画", superchat_reading: "スパチャ読み", anniversary: "周年記念", endurance: "耐久", announce: "お知らせ", watchalong: "同時視聴", clubhouse51: "アソビ大全51", outfit_reveal: "新衣装", } }, en: { next: "NEXT", filter: { favorite: "Favorite", lives: "Include lives", all: "ALL", jp: "Hololive JP", en: "Hololive EN", id: "Hololive ID", stars: "Holostars", outside: "Collabo with outside", video: "Video", singing: "Singing", talk: "Talk", recommend: "Recommend", pvp: "PvP", rpg: "RPG", coop: "Co-op", membersonly: "Members Only" }, topic: {} }, }; const t = localize[window.navigator.language] || localize.en; var self = this; // ui this.render = function () { this.config = Object.assign({ lives: false, favorite: false, }, storage.archive.options); this.$style = document.createElement("style"); this.$style.id = "hololive-custom-style"; this.$style.innerHTML = ` #hololive-custom-viewer { --custom-viewer-background: #fff; --custom-viewer-items-background: #fff; --custom-viewer-items-caption-color: #212121; --custom-viewer-items-caption-hover-text-shadow: #464ea7a8; --custom-viewer-items-info-color: #202020; --custom-viewer-items-info-background: #fffd; --custom-viewer-filter-color: #202020; --custom-viewer-filter-hover-background: #bfbfbf; --custom-viewer-filter-enabled-fill: gold; --custom-viewer-slim-filter-background: #cfcfcf; --custom-viewer-slim-filter-hover-background: #bfbfbf; --custom-viewer-filter-recommend-color: #000; --custom-viewer-filter-recommend-background: gold; --custom-viewer-next-color: #202020; --custom-viewer-next-background: #cfcfcf; --custom-viewer-next-hover-background: #bfbfbf; } [dark] #hololive-custom-viewer { --custom-viewer-background: #000; --custom-viewer-items-background: #000; --custom-viewer-items-caption-color: #ececec; --custom-viewer-items-caption-hover-text-shadow: #fff; --custom-viewer-items-info-color: #fff; --custom-viewer-items-info-background: #202020dd; --custom-viewer-filter-color: #ececec; --custom-viewer-filter-hover-background: #404040; --custom-viewer-filter-enabled-fill: gold; --custom-viewer-slim-filter-background: #202020; --custom-viewer-slim-filter-hover-background: #404040; --custom-viewer-filter-recommend-color: #000; --custom-viewer-filter-recommend-background: gold; --custom-viewer-next-color: #ececec; --custom-viewer-next-background: #202020; --custom-viewer-next-hover-background: #404040; } #hololive-custom-viewer { display: flex; position: fixed; justify-content: center; bottom: 0; right: 0; left: 0; top: 0; background: var(--custom-viewer-items-background); z-index: 3000; padding: 30px; transitioin: opacity 0.1s linear; } #hololive-custom-viewer.hidden { opacity: 0; display: none; } #hololive-custom-viewer #sub { width: 200px; overflow-y: scroll; scrollbar-width: none; box-sizing: border-box; } #hololive-custom-viewer #main { overflow-y: scroll; scrollbar-width: none; margin: 10px 0; padding: 20px; width: calc(min(100%,1280px)); text-align: left; box-sizing: border-box; } #hololive-custom-viewer #sub::-webkit-scrollbar, #hololive-custom-viewer #sub::-webkit-scrollbar-thumb, #hololive-custom-viewer #main::-webkit-scrollbar, #hololive-custom-viewer #main::-webkit-scrollbar-thumb { display: none; } #hololive-custom-viewer .hololive-stream { display: inline-block; position: relative; font-size: 10px; border-radius: 5px; margin: 0 0 16px 8px; width: 192px; vertical-align: top; text-decoration: none; color: #acacac; overflow: hidden; } #hololive-custom-viewer .hololive-stream.hidden { display: none; } #hololive-custom-viewer .hololive-stream .thumbnail { position: relative; vertical-align: middle; width: 192px; height: 108px; box-sizing: border-box; border-radius: 3px; background-size: 100%; background-repeat: 1; background-position: center center; transition: background-size 0.15s ease-in-out; } #hololive-custom-viewer .hololive-stream:hover .thumbnail { background-size: 108%; } #hololive-custom-viewer .hololive-stream .title-wrapper { color: var(--custom-viewer-items-caption-color); margin: 0.5rem; font-size: 1.5rem; line-height: 1.75rem; height: 5.25rem; word-break: break-all; overflow: hidden; transition: text-shadow 0.15s ease-in-out; } #hololive-custom-viewer .hololive-stream:hover .title-wrapper { text-shadow: 0px 0px 2px var(--custom-viewer-items-caption-hover-text-shadow); } #hololive-custom-viewer .hololive-stream .date, #hololive-custom-viewer .hololive-stream .topic { position: absolute; padding: 1px 5px; left: 3px; border-radius: 2px; color: var(--custom-viewer-items-info-color); background: var(--custom-viewer-items-info-background); z-index: 1; letter-spacing: .025em; white-space: nowrap; overflow: hidden; transition: opacity 0.1s linear; } #hololive-custom-viewer .hololive-stream:hover .date, #hololive-custom-viewer .hololive-stream:hover .topic { opacity: 0; } #hololive-custom-viewer .hololive-stream .date { top: 2px; font-size: 10px; } #hololive-custom-viewer .hololive-stream[stream-status=live] .date { background: #f00c; } #hololive-custom-viewer .hololive-stream .topic { font-size: 12px; bottom: 2px; text-transform: capitalize; } #hololive-custom-viewer .hololive-stream .title { display: inline; } #hololive-custom-viewer .hololive-stream .name { display: inline; vertical-align: middle; margin-left: 6px; font-size: 9px; opacity: 0.7; } #hololive-custom-viewer .hololive-stream .photo { position: absolute; right: -12px; top: -12px; width: 48px; height: 48px; border-radius: 24px; background-size: cover; transition: transform 0.15s ease-in-out, opacity 0.15s ease-in-out; } #hololive-custom-viewer .hololive-stream:hover .photo { transform: scale(0) translate(10px, -10px); opacity: 0; } #hololive-custom-viewer .hololive-stream .site { position: absolute; right: 3px; bottom: 3px; width: 24px; height: 24px; background-size: 100%; transition: opacity 0.1s ease-in-out; } #hololive-custom-viewer .hololive-stream .site.twitch { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAETSURBVHgB7ZU7DoJAEIb/JV7MBq/hCVROIJ7AqI2t0d5WsTF2dhzBI1hbsLIYwyPADgzrFvI1PJbk+5lZBoEaNq6cR4APA0wDIdTRgQV5lgGI8skZnbAe5a8ditwkjk15LoANeS5AW7nqabGvdfcrA9iiD9AHsB5gACZVI5o6uv+rBbct7AW4H4Dw+DmPp+7ipwGU/L5P5V4g/O8aexM2kkusvEsqVxitQOHNd7F8VnyGXIHiny37mWVFZSTyQIzL1tgV0MljQrwwq1pkBaDIoxeG3lU80XUAgvyhk/MC6OSOXs4KoJWfxIPysACRlSslV1YGpwIhV65oOwlDygYzEqBuqLShUQuSWd6hvBFLV/owwBuAI3t8NBey8QAAAABJRU5ErkJggg=='); } #hololive-custom-viewer .hololive-stream .site.twitter { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJkSURBVHgB7VZBbtpQEH3zIW0WVYuXVaH4Bs0NSk4AOUFhEarskhMknIDsqkKlcIT0BNAT1D1B3ZJK3dmVuirwp/MhVmzAxiagKBJv9+ePZ97M/JkxsMMODwzChlD84FWQp3MxeCDHAhiumB+MJrr1+8Ryw3p/9+H4DctfIPCq49Xlw8Kv99YlMuB19885gy/i7llziwGfFFWJyR02XzSCuwiBUse7BlFVaz5LS8KQVkRXaXRJsqImfDjKSZBNyzEyFWFKVJ4KFbWLElUao6KbSk8i9TXgTPaorxTskPwOxa7/9baGt4zg8oQbNyfWYJlRU0/KUx9ZwNwYNq1ecFRzl18QpW0bB0Ks//KjV1uwlbuLJA3GxEdh5wb5yGEPl3qMd2xecYQHKnlFlVLX95kxYCFKGg5IlU2a0uLpCM68LEJA+sJ/Dm6Jy3aMjQIRakRUm+UuvfOp/X34iQSejeFo0Hdx4optG5uFH/R+GHNvANcm3VtwLs+Lvy2TRwhIOnrYHhysIuDKcCDwGbYAjglOzQt+HssElF6dvoNNOZeuCSbfSgIGMjILMo4/ExZf7TqghNLmlwm1gpSC2tmaLAZMvWGz0Iu7XpqBm2NrQNN5cD+Y5ZOTdZyok3RZMusZOJUN+QZrQFb0oQkG6xIIYHe8A03Unx/Ryd6jS2ctAsbxmFRVynGKlM5na5ePVkUe0p+h9MmraS2zXqYgmSWjOPtElHbLTVB3Q79gqQlMScxqXpeav0UWiGMmXKSNOpZAAPvKs/U/1MRoxRxl+5WD+psUy2D5IdmRVoWjnqDnLlkyO+zwaPAf1zXwZL751PUAAAAASUVORK5CYII='); } #hololive-custom-viewer .hololive-stream:hover .site { opacity: 0; } #hololive-custom-viewer .filter { display: block; font-size: 14px; width: 100%; padding: 8px 16px; cursor: pointer; maring: 0; color: var(--custom-viewer-filter-color); background: var(--custom-viewer-background); border-radius: 3px; box-sizing: border-box; transition: background 0.1s linear, padding 0.1s ease-in-out; } #hololive-custom-viewer .filter svg { fill: var(--custom-viewer-filter-color); vertical-align: middle; } #hololive-custom-viewer .filter.enabled svg { fill: var(--custom-viewer-filter-enabled-fill); } #hololive-custom-viewer .filter span { padding-left: 8px; } #hololive-custom-viewer .filter:hover { background: var(--custom-viewer-filter-hover-background); padding-left: 24px; } #hololive-custom-viewer .filter[filter=recommend] { color: var(--custom-viewer-filter-recommend-color); background: var(--custom-viewer-filter-recommend-background); } #hololive-custom-viewer .filter[filter=recommend]:hover { background: var(--custom-viewer-filter-recommend-background); } #hololive-custom-viewer .child { padding-left: 8px; } #hololive-custom-viewer .filter[count]::after { display: inline-block; content: attr(count); font-weight: bold; margin-left: 12px; } #hololive-custom-viewer #next { display: block; font-size: 16px; color: #efefef; text-align: center; margin-top: 20px; padding: 16px; cursor: pointer; color: var(--custom-viewer-next-color); background: var(--custom-viewer-next-background); padding: 16px; border-radius: 3px; transition: background 0.1s linear; } #hololive-custom-viewer #next.hidden { opacity: 0; } #hololive-custom-viewer #next:hover { background: var(--custom-viewer-next-hover-background); } /* slim style */ @media screen and (max-width: 1600px) { #hololive-custom-viewer { flex-direction: column; justify-content: start; } #hololive-custom-viewer #sub { width: calc(min(100%,1280px)); overflow-y: unset; margin: 0 auto; } #hololive-custom-viewer #main { max-width: calc(min(100%,1280px)); text-align: center; margin: 10px auto; } #hololive-custom-viewer .child { padding-left: 0; display: inline; } #hololive-custom-viewer .filter { display: inline-block; padding: 6px 16px; width: unset; margin: 0 6px 6px 0; line-height: 1.5; border-radius: 20px; background: var(--custom-viewer-slim-filter-background); } #hololive-custom-viewer .filter:hover { background: var(--custom-viewer-slim-filter-hover-background); padding-left: 16px; } } `; document.body.appendChild(this.$style); this.$container = document.createElement("div"); this.$container.id = "hololive-custom-viewer"; this.$container.classList.add("hidden"); this.$container.innerHTML = ` <div id="sub"> <!-- <a class="filter no-count ${this.config.favorite ? "enabled" : ""}" filter="favorite"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="14px" height="14px" viewBox="0 0 512 512"> <path class="shape" d="M473.984,74.248c-50.688-50.703-132.875-50.703-183.563,0c-17.563,17.547-29.031,38.891-34.438,61.391 c-5.375-22.5-16.844-43.844-34.406-61.391c-50.688-50.703-132.875-50.703-183.563,0c-50.688,50.688-50.688,132.875,0,183.547 l217.969,217.984l218-217.984C524.672,207.123,524.672,124.936,473.984,74.248z" ></path> </svg> <span>${t.filter.favorite}</span> </a> --> <a class="filter no-count ${this.config.lives ? "enabled" : ""}" filter="lives"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="14px" height="14px" viewBox="0 0 512 512"> <g><polygon points="352.188,0 131.781,290.125 224.172,290.125 148.313,512 380.219,223.438 284.328,223.438"></polygon></g> </svg> <span>${t.filter.lives}</span> </a> <a class="filter" filter="all">${t.filter.all}</a> <a class="filter" filter="recommend">${t.filter.recommend}</a> <a class="filter" filter="jp">${t.filter.jp}</a> <a class="filter" filter="en">${t.filter.en}</a> <a class="filter" filter="id">${t.filter.id}</a> <a class="filter" filter="stars">${t.filter.stars}</a> <a class="filter" filter="outside">${t.filter.outside}</a> <a class="filter" filter="talk">${t.filter.talk}</a> <a class="filter" filter="singing">${t.filter.singing}</a> <a class="filter" filter="rpg">${t.filter.rpg}</a> <a class="filter" filter="horror">${t.filter.horror}</a> <a class="filter" filter="pvp">${t.filter.pvp}</a> <a class="filter" filter="coop">${t.filter.coop}</a> <a class="filter" filter="video">${t.filter.video}</a> <a class="filter" filter="membersonly">${t.filter.membersonly}</a> </div> <div id="main"><div id="contents"></div><a id="next">${t.next}</a></div> `; document.body.appendChild(this.$container); this.$sub = this.$container.querySelector("#sub"); this.$contents = this.$container.querySelector("#contents"); this.$next = this.$container.querySelector("#next"); // events this.$container.addEventListener("click", ev => { if (ev.target == this.$container || !ev.path.find(e => e.tagName == "A")) { this.close(); } }); this.$sub.querySelectorAll(".filter:not(.no-count)").forEach($filter =>{ var filter = customFilters[$filter.getAttribute("filter")]; if (filter) { $filter.filter = filter; $filter.addEventListener("click", ev => this.filterByCategory(filter)); } }); /* this.$sub.querySelector("[filter=favorite]").addEventListener("click", async ev => { var enabled = this.config.favorite = !this.config.favorite; storage.archive.options = this.config; ev.target.classList.toggle("enabled", enabled); var streams = await this.updateStreams({}, true); if (streams) { this.drawStreams(streams); } });*/ this.$livesFilter = this.$container.querySelector("#sub [filter=lives]"); this.$livesFilter.addEventListener("click", async ev =>{ var enabled = this.config.lives = !this.config.lives; storage.archive.options = this.config; this.$livesFilter.classList.toggle("enabled", enabled); var streams = await this.updateStreams({}, true); if (streams) { this.drawStreams(streams); } }); this.$next.addEventListener("click", async ev => { try{ this.$next.classList.add("hidden"); await this.continue(); } finally { this.$next.classList.remove("hidden"); } }); } this.createStream = function (data) { // console.log(data); try { var $stream = document.createElement("a"); $stream.data = data; $stream.classList.add("hololive-stream"); $stream.setAttribute("stream-status", data.status); var site = ""; // サムネサイズダウン、Youtube以外のサムネURL取得 var thumbnail = data.thumbnail, href; if (data.type == "placeholder") { // twitch if (data.link.indexOf("https://twitch.tv/") == 0) { thumbnail = thumbnail.replace("1920x1080", "426x240"); site = "twitch"; } // twitter if (thumbnail.indexOf("https://pbs.twimg.com/") == 0) { thumbnail = thumbnail.replace("name=large", "name=small"); site = "twitter"; } href = data.link; $stream.target = "_blank"; } else if (data.type == "stream") { thumbnail = `https://i.ytimg.com/vi/${data.id}/mqdefault.jpg?${new Date(data.published_at).getTime()}`; href = "/watch/" + data.id; } data.thumbnail = thumbnail; // トピック var topic = ""; var title = data.title; if (data.topic_id) { topic = t.topic[data.topic_id.toLowerCase()] || data.topic_id.replace(/_/g, " "); } else { var m = data.title.match(/^【([^】]*?)】/) || data.title.match(/^≪([^≫]*?)≫/) || data.title.match(/(#.*?)\s/); if (m) { topic = m[1].trim(); } } title = title.replace(/【[^】]*?】/g, ""); title = title.replace(/≪[^≫]*?≫/g, ""); title = title.trim(); var name = data.channel.name; // 配信時間 var hours, mins, secs, duration = data.duration; // var time = this.toLiveDate(data.start_actual, data.start_scheduled, data.ended, data.published_at); if (data.status == "live") { duration = Math.floor((new Date().getTime() - new Date(data.available_at).getTime()) / 1000); } hours = Math.floor(duration / 60 / 60); mins = ("00" + Math.floor(duration / 60 % 60)).slice(-2); secs = ("00" + Math.floor(duration % 60)).slice(-2); var icon = data.channel.photo.replace("=s800", "=s144"); $stream.href = href; $stream.innerHTML = ` <div class="thumbnail" style="background-image: url('${thumbnail}')"> <div class="date">${hours}:${mins}:${secs}</div> <div class="topic">${topic}</div> <div class="site" class="${site}"></div> </div> <div class="title-wrapper"><div class="title">${title}</div><div class="name">${name}</div></div> <div class="photo" style="background-image: url('${icon}');" /> `; $stream.addEventListener("mouseenter", this.previewStream, false); } catch(ex) { console.log(ex); } return $stream; } this.updateLiveDate = function () { if (!this.$contents) return; this.$contents.querySelectorAll(".hololive-stream[stream-status=live]").forEach(e => { var duration = Math.floor((new Date().getTime() - new Date(e.data.available_at).getTime()) / 1000); var hours = Math.floor(duration / 60 / 60); var mins = ("00" + Math.floor(duration / 60 % 60)).slice(-2); var secs = ("00" + Math.floor(duration % 60)).slice(-2); e.querySelector(".date").innerText = `${hours}:${mins}:${secs}`; }); } this.drawStreams = function (newStreams, isAppend) { this.archive = isAppend ? this.archive.concat(newStreams) : newStreams; if (!isAppend) { this.$contents.innerHTML = ""; } newStreams.forEach(data => self.$contents.appendChild(self.createStream(data))); this.countCategory(); this.filterByCategory(); } this.open = async function () { this.config = Object.assign({ lives: false, favorite: false, }, storage.archive.options); this.$container.classList.remove("hidden"); this.updateLiveDateTimer = setInterval(() => this.updateLiveDate(), 1000); var streams = await this.updateStreams({}, false).catch(ev => console.log(ev)); if (streams) { this.drawStreams(streams, false); } } this.filterByCategory = function (filter) { if (filter) { this.filter = filter; } this.$contents.querySelectorAll(".hololive-stream").forEach($s => { $s.classList.toggle("hidden", !$s.hasAttribute(`category-${filter.name}`)); }); } this.countCategory = function () { // フィルタ結果を属性にキャッシュ var $streams = this.$contents.querySelectorAll(".hololive-stream:not(.counted)"); $streams.forEach($s => { for (var key in customFilters) { var filter = customFilters[key]; if (filter.match($s.data)) { $s.setAttribute(`category-${filter.name}`, true); } } $s.classList.add("counted"); }); this.$sub.querySelectorAll(".filter:not(.no-count)").forEach($f => { var filterName = $f.getAttribute("filter"); $f.setAttribute("count", this.$contents.querySelectorAll(`[category-${filterName}=true]`).length); }); } this.continue = async function () { var newStreams = await this.updateStreams({ offset: this.archive.length, }, true); if (newStreams) { this.drawStreams(newStreams, true); } } this.close = function () { this.$container.classList.add("hidden"); this.updateLiveDateTimer = clearInterval(this.updateLiveDateTimer); } // this.updateStreams = async function (options, isForce = false) { try { if (this.isRefreshing) return false; this.isRefreshing = true; // 十分に新しい、HoloDexから更新の必要はない if (!isForce && Date.now() - this.lastUpdated < REFRESH_INTERVAL) { return false; } if (this.config.lives) { options.status = "past,missing,live,placeholder"; } var response = await holodex.getVideos(options, this.config.favorite).catch(ex => { console.log("cache", ex); }); if (response && Array.isArray(response.items)) { this.lastUpdated = new Date().getTime(); return response.items; } console.log(response); return false; } catch (ex) { console.log(ex); return false; } finally { this.isRefreshing = false; } } // utils } function GenFilter(name, channels) { this.name = name; this.channels = channels || []; this.match = stream => this.channels.indexOf(stream.channel.id) >= 0; } function CategoryFilter(name, category, freewords) { this.name = name; this.category = Array.isArray(category) ? category : [ category ]; this.category = this.category.map(c => c.toLowerCase()); this.freewords = Array.isArray(freewords) ? freewords : [ freewords ]; this.match = (stream => { if (this.category && this.category.indexOf(String(stream.topic_id).toLowerCase()) >= 0) { return true; } if (this.freewords && this.freewords.find(w => stream.title.match(w))) { return true; } return false; }); } function Filter(name, func) { this.name = name; this.func = func; this.match = stream => func(stream); } function initilizeFilters() { var filters = {}; filters.all = new Filter("all", () => true); filters.jp = new GenFilter("jp", [ // hololive official "UCJFZiqLMntJufDCHc6bQixg", // jp0 "UC0TXe_LYZ4scaW2XMyi5_kw", "UC5CwaMl1eIgY8h02uZw7u8A", "UCDqI2jOz0weumE8s7paEk6g", "UC-hM6YJuNYVAmUWxeIr9FeA", "UCp6993wxpyDPHUpavwDFqgg", // jp1 "UC1CfXB_kRs3C-zaeTG3oGyg", "UCD8HOxPs4Xvsm8H0ZxXGiBw", "UCdn5BQ06XqgXoAxIhbqw5Rg", "UCFTLzh12_nrtzqBPsTCqenA", "UCHj_mh57PVMXhAUDphUQDFA", "UCLbtM3JZfRTg8v2KGag-RMw", "UCQ0UDLQCjY0rmuxCDE38FGg", // jp2 "UC1opHUrw8rvnsadT-iGp7Cg", "UC1suqwovbL1kzsoaZgFZLKg", "UC7fk0CB07ly8oSl0aqKkqFg", "UCp3tgHXw_HI0QMk1K8qh3gQ", "UCvzGlP9oQwU--Y0r9id_jnA", "UCXTpFs_3PqI41qX2d9tL2Rw", // gamers "UCdn5BQ06XqgXoAxIhbqw5Rg", "UChAnqc_AY5_I3Px5dig3X1Q", "UCp-5t9SrOQwXMU7iIjQfARg", "UCvaTdHTWBGv3MKj3KVqJVCw", // jp3 "UC1DCedRgGHBdm81E1llLhOQ", "UCCzUftO8KOVkV4wQG1vkUvg", "UCdyqAaZDKHXg4Ahi7VENThQ", "UCvInZx9h3jC2JzsIzoOebWg", // jp4 "UC1uv2Oq6kNxgATlCiez59hw", "UCa9Y57gfeY0Zro_noHRVrnw", "UCqm3BQLlJfvkTsX_hvm0UmA", "UCZlDXzGoo7d44bwdNObFacg", // jp5 "UCAWSyEs_Io8MtpY3m-zqILA", "UCFKOVgVbGmX65RxO3EtH3iw", "UCK9V2B22uJYu3N7eR_BT9QA", "UCUKD-uaobj9jiqB-VXt71mA", // holox "UC6eWCld0KwmyHFbAqK3V-Rw", "UCENwRMx5Yh42zWpzURebzTw", "UCIBY1ollUsauvVi4hW4cumw", "UCs9_O1tRPMQTHQ-N_L6FU2g", "UC_vMYWcDjmfdpH6r4TTn1MQ", ]); filters.en = new GenFilter("en", [ // en offical "UCotXwY6s8pWmuWd_snKYjhg", //enmyth "UCHsx4Hqa-1ORjQTh9TYDhww", "UCL_qhgtOy0dy1Agp8vkySQg", "UCMwGHR0BTZuLsmjY_NT5Pwg", "UCoSrY_IQQVpmIRZ9Xf-y93g", "UCyl1z3jo3XHR1riLFKG5UAg", // enhope "UC8rcEBzJSleTkf_-agPM20g", // encouncil "UC3n5uGu18FoCy23ggWWp8tA", "UCgmPnx-EEeOrZSg5Tiw7ZRQ", "UCmbs8T6MWqUHP1tIQvSgKrg", "UCO_aKKYxn4tvrqPjcTzZ6EQ", "UCsUj0dszADCGbF3gNrQEuSQ", // en shorts "UCNoxM_Kxoa-_gOtoyjbux7Q", ]); filters.id = new GenFilter("id", [ // id1 "UCAoy6rzhSf4ydcYjJw3WoVg", "UCfrWoRGlawPQDQxxeIDRP0Q", "UCOyYb1c43VlX9rc_lT6NKQw", "UCP0BspO_AMEe3aQqqpo89Dg", // id2 "UC727SQYUvx5pDDGQpTICNWg", "UChgTyjG-pdNvxxhdsXfHQ5Q", "UCYz_5n-uDuChHtLo7My1HnQ", // id3 "UCjLEmnpCNeisMxy134KPwWw", "UCTvHWSfBZgtxE4sILOaurIQ", "UCZLZ8Jjx_RN2CXloOmgTHVg" ]); filters.stars = new GenFilter("stars", [ // official stars "UCWsfcksUUpoEvhia0_ut0bA", // stars1 "UC6t3-_N8A6ME1JShZHHqOMw", "UC9mf_ZVpouoILRY9NUIaK-w", "UCKeAhJvy8zgXWbh9duVjIaQ", "UCZgOv3YDEs-ZnZWDYVwJdmA", // stars2 "UCANDOlYTJT7N5jlRC3zfzVA", "UCGNI4MENvnsymYjKiZwv9eg", "UCNVEsYbiZjH5QLmGeSgTSzg", // stars3 "UChSvpZYRPh0FvG4SJGSga3g", "UCwL7dgTxKo8Y4RFIKWaf8gA", // stars4 "UCc88OV45ICgHbn3ZqLLb52w", "UCdfMHxjcCc2HSd9qFvfJgjg", "UCgRqGV1gBf2Esxh0Tz1vxzw", "UCkT1u65YS49ca_LsFwcTakw", ]); filters.outside = new Filter("outside", (() => { var allChannels = [].concat(filters.jp.channels, filters.en.channels, filters.id.channels, filters.stars.channels); return s => allChannels.indexOf(s.channel.id) == -1; })()); filters.singing = new CategoryFilter("singing", "singing"); filters.talk = new CategoryFilter("talk", [ "talk", "morning", "drawing", "asmr", "cooking_stream", "totus", "english_only", "duolingo", "mashmallow", "japanese_only", "english_lesson", ]); filters.recommend = new CategoryFilter("recommend", [ "announce", "celebration", "3d_stream", "debut", "birthday", "anniversary", "totsu", "outfit_reveal"]); filters.membersonly = new CategoryFilter("membersonly", "membersonly", ["メン限", "メンバー限定"]); filters.rpg = new CategoryFilter("rpg", [ "elden_ring", "nier", "zelda", "final_fantasy", "cyberpunk_2077", "assassings_creed", "undertale", "dragon_quest", "elder_scrolls", "ghostwire: tokyo", "yakuza", "pokemon", "dark_souls", "gta5", "detroit:_become_human", "death_stranding", "metal_gear", "fire_emblem", "mafia", "devil_may_cry", "bloodborne", "dying_light", "bioshock" ]); filters.pvp = new CategoryFilter("pvp", [ "apex", "overwatch", "splatoon", "valorant", "dbd", "league_of_legends", "fallguys", "call_of_duty", "pupg", "fortnite", "slither.io", "smash", "mariokart", "tetris", "tarkov", "rainbow_six", "dota", ]); filters.coop = new CategoryFilter("coop", [ "minecraft", "raft", "ark", "l4d2", "it_takes_two", "overcooked", "project_winter", "gartic_phone", "phasmophobia", "among_us", "craftopia", "devour", "7_days_to_die", "back4blood", "we_were_here", "a_way_out", "mario_party", "uno", "human_fall_flat", "super_bunny_man", "ultimate_chicken_horse", "ghost_exorism_inc.", "operation_tango", "monopoly", "keep_talking_nobody_explode", "unravel_two", "hide_and_shriek", "hacktag", ]); filters.horror = new CategoryFilter("horror", [ "ib", "phasmophobia", "outlast", "night_deliverry", "devour", "yomawari", "little_nightmares", "the_closing_shift", "ao_oni", "friday_night_funkin'", "convenience_store", "inunaki_tunnel", "residentevil", ]); filters.video = new Filter("video", (() => { var category = ["shorts", "music_covor", "original_song", "dancing"]; return stream => { if (category.indexOf(String(stream.topic_id).toLowerCase()) >= 0) { return true; } if (stream.start_actual && stream.start_scheduled && stream.available_at && stream.available_at == stream.start_actual == stream.start_scheduled) { return true; } if (0 < stream.duration && stream.duration < 60 * 6) { return true; } return false; } })()); return filters; } // @see https://holodex.stoplight.io/docs/holodex/YXBpOjExNjIwMjM0-holodex-holo-api-v2 function Holodex() { this.createXHRData = function (options) { var data = ""; for (var key in options) { var value = options[key]; if (Array.isArray(value)) { value += value.join(","); } data += `${encodeURIComponent(key)}=${encodeURIComponent(value)}&`; } return data; } this.XHR = function (api, data) { // console.log(api, data); return new Promise((resolve, reject) => { var options = { method: "GET", url: api, headers: { "X-APIKEY": "661f7f8f-e353-4d33-96f8-187e574d62d0", "user-agent": `${APPNAME}/${VERSION}`, }, onload: (context) => { try { resolve(JSON.parse(context.responseText)); } catch (ex) { reject(context); } }, onerror: reject, onabort: reject, ontimeout: reject, }; if (data) { options.method = "POST"; options.data = data.replace("&20", "+"); options.headers.application = "x-www-form-urlencoded"; } GM.xmlHttpRequest(options); }); } this.getLive = function(options = {}, isUser = false) { options = Object.assign({ type: "placeholder,stream", org: "Hololive", lang: window.navigator.language == "ja" ? "ja" : "en", }, options); isUser = false; // https://holodex.net/api/v2/live?type=placeholder%2Cstream&org=Hololive return this.XHR(`https://holodex.net/api/v2/${isUser ? "users/" : ""}live?${this.createXHRData(options)}`); } this.getTopics = function () { return this.XHR(`https://holodex.net/api/v2/topics`); } this.getVideos = function(options = {}, isUser = false) { options = Object.assign({ // // past, missing, live, placeholder status: "past,missing", // stream, clip type: "stream", paginated: false, to: new Date().toISOString(), org: "Hololive", // asc,desc // order: "desc", // <=50 limit: 50, offset: 0, // clips,refers,sources,simulcasts,mentions,description,live_info,channel_stats,songs // includes: "live_info,refers", // en,ja // lang: "all", lang: window.navigator.language == "ja" ? "ja" : "en", }, options); // users api dont allow isUser = false; return this.XHR(`https://holodex.net/api/v2/${isUser ? "users/" : ""}videos?${this.createXHRData(options)}`); } this.getChannels = function () { var options = { org: "Hololive", limit: 50, offset: 0, }; return this.XHR(`https://holodex.net/api/v2/channels?${this.createXHRData(options)}`); } } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址