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 PlaylistParser from "./parsers/PlaylistParser"
import SearchParser from "./parsers/SearchParser" import SearchParser from "./parsers/SearchParser"
import SongParser from "./parsers/SongParser" import SongParser from "./parsers/SongParser"
import traverse from "./traverse" import traverse from "./utils/traverse"
import VideoParser from "./parsers/VideoParser" import VideoParser from "./parsers/VideoParser"
import { import {
AlbumDetailed, AlbumDetailed,

View File

@ -1,162 +1,18 @@
import ObjectValidator from "validate-any/dist/validators/ObjectValidator"
import Validator from "validate-any/dist/classes/Validator" import Validator from "validate-any/dist/classes/Validator"
import YTMusic, { import YTMusic from ".."
AlbumBasic,
AlbumDetailed,
AlbumFull,
ArtistBasic,
ArtistDetailed,
ArtistFull,
PlaylistFull,
SongDetailed,
SongFull,
ThumbnailFull,
VideoDetailed,
VideoFull
} from ".."
import { import {
BOOLEAN, ALBUM_DETAILED,
iValidationError, ALBUM_FULL,
LIST, ARTIST_DETAILED,
NULL, ARTIST_FULL,
NUMBER, PLAYLIST_FULL,
OBJECT, PLAYLIST_VIDEO,
OR, SONG_DETAILED,
STRING, SONG_FULL,
validate VIDEO_DETAILED,
} from "validate-any" VIDEO_FULL
} from "../interfaces"
//#region Interfaces import { iValidationError, LIST, STRING, validate } from "validate-any"
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
const issues: iValidationError[][] = [] const issues: iValidationError[][] = []
const queries = ["Lilac", "Weekend", "Eill", "Eminem", "Lisa Hannigan"] const queries = ["Lilac", "Weekend", "Eill", "Eminem", "Lisa Hannigan"]
@ -231,7 +87,7 @@ queries.forEach(query => {
ytmusic ytmusic
.search(query, "PLAYLIST") .search(query, "PLAYLIST")
.then(playlists => { .then(playlists => {
_expect(playlists, LIST(PLAYLIST_DETAILED)) _expect(playlists, LIST(PLAYLIST_FULL))
done() done()
}) })
.catch(done) .catch(done)
@ -246,7 +102,7 @@ queries.forEach(query => {
LIST( LIST(
ALBUM_DETAILED, ALBUM_DETAILED,
ARTIST_DETAILED, ARTIST_DETAILED,
PLAYLIST_DETAILED, PLAYLIST_FULL,
SONG_DETAILED, SONG_DETAILED,
VIDEO_DETAILED VIDEO_DETAILED
) )
@ -327,7 +183,7 @@ queries.forEach(query => {
.search(query, "PLAYLIST") .search(query, "PLAYLIST")
.then(playlists => ytmusic.getPlaylist(playlists[0]!.playlistId!)) .then(playlists => ytmusic.getPlaylist(playlists[0]!.playlistId!))
.then(playlist => { .then(playlist => {
_expect(playlist, PLAYLIST_DETAILED) _expect(playlist, PLAYLIST_FULL)
done() done()
}) })
.catch(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 SongParser from "./SongParser"
import traverse from "../traverse" import traverse from "../utils/traverse"
import { ALBUM_DETAILED, ALBUM_FULL } from "../interfaces"
import { AlbumDetailed, AlbumFull, ArtistBasic } from ".." import { AlbumDetailed, AlbumFull, ArtistBasic } from ".."
export default class AlbumParser { export default class AlbumParser {
@ -14,7 +16,8 @@ export default class AlbumParser {
const thumbnails = [traverse(data, "header", "thumbnails")].flat() const thumbnails = [traverse(data, "header", "thumbnails")].flat()
const description = traverse(data, "description", "text") const description = traverse(data, "description", "text")
return { return checkType<AlbumFull>(
{
type: "ALBUM", type: "ALBUM",
...albumBasic, ...albumBasic,
playlistId: traverse(data, "buttonRenderer", "playlistId"), playlistId: traverse(data, "buttonRenderer", "playlistId"),
@ -27,13 +30,16 @@ export default class AlbumParser {
.map((item: any) => .map((item: any) =>
SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails) SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails)
) )
} },
ALBUM_FULL
)
} }
public static parseSearchResult(item: any): AlbumDetailed { public static parseSearchResult(item: any): AlbumDetailed {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverse(item, "flexColumns")
return { return checkType<AlbumDetailed>(
{
type: "ALBUM", type: "ALBUM",
albumId: [traverse(item, "browseId")].flat().at(-1), albumId: [traverse(item, "browseId")].flat().at(-1),
playlistId: traverse(item, "overlay", "playlistId"), playlistId: traverse(item, "overlay", "playlistId"),
@ -43,11 +49,14 @@ export default class AlbumParser {
name: traverse(flexColumns[0], "runs", "text"), name: traverse(flexColumns[0], "runs", "text"),
year: +traverse(flexColumns[1], "runs", "text").at(-1), year: +traverse(flexColumns[1], "runs", "text").at(-1),
thumbnails: [traverse(item, "thumbnails")].flat() thumbnails: [traverse(item, "thumbnails")].flat()
} },
ALBUM_DETAILED
)
} }
public static parseArtistAlbum(item: any, artistBasic: ArtistBasic): AlbumDetailed { public static parseArtistAlbum(item: any, artistBasic: ArtistBasic): AlbumDetailed {
return { return checkType<AlbumDetailed>(
{
type: "ALBUM", type: "ALBUM",
albumId: [traverse(item, "browseId")].flat().at(-1), albumId: [traverse(item, "browseId")].flat().at(-1),
playlistId: traverse(item, "thumbnailOverlay", "playlistId"), playlistId: traverse(item, "thumbnailOverlay", "playlistId"),
@ -55,11 +64,14 @@ export default class AlbumParser {
artists: [artistBasic], artists: [artistBasic],
year: +traverse(item, "subtitle", "text").at(-1), year: +traverse(item, "subtitle", "text").at(-1),
thumbnails: [traverse(item, "thumbnails")].flat() thumbnails: [traverse(item, "thumbnails")].flat()
} },
ALBUM_DETAILED
)
} }
public static parseArtistTopAlbums(item: any, artistBasic: ArtistBasic): AlbumDetailed { public static parseArtistTopAlbums(item: any, artistBasic: ArtistBasic): AlbumDetailed {
return { return checkType<AlbumDetailed>(
{
type: "ALBUM", type: "ALBUM",
albumId: traverse(item, "browseId").at(-1), albumId: traverse(item, "browseId").at(-1),
playlistId: traverse(item, "musicPlayButtonRenderer", "playlistId"), playlistId: traverse(item, "musicPlayButtonRenderer", "playlistId"),
@ -67,6 +79,8 @@ export default class AlbumParser {
artists: [artistBasic], artists: [artistBasic],
year: +traverse(item, "subtitle", "text").at(-1), year: +traverse(item, "subtitle", "text").at(-1),
thumbnails: [traverse(item, "thumbnails")].flat() thumbnails: [traverse(item, "thumbnails")].flat()
} },
ALBUM_DETAILED
)
} }
} }

View File

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

View File

@ -1,9 +1,12 @@
import traverse from "../traverse" import checkType from "../utils/checkType"
import traverse from "../utils/traverse"
import { PLAYLIST_FULL } from "../interfaces"
import { PlaylistFull } from ".." import { PlaylistFull } from ".."
export default class PlaylistParser { export default class PlaylistParser {
public static parse(data: any, playlistId: string): PlaylistFull { public static parse(data: any, playlistId: string): PlaylistFull {
return { return checkType<PlaylistFull>(
{
type: "PLAYLIST", type: "PLAYLIST",
playlistId, playlistId,
name: traverse(data, "header", "title", "text").at(0), name: traverse(data, "header", "title", "text").at(0),
@ -17,14 +20,17 @@ export default class PlaylistParser {
.at(0) .at(0)
.replaceAll(",", ""), .replaceAll(",", ""),
thumbnails: traverse(data, "header", "thumbnails") thumbnails: traverse(data, "header", "thumbnails")
} },
PLAYLIST_FULL
)
} }
public static parseSearchResult(item: any): PlaylistFull { public static parseSearchResult(item: any): PlaylistFull {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverse(item, "flexColumns")
const artistId = traverse(flexColumns[1], "browseId") const artistId = traverse(flexColumns[1], "browseId")
return { return checkType<PlaylistFull>(
{
type: "PLAYLIST", type: "PLAYLIST",
playlistId: traverse(item, "overlay", "playlistId"), playlistId: traverse(item, "overlay", "playlistId"),
name: traverse(flexColumns[0], "runs", "text"), name: traverse(flexColumns[0], "runs", "text"),
@ -38,6 +44,8 @@ export default class PlaylistParser {
.at(0) .at(0)
.replaceAll(",", ""), .replaceAll(",", ""),
thumbnails: [traverse(item, "thumbnails")].flat() thumbnails: [traverse(item, "thumbnails")].flat()
} },
PLAYLIST_FULL
)
} }
} }

View File

@ -2,7 +2,7 @@ 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 "../traverse" import traverse from "../utils/traverse"
import VideoParser from "./VideoParser" import VideoParser from "./VideoParser"
import { SearchResult } from ".." import { SearchResult } from ".."

View File

@ -1,10 +1,14 @@
import checkType from "../utils/checkType"
import Parser from "./Parser" 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 { AlbumBasic, ArtistBasic, SongDetailed, SongFull, ThumbnailFull } from ".."
import { LIST, OBJECT, STRING } from "validate-any"
export default class SongParser { export default class SongParser {
public static parse(data: any): SongFull { public static parse(data: any): SongFull {
return { return checkType<SongFull>(
{
type: "SONG", type: "SONG",
videoId: traverse(data, "videoDetails", "videoId"), videoId: traverse(data, "videoDetails", "videoId"),
name: traverse(data, "videoDetails", "title"), name: traverse(data, "videoDetails", "title"),
@ -19,14 +23,17 @@ export default class SongParser {
description: traverse(data, "description"), description: traverse(data, "description"),
formats: traverse(data, "streamingData", "formats"), formats: traverse(data, "streamingData", "formats"),
adaptiveFormats: traverse(data, "streamingData", "adaptiveFormats") adaptiveFormats: traverse(data, "streamingData", "adaptiveFormats")
} },
SONG_FULL
)
} }
public static parseSearchResult(item: any): SongDetailed { public static parseSearchResult(item: any): SongDetailed {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverse(item, "flexColumns")
const videoId = traverse(item, "playlistItemData", "videoId") const videoId = traverse(item, "playlistItemData", "videoId")
return { return checkType<SongDetailed>(
{
type: "SONG", type: "SONG",
videoId: videoId instanceof Array ? null : videoId, videoId: videoId instanceof Array ? null : videoId,
name: traverse(flexColumns[0], "runs", "text"), name: traverse(flexColumns[0], "runs", "text"),
@ -40,14 +47,17 @@ export default class SongParser {
}, },
duration: Parser.parseDuration(traverse(flexColumns[1], "runs", "text").at(-1)), duration: Parser.parseDuration(traverse(flexColumns[1], "runs", "text").at(-1)),
thumbnails: [traverse(item, "thumbnails")].flat() thumbnails: [traverse(item, "thumbnails")].flat()
} },
SONG_DETAILED
)
} }
public static parseArtistSong(item: any): SongDetailed { public static parseArtistSong(item: any): SongDetailed {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverse(item, "flexColumns")
const videoId = traverse(item, "playlistItemData", "videoId") const videoId = traverse(item, "playlistItemData", "videoId")
return { return checkType<SongDetailed>(
{
type: "SONG", type: "SONG",
videoId: videoId instanceof Array ? null : videoId, videoId: videoId instanceof Array ? null : videoId,
name: traverse(flexColumns[0], "runs", "text"), name: traverse(flexColumns[0], "runs", "text"),
@ -64,7 +74,9 @@ export default class SongParser {
}, },
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")), duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")),
thumbnails: [traverse(item, "thumbnails")].flat() thumbnails: [traverse(item, "thumbnails")].flat()
} },
SONG_DETAILED
)
} }
public static parseArtistTopSong( public static parseArtistTopSong(
@ -74,7 +86,8 @@ export default class SongParser {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverse(item, "flexColumns")
const videoId = traverse(item, "playlistItemData", "videoId") const videoId = traverse(item, "playlistItemData", "videoId")
return { return checkType<Omit<SongDetailed, "duration">>(
{
type: "SONG", type: "SONG",
videoId: videoId instanceof Array ? null : videoId, videoId: videoId instanceof Array ? null : videoId,
name: traverse(flexColumns[0], "runs", "text"), name: traverse(flexColumns[0], "runs", "text"),
@ -84,7 +97,16 @@ export default class SongParser {
name: traverse(flexColumns[2], "browseId") 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( public static parseAlbumSong(
@ -96,7 +118,8 @@ export default class SongParser {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverse(item, "flexColumns")
const videoId = traverse(item, "playlistItemData", "videoId") const videoId = traverse(item, "playlistItemData", "videoId")
return { return checkType<SongDetailed>(
{
type: "SONG", type: "SONG",
videoId: videoId instanceof Array ? null : videoId, videoId: videoId instanceof Array ? null : videoId,
name: traverse(flexColumns[0], "runs", "text"), name: traverse(flexColumns[0], "runs", "text"),
@ -104,6 +127,8 @@ export default class SongParser {
album: albumBasic, album: albumBasic,
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")), duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")),
thumbnails thumbnails
} },
SONG_DETAILED
)
} }
} }

View File

@ -1,5 +1,7 @@
import checkType from "../utils/checkType"
import Parser from "./Parser" import Parser from "./Parser"
import traverse from "../traverse" import traverse from "../utils/traverse"
import { PLAYLIST_VIDEO } from "../interfaces"
import { VideoDetailed, VideoFull } from ".." import { VideoDetailed, VideoFull } from ".."
export default class VideoParser { export default class VideoParser {
@ -47,7 +49,8 @@ export default class VideoParser {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverse(item, "flexColumns")
const videoId = traverse(item, "playNavigationEndpoint", "videoId") const videoId = traverse(item, "playNavigationEndpoint", "videoId")
return { return checkType<Omit<VideoDetailed, "views">>(
{
type: "VIDEO", type: "VIDEO",
videoId: videoId instanceof Array ? null : videoId, videoId: videoId instanceof Array ? null : videoId,
name: traverse(flexColumns[0], "runs", "text"), name: traverse(flexColumns[0], "runs", "text"),
@ -57,6 +60,8 @@ export default class VideoParser {
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })), .map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })),
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")), duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")),
thumbnails: [traverse(item, "thumbnails")].flat() 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
}
}