Improved comment parsing

This commit is contained in:
WardPearce
2026-02-12 18:50:46 +13:00
parent 072e3dce6f
commit 27fd30d927
7 changed files with 43 additions and 20 deletions
+1 -1
View File
@@ -252,7 +252,7 @@ export async function getFeed(
fetchOptions: RequestInit = {}
): Promise<Feed> {
if (isYTBackend()) {
return getFeedYTjs(maxResults, page);
return getFeedYTjs();
}
const path = buildPath('auth/feed');
@@ -128,9 +128,7 @@ export async function parseChannelRSS(channelId: string): Promise<void> {
}
}
// Ignored to match non ytjs version of getfeed.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function getFeedYTjs(maxResults: number, page: number): Promise<Feed> {
export async function getFeedYTjs(): Promise<Feed> {
const channelSubscriptions = await localDb.channelSubscriptions.toArray();
const toUpdatePromises: Promise<void>[] = [];
@@ -2,7 +2,7 @@
import { page } from '$app/stores';
import { getBestThumbnail, ImageCache } from '$lib/images';
import { videoLength } from '$lib/numbers';
import { generateChapterWebVTT, type PhasedDescription, type Timestamp } from '$lib/description';
import { generateChapterWebVTT, type parsedDescription, type Timestamp } from '$lib/description';
import { SystemBars, SystemBarsStyle, SystemBarType } from '@capacitor/core';
import { Capacitor } from '@capacitor/core';
import { ScreenOrientation, type ScreenOrientationResult } from '@capacitor/screen-orientation';
@@ -67,7 +67,7 @@
} from '$lib/timelineThumbnails';
interface Props {
data: { video: VideoPlay; content: PhasedDescription; playlistId: string | null };
data: { video: VideoPlay; content: parsedDescription; playlistId: string | null };
currentTime?: number;
userManualSeeking?: boolean;
isEmbed?: boolean;
@@ -9,6 +9,7 @@
import CommentSelf from './Comment.svelte';
import { insecureRequestImageHandler, truncate } from '$lib/misc';
import { _ } from '$lib/i18n';
import { extractActualLink } from '$lib/description';
interface Props {
comment: Comment;
@@ -36,15 +37,39 @@
}
}
}
function parseComment(html: string): string {
console.log(html);
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
function commentTimestamps(html: string): string {
const regex =
/<a href="([^"]+)" data-onclick="jump_to_time" data-jump-time="(\d+)">([^<]+)<\/a>\s*(.+)/g;
const replacement = `<a href=${resolve(`/watch/[videoId]?time=$2`, { videoId: videoId })} data-sveltekit-preload-data="off" class="link">$3 $4</a>`;
const links = doc.querySelectorAll('a');
const processedHtml = html.replace(regex, replacement);
links.forEach((link) => {
const href = link.getAttribute('href');
if (!href) return;
return processedHtml;
const realHref = extractActualLink(href);
const dataOnClick = link.getAttribute('data-onclick');
link.classList.add('link');
if (dataOnClick === 'jump_to_time' || (realHref && realHref.includes('t='))) {
const match = realHref?.match(/t=(\d+)/);
const timestamp = match ? match[1] : '';
const newHref = resolve(`/watch/[videoId]?time=${timestamp}`, { videoId });
link.setAttribute('href', newHref);
link.removeAttribute('data-onclick');
link.setAttribute('data-sveltekit-preload-data', 'off');
} else {
link.setAttribute('href', realHref);
link.setAttribute('target', '_blank');
link.setAttribute('referrerpolicy', 'no-referrer');
}
});
return doc.documentElement.outerHTML;
}
let userPfp = $state('');
@@ -77,7 +102,7 @@
</a>
<p class="no-margin">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html commentTimestamps(comment.contentHtml)}
{@html parseComment(comment.contentHtml)}
<!-- Comment comes directly from YT so is already sanitized -->
</p>
<div class="comment-actions">
+3 -3
View File
@@ -5,7 +5,7 @@ import { convertToSeconds, padTime } from './time';
export type Timestamp = { title: string; time: number; timePretty: string; endTime: number };
export type Timestamps = Timestamp[];
export interface PhasedDescription {
export interface parsedDescription {
description: string;
timestamps: Timestamps;
}
@@ -41,11 +41,11 @@ function cleanTimestampTitle(title: string): string {
return title.replace(/^[\s\-•|:/\\*#>~]+/, '').trim();
}
export function phaseDescription(
export function parseDescription(
videoId: string,
content: string,
fallbackPatch?: 'youtubejs' | 'piped'
): PhasedDescription {
): parsedDescription {
const timestamps: Timestamps = [];
const lines = content.split('\n');
const filteredLines: string[] = [];
+2 -2
View File
@@ -21,7 +21,7 @@ import type {
VideoBase,
VideoPlay
} from './api/model';
import type { PhasedDescription } from './description';
import type { parsedDescription } from './description';
import { ensureNoTrailingSlash } from './misc';
import type { EngineFallback } from './api/misc';
@@ -182,7 +182,7 @@ export const playerAndroidPauseOnNetworkChange = persist(
export const playerPlaylistHistory: Writable<string[]> = writable([]);
export interface PlayerState {
data: { video: VideoPlay; content: PhasedDescription; playlistId: string | null };
data: { video: VideoPlay; content: parsedDescription; playlistId: string | null };
playerElement?: HTMLMediaElement | undefined;
}
+2 -2
View File
@@ -13,7 +13,7 @@ import {
returnYTDislikesInstanceStore,
returnYtDislikesStore
} from '$lib/store';
import { phaseDescription } from '$lib/description';
import { parseDescription } from '$lib/description';
import { error } from '@sveltejs/kit';
import { get } from 'svelte/store';
import { _ } from './i18n';
@@ -71,7 +71,7 @@ export async function getWatchDetails(videoId: string, url: URL) {
return {
video: video,
content: phaseDescription(video.videoId, video.descriptionHtml, video.fallbackPatch),
content: parseDescription(video.videoId, video.descriptionHtml, video.fallbackPatch),
playlistId: playlistId,
streamed: {
personalPlaylists: personalPlaylists,