mostly complete testing, preparing for data type change

This commit is contained in:
zS1L3NT Mac 2023-12-27 23:25:22 +08:00
parent 2178137c29
commit dd06c5ac65
No known key found for this signature in database
GPG Key ID: 02BE07CD431E4F42
9 changed files with 140 additions and 82 deletions

View File

@ -9,7 +9,7 @@ export const ThumbnailFull = type({
export type ArtistBasic = typeof ArtistBasic.infer export type ArtistBasic = typeof ArtistBasic.infer
export const ArtistBasic = type({ export const ArtistBasic = type({
artistId: "string", artistId: "string|null",
name: "string", name: "string",
}) })
@ -120,7 +120,6 @@ export const AlbumFull = type({
artists: [ArtistBasic, "[]"], artists: [ArtistBasic, "[]"],
year: "number|null", year: "number|null",
thumbnails: [ThumbnailFull, "[]"], thumbnails: [ThumbnailFull, "[]"],
description: "string",
songs: [SongDetailed, "[]"], songs: [SongDetailed, "[]"],
}) })

View File

@ -360,7 +360,12 @@ export default class YTMusic {
const lyricsData = await this.constructRequest("browse", { browseId }) const lyricsData = await this.constructRequest("browse", { browseId })
const lyrics = traverseString(lyricsData, "description", "runs", "text")() const lyrics = traverseString(lyricsData, "description", "runs", "text")()
return lyrics ? lyrics.replaceAll("\r", "").split("\n") : null return lyrics
? lyrics
.replaceAll("\r", "")
.split("\n")
.filter(v => !!v)
: null
} }
/** /**

View File

@ -10,12 +10,14 @@ export default class AlbumParser {
albumId, albumId,
name: traverseString(data, "header", "title", "text")(), name: traverseString(data, "header", "title", "text")(),
} }
const artists: ArtistBasic[] = traverseList(data, "header", "subtitle", "runs") const artists: ArtistBasic[] = traverseList(data, "header", "subtitle", "runs")
.filter(run => "navigationEndpoint" in run) .filter(run => "navigationEndpoint" in run)
.map(run => ({ .map(run => ({
artistId: traverseString(run, "browseId")(), artistId: traverseString(run, "browseId")(),
name: traverseString(run, "text")(), name: traverseString(run, "text")(),
})) }))
const thumbnails = traverseList(data, "header", "thumbnails") const thumbnails = traverseList(data, "header", "thumbnails")
return checkType( return checkType(
@ -28,7 +30,6 @@ export default class AlbumParser {
traverseString(data, "header", "subtitle", "text")(-1), traverseString(data, "header", "subtitle", "text")(-1),
), ),
thumbnails, thumbnails,
description: traverseString(data, "description", "text")(),
songs: traverseList(data, "musicResponsiveListItemRenderer").map(item => songs: traverseList(data, "musicResponsiveListItemRenderer").map(item =>
SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails), SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails),
), ),

View File

@ -1,4 +1,4 @@
import { ArtistBasic, ArtistDetailed, ArtistFull } from "../@types/types" import { ArtistDetailed, ArtistFull } from "../@types/types"
import checkType from "../utils/checkType" import checkType from "../utils/checkType"
import traverseList from "../utils/traverseList" import traverseList from "../utils/traverseList"
import traverseString from "../utils/traverseString" import traverseString from "../utils/traverseString"
@ -9,7 +9,7 @@ import VideoParser from "./VideoParser"
export default class ArtistParser { export default class ArtistParser {
public static parse(data: any, artistId: string): ArtistFull { public static parse(data: any, artistId: string): ArtistFull {
const artistBasic: ArtistBasic = { const artistBasic = {
artistId, artistId,
name: traverseString(data, "header", "title", "text")(), name: traverseString(data, "header", "title", "text")(),
} }
@ -58,13 +58,16 @@ export default class ArtistParser {
} }
public static parseSearchResult(item: any): ArtistDetailed { public static parseSearchResult(item: any): ArtistDetailed {
const flexColumns = traverseList(item, "flexColumns") const columns = traverseList(item, "flexColumns")
// No specific way to identify the title
const title = columns[0]
return checkType( return checkType(
{ {
type: "ARTIST", type: "ARTIST",
artistId: traverseString(item, "browseId")(), artistId: traverseString(item, "browseId")(),
name: traverseString(flexColumns[0], "runs", "text")(), name: traverseString(title, "runs", "text")(),
thumbnails: traverseList(item, "thumbnails"), thumbnails: traverseList(item, "thumbnails"),
}, },
ArtistDetailed, ArtistDetailed,

View File

@ -1,18 +1,22 @@
import { PlaylistDetailed, PlaylistFull } from "../@types/types" import { PlaylistDetailed, PlaylistFull } from "../@types/types"
import checkType from "../utils/checkType" import checkType from "../utils/checkType"
import { isArtist } from "../utils/filters"
import traverse from "../utils/traverse"
import traverseList from "../utils/traverseList" import traverseList from "../utils/traverseList"
import traverseString from "../utils/traverseString" import traverseString from "../utils/traverseString"
export default class PlaylistParser { export default class PlaylistParser {
public static parse(data: any, playlistId: string): PlaylistFull { public static parse(data: any, playlistId: string): PlaylistFull {
const artist = traverse(data, "header", "subtitle")
return checkType( return checkType(
{ {
type: "PLAYLIST", type: "PLAYLIST",
playlistId, playlistId,
name: traverseString(data, "header", "title", "text")(), name: traverseString(data, "header", "title", "text")(),
artist: { artist: {
artistId: traverseString(data, "header", "subtitle", "browseId")(), name: traverseString(artist, "text")(),
name: traverseString(data, "header", "subtitle", "text")(2), artistId: traverseString(artist, "browseId")(),
}, },
videoCount: videoCount:
+traverseList(data, "header", "secondSubtitle", "text") +traverseList(data, "header", "secondSubtitle", "text")
@ -27,16 +31,21 @@ export default class PlaylistParser {
} }
public static parseSearchResult(item: any): PlaylistDetailed { public static parseSearchResult(item: any): PlaylistDetailed {
const flexColumns = traverseList(item, "flexColumns") const columns = traverseList(item, "flexColumns", "runs").flat()
// No specific way to identify the title
const title = columns[0]
// Possibility to be empty because it's by YouTube Music
const artist = columns.find(isArtist)
return checkType( return checkType(
{ {
type: "PLAYLIST", type: "PLAYLIST",
playlistId: traverseString(item, "overlay", "playlistId")(), playlistId: traverseString(item, "overlay", "playlistId")(),
name: traverseString(flexColumns[0], "runs", "text")(), name: traverseString(title, "text")(),
artist: { artist: {
artistId: traverseString(flexColumns[1], "browseId")(), name: artist ? traverseString(artist, "text")() : "YouTube Music",
name: traverseString(flexColumns[1], "runs", "text")(-3), artistId: traverseString(artist, "browseId")() || null,
}, },
thumbnails: traverseList(item, "thumbnails"), thumbnails: traverseList(item, "thumbnails"),
}, },

View File

@ -1,5 +1,6 @@
import { AlbumBasic, ArtistBasic, SongDetailed, SongFull, ThumbnailFull } from "../@types/types" import { AlbumBasic, ArtistBasic, SongDetailed, SongFull, ThumbnailFull } from "../@types/types"
import checkType from "../utils/checkType" import checkType from "../utils/checkType"
import { isAlbum, isArtist, isDuration, isTitle } from "../utils/filters"
import traverseList from "../utils/traverseList" import traverseList from "../utils/traverseList"
import traverseString from "../utils/traverseString" import traverseString from "../utils/traverseString"
import Parser from "./Parser" import Parser from "./Parser"
@ -28,25 +29,29 @@ export default class SongParser {
} }
public static parseSearchResult(item: any): SongDetailed { public static parseSearchResult(item: any): SongDetailed {
const flexColumns = traverseList(item, "flexColumns") const columns = traverseList(item, "flexColumns", "runs").flat()
const title = columns.find(isTitle)
const artist = columns.find(isArtist)
const album = columns.find(isAlbum)
const duration = columns.find(isDuration)
return checkType( return checkType(
{ {
type: "SONG", type: "SONG",
videoId: traverseString(item, "playlistItemData", "videoId")(), videoId: traverseString(item, "playlistItemData", "videoId")(),
name: traverseString(flexColumns[0], "runs", "text")(), name: traverseString(title, "text")(),
artists: traverseList(flexColumns[1], "runs") artists: [
.filter(run => "navigationEndpoint" in run) {
.map(run => ({ name: traverseString(artist, "text")(),
artistId: traverseString(run, "browseId")(), artistId: traverseString(artist, "browseId")(),
name: traverseString(run, "text")(),
}))
.slice(0, -1),
album: {
albumId: traverseString(flexColumns[1], "runs", "browseId")(-1),
name: traverseString(flexColumns[1], "runs", "text")(-3),
}, },
duration: Parser.parseDuration(traverseString(flexColumns[1], "runs", "text")(-1)), ],
album: {
name: traverseString(album, "text")(),
albumId: traverseString(album, "browseId")(),
},
duration: Parser.parseDuration(duration.text),
thumbnails: traverseList(item, "thumbnails"), thumbnails: traverseList(item, "thumbnails"),
}, },
SongDetailed, SongDetailed,
@ -54,27 +59,29 @@ export default class SongParser {
} }
public static parseArtistSong(item: any): SongDetailed { public static parseArtistSong(item: any): SongDetailed {
const flexColumns = traverseList(item, "flexColumns") const columns = traverseList(item, "flexColumns", "runs").flat()
const videoId = traverseString(item, "playlistItemData", "videoId")()
const title = columns.find(isTitle)
const artist = columns.find(isArtist)
const album = columns.find(isAlbum)
const duration = columns.find(isDuration)
return checkType( return checkType(
{ {
type: "SONG", type: "SONG",
videoId, videoId: traverseString(item, "playlistItemData", "videoId")(),
name: traverseString(flexColumns[0], "runs", "text")(), name: traverseString(title, "text")(),
artists: traverseList(flexColumns[1], "runs") artists: [
.filter(run => "navigationEndpoint" in run) {
.map(run => ({ name: traverseString(artist, "text")(),
artistId: traverseString(run, "browseId")(), artistId: traverseString(artist, "browseId")(),
name: traverseString(run, "text")(),
})),
album: {
albumId: traverseString(flexColumns[2], "browseId")(),
name: traverseString(flexColumns[2], "runs", "text")(),
}, },
duration: Parser.parseDuration( ],
traverseString(item, "fixedColumns", "runs", "text")(), album: {
), name: traverseString(album, "text")(),
albumId: traverseString(album, "browseId")(),
},
duration: duration ? Parser.parseDuration(duration.text) : null,
thumbnails: traverseList(item, "thumbnails"), thumbnails: traverseList(item, "thumbnails"),
}, },
SongDetailed, SongDetailed,
@ -82,18 +89,20 @@ export default class SongParser {
} }
public static parseArtistTopSong(item: any, artistBasic: ArtistBasic): SongDetailed { public static parseArtistTopSong(item: any, artistBasic: ArtistBasic): SongDetailed {
const flexColumns = traverseList(item, "flexColumns") const columns = traverseList(item, "flexColumns", "runs").flat()
const videoId = traverseString(item, "playlistItemData", "videoId")()
const title = columns.find(isTitle)
const album = columns.find(isAlbum)
return checkType( return checkType(
{ {
type: "SONG", type: "SONG",
videoId, videoId: traverseString(item, "playlistItemData", "videoId")(),
name: traverseString(flexColumns[0], "runs", "text")(), name: traverseString(title, "text")(),
artists: [artistBasic], artists: [artistBasic],
album: { album: {
albumId: traverseString(flexColumns[2], "browseId")(), name: traverseString(album, "text")(),
name: traverseString(flexColumns[2], "runs", "text")(), albumId: traverseString(album, "browseId")(),
}, },
duration: null, duration: null,
thumbnails: traverseList(item, "thumbnails"), thumbnails: traverseList(item, "thumbnails"),
@ -108,19 +117,19 @@ export default class SongParser {
albumBasic: AlbumBasic, albumBasic: AlbumBasic,
thumbnails: ThumbnailFull[], thumbnails: ThumbnailFull[],
): SongDetailed { ): SongDetailed {
const flexColumns = traverseList(item, "flexColumns") const columns = traverseList(item, "flexColumns", "runs").flat()
const videoId = traverseString(item, "playlistItemData", "videoId")()
const title = columns.find(isTitle)
const duration = columns.find(isDuration)
return checkType( return checkType(
{ {
type: "SONG", type: "SONG",
videoId, videoId: traverseString(item, "playlistItemData", "videoId")(),
name: traverseString(flexColumns[0], "runs", "text")(), name: traverseString(title, "text")(),
artists, artists,
album: albumBasic, album: albumBasic,
duration: Parser.parseDuration( duration: duration ? Parser.parseDuration(duration.text) : null,
traverseString(item, "fixedColumns", "runs", "text")(),
),
thumbnails, thumbnails,
}, },
SongDetailed, SongDetailed,

View File

@ -1,5 +1,6 @@
import { ArtistBasic, VideoDetailed, VideoFull } from "../@types/types" import { ArtistBasic, VideoDetailed, VideoFull } from "../@types/types"
import checkType from "../utils/checkType" import checkType from "../utils/checkType"
import { isArtist, isDuration, isTitle } from "../utils/filters"
import traverse from "../utils/traverse" import traverse from "../utils/traverse"
import traverseList from "../utils/traverseList" import traverseList from "../utils/traverseList"
import traverseString from "../utils/traverseString" import traverseString from "../utils/traverseString"
@ -28,20 +29,23 @@ export default class VideoParser {
} }
public static parseSearchResult(item: any): VideoDetailed { public static parseSearchResult(item: any): VideoDetailed {
const flexColumns = traverseList(item, "flexColumns") const columns = traverseList(item, "flexColumns", "runs").flat()
const videoId = traverseString(item, "playNavigationEndpoint", "videoId")()
const title = columns.find(isTitle)
const artist = columns.find(isArtist)
const duration = columns.find(isDuration)
return { return {
type: "VIDEO", type: "VIDEO",
videoId, videoId: traverseString(item, "playNavigationEndpoint", "videoId")(),
name: traverseString(flexColumns[0], "runs", "text")(), name: traverseString(title, "text")(),
artists: traverseList(flexColumns[1], "runs") artists: [
.filter(run => "navigationEndpoint" in run) {
.map(run => ({ name: traverseString(artist, "text")(),
artistId: traverseString(run, "browseId")(), artistId: traverseString(artist, "browseId")(),
name: traverseString(run, "text")(), },
})), ],
duration: Parser.parseDuration(traverseString(flexColumns[1], "text")(-1)), duration: Parser.parseDuration(duration.text),
thumbnails: traverseList(item, "thumbnails"), thumbnails: traverseList(item, "thumbnails"),
} }
} }
@ -58,25 +62,28 @@ export default class VideoParser {
} }
public static parsePlaylistVideo(item: any): VideoDetailed { public static parsePlaylistVideo(item: any): VideoDetailed {
const flexColumns = traverseList(item, "flexColumns") const columns = traverseList(item, "flexColumns", "runs").flat()
const videoId =
traverseString(item, "playNavigationEndpoint", "videoId")() || const title = columns.find(isTitle) || columns[0]
traverseList(item, "thumbnails")[0].url.match(/https:\/\/i\.ytimg\.com\/vi\/(.+)\//)[1] const artist = columns.find(isArtist) || columns[1]
const duration = columns.find(isDuration)
return checkType( return checkType(
{ {
type: "VIDEO", type: "VIDEO",
videoId, videoId:
name: traverseString(flexColumns[0], "runs", "text")(), traverseString(item, "playNavigationEndpoint", "videoId")() ||
artists: traverseList(flexColumns[1], "runs") traverseList(item, "thumbnails")[0].url.match(
.filter(run => "navigationEndpoint" in run) /https:\/\/i\.ytimg\.com\/vi\/(.+)\//,
.map(run => ({ )[1],
artistId: traverseString(run, "browseId")(), name: traverseString(title, "text")(),
name: traverseString(run, "text")(), artists: [
})), {
duration: Parser.parseDuration( name: traverseString(artist, "text")(),
traverseString(item, "fixedColumns", "runs", "text")(), artistId: traverseString(artist, "browseId")() || null,
), },
],
duration: duration ? Parser.parseDuration(duration.text) : null,
thumbnails: traverseList(item, "thumbnails"), thumbnails: traverseList(item, "thumbnails"),
}, },
VideoDetailed, VideoDetailed,

View File

@ -21,8 +21,14 @@ const errors: Problem[] = []
const queries = ["Lilac", "Weekend", "Eill", "Eminem", "Lisa Hannigan"] const queries = ["Lilac", "Weekend", "Eill", "Eminem", "Lisa Hannigan"]
const expect = (data: any, type: Type) => { const expect = (data: any, type: Type) => {
const result = type(data) const result = type(data)
if (!result.data && result.problems?.length) { if (result.problems?.length) {
errors.push(...result.problems!) errors.push(...result.problems!)
} else {
const empty = JSON.stringify(result.data).match(/"\w+":""/g)
if (empty) {
console.log(result.data, empty)
}
equal(empty, null)
} }
equal(result.problems, undefined) equal(result.problems, undefined)
} }

19
src/utils/filters.ts Normal file
View File

@ -0,0 +1,19 @@
import traverseString from "./traverseString"
export const isTitle = (data: any) => {
return traverseString(data, "musicVideoType")().startsWith("MUSIC_VIDEO_TYPE_")
}
export const isArtist = (data: any) => {
return ["MUSIC_PAGE_TYPE_USER_CHANNEL", "MUSIC_PAGE_TYPE_ARTIST"].includes(
traverseString(data, "pageType")(),
)
}
export const isAlbum = (data: any) => {
return traverseString(data, "pageType")() === "MUSIC_PAGE_TYPE_ALBUM"
}
export const isDuration = (data: any) => {
return traverseString(data, "text")().match(/(\d{1,2}:)?\d{1,2}:\d{1,2}/)
}