Add region/language support and top result card parsing

This commit is contained in:
Your Name
2026-01-11 11:25:51 +01:00
parent 8a3a8ff585
commit bb5da73408
2 changed files with 88 additions and 10 deletions

92
lib.ts
View File

@@ -24,7 +24,7 @@ export class YTMusic {
}; };
} }
async search(query: string, filter?: string, continuationToken?: string, ignoreSpelling = false) { async search(query: string, filter?: string, continuationToken?: string, _ignoreSpelling = false, region?: string, language?: string) {
// Normalize the query to handle Arabic and other Unicode characters properly // Normalize the query to handle Arabic and other Unicode characters properly
const normalizedQuery = query.normalize("NFC"); const normalizedQuery = query.normalize("NFC");
@@ -35,7 +35,16 @@ export class YTMusic {
? { query: normalizedQuery, params: filterParams } ? { query: normalizedQuery, params: filterParams }
: { query: normalizedQuery }; : { query: normalizedQuery };
const data = await this.makeRequest("search", params); // Use custom context if region or language specified
const context = (region || language) ? {
client: {
...this.context.client,
gl: region || this.context.client.gl,
hl: language || this.context.client.hl,
}
} : this.context;
const data = await this.makeRequestWithContext("search", params, context);
return this.parseSearchResults(data); return this.parseSearchResults(data);
} }
@@ -246,16 +255,31 @@ export class YTMusic {
return response.json(); return response.json();
} }
private async makeRequestWithContext(endpoint: string, params: any, context: any) {
const url = `${this.baseURL}/${endpoint}?key=${this.apiKey}`;
const body = { context, ...params };
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return response.json();
}
private getFilterParams(filter?: string): string | undefined { private getFilterParams(filter?: string): string | undefined {
// Return undefined for no filter (searches everything) // Return undefined for no filter (searches everything - mixed results)
if (!filter) return undefined; if (!filter) return undefined;
// These params are from YouTube Music's actual web requests
const filterMap: Record<string, string> = { const filterMap: Record<string, string> = {
songs: "EgWKAQIIAWoKEAMQBBAJEAoQBQ%3D%3D", songs: "EgWKAQIIAWoKEAkQAxAEEAoQBQ%3D%3D",
videos: "EgWKAQIQAWoKEAMQBBAJEAoQBQ%3D%3D", videos: "EgWKAQIQAWoKEAkQAxAEEAoQBQ%3D%3D",
albums: "EgWKAQIYAWoKEAMQBBAJEAoQBQ%3D%3D", albums: "EgWKAQIYAWoKEAkQAxAEEAoQBQ%3D%3D",
artists: "EgWKAQIgAWoKEAMQBBAJEAoQBQ%3D%3D", artists: "EgWKAQIgAWoKEAkQAxAEEAoQBQ%3D%3D",
playlists: "EgWKAQIoAWoKEAMQBBAJEAoQBQ%3D%3D", playlists: "EgWKAQIoAWoKEAkQAxAEEAoQBQ%3D%3D",
community_playlists: "EgeKAQQoAEABagoQAxAEEAkQChAF",
featured_playlists: "EgeKAQQoADgBagoQAxAEEAkQChAF",
}; };
return filterMap[filter] || undefined; return filterMap[filter] || undefined;
} }
@@ -287,6 +311,13 @@ export class YTMusic {
if (results.length === 0) { if (results.length === 0) {
const sections = data?.contents?.tabbedSearchResultsRenderer?.tabs?.[0]?.tabRenderer?.content?.sectionListRenderer?.contents || []; const sections = data?.contents?.tabbedSearchResultsRenderer?.tabs?.[0]?.tabRenderer?.content?.sectionListRenderer?.contents || [];
for (const section of sections) { for (const section of sections) {
// Handle top result card (musicCardShelfRenderer)
if (section.musicCardShelfRenderer) {
const card = section.musicCardShelfRenderer;
const topResult = this.parseTopResultCard(card);
if (topResult) results.push(topResult);
}
// Handle regular shelf results
if (section.musicShelfRenderer) { if (section.musicShelfRenderer) {
for (const item of section.musicShelfRenderer.contents || []) { for (const item of section.musicShelfRenderer.contents || []) {
const parsed = this.parseMusicItem(item.musicResponsiveListItemRenderer); const parsed = this.parseMusicItem(item.musicResponsiveListItemRenderer);
@@ -300,6 +331,51 @@ export class YTMusic {
return { results, continuationToken }; return { results, continuationToken };
} }
private parseTopResultCard(card: any) {
if (!card) return null;
const title = card.title?.runs?.[0]?.text;
const subtitleRuns = card.subtitle?.runs || [];
const thumbnail = card.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.[0]?.url;
// Extract video ID from various possible locations
const videoId = card.onTap?.watchEndpoint?.videoId ||
card.buttons?.[0]?.buttonRenderer?.command?.watchEndpoint?.videoId;
// Extract browse ID for artists/albums
const browseId = card.onTap?.browseEndpoint?.browseId;
// Determine type from subtitle
const subtitleText = subtitleRuns.map((r: any) => r.text).join("");
let resultType = "song";
if (subtitleText.toLowerCase().includes("video") || subtitleText.toLowerCase().includes("vidéo")) {
resultType = "video";
} else if (subtitleText.toLowerCase().includes("artist") || subtitleText.toLowerCase().includes("artiste")) {
resultType = "artist";
} else if (subtitleText.toLowerCase().includes("album")) {
resultType = "album";
} else if (subtitleText.toLowerCase().includes("playlist")) {
resultType = "playlist";
}
// Extract artist name from subtitle
const artistRun = subtitleRuns.find((r: any) =>
r.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === "MUSIC_PAGE_TYPE_ARTIST"
);
const artists = artistRun ? [{ name: artistRun.text, id: artistRun.navigationEndpoint?.browseEndpoint?.browseId }] : [];
return {
title,
thumbnails: [{ url: thumbnail }],
videoId,
browseId,
artists,
resultType,
isTopResult: true,
subtitle: subtitleText,
};
}
private parseSuggestions(data: any): string[] { private parseSuggestions(data: any): string[] {
const suggestions: string[] = []; const suggestions: string[] = [];
const contents = data?.contents?.[0]?.searchSuggestionsSectionRenderer?.contents || data?.contents || []; const contents = data?.contents?.[0]?.searchSuggestionsSectionRenderer?.contents || data?.contents || [];

6
mod.ts
View File

@@ -100,13 +100,15 @@ async function handler(req: Request): Promise<Response> {
const filter = searchParams.get("filter") || undefined; const filter = searchParams.get("filter") || undefined;
const continuationToken = searchParams.get("continuationToken") || undefined; const continuationToken = searchParams.get("continuationToken") || undefined;
const ignoreSpelling = searchParams.get("ignore_spelling") === "true"; const ignoreSpelling = searchParams.get("ignore_spelling") === "true";
const region = searchParams.get("region") || searchParams.get("gl") || undefined;
const language = searchParams.get("language") || searchParams.get("hl") || undefined;
if (!query && !continuationToken) { if (!query && !continuationToken) {
return error("Missing required query parameter 'q' or 'continuationToken'"); return error("Missing required query parameter 'q' or 'continuationToken'");
} }
const results = await ytmusic.search(query || "", filter, continuationToken, ignoreSpelling); const results = await ytmusic.search(query || "", filter, continuationToken, ignoreSpelling, region, language);
return json({ query, filter, ...results }); return json({ query, filter, region, language, ...results });
} }
// Search suggestions // Search suggestions