├── .gitattributes ├── .npmrc ├── .github ├── dependabot.yml ├── workflows │ ├── cron.yml │ ├── pull_request.yml │ └── main.yml └── ISSUE_TEMPLATE │ └── bug.md ├── .editorconfig ├── test ├── error.js ├── parse-data.js ├── get-tracks.js ├── get-data.js ├── get-preview.js └── fixtures │ ├── base64.html │ └── nextjs.html ├── .gitignore ├── LICENSE.md ├── README.md ├── package.json ├── src └── index.js └── CHANGELOG.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | unsafe-perm=true 2 | save-prefix=~ 3 | save=false 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: 'github-actions' 8 | directory: '/' 9 | schedule: 10 | # Check for updates to GitHub Actions every weekday 11 | interval: 'daily' 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | max_line_length = 80 13 | indent_brace_style = 1TBS 14 | spaces_around_operators = true 15 | quote_type = auto 16 | 17 | [package.json] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /test/error.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('ava') 4 | 5 | const { throwError } = require('..') 6 | 7 | test('error provides details about next steps', async t => { 8 | const error = await t.throws(() => 9 | throwError("Couldn't find scripts to get the data.") 10 | ) 11 | t.is( 12 | error.message, 13 | "Couldn't find scripts to get the data.\nPlease report the problem at https://github.com/microlinkhq/spotify-url-info/issues." 14 | ) 15 | }) 16 | -------------------------------------------------------------------------------- /.github/workflows/cron.yml: -------------------------------------------------------------------------------- 1 | name: cron 2 | 3 | on: 4 | schedule: 5 | # Cron job every day at 12:00 6 | # https://crontab.guru/#0_12_*_*_* 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v6 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v6 17 | with: 18 | node-version: lts/* 19 | - name: Setup PNPM 20 | uses: pnpm/action-setup@v4 21 | with: 22 | version: latest 23 | run_install: true 24 | - name: Test 25 | run: npm test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############################ 2 | # npm 3 | ############################ 4 | node_modules 5 | npm-debug.log 6 | .node_history 7 | yarn.lock 8 | package-lock.json 9 | 10 | ############################ 11 | # tmp, editor & OS files 12 | ############################ 13 | .tmp 14 | *.swo 15 | *.swp 16 | *.swn 17 | *.swm 18 | .DS_Store 19 | *# 20 | *~ 21 | .idea 22 | *sublime* 23 | nbproject 24 | src/auto-domains.json 25 | 26 | ############################ 27 | # Tests 28 | ############################ 29 | testApp 30 | coverage 31 | .nyc_output 32 | 33 | ############################ 34 | # Other 35 | ############################ 36 | .envrc 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 3 | about: Post a bug report when the library is not working for some or all spotify URLS 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **Spotify URL(s)** 13 | The specific spotify URLs you are having issues with. Even If it's broken for all URLs, please put at least one that caused an error. 14 | 15 | **Your Code** 16 | The part of your code where you use the functions you imported from the library and the surrounding lines / function. 17 | 18 | **Error Message** 19 | The error message you get when running the code. 20 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: pull_request 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | if: github.ref != 'refs/heads/master' 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v6 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v6 22 | with: 23 | node-version: lts/* 24 | - name: Setup PNPM 25 | uses: pnpm/action-setup@v4 26 | with: 27 | version: latest 28 | run_install: true 29 | - name: Test 30 | run: npm test 31 | - name: Report 32 | run: npx c8 report --reporter=text-lcov > coverage/lcov.info 33 | - name: Coverage 34 | uses: coverallsapp/github-action@main 35 | with: 36 | github-token: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2021 Microlink (microlink.io) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | contributors: 10 | if: "${{ github.event.head_commit.message != 'build: contributors' }}" 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v6 15 | with: 16 | fetch-depth: 0 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v6 20 | with: 21 | node-version: lts/* 22 | - name: Contributors 23 | run: | 24 | git config --global user.email ${{ secrets.GIT_EMAIL }} 25 | git config --global user.name ${{ secrets.GIT_USERNAME }} 26 | npm run contributors 27 | - name: Push changes 28 | run: | 29 | git push origin ${{ github.head_ref }} 30 | 31 | release: 32 | if: | 33 | !startsWith(github.event.head_commit.message, 'chore(release):') && 34 | !startsWith(github.event.head_commit.message, 'docs:') && 35 | !startsWith(github.event.head_commit.message, 'ci:') 36 | needs: [contributors] 37 | runs-on: ubuntu-latest 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v6 41 | with: 42 | fetch-depth: 2 43 | 44 | token: ${{ secrets.GITHUB_TOKEN }} 45 | - name: Setup Node.js 46 | uses: actions/setup-node@v6 47 | with: 48 | node-version: lts/* 49 | - name: Setup PNPM 50 | uses: pnpm/action-setup@v4 51 | with: 52 | version: latest 53 | run_install: true 54 | - name: Test 55 | run: npm test 56 | - name: Release 57 | env: 58 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 59 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 60 | run: | 61 | git config --global user.email ${{ secrets.GIT_EMAIL }} 62 | git config --global user.name ${{ secrets.GIT_USERNAME }} 63 | git pull origin master 64 | npm run release 65 | -------------------------------------------------------------------------------- /test/parse-data.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mapValuesDeep = require('map-values-deep') 4 | const { readFile } = require('fs/promises') 5 | 6 | const path = require('path') 7 | const test = require('ava') 8 | 9 | const { parseData } = require('..') 10 | 11 | const toTypeof = value => typeof value 12 | 13 | const expected = mapValuesDeep( 14 | { 15 | type: 'track', 16 | name: 'Immaterial', 17 | uri: 'spotify:track:5nTtCOCds6I0PHMNtqelas', 18 | id: '5nTtCOCds6I0PHMNtqelas', 19 | title: 'Immaterial', 20 | artists: [ 21 | { 22 | name: 'SOPHIE', 23 | uri: 'spotify:artist:5a2w2tgpLwv26BYJf2qYwu' 24 | } 25 | ], 26 | coverArt: { 27 | extractedColors: { 28 | colorDark: { 29 | hex: '#785870' 30 | }, 31 | colorLight: { 32 | hex: '#926B88' 33 | } 34 | }, 35 | sources: [ 36 | { 37 | url: 'https://i.scdn.co/image/ab67616d00001e026b03d8c63599cc94263d7d60', 38 | width: 300, 39 | height: 300 40 | }, 41 | { 42 | url: 'https://i.scdn.co/image/ab67616d000048516b03d8c63599cc94263d7d60', 43 | width: 64, 44 | height: 64 45 | }, 46 | { 47 | url: 'https://i.scdn.co/image/ab67616d0000b2736b03d8c63599cc94263d7d60', 48 | width: 640, 49 | height: 640 50 | } 51 | ] 52 | }, 53 | releaseDate: { 54 | isoString: '2018-06-15T00:00:00Z' 55 | }, 56 | duration: 232806, 57 | maxDuration: 232806, 58 | isPlayable: true, 59 | isExplicit: false, 60 | audioPreview: { 61 | url: 'https://p.scdn.co/mp3-preview/97b5eb03593683855fffada4248fcfffe4dcc263', 62 | format: 'MP3_96' 63 | }, 64 | hasVideo: false, 65 | relatedEntityUri: 'spotify:artist:5a2w2tgpLwv26BYJf2qYwu' 66 | }, 67 | toTypeof 68 | ) 69 | 70 | test('from base64', async t => { 71 | const html = await readFile( 72 | path.join(__dirname, './fixtures/base64.html'), 73 | 'utf-8' 74 | ) 75 | 76 | const data = parseData(html) 77 | t.deepEqual(mapValuesDeep(data, toTypeof), expected) 78 | }) 79 | 80 | test('from nextjs', async t => { 81 | const html = await readFile( 82 | path.join(__dirname, './fixtures/nextjs.html'), 83 | 'utf-8' 84 | ) 85 | 86 | const data = parseData(html) 87 | t.deepEqual(mapValuesDeep(data, toTypeof), expected) 88 | }) 89 | -------------------------------------------------------------------------------- /test/get-tracks.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fetch = require('isomorphic-unfetch') 4 | const test = require('ava') 5 | 6 | const { getTracks } = require('..')(fetch) 7 | 8 | test('getting data for empty url should return rejection', async t => { 9 | const error = await t.throwsAsync(() => getTracks(''), { 10 | instanceOf: TypeError 11 | }) 12 | t.is(error.message, "Couldn't parse '' as valid URL") 13 | }) 14 | 15 | test('getting data for non url string should return rejection', async t => { 16 | const error = await t.throwsAsync( 17 | () => getTracks('arti39anptrackspotify:://https'), 18 | { 19 | instanceOf: TypeError 20 | } 21 | ) 22 | t.is( 23 | error.message, 24 | "Couldn't parse 'arti39anptrackspotify:://https' as valid URL" 25 | ) 26 | }) 27 | 28 | test('getting data for non spotify url string should return rejection', async t => { 29 | const error = await t.throwsAsync( 30 | () => getTracks('http://google.com/5a2w2tgpLwv26BYJf2qYwu'), 31 | { 32 | instanceOf: TypeError 33 | } 34 | ) 35 | t.is( 36 | error.message, 37 | "Couldn't parse 'http://google.com/5a2w2tgpLwv26BYJf2qYwu' as valid URL" 38 | ) 39 | }) 40 | 41 | test('get tracks for spotify track', async t => { 42 | const url = 'https://open.spotify.com/track/5nTtCOCds6I0PHMNtqelas' 43 | const tracks = await getTracks(url) 44 | t.true(Array.isArray(tracks)) 45 | t.is(tracks[0].name, 'Immaterial') 46 | t.is(tracks[0].previewUrl.includes('/mp3-preview/'), true) 47 | }) 48 | 49 | test('get tracks for spotify artist', async t => { 50 | const url = 'https://open.spotify.com/artist/5a2w2tgpLwv26BYJf2qYwu' 51 | const tracks = await getTracks(url) 52 | t.true(Array.isArray(tracks)) 53 | t.is(tracks[0].name, 'Ponyboy') 54 | t.is(tracks[0].previewUrl.includes('/mp3-preview/'), true) 55 | }) 56 | 57 | test('get tracks for spotify album', async t => { 58 | const url = 'https://open.spotify.com/album/4tDBsfbHRJ9OdcMO9bmnai' 59 | const tracks = await getTracks(url) 60 | t.true(Array.isArray(tracks)) 61 | t.is(tracks[1].name, 'ELLE') 62 | t.is(tracks[1].previewUrl.includes('/mp3-preview/'), true) 63 | }) 64 | 65 | test('get tracks for spotify playlist', async t => { 66 | const url = 'https://open.spotify.com/playlist/3Q4cPwMHY95ZHXtmcU2xvH' 67 | const tracks = await getTracks(url) 68 | t.true(Array.isArray(tracks)) 69 | t.is(tracks[1].name, 'ELLE') 70 | t.is(tracks[1].previewUrl.includes('/mp3-preview/'), true) 71 | }) 72 | 73 | test('get tracks for spotify episode', async t => { 74 | const url = 'http://open.spotify.com/episode/64TORH3xleuD1wcnFsrH1E' 75 | const tracks = await getTracks(url) 76 | t.true(Array.isArray(tracks)) 77 | t.is(tracks[0].name, 'Hasty Treat - Modules in Node') 78 | t.is(tracks[0].previewUrl.includes('.spotifycdn.'), true) 79 | }) 80 | -------------------------------------------------------------------------------- /test/get-data.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fetch = require('isomorphic-unfetch') 4 | const test = require('ava') 5 | 6 | const { getLink, getData } = require('..')(fetch) 7 | 8 | test('getting data for empty url should return rejection', async t => { 9 | const error = await t.throwsAsync(() => getData(''), { 10 | instanceOf: TypeError 11 | }) 12 | t.is(error.message, "Couldn't parse '' as valid URL") 13 | }) 14 | 15 | test('getting data for non url string should return rejection', async t => { 16 | const error = await t.throwsAsync( 17 | () => getData('arti39anptrackspotify:://https'), 18 | { 19 | instanceOf: TypeError 20 | } 21 | ) 22 | t.is( 23 | error.message, 24 | "Couldn't parse 'arti39anptrackspotify:://https' as valid URL" 25 | ) 26 | }) 27 | 28 | test('getting data for non spotify url string should return rejection', async t => { 29 | const error = await t.throwsAsync( 30 | () => getData('http://google.com/5a2w2tgpLwv26BYJf2qYwu'), 31 | { 32 | instanceOf: TypeError 33 | } 34 | ) 35 | t.is( 36 | error.message, 37 | "Couldn't parse 'http://google.com/5a2w2tgpLwv26BYJf2qYwu' as valid URL" 38 | ) 39 | }) 40 | 41 | test('getting data for a deleted spotify url should return rejection', async t => { 42 | const error = await t.throwsAsync( 43 | () => getData('https://open.spotify.com/playlist/7E6aXqOtSnwECFLiCosTmM'), 44 | { 45 | instanceOf: TypeError 46 | } 47 | ) 48 | t.is( 49 | error.message, 50 | "Couldn't find any data in embed page that we know how to parse.\nPlease report the problem at https://github.com/microlinkhq/spotify-url-info/issues." 51 | ) 52 | }) 53 | 54 | test('get data for spotify track', async t => { 55 | const url = 'https://open.spotify.com/track/5nTtCOCds6I0PHMNtqelas' 56 | const data = await getData(url) 57 | t.is(data.type, 'track') 58 | t.is(data.name, 'Immaterial') 59 | t.is(getLink(data), url) 60 | }) 61 | 62 | test('get data for spotify artist', async t => { 63 | const url = 'https://open.spotify.com/artist/5a2w2tgpLwv26BYJf2qYwu' 64 | const data = await getData(url) 65 | t.is(data.type, 'artist') 66 | t.is(data.name, 'SOPHIE') 67 | t.is(getLink(data), url) 68 | }) 69 | 70 | test('get data for spotify album', async t => { 71 | const url = 'https://open.spotify.com/album/4tDBsfbHRJ9OdcMO9bmnai' 72 | const data = await getData(url) 73 | t.is(data.type, 'album') 74 | t.is(data.name, 'PRODUCT') 75 | t.is(getLink(data), url) 76 | }) 77 | 78 | test('get data for spotify playlist', async t => { 79 | const url = 'https://open.spotify.com/playlist/3Q4cPwMHY95ZHXtmcU2xvH' 80 | const data = await getData(url) 81 | t.is(data.type, 'playlist') 82 | t.is(data.name, 'SOPHIE – PRODUCT') 83 | t.is( 84 | getLink(data), 85 | 'https://open.spotify.com/playlist/3Q4cPwMHY95ZHXtmcU2xvH' 86 | ) 87 | }) 88 | 89 | test('get data for spotify episode', async t => { 90 | const data = await getData( 91 | 'http://open.spotify.com/episode/64TORH3xleuD1wcnFsrH1E' 92 | ) 93 | 94 | t.is(data.type, 'episode') 95 | t.is(data.name, 'Hasty Treat - Modules in Node') 96 | }) 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | microlink logo 3 | microlink logo 4 |
5 |
6 |
7 | 8 | ![Last version](https://img.shields.io/github/tag/microlinkhq/spotify-url-info.svg?style=flat-square) 9 | [![Coverage Status](https://img.shields.io/coveralls/microlinkhq/spotify-url-info.svg?style=flat-square)](https://coveralls.io/github/microlinkhq/spotify.url-info) 10 | [![NPM Status](https://img.shields.io/npm/dm/spotify-url-info.svg?style=flat-square)](https://www.npmjs.org/package/spotify-url-info) 11 | 12 | > Get metadata from Spotify URLs. 13 | 14 | ## Install 15 | 16 | ```bash 17 | npm install spotify-url-info 18 | ``` 19 | 20 | ## Usage 21 | 22 | In order to use the library, you have to provide the fetch agent to use: 23 | 24 | ```js 25 | const fetch = require('isomorphic-unfetch') 26 | const { getData, getPreview, getTracks, getDetails } = 27 | require('spotify-url-info')(fetch) 28 | ``` 29 | 30 | There are four functions: 31 | 32 | - **getData**
33 | Provides the full available data, in a shape that is very similar to [what the spotify API returns](https://developer.spotify.com/documentation/web-api/reference/object-model/). 34 | 35 | - **getPreview**
36 | Always returns the same fields for different types of resources (album, artist, playlist, track). The preview track is the first in the Album, Playlist, etc. 37 | 38 | - **getTracks**
39 | Returns array with tracks. This data is passed on straight from spotify, so the shape could change.Only the first 100 tracks will be returned. 40 | 41 | - **getDetails**
42 | Returns both the preview and tracks. Should be used if you require information from both of them so that only one request is made. 43 | 44 | All the methods receive a Spotify URL (play. or open.) as first argument: 45 | 46 | ```js 47 | getPreview('https://open.spotify.com/track/5nTtCOCds6I0PHMNtqelas').then(data => 48 | console.log(data) 49 | ) 50 | ``` 51 | 52 | Additionally, you can provide fetch agent options as second argument: 53 | 54 | ```js 55 | getPreview('https://open.spotify.com/track/5nTtCOCds6I0PHMNtqelas', { 56 | headers: { 57 | 'user-agent': 'googlebot' 58 | } 59 | }).then(data => console.log(data)) 60 | ``` 61 | 62 | It returns back the information related to the Spotify URL: 63 | 64 | ```json 65 | { 66 | "title": "Immaterial", 67 | "type": "track", 68 | "track": "Immaterial", 69 | "artist": "SOPHIE", 70 | "image": "https://i.scdn.co/image/d6f496a6708d22a2f867e5acb84afb0eb0b07bc1", 71 | "audio": "https://p.scdn.co/mp3-preview/6be8eb12ff18ae09b7a6d38ff1e5327fd128a74e?cid=162b7dc01f3a4a2ca32ed3cec83d1e02", 72 | "link": "https://open.spotify.com/track/5nTtCOCds6I0PHMNtqelas", 73 | "embed": "https://embed.spotify.com/?uri=spotify:track:5nTtCOCds6I0PHMNtqelas", 74 | "date": "2018-06-15T00:00:00.000Z", 75 | "description": "description of a podcast episode" 76 | } 77 | ``` 78 | 79 | When a field can't be retrieved, the value will be `undefined`. 80 | 81 | There are no guarantees about the shape of this data, because it varies with different media and scraping methods. Handle it carefully. 82 | 83 | ## License 84 | 85 | **spotify-url-info** © [microlink.io](https://microlink.io), released under the [MIT](https://github.com/microlinkhq/spotify-url-info/blob/master/LICENSE.md) License.
86 | Authored by [Karl Sander](https://github.com/karlsander) and maintained by [Kiko Beats](https://kikobeats.com) with help from [contributors](https://github.com/microlinkhq/spotify-url-info/contributors). 87 | 88 | > [microlink.io](https://microlink.io) · GitHub [microlink.io](https://github.com/microlinkhq) · X [@microlinkhq](https://x.com/microlinkhq) 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spotify-url-info", 3 | "description": "Get metadata from Spotify URLs", 4 | "homepage": "https://github.com/microlinkhq/spotify-url-info", 5 | "version": "3.2.18", 6 | "main": "src/index.js", 7 | "author": { 8 | "email": "kall@kall.ws", 9 | "name": "Karl Sander" 10 | }, 11 | "contributors": [ 12 | { 13 | "name": "Kiko Beats", 14 | "email": "josefrancisco.verdu@gmail.com" 15 | }, 16 | { 17 | "name": "DaliborTrampota", 18 | "email": "dalibor.trampota@gmail.com" 19 | }, 20 | { 21 | "name": "crxts", 22 | "email": "49580728+crxts@users.noreply.github.com" 23 | }, 24 | { 25 | "name": "kaaax0815", 26 | "email": "999999bst@gmail.com" 27 | }, 28 | { 29 | "name": "Victor", 30 | "email": "victor@offspringdigital.com" 31 | }, 32 | { 33 | "name": "Eitho", 34 | "email": "62159998+Eithoo@users.noreply.github.com" 35 | }, 36 | { 37 | "name": "D3SOX", 38 | "email": "d3sox@protonmail.com" 39 | }, 40 | { 41 | "name": "kaname-png", 42 | "email": "inmortaldragonxspace@outlook.com" 43 | }, 44 | { 45 | "name": "KeepSOBP", 46 | "email": "keepsobp@naver.com" 47 | } 48 | ], 49 | "repository": { 50 | "type": "git", 51 | "url": "git+https://github.com/microlinkhq/spotify-url-info.git" 52 | }, 53 | "bugs": { 54 | "url": "https://github.com/microlinkhq/spotify-url-info/issues" 55 | }, 56 | "keywords": [ 57 | "embed", 58 | "link-preview", 59 | "metadata", 60 | "music", 61 | "spotify", 62 | "spotify-urls" 63 | ], 64 | "dependencies": { 65 | "himalaya": "~1.1.0", 66 | "spotify-uri": "~4.1.0" 67 | }, 68 | "devDependencies": { 69 | "@commitlint/cli": "latest", 70 | "@commitlint/config-conventional": "latest", 71 | "@ksmithut/prettier-standard": "latest", 72 | "ava": "latest", 73 | "c8": "latest", 74 | "ci-publish": "latest", 75 | "finepack": "latest", 76 | "git-authors-cli": "latest", 77 | "github-generate-release": "latest", 78 | "isomorphic-unfetch": "latest", 79 | "lodash": "latest", 80 | "map-values-deep": "latest", 81 | "nano-staged": "latest", 82 | "simple-git-hooks": "latest", 83 | "standard": "latest", 84 | "standard-markdown": "latest", 85 | "standard-version": "latest" 86 | }, 87 | "engines": { 88 | "node": ">= 12" 89 | }, 90 | "files": [ 91 | "src" 92 | ], 93 | "scripts": { 94 | "clean": "rm -rf node_modules", 95 | "contributors": "(npx git-authors-cli && npx finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true", 96 | "coverage": "nyc report --reporter=text-lcov | npx coveralls", 97 | "lint": "standard-markdown README.md && standard", 98 | "postrelease": "npm run release:tags && npm run release:github && (ci-publish || npm publish --access=public)", 99 | "pretest": "npm run lint", 100 | "release": "standard-version -a", 101 | "release:github": "github-generate-release", 102 | "release:tags": "git push --follow-tags origin HEAD:master", 103 | "test": "c8 ava" 104 | }, 105 | "license": "MIT", 106 | "ava": { 107 | "serial": true 108 | }, 109 | "commitlint": { 110 | "extends": [ 111 | "@commitlint/config-conventional" 112 | ], 113 | "rules": { 114 | "body-max-line-length": [ 115 | 0 116 | ] 117 | } 118 | }, 119 | "nano-staged": { 120 | "*.js": [ 121 | "prettier-standard", 122 | "standard --fix" 123 | ], 124 | "*.md": [ 125 | "standard-markdown" 126 | ], 127 | "package.json": [ 128 | "finepack" 129 | ] 130 | }, 131 | "simple-git-hooks": { 132 | "commit-msg": "npx commitlint --edit", 133 | "pre-commit": "npx nano-staged" 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const spotifyURI = require('spotify-uri') 4 | const { parse } = require('himalaya') 5 | 6 | const TYPE = { 7 | ALBUM: 'album', 8 | ARTIST: 'artist', 9 | EPISODE: 'episode', 10 | PLAYLIST: 'playlist', 11 | TRACK: 'track' 12 | } 13 | 14 | const ERROR = { 15 | REPORT: 16 | 'Please report the problem at https://github.com/microlinkhq/spotify-url-info/issues.', 17 | NOT_DATA: "Couldn't find any data in embed page that we know how to parse.", 18 | NOT_SCRIPTS: "Couldn't find scripts to get the data." 19 | } 20 | 21 | const SUPPORTED_TYPES = Object.values(TYPE) 22 | 23 | const throwError = (message, html) => { 24 | const error = new TypeError(`${message}\n${ERROR.REPORT}`) 25 | error.html = html 26 | throw error 27 | } 28 | 29 | const parseData = html => { 30 | const embed = parse(html) 31 | 32 | let scripts = embed.find(el => el.tagName === 'html') 33 | if (scripts === undefined) return throwError(ERROR.NOT_SCRIPTS, html) 34 | 35 | scripts = scripts.children 36 | .find(el => el.tagName === 'body') 37 | .children.filter(({ tagName }) => tagName === 'script') 38 | 39 | let script = scripts.find(script => 40 | script.attributes.some(({ value }) => value === 'resource') 41 | ) 42 | 43 | if (script !== undefined) { 44 | return normalizeData({ 45 | data: JSON.parse(Buffer.from(script.children[0].content, 'base64')) 46 | }) 47 | } 48 | 49 | script = scripts.find(script => 50 | script.attributes.some(({ value }) => value === 'initial-state') 51 | ) 52 | 53 | if (script !== undefined) { 54 | const data = JSON.parse(Buffer.from(script.children[0].content, 'base64')) 55 | .data.entity 56 | return normalizeData({ data }) 57 | } 58 | 59 | script = scripts.find(script => 60 | script.attributes.some(({ value }) => value === '__NEXT_DATA__') 61 | ) 62 | 63 | if (script !== undefined) { 64 | const string = Buffer.from(script.children[0].content) 65 | const data = JSON.parse(string).props.pageProps.state?.data.entity 66 | if (data !== undefined) return normalizeData({ data }) 67 | } 68 | 69 | return throwError(ERROR.NOT_DATA, html) 70 | } 71 | 72 | const createGetData = fetch => async (url, opts) => { 73 | const parsedUrl = getParsedUrl(url) 74 | const embedURL = spotifyURI.formatEmbedURL(parsedUrl) 75 | const response = await fetch(embedURL, opts) 76 | const text = await response.text() 77 | return parseData(text) 78 | } 79 | 80 | function getParsedUrl (url) { 81 | try { 82 | const parsedURL = spotifyURI.parse(url) 83 | if (!parsedURL.type) throw new TypeError() 84 | return spotifyURI.formatEmbedURL(parsedURL) 85 | } catch (_) { 86 | throw new TypeError(`Couldn't parse '${url}' as valid URL`) 87 | } 88 | } 89 | 90 | const getImages = data => 91 | data.coverArt?.sources || data.images || data.visualIdentity.image 92 | 93 | const getDate = data => data.releaseDate?.isoString || data.release_date 94 | 95 | const getLink = data => spotifyURI.formatOpenURL(data.uri) 96 | 97 | function getArtistTrack (track) { 98 | return track.show 99 | ? track.show.publisher 100 | : [] 101 | .concat(track.artists) 102 | .filter(Boolean) 103 | .map(a => a.name) 104 | .reduce( 105 | (acc, name, index, array) => 106 | index === 0 107 | ? name 108 | : acc + (array.length - 1 === index ? ' & ' : ', ') + name, 109 | '' 110 | ) 111 | } 112 | 113 | const getTracks = data => 114 | data.trackList ? data.trackList.map(toTrack) : [toTrack(data)] 115 | 116 | function getPreview (data) { 117 | const [track] = getTracks(data) 118 | const date = getDate(data) 119 | 120 | return { 121 | date: date ? new Date(date).toISOString() : date, 122 | title: data.name, 123 | type: data.type, 124 | track: track.name, 125 | description: data.description || data.subtitle || track.description, 126 | artist: track.artist, 127 | image: getImages(data)?.reduce((a, b) => (a.width > b.width ? a : b))?.url, 128 | audio: track.previewUrl, 129 | link: getLink(data), 130 | embed: `https://embed.spotify.com/?uri=${data.uri}` 131 | } 132 | } 133 | 134 | const toTrack = track => ({ 135 | artist: getArtistTrack(track) || track.subtitle, 136 | duration: track.duration, 137 | name: track.title, 138 | previewUrl: track.isPlayable ? track.audioPreview.url : undefined, 139 | uri: track.uri 140 | }) 141 | 142 | const normalizeData = ({ data }) => { 143 | if (!data || !data.type || !data.name) { 144 | throw new Error("Data doesn't seem to be of the right shape to parse") 145 | } 146 | 147 | if (!SUPPORTED_TYPES.includes(data.type)) { 148 | throw new Error( 149 | `Not an ${SUPPORTED_TYPES.join(', ')}. Only these types can be parsed` 150 | ) 151 | } 152 | 153 | data.type = data.uri.split(':')[1] 154 | 155 | return data 156 | } 157 | 158 | module.exports = fetch => { 159 | const getData = createGetData(fetch) 160 | return { 161 | getLink, 162 | getData, 163 | getPreview: (url, opts) => getData(url, opts).then(getPreview), 164 | getTracks: (url, opts) => getData(url, opts).then(getTracks), 165 | getDetails: (url, opts) => 166 | getData(url, opts).then(data => ({ 167 | preview: getPreview(data), 168 | tracks: getTracks(data) 169 | })) 170 | } 171 | } 172 | 173 | module.exports.parseData = parseData 174 | module.exports.throwError = throwError 175 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### 3.2.18 (2024-11-17) 6 | 7 | ### 3.2.17 (2024-09-30) 8 | 9 | ### 3.2.16 (2024-07-06) 10 | 11 | ### 3.2.15 (2024-05-24) 12 | 13 | ### 3.2.14 (2024-05-07) 14 | 15 | ### 3.2.13 (2024-02-08) 16 | 17 | ### 3.2.12 (2024-01-31) 18 | 19 | ### 3.2.11 (2024-01-31) 20 | 21 | ### 3.2.10 (2023-12-06) 22 | 23 | ### 3.2.9 (2023-10-23) 24 | 25 | ### 3.2.8 (2023-09-23) 26 | 27 | ### 3.2.7 (2023-09-23) 28 | 29 | ### 3.2.6 (2023-07-29) 30 | 31 | ### 3.2.5 (2023-05-18) 32 | 33 | ### 3.2.4 (2023-05-13) 34 | 35 | ### 3.2.3 (2022-12-29) 36 | 37 | 38 | ### Bug Fixes 39 | 40 | * **build:** prevent publish under release commit ([4556002](https://github.com/microlinkhq/spotify-url-info/commit/45560028becb1260b12ffb16e9e664c9909fa31f)) 41 | 42 | ### 3.2.2 (2022-12-29) 43 | 44 | ### [3.2.1](https://github.com/microlinkhq/spotify-url-info/compare/v3.2.0-0...v3.2.1) (2022-12-29) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * remove old types ([84a9873](https://github.com/microlinkhq/spotify-url-info/commit/84a987327a3901ec3622afdfebef16fa230cb473)) 50 | 51 | ## 3.2.0 (2022-12-29) 52 | 53 | ## [3.2.0-0](https://github.com/microlinkhq/spotify-url-info/compare/v3.1.10...v3.2.0-0) (2022-12-29) 54 | 55 | ### 3.1.10 (2022-12-12) 56 | 57 | ### 3.1.9 (2022-10-01) 58 | 59 | ### 3.1.8 (2022-09-09) 60 | 61 | ### 3.1.7 (2022-09-02) 62 | 63 | ### 3.1.6 (2022-09-02) 64 | 65 | ### [3.1.5](https://github.com/microlinkhq/spotify-url-info/compare/v3.1.4...v3.1.5) (2022-09-02) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * changes in Spotify embeds ([53a5401](https://github.com/microlinkhq/spotify-url-info/commit/53a5401ac34a92681c3174ecbe63ed4550720f07)) 71 | * ensure spotify URL is valid ([c2e3436](https://github.com/microlinkhq/spotify-url-info/commit/c2e343651ba1df801532501259705bec80b0d5c3)) 72 | 73 | ### 3.1.4 (2022-08-05) 74 | 75 | ### 3.1.3 (2022-08-05) 76 | 77 | 78 | ### Bug Fixes 79 | 80 | * get description from an episode ([2aba83d](https://github.com/microlinkhq/spotify-url-info/commit/2aba83da3ab8b3ba39363b487f35c503f86e3bd2)) 81 | 82 | ### 3.1.2 (2022-05-15) 83 | 84 | ### 3.1.1 (2022-05-02) 85 | 86 | 87 | ### Bug Fixes 88 | 89 | * add named exports ([92dd313](https://github.com/microlinkhq/spotify-url-info/commit/92dd313b4513f88da382afda05d76dd6a94532b5)), closes [#92](https://github.com/microlinkhq/spotify-url-info/issues/92) 90 | 91 | ## [3.1.0](https://github.com/microlinkhq/spotify-url-info/compare/v3.0.7...v3.1.0) (2022-04-12) 92 | 93 | 94 | ### Features 95 | 96 | * add getDetails which returns both preview and tracks ([7e72e4d](https://github.com/microlinkhq/spotify-url-info/commit/7e72e4d047907b287a97c3e0cd7be0bf9eff197d)) 97 | 98 | ### 3.0.7 (2022-04-11) 99 | 100 | ### 3.0.6 (2022-04-08) 101 | 102 | ### 3.0.5 (2022-04-02) 103 | 104 | ### 3.0.4 (2022-04-02) 105 | 106 | ### 3.0.3 (2022-04-01) 107 | 108 | ### 3.0.2 (2022-03-29) 109 | 110 | ### 3.0.1 (2022-03-29) 111 | 112 | ## [3.0.0](https://github.com/microlinkhq/spotify-url-info/compare/v2.2.9...v3.0.0) (2022-03-20) 113 | 114 | 115 | ### ⚠ BREAKING CHANGES 116 | 117 | * The library will be shipped without a default fetch agent. 118 | 119 | ### Features 120 | 121 | * pass fetch agent as necessary dependency ([649778b](https://github.com/microlinkhq/spotify-url-info/commit/649778be126d9ced15228f7c8c7f9ee85d7e9f7c)) 122 | 123 | ### 2.2.9 (2022-03-20) 124 | 125 | ### 2.2.8 (2022-03-20) 126 | 127 | ### 2.2.7 (2022-03-02) 128 | 129 | ### 2.2.6 (2022-02-24) 130 | 131 | ### 2.2.5 (2022-01-30) 132 | 133 | 134 | ### Bug Fixes 135 | 136 | * files meta field ([0679da6](https://github.com/microlinkhq/spotify-url-info/commit/0679da64572287bce4b0d96ff65a6840e2df17b2)), closes [#77](https://github.com/microlinkhq/spotify-url-info/issues/77) 137 | 138 | ### [2.2.4](https://github.com/microlinkhq/spotify-url-info/compare/v2.2.3...v2.2.4) (2022-01-29) 139 | 140 | ### [2.2.3](https://github.com/microlinkhq/spotify-url-info/compare/v2.2.2...v2.2.3) (2021-05-21) 141 | 142 | 143 | ### Bug Fixes 144 | 145 | * add typings files ([5f98597](https://github.com/microlinkhq/spotify-url-info/commit/5f98597f45bceadf9c05e4ed9ccbe03d7fa80ebc)) 146 | 147 | ### [2.2.2](https://github.com/microlinkhq/spotify-url-info/compare/v2.2.1...v2.2.2) (2021-05-21) 148 | 149 | ### [2.2.1](https://github.com/karlsander/spotify-url-info/compare/v2.2.1-0...v2.2.1) (2021-05-21) 150 | 151 | 152 | ### Bug Fixes 153 | 154 | * coverage ([d4c3a62](https://github.com/karlsander/spotify-url-info/commit/d4c3a6237d751332d5ecbade0fd64381309571db)) 155 | 156 | ### 2.2.0 157 | 158 | - add `getTracks` feature (thanks [@DaliborTrampota](https://github.com/DaliborTrampota)!) 159 | 160 | ### 2.1.0 161 | 162 | Warning: The data returned from `getData` can change at any time. For example, the newer podcast embed does not provide `dominantColor` anymore. I do not consider that a breaking change for this library. The only guarantee is that you get the data spotify makes available. You need to add safety checks in your application code. Only the data shape returned by `getPreview` is guaranteed. 163 | 164 | - fixes an issue with encoded data in the parsed html page (issue #55) 165 | - add support for scraping a different type of embed page, currently used in podcast episodes (fixes issue #54) 166 | 167 | ### 2.0.0 168 | 169 | - Drop support for EOL node versions, which is technically breaking 🤷‍♂️ 170 | 171 | ### 1.4.0 172 | 173 | - Support for podcast episodes on spotify (contributed by @kikobeats) 174 | - new `description` and `date` fields in the preview object (contributed by @kikobeats) 175 | 176 | ### 1.3.1 177 | 178 | - update dependencies 179 | 180 | ### 1.3.0 181 | 182 | - remove lockfile 183 | 184 | ### 1.2.0 185 | 186 | - now uses Himalaya for html parsing instead of cheerio, its more complex / brittle but the bundle is way smaller so it can be used inside apps 187 | 188 | ### 1.1.1 189 | 190 | - generate embed url for preview with string concatination instead of using spotifyURL package 191 | - bump dependency versions 192 | 193 | ### 1.1.0 194 | 195 | - add embed field to `getPreview` result 196 | 197 | ### 1.0.0 198 | 199 | - first public release 200 | -------------------------------------------------------------------------------- /test/get-preview.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fetch = require('isomorphic-unfetch') 4 | const test = require('ava') 5 | 6 | const { getPreview } = require('..')(fetch) 7 | 8 | test('getting preview for empty url should return rejection', async t => { 9 | const error = await t.throwsAsync(() => getPreview(''), { 10 | instanceOf: TypeError 11 | }) 12 | t.is(error.message, "Couldn't parse '' as valid URL") 13 | }) 14 | 15 | test('getting preview for non url string should return rejection', async t => { 16 | const error = await t.throwsAsync( 17 | () => getPreview('arti39anptrackspotify:://https'), 18 | { 19 | instanceOf: TypeError 20 | } 21 | ) 22 | t.is( 23 | error.message, 24 | "Couldn't parse 'arti39anptrackspotify:://https' as valid URL" 25 | ) 26 | }) 27 | 28 | test('getting preview for non spotify url string should return rejection', async t => { 29 | const error = await t.throwsAsync( 30 | () => getPreview('http://google.com/5a2w2tgpLwv26BYJf2qYwu'), 31 | { 32 | instanceOf: TypeError 33 | } 34 | ) 35 | t.is( 36 | error.message, 37 | "Couldn't parse 'http://google.com/5a2w2tgpLwv26BYJf2qYwu' as valid URL" 38 | ) 39 | }) 40 | 41 | test.skip('getting preview for non spotify url string that looks like a spotify url should return rejection', async t => { 42 | const error = await t.throwsAsync( 43 | () => getPreview('http://google.com/track/5nTtCOCds6I0PHMNtqelas'), 44 | { 45 | instanceOf: TypeError 46 | } 47 | ) 48 | t.is( 49 | error.message, 50 | "Couldn't parse 'http://google.com/track/5nTtCOCds6I0PHMNtqelas' as valid URL" 51 | ) 52 | }) 53 | 54 | test('get preview for spotify track', async t => { 55 | const url = 'https://open.spotify.com/track/5nTtCOCds6I0PHMNtqelas' 56 | const preview = await getPreview(url) 57 | t.is(preview.title, 'Immaterial') 58 | t.is(preview.date, '2018-06-15T00:00:00.000Z') 59 | t.is(preview.description, undefined) 60 | t.is(preview.type, 'track') 61 | t.is(preview.artist, 'SOPHIE') 62 | t.is(preview.track, 'Immaterial') 63 | t.true(preview.image.includes('://')) 64 | t.true(preview.audio.includes('/mp3-preview/')) 65 | t.true(preview.link.includes('open.spotify.com/track/')) 66 | t.true(preview.embed.includes('https://embed.spotify.com/?uri=spotify:track')) 67 | }) 68 | 69 | test('get preview for spotify artist', async t => { 70 | const url = 'https://open.spotify.com/artist/5a2w2tgpLwv26BYJf2qYwu' 71 | const preview = await getPreview(url) 72 | t.is(preview.date, undefined) 73 | t.is(preview.description, 'Top tracks') 74 | t.is(preview.track, 'Ponyboy') 75 | t.is(preview.title, 'SOPHIE') 76 | t.is(preview.type, 'artist') 77 | 78 | t.is( 79 | preview.embed, 80 | 'https://embed.spotify.com/?uri=spotify:artist:5a2w2tgpLwv26BYJf2qYwu' 81 | ) 82 | 83 | t.true(preview.artist.includes('SOPHIE')) 84 | t.true(preview.image.includes('://')) 85 | t.true(preview.audio.includes('/mp3-preview/')) 86 | t.true(preview.link.includes('open.spotify.com/artist/')) 87 | }) 88 | 89 | test('get preview for spotify album', async t => { 90 | const url = 'https://open.spotify.com/album/7vQKfsKKrI0xObMqojazHR' 91 | const preview = await getPreview(url) 92 | t.is(preview.description, 'SOPHIE') 93 | t.is(preview.title, "OIL OF EVERY PEARL'S UN-INSIDES NON-STOP REMIX ALBUM") 94 | t.is(preview.type, 'album') 95 | t.is(preview.artist.includes('SOPHIE'), true) 96 | t.is(preview.track, 'Cold World') 97 | t.is(preview.image.includes('://'), true) 98 | t.true(preview.audio.includes('/mp3-preview/')) 99 | t.is(preview.link.includes('open.spotify.com/album/'), true) 100 | t.is( 101 | preview.embed, 102 | 'https://embed.spotify.com/?uri=spotify:album:7vQKfsKKrI0xObMqojazHR' 103 | ) 104 | }) 105 | 106 | test('get preview for spotify playlist', async t => { 107 | const url = 'https://open.spotify.com/playlist/3Q4cPwMHY95ZHXtmcU2xvH' 108 | const preview = await getPreview(url) 109 | t.is(preview.date, undefined) 110 | t.is(preview.description, 'sophiemsmsmsm') 111 | t.is(preview.title, 'SOPHIE – PRODUCT') 112 | t.is(preview.type, 'playlist') 113 | t.is(preview.artist.includes('SOPHIE'), true) 114 | t.is(preview.track, 'BIPP') 115 | t.is(preview.image.includes('://'), true) 116 | t.is(preview.audio.includes('/mp3-preview/'), true) 117 | t.is(preview.link.includes('/playlist/'), true) 118 | t.is( 119 | preview.embed, 120 | 'https://embed.spotify.com/?uri=spotify:playlist:3Q4cPwMHY95ZHXtmcU2xvH' 121 | ) 122 | }) 123 | 124 | test('get preview for spotify episode', async t => { 125 | const url = 'http://open.spotify.com/episode/64TORH3xleuD1wcnFsrH1E' 126 | const preview = await getPreview(url) 127 | t.is(preview.title, 'Hasty Treat - Modules in Node') 128 | t.is(preview.description, 'Syntax - Tasty Web Development Treats') 129 | t.is(preview.type, 'episode') 130 | t.is(preview.artist, 'Syntax - Tasty Web Development Treats') 131 | t.is(preview.track, 'Hasty Treat - Modules in Node') 132 | t.is(preview.date, '2020-01-06T14:00:00.000Z') 133 | t.is(preview.image.includes('://'), true) 134 | t.is(preview.audio.includes('.spotifycdn.'), true) 135 | t.is(preview.link.includes('/episode/'), true) 136 | t.is( 137 | preview.embed, 138 | 'https://embed.spotify.com/?uri=spotify:episode:64TORH3xleuD1wcnFsrH1E' 139 | ) 140 | }) 141 | 142 | test('get preview for spotify playlist with episode inside', async t => { 143 | const url = 'https://open.spotify.com/playlist/26q1NUbChiQDqjwO4SDdRD' 144 | const preview = await getPreview(url) 145 | t.is(preview.date, undefined) 146 | t.is(preview.title, 'spotify-url-with-episode') 147 | t.is(preview.type, 'playlist') 148 | t.is(preview.artist, 'Droids And Druids') 149 | t.is(preview.track, '4x01: Barbieland & Matrix') 150 | t.is(preview.image.includes('://'), true) 151 | t.is(preview.audio.includes('.spotifycdn.'), true) 152 | t.is(preview.link.includes('open.spotify.com/playlist/'), true) 153 | t.is( 154 | preview.embed, 155 | 'https://embed.spotify.com/?uri=spotify:playlist:26q1NUbChiQDqjwO4SDdRD' 156 | ) 157 | }) 158 | 159 | test('get preview for spotify collaborative playlist', async t => { 160 | const url = 'https://open.spotify.com/playlist/29n3VgifrVF9ZxFV9B6yRA' 161 | const preview = await getPreview(url) 162 | t.is(preview.date, undefined) 163 | t.is(preview.title, '🤘 ROCK') 164 | t.is(preview.type, 'playlist') 165 | t.is(preview.artist, 'Metalocalypse: Dethklok') 166 | t.is(preview.track, 'Awaken') 167 | t.is(preview.image.includes('://'), true) 168 | t.is(preview.audio.includes('/mp3-preview/'), true) 169 | t.is(preview.link.includes('open.spotify.com/playlist/'), true) 170 | t.is( 171 | preview.embed, 172 | 'https://embed.spotify.com/?uri=spotify:playlist:29n3VgifrVF9ZxFV9B6yRA' 173 | ) 174 | }) 175 | 176 | test('get preview for spotify album with constructed uri', async t => { 177 | const preview = await getPreview('spotify:album:7vQKfsKKrI0xObMqojazHR') 178 | 179 | t.is(preview.title, "OIL OF EVERY PEARL'S UN-INSIDES NON-STOP REMIX ALBUM") 180 | t.is(preview.type, 'album') 181 | t.is(preview.artist.includes('SOPHIE'), true) 182 | t.is(preview.track, 'Cold World') 183 | t.is(preview.image.includes('://'), true) 184 | t.is(preview.audio.includes('/mp3-preview/'), true) 185 | t.is(preview.link.includes('open.spotify.com/album/'), true) 186 | t.is( 187 | preview.embed, 188 | 'https://embed.spotify.com/?uri=spotify:album:7vQKfsKKrI0xObMqojazHR' 189 | ) 190 | }) 191 | 192 | test('get preview for spotify album with play url', async t => { 193 | const url = 'https://play.spotify.com/album/4tDBsfbHRJ9OdcMO9bmnai' 194 | const preview = await getPreview(url) 195 | t.is(preview.title, 'PRODUCT') 196 | t.is(preview.type, 'album') 197 | t.is(preview.artist.includes('SOPHIE'), true) 198 | t.is(preview.track, 'BIPP') 199 | t.is(preview.image.includes('://'), true) 200 | t.is(preview.audio, undefined) 201 | t.is(preview.link.includes('open.spotify.com/album/'), true) 202 | t.is( 203 | preview.embed, 204 | 'https://embed.spotify.com/?uri=spotify:album:4tDBsfbHRJ9OdcMO9bmnai' 205 | ) 206 | }) 207 | 208 | test('get preview for spotify Album with twitter embed url', async t => { 209 | const url = 210 | 'https://open.spotify.com/embed/album/4tDBsfbHRJ9OdcMO9bmnai?utm_campaign=twitter-player&utm_source=open&utm_medium=twitter' 211 | const preview = await getPreview(url) 212 | t.is(preview.title, 'PRODUCT') 213 | t.is(preview.type, 'album') 214 | t.is(preview.artist.includes('SOPHIE'), true) 215 | t.is(preview.track, 'BIPP') 216 | t.is(preview.image.includes('://'), true) 217 | t.is(preview.audio, undefined) 218 | t.is(preview.link.includes('open.spotify.com/album/'), true) 219 | t.is( 220 | preview.embed, 221 | 'https://embed.spotify.com/?uri=spotify:album:4tDBsfbHRJ9OdcMO9bmnai' 222 | ) 223 | }) 224 | 225 | test('get preview for spotify Track with constructed embed url', async t => { 226 | const url = 227 | 'https://embed.spotify.com/?uri=spotify:album:4tDBsfbHRJ9OdcMO9bmnai' 228 | const preview = await getPreview(url) 229 | t.is(preview.title, 'PRODUCT') 230 | t.is(preview.type, 'album') 231 | t.is(preview.artist.includes('SOPHIE'), true) 232 | t.is(preview.track, 'BIPP') 233 | t.is(preview.image.includes('://'), true) 234 | t.is(preview.audio, undefined) 235 | t.is(preview.link.includes('open.spotify.com/album/'), true) 236 | t.is( 237 | preview.embed, 238 | 'https://embed.spotify.com/?uri=spotify:album:4tDBsfbHRJ9OdcMO9bmnai' 239 | ) 240 | }) 241 | 242 | test('list multiple artists as one', async t => { 243 | const url = 'https://open.spotify.com/track/5ddFjrPG8NgQQ6xlOQIVd2' 244 | const preview = await getPreview(url) 245 | t.is(preview.artist, 'C. Tangana, Niño de Elche & La Húngara') 246 | }) 247 | 248 | test('get preview for spotify track with no cover', async t => { 249 | const url = 'https://open.spotify.com/track/4AjDdThsTlHF90gZTYVZzR' 250 | const preview = await getPreview(url) 251 | t.true(preview.image.includes('://')) 252 | }) 253 | -------------------------------------------------------------------------------- /test/fixtures/base64.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Spotify Embed 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 | 32 | 70 |
71 |
72 |
73 |
74 | 97 | 107 | 108 |
111 |
112 |
113 |
114 |
115 |
116 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /test/fixtures/nextjs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Spotify | Immaterial 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 302 | 303 | 304 | 305 |
306 | 340 |
341 |
342 |
343 |
344 |
345 | 346 |
347 |
348 |

Immaterial

349 |
    PreviewE
350 |

SOPHIE

351 |
352 |
355 |
356 |
357 |
358 |
359 |
    PreviewE
360 |

361 | 417 |
418 | Immaterial·SOPHIE 424 |
425 |

426 |
427 |
    PreviewE
428 |
431 |
432 |
433 |
434 |
435 | 436 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | --------------------------------------------------------------------------------