Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement Next Song retrieval and parsing #35

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Binary file modified bun.lockb
Binary file not shown.
29 changes: 29 additions & 0 deletions src/YTMusic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Cookie, CookieJar } from "tough-cookie"
import { FE_MUSIC_HOME } from "./constants"
import AlbumParser from "./parsers/AlbumParser"
import ArtistParser from "./parsers/ArtistParser"
import NextParser from "./parsers/NextParser"
import Parser from "./parsers/Parser"
import PlaylistParser from "./parsers/PlaylistParser"
import SearchParser from "./parsers/SearchParser"
Expand All @@ -15,6 +16,7 @@ import {
ArtistDetailed,
ArtistFull,
HomeSection,
NextResult,
PlaylistDetailed,
PlaylistFull,
SearchResult,
Expand Down Expand Up @@ -528,4 +530,31 @@ export default class YTMusic {

return sections.map(Parser.parseHomeSection)
}

/**
* Get content for next song.
*
* @param videoId Video ID
* @param listId list ID
* @param params
*
* @returns List of the next song
*/
public async getNext(
videoId: string,
listId: string,
params?: string,
): Promise<NextResult[]> {
const data = await this.constructRequest("next", {
enablePersistentPlaylistPanel: true,
isAudioOnly: true,
params: params,
playlistId: listId,
tunerSettingValue: "AUTOMIX_SETTING_NORMAL",
videoId: videoId,
})

const contents = traverse(traverseList(data, "tabs", "tabRenderer")[0], "contents")
return contents.map(NextParser.parse)
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type {
VideoDetailed,
VideoFull,
HomeSection,
NextResult,
} from "./types"

export default YTMusic
28 changes: 28 additions & 0 deletions src/parsers/NextParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ArtistBasic, NextResult } from "../types"
import checkType from "../utils/checkType"
import { traverse, traverseList, traverseString } from "../utils/traverse"

export default class NextParser {
public static parse(data: any): NextResult {
const nextData = traverseString(data, "playlistPanelVideoRenderer")
const artistData = traverse(nextData, "longBylineText")
const artistBasic: ArtistBasic = {
artistId: traverseString(artistData, "browseId") || null,
name: traverseString(artistData, "text"),
}

return checkType(
{
index: +traverseString(nextData, "navigationEndpoint", "index"),
selected: !!traverseString(nextData, "selected"),
params: traverseString(nextData, "navigationEndpoint", "params"),
name: traverseString(nextData, "title", "text"),
artist: artistBasic,
playlistId: traverseString(nextData, "navigationEndpoint", "playlistId"),
videoId: traverseString(nextData, "videoId"),
thumbnails: traverseList(nextData, "thumbnails"),
},
NextResult,
)
}
}
12 changes: 12 additions & 0 deletions src/parsers/SongParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default class SongParser {

public static parseSearchResult(item: any): SongDetailed {
const columns = traverseList(item, "flexColumns", "runs")
const listData = traverseList(item, "menu", "navigationEndpoint")

// It is not possible to identify the title and author
const title = columns[0]
Expand All @@ -50,13 +51,16 @@ export default class SongParser {
: null,
duration: Parser.parseDuration(duration?.text),
thumbnails: traverseList(item, "thumbnails"),
listId: traverseString(listData, "playlistId") || null,
params: traverseString(listData, "params") || null,
},
SongDetailed,
)
}

public static parseArtistSong(item: any, artistBasic: ArtistBasic): SongDetailed {
const columns = traverseList(item, "flexColumns", "runs").flat()
const listData = traverseList(item, "menu", "navigationEndpoint")

const title = columns.find(isTitle)
const album = columns.find(isAlbum)
Expand All @@ -76,13 +80,16 @@ export default class SongParser {
: null,
duration: Parser.parseDuration(duration?.text),
thumbnails: traverseList(item, "thumbnails"),
listId: traverseString(listData, "playlistId") || null,
params: traverseString(listData, "params") || null,
},
SongDetailed,
)
}

