您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Make Youtube be more comfortable to use
// ==UserScript== // @name Comfortable Youtube // @description Make Youtube be more comfortable to use // @encoding utf-8 // @namespace https://github.com/01101sam/Comfortable-Youtube // @homepage https://github.com/01101sam/Comfortable-Youtube // @supportURL https://github.com/01101sam/Comfortable-Youtube/issues // @author Sam01101 // @version 1.4.5 // @icon https://www.youtube.com/favicon.ico // @license MIT // @run-at document-start // @match http*://youtube.com/* // @match http*://www.youtube.com/* // @compatible chrome // @compatible firefox // @compatible edge // @connect youtube.com // @grant GM_setValue // @grant GM_getValue // ==/UserScript== // noinspection ExceptionCaughtLocallyJS // Utils function getTime() { // Get the current time in seconds const now = Date.now() / 1000, // Extract the integer part (seconds since Unix epoch) seconds = Math.floor(now); return { seconds: seconds.toString(), // and fractional part (simulate nanoseconds) nanos: Math.floor((now - seconds) * 1e9) }; } function numberFormat(numberState) { return getNumberFormatter().format(numberState); } // https://github.com/search?q=repo%3AAnarios/return-youtube-dislike%20getNumberFormatter&type=code function getNumberFormatter() { let userLocales; if (document.documentElement.lang) { userLocales = document.documentElement.lang; } else if (navigator.language) { userLocales = navigator.language; } else { try { userLocales = new URL( Array.from(document.querySelectorAll("head > link[rel='search']")) ?.find((n) => n?.getAttribute("href")?.includes("?locale=")) ?.getAttribute("href") )?.searchParams?.get("locale"); } catch { cLog( "Cannot find browser locale. Use en as default for number formatting." ); userLocales = "en"; } } return Intl.NumberFormat(userLocales, { notation: "compact", compactDisplay: "short", }); } // region Player JSON // Remove Ads function removeAds(json) { for (const key of ["adBreakHeartbeatParams", "adPlacements", "adSlots", "playerAds"]) { if (json[key]) { console.debug(`Video ${key} removed.`); delete json[key]; } } } // Remove "Are you there? (youThere)" Prompt function removeYouThere(json) { if (json.messages) json.messages = json.messages.filter(message => { if ("youThereRenderer" in message) console.debug('"Are you there (youThere)" asking removed.'); return !("youThereRenderer" in message); }); } // Remove tracking function removeTracking(json) { if (json.playbackTracking) { ["ptrackingUrl", "atrUrl", "qoeUrl"].forEach(urlName => { delete json.playbackTracking[urlName]; }); console.debug("Youtube tracking removed."); } } // Restore download feature function restoreDownload(json) { if (json.frameworkUpdates.entityBatchUpdate?.mutations) { const payload = json.frameworkUpdates.entityBatchUpdate.mutations[0].payload; if (payload.offlineabilityEntity && payload.offlineabilityEntity.addToOfflineButtonState !== "ADD_TO_OFFLINE_BUTTON_STATE_ENABLED") { const entityKey = payload.offlineabilityEntity.key; payload.offlineabilityEntity = { addToOfflineButtonState: "ADD_TO_OFFLINE_BUTTON_STATE_ENABLED", contentCheckOk: false, key: entityKey, racyCheckOk: false, offlineabilityRenderer: "CAEaGQoVChMKEeWFqOmrmOa4hSAoMTA4MHApGAcaEgoOCgwKCumrmCAoNzIwcCkYAhoSCg4KDAoK5LitICgzNjBwKRgBGhIKDgoMCgrkvY4gKDE0NHApGAQiDTILb2ZmbGluZWxpc3Q=" } if (!json.playabilityStatus.offlineability) json.playabilityStatus.offlineability = { "offlineabilityRenderer": { "offlineable": true } } console.debug("Download feature restored."); } } } // Modify player function handlePlayer(playerJson) { const liveBroadcastDetails = playerJson.microformat?.playerMicroformatRenderer?.liveBroadcastDetails; if (liveBroadcastDetails && liveBroadcastDetails?.isLiveNow) { isLive = true; } removeAds(playerJson); removeYouThere(playerJson); removeTracking(playerJson); restoreDownload(playerJson); } // endregion // region Browse JSON const browseJsonFilterRenderFunc = content => { const deleteRender = !( content.richItemRenderer?.content?.adSlotRenderer || content.richSectionRenderer?.content?.statementBannerRenderer?.titleFontFamily === "PROMO_FONT_FAMILY_YOUTUBE_SANS_BOLD" ); if (!deleteRender) console.debug("Video slot AD removed."); return deleteRender; }; // Remove promoion overlay function removePromoOverlay(json) { if (json.overlay?.mealbarPromoRenderer) { delete json.overlay.mealbarPromoRenderer; console.debug("Chrome recommendion removed."); } } // Remove home page banner promotion function removeHeaderMasterThreadPromo(json) { if (json.contents?.twoColumnBrowseResultsRenderer?.tabs) { const tab = json.contents.twoColumnBrowseResultsRenderer.tabs.find(tab => tab.tabRenderer?.selected && tab.tabRenderer.content?.richGridRenderer)?.tabRenderer?.content; if (tab) tab.richGridRenderer.contents = tab.richGridRenderer.contents.filter(browseJsonFilterRenderFunc); if ( tab && tab.richGridRenderer.masthead && ( tab.richGridRenderer.masthead.bannerPromoRenderer || tab.richGridRenderer.masthead.adSlotRenderer ) ) { delete tab.richGridRenderer.masthead; console.debug("Removed home page banner promotion."); } } } // Remove home page feed promotion (For continue browsing homepage) function removeHomePageFeedPrototion(json) { if (json.onResponseReceivedActions) { const actions = json.onResponseReceivedActions.find(action => action.appendContinuationItemsAction); if (actions) actions.appendContinuationItemsAction.continuationItems = actions.appendContinuationItemsAction.continuationItems.filter(browseJsonFilterRenderFunc); } } // Remove sign-in promotion function removeSignInPromo(json) { if (json.items) json.items = json.items.filter(item => !item.guideSigninPromoRenderer); } // Remove search result promotion function removeSearchResultPromo(json) { if (json.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents) { const contents = json.contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents; for (const content of contents) { if (content.itemSectionRenderer) { content.itemSectionRenderer.contents = content.itemSectionRenderer.contents.filter(content => { if (content.adSlotRenderer) console.debug("Search result AD removed."); return !content.adSlotRenderer; }); } } } } // Modify browse function handleBrowse(browseJson) { removePromoOverlay(browseJson); removeHeaderMasterThreadPromo(browseJson); removeHomePageFeedPrototion(browseJson); removeSignInPromo(browseJson); removeSearchResultPromo(browseJson); } // endregion function removeNextWatchColAd(json) { if (json.contents?.twoColumnWatchNextResults?.secondaryResults?.secondaryResults?.results) { const results = json.contents.twoColumnWatchNextResults.secondaryResults.secondaryResults.results; for (const result of results) { if (result.adSlotRenderer) { console.debug("Next watch column AD removed."); delete result.adSlotRenderer; } } } } // Modify next function handleNext(nextJson) { removeNextWatchColAd(nextJson); } // endregion // region Download function fetchPlayerSync(url, context, videoId) { console.debug("[Comfortable YT]", "Download:", "Fetching video player response"); const xhr = window.unsafeWindow.XMLHttpRequest(); let playerResponseJson; try { xhr.open("POST", url, false); xhr.withCredentials = true; xhr.setRequestHeader("Content-Type", "application/json"); xhr.send(JSON.stringify({ context, videoId })); if (xhr.status !== 200) throw new Error(`Player response status is ${xhr.status}`); else if (!xhr.responseText) throw new Error("Player response is empty"); playerResponseJson = JSON.parse(xhr.responseText); if (playerResponseJson.playabilityStatus?.status !== "OK") { console.error("[Comfortable YT]", "Video player download failed:", `Player response status is ${playerResponseJson.playabilityStatus?.status}`); return; } return playerResponseJson; } catch (e) { console.error("[Comfortable YT]", "Video player download failed:", "Failed to fetch player response:", e); } } // Handle offline video function handleOfflineVideo(url, requestJson, json) { const requestUrl = new URL(url.startsWith("/") ? `https://www.youtube.com${url}` : url); requestUrl.pathname = "/youtubei/v1/player"; // Fetch playerResponseJson const playerResponseJson = fetchPlayerSync(requestUrl.toString(), requestJson.context, requestJson.videoIds[0]); if (!playerResponseJson) return; console.debug("Replaced offline video data."); return { responseContext: playerResponseJson.responseContext, videos: [{ offlineVideoData: { videoId: requestJson.videoIds[0], thumbnail: playerResponseJson.videoDetails.thumbnail, // channel: { // offlineChannelData: { // channelId: playerResponseJson.videoDetails.channelId, // thumbnail: playerResponseJson.microformat.playerMicroformatRenderer.thumbnail, // title: playerResponseJson.videoDetails.author, // isChannelOwner: playerResponseJson.videoDetails.isOwnerViewing // } // }, title: playerResponseJson.videoDetails.title, // lengthText: "0:00", // publishedTimestamp: "0", viewCount: playerResponseJson.videoDetails.viewCount, shareUrl: `https://youtu.be/${playerResponseJson.videoDetails.videoId}`, description: playerResponseJson.microformat.playerMicroformatRenderer.description, shortViewCountText: numberFormat(playerResponseJson.videoDetails.viewCount), // likesCount: "0", lengthSeconds: playerResponseJson.videoDetails.lengthSeconds } }] } } // Handle download action function handleDownloadAction(requestJson, json) { const videoId = requestJson["videoId"], formatType = requestJson["preferredFormatType"], entityKey = btoa(atob("EgsAAAAAAAAAAAAAACDKASgB").replace("\x00".repeat(11), videoId)); if (json.onResponseReceivedCommand.openPopupAction?.popup?.offlinePromoRenderer) { // Not premium const clickTrackingParams = json.onResponseReceivedCommand.clickTrackingParams; const commandOne = requestJson.preferredFormatType === "UNKNOWN_FORMAT_TYPE" ? { clickTrackingParams, openPopupAction: { popupType: "DIALOG", replacePopup: true, popup: { downloadQualitySelectorRenderer: { trackingParams: clickTrackingParams, downloadQualityPickerEntityKey: entityKey, premiumIcon: { "iconType": "SAD" }, premiumDescription: { runs: [{ text: "You are not YouTube Premium" }] }, onSubmitEndpoint: { clickTrackingParams, offlineVideoEndpoint: { action: "ACTION_ADD", videoId, actionParams: { formatType, settingsAction: "DOWNLOAD_QUALITY_SETTINGS_ACTION_SAVE" } } } } } } } : { clickTrackingParams, offlineVideoEndpoint: { videoId, action: "ACTION_ADD", actionParams: { "formatType": formatType, "settingsAction": "DOWNLOAD_QUALITY_SETTINGS_ACTION_ALREADY_SAVED" } } }; if (requestJson.preferredFormatType === "UNKNOWN_FORMAT_TYPE") { // Next, frameworkUpdates json.frameworkUpdates = { entityBatchUpdate: { timestamp: getTime(), mutations: [{ entityKey, type: "ENTITY_MUTATION_TYPE_REPLACE", payload: { downloadQualityPickerEntity: { "key": entityKey, "formats": [ { "name": "HD (1080p)", "format": "HD_1080" }, { "name": "HD (720p)", "format": "HD" }, { "name": "SD (480p)", "format": "SD" }, { "name": "LD (144p)", "format": "LD" }, { "name": "Use this feature at your risk!", "availabilityType": "OFFLINEABILITY_AVAILABILITY_TYPE_PREMIUM_LOCKED", "format": "UNKNOWN_FORMAT_TYPE" } ] } } }] } } } json.onResponseReceivedCommand = { clickTrackingParams, commandExecutorCommand: { "commands": [ commandOne, { clickTrackingParams, signalAction: { signal: "REQUEST_PERSISTENT_STORAGE" } } ] } } console.debug("Replaced download action from normal user to Premium."); } } // Handle Playback data entity function handlePlaybackDataEntity(url, requestJson, json) { function toTransferKey(str) { // Step 1: URL decode const urlDecoded = decodeURIComponent(str); // Step 2: Base64 decode const base64Decoded = atob(urlDecoded); // Step 3: Convert to byte array const byteArray = new Uint8Array([...base64Decoded].map(c => c.charCodeAt(0))); // Step 4: Change the last third byte byteArray[14]++; // Step 5: Convert back to string const modifiedString = String.fromCharCode.apply(null, byteArray); // Step 6: Base64 encode const base64Encoded = btoa(modifiedString); // Step 7: URL encode return encodeURIComponent(base64Encoded); } const mutations = json.frameworkUpdates.entityBatchUpdate.mutations, entityKey = requestJson["videos"][0]["entityKey"], transferEntityKey = toTransferKey(entityKey), requestUrl = new URL(url.startsWith("/") ? `https://www.youtube.com${url}` : url), lastUpdatedTimestampSeconds = Number(mutations[0].payload['offlineVideoPolicy']['lastUpdatedTimestampSeconds']); requestUrl.pathname = "/youtubei/v1/player"; // Fetch playerResponseJson const playerResponseJson = fetchPlayerSync(requestUrl.toString(), requestJson.context, atob(decodeURIComponent(entityKey)).substring(2, 13)); if (!playerResponseJson) return; // Edit offlineVideoPolicy Object.assign(mutations[0].payload['offlineVideoPolicy'], { action: "OFFLINE_VIDEO_POLICY_ACTION_OK", shortMessageForDisabledAction: undefined, expirationTimestamp: lastUpdatedTimestampSeconds + (1 * 60 * 60 * 24 * 365), // 1 Year }); if (requestJson["videos"][0]["refreshData"]) { // Offline video refresh console.debug("Replaced offline video refresh playback data entity."); return; } // Edit playbackData Object.assign(mutations[1].payload['playbackData'], { transfer: transferEntityKey, streamDownloadTimestampSeconds: mutations[1].payload['playbackData']['playerResponseTimestamp'], playerResponseJson: JSON.stringify({ responseContext: playerResponseJson['responseContext'], attestation: playerResponseJson['attestation'], microformat: playerResponseJson['microformat'], offlineState: { action: "OK", expiresInSeconds: 1 * 60 * 60 * 24 * 365, // 1 Year isOfflineSharingAllowed: true, refreshInSeconds: 1 * 60 * 60 * 24 * 365, // 1 Year }, playabilityStatus: playerResponseJson['playabilityStatus'], playbackTracking: playerResponseJson['playbackTracking'], playerConfig: playerResponseJson['playerConfig'], storyboards: playerResponseJson['storyboards'], streamingData: playerResponseJson['streamingData'], videoDetails: playerResponseJson['videoDetails'], trackingParams: playerResponseJson['trackingParams'] }) }); // Add orchestrationActions Object.assign(json, { orchestrationActions: [{ entityKey: transferEntityKey, actionType: "OFFLINE_ORCHESTRATION_ACTION_TYPE_ADD", actionMetadata: { priority: 1, transferEntityActionMetadata: requestJson["videos"][0].downloadParameters?.maximumDownloadQuality ? { maximumDownloadQuality: requestJson["videos"][0].downloadParameters.maximumDownloadQuality } : undefined } }] }); console.debug("Replaced playback data entity."); } // endregion (function () { "use strict"; const wind = window.unsafeWindow; let lastAudioUrl; let isLive = false; // Add a option to settings panel function addSettingOption(text, idName, callback) { const elem = document.getElementById(idName); if (elem) { // Already exist, callback is added elem.onclick = function (e) { GM_setValue(idName, !GM_getValue(idName)); this.setAttribute("aria-checked", GM_getValue(idName)); callback(GM_getValue(idName)); }; return; } const divMenuItemElem = document.createElement("div"); const divIconElem = document.createElement("div"); const divLabelElem = document.createElement("div"); const divContentElem = document.createElement("div"); divMenuItemElem.classList.add("ytp-menuitem"); divIconElem.classList.add("ytp-menuitem-icon"); divLabelElem.classList.add("ytp-menuitem-label"); divContentElem.classList.add("ytp-menuitem-content"); divContentElem.innerHTML = '<div class="ytp-menuitem-toggle-checkbox">'; divLabelElem.innerText = text; divMenuItemElem.id = idName; divMenuItemElem.setAttribute("aria-checked", GM_getValue(idName)); divMenuItemElem.onclick = function (e) { GM_setValue(idName, !GM_getValue(idName)); this.setAttribute("aria-checked", GM_getValue(idName)); callback(GM_getValue(idName)); }; divMenuItemElem.appendChild(divIconElem); divMenuItemElem.appendChild(divLabelElem); divMenuItemElem.appendChild(divContentElem); document .getElementsByClassName("ytp-panel-menu")[0] .appendChild(divMenuItemElem); } // Listener to reset var wind.addEventListener("yt-navigate-start", function () { lastAudioUrl = undefined; isLive = false; }); // Listener to load options wind.addEventListener("yt-navigate-finish", function () { const video = document.getElementsByTagName("video")[0]; if (video) { addSettingOption("Audio Mode", "audio-mode", audioModeCall); // Init audioModeCall(GM_getValue("audio-mode")); } }); // Audio Mode function audioModeCall(enabled) { // console.debug("Audio mode", enabled); if (isLive) { const container = document.getElementsByClassName( "html5-video-container" )[0]; container.style.display = enabled ? "none" : ""; } else audioReplaceSrcUrl(); } // Audio Mode - Replace source url function audioReplaceSrcUrl() { const videoElem = document.getElementsByClassName("html5-main-video")[0]; const isPuased = videoElem.paused; const currTime = videoElem.currentTime; const enabled = GM_getValue("audio-mode"); if (enabled && !lastAudioUrl) videoElem.load(); else if (enabled && lastAudioUrl && videoElem.src.startsWith("blob")) { // console.debug("Replaced to audio url"); videoElem.src = lastAudioUrl; videoElem.pause(); videoElem.currentTime = currTime; if (!isPuased) videoElem.play(); } else if (!enabled && lastAudioUrl) { const player = document.getElementsByTagName("ytd-player")[0].getPlayer(); // console.debug("Player refreshed"); const videoId = player.getVideoData().video_id; player.cueVideoById(videoId); videoElem.pause(); videoElem.currentTime = currTime; if (!isPuased) player.playVideo(); } } // Apply network hook function applyNetworkHook() { const OldXMLHttpRequest = wind.XMLHttpRequest, origFetch = wind.fetch; class XMLHttpRequestHook { constructor() { const xhr = new OldXMLHttpRequest(); // Hook original method this._open = xhr.open.bind(xhr); this._send = xhr.send.bind(xhr); this.xhr = xhr; // Args store this.blocked = false; // Block sending request this.requestBody = null; this.requestArgs = null; this.mockResponse = null; this.mockReadyState = null; this.mockStatus = null; // Callback this.callbacks = {}; } get status() { return this.mockStatus ? this.mockStatus : this.xhr.status; } get readyState() { return this.mockReadyState ? this.mockReadyState : this.xhr.readyState; } get response() { return this.mockResponse && this.xhr.readyState === 4 ? this.mockResponse : this.xhr.response; } get responseText() { return this.mockResponse && this.xhr.readyState === 4 ? this.mockResponse : this.xhr.responseText; } addEventListener(event, callback) { if ([ "loadstart", "progress", "load", "loadend", "readystatechange" ].includes(event)) return this.callbacks[event] = callback; return this.xhr[`on${event}`] = callback; } eventHook() { this.mockResponse = handleXMLResponse.call(this.xhr, this, this.requestBody, this.requestArgs); } open(method, url) { if (url.startsWith("//")) url = `https:${url}`; else if (!url.startsWith("http")) url = `${location.origin}${url}`; let parsedUrl; try { parsedUrl = new URL(url); } catch (e) { return this.xhr.open.apply(this.xhr, arguments); } // console.debug("[Comfortable YT] XMLHttpRequest", method, url); this._open.apply(this.xhr, arguments); this.requestArgs = arguments; if (!handleRequest("XML", method, parsedUrl)) { this.blocked = true; this.mockReadyState = 4; // console.debug("[Comfortable YT] Blocked:", method, url); } } send(body) { if (this.blocked) { Object.values(this.callbacks).forEach(callback => callback.call(this.xhr)); return; } if (body) this.requestBody = body; const that = this; Object.entries(this.callbacks).forEach(([event, callback]) => this.xhr[`on${event}`] = function () { (`${event}Hook` in that ? that[`${event}Hook`] : that.eventHook).call(that); callback.call(that); }); if (!Object.keys(this.callbacks).length) this.xhr.onreadystatechange = this.eventHook.bind(this); this._send.apply(this.xhr, arguments); } } const xmlHttpRequestHook = function () { const hook = new XMLHttpRequestHook(), xhr = hook.xhr; return new Proxy(xhr, { get(_, key) { if (key.startsWith("on") && key.substring(2) in hook.callbacks) return hook.callbacks[key.substring(2)]; const value = (key in hook ? hook : xhr)[key]; return typeof value === 'function' ? value.bind(key in hook ? hook : xhr) : value; }, set(_, key, value) { if (key.startsWith("on")) { // Usually event listener if (typeof value === 'function') { hook.addEventListener(key.substring(2), value); return true; } } (key in hook ? hook : xhr)[key] = value; return true; } }); } const fetchHook = async function (request) { try { let url = request.url || request; if (url.startsWith("//")) url = `https:${url}`; else if (!url.startsWith("http")) url = `${location.origin}${url}`; if (!handleRequest("Fetch", request.method || "GET", new URL(url))) return new Response(); const clonedRequest = request instanceof Request ? request.clone() : null; // console.debug("[Comfortable YT] fetch", url, request); const response = await origFetch.apply(this, arguments); if (["error", "opaqueredirect"].includes(response.type) || response.status === 0) return response; return await handleFetchResponse(clonedRequest, response); } catch (e) { console.error("[Comfortable YT]", "fetch error:", e); } } wind.XMLHttpRequest = xmlHttpRequestHook; wind.fetch = fetchHook; wind.navigator.sendBeacon = (url, data) => true; // Block analytics data sent to Youtube console.info("[Comfortable YT]", "Network hook applied."); } // Handle request function handleRequest(source, method, url) { if (url.pathname.startsWith("/youtubei/v1/log_event") || url.pathname.startsWith("/log")) return; try { if (GM_getValue("audio-mode")) { // Replace to audio source url if ( (url.searchParams.get("mime") || "").includes("audio") && !isLive ) { url.searchParams.delete("range"); lastAudioUrl = url.toString(); audioReplaceSrcUrl(); } } } catch (e) { console.error("[Comfortable YT]", `handle${source}Request`, "Error:", e); } return true; } // Handle xml response function handleXMLResponse(hook, requestText, requestArgs) { if (this.readyState === 4 && this.responseURL) { try { const url = new URL(this.responseURL); let jsonResp; // console.debug("[Comfortable YT] URL Path: ", url.pathname); switch (url.pathname) { case "/youtubei/v1/player": jsonResp = JSON.parse(this.responseText); handlePlayer(jsonResp); return JSON.stringify(jsonResp); case "/youtubei/v1/browse": jsonResp = JSON.parse(this.responseText); handleBrowse(jsonResp); return JSON.stringify(jsonResp); case "/youtubei/v1/next": jsonResp = JSON.parse(this.responseText); handleNext(jsonResp); return JSON.stringify(jsonResp); case "/youtubei/v1/offline": if (this.status !== 401) return; jsonResp = JSON.parse(this.responseText); jsonResp = handleOfflineVideo(requestArgs[1], JSON.parse(requestText), jsonResp); Object.defineProperty(hook, "status", { writable: true }); hook.status = 200; return JSON.stringify(jsonResp); case "/youtubei/v1/offline/get_download_action": jsonResp = JSON.parse(this.responseText); handleDownloadAction(null, jsonResp); return JSON.stringify(jsonResp); case "/youtubei/v1/offline/get_playback_data_entity": jsonResp = JSON.parse(this.responseText); handlePlaybackDataEntity(requestArgs[1], JSON.parse(requestText), jsonResp); return JSON.stringify(jsonResp); default: // console.debug(url.pathname); break; } } catch (e) { console.error("[Comfortable YT]", "handleXMLResponse", `URL(${this.responseURL})`, "Error:", e); } } } // Handle fetch response async function handleFetchResponse(req, resp) { try { if (!(resp.headers.get("Content-Type") || "").includes("application/json")) return resp; const url = new URL(resp.url || (req && req.url)); if (!url.pathname.startsWith("/youtubei/v1/")) return resp; let jsonResp; switch (url.pathname) { case "/youtubei/v1/player": jsonResp = await resp.json(); handlePlayer(jsonResp); resp = createFetchResponse(jsonResp, resp); break; case "/youtubei/v1/browse": jsonResp = await resp.json(); handleBrowse(jsonResp); resp = createFetchResponse(jsonResp, resp); break; case "/youtubei/v1/next": jsonResp = await resp.json(); handleNext(jsonResp); resp = createFetchResponse(jsonResp, resp); break; case "/youtubei/v1/offline": if (resp.status === 401) return; jsonResp = await resp.json(); jsonResp = handleOfflineVideo(req.url, await req.json(), jsonResp); resp = createFetchResponse(jsonResp, resp, 200); break; case "/youtubei/v1/offline/get_download_action": jsonResp = await resp.json(); handleDownloadAction(await req.json(), jsonResp); resp = createFetchResponse(jsonResp, resp); break; case "/youtubei/v1/offline/get_playback_data_entity": jsonResp = await resp.json(); handlePlaybackDataEntity(req.url, await req.json(), jsonResp); resp = createFetchResponse(jsonResp, resp); break; default: // console.debug(url.pathname); break; } } catch (e) { console.error("[Comfortable YT]", "handleFetchResponse", `URL(${resp.url})`, "Error:", e); } return resp; } // Handle fetch response body function createFetchResponse(json, data, customStatus) { const response = new Response(JSON.stringify(json), { status: customStatus || data.status, statusText: data.statusText, headers: data.headers, }); ["ok", "redirected", "type", "url"].forEach(items => { Object.defineProperty(response, items, { value: data[items] }); }); return response; } // Hook ytInitialPlayerResponse before page is fully loaded Object.defineProperty(wind, "ytInitialPlayerResponse", { configurable: true, set: function (playerResponse) { delete wind.ytInitialPlayerResponse; handlePlayer(playerResponse); wind.ytInitialPlayerResponse = playerResponse; }, }); // Hook ytInitialData before page is fully loaded Object.defineProperty(wind, "ytInitialData", { configurable: true, set: function (browseJson) { delete wind.ytInitialData; handleBrowse(browseJson); handleNext(browseJson); // The next video result also visible in Initial Data wind.ytInitialData = browseJson; }, }); // Init applyNetworkHook(); })();