Implemented get comment replies

This commit is contained in:
WardPearce
2026-02-11 22:09:26 +13:00
parent c3a1aa56c5
commit cbc16ecc7f
11 changed files with 164 additions and 35 deletions
+29 -18
View File
@@ -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<string, string> } {
}
export async function getPopular(fetchOptions?: RequestInit): Promise<Video[]> {
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<ResolvedUrl> {
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<VideoPlay> {
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<Comments> {
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<SearchSuggestion> {
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<SearchResults> {
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));
+7 -1
View File
@@ -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';
}
}
+6
View File
@@ -168,6 +168,7 @@ export interface Comment {
replyCount: number;
continuation: string;
};
getReplies?: () => Promise<Comments>;
}
export interface Comments {
@@ -307,3 +308,8 @@ export interface SynciousSaveProgressModel {
export type SearchResults = (Channel | Video | Playlist | HashTag)[] & {
getContinuation?: () => Promise<SearchResults>;
};
export type CommentsOptions = {
sort_by?: 'top' | 'new';
continuation?: string;
};
@@ -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<Comments> {
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<Comments> => {
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')
};
}
@@ -0,0 +1,14 @@
import { getInnertube } from '.';
import type { ResolvedUrl } from '../model';
export async function getResolveUrlYTjs(url: string): Promise<ResolvedUrl> {
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] : ''
};
}
@@ -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<SearchResults> {
const innertube = await getInnertube();
searchSetDefaults(options);
const innerResults = await innertube.search(search, {
sort_by: options.sort_by,
duration: options.duration,
@@ -0,0 +1,13 @@
import { getInnertube } from '.';
import type { SearchSuggestion } from '../model';
export async function getSearchSuggestionsYTjs(search: string): Promise<SearchSuggestion> {
const innertube = await getInnertube();
const searchSuggestions = await innertube.getSearchSuggestions(search);
return {
query: search,
suggestions: searchSuggestions
};
}
@@ -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') } });
}
@@ -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
}
}
}
+5
View File
@@ -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<T>(
return null;
}
export function isYTBackend(): boolean {
return get(backendInUseStore) === 'yt' && Capacitor.isNativePlatform();
}
+1 -3
View File
@@ -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;
}