diff --git a/materialious/src/lib/api/index.ts b/materialious/src/lib/api/index.ts index af91b420..6582c461 100644 --- a/materialious/src/lib/api/index.ts +++ b/materialious/src/lib/api/index.ts @@ -3,7 +3,6 @@ import { Capacitor } from '@capacitor/core'; import { get } from 'svelte/store'; import { authStore, - backendInUseStore, deArrowInstanceStore, deArrowThumbnailInstanceStore, instanceStore, @@ -29,10 +28,15 @@ import type { Video, VideoPlay, SearchOptions, - SearchResults + SearchResults, + CommentsOptions } from './model'; -import { searchSetDefaults } from './misc'; +import { commentsSetDefaults, searchSetDefaults } from './misc'; import { getSearchYTjs } from './youtubejs/search'; +import { isYTBackend } from '$lib/misc'; +import { getSearchSuggestionsYTjs } from './youtubejs/searchSuggestions'; +import { getResolveUrlYTjs } from './youtubejs/misc'; +import { getCommentsYTjs } from './youtubejs/comments'; export function buildPath(path: string): URL { return new URL(`${get(instanceStore)}/api/v1/${path}`); @@ -73,11 +77,20 @@ export function buildAuthHeaders(): { headers: Record } { } export async function getPopular(fetchOptions?: RequestInit): Promise { + if (isYTBackend()) { + // Home page will be subscription page when using Youtube backend + return []; + } + const resp = await fetchErrorHandle(await fetch(buildPath('popular'), fetchOptions)); return await resp.json(); } export async function getResolveUrl(url: string): Promise { + if (isYTBackend()) { + return await getResolveUrlYTjs(url); + } + const resp = await fetchErrorHandle(await fetch(`${buildPath('resolveurl')}?url=${url}`)); return await resp.json(); } @@ -87,7 +100,7 @@ export async function getVideo( local: boolean = false, fetchOptions?: RequestInit ): Promise { - if (get(playerYouTubeJsAlways) && Capacitor.isNativePlatform()) { + if ((get(playerYouTubeJsAlways) && Capacitor.isNativePlatform()) || isYTBackend()) { return await getVideoYTjs(videoId); } @@ -113,23 +126,17 @@ export async function getDislikes( export async function getComments( videoId: string, - parameters: { - sort_by?: 'top' | 'new'; - source?: 'youtube' | 'reddit'; - continuation?: string; - }, + options: CommentsOptions, fetchOptions?: RequestInit ): Promise { - if (typeof parameters.sort_by === 'undefined') { - parameters.sort_by = 'top'; - } + commentsSetDefaults(options); - if (typeof parameters.source === 'undefined') { - parameters.source = 'youtube'; + if (isYTBackend()) { + return await getCommentsYTjs(videoId, options); } const path = buildPath(`comments/${videoId}`); - path.search = new URLSearchParams(parameters).toString(); + path.search = new URLSearchParams(options).toString(); const resp = await fetchErrorHandle(await fetch(path, fetchOptions)); return await resp.json(); } @@ -184,6 +191,10 @@ export async function getSearchSuggestions( search: string, fetchOptions?: RequestInit ): Promise { + if (isYTBackend()) { + return getSearchSuggestionsYTjs(search); + } + const path = buildPath('search/suggestions'); path.search = new URLSearchParams({ q: search }).toString(); const resp = await fetchErrorHandle(await fetch(path, fetchOptions)); @@ -200,12 +211,12 @@ export async function getSearch( options: SearchOptions, fetchOptions?: RequestInit ): Promise { - if (get(backendInUseStore) === 'yt') { + searchSetDefaults(options); + + if (isYTBackend()) { return await getSearchYTjs(search, options); } - searchSetDefaults(options); - const path = buildPath('search'); path.search = new URLSearchParams({ ...options, q: search }).toString(); const resp = await fetchErrorHandle(await fetch(setRegion(path), fetchOptions)); diff --git a/materialious/src/lib/api/misc.ts b/materialious/src/lib/api/misc.ts index 180be092..f220a95a 100644 --- a/materialious/src/lib/api/misc.ts +++ b/materialious/src/lib/api/misc.ts @@ -1,4 +1,4 @@ -import type { SearchOptions } from './model'; +import type { CommentsOptions, SearchOptions } from './model'; export function searchSetDefaults(options: SearchOptions) { if (typeof options.sort_by === 'undefined') { @@ -13,3 +13,9 @@ export function searchSetDefaults(options: SearchOptions) { options.page = '1'; } } + +export function commentsSetDefaults(options: CommentsOptions) { + if (typeof options.sort_by === 'undefined') { + options.sort_by = 'top'; + } +} diff --git a/materialious/src/lib/api/model.ts b/materialious/src/lib/api/model.ts index 34ce0b87..906215f6 100644 --- a/materialious/src/lib/api/model.ts +++ b/materialious/src/lib/api/model.ts @@ -168,6 +168,7 @@ export interface Comment { replyCount: number; continuation: string; }; + getReplies?: () => Promise; } export interface Comments { @@ -307,3 +308,8 @@ export interface SynciousSaveProgressModel { export type SearchResults = (Channel | Video | Playlist | HashTag)[] & { getContinuation?: () => Promise; }; + +export type CommentsOptions = { + sort_by?: 'top' | 'new'; + continuation?: string; +}; diff --git a/materialious/src/lib/api/youtubejs/comments.ts b/materialious/src/lib/api/youtubejs/comments.ts new file mode 100644 index 00000000..895cac97 --- /dev/null +++ b/materialious/src/lib/api/youtubejs/comments.ts @@ -0,0 +1,76 @@ +import { extractNumber } from '$lib/numbers'; +import { YTNodes } from 'youtubei.js'; +import { getInnertube } from '.'; +import type { Comment, Comments, CommentsOptions, Thumbnail } from '../model'; + +function invidiousSchema(result: YTNodes.CommentView): Comment | undefined { + if (!result.author) return; + + return { + author: result.author.name ?? '', + authorId: result.author.id ?? '', + authorUrl: `/channel/${result.author.id}`, + authorThumbnails: result.author.thumbnails as Thumbnail[], + isEdited: false, + isPinned: result.is_pinned === true, + content: result.content?.text ?? '', + contentHtml: result.content?.toHTML() ?? '', + published: 0, // Placeholder, adjust accordingly + publishedText: result.published_time ?? '', + likeCount: extractNumber(result.like_count ?? '0'), + authorIsChannelOwner: result.author_is_channel_owner === true, + creatorHeart: { + creatorName: result.heart_active_tooltip?.split('@')[1] ?? '', + creatorThumbnail: result.creator_thumbnail_url ?? '' + }, + replies: { + replyCount: extractNumber(result.reply_count ?? '0'), + continuation: '' + } + }; +} + +export async function getCommentsYTjs( + videoId: string, + options: CommentsOptions +): Promise { + const innertube = await getInnertube(); + + const innerResults = await innertube.getComments( + videoId, + options.sort_by === 'top' ? 'TOP_COMMENTS' : 'NEWEST_FIRST' + ); + + const comments: Comment[] = []; + + innerResults.contents.forEach(async (result) => { + if (result.comment) { + const invidiousResult = invidiousSchema(result.comment); + if (invidiousResult) { + if (result.has_replies) + invidiousResult.getReplies = async (): Promise => { + const replies = await result.getReplies(); + if (!replies.replies) return { videoId: '', comments: [], commentCount: 0 }; + + const comments: Comment[] = replies.replies + .map((reply) => invidiousSchema(reply)) + .filter((comment): comment is Comment => !!comment); + + return { + videoId: '', + comments: comments, + commentCount: 0 + }; + }; + + comments.push(invidiousResult); + } + } + }); + + return { + videoId: videoId, + comments: comments, + commentCount: extractNumber(innerResults.header?.comments_count.text ?? '0') + }; +} diff --git a/materialious/src/lib/api/youtubejs/misc.ts b/materialious/src/lib/api/youtubejs/misc.ts new file mode 100644 index 00000000..5219be15 --- /dev/null +++ b/materialious/src/lib/api/youtubejs/misc.ts @@ -0,0 +1,14 @@ +import { getInnertube } from '.'; +import type { ResolvedUrl } from '../model'; + +export async function getResolveUrlYTjs(url: string): Promise { + const innertube = await getInnertube(); + + const resloved = await innertube.resolveURL(url); + + return { + pageType: resloved.metadata.page_type ?? '', + params: '', + ucid: resloved.metadata.url ? resloved.metadata.url?.split('/')[2] : '' + }; +} diff --git a/materialious/src/lib/api/youtubejs/search.ts b/materialious/src/lib/api/youtubejs/search.ts index 1f495f96..632528fa 100644 --- a/materialious/src/lib/api/youtubejs/search.ts +++ b/materialious/src/lib/api/youtubejs/search.ts @@ -1,7 +1,6 @@ import { cleanNumber, extractNumber } from '$lib/numbers'; import { convertToSeconds } from '$lib/time'; import { getInnertube } from '.'; -import { searchSetDefaults } from '../misc'; import type { Channel, SearchOptions, SearchResults, Thumbnail, Video } from '../model'; import { YTNodes, type Types, YT } from 'youtubei.js'; @@ -68,8 +67,6 @@ export async function getSearchYTjs( ): Promise { const innertube = await getInnertube(); - searchSetDefaults(options); - const innerResults = await innertube.search(search, { sort_by: options.sort_by, duration: options.duration, diff --git a/materialious/src/lib/api/youtubejs/searchSuggestions.ts b/materialious/src/lib/api/youtubejs/searchSuggestions.ts new file mode 100644 index 00000000..078fe57e --- /dev/null +++ b/materialious/src/lib/api/youtubejs/searchSuggestions.ts @@ -0,0 +1,13 @@ +import { getInnertube } from '.'; +import type { SearchSuggestion } from '../model'; + +export async function getSearchSuggestionsYTjs(search: string): Promise { + const innertube = await getInnertube(); + + const searchSuggestions = await innertube.getSearchSuggestions(search); + + return { + query: search, + suggestions: searchSuggestions + }; +} diff --git a/materialious/src/lib/components/Player.svelte b/materialious/src/lib/components/Player.svelte index 5fd68784..38ab2280 100644 --- a/materialious/src/lib/components/Player.svelte +++ b/materialious/src/lib/components/Player.svelte @@ -58,7 +58,7 @@ import { Network, type ConnectionStatus } from '@capacitor/network'; import { fade } from 'svelte/transition'; import { addToast } from './Toast.svelte'; - import { isMobile, truncate } from '$lib/misc'; + import { isMobile, isYTBackend, truncate } from '$lib/misc'; import { generateThumbnailWebVTT, drawTimelineThumbnail, @@ -485,7 +485,7 @@ sabrAdapter = await injectSabr(data.video, player); - if (data.video.fallbackPatch === 'youtubejs') { + if (data.video.fallbackPatch === 'youtubejs' && !isYTBackend()) { addToast({ data: { text: $_('player.youtubeJsFallBack') } }); } diff --git a/materialious/src/lib/components/watch/Comment.svelte b/materialious/src/lib/components/watch/Comment.svelte index 38ba9b07..8654d065 100644 --- a/materialious/src/lib/components/watch/Comment.svelte +++ b/materialious/src/lib/components/watch/Comment.svelte @@ -23,14 +23,17 @@ const replyText: string = comment.replies?.replyCount > 1 ? $_('replies') : $_('reply'); async function loadReplies(continuation: string) { - try { - replies = await getComments(videoId, { - continuation: continuation, - sort_by: 'top', - source: 'youtube' - }); - } catch { - // Continue regardless of error + if (comment.getReplies) { + replies = await comment.getReplies(); + } else { + try { + replies = await getComments(videoId, { + continuation: continuation, + sort_by: 'top' + }); + } catch { + // Continue regardless of error + } } } diff --git a/materialious/src/lib/misc.ts b/materialious/src/lib/misc.ts index da0c950f..9abe8b3a 100644 --- a/materialious/src/lib/misc.ts +++ b/materialious/src/lib/misc.ts @@ -4,6 +4,7 @@ import he from 'he'; import type Peer from 'peerjs'; import { get } from 'svelte/store'; import { + backendInUseStore, interfaceAllowInsecureRequests, interfaceAndroidUseNativeShare, isAndroidTvStore @@ -222,3 +223,7 @@ export function findElementForTime( return null; } + +export function isYTBackend(): boolean { + return get(backendInUseStore) === 'yt' && Capacitor.isNativePlatform(); +} diff --git a/materialious/src/lib/watch.ts b/materialious/src/lib/watch.ts index fa51916d..95a61285 100644 --- a/materialious/src/lib/watch.ts +++ b/materialious/src/lib/watch.ts @@ -47,9 +47,7 @@ export async function getWatchDetails(videoId: string, url: URL) { let comments; try { - comments = video.liveNow - ? null - : getComments(videoId, { sort_by: 'top', source: 'youtube' }, { priority: 'low' }); + comments = video.liveNow ? null : getComments(videoId, { sort_by: 'top' }, { priority: 'low' }); } catch { comments = null; }