✨ mostly complete testing, preparing for data type change
This commit is contained in:
parent
2178137c29
commit
dd06c5ac65
|
|
@ -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, "[]"],
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
name: traverseString(title, "text")(),
|
||||
artists: [
|
||||
{
|
||||
name: traverseString(artist, "text")(),
|
||||
artistId: traverseString(artist, "browseId")(),
|
||||
},
|
||||
],
|
||||
album: {
|
||||
albumId: traverseString(flexColumns[1], "runs", "browseId")(-1),
|
||||
name: traverseString(flexColumns[1], "runs", "text")(-3),
|
||||
name: traverseString(album, "text")(),
|
||||
albumId: traverseString(album, "browseId")(),
|
||||
},
|
||||
duration: Parser.parseDuration(traverseString(flexColumns[1], "runs", "text")(-1)),
|
||||
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")(),
|
||||
})),
|
||||
videoId: traverseString(item, "playlistItemData", "videoId")(),
|
||||
name: traverseString(title, "text")(),
|
||||
artists: [
|
||||
{
|
||||
name: traverseString(artist, "text")(),
|
||||
artistId: traverseString(artist, "browseId")(),
|
||||
},
|
||||
],
|
||||
album: {
|
||||
albumId: traverseString(flexColumns[2], "browseId")(),
|
||||
name: traverseString(flexColumns[2], "runs", "text")(),
|
||||
name: traverseString(album, "text")(),
|
||||
albumId: traverseString(album, "browseId")(),
|
||||
},
|
||||
duration: Parser.parseDuration(
|
||||
traverseString(item, "fixedColumns", "runs", "text")(),
|
||||
),
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}/)
|
||||
}
|
||||
Loading…
Reference in New Issue