public static parseArtistTopSong(item: any, artistBasic: ArtistBasic): SongDetailed {
const columns = traverseList(item, "flexColumns", "runs").flat()
const listData = traverseList(item, "menu", "navigationEndpoint")

const title = columns.find(isTitle)
const album = columns.find(isAlbum)
Expand All @@ -99,6 +106,8 @@ export default class SongParser {
},
duration: null,
thumbnails: traverseList(item, "thumbnails"),
listId: traverseString(listData, "playlistId") || null,
params: traverseString(listData, "params") || null,
},
SongDetailed,
)
Expand All @@ -112,6 +121,7 @@ export default class SongParser {
): SongDetailed {
const title = traverseList(item, "flexColumns", "runs").find(isTitle)
const duration = traverseList(item, "fixedColumns", "runs").find(isDuration)
const listData = traverseList(item, "menu", "navigationEndpoint")

return checkType(
{
Expand All @@ -122,6 +132,8 @@ export default class SongParser {
album: albumBasic,
duration: Parser.parseDuration(duration?.text),
thumbnails,
listId: traverseString(listData, "playlistId") || null,
params: traverseString(listData, "params") || null,
},
SongDetailed,
)
Expand Down
10 changes: 10 additions & 0 deletions src/parsers/VideoParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default class VideoParser {

public static parseSearchResult(item: any): VideoDetailed {
const columns = traverseList(item, "flexColumns", "runs").flat()
const listData = traverseList(item, "menu", "navigationEndpoint")

const title = columns.find(isTitle)
const artist = columns.find(isArtist) || columns[1]
Expand All @@ -40,22 +41,29 @@ export default class VideoParser {
},
duration: Parser.parseDuration(duration?.text),
thumbnails: traverseList(item, "thumbnails"),
listId: traverseString(listData, "playlistId") || null,
params: traverseString(listData, "params") || null,
}
}

public static parseArtistTopVideo(item: any, artistBasic: ArtistBasic): VideoDetailed {
const listData = traverseList(item, "menu", "navigationEndpoint")

return {
type: "VIDEO",
videoId: traverseString(item, "videoId"),
name: traverseString(item, "runs", "text"),
artist: artistBasic,
duration: null,
thumbnails: traverseList(item, "thumbnails"),
listId: traverseString(listData, "playlistId") || null,
params: traverseString(listData, "params") || null,
}
}

public static parsePlaylistVideo(item: any): VideoDetailed {
const columns = traverseList(item, "flexColumns", "runs").flat()
const listData = traverseList(item, "menu", "navigationEndpoint")

const title = columns.find(isTitle) || columns[0]
const artist = columns.find(isArtist) || columns[1]
Expand All @@ -76,6 +84,8 @@ export default class VideoParser {
},
duration: Parser.parseDuration(duration?.text),
thumbnails: traverseList(item, "thumbnails"),
listId: traverseString(listData, "playlistId") || null,
params: traverseString(listData, "params") || null,
},
VideoDetailed,
)
Expand Down
18 changes: 17 additions & 1 deletion src/tests/core.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
AlbumFull,
ArtistDetailed,
ArtistFull,
NextResult,
PlaylistDetailed,
PlaylistFull,
SearchResult,
Expand All @@ -20,6 +21,16 @@ import {

const errors: ZodError[] = []
const queries = ["Lilac", "Weekend", "Eill", "Eminem", "Lisa Hannigan"]

const nextQueries: [string, string][] = [
// videoId listId
["0-q1KafFCLU", "RDAMVMv7bnOxV4jAc"],
["3_g2un5M350", "RDAOHSpo_Uv9STIRtF73zMywLg"],
["k9r74T2d5zc", "RDAO58VCzmAxo6veMZY49UqvQw"],
["2JFLxtcMQBM", "RDAOASZzAB4N6PSfsOwzAhxYyQ"],
["g9m6oj9JvnE", "RDAOjWghT6s3mcT8SVl7jgbCXw"],
]

const expect = (data: any, type: ZodType) => {
const result = type.safeParse(data)

Expand All @@ -39,7 +50,7 @@ const expect = (data: any, type: ZodType) => {
const ytmusic = new YTMusic()
beforeAll(() => ytmusic.initialize())

queries.forEach(query => {
queries.forEach((query, index) => {
describe("Query: " + query, () => {
it("Search suggestions", async () => {
const suggestions = await ytmusic.getSearchSuggestions(query)
Expand Down Expand Up @@ -76,6 +87,11 @@ queries.forEach(query => {
expect(results, z.array(SearchResult))
})

it("Get Next", async () => {
const results = await ytmusic.getNext(...nextQueries[index]!)
expect(results, z.array(NextResult))
})

it("Get lyrics of the first song result", async () => {
const songs = await ytmusic.searchSongs(query)
const lyrics = await ytmusic.getLyrics(songs[0]!.videoId)
Expand Down
20 changes: 20 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export const SongDetailed = z
album: z.nullable(AlbumBasic),
duration: z.nullable(z.number()),
thumbnails: z.array(ThumbnailFull),
// for unavailable content it will be null
listId: z.nullable(z.string()),
params: z.nullable(z.string()),
})
.strict()

Expand All @@ -47,6 +50,9 @@ export const VideoDetailed = z
artist: ArtistBasic,
duration: z.nullable(z.number()),
thumbnails: z.array(ThumbnailFull),
// for unavailable content it will be null
listId: z.nullable(z.string()),
params: z.nullable(z.string()),
})
.strict()

Expand Down Expand Up @@ -172,3 +178,17 @@ export const HomeSection = z
contents: z.array(z.union([AlbumDetailed, PlaylistDetailed, SongDetailed])),
})
.strict()

export type NextResult = z.infer<typeof NextResult>
export const NextResult = z
.object({
index: z.number(),
name: z.string(),
artist: ArtistBasic,
playlistId: z.string(),
videoId: z.string(),
selected: z.boolean(),
params: z.string(),
thumbnails: z.array(ThumbnailFull),
})
.strict()
15 changes: 6 additions & 9 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@
"target": "esnext",
"lib": ["esnext"],
"module": "esnext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,

/** Project */
"rootDir": "src",
"outDir": "dist/esm",
"outDir": "dist",
// "typeRoots": ["./node_modules/@types", "./src/@types"],
"types": ["bun-types"],
"declaration": true,
"declarationDir": "dist/types",

/** Type Checking */
"strict": true,
Expand All @@ -30,16 +29,14 @@
"allowUnusedLabels": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"exactOptionalPropertyTypes": true,

/** Other */
// "noEmit": false,
// "noEmit": true,
// "allowJs": true,
// "jsx": "react-jsx",
"skipLibCheck": true
"skipLibCheck": true,
// "isolatedModules": true,
// "incremental": true
},
"include": ["src"],
"exclude": ["src/tests"]
"include": ["src"]
}
Loading