More type safe traversing

This commit is contained in:
Zechariah 2022-03-28 01:02:09 +08:00
parent 12f3cd9d70
commit dea7d8883b
10 changed files with 195 additions and 158 deletions

View File

@ -8,7 +8,7 @@ export interface ThumbnailFull {
export interface SongDetailed { export interface SongDetailed {
type: "SONG" type: "SONG"
videoId: string | null videoId: string
name: string name: string
artists: ArtistBasic[] artists: ArtistBasic[]
album: AlbumBasic album: AlbumBasic
@ -24,7 +24,7 @@ export interface SongFull extends Omit<SongDetailed, "album"> {
export interface VideoDetailed { export interface VideoDetailed {
type: "VIDEO" type: "VIDEO"
videoId: string | null videoId: string
name: string name: string
artists: ArtistBasic[] artists: ArtistBasic[]
views: number views: number
@ -41,7 +41,7 @@ export interface VideoFull extends VideoDetailed {
} }
export interface ArtistBasic { export interface ArtistBasic {
artistId: string | null artistId: string
name: string name: string
} }
@ -52,7 +52,7 @@ export interface ArtistDetailed extends ArtistBasic {
} }
export interface ArtistFull extends ArtistDetailed { export interface ArtistFull extends ArtistDetailed {
description: string | null description: string
subscribers: number subscribers: number
topSongs: Omit<SongDetailed, "duration">[] topSongs: Omit<SongDetailed, "duration">[]
topAlbums: AlbumDetailed[] topAlbums: AlbumDetailed[]
@ -72,7 +72,7 @@ export interface AlbumDetailed extends AlbumBasic {
} }
export interface AlbumFull extends AlbumDetailed { export interface AlbumFull extends AlbumDetailed {
description: string | null description: string
songs: SongDetailed[] songs: SongDetailed[]
} }

View File

@ -22,7 +22,7 @@ export const THUMBNAIL_FULL: ObjectValidator<ThumbnailFull> = OBJECT({
}) })
export const ARTIST_BASIC: ObjectValidator<ArtistBasic> = OBJECT({ export const ARTIST_BASIC: ObjectValidator<ArtistBasic> = OBJECT({
artistId: OR(STRING(), NULL()), artistId: STRING(),
name: STRING() name: STRING()
}) })
@ -33,7 +33,7 @@ export const ALBUM_BASIC: ObjectValidator<AlbumBasic> = OBJECT({
export const SONG_DETAILED: ObjectValidator<SongDetailed> = OBJECT({ export const SONG_DETAILED: ObjectValidator<SongDetailed> = OBJECT({
type: STRING("SONG"), type: STRING("SONG"),
videoId: OR(STRING(), NULL()), videoId: STRING(),
name: STRING(), name: STRING(),
artists: LIST(ARTIST_BASIC), artists: LIST(ARTIST_BASIC),
album: ALBUM_BASIC, album: ALBUM_BASIC,
@ -43,7 +43,7 @@ export const SONG_DETAILED: ObjectValidator<SongDetailed> = OBJECT({
export const VIDEO_DETAILED: ObjectValidator<VideoDetailed> = OBJECT({ export const VIDEO_DETAILED: ObjectValidator<VideoDetailed> = OBJECT({
type: STRING("VIDEO"), type: STRING("VIDEO"),
videoId: OR(STRING(), NULL()), videoId: STRING(),
name: STRING(), name: STRING(),
artists: LIST(ARTIST_BASIC), artists: LIST(ARTIST_BASIC),
views: NUMBER(), views: NUMBER(),
@ -70,7 +70,7 @@ export const ALBUM_DETAILED: ObjectValidator<AlbumDetailed> = OBJECT({
export const SONG_FULL: ObjectValidator<SongFull> = OBJECT({ export const SONG_FULL: ObjectValidator<SongFull> = OBJECT({
type: STRING("SONG"), type: STRING("SONG"),
videoId: OR(STRING(), NULL()), videoId: STRING(),
name: STRING(), name: STRING(),
artists: LIST(ARTIST_BASIC), artists: LIST(ARTIST_BASIC),
duration: NUMBER(), duration: NUMBER(),
@ -82,7 +82,7 @@ export const SONG_FULL: ObjectValidator<SongFull> = OBJECT({
export const VIDEO_FULL: ObjectValidator<VideoFull> = OBJECT({ export const VIDEO_FULL: ObjectValidator<VideoFull> = OBJECT({
type: STRING("VIDEO"), type: STRING("VIDEO"),
videoId: OR(STRING(), NULL()), videoId: STRING(),
name: STRING(), name: STRING(),
artists: LIST(ARTIST_BASIC), artists: LIST(ARTIST_BASIC),
views: NUMBER(), views: NUMBER(),
@ -100,7 +100,7 @@ export const ARTIST_FULL: ObjectValidator<ArtistFull> = OBJECT({
name: STRING(), name: STRING(),
type: STRING("ARTIST"), type: STRING("ARTIST"),
thumbnails: LIST(THUMBNAIL_FULL), thumbnails: LIST(THUMBNAIL_FULL),
description: OR(STRING(), NULL()), description: STRING(),
subscribers: NUMBER(), subscribers: NUMBER(),
topSongs: LIST( topSongs: LIST(
OBJECT({ OBJECT({
@ -123,7 +123,7 @@ export const ALBUM_FULL: ObjectValidator<AlbumFull> = OBJECT({
artists: LIST(ARTIST_BASIC), artists: LIST(ARTIST_BASIC),
year: NUMBER(), year: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL), thumbnails: LIST(THUMBNAIL_FULL),
description: OR(STRING(), NULL()), description: STRING(),
songs: LIST(SONG_DETAILED) songs: LIST(SONG_DETAILED)
}) })
@ -138,7 +138,7 @@ export const PLAYLIST_FULL: ObjectValidator<PlaylistFull> = OBJECT({
export const PLAYLIST_VIDEO: ObjectValidator<Omit<VideoDetailed, "views">> = OBJECT({ export const PLAYLIST_VIDEO: ObjectValidator<Omit<VideoDetailed, "views">> = OBJECT({
type: STRING("VIDEO"), type: STRING("VIDEO"),
videoId: OR(STRING(), NULL()), videoId: STRING(),
name: STRING(), name: STRING(),
artists: LIST(ARTIST_BASIC), artists: LIST(ARTIST_BASIC),
duration: NUMBER(), duration: NUMBER(),

View File

@ -1,33 +1,34 @@
import checkType from "../utils/checkType" import checkType from "../utils/checkType"
import SongParser from "./SongParser" import SongParser from "./SongParser"
import traverse from "../utils/traverse" import traverseList from "../utils/traverseList"
import traverseString from "../utils/traverseString"
import { ALBUM_DETAILED, ALBUM_FULL } from "../interfaces" import { ALBUM_DETAILED, ALBUM_FULL } from "../interfaces"
import { AlbumDetailed, AlbumFull, ArtistBasic } from ".." import { AlbumBasic, AlbumDetailed, AlbumFull, ArtistBasic } from ".."
export default class AlbumParser { export default class AlbumParser {
public static parse(data: any, albumId: string): AlbumFull { public static parse(data: any, albumId: string): AlbumFull {
const albumBasic = { const albumBasic: AlbumBasic = {
albumId, albumId,
name: traverse(data, "header", "title", "text").at(0) name: traverseString(data, "header", "title", "text")()
} }
const artists = traverse(data, "header", "subtitle", "runs") const artists: ArtistBasic[] = traverseList(data, "header", "subtitle", "runs")
.filter((run: any) => "navigationEndpoint" in run) .filter(run => "navigationEndpoint" in run)
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })) .map(run => ({
const thumbnails = [traverse(data, "header", "thumbnails")].flat() artistId: traverseString(run, "browseId")(),
const description = traverse(data, "description", "text") name: traverseString(run, "text")()
}))
const thumbnails = traverseList(data, "header", "thumbnails")
return checkType<AlbumFull>( return checkType<AlbumFull>(
{ {
type: "ALBUM", type: "ALBUM",
...albumBasic, ...albumBasic,
playlistId: traverse(data, "buttonRenderer", "playlistId"), playlistId: traverseString(data, "buttonRenderer", "playlistId")(),
artists, artists,
year: +traverse(data, "header", "subtitle", "text").at(-1), year: +traverseString(data, "header", "subtitle", "text")(-1),
thumbnails, thumbnails,
description: description instanceof Array ? null : description, description: traverseString(data, "description", "text")(),
songs: [traverse(data, "musicResponsiveListItemRenderer")] songs: traverseList(data, "musicResponsiveListItemRenderer").map(item =>
.flat()
.map((item: any) =>
SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails) SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails)
) )
}, },
@ -36,19 +37,22 @@ export default class AlbumParser {
} }
public static parseSearchResult(item: any): AlbumDetailed { public static parseSearchResult(item: any): AlbumDetailed {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverseList(item, "flexColumns")
return checkType<AlbumDetailed>( return checkType<AlbumDetailed>(
{ {
type: "ALBUM", type: "ALBUM",
albumId: [traverse(item, "browseId")].flat().at(-1), albumId: traverseString(item, "browseId")(-1),
playlistId: traverse(item, "overlay", "playlistId"), playlistId: traverseString(item, "overlay", "playlistId")(),
artists: traverse(flexColumns[1], "runs") artists: traverseList(flexColumns[1], "runs")
.filter((run: any) => "navigationEndpoint" in run) .filter(run => "navigationEndpoint" in run)
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })), .map(run => ({
name: traverse(flexColumns[0], "runs", "text"), artistId: traverseString(run, "browseId")(),
year: +traverse(flexColumns[1], "runs", "text").at(-1), name: traverseString(run, "text")()
thumbnails: [traverse(item, "thumbnails")].flat() })),
name: traverseString(flexColumns[0], "runs", "text")(),
year: +traverseString(flexColumns[1], "runs", "text")(-1),
thumbnails: traverseList(item, "thumbnails")
}, },
ALBUM_DETAILED ALBUM_DETAILED
) )
@ -58,12 +62,12 @@ export default class AlbumParser {
return checkType<AlbumDetailed>( return checkType<AlbumDetailed>(
{ {
type: "ALBUM", type: "ALBUM",
albumId: [traverse(item, "browseId")].flat().at(-1), albumId: traverseString(item, "browseId")(-1),
playlistId: traverse(item, "thumbnailOverlay", "playlistId"), playlistId: traverseString(item, "thumbnailOverlay", "playlistId")(),
name: traverse(item, "title", "text").at(0), name: traverseString(item, "title", "text")(),
artists: [artistBasic], artists: [artistBasic],
year: +traverse(item, "subtitle", "text").at(-1), year: +traverseString(item, "subtitle", "text")(-1),
thumbnails: [traverse(item, "thumbnails")].flat() thumbnails: traverseList(item, "thumbnails")
}, },
ALBUM_DETAILED ALBUM_DETAILED
) )
@ -73,12 +77,12 @@ export default class AlbumParser {
return checkType<AlbumDetailed>( return checkType<AlbumDetailed>(
{ {
type: "ALBUM", type: "ALBUM",
albumId: traverse(item, "browseId").at(-1), albumId: traverseString(item, "browseId")(-1),
playlistId: traverse(item, "musicPlayButtonRenderer", "playlistId"), playlistId: traverseString(item, "musicPlayButtonRenderer", "playlistId")(),
name: traverse(item, "title", "text").at(0), name: traverseString(item, "title", "text")(),
artists: [artistBasic], artists: [artistBasic],
year: +traverse(item, "subtitle", "text").at(-1), year: +traverseString(item, "subtitle", "text")(-1),
thumbnails: [traverse(item, "thumbnails")].flat() thumbnails: traverseList(item, "thumbnails")
}, },
ALBUM_DETAILED ALBUM_DETAILED
) )

View File

@ -2,31 +2,33 @@ import AlbumParser from "./AlbumParser"
import checkType from "../utils/checkType" import checkType from "../utils/checkType"
import Parser from "./Parser" import Parser from "./Parser"
import SongParser from "./SongParser" import SongParser from "./SongParser"
import traverse from "../utils/traverse" import traverseList from "../utils/traverseList"
import traverseString from "../utils/traverseString"
import { ARTIST_DETAILED, ARTIST_FULL } from "../interfaces" import { ARTIST_DETAILED, ARTIST_FULL } from "../interfaces"
import { ArtistDetailed, ArtistFull } from ".." import { ArtistBasic, ArtistDetailed, ArtistFull } from ".."
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 = { const artistBasic: ArtistBasic = {
artistId, artistId,
name: traverse(data, "header", "title", "text").at(0) name: traverseString(data, "header", "title", "text")()
} }
const description = traverse(data, "header", "description", "text") const description = traverseString(data, "header", "description", "text")()
return checkType<ArtistFull>( return checkType<ArtistFull>(
{ {
type: "ARTIST", type: "ARTIST",
...artistBasic, ...artistBasic,
thumbnails: [traverse(data, "header", "thumbnails")].flat(), thumbnails: traverseList(data, "header", "thumbnails"),
description: description instanceof Array ? null : description, description,
subscribers: Parser.parseNumber(traverse(data, "subscriberCountText", "text")), subscribers: Parser.parseNumber(
topSongs: traverse(data, "musicShelfRenderer", "contents").map((item: any) => traverseString(data, "subscriberCountText", "text")()
),
topSongs: traverseList(data, "musicShelfRenderer", "contents").map(item =>
SongParser.parseArtistTopSong(item, artistBasic) SongParser.parseArtistTopSong(item, artistBasic)
), ),
topAlbums: [traverse(data, "musicCarouselShelfRenderer")] topAlbums: traverseList(data, "musicCarouselShelfRenderer")
.flat()
.at(0) .at(0)
.contents.map((item: any) => .contents.map((item: any) =>
AlbumParser.parseArtistTopAlbums(item, artistBasic) AlbumParser.parseArtistTopAlbums(item, artistBasic)
@ -37,14 +39,14 @@ export default class ArtistParser {
} }
public static parseSearchResult(item: any): ArtistDetailed { public static parseSearchResult(item: any): ArtistDetailed {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverseList(item, "flexColumns")
return checkType<ArtistDetailed>( return checkType<ArtistDetailed>(
{ {
type: "ARTIST", type: "ARTIST",
artistId: traverse(item, "browseId"), artistId: traverseString(item, "browseId")(),
name: traverse(flexColumns[0], "runs", "text"), name: traverseString(flexColumns[0], "runs", "text")(),
thumbnails: [traverse(item, "thumbnails")].flat() thumbnails: traverseList(item, "thumbnails")
}, },
ARTIST_DETAILED ARTIST_DETAILED
) )

View File

@ -1,5 +1,6 @@
import checkType from "../utils/checkType" import checkType from "../utils/checkType"
import traverse from "../utils/traverse" import traverseList from "../utils/traverseList"
import traverseString from "../utils/traverseString"
import { PLAYLIST_FULL } from "../interfaces" import { PLAYLIST_FULL } from "../interfaces"
import { PlaylistFull } from ".." import { PlaylistFull } from ".."
@ -9,41 +10,41 @@ export default class PlaylistParser {
{ {
type: "PLAYLIST", type: "PLAYLIST",
playlistId, playlistId,
name: traverse(data, "header", "title", "text").at(0), name: traverseString(data, "header", "title", "text")(),
artist: { artist: {
artistId: traverse(data, "header", "subtitle", "browseId"), artistId: traverseString(data, "header", "subtitle", "browseId")(),
name: traverse(data, "header", "subtitle", "text").at(2) name: traverseString(data, "header", "subtitle", "text")(2)
}, },
videoCount: +traverse(data, "header", "secondSubtitle", "text") videoCount: +traverseList(data, "header", "secondSubtitle", "text")
.at(0) .at(0)
.split(" ") .split(" ")
.at(0) .at(0)
.replaceAll(",", ""), .replaceAll(",", ""),
thumbnails: traverse(data, "header", "thumbnails") thumbnails: traverseList(data, "header", "thumbnails")
}, },
PLAYLIST_FULL PLAYLIST_FULL
) )
} }
public static parseSearchResult(item: any): PlaylistFull { public static parseSearchResult(item: any): PlaylistFull {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverseList(item, "flexColumns")
const artistId = traverse(flexColumns[1], "browseId") const artistId = traverseString(flexColumns[1], "browseId")()
return checkType<PlaylistFull>( return checkType<PlaylistFull>(
{ {
type: "PLAYLIST", type: "PLAYLIST",
playlistId: traverse(item, "overlay", "playlistId"), playlistId: traverseString(item, "overlay", "playlistId")(),
name: traverse(flexColumns[0], "runs", "text"), name: traverseString(flexColumns[0], "runs", "text")(),
artist: { artist: {
artistId: artistId instanceof Array ? null : artistId, artistId,
name: traverse(flexColumns[1], "runs", "text").at(-2) name: traverseString(flexColumns[1], "runs", "text")(-2)
}, },
videoCount: +traverse(flexColumns[1], "runs", "text") videoCount: +traverseList(flexColumns[1], "runs", "text")
.at(-1) .at(-1)
.split(" ") .split(" ")
.at(0) .at(0)
.replaceAll(",", ""), .replaceAll(",", ""),
thumbnails: [traverse(item, "thumbnails")].flat() thumbnails: traverseList(item, "thumbnails")
}, },
PLAYLIST_FULL PLAYLIST_FULL
) )

View File

@ -2,14 +2,14 @@ import AlbumParser from "./AlbumParser"
import ArtistParser from "./ArtistParser" import ArtistParser from "./ArtistParser"
import PlaylistParser from "./PlaylistParser" import PlaylistParser from "./PlaylistParser"
import SongParser from "./SongParser" import SongParser from "./SongParser"
import traverse from "../utils/traverse" import traverseList from "../utils/traverseList"
import VideoParser from "./VideoParser" import VideoParser from "./VideoParser"
import { SearchResult } from ".." import { SearchResult } from ".."
export default class SearchParser { export default class SearchParser {
public static parse(item: any): SearchResult { public static parse(item: any): SearchResult {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverseList(item, "flexColumns")
const type = [traverse(flexColumns[1], "runs", "text")].flat().at(0) as const type = traverseList(flexColumns[1], "runs", "text").at(0) as
| "Song" | "Song"
| "Video" | "Video"
| "Artist" | "Artist"

View File

@ -1,6 +1,7 @@
import checkType from "../utils/checkType" import checkType from "../utils/checkType"
import Parser from "./Parser" import Parser from "./Parser"
import traverse from "../utils/traverse" import traverseList from "../utils/traverseList"
import traverseString from "../utils/traverseString"
import { ALBUM_BASIC, ARTIST_BASIC, SONG_DETAILED, SONG_FULL, THUMBNAIL_FULL } from "../interfaces" import { ALBUM_BASIC, ARTIST_BASIC, SONG_DETAILED, SONG_FULL, THUMBNAIL_FULL } from "../interfaces"
import { AlbumBasic, ArtistBasic, SongDetailed, SongFull, ThumbnailFull } from ".." import { AlbumBasic, ArtistBasic, SongDetailed, SongFull, ThumbnailFull } from ".."
import { LIST, OBJECT, STRING } from "validate-any" import { LIST, OBJECT, STRING } from "validate-any"
@ -10,70 +11,73 @@ export default class SongParser {
return checkType<SongFull>( return checkType<SongFull>(
{ {
type: "SONG", type: "SONG",
videoId: traverse(data, "videoDetails", "videoId"), videoId: traverseString(data, "videoDetails", "videoId")(),
name: traverse(data, "videoDetails", "title"), name: traverseString(data, "videoDetails", "title")(),
artists: [ artists: [
{ {
artistId: traverse(data, "videoDetails", "channelId"), artistId: traverseString(data, "videoDetails", "channelId")(),
name: traverse(data, "author") name: traverseString(data, "author")()
} }
], ],
duration: +traverse(data, "videoDetails", "lengthSeconds"), duration: +traverseString(data, "videoDetails", "lengthSeconds"),
thumbnails: [traverse(data, "videoDetails", "thumbnails")].flat(), thumbnails: traverseList(data, "videoDetails", "thumbnails"),
description: traverse(data, "description"), description: traverseString(data, "description")(),
formats: traverse(data, "streamingData", "formats"), formats: traverseList(data, "streamingData", "formats"),
adaptiveFormats: traverse(data, "streamingData", "adaptiveFormats") adaptiveFormats: traverseList(data, "streamingData", "adaptiveFormats")
}, },
SONG_FULL SONG_FULL
) )
} }
public static parseSearchResult(item: any): SongDetailed { public static parseSearchResult(item: any): SongDetailed {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverseList(item, "flexColumns")
const videoId = traverse(item, "playlistItemData", "videoId")
return checkType<SongDetailed>( return checkType<SongDetailed>(
{ {
type: "SONG", type: "SONG",
videoId: videoId instanceof Array ? null : videoId, videoId: traverseString(item, "playlistItemData", "videoId")(),
name: traverse(flexColumns[0], "runs", "text"), name: traverseString(flexColumns[0], "runs", "text")(),
artists: traverse(flexColumns[1], "runs") artists: traverseList(flexColumns[1], "runs")
.filter((run: any) => "navigationEndpoint" in run) .filter(run => "navigationEndpoint" in run)
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })) .map(run => ({
artistId: traverseString(run, "browseId")(),
name: traverseString(run, "text")()
}))
.slice(0, -1), .slice(0, -1),
album: { album: {
albumId: traverse(item, "browseId").at(-1), albumId: traverseString(item, "browseId")(-1),
name: traverse(flexColumns[1], "runs", "text").at(-3) name: traverseString(flexColumns[1], "runs", "text")(-3)
}, },
duration: Parser.parseDuration(traverse(flexColumns[1], "runs", "text").at(-1)), duration: Parser.parseDuration(traverseString(flexColumns[1], "runs", "text")(-1)),
thumbnails: [traverse(item, "thumbnails")].flat() thumbnails: traverseList(item, "thumbnails")
}, },
SONG_DETAILED SONG_DETAILED
) )
} }
public static parseArtistSong(item: any): SongDetailed { public static parseArtistSong(item: any): SongDetailed {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverseList(item, "flexColumns")
const videoId = traverse(item, "playlistItemData", "videoId") const videoId = traverseString(item, "playlistItemData", "videoId")()
return checkType<SongDetailed>( return checkType<SongDetailed>(
{ {
type: "SONG", type: "SONG",
videoId: videoId instanceof Array ? null : videoId, videoId,
name: traverse(flexColumns[0], "runs", "text"), name: traverseString(flexColumns[0], "runs", "text")(),
artists: [traverse(flexColumns[1], "runs")] artists: traverseList(flexColumns[1], "runs")
.flat() .filter(run => "navigationEndpoint" in run)
.filter((item: any) => "navigationEndpoint" in item) .map(run => ({
.map((run: any) => ({ artistId: traverseString(run, "browseId")(),
artistId: traverse(run, "browseId"), name: traverseString(run, "text")()
name: run.text
})), })),
album: { album: {
albumId: traverse(flexColumns[2], "browseId"), albumId: traverseString(flexColumns[2], "browseId")(),
name: traverse(flexColumns[2], "runs", "text") name: traverseString(flexColumns[2], "runs", "text")()
}, },
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")), duration: Parser.parseDuration(
thumbnails: [traverse(item, "thumbnails")].flat() traverseString(item, "fixedColumns", "runs", "text")()
),
thumbnails: traverseList(item, "thumbnails")
}, },
SONG_DETAILED SONG_DETAILED
) )
@ -83,20 +87,20 @@ export default class SongParser {
item: any, item: any,
artistBasic: ArtistBasic artistBasic: ArtistBasic
): Omit<SongDetailed, "duration"> { ): Omit<SongDetailed, "duration"> {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverseList(item, "flexColumns")
const videoId = traverse(item, "playlistItemData", "videoId") const videoId = traverseString(item, "playlistItemData", "videoId")()
return checkType<Omit<SongDetailed, "duration">>( return checkType<Omit<SongDetailed, "duration">>(
{ {
type: "SONG", type: "SONG",
videoId: videoId instanceof Array ? null : videoId, videoId,
name: traverse(flexColumns[0], "runs", "text"), name: traverseString(flexColumns[0], "runs", "text")(),
artists: [artistBasic], artists: [artistBasic],
album: { album: {
albumId: traverse(flexColumns[2], "runs", "text"), albumId: traverseString(flexColumns[2], "runs", "text")(),
name: traverse(flexColumns[2], "browseId") name: traverseString(flexColumns[2], "browseId")()
}, },
thumbnails: [traverse(item, "thumbnails")].flat() thumbnails: traverseList(item, "thumbnails")
}, },
OBJECT({ OBJECT({
type: STRING("SONG"), type: STRING("SONG"),
@ -115,17 +119,19 @@ export default class SongParser {
albumBasic: AlbumBasic, albumBasic: AlbumBasic,
thumbnails: ThumbnailFull[] thumbnails: ThumbnailFull[]
): SongDetailed { ): SongDetailed {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverseList(item, "flexColumns")
const videoId = traverse(item, "playlistItemData", "videoId") const videoId = traverseString(item, "playlistItemData", "videoId")()
return checkType<SongDetailed>( return checkType<SongDetailed>(
{ {
type: "SONG", type: "SONG",
videoId: videoId instanceof Array ? null : videoId, videoId,
name: traverse(flexColumns[0], "runs", "text"), name: traverseString(flexColumns[0], "runs", "text")(),
artists, artists,
album: albumBasic, album: albumBasic,
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")), duration: Parser.parseDuration(
traverseString(item, "fixedColumns", "runs", "text")()
),
thumbnails thumbnails
}, },
SONG_DETAILED SONG_DETAILED

View File

@ -1,6 +1,8 @@
import checkType from "../utils/checkType" import checkType from "../utils/checkType"
import Parser from "./Parser" import Parser from "./Parser"
import traverse from "../utils/traverse" import traverse from "../utils/traverse"
import traverseList from "../utils/traverseList"
import traverseString from "../utils/traverseString"
import { PLAYLIST_VIDEO } from "../interfaces" import { PLAYLIST_VIDEO } from "../interfaces"
import { VideoDetailed, VideoFull } from ".." import { VideoDetailed, VideoFull } from ".."
@ -8,58 +10,66 @@ export default class VideoParser {
public static parse(data: any): VideoFull { public static parse(data: any): VideoFull {
return { return {
type: "VIDEO", type: "VIDEO",
videoId: traverse(data, "videoDetails", "videoId"), videoId: traverseString(data, "videoDetails", "videoId")(),
name: traverse(data, "videoDetails", "title"), name: traverseString(data, "videoDetails", "title")(),
artists: [ artists: [
{ {
artistId: traverse(data, "videoDetails", "channelId"), artistId: traverseString(data, "videoDetails", "channelId")(),
name: traverse(data, "author") name: traverseString(data, "author")()
} }
], ],
views: +traverse(data, "videoDetails", "viewCount"), views: +traverseString(data, "videoDetails", "viewCount")(),
duration: +traverse(data, "videoDetails", "lengthSeconds"), duration: +traverseString(data, "videoDetails", "lengthSeconds")(),
thumbnails: [traverse(data, "videoDetails", "thumbnails")].flat(), thumbnails: traverseList(data, "videoDetails", "thumbnails"),
description: traverse(data, "description"), description: traverseString(data, "description")(),
unlisted: traverse(data, "unlisted"), unlisted: traverse(data, "unlisted"),
familySafe: traverse(data, "familySafe"), familySafe: traverse(data, "familySafe"),
paid: traverse(data, "paid"), paid: traverse(data, "paid"),
tags: traverse(data, "tags") tags: traverseList(data, "tags")
} }
} }
public static parseSearchResult(item: any): VideoDetailed { public static parseSearchResult(item: any): VideoDetailed {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverseList(item, "flexColumns")
const videoId = traverse(item, "playNavigationEndpoint", "videoId") const videoId = traverseString(item, "playNavigationEndpoint", "videoId")()
return { return {
type: "VIDEO", type: "VIDEO",
videoId: videoId instanceof Array ? null : videoId, videoId,
name: traverse(flexColumns[0], "runs", "text"), name: traverseString(flexColumns[0], "runs", "text")(),
artists: [traverse(flexColumns[1], "runs")] artists: traverseList(flexColumns[1], "runs")
.flat() .filter(run => "navigationEndpoint" in run)
.filter((run: any) => "navigationEndpoint" in run) .map(run => ({
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })), artistId: traverseString(run, "browseId")(),
views: Parser.parseNumber(traverse(flexColumns[1], "runs", "text").at(-3).slice(0, -6)), name: traverseString(run, "text")()
duration: Parser.parseDuration(traverse(flexColumns[1], "text").at(-1)), })),
thumbnails: [traverse(item, "thumbnails")].flat() views: Parser.parseNumber(
traverseString(flexColumns[1], "runs", "text")(-3).slice(0, -6)
),
duration: Parser.parseDuration(traverseString(flexColumns[1], "text")(-1)),
thumbnails: traverseList(item, "thumbnails")
} }
} }
public static parsePlaylistVideo(item: any): Omit<VideoDetailed, "views"> { public static parsePlaylistVideo(item: any): Omit<VideoDetailed, "views"> {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverseList(item, "flexColumns")
const videoId = traverse(item, "playNavigationEndpoint", "videoId") const videoId = traverseString(item, "playNavigationEndpoint", "videoId")()
return checkType<Omit<VideoDetailed, "views">>( return checkType<Omit<VideoDetailed, "views">>(
{ {
type: "VIDEO", type: "VIDEO",
videoId: videoId instanceof Array ? null : videoId, videoId,
name: traverse(flexColumns[0], "runs", "text"), name: traverseString(flexColumns[0], "runs", "text")(),
artists: [traverse(flexColumns[1], "runs")] artists: traverseList(flexColumns[1], "runs")
.flat() .filter(run => "navigationEndpoint" in run)
.filter((run: any) => "navigationEndpoint" in run) .map(run => ({
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })), artistId: traverseString(run, "browseId")(),
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")), name: traverseString(run, "text")()
thumbnails: [traverse(item, "thumbnails")].flat() })),
duration: Parser.parseDuration(
traverseString(item, "fixedColumns", "runs", "text")()
),
thumbnails: traverseList(item, "thumbnails")
}, },
PLAYLIST_VIDEO PLAYLIST_VIDEO
) )

View File

@ -0,0 +1,7 @@
import traverse from "./traverse"
export default (data: any, ...keys: string[]): any[] => {
const value = traverse(data, ...keys)
const flatValue = [value].flat()
return flatValue
}

View File

@ -0,0 +1,7 @@
import traverse from "./traverse"
export default (data: any, ...keys: string[]) => (index = 0): string => {
const value = traverse(data, ...keys)
const flatValue = [value].flat().at(index)
return flatValue || ""
}