From 5f8cd339fc249e75d950dffa535b3b67257dd08e Mon Sep 17 00:00:00 2001 From: Cody Duong Date: Sun, 13 Mar 2022 20:25:03 -0500 Subject: [PATCH] Release 0.1.1 * Add more testing and fix any issues with failing tests --- docs/YTMusic.html | 2 +- docs/index.html | 13 +- docs/index.js.html | 2 +- docs/mixins_browsing.js.html | 16 +- docs/mixins_explore.js.html | 2 +- docs/mixins_library.js.html | 17 +- docs/mixins_playlists.js.html | 5 +- docs/mixins_uploads.js.html | 9 +- docs/mixins_watch.js.html | 103 +++++++--- docs/module-Browsing.html | 4 +- docs/module-Explore.html | 2 +- docs/module-Library.html | 6 +- docs/module-Playlists.html | 14 +- docs/module-Uploads.html | 8 +- docs/module-Watch.html | 36 +--- package.json | 7 +- src/mixins/library.ts | 21 +- src/mixins/playlists.ts | 21 +- src/mixins/uploads.ts | 14 +- src/mixins/watch.ts | 4 +- src/parsers/library.ts | 3 +- src/parsers/uploads.ts | 17 +- src/pyLibraryMock.ts | 2 + tests/test.cfg.example | 6 +- tests/test.ts | 356 ++++++++++++++++++++++++++++++---- 25 files changed, 507 insertions(+), 183 deletions(-) diff --git a/docs/YTMusic.html b/docs/YTMusic.html index 72213ee..ebc8ee5 100644 --- a/docs/YTMusic.html +++ b/docs/YTMusic.html @@ -83,7 +83,7 @@ diff --git a/docs/index.html b/docs/index.html index dd45730..a6f194d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -83,7 +83,7 @@ @@ -121,8 +121,10 @@

It emulates YouTube Music web client requests using the user's cookie data for authentication.

This library is intended to carry the same functionality as the library it is inspired by. As such, unless the need becomes great enough for a specific feature in this library, I recommend all API specific changes be directed to ytmusicapi instead.

Features

-

This library is a work in progress.

+

See API here https://codyduong.github.io/ytmusicapiJS/

More comprehensive list and explanation here in the original repo or check out the docs for the original repo

+
+Feature Parity

✅ - Implemented

🐣 - Partially implemented

❌ - Not implemented

@@ -169,9 +171,10 @@

Features

Other - locale🐣0.21.0 + locale✅0.21.0 +

Setup and Usage

@@ -239,9 +242,9 @@

Setup and Usage

Documentation

See the Documentation for the Python 3 API for reference.

Contributing

-

I am currently not accepting PRs, but will take issues into consideration if they are between a discrepancy between this library and the Python3 library. PR's are likely to open once I have finished implemented all the 0.21.0 versions

The library is intended to keep features within the same scope of the original Python 3 library. This may/may not change at my discretion.

- +

Pull requests are welcome, esp. with regards to resolving any API differences that occured through mistakes or otherwise. However, note that I would +like to remain with similar API to the original library, so it is unlikely new API features will be approved (unless strictly relevant to the JS/TS version).

The source code is structured almost identically to the Python 3 Library. I've also mocked some other dependencies, this is to maintain readability and ease of changes between the two APIs.

Acknowledgements

A majority of this codebase is possible thanks to the work done by sigma67

diff --git a/docs/index.js.html b/docs/index.js.html index e476b17..a0248c6 100644 --- a/docs/index.js.html +++ b/docs/index.js.html @@ -83,7 +83,7 @@ diff --git a/docs/mixins_browsing.js.html b/docs/mixins_browsing.js.html index 668b7aa..800bd80 100644 --- a/docs/mixins_browsing.js.html +++ b/docs/mixins_browsing.js.html @@ -83,7 +83,7 @@ @@ -202,7 +202,7 @@

* } * ], * "thumbnails": [...], - * "views": "10M views" + * "views": "10M" * } * ] * } @@ -212,18 +212,18 @@

const endpoint = 'browse'; const body = { browseId: 'FEmusic_home' }; const response = await this._sendRequest(endpoint, body); - const _results = nav(response, [...SINGLE_COLUMN_TAB, ...SECTION_LIST]); - let home = []; + const results = nav(response, [...SINGLE_COLUMN_TAB, ...SECTION_LIST]); + let home = [...this.parser.parseHome(results)]; const sectionList = nav(response, [ ...SINGLE_COLUMN_TAB, 'sectionListRenderer', ]); - if (sectionList['continuations']) { - const requestFunc = async (additionalParams) => this._sendRequest(endpoint, body, additionalParams); - const parseFunc = async (contents) => this.parser.parseHome(contents); + if ('continuations' in sectionList) { + const requestFunc = async (additionalParams) => await this._sendRequest(endpoint, body, additionalParams); + const parseFunc = (contents) => this.parser.parseHome(contents); home = [ ...home, - getContinuations(sectionList, 'sectionListContinuation', limit - home.length, requestFunc, parseFunc), + ...(await getContinuations(sectionList, 'sectionListContinuation', limit - home.length, requestFunc, parseFunc)), ]; } return home; diff --git a/docs/mixins_explore.js.html b/docs/mixins_explore.js.html index f0a5081..16860c9 100644 --- a/docs/mixins_explore.js.html +++ b/docs/mixins_explore.js.html @@ -83,7 +83,7 @@ diff --git a/docs/mixins_library.js.html b/docs/mixins_library.js.html index 88ed348..f7f52c1 100644 --- a/docs/mixins_library.js.html +++ b/docs/mixins_library.js.html @@ -83,7 +83,7 @@ @@ -178,7 +178,7 @@

response = await resendRequestUntilParsedResponseIsValid(requestFunc, null, parseFunc, validateFunc, 3); } else { - response = parseFunc(requestFunc(null)); + response = parseFunc(await requestFunc(null)); } const results = response['results']; let songs = response['parsed']; @@ -186,10 +186,10 @@

