Skip to content
Snippets Groups Projects
YoutubeScript.js 465 KiB
Newer Older
Kelvin's avatar
Kelvin committed
					if(url.indexOf("&cpn=") < 0)   
							url = url + "&cpn=" + nonce;   
					   
					const duration = parseInt(parseInt(y.approxDurationMs) / 1000);   
					if(isNaN(duration))   
							return null;   

					if(!y.initRange?.end || !y.indexRange?.end)   
							return null;   

					return new YTAudioSource({   
							name: prefix + (y.audioTrack?.displayName ? y.audioTrack.displayName : codecs),   
							container: container,   
							bitrate: y.bitrate,   
							url: url,   
							duration: (!isNaN(duration)) ? duration : 0,   
							container: y.mimeType.substring(0, y.mimeType.indexOf(';')),   
							codec: codecs,   
							language: ytLangIdToLanguage(y.audioTrack?.id),   

							itagId: y.itag,   
							initStart: parseInt(y.initRange?.start),   
							initEnd: parseInt(y.initRange?.end),   
							indexStart: parseInt(y.indexRange?.start),   
							indexEnd: parseInt(y.indexRange?.end),   
							audioChannels: y.audioChannels   
Kelvin's avatar
Kelvin committed
					}, contextData.url);   
Kelvin's avatar
Kelvin committed
			}).filter(x=>x!=null),   
	) : new VideoSourceDescriptor([])   
}

Koen's avatar
Koen committed
function toSRTTime(sec, withDot) {
	let hours = 0;
	let minutes = 0;
	let seconds = sec;
	let remainder = 0;

	remainder = parseInt((seconds % 1) * 100);
	minutes = parseInt(seconds / 60);
	seconds = parseInt(seconds % 60);
	hours = parseInt(minutes / 60);
	minutes = minutes % 60;

	return ("" + hours).padStart(2, '0') + ":" +
		("" + minutes).padStart(2, '0') + ":" +
		("" + seconds).padStart(2, '0') + ((withDot) ? "." : ",") +
		("" + remainder).padEnd(3, '0');
}

function extractVideoOwnerRenderer_AuthorLink(renderer) {
	const id = renderer?.navigationEndpoint?.browseEndpoint?.browseId;
    const url = (!id) ? extractRuns_Url(renderer.title.runs) : URL_BASE + "/channel/" + id;

    const hasMembership = !!(renderer?.membershipButton?.buttonRenderer)
    let membershipUrl = (hasMembership) ? url + "/join" : null;

Koen's avatar
Koen committed
	let bestThumbnail = null;
	if(renderer.thumbnail?.thumbnails)
		bestThumbnail = renderer.thumbnail.thumbnails[renderer.thumbnail.thumbnails.length - 1].url;

	let subscribers = null;
	if(renderer.subscriberCountText)
		subscribers = extractHumanNumber_Integer(extractText_String(renderer.subscriberCountText));
Koen's avatar
Koen committed
	return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id, PLATFORM_CLAIMTYPE), 
Koen's avatar
Koen committed
		extractRuns_String(renderer.title.runs),
Koen's avatar
Koen committed
		bestThumbnail,
		subscribers, membershipUrl);
