diff --git a/src/YTMusic.ts b/src/YTMusic.ts index b84e350..3a32df4 100644 --- a/src/YTMusic.ts +++ b/src/YTMusic.ts @@ -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, diff --git a/src/__tests__/traversing.spec.ts b/src/__tests__/traversing.spec.ts index 92a5757..487a389 100644 --- a/src/__tests__/traversing.spec.ts +++ b/src/__tests__/traversing.spec.ts @@ -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 = OBJECT({ - url: STRING(), - width: NUMBER(), - height: NUMBER() -}) - -const ARTIST_BASIC: ObjectValidator = OBJECT({ - artistId: OR(STRING(), NULL()), - name: STRING() -}) - -const ALBUM_BASIC: ObjectValidator = OBJECT({ - albumId: STRING(), - name: STRING() -}) - -const SONG_DETAILED: ObjectValidator = 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 = 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 = OBJECT({ - artistId: STRING(), - name: STRING(), - type: STRING("ARTIST"), - thumbnails: LIST(THUMBNAIL_FULL) -}) - -const ALBUM_DETAILED: ObjectValidator = OBJECT({ - type: STRING("ALBUM"), - albumId: STRING(), - playlistId: STRING(), - name: STRING(), - artists: LIST(ARTIST_BASIC), - year: NUMBER(), - thumbnails: LIST(THUMBNAIL_FULL) -}) - -const SONG_FULL: ObjectValidator = 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 = 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 = 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 = 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 = OBJECT({ - type: STRING("PLAYLIST"), - playlistId: STRING(), - name: STRING(), - artist: ARTIST_BASIC, - videoCount: NUMBER(), - thumbnails: LIST(THUMBNAIL_FULL) -}) - -const PLAYLIST_VIDEO: ObjectValidator> = 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) diff --git a/src/interfaces.ts b/src/interfaces.ts new file mode 100644 index 0000000..d9a4136 --- /dev/null +++ b/src/interfaces.ts @@ -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 = OBJECT({ + url: STRING(), + width: NUMBER(), + height: NUMBER() +}) + +export const ARTIST_BASIC: ObjectValidator = OBJECT({ + artistId: OR(STRING(), NULL()), + name: STRING() +}) + +export const ALBUM_BASIC: ObjectValidator = OBJECT({ + albumId: STRING(), + name: STRING() +}) + +export const SONG_DETAILED: ObjectValidator = 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 = 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 = OBJECT({ + artistId: STRING(), + name: STRING(), + type: STRING("ARTIST"), + thumbnails: LIST(THUMBNAIL_FULL) +}) + +export const ALBUM_DETAILED: ObjectValidator = 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 = 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 = 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 = 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 = 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 = OBJECT({ + type: STRING("PLAYLIST"), + playlistId: STRING(), + name: STRING(), + artist: ARTIST_BASIC, + videoCount: NUMBER(), + thumbnails: LIST(THUMBNAIL_FULL) +}) + +export const PLAYLIST_VIDEO: ObjectValidator> = OBJECT({ + type: STRING("VIDEO"), + videoId: OR(STRING(), NULL()), + name: STRING(), + artists: LIST(ARTIST_BASIC), + duration: NUMBER(), + thumbnails: LIST(THUMBNAIL_FULL) +}) diff --git a/src/parsers/AlbumParser.ts b/src/parsers/AlbumParser.ts index 972822d..a60d723 100644 --- a/src/parsers/AlbumParser.ts +++ b/src/parsers/AlbumParser.ts @@ -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( + { + 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( + { + 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( + { + 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( + { + 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 + ) } } diff --git a/src/parsers/ArtistParser.ts b/src/parsers/ArtistParser.ts index be9761e..93066da 100644 --- a/src/parsers/ArtistParser.ts +++ b/src/parsers/ArtistParser.ts @@ -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( + { + 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( + { + type: "ARTIST", + artistId: traverse(item, "browseId"), + name: traverse(flexColumns[0], "runs", "text"), + thumbnails: [traverse(item, "thumbnails")].flat() + }, + ARTIST_DETAILED + ) } } diff --git a/src/parsers/PlaylistParser.ts b/src/parsers/PlaylistParser.ts index 982f10f..321f830 100644 --- a/src/parsers/PlaylistParser.ts +++ b/src/parsers/PlaylistParser.ts @@ -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( + { + 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( + { + 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 + ) } } diff --git a/src/parsers/SearchParser.ts b/src/parsers/SearchParser.ts index 23c6786..4abd599 100644 --- a/src/parsers/SearchParser.ts +++ b/src/parsers/SearchParser.ts @@ -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 ".." diff --git a/src/parsers/SongParser.ts b/src/parsers/SongParser.ts index 4da8f97..3b80213 100644 --- a/src/parsers/SongParser.ts +++ b/src/parsers/SongParser.ts @@ -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( + { + 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( + { + 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( + { + 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>( + { + 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( + { + 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 + ) } } diff --git a/src/parsers/VideoParser.ts b/src/parsers/VideoParser.ts index 05bc947..8e9e60f 100644 --- a/src/parsers/VideoParser.ts +++ b/src/parsers/VideoParser.ts @@ -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>( + { + 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 + ) } } diff --git a/src/utils/checkType.ts b/src/utils/checkType.ts new file mode 100644 index 0000000..6225b48 --- /dev/null +++ b/src/utils/checkType.ts @@ -0,0 +1,16 @@ +import Validator from "validate-any/dist/classes/Validator" +import { validate } from "validate-any" + +export default (data: T, validator: Validator): 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 + } +} diff --git a/src/traverse.ts b/src/utils/traverse.ts similarity index 100% rename from src/traverse.ts rename to src/utils/traverse.ts