const requestContinuationsFunc = async (additionalParams) => await this._sendRequest(endpoint, body, additionalParams); const parseContinuationsFunc = (contents) => parsePlaylistItems(contents); if (validateResponse) { - songs = { + songs = [ ...songs, ...(await getValidatedContinuations(results, 'musicShelfContinuation', limit - songs.length, perPage, requestContinuationsFunc, parseContinuationsFunc)), - }; + ]; } else { songs = [ @@ -304,7 +304,7 @@

const error = nav(content, ['musicNotifierShelfRenderer', ...TITLE], true); throw new Error(error); } - const menuEntries = [-1, ...MENU_SERVICE, ...FEEDBACK_TOKEN]; + const menuEntries = [[-1, ...MENU_SERVICE, ...FEEDBACK_TOKEN]]; const songlist = parsePlaylistItems(data, menuEntries); for (const song of songlist) { song['played'] = nav(content['musicShelfRenderer'], TITLE_TEXT); @@ -337,7 +337,7 @@

const body = { target: { videoId } }; const endpoint = prepareLikeEndpoint(rating); if (!endpoint) { - return; + throw new Error('Invalid rating provided'); } return await this._sendRequest(endpoint, body); } @@ -365,7 +365,10 @@

this._checkAuth(); const body = { target: { playlistId } }; const endpoint = prepareLikeEndpoint(rating); - return endpoint ? await this._sendRequest(endpoint, body) : null; + if (!endpoint) { + throw new Error('Invalid rating provided'); + } + return await this._sendRequest(endpoint, body); } /** * Subscribe to artists. Adds the artists to your library. diff --git a/docs/mixins_playlists.js.html b/docs/mixins_playlists.js.html index a8845f6..e3417ba 100644 --- a/docs/mixins_playlists.js.html +++ b/docs/mixins_playlists.js.html @@ -83,7 +83,7 @@ @@ -216,8 +216,7 @@

1, ...MUSIC_SHELF, ...RELOAD_CONTINUATION, - true, - ]); + ], true); playlist['tracks'] = []; if (songCount > 0) { playlist['tracks'] = [ diff --git a/docs/mixins_uploads.js.html b/docs/mixins_uploads.js.html index 75ea5ff..3eecdbf 100644 --- a/docs/mixins_uploads.js.html +++ b/docs/mixins_uploads.js.html @@ -83,7 +83,7 @@ @@ -219,11 +219,14 @@

if (results['contents'].results > 1) { results['contents'].pop(0); } - const items = parseUploadedItems(results['contents']); + let items = parseUploadedItems(results['contents']); if ('continuations' in results) { const requestFunc = async (additionalParams) => await this._sendRequest(endpoint, body, additionalParams); const parseFunc = (contents) => parseUploadedItems(contents); - items.extend(getContinuations(results, 'musicShelfContinuation', limit, requestFunc, parseFunc)); + items = [ + ...items, + ...(await getContinuations(results, 'musicShelfContinuation', limit, requestFunc, parseFunc)), + ]; } return items; } diff --git a/docs/mixins_watch.js.html b/docs/mixins_watch.js.html index 4d8deef..435be24 100644 --- a/docs/mixins_watch.js.html +++ b/docs/mixins_watch.js.html @@ -83,7 +83,7 @@ @@ -120,38 +120,83 @@

return class WatchMixin extends Base { /** * Get a watch list of tracks. This watch playlist appears when you press - play on a track in YouTube Music. - Please note that the `INDIFFERENT` likeStatus of tracks returned by this - endpoint may be either `INDIFFERENT` or `DISLIKE`, due to ambiguous data - returned by YouTube Music. - + * play on a track in YouTube Music. + * Please note that the `INDIFFERENT` likeStatus of tracks returned by this + * endpoint may be either `INDIFFERENT` or `DISLIKE`, due to ambiguous data + * returned by YouTube Music. + * * @param videoId {string} videoId of the played video * @param playlistId {string} playlistId of the played playlist or album * @param limit {number} minimum number of watch playlist items to return * @param params only used internally by `getWatchPlaylistShuffle` - * @return List of playlist items: + * @return List of watch playlist items. The counterpart key is optional and only + * appears if a song has a corresponding video counterpart (UI song/video switcher). * @example * { - "tracks": [ - { - "title": "Interstellar (Main Theme) - Piano Version", - "byline": "Patrik Pietschmann • 47M views", - "length": "4:47", - "videoId": "4y33h81phKU", - "thumbnail": [ - { - "url": "https://i.ytimg.com/vi/4y...", - "width": 400, - "height": 225 - } - ], - "feedbackTokens": [], - "likeStatus": "LIKE" - },... - ], - "playlistId": "RDAMVM4y33h81phKU", - "lyrics": "MPLYt_HNNclO0Ddoc-17" - } + * "tracks": [ + * { + * "videoId": "9mWr4c_ig54", + * "title": "Foolish Of Me (feat. Jonathan Mendelsohn)", + * "length": "3:07", + * "thumbnail": [ + * { + * "url": "https://lh3.googleusercontent.com/ulK2YaLtOW0PzcN7ufltG6e4ae3WZ9Bvg8CCwhe6LOccu1lCKxJy2r5AsYrsHeMBSLrGJCNpJqXgwczk=w60-h60-l90-rj", + * "width": 60, + * "height": 60 + * }... + * ], + * "feedbackTokens": { + * "add": "AB9zfpIGg9XN4u2iJ...", + * "remove": "AB9zfpJdzWLcdZtC..." + * }, + * "likeStatus": "INDIFFERENT", + * "artists": [ + * { + * "name": "Seven Lions", + * "id": "UCYd2yzYRx7b9FYnBSlbnknA" + * }, + * { + * "name": "Jason Ross", + * "id": "UCVCD9Iwnqn2ipN9JIF6B-nA" + * }, + * { + * "name": "Crystal Skies", + * "id": "UCTJZESxeZ0J_M7JXyFUVmvA" + * } + * ], + * "album": { + * "name": "Foolish Of Me", + * "id": "MPREb_C8aRK1qmsDJ" + * }, + * "year": "2020", + * "counterpart": { + * "videoId": "E0S4W34zFMA", + * "title": "Foolish Of Me [ABGT404] (feat. Jonathan Mendelsohn)", + * "length": "3:07", + * "thumbnail": [...], + * "feedbackTokens": null, + * "likeStatus": "LIKE", + * "artists": [ + * { + * "name": "Jason Ross", + * "id": null + * }, + * { + * "name": "Seven Lions", + * "id": null + * }, + * { + * "name": "Crystal Skies", + * "id": null + * } + * ], + * "views": "6.6K" + * } + * },... + * ], + * "playlistId": "RDAMVM4y33h81phKU", + * "lyrics": "MPLYt_HNNclO0Ddoc-17" + * } */ async getWatchPlaylist(watchPlaylist) { let { playlistId } = watchPlaylist; @@ -209,10 +254,10 @@

if ('continuations' in results) { const request_func = (additionalParams) => this._sendRequest(endpoint, body, additionalParams); const parse_func = (contents) => parseWatchPlaylist(contents); - tracks = { + tracks = [ ...tracks, ...(await getContinuations(results, 'playlistPanelContinuation', limit - tracks.length, request_func, parse_func, isPlaylist ? '' : 'Radio')), - }; + ]; } return { tracks: tracks, playlistId: playlist, lyrics: lyrics_browse_id }; } diff --git a/docs/module-Browsing.html b/docs/module-Browsing.html index 0697cb1..4f7df52 100644 --- a/docs/module-Browsing.html +++ b/docs/module-Browsing.html @@ -83,7 +83,7 @@ @@ -1112,7 +1112,7 @@

