您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Exposes API of Youtube to control Youtube remotely
// ==UserScript== // @name Youtube API Exposer // @description Exposes API of Youtube to control Youtube remotely // @version 2.1.2 // @compatible firefox // @namespace https://github.com/albionah // @homepageURL https://github.com/albionah/YoutubeApiExposer // @author albionah // @match *://*.youtube.com/* // @grant unsafeWindow // @license GNU General Public License v3.0 // ==/UserScript== let connection; function getBasicElements() { return new Promise((resolve) => { const video = document.querySelector("video"); const player = unsafeWindow.document.getElementById("movie_player"); if (video && player) { resolve({video, player}); } else { setTimeout(() => getBasicElements().then(resolve), 1000); } }); } getBasicElements().then(({video, player}) => { video.addEventListener("canplaythrough", () => { console.debug("event canplaythrough"); uploadBasicInfo(player); }); video.addEventListener("play", () => { console.debug("event play"); uploadBasicInfo(player); }); video.addEventListener("pause", () => { console.debug("event pause"); uploadBasicInfo(player); }); connect(video, player); }); function getMediaInfo(player) { return new Promise((resolve) => { const title = player.getVideoData()?.title; if (title) { const stats = player.getVideoStats(); resolve({ title: title ?? h1s[0].textContent, videoId: stats.docid, duration: Number.parseFloat(stats.len), currentPosition: { position: Number.parseFloat(stats.lct), timestamp: new Date().getTime() }, isPlaying: stats.vpa !== "1" }); } else { setTimeout(() => getMediaInfo().then(resolve), 100); } }); } function uploadBasicInfo(player) { getMediaInfo(player).then((mediaInfo) => { console.debug("media info", mediaInfo); connection?.send(JSON.stringify(mediaInfo)); }); } const videoIdPattern = new RegExp("\/watch\\?v=(.+)"); function findSuitableLink() { const links = unsafeWindow.document.querySelectorAll("a.yt-simple-endpoint.ytd-thumbnail"); const suitableLink = Array.from(links).find((el) => el.href.match(videoIdPattern)); if (suitableLink) return suitableLink; else throw new Error("cannot hot reload"); } function watchWithHotReload(newVideoId) { const element = findSuitableLink(); element.data.commandMetadata.webCommandMetadata.url = "/watch?v=" + newVideoId; element.data.watchEndpoint.videoId = newVideoId; element.click(); } function watchWithHardReload(videoId) { location.href = `?v=${videoId}`; } function watch(videoId) { try { watchWithHotReload(videoId); } catch (error) { console.warn(error.message); watchWithHardReload(videoId); } } function connect(video, player) { const websocket = new WebSocket("ws://localhost:7789"); websocket.onopen = () => { console.debug("connected to Youtube controller"); connection = websocket; uploadBasicInfo(player); } websocket.onmessage = (rawMessage) => { try { console.debug(rawMessage.data); const message = JSON.parse(rawMessage.data); switch (message.type) { case "playOrPause": const event = new KeyboardEvent('keydown', {'keyCode': 75, 'which': 75}); document.dispatchEvent(event); break; case "play": video.play(); break; case "pause": video.pause(); break; case "stop": video.stop(); break; case "watchPrevious": player.previousVideo(); break; case "watchNext": player.nextVideo(); break; case "watch": console.debug(`watch ${message.id}`); watch(message.id); break; } } catch (error) { console.error(error); } } websocket.onclose = (event) => { console.info('Socket is closed. Reconnect will be attempted in 3 seconds.', event.reason); connection = undefined; setTimeout(() => connect(video, player), 3000); }; websocket.onerror = (error) => { console.error('Socket encountered error: ', error.message, 'Closing socket'); websocket.close(); }; } window.addEventListener('beforeunload', () => { connection?.close(); });