Validate all types before returning to user

This commit is contained in:
Zechariah 2022-03-27 21:56:30 +08:00
parent 892fd5f82f
commit 45692dcaa8
11 changed files with 415 additions and 335 deletions

View File

@ -4,7 +4,7 @@ import axios, { AxiosInstance } from "axios"
import PlaylistParser from "./parsers/PlaylistParser"
import SearchParser from "./parsers/SearchParser"
import SongParser from "./parsers/SongParser"
import traverse from "./traverse"
import traverse from "./utils/traverse"
import VideoParser from "./parsers/VideoParser"
import {
AlbumDetailed,

View File

@ -1,162 +1,18 @@
import ObjectValidator from "validate-any/dist/validators/ObjectValidator"
import Validator from "validate-any/dist/classes/Validator"
import YTMusic, {
AlbumBasic,
AlbumDetailed,
AlbumFull,
ArtistBasic,
ArtistDetailed,
ArtistFull,
PlaylistFull,
SongDetailed,
SongFull,
ThumbnailFull,
VideoDetailed,
VideoFull
} from ".."
import YTMusic from ".."
import {
BOOLEAN,
iValidationError,
LIST,
NULL,
NUMBER,
OBJECT,
OR,
STRING,
validate
} from "validate-any"
//#region Interfaces
const THUMBNAIL_FULL: ObjectValidator<ThumbnailFull> = OBJECT({
url: STRING(),
width: NUMBER(),
height: NUMBER()
})
const ARTIST_BASIC: ObjectValidator<ArtistBasic> = OBJECT({
artistId: OR(STRING(), NULL()),
name: STRING()
})
const ALBUM_BASIC: ObjectValidator<AlbumBasic> = OBJECT({
albumId: STRING(),
name: STRING()
})
const SONG_DETAILED: ObjectValidator<SongDetailed> = OBJECT({
type: STRING("SONG"),
videoId: OR(STRING(), NULL()),
name: STRING(),
artists: LIST(ARTIST_BASIC),
album: ALBUM_BASIC,
duration: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL)
})
const VIDEO_DETAILED: ObjectValidator<VideoDetailed> = OBJECT({
type: STRING("VIDEO"),
videoId: OR(STRING(), NULL()),
name: STRING(),
artists: LIST(ARTIST_BASIC),
views: NUMBER(),
duration: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL)
})
const ARTIST_DETAILED: ObjectValidator<ArtistDetailed> = OBJECT({
artistId: STRING(),
name: STRING(),
type: STRING("ARTIST"),
thumbnails: LIST(THUMBNAIL_FULL)
})
const ALBUM_DETAILED: ObjectValidator<AlbumDetailed> = OBJECT({
type: STRING("ALBUM"),
albumId: STRING(),
playlistId: STRING(),
name: STRING(),
artists: LIST(ARTIST_BASIC),
year: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL)
})
const SONG_FULL: ObjectValidator<SongFull> = OBJECT({
type: STRING("SONG"),
videoId: OR(STRING(), NULL()),
name: STRING(),
artists: LIST(ARTIST_BASIC),
duration: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL),
description: STRING(),
formats: LIST(OBJECT()),
adaptiveFormats: LIST(OBJECT())
})
const VIDEO_FULL: ObjectValidator<VideoFull> = OBJECT({
type: STRING("VIDEO"),
videoId: OR(STRING(), NULL()),
name: STRING(),
artists: LIST(ARTIST_BASIC),
views: NUMBER(),
duration: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL),
description: STRING(),
unlisted: BOOLEAN(),
familySafe: BOOLEAN(),
paid: BOOLEAN(),
tags: LIST(STRING())
})
const ARTIST_FULL: ObjectValidator<ArtistFull> = OBJECT({
artistId: STRING(),
name: STRING(),
type: STRING("ARTIST"),
thumbnails: LIST(THUMBNAIL_FULL),
description: OR(STRING(), NULL()),
subscribers: NUMBER(),
topSongs: LIST(
OBJECT({
type: STRING("SONG"),
videoId: STRING(),
name: STRING(),
artists: LIST(ARTIST_BASIC),
album: ALBUM_BASIC,
thumbnails: LIST(THUMBNAIL_FULL)
})
),
topAlbums: LIST(ALBUM_DETAILED)
})
const ALBUM_FULL: ObjectValidator<AlbumFull> = OBJECT({
type: STRING("ALBUM"),
albumId: STRING(),
playlistId: STRING(),
name: STRING(),
artists: LIST(ARTIST_BASIC),
year: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL),
description: OR(STRING(), NULL()),
songs: LIST(SONG_DETAILED)
})
const PLAYLIST_DETAILED: ObjectValidator<PlaylistFull> = OBJECT({
type: STRING("PLAYLIST"),
playlistId: STRING(),
name: STRING(),
artist: ARTIST_BASIC,
videoCount: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL)
})
const PLAYLIST_VIDEO: ObjectValidator<Omit<VideoDetailed, "views">> = OBJECT({
type: STRING("VIDEO"),
videoId: OR(STRING(), NULL()),
name: STRING(),
artists: LIST(ARTIST_BASIC),
duration: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL)
})
//#endregion
ALBUM_DETAILED,
ALBUM_FULL,
ARTIST_DETAILED,
ARTIST_FULL,
PLAYLIST_FULL,
PLAYLIST_VIDEO,
SONG_DETAILED,
SONG_FULL,
VIDEO_DETAILED,
VIDEO_FULL
} from "../interfaces"
import { iValidationError, LIST, STRING, validate } from "validate-any"
const issues: iValidationError[][] = []
const queries = ["Lilac", "Weekend", "Eill", "Eminem", "Lisa Hannigan"]
@ -231,7 +87,7 @@ queries.forEach(query => {
ytmusic
.search(query, "PLAYLIST")
.then(playlists => {
_expect(playlists, LIST(PLAYLIST_DETAILED))
_expect(playlists, LIST(PLAYLIST_FULL))
done()
})
.catch(done)
@ -246,7 +102,7 @@ queries.forEach(query => {
LIST(
ALBUM_DETAILED,
ARTIST_DETAILED,
PLAYLIST_DETAILED,
PLAYLIST_FULL,
SONG_DETAILED,
VIDEO_DETAILED
)
@ -327,7 +183,7 @@ queries.forEach(query => {
.search(query, "PLAYLIST")
.then(playlists => ytmusic.getPlaylist(playlists[0]!.playlistId!))
.then(playlist => {
_expect(playlist, PLAYLIST_DETAILED)
_expect(playlist, PLAYLIST_FULL)
done()
})
.catch(done)

146
src/interfaces.ts Normal file
View File

@ -0,0 +1,146 @@
import ObjectValidator from "validate-any/dist/validators/ObjectValidator"
import {
AlbumBasic,
AlbumDetailed,
AlbumFull,
ArtistBasic,
ArtistDetailed,
ArtistFull,
PlaylistFull,
SongDetailed,
SongFull,
ThumbnailFull,
VideoDetailed,
VideoFull
} from "."
import { BOOLEAN, LIST, NULL, NUMBER, OBJECT, OR, STRING } from "validate-any"
export const THUMBNAIL_FULL: ObjectValidator<ThumbnailFull> = OBJECT({
url: STRING(),
width: NUMBER(),
height: NUMBER()
})
export const ARTIST_BASIC: ObjectValidator<ArtistBasic> = OBJECT({
artistId: OR(STRING(), NULL()),
name: STRING()
})
export const ALBUM_BASIC: ObjectValidator<AlbumBasic> = OBJECT({
albumId: STRING(),
name: STRING()
})
export const SONG_DETAILED: ObjectValidator<SongDetailed> = OBJECT({
type: STRING("SONG"),
videoId: OR(STRING(), NULL()),
name: STRING(),
artists: LIST(ARTIST_BASIC),
album: ALBUM_BASIC,
duration: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL)
})
export const VIDEO_DETAILED: ObjectValidator<VideoDetailed> = OBJECT({
type: STRING("VIDEO"),
videoId: OR(STRING(), NULL()),
name: STRING(),
artists: LIST(ARTIST_BASIC),
views: NUMBER(),
duration: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL)
})
export const ARTIST_DETAILED: ObjectValidator<ArtistDetailed> = OBJECT({
artistId: STRING(),
name: STRING(),
type: STRING("ARTIST"),
thumbnails: LIST(THUMBNAIL_FULL)
})
export const ALBUM_DETAILED: ObjectValidator<AlbumDetailed> = OBJECT({
type: STRING("ALBUM"),
albumId: STRING(),
playlistId: STRING(),
name: STRING(),
artists: LIST(ARTIST_BASIC),
year: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL)
})
export const SONG_FULL: ObjectValidator<SongFull> = OBJECT({
type: STRING("SONG"),
videoId: OR(STRING(), NULL()),
name: STRING(),
artists: LIST(ARTIST_BASIC),
duration: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL),
description: STRING(),
formats: LIST(OBJECT()),
adaptiveFormats: LIST(OBJECT())
})
export const VIDEO_FULL: ObjectValidator<VideoFull> = OBJECT({
type: STRING("VIDEO"),
videoId: OR(STRING(), NULL()),
name: STRING(),
artists: LIST(ARTIST_BASIC),
views: NUMBER(),
duration: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL),
description: STRING(),
unlisted: BOOLEAN(),
familySafe: BOOLEAN(),
paid: BOOLEAN(),
tags: LIST(STRING())
})
export const ARTIST_FULL: ObjectValidator<ArtistFull> = OBJECT({
artistId: STRING(),
name: STRING(),
type: STRING("ARTIST"),
thumbnails: LIST(THUMBNAIL_FULL),
description: OR(STRING(), NULL()),
subscribers: NUMBER(),
topSongs: LIST(
OBJECT({
type: STRING("SONG"),
videoId: STRING(),
name: STRING(),
artists: LIST(ARTIST_BASIC),
album: ALBUM_BASIC,
thumbnails: LIST(THUMBNAIL_FULL)
})
),
topAlbums: LIST(ALBUM_DETAILED)
})
export const ALBUM_FULL: ObjectValidator<AlbumFull> = OBJECT({
type: STRING("ALBUM"),
albumId: STRING(),
playlistId: STRING(),
name: STRING(),
artists: LIST(ARTIST_BASIC),
year: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL),
description: OR(STRING(), NULL()),
songs: LIST(SONG_DETAILED)
})
export const PLAYLIST_FULL: ObjectValidator<PlaylistFull> = OBJECT({
type: STRING("PLAYLIST"),
playlistId: STRING(),
name: STRING(),
artist: ARTIST_BASIC,
videoCount: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL)
})
export const PLAYLIST_VIDEO: ObjectValidator<Omit<VideoDetailed, "views">> = OBJECT({
type: STRING("VIDEO"),
videoId: OR(STRING(), NULL()),
name: STRING(),
artists: LIST(ARTIST_BASIC),
duration: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL)
})

