├── docs ├── .nojekyll ├── assets │ └── images │ │ ├── icons.png │ │ ├── icons@2x.png │ │ ├── widgets.png │ │ └── widgets@2x.png ├── modules │ ├── _lib_context_.html │ ├── _lib_headers_.html │ ├── _index_.html │ ├── _models_album_.html │ ├── _models_artist_.html │ ├── _models_carousel_.html │ ├── _models_playlist_.html │ ├── _models_subtitle_.html │ ├── _models_search_.html │ ├── _models_homepage_.html │ ├── _models_thumbnail_.html │ ├── _models_navigationendpoint_.html │ ├── _models_song_.html │ ├── _models_carouselitem_.html │ └── _lib_endpoints_player_.html ├── interfaces │ ├── _models_search_.searchitem.html │ └── _models_subtitle_.subtitle.html └── globals.html ├── .prettierignore ├── .yarnrc.yml ├── cookie_example.json ├── .prettierrc ├── models ├── Thumbnail.ts ├── Search.ts ├── Subtitle.ts ├── HomePage.ts ├── Album.ts ├── Carousel.ts ├── NavigationEndpoint.ts ├── Playlist.ts ├── Artist.ts ├── Song.ts └── CarouselItem.ts ├── lib ├── headers.json ├── endpoints │ ├── Player.ts │ ├── Browse.ts │ ├── Browsing.ts │ ├── HomePage.ts │ ├── Search.ts │ └── Playlist.ts ├── context.json ├── continuations.ts └── utils.ts ├── tsconfig.json ├── LICENSE ├── .github └── workflows │ └── docs_lint.yml ├── package.json ├── .gitignore ├── .npmignore ├── README.md └── index.ts /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | node_modules/ -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /cookie_example.json: -------------------------------------------------------------------------------- 1 | {"cookie": "content"} -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } -------------------------------------------------------------------------------- /docs/assets/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vladdenisov/ytmusic-api/HEAD/docs/assets/images/icons.png -------------------------------------------------------------------------------- /docs/assets/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vladdenisov/ytmusic-api/HEAD/docs/assets/images/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/images/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vladdenisov/ytmusic-api/HEAD/docs/assets/images/widgets.png -------------------------------------------------------------------------------- /docs/assets/images/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vladdenisov/ytmusic-api/HEAD/docs/assets/images/widgets@2x.png -------------------------------------------------------------------------------- /models/Thumbnail.ts: -------------------------------------------------------------------------------- 1 | export default interface Thumbnail { 2 | readonly height: number 3 | readonly width: number 4 | readonly url: string 5 | } 6 | -------------------------------------------------------------------------------- /models/Search.ts: -------------------------------------------------------------------------------- 1 | import Subtitle from './Subtitle' 2 | import { Song } from './Song' 3 | 4 | export default interface SearchItem { 5 | query: string 6 | contents: any[] 7 | } 8 | -------------------------------------------------------------------------------- /models/Subtitle.ts: -------------------------------------------------------------------------------- 1 | import NavigationEndpoint from './NavigationEndpoint' 2 | export default interface Subtitle { 3 | readonly text: string 4 | navigationEndpoint?: NavigationEndpoint 5 | } 6 | -------------------------------------------------------------------------------- /models/HomePage.ts: -------------------------------------------------------------------------------- 1 | import Carousel from './Carousel' 2 | 3 | export default interface HomePage { 4 | title: string 5 | content?: Carousel[] 6 | browseId: string 7 | continue?: () => Promise 8 | } 9 | -------------------------------------------------------------------------------- /models/Album.ts: -------------------------------------------------------------------------------- 1 | import Thumbnail from './Thumbnail' 2 | 3 | export default interface Album { 4 | title: Text 5 | thumbnail: Thumbnail[] 6 | year: string 7 | browseId: string 8 | url: string 9 | author: string 10 | } 11 | -------------------------------------------------------------------------------- /models/Carousel.ts: -------------------------------------------------------------------------------- 1 | import CarouselItem from './CarouselItem' 2 | import NavigationEndpoint from './NavigationEndpoint' 3 | export default interface Carousel { 4 | readonly title: string 5 | readonly strapline?: { text: string; navigationEndpoint?: NavigationEndpoint } 6 | readonly content: Array 7 | } 8 | -------------------------------------------------------------------------------- /lib/headers.json: -------------------------------------------------------------------------------- 1 | { 2 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:72.0) Gecko/20100101 Firefox/72.0", 3 | "Accept": "*/*", 4 | "Accept-Language": "en-US,en;q=0.5", 5 | "Content-Type": "application/json", 6 | "X-Goog-AuthUser": "0", 7 | "origin": "https://music.youtube.com", 8 | "X-Goog-Visitor-Id": "CgtWaTB2WWRDeEFUYyjhv-X8BQ%3D%3D" 9 | } -------------------------------------------------------------------------------- /models/NavigationEndpoint.ts: -------------------------------------------------------------------------------- 1 | export default interface NavigationEndpoint { 2 | browseEndpoint?: { 3 | browseId: string 4 | pageType: string 5 | } 6 | watchEndpoint?: { 7 | params?: string 8 | playlistId?: string 9 | videoId: string 10 | } 11 | watchPlaylistEndpoint?: { 12 | playlistId: string 13 | params: string 14 | } 15 | clickTrackingParams: string 16 | } 17 | -------------------------------------------------------------------------------- /models/Playlist.ts: -------------------------------------------------------------------------------- 1 | import Subtitle from './Subtitle' 2 | import Thumbnail from './Thumbnail' 3 | import { Song } from './Song' 4 | 5 | export default interface Playlist { 6 | title: string 7 | thumbnail: Thumbnail[] 8 | subtitle?: Subtitle[] 9 | secondSubtitle?: Subtitle[] 10 | playlistId: string 11 | content: Song[] 12 | setVideoId?: string 13 | continue?: () => Promise 14 | } 15 | -------------------------------------------------------------------------------- /models/Artist.ts: -------------------------------------------------------------------------------- 1 | import { Song } from './Song' 2 | import Album from './Album' 3 | 4 | export default interface Artist { 5 | name: string 6 | description?: string 7 | views?: string 8 | songs: { 9 | browseId?: string 10 | results: Song[] 11 | } 12 | albums: { 13 | browseId?: string 14 | results: Album[] 15 | } 16 | singles?: { 17 | browseId?: string 18 | results: Album[] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es2017", "es7", "es6", "dom"], 6 | "declaration": true, 7 | "outDir": "dist", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true, 11 | "downlevelIteration": true 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | "dist", 16 | "*.json", 17 | "testing" 18 | ] 19 | } -------------------------------------------------------------------------------- /models/Song.ts: -------------------------------------------------------------------------------- 1 | import Thumbnail from './Thumbnail' 2 | import NavigationEndpoint from './NavigationEndpoint' 3 | 4 | export interface Song { 5 | readonly title: Text | string 6 | readonly duration?: string 7 | readonly thumbnail: Thumbnail[] 8 | readonly author: Text[] 9 | readonly album?: Text 10 | readonly url: string 11 | readonly id: string 12 | } 13 | interface Text { 14 | text: string 15 | navigationEndpoint?: NavigationEndpoint 16 | } 17 | -------------------------------------------------------------------------------- /models/CarouselItem.ts: -------------------------------------------------------------------------------- 1 | import Subtitle from './Subtitle' 2 | import NavigationEndpoint from './NavigationEndpoint' 3 | export default interface CarouselItem { 4 | readonly thumbnail: Array<{ 5 | height: number 6 | width: number 7 | url: string 8 | }> 9 | readonly title: string 10 | readonly subtitle: Subtitle[] 11 | readonly navigationEndpoint: NavigationEndpoint 12 | } 13 | interface CarouselItemTitle { 14 | readonly text: string 15 | readonly navigationEndpoint: NavigationEndpoint 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Vlad Denisov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/docs_lint.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Docs Updater & Linter 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm install 28 | - run: npm run lint 29 | - run: npm run docs 30 | - name: Commit changes 31 | uses: EndBug/add-and-commit@v4 32 | with: 33 | author_name: Vlad Denisov 34 | author_email: vlad.a.denisov@gmail.com 35 | message: "ci: lint & update docs" 36 | add: "*" 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /lib/endpoints/Player.ts: -------------------------------------------------------------------------------- 1 | import { Song } from '../../models/Song' 2 | import * as utils from '../utils' 3 | 4 | export const Player = async ( 5 | cookie: string, 6 | args: any, 7 | videoId: string 8 | ): Promise => { 9 | const body: any = utils.generateBody({ userID: args.userID }) 10 | body.videoId = videoId 11 | const response = await utils.sendRequest(cookie, { 12 | endpoint: 'next', 13 | userID: args.userID, 14 | authUser: args.authUser, 15 | body 16 | }) 17 | if (response.error) throw new Error(response.error.status) 18 | const content = 19 | response.contents.singleColumnMusicWatchNextResultsRenderer.tabbedRenderer 20 | .watchNextTabbedResultsRenderer.tabs[0].tabRenderer.content 21 | .musicQueueRenderer.content.playlistPanelRenderer.contents[0] 22 | .playlistPanelVideoRenderer 23 | return { 24 | title: content.title.runs[0].text, 25 | duration: content.lengthText.runs[0].text, 26 | thumbnail: content.thumbnail.thumbnails, 27 | author: [content.longBylineText.runs[0]], 28 | album: content.longBylineText.runs[content.longBylineText.runs - 3], 29 | url: `https://music.youtube.com/watch?v=${content.navigationEndpoint.watchEndpoint.videoId}`, 30 | id: content.navigationEndpoint.watchEndpoint.videoId 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/context.json: -------------------------------------------------------------------------------- 1 | { 2 | "context": { 3 | "capabilities": {}, 4 | "client": { 5 | "clientName": "WEB_REMIX", 6 | "clientVersion": "0.1", 7 | "experimentIds": [], 8 | "experimentsToken": "", 9 | "gl": "DE", 10 | "hl": "en", 11 | "locationInfo": { 12 | "locationPermissionAuthorizationStatus": "LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED" 13 | }, 14 | "musicAppInfo": { 15 | "musicActivityMasterSwitch": "MUSIC_ACTIVITY_MASTER_SWITCH_INDETERMINATE", 16 | "musicLocationMasterSwitch": "MUSIC_LOCATION_MASTER_SWITCH_INDETERMINATE", 17 | "pwaInstallabilityStatus": "PWA_INSTALLABILITY_STATUS_UNKNOWN" 18 | }, 19 | "utcOffsetMinutes": 60 20 | }, 21 | "request": { 22 | "internalExperimentFlags": [ 23 | { 24 | "key": "force_music_enable_outertube_tastebuilder_browse", 25 | "value": "true" 26 | }, 27 | { 28 | "key": "force_music_enable_outertube_playlist_detail_browse", 29 | "value": "true" 30 | }, 31 | { 32 | "key": "force_music_enable_outertube_search_suggestions", 33 | "value": "true" 34 | } 35 | ], 36 | "sessionIndex": {} 37 | }, 38 | "user": { 39 | "enableSafetyMode": false 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ytmusic", 3 | "version": "0.0.9", 4 | "description": "Unofficial Youtube Music Api", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "keywords": [ 10 | "youtube api", 11 | "youtube music", 12 | "ytmusic", 13 | "youtube", 14 | "music" 15 | ], 16 | "types": "dist/index.d.ts", 17 | "repository": "git+https://github.com/vladdenisov/ytmusic-api.git", 18 | "bugs": { 19 | "url": "https://github.com/vladdenisov/ytmusic-api/issues" 20 | }, 21 | "homepage": "https://github.com/vladdenisov/ytmusic-api#readme", 22 | "scripts": { 23 | "test": "echo \"Error: no test specified\"", 24 | "lint": "prettier \"{,!(node_modules)/**/}*.{ts,js}\" --write", 25 | "build": "tsc", 26 | "watch": "tsc --watch", 27 | "docs": "typedoc --out docs && touch docs/.nojekyll" 28 | }, 29 | "author": "Vlad Denisov", 30 | "license": "MIT", 31 | "prettier": { 32 | "trailingComma": "none", 33 | "tabWidth": 2, 34 | "semi": false, 35 | "singleQuote": true, 36 | "parser": "typescript" 37 | }, 38 | "dependencies": { 39 | "axios": "^0.21.1", 40 | "cookie": "^0.4.1", 41 | "node-fetch": "^2.6.0", 42 | "sha1": "^1.1.1" 43 | }, 44 | "devDependencies": { 45 | "@types/cookie": "^0.4.0", 46 | "@types/node-fetch": "^2.5.7", 47 | "eledoc": "^0.2.1", 48 | "husky": "^4.2.5", 49 | "prettier": "^2.0.5", 50 | "typedoc": "^0.17.7", 51 | "typescript": "^3.9.5" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/continuations.ts: -------------------------------------------------------------------------------- 1 | import * as utils from './utils' 2 | 3 | export interface ContinuationData { 4 | nextContinuationData: { 5 | continuation: string 6 | clickTrackingParams: string 7 | } 8 | } 9 | 10 | export interface ContinuationsContainer { 11 | continuations: { 12 | [index: number]: ContinuationData 13 | } 14 | } 15 | 16 | function pickContinuationObject(continuationContents: any) { 17 | for (const key of Object.keys(continuationContents)) { 18 | if (key.endsWith('Continuation')) { 19 | return continuationContents[key] 20 | } 21 | } 22 | } 23 | 24 | export function createContinuation( 25 | cookie: string, 26 | args: { userID?: string; authUser?: number } | undefined, 27 | parseContents: (renderer: any) => C, 28 | baseObject: T, 29 | container?: ContinuationsContainer 30 | ): (() => Promise) | undefined { 31 | if (!container?.continuations?.[0]?.nextContinuationData) { 32 | return 33 | } 34 | 35 | const continuation = container.continuations[0].nextContinuationData 36 | 37 | return async () => { 38 | const body: any = utils.generateBody({ userID: args?.userID }) 39 | const response = await utils.sendRequest(cookie, { 40 | endpoint: 'browse', 41 | body, 42 | cToken: continuation.continuation, 43 | itct: continuation.clickTrackingParams, 44 | authUser: args?.authUser 45 | }) 46 | 47 | const data = pickContinuationObject(response.continuationContents) 48 | const content = parseContents(data.contents) 49 | const result = { 50 | ...baseObject, 51 | content, 52 | continue: createContinuation( 53 | cookie, 54 | args, 55 | parseContents, 56 | baseObject, 57 | data 58 | ) 59 | } 60 | return result 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | lerna-debug.log* 7 | package-lock.* 8 | .DS_Store 9 | # Config 10 | config.json 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | cookie.json 112 | 113 | .dist/ 114 | .vscode/ 115 | testing/ 116 | 117 | .yarn/* 118 | !.yarn/patches 119 | !.yarn/releases 120 | !.yarn/plugins 121 | !.yarn/sdks 122 | !.yarn/versions 123 | 124 | .DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | lerna-debug.log* 7 | package-lock.* 8 | # Config 9 | config.json 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and not Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # Stores VSCode versions used for testing VSCode extensions 108 | .vscode-test 109 | 110 | cookie.json 111 | node_modules 112 | 113 | .dist/ 114 | .vscode/ 115 | testing/ 116 | models/ 117 | lib/ 118 | index.ts 119 | cookie_example.json 120 | docs/ 121 | .github/ 122 | .prettierrc 123 | .prettierignore 124 | tsconfig.json 125 | 126 | 127 | .yarn/* 128 | !.yarn/patches 129 | !.yarn/releases 130 | !.yarn/plugins 131 | !.yarn/sdks 132 | !.yarn/versions -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unofficial YouTube Music API 2 | ![GitHub last commit](https://img.shields.io/github/last-commit/vladdenisov/ytmusic-api) ![GitHub](https://img.shields.io/github/license/vladdenisov/ytmusic-api) ![npm bundle size](https://img.shields.io/bundlephobia/min/ytmusic) ![Snyk Vulnerabilities for GitHub Repo](https://img.shields.io/snyk/vulnerabilities/github/vladdenisov/ytmusic-api) 3 | 4 | Simple API Project for NodeJS written in TypeScript inspired by [sigma67's python library](https://github.com/sigma67/ytmusicapi) 5 | 6 | ## Getting Started 7 | 8 | These instructions will get you a copy of the project up and running on your local machine for development. 9 | 10 | ### Prerequisites 11 | 12 | 1. NodeJS 10+ 13 | 2. Yarn v2 14 | 3. Google Account (optional) 15 | 16 | ### Installing 17 | #### Using npm 18 | [![NPM](https://nodei.co/npm/ytmusic.png?compact=true)](https://nodei.co/npm/ytmusic/) 19 | 20 | #### Building it yourself 21 | Clone github repo 22 | ```sh 23 | $ git clone https://github.com/vladdenisov/ytmusic-api 24 | ``` 25 | Install dependencies 26 | ```sh 27 | $ yarn 28 | ``` 29 | Build it 30 | ```sh 31 | $ yarn build 32 | ``` 33 | You will see `dist` folder, where all built files are stored. 34 | 35 | ### Usage 36 | 37 | #### Anonymous usage 38 | 39 | You should be able to just use it, if you need only information-getting features: 40 | 41 | ```js 42 | const api = new YTMUSIC() 43 | const data = await api.getPlaylist('RDCLAK5uy_k1Wu8QbZASiGVqr1wmie9NIYo38aBqscQ') 44 | console.log(data.title) 45 | ``` 46 | 47 | #### Getting the cookie 48 | Get the auth cookie from requests to YTMusic in your browser: 49 | 50 | - Open [YouTube Music](https://music.youtube.com/) in browser 51 | - Go to the developer tools (Ctrl-Shift-I) and find an authenticated POST request. You can filter for /browse to easily find a suitable request. 52 | - Copy `cookie` from `Request Headers` 53 | 54 | #### Using in code 55 | Import lib to your code: 56 | ```js 57 | const { YTMUSIC } = require('ytmusic') 58 | // or if you build it yourself 59 | const { YTMUSIC } = require('path/to/ytmusic/dist/index.js') 60 | ``` 61 | Create new Instance of api with your cookie: 62 | ```js 63 | const api = new YTMUSIC("cookie") 64 | 65 | // or if you want it to use not default account, specify userID (refer to docs to get it): 66 | const api = new YTMUSIC("cookie", "userID") 67 | ``` 68 | Use it: 69 | ```js 70 | const data = await api.getPlaylist('RDCLAK5uy_k1Wu8QbZASiGVqr1wmie9NIYo38aBqscQ') 71 | console.log(data.title) 72 | // { text: '80s Pop-Rock Anthems' } 73 | ``` 74 | ## Built With 75 | 76 | * [TypeScript](https://www.typescriptlang.org/) - JavaScript that scales. 77 | * [node-fetch](https://www.npmjs.com/package/node-fetch) - A light-weight module that brings window.fetch to Node.js 78 | 79 | ## Contributing 80 | 81 | ~~Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.~~ 82 | 83 | Just contribute <3 84 | 85 | ## Versioning 86 | 87 | We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/your/project/tags). 88 | 89 | ## Authors 90 | 91 | * **Vlad Denisov** - *Initial work* - [vladdenisov](https://github.com/vladdenisov) 92 | * **Daniel Leong** - [dhleong](https://github.com/dhleong) 93 | 94 | See also the list of [contributors](https://github.com/your/project/contributors) who participated in this project. 95 | 96 | ## License 97 | 98 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 99 | -------------------------------------------------------------------------------- /lib/endpoints/Browse.ts: -------------------------------------------------------------------------------- 1 | import * as utils from '../utils' 2 | import Album from '../../models/Album' 3 | import Playlist from '../../models/Playlist' 4 | import { Song } from '../../models/Song' 5 | export const getPlaylists = async ( 6 | cookie: string, 7 | args: { 8 | userID?: string 9 | authUser?: number 10 | } 11 | ): Promise => { 12 | const result = await utils.sendRequest(cookie, { 13 | endpoint: 'browse', 14 | userID: args.userID, 15 | authUser: args.authUser 16 | }) 17 | if (result.error) throw new Error(result.error.status) 18 | 19 | throw new Error(JSON.stringify(result, null, 2)) 20 | 21 | const data = 22 | result.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer 23 | .content.sectionListRenderer.contents 24 | 25 | let songs: { browseId?: string; results: Song[] } = { results: [] } 26 | let albums: { browseId?: string; results: Album[] } = { results: [] } 27 | if (data[0].musicShelfRenderer.contents) { 28 | if (data[0].musicShelfRenderer.bottomEndpoint) 29 | songs.browseId = 30 | data[0].musicShelfRenderer.bottomEndpoint.browseEndpoint.browseId 31 | data[0].musicShelfRenderer.contents.map((e: any, i: number) => { 32 | e = e.musicResponsiveListItemRenderer 33 | songs.results.push({ 34 | title: 35 | e.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text 36 | .runs[0], 37 | thumbnail: e.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails, 38 | author: 39 | e.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs, 40 | album: 41 | e.flexColumns[2].musicResponsiveListItemFlexColumnRenderer.text 42 | .runs[0], 43 | id: e.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text 44 | .runs[0].navigationEndpoint.watchEndpoint.videoId, 45 | url: `https://music.youtube.com/watch?v=${e.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].navigationEndpoint.watchEndpoint.videoId}` 46 | }) 47 | }) 48 | } 49 | if (data[1].musicCarouselShelfRenderer) { 50 | if ( 51 | data[1].musicCarouselShelfRenderer.header 52 | .musicCarouselShelfBasicHeaderRenderer.title.runs[0].text === 'Albums' 53 | ) { 54 | if ( 55 | data[1].musicCarouselShelfRenderer.header 56 | .musicCarouselShelfBasicHeaderRenderer.moreContentButton 57 | ) 58 | albums.browseId = 59 | data[1].musicCarouselShelfRenderer.header.musicCarouselShelfBasicHeaderRenderer.moreContentButton 60 | data[1].musicCarouselShelfRenderer.contents.map((e: any, i: number) => { 61 | e = e.musicTwoRowItemRenderer 62 | albums.results.push({ 63 | title: e.title.runs[0], 64 | thumbnail: 65 | e.thumbnailRenderer.musicThumbnailRenderer.thumbnail.thumbnails, 66 | year: e.subtitle.runs[2].text, 67 | author: e.subtitle.runs[0].text, 68 | browseId: e.navigationEndpoint.browseEndpoint.browseId, 69 | url: `https://music.youtube.com/playlist?list=${e.thumbnailOverlay.musicItemThumbnailOverlayRenderer.content.musicPlayButtonRenderer.playNavigationEndpoint.watchPlaylistEndpoint.playlistId}` 70 | }) 71 | }) 72 | } 73 | } 74 | let artist = { 75 | name: result.header.musicImmersiveHeaderRenderer.title.runs[0].text, 76 | description: result.header.musicImmersiveHeaderRenderer.description 77 | ? result.header.musicImmersiveHeaderRenderer.description.runs[0].text 78 | : '', 79 | songs, 80 | albums 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/endpoints/Browsing.ts: -------------------------------------------------------------------------------- 1 | import * as utils from '../utils' 2 | import Album from '../../models/Album' 3 | import Artist from '../../models/Artist' 4 | import { Song } from '../../models/Song' 5 | export const getArtist = async ( 6 | cookie: string, 7 | args: { 8 | userID?: string 9 | authUser?: number 10 | }, 11 | channelId: string 12 | ): Promise => { 13 | const result = await utils.sendRequest(cookie, { 14 | id: channelId, 15 | type: 'ARTIST', 16 | endpoint: 'browse', 17 | userID: args.userID, 18 | authUser: args.authUser 19 | }) 20 | if (result.error) throw new Error(result.error.status) 21 | 22 | const data = 23 | result.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer 24 | .content.sectionListRenderer.contents 25 | let songs: { browseId?: string; results: Song[] } = { results: [] } 26 | let albums: { browseId?: string; results: Album[] } = { results: [] } 27 | if (data[0].musicShelfRenderer.contents) { 28 | if (data[0].musicShelfRenderer.bottomEndpoint) 29 | songs.browseId = 30 | data[0].musicShelfRenderer.bottomEndpoint.browseEndpoint.browseId 31 | data[0].musicShelfRenderer.contents.map((e: any, i: number) => { 32 | e = e.musicResponsiveListItemRenderer 33 | songs.results.push({ 34 | title: 35 | e.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text 36 | .runs[0], 37 | thumbnail: e.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails, 38 | author: 39 | e.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs, 40 | album: 41 | e.flexColumns[2].musicResponsiveListItemFlexColumnRenderer.text 42 | .runs[0], 43 | id: e.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text 44 | .runs[0].navigationEndpoint.watchEndpoint.videoId, 45 | url: `https://music.youtube.com/watch?v=${e.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].navigationEndpoint.watchEndpoint.videoId}` 46 | }) 47 | }) 48 | } 49 | if (data[1].musicCarouselShelfRenderer) { 50 | if ( 51 | data[1].musicCarouselShelfRenderer.header 52 | .musicCarouselShelfBasicHeaderRenderer.title.runs[0].text === 'Albums' 53 | ) { 54 | if ( 55 | data[1].musicCarouselShelfRenderer.header 56 | .musicCarouselShelfBasicHeaderRenderer.moreContentButton 57 | ) 58 | albums.browseId = 59 | data[1].musicCarouselShelfRenderer.header.musicCarouselShelfBasicHeaderRenderer.moreContentButton 60 | data[1].musicCarouselShelfRenderer.contents.map((e: any, i: number) => { 61 | e = e.musicTwoRowItemRenderer 62 | albums.results.push({ 63 | title: e.title.runs[0], 64 | thumbnail: 65 | e.thumbnailRenderer.musicThumbnailRenderer.thumbnail.thumbnails, 66 | year: e.subtitle.runs[2].text, 67 | author: e.subtitle.runs[0].text, 68 | browseId: e.navigationEndpoint.browseEndpoint.browseId, 69 | url: `https://music.youtube.com/playlist?list=${e.thumbnailOverlay.musicItemThumbnailOverlayRenderer.content.musicPlayButtonRenderer.playNavigationEndpoint.watchPlaylistEndpoint.playlistId}` 70 | }) 71 | }) 72 | } 73 | } 74 | let artist = { 75 | name: result.header.musicImmersiveHeaderRenderer.title.runs[0].text, 76 | description: result.header.musicImmersiveHeaderRenderer.description 77 | ? result.header.musicImmersiveHeaderRenderer.description.runs[0].text 78 | : '', 79 | songs, 80 | albums 81 | } 82 | return artist 83 | } 84 | -------------------------------------------------------------------------------- /lib/endpoints/HomePage.ts: -------------------------------------------------------------------------------- 1 | import HomePage from '../../models/HomePage' 2 | import Carousel from '../../models/Carousel' 3 | import CarouselItem from '../../models/CarouselItem' 4 | import Subtitle from '../../models/Subtitle' 5 | import * as utils from '../utils' 6 | import { createContinuation } from '../continuations' 7 | 8 | const parseTwoRowItemRenderer = utils.parser((e: any) => { 9 | const item: CarouselItem = { 10 | thumbnail: [], 11 | title: e.title.runs[0], 12 | subtitle: [], 13 | navigationEndpoint: e.navigationEndpoint 14 | } 15 | e.thumbnailRenderer.musicThumbnailRenderer.thumbnail.thumbnails.forEach( 16 | (el: any) => item.thumbnail.push(el) 17 | ) 18 | e.subtitle.runs.forEach((el: any) => { 19 | let sub: Subtitle = { 20 | text: el.text 21 | } 22 | if (el.navigationEndpoint) { 23 | sub.navigationEndpoint = el.navigationEndpoint 24 | } 25 | item.subtitle.push(sub) 26 | }) 27 | return item 28 | }) 29 | 30 | const parseFlexColumnRenderer = utils.parser((item: any) => { 31 | return { 32 | thumbnail: [], 33 | title: item.text.runs[0].text, 34 | subtitle: [], 35 | navigationEndpoint: item.text.runs[0].navigationEndpoint 36 | } as CarouselItem 37 | }) 38 | 39 | function parseResponsiveListItemRenderer(e: any): CarouselItem[] | undefined { 40 | if (e.flexColumns) { 41 | return utils.filterMap(e.flexColumns, (item: any) => { 42 | if (item.musicResponsiveListItemFlexColumnRenderer) { 43 | return parseFlexColumnRenderer( 44 | item.musicResponsiveListItemFlexColumnRenderer 45 | ) 46 | } 47 | 48 | throw new Error(`Unexpected flexColumn content: ${JSON.stringify(item)}`) 49 | }) 50 | } 51 | 52 | throw new Error(`Unexpected responsive contents: ${JSON.stringify(e)}`) 53 | } 54 | 55 | function parseCarouselItems(e: any) { 56 | if (e.musicTwoRowItemRenderer) { 57 | return [parseTwoRowItemRenderer(e.musicTwoRowItemRenderer)] 58 | } 59 | 60 | if (e.musicResponsiveListItemRenderer) { 61 | return parseResponsiveListItemRenderer(e.musicResponsiveListItemRenderer) 62 | } 63 | 64 | throw new Error(`Unexpected carousel contents: ${JSON.stringify(e)}`) 65 | } 66 | 67 | const parseCarouselContents = (contents: any): Carousel[] => 68 | utils.filterMap(contents, (carousel: any) => { 69 | if (carousel.musicTastebuilderShelfRenderer) return 70 | const ctx = carousel.musicCarouselShelfRenderer 71 | ? carousel.musicCarouselShelfRenderer 72 | : carousel.musicImmersiveCarouselShelfRenderer 73 | const content: CarouselItem[] = utils.filterFlatMap( 74 | ctx.contents, 75 | parseCarouselItems 76 | ) 77 | return { 78 | title: 79 | ctx.header.musicCarouselShelfBasicHeaderRenderer.title.runs[0].text, 80 | content, 81 | strapline: ctx.header.musicCarouselShelfBasicHeaderRenderer.strapline 82 | ? ctx.header.musicCarouselShelfBasicHeaderRenderer.strapline.runs 83 | : undefined 84 | } 85 | }) 86 | 87 | /** 88 | * Returns First Part of HomePage 89 | * 90 | * @usage 91 | * 92 | * ```js 93 | * const api = new YTMUSIC(cookie) 94 | * const data = await api.getHomePage() 95 | * ``` 96 | * 97 | * You can call `data.continue()` to get next part 98 | * @returns {@link HomePage} 99 | * 100 | */ 101 | export const getHomePage = async ( 102 | cookie: string, 103 | args: { 104 | userID?: string 105 | authUser?: number 106 | } 107 | ): Promise => { 108 | const response = await utils.sendRequest(cookie, { 109 | id: 'FEmusic_home', 110 | endpoint: 'browse', 111 | userID: args.userID, 112 | authUser: args.authUser 113 | }) 114 | 115 | const data = 116 | response.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer 117 | const sectionListRenderer = data.content.sectionListRenderer 118 | 119 | const content: Carousel[] = parseCarouselContents( 120 | sectionListRenderer.contents 121 | ) 122 | 123 | const home: HomePage = { 124 | title: data.title, 125 | browseId: data.endpoint.browseEndpoint.browseId, 126 | content 127 | } 128 | home.continue = createContinuation( 129 | cookie, 130 | args, 131 | parseCarouselContents, 132 | home, 133 | sectionListRenderer 134 | ) 135 | 136 | return home 137 | } 138 | 139 | /** 140 | * Returns Full HomePage 141 | * 142 | * @usage 143 | * ```js 144 | * const api = new YTMUSIC(cookie) 145 | * const data = await api.getFullHomePage() 146 | * ``` 147 | * @returns {@link HomePage} 148 | * 149 | */ 150 | export const getFullHomePage = async ( 151 | cookie: string, 152 | args: { 153 | userID?: string 154 | authUser?: number 155 | } 156 | ) => { 157 | const home = await getHomePage(cookie, args) 158 | while (true) { 159 | const t = await home.continue?.() 160 | if (!t || !t.content) break 161 | 162 | home.content?.push(...t.content) 163 | if (!t.continue) break 164 | 165 | home.continue = t.continue 166 | } 167 | return home 168 | } 169 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import * as HomePage from './lib/endpoints/HomePage' 2 | import * as Playlist from './lib/endpoints/Playlist' 3 | import * as Browsing from './lib/endpoints/Browsing' 4 | import * as Browse from './lib/endpoints/Browse' 5 | import { search } from './lib/endpoints/Search' 6 | import { Player } from './lib/endpoints/Player' 7 | import { getURLVideoID } from './lib/utils' 8 | export class YTMUSIC { 9 | userID?: string 10 | authUser: number | undefined 11 | constructor( 12 | private cookie: string, 13 | private args?: { userID: string; authUser?: number } 14 | ) { 15 | this.cookie = cookie 16 | if (args?.userID) { 17 | this.userID = args.userID 18 | } 19 | if (args?.authUser) { 20 | this.authUser = args.authUser 21 | } 22 | } 23 | /** 24 | * Returns First Part of HomePage 25 | * 26 | * @usage 27 | * 28 | * ```js 29 | * const api = new YTMUSIC(cookie) 30 | * const data = await api.getHomePage() 31 | * ``` 32 | * 33 | * You can call `data.continue()` to get next part 34 | * @returns {@link HomePage} 35 | * 36 | */ 37 | getHomePage = async () => { 38 | return await HomePage.getHomePage(this.cookie, this) 39 | } 40 | 41 | getPlaylists = async () => { 42 | return await Browse.getPlaylists(this.cookie, this) 43 | } 44 | 45 | getPlaylist = async (id: string, limit?: number) => 46 | await Playlist.getPlaylist(this.cookie, this, id, limit) 47 | addToPlaylist = async (ids: string[], playlistId: string) => 48 | await Playlist.addToPlaylist(this.cookie, this, ids, playlistId) 49 | /** 50 | * Returns Full HomePage 51 | * 52 | * @usage 53 | * ```js 54 | * const api = new YTMUSIC(cookie) 55 | * const data = await api.getFullHomePage() 56 | * ``` 57 | * @returns {@link HomePage} 58 | * 59 | */ 60 | getFullHomePage = async () => { 61 | return await HomePage.getFullHomePage(this.cookie, this) 62 | } 63 | /** 64 | * Search 65 | * 66 | * @usage 67 | * 68 | * ```js 69 | * const api = new YTMUSIC(cookie) 70 | * const data = await api.createPlaylist('Summer Songs', 'PUBLIC', 'Some songs for summer') 71 | * const songs = await api.search('Hot stuff') 72 | * await api.addToPlaylist([songs[0].id], playlist.id) 73 | * ``` 74 | * @param query - Search query 75 | * @param filter - What type to search 76 | * @param max - maximum results (recommended 1-3, because next results might be unparsable) default: infinity 77 | * 78 | */ 79 | search = async ( 80 | query: string, 81 | options?: { 82 | filter?: 'songs' | 'albums' | 'playlists' | 'videos' | 'artists' 83 | max?: number 84 | } 85 | ) => await search(this.cookie, this, query, options) 86 | /** 87 | * Create playlist 88 | * 89 | * @usage 90 | * 91 | * ```js 92 | * const api = new YTMUSIC(cookie) 93 | * const data = await api.createPlaylist('Summer Songs', 'PUBLIC', 'Some songs for summer') 94 | * await api.addToPlaylist(['-mLpe7KUg9U', '5hFevwJ4JXI'], playlist.id) 95 | * ``` 96 | * @param title - Title 97 | * @param privacyStatus - Privacy Status of playlist 98 | * @param description - Description of playlist 99 | ``` 100 | */ 101 | createPlaylist = async ( 102 | title: string, 103 | privacyStatus: 'PRIVATE' | 'PUBLIC' | 'UNLISTED', 104 | description?: string 105 | ) => 106 | await Playlist.createPlaylist( 107 | this.cookie, 108 | this, 109 | title, 110 | privacyStatus, 111 | description 112 | ) 113 | /** 114 | * Delete playlist 115 | * 116 | * @usage 117 | * 118 | * ```js 119 | * const api = new YTMUSIC(cookie) 120 | * const data = await api.createPlaylist('Summer Songs', 'PUBLIC', 'Some songs for summer') 121 | * await api.deletePlaylist(playlist.id) 122 | * ``` 123 | * @param id - id of the playlist 124 | */ 125 | deletePlaylist = async (id: string) => 126 | await Playlist.deletePlaylist(this.cookie, this, id) 127 | /** 128 | * Remove song(s) from playlist 129 | * 130 | * @usage 131 | * 132 | * ```js 133 | * const api = new YTMUSIC(cookie) 134 | * const data = await api.removeFromPlaylist(['-mLpe7KUg9U', '5hFevwJ4JXI'], 'RDAMVM5hFevwJ4JXI') 135 | * ``` 136 | * @param ids - Array of song ids to remove 137 | * @param playlistId - ID of playlist 138 | ``` 139 | */ 140 | removeFromPlaylist = async ( 141 | ids: string[], 142 | playlistId: string, 143 | setVideoId?: string 144 | ) => 145 | await Playlist.removeFromPlaylist( 146 | this.cookie, 147 | this, 148 | ids, 149 | playlistId, 150 | setVideoId 151 | ) 152 | getArtist = async (channelId: string) => 153 | Browsing.getArtist(this.cookie, this, channelId) 154 | 155 | /** 156 | * Get song info 157 | * 158 | * @usage 159 | * 160 | * ```js 161 | * const api = new YTMUSIC(cookie) 162 | * const song = await api.getSongInfo('https://music.youtube.com/watch?v=DPXHMBKY39M&feature=share') 163 | * console.log(song.title) 164 | * ``` 165 | * @param url - Search query 166 | * 167 | */ 168 | getSongInfo = async (url: string) => 169 | await Player(this.cookie, this, getURLVideoID(url)) 170 | } 171 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import cookie from 'cookie' 2 | /** 3 | * @ignore 4 | */ 5 | const sha1 = require('sha1') 6 | import fetch from 'node-fetch' 7 | import contextJSON from './context.json' 8 | import headersJSON from './headers.json' 9 | /** 10 | * @ignore 11 | */ 12 | const getSAPISID = (raw: string): string => { 13 | const parsed = cookie.parse(raw) 14 | return parsed['SAPISID'] 15 | } 16 | /** 17 | * @ignore 18 | */ 19 | const getAuthToken = (raw_cookie: string): string => { 20 | const date = new Date().getTime() 21 | return `SAPISIDHASH ${date}_${sha1( 22 | date + ' ' + getSAPISID(raw_cookie) + ' ' + 'https://music.youtube.com' 23 | )}` 24 | } 25 | /** 26 | * @ignore 27 | */ 28 | export const generateBody = (args: { 29 | id?: string 30 | type?: string 31 | userID?: string 32 | }): string | object => { 33 | const context: any = contextJSON.context 34 | if (args.userID) context.user.onBehalfOfUser = args.userID 35 | if (args.type) 36 | return JSON.stringify({ 37 | context, 38 | browseEndpointContextSupportedConfigs: { 39 | browseEndpointContextMusicConfig: { 40 | pageType: 'MUSIC_PAGE_TYPE_' + args.type 41 | } 42 | }, 43 | browseId: args.id 44 | }) 45 | else if (args.id) 46 | return JSON.stringify({ 47 | context, 48 | browseId: args.id 49 | }) 50 | else return { context } 51 | } 52 | /** 53 | * @ignore 54 | */ 55 | export const generateHeaders = (cookie: string, authUser?: number): object => { 56 | if (!cookie) { 57 | return headersJSON 58 | } 59 | const token = getAuthToken(cookie) 60 | if (!authUser) authUser = 0 61 | return { 62 | ...headersJSON, 63 | Authorization: token, 64 | 'X-Goog-AuthUser': `${authUser}`, 65 | Cookie: cookie, 66 | 'x-youtube-client-version': '0.1' 67 | } 68 | } 69 | /** 70 | * @ignore 71 | */ 72 | export const sendRequest = async ( 73 | c: string, 74 | args: { 75 | id?: string 76 | type?: string 77 | body?: object 78 | endpoint: string 79 | userID?: string 80 | cToken?: string 81 | itct?: string 82 | authUser?: number 83 | } 84 | ) => { 85 | const headers: object = generateHeaders(c, args.authUser) 86 | const options: { 87 | method: string 88 | headers: any 89 | body: any 90 | } = { 91 | method: 'POST', 92 | headers: headers, 93 | body: args.body 94 | ? JSON.stringify(args.body) 95 | : generateBody({ id: args.id, type: args.type, userID: args.userID }) 96 | } 97 | const addParams: string = `${ 98 | args.cToken 99 | ? 'ctoken=' + 100 | args.cToken + 101 | '&continuation=' + 102 | args.cToken + 103 | '&itct=' + 104 | args.itct + 105 | '&' 106 | : '' 107 | }` 108 | return fetch( 109 | `https://music.youtube.com/youtubei/v1/${args.endpoint}?${addParams}alt=json&key=AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30`, 110 | options 111 | ) 112 | .then((data) => data.json()) 113 | .then((data) => { 114 | return data 115 | }) 116 | } 117 | 118 | export function filterMap( 119 | collection: T[], 120 | f: (item: T) => R | undefined | null 121 | ): R[] { 122 | const result: R[] = [] 123 | for (const item of collection) { 124 | const mapped = f(item) 125 | if (mapped != null) { 126 | result.push(mapped) 127 | } 128 | } 129 | return result 130 | } 131 | 132 | export function filterFlatMap( 133 | collection: T[], 134 | f: (item: T) => R[] | undefined | null 135 | ): R[] { 136 | const result: R[] = [] 137 | for (const item of collection) { 138 | const mapped = f(item) 139 | if (mapped != null) { 140 | result.push(...mapped) 141 | } 142 | } 143 | return result 144 | } 145 | 146 | /** 147 | * Wraps a function that accepts input T and parses it into output R. 148 | * In the normal case, this is a no-op; if the function throws, however, 149 | * we will augment the thrown Error with context bout what was being parsed. 150 | */ 151 | export function parser( 152 | f: (...input: T) => R 153 | ): (...input: T) => R { 154 | return function parserWrapper(...input: T) { 155 | try { 156 | return f(...input) 157 | } catch (e) { 158 | throw new Error( 159 | `Unexpected error: ${e.message}\nParsing: ${JSON.stringify( 160 | input[0], 161 | null, 162 | 2 163 | )}\n${e.stack}` 164 | ) 165 | } 166 | } 167 | } 168 | 169 | /** 170 | * Get video ID. 171 | * 172 | * There are a few type of video URL formats. 173 | * - https://www.youtube.com/watch?v=VIDEO_ID 174 | * - https://m.youtube.com/watch?v=VIDEO_ID 175 | * - https://youtu.be/VIDEO_ID 176 | * - https://www.youtube.com/v/VIDEO_ID 177 | * - https://www.youtube.com/embed/VIDEO_ID 178 | * - https://music.youtube.com/watch?v=VIDEO_ID 179 | * - https://gaming.youtube.com/watch?v=VIDEO_ID 180 | * 181 | * Credit: https://github.com/fent/node-ytdl-core/blob/master/lib/url-utils.js 182 | * @param {string} link 183 | * @return {string} 184 | * @throws {Error} If unable to find a id 185 | * @throws {TypeError} If videoid doesn't match specs 186 | */ 187 | const validQueryDomains = new Set([ 188 | 'youtube.com', 189 | 'www.youtube.com', 190 | 'm.youtube.com', 191 | 'music.youtube.com', 192 | 'gaming.youtube.com' 193 | ]) 194 | const validPathDomains = 195 | /^https?:\/\/(youtu\.be\/|(www\.)?youtube\.com\/(embed|v|shorts)\/)/ 196 | export const getURLVideoID = (link: string) => { 197 | const parsed = new URL(link.trim()) 198 | let id = parsed.searchParams.get('v') 199 | if (validPathDomains.test(link.trim()) && !id) { 200 | const paths = parsed.pathname.split('/') 201 | id = parsed.host === 'youtu.be' ? paths[1] : paths[2] 202 | } else if (parsed.hostname && !validQueryDomains.has(parsed.hostname)) { 203 | throw Error('Not a YouTube domain') 204 | } 205 | if (!id) { 206 | throw Error(`No video id found: "${link}"`) 207 | } 208 | id = id.substring(0, 11) 209 | return id 210 | } 211 | -------------------------------------------------------------------------------- /lib/endpoints/Search.ts: -------------------------------------------------------------------------------- 1 | import * as utils from '../utils' 2 | import Thumbnail from '../../models/Thumbnail' 3 | /** 4 | * Search 5 | * 6 | * @usage 7 | * 8 | * ```js 9 | * const api = new YTMUSIC(cookie) 10 | * const data = await api.createPlaylist('Summer Songs', 'PUBLIC', 'Some songs for summer') 11 | * const songs = await api.search('Hot stuff') 12 | * await api.addToPlaylist([songs[0].id], playlist.id) 13 | * ``` 14 | * @param query - Search query 15 | * @param filter - What type to search 16 | * @param max - maximum results (recommended 1-3, because next results might be unparsable) default: infinity 17 | * 18 | */ 19 | export const search = async ( 20 | cookie: string, 21 | args: any, 22 | query: string, 23 | options?: { 24 | filter?: string 25 | max?: number 26 | } 27 | ): Promise< 28 | | Array<{ 29 | type: 'song' | 'album' | 'playlist' | 'video' | 'artist' 30 | title: Text 31 | url: string 32 | tracksCount?: number 33 | thumbnails: Thumbnail[] 34 | author?: Text 35 | id?: string 36 | album?: Text 37 | [propName: string]: any 38 | }> 39 | | Error 40 | > => { 41 | const body: any = utils.generateBody({ userID: args.userID }) 42 | if (options?.filter) { 43 | let param: string 44 | switch (options.filter) { 45 | case 'songs': 46 | param = 'RAAGAAgACgA' 47 | case 'videos': 48 | param = 'BABGAAgACgA' 49 | case 'albums': 50 | param = 'BAAGAEgACgA' 51 | case 'artists': 52 | param = 'BAAGAAgASgA' 53 | case 'playlists': 54 | param = 'BAAGAAgACgB' 55 | default: 56 | param = 'RAAGAAgACgA' 57 | } 58 | body.params = `Eg-KAQwIA${param}'MABqChAEEAMQCRAFEAo%3D'` 59 | } 60 | body.query = query 61 | const response = await utils.sendRequest(cookie, { 62 | endpoint: 'search', 63 | userID: args.userID, 64 | authUser: args.authUser, 65 | body 66 | }) 67 | if (response.error) throw new Error(response.error.status) 68 | const contents = 69 | response.contents.tabbedSearchResultsRenderer.tabs[0].tabRenderer.content 70 | .sectionListRenderer.contents 71 | let results: any = [] 72 | if (contents[0].messageRenderer) return [] 73 | contents.map((ctx: any) => { 74 | if (ctx.itemSectionRenderer) return 75 | ctx = ctx.musicShelfRenderer 76 | ctx.contents.map((e: any, i: number) => { 77 | if (options?.max && i > options?.max - 1) return 78 | try { 79 | e = e.musicResponsiveListItemRenderer 80 | let type: string 81 | if (options?.filter) 82 | type = options.filter.slice(0, options.filter.length - 1) 83 | else 84 | type = ctx.title.runs[0].text 85 | .toLowerCase() 86 | .slice(0, ctx.title.runs[0].text.length - 1) 87 | if (type === 'top resul') { 88 | type = 89 | e.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text.toLowerCase() 90 | } 91 | if (!['user playlist', 'song', 'video', 'artist'].includes(type)) 92 | type = 'album' 93 | // if (!options.filter) e.flexColumns.splice(1, 1) 94 | let result: any = { 95 | type, 96 | title: 97 | e.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text 98 | .runs[0].text, 99 | thumbnails: e.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails 100 | } 101 | type = type.toLowerCase() 102 | if (['user playlist', 'song', 'video', 'album'].includes(type)) { 103 | result.author = 104 | e.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[2].text 105 | if (type === 'song') { 106 | if (e.playlistItemData) { 107 | result.url = `https://music.youtube.com/watch?v=${e.playlistItemData.videoId}&list=${e.overlay.musicItemThumbnailOverlayRenderer.content.musicPlayButtonRenderer.playNavigationEndpoint.watchEndpoint.playlistId}` 108 | result.id = e.playlistItemData.videoId 109 | } 110 | result.album = 111 | e.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[4].text 112 | result.album_browse_id = 113 | e.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[4].navigationEndpoint.browseEndpoint.browseId 114 | result.duration = 115 | e.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[6].text 116 | 117 | result.artist_browse_id = 118 | e.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[2].navigationEndpoint.browseEndpoint.browseId 119 | } 120 | if (type === 'video') { 121 | if (e.playlistItemData) { 122 | result.url = `https://music.youtube.com/watch?v=${e.playlistItemData.videoId}&list=${e.overlay.musicItemThumbnailOverlayRenderer.content.musicPlayButtonRenderer.playNavigationEndpoint.watchEndpoint.playlistId}` 123 | result.id = e.playlistItemData.videoId 124 | } 125 | result.views = 126 | e.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[4].text 127 | result.duration = 128 | e.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[6].text 129 | } 130 | if (type === 'user playlist') { 131 | result.type = 'playlist' 132 | result.url = `https://music.youtube.com/playlist?list=${e.navigationEndpoint.browseEndpoint.browseId}` 133 | result.tracksCount = parseInt( 134 | e.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text 135 | .runs[4].text 136 | ) 137 | result.id = e.navigationEndpoint.browseEndpoint.browseId.slice(2) 138 | } 139 | if (type === 'album') { 140 | result.url = `https://music.youtube.com/browse/${e.navigationEndpoint.browseEndpoint.browseId}` 141 | result.year = 142 | e.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[4].text 143 | } 144 | } else { 145 | result.subs = 146 | e.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text 147 | result.url = `https://music.youtube.com/browse/${e.navigationEndpoint.browseEndpoint.browseId}` 148 | } 149 | results.push(result) 150 | } catch (e) { 151 | console.log(e) 152 | return 153 | } 154 | }) 155 | }) 156 | return results 157 | } 158 | -------------------------------------------------------------------------------- /docs/modules/_lib_context_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "lib/context" | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Module "lib/context"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 155 |
156 |
157 |
158 |
159 |

Legend

160 |
161 |
    162 |
  • Variable
  • 163 |
  • Function
  • 164 |
  • Function with type parameter
  • 165 |
166 |
    167 |
  • Interface
  • 168 |
169 |
    170 |
  • Class
  • 171 |
172 |
173 |
174 |
175 |
176 |

Generated using TypeDoc

177 |
178 |
179 | 180 | 181 | -------------------------------------------------------------------------------- /docs/modules/_lib_headers_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "lib/headers" | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Module "lib/headers"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 155 |
156 |
157 |
158 |
159 |

Legend

160 |
161 |
    162 |
  • Variable
  • 163 |
  • Function
  • 164 |
  • Function with type parameter
  • 165 |
166 |
    167 |
  • Interface
  • 168 |
169 |
    170 |
  • Class
  • 171 |
172 |
173 |
174 |
175 |
176 |

Generated using TypeDoc

177 |
178 |
179 | 180 | 181 | -------------------------------------------------------------------------------- /docs/modules/_index_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "index" | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Module "index"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Classes

75 | 78 |
79 |
80 |
81 |
82 |
83 | 171 |
172 |
173 |
174 |
175 |

Legend

176 |
177 |
    178 |
  • Variable
  • 179 |
  • Function
  • 180 |
  • Function with type parameter
  • 181 |
182 |
    183 |
  • Interface
  • 184 |
185 |
    186 |
  • Class
  • 187 |
188 |
189 |
190 |
191 |
192 |

Generated using TypeDoc

193 |
194 |
195 | 196 | 197 | -------------------------------------------------------------------------------- /docs/modules/_models_album_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "models/Album" | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Module "models/Album"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Interfaces

75 | 78 |
79 |
80 |
81 |
82 |
83 | 171 |
172 |
173 |
174 |
175 |

Legend

176 |
177 |
    178 |
  • Variable
  • 179 |
  • Function
  • 180 |
  • Function with type parameter
  • 181 |
182 |
    183 |
  • Interface
  • 184 |
185 |
    186 |
  • Class
  • 187 |
188 |
189 |
190 |
191 |
192 |

Generated using TypeDoc

193 |
194 |
195 | 196 | 197 | -------------------------------------------------------------------------------- /docs/modules/_models_artist_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "models/Artist" | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Module "models/Artist"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Interfaces

75 | 78 |
79 |
80 |
81 |
82 |
83 | 171 |
172 |
173 |
174 |
175 |

Legend

176 |
177 |
    178 |
  • Variable
  • 179 |
  • Function
  • 180 |
  • Function with type parameter
  • 181 |
182 |
    183 |
  • Interface
  • 184 |
185 |
    186 |
  • Class
  • 187 |
188 |
189 |
190 |
191 |
192 |

Generated using TypeDoc

193 |
194 |
195 | 196 | 197 | -------------------------------------------------------------------------------- /docs/modules/_models_carousel_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "models/Carousel" | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Module "models/Carousel"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Interfaces

75 | 78 |
79 |
80 |
81 |
82 |
83 | 171 |
172 |
173 |
174 |
175 |

Legend

176 |
177 |
    178 |
  • Variable
  • 179 |
  • Function
  • 180 |
  • Function with type parameter
  • 181 |
182 |
    183 |
  • Interface
  • 184 |
185 |
    186 |
  • Class
  • 187 |
188 |
189 |
190 |
191 |
192 |

Generated using TypeDoc

193 |
194 |
195 | 196 | 197 | -------------------------------------------------------------------------------- /docs/modules/_models_playlist_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "models/Playlist" | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Module "models/Playlist"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Interfaces

75 | 78 |
79 |
80 |
81 |
82 |
83 | 171 |
172 |
173 |
174 |
175 |

Legend

176 |
177 |
    178 |
  • Variable
  • 179 |
  • Function
  • 180 |
  • Function with type parameter
  • 181 |
182 |
    183 |
  • Interface
  • 184 |
185 |
    186 |
  • Class
  • 187 |
188 |
189 |
190 |
191 |
192 |

Generated using TypeDoc

193 |
194 |
195 | 196 | 197 | -------------------------------------------------------------------------------- /docs/modules/_models_subtitle_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "models/Subtitle" | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Module "models/Subtitle"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Interfaces

75 | 78 |
79 |
80 |
81 |
82 |
83 | 171 |
172 |
173 |
174 |
175 |

Legend

176 |
177 |
    178 |
  • Variable
  • 179 |
  • Function
  • 180 |
  • Function with type parameter
  • 181 |
182 |
    183 |
  • Interface
  • 184 |
185 |
    186 |
  • Class
  • 187 |
188 |
189 |
190 |
191 |
192 |

Generated using TypeDoc

193 |
194 |
195 | 196 | 197 | -------------------------------------------------------------------------------- /docs/modules/_models_search_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "models/Search" | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Module "models/Search"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Interfaces

75 | 78 |
79 |
80 |
81 |
82 |
83 | 171 |
172 |
173 |
174 |
175 |

Legend

176 |
177 |
    178 |
  • Variable
  • 179 |
  • Function
  • 180 |
  • Function with type parameter
  • 181 |
182 |
    183 |
  • Interface
  • 184 |
185 |
    186 |
  • Class
  • 187 |
188 |
189 |
190 |
191 |
192 |

Generated using TypeDoc

193 |
194 |
195 | 196 | 197 | -------------------------------------------------------------------------------- /docs/modules/_models_homepage_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "models/HomePage" | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Module "models/HomePage"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Interfaces

75 | 78 |
79 |
80 |
81 |
82 |
83 | 171 |
172 |
173 |
174 |
175 |

Legend

176 |
177 |
    178 |
  • Variable
  • 179 |
  • Function
  • 180 |
  • Function with type parameter
  • 181 |
182 |
    183 |
  • Interface
  • 184 |
185 |
    186 |
  • Class
  • 187 |
188 |
189 |
190 |
191 |
192 |

Generated using TypeDoc

193 |
194 |
195 | 196 | 197 | -------------------------------------------------------------------------------- /docs/modules/_models_thumbnail_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "models/Thumbnail" | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Module "models/Thumbnail"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Interfaces

75 | 78 |
79 |
80 |
81 |
82 |
83 | 171 |
172 |
173 |
174 |
175 |

Legend

176 |
177 |
    178 |
  • Variable
  • 179 |
  • Function
  • 180 |
  • Function with type parameter
  • 181 |
182 |
    183 |
  • Interface
  • 184 |
185 |
    186 |
  • Class
  • 187 |
188 |
189 |
190 |
191 |
192 |

Generated using TypeDoc

193 |
194 |
195 | 196 | 197 | -------------------------------------------------------------------------------- /docs/modules/_models_navigationendpoint_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "models/NavigationEndpoint" | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Module "models/NavigationEndpoint"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Interfaces

75 | 78 |
79 |
80 |
81 |
82 |
83 | 171 |
172 |
173 |
174 |
175 |

Legend

176 |
177 |
    178 |
  • Variable
  • 179 |
  • Function
  • 180 |
  • Function with type parameter
  • 181 |
182 |
    183 |
  • Interface
  • 184 |
185 |
    186 |
  • Class
  • 187 |
188 |
189 |
190 |
191 |
192 |

Generated using TypeDoc

193 |
194 |
195 | 196 | 197 | -------------------------------------------------------------------------------- /docs/modules/_models_song_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "models/Song" | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Module "models/Song"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Interfaces

75 | 79 |
80 |
81 |
82 |
83 |
84 | 175 |
176 |
177 |
178 |
179 |

Legend

180 |
181 |
    182 |
  • Variable
  • 183 |
  • Function
  • 184 |
  • Function with type parameter
  • 185 |
186 |
    187 |
  • Interface
  • 188 |
189 |
    190 |
  • Class
  • 191 |
192 |
193 |
194 |
195 |
196 |

Generated using TypeDoc

197 |
198 |
199 | 200 | 201 | -------------------------------------------------------------------------------- /docs/modules/_models_carouselitem_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "models/CarouselItem" | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Module "models/CarouselItem"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Interfaces

75 | 79 |
80 |
81 |
82 |
83 |
84 | 175 |
176 |
177 |
178 |
179 |

Legend

180 |
181 |
    182 |
  • Variable
  • 183 |
  • Function
  • 184 |
  • Function with type parameter
  • 185 |
186 |
    187 |
  • Interface
  • 188 |
189 |
    190 |
  • Class
  • 191 |
192 |
193 |
194 |
195 |
196 |

Generated using TypeDoc

197 |
198 |
199 | 200 | 201 | -------------------------------------------------------------------------------- /lib/endpoints/Playlist.ts: -------------------------------------------------------------------------------- 1 | import Playlist from '../../models/Playlist' 2 | import { Song } from '../../models/Song' 3 | import { createContinuation } from '../continuations' 4 | import * as utils from '../utils' 5 | 6 | const parsePlaylist = utils.parser((response: any, playlistId: string) => { 7 | const header = response.header.musicDetailHeaderRenderer 8 | ? response.header.musicDetailHeaderRenderer 9 | : response.header.musicEditablePlaylistDetailHeaderRenderer.header 10 | .musicDetailHeaderRenderer 11 | const playlist: Playlist = { 12 | title: header.title.runs[0], 13 | thumbnail: 14 | header.thumbnail.croppedSquareThumbnailRenderer.thumbnail.thumbnails, 15 | playlistId, 16 | content: [], 17 | subtitle: header.subtitle.runs, 18 | secondSubtitle: header.secondSubtitle.runs 19 | } 20 | return playlist 21 | }) 22 | 23 | const parseSong = utils.parser((e: any, playlistId: string) => { 24 | const primaryTextRun = 25 | e.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0] 26 | const id = primaryTextRun?.navigationEndpoint?.watchEndpoint?.videoId 27 | if (!id) { 28 | // NOTE: It is apparently possible to have items that don't have an ID! 29 | // The Web UI renders them as disabled, and the only available action is to 30 | // remove them from the playlist. For now, we will wimply omit them from 31 | // results, since having an optional ID would be quite a breaking change 32 | return 33 | } 34 | 35 | return { 36 | id, 37 | duration: 38 | e.fixedColumns[0].musicResponsiveListItemFixedColumnRenderer.text.runs[0] 39 | .text, 40 | thumbnail: e.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails, 41 | title: primaryTextRun, 42 | author: 43 | e.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs, 44 | album: 45 | e.flexColumns[2].musicResponsiveListItemFlexColumnRenderer.text.runs?.[0], 46 | url: `https://music.youtube.com/watch?v=${id}&list=${playlistId}` 47 | } as Song 48 | }) 49 | 50 | const parsePlaylistContents = utils.parser( 51 | (contents: any, playlistId: string, limit: number | undefined) => { 52 | const content: Song[] = [] 53 | for (let i = 0; i < contents.length; ++i) { 54 | const e = contents[i].musicResponsiveListItemRenderer 55 | if (limit && i > limit - 1) break 56 | 57 | const song = parseSong(e, playlistId) 58 | if (song) { 59 | content.push(song) 60 | } 61 | } 62 | return content 63 | } 64 | ) 65 | 66 | /** 67 | * Returns Playlist Info 68 | * 69 | * @usage 70 | * 71 | * ```js 72 | * const api = new YTMUSIC(cookie) 73 | * const data = await api.getPlaylist('RDAMVM5hFevwJ4JXI') 74 | * ``` 75 | * @param id - playlist ID 76 | * @returns {@link Playlist} 77 | * 78 | */ 79 | export const getPlaylist = async ( 80 | cookie: string, 81 | args: { 82 | userID?: string 83 | authUser?: number 84 | }, 85 | id: string, 86 | limit?: number 87 | ): Promise => { 88 | const response = await utils.sendRequest(cookie, { 89 | id: id.startsWith('VL') ? id : `VL${id}`, 90 | type: 'PLAYLIST', 91 | endpoint: 'browse', 92 | authUser: args.authUser 93 | }) 94 | const playlist = parsePlaylist(response, id) 95 | playlist.playlistId = id 96 | 97 | const data = 98 | response.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer 99 | .content.sectionListRenderer.contents[0].musicPlaylistShelfRenderer 100 | if (!data.contents) return playlist 101 | 102 | playlist.content = parsePlaylistContents(data.contents, id, limit) 103 | if (data.contents[0].playlistItemData) { 104 | playlist.setVideoId = data.contents[0].playlistItemData.playlistSetVideoId 105 | } 106 | 107 | const remainingLimit = limit ? limit - playlist.content.length : undefined 108 | if (remainingLimit == null || remainingLimit > 0) { 109 | playlist.continue = createContinuation( 110 | cookie, 111 | args, 112 | (contents) => parsePlaylistContents(contents, id, remainingLimit), 113 | playlist, 114 | data 115 | ) 116 | } 117 | 118 | return playlist 119 | } 120 | 121 | /** 122 | * Add song(s) to playlist 123 | * 124 | * @usage 125 | * 126 | * ```js 127 | * const api = new YTMUSIC(cookie) 128 | * const data = await api.addToPlaylist(['-mLpe7KUg9U', '5hFevwJ4JXI'], 'RDAMVM5hFevwJ4JXI') 129 | * ``` 130 | * @param ids - Array of song ids to add 131 | * @param playlistId - ID of playlist 132 | * @returns ```js 133 | * { 134 | status: string 135 | playlistName?: string 136 | ids: string[] 137 | playlistId: string 138 | } 139 | ``` 140 | */ 141 | export const addToPlaylist = async ( 142 | cookie: string, 143 | args: { 144 | userID?: string 145 | authUser?: number 146 | }, 147 | ids: string[], 148 | playlistId: string 149 | ): Promise<{ 150 | status: string 151 | playlistName?: string 152 | ids: string[] 153 | playlistId: string 154 | error?: string 155 | }> => { 156 | if (!playlistId) throw new Error('You must specify playlist id') 157 | const body: any = utils.generateBody({ userID: args.userID }) 158 | body.playlistId = playlistId 159 | if (args.userID) body.context.user.onBehalfOfUser = args.userID 160 | body.actions = [] 161 | for (let id of ids) { 162 | body.actions.push({ action: 'ACTION_ADD_VIDEO', addedVideoId: id }) 163 | } 164 | const response = await utils.sendRequest(cookie, { 165 | body, 166 | authUser: args.authUser, 167 | endpoint: 'browse/edit_playlist' 168 | }) 169 | if (response.actions[0].openPopupAction) { 170 | return { 171 | status: response.status, 172 | playlistName: 173 | response.actions[0].openPopupAction.popup.notificationActionRenderer 174 | .responseText.runs[1].text, 175 | ids, 176 | playlistId 177 | } 178 | } else { 179 | return { 180 | status: response.status, 181 | error: 182 | response.actions[0].addToToastAction.item.notificationActionRenderer 183 | .responseText.runs[0].text, 184 | ids, 185 | playlistId 186 | } 187 | } 188 | } 189 | /** 190 | * Create playlist 191 | * 192 | * @usage 193 | * 194 | * ```js 195 | * const api = new YTMUSIC(cookie) 196 | * const data = await api.createPlaylist('Summer Songs', 'PUBLIC', 'Some songs for summer') 197 | * await api.addToPlaylist(['-mLpe7KUg9U', '5hFevwJ4JXI'], playlist.id) 198 | * ``` 199 | * @param title - Title 200 | * @param privacyStatus - Privacy Status of playlist 201 | * @param description - Description of playlist 202 | */ 203 | export const createPlaylist = async ( 204 | cookie: string, 205 | args: { 206 | userID?: string 207 | authUser?: number 208 | }, 209 | title: string, 210 | privacyStatus: 'PRIVATE' | 'PUBLIC' | 'UNLISTED', 211 | description?: string 212 | ): Promise<{ id: string } | Error> => { 213 | if (!description) description = '' 214 | if (!['PRIVATE', 'PUBLIC', 'UNLISTED'].includes(privacyStatus)) 215 | throw new Error('Unknown privacyStatus') 216 | if (!title) throw new Error('Title cannot be empty') 217 | 218 | const body: any = utils.generateBody({ 219 | userID: args.userID 220 | }) 221 | body.title = title 222 | body.description = description 223 | body.privacyStatus = privacyStatus 224 | const response = await utils.sendRequest(cookie, { 225 | body, 226 | authUser: args.authUser, 227 | endpoint: 'playlist/create' 228 | }) 229 | if (response.error) throw new Error(response.error.status) 230 | return { 231 | id: response.playlistId 232 | } 233 | } 234 | /** 235 | * Delete playlist 236 | * 237 | * @usage 238 | * 239 | * ```js 240 | * const api = new YTMUSIC(cookie) 241 | * const data = await api.createPlaylist('Summer Songs', 'PUBLIC', 'Some songs for summer') 242 | * await api.deletePlaylist(playlist.id) 243 | * ``` 244 | * @param id - id of the playlist 245 | */ 246 | export const deletePlaylist = async ( 247 | cookie: string, 248 | args: { 249 | userID?: string 250 | authUser?: number 251 | }, 252 | id: string 253 | ): Promise<{ id: string } | Error> => { 254 | if (!id) throw new Error('You must specify playlist id') 255 | const body: any = utils.generateBody({ 256 | userID: args.userID 257 | }) 258 | body.playlistId = id 259 | const response = await utils.sendRequest(cookie, { 260 | body, 261 | authUser: args.authUser, 262 | endpoint: 'playlist/delete' 263 | }) 264 | if (response.error) throw new Error(response.error.status) 265 | return { 266 | id 267 | } 268 | } 269 | 270 | /** 271 | * Remove song(s) from playlist 272 | * 273 | * @usage 274 | * 275 | * ```js 276 | * const api = new YTMUSIC(cookie) 277 | * const data = await api.removeFromPlaylist(['-mLpe7KUg9U', '5hFevwJ4JXI'], 'RDAMVM5hFevwJ4JXI') 278 | * ``` 279 | * @param ids - Array of song ids to remove 280 | * @param playlistId - ID of playlist 281 | ``` 282 | */ 283 | export const removeFromPlaylist = async ( 284 | cookie: string, 285 | args: { 286 | userID?: string 287 | authUser?: number 288 | }, 289 | ids: string[], 290 | playlistId: string, 291 | setVideoId?: string 292 | ): Promise< 293 | | { 294 | status: string 295 | playlistName?: string 296 | ids: string[] 297 | playlistId: string 298 | } 299 | | Error 300 | > => { 301 | if (!playlistId) { 302 | throw new Error('You must specify playlist id') 303 | } 304 | if (!setVideoId) { 305 | const pl = await getPlaylist(cookie, args, playlistId, 1) 306 | if (!pl.setVideoId) throw new Error("You don't own this playlist") 307 | setVideoId = pl.setVideoId 308 | } 309 | const body: any = utils.generateBody({ userID: args.userID }) 310 | body.playlistId = playlistId 311 | if (args.userID) body.context.user.onBehalfOfUser = args.userID 312 | body.actions = [] 313 | for (let id of ids) { 314 | body.actions.push({ 315 | action: 'ACTION_REMOVE_VIDEO', 316 | removedVideoId: id, 317 | setVideoId: setVideoId 318 | }) 319 | } 320 | const response = await utils.sendRequest(cookie, { 321 | body, 322 | authUser: args.authUser, 323 | endpoint: 'browse/edit_playlist' 324 | }) 325 | return { 326 | status: response.status, 327 | playlistName: 328 | response.actions[0].openPopupAction.popup.notificationActionRenderer 329 | .responseText.runs[1].text, 330 | ids, 331 | playlistId 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /docs/modules/_lib_endpoints_player_.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | "lib/endpoints/Player" | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 62 |

Module "lib/endpoints/Player"

63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |

Index

71 |
72 |
73 |
74 |

Functions

75 | 78 |
79 |
80 |
81 |
82 |
83 |

Functions

84 |
85 | 86 |

Const Player

87 |
    88 |
  • Player(cookie: string, args: any, videoId: string): Promise<Song>
  • 89 |
90 |
    91 |
  • 92 | 97 |

    Parameters

    98 |
      99 |
    • 100 |
      cookie: string
      101 |
    • 102 |
    • 103 |
      args: any
      104 |
    • 105 |
    • 106 |
      videoId: string
      107 |
    • 108 |
    109 |

    Returns Promise<Song>

    110 |
  • 111 |
112 |
113 |
114 |
115 | 203 |
204 |
205 |
206 |
207 |

Legend

208 |
209 |
    210 |
  • Variable
  • 211 |
  • Function
  • 212 |
  • Function with type parameter
  • 213 |
214 |
    215 |
  • Interface
  • 216 |
217 |
    218 |
  • Class
  • 219 |
220 |
221 |
222 |
223 |
224 |

Generated using TypeDoc

225 |
226 |
227 | 228 | 229 | -------------------------------------------------------------------------------- /docs/interfaces/_models_search_.searchitem.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SearchItem | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 65 |

Interface SearchItem

66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |

Hierarchy

74 |
    75 |
  • 76 | SearchItem 77 |
  • 78 |
79 |
80 |
81 |

Index

82 |
83 |
84 |
85 |

Properties

86 | 90 |
91 |
92 |
93 |
94 |
95 |

Properties

96 |
97 | 98 |

contents

99 |
contents: any[]
100 | 105 |
106 |
107 | 108 |

query

109 |
query: string
110 | 115 |
116 |
117 |
118 | 218 |
219 |
220 |
221 |
222 |

Legend

223 |
224 |
    225 |
  • Variable
  • 226 |
  • Function
  • 227 |
  • Function with type parameter
  • 228 |
229 |
    230 |
  • Interface
  • 231 |
  • Property
  • 232 |
233 |
    234 |
  • Class
  • 235 |
236 |
237 |
238 |
239 |
240 |

Generated using TypeDoc

241 |
242 |
243 | 244 | 245 | -------------------------------------------------------------------------------- /docs/interfaces/_models_subtitle_.subtitle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Subtitle | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 65 |

Interface Subtitle

66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |

Hierarchy

74 |
    75 |
  • 76 | Subtitle 77 |
  • 78 |
79 |
80 |
81 |

Index

82 |
83 |
84 |
85 |

Properties

86 | 90 |
91 |
92 |
93 |
94 |
95 |

Properties

96 |
97 | 98 |

Optional navigationEndpoint

99 |
navigationEndpoint: NavigationEndpoint
100 | 105 |
106 |
107 | 108 |

Readonly text

109 |
text: string
110 | 115 |
116 |
117 |
118 | 218 |
219 |
220 |
221 |
222 |

Legend

223 |
224 |
    225 |
  • Variable
  • 226 |
  • Function
  • 227 |
  • Function with type parameter
  • 228 |
229 |
    230 |
  • Interface
  • 231 |
  • Property
  • 232 |
233 |
    234 |
  • Class
  • 235 |
236 |
237 |
238 |
239 |
240 |

Generated using TypeDoc

241 |
242 |
243 | 244 | 245 | -------------------------------------------------------------------------------- /docs/globals.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ytmusic 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 27 |
28 |
29 | Options 30 |
31 |
32 | All 33 |
    34 |
  • Public
  • 35 |
  • Public/Protected
  • 36 |
  • All
  • 37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | Menu 48 |
49 |
50 |
51 |
52 |
53 |
54 | 59 |

ytmusic

60 |
61 |
62 |
63 |
64 |
65 | 101 | 186 |
187 |
188 |
189 |
190 |

Legend

191 |
192 |
    193 |
  • Variable
  • 194 |
  • Function
  • 195 |
  • Function with type parameter
  • 196 |
197 |
    198 |
  • Interface
  • 199 |
200 |
    201 |
  • Class
  • 202 |
203 |
204 |
205 |
206 |
207 |

Generated using TypeDoc

208 |
209 |
210 | 211 | 212 | --------------------------------------------------------------------------------