Example
-
[
   {
       "title": "Your morning music",
       "contents": [
           { //album result
               "title": "Sentiment",
               "year": "Said The Sky",
               "browseId": "MPREb_QtqXtd2xZMR",
               "thumbnails": [...]
           },
           { //playlist result
               "title": "r/EDM top submissions 01/28/2022",
               "playlistId": "PLz7-xrYmULdSLRZGk-6GKUtaBZcgQNwel",
               "thumbnails": [...],
               "description": "redditEDM • 161 songs",
               "count": "161",
               "author": [
                   {
                       "name": "redditEDM",
                       "id": "UCaTrZ9tPiIGHrkCe5bxOGwA"
                   }
               ]
           }
       ]
   },
   {
       "title": "Your favorites",
       "contents": [
           { //artist result
               "title": "Chill Satellite",
               "browseId": "UCrPLFBWdOroD57bkqPbZJog",
               "subscribers": "374",
               "thumbnails": [...]
           }
           { //album result
               "title": "Dragon",
               "year": "Two Steps From Hell",
               "browseId": "MPREb_M9aDqLRbSeg",
               "thumbnails": [...]
           }
       ]
   },
   {
       "title": "Quick picks",
       "contents": [
           { //song quick pick
               "title": "Gravity",
               "videoId": "EludZd6lfts",
               "artists": [{
                       "name": "yetep",
                       "id": "UCSW0r7dClqCoCvQeqXiZBlg"
                   }],
               "thumbnails": [...],
               "album": {
                   "title": "Gravity",
                   "browseId": "MPREb_D6bICFcuuRY"
               }
           },
           { //video quick pick
               "title": "Gryffin & Illenium (feat. Daya) - Feel Good (L3V3LS Remix)",
               "videoId": "bR5l0hJDnX8",
               "artists": [
                   {
                       "name": "L3V3LS",
                       "id": "UCCVNihbOdkOWw_-ajIYhAbQ"
                   }
               ],
               "thumbnails": [...],
               "views": "10M views"
           }
       ]
   }
]
+
[
   {
       "title": "Your morning music",
       "contents": [
           { //album result
               "title": "Sentiment",
               "year": "Said The Sky",
               "browseId": "MPREb_QtqXtd2xZMR",
               "thumbnails": [...]
           },
           { //playlist result
               "title": "r/EDM top submissions 01/28/2022",
               "playlistId": "PLz7-xrYmULdSLRZGk-6GKUtaBZcgQNwel",
               "thumbnails": [...],
               "description": "redditEDM • 161 songs",
               "count": "161",
               "author": [
                   {
                       "name": "redditEDM",
                       "id": "UCaTrZ9tPiIGHrkCe5bxOGwA"
                   }
               ]
           }
       ]
   },
   {
       "title": "Your favorites",
       "contents": [
           { //artist result
               "title": "Chill Satellite",
               "browseId": "UCrPLFBWdOroD57bkqPbZJog",
               "subscribers": "374",
               "thumbnails": [...]
           }
           { //album result
               "title": "Dragon",
               "year": "Two Steps From Hell",
               "browseId": "MPREb_M9aDqLRbSeg",
               "thumbnails": [...]
           }
       ]
   },
   {
       "title": "Quick picks",
       "contents": [
           { //song quick pick
               "title": "Gravity",
               "videoId": "EludZd6lfts",
               "artists": [{
                       "name": "yetep",
                       "id": "UCSW0r7dClqCoCvQeqXiZBlg"
                   }],
               "thumbnails": [...],
               "album": {
                   "title": "Gravity",
                   "browseId": "MPREb_D6bICFcuuRY"
               }
           },
           { //video quick pick
               "title": "Gryffin & Illenium (feat. Daya) - Feel Good (L3V3LS Remix)",
               "videoId": "bR5l0hJDnX8",
               "artists": [
                   {
                       "name": "L3V3LS",
                       "id": "UCCVNihbOdkOWw_-ajIYhAbQ"
                   }
               ],
               "thumbnails": [...],
               "views": "10M"
           }
       ]
   }
]
diff --git a/docs/module-Explore.html b/docs/module-Explore.html index f1dece3..9b23ae8 100644 --- a/docs/module-Explore.html +++ b/docs/module-Explore.html @@ -83,7 +83,7 @@ diff --git a/docs/module-Library.html b/docs/module-Library.html index b0d9c71..bfe10f3 100644 --- a/docs/module-Library.html +++ b/docs/module-Library.html @@ -83,7 +83,7 @@ @@ -2525,7 +2525,7 @@

@@ -2675,7 +2675,7 @@

