認証周りの修正
This commit is contained in:
parent
a05af1a208
commit
16ae1084da
|
|
@ -7,70 +7,75 @@ import type {
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
const LOG = "[yt-playlist-features]";
|
export interface InitialParseResult {
|
||||||
|
metadata: PlaylistMetadata;
|
||||||
|
videos: PlaylistVideo[];
|
||||||
|
}
|
||||||
|
|
||||||
export function parsePlaylistData(raw: any): PlaylistData | null {
|
export function parseInitialData(raw: any): InitialParseResult | null {
|
||||||
try {
|
try {
|
||||||
const metadata = extractMetadata(raw);
|
const metadata = extractMetadata(raw);
|
||||||
if (!metadata) {
|
if (!metadata) return null;
|
||||||
console.warn(LOG, "Could not extract metadata");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const videoListContents = findVideoListContents(raw);
|
const videoListContents = findVideoListContents(raw);
|
||||||
if (!videoListContents) {
|
if (!videoListContents) return null;
|
||||||
console.warn(LOG, "Could not find video list");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const videos: PlaylistVideo[] = [];
|
const { videos } = parseVideoList(videoListContents);
|
||||||
let hasMore = false;
|
return { metadata, videos };
|
||||||
|
|
||||||
for (const item of videoListContents) {
|
|
||||||
if (item.playlistVideoRenderer) {
|
|
||||||
const video = parseVideo(item.playlistVideoRenderer);
|
|
||||||
if (video) videos.push(video);
|
|
||||||
}
|
|
||||||
if (item.continuationItemRenderer) {
|
|
||||||
hasMore = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
metadata,
|
|
||||||
videos,
|
|
||||||
extractedAt: new Date().toISOString(),
|
|
||||||
isComplete: !hasMore,
|
|
||||||
extractedCount: videos.length,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(LOG, "Failed to parse playlist data:", e);
|
console.error("[yt-playlist-features] Failed to parse playlist data:", e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildPlaylistData(
|
||||||
|
metadata: PlaylistMetadata,
|
||||||
|
videos: PlaylistVideo[],
|
||||||
|
isComplete: boolean,
|
||||||
|
): PlaylistData {
|
||||||
|
return {
|
||||||
|
metadata,
|
||||||
|
videos,
|
||||||
|
extractedAt: new Date().toISOString(),
|
||||||
|
isComplete,
|
||||||
|
extractedCount: videos.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- internal helpers ---
|
||||||
|
|
||||||
|
function parseVideoList(contents: any[]): {
|
||||||
|
videos: PlaylistVideo[];
|
||||||
|
} {
|
||||||
|
const videos: PlaylistVideo[] = [];
|
||||||
|
|
||||||
|
for (const item of contents) {
|
||||||
|
if (item.playlistVideoRenderer) {
|
||||||
|
const video = parseVideo(item.playlistVideoRenderer);
|
||||||
|
if (video) videos.push(video);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { videos };
|
||||||
|
}
|
||||||
|
|
||||||
function extractMetadata(raw: any): PlaylistMetadata | null {
|
function extractMetadata(raw: any): PlaylistMetadata | null {
|
||||||
// Extract playlist ID from URL as fallback
|
|
||||||
const playlistId = extractPlaylistId(raw);
|
const playlistId = extractPlaylistId(raw);
|
||||||
if (!playlistId) return null;
|
if (!playlistId) return null;
|
||||||
|
|
||||||
// Title from metadata.playlistMetadataRenderer
|
|
||||||
const metadataRenderer = raw?.metadata?.playlistMetadataRenderer;
|
const metadataRenderer = raw?.metadata?.playlistMetadataRenderer;
|
||||||
const title = metadataRenderer?.title ?? "";
|
const title = metadataRenderer?.title ?? "";
|
||||||
|
|
||||||
// Sidebar has primary and secondary info
|
|
||||||
const sidebarItems =
|
const sidebarItems =
|
||||||
raw?.sidebar?.playlistSidebarRenderer?.items ?? [];
|
raw?.sidebar?.playlistSidebarRenderer?.items ?? [];
|
||||||
const primaryInfo = sidebarItems[0]?.playlistSidebarPrimaryInfoRenderer;
|
const primaryInfo = sidebarItems[0]?.playlistSidebarPrimaryInfoRenderer;
|
||||||
const secondaryInfo = sidebarItems[1]?.playlistSidebarSecondaryInfoRenderer;
|
const secondaryInfo = sidebarItems[1]?.playlistSidebarSecondaryInfoRenderer;
|
||||||
|
|
||||||
// Stats from primary info (e.g. "87 本の動画", "視聴回数 1,234 回", "最終更新日...")
|
|
||||||
const stats = primaryInfo?.stats ?? [];
|
const stats = primaryInfo?.stats ?? [];
|
||||||
const videoCount = parseVideoCount(stats[0]);
|
const videoCount = parseVideoCount(stats[0]);
|
||||||
const viewCountText = extractText(stats[1]) || null;
|
const viewCountText = extractText(stats[1]) || null;
|
||||||
const lastUpdatedText = extractText(stats[2]) || null;
|
const lastUpdatedText = extractText(stats[2]) || null;
|
||||||
|
|
||||||
// Description from pageHeaderViewModel or primary info
|
|
||||||
const pageHeaderVM =
|
const pageHeaderVM =
|
||||||
raw?.header?.pageHeaderRenderer?.content?.pageHeaderViewModel;
|
raw?.header?.pageHeaderRenderer?.content?.pageHeaderViewModel;
|
||||||
const description =
|
const description =
|
||||||
|
|
@ -78,14 +83,12 @@ function extractMetadata(raw: any): PlaylistMetadata | null {
|
||||||
extractText(primaryInfo?.description) ||
|
extractText(primaryInfo?.description) ||
|
||||||
"";
|
"";
|
||||||
|
|
||||||
// Owner from secondary info
|
|
||||||
const ownerRenderer =
|
const ownerRenderer =
|
||||||
secondaryInfo?.videoOwner?.videoOwnerRenderer;
|
secondaryInfo?.videoOwner?.videoOwnerRenderer;
|
||||||
const ownerRun = ownerRenderer?.title?.runs?.[0];
|
const ownerRun = ownerRenderer?.title?.runs?.[0];
|
||||||
const ownerEndpoint =
|
const ownerEndpoint =
|
||||||
ownerRun?.navigationEndpoint?.browseEndpoint;
|
ownerRun?.navigationEndpoint?.browseEndpoint;
|
||||||
|
|
||||||
// Thumbnails from pageHeaderViewModel heroImage or primary info
|
|
||||||
const thumbnails = extractPlaylistThumbnails(pageHeaderVM, primaryInfo);
|
const thumbnails = extractPlaylistThumbnails(pageHeaderVM, primaryInfo);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -93,7 +96,7 @@ function extractMetadata(raw: any): PlaylistMetadata | null {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
videoCount,
|
videoCount,
|
||||||
totalDurationText: null, // not available in current structure
|
totalDurationText: null,
|
||||||
viewCountText,
|
viewCountText,
|
||||||
lastUpdatedText,
|
lastUpdatedText,
|
||||||
thumbnails,
|
thumbnails,
|
||||||
|
|
@ -108,21 +111,18 @@ function extractMetadata(raw: any): PlaylistMetadata | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractPlaylistId(raw: any): string | null {
|
function extractPlaylistId(raw: any): string | null {
|
||||||
// Try microformat
|
|
||||||
const microformat =
|
const microformat =
|
||||||
raw?.microformat?.microformatDataRenderer?.urlCanonical;
|
raw?.microformat?.microformatDataRenderer?.urlCanonical;
|
||||||
if (microformat) {
|
if (microformat) {
|
||||||
const match = microformat.match(/[?&]list=([^&]+)/);
|
const match = microformat.match(/[?&]list=([^&]+)/);
|
||||||
if (match) return match[1];
|
if (match) return match[1];
|
||||||
}
|
}
|
||||||
// Try from appindexing link
|
|
||||||
const appLink =
|
const appLink =
|
||||||
raw?.metadata?.playlistMetadataRenderer?.androidAppindexingLink;
|
raw?.metadata?.playlistMetadataRenderer?.androidAppindexingLink;
|
||||||
if (appLink) {
|
if (appLink) {
|
||||||
const match = appLink.match(/[?&]list=([^&]+)/);
|
const match = appLink.match(/[?&]list=([^&]+)/);
|
||||||
if (match) return match[1];
|
if (match) return match[1];
|
||||||
}
|
}
|
||||||
// Fallback to URL
|
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
return url.searchParams.get("list");
|
return url.searchParams.get("list");
|
||||||
}
|
}
|
||||||
|
|
@ -193,7 +193,6 @@ function extractPlaylistThumbnails(
|
||||||
pageHeaderVM: any,
|
pageHeaderVM: any,
|
||||||
primaryInfo: any,
|
primaryInfo: any,
|
||||||
): Thumbnail[] {
|
): Thumbnail[] {
|
||||||
// Try heroImage from pageHeaderViewModel
|
|
||||||
const heroThumbnails =
|
const heroThumbnails =
|
||||||
pageHeaderVM?.heroImage?.contentPreviewImageViewModel?.image?.sources;
|
pageHeaderVM?.heroImage?.contentPreviewImageViewModel?.image?.sources;
|
||||||
if (heroThumbnails?.length) {
|
if (heroThumbnails?.length) {
|
||||||
|
|
@ -203,7 +202,6 @@ function extractPlaylistThumbnails(
|
||||||
height: t.height ?? 0,
|
height: t.height ?? 0,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
// Fallback to primary info thumbnail
|
|
||||||
return (
|
return (
|
||||||
primaryInfo?.thumbnailRenderer?.playlistVideoThumbnailRenderer?.thumbnail
|
primaryInfo?.thumbnailRenderer?.playlistVideoThumbnailRenderer?.thumbnail
|
||||||
?.thumbnails ??
|
?.thumbnails ??
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import browser from "webextension-polyfill";
|
import browser from "webextension-polyfill";
|
||||||
import { onPlaylistPageReady, getPlaylistId } from "./navigation";
|
import { onPlaylistPageReady, getPlaylistId } from "./navigation";
|
||||||
import { parsePlaylistData } from "./extractor";
|
import { parseInitialData, buildPlaylistData } from "./extractor";
|
||||||
|
import type { PlaylistVideo } from "../types/playlist";
|
||||||
import type { Message } from "../shared/messages";
|
import type { Message } from "../shared/messages";
|
||||||
|
|
||||||
const LOG_PREFIX = "[yt-playlist-features]";
|
const LOG = "[yt-playlist-features]";
|
||||||
|
|
||||||
// Track the last extracted playlist ID to avoid duplicate extractions
|
|
||||||
let lastExtractedId: string | null = null;
|
let lastExtractedId: string | null = null;
|
||||||
|
|
||||||
function injectPageScript(): void {
|
function injectPageScript(): void {
|
||||||
|
|
@ -19,29 +19,66 @@ function handlePlaylistData(event: Event): void {
|
||||||
const detail = (event as CustomEvent).detail;
|
const detail = (event as CustomEvent).detail;
|
||||||
if (!detail) return;
|
if (!detail) return;
|
||||||
|
|
||||||
let raw: any;
|
let payload: any;
|
||||||
try {
|
try {
|
||||||
raw = JSON.parse(detail);
|
payload = JSON.parse(detail);
|
||||||
} catch {
|
} catch {
|
||||||
console.error(LOG_PREFIX, "Failed to parse ytInitialData JSON");
|
console.error(LOG, "Failed to parse payload JSON");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playlistData = parsePlaylistData(raw);
|
if (payload.error) {
|
||||||
if (!playlistData) {
|
console.warn(LOG, "Page script error:", payload.error);
|
||||||
console.warn(LOG_PREFIX, "Could not extract playlist data from ytInitialData");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { videos: domVideos, initialData } = payload;
|
||||||
|
|
||||||
|
// Extract metadata from ytInitialData
|
||||||
|
const parsed = initialData ? parseInitialData(initialData) : null;
|
||||||
|
const metadata = parsed?.metadata;
|
||||||
|
if (!metadata) {
|
||||||
|
console.warn(LOG, "Could not extract playlist metadata");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert DOM-extracted videos to PlaylistVideo[]
|
||||||
|
const videos: PlaylistVideo[] = domVideos.map((v: any) => {
|
||||||
|
const bylineRun = v.shortBylineText?.runs?.[0];
|
||||||
|
const bylineEndpoint = bylineRun?.navigationEndpoint?.browseEndpoint;
|
||||||
|
|
||||||
|
return {
|
||||||
|
videoId: v.videoId,
|
||||||
|
title: v.title,
|
||||||
|
index: v.index,
|
||||||
|
durationSeconds: v.lengthSeconds,
|
||||||
|
durationText: v.lengthText || null,
|
||||||
|
thumbnails: v.thumbnails,
|
||||||
|
channel: {
|
||||||
|
name: bylineRun?.text ?? "",
|
||||||
|
channelId: bylineEndpoint?.browseId ?? "",
|
||||||
|
url: bylineRun?.navigationEndpoint?.commandMetadata
|
||||||
|
?.webCommandMetadata?.url ?? "",
|
||||||
|
},
|
||||||
|
isPlayable: v.isPlayable,
|
||||||
|
isLive:
|
||||||
|
v.badges?.some(
|
||||||
|
(b: any) =>
|
||||||
|
b.metadataBadgeRenderer?.style === "BADGE_STYLE_TYPE_LIVE_NOW",
|
||||||
|
) ?? false,
|
||||||
|
} satisfies PlaylistVideo;
|
||||||
|
});
|
||||||
|
|
||||||
|
const playlistData = buildPlaylistData(metadata, videos, true);
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
LOG_PREFIX,
|
LOG,
|
||||||
`Extracted playlist: "${playlistData.metadata.title}" (${playlistData.extractedCount} videos)`,
|
`Extracted playlist: "${playlistData.metadata.title}" (${playlistData.extractedCount}/${metadata.videoCount} videos)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Send to background service worker for storage
|
|
||||||
const message: Message = { type: "PLAYLIST_EXTRACTED", data: playlistData };
|
const message: Message = { type: "PLAYLIST_EXTRACTED", data: playlistData };
|
||||||
browser.runtime.sendMessage(message).catch((err) => {
|
browser.runtime.sendMessage(message).catch((err) => {
|
||||||
console.error(LOG_PREFIX, "Failed to send playlist data to background:", err);
|
console.error(LOG, "Failed to send playlist data to background:", err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,8 +91,7 @@ onPlaylistPageReady(() => {
|
||||||
if (!playlistId || playlistId === lastExtractedId) return;
|
if (!playlistId || playlistId === lastExtractedId) return;
|
||||||
lastExtractedId = playlistId;
|
lastExtractedId = playlistId;
|
||||||
|
|
||||||
// Small delay to ensure ytInitialData is updated after SPA navigation
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
injectPageScript();
|
injectPageScript();
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
@ -1,11 +1,282 @@
|
||||||
// Runs in the page's JS context (not isolated world).
|
// Runs in page context.
|
||||||
// Reads ytInitialData and sends it to the content script via CustomEvent.
|
// Fetches all playlist videos using YouTube's browse API from page context
|
||||||
|
// (includes cookies/auth via same-origin credentials).
|
||||||
|
// Replicates YouTube's internal request format including auth headers.
|
||||||
|
|
||||||
const data = (window as any).ytInitialData;
|
(async () => {
|
||||||
if (data) {
|
const LOG = "[yt-playlist-features]";
|
||||||
document.dispatchEvent(
|
const w = window as any;
|
||||||
new CustomEvent("__yt_playlist_ext_data", {
|
|
||||||
detail: JSON.stringify(data),
|
const initialData = w.ytInitialData;
|
||||||
}),
|
if (!initialData) {
|
||||||
);
|
sendResult({ error: "no-ytInitialData" });
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfg = w.ytcfg?.data_ ?? {};
|
||||||
|
const apiKey = cfg.INNERTUBE_API_KEY ?? "";
|
||||||
|
const baseContext = cfg.INNERTUBE_CONTEXT ?? null;
|
||||||
|
|
||||||
|
const videoList = findVideoListContents(initialData);
|
||||||
|
if (!videoList) {
|
||||||
|
sendResult({ error: "no-video-list" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allRenderers: any[] = [];
|
||||||
|
let continuation: string | null = null;
|
||||||
|
let continuationClickTracking: string | null = null;
|
||||||
|
|
||||||
|
for (const item of videoList) {
|
||||||
|
if (item.playlistVideoRenderer) {
|
||||||
|
allRenderers.push(item.playlistVideoRenderer);
|
||||||
|
}
|
||||||
|
if (item.continuationItemRenderer) {
|
||||||
|
const extracted = extractContinuationInfo(item.continuationItemRenderer);
|
||||||
|
continuation = extracted.token;
|
||||||
|
continuationClickTracking = extracted.clickTracking;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build auth headers matching YouTube's internal requests
|
||||||
|
const authHeaders = buildAuthHeaders();
|
||||||
|
|
||||||
|
// Fetch remaining pages
|
||||||
|
let page = 1;
|
||||||
|
while (continuation && page < 50) {
|
||||||
|
try {
|
||||||
|
const body = buildRequestBody(baseContext, continuation, continuationClickTracking);
|
||||||
|
const res = await fetch(
|
||||||
|
`https://www.youtube.com/youtubei/v1/browse?prettyPrint=false`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...authHeaders,
|
||||||
|
},
|
||||||
|
credentials: "same-origin",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!res.ok) break;
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
continuation = null;
|
||||||
|
continuationClickTracking = null;
|
||||||
|
|
||||||
|
for (const action of data.onResponseReceivedActions ?? []) {
|
||||||
|
const items = action?.appendContinuationItemsAction?.continuationItems;
|
||||||
|
if (!items) continue;
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.playlistVideoRenderer) {
|
||||||
|
allRenderers.push(item.playlistVideoRenderer);
|
||||||
|
}
|
||||||
|
if (item.continuationItemRenderer) {
|
||||||
|
const extracted = extractContinuationInfo(item.continuationItemRenderer);
|
||||||
|
continuation = extracted.token;
|
||||||
|
continuationClickTracking = extracted.clickTracking;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
page++;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(LOG, `Continuation page ${page} failed:`, e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const videos = allRenderers.map((d, i) => ({
|
||||||
|
videoId: d.videoId,
|
||||||
|
title: textOf(d.title),
|
||||||
|
index: i,
|
||||||
|
lengthSeconds: d.lengthSeconds ? parseInt(d.lengthSeconds, 10) : null,
|
||||||
|
lengthText: textOf(d.lengthText),
|
||||||
|
thumbnails: d.thumbnail?.thumbnails ?? [],
|
||||||
|
shortBylineText: d.shortBylineText,
|
||||||
|
isPlayable: d.isPlayable !== false,
|
||||||
|
badges: d.badges ?? [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log(LOG, `Extraction: ${videos.length} videos (${page - 1} continuation pages)`);
|
||||||
|
|
||||||
|
sendResult({
|
||||||
|
videos,
|
||||||
|
initialData,
|
||||||
|
innertubeApiKey: apiKey,
|
||||||
|
innertubeContext: baseContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- helpers ---
|
||||||
|
|
||||||
|
function buildAuthHeaders(): Record<string, string> {
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
|
||||||
|
// SAPISIDHASH authorization
|
||||||
|
const sapisid = getCookie("SAPISID") || getCookie("__Secure-3PAPISID");
|
||||||
|
if (sapisid) {
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000);
|
||||||
|
const origin = window.location.origin;
|
||||||
|
// SHA-1 hash of "timestamp SAPISID origin"
|
||||||
|
headers["Authorization"] = `SAPISIDHASH ${timestamp}_${sha1(`${timestamp} ${sapisid} ${origin}`)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth user
|
||||||
|
const authUser = cfg.SESSION_INDEX ?? "0";
|
||||||
|
headers["X-Goog-AuthUser"] = String(authUser);
|
||||||
|
|
||||||
|
// Visitor ID
|
||||||
|
const visitorData = baseContext?.client?.visitorData;
|
||||||
|
if (visitorData) {
|
||||||
|
headers["X-Goog-Visitor-Id"] = visitorData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCookie(name: string): string | null {
|
||||||
|
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
|
||||||
|
return match ? decodeURIComponent(match[1]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sha1(str: string): string {
|
||||||
|
const utf8 = new TextEncoder().encode(str);
|
||||||
|
const len = utf8.length;
|
||||||
|
|
||||||
|
// Pre-processing
|
||||||
|
const bitLen = len * 8;
|
||||||
|
const padLen = (((len + 8) >> 6) + 1) << 6;
|
||||||
|
const padded = new Uint8Array(padLen);
|
||||||
|
padded.set(utf8);
|
||||||
|
padded[len] = 0x80;
|
||||||
|
// Length in bits as big-endian 64-bit
|
||||||
|
const view = new DataView(padded.buffer);
|
||||||
|
view.setUint32(padLen - 4, bitLen, false);
|
||||||
|
|
||||||
|
let h0 = 0x67452301;
|
||||||
|
let h1 = 0xefcdab89;
|
||||||
|
let h2 = 0x98badcfe;
|
||||||
|
let h3 = 0x10325476;
|
||||||
|
let h4 = 0xc3d2e1f0;
|
||||||
|
|
||||||
|
const w = new Uint32Array(80);
|
||||||
|
|
||||||
|
for (let offset = 0; offset < padLen; offset += 64) {
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
w[i] = view.getUint32(offset + i * 4, false);
|
||||||
|
}
|
||||||
|
for (let i = 16; i < 80; i++) {
|
||||||
|
const x = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16];
|
||||||
|
w[i] = (x << 1) | (x >>> 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
let a = h0, b = h1, c = h2, d = h3, e = h4;
|
||||||
|
|
||||||
|
for (let i = 0; i < 80; i++) {
|
||||||
|
let f: number, k: number;
|
||||||
|
if (i < 20) {
|
||||||
|
f = (b & c) | (~b & d);
|
||||||
|
k = 0x5a827999;
|
||||||
|
} else if (i < 40) {
|
||||||
|
f = b ^ c ^ d;
|
||||||
|
k = 0x6ed9eba1;
|
||||||
|
} else if (i < 60) {
|
||||||
|
f = (b & c) | (b & d) | (c & d);
|
||||||
|
k = 0x8f1bbcdc;
|
||||||
|
} else {
|
||||||
|
f = b ^ c ^ d;
|
||||||
|
k = 0xca62c1d6;
|
||||||
|
}
|
||||||
|
const temp = (((a << 5) | (a >>> 27)) + f + e + k + w[i]) | 0;
|
||||||
|
e = d;
|
||||||
|
d = c;
|
||||||
|
c = (b << 30) | (b >>> 2);
|
||||||
|
b = a;
|
||||||
|
a = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
h0 = (h0 + a) | 0;
|
||||||
|
h1 = (h1 + b) | 0;
|
||||||
|
h2 = (h2 + c) | 0;
|
||||||
|
h3 = (h3 + d) | 0;
|
||||||
|
h4 = (h4 + e) | 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [h0, h1, h2, h3, h4]
|
||||||
|
.map((v) => (v >>> 0).toString(16).padStart(8, "0"))
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRequestBody(
|
||||||
|
ctx: any,
|
||||||
|
token: string,
|
||||||
|
clickTracking: string | null,
|
||||||
|
): any {
|
||||||
|
const enrichedContext = JSON.parse(JSON.stringify(ctx));
|
||||||
|
|
||||||
|
// Use continuation endpoint's clickTrackingParams
|
||||||
|
if (clickTracking) {
|
||||||
|
enrichedContext.clickTracking = { clickTrackingParams: clickTracking };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { context: enrichedContext, continuation: token };
|
||||||
|
}
|
||||||
|
|
||||||
|
function findVideoListContents(raw: any): any[] | null {
|
||||||
|
const tabs = raw?.contents?.twoColumnBrowseResultsRenderer?.tabs;
|
||||||
|
if (!tabs?.length) return null;
|
||||||
|
const section =
|
||||||
|
tabs[0]?.tabRenderer?.content?.sectionListRenderer?.contents;
|
||||||
|
if (!section?.length) return null;
|
||||||
|
const items = section[0]?.itemSectionRenderer?.contents;
|
||||||
|
if (!items?.length) return null;
|
||||||
|
return items[0]?.playlistVideoListRenderer?.contents ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractContinuationInfo(cir: any): { token: string | null; clickTracking: string | null } {
|
||||||
|
const ep = cir?.continuationEndpoint;
|
||||||
|
if (!ep) return { token: null, clickTracking: null };
|
||||||
|
|
||||||
|
const clickTracking = ep.clickTrackingParams ?? null;
|
||||||
|
|
||||||
|
if (ep.continuationCommand?.token) {
|
||||||
|
return { token: ep.continuationCommand.token, clickTracking };
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmds = ep.commandExecutorCommand?.commands;
|
||||||
|
if (cmds) {
|
||||||
|
for (const cmd of cmds) {
|
||||||
|
if (cmd.continuationCommand?.token) {
|
||||||
|
return { token: cmd.continuationCommand.token, clickTracking };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { token: deepFindToken(ep), clickTracking };
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepFindToken(obj: any, depth = 0): string | null {
|
||||||
|
if (!obj || typeof obj !== "object" || depth > 6) return null;
|
||||||
|
if (typeof obj.token === "string" && obj.token.length > 10) return obj.token;
|
||||||
|
for (const k of Object.keys(obj)) {
|
||||||
|
const r = deepFindToken(obj[k], depth + 1);
|
||||||
|
if (r) return r;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function textOf(obj: any): string {
|
||||||
|
if (!obj) return "";
|
||||||
|
if (typeof obj === "string") return obj;
|
||||||
|
if (obj.simpleText) return obj.simpleText;
|
||||||
|
if (obj.runs) return obj.runs.map((r: any) => r.text).join("");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendResult(payload: any) {
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent("__yt_playlist_ext_data", {
|
||||||
|
detail: JSON.stringify(payload),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user