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 {
type: "SONG"
videoId: string | null
videoId: string
name: string
artists: ArtistBasic[]
album: AlbumBasic
@ -24,7 +24,7 @@ export interface SongFull extends Omit<SongDetailed, "album"> {
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<SongDetailed, "duration">[]
topAlbums: AlbumDetailed[]
@ -72,7 +72,7 @@ export interface AlbumDetailed extends AlbumBasic {
}
export interface AlbumFull extends AlbumDetailed {
description: string | null
description: string
songs: SongDetailed[]
}

View File

@ -22,7 +22,7 @@ export const THUMBNAIL_FULL: ObjectValidator<ThumbnailFull> = OBJECT({
})
export const ARTIST_BASIC: ObjectValidator<ArtistBasic> = OBJECT({
artistId: OR(STRING(), NULL()),
artistId: STRING(),
name: STRING()
})
@ -33,7 +33,7 @@ export const ALBUM_BASIC: ObjectValidator<AlbumBasic> = OBJECT({
export const SONG_DETAILED: ObjectValidator<SongDetailed> = 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<SongDetailed> = OBJECT({
export const VIDEO_DETAILED: ObjectValidator<VideoDetailed> = 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<AlbumDetailed> = OBJECT({
export const SONG_FULL: ObjectValidator<SongFull> = 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<SongFull> = OBJECT({
export const VIDEO_FULL: ObjectValidator<VideoFull> = 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<ArtistFull> = 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<AlbumFull> = 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<PlaylistFull> = OBJECT({
export const PLAYLIST_VIDEO: ObjectValidator<Omit<VideoDetailed, "views">> = OBJECT({
type: STRING("VIDEO"),
videoId: OR(STRING(), NULL()),
videoId: STRING(),
name: STRING(),
artists: LIST(ARTIST_BASIC),
duration: NUMBER(),

View File

@ -1,33 +1,34 @@
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<AlbumFull>(
{
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) =>
description: traverseString(data, "description", "text")(),
songs: traverseList(data, "musicResponsiveListItemRenderer").map(item =>
SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails)
)
},
@ -36,19 +37,22 @@ export default class AlbumParser {
}
public static parseSearchResult(item: any): AlbumDetailed {
const flexColumns = traverse(item, "flexColumns")
const flexColumns = traverseList(item, "flexColumns")
return checkType<AlbumDetailed>(
{
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<AlbumDetailed>(
{
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<AlbumDetailed>(
{
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
)

View File

@ -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<ArtistFull>(
{
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<ArtistDetailed>(
{
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
)

View File

@ -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<PlaylistFull>(
{
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
)

View File

@ -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"

View File

@ -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<SongFull>(
{
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<SongDetailed>(
{
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<SongDetailed>(
{
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<SongDetailed, "duration"> {
const flexColumns = traverse(item, "flexColumns")
const videoId = traverse(item, "playlistItemData", "videoId")
const flexColumns = traverseList(item, "flexColumns")
const videoId = traverseString(item, "playlistItemData", "videoId")()
return checkType<Omit<SongDetailed, "duration">>(
{
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<SongDetailed>(
{
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

View File

@ -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<VideoDetailed, "views"> {
const flexColumns = traverse(item, "flexColumns")
const videoId = traverse(item, "playNavigationEndpoint", "videoId")
const flexColumns = traverseList(item, "flexColumns")
const videoId = traverseString(item, "playNavigationEndpoint", "videoId")()
return checkType<Omit<VideoDetailed, "views">>(
{
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
)

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 || ""
}