forked from github-mirror/Verome-API
Fix album, artist, playlist, and related endpoints for new YouTube API structure
This commit is contained in:
156
lib.ts
156
lib.ts
@@ -69,14 +69,51 @@ export class YTMusic {
|
|||||||
|
|
||||||
async getAlbum(browseId: string) {
|
async getAlbum(browseId: string) {
|
||||||
const data = await this.makeRequest("browse", { browseId });
|
const data = await this.makeRequest("browse", { browseId });
|
||||||
const header = data?.header?.musicDetailHeaderRenderer || data?.header?.musicImmersiveHeaderRenderer || {};
|
|
||||||
const contents = data?.contents?.singleColumnBrowseResultsRenderer?.tabs?.[0]?.tabRenderer?.content?.sectionListRenderer?.contents || [];
|
// Handle different header types
|
||||||
|
const header = data?.header?.musicDetailHeaderRenderer ||
|
||||||
|
data?.header?.musicImmersiveHeaderRenderer ||
|
||||||
|
data?.header?.musicVisualHeaderRenderer || {};
|
||||||
|
|
||||||
|
// Handle both single and two column layouts
|
||||||
|
const singleColumn = data?.contents?.singleColumnBrowseResultsRenderer?.tabs?.[0]?.tabRenderer?.content?.sectionListRenderer?.contents;
|
||||||
|
const twoColumn = data?.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer?.contents;
|
||||||
|
const contents = singleColumn || twoColumn || [];
|
||||||
|
|
||||||
|
// Extract title and artist from header
|
||||||
|
const title = header.title?.runs?.[0]?.text;
|
||||||
|
const subtitleRuns = header.subtitle?.runs || header.straplineTextOne?.runs || [];
|
||||||
|
const artist = subtitleRuns.find((r: any) => r.navigationEndpoint)?.text || subtitleRuns[0]?.text;
|
||||||
|
|
||||||
|
// Get thumbnail
|
||||||
|
const thumbnail = header.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.slice(-1)[0]?.url ||
|
||||||
|
header.thumbnail?.croppedSquareThumbnailRenderer?.thumbnail?.thumbnails?.slice(-1)[0]?.url;
|
||||||
|
|
||||||
|
// Parse tracks
|
||||||
|
const tracks = this.parseTracksFromContents(contents);
|
||||||
|
|
||||||
|
// Get album metadata from two column layout
|
||||||
|
const primaryContents = data?.contents?.twoColumnBrowseResultsRenderer?.tabs?.[0]?.tabRenderer?.content?.sectionListRenderer?.contents || [];
|
||||||
|
let year = "";
|
||||||
|
let trackCount = tracks.length;
|
||||||
|
|
||||||
|
for (const section of primaryContents) {
|
||||||
|
const descShelf = section.musicDescriptionShelfRenderer;
|
||||||
|
if (descShelf) {
|
||||||
|
const subHeader = descShelf.subheader?.runs?.[0]?.text || "";
|
||||||
|
const yearMatch = subHeader.match(/\d{4}/);
|
||||||
|
if (yearMatch) year = yearMatch[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: header.title?.runs?.[0]?.text,
|
browseId,
|
||||||
artist: header.subtitle?.runs?.[0]?.text,
|
title,
|
||||||
thumbnail: header.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.[0]?.url,
|
artist,
|
||||||
tracks: this.parseTracksFromContents(contents),
|
thumbnail,
|
||||||
|
year,
|
||||||
|
trackCount,
|
||||||
|
tracks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,10 +121,49 @@ export class YTMusic {
|
|||||||
const data = await this.makeRequest("browse", { browseId });
|
const data = await this.makeRequest("browse", { browseId });
|
||||||
const header = data?.header?.musicImmersiveHeaderRenderer || data?.header?.musicVisualHeaderRenderer || {};
|
const header = data?.header?.musicImmersiveHeaderRenderer || data?.header?.musicVisualHeaderRenderer || {};
|
||||||
|
|
||||||
|
// Get contents for top songs, albums, etc.
|
||||||
|
const contents = data?.contents?.singleColumnBrowseResultsRenderer?.tabs?.[0]?.tabRenderer?.content?.sectionListRenderer?.contents || [];
|
||||||
|
|
||||||
|
// Parse sections
|
||||||
|
const topSongs: any[] = [];
|
||||||
|
const albums: any[] = [];
|
||||||
|
const singles: any[] = [];
|
||||||
|
const videos: any[] = [];
|
||||||
|
|
||||||
|
for (const section of contents) {
|
||||||
|
const shelf = section.musicShelfRenderer;
|
||||||
|
const carousel = section.musicCarouselShelfRenderer;
|
||||||
|
|
||||||
|
if (shelf) {
|
||||||
|
const title = shelf.title?.runs?.[0]?.text?.toLowerCase() || "";
|
||||||
|
if (title.includes("song")) {
|
||||||
|
for (const item of shelf.contents || []) {
|
||||||
|
const parsed = this.parseMusicItem(item.musicResponsiveListItemRenderer);
|
||||||
|
if (parsed) topSongs.push(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (carousel) {
|
||||||
|
const title = carousel.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs?.[0]?.text?.toLowerCase() || "";
|
||||||
|
const items = (carousel.contents || []).map((item: any) => this.parseTwoRowItem(item.musicTwoRowItemRenderer)).filter(Boolean);
|
||||||
|
|
||||||
|
if (title.includes("album")) albums.push(...items);
|
||||||
|
else if (title.includes("single")) singles.push(...items);
|
||||||
|
else if (title.includes("video")) videos.push(...items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
browseId,
|
||||||
name: header.title?.runs?.[0]?.text,
|
name: header.title?.runs?.[0]?.text,
|
||||||
description: header.description?.runs?.[0]?.text,
|
description: header.description?.runs?.[0]?.text,
|
||||||
thumbnail: header.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.[0]?.url,
|
thumbnail: header.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.slice(-1)[0]?.url,
|
||||||
|
subscribers: header.subscriptionButton?.subscribeButtonRenderer?.subscriberCountText?.runs?.[0]?.text,
|
||||||
|
topSongs,
|
||||||
|
albums,
|
||||||
|
singles,
|
||||||
|
videos,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,14 +217,29 @@ export class YTMusic {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getPlaylist(playlistId: string) {
|
async getPlaylist(playlistId: string) {
|
||||||
const data = await this.makeRequest("browse", { browseId: `VL${playlistId.replace(/^VL/, "")}` });
|
const browseId = `VL${playlistId.replace(/^VL/, "")}`;
|
||||||
const header = data?.header?.musicDetailHeaderRenderer || {};
|
const data = await this.makeRequest("browse", { browseId });
|
||||||
const contents = data?.contents?.singleColumnBrowseResultsRenderer?.tabs?.[0]?.tabRenderer?.content?.sectionListRenderer?.contents || [];
|
|
||||||
|
// Handle different header types
|
||||||
|
const header = data?.header?.musicDetailHeaderRenderer ||
|
||||||
|
data?.header?.musicEditablePlaylistDetailHeaderRenderer?.header?.musicDetailHeaderRenderer || {};
|
||||||
|
|
||||||
|
// Handle both single and two column layouts
|
||||||
|
const singleColumn = data?.contents?.singleColumnBrowseResultsRenderer?.tabs?.[0]?.tabRenderer?.content?.sectionListRenderer?.contents;
|
||||||
|
const twoColumn = data?.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer?.contents;
|
||||||
|
const contents = singleColumn || twoColumn || [];
|
||||||
|
|
||||||
|
// Parse subtitle for author and track count
|
||||||
|
const subtitleRuns = header.subtitle?.runs || [];
|
||||||
|
const author = subtitleRuns.find((r: any) => r.navigationEndpoint)?.text || subtitleRuns[0]?.text;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
playlistId: playlistId.replace(/^VL/, ""),
|
||||||
title: header.title?.runs?.[0]?.text,
|
title: header.title?.runs?.[0]?.text,
|
||||||
author: header.subtitle?.runs?.[0]?.text,
|
author,
|
||||||
thumbnail: header.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.[0]?.url,
|
description: header.description?.runs?.[0]?.text,
|
||||||
|
thumbnail: header.thumbnail?.croppedSquareThumbnailRenderer?.thumbnail?.thumbnails?.slice(-1)[0]?.url ||
|
||||||
|
header.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.slice(-1)[0]?.url,
|
||||||
tracks: this.parseTracksFromContents(contents),
|
tracks: this.parseTracksFromContents(contents),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -216,10 +307,30 @@ export class YTMusic {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
const secondaryResults = data?.contents?.twoColumnWatchNextResults?.secondaryResults?.secondaryResults?.results || [];
|
const secondaryResults = data?.contents?.twoColumnWatchNextResults?.secondaryResults?.secondaryResults?.results || [];
|
||||||
|
const results: any[] = [];
|
||||||
|
|
||||||
return secondaryResults
|
for (const item of secondaryResults) {
|
||||||
.filter((item: any) => item.compactVideoRenderer)
|
// Handle new lockupViewModel format
|
||||||
.map((item: any) => {
|
if (item.lockupViewModel) {
|
||||||
|
const lockup = item.lockupViewModel;
|
||||||
|
const metadata = lockup.metadata?.lockupMetadataViewModel;
|
||||||
|
const contentImage = lockup.contentImage?.collectionThumbnailViewModel?.primaryThumbnail?.thumbnailViewModel;
|
||||||
|
|
||||||
|
const videoIdMatch = lockup.rendererContext?.commandContext?.onTap?.innertubeCommand?.watchEndpoint?.videoId ||
|
||||||
|
lockup.contentId;
|
||||||
|
|
||||||
|
if (videoIdMatch) {
|
||||||
|
results.push({
|
||||||
|
videoId: videoIdMatch,
|
||||||
|
title: metadata?.title?.content,
|
||||||
|
artist: metadata?.metadata?.contentMetadataViewModel?.metadataRows?.[0]?.metadataParts?.[0]?.text?.content,
|
||||||
|
thumbnail: contentImage?.image?.sources?.[0]?.url,
|
||||||
|
duration: metadata?.metadata?.contentMetadataViewModel?.metadataRows?.[0]?.metadataParts?.[2]?.text?.content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle old compactVideoRenderer format (fallback)
|
||||||
|
else if (item.compactVideoRenderer) {
|
||||||
const video = item.compactVideoRenderer;
|
const video = item.compactVideoRenderer;
|
||||||
const durationText = video.lengthText?.simpleText || "";
|
const durationText = video.lengthText?.simpleText || "";
|
||||||
let durationSeconds = 0;
|
let durationSeconds = 0;
|
||||||
@@ -229,18 +340,19 @@ export class YTMusic {
|
|||||||
else if (parts.length === 3) durationSeconds = parts[0] * 3600 + parts[1] * 60 + parts[2];
|
else if (parts.length === 3) durationSeconds = parts[0] * 3600 + parts[1] * 60 + parts[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
if (video.videoId && !(durationSeconds > 0 && durationSeconds <= 60)) {
|
||||||
|
results.push({
|
||||||
videoId: video.videoId,
|
videoId: video.videoId,
|
||||||
title: video.title?.simpleText || video.title?.runs?.[0]?.text,
|
title: video.title?.simpleText || video.title?.runs?.[0]?.text,
|
||||||
artist: video.shortBylineText?.runs?.[0]?.text,
|
artist: video.shortBylineText?.runs?.[0]?.text,
|
||||||
thumbnail: video.thumbnail?.thumbnails?.[0]?.url,
|
thumbnail: video.thumbnail?.thumbnails?.[0]?.url,
|
||||||
duration: durationText,
|
duration: durationText,
|
||||||
duration_seconds: durationSeconds,
|
});
|
||||||
isShort: durationSeconds > 0 && durationSeconds <= 60,
|
}
|
||||||
};
|
}
|
||||||
})
|
}
|
||||||
.filter((v: any) => v.videoId && !v.isShort)
|
|
||||||
.slice(0, 20);
|
return results.slice(0, 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async makeRequest(endpoint: string, params: any) {
|
private async makeRequest(endpoint: string, params: any) {
|
||||||
|
|||||||
Reference in New Issue
Block a user