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
- 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)
- [![@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/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)
- [![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)
- Axios
- [![axios](https://img.shields.io/github/package-json/dependency-version/zS1L3NT/ts-npm-ytmusic-api/axios?style=flat-square)](https://npmjs.com/package/axios)
- Tough Cookie
- [![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
- [![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)
- [![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)
- [![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
- [![@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)
- [![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)
- Miscellaneous
- [![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)
- TypeScript
- [![@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/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)
- [![@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)
- [![@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)
- [![typescript](https://img.shields.io/badge/typescript-%5E4.9.4-red?style=flat-square)](https://npmjs.com/package/typescript/v/4.9.4)
- Mocha
- [![mocha](https://img.shields.io/badge/mocha-%5E10.2.0-red?style=flat-square)](https://npmjs.com/package/mocha/v/10.2.0)
- [![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)
- [![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)
- VuePress
- [![@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](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)
- Miscellaneous
- [![axios](https://img.shields.io/badge/axios-%5E0.27.2-red?style=flat-square)](https://npmjs.com/package/axios/v/0.27.2)
- [![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)
- [![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",
"version": "3.1.1",
"version": "4.0.0",
"description": "YouTube Music API",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -17,18 +17,20 @@
},
"dependencies": {
"axios": "^0.27.2",
"tough-cookie": "^4.0.0",
"validate-any": "1.3.2"
"tough-cookie": "^4.1.2",
"zod": "^3.20.2",
"zod-to-json-schema": "^3.20.1"
},
"devDependencies": {
"@types/mocha": "^9.1.1",
"@types/node": "^17.0.36",
"@types/json-schema": "^7.0.11",
"@types/mocha": "^10.0.1",
"@types/node": "^18.11.17",
"@types/tough-cookie": "^4.0.2",
"@vuepress/plugin-search": "^2.0.0-beta.46",
"mocha": "^10.0.0",
"mocha": "^10.2.0",
"mocha.parallel": "^0.15.6",
"ts-mocha": "^10.0.0",
"typescript": "^4.7.2",
"typescript": "^4.9.4",
"vuepress": "^2.0.0-beta.46"
},
"keywords": [

View File

@ -1,33 +1,37 @@
lockfileVersion: 5.4
specifiers:
'@types/mocha': ^9.1.1
'@types/node': ^17.0.36
'@types/json-schema': ^7.0.11
'@types/mocha': ^10.0.1
'@types/node': ^18.11.17
'@types/tough-cookie': ^4.0.2
'@vuepress/plugin-search': ^2.0.0-beta.46
axios: ^0.27.2
mocha: ^10.0.0
mocha: ^10.2.0
mocha.parallel: ^0.15.6
tough-cookie: ^4.0.0
tough-cookie: ^4.1.2
ts-mocha: ^10.0.0
typescript: ^4.7.2
validate-any: 1.3.2
typescript: ^4.9.4
vuepress: ^2.0.0-beta.46
zod: ^3.20.2
zod-to-json-schema: ^3.20.1
dependencies:
axios: 0.27.2
tough-cookie: 4.0.0
validate-any: 1.3.2
tough-cookie: 4.1.2
zod: 3.20.2
zod-to-json-schema: 3.20.1_zod@3.20.2
devDependencies:
'@types/mocha': 9.1.1
'@types/node': 17.0.36
'@types/json-schema': 7.0.11
'@types/mocha': 10.0.1
'@types/node': 18.11.17
'@types/tough-cookie': 4.0.2
'@vuepress/plugin-search': 2.0.0-beta.46
mocha: 10.0.0
mocha.parallel: 0.15.6_mocha@10.0.0
ts-mocha: 10.0.0_mocha@10.0.0
typescript: 4.7.2
mocha: 10.2.0
mocha.parallel: 0.15.6_mocha@10.2.0
ts-mocha: 10.0.0_mocha@10.2.0
typescript: 4.9.4
vuepress: 2.0.0-beta.46
packages:
@ -83,7 +87,11 @@ packages:
/@types/fs-extra/9.0.13:
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
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
/@types/json5/0.0.29:
@ -106,26 +114,22 @@ packages:
resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==}
dev: true
/@types/mocha/9.1.1:
resolution: {integrity: sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==}
/@types/mocha/10.0.1:
resolution: {integrity: sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==}
dev: true
/@types/ms/0.7.31:
resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
dev: true
/@types/node/17.0.36:
resolution: {integrity: sha512-V3orv+ggDsWVHP99K3JlwtH20R7J4IhI1Kksgc+64q5VxgfRkQG8Ws3MFm/FZOKDYGy9feGFlZ70/HpCNe9QaA==}
/@types/node/18.11.17:
resolution: {integrity: sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==}
dev: true
/@types/tough-cookie/4.0.2:
resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==}
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:
resolution: {integrity: sha512-SmQLDyhz+6lGJhPELsBdzXGc+AcaT8stgkbiTFGpXPe8Tl1tJaBw1A6pxDqDuRsVkD8uscrkx3hA7QDOoKYtyw==}
engines: {node: '>=12.0.0'}
@ -588,7 +592,7 @@ packages:
/axios/0.27.2:
resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
dependencies:
follow-redirects: 1.14.9
follow-redirects: 1.15.2
form-data: 4.0.0
transitivePeerDependencies:
- debug
@ -747,7 +751,7 @@ packages:
dev: false
/concat-map/0.0.1:
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true
/connect-history-api-fallback/1.6.0:
@ -1132,8 +1136,8 @@ packages:
hasBin: true
dev: true
/follow-redirects/1.14.9:
resolution: {integrity: sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==}
/follow-redirects/1.15.2:
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
@ -1510,22 +1514,21 @@ packages:
minimist: 1.2.6
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==}
peerDependencies:
mocha: '>=2.2.5'
dependencies:
bluebird: 2.11.0
mocha: 10.0.0
mocha: 10.2.0
semaphore: 1.1.0
dev: true
/mocha/10.0.0:
resolution: {integrity: sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==}
/mocha/10.2.0:
resolution: {integrity: sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==}
engines: {node: '>= 14.0.0'}
hasBin: true
dependencies:
'@ungap/promise-all-settled': 1.1.2
ansi-colors: 4.1.1
browser-stdout: 1.3.1
chokidar: 3.5.3
@ -1591,7 +1594,7 @@ packages:
dev: true
/once/1.4.0:
resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=}
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies:
wrappy: 1.0.2
dev: true
@ -1638,7 +1641,7 @@ packages:
dev: true
/path-is-absolute/1.0.1:
resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=}
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'}
dev: true
@ -1692,6 +1695,10 @@ packages:
engines: {node: '>=6'}
dev: false
/querystringify/2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
dev: false
/queue-microtask/1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true
@ -1719,10 +1726,14 @@ packages:
dev: true
/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'}
dev: true
/requires-port/1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: false
/resolve/1.22.0:
resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==}
hasBin: true
@ -1916,27 +1927,28 @@ packages:
resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==}
dev: true
/tough-cookie/4.0.0:
resolution: {integrity: sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==}
/tough-cookie/4.1.2:
resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==}
engines: {node: '>=6'}
dependencies:
psl: 1.8.0
punycode: 2.1.1
universalify: 0.1.2
universalify: 0.2.0
url-parse: 1.5.10
dev: false
/ts-debounce/4.0.0:
resolution: {integrity: sha512-+1iDGY6NmOGidq7i7xZGA4cm8DAa6fqdYcvO5Z6yBevH++Bdo9Qt/mN0TzHUgcCcKv1gmh9+W5dHqz8pMWbCbg==}
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==}
engines: {node: '>= 6.X.X'}
hasBin: true
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
dependencies:
mocha: 10.0.0
mocha: 10.2.0
ts-node: 7.0.1
optionalDependencies:
tsconfig-paths: 3.14.1
@ -1968,8 +1980,8 @@ packages:
dev: true
optional: true
/typescript/4.7.2:
resolution: {integrity: sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==}
/typescript/4.9.4:
resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==}
engines: {node: '>=4.2.0'}
hasBin: true
dev: true
@ -1978,8 +1990,8 @@ packages:
resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==}
dev: true
/universalify/0.1.2:
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
/universalify/0.2.0:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'}
dev: false
@ -1993,14 +2005,17 @@ packages:
engines: {node: '>=4'}
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:
resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=}
dev: true
/validate-any/1.3.2:
resolution: {integrity: sha512-L1SpFcOpnFR4u7slwdtVRecyI/SAun/QvoFM3pJw+hE/4/D5LJMs2wcf8OYVOM8CpNab/zsXb58w9ck3XkIYNg==}
dev: false
/vite/2.9.9:
resolution: {integrity: sha512-ffaam+NgHfbEmfw/Vuh6BHKKlI/XIAhxE5QSS7gFLIngxg171mg1P3a4LSRME0z2ZU1ScxoKzphkipcYwSD5Ew==}
engines: {node: '>=12.2.0'}
@ -2123,7 +2138,7 @@ packages:
dev: true
/wrappy/1.0.2:
resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=}
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: true
/y18n/5.0.8:
@ -2168,3 +2183,15 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
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 { 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 ArtistParser from "./parsers/ArtistParser"
import PlaylistParser from "./parsers/PlaylistParser"
import SearchParser from "./parsers/SearchParser"
import SongParser from "./parsers/SongParser"
import VideoParser from "./parsers/VideoParser"
import {
AlbumDetailed, AlbumFull, ArtistDetailed, ArtistFull, PlaylistFull, SearchResult, SongDetailed,
SongFull, VideoDetailed, VideoFull
} from "./schemas"
import traverse from "./utils/traverse"
import traverseList from "./utils/traverseList"
import traverseString from "./utils/traverseString"
@ -122,7 +123,7 @@ export default class YTMusic {
const headers: Record<string, any> = {
...this.client.defaults.headers,
"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-Version": this.config.INNERTUBE_CLIENT_VERSION,
"X-YouTube-Device": this.config.DEVICE,
@ -213,7 +214,7 @@ export default class YTMusic {
*
* @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", {
query,
params: null
@ -227,7 +228,7 @@ export default class YTMusic {
*
* @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", {
query,
params: "Eg-KAQwIARAAGAAgACgAMABqChAEEAMQCRAFEAo%3D"
@ -243,7 +244,7 @@ export default class YTMusic {
*
* @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", {
query,
params: "Eg-KAQwIABABGAAgACgAMABqChAEEAMQCRAFEAo%3D"
@ -259,7 +260,7 @@ export default class YTMusic {
*
* @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", {
query,
params: "Eg-KAQwIABAAGAAgASgAMABqChAEEAMQCRAFEAo%3D"
@ -275,7 +276,7 @@ export default class YTMusic {
*
* @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", {
query,
params: "Eg-KAQwIABAAGAEgACgAMABqChAEEAMQCRAFEAo%3D"
@ -291,7 +292,7 @@ export default class YTMusic {
*
* @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", {
query,
params: "Eg-KAQwIABAAGAAgACgBMABqChAEEAMQCRAFEAo%3D"
@ -308,7 +309,7 @@ export default class YTMusic {
* @param videoId Video ID
* @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")
const data = await this.constructRequest("player", { videoId })
@ -323,7 +324,7 @@ export default class YTMusic {
* @param videoId Video ID
* @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")
const data = await this.constructRequest("player", { videoId })
@ -338,8 +339,10 @@ export default class YTMusic {
* @param artistId Artist ID
* @returns Artist Data
*/
public async getArtist(artistId: string): Promise<ArtistFull> {
const data = await this.constructRequest("browse", { browseId: artistId })
public async getArtist(artistId: string): Promise<z.infer<typeof ArtistFull>> {
const data = await this.constructRequest("browse", {
browseId: artistId
})
return ArtistParser.parse(data, artistId)
}
@ -350,13 +353,17 @@ export default class YTMusic {
* @param artistId Artist ID
* @returns Artist's Songs
*/
public async getArtistSongs(artistId: string): Promise<SongDetailed[]> {
const artistData = await this.constructRequest("browse", { browseId: artistId })
public async getArtistSongs(artistId: string): Promise<z.infer<typeof SongDetailed>[]> {
const artistData = await this.constructRequest("browse", {
browseId: artistId
})
const browseToken = traverse(artistData, "musicShelfRenderer", "title", "browseId")
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 moreSongsData = await this.constructRequest(
"browse",
@ -376,8 +383,10 @@ export default class YTMusic {
* @param artistId Artist ID
* @returns Artist's Albums
*/
public async getArtistAlbums(artistId: string): Promise<AlbumDetailed[]> {
const artistData = await this.constructRequest("browse", { browseId: artistId })
public async getArtistAlbums(artistId: string): Promise<z.infer<typeof AlbumDetailed>[]> {
const artistData = await this.constructRequest("browse", {
browseId: artistId
})
const artistAlbumsData = traverseList(artistData, "musicCarouselShelfRenderer")[0]
const browseBody = traverse(artistAlbumsData, "moreContentButton", "browseEndpoint")
@ -397,8 +406,10 @@ export default class YTMusic {
* @param albumId Album ID
* @returns Album Data
*/
public async getAlbum(albumId: string): Promise<AlbumFull> {
const data = await this.constructRequest("browse", { browseId: albumId })
public async getAlbum(albumId: string): Promise<z.infer<typeof AlbumFull>> {
const data = await this.constructRequest("browse", {
browseId: albumId
})
return AlbumParser.parse(data, albumId)
}
@ -409,9 +420,11 @@ export default class YTMusic {
* @param playlistId Playlist ID
* @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
const data = await this.constructRequest("browse", { browseId: playlistId })
const data = await this.constructRequest("browse", {
browseId: playlistId
})
return PlaylistParser.parse(data, playlistId)
}
@ -422,9 +435,11 @@ export default class YTMusic {
* @param playlistId Playlist ID
* @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
const playlistData = await this.constructRequest("browse", { browseId: playlistId })
const playlistData = await this.constructRequest("browse", {
browseId: playlistId
})
const songs = traverseList(
playlistData,

View File

@ -1,22 +1,21 @@
import assert from "assert"
import describeParallel from "mocha.parallel"
import { iValidationError, LIST, STRING, validate } from "validate-any"
import Validator from "validate-any/dist/classes/Validator"
import { z } from "zod"
import YTMusic from "../"
import {
ALBUM_DETAILED, ALBUM_FULL, ARTIST_DETAILED, ARTIST_FULL, PLAYLIST_FULL, SONG_DETAILED,
SONG_FULL, VIDEO_DETAILED, VIDEO_FULL
} from "../interfaces"
AlbumDetailed, AlbumFull, ArtistDetailed, ArtistFull, PlaylistFull, SongDetailed, SongFull,
VideoDetailed, VideoFull
} from "../schemas"
import YTMusic from "../YTMusic"
const issues: iValidationError[][] = []
const errors = <z.ZodError<any>[]>[]
const queries = ["Lilac", "Weekend", "Eill", "Eminem", "Lisa Hannigan"]
const expect = (data: any, validator: Validator<any>) => {
const { errors } = validate(data, validator)
if (errors.length > 0) {
issues.push(errors)
const expect = (data: any, schema: z.Schema) => {
const result = schema.safeParse(data)
if (!result.success) {
errors.push(result.error)
}
assert.equal(errors.length, 0)
assert.equal(result.success, true)
}
const ytmusic = new YTMusic()
@ -26,90 +25,95 @@ queries.forEach(query => {
describeParallel("Query: " + query, () => {
it("Search suggestions", async () => {
const suggestions = await ytmusic.getSearchSuggestions(query)
expect(suggestions, LIST(STRING()))
expect(suggestions, z.array(z.string()))
})
it("Search Songs", async () => {
const songs = await ytmusic.searchSongs(query)
expect(songs, LIST(SONG_DETAILED))
expect(songs, z.array(SongDetailed))
})
it("Search Videos", async () => {
const videos = await ytmusic.searchVideos(query)
expect(videos, LIST(VIDEO_DETAILED))
expect(videos, z.array(VideoDetailed))
})
it("Search Artists", async () => {
const artists = await ytmusic.searchArtists(query)
expect(artists, LIST(ARTIST_DETAILED))
expect(artists, z.array(ArtistDetailed))
})
it("Search Albums", async () => {
const albums = await ytmusic.searchAlbums(query)
expect(albums, LIST(ALBUM_DETAILED))
expect(albums, z.array(AlbumDetailed))
})
it("Search Playlists", async () => {
const playlists = await ytmusic.searchPlaylists(query)
expect(playlists, LIST(PLAYLIST_FULL))
expect(playlists, z.array(PlaylistFull))
})
it("Search All", async () => {
const results = await ytmusic.search(query)
expect(
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 () => {
const songs = await ytmusic.searchSongs(query)
const song = await ytmusic.getSong(songs[0]!.videoId)
expect(song, SONG_FULL)
expect(song, SongFull)
})
it("Get details of the first video result", async () => {
const videos = await ytmusic.searchVideos(query)
const video = await ytmusic.getVideo(videos[0]!.videoId)
expect(video, VIDEO_FULL)
expect(video, VideoFull)
})
it("Get details of the first artist result", async () => {
const artists = await ytmusic.searchArtists(query)
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 () => {
const artists = await ytmusic.searchArtists(query)
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 () => {
const artists = await ytmusic.searchArtists(query)
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 () => {
const albums = await ytmusic.searchAlbums(query)
const album = await ytmusic.getAlbum(albums[0]!.albumId)
expect(album, ALBUM_FULL)
expect(album, AlbumFull)
})
it("Get details of the first playlist result", async () => {
const playlists = await ytmusic.searchPlaylists(query)
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 () => {
const playlists = await ytmusic.searchPlaylists(query)
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 { ALBUM_DETAILED, ALBUM_FULL } from "../interfaces"
import { AlbumBasic, AlbumDetailed, AlbumFull, ArtistBasic } from "../schemas"
import checkType from "../utils/checkType"
import traverseList from "../utils/traverseList"
import traverseString from "../utils/traverseString"
@ -19,7 +18,7 @@ export default class AlbumParser {
}))
const thumbnails = traverseList(data, "header", "thumbnails")
return checkType<AlbumFull>(
return checkType(
{
type: "ALBUM",
...albumBasic,
@ -34,14 +33,14 @@ export default class AlbumParser {
SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails)
)
},
ALBUM_FULL
AlbumFull
)
}
public static parseSearchResult(item: any): AlbumDetailed {
const flexColumns = traverseList(item, "flexColumns")
return checkType<AlbumDetailed>(
return checkType(
{
type: "ALBUM",
albumId: traverseString(item, "browseId")(-1),
@ -56,12 +55,12 @@ export default class AlbumParser {
name: traverseString(flexColumns[0], "runs", "text")(),
thumbnails: traverseList(item, "thumbnails")
},
ALBUM_DETAILED
AlbumDetailed
)
}
public static parseArtistAlbum(item: any, artistBasic: ArtistBasic): AlbumDetailed {
return checkType<AlbumDetailed>(
return checkType(
{
type: "ALBUM",
albumId: traverseString(item, "browseId")(-1),
@ -71,12 +70,12 @@ export default class AlbumParser {
year: AlbumParser.processYear(traverseString(item, "subtitle", "text")(-1)),
thumbnails: traverseList(item, "thumbnails")
},
ALBUM_DETAILED
AlbumDetailed
)
}
public static parseArtistTopAlbums(item: any, artistBasic: ArtistBasic): AlbumDetailed {
return checkType<AlbumDetailed>(
return checkType(
{
type: "ALBUM",
albumId: traverseString(item, "browseId")(-1),
@ -86,7 +85,7 @@ export default class AlbumParser {
year: AlbumParser.processYear(traverseString(item, "subtitle", "text")(-1)),
thumbnails: traverseList(item, "thumbnails")
},
ALBUM_DETAILED
AlbumDetailed
)
}

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
import { VideoDetailed, VideoFull } from "../"
import { VIDEO_DETAILED } from "../interfaces"
import { VideoDetailed, VideoFull } from "../schemas"
import checkType from "../utils/checkType"
import traverse from "../utils/traverse"
import traverseList from "../utils/traverseList"
@ -53,7 +52,7 @@ export default class VideoParser {
traverseString(item, "playNavigationEndpoint", "videoId")() ||
traverseList(item, "thumbnails")[0].url.match(/https:\/\/i\.ytimg\.com\/vi\/(.+)\//)[1]
return checkType<VideoDetailed>(
return checkType(
{
type: "VIDEO",
videoId,
@ -69,7 +68,7 @@ export default class VideoParser {
),
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 { validate } from "validate-any"
import { z } from "zod"
import zodtojson from "zod-to-json-schema"
export default <T>(data: T, validator: Validator<T>): T => {
const result = validate(data, validator)
export default <T extends z.Schema>(data: z.infer<T>, schema: T): z.infer<T> => {
const result = schema.safeParse(data)
if (result.success) {
return result.data
return data
} else {
console.error(
"Invalid data schema, please report to https://github.com/zS1L3NT/ts-npm-ytmusic-api/issues/new/choose",
JSON.stringify(
{
expected: validator.getSchema(),
actual: data,
errors: result.errors
schema: zodtojson(schema),
data,
error: result.error
},
null,
2