From f1a64d5294fea8fab2f5054df09f18be8ce4ebbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=A0=E4=B8=8B=20=E5=85=8B=E5=BD=A6?= Date: Sun, 10 Aug 2025 21:39:12 +0900 Subject: [PATCH 01/11] simplify new Date().getTime() --- src/mixins/_request.ts | 2 +- src/mixins/browsing.ts | 2 +- src/mixins/utils.ts | 3 +-- tests/browsing/utils.test.ts | 5 +---- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/mixins/_request.ts b/src/mixins/_request.ts index c0786c0..766e225 100644 --- a/src/mixins/_request.ts +++ b/src/mixins/_request.ts @@ -51,7 +51,7 @@ export async function request(endpoint: string, options: FetchData) { ...CONSTANTS2.HEADERS, ...auth_headers, "Content-Type": "application/json", - "X-Goog-Request-Time": (new Date()).getTime().toString(), + "X-Goog-Request-Time": Date.now().toString(), ...options.headers, }, params: { diff --git a/src/mixins/browsing.ts b/src/mixins/browsing.ts index f40362b..42dabd3 100644 --- a/src/mixins/browsing.ts +++ b/src/mixins/browsing.ts @@ -388,7 +388,7 @@ export async function get_song( parse_format, ), expires: new Date( - new Date().getTime() + + Date.now() + (Number(response.streamingData.expiresInSeconds) * 1000), ), videoDetails: { diff --git a/src/mixins/utils.ts b/src/mixins/utils.ts index 7976608..1e62cc0 100644 --- a/src/mixins/utils.ts +++ b/src/mixins/utils.ts @@ -27,8 +27,7 @@ export function prepare_like_endpoint(status: LikeStatus) { export function get_timestamp() { const one_day = 24 * 60 * 60 * 1000; - return Math.round((new Date().getTime() - new Date(0).getTime()) / one_day) - - 7; + return Math.round(Date.now() / one_day) - 7; } export async function check_auth() { diff --git a/tests/browsing/utils.test.ts b/tests/browsing/utils.test.ts index 16af264..55da4d0 100644 --- a/tests/browsing/utils.test.ts +++ b/tests/browsing/utils.test.ts @@ -11,10 +11,7 @@ import * as muse from "../../src/mod.ts" it("get_timestamp", () => { const one_day = 24 * 60 * 60 * 1000; - const timestamp = Math.round( - (new Date().getTime() - new Date(0).getTime()) / - one_day, - ) - 7; + const timestamp = Math.round(Date.now() / one_day) - 7; assertEquals(muse.get_timestamp(), timestamp); }); From 92e8452cf276f21cfcdbef21a9f31f994d136100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=A0=E4=B8=8B=20=E5=85=8B=E5=BD=A6?= Date: Mon, 11 Aug 2025 21:53:58 +0900 Subject: [PATCH 02/11] Better typing of jsonpath functions --- scripts/locales/strings.ts | 45 ++++++++++++++------------------------ src/errors.ts | 2 +- src/util.ts | 39 ++++++++++++++++----------------- 3 files changed, 36 insertions(+), 50 deletions(-) diff --git a/scripts/locales/strings.ts b/scripts/locales/strings.ts index fdfd7d6..f04b9f3 100644 --- a/scripts/locales/strings.ts +++ b/scripts/locales/strings.ts @@ -1,11 +1,12 @@ // This module gets all translated strings for all languages import LOCALES from "../../locales/locales.json" with { type: "json" }; +import { MuseError } from "../../mod.ts"; import { request_json } from "../../src/mixins/_request.ts"; -import { set_option } from "../../src/mod.ts"; +import { ERROR_CODE, set_option } from "../../src/mod.ts"; import { setup } from "../../src/setup.ts"; import { DenoFileStore } from "../../src/store.ts"; -import { jo, jom } from "../../src/util.ts"; +import { jm, jo } from "../../src/util.ts"; import { cache_fetch } from "../../src/util/cache-fetch.ts"; setup({ @@ -154,15 +155,6 @@ async function get_uri_response( return null; } -export interface PathDeclaration { - path: string; - value: string; - parent: any; - parentProperty: string; - hasArrExpr: boolean; - pointer: string; -} - export interface FetchMapItem { uri: string; paths: string[]; @@ -172,14 +164,14 @@ export const fetch_map: FetchMapItem[] = [ { uri: "browse:FEmusic_explore", paths: [ - "json.contents.singleColumnBrowseResultsRenderer.tabs.0.tabRenderer.content.sectionListRenderer.contents[1:].musicCarouselShelfRenderer.header.musicCarouselShelfBasicHeaderRenderer.title.runs[0].text", + "json.contents.singleColumnBrowseResultsRenderer.tabs.0.tabRenderer.content.sectionListRenderer.contents[1:].musicCarouselShelfRenderer.header.musicCarouselShelfBasicHeaderRenderer.title.runs.0.text", ], }, { // We select the US because they are the ones who can see Genres uri: "browse:FEmusic_charts?formData.selectedValues=[US]", paths: [ - "json.contents.singleColumnBrowseResultsRenderer.tabs.0.tabRenderer.content.sectionListRenderer.contents.*.musicCarouselShelfRenderer.header.musicCarouselShelfBasicHeaderRenderer.title.runs[0].text", + "json.contents.singleColumnBrowseResultsRenderer.tabs.0.tabRenderer.content.sectionListRenderer.contents.*.musicCarouselShelfRenderer.header.musicCarouselShelfBasicHeaderRenderer.title.runs.0.text", ], }, { @@ -188,8 +180,8 @@ export const fetch_map: FetchMapItem[] = [ // and make sure the item you add is NOT the first item in any category uri: "browse:UC9vrvNSL3xcWGSkV86REBSg", paths: [ - "json.contents.singleColumnBrowseResultsRenderer.tabs.0.tabRenderer.content.sectionListRenderer.contents.*.musicShelfRenderer.title.runs[0].text", - "json.contents.singleColumnBrowseResultsRenderer.tabs.0.tabRenderer.content.sectionListRenderer.contents.*.musicCarouselShelfRenderer.header.musicCarouselShelfBasicHeaderRenderer.title.runs[0].text", + "json.contents.singleColumnBrowseResultsRenderer.tabs.0.tabRenderer.content.sectionListRenderer.contents.*.musicShelfRenderer.title.runs.0.text", + "json.contents.singleColumnBrowseResultsRenderer.tabs.0.tabRenderer.content.sectionListRenderer.contents.*.musicCarouselShelfRenderer.header.musicCarouselShelfBasicHeaderRenderer.title.runs.0.text", ], }, { @@ -204,13 +196,13 @@ export const fetch_map: FetchMapItem[] = [ uri: "search:get+lucky", paths: [ "json.contents.tabbedSearchResultsRenderer.tabs.0.tabRenderer.content.sectionListRenderer.header.chipCloudRenderer.chips.*.chipCloudChipRenderer.text.runs.0.text", - "json.contents.tabbedSearchResultsRenderer.tabs.0.tabRenderer.content.sectionListRenderer.contents.*.musicShelfRenderer.contents.0.musicResponsiveListItemRenderer.flexColumns.1.musicResponsiveListItemFlexColumnRenderer.text.runs[0].text", + "json.contents.tabbedSearchResultsRenderer.tabs.0.tabRenderer.content.sectionListRenderer.contents.*.musicShelfRenderer.contents.0.musicResponsiveListItemRenderer.flexColumns.1.musicResponsiveListItemFlexColumnRenderer.text.runs.0.text", "json.contents.tabbedSearchResultsRenderer.tabs.0.tabRenderer.content.sectionListRenderer.contents.0.musicCardShelfRenderer.subtitle.runs.0.text", ], }, ]; -const base_strings = new Map(); +const base_strings = new Map(); async function get_language_map() { const map = new Map(); @@ -219,13 +211,18 @@ async function get_language_map() { const data = await get_uri_response(item.uri); for (const path of item.paths) { - const items = jom(data, path, "all") as PathDeclaration[] ?? - []; + const items = jm(data, path, "all"); english_strings.forEach((string, key) => { const match = items.find((item) => item.value === string); if (!match) return; + if (typeof match.value !== "string") { + throw new MuseError( + ERROR_CODE.PARSING_INVALID_JSON, + `Expected string value for key "${key}": ${JSON.stringify(match)}`, + ); + } map.set(key, [item.uri, match.path]); base_strings.set(key, match.value); @@ -238,16 +235,6 @@ async function get_language_map() { const base_map = await get_language_map(); -async function _test_fetch_map_item(item: FetchMapItem) { - const data = await get_uri_response(item.uri); - - const items = - item.paths.map((path) => jom(data, path)).flat() as PathDeclaration[] ?? - []; - - console.log("items", items); -} - // console.log("base map", base_strings); async function get_strings_for_lang(lang: string) { diff --git a/src/errors.ts b/src/errors.ts index bf548e4..a98b071 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -26,7 +26,7 @@ export enum ERROR_CODE { } export class MuseError extends Error { - name = "MuseError"; + override name = "MuseError"; code: ERROR_CODE; constructor(code: ERROR_CODE, message: string, options?: ErrorOptions) { super(message, options); diff --git a/src/util.ts b/src/util.ts index f135774..3afd69d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -17,32 +17,31 @@ export const debug = (...args: unknown[]) => { if (get_option("debug")) console.debug(...args); }; -export const jom = ( - json: unknown, - path: string, - resultType?: JSONPathOptions["resultType"], -): any => { - const result = JSONPath({ path, json: json as JSON, resultType }); - return result.length ? result : null; -}; -export const jo = ( - json: unknown, - path: string, - ...others: string[] -): any => { - const result = JSONPath({ - path: [path, ...others].join("."), - json: json as JSON, - }); +export interface JSONPathAllItem { + path: string; + value: any; + parent: any; + parentProperty: string; + hasArrExpr: boolean; + pointer: string; +} + +export const jm: { + (json: unknown, path: string, resultType?: "value"): any[]; + (json: unknown, path: string, resultType: "all"): JSONPathAllItem[]; +} = (json: unknown, path: string, resultType): any => + JSONPath({ path, json: json as JSON, resultType }); +export const jo = (json: unknown, ...paths: [string, ...string[]]): any => { + const result = jm(json, paths.join(".")); return result.length ? result[0] : null; }; -export const j = (json: unknown, path: string, ...others: string[]) => { - const result = jo(json, path, ...others); +export const j = (json: unknown, ...paths: [string, ...string[]]) => { + const result = jo(json, ...paths); if (result == null) { throw new MuseError( ERROR_CODE.PARSING_INVALID_JSON, - `JSONPath expression "${[path, ...others]}" returned nothing`, + `JSONPath expression "${paths.join(".")}" returned nothing`, ); } From 153694317e1ee75df5b7e265e26e7c977bb5185f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=A0=E4=B8=8B=20=E5=85=8B=E5=BD=A6?= Date: Tue, 12 Aug 2025 17:29:06 +0900 Subject: [PATCH 03/11] chore: uninstall lodash-es --- deno.lock | 7 +------ src/deps.ts | 1 - 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/deno.lock b/deno.lock index 59a3b71..055fc17 100644 --- a/deno.lock +++ b/deno.lock @@ -23,8 +23,7 @@ "jsr:@ts-morph/bootstrap@^0.24.0": "jsr:@ts-morph/bootstrap@0.24.0", "jsr:@ts-morph/common@^0.24.0": "jsr:@ts-morph/common@0.24.0", "npm:jsonpath-plus@10.0.0": "npm:jsonpath-plus@10.0.0_jsep@1.3.9", - "npm:jsonpath-plus@10.1.0": "npm:jsonpath-plus@10.1.0_jsep@1.3.9", - "npm:lodash-es@4.17.21": "npm:lodash-es@4.17.21" + "npm:jsonpath-plus@10.1.0": "npm:jsonpath-plus@10.1.0_jsep@1.3.9" }, "jsr": { "@david/code-block-writer@13.0.3": { @@ -151,10 +150,6 @@ "@jsep-plugin/regex": "@jsep-plugin/regex@1.0.3_jsep@1.3.9", "jsep": "jsep@1.3.9" } - }, - "lodash-es@4.17.21": { - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "dependencies": {} } } }, diff --git a/src/deps.ts b/src/deps.ts index 7b5bb8d..b804275 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -1,3 +1,2 @@ export { JSONPath, type JSONPathOptions } from "npm:jsonpath-plus@10.1.0"; -export { omit } from "npm:lodash-es@4.17.21"; export { basename, extname } from "jsr:@std/path"; From 7c3c7c8ae09cd1f1c9cddd0d7319e00a52452f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=A0=E4=B8=8B=20=E5=85=8B=E5=BD=A6?= Date: Tue, 12 Aug 2025 18:50:11 +0900 Subject: [PATCH 04/11] Refactor auth.ts --- src/auth.ts | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index 2e51372..1dc1575 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -4,6 +4,19 @@ import { wait } from "./util.ts"; import { Store } from "./store.ts"; import { ERROR_CODE, MuseError } from "./errors.ts"; +interface TokenResponseFromLoginCode { + access_token: string; + refresh_token: string; + token_type: string; + expires_in: number; + scope: string; +} + +type TokenResponseFromRefreshToken = Omit< + TokenResponseFromLoginCode, + "refresh_token" +>; + export interface Token { access_token: string; refresh_token: string; @@ -95,10 +108,10 @@ export class Authenticator extends EventTarget { let fn: () => Promise = () => Promise.resolve(); this.dispatchEvent( new CustomEvent("requires-login", { - detail: (_fn: () => Promise) => { + detail: (_fn) => { fn = _fn; }, - }) as RequiresLoginEvent, + }) satisfies RequiresLoginEvent, ); await fn(); @@ -130,13 +143,11 @@ export class Authenticator extends EventTarget { throw new MuseError( ERROR_CODE.AUTH_CANT_GET_LOGIN_CODE, "Can't get login code", - { - cause: text, - }, + { cause: text }, ); } - const data = response.json() as Promise; + const data = await response.json() as LoginCode; return data; } @@ -159,25 +170,19 @@ export class Authenticator extends EventTarget { ): Promise; async load_token_with_code(...args: any[]): Promise { - let res: Token | null = null; + let res: TokenResponseFromLoginCode | null = null; let tries = 0; let code: string, interval: number, signal: AbortSignal | undefined; if (typeof args[0] === "string") { - code = args[0]; - interval = args[1] ?? 5; - signal = args[2]; + [code, interval = 5, signal] = args; } else { - code = args[0].device_code; - interval = args[0].interval; - signal = args[1]; + [{ device_code: code, interval }, signal] = args; } while (!res || !res.refresh_token) { - if (signal?.aborted) { - throw new DOMException("Aborted", "AbortError"); - } + signal?.throwIfAborted(); const response = await this.client.request( "https://oauth2.googleapis.com/token", @@ -193,7 +198,7 @@ export class Authenticator extends EventTarget { }, ); - res = await response.json() as Token; + res = await response.json() as TokenResponseFromLoginCode; if (!response.ok) await wait(interval * 1000); @@ -245,7 +250,7 @@ export class Authenticator extends EventTarget { ); } - const new_token = await res.json() as Token; + const new_token = await res.json() as TokenResponseFromRefreshToken; this.token = { ...token, @@ -253,7 +258,7 @@ export class Authenticator extends EventTarget { expires_date: new Date(Date.now() + new_token.expires_in * 1000), }; - return this.token!; + return this.token; } return token; From 0736cc425f0bcd1dd1fb69f3eb3524fb641a4dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=A0=E4=B8=8B=20=E5=85=8B=E5=BD=A6?= Date: Tue, 12 Aug 2025 19:11:21 +0900 Subject: [PATCH 05/11] Refactor setup.ts --- src/setup.ts | 18 ++++++++---------- tests/util.ts | 4 +--- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/setup.ts b/src/setup.ts index bf98929..91705be 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -56,26 +56,24 @@ export interface SetupOptions extends Omit { } export function setup(passed_options: Partial = {}) { - const options_without_auth = { ...passed_options }; - delete options_without_auth.auth; + set_options({ ...passed_options, auth: undefined }); - set_options(options_without_auth as Omit); - - if (passed_options.store) { - const visitor_id = passed_options.store.get("visitor_id"); + const { auth, store, client } = passed_options; + if (store) { + const visitor_id = store.get("visitor_id"); if (visitor_id) { set_option("visitor_id", visitor_id as string); } } - if (passed_options.auth || passed_options.client || passed_options.store) { - if (passed_options.auth instanceof Authenticator) { - options.auth = passed_options.auth; + if (auth || client || store) { + if (auth instanceof Authenticator) { + options.auth = auth; } else { options.auth = new Authenticator({ client: options.client, store: options.store, - ...(passed_options.auth ?? {}) as PureAuthenticatorOptions, + token: auth?.token, }); } } diff --git a/tests/util.ts b/tests/util.ts index c470a05..89409de 100644 --- a/tests/util.ts +++ b/tests/util.ts @@ -40,11 +40,9 @@ export const auth_flow = async () => { }; export const init_client = () => { - const client = setup({ + setup({ store: new DenoFileStore("store/muse-store.json"), }); auth_flow(); - - return client; }; From 9e8bc1567bc5f8fe76deaf67af526d0b83e41f77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=A0=E4=B8=8B=20=E5=85=8B=E5=BD=A6?= Date: Tue, 12 Aug 2025 19:35:15 +0900 Subject: [PATCH 06/11] Fix set_options --- src/setup.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/setup.ts b/src/setup.ts index 91705be..a2c23de 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -41,7 +41,12 @@ export function get_option(name: Name) { } export function set_options(passed_options: Partial) { - Object.assign(options, passed_options); + Object.assign( + options, + Object.fromEntries( + Object.entries(passed_options).filter(([, value]) => value !== undefined), + ), + ); } export function set_option( From 9eada0a7f8ccd61e61783d40afd06b60c5e339fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=A0=E4=B8=8B=20=E5=85=8B=E5=BD=A6?= Date: Sun, 24 Aug 2025 18:29:10 +0900 Subject: [PATCH 07/11] improve typing of find_object_by_key, find_objects_by_key --- src/nav.ts | 57 ++++++++++++++++++++++++++++++++++++++++++++- src/parsers/util.ts | 4 ++-- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/nav.ts b/src/nav.ts index 2251366..1260db7 100644 --- a/src/nav.ts +++ b/src/nav.ts @@ -96,8 +96,46 @@ interface ObjectWithNested { [key: number]: any; } -type ObjectList = Array<{ [key: string]: any } | ObjectWithNested>; +type ObjectList = ReadonlyArray<{ [key: string]: any } | ObjectWithNested>; +export function find_object_by_key< + const K extends string, + T extends { [key in K]?: any }, +>( + objectList: ReadonlyArray, + key: K, + nested?: undefined, + isKey?: false, +): T | null; +export function find_object_by_key< + const K extends string, + T extends { [key in K]?: any }, +>( + objectList: ReadonlyArray, + key: K, + nested: undefined, + isKey: true, +): T[K] | null; +export function find_object_by_key< + const K extends string, + T extends { [key in K]?: any }, + const K2 extends string, +>( + objectList: ReadonlyArray<{ [_key in K2]: T }>, + key: K, + nested: K2, + isKey?: false, +): T | null; +export function find_object_by_key< + const K extends string, + T extends { [key in K]?: any }, + const K2 extends string, +>( + objectList: ReadonlyArray<{ [_key in K2]: T }>, + key: K, + nested: K2, + isKey: true, +): T[K] | null; export function find_object_by_key( objectList: ObjectList, key: string, @@ -113,6 +151,23 @@ export function find_object_by_key( return null; } +export function find_objects_by_key< + const K extends string, + T extends { [_key in K]?: any }, +>( + object_list: ReadonlyArray, + key: K, + nested?: undefined, +): T[]; +export function find_objects_by_key< + const K extends string, + T extends { [_key in K]?: any }, + const K2 extends string, +>( + object_list: ReadonlyArray<{ [_key in K2]: T }>, + key: K, + nested: K2, +): T[]; export function find_objects_by_key( object_list: any, key: string, diff --git a/src/parsers/util.ts b/src/parsers/util.ts index d2bf41e..f7533b9 100644 --- a/src/parsers/util.ts +++ b/src/parsers/util.ts @@ -25,10 +25,10 @@ export function get_menu_playlists(data: any) { const watch_menu = find_objects_by_key( j(data, MENU_ITEMS), "menuNavigationItemRenderer", - ) as any; + ); for ( - const item of watch_menu.map((menu: any) => menu.menuNavigationItemRenderer) + const item of watch_menu.map((menu) => menu.menuNavigationItemRenderer) ) { const icon = j(item, "icon.iconType"); From 52b527dd35d835d9bafc685405e67c6d99acfb78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=A0=E4=B8=8B=20=E5=85=8B=E5=BD=A6?= Date: Sun, 24 Aug 2025 18:29:33 +0900 Subject: [PATCH 08/11] replace findIndex < 0 with !some --- src/request.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/request.ts b/src/request.ts index e86d267..ca9da8c 100644 --- a/src/request.ts +++ b/src/request.ts @@ -69,7 +69,7 @@ export class FetchClient extends RequestClient { // headers.set("X-Goog-Visitor-Id", get_option("visitor_id")); if (lang) { - if (LOCALES.languages.findIndex((e) => e.value === lang) < 0) { + if (!LOCALES.languages.some((e) => e.value === lang)) { throw new MuseError( ERROR_CODE.UNSUPPORTED_LANGUAGE, `Unsupported locale: ${lang}`, @@ -86,7 +86,7 @@ export class FetchClient extends RequestClient { } if (location) { - if (LOCALES.locations.findIndex((e) => e.value === location) < 0) { + if (!LOCALES.locations.some((e) => e.value === location)) { throw new MuseError( ERROR_CODE.UNSUPPORTED_LOCATION, `Unsupported location: ${location}`, From e750da714c10110d56aeffaa2d0841c8f5436bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=A0=E4=B8=8B=20=E5=85=8B=E5=BD=A6?= Date: Sun, 24 Aug 2025 18:34:44 +0900 Subject: [PATCH 09/11] Fix tests/browsing/utils.test.ts --- tests/browsing/utils.test.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/browsing/utils.test.ts b/tests/browsing/utils.test.ts index 55da4d0..81a99c6 100644 --- a/tests/browsing/utils.test.ts +++ b/tests/browsing/utils.test.ts @@ -4,35 +4,37 @@ import { assertRejects, beforeAll, describe, + get_option, it, } from "../util.ts"; - -import * as muse from "../../src/mod.ts" +import * as utils from "../../src/mixins/utils.ts"; it("get_timestamp", () => { const one_day = 24 * 60 * 60 * 1000; const timestamp = Math.round(Date.now() / one_day) - 7; - assertEquals(muse.get_timestamp(), timestamp); + assertEquals(utils.get_timestamp(), timestamp); }); describe("check_auth", () => { let stored_token: any; beforeAll(async () => { - stored_token = muse.get_option("auth").has_token() ? await muse.get_option("auth").get_token() : null; + stored_token = get_option("auth").has_token() + ? await get_option("auth").get_token() + : null; }); afterAll(() => { - muse.get_option("auth").token = stored_token; + get_option("auth").token = stored_token; }); it("should throw error if not logged in", () => { - muse.get_option("auth").token = null; - assertRejects(() => muse.check_auth()); + get_option("auth").token = null; + assertRejects(() => utils.check_auth()); }); it("should not throw error if logged in", () => { - muse.get_option("auth").token = { dummy: "token" } as any; - muse.check_auth(); + get_option("auth").token = { dummy: "token" } as any; + utils.check_auth(); }); }); From b613566d4c7fd264d4766e269291abd9dd8b6639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=A0=E4=B8=8B=20=E5=85=8B=E5=BD=A6?= Date: Sun, 24 Aug 2025 18:46:09 +0900 Subject: [PATCH 10/11] Fix tests/browsing/home.test.ts (excl. must handle limit) --- tests/browsing/home.test.ts | 97 ++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 40 deletions(-) diff --git a/tests/browsing/home.test.ts b/tests/browsing/home.test.ts index 2a36c72..ce7a55c 100644 --- a/tests/browsing/home.test.ts +++ b/tests/browsing/home.test.ts @@ -10,7 +10,7 @@ import { it, } from "../util.ts"; -import { get_home } from "../../mod.ts"; +import { get_home, MixedItem } from "../../mod.ts"; beforeEach(() => { init_client(); @@ -20,13 +20,13 @@ describe("home", () => { let home: Awaited>; beforeAll(async () => { - home = await get_home(6); + home = await get_home({ limit: 6 }); }); it("must handle limit", async () => { assert(home.results.length >= 6, "should have alteast have 6 results"); - const home30 = await get_home(30); + const home30 = await get_home({ limit: 30 }); assert( // there are no more results home30.continuation == null || @@ -73,82 +73,99 @@ describe("home", () => { }); describe("result", () => { - let result: any; + let results: any[]; beforeAll(() => { - const get_value = (result: any) => { - return Number(typeof result.subtitle === "string") + - Number(result.thumbnails !== null); - }; - // tries to find result with all the options - result = home - .results - .sort((a, b) => { - return get_value(b) - get_value(a); - })[0]; + results = home.results; }); it("title", () => { - assertEquals(typeof result.title, "string", "title must be a string"); + for (const result of results) { + assertEquals(typeof result.title, "string", "title must be a string"); - assert(result.title.length > 0, "title must not be blank"); + assert(result.title.length > 0, "title must not be blank"); + } }); it("browseId", () => { - assertEquals( - typeof result.browseId, - "string", - "browseId must be a string", - ); - - assert(result.browseId.length > 0, "browseId must not be blank"); + const browseIds = results.map((result) => result.browseId) + .filter((id) => id != null); + if (browseIds.length === 0) { + return console.warn("couldnt find a result with a browseId"); + } + for (const browseId of browseIds) { + if (browseId != null) { + assertEquals( + typeof browseId, + "string", + "browseId must be a string", + ); + + assert(browseId.length > 0, "browseId must not be blank"); + } + } }); it("subtitle", () => { - if (!result.subtitle) { + const subtitles = results.map((result) => result.subtitle) + .filter((id) => id != null); + if (subtitles.length === 0) { return console.warn("couldnt find a result with a subtitle"); } - assertEquals( - typeof result.subtitle, - "string", - "subtitle must be a string", - ); + for (const subtitle of subtitles) { + assertEquals( + typeof subtitle, + "string", + "subtitle must be a string", + ); - assert(result.subtitle.length > 0, "subtitle must not be blank"); + assert(subtitle.length > 0, "subtitle must not be blank"); + } }); it("thumbnails", () => { - if (!result.thumbnails) { - return console.warn("couldnt find a result with a thumbnails"); + const thumbnailsList = results.map((result) => result.thumbnails) + .filter((id) => id != null); + if (thumbnailsList.length === 0) { + return console.warn("couldnt find a result with thumbnails"); } - assertThumbnails(result.thumbnails); + for (const thumbnails of thumbnailsList) { + assertThumbnails(thumbnails); + } }); it("should have contents", () => { - assert(result.contents); - assert(result.contents.length > 0); + for (const result of results) { + assert(Array.isArray(result.contents)); + assert(result.contents.length > 0); + } }); }); describe("all contents", () => { - let contents: any[]; + let contents: Exclude[]; beforeAll(() => { - contents = home.results.reduce( - (acc, curr) => [...acc, ...curr.contents], - [], - ); + contents = home.results.map((x) => x.contents) + .filter((x) => Array.isArray(x)) + .flat() + .filter((x) => x != null); }); it("each should have known type", () => { contents.forEach((content) => { assertArrayIncludes([ "playlist", + "watch-playlist", + "inline-video", "song", + "video", "album", "artist", + "channel", + "flat-song", ], [content.type]); }); }); From 6af02c003379ac5d187ea4d349c661f851af38d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=A0=E4=B8=8B=20=E5=85=8B=E5=BD=A6?= Date: Sun, 24 Aug 2025 23:20:20 +0900 Subject: [PATCH 11/11] Refactor parsers/browsing.ts --- src/parsers/browsing.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/parsers/browsing.ts b/src/parsers/browsing.ts index aadda8b..7bfd15f 100644 --- a/src/parsers/browsing.ts +++ b/src/parsers/browsing.ts @@ -828,19 +828,20 @@ export function parse_watch_playlist(data: any): WatchPlaylist { }; } -type Translatable = keyof typeof STRINGS[keyof typeof STRINGS]; +type Language = keyof typeof STRINGS; +type Translatable = keyof typeof STRINGS[Language]; -export function _(id: Translatable) { - const result = STRINGS[get_option("language") as keyof typeof STRINGS]; +export function _(id: Translatable): string { + const result = STRINGS[get_option("language") as Language]; return result?.[id] ?? id; } -export function __(value: string) { +export function __(value: string): Translatable | null { // does the reverse of _() // it returns the key of the string or null if it doesn't exist - const result = STRINGS[get_option("language") as keyof typeof STRINGS]; + const result = STRINGS[get_option("language") as Language]; if (result) { for (const key in result) { @@ -874,8 +875,8 @@ export function parse_two_columns(json: any) { }; } -export function parse_description_runs(runs: any) { - return runs.map((run: any) => { +export function parse_description_runs(runs: any[]) { + return runs.map((run) => { return jo(run, "navigationEndpoint.urlEndpoint.url") ?? run.text; }).join(""); }