View File

@ -1,5 +1,7 @@
import checkType from "../utils/checkType"
import SongParser from "./SongParser"
import traverse from "../traverse"
import traverse from "../utils/traverse"
import { ALBUM_DETAILED, ALBUM_FULL } from "../interfaces"
import { AlbumDetailed, AlbumFull, ArtistBasic } from ".."
export default class AlbumParser {
@ -14,59 +16,71 @@ export default class AlbumParser {
const thumbnails = [traverse(data, "header", "thumbnails")].flat()
const description = traverse(data, "description", "text")
return {
type: "ALBUM",
...albumBasic,
playlistId: traverse(data, "buttonRenderer", "playlistId"),
artists,
year: +traverse(data, "header", "subtitle", "text").at(-1),
thumbnails,
description: description instanceof Array ? null : description,
songs: [traverse(data, "musicResponsiveListItemRenderer")]
.flat()
.map((item: any) =>
SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails)
)
}
return checkType<AlbumFull>(
{
type: "ALBUM",
...albumBasic,
playlistId: traverse(data, "buttonRenderer", "playlistId"),
artists,
year: +traverse(data, "header", "subtitle", "text").at(-1),
thumbnails,
description: description instanceof Array ? null : description,
songs: [traverse(data, "musicResponsiveListItemRenderer")]
.flat()
.map((item: any) =>
SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails)
)
},
ALBUM_FULL
)
}
public static parseSearchResult(item: any): AlbumDetailed {
const flexColumns = traverse(item, "flexColumns")
return {
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()
}
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()
},
ALBUM_DETAILED
)
}
public static parseArtistAlbum(item: any, artistBasic: ArtistBasic): AlbumDetailed {
return {
type: "ALBUM",
albumId: [traverse(item, "browseId")].flat().at(-1),
playlistId: traverse(item, "thumbnailOverlay", "playlistId"),
name: traverse(item, "title", "text").at(0),
artists: [artistBasic],
year: +traverse(item, "subtitle", "text").at(-1),
thumbnails: [traverse(item, "thumbnails")].flat()
}
return checkType<AlbumDetailed>(
{
type: "ALBUM",
albumId: [traverse(item, "browseId")].flat().at(-1),
playlistId: traverse(item, "thumbnailOverlay", "playlistId"),
name: traverse(item, "title", "text").at(0),
artists: [artistBasic],
year: +traverse(item, "subtitle", "text").at(-1),
thumbnails: [traverse(item, "thumbnails")].flat()
},
ALBUM_DETAILED
)
}
public static parseArtistTopAlbums(item: any, artistBasic: ArtistBasic): AlbumDetailed {
return {
type: "ALBUM",
albumId: traverse(item, "browseId").at(-1),
playlistId: traverse(item, "musicPlayButtonRenderer", "playlistId"),
name: traverse(item, "title", "text").at(0),
artists: [artistBasic],
year: +traverse(item, "subtitle", "text").at(-1),
thumbnails: [traverse(item, "thumbnails")].flat()
}
return checkType<AlbumDetailed>(
{
type: "ALBUM",
albumId: traverse(item, "browseId").at(-1),
playlistId: traverse(item, "musicPlayButtonRenderer", "playlistId"),
name: traverse(item, "title", "text").at(0),
artists: [artistBasic],
year: +traverse(item, "subtitle", "text").at(-1),
thumbnails: [traverse(item, "thumbnails")].flat()
},
ALBUM_DETAILED
)
}
}