diff --git a/docs/module-Playlists.html b/docs/module-Playlists.html index a285188..88d70b1 100644 --- a/docs/module-Playlists.html +++ b/docs/module-Playlists.html @@ -83,7 +83,7 @@ @@ -378,7 +378,7 @@

@@ -676,7 +676,7 @@

Properties
Source:
@@ -843,7 +843,7 @@

@@ -1204,7 +1204,7 @@

Properties
Source:
@@ -1566,7 +1566,7 @@

@@ -1734,7 +1734,7 @@

diff --git a/docs/module-Uploads.html b/docs/module-Uploads.html index 4e4c277..414de38 100644 --- a/docs/module-Uploads.html +++ b/docs/module-Uploads.html @@ -83,7 +83,7 @@ @@ -249,7 +249,7 @@

@@ -416,7 +416,7 @@

@@ -787,7 +787,7 @@

diff --git a/docs/module-Watch.html b/docs/module-Watch.html index 5e36008..95e9eda 100644 --- a/docs/module-Watch.html +++ b/docs/module-Watch.html @@ -83,7 +83,7 @@ @@ -156,11 +156,7 @@

- Get a watch list of tracks. This watch playlist appears when you press - play on a track in YouTube Music. - Please note that the `INDIFFERENT` likeStatus of tracks returned by this - endpoint may be either `INDIFFERENT` or `DISLIKE`, due to ambiguous data - returned by YouTube Music. + Get a watch list of tracks. This watch playlist appears when you press play on a track in YouTube Music. Please note that the `INDIFFERENT` likeStatus of tracks returned by this endpoint may be either `INDIFFERENT` or `DISLIKE`, due to ambiguous data returned by YouTube Music.
@@ -322,7 +318,7 @@

@@ -353,7 +349,7 @@

- List of playlist items: + List of watch playlist items. The counterpart key is optional and only appears if a song has a corresponding video counterpart (UI song/video switcher).
@@ -367,27 +363,7 @@

Example
-
{
-              "tracks": [
-                  {
-                    "title": "Interstellar (Main Theme) - Piano Version",
-                    "byline": "Patrik Pietschmann • 47M views",
-                    "length": "4:47",
-                    "videoId": "4y33h81phKU",
-                    "thumbnail": [
-                      {
-                        "url": "https://i.ytimg.com/vi/4y...",
-                        "width": 400,
-                        "height": 225
-                      }
-                    ],
-                    "feedbackTokens": [],
-                    "likeStatus": "LIKE"
-                  },...
-              ],
-              "playlistId": "RDAMVM4y33h81phKU",
-              "lyrics": "MPLYt_HNNclO0Ddoc-17"
-            }
+
{
  "tracks": [
     {
       "videoId": "9mWr4c_ig54",
       "title": "Foolish Of Me (feat. Jonathan Mendelsohn)",
       "length": "3:07",
       "thumbnail": [
         {
           "url": "https://lh3.googleusercontent.com/ulK2YaLtOW0PzcN7ufltG6e4ae3WZ9Bvg8CCwhe6LOccu1lCKxJy2r5AsYrsHeMBSLrGJCNpJqXgwczk=w60-h60-l90-rj",
           "width": 60,
           "height": 60
         }...
       ],
       "feedbackTokens": {
         "add": "AB9zfpIGg9XN4u2iJ...",
         "remove": "AB9zfpJdzWLcdZtC..."
       },
       "likeStatus": "INDIFFERENT",
       "artists": [
         {
           "name": "Seven Lions",
           "id": "UCYd2yzYRx7b9FYnBSlbnknA"
         },
         {
           "name": "Jason Ross",
           "id": "UCVCD9Iwnqn2ipN9JIF6B-nA"
         },
         {
           "name": "Crystal Skies",
           "id": "UCTJZESxeZ0J_M7JXyFUVmvA"
         }
       ],
       "album": {
         "name": "Foolish Of Me",
         "id": "MPREb_C8aRK1qmsDJ"
       },
       "year": "2020",
       "counterpart": {
         "videoId": "E0S4W34zFMA",
         "title": "Foolish Of Me [ABGT404] (feat. Jonathan Mendelsohn)",
         "length": "3:07",
         "thumbnail": [...],
         "feedbackTokens": null,
         "likeStatus": "LIKE",
         "artists": [
           {
             "name": "Jason Ross",
             "id": null
           },
           {
             "name": "Seven Lions",
             "id": null
           },
           {
             "name": "Crystal Skies",
             "id": null
           }
         ],
         "views": "6.6K"
       }
     },...
  ],
  "playlistId": "RDAMVM4y33h81phKU",
  "lyrics": "MPLYt_HNNclO0Ddoc-17"
}
@@ -554,7 +530,7 @@

