您需要先安装一个扩展,例如 篡改猴、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.13 // @description You can check schedule of recently hololive stream on youtube // @description:ja ホロライブの直近のスケジュールをYoutubeで確認できます // @author You // @icon https://www.google.com/s2/favicons?sz=64&domain=holodex.net // @match https://www.youtube.com/* // @grant GM.xmlHttpRequest // @grant GM.getValue // @grant GM.setValue // @run-at document-end // @noframes // @require https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.1.6/purify.min.js // ==/UserScript== (function() { 'use strict'; const VERSION = "1.0.13", APPNAME = "DHLSOY"; const REFRESH_INTERVAL = 3 * 60 * 1000; // 5 mins const ERROR_TIMEOUT = 3000; // 3 secs const MAX_STREAM_LENGTH = 12 * 60 * 60 * 1000; // 12 hours 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); }, }, favorite: { get useFilter() { return catchParseError(() => JSON.parse(localStorage.ht_f_useFilter), false); }, set useFilter(value) { localStorage.ht_f_useFilter = JSON.stringify(value); }, get channels() { var fav = catchParseError(() => JSON.parse(localStorage.ht_f_channels), null); return Array.isArray(fav) ? fav : []; }, set channels(value) { localStorage.ht_f_channels = JSON.stringify(value); }, } } const holodex = new Holodex(), quickBar = new QuickBar(), customView = new CustomViewer(); var customFilters, favorites = [], myPolicy; // unsafeWindow.holodex = holodex; initilize(); return; function initilize() { if (!window.DOMPurify || !window.trustedTypes || !window.trustedTypes.createPolicy) { setTimeout(initilize, 100); return; } if (document.enabledHololiveSchedule) return; document.enabledHololiveSchedule = true; myPolicy = window.trustedTypes.createPolicy('DHLSOY', { createHTML: (to_escape) => window.DOMPurify.sanitize(to_escape, { RETURN_TRUSTED_TYPE: false, CUSTOM_ELEMENT_HANDLING: { tagNameCheck: (tagName) => true, attributeNameCheck: (attr) => true, allowCustomizedBuiltInElements: true, }, ADD_ATTR: ["on"], }), }); try { customFilters = initilizeFilters(); quickBar.render(); customView.render(); } catch(ex) { console.log(ex); } } function catchParseError(func, defaultValue) { try { return func(); } catch { return defaultValue; } } function removeAllChildNodes(parent) { parent.innerText = ""; } function toggleFavorite(channel, enable) { if (!channel) return false; if (enable) { if (favorites.indexOf(channel) < 0) { favorites.push(channel); } } else { favorites = favorites.filter(f => f != channel); } storage.favorite.channels = favorites; } 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.categoryFilter; this.useFavoriteFilter = false; this.whitelistFilters = []; this.$preview = null; this.$previewThumbnail = null; this.$previewTitle = null; // ui this.render = async function () { self.categoryFilter = customFilters.all; self.whitelistFilters = [self.filterByCategory, self.filterByFavorite]; self.useFavoriteFilter = storage.favorite.useFilter; this.$style = document.createElement("style"); this.$style.id = "hololive-schedule-style"; this.$style.innerText = ` #hololive-schedule { --color: #212121; --background-color: #ffffff; --hover-background-color: #ececec; --icon-hover-background-color: #d9d9d9; --icon-fill: #212121; --background-youtube: #ff0000; --background-twitch: #a970ff; --background-agqr: #e50065; --background-abema: #000000; --background-x: #000000; } [dark] #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 #schedule:empty:after { display: block; content: "Loading ..."; vertical-align: middle; } #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: cover; background-repeat: 1; background-position: center center; opacity: 1; transition: opacity 0.1s ease; } #hololive-schedule .hololive-stream .thumbnail { } #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; transition: opacity 0.1s ease; overflow: hidden; } #hololive-schedule { a:link, a:visited, a:hover, a:active { color: white; } } #hololive-schedule .hololive-stream .photo:after { content: "★"; background: #8888; display: block; position: absolute; top: 0; right: 0; bottom: 0; left: 0; font-size: 24px; text-align: center; opacity: 0; line-height: 32px; } #hololive-schedule .hololive-stream.favorite .photo:after { background: #ff08; } #hololive-schedule .hololive-stream:hover .photo:after { 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 .tools a.favorite[on] { color: yellow; } #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; position: relative; 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; } #thumbnail-preview[platform]::before { display: block; position: absolute; content: attr(platform); color: #fff; font-weight: bold; padding: 3px 5px; border-radius: 1px; left: 5px; top: 5px; z-idnex: 1111111; text-transform: capitalize; } #thumbnail-preview[platform="youtube"]::before { background: var(--background-youtube); } #thumbnail-preview[platform="twitch"]::before { background: var(--background-twitch); } #thumbnail-preview[platform="x"]::before { background: var(--background-x); } #thumbnail-preview[platform="abema"]::before { background: var(--background-abema); } #thumbnail-preview[platform="agqr"]::before { background: var(--background-agqr); } #hololive-schedule #thumbnail-preview iframe { width: 512px; height: 288px; } #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); console.log("favoritefilter", self.useFavoriteFilter); this.$container = document.createElement("div"); this.$container.id = "hololive-schedule"; this.$container.classList.add("visible"); this.$container.innerHTML = myPolicy.createHTML(` <div class="tools left"> <a class="filter favorite" ${self.useFavoriteFilter ? "on" : ""}><span>★</span></a> <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="devis"><span>DEV_IS</span></a> <a class="filter" filter="stars_all">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", self.$preview.classList.add("hidden"), false); this.$container.addEventListener("mouseleave", this.close, false); this.updateVisibility(); this.$container.addEventListener("click", ev => { var photo = ev.target.closest(".photo"); if (!photo) return; ev.preventDefault(); var stream = photo.closest(".hololive-stream"); var channel = stream.data?.channel?.id; if (!channel) return; var isFavorite = !stream.classList.contains("favorite"); toggleFavorite(channel, isFavorite); self.$container.querySelectorAll(".hololive-stream").forEach(s => { if (s.data.channel.id == channel) { s.classList.toggle("favorite", isFavorite); } }); self.filter(); }); this.$container.querySelectorAll(".filter").forEach($f => { $f.addEventListener("click", () => { var cf = customFilters[$f.getAttribute("filter")]; if (cf) { self.categoryFilter = cf; self.filter(); } }); }); var fav = this.$container.querySelector(".filter.favorite"); fav.addEventListener("click", () => { self.useFavoriteFilter = fav.toggleAttribute("on"); self.filter(); storage.favorite.useFilter = self.useFavoriteFilter; }); this.$container.querySelector("#archive").addEventListener("click", this.onOpenCustomView); } this.drawSchedule = function () { // console.log(this.schedule); var streams = this.schedule; this.$schedule.innerText = ""; this.$schedule.style.left = "0px"; // console.log(streams); streams // .filter(data => !(data.status == "upcoming" && new Date().getTime() - new Date(data.available_at).getTime() > 0)) // fix to status bug in holodex .filter(data => !(data.status == "live" && new Date().getTime() - new Date(data.available_at).getTime() > MAX_STREAM_LENGTH)) // older .forEach(streamData => { var stream = this.createStream(streamData); this.$schedule.appendChild(stream); }); this.filter(); } // 枠をつくる 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 href, platform = "youtube"; if (data.type == "placeholder") { // twitch if (data.link && data.link.indexOf("https://twitch.tv/") == 0) { data.thumbnail = data.thumbnail.replace("1920x1080", "426x240"); platform = "twitch"; } // twitter if (data.thumbnail && data.thumbnail.indexOf("https://pbs.twimg.com/") == 0) { data.thumbnail = data.thumbnail.replace("name=large", "name=small"); platform = "x"; } // agqr if (data.link && data.link.indexOf("agqr") >= 0) { platform = "radio"; } // abema if (data.link && data.link.indexOf("//abema.tv/") > 0) { platform = "abema"; } data.platform = platform; 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"); data.platform = platform; $stream.setAttribute("platform", data.platform); $stream.href = href; $stream.classList.toggle("favorite", favorites.indexOf(data.channel?.id) >= 0); $stream.innerHTML = myPolicy.createHTML(` <div class="date">${time}</div> <div class="thumbnail" style="background-image: url('${data.thumbnail}'), url('${data.channel.photo}')"></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.initilizeTwitchAPI = function () { var script = document.createElement("script"); script.src = myPolicy.createScriptURL("https://player.twitch.tv/js/embed/v1.js"); return new Promise((resolve) => { script.addEventListener("load", () => setTimeout(resolve, 50)); document.querySelector("head").appendChild(script); }); } this.initilizeYoutubeAPI = function () { var script = document.createElement("script"); script.src = myPolicy.createScriptURL("https://www.youtube.com/player_api"); return new Promise((resolve) => { script.addEventListener("load", () => setTimeout(resolve, 50)); document.querySelector("head").appendChild(script); }); } this.previewTwitch = function (channel) { var player = new Twitch.Player("thumbnail-preview", { channel: channel, parent: ["www.youtube.com"] }); player.addEventListener(Twitch.Player.READY, () => { setTimeout(() => { player.setMuted(false); }, 200); }); return player; } this.previewYoutube = function (videoId) { var player = new YT.Player("thumbnail-preview-placeholder", { videoId: videoId, playerVars: { autoplay: 1, enablejsapi: 1, origin: "www.youtube.com", controls: 1, // 0: ユーザー設定の音量が維持されない fs: 0, modestbranding: 0, } }); return player; } this.previewStream = async function (ev) { removeAllChildNodes(self.$previewThumbnail); var data = ev.target.data; if (data) { self.$previewTitle.innerText = data.title; self.$previewThumbnail.style.backgroundImage = `url('${data.thumbnail}'), url('${data.channel.photo}')`; self.$previewThumbnail.setAttribute("stream-status", data.status); self.$previewThumbnail.setAttribute("platform", data.platform); var mainVideo = document.querySelector("video"); if (data.topic_id == "membersonly") { } // youtube else if (data.type == "stream" && data.status == "live") { if (mainVideo && mainVideo.played && mainVideo.played.length > 0) { if (typeof YT == "undefined") { await self.initilizeYoutubeAPI(); } self.$previewThumbnail.innerHTML = myPolicy.createHTML(`<div id="thumbnail-preview-placeholder"></div>`); self.previewYoutube(data.id); } } // twitch else if (data.status == "live" && data.link && data.link.indexOf("https://twitch.tv/") == 0) { var channel = data.link.split("/").slice(-1)[0]; if (typeof Twitch == "undefined") { await self.initilizeTwitchAPI(); } self.previewTwitch(channel); } } self.$preview.classList.toggle("hidden", !data); } // フルスクリーンのときは表示しない this.updateVisibility = function () { var html = document.querySelector("html"); var app = document.querySelector("ytd-app"); var isScrolled = ((app && app.scrollTop) || html.scrollTop || html.scrollY || window.scrollY || window.scrollTop || document.body.scrollY || document.body.scrollTop) > 100; self.$container.classList.toggle("visible", isScrolled); } this.filter = function () { this.$container.querySelectorAll(".hololive-stream").forEach(e => { var isDisplay = true; for (var i = 0; i < self.whitelistFilters.length; i++) { isDisplay &= self.whitelistFilters[i](e.data); } e.classList.toggle("hidden", !isDisplay); }); this.$schedule.style.left = 0; } this.filterByCategory = function(data) { if (!self.categoryFilter) return false; return self.categoryFilter.match(data); } this.filterByFavorite = function (data) { if (!self.useFavoriteFilter) return true; return customFilters.favorite.match(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; // 十分に新しい、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(); } this.close = function() { self.$preview.classList.add("hidden"); removeAllChildNodes(self.$previewThumbnail); // stop iframe preview } } function CustomViewer() { const localize = { ja: { next: "NEXT", filter: { favorite: "お気に入り", lives: "配信中を含める", all: "ALL", jp: "ホロライブ", en: "ホロライブ EN", id: "ホロライブ ID", devis: "DEV_IS", stars: "ホロスターズ", stars_en: "ホロスターズ EN", outside: "外部コラボ", video: "動画", singing: "歌枠", talk: "雑談", recommend: "注目", pvp: "対戦ゲーム", rpg: "RPG", coop: "ローカル対戦/協力", horror: "ホラー", hololive: "Hololive Indie", 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: "新衣装", debut_stream: "初配信", animation: "アニメ", camera_stream: "カメラ配信", } }, en: { next: "NEXT", filter: { favorite: "Favorite", lives: "Include lives", all: "ALL", jp: "Hololive JP", en: "Hololive EN", id: "Hololive ID", devis: "DEV_IS", stars: "Holostars", stars_en: "Holostars EN", outside: "Collabo with outside", video: "Video", singing: "Singing", talk: "Talk", recommend: "Recommend", pvp: "PvP", rpg: "RPG", coop: "Co-op", hololive: "Hololive Indie", 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.innerText = ` #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.x { 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 .button { 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 .button svg { fill: var(--custom-viewer-filter-color); vertical-align: middle; } #hololive-custom-viewer .button.enabled svg { fill: var(--custom-viewer-filter-enabled-fill); } #hololive-custom-viewer .button span { padding-left: 8px; } #hololive-custom-viewer .button:hover { background: var(--custom-viewer-filter-hover-background); padding-left: 24px; } #hololive-custom-viewer .button.filter[filter=recommend], #hololive-custom-viewer .button.reload { color: var(--custom-viewer-filter-recommend-color); background: var(--custom-viewer-filter-recommend-background); } #hololive-custom-viewer .button.reload svg { fill: var(--custom-viewer-filter-recommend-color); } #hololive-custom-viewer .button.filter[filter=recommend]:hover, #hololive-custom-viewer .button.reload:hover { background: var(--custom-viewer-filter-recommend-background); } #hololive-custom-viewer .button.filter[count]::after { display: inline-block; content: attr(count); font-weight: bold; margin-left: 12px; } #hololive-custom-viewer .button.hidden { display: none; } #hololive-custom-viewer .child { padding-left: 8px; } #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 .button { 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 .button: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 = myPolicy.createHTML(` <div id="sub"> <!-- <a class="button 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="button 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="button filter" filter="all">${t.filter.all}</a> <a class="button filter" filter="recommend">${t.filter.recommend}</a> <a class="button filter" filter="jp">${t.filter.jp}</a> <a class="button filter" filter="en">${t.filter.en}</a> <a class="button filter" filter="id">${t.filter.id}</a> <a class="button filter" filter="devis">${t.filter.devis}</a> <a class="button filter" filter="stars">${t.filter.stars}</a> <a class="button filter" filter="stars_en">${t.filter.stars_en}</a> <a class="button filter" filter="outside">${t.filter.outside}</a> <a class="button filter" filter="talk">${t.filter.talk}</a> <a class="button filter" filter="singing">${t.filter.singing}</a> <a class="button filter" filter="rpg">${t.filter.rpg}</a> <a class="button filter" filter="horror">${t.filter.horror}</a> <a class="button filter" filter="pvp">${t.filter.pvp}</a> <a class="button filter" filter="coop">${t.filter.coop}</a> <a class="button filter" filter="hololive">${t.filter.hololive}</a> <a class="button filter" filter="video">${t.filter.video}</a> <a class="button filter" filter="membersonly">${t.filter.membersonly}</a> <a class="button reload hidden"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" height="14px" width="14px" viewBox="0 0 512 512"> <g> <path d="M446.025,92.206c-40.762-42.394-97.487-69.642-160.383-72.182c-15.791-0.638-29.114,11.648-29.752,27.433 c-0.638,15.791,11.648,29.114,27.426,29.76c47.715,1.943,90.45,22.481,121.479,54.681c30.987,32.235,49.956,75.765,49.971,124.011 c-0.015,49.481-19.977,94.011-52.383,126.474c-32.462,32.413-76.999,52.368-126.472,52.382 c-49.474-0.015-94.025-19.97-126.474-52.382c-32.405-32.463-52.368-76.992-52.382-126.474c0-3.483,0.106-6.938,0.302-10.364 l34.091,16.827c3.702,1.824,8.002,1.852,11.35,0.086c3.362-1.788,5.349-5.137,5.264-8.896l-3.362-149.834 c-0.114-4.285-2.88-8.357-7.094-10.464c-4.242-2.071-9.166-1.809-12.613,0.738L4.008,182.45c-3.05,2.221-4.498,5.831-3.86,9.577 c0.61,3.759,3.249,7.143,6.966,8.974l35.722,17.629c-1.937,12.166-3.018,24.602-3.018,37.279 c-0.014,65.102,26.475,124.31,69.153,166.944C151.607,465.525,210.8,492.013,275.91,492 c65.095,0.014,124.302-26.475,166.937-69.146c42.678-42.635,69.167-101.842,69.154-166.944 C512.014,192.446,486.844,134.565,446.025,92.206z"></path> </g> </a> </div> <div id="main"> <div id="contents"></div> <a id="next">${t.next}</a> </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"); this.$reload = this.$container.querySelector(".reload"); // events this.$container.addEventListener("click", ev => { if (ev.target == this.$container || !ev.target.tagName == "A") { this.close(); return; } }); 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.$reload.addEventListener("click", async ev => { try { var streams = await this.updateStreams({}, true); if (streams) { this.drawStreams(streams); } } finally { this.$reload.classList.add("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); $stream.setAttribute("topic", data.topic_id); 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 = "x"; } 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.replace(/(※.*?|※?\s?)(ネタバレ(あり)?|spoiler alert)/gi, ""); 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 = myPolicy.createHTML(` <div class="thumbnail" style="background-image: url('${data.thumbnail}'), url('${data.channel.photo}');"> <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) { removeAllChildNodes(this.$contents); } newStreams // .filter(data => !(data.status == "upcoming" && new Date().getTime() - new Date(data.available_at).getTime() > 0)) // fix to status bug in holodex .forEach(data => self.$contents.appendChild(self.createStream(data))); this.countCategory(); this.filterByCategory(); if (!isAppend) { this.$reload.classList.add("hidden"); this.shouldReloadTimer = clearTimeout(this.shouldReloadTimer); this.shouldReloadTimer = setTimeout(() => this.$reload.classList.remove("hidden"), 180 * 1000); } } this.open = async function () { this.config = Object.assign({ lives: false, favorite: false, }, storage.archive.options); this.$container.classList.remove("hidden"); this.$reload.classList.add("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 (newFilter) { if (newFilter) { this.filter = newFilter; } this.$contents.querySelectorAll(".hololive-stream").forEach($s => { $s.classList.toggle("hidden", !$s.hasAttribute(`category-${this.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.shouldReloadTimer = clearTimeout(this.shouldReloadTimer); } // 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 = Date.now(); 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 = {}; favorites = storage.favorite.channels; filters.all = new Filter("all", () => true); filters.favorite = new Filter("favorite", (data) => { if (!favorites) return true; return favorites.indexOf(data.channel.id) >= 0; }); 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 advant "UC9p_lqQ0FEDz327Vgf5JwqA", "UC_sFNM0z0MWm9A6WlKPuMMg", "UCt9H_RpQzhxzlyBxFqrdHqA", "UCgnfPPb9JI3e9A4cXHnWbyg", // en shorts "UCNoxM_Kxoa-_gOtoyjbux7Q", // en advant "UCt9H_RpQzhxzlyBxFqrdHqA", // "@FUWAMOCOch", "UCgnfPPb9JI3e9A4cXHnWbyg", // "@ShioriNovella", "UC9p_lqQ0FEDz327Vgf5JwqA", // "@KosekiBijou", "UC_sFNM0z0MWm9A6WlKPuMMg", // "@NerissaRavencroft", ]); 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.devis = new GenFilter("devis", [ // official "UC10wVt6hoQiwySRhz7RdOUA", // "@hololiveDEV_IS" // regloss "UCMGfV7TVTmHhEErVJg1oHBQ", // "@HiodoshiAo", "UCWQtYtq9EOB4-I5P-3fh8lA", // "@OtonoseKanade", "UCtyWhCj3AqKh2dXctLkDtng", // "@IchijouRirika", "UCdXAk5MpyLD8594lm_OvtGQ", // "@JuufuuteiRaden", "UC1iA6_NT4mtAcIII6ygrvCw", // "@TodorokiHajime", // glowflow "UC9LSiN9hXI55svYEBrrK-tw", // @IsakiRiona "UCGzTVXqMQHa4AgJVJIVvtDQ", // @KikiraraVivi "UCjk2nKmHzgH5Xy-C5qYRd5A", // @MizumiyaSu "UCKMWFR6lAstLa7Vbf5dH7ig", // @RindoChihaya "UCuI_opAVX6qbxZY-a-AxFuQ", // @KoganeiNiko ]); 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.stars_en = new GenFilter("stars_en", [ // stars en official "UCJxZpzx4wHzEYD-eCiZPikg", // tempus "UCyxtGMdWlURZ30WSnEjDOQw", "UC2hx0xVkMoHGWijwr_lA01w", // tempus 2 "UCHP4f7G2dWD4qib7BMatGAw", "UC060r4zABV18vcahAWR1n7w", "UC7gxU6NXjKF1LrgOddPzgTw", "UCMqGG8BRAiI1lJfKOpETM_w", // armis "UCajbFh6e_R8QZdHAMbbi4rQ", "UCJv02SHZgav7Mv3V0kBOR8Q", "UCLk1hcmxg8rJ3Nm1_GvxTRA", "UCTVSOgYuSWmNAt-lnJPkEEw", ]); filters.stars_all = new GenFilter("stars_all", [].concat(filters.stars.channels, filters.stars_en.channels)); filters.outside = new Filter("outside", (() => { var allChannels = [].concat(filters.jp.channels, filters.en.channels, filters.id.channels, filters.devis.channels, filters.stars.channels, filters.stars_en.channels); return s => allChannels.indexOf(s.channel.id) == -1; })()); filters.singing = new CategoryFilter("singing", ["singing"]); filters.talk = new CategoryFilter("talk", [ "asmr","camera_stream","chatbot","cooking_stream","drawing","drawing_personality_test","duolingo","eat","english_lesson","english_only", "japanese_lesson","japanese_only","mashmallow","morning","review","talk","totus","trpg","unboxing","vlog","vrchat","watchalong" ]); filters.recommend = new CategoryFilter("recommend", [ "announce", "celebration", "3d_stream", "debut_stream", "birthday", "anniversary", "totsu", "outfit_reveal", "teaser"]); filters.membersonly = new CategoryFilter("membersonly", "membersonly", ["メン限", "メンバー限定"]); filters.rpg = new CategoryFilter("rpg", [ "13_sentinels:_aegis_rim","ace_attorney","armored_core","assassings_creed","bioshock","bloodborne","black_myth:wukong","cyberpunk_2077", "dark_souls","dead_space","death_stranding","demon's_soul","detroit:_become_human","devil_may_cry","dragon_ball_z_:_kakarot","dragon's_dogma","dragon_quest","dying_light", "earthbound","elden_ring","elder_scrolls", "fate/samurai_remnant","fallout","final_fantasy","final_fantasy_online","fire_emblem","fire_emblem", "ghostwire: tokyo","gta","gta5", "hogwarts_legacy","judgment","live_a_live", "mafia","metal_gear","metal_gear","nier","no_man's_sky", "omori","one_piece_odyssey","persona","persona","pokemon","read_dead","romancing_saga","sekiro","starfield", "tales_of","undertale","undertale","yakuza","yokai_watch","witcher3","zelda", ]); filters.pvp = new CategoryFilter("pvp", [ "apex","call_of_duty","counter-strike", "darker_and_darker","dbd","deltarune","dota","fallguys","fortnite","guilty_gear", "hearthstone","league_of_legends","mariokart","mahjong", "overwatch","pupg","rainbow_six", "slither.io","smash","soulcalibur","splatoon","street_fighter","tarkov","tarkov","teamfight_tactics","tetris", "valorant","yu-gi-oh!" ]); filters.coop = new CategoryFilter("coop", [ "7_days_to_die","a_way_out","among_us","animal_crossing","ark","buckshot_roulette","back4blood","bokura","content_warning","core_keeper","craftopia", "devour","fifa","gartic_phone","ghost_exorism_inc.","gta", "hacktag","heave_ho","hide_and_shriek","human_fall_flat","it_takes_two","keep_talking_nobody_explode", "l4d2","lethal_company","liar's_bar","mario_party","minecraft","monhan","monopoly", "operation_tango","overcooked","party_animals","phasmophobia","pico_park","plateup","project_winter","pummel_party","raft","rust", "school_labyrinth","super_bunny_man","terraria","the_forest","ultimate_chicken_horse","uno","unrailed","unravel_two", "valheim","vrchat","words_on_stream","we_were_here" ]); filters.horror = new CategoryFilter("horror", [ "ao_oni","convenience_store","devour","don't_scream", "fatal_frame","friday_night_funkin'","ghost_room", "ib","imi_ga_wakaru_to_kowai_manga","inunaki_tunnel","jumpscare_scare_jump","kinki_spiritual_affairs_bureau","kusodeka_bayashi", "little_nightmares","luigi's_mansion","night_deliverry","night_security", "naribikimura", "outlast","poppy_playtime","parasocial","phasmophobia","residentevil","poppy_playtime", "school_labyrinth","silent_hill","shadow_corridor","shinkansen_0","soma", "the_bathhouse","the_closing_shift","the_karaoke","the_working_dead","tsugunohi","visage","yomawari", ]); filters.hololive = new CategoryFilter("hololive", [ "holocure","hololiveerror","hololive_treasure_mountain","holoparade","holo_x_break","idle_showdown","miko_in_maguma","wowowow_korone_box", ], ["Miko In Maguma","WOWOWOW KORONE"]); filters.video = new Filter("video", (() => { var category = ["shorts", "music_covor", "original_song", "dancing", "animation"]; 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; } // Using Holodex/HoloAPI V2 (2.0) // @see https://docs.holodex.net/#section/LICENSE function Holodex() { this.APIKEY = atob('NjYxZjdmOGYtZTM1My00ZDMzLTk2ZjgtMTg3ZTU3NGQ2MmQw'); this.UA = `${APPNAME}/${VERSION}`; 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": this.APIKEY, "user-agent": this.UA, }, 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)}`); } this.getFavorite = function (isUser = true) { return this.XHR(`https://holodex.net/api/v2/users/${isUser ? "users/" : ""}favorites`); } } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址