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 const ArtistBasic = type({
artistId: "string",
artistId: "string|null",
name: "string",
})
@ -120,7 +120,6 @@ export const AlbumFull = type({
artists: [ArtistBasic, "[]"],
year: "number|null",
thumbnails: [ThumbnailFull, "[]"],
description: "string",
songs: [SongDetailed, "[]"],
})

View File

@ -360,7 +360,12 @@ export default class YTMusic {
const lyricsData = await this.constructRequest("browse", { browseId })
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,
name: traverseString(data, "header", "title", "text")(),
}
const artists: ArtistBasic[] = traverseList(data, "header", "subtitle", "runs")
.filter(run => "navigationEndpoint" in run)
.map(run => ({
artistId: traverseString(run, "browseId")(),
name: traverseString(run, "text")(),
}))
const thumbnails = traverseList(data, "header", "thumbnails")
return checkType(
@ -28,7 +30,6 @@ export default class AlbumParser {
traverseString(data, "header", "subtitle", "text")(-1),
),
thumbnails,
description: traverseString(data, "description", "text")(),
songs: traverseList(data, "musicResponsiveListItemRenderer").map(item =>
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 traverseList from "../utils/traverseList"
import traverseString from "../utils/traverseString"
@ -9,7 +9,7 @@ import VideoParser from "./VideoParser"
export default class ArtistParser {
public static parse(data: any, artistId: string): ArtistFull {
const artistBasic: ArtistBasic = {
const artistBasic = {
artistId,
name: traverseString(data, "header", "title", "text")(),
}
@ -58,13 +58,16 @@ export default class ArtistParser {
}
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(
{
type: "ARTIST",
artistId: traverseString(item, "browseId")(),
name: traverseString(flexColumns[0], "runs", "text")(),
name: traverseString(title, "runs", "text")(),
thumbnails: traverseList(item, "thumbnails"),
},
ArtistDetailed,

View File

@ -1,18 +1,22 @@
import { PlaylistDetailed, PlaylistFull } from "../@types/types"
import checkType from "../utils/checkType"
import { isArtist } from "../utils/filters"
import traverse from "../utils/traverse"
import traverseList from "../utils/traverseList"
import traverseString from "../utils/traverseString"
export default class PlaylistParser {
public static parse(data: any, playlistId: string): PlaylistFull {
const artist = traverse(data, "header", "subtitle")
return checkType(
{
type: "PLAYLIST",
playlistId,
name: traverseString(data, "header", "title", "text")(),
artist: {
artistId: traverseString(data, "header", "subtitle", "browseId")(),
name: traverseString(data, "header", "subtitle", "text")(2),
name: traverseString(artist, "text")(),
artistId: traverseString(artist, "browseId")(),
},
videoCount:
+traverseList(data, "header", "secondSubtitle", "text")
@ -27,16 +31,21 @@ export default class PlaylistParser {
}
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(
{
type: "PLAYLIST",
playlistId: traverseString(item, "overlay", "playlistId")(),
name: traverseString(flexColumns[0], "runs", "text")(),
name: traverseString(title, "text")(),
artist: {
artistId: traverseString(flexColumns[1], "browseId")(),
name: traverseString(flexColumns[1], "runs", "text")(-3),
name: artist ? traverseString(artist, "text")() : "YouTube Music",
artistId: traverseString(artist, "browseId")() || null,
},
thumbnails: traverseList(item, "thumbnails"),
},

View File

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

View File

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

View File

@ -21,8 +21,14 @@ const errors: Problem[] = []
const queries = ["Lilac", "Weekend", "Eill", "Eminem", "Lisa Hannigan"]
const expect = (data: any, type: Type) => {
const result = type(data)
if (!result.data && result.problems?.length) {
if (result.problems?.length) {
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)
}

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}/)
}