diff --git a/README.md b/README.md index c24e792..9e39685 100644 --- a/README.md +++ b/README.md @@ -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) \ No newline at end of file diff --git a/package.json b/package.json index 657da23..781c0e3 100644 --- a/package.json +++ b/package.json @@ -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": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 166bf83..b1b257b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/YTMusic.ts b/src/YTMusic.ts index 671fa38..c67605d 100644 --- a/src/YTMusic.ts +++ b/src/YTMusic.ts @@ -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 = { ...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 { + public async search(query: string): Promise[]> { 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 { + public async searchSongs(query: string): Promise[]> { 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 { + public async searchVideos(query: string): Promise[]> { 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 { + public async searchArtists(query: string): Promise[]> { 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 { + public async searchAlbums(query: string): Promise[]> { 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 { + public async searchPlaylists(query: string): Promise[]> { 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 { + public async getSong(videoId: string): Promise> { 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 { + public async getVideo(videoId: string): Promise> { 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 { - const data = await this.constructRequest("browse", { browseId: artistId }) + public async getArtist(artistId: string): Promise> { + 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 { - const artistData = await this.constructRequest("browse", { browseId: artistId }) + public async getArtistSongs(artistId: string): Promise[]> { + 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 { - const artistData = await this.constructRequest("browse", { browseId: artistId }) + public async getArtistAlbums(artistId: string): Promise[]> { + 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 { - const data = await this.constructRequest("browse", { browseId: albumId }) + public async getAlbum(albumId: string): Promise> { + 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 { + public async getPlaylist(playlistId: string): Promise> { 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 { + public async getPlaylistVideos(playlistId: string): Promise[]> { 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, diff --git a/src/__tests__/traversing.spec.ts b/src/__tests__/traversing.spec.ts index ee592eb..36bc653 100644 --- a/src/__tests__/traversing.spec.ts +++ b/src/__tests__/traversing.spec.ts @@ -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 = []>[] const queries = ["Lilac", "Weekend", "Eill", "Eminem", "Lisa Hannigan"] -const expect = (data: any, validator: Validator) => { - 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)) diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 7b899ee..0000000 --- a/src/index.ts +++ /dev/null @@ -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 { - 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[] - 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 diff --git a/src/interfaces.ts b/src/interfaces.ts deleted file mode 100644 index 5ec484a..0000000 --- a/src/interfaces.ts +++ /dev/null @@ -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 = OBJECT({ - url: STRING(), - width: NUMBER(), - height: NUMBER() -}) - -export const ARTIST_BASIC: ObjectValidator = OBJECT({ - artistId: STRING(), - name: STRING() -}) - -export const ALBUM_BASIC: ObjectValidator = OBJECT({ - albumId: STRING(), - name: STRING() -}) - -export const SONG_DETAILED: ObjectValidator = 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 = OBJECT({ - type: STRING("VIDEO"), - videoId: STRING(), - name: STRING(), - artists: LIST(ARTIST_BASIC), - duration: NUMBER(), - thumbnails: LIST(THUMBNAIL_FULL) -}) - -export const ARTIST_DETAILED: ObjectValidator = OBJECT({ - artistId: STRING(), - name: STRING(), - type: STRING("ARTIST"), - thumbnails: LIST(THUMBNAIL_FULL) -}) - -export const ALBUM_DETAILED: ObjectValidator = OBJECT({ - type: STRING("ALBUM"), - albumId: STRING(), - playlistId: STRING(), - name: STRING(), - artists: LIST(ARTIST_BASIC), - year: OR(NUMBER(), NULL()), - thumbnails: LIST(THUMBNAIL_FULL) -}) - -export const SONG_FULL: ObjectValidator = 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 = 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 = 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 = 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 = OBJECT({ - type: STRING("PLAYLIST"), - playlistId: STRING(), - name: STRING(), - artist: ARTIST_BASIC, - videoCount: NUMBER(), - thumbnails: LIST(THUMBNAIL_FULL) -}) diff --git a/src/parsers/AlbumParser.ts b/src/parsers/AlbumParser.ts index fcef4c5..ccada83 100644 --- a/src/parsers/AlbumParser.ts +++ b/src/parsers/AlbumParser.ts @@ -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( + 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( + 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( + 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( + 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 ) } diff --git a/src/parsers/ArtistParser.ts b/src/parsers/ArtistParser.ts index c4b22e6..cdb109d 100644 --- a/src/parsers/ArtistParser.ts +++ b/src/parsers/ArtistParser.ts @@ -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( + 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( + return checkType( { type: "ARTIST", artistId: traverseString(item, "browseId")(), name: traverseString(flexColumns[0], "runs", "text")(), thumbnails: traverseList(item, "thumbnails") }, - ARTIST_DETAILED + ArtistDetailed ) } } diff --git a/src/parsers/PlaylistParser.ts b/src/parsers/PlaylistParser.ts index 45b2772..0ce2355 100644 --- a/src/parsers/PlaylistParser.ts +++ b/src/parsers/PlaylistParser.ts @@ -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( + 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( + 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 ) } } diff --git a/src/parsers/SearchParser.ts b/src/parsers/SearchParser.ts index 85de453..dc68441 100644 --- a/src/parsers/SearchParser.ts +++ b/src/parsers/SearchParser.ts @@ -1,4 +1,4 @@ -import { SearchResult } from "../" +import { SearchResult } from "../schemas" import traverseList from "../utils/traverseList" import AlbumParser from "./AlbumParser" import ArtistParser from "./ArtistParser" diff --git a/src/parsers/SongParser.ts b/src/parsers/SongParser.ts index c288dcd..28ae8ba 100644 --- a/src/parsers/SongParser.ts +++ b/src/parsers/SongParser.ts @@ -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( + 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( + 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( + 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>( + 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( + return checkType( { type: "SONG", videoId, @@ -135,7 +125,7 @@ export default class SongParser { ), thumbnails }, - SONG_DETAILED + SongDetailed ) } } diff --git a/src/parsers/VideoParser.ts b/src/parsers/VideoParser.ts index 11f1e34..c5ee6d4 100644 --- a/src/parsers/VideoParser.ts +++ b/src/parsers/VideoParser.ts @@ -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( + return checkType( { type: "VIDEO", videoId, @@ -69,7 +68,7 @@ export default class VideoParser { ), thumbnails: traverseList(item, "thumbnails") }, - VIDEO_DETAILED + VideoDetailed ) } } diff --git a/src/schemas.ts b/src/schemas.ts new file mode 100644 index 0000000..47f3aa5 --- /dev/null +++ b/src/schemas.ts @@ -0,0 +1,137 @@ +import { z } from "zod" + +export type ThumbnailFull = z.infer +export const ThumbnailFull = z.object({ + url: z.string(), + width: z.number(), + height: z.number() +}) + +export type ArtistBasic = z.infer +export const ArtistBasic = z.object({ + artistId: z.string(), + name: z.string() +}) + +export type AlbumBasic = z.infer +export const AlbumBasic = z.object({ + albumId: z.string(), + name: z.string() +}) + +export type SongDetailed = z.infer +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 +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 +export const ArtistDetailed = z.object({ + artistId: z.string(), + name: z.string(), + type: z.literal("ARTIST"), + thumbnails: z.array(ThumbnailFull) +}) + +export type AlbumDetailed = z.infer +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 +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 +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 +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 +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 +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 +export const SearchResult = SongDetailed.or(VideoDetailed) + .or(AlbumDetailed) + .or(ArtistDetailed) + .or(PlaylistFull) diff --git a/src/utils/checkType.ts b/src/utils/checkType.ts index f3842f1..39a70f6 100644 --- a/src/utils/checkType.ts +++ b/src/utils/checkType.ts @@ -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 (data: T, validator: Validator): T => { - const result = validate(data, validator) +export default (data: z.infer, schema: T): z.infer => { + 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