View File

@ -1,7 +1,9 @@
import AlbumParser from "./AlbumParser"
import checkType from "../utils/checkType"
import Parser from "./Parser"
import SongParser from "./SongParser"
import traverse from "../traverse"
import traverse from "../utils/traverse"
import { ARTIST_DETAILED, ARTIST_FULL } from "../interfaces"
import { ArtistDetailed, ArtistFull } from ".."
export default class ArtistParser {
@ -13,30 +15,38 @@ export default class ArtistParser {
const description = traverse(data, "header", "description", "text")
return {
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) =>
SongParser.parseArtistTopSong(item, artistBasic)
),
topAlbums: [traverse(data, "musicCarouselShelfRenderer")]
.flat()
.at(0)
.contents.map((item: any) => AlbumParser.parseArtistTopAlbums(item, artistBasic))
}
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) =>
SongParser.parseArtistTopSong(item, artistBasic)
),
topAlbums: [traverse(data, "musicCarouselShelfRenderer")]
.flat()
.at(0)
.contents.map((item: any) =>
AlbumParser.parseArtistTopAlbums(item, artistBasic)
)
},
ARTIST_FULL
)
}
public static parseSearchResult(item: any): ArtistDetailed {
const flexColumns = traverse(item, "flexColumns")
return {
type: "ARTIST",
artistId: traverse(item, "browseId"),
name: traverse(flexColumns[0], "runs", "text"),
thumbnails: [traverse(item, "thumbnails")].flat()
}
return checkType<ArtistDetailed>(
{
type: "ARTIST",
artistId: traverse(item, "browseId"),
name: traverse(flexColumns[0], "runs", "text"),
thumbnails: [traverse(item, "thumbnails")].flat()
},
ARTIST_DETAILED
)
}
}