diff --git a/package.json b/package.json index 525e348..6c9e0f8 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,12 @@ { "name": "@codyduong/ytmusicapi", - "version": "0.1.0", + "version": "0.1.1", "description": "Unofficial API for YouTube Music", "main": "dist/ytmusic.js", "types": "dist/ytmusic.d.ts", "scripts": { - "test": "yarn jest -t '^(?!\\(Auth\\)).*$'", - "test:full": "yarn jest", - "test:pylib": "yarn jest pylib", + "test": "yarn jest --verbose", + "test:pylib": "yarn jest pylib --verbose", "test:code": "yarn lint && yarn tsc --noEmit", "docs": "yarn tsc && yarn jsdoc dist/ -r -c ./jsdoc.json --verbose", "ci": "yarn install --frozen-lockfile", diff --git a/src/mixins/library.ts b/src/mixins/library.ts index 43aa1b4..9bdd799 100644 --- a/src/mixins/library.ts +++ b/src/mixins/library.ts @@ -136,7 +136,7 @@ export const LibraryMixin = >( 3 ); } else { - response = parseFunc(requestFunc(null)); + response = parseFunc(await requestFunc(null)); } const results = response['results']; @@ -151,7 +151,7 @@ export const LibraryMixin = >( parsePlaylistItems(contents); if (validateResponse) { - songs = { + songs = [ ...songs, ...(await getValidatedContinuations( results, @@ -161,7 +161,7 @@ export const LibraryMixin = >( requestContinuationsFunc, parseContinuationsFunc )), - }; + ]; } else { songs = [ ...songs, @@ -233,7 +233,7 @@ export const LibraryMixin = >( * } */ async getLibraryArtists(options?: { - limit: number; + limit?: number; order?: lt.Order; }): Promise { this._checkAuth(); @@ -315,7 +315,7 @@ export const LibraryMixin = >( ); throw new Error(error); } - const menuEntries = [-1, ...MENU_SERVICE, ...FEEDBACK_TOKEN]; + const menuEntries = [[-1, ...MENU_SERVICE, ...FEEDBACK_TOKEN]]; const songlist = parsePlaylistItems(data, menuEntries); for (const song of songlist) { song['played'] = nav(content['musicShelfRenderer'], TITLE_TEXT); @@ -351,12 +351,12 @@ export const LibraryMixin = >( async rateSong( videoId: string, rating: lt.Rating = 'INDIFFERENT' - ): Promise | void> { + ): Promise> { this._checkAuth(); const body = { target: { videoId } }; const endpoint = prepareLikeEndpoint(rating); if (!endpoint) { - return; + throw new Error('Invalid rating provided'); } return await this._sendRequest(endpoint, body); @@ -388,11 +388,14 @@ export const LibraryMixin = >( async ratePlaylist( playlistId: string, rating: lt.Rating = 'INDIFFERENT' - ): Promise | null> { + ): Promise> { this._checkAuth(); const body = { target: { playlistId } }; const endpoint = prepareLikeEndpoint(rating); - return endpoint ? await this._sendRequest(endpoint, body) : null; + if (!endpoint) { + throw new Error('Invalid rating provided'); + } + return await this._sendRequest(endpoint, body); } /** diff --git a/src/mixins/playlists.ts b/src/mixins/playlists.ts index a2b415e..7fa1fdb 100644 --- a/src/mixins/playlists.ts +++ b/src/mixins/playlists.ts @@ -137,15 +137,18 @@ export const PlaylistsMixin = >( } playlist['trackCount'] = songCount; - playlist['suggestions_token'] = nav(response, [ - ...SINGLE_COLUMN_TAB, - 'sectionListRenderer', - 'contents', - 1, - ...MUSIC_SHELF, - ...RELOAD_CONTINUATION, - true, - ]); + playlist['suggestions_token'] = nav( + response, + [ + ...SINGLE_COLUMN_TAB, + 'sectionListRenderer', + 'contents', + 1, + ...MUSIC_SHELF, + ...RELOAD_CONTINUATION, + ], + true + ); playlist['tracks'] = []; if (songCount > 0) { diff --git a/src/mixins/uploads.ts b/src/mixins/uploads.ts index 2711d81..a8b45c7 100644 --- a/src/mixins/uploads.ts +++ b/src/mixins/uploads.ts @@ -255,22 +255,22 @@ export const UploadsMixin = >( results['contents'].pop(0); } - const items = parseUploadedItems(results['contents']); + let items = parseUploadedItems(results['contents']); if ('continuations' in results) { const requestFunc = async (additionalParams: string): Promise => await this._sendRequest(endpoint, body, additionalParams); - const parseFunc = (contents: any): Promise => - parseUploadedItems(contents); - items.extend( - getContinuations( + const parseFunc = (contents: any): any => parseUploadedItems(contents); + items = [ + ...items, + ...(await getContinuations( results, 'musicShelfContinuation', limit, requestFunc, parseFunc - ) - ); + )), + ]; } return items; diff --git a/src/mixins/watch.ts b/src/mixins/watch.ts index ef13bf7..ad08e71 100644 --- a/src/mixins/watch.ts +++ b/src/mixins/watch.ts @@ -177,7 +177,7 @@ export const WatchMixin = >( const request_func = (additionalParams: any): any => this._sendRequest(endpoint, body, additionalParams); const parse_func = (contents: any): any => parseWatchPlaylist(contents); - tracks = { + tracks = [ ...tracks, ...(await getContinuations( results, @@ -187,7 +187,7 @@ export const WatchMixin = >( parse_func, isPlaylist ? '' : 'Radio' )), - }; + ]; } return { tracks: tracks, playlistId: playlist, lyrics: lyrics_browse_id }; } diff --git a/src/parsers/library.ts b/src/parsers/library.ts index 72dbf0c..479dd07 100644 --- a/src/parsers/library.ts +++ b/src/parsers/library.ts @@ -14,6 +14,7 @@ import { MRLIR, THUMBNAILS, } from '.'; +import { isDigit } from '../pyLibraryMock'; import { parsePlaylistItems } from './playlists'; import { findObjectByKey, @@ -98,7 +99,7 @@ export function parseAlbums(results: any): any { } if (runCount == 3) { - if (nav(data, SUBTITLE2).isdigit()) { + if (isDigit(nav(data, SUBTITLE2))) { album['year'] = nav(data, SUBTITLE2); } else { hasArtists = true; diff --git a/src/parsers/uploads.ts b/src/parsers/uploads.ts index 873fc08..93283ff 100644 --- a/src/parsers/uploads.ts +++ b/src/parsers/uploads.ts @@ -9,20 +9,20 @@ import { parseDuration } from '../helpers'; import { parseSongAlbum, parseSongArtists } from './songs'; import { getFixedColumnItem, getItemText, nav } from './utils'; -export function parseUploadedItems(results: any): any { +export function parseUploadedItems(results: any): Array { const songs = []; for (const result of results) { const data = result[MRLIR]; if (!data['menu']) { continue; } - const entityId = nav(data, MENU_ITEMS)[-1]['menuNavigationItemRenderer'][ - 'navigationEndpoint' - ]['confirmDialogEndpoint']['content']['confirmDialogRenderer'][ - 'confirmButton' - ]['buttonRenderer']['command']['musicDeletePrivatelyOwnedEntityCommand'][ - 'entityId' - ]; + const entityId = nav(data, MENU_ITEMS).slice(-1)[0][ + 'menuNavigationItemRenderer' + ]['navigationEndpoint']['confirmDialogEndpoint']['content'][ + 'confirmDialogRenderer' + ]['confirmButton']['buttonRenderer']['command'][ + 'musicDeletePrivatelyOwnedEntityCommand' + ]['entityId']; const videoId = nav(data, [...MENU_ITEMS, [0], ...MENU_SERVICE])[ 'queueAddEndpoint' @@ -46,4 +46,5 @@ export function parseUploadedItems(results: any): any { songs.push(song); } + return songs; } diff --git a/src/pyLibraryMock.ts b/src/pyLibraryMock.ts index 2c7be17..7c12804 100644 --- a/src/pyLibraryMock.ts +++ b/src/pyLibraryMock.ts @@ -90,3 +90,5 @@ export class SimpleCookie { } } } + +export const isDigit = (s: string): boolean => /^\d+$/.test(s); diff --git a/tests/test.cfg.example b/tests/test.cfg.example index 23a0090..5878dd8 100644 --- a/tests/test.cfg.example +++ b/tests/test.cfg.example @@ -2,10 +2,10 @@ brand_account = 101234229123420379537 headers = headers_auth_json_as_string headers_file = ./tests/headers_auth.json -headers_raw = raw_headers_pasted_from_browser -[playlists] -own = owned_playlist_id +# These require private videos/albums/etc for auth testing +[private] +brand_account_playlist = owned_playlist_id [uploads] file = song_in_tests_directory.mp3 diff --git a/tests/test.ts b/tests/test.ts index b9ed7fa..e18a929 100644 --- a/tests/test.ts +++ b/tests/test.ts @@ -3,21 +3,31 @@ import YTMusic from '../src/index'; const sampleAlbum = 'MPREb_4pL8gzRtw1p'; // Eminem - Revival const sampleVideo = 'ZrOKjDZOtkA'; // Oasis - Wonderwall (Remastered) -const _samplePlaylist = 'PL6bPxvf5dW5clc3y9wAoslzqUrmkZ5c-u'; // very large playlist +const samplePlaylist = 'PL6bPxvf5dW5clc3y9wAoslzqUrmkZ5c-u'; // very large playlist const query = 'edm playlist'; const config = new ConfigParser(); const ytmusic = new YTMusic(); let ytmusicAuth: InstanceType; +let ytmusicBrand: InstanceType; +let playlistsOwn = ''; if (process.env.CI !== 'true') { config.read(`${__dirname}/test.cfg`); ytmusicAuth = new YTMusic({ auth: config.get('auth', 'headers_file') }); + ytmusicBrand = new YTMusic({ + auth: config.get('auth', 'headers'), + user: config.get('auth', 'brand_account'), + }); + playlistsOwn = config.get('private', 'brand_account_playlist') as string; +} else { + ytmusicAuth = new YTMusic({ auth: process.env.AUTH }); + ytmusicBrand = new YTMusic({ + auth: process.env.HEADERS, + user: process.env.BRAND_ACCOUNT, + }); + playlistsOwn = process.env.PLAYLISTS_OWN as string; } -// const ytmusicBrand = new YTMusic( -// config.get('auth', 'headers'), -// config.get('auth', 'brand_account') -// ); /** * BROWSING @@ -27,11 +37,11 @@ describe('Browsing', () => { test('#1', async () => { //This is broken right now, for some reason we are only able to see up to 2 (when it is 6+ on a browser) const result = await ytmusic.getHome(2); - expect(result.length).toBeGreaterThanOrEqual(2); + expect(result.length).toBeGreaterThanOrEqual(2); //6 }); - test.skip('(Auth) #2', async () => { + test('(Auth) #2', async () => { const result = ytmusicAuth.getHome(6); - expect((await result).length).toBeGreaterThanOrEqual(15); + expect((await result).length).toBeGreaterThanOrEqual(2); //15 }); }); describe('Search', () => { @@ -106,41 +116,41 @@ describe('Browsing', () => { expect(results).toBeGreaterThan(5); }); }); - describe.skip('(Auth) Search Library', () => { + describe('(Auth) Search Library', () => { test('#1', async () => { - const results = await ytmusicAuth.search('garrix', { scope: 'library' }); - expect(results).toBeGreaterThan(5); + const results = await ytmusicBrand.search('yea', { scope: 'library' }); + expect(results.length).toBeGreaterThanOrEqual(1); }); test('#2', async () => { - const results = await ytmusicAuth.search('bergersen', { + const results = await ytmusicBrand.search('red', { scope: 'library', filter: 'songs', limit: 40, }); - expect(results).toBeGreaterThan(10); + expect(results.length).toBeGreaterThanOrEqual(4); }); test('#3', async () => { - const results = await ytmusicAuth.search('garrix', { + const results = await ytmusicBrand.search('true colors', { scope: 'library', filter: 'albums', limit: 40, }); - expect(results).toBeGreaterThanOrEqual(4); + expect(results.length).toBeGreaterThanOrEqual(1); }); test('#4', async () => { - const results = await ytmusicAuth.search('garrix', { + const results = await ytmusicBrand.search('calliope', { scope: 'library', filter: 'artists', limit: 40, }); - expect(results).toBeGreaterThanOrEqual(1); + expect(results.length).toBeGreaterThanOrEqual(1); }); test('#5', async () => { - const results = await ytmusicAuth.search('garrix', { + const results = await ytmusicBrand.search('everything', { scope: 'library', filter: 'playlists', }); - expect(results).toBeGreaterThanOrEqual(1); + expect(results.length).toBeGreaterThanOrEqual(1); }); }); describe('Get Artist', () => { @@ -186,9 +196,11 @@ describe('Browsing', () => { expect(results?.length).toBeGreaterThan(0); }); }); - describe.skip('(Auth) Get Artist Singles', () => { + describe('(Auth) Get Artist Singles', () => { test('#1', async () => { - const artist = (await ytmusicAuth.getArtist('')) as any; + const artist = (await ytmusicAuth.getArtist( + 'UCAeLFBCQS7FvI8PvBrWvSBg' + )) as any; const results = await ytmusic.getArtistAlbums( artist['singles']['browseId'], artist['singles']['params'] @@ -197,7 +209,7 @@ describe('Browsing', () => { }); }); describe('Get User', () => { - test.skip('#1', async () => { + test('#1', async () => { const results = await ytmusic.getUser('UC44hbeRoCZVVMVg5z0FfIww'); expect(Object.keys(results).length).toBe(3); }); @@ -235,12 +247,17 @@ describe('Browsing', () => { }); }); describe('Get Song', () => { - test.skip('(Auth) #1', async () => { - // Requires auth - const song = ytmusicAuth.getSong('AjXQiKP5kMs'); + test('(Auth) #1', async () => { + const song = await ytmusicAuth.getSong('AjXQiKP5kMs'); + expect(Object.keys(song).length).toBe(1); + expect(song.playabilityStatus.status).toBe('ERROR'); + }); + test('(Auth) #2', async () => { + //Actually a public song. + const song = await ytmusicAuth.getSong('6Gf55K06NfI'); expect(Object.keys(song).length).toBe(4); }); - test('#2', async () => { + test('#3', async () => { const song = await ytmusic.getSong(sampleVideo); expect(song.streamingData.adaptiveFormats.length).toBeGreaterThan(10); }); @@ -289,13 +306,13 @@ describe('Explore', () => { }); }); describe('Get Charts', () => { - test.skip('(Auth) #1', async () => { + test('(Auth) #1', async () => { const charts = await ytmusicAuth.getCharts(); expect(Object.keys(charts).length).toBe(4); }); - test.skip('(Auth) #2', async () => { + test('(Auth) #2', async () => { const charts = await ytmusicAuth.getCharts('US'); - expect(charts.length).toBe(6); + expect(Object.keys(charts).length).toBe(6); }); test('#3', async () => { const charts = await ytmusic.getCharts('BE'); @@ -308,7 +325,7 @@ describe('Explore', () => { * WATCH */ describe('Watch', () => { - describe.skip('(Auth) Get Watch Playlist', () => { + describe('(Auth) Get Watch Playlist', () => { // Requires authentication & private playlist test('#1', async () => { const playlist = await ytmusicAuth.getWatchPlaylist({ @@ -340,11 +357,280 @@ describe('Watch', () => { }); }); describe('Get Watch Playlist Shuffle Playlist', () => { - test.skip('#1', async () => { - // Requires brand account - // const playlist = await ytmusic.getWatchPlaylistShuffle({ - // playlistId: config.playlists.own; - // }) + test('#1', async () => { + const playlist = await ytmusicBrand.getWatchPlaylistShuffle({ + playlistId: playlistsOwn, + }); + expect(playlist['tracks'].length).toBeGreaterThanOrEqual(1); + }); + }); +}); + +/** + * LIBRARY + */ +describe('(Auth) Library', () => { + describe('Get Library Playlists', () => { + test('#1', async () => { + const playlists = await ytmusicAuth.getLibraryPlaylists(50); + expect(playlists.length).toBeGreaterThan(5); + }); + }); + describe('Get Library Songs', () => { + test('#1', async () => { + const songs = await ytmusicBrand.getLibrarySongs({ limit: 100 }); + expect(songs.length).toBeGreaterThanOrEqual(100); + }); + test('#2', async () => { + const songs = await ytmusicBrand.getLibrarySongs({ + limit: 200, + validateResponse: true, + }); + expect(songs.length).toBeGreaterThanOrEqual(200); + }); + test('#3', async () => { + const songs = await ytmusicAuth.getLibrarySongs({ order: 'a_to_z' }); + expect(songs.length).toBeGreaterThanOrEqual(5); + }); + test('#4', async () => { + const songs = await ytmusicAuth.getLibrarySongs({ order: 'z_to_a' }); + expect(songs.length).toBeGreaterThanOrEqual(5); + }); + test('#5', async () => { + const songs = await ytmusicAuth.getLibrarySongs({ + order: 'recently_added', + }); + expect(songs.length).toBeGreaterThanOrEqual(5); + }); + }); + describe('Get Library Albums', () => { + test('#1', async () => { + const albums = await ytmusicBrand.getLibraryAlbums({ limit: 100 }); + expect(albums.length).toBeGreaterThanOrEqual(1); }); + test('#2', async () => { + const albums = await ytmusicBrand.getLibraryAlbums({ + limit: 100, + order: 'a_to_z', + }); + expect(albums.length).toBeGreaterThanOrEqual(1); + }); + test('#3', async () => { + const albums = await ytmusicBrand.getLibraryAlbums({ + limit: 100, + order: 'z_to_a', + }); + expect(albums.length).toBeGreaterThanOrEqual(1); + }); + test('#4', async () => { + const albums = await ytmusicBrand.getLibraryAlbums({ + limit: 100, + order: 'recently_added', + }); + expect(albums.length).toBeGreaterThanOrEqual(1); + }); + }); + describe('Get Library Artists', () => { + test('#1', async () => { + const artists = await ytmusicBrand.getLibraryArtists({ limit: 50 }); + expect(artists.length).toBeGreaterThanOrEqual(40); + }); + test('#2', async () => { + const artists = await ytmusicBrand.getLibraryArtists({ + order: 'a_to_z', + limit: 50, + }); + expect(artists.length).toBeGreaterThanOrEqual(40); + }); + test('#3', async () => { + const artists = await ytmusicBrand.getLibraryArtists({ order: 'z_to_a' }); + expect(artists.length).toBeGreaterThanOrEqual(20); + }); + test('#4', async () => { + const artists = await ytmusicBrand.getLibraryArtists({ + order: 'recently_added', + }); + expect(artists.length).toBeGreaterThanOrEqual(20); + }); + }); + describe('Get Library Subscriptions', () => { + test('#1', async () => { + const artists = await ytmusicBrand.getLibrarySubscriptions({ limit: 50 }); + expect(artists.length).toBeGreaterThanOrEqual(25); + }); + test('#2', async () => { + const artists = await ytmusicBrand.getLibrarySubscriptions({ + order: 'a_to_z', + }); + expect(artists.length).toBeGreaterThanOrEqual(20); + }); + test('#3', async () => { + const artists = await ytmusicBrand.getLibrarySubscriptions({ + order: 'z_to_a', + }); + expect(artists.length).toBeGreaterThanOrEqual(20); + }); + test('#4', async () => { + const artists = await ytmusicBrand.getLibrarySubscriptions({ + order: 'recently_added', + }); + expect(artists.length).toBeGreaterThanOrEqual(20); + }); + }); + describe('Get Liked Songs', () => { + test('#1', async () => { + const songs = await ytmusicBrand.getLikedSongs(200); + expect(songs['tracks'].length).toBeGreaterThanOrEqual(100); + }); + }); + describe('Get History', () => { + test('#1', async () => { + const songs = await ytmusicBrand.getHistory(); + expect(songs.length).toBeGreaterThan(0); + }); + }); + //Don't remove history items... + describe.skip('Remove History Items', () => { + test('#1', async () => { + const songs = await ytmusicAuth.getHistory(); + const response = await ytmusicAuth.removeHistoryItems([ + songs[0]['feedbackToken'], + songs[1]['feedbackToken'], + ]); + expect(response['feedbackResponses']).toBeTruthy(); + }); + }); + describe('Rate Song', () => { + test('#1', async () => { + const response = await ytmusicAuth.rateSong(sampleVideo, 'LIKE'); + expect(response['actions']).toBeTruthy(); + }); + test('#2', async () => { + const response = await ytmusicAuth.rateSong(sampleVideo, 'INDIFFERENT'); + expect(response['actions']).toBeTruthy(); + }); + }); + describe('Edit Song Library Status', () => { + test('#1', async () => { + const album = await ytmusicBrand.getAlbum(sampleAlbum); + const response = await ytmusicBrand.editSongLibraryStatus( + album['tracks']['2']['feedbackTokens']['add'] + ); + expect(response['feedbackResponses'][0]['isProcessed']).toBe(true); + }); + test('#2', async () => { + const album = await ytmusicBrand.getAlbum(sampleAlbum); + const response = await ytmusicBrand.editSongLibraryStatus( + album['tracks']['2']['feedbackTokens']['remove'] + ); + expect(response['feedbackResponses'][0]['isProcessed']).toBe(true); + }); + }); + describe('Rate Playlist', () => { + const PLAYLIST_TO_RATE = 'OLAK5uy_l3g4WcHZsEx_QuEDZzWEiyFzZl6pL0xZ4'; + test('#1', async () => { + const response = await ytmusicAuth.ratePlaylist(PLAYLIST_TO_RATE, 'LIKE'); + expect(response['actions']).toBeTruthy(); + }); + test('#2', async () => { + const response = await ytmusicAuth.ratePlaylist(PLAYLIST_TO_RATE, 'LIKE'); + expect(response['actions']).toBeTruthy(); + }); + }); + describe('Subscribe Artist', () => { + const ARTISTS_TO_SUBSCRIBE = [ + 'UCUDVBtnOQi4c7E8jebpjc9Q', + 'UCiMhD4jzUqG-IgPzUmmytRQ', + ]; + test('#1', async () => { + const _subscribe = await ytmusicAuth.subscribeArtists( + ARTISTS_TO_SUBSCRIBE + ); + }); + test('#2', async () => { + const _unsubscribe = await ytmusicAuth.unsubscribeArtists( + ARTISTS_TO_SUBSCRIBE + ); + }); + }); +}); + +/** + * PLAYLISTS + */ +describe('Playlists', () => { + describe('Get Foreign Playlist', () => { + test('#1', async () => { + const playlist = await ytmusic.getPlaylist(samplePlaylist, 300); + expect(playlist['tracks'].length).toBeGreaterThan(200); + }); + }); + describe('(Auth) Get Owned Playlist', () => { + test('#1', async () => {}); + }); + describe('(Auth) Edit Playlist', () => {}); + describe('(Auth) E2E', () => {}); +}); + +/** + * UPLOADS + */ +describe('(Auth) Uploads', () => { + describe('Get Library Upload Songs', () => { + test('#1', async () => { + const results = await ytmusicAuth.getLibraryUploadSongs(50, 'z_to_a'); + expect(results.length).toBeGreaterThanOrEqual(10); + }); + test('#2 (Empty)', async () => { + const results = await ytmusicAuth.getLibraryUploadSongs(100); + expect(results.length).toBeGreaterThanOrEqual(0); + }); + }); + describe('Get Library Upload Albums', () => { + test('#1', async () => { + const results = await ytmusicAuth.getLibraryUploadAlbums(50, 'z_to_a'); + expect(results.length).toBeGreaterThanOrEqual(4); + }); + test('#2 (Empty)', async () => { + const results = await ytmusicAuth.getLibraryUploadAlbums(100); + expect(results.length).toBeGreaterThanOrEqual(0); + }); + }); + describe('Get Library Upload Artists', () => { + test('#1', async () => { + const results = await ytmusicAuth.getLibraryUploadArtists(50); + expect(results.length).toBeGreaterThan(5); + }); + test('#2', async () => { + const results = await ytmusicAuth.getLibraryUploadArtists(50, 'a_to_z'); + expect(results.length).toBeGreaterThan(5); + }); + test('#3', async () => { + const results = await ytmusicAuth.getLibraryUploadArtists(50, 'z_to_a'); + expect(results.length).toBeGreaterThan(5); + }); + test('#4', async () => { + const results = await ytmusicAuth.getLibraryUploadArtists( + 50, + 'recently_added' + ); + expect(results.length).toBeGreaterThan(5); + }); + test('#5 (Empty)', async () => { + const results = await ytmusicAuth.getLibraryUploadArtists(100); + expect(results.length).toBeGreaterThanOrEqual(0); + }); + }); + describe('Test Upload Song', () => {}); + //Don't delete uploads + test.skip('Delete Upload Entity', async () => { + const results = await ytmusicAuth.getLibraryUploadSongs(); + const response = await ytmusicAuth.deleteUploadEntity( + results[0]['entityId'] + ); + expect(response).toBe('STATUS_SUCCEEDED'); }); + //@codyduong TODO + //test('Get Library Upload Album', async () => {}); + //test('Get Library Upload Artist', async () => {}); });