new version of ytmusic-api abandoning validate-any

This commit is contained in:
zS1L3NT 2022-12-25 01:45:28 +08:00
parent c34ea72bfd
commit ed3b4127f8
15 changed files with 355 additions and 401 deletions

View File

@ -38,20 +38,21 @@ $ npm run test
## Built with ## Built with
- TypeScript - TypeScript
- [![@types/mocha](https://img.shields.io/github/package-json/dependency-version/zS1L3NT/ts-npm-ytmusic-api/dev/@types/mocha?style=flat-square)](https://npmjs.com/package/@types/mocha) - TypeScript
- [![@types/node](https://img.shields.io/github/package-json/dependency-version/zS1L3NT/ts-npm-ytmusic-api/dev/@types/node?style=flat-square)](https://npmjs.com/package/@types/node) - [![@types/json-schema](https://img.shields.io/badge/%40types%2Fjson--schema-%5E7.0.11-red?style=flat-square)](https://npmjs.com/package/@types/json-schema/v/7.0.11)
- [![@types/tough-cookie](https://img.shields.io/github/package-json/dependency-version/zS1L3NT/ts-npm-ytmusic-api/dev/@types/tough-cookie?style=flat-square)](https://npmjs.com/package/@types/tough-cookie) - [![@types/mocha](https://img.shields.io/badge/%40types%2Fmocha-%5E10.0.1-red?style=flat-square)](https://npmjs.com/package/@types/mocha/v/10.0.1)
- [![typescript](https://img.shields.io/github/package-json/dependency-version/zS1L3NT/ts-npm-ytmusic-api/dev/typescript?style=flat-square)](https://npmjs.com/package/typescript) - [![@types/node](https://img.shields.io/badge/%40types%2Fnode-%5E18.11.17-red?style=flat-square)](https://npmjs.com/package/@types/node/v/18.11.17)
- Axios - [![@types/tough-cookie](https://img.shields.io/badge/%40types%2Ftough--cookie-%5E4.0.2-red?style=flat-square)](https://npmjs.com/package/@types/tough-cookie/v/4.0.2)
- [![axios](https://img.shields.io/github/package-json/dependency-version/zS1L3NT/ts-npm-ytmusic-api/axios?style=flat-square)](https://npmjs.com/package/axios) - [![typescript](https://img.shields.io/badge/typescript-%5E4.9.4-red?style=flat-square)](https://npmjs.com/package/typescript/v/4.9.4)
- Tough Cookie - Mocha
- [![tough-cookie](https://img.shields.io/github/package-json/dependency-version/zS1L3NT/ts-npm-ytmusic-api/tough-cookie?style=flat-square)](https://npmjs.com/package/tough-cookie) - [![mocha](https://img.shields.io/badge/mocha-%5E10.2.0-red?style=flat-square)](https://npmjs.com/package/mocha/v/10.2.0)
- Mocha - [![mocha.parallel](https://img.shields.io/badge/mocha.parallel-%5E0.15.6-red?style=flat-square)](https://npmjs.com/package/mocha.parallel/v/0.15.6)
- [![mocha](https://img.shields.io/github/package-json/dependency-version/zS1L3NT/ts-npm-ytmusic-api/dev/mocha?style=flat-square)](https://npmjs.com/package/mocha) - [![ts-mocha](https://img.shields.io/badge/ts--mocha-%5E10.0.0-red?style=flat-square)](https://npmjs.com/package/ts-mocha/v/10.0.0)
- [![mocha.parallel](https://img.shields.io/github/package-json/dependency-version/zS1L3NT/ts-npm-ytmusic-api/dev/mocha.parallel?style=flat-square)](https://npmjs.com/package/mocha.parallel) - VuePress
- [![ts-mocha](https://img.shields.io/github/package-json/dependency-version/zS1L3NT/ts-npm-ytmusic-api/dev/ts-mocha?style=flat-square)](https://npmjs.com/package/ts-mocha) - [![@vuepress/plugin-search](https://img.shields.io/badge/%40vuepress%2Fplugin--search-%5E2.0.0--beta.46-red?style=flat-square)](https://npmjs.com/package/@vuepress/plugin-search/v/2.0.0-beta.46)
- VuePress - [![vuepress](https://img.shields.io/badge/vuepress-%5E2.0.0--beta.46-red?style=flat-square)](https://npmjs.com/package/vuepress/v/2.0.0-beta.46)
- [![@vuepress/plugin-search](https://img.shields.io/github/package-json/dependency-version/zS1L3NT/ts-npm-ytmusic-api/dev/@vuepress/plugin-search?style=flat-square)](https://npmjs.com/package/@vuepress/plugin-search) - Miscellaneous
- [![vuepress](https://img.shields.io/github/package-json/dependency-version/zS1L3NT/ts-npm-ytmusic-api/dev/vuepress?style=flat-square)](https://npmjs.com/package/vuepress) - [![axios](https://img.shields.io/badge/axios-%5E0.27.2-red?style=flat-square)](https://npmjs.com/package/axios/v/0.27.2)
- Miscellaneous - [![tough-cookie](https://img.shields.io/badge/tough--cookie-%5E4.1.2-red?style=flat-square)](https://npmjs.com/package/tough-cookie/v/4.1.2)
- [![validate-any](https://img.shields.io/github/package-json/dependency-version/zS1L3NT/ts-npm-ytmusic-api/validate-any?style=flat-square)](https://npmjs.com/package/validate-any) - [![zod](https://img.shields.io/badge/zod-%5E3.20.2-red?style=flat-square)](https://npmjs.com/package/zod/v/3.20.2)
- [![zod-to-json-schema](https://img.shields.io/badge/zod--to--json--schema-%5E3.20.1-red?style=flat-square)](https://npmjs.com/package/zod-to-json-schema/v/3.20.1)

View File

@ -1,6 +1,6 @@
{ {
"name": "ytmusic-api", "name": "ytmusic-api",
"version": "3.1.1", "version": "4.0.0",
"description": "YouTube Music API", "description": "YouTube Music API",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@ -17,18 +17,20 @@
}, },
"dependencies": { "dependencies": {
"axios": "^0.27.2", "axios": "^0.27.2",
"tough-cookie": "^4.0.0", "tough-cookie": "^4.1.2",
"validate-any": "1.3.2" "zod": "^3.20.2",
"zod-to-json-schema": "^3.20.1"
}, },
"devDependencies": { "devDependencies": {
"@types/mocha": "^9.1.1", "@types/json-schema": "^7.0.11",
"@types/node": "^17.0.36", "@types/mocha": "^10.0.1",
"@types/node": "^18.11.17",
"@types/tough-cookie": "^4.0.2", "@types/tough-cookie": "^4.0.2",
"@vuepress/plugin-search": "^2.0.0-beta.46", "@vuepress/plugin-search": "^2.0.0-beta.46",
"mocha": "^10.0.0", "mocha": "^10.2.0",
"mocha.parallel": "^0.15.6", "mocha.parallel": "^0.15.6",
"ts-mocha": "^10.0.0", "ts-mocha": "^10.0.0",
"typescript": "^4.7.2", "typescript": "^4.9.4",
"vuepress": "^2.0.0-beta.46" "vuepress": "^2.0.0-beta.46"
}, },
"keywords": [ "keywords": [

View File

@ -1,33 +1,37 @@
lockfileVersion: 5.4 lockfileVersion: 5.4
specifiers: specifiers:
'@types/mocha': ^9.1.1 '@types/json-schema': ^7.0.11
'@types/node': ^17.0.36 '@types/mocha': ^10.0.1
'@types/node': ^18.11.17
'@types/tough-cookie': ^4.0.2 '@types/tough-cookie': ^4.0.2
'@vuepress/plugin-search': ^2.0.0-beta.46 '@vuepress/plugin-search': ^2.0.0-beta.46
axios: ^0.27.2 axios: ^0.27.2
mocha: ^10.0.0 mocha: ^10.2.0
mocha.parallel: ^0.15.6 mocha.parallel: ^0.15.6
tough-cookie: ^4.0.0 tough-cookie: ^4.1.2
ts-mocha: ^10.0.0 ts-mocha: ^10.0.0
typescript: ^4.7.2 typescript: ^4.9.4
validate-any: 1.3.2
vuepress: ^2.0.0-beta.46 vuepress: ^2.0.0-beta.46
zod: ^3.20.2
zod-to-json-schema: ^3.20.1
dependencies: dependencies:
axios: 0.27.2 axios: 0.27.2
tough-cookie: 4.0.0 tough-cookie: 4.1.2
validate-any: 1.3.2 zod: 3.20.2
zod-to-json-schema: 3.20.1_zod@3.20.2
devDependencies: devDependencies:
'@types/mocha': 9.1.1 '@types/json-schema': 7.0.11
'@types/node': 17.0.36 '@types/mocha': 10.0.1
'@types/node': 18.11.17
'@types/tough-cookie': 4.0.2 '@types/tough-cookie': 4.0.2
'@vuepress/plugin-search': 2.0.0-beta.46 '@vuepress/plugin-search': 2.0.0-beta.46
mocha: 10.0.0 mocha: 10.2.0
mocha.parallel: 0.15.6_mocha@10.0.0 mocha.parallel: 0.15.6_mocha@10.2.0
ts-mocha: 10.0.0_mocha@10.0.0 ts-mocha: 10.0.0_mocha@10.2.0
typescript: 4.7.2 typescript: 4.9.4
vuepress: 2.0.0-beta.46 vuepress: 2.0.0-beta.46
packages: packages:
@ -83,7 +87,11 @@ packages:
/@types/fs-extra/9.0.13: /@types/fs-extra/9.0.13:
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
dependencies: dependencies:
'@types/node': 17.0.36 '@types/node': 18.11.17
dev: true
/@types/json-schema/7.0.11:
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
dev: true dev: true
/@types/json5/0.0.29: /@types/json5/0.0.29:
@ -106,26 +114,22 @@ packages:
resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==} resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==}
dev: true dev: true
/@types/mocha/9.1.1: /@types/mocha/10.0.1:
resolution: {integrity: sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==} resolution: {integrity: sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==}
dev: true dev: true
/@types/ms/0.7.31: /@types/ms/0.7.31:
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
dev: true dev: true
/@types/node/17.0.36: /@types/node/18.11.17:
resolution: {integrity: sha512-V3orv+ggDsWVHP99K3JlwtH20R7J4IhI1Kksgc+64q5VxgfRkQG8Ws3MFm/FZOKDYGy9feGFlZ70/HpCNe9QaA==} resolution: {integrity: sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==}
dev: true dev: true
/@types/tough-cookie/4.0.2: /@types/tough-cookie/4.0.2:
resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==} resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==}
dev: true dev: true
/@ungap/promise-all-settled/1.1.2:
resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==}
dev: true
/@vitejs/plugin-vue/2.3.3_vite@2.9.9+vue@3.2.36: /@vitejs/plugin-vue/2.3.3_vite@2.9.9+vue@3.2.36:
resolution: {integrity: sha512-SmQLDyhz+6lGJhPELsBdzXGc+AcaT8stgkbiTFGpXPe8Tl1tJaBw1A6pxDqDuRsVkD8uscrkx3hA7QDOoKYtyw==} resolution: {integrity: sha512-SmQLDyhz+6lGJhPELsBdzXGc+AcaT8stgkbiTFGpXPe8Tl1tJaBw1A6pxDqDuRsVkD8uscrkx3hA7QDOoKYtyw==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@ -588,7 +592,7 @@ packages:
/axios/0.27.2: /axios/0.27.2:
resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
dependencies: dependencies:
follow-redirects: 1.14.9 follow-redirects: 1.15.2
form-data: 4.0.0 form-data: 4.0.0
transitivePeerDependencies: transitivePeerDependencies:
- debug - debug
@ -747,7 +751,7 @@ packages:
dev: false dev: false
/concat-map/0.0.1: /concat-map/0.0.1:
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true dev: true
/connect-history-api-fallback/1.6.0: /connect-history-api-fallback/1.6.0:
@ -1132,8 +1136,8 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/follow-redirects/1.14.9: /follow-redirects/1.15.2:
resolution: {integrity: sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==} resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
peerDependencies: peerDependencies:
debug: '*' debug: '*'
@ -1510,22 +1514,21 @@ packages:
minimist: 1.2.6 minimist: 1.2.6
dev: true dev: true
/mocha.parallel/0.15.6_mocha@10.0.0: /mocha.parallel/0.15.6_mocha@10.2.0:
resolution: {integrity: sha512-pWph+QieKGjk7cHY2hB78wyKJDOQLyOMDuBLQLrFL7riJb8qbQBlCY3XztFHv0D1d4I1gCpiwFNjd4LhVOXPew==} resolution: {integrity: sha512-pWph+QieKGjk7cHY2hB78wyKJDOQLyOMDuBLQLrFL7riJb8qbQBlCY3XztFHv0D1d4I1gCpiwFNjd4LhVOXPew==}
peerDependencies: peerDependencies:
mocha: '>=2.2.5' mocha: '>=2.2.5'
dependencies: dependencies:
bluebird: 2.11.0 bluebird: 2.11.0
mocha: 10.0.0 mocha: 10.2.0
semaphore: 1.1.0 semaphore: 1.1.0
dev: true dev: true
/mocha/10.0.0: /mocha/10.2.0:
resolution: {integrity: sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==} resolution: {integrity: sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==}
engines: {node: '>= 14.0.0'} engines: {node: '>= 14.0.0'}
hasBin: true hasBin: true
dependencies: dependencies:
'@ungap/promise-all-settled': 1.1.2
ansi-colors: 4.1.1 ansi-colors: 4.1.1
browser-stdout: 1.3.1 browser-stdout: 1.3.1
chokidar: 3.5.3 chokidar: 3.5.3
@ -1591,7 +1594,7 @@ packages:
dev: true dev: true
/once/1.4.0: /once/1.4.0:
resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=} resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies: dependencies:
wrappy: 1.0.2 wrappy: 1.0.2
dev: true dev: true
@ -1638,7 +1641,7 @@ packages:
dev: true dev: true
/path-is-absolute/1.0.1: /path-is-absolute/1.0.1:
resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=} resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
@ -1692,6 +1695,10 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: false dev: false
/querystringify/2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
dev: false
/queue-microtask/1.2.3: /queue-microtask/1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true dev: true
@ -1719,10 +1726,14 @@ packages:
dev: true dev: true
/require-directory/2.1.1: /require-directory/2.1.1:
resolution: {integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I=} resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/requires-port/1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: false
/resolve/1.22.0: /resolve/1.22.0:
resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==} resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==}
hasBin: true hasBin: true
@ -1916,27 +1927,28 @@ packages:
resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==}
dev: true dev: true
/tough-cookie/4.0.0: /tough-cookie/4.1.2:
resolution: {integrity: sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==} resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
dependencies: dependencies:
psl: 1.8.0 psl: 1.8.0
punycode: 2.1.1 punycode: 2.1.1
universalify: 0.1.2 universalify: 0.2.0
url-parse: 1.5.10
dev: false dev: false
/ts-debounce/4.0.0: /ts-debounce/4.0.0:
resolution: {integrity: sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==} resolution: {integrity: sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==}
dev: true dev: true
/ts-mocha/10.0.0_mocha@10.0.0: /ts-mocha/10.0.0_mocha@10.2.0:
resolution: {integrity: sha512-VRfgDO+iiuJFlNB18tzOfypJ21xn2xbuZyDvJvqpTbWgkAgD17ONGr8t+Tl8rcBtOBdjXp5e/Rk+d39f7XBHRw==} resolution: {integrity: sha512-VRfgDO+iiuJFlNB18tzOfypJ21xn2xbuZyDvJvqpTbWgkAgD17ONGr8t+Tl8rcBtOBdjXp5e/Rk+d39f7XBHRw==}
engines: {node: '>= 6.X.X'} engines: {node: '>= 6.X.X'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
mocha: ^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X || ^9.X.X || ^10.X.X mocha: ^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X || ^9.X.X || ^10.X.X
dependencies: dependencies:
mocha: 10.0.0 mocha: 10.2.0
ts-node: 7.0.1 ts-node: 7.0.1
optionalDependencies: optionalDependencies:
tsconfig-paths: 3.14.1 tsconfig-paths: 3.14.1
@ -1968,8 +1980,8 @@ packages:
dev: true dev: true
optional: true optional: true
/typescript/4.7.2: /typescript/4.9.4:
resolution: {integrity: sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==} resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==}
engines: {node: '>=4.2.0'} engines: {node: '>=4.2.0'}
hasBin: true hasBin: true
dev: true dev: true
@ -1978,8 +1990,8 @@ packages:
resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==}
dev: true dev: true
/universalify/0.1.2: /universalify/0.2.0:
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'} engines: {node: '>= 4.0.0'}
dev: false dev: false
@ -1993,14 +2005,17 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
/url-parse/1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
dependencies:
querystringify: 2.2.0
requires-port: 1.0.0
dev: false
/util-deprecate/1.0.2: /util-deprecate/1.0.2:
resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=} resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=}
dev: true dev: true
/validate-any/1.3.2:
resolution: {integrity: sha512-L1SpFcOpnFR4u7slwdtVRecyI/SAun/QvoFM3pJw+hE/4/D5LJMs2wcf8OYVOM8CpNab/zsXb58w9ck3XkIYNg==}
dev: false
/vite/2.9.9: /vite/2.9.9:
resolution: {integrity: sha512-ffaam+NgHfbEmfw/Vuh6BHKKlI/XIAhxE5QSS7gFLIngxg171mg1P3a4LSRME0z2ZU1ScxoKzphkipcYwSD5Ew==} resolution: {integrity: sha512-ffaam+NgHfbEmfw/Vuh6BHKKlI/XIAhxE5QSS7gFLIngxg171mg1P3a4LSRME0z2ZU1ScxoKzphkipcYwSD5Ew==}
engines: {node: '>=12.2.0'} engines: {node: '>=12.2.0'}
@ -2123,7 +2138,7 @@ packages:
dev: true dev: true
/wrappy/1.0.2: /wrappy/1.0.2:
resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: true dev: true
/y18n/5.0.8: /y18n/5.0.8:
@ -2168,3 +2183,15 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true dev: true
/zod-to-json-schema/3.20.1_zod@3.20.2:
resolution: {integrity: sha512-U+zmNJUKqzv92E+LdEYv0g2LxBLks4HAwfC6cue8jXby5PAeSEPGO4xV9Sl4zmLYyFvJkm0FOfOs6orUO+AI1w==}
peerDependencies:
zod: ^3.20.0
dependencies:
zod: 3.20.2
dev: false
/zod/3.20.2:
resolution: {integrity: sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==}
dev: false

View File

@ -1,16 +1,17 @@
import axios, { AxiosInstance } from "axios" import axios, { AxiosInstance } from "axios"
import { Cookie, CookieJar } from "tough-cookie" import { Cookie, CookieJar } from "tough-cookie"
import { z } from "zod"
import {
AlbumDetailed, AlbumFull, ArtistDetailed, ArtistFull, PlaylistFull, SearchResult, SongDetailed,
SongFull, VideoDetailed, VideoFull
} from "./"
import AlbumParser from "./parsers/AlbumParser" import AlbumParser from "./parsers/AlbumParser"
import ArtistParser from "./parsers/ArtistParser" import ArtistParser from "./parsers/ArtistParser"
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 VideoParser from "./parsers/VideoParser" import VideoParser from "./parsers/VideoParser"
import {
AlbumDetailed, AlbumFull, ArtistDetailed, ArtistFull, PlaylistFull, SearchResult, SongDetailed,
SongFull, VideoDetailed, VideoFull
} from "./schemas"
import traverse from "./utils/traverse" import traverse from "./utils/traverse"
import traverseList from "./utils/traverseList" import traverseList from "./utils/traverseList"
import traverseString from "./utils/traverseString" import traverseString from "./utils/traverseString"
@ -122,7 +123,7 @@ export default class YTMusic {
const headers: Record<string, any> = { const headers: Record<string, any> = {
...this.client.defaults.headers, ...this.client.defaults.headers,
"x-origin": this.client.defaults.baseURL, "x-origin": this.client.defaults.baseURL,
"X-Goog-Visitor-Id": this.config.VISITOR_DATA || '', "X-Goog-Visitor-Id": this.config.VISITOR_DATA || "",
"X-YouTube-Client-Name": this.config.INNERTUBE_CONTEXT_CLIENT_NAME, "X-YouTube-Client-Name": this.config.INNERTUBE_CONTEXT_CLIENT_NAME,
"X-YouTube-Client-Version": this.config.INNERTUBE_CLIENT_VERSION, "X-YouTube-Client-Version": this.config.INNERTUBE_CLIENT_VERSION,
"X-YouTube-Device": this.config.DEVICE, "X-YouTube-Device": this.config.DEVICE,
@ -213,7 +214,7 @@ export default class YTMusic {
* *
* @param query Query string * @param query Query string
*/ */
public async search(query: string): Promise<SearchResult[]> { public async search(query: string): Promise<z.infer<typeof SearchResult>[]> {
const searchData = await this.constructRequest("search", { const searchData = await this.constructRequest("search", {
query, query,
params: null params: null
@ -227,7 +228,7 @@ export default class YTMusic {
* *
* @param query Query string * @param query Query string
*/ */
public async searchSongs(query: string): Promise<SongDetailed[]> { public async searchSongs(query: string): Promise<z.infer<typeof SongDetailed>[]> {
const searchData = await this.constructRequest("search", { const searchData = await this.constructRequest("search", {
query, query,
params: "Eg-KAQwIARAAGAAgACgAMABqChAEEAMQCRAFEAo%3D" params: "Eg-KAQwIARAAGAAgACgAMABqChAEEAMQCRAFEAo%3D"
@ -243,7 +244,7 @@ export default class YTMusic {
* *
* @param query Query string * @param query Query string
*/ */
public async searchVideos(query: string): Promise<VideoDetailed[]> { public async searchVideos(query: string): Promise<z.infer<typeof VideoDetailed>[]> {
const searchData = await this.constructRequest("search", { const searchData = await this.constructRequest("search", {
query, query,
params: "Eg-KAQwIABABGAAgACgAMABqChAEEAMQCRAFEAo%3D" params: "Eg-KAQwIABABGAAgACgAMABqChAEEAMQCRAFEAo%3D"
@ -259,7 +260,7 @@ export default class YTMusic {
* *
* @param query Query string * @param query Query string
*/ */
public async searchArtists(query: string): Promise<ArtistDetailed[]> { public async searchArtists(query: string): Promise<z.infer<typeof ArtistDetailed>[]> {
const searchData = await this.constructRequest("search", { const searchData = await this.constructRequest("search", {
query, query,
params: "Eg-KAQwIABAAGAAgASgAMABqChAEEAMQCRAFEAo%3D" params: "Eg-KAQwIABAAGAAgASgAMABqChAEEAMQCRAFEAo%3D"
@ -275,7 +276,7 @@ export default class YTMusic {
* *
* @param query Query string * @param query Query string
*/ */
public async searchAlbums(query: string): Promise<AlbumDetailed[]> { public async searchAlbums(query: string): Promise<z.infer<typeof AlbumDetailed>[]> {
const searchData = await this.constructRequest("search", { const searchData = await this.constructRequest("search", {
query, query,
params: "Eg-KAQwIABAAGAEgACgAMABqChAEEAMQCRAFEAo%3D" params: "Eg-KAQwIABAAGAEgACgAMABqChAEEAMQCRAFEAo%3D"
@ -291,7 +292,7 @@ export default class YTMusic {
* *
* @param query Query string * @param query Query string
*/ */
public async searchPlaylists(query: string): Promise<PlaylistFull[]> { public async searchPlaylists(query: string): Promise<z.infer<typeof PlaylistFull>[]> {
const searchData = await this.constructRequest("search", { const searchData = await this.constructRequest("search", {
query, query,
params: "Eg-KAQwIABAAGAAgACgBMABqChAEEAMQCRAFEAo%3D" params: "Eg-KAQwIABAAGAAgACgBMABqChAEEAMQCRAFEAo%3D"
@ -308,7 +309,7 @@ export default class YTMusic {
* @param videoId Video ID * @param videoId Video ID
* @returns Song Data * @returns Song Data
*/ */
public async getSong(videoId: string): Promise<SongFull> { public async getSong(videoId: string): Promise<z.infer<typeof SongFull>> {
if (!videoId.match(/^[a-zA-Z0-9-_]{11}$/)) throw new Error("Invalid videoId") if (!videoId.match(/^[a-zA-Z0-9-_]{11}$/)) throw new Error("Invalid videoId")
const data = await this.constructRequest("player", { videoId }) const data = await this.constructRequest("player", { videoId })
@ -323,7 +324,7 @@ export default class YTMusic {
* @param videoId Video ID * @param videoId Video ID
* @returns Video Data * @returns Video Data
*/ */
public async getVideo(videoId: string): Promise<VideoFull> { public async getVideo(videoId: string): Promise<z.infer<typeof VideoFull>> {
if (!videoId.match(/^[a-zA-Z0-9-_]{11}$/)) throw new Error("Invalid videoId") if (!videoId.match(/^[a-zA-Z0-9-_]{11}$/)) throw new Error("Invalid videoId")
const data = await this.constructRequest("player", { videoId }) const data = await this.constructRequest("player", { videoId })
@ -338,8 +339,10 @@ export default class YTMusic {
* @param artistId Artist ID * @param artistId Artist ID
* @returns Artist Data * @returns Artist Data
*/ */
public async getArtist(artistId: string): Promise<ArtistFull> { public async getArtist(artistId: string): Promise<z.infer<typeof ArtistFull>> {
const data = await this.constructRequest("browse", { browseId: artistId }) const data = await this.constructRequest("browse", {
browseId: artistId
})
return ArtistParser.parse(data, artistId) return ArtistParser.parse(data, artistId)
} }
@ -350,13 +353,17 @@ export default class YTMusic {
* @param artistId Artist ID * @param artistId Artist ID
* @returns Artist's Songs * @returns Artist's Songs
*/ */
public async getArtistSongs(artistId: string): Promise<SongDetailed[]> { public async getArtistSongs(artistId: string): Promise<z.infer<typeof SongDetailed>[]> {
const artistData = await this.constructRequest("browse", { browseId: artistId }) const artistData = await this.constructRequest("browse", {
browseId: artistId
})
const browseToken = traverse(artistData, "musicShelfRenderer", "title", "browseId") const browseToken = traverse(artistData, "musicShelfRenderer", "title", "browseId")
if (browseToken instanceof Array) return [] if (browseToken instanceof Array) return []
const songsData = await this.constructRequest("browse", { browseId: browseToken }) const songsData = await this.constructRequest("browse", {
browseId: browseToken
})
const continueToken = traverse(songsData, "continuation") const continueToken = traverse(songsData, "continuation")
const moreSongsData = await this.constructRequest( const moreSongsData = await this.constructRequest(
"browse", "browse",
@ -376,8 +383,10 @@ export default class YTMusic {
* @param artistId Artist ID * @param artistId Artist ID
* @returns Artist's Albums * @returns Artist's Albums
*/ */
public async getArtistAlbums(artistId: string): Promise<AlbumDetailed[]> { public async getArtistAlbums(artistId: string): Promise<z.infer<typeof AlbumDetailed>[]> {
const artistData = await this.constructRequest("browse", { browseId: artistId }) const artistData = await this.constructRequest("browse", {
browseId: artistId
})
const artistAlbumsData = traverseList(artistData, "musicCarouselShelfRenderer")[0] const artistAlbumsData = traverseList(artistData, "musicCarouselShelfRenderer")[0]
const browseBody = traverse(artistAlbumsData, "moreContentButton", "browseEndpoint") const browseBody = traverse(artistAlbumsData, "moreContentButton", "browseEndpoint")
@ -397,8 +406,10 @@ export default class YTMusic {
* @param albumId Album ID * @param albumId Album ID
* @returns Album Data * @returns Album Data
*/ */
public async getAlbum(albumId: string): Promise<AlbumFull> { public async getAlbum(albumId: string): Promise<z.infer<typeof AlbumFull>> {
const data = await this.constructRequest("browse", { browseId: albumId }) const data = await this.constructRequest("browse", {
browseId: albumId
})
return AlbumParser.parse(data, albumId) return AlbumParser.parse(data, albumId)
} }
@ -409,9 +420,11 @@ export default class YTMusic {
* @param playlistId Playlist ID * @param playlistId Playlist ID
* @returns Playlist Data * @returns Playlist Data
*/ */
public async getPlaylist(playlistId: string): Promise<PlaylistFull> { public async getPlaylist(playlistId: string): Promise<z.infer<typeof PlaylistFull>> {
if (playlistId.startsWith("PL")) playlistId = "VL" + playlistId if (playlistId.startsWith("PL")) playlistId = "VL" + playlistId
const data = await this.constructRequest("browse", { browseId: playlistId }) const data = await this.constructRequest("browse", {
browseId: playlistId
})
return PlaylistParser.parse(data, playlistId) return PlaylistParser.parse(data, playlistId)
} }
@ -422,9 +435,11 @@ export default class YTMusic {
* @param playlistId Playlist ID * @param playlistId Playlist ID
* @returns Playlist's Videos * @returns Playlist's Videos
*/ */
public async getPlaylistVideos(playlistId: string): Promise<VideoDetailed[]> { public async getPlaylistVideos(playlistId: string): Promise<z.infer<typeof VideoDetailed>[]> {
if (playlistId.startsWith("PL")) playlistId = "VL" + playlistId if (playlistId.startsWith("PL")) playlistId = "VL" + playlistId
const playlistData = await this.constructRequest("browse", { browseId: playlistId }) const playlistData = await this.constructRequest("browse", {
browseId: playlistId
})
const songs = traverseList( const songs = traverseList(
playlistData, playlistData,

View File

@ -1,22 +1,21 @@
import assert from "assert" import assert from "assert"
import describeParallel from "mocha.parallel" import describeParallel from "mocha.parallel"
import { iValidationError, LIST, STRING, validate } from "validate-any" import { z } from "zod"
import Validator from "validate-any/dist/classes/Validator"
import YTMusic from "../"
import { import {
ALBUM_DETAILED, ALBUM_FULL, ARTIST_DETAILED, ARTIST_FULL, PLAYLIST_FULL, SONG_DETAILED, AlbumDetailed, AlbumFull, ArtistDetailed, ArtistFull, PlaylistFull, SongDetailed, SongFull,
SONG_FULL, VIDEO_DETAILED, VIDEO_FULL VideoDetailed, VideoFull
} from "../interfaces" } from "../schemas"
import YTMusic from "../YTMusic"
const issues: iValidationError[][] = [] const errors = <z.ZodError<any>[]>[]
const queries = ["Lilac", "Weekend", "Eill", "Eminem", "Lisa Hannigan"] const queries = ["Lilac", "Weekend", "Eill", "Eminem", "Lisa Hannigan"]
const expect = (data: any, validator: Validator<any>) => { const expect = (data: any, schema: z.Schema) => {
const { errors } = validate(data, validator) const result = schema.safeParse(data)
if (errors.length > 0) { if (!result.success) {
issues.push(errors) errors.push(result.error)
} }
assert.equal(errors.length, 0) assert.equal(result.success, true)
} }
const ytmusic = new YTMusic() const ytmusic = new YTMusic()
@ -26,90 +25,95 @@ queries.forEach(query => {
describeParallel("Query: " + query, () => { describeParallel("Query: " + query, () => {
it("Search suggestions", async () => { it("Search suggestions", async () => {
const suggestions = await ytmusic.getSearchSuggestions(query) const suggestions = await ytmusic.getSearchSuggestions(query)
expect(suggestions, LIST(STRING())) expect(suggestions, z.array(z.string()))
}) })
it("Search Songs", async () => { it("Search Songs", async () => {
const songs = await ytmusic.searchSongs(query) const songs = await ytmusic.searchSongs(query)
expect(songs, LIST(SONG_DETAILED)) expect(songs, z.array(SongDetailed))
}) })
it("Search Videos", async () => { it("Search Videos", async () => {
const videos = await ytmusic.searchVideos(query) const videos = await ytmusic.searchVideos(query)
expect(videos, LIST(VIDEO_DETAILED)) expect(videos, z.array(VideoDetailed))
}) })
it("Search Artists", async () => { it("Search Artists", async () => {
const artists = await ytmusic.searchArtists(query) const artists = await ytmusic.searchArtists(query)
expect(artists, LIST(ARTIST_DETAILED)) expect(artists, z.array(ArtistDetailed))
}) })
it("Search Albums", async () => { it("Search Albums", async () => {
const albums = await ytmusic.searchAlbums(query) const albums = await ytmusic.searchAlbums(query)
expect(albums, LIST(ALBUM_DETAILED)) expect(albums, z.array(AlbumDetailed))
}) })
it("Search Playlists", async () => { it("Search Playlists", async () => {
const playlists = await ytmusic.searchPlaylists(query) const playlists = await ytmusic.searchPlaylists(query)
expect(playlists, LIST(PLAYLIST_FULL)) expect(playlists, z.array(PlaylistFull))
}) })
it("Search All", async () => { it("Search All", async () => {
const results = await ytmusic.search(query) const results = await ytmusic.search(query)
expect( expect(
results, results,
LIST(ALBUM_DETAILED, ARTIST_DETAILED, PLAYLIST_FULL, SONG_DETAILED, VIDEO_DETAILED) z.array(
AlbumDetailed.or(ArtistDetailed)
.or(PlaylistFull)
.or(SongDetailed)
.or(VideoDetailed)
)
) )
}) })
it("Get details of the first song result", async () => { it("Get details of the first song result", async () => {
const songs = await ytmusic.searchSongs(query) const songs = await ytmusic.searchSongs(query)
const song = await ytmusic.getSong(songs[0]!.videoId) const song = await ytmusic.getSong(songs[0]!.videoId)
expect(song, SONG_FULL) expect(song, SongFull)
}) })
it("Get details of the first video result", async () => { it("Get details of the first video result", async () => {
const videos = await ytmusic.searchVideos(query) const videos = await ytmusic.searchVideos(query)
const video = await ytmusic.getVideo(videos[0]!.videoId) const video = await ytmusic.getVideo(videos[0]!.videoId)
expect(video, VIDEO_FULL) expect(video, VideoFull)
}) })
it("Get details of the first artist result", async () => { it("Get details of the first artist result", async () => {
const artists = await ytmusic.searchArtists(query) const artists = await ytmusic.searchArtists(query)
const artist = await ytmusic.getArtist(artists[0]!.artistId) const artist = await ytmusic.getArtist(artists[0]!.artistId)
expect(artist, ARTIST_FULL) expect(artist, ArtistFull)
}) })
it("Get the songs of the first artist result", async () => { it("Get the songs of the first artist result", async () => {
const artists = await ytmusic.searchArtists(query) const artists = await ytmusic.searchArtists(query)
const songs = await ytmusic.getArtistSongs(artists[0]!.artistId) const songs = await ytmusic.getArtistSongs(artists[0]!.artistId)
expect(songs, LIST(SONG_DETAILED)) expect(songs, z.array(SongDetailed))
}) })
it("Get the albums of the first artist result", async () => { it("Get the albums of the first artist result", async () => {
const artists = await ytmusic.searchArtists(query) const artists = await ytmusic.searchArtists(query)
const albums = await ytmusic.getArtistAlbums(artists[0]!.artistId) const albums = await ytmusic.getArtistAlbums(artists[0]!.artistId)
expect(albums, LIST(ALBUM_DETAILED)) expect(albums, z.array(AlbumDetailed))
}) })
it("Get details of the first album result", async () => { it("Get details of the first album result", async () => {
const albums = await ytmusic.searchAlbums(query) const albums = await ytmusic.searchAlbums(query)
const album = await ytmusic.getAlbum(albums[0]!.albumId) const album = await ytmusic.getAlbum(albums[0]!.albumId)
expect(album, ALBUM_FULL) expect(album, AlbumFull)
}) })
it("Get details of the first playlist result", async () => { it("Get details of the first playlist result", async () => {
const playlists = await ytmusic.searchPlaylists(query) const playlists = await ytmusic.searchPlaylists(query)
const playlist = await ytmusic.getPlaylist(playlists[0]!.playlistId) const playlist = await ytmusic.getPlaylist(playlists[0]!.playlistId)
expect(playlist, PLAYLIST_FULL) expect(playlist, PlaylistFull)
}) })
it("Get the videos of the first playlist result", async () => { it("Get the videos of the first playlist result", async () => {
const playlists = await ytmusic.searchPlaylists(query) const playlists = await ytmusic.searchPlaylists(query)
const videos = await ytmusic.getPlaylistVideos(playlists[0]!.playlistId) const videos = await ytmusic.getPlaylistVideos(playlists[0]!.playlistId)
expect(videos, LIST(VIDEO_DETAILED)) expect(videos, z.array(VideoDetailed))
}) })
}) })
}) })
after(() => console.log("Issues:", issues)) after(() => console.log("Issues:", errors))

View File

@ -1,93 +0,0 @@
import YTMusic from "./YTMusic"
export interface ThumbnailFull {
url: string
width: number
height: number
}
export interface SongDetailed {
type: "SONG"
videoId: string
name: string
artists: ArtistBasic[]
album: AlbumBasic
duration: number
thumbnails: ThumbnailFull[]
}
export interface SongFull extends Omit<SongDetailed, "album"> {
description: string
formats: any[]
adaptiveFormats: any[]
}
export interface VideoDetailed {
type: "VIDEO"
videoId: string
name: string
artists: ArtistBasic[]
duration: number
thumbnails: ThumbnailFull[]
}
export interface VideoFull extends VideoDetailed {
description: string
unlisted: boolean
familySafe: boolean
paid: boolean
tags: string[]
}
export interface ArtistBasic {
artistId: string
name: string
}
export interface ArtistDetailed extends ArtistBasic {
type: "ARTIST"
artistId: string
thumbnails: ThumbnailFull[]
}
export interface ArtistFull extends ArtistDetailed {
description: string
topSongs: Omit<SongDetailed, "duration">[]
topAlbums: AlbumDetailed[]
}
export interface AlbumBasic {
albumId: string
name: string
}
export interface AlbumDetailed extends AlbumBasic {
type: "ALBUM"
playlistId: string
artists: ArtistBasic[]
year: number | null
thumbnails: ThumbnailFull[]
}
export interface AlbumFull extends AlbumDetailed {
description: string
songs: SongDetailed[]
}
export interface PlaylistFull {
type: "PLAYLIST"
playlistId: string
name: string
artist: ArtistBasic
videoCount: number
thumbnails: ThumbnailFull[]
}
export type SearchResult =
| SongDetailed
| VideoDetailed
| AlbumDetailed
| ArtistDetailed
| PlaylistFull
export default YTMusic

View File

@ -1,125 +0,0 @@
import { BOOLEAN, LIST, NULL, NUMBER, OBJECT, OR, STRING } from "validate-any"
import ObjectValidator from "validate-any/dist/validators/ObjectValidator"
import {
AlbumBasic, AlbumDetailed, AlbumFull, ArtistBasic, ArtistDetailed, ArtistFull, PlaylistFull,
SongDetailed, SongFull, ThumbnailFull, VideoDetailed, VideoFull
} from "./"
export const THUMBNAIL_FULL: ObjectValidator<ThumbnailFull> = OBJECT({
url: STRING(),
width: NUMBER(),
height: NUMBER()
})
export const ARTIST_BASIC: ObjectValidator<ArtistBasic> = OBJECT({
artistId: STRING(),
name: STRING()
})
export const ALBUM_BASIC: ObjectValidator<AlbumBasic> = OBJECT({
albumId: STRING(),
name: STRING()
})
export const SONG_DETAILED: ObjectValidator<SongDetailed> = OBJECT({
type: STRING("SONG"),
videoId: STRING(),
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: STRING(),
name: STRING(),
artists: LIST(ARTIST_BASIC),
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: OR(NUMBER(), NULL()),
thumbnails: LIST(THUMBNAIL_FULL)
})
export const SONG_FULL: ObjectValidator<SongFull> = OBJECT({
type: STRING("SONG"),
videoId: STRING(),
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: STRING(),
name: STRING(),
artists: LIST(ARTIST_BASIC),
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: STRING(),
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: OR(NUMBER(), NULL()),
thumbnails: LIST(THUMBNAIL_FULL),
description: STRING(),
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)
})

View File

@ -1,5 +1,4 @@
import { AlbumBasic, AlbumDetailed, AlbumFull, ArtistBasic } from "../" import { AlbumBasic, AlbumDetailed, AlbumFull, ArtistBasic } from "../schemas"
import { ALBUM_DETAILED, ALBUM_FULL } from "../interfaces"
import checkType from "../utils/checkType" import checkType from "../utils/checkType"
import traverseList from "../utils/traverseList" import traverseList from "../utils/traverseList"
import traverseString from "../utils/traverseString" import traverseString from "../utils/traverseString"
@ -19,7 +18,7 @@ export default class AlbumParser {
})) }))
const thumbnails = traverseList(data, "header", "thumbnails") const thumbnails = traverseList(data, "header", "thumbnails")
return checkType<AlbumFull>( return checkType(
{ {
type: "ALBUM", type: "ALBUM",
...albumBasic, ...albumBasic,
@ -34,14 +33,14 @@ export default class AlbumParser {
SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails) SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails)
) )
}, },
ALBUM_FULL AlbumFull
) )
} }
public static parseSearchResult(item: any): AlbumDetailed { public static parseSearchResult(item: any): AlbumDetailed {
const flexColumns = traverseList(item, "flexColumns") const flexColumns = traverseList(item, "flexColumns")
return checkType<AlbumDetailed>( return checkType(
{ {
type: "ALBUM", type: "ALBUM",
albumId: traverseString(item, "browseId")(-1), albumId: traverseString(item, "browseId")(-1),
@ -56,12 +55,12 @@ export default class AlbumParser {
name: traverseString(flexColumns[0], "runs", "text")(), name: traverseString(flexColumns[0], "runs", "text")(),
thumbnails: traverseList(item, "thumbnails") thumbnails: traverseList(item, "thumbnails")
}, },
ALBUM_DETAILED AlbumDetailed
) )
} }
public static parseArtistAlbum(item: any, artistBasic: ArtistBasic): AlbumDetailed { public static parseArtistAlbum(item: any, artistBasic: ArtistBasic): AlbumDetailed {
return checkType<AlbumDetailed>( return checkType(
{ {
type: "ALBUM", type: "ALBUM",
albumId: traverseString(item, "browseId")(-1), albumId: traverseString(item, "browseId")(-1),
@ -71,12 +70,12 @@ export default class AlbumParser {
year: AlbumParser.processYear(traverseString(item, "subtitle", "text")(-1)), year: AlbumParser.processYear(traverseString(item, "subtitle", "text")(-1)),
thumbnails: traverseList(item, "thumbnails") thumbnails: traverseList(item, "thumbnails")
}, },
ALBUM_DETAILED AlbumDetailed
) )
} }
public static parseArtistTopAlbums(item: any, artistBasic: ArtistBasic): AlbumDetailed { public static parseArtistTopAlbums(item: any, artistBasic: ArtistBasic): AlbumDetailed {
return checkType<AlbumDetailed>( return checkType(
{ {
type: "ALBUM", type: "ALBUM",
albumId: traverseString(item, "browseId")(-1), albumId: traverseString(item, "browseId")(-1),
@ -86,7 +85,7 @@ export default class AlbumParser {
year: AlbumParser.processYear(traverseString(item, "subtitle", "text")(-1)), year: AlbumParser.processYear(traverseString(item, "subtitle", "text")(-1)),
thumbnails: traverseList(item, "thumbnails") thumbnails: traverseList(item, "thumbnails")
}, },
ALBUM_DETAILED AlbumDetailed
) )
} }

View File

@ -1,5 +1,4 @@
import { ArtistBasic, ArtistDetailed, ArtistFull } from "../" import { ArtistBasic, ArtistDetailed, ArtistFull } from "../schemas"
import { ARTIST_DETAILED, ARTIST_FULL } from "../interfaces"
import checkType from "../utils/checkType" import checkType from "../utils/checkType"
import traverseList from "../utils/traverseList" import traverseList from "../utils/traverseList"
import traverseString from "../utils/traverseString" import traverseString from "../utils/traverseString"
@ -15,7 +14,7 @@ export default class ArtistParser {
const description = traverseString(data, "header", "description", "text")() const description = traverseString(data, "header", "description", "text")()
return checkType<ArtistFull>( return checkType(
{ {
type: "ARTIST", type: "ARTIST",
...artistBasic, ...artistBasic,
@ -30,21 +29,21 @@ export default class ArtistParser {
AlbumParser.parseArtistTopAlbums(item, artistBasic) AlbumParser.parseArtistTopAlbums(item, artistBasic)
) )
}, },
ARTIST_FULL ArtistFull
) )
} }
public static parseSearchResult(item: any): ArtistDetailed { public static parseSearchResult(item: any): ArtistDetailed {
const flexColumns = traverseList(item, "flexColumns") const flexColumns = traverseList(item, "flexColumns")
return checkType<ArtistDetailed>( return checkType(
{ {
type: "ARTIST", type: "ARTIST",
artistId: traverseString(item, "browseId")(), artistId: traverseString(item, "browseId")(),
name: traverseString(flexColumns[0], "runs", "text")(), name: traverseString(flexColumns[0], "runs", "text")(),
thumbnails: traverseList(item, "thumbnails") thumbnails: traverseList(item, "thumbnails")
}, },
ARTIST_DETAILED ArtistDetailed
) )
} }
} }

View File

@ -1,12 +1,11 @@
import { PlaylistFull } from "../" import { PlaylistFull } from "../schemas"
import { PLAYLIST_FULL } from "../interfaces"
import checkType from "../utils/checkType" import checkType from "../utils/checkType"
import traverseList from "../utils/traverseList" import traverseList from "../utils/traverseList"
import traverseString from "../utils/traverseString" import traverseString from "../utils/traverseString"
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 checkType<PlaylistFull>( return checkType(
{ {
type: "PLAYLIST", type: "PLAYLIST",
playlistId, playlistId,
@ -22,7 +21,7 @@ export default class PlaylistParser {
.replaceAll(",", ""), .replaceAll(",", ""),
thumbnails: traverseList(data, "header", "thumbnails") thumbnails: traverseList(data, "header", "thumbnails")
}, },
PLAYLIST_FULL PlaylistFull
) )
} }
@ -30,7 +29,7 @@ export default class PlaylistParser {
const flexColumns = traverseList(item, "flexColumns") const flexColumns = traverseList(item, "flexColumns")
const artistId = traverseString(flexColumns[1], "browseId")() const artistId = traverseString(flexColumns[1], "browseId")()
return checkType<PlaylistFull>( return checkType(
{ {
type: "PLAYLIST", type: "PLAYLIST",
playlistId: traverseString(item, "overlay", "playlistId")(), playlistId: traverseString(item, "overlay", "playlistId")(),
@ -46,7 +45,7 @@ export default class PlaylistParser {
.replaceAll(",", ""), .replaceAll(",", ""),
thumbnails: traverseList(item, "thumbnails") thumbnails: traverseList(item, "thumbnails")
}, },
PLAYLIST_FULL PlaylistFull
) )
} }
} }

View File

@ -1,4 +1,4 @@
import { SearchResult } from "../" import { SearchResult } from "../schemas"
import traverseList from "../utils/traverseList" import traverseList from "../utils/traverseList"
import AlbumParser from "./AlbumParser" import AlbumParser from "./AlbumParser"
import ArtistParser from "./ArtistParser" import ArtistParser from "./ArtistParser"

View File

@ -1,7 +1,4 @@
import { LIST, OBJECT, STRING } from "validate-any" import { AlbumBasic, ArtistBasic, SongDetailed, SongFull, ThumbnailFull } from "../schemas"
import { AlbumBasic, ArtistBasic, SongDetailed, SongFull, ThumbnailFull } from "../"
import { ALBUM_BASIC, ARTIST_BASIC, SONG_DETAILED, SONG_FULL, THUMBNAIL_FULL } from "../interfaces"
import checkType from "../utils/checkType" import checkType from "../utils/checkType"
import traverseList from "../utils/traverseList" import traverseList from "../utils/traverseList"
import traverseString from "../utils/traverseString" import traverseString from "../utils/traverseString"
@ -9,7 +6,7 @@ import Parser from "./Parser"
export default class SongParser { export default class SongParser {
public static parse(data: any): SongFull { public static parse(data: any): SongFull {
return checkType<SongFull>( return checkType(
{ {
type: "SONG", type: "SONG",
videoId: traverseString(data, "videoDetails", "videoId")(), videoId: traverseString(data, "videoDetails", "videoId")(),
@ -26,14 +23,14 @@ export default class SongParser {
formats: traverseList(data, "streamingData", "formats"), formats: traverseList(data, "streamingData", "formats"),
adaptiveFormats: traverseList(data, "streamingData", "adaptiveFormats") adaptiveFormats: traverseList(data, "streamingData", "adaptiveFormats")
}, },
SONG_FULL SongFull
) )
} }
public static parseSearchResult(item: any): SongDetailed { public static parseSearchResult(item: any): SongDetailed {
const flexColumns = traverseList(item, "flexColumns") const flexColumns = traverseList(item, "flexColumns")
return checkType<SongDetailed>( return checkType(
{ {
type: "SONG", type: "SONG",
videoId: traverseString(item, "playlistItemData", "videoId")(), videoId: traverseString(item, "playlistItemData", "videoId")(),
@ -52,7 +49,7 @@ export default class SongParser {
duration: Parser.parseDuration(traverseString(flexColumns[1], "runs", "text")(-1)), duration: Parser.parseDuration(traverseString(flexColumns[1], "runs", "text")(-1)),
thumbnails: traverseList(item, "thumbnails") thumbnails: traverseList(item, "thumbnails")
}, },
SONG_DETAILED SongDetailed
) )
} }
@ -60,7 +57,7 @@ export default class SongParser {
const flexColumns = traverseList(item, "flexColumns") const flexColumns = traverseList(item, "flexColumns")
const videoId = traverseString(item, "playlistItemData", "videoId")() const videoId = traverseString(item, "playlistItemData", "videoId")()
return checkType<SongDetailed>( return checkType(
{ {
type: "SONG", type: "SONG",
videoId, videoId,
@ -80,7 +77,7 @@ export default class SongParser {
), ),
thumbnails: traverseList(item, "thumbnails") thumbnails: traverseList(item, "thumbnails")
}, },
SONG_DETAILED SongDetailed
) )
} }
@ -91,7 +88,7 @@ export default class SongParser {
const flexColumns = traverseList(item, "flexColumns") const flexColumns = traverseList(item, "flexColumns")
const videoId = traverseString(item, "playlistItemData", "videoId")() const videoId = traverseString(item, "playlistItemData", "videoId")()
return checkType<Omit<SongDetailed, "duration">>( return checkType(
{ {
type: "SONG", type: "SONG",
videoId, videoId,
@ -103,14 +100,7 @@ export default class SongParser {
}, },
thumbnails: traverseList(item, "thumbnails") thumbnails: traverseList(item, "thumbnails")
}, },
OBJECT({ SongDetailed.omit({ duration: true })
type: STRING("SONG"),
videoId: STRING(),
name: STRING(),
artists: LIST(ARTIST_BASIC),
album: ALBUM_BASIC,
thumbnails: LIST(THUMBNAIL_FULL)
})
) )
} }
@ -123,7 +113,7 @@ export default class SongParser {
const flexColumns = traverseList(item, "flexColumns") const flexColumns = traverseList(item, "flexColumns")
const videoId = traverseString(item, "playlistItemData", "videoId")() const videoId = traverseString(item, "playlistItemData", "videoId")()
return checkType<SongDetailed>( return checkType(
{ {
type: "SONG", type: "SONG",
videoId, videoId,
@ -135,7 +125,7 @@ export default class SongParser {
), ),
thumbnails thumbnails
}, },
SONG_DETAILED SongDetailed
) )
} }
} }

View File

@ -1,5 +1,4 @@
import { VideoDetailed, VideoFull } from "../" import { VideoDetailed, VideoFull } from "../schemas"
import { VIDEO_DETAILED } from "../interfaces"
import checkType from "../utils/checkType" import checkType from "../utils/checkType"
import traverse from "../utils/traverse" import traverse from "../utils/traverse"
import traverseList from "../utils/traverseList" import traverseList from "../utils/traverseList"
@ -53,7 +52,7 @@ export default class VideoParser {
traverseString(item, "playNavigationEndpoint", "videoId")() || traverseString(item, "playNavigationEndpoint", "videoId")() ||
traverseList(item, "thumbnails")[0].url.match(/https:\/\/i\.ytimg\.com\/vi\/(.+)\//)[1] traverseList(item, "thumbnails")[0].url.match(/https:\/\/i\.ytimg\.com\/vi\/(.+)\//)[1]
return checkType<VideoDetailed>( return checkType(
{ {
type: "VIDEO", type: "VIDEO",
videoId, videoId,
@ -69,7 +68,7 @@ export default class VideoParser {
), ),
thumbnails: traverseList(item, "thumbnails") thumbnails: traverseList(item, "thumbnails")
}, },
VIDEO_DETAILED VideoDetailed
) )
} }
} }

137
src/schemas.ts Normal file
View File

@ -0,0 +1,137 @@
import { z } from "zod"
export type ThumbnailFull = z.infer<typeof ThumbnailFull>
export const ThumbnailFull = z.object({
url: z.string(),
width: z.number(),
height: z.number()
})
export type ArtistBasic = z.infer<typeof ArtistBasic>
export const ArtistBasic = z.object({
artistId: z.string(),
name: z.string()
})
export type AlbumBasic = z.infer<typeof AlbumBasic>
export const AlbumBasic = z.object({
albumId: z.string(),
name: z.string()
})
export type SongDetailed = z.infer<typeof SongDetailed>
export const SongDetailed = z.object({
type: z.literal("SONG"),
videoId: z.string(),
name: z.string(),
artists: z.array(ArtistBasic),
album: AlbumBasic,
duration: z.number(),
thumbnails: z.array(ThumbnailFull)
})
export type VideoDetailed = z.infer<typeof VideoDetailed>
export const VideoDetailed = z.object({
type: z.literal("VIDEO"),
videoId: z.string(),
name: z.string(),
artists: z.array(ArtistBasic),
duration: z.number(),
thumbnails: z.array(ThumbnailFull)
})
export type ArtistDetailed = z.infer<typeof ArtistDetailed>
export const ArtistDetailed = z.object({
artistId: z.string(),
name: z.string(),
type: z.literal("ARTIST"),
thumbnails: z.array(ThumbnailFull)
})
export type AlbumDetailed = z.infer<typeof AlbumDetailed>
export const AlbumDetailed = z.object({
type: z.literal("ALBUM"),
albumId: z.string(),
playlistId: z.string(),
name: z.string(),
artists: z.array(ArtistBasic),
year: z.number().nullable(),
thumbnails: z.array(ThumbnailFull)
})
export type SongFull = z.infer<typeof SongFull>
export const SongFull = z.object({
type: z.literal("SONG"),
videoId: z.string(),
name: z.string(),
artists: z.array(ArtistBasic),
duration: z.number(),
thumbnails: z.array(ThumbnailFull),
description: z.string(),
formats: z.array(z.any()),
adaptiveFormats: z.array(z.any())
})
export type VideoFull = z.infer<typeof VideoFull>
export const VideoFull = z.object({
type: z.literal("VIDEO"),
videoId: z.string(),
name: z.string(),
artists: z.array(ArtistBasic),
duration: z.number(),
thumbnails: z.array(ThumbnailFull),
description: z.string(),
unlisted: z.boolean(),
familySafe: z.boolean(),
paid: z.boolean(),
tags: z.array(z.string())
})
export type ArtistFull = z.infer<typeof ArtistFull>
export const ArtistFull = z.object({
artistId: z.string(),
name: z.string(),
type: z.literal("ARTIST"),
thumbnails: z.array(ThumbnailFull),
description: z.string(),
topSongs: z.array(
z.object({
type: z.literal("SONG"),
videoId: z.string(),
name: z.string(),
artists: z.array(ArtistBasic),
album: AlbumBasic,
thumbnails: z.array(ThumbnailFull)
})
),
topAlbums: z.array(AlbumDetailed)
})
export type AlbumFull = z.infer<typeof AlbumFull>
export const AlbumFull = z.object({
type: z.literal("ALBUM"),
albumId: z.string(),
playlistId: z.string(),
name: z.string(),
artists: z.array(ArtistBasic),
year: z.number().nullable(),
thumbnails: z.array(ThumbnailFull),
description: z.string(),
songs: z.array(SongDetailed)
})
export type PlaylistFull = z.infer<typeof PlaylistFull>
export const PlaylistFull = z.object({
type: z.literal("PLAYLIST"),
playlistId: z.string(),
name: z.string(),
artist: ArtistBasic,
videoCount: z.number(),
thumbnails: z.array(ThumbnailFull)
})
export type SearchResult = z.infer<typeof SearchResult>
export const SearchResult = SongDetailed.or(VideoDetailed)
.or(AlbumDetailed)
.or(ArtistDetailed)
.or(PlaylistFull)

View File

@ -1,18 +1,18 @@
import Validator from "validate-any/dist/classes/Validator" import { z } from "zod"
import { validate } from "validate-any" import zodtojson from "zod-to-json-schema"
export default <T>(data: T, validator: Validator<T>): T => { export default <T extends z.Schema>(data: z.infer<T>, schema: T): z.infer<T> => {
const result = validate(data, validator) const result = schema.safeParse(data)
if (result.success) { if (result.success) {
return result.data return data
} else { } else {
console.error( console.error(
"Invalid data schema, please report to https://github.com/zS1L3NT/ts-npm-ytmusic-api/issues/new/choose", "Invalid data schema, please report to https://github.com/zS1L3NT/ts-npm-ytmusic-api/issues/new/choose",
JSON.stringify( JSON.stringify(
{ {
expected: validator.getSchema(), schema: zodtojson(schema),
actual: data, data,
errors: result.errors error: result.error
}, },
null, null,
2 2