Koen's avatar
Koen committed
}
function extractTwoColumnWatchNextResultContents_CommentsPager(contextUrl, contents, useLogin, engagementPanels) {
Koen's avatar
Koen committed
	//Add additional/better details

	let totalComments = 0;
	let commentsToken = null;
	for(let i = 0; i < contents.length; i++) {
		const content = contents[i];
		switchKey(content, {
			videoPrimaryInfoRenderer(renderer) { },
			videoSecondaryInfoRenderer(renderer) { },
			itemSectionRenderer(itemSectionRenderer) {
				const contents = itemSectionRenderer.contents;
				const content = contents && contents.length > 0 ? contents[0] : null;
				if(content)
					switchKey(content, {
						commentsEntryPointHeaderRenderer(renderer) {
							const commentCount = extractText_String(renderer.commentCount);
							if(commentCount) {
								totalComments = parseInt(commentCount);
Koen's avatar
Koen committed
							}
						},
						continuationItemRenderer(continueRenderer) {
							if(itemSectionRenderer.targetId == 'comments-section' && continueRenderer?.continuationEndpoint?.continuationCommand) {
Koen's avatar
Koen committed
								commentsToken = continueRenderer.continuationEndpoint.continuationCommand.token;
							}
						}
					});
			}
		});
	}
	const commentSectionPanel = engagementPanels?.find(x=>x?.engagementPanelSectionListRenderer?.panelIdentifier == "engagement-panel-comments-section");
	const altContinuation = commentSectionPanel?.engagementPanelSectionListRenderer?.content?.sectionListRenderer?.contents
		?.find(y=>true)?.itemSectionRenderer;
	if(altContinuation != null && !commentsToken && altContinuation.sectionIdentifier == "comment-item-section") {
		const continuationRenderer = altContinuation?.contents?.find(y=>true)?.continuationItemRenderer;
		const altToken = continuationRenderer?.continuationEndpoint?.continuationCommand?.token;
		if(altToken)
			commentsToken = altToken;
	}

Koen's avatar
Koen committed
	if(!commentsToken)
		return new CommentPager([], false);
	return requestCommentPager(contextUrl, commentsToken, useLogin, useLogin) ??  new CommentPager([], false);
Koen's avatar
Koen committed
}
function requestCommentPager(contextUrl, continuationToken, useLogin, useMobile) {
Koen's avatar
Koen committed
	const data = requestNext({
		continuation: continuationToken
Koen's avatar
Koen committed
	if(IS_TESTING)
	    console.log("data", data);
	const endpoints = data?.onResponseReceivedCommands ?? data?.onResponseReceivedActions ?? data?.onResponseReceivedEndpoints;
Kelvin's avatar
Kelvin committed
	if(!endpoints) {
	    log("Comment object:\n" + JSON.stringify(data, null, "   "));
	    if(bridge.devSubmit) bridge.devSubmit("requestCommentPager - No comment endpoints", JSON.stringify(data));
Kelvin's avatar
Kelvin committed
	    throw new ScriptException("No comment endpoints provided by Youtube");
	}
Kelvin's avatar
Kelvin committed
	let commentsContinuation = null;
Koen's avatar
Koen committed
	for(let i = 0; i < endpoints.length; i++) {
		const endpoint = endpoints[i];
		const continuationItems = endpoint.reloadContinuationItemsCommand?.continuationItems ??
			endpoint.appendContinuationItemsAction?.continuationItems;
		if(continuationItems && continuationItems.length > 0) {
			let comments = [];

			if(continuationItems && continuationItems.length > 0) {
				for(let continuationItem of continuationItems) {
					switchKey(continuationItem, {
						commentThreadRenderer(renderer) {
							const commentRenderer = renderer?.comment?.commentRenderer;
							if(!commentRenderer)
								return;

							const replyCount = (commentRenderer.replyCount ? commentRenderer?.replyCount : 0);
Kelvin's avatar
Kelvin committed
							let replyContinuation = renderer.replies?.commentRepliesRenderer?.contents?.length == 1 ?
								(renderer.replies.commentRepliesRenderer.contents[0]?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token
									?? renderer.replies.commentRepliesRenderer.contents[0]?.continuationItemRenderer?.button?.buttonRenderer?.command?.continuationCommand?.token) :
Koen's avatar
Koen committed
								null;
							comments.push(extractCommentRenderer_Comment(contextUrl, commentRenderer, replyCount, replyContinuation, useLogin, useMobile));
Koen's avatar
Koen committed
						},
						commentRenderer(renderer) {
							comments.push(extractCommentRenderer_Comment(contextUrl, renderer, 0, null, useLogin, useMobile));
Koen's avatar
Koen committed
						},
						continuationItemRenderer(renderer) {
							if(renderer?.continuationEndpoint?.continuationCommand?.token)
								commentsContinuation = renderer?.continuationEndpoint?.continuationCommand?.token;
							else if(renderer?.button?.buttonRenderer?.command?.continuationCommand?.token)
							    commentsContinuation = renderer.button.buttonRenderer.command.continuationCommand.token
						}
					});
				}
				if(comments.length > 0) {
					return new YTCommentPager(comments, commentsContinuation, contextUrl, useLogin, useMobile);

	if(data?.frameworkUpdates?.entityBatchUpdate?.mutations) {
		log("New comments model");
		const mutations = data.frameworkUpdates.entityBatchUpdate.mutations;
		if(mutations.length > 0) {
			const comments = [];
			
			let parentItems = [];
			for(let i = 0; i < endpoints.length; i++)
				parentItems.push(...(endpoints[i].reloadContinuationItemsCommand?.continuationItems ??
					endpoints[i].appendContinuationItemsAction?.continuationItems ?? 
					[]));
			parentItems = parentItems.filter(x=>x.commentThreadRenderer);
			const commentObjects = mutations.filter(x=>x?.payload?.commentEntityPayload);

			for(let commentObject of commentObjects) {
				const cobj = commentObject?.payload?.commentEntityPayload ?? {};
				const parent = parentItems.find(x=>x.commentThreadRenderer?.commentViewModel?.commentViewModel?.commentKey == commentObject.entityKey);
				const replyContents = parent?.commentThreadRenderer?.replies?.commentRepliesRenderer?.contents;
Kelvin's avatar
Kelvin committed
				const replyContinuation = ((replyContents?.length ?? 0) > 0) ? 
					(replyContents[0].continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token ??
						replyContents[0].continuationItemRenderer?.button?.buttonRenderer?.command?.continuationCommand?.token) : 
					null;
				
				const authorEndpoint = cobj.author?.channelCommand?.innertubeCommand?.commandMetadata?.webCommandMetadata?.url;
				comments.push(new YTComment({
					contextUrl: contextUrl,
					author: new PlatformAuthorLink(new PlatformID(PLATFORM, cobj?.author?.displayName, config.id, PLATFORM_CLAIMTYPE), cobj.author.displayName, (authorEndpoint) ? URL_BASE + authorEndpoint : "", cobj.author.avatarThumbnailUrl),
					message: cobj.properties?.content?.content ?? "",
					rating: new RatingLikes(extractHumanNumber_Integer(cobj.toolbar?.likeCountLiked) ?? 0),
					date: (extractAgoTextRuns_Timestamp(cobj?.properties?.publishedTime) ?? 0),
					replyCount: extractFirstNumber_Integer(cobj?.toolbar?.replyCount) ?? 0,
					context: { replyContinuation: replyContinuation, useLogin: useLogin + "", useMobile: useMobile + "" }
				return new YTCommentPager(comments, commentsContinuation, contextUrl, useLogin, useMobile);
Kelvin's avatar
Kelvin committed
	log("Comment object:\n" + JSON.stringify(data, null, "   "));
    if(bridge.devSubmit) bridge.devSubmit("requestCommentPager - No comment endpoints", JSON.stringify(data));
Kelvin's avatar
Kelvin committed
	throw new ScriptException("No valid comment endpoint provided by Youtube");
Koen's avatar
Koen committed
}

function extractSingleColumnBrowseResultsRenderer_Tabs(renderer, contextData) {
	const tabs = [];
	if(!renderer.tabs) {
	    if(bridge.devSubmit) bridge.devSubmit("extractSingleColumnBrowseResultsRenderer_Tabs - No tabs found", JSON.stringify(renderer));
	    throw new ScriptException("No tabs found");
	}
Koen's avatar
Koen committed

	for(let i = 0; i < renderer.tabs.length; i++) {
		const tab = renderer.tabs[i];

		if(!tab.tabRenderer)
			continue;

		const tabRenderer = tab.tabRenderer; //TODO: Check if this is ever null?
		const isDefault = tabRenderer.selected;
		const title = tabRenderer.title;
		const content = tabRenderer.content;
		
		if(!content)
			continue; //.endpoint

		const tabContentRendererName = Object.keys(content)[0];
		if(!tabContentRendererName) continue;
		let tabResult = undefined;
		switchKey(content, {
			richGridRenderer(renderer) {
				tabResult = extractRichGridRenderer_Shelves(content[tabContentRendererName], contextData);
			},
			sectionListRenderer(renderer) {
				if(!renderer.contents)
					return;
				tabResult = extractSectionListRenderer_Sections(renderer, contextData);
			},
			default() {
			    if(bridge.devSubmit) bridge.devSubmit("extractSingleColumnBrowseResultsRenderer_Tabs - Unknown tab renderer: " + tabContentRendererName, JSON.stringify(content));
Koen's avatar
Koen committed
				throw new ScriptException("Unknown tab renderer: " + tabContentRendererName);
			}
		});
		if(tabResult) {
			tabResult.isDefault = !!isDefault;
			tabResult.title = title;
			tabs.push(tabResult);
		}
	}

	return tabs;
}
function extractTwoColumnBrowseResultsRenderer_Tabs(renderer, contextData) {
	const tabs = [];
	if(!renderer.tabs)
	{
	    if(bridge.devSubmit) bridge.devSubmit("extractTwoColumnBrowseResultsRenderer_Tabs - No tabs found", JSON.stringify(renderer));
	    throw new ScriptException("No tabs found");
	}
Koen's avatar
Koen committed

	for(let i = 0; i < renderer.tabs.length; i++) {
		const tab = renderer.tabs[i];

		if(!tab.tabRenderer && !tab.expandableTabRenderer)
			continue;

		const tabRenderer = tab.tabRenderer ?? tab.expandableTabRenderer;
		const isDefault = tabRenderer.selected;
		const title = tabRenderer.title;
		const content = tabRenderer.content;
		
		if(!content)
			continue; //.endpoint

		const tabContentRendererName = Object.keys(content)[0];
		if(!tabContentRendererName) continue;
		let tabResult = undefined;
		switchKey(content, {
			richGridRenderer(renderer) {
				tabResult = extractRichGridRenderer_Shelves(renderer, contextData);
			},
			sectionListRenderer(renderer) {
				//Channel sectioned tabs..
				tabResult = extractSectionListRenderer_Sections(renderer, contextData)
			},
			default() {
	            if(bridge.devSubmit) bridge.devSubmit("extractTwoColumnBrowseResultsRenderer_Tabs - Unknown tab renderer: " + tabContentRendererName, JSON.stringify(renderer));
Koen's avatar
Koen committed
				throw new ScriptException("Unknown tab renderer: " + tabContentRendererName);
			}
		});
		if(tabResult) {
			tabResult.isDefault = !!isDefault;
			tabResult.title = title;
			tabs.push(tabResult);
		}
	}

	return tabs;
}
function extractRichGridRenderer_Shelves(richGridRenderer, contextData) {
	const contents = richGridRenderer.contents;

Kelvin's avatar
Kelvin committed
	let shelves = [];
	let videos = [];
Koen's avatar
Koen committed

	let continuation = null;

	for(let ci = 0; ci < contents.length; ci++) {
		const content = contents[ci];		
		switchKey(content, {
			richSectionRenderer(renderer) {
				shelves.push(extractRichSectionRenderer_Shelf(renderer, contextData));
			},
			richItemRenderer(renderer) {
				videos.push(extractRichItemRenderer_Video(renderer, contextData));
			},
			continuationItemRenderer(renderer) {
				continuation = extractContinuationItemRenderer_Continuation(renderer, contextData);
			},
Kelvin's avatar
Kelvin committed
			itemSectionRenderer(renderer) {
Kelvin's avatar
Kelvin committed
		        const items = extractItemSectionRenderer_Shelves(renderer, contextData);
Kelvin's avatar
Kelvin committed

		        if(items.shelves)
		            shelves = shelves.concat(items.shelves);
		        if(items.videos)
		            videos = videos.concat(items.videos);
			},
Koen's avatar
Koen committed
			default(name) {
				log("Unknown shelf/section renderer in extractRichGridRenderer_Shelves: " + name);
			}
		});
	}
	
	return {
		shelves: shelves.filter(x=>x != null),
		videos: videos.filter(x=>x != null),
		continuation: continuation
	};
}
function extractSectionListRenderer_Sections(sectionListRenderer, contextData) {
	const contents = sectionListRenderer.contents;

	let shelves = [];
	let videos = [];
	let channels = [];
	let playlists = [];
	let continuation = null;

	let subContinuations = [];
Koen's avatar
Koen committed
	for(let i = 0; i < contents.length; i++) {
		const item = contents[i];
		switchKey(item, {
			itemSectionRenderer(renderer) {
Kelvin's avatar
Kelvin committed
				const items = extractItemSectionRenderer_Shelves(renderer, contextData);
Koen's avatar
Koen committed
				if(items.videos.length > 0)
					videos.push(...items.videos);
				if(items.channels.length > 0)
					channels.push(...items.channels);
				if(items.playlists.length > 0)
					playlists.push(...items.playlists);
				if(items.shelves)
					shelves.push(...items.shelves);
				if(items.continuation)
					subContinuations.push(items.continuation);
Koen's avatar
Koen committed
			},
			continuationItemRenderer(renderer) {
				continuation = extractContinuationItemRenderer_Continuation(renderer, contextData);
			}
		});
	}
	return {
		shelves: shelves,
		videos: videos,
		channels: channels,
		playlists: playlists,
		continuation: continuation,
		subContinuations: subContinuations
Koen's avatar
Koen committed
	};
}
function extractItemSectionRenderer_Shelves(itemSectionRenderer, contextData) {
Kelvin's avatar
Kelvin committed
	const contents = itemSectionRenderer.contents ?? itemSectionRenderer.results;
Koen's avatar
Koen committed
	let shelves = [];
	let videos = [];
	let channels = [];
	let playlists = [];
Kelvin's avatar
Kelvin committed
	let continuationToken = undefined;
Koen's avatar
Koen committed

	contents.forEach((item)=>{
		switchKey(item, {
			channelFeaturedContentRenderer(renderer) {
				if(renderer.items) {
					let videos = switchKeyVideos(renderer.items, contextData);
					if(videos && videos.length > 0) {
						shelves.push({
							name: "Featured",
							type: "Shelf",
							videos: videos
						});
					}
				}
			},
			channelRenderer(renderer) {
			    const channel = extractChannelRenderer_AuthorLink(renderer);
			    if(channel)
			        channels.push(channel);
			},
			playlistRenderer(renderer) {
			    const playlist = extractPlaylistRenderer_Playlist(renderer);
			    if(playlist)
			        playlists.push(playlist);
			},
			lockupViewModel(renderer) {
				const playlist = extractPlaylistLockupViewModel_Playlist(renderer);
				if(playlist)
					playlists.push(playlist);
			},
Kelvin's avatar
Kelvin committed
			shelfRenderer(renderer) {
			    const shelf = extractShelfRenderer_Shelf(renderer);
			    if(shelf)
				    shelves.push(shelf);
			},
Kelvin's avatar
Kelvin committed
			gridRenderer(renderer) {
				const shelf = extractGridRenderer_Shelf(renderer, contextData);
				if(shelf.playlists.length > 0)
					playlists.push(...shelf.playlists);
				if(shelf.continuation)
					continuationToken = shelf.continuation;
Kelvin's avatar
Kelvin committed
			},
Kelvin's avatar
Kelvin committed
			continuationItemRenderer(renderer) {
				const token = renderer?.continuationEndpoint?.continuationCommand?.token
				if(token)
					continuationToken = token;
			},
Koen's avatar
Koen committed
			default() {
				const video = switchKeyVideo(item, contextData);
				if(video)
					videos.push(video);
			}
		});
		
	});

	return {
		shelves: shelves.filter(x=>x != null),
		videos: videos.filter(x=>x != null),
		channels: channels.filter(x=>x != null),
Kelvin's avatar
Kelvin committed
		playlists: playlists.filter(x=>x != null),
		continuation: continuationToken
Koen's avatar
Koen committed
	};
}
Kelvin's avatar
Kelvin committed
function extractGridRenderer_Shelf(gridRenderer, contextData) {
	const contents = gridRenderer.items;
	let shelves = [];
	let videos = [];
	let channels = [];
	let playlists = [];

	let continuation = undefined;
Kelvin's avatar
Kelvin committed
	contents.forEach((item)=>{
		switchKey(item, {
			gridPlaylistRenderer(renderer) {
			    const playlist = extractPlaylistRenderer_Playlist(renderer, contextData);
			    if(playlist)
			        playlists.push(playlist);
			},
			lockupViewModel(renderer) {
				const playlist = extractPlaylistLockupViewModel_Playlist(renderer, contextData);
				if(playlist)
					playlists.push(playlist);
			},
			continuationItemRenderer(renderer) {
				if(renderer?.continuationEndpoint?.continuationCommand?.token)
					continuation = renderer.continuationEndpoint.continuationCommand.token;
			},
Kelvin's avatar
Kelvin committed
			default() {
				const video = switchKeyVideo(item, contextData);
				if(video)
					videos.push(video);
			}
		});
		
	});

	return {
		videos: videos.filter(x=>x != null),
		channels: channels.filter(x=>x != null),
		playlists: playlists.filter(x=>x != null),
		continuation: continuation
Koen's avatar
Koen committed
function switchKeyVideos(contents, contextData) {
	let videos = [];
	for(let content of contents) {
		const video = switchKeyVideo(content, contextData);
		if(video)
			videos.push(video);
	}
	return videos;
}
function switchKeyVideo(content, contextData) {
	return switchKey(content, {
		channelFeaturedContentRenderer(renderer) {
			return extractVideoRenderer_Video(renderer, contextData);
		},
		videoRenderer(renderer) {
			return extractVideoRenderer_Video(renderer, contextData);
		},
		compactVideoRenderer(renderer) {
			return extractVideoWithContextRenderer_Video(renderer, contextData);
		},
		videoWithContextRenderer(renderer) {
			return extractVideoWithContextRenderer_Video(renderer, contextData);
		},
		reelItemRenderer(renderer) {
			return extractReelItemRenderer_Video(renderer, contextData);
		},
		adSlotRenderer(adSlot) {
			return null;
		},
		default(name) {
			return null;
		}
	});
}
//#endregion

//#region Element Extractors
function extractShelfRenderer_Shelf(shelfRenderer, contextData) {
Kelvin's avatar
Kelvin committed
    let name = extractText_String(shelfRenderer.title);
    return switchKey(shelfRenderer.content, {
        expandedShelfContentsRenderer(renderer) {
            return {
                name: name,
                type: "Shelf",
                videos: switchKeyVideos(renderer.items)
            };
        },
        default() {
            return null;
        }
    });
Koen's avatar
Koen committed
}
function extractContinuationItemRenderer_Continuation(continuationItemRenderer) {
	return  {
		url: continuationItemRenderer.continuationEndpoint.commandMetadata.apiUrl, //TODO: See if this is useful at all
		token: continuationItemRenderer.continuationEndpoint.continuationCommand.token
	};
}
function extractRichSectionRenderer_Shelf(sectionRenderer, contextData) {
	const content = sectionRenderer.content;

	return switchKey(content, {
		richShelfRenderer(renderer) {
			return extractRichShelfRenderer_Shelf(renderer, contextData);
		},
		default(name) {
			log("Unknown shelf renderer in extractRichSectionRenderer_Shelf: " + name);
			return null;
			//throw new ScriptException("Unknown shelf renderer: " + name);
		}
	});
}
function extractRichShelfRenderer_Shelf(shelfRenderer, contextData) {
	const shelf = {
		name: extractRuns_String(shelfRenderer.title?.runs),
		type: "Shelf",
		videos: []
	};

	for(let itemi = 0; itemi < shelfRenderer.contents.length; itemi++) {
		const item = shelfRenderer.contents[itemi];
		switchKey(item, {
			richItemRenderer(renderer) {
				shelf.videos.push(extractRichItemRenderer_Video(renderer), contextData);
			},
			default(name) {
				log("Unknown shelf renderer in extractRichShelfRenderer_Shelf: " + name);
				//throw new ScriptException("Unknown item renderer: " + name);
			}
		});
	}

	shelf.videos = shelf.videos.filter(x=>x != null);
	return shelf;
}
//#endregion

//#region Item Extractor
function extractRichItemRenderer_Video(itemRenderer, contextData) {
	const content = itemRenderer.content;
	return switchKeyVideo(content, contextData); 
}
function extractVideoWithContextRenderer_Video(videoRenderer, contextData) {

	const liveBadges = videoRenderer.thumbnailOverlays?.filter(x=>
Kelvin's avatar
Kelvin committed
		x.thumbnailOverlayTimeStatusRenderer?.style == "LIVE" ||
Koen's avatar
Koen committed
		x.thumbnailOverlayTimeStatusRenderer?.accessibility?.accessibilityData?.label == "LIVE");
	let isLive = liveBadges != null && liveBadges.length > 0;

Kelvin's avatar
Kelvin committed
	isLive = isLive || ((videoRenderer.badges?.filter(x=>x.metadataBadgeRenderer?.style == "BADGE_STYLE_TYPE_LIVE_NOW")?.length ?? 0) > 0)
Kelvin's avatar
Kelvin committed

Koen's avatar
Koen committed
	let plannedDate = null;
	if(videoRenderer.upcomingEventData?.startTime)
		plannedDate = parseInt(videoRenderer.upcomingEventData.startTime);
	isLive = isLive || !!plannedDate
Koen's avatar
Koen committed
	//if(!isLive && !videoRenderer.publishedTimeText?.simpleText)
	//	return  null; //Not a normal video


	const author = (contextData && contextData.authorLink) ?
		contextData.authorLink : extractVideoWithContextRenderer_AuthorLink(videoRenderer);

	if(IS_TESTING)
Kelvin's avatar
Kelvin committed
		;//console.log(videoRenderer);
Koen's avatar
Koen committed

Kelvin's avatar
Kelvin committed
	//if(!videoRenderer?.lengthText?.runs || !videoRenderer.publishedTimeText?.runs)
	//	isLive = true; //If no length, live after all?
Koen's avatar
Koen committed

    let viewCount = 0;
Kelvin's avatar
Kelvin committed
    if(videoRenderer?.shortViewCountText)
        viewCount = extractHumanNumber_Integer(extractText_String(videoRenderer.shortViewCountText));
Koen's avatar
Koen committed
    else log("No viewcount found on video " + videoRenderer.videoId);

	const title = (videoRenderer.headline) ? extractText_String(videoRenderer.headline) : extractText_String(videoRenderer.title);

Koen's avatar
Koen committed
	if (isLive) {
Koen's avatar
Koen committed
		return new PlatformVideo({
			id: new PlatformID(PLATFORM, videoRenderer.videoId, config.id),
			name: escapeUnicode(title),
Koen's avatar
Koen committed
			thumbnails: extractThumbnail_Thumbnails(videoRenderer.thumbnail),
			author: author,
Koen's avatar
Koen committed
			uploadDate: plannedDate ?? parseInt(new Date().getTime() / 1000),
Koen's avatar
Koen committed
			duration: 0,
			viewCount: viewCount,
			url: URL_BASE + "/watch?v=" + videoRenderer.videoId,
			isLive: true,
			extractType: "VideoWithContext"
		});
Koen's avatar
Koen committed
	} else {
Koen's avatar
Koen committed
		return new PlatformVideo({
			id: new PlatformID(PLATFORM, videoRenderer.videoId, config.id),
			name: escapeUnicode(title),
Koen's avatar
Koen committed
			thumbnails: extractThumbnail_Thumbnails(videoRenderer.thumbnail),
			author: author,
			uploadDate: parseInt(extractAgoText_Timestamp(extractText_String(videoRenderer.publishedTimeText))),
			duration: extractHumanTime_Seconds(extractText_String(videoRenderer.lengthText)),
			viewCount: viewCount,
			url: URL_BASE + "/watch?v=" + videoRenderer.videoId,
			isLive: false,
			extractType: "VideoWithContext"
		});
Koen's avatar
Koen committed
	}
Koen's avatar
Koen committed
}
function extractVideoRenderer_Video(videoRenderer, contextData) {

	const liveBadges = videoRenderer.badges?.filter(x=>x.metadataBadgeRenderer?.label == "LIVE");
	const liveOverlays = videoRenderer.thumbnailOverlays?.filter(x=>
		x.thumbnailOverlayTimeStatusRenderer?.style == "LIVE" ||
		x.thumbnailOverlayTimeStatusRenderer?.accessibility?.accessibilityData?.label == "LIVE");
	let isLive = (liveBadges != null && liveBadges.length > 0) ||
		(liveOverlays != null && liveOverlays.length > 0);

	let plannedDate = null;
	if(videoRenderer.upcomingEventData?.startTime)
		plannedDate = parseInt(videoRenderer.upcomingEventData.startTime);
	if(plannedDate)
		isLive = true;
Kelvin's avatar
Kelvin committed
	
Kelvin's avatar
Kelvin committed
	isLive = isLive || ((videoRenderer.badges?.filter(x=>x.metadataBadgeRenderer?.style == "BADGE_STYLE_TYPE_LIVE_NOW")?.length ?? 0) > 0)
Koen's avatar
Koen committed

	if(!isLive && !videoRenderer.publishedTimeText?.simpleText)
		return  null; //Not a normal video



	const author = (contextData && contextData.authorLink) ?
		contextData.authorLink : extractVideoRenderer_AuthorLink(videoRenderer);

	if(IS_TESTING)
		console.log(videoRenderer);

Koen J's avatar
Koen J committed
//	if(!videoRenderer?.lengthText?.simpleText)
//		isLive = true; //If no length, live after all?
Koen's avatar
Koen committed

	if(isLive)
		return new PlatformVideo({
			id: new PlatformID(PLATFORM, videoRenderer.videoId, config.id),
			name: escapeUnicode(extractRuns_String(videoRenderer.title.runs)),
Koen's avatar
Koen committed
			thumbnails: extractThumbnail_Thumbnails(videoRenderer.thumbnail),
			author: author,
			uploadDate: plannedDate ?? parseInt(new Date().getTime()/1000),
			duration: 0,
			viewCount: !videoRenderer.viewCountText ? 0 : extractRuns_ViewerCount(videoRenderer.viewCountText.runs),
			url: URL_BASE + "/watch?v=" + videoRenderer.videoId,
			isLive: true,
			extractType: "Video"
		});
	else
		return new PlatformVideo({
			id: new PlatformID(PLATFORM, videoRenderer.videoId, config.id),
			name: escapeUnicode(extractRuns_String(videoRenderer.title.runs)),
Koen's avatar
Koen committed
			thumbnails: extractThumbnail_Thumbnails(videoRenderer.thumbnail),
			author: author,
			uploadDate: parseInt(extractAgoText_Timestamp(videoRenderer.publishedTimeText.simpleText)),
			duration: extractHumanTime_Seconds(videoRenderer.lengthText.simpleText),
			viewCount: extractFirstNumber_Integer(extractText_String(videoRenderer.viewCountText)),
			url: URL_BASE + "/watch?v=" + videoRenderer.videoId,
			isLive: false,
			extractType: "Video"
		});
}
function extractReelItemRenderer_Video(reelItemRenderer) {
	//We don't do shorts for now..
	return null;
}
function extractPlaylistVideoRenderer_Video(videoRenderer, contextData) {
    if(!videoRenderer.lengthText)
        return null;

	const author = (contextData && contextData.authorLink) ?
		contextData.authorLink : extractRuns_AuthorLink(videoRenderer.shortBylineText?.runs);

	if(IS_TESTING)
		console.log(videoRenderer);

    let date = 0;
    if(videoRenderer?.publishedTimeText?.simpleText)
        date = parseInt(extractAgoText_Timestamp(videoRenderer.publishedTimeText.simpleText));


	return new PlatformVideo({
		id: new PlatformID(PLATFORM, videoRenderer.videoId, config.id),
		name: escapeUnicode(extractRuns_String(videoRenderer.title.runs)),
Koen's avatar
Koen committed
		thumbnails: extractThumbnail_Thumbnails(videoRenderer.thumbnail),
		author: author,
		uploadDate: date,
		duration: extractHumanTime_Seconds(extractText_String(videoRenderer.lengthText)),
		viewCount: 0,//extractFirstNumber_Integer(videoRenderer.viewCountText.simpleText),
		url: URL_BASE + "/watch?v=" + videoRenderer.videoId,
		isLive: false,
		extractType: "Video"
	});
}
function extractPlaylistRenderer_Playlist(playlistRenderer, contextData) {
	const author = (contextData && contextData.authorLink) ?
		contextData.authorLink : extractRuns_AuthorLink(playlistRenderer.shortBylineText?.runs);

Kelvin's avatar
Kelvin committed
	let thumbnail = (playlistRenderer.thumbnails && playlistRenderer.thumbnails.length > 0) ? extractThumbnail_BestUrl(playlistRenderer.thumbnails[0]) : null;
	if(!thumbnail && playlistRenderer.thumbnail)
		thumbnail = extractThumbnail_BestUrl(playlistRenderer.thumbnail);

Koen's avatar
Koen committed
    return new PlatformPlaylist({
		id: new PlatformID(PLATFORM, playlistRenderer.playlistId, config.id),
		author: author,
        name: extractText_String(playlistRenderer.title),
Kelvin's avatar
Kelvin committed
        thumbnail: thumbnail,
Koen's avatar
Koen committed
        url: URL_PLAYLIST + playlistRenderer.playlistId,
        videoCount: extractFirstNumber_Integer(extractText_String(playlistRenderer.videoCountText)),
    });
}

const REGEX_VIDEO_COUNT = /[0-9]+ videos/;
function extractPlaylistLockupViewModel_Playlist(playlistRenderer, contextData) {
	const author = (contextData && contextData.authorLink) ?
		contextData.authorLink : null;
	
		const thumbnailViewModel = playlistRenderer?.contentImage?.collectionThumbnailViewModel?.primaryThumbnail?.thumbnailViewModel;
		let thumbnail = (thumbnailViewModel?.image?.sources?.length > 0) ?
			thumbnailViewModel.image.sources[0].url :
			null;
		thumbnail = thumbnail ?? "";

		let videoCount = -1;
		if(thumbnailViewModel?.overlays?.length > 0) {
			for(let overlay of thumbnailViewModel?.overlays) {
				if(overlay.thumbnailOverlayBadgeViewModel?.thumbnailBadges) {
					for(let subOverlay of overlay.thumbnailOverlayBadgeViewModel.thumbnailBadges) {
						if(subOverlay.thumbnailBadgeViewModel?.text) {
							if(REGEX_VIDEO_COUNT.test(subOverlay.thumbnailBadgeViewModel.text)) {
								videoCount = extractFirstNumber_Integer(subOverlay.thumbnailBadgeViewModel.text);
								break;
							}
						}
					}
				}
				if(videoCount >= 0)
					break;
			}
		}

		let id = playlistRenderer?.rendererContext?.commandContext?.onTap?.innertubeCommand?.watchEndpoint?.playlistId;

		if(IS_TESTING) {
			console.log("New playlist model: ", [
				thumbnail, author, id, videoCount
			]);
		}
		return new PlatformPlaylist({
			id: new PlatformID(PLATFORM, playlistRenderer.playlistId, config.id),
			author: author,
			name: extractText_String(playlistRenderer.metadata?.lockupMetadataViewModel?.title),
			thumbnail: thumbnail,
			url: URL_PLAYLIST + id,
			videoCount: videoCount,
		});
}


Koen's avatar
Koen committed
function extractChannelRenderer_AuthorLink(channelRenderer) {
    const id = channelRenderer.channelId;
    const name = extractText_String(channelRenderer.title);
    const channelUrl = extractNavigationEndpoint_Url(channelRenderer.navigationEndpoint);
    let thumbUrl = extractThumbnail_BestUrl(channelRenderer.thumbnail);
    if(thumbUrl.startsWith("//"))
        thumbUrl = "https:" + thumbUrl;

	const subscribers = extractHumanNumber_Integer(extractText_String(channelRenderer.videoCountText));

Kelvin's avatar
Kelvin committed
	return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id, PLATFORM_CLAIMTYPE), name, channelUrl, thumbUrl, subscribers);
Koen's avatar
Koen committed
}

function extractRuns_AuthorLink(runs) {
    if(!runs || runs.length == 0)
        return null;

    const id = runs[0]?.navigationEndpoint?.browseEndpoint?.browseId;
    const name = extractRuns_String(runs);
    const channelUrl = extractNavigationEndpoint_Url(runs[0]?.navigationEndpoint);
    const thumbUrl = null;

Kelvin's avatar
Kelvin committed
	return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id, PLATFORM_CLAIMTYPE), name, channelUrl, thumbUrl ?? "");
Koen's avatar
Koen committed
}

function extractThumbnail_Thumbnails(thumbnail) {
	return new Thumbnails(thumbnail.thumbnails.map(x=>new Thumbnail(escapeUnicode(x.url), x.height)));
Koen's avatar
Koen committed
}
function extractThumbnail_BestUrl(thumbnail) {
    if(!thumbnail?.thumbnails || thumbnail.thumbnails.length <= 0)
        return null;
    let bestUrl = thumbnail.thumbnails[0].url;
    let bestHeight = thumbnail.thumbnails[0].height;
    for(let thumb of thumbnail.thumbnails)
        if(thumb.height > bestHeight) {
            bestUrl = thumb.url;
            bestHeight = thumb.height;
        }
    return bestUrl;
}
function extractVideoWithContextRenderer_AuthorLink(videoRenderer) {
	let id = videoRenderer.channelThumbnail?.channelThumbnailWithLinkRenderer?.navigationEndpoint?.browseEndpoint?.browseId;
	const name = extractRuns_String(videoRenderer.shortBylineText.runs);
	const channelThumbs = videoRenderer.channelThumbnail.channelThumbnailWithLinkRenderer?.thumbnail?.thumbnails;
	const thumbUrl = channelThumbs && channelThumbs.length > 0 ? channelThumbs[0].url : null;
	let channelUrl = videoRenderer.channelThumbnail?.channelThumbnailWithLinkRenderer?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl;
	if(channelUrl) channelUrl = URL_BASE + channelUrl;
Koen's avatar
Koen committed
	if (id) channelUrl = URL_BASE + "/channel/" + id;
Kelvin's avatar
Kelvin committed
	return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id, PLATFORM_CLAIMTYPE), name, channelUrl, thumbUrl);
Koen's avatar
Koen committed
}
function extractVideoRenderer_AuthorLink(videoRenderer) {
	const id = videoRenderer.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer?.navigationEndpoint?.browseEndpoint?.browseId;
	const name = extractText_String(videoRenderer.ownerText)//extractRuns_String(videoRenderer.ownerText.runs);
Koen's avatar
Koen committed
	const channelIcon = videoRenderer.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer;
	const thumbUrl = channelIcon.thumbnail.thumbnails[0].url;
Koen's avatar
Koen committed
	const channelUrl = (!id) ? extractRuns_Url(videoRenderer.ownerText.runs) : URL_BASE + "/channel/" + id;
Koen's avatar
Koen committed

Kelvin's avatar
Kelvin committed
	return new PlatformAuthorLink(new PlatformID(PLATFORM, id, config.id, PLATFORM_CLAIMTYPE), name, channelUrl, thumbUrl);
Koen's avatar
Koen committed
}
function extractCommentRenderer_Comment(contextUrl, commentRenderer, replyCount, replyContinuation, useLogin, useMobile) {
	const authorName = extractText_String(commentRenderer.authorText) ?? "";
Koen's avatar
Koen committed
	const authorEndpoint = commentRenderer.authorEndpoint?.commandMetadata?.webCommandMetadata?.url ?? "";
	const authorThumbnail = (commentRenderer.authorThumbnail?.thumbnails ? 
		commentRenderer.authorThumbnail.thumbnails[commentRenderer.authorThumbnail.thumbnails.length - 1].url :
		""	
	);
	return new YTComment({
		contextUrl: contextUrl,
Kelvin's avatar
Kelvin committed
		author: new PlatformAuthorLink(new PlatformID(PLATFORM, null, config.id, PLATFORM_CLAIMTYPE), authorName, URL_BASE + authorEndpoint, authorThumbnail),
Koen's avatar
Koen committed
		message: extractRuns_String(commentRenderer.contentText?.runs) ?? "",
		rating: new RatingLikes(commentRenderer?.voteCount?.simpleText ? extractHumanNumber_Integer(commentRenderer.voteCount.simpleText) : 0),
		date: (commentRenderer.publishedTimeText?.runs ? extractAgoTextRuns_Timestamp(commentRenderer.publishedTimeText.runs) : 0),
		replyCount: replyCount ?? 0,
		context: { replyContinuation: replyContinuation, useLogin: useLogin + "", useMobile: useMobile + "" }
Koen's avatar
Koen committed
	})
}
//#endregion

Kelvin's avatar
Kelvin committed
function convertIfOtherUrl(url) {
    url = convertIfShortUrl(url);
    url = convertIfEmbedUrl(url);
    url = convertIfMusicUrl(url);
    return url;
}
function convertIfMusicUrl(url) {
    const musicMatch = url.match(REGEX_VIDEO_URL_DESKTOP);
    if(musicMatch && musicMatch.length == 3 && musicMatch[1]?.toLowerCase() == "music")
        url = URL_BASE + "/watch?v=" + musicMatch[1];
Kelvin's avatar
Kelvin committed
    return url;
}
function convertIfEmbedUrl(url) {
    const embedMatch = url.match(REGEX_VIDEO_URL_EMBED);
    if(embedMatch && embedMatch.length == 3) {
        let id = embedMatch[2];
        if(id.indexOf("?") > 0)
            id = id.substring(0, id.indexOf("?"));
        url = URL_BASE + "/watch?v=" + id;
    }
    return url;
}
Koen's avatar
Koen committed
function convertIfShortUrl(url) {
    const shortMatch = url.match(REGEX_VIDEO_URL_SHORT);
    if(shortMatch && shortMatch.length == 3) {
        let id = shortMatch[2];
        if(id.indexOf("?") > 0)
            id = id.substring(0, id.indexOf("?"));
        url = URL_BASE + "/watch?v=" + id;
    }
    return url;
}


//#region Basic Extractors
function extractText_String(item) {
    if(typeof item === 'string')
        return item;
    if(item?.simpleText)
        return item.simpleText;
    if(item?.runs)
        return extractRuns_String(item.runs);
	if(item?.content && typeof item.content == 'string')
		return item.content;
Koen's avatar
Koen committed

	if(item)
		log("Unknown string object: " + JSON.stringify(item, null, "   "));
    return null;
}
function extractRuns_String(runs) {
	if(!runs)
		return null;

	let str = "";
	for(let runi = 0; runi < runs.length; runi++) {
		const run = runs[runi];
		if(run.text)
			str += run.text;
		else if(run.emoji?.image?.accessibility?.accessibilityData?.label)
			str += "__" + run.emoji?.image?.accessibility?.accessibilityData?.label + "__"
	}
	return str;
}
function extractRuns_Html(runs) {
	if(!runs)
		return null;

	let str = "";
	for(let runi = 0; runi < runs.length; runi++) {
		const run = runs[runi];
		if(run.text)
			str += run.text;
	}
	return str;
}
function extractRuns_Url(runs) {
	for(let runi = 0; runi < runs.length; runi++) {
		const run = runs[runi];

		if(run.navigationEndpoint && run.navigationEndpoint.browseEndpoint && run.navigationEndpoint.browseEndpoint.canonicalBaseUrl)
			return URL_BASE + run.navigationEndpoint.browseEndpoint.canonicalBaseUrl;
	}
}

function extractNavigationEndpoint_Url(navEndpoint, baseUrl) {
    if(!baseUrl)
        baseUrl = URL_BASE;
    if(!navEndpoint)
Koen's avatar
Koen committed
		return null;
	if(navEndpoint?.browseEndpoint?.browseId && navEndpoint?.browseEndpoint?.canonicalBaseUrl && navEndpoint.browseEndpoint.canonicalBaseUrl.startsWith("/@"))
		return baseUrl + "/channel/" + navEndpoint?.browseEndpoint?.browseId;
Koen's avatar
Koen committed
    if(navEndpoint?.browseEndpoint?.canonicalBaseUrl)
        return baseUrl + navEndpoint?.browseEndpoint?.canonicalBaseUrl;
    if(navEndpoint.commandMetadata?.webCommandMetadata?.url)
        return baseUrl + navEndpoint.commandMetadata?.webCommandMetadata?.url;
    return null;
}

function extractAgoTextRuns_Timestamp(runs) {
	const runStr = (typeof runs === "string") ? runs : extractRuns_String(runs);
Koen's avatar
Koen committed
	return extractAgoText_Timestamp(runStr);
}
function extractAgoText_Timestamp(str) {
Kelvin's avatar
Kelvin committed
	if(!str)