View File

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

View File

@ -2,7 +2,7 @@ import AlbumParser from "./AlbumParser"
import ArtistParser from "./ArtistParser"
import PlaylistParser from "./PlaylistParser"
import SongParser from "./SongParser"
import traverse from "../traverse"
import traverse from "../utils/traverse"
import VideoParser from "./VideoParser"
import { SearchResult } from ".."

View File

@ -1,70 +1,82 @@
import checkType from "../utils/checkType"
import Parser from "./Parser"
import traverse from "../traverse"
import traverse from "../utils/traverse"
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"
export default class SongParser {
public static parse(data: any): SongFull {
return {
type: "SONG",
videoId: traverse(data, "videoDetails", "videoId"),
name: traverse(data, "videoDetails", "title"),
artists: [
{
artistId: traverse(data, "videoDetails", "channelId"),
name: traverse(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")
}
return checkType<SongFull>(
{
type: "SONG",
videoId: traverse(data, "videoDetails", "videoId"),
name: traverse(data, "videoDetails", "title"),
artists: [
{
artistId: traverse(data, "videoDetails", "channelId"),
name: traverse(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")
},
SONG_FULL
)
}
public static parseSearchResult(item: any): SongDetailed {
const flexColumns = traverse(item, "flexColumns")
const videoId = traverse(item, "playlistItemData", "videoId")
return {
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 }))
.slice(0, -1),
album: {
albumId: traverse(item, "browseId").at(-1),
name: traverse(flexColumns[1], "runs", "text").at(-3)
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 }))
.slice(0, -1),
album: {
albumId: traverse(item, "browseId").at(-1),
name: traverse(flexColumns[1], "runs", "text").at(-3)
},
duration: Parser.parseDuration(traverse(flexColumns[1], "runs", "text").at(-1)),
thumbnails: [traverse(item, "thumbnails")].flat()
},
duration: Parser.parseDuration(traverse(flexColumns[1], "runs", "text").at(-1)),
thumbnails: [traverse(item, "thumbnails")].flat()
}
SONG_DETAILED
)
}
public static parseArtistSong(item: any): SongDetailed {
const flexColumns = traverse(item, "flexColumns")
const videoId = traverse(item, "playlistItemData", "videoId")
return {
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
})),
album: {
albumId: traverse(flexColumns[2], "browseId"),
name: traverse(flexColumns[2], "runs", "text")
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
})),
album: {
albumId: traverse(flexColumns[2], "browseId"),
name: traverse(flexColumns[2], "runs", "text")
},
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")),
thumbnails: [traverse(item, "thumbnails")].flat()
},
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")),
thumbnails: [traverse(item, "thumbnails")].flat()
}
SONG_DETAILED
)
}
public static parseArtistTopSong(
@ -74,17 +86,27 @@ export default class SongParser {
const flexColumns = traverse(item, "flexColumns")
const videoId = traverse(item, "playlistItemData", "videoId")
return {
type: "SONG",
videoId: videoId instanceof Array ? null : videoId,
name: traverse(flexColumns[0], "runs", "text"),
artists: [artistBasic],
album: {
albumId: traverse(flexColumns[2], "runs", "text"),
name: traverse(flexColumns[2], "browseId")
return checkType<Omit<SongDetailed, "duration">>(
{
type: "SONG",
videoId: videoId instanceof Array ? null : videoId,
name: traverse(flexColumns[0], "runs", "text"),
artists: [artistBasic],
album: {
albumId: traverse(flexColumns[2], "runs", "text"),
name: traverse(flexColumns[2], "browseId")
},
thumbnails: [traverse(item, "thumbnails")].flat()
},
thumbnails: [traverse(item, "thumbnails")].flat()
}
OBJECT({
type: STRING("SONG"),
videoId: STRING(),
name: STRING(),
artists: LIST(ARTIST_BASIC),
album: ALBUM_BASIC,
thumbnails: LIST(THUMBNAIL_FULL)
})
)
}
public static parseAlbumSong(
@ -96,14 +118,17 @@ export default class SongParser {
const flexColumns = traverse(item, "flexColumns")
const videoId = traverse(item, "playlistItemData", "videoId")
return {
type: "SONG",
videoId: videoId instanceof Array ? null : videoId,
name: traverse(flexColumns[0], "runs", "text"),
artists,
album: albumBasic,
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")),
thumbnails
}
return checkType<SongDetailed>(
{
type: "SONG",
videoId: videoId instanceof Array ? null : videoId,
name: traverse(flexColumns[0], "runs", "text"),
artists,
album: albumBasic,
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")),
thumbnails
},
SONG_DETAILED
)
}
}

View File

@ -1,5 +1,7 @@
import checkType from "../utils/checkType"
import Parser from "./Parser"
import traverse from "../traverse"
import traverse from "../utils/traverse"
import { PLAYLIST_VIDEO } from "../interfaces"
import { VideoDetailed, VideoFull } from ".."
export default class VideoParser {
@ -47,16 +49,19 @@ export default class VideoParser {
const flexColumns = traverse(item, "flexColumns")
const videoId = traverse(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 })),
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")),
thumbnails: [traverse(item, "thumbnails")].flat()
}
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()
},
PLAYLIST_VIDEO
)
}
}

16
src/utils/checkType.ts Normal file
View File

@ -0,0 +1,16 @@
import Validator from "validate-any/dist/classes/Validator"
import { validate } from "validate-any"
export default <T>(data: T, validator: Validator<T>): T => {
const result = validate(data, validator)
if (result.success) {
return result.data
} else {
console.error("Invalid data schema, please report as an issue", {
expected: validator.formatSchema(),
actual: data,
errors: result.errors
})
return data
}
}