Started inital search patch with ytjs

This commit is contained in:
WardPearce
2026-02-11 20:23:22 +13:00
parent fb217ecfdb
commit 65d8344ae8
11 changed files with 157 additions and 56 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "Materialious",
"version": "1.13.19",
"version": "1.14.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "Materialious",
"version": "1.13.19",
"version": "1.14.0",
"license": "MIT",
"dependencies": {
"@capacitor-community/electron": "^5.0.0",
+11 -27
View File
@@ -1,4 +1,4 @@
import { getVideoTYjs } from '$lib/api/youtubejs';
import { getVideoYTjs } from '$lib/api/youtubejs/video';
import { Capacitor } from '@capacitor/core';
import { get } from 'svelte/store';
import {
@@ -13,15 +13,12 @@ import {
synciousInstanceStore
} from '../store';
import type {
Channel,
ChannelContentPlaylists,
ChannelContentVideos,
ChannelPage,
Comments,
DeArrow,
Feed,
HashTag,
Playlist,
PlaylistPage,
ResolvedUrl,
ReturnYTDislikes,
@@ -29,8 +26,12 @@ import type {
Subscription,
ApiExntendedProgressModel,
Video,
VideoPlay
VideoPlay,
SearchOptions,
SearchResults
} from './model';
import { searchSetDefaults } from './misc';
import { getSearchYTjs } from './youtubejs/search';
export function buildPath(path: string): URL {
return new URL(`${get(instanceStore)}/api/v1/${path}`);
@@ -86,13 +87,13 @@ export async function getVideo(
fetchOptions?: RequestInit
): Promise<VideoPlay> {
if (get(playerYouTubeJsAlways) && Capacitor.isNativePlatform()) {
return await getVideoTYjs(videoId);
return await getVideoYTjs(videoId);
}
const resp = await fetch(setRegion(buildPath(`videos/${videoId}?local=${local}`)), fetchOptions);
if (!resp.ok && get(playerYouTubeJsFallback) && Capacitor.isNativePlatform()) {
return await getVideoTYjs(videoId);
return await getVideoYTjs(videoId);
} else {
await fetchErrorHandle(resp);
}
@@ -193,31 +194,14 @@ export async function getHashtag(tag: string, page: number = 0): Promise<{ resul
return await resp.json();
}
export interface SearchOptions {
sort_by?: 'relevance' | 'rating' | 'upload_date' | 'view_count';
type?: 'video' | 'playlist' | 'channel' | 'all';
duration?: 'short' | 'medium' | 'long';
date?: 'hour' | 'today' | 'week' | 'month' | 'year';
features?: string;
page?: string;
}
export async function getSearch(
search: string,
options: SearchOptions,
fetchOptions?: RequestInit
): Promise<(Channel | Video | Playlist | HashTag)[]> {
if (typeof options.sort_by === 'undefined') {
options.sort_by = 'relevance';
}
): Promise<SearchResults> {
searchSetDefaults(options);
if (typeof options.type === 'undefined') {
options.type = 'all';
}
if (typeof options.page === 'undefined') {
options.page = '1';
}
await getSearchYTjs(search, options);
const path = buildPath('search');
path.search = new URLSearchParams({ ...options, q: search }).toString();
+15
View File
@@ -0,0 +1,15 @@
import type { SearchOptions } from './model';
export function searchSetDefaults(options: SearchOptions) {
if (typeof options.sort_by === 'undefined') {
options.sort_by = 'relevance';
}
if (typeof options.type === 'undefined') {
options.type = 'all';
}
if (typeof options.page === 'undefined') {
options.page = '1';
}
}
+11 -1
View File
@@ -1,5 +1,14 @@
import type { ApiResponse, Innertube, YT } from 'youtubei.js';
export interface SearchOptions {
sort_by?: 'relevance' | 'rating' | 'upload_date' | 'view_count';
type?: 'video' | 'playlist' | 'channel' | 'all';
duration?: 'short' | 'medium' | 'long';
date?: 'hour' | 'today' | 'week' | 'month' | 'year';
features?: string;
page?: string;
}
export interface Image {
url: string;
width: number;
@@ -7,7 +16,6 @@ export interface Image {
}
export interface Thumbnail {
quality: string;
url: string;
width: number;
height: number;
@@ -295,3 +303,5 @@ export interface ApiExntendedProgressModel {
export interface SynciousSaveProgressModel {
time: number;
}
export type SearchResults = (Channel | Video | Playlist | HashTag)[];
@@ -0,0 +1,19 @@
import { interfaceRegionStore } from '$lib/store';
import { USER_AGENT } from 'bgutils-js';
import { get } from 'svelte/store';
import Innertube, { UniversalCache } from 'youtubei.js';
let innertube: Innertube | undefined;
export async function getInnertube(): Promise<Innertube> {
if (innertube) return innertube;
innertube = await Innertube.create({
fetch: fetch,
cache: new UniversalCache(true),
location: get(interfaceRegionStore),
user_agent: USER_AGENT
});
return innertube;
}
@@ -0,0 +1,68 @@
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 } from 'youtubei.js';
export async function getSearchYTjs(
search: string,
options: SearchOptions
): Promise<SearchResults> {
const innertube = await getInnertube();
searchSetDefaults(options);
const innerResults = await innertube.search(search, {
sort_by: options.sort_by,
duration: options.duration,
features: [options.features] as Types.Feature[],
upload_date: options.date
});
const searchResults: SearchResults = [];
innerResults.results.forEach((result) => {
if (result.is(YTNodes.Video)) {
const views = extractNumber(result.view_count?.toString() || '');
const patchedResult: Video = {
type: 'video',
title: result.title.toString(),
videoId: result.video_id,
viewCountText: cleanNumber(views),
viewCount: views,
videoThumbnails: result.thumbnails as Thumbnail[],
published: 0,
publishedText: result.published?.toString() || '',
description: '',
descriptionHtml: '',
authorUrl: `/channel/${result.author.id}`,
authorId: result.author.id,
authorVerified: false,
liveNow: false,
isUpcoming: false,
premium: false,
author: result.author.name,
lengthSeconds: result.length_text?.text ? convertToSeconds(result.length_text.text) : 0
};
searchResults.push(patchedResult);
} else if (result.is(YTNodes.Channel)) {
const patchedResult: Channel = {
type: 'channel',
authorId: result.id,
author: result.author.name,
authorUrl: `/channel/${result.id}`,
authorVerified: result.author.is_verified === true,
subCount: result.video_count.text ? extractNumber(result.video_count.text) : 0,
totalViews: 0,
autoGenerated: false,
description: result.description_snippet.text ?? '',
descriptionHml: result.description_snippet.toHTML() ?? '',
authorThumbnails: result.author.thumbnails as Thumbnail[]
};
searchResults.push(patchedResult);
}
});
return searchResults;
}
@@ -8,13 +8,14 @@ import type {
VideoBase,
VideoPlay
} from '$lib/api/model';
import { interfaceRegionStore, poTokenCacheStore } from '$lib/store';
import { poTokenCacheStore } from '$lib/store';
import { convertToSeconds } from '$lib/time';
import { Capacitor } from '@capacitor/core';
import { USER_AGENT } from 'bgutils-js';
import { get } from 'svelte/store';
import type { Types } from 'youtubei.js';
import { Innertube, UniversalCache, Utils, YT, YTNodes, Platform } from 'youtubei.js';
import { Utils, YT, YTNodes, Platform } from 'youtubei.js';
import { getInnertube } from '.';
import { cleanNumber, extractNumber } from '$lib/numbers';
Platform.shim.eval = async (
data: Types.BuildScriptResult,
@@ -35,17 +36,12 @@ Platform.shim.eval = async (
return new Function(code)();
};
export async function getVideoTYjs(videoId: string): Promise<VideoPlay> {
export async function getVideoYTjs(videoId: string): Promise<VideoPlay> {
if (!Capacitor.isNativePlatform()) {
throw new Error('Platform not supported');
}
const youtube = await Innertube.create({
fetch: fetch,
cache: new UniversalCache(false),
location: get(interfaceRegionStore),
user_agent: USER_AGENT
});
const innertube = await getInnertube();
const requestKey = 'O43z0dpjhgX20SCx4KAo';
@@ -57,7 +53,7 @@ export async function getVideoTYjs(videoId: string): Promise<VideoPlay> {
const clientPlaybackNonce = Utils.generateRandomString(16);
const watchEndpoint = new YTNodes.NavigationEndpoint({ watchEndpoint: { videoId } });
const rawPlayerResponse = await watchEndpoint.call(youtube.actions, {
const rawPlayerResponse = await watchEndpoint.call(innertube.actions, {
contentCheckOk: true,
racyCheckOk: true,
playbackContext: {
@@ -65,16 +61,16 @@ export async function getVideoTYjs(videoId: string): Promise<VideoPlay> {
pyv: true
},
contentPlaybackContext: {
signatureTimestamp: youtube.session.player?.signature_timestamp
signatureTimestamp: innertube.session.player?.signature_timestamp
}
}
});
const rawNextResponse = await watchEndpoint.call(youtube.actions, {
const rawNextResponse = await watchEndpoint.call(innertube.actions, {
override_endpoint: '/next'
});
const video = new YT.VideoInfo(
[rawPlayerResponse, rawNextResponse],
youtube.actions,
innertube.actions,
clientPlaybackNonce
);
@@ -82,7 +78,7 @@ export async function getVideoTYjs(videoId: string): Promise<VideoPlay> {
throw new Error('Unable to pull video info from youtube.js');
}
const challengeResponse = await youtube.getAttestationChallenge('ENGAGEMENT_TYPE_UNBOUND');
const challengeResponse = await innertube.getAttestationChallenge('ENGAGEMENT_TYPE_UNBOUND');
poTokenCacheStore.set(await platformMinter(requestKey, videoId, challengeResponse));
let dashUri: string | undefined;
@@ -139,7 +135,7 @@ export async function getVideoTYjs(videoId: string): Promise<VideoPlay> {
let authorThumbnails: Image[];
if (video.basic_info.channel_id) {
const channel = await youtube.getChannel(video.basic_info.channel_id);
const channel = await innertube.getChannel(video.basic_info.channel_id);
authorThumbnails = channel.metadata.avatar as Image[];
} else {
authorThumbnails = [];
@@ -151,7 +147,7 @@ export async function getVideoTYjs(videoId: string): Promise<VideoPlay> {
url.searchParams.set('potc', '1');
url.searchParams.set('pot', get(poTokenCacheStore) ?? '');
url.searchParams.set('c', youtube.session.context.client.clientName);
url.searchParams.set('c', innertube.session.context.client.clientName);
url.searchParams.set('fmt', 'vtt');
// Remove &xosf=1 as it adds `position:63% line:0%` to the subtitle lines
@@ -190,10 +186,13 @@ export async function getVideoTYjs(videoId: string): Promise<VideoPlay> {
videoThumbnails: (recommended?.content_image.image as Thumbnail[]) || [],
videoId: recommended.content_id,
title: recommended.metadata.title.toString(),
viewCountText:
(recommended.metadata.metadata.metadata_rows[1]?.metadata_parts?.[0]?.text ?? '')
.toString()
.replace('views', '') || '',
viewCountText: cleanNumber(
extractNumber(
(
recommended.metadata.metadata.metadata_rows[1]?.metadata_parts?.[0]?.text ?? ''
).toString()
)
),
author:
(
recommended.metadata.metadata.metadata_rows[0]?.metadata_parts?.[0]?.text ?? ''
@@ -271,7 +270,7 @@ export async function getVideoTYjs(videoId: string): Promise<VideoPlay> {
keywords: video.basic_info.keywords || [],
allowedRegions: [],
ytjs: {
innertube: youtube,
innertube: innertube,
video: video,
clientPlaybackNonce: clientPlaybackNonce,
rawApiResponse: rawPlayerResponse
@@ -44,7 +44,7 @@
synciousStore
} from '../store';
import { setStatusBarColor } from '../theme';
import { getVideoTYjs } from '$lib/api/youtubejs';
import { getVideoYTjs } from '$lib/api/youtubejs/video';
import {
goToNextVideo,
goToPreviousVideo,
@@ -624,7 +624,7 @@
async function reloadVideo() {
showVideoRetry = false;
data.video = await getVideoTYjs(data.video.videoId);
data.video = await getVideoYTjs(data.video.videoId);
await loadVideo();
}
+5
View File
@@ -1,5 +1,10 @@
import humanNumber from 'human-number';
export function extractNumber(input: string): number {
const digits = input.replace(/\D+/g, '');
return digits === '' ? NaN : Number(digits);
}
export function numberWithCommas(number: number) {
if (typeof number === 'undefined') return;
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
@@ -1,10 +1,11 @@
<script lang="ts">
import { getSearch, type SearchOptions } from '$lib/api';
import { getSearch } from '$lib/api';
import PageLoading from '$lib/components/PageLoading.svelte';
import { searchCacheStore } from '$lib/store';
import { _ } from '$lib/i18n';
import InfiniteLoading, { type InfiniteEvent } from 'svelte-infinite-loading';
import ItemsList from '$lib/components/ItemsList.svelte';
import type { SearchOptions } from '$lib/api/model.js';
let { data } = $props();
File diff suppressed because one or more lines are too long