From dea7d8883bf75010821cdc348ced438c01e70de6 Mon Sep 17 00:00:00 2001 From: Zechariah Date: Mon, 28 Mar 2022 01:02:09 +0800 Subject: [PATCH] More type safe traversing --- src/index.ts | 10 ++-- src/interfaces.ts | 16 +++--- src/parsers/AlbumParser.ts | 76 ++++++++++++++------------ src/parsers/ArtistParser.ts | 32 ++++++----- src/parsers/PlaylistParser.ts | 29 +++++----- src/parsers/SearchParser.ts | 6 +- src/parsers/SongParser.ts | 100 ++++++++++++++++++---------------- src/parsers/VideoParser.ts | 70 ++++++++++++++---------- src/utils/traverseList.ts | 7 +++ src/utils/traverseString.ts | 7 +++ 10 files changed, 195 insertions(+), 158 deletions(-) create mode 100644 src/utils/traverseList.ts create mode 100644 src/utils/traverseString.ts diff --git a/src/index.ts b/src/index.ts index 4e3180d..a758380 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,7 @@ export interface ThumbnailFull { export interface SongDetailed { type: "SONG" - videoId: string | null + videoId: string name: string artists: ArtistBasic[] album: AlbumBasic @@ -24,7 +24,7 @@ export interface SongFull extends Omit { export interface VideoDetailed { type: "VIDEO" - videoId: string | null + videoId: string name: string artists: ArtistBasic[] views: number @@ -41,7 +41,7 @@ export interface VideoFull extends VideoDetailed { } export interface ArtistBasic { - artistId: string | null + artistId: string name: string } @@ -52,7 +52,7 @@ export interface ArtistDetailed extends ArtistBasic { } export interface ArtistFull extends ArtistDetailed { - description: string | null + description: string subscribers: number topSongs: Omit[] topAlbums: AlbumDetailed[] @@ -72,7 +72,7 @@ export interface AlbumDetailed extends AlbumBasic { } export interface AlbumFull extends AlbumDetailed { - description: string | null + description: string songs: SongDetailed[] } diff --git a/src/interfaces.ts b/src/interfaces.ts index d9a4136..6b45aef 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -22,7 +22,7 @@ export const THUMBNAIL_FULL: ObjectValidator = OBJECT({ }) export const ARTIST_BASIC: ObjectValidator = OBJECT({ - artistId: OR(STRING(), NULL()), + artistId: STRING(), name: STRING() }) @@ -33,7 +33,7 @@ export const ALBUM_BASIC: ObjectValidator = OBJECT({ export const SONG_DETAILED: ObjectValidator = OBJECT({ type: STRING("SONG"), - videoId: OR(STRING(), NULL()), + videoId: STRING(), name: STRING(), artists: LIST(ARTIST_BASIC), album: ALBUM_BASIC, @@ -43,7 +43,7 @@ export const SONG_DETAILED: ObjectValidator = OBJECT({ export const VIDEO_DETAILED: ObjectValidator = OBJECT({ type: STRING("VIDEO"), - videoId: OR(STRING(), NULL()), + videoId: STRING(), name: STRING(), artists: LIST(ARTIST_BASIC), views: NUMBER(), @@ -70,7 +70,7 @@ export const ALBUM_DETAILED: ObjectValidator = OBJECT({ export const SONG_FULL: ObjectValidator = OBJECT({ type: STRING("SONG"), - videoId: OR(STRING(), NULL()), + videoId: STRING(), name: STRING(), artists: LIST(ARTIST_BASIC), duration: NUMBER(), @@ -82,7 +82,7 @@ export const SONG_FULL: ObjectValidator = OBJECT({ export const VIDEO_FULL: ObjectValidator = OBJECT({ type: STRING("VIDEO"), - videoId: OR(STRING(), NULL()), + videoId: STRING(), name: STRING(), artists: LIST(ARTIST_BASIC), views: NUMBER(), @@ -100,7 +100,7 @@ export const ARTIST_FULL: ObjectValidator = OBJECT({ name: STRING(), type: STRING("ARTIST"), thumbnails: LIST(THUMBNAIL_FULL), - description: OR(STRING(), NULL()), + description: STRING(), subscribers: NUMBER(), topSongs: LIST( OBJECT({ @@ -123,7 +123,7 @@ export const ALBUM_FULL: ObjectValidator = OBJECT({ artists: LIST(ARTIST_BASIC), year: NUMBER(), thumbnails: LIST(THUMBNAIL_FULL), - description: OR(STRING(), NULL()), + description: STRING(), songs: LIST(SONG_DETAILED) }) @@ -138,7 +138,7 @@ export const PLAYLIST_FULL: ObjectValidator = OBJECT({ export const PLAYLIST_VIDEO: ObjectValidator> = OBJECT({ type: STRING("VIDEO"), - videoId: OR(STRING(), NULL()), + videoId: STRING(), name: STRING(), artists: LIST(ARTIST_BASIC), duration: NUMBER(), diff --git a/src/parsers/AlbumParser.ts b/src/parsers/AlbumParser.ts index a60d723..ba5f1d5 100644 --- a/src/parsers/AlbumParser.ts +++ b/src/parsers/AlbumParser.ts @@ -1,54 +1,58 @@ import checkType from "../utils/checkType" 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 { AlbumDetailed, AlbumFull, ArtistBasic } from ".." +import { AlbumBasic, AlbumDetailed, AlbumFull, ArtistBasic } from ".." export default class AlbumParser { public static parse(data: any, albumId: string): AlbumFull { - const albumBasic = { + const albumBasic: AlbumBasic = { albumId, - name: traverse(data, "header", "title", "text").at(0) + name: traverseString(data, "header", "title", "text")() } - const artists = traverse(data, "header", "subtitle", "runs") - .filter((run: any) => "navigationEndpoint" in run) - .map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })) - const thumbnails = [traverse(data, "header", "thumbnails")].flat() - const description = traverse(data, "description", "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( { type: "ALBUM", ...albumBasic, - playlistId: traverse(data, "buttonRenderer", "playlistId"), + playlistId: traverseString(data, "buttonRenderer", "playlistId")(), artists, - year: +traverse(data, "header", "subtitle", "text").at(-1), + year: +traverseString(data, "header", "subtitle", "text")(-1), thumbnails, - description: description instanceof Array ? null : description, - songs: [traverse(data, "musicResponsiveListItemRenderer")] - .flat() - .map((item: any) => - SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails) - ) + description: traverseString(data, "description", "text")(), + songs: traverseList(data, "musicResponsiveListItemRenderer").map(item => + SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails) + ) }, ALBUM_FULL ) } public static parseSearchResult(item: any): AlbumDetailed { - const flexColumns = traverse(item, "flexColumns") + const flexColumns = traverseList(item, "flexColumns") return checkType( { type: "ALBUM", - albumId: [traverse(item, "browseId")].flat().at(-1), - playlistId: traverse(item, "overlay", "playlistId"), - artists: traverse(flexColumns[1], "runs") - .filter((run: any) => "navigationEndpoint" in run) - .map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })), - name: traverse(flexColumns[0], "runs", "text"), - year: +traverse(flexColumns[1], "runs", "text").at(-1), - thumbnails: [traverse(item, "thumbnails")].flat() + albumId: traverseString(item, "browseId")(-1), + playlistId: traverseString(item, "overlay", "playlistId")(), + artists: traverseList(flexColumns[1], "runs") + .filter(run => "navigationEndpoint" in run) + .map(run => ({ + artistId: traverseString(run, "browseId")(), + name: traverseString(run, "text")() + })), + name: traverseString(flexColumns[0], "runs", "text")(), + year: +traverseString(flexColumns[1], "runs", "text")(-1), + thumbnails: traverseList(item, "thumbnails") }, ALBUM_DETAILED ) @@ -58,12 +62,12 @@ export default class AlbumParser { return checkType( { type: "ALBUM", - albumId: [traverse(item, "browseId")].flat().at(-1), - playlistId: traverse(item, "thumbnailOverlay", "playlistId"), - name: traverse(item, "title", "text").at(0), + albumId: traverseString(item, "browseId")(-1), + playlistId: traverseString(item, "thumbnailOverlay", "playlistId")(), + name: traverseString(item, "title", "text")(), artists: [artistBasic], - year: +traverse(item, "subtitle", "text").at(-1), - thumbnails: [traverse(item, "thumbnails")].flat() + year: +traverseString(item, "subtitle", "text")(-1), + thumbnails: traverseList(item, "thumbnails") }, ALBUM_DETAILED ) @@ -73,12 +77,12 @@ export default class AlbumParser { return checkType( { type: "ALBUM", - albumId: traverse(item, "browseId").at(-1), - playlistId: traverse(item, "musicPlayButtonRenderer", "playlistId"), - name: traverse(item, "title", "text").at(0), + albumId: traverseString(item, "browseId")(-1), + playlistId: traverseString(item, "musicPlayButtonRenderer", "playlistId")(), + name: traverseString(item, "title", "text")(), artists: [artistBasic], - year: +traverse(item, "subtitle", "text").at(-1), - thumbnails: [traverse(item, "thumbnails")].flat() + year: +traverseString(item, "subtitle", "text")(-1), + thumbnails: traverseList(item, "thumbnails") }, ALBUM_DETAILED ) diff --git a/src/parsers/ArtistParser.ts b/src/parsers/ArtistParser.ts index 93066da..eca147a 100644 --- a/src/parsers/ArtistParser.ts +++ b/src/parsers/ArtistParser.ts @@ -2,31 +2,33 @@ import AlbumParser from "./AlbumParser" import checkType from "../utils/checkType" import Parser from "./Parser" 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 { ArtistDetailed, ArtistFull } from ".." +import { ArtistBasic, ArtistDetailed, ArtistFull } from ".." export default class ArtistParser { public static parse(data: any, artistId: string): ArtistFull { - const artistBasic = { + const artistBasic: ArtistBasic = { 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( { type: "ARTIST", ...artistBasic, - thumbnails: [traverse(data, "header", "thumbnails")].flat(), - description: description instanceof Array ? null : description, - subscribers: Parser.parseNumber(traverse(data, "subscriberCountText", "text")), - topSongs: traverse(data, "musicShelfRenderer", "contents").map((item: any) => + thumbnails: traverseList(data, "header", "thumbnails"), + description, + subscribers: Parser.parseNumber( + traverseString(data, "subscriberCountText", "text")() + ), + topSongs: traverseList(data, "musicShelfRenderer", "contents").map(item => SongParser.parseArtistTopSong(item, artistBasic) ), - topAlbums: [traverse(data, "musicCarouselShelfRenderer")] - .flat() + topAlbums: traverseList(data, "musicCarouselShelfRenderer") .at(0) .contents.map((item: any) => AlbumParser.parseArtistTopAlbums(item, artistBasic) @@ -37,14 +39,14 @@ export default class ArtistParser { } public static parseSearchResult(item: any): ArtistDetailed { - const flexColumns = traverse(item, "flexColumns") + const flexColumns = traverseList(item, "flexColumns") return checkType( { type: "ARTIST", - artistId: traverse(item, "browseId"), - name: traverse(flexColumns[0], "runs", "text"), - thumbnails: [traverse(item, "thumbnails")].flat() + artistId: traverseString(item, "browseId")(), + name: traverseString(flexColumns[0], "runs", "text")(), + thumbnails: traverseList(item, "thumbnails") }, ARTIST_DETAILED ) diff --git a/src/parsers/PlaylistParser.ts b/src/parsers/PlaylistParser.ts index 321f830..9c251a5 100644 --- a/src/parsers/PlaylistParser.ts +++ b/src/parsers/PlaylistParser.ts @@ -1,5 +1,6 @@ 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 { PlaylistFull } from ".." @@ -9,41 +10,41 @@ export default class PlaylistParser { { type: "PLAYLIST", playlistId, - name: traverse(data, "header", "title", "text").at(0), + name: traverseString(data, "header", "title", "text")(), artist: { - artistId: traverse(data, "header", "subtitle", "browseId"), - name: traverse(data, "header", "subtitle", "text").at(2) + artistId: traverseString(data, "header", "subtitle", "browseId")(), + name: traverseString(data, "header", "subtitle", "text")(2) }, - videoCount: +traverse(data, "header", "secondSubtitle", "text") + videoCount: +traverseList(data, "header", "secondSubtitle", "text") .at(0) .split(" ") .at(0) .replaceAll(",", ""), - thumbnails: traverse(data, "header", "thumbnails") + thumbnails: traverseList(data, "header", "thumbnails") }, PLAYLIST_FULL ) } public static parseSearchResult(item: any): PlaylistFull { - const flexColumns = traverse(item, "flexColumns") - const artistId = traverse(flexColumns[1], "browseId") + const flexColumns = traverseList(item, "flexColumns") + const artistId = traverseString(flexColumns[1], "browseId")() return checkType( { type: "PLAYLIST", - playlistId: traverse(item, "overlay", "playlistId"), - name: traverse(flexColumns[0], "runs", "text"), + playlistId: traverseString(item, "overlay", "playlistId")(), + name: traverseString(flexColumns[0], "runs", "text")(), artist: { - artistId: artistId instanceof Array ? null : artistId, - name: traverse(flexColumns[1], "runs", "text").at(-2) + artistId, + name: traverseString(flexColumns[1], "runs", "text")(-2) }, - videoCount: +traverse(flexColumns[1], "runs", "text") + videoCount: +traverseList(flexColumns[1], "runs", "text") .at(-1) .split(" ") .at(0) .replaceAll(",", ""), - thumbnails: [traverse(item, "thumbnails")].flat() + thumbnails: traverseList(item, "thumbnails") }, PLAYLIST_FULL ) diff --git a/src/parsers/SearchParser.ts b/src/parsers/SearchParser.ts index 4abd599..ef4ed56 100644 --- a/src/parsers/SearchParser.ts +++ b/src/parsers/SearchParser.ts @@ -2,14 +2,14 @@ import AlbumParser from "./AlbumParser" import ArtistParser from "./ArtistParser" import PlaylistParser from "./PlaylistParser" import SongParser from "./SongParser" -import traverse from "../utils/traverse" +import traverseList from "../utils/traverseList" import VideoParser from "./VideoParser" import { SearchResult } from ".." export default class SearchParser { public static parse(item: any): SearchResult { - const flexColumns = traverse(item, "flexColumns") - const type = [traverse(flexColumns[1], "runs", "text")].flat().at(0) as + const flexColumns = traverseList(item, "flexColumns") + const type = traverseList(flexColumns[1], "runs", "text").at(0) as | "Song" | "Video" | "Artist" diff --git a/src/parsers/SongParser.ts b/src/parsers/SongParser.ts index 3b80213..31bac30 100644 --- a/src/parsers/SongParser.ts +++ b/src/parsers/SongParser.ts @@ -1,6 +1,7 @@ import checkType from "../utils/checkType" 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 { AlbumBasic, ArtistBasic, SongDetailed, SongFull, ThumbnailFull } from ".." import { LIST, OBJECT, STRING } from "validate-any" @@ -10,70 +11,73 @@ export default class SongParser { return checkType( { type: "SONG", - videoId: traverse(data, "videoDetails", "videoId"), - name: traverse(data, "videoDetails", "title"), + videoId: traverseString(data, "videoDetails", "videoId")(), + name: traverseString(data, "videoDetails", "title")(), artists: [ { - artistId: traverse(data, "videoDetails", "channelId"), - name: traverse(data, "author") + artistId: traverseString(data, "videoDetails", "channelId")(), + name: traverseString(data, "author")() } ], - duration: +traverse(data, "videoDetails", "lengthSeconds"), - thumbnails: [traverse(data, "videoDetails", "thumbnails")].flat(), - description: traverse(data, "description"), - formats: traverse(data, "streamingData", "formats"), - adaptiveFormats: traverse(data, "streamingData", "adaptiveFormats") + duration: +traverseString(data, "videoDetails", "lengthSeconds"), + thumbnails: traverseList(data, "videoDetails", "thumbnails"), + description: traverseString(data, "description")(), + formats: traverseList(data, "streamingData", "formats"), + adaptiveFormats: traverseList(data, "streamingData", "adaptiveFormats") }, SONG_FULL ) } public static parseSearchResult(item: any): SongDetailed { - const flexColumns = traverse(item, "flexColumns") - const videoId = traverse(item, "playlistItemData", "videoId") + const flexColumns = traverseList(item, "flexColumns") return checkType( { type: "SONG", - videoId: videoId instanceof Array ? null : videoId, - name: traverse(flexColumns[0], "runs", "text"), - artists: traverse(flexColumns[1], "runs") - .filter((run: any) => "navigationEndpoint" in run) - .map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })) + 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: traverse(item, "browseId").at(-1), - name: traverse(flexColumns[1], "runs", "text").at(-3) + albumId: traverseString(item, "browseId")(-1), + name: traverseString(flexColumns[1], "runs", "text")(-3) }, - duration: Parser.parseDuration(traverse(flexColumns[1], "runs", "text").at(-1)), - thumbnails: [traverse(item, "thumbnails")].flat() + duration: Parser.parseDuration(traverseString(flexColumns[1], "runs", "text")(-1)), + thumbnails: traverseList(item, "thumbnails") }, SONG_DETAILED ) } public static parseArtistSong(item: any): SongDetailed { - const flexColumns = traverse(item, "flexColumns") - const videoId = traverse(item, "playlistItemData", "videoId") + const flexColumns = traverseList(item, "flexColumns") + const videoId = traverseString(item, "playlistItemData", "videoId")() return checkType( { type: "SONG", - videoId: videoId instanceof Array ? null : videoId, - name: traverse(flexColumns[0], "runs", "text"), - artists: [traverse(flexColumns[1], "runs")] - .flat() - .filter((item: any) => "navigationEndpoint" in item) - .map((run: any) => ({ - artistId: traverse(run, "browseId"), - name: run.text + 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: traverse(flexColumns[2], "browseId"), - name: traverse(flexColumns[2], "runs", "text") + albumId: traverseString(flexColumns[2], "browseId")(), + name: traverseString(flexColumns[2], "runs", "text")() }, - duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")), - thumbnails: [traverse(item, "thumbnails")].flat() + duration: Parser.parseDuration( + traverseString(item, "fixedColumns", "runs", "text")() + ), + thumbnails: traverseList(item, "thumbnails") }, SONG_DETAILED ) @@ -83,20 +87,20 @@ export default class SongParser { item: any, artistBasic: ArtistBasic ): Omit { - const flexColumns = traverse(item, "flexColumns") - const videoId = traverse(item, "playlistItemData", "videoId") + const flexColumns = traverseList(item, "flexColumns") + const videoId = traverseString(item, "playlistItemData", "videoId")() return checkType>( { type: "SONG", - videoId: videoId instanceof Array ? null : videoId, - name: traverse(flexColumns[0], "runs", "text"), + videoId, + name: traverseString(flexColumns[0], "runs", "text")(), artists: [artistBasic], album: { - albumId: traverse(flexColumns[2], "runs", "text"), - name: traverse(flexColumns[2], "browseId") + albumId: traverseString(flexColumns[2], "runs", "text")(), + name: traverseString(flexColumns[2], "browseId")() }, - thumbnails: [traverse(item, "thumbnails")].flat() + thumbnails: traverseList(item, "thumbnails") }, OBJECT({ type: STRING("SONG"), @@ -115,17 +119,19 @@ export default class SongParser { albumBasic: AlbumBasic, thumbnails: ThumbnailFull[] ): SongDetailed { - const flexColumns = traverse(item, "flexColumns") - const videoId = traverse(item, "playlistItemData", "videoId") + const flexColumns = traverseList(item, "flexColumns") + const videoId = traverseString(item, "playlistItemData", "videoId")() return checkType( { type: "SONG", - videoId: videoId instanceof Array ? null : videoId, - name: traverse(flexColumns[0], "runs", "text"), + videoId, + name: traverseString(flexColumns[0], "runs", "text")(), artists, album: albumBasic, - duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")), + duration: Parser.parseDuration( + traverseString(item, "fixedColumns", "runs", "text")() + ), thumbnails }, SONG_DETAILED diff --git a/src/parsers/VideoParser.ts b/src/parsers/VideoParser.ts index 8e9e60f..fcf33b2 100644 --- a/src/parsers/VideoParser.ts +++ b/src/parsers/VideoParser.ts @@ -1,6 +1,8 @@ import checkType from "../utils/checkType" import Parser from "./Parser" import traverse from "../utils/traverse" +import traverseList from "../utils/traverseList" +import traverseString from "../utils/traverseString" import { PLAYLIST_VIDEO } from "../interfaces" import { VideoDetailed, VideoFull } from ".." @@ -8,58 +10,66 @@ export default class VideoParser { public static parse(data: any): VideoFull { return { type: "VIDEO", - videoId: traverse(data, "videoDetails", "videoId"), - name: traverse(data, "videoDetails", "title"), + videoId: traverseString(data, "videoDetails", "videoId")(), + name: traverseString(data, "videoDetails", "title")(), artists: [ { - artistId: traverse(data, "videoDetails", "channelId"), - name: traverse(data, "author") + artistId: traverseString(data, "videoDetails", "channelId")(), + name: traverseString(data, "author")() } ], - views: +traverse(data, "videoDetails", "viewCount"), - duration: +traverse(data, "videoDetails", "lengthSeconds"), - thumbnails: [traverse(data, "videoDetails", "thumbnails")].flat(), - description: traverse(data, "description"), + views: +traverseString(data, "videoDetails", "viewCount")(), + duration: +traverseString(data, "videoDetails", "lengthSeconds")(), + thumbnails: traverseList(data, "videoDetails", "thumbnails"), + description: traverseString(data, "description")(), unlisted: traverse(data, "unlisted"), familySafe: traverse(data, "familySafe"), paid: traverse(data, "paid"), - tags: traverse(data, "tags") + tags: traverseList(data, "tags") } } public static parseSearchResult(item: any): VideoDetailed { - const flexColumns = traverse(item, "flexColumns") - const videoId = traverse(item, "playNavigationEndpoint", "videoId") + const flexColumns = traverseList(item, "flexColumns") + const videoId = traverseString(item, "playNavigationEndpoint", "videoId")() return { type: "VIDEO", - videoId: videoId instanceof Array ? null : videoId, - name: traverse(flexColumns[0], "runs", "text"), - artists: [traverse(flexColumns[1], "runs")] - .flat() - .filter((run: any) => "navigationEndpoint" in run) - .map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })), - views: Parser.parseNumber(traverse(flexColumns[1], "runs", "text").at(-3).slice(0, -6)), - duration: Parser.parseDuration(traverse(flexColumns[1], "text").at(-1)), - thumbnails: [traverse(item, "thumbnails")].flat() + 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")() + })), + 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 { - const flexColumns = traverse(item, "flexColumns") - const videoId = traverse(item, "playNavigationEndpoint", "videoId") + const flexColumns = traverseList(item, "flexColumns") + const videoId = traverseString(item, "playNavigationEndpoint", "videoId")() return checkType>( { type: "VIDEO", - videoId: videoId instanceof Array ? null : videoId, - name: traverse(flexColumns[0], "runs", "text"), - artists: [traverse(flexColumns[1], "runs")] - .flat() - .filter((run: any) => "navigationEndpoint" in run) - .map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })), - duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")), - thumbnails: [traverse(item, "thumbnails")].flat() + 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")() + ), + thumbnails: traverseList(item, "thumbnails") }, PLAYLIST_VIDEO ) diff --git a/src/utils/traverseList.ts b/src/utils/traverseList.ts new file mode 100644 index 0000000..5f5c644 --- /dev/null +++ b/src/utils/traverseList.ts @@ -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 +} \ No newline at end of file diff --git a/src/utils/traverseString.ts b/src/utils/traverseString.ts new file mode 100644 index 0000000..6d10a32 --- /dev/null +++ b/src/utils/traverseString.ts @@ -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 || "" +}