├── .gitignore ├── .nvmrc ├── .prettierrc ├── .travis.yml ├── README.md ├── package.json ├── src ├── artist-list.ts ├── gatsby-node.ts ├── index.ts ├── spotify-api.ts ├── token-tool.ts └── types │ ├── spotify-playlists.ts │ ├── spotify-recent.ts │ ├── spotify-token.ts │ ├── spotify-top-artists.ts │ └── spotify-top-tracks.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /types/ 2 | /*.js 3 | /*.d.ts 4 | yarn-error.log 5 | build/ 6 | node_modules/ 7 | lib/ 8 | service-account.json -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 11 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: yarn 3 | 4 | install: 5 | - yarn 6 | 7 | script: 8 | - yarn lint 9 | - yarn build 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gatsby-source-spotify 2 | 3 | [![npm](https://img.shields.io/npm/v/gatsby-source-spotify.svg)](https://www.npmjs.com/package/gatsby-source-spotify) [![Build Status](https://travis-ci.org/leolabs/gatsby-source-spotify.svg?branch=master)](https://travis-ci.org/leolabs/gatsby-source-spotify) 4 | 5 | This source plugin for Gatsby fetches personal statistics and playlists from 6 | [Spotify](https://spotify.com). You can use this to display a list of your 7 | favorite artists and tracks, or your public playlists. 8 | 9 | gatsby-source-spotify is compatible with [gatsby-image](https://www.gatsbyjs.org/packages/gatsby-image/). 10 | Images are always accessible using the `image` key of a node. Downloaded images are 11 | cached locally to improve build times. 12 | 13 | ## Install 14 | 15 | ```shell 16 | $ npm install gatsby-source-spotify 17 | ``` 18 | 19 | ## Configuration 20 | 21 | To use this plugin, you have to provide a client id, a client secret, 22 | and a personal refresh token from Spotify. To do this, first 23 | [create a new Spotify App](https://developer.spotify.com/dashboard/applications). 24 | 25 | After you created it, click the "Edit Settings" button on the application dashboard, add `http://localhost:5071/spotify` to the "Redirect URIs" section and hit save. 26 | 27 | You can then run gatsby-source-spotify's integrated tool to log in using your 28 | Spotify account and to get your refresh token. 29 | 30 | ```shell 31 | $ npx gatsby-source-spotify token 32 | ``` 33 | 34 | **"Illegal Scope" error** 35 | If you get an "Illegal Scope" error from Spotify, you may need to delete your Spotify app and create a new one, see issue [#5](https://github.com/leolabs/gatsby-source-spotify/issues/5#issuecomment-503015275). 36 | 37 | Put those credentials into your `gatsby-config.js` and you're good to go 🎉 38 | 39 | ```javascript 40 | { 41 | resolve: `gatsby-source-spotify`, 42 | options: { 43 | clientId: ``, 44 | clientSecret: ``, 45 | refreshToken: ``, 46 | 47 | fetchPlaylists: true, // optional. Set to false to disable fetching of your playlists 48 | fetchRecent: true, // optional. Set to false to disable fetching of your recently played tracks 49 | timeRanges: ['short_term', 'medium_term', 'long_term'], // optional. Set time ranges to be fetched 50 | }, 51 | }, 52 | ``` 53 | 54 | ## Time Ranges 55 | 56 | According to Spotify, the time ranges are specified as follows: 57 | 58 | - `short_term`: Data from the last four weeks 59 | - `medium_term`: Data from the last six months 60 | - `long_term`: All data since the account's creation 61 | 62 | ## Querying Data 63 | 64 | For your top artists and tracks, I'd recommend filtering by one `time_range` and 65 | sorting by `order`. This ensures that you get the correct results. 66 | 67 | Example for your top artists with images and genres: 68 | 69 | ```graphql 70 | { 71 | allSpotifyTopArtist( 72 | filter: { time_range: { eq: "medium_term" } } 73 | sort: { fields: order } 74 | ) { 75 | edges { 76 | node { 77 | name 78 | genres 79 | image { 80 | localFile { 81 | childImageSharp { 82 | fluid(maxWidth: 400) { 83 | ...GatsbyImageSharpFluid_withWebp 84 | } 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | ``` 93 | 94 | ## Contributing 95 | 96 | If you're interested in contributing, please feel free to open a pull request. 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-source-spotify", 3 | "version": "2.0.0", 4 | "description": "Gatsby source plugin for using data from Spotify on your website", 5 | "main": "index.js", 6 | "bin": "token-tool.js", 7 | "author": "Leo Bernard ", 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/leolabs/gatsby-source-spotify.git" 12 | }, 13 | "homepage": "https://github.com/leolabs/gatsby-source-spotify", 14 | "keywords": [ 15 | "gatsby", 16 | "gatsby-plugin", 17 | "gatsby-source-plugin", 18 | "spotify" 19 | ], 20 | "scripts": { 21 | "prepublishOnly": "yarn build", 22 | "build": "tsc", 23 | "format": "prettier --write 'src/**/*", 24 | "lint": "tslint --project tsconfig.json" 25 | }, 26 | "files": [ 27 | "*.js", 28 | "*.d.ts" 29 | ], 30 | "publishConfig": { 31 | "access": "public" 32 | }, 33 | "dependencies": { 34 | "@types/open": "^6.2.1", 35 | "commander": "^2.20.0", 36 | "gatsby-node-helpers": "^1.2.1", 37 | "gatsby-source-filesystem": "^4.0.0", 38 | "inquirer": "^6.5.0", 39 | "node-fetch": "^2.6.0", 40 | "open": "^6.4.0" 41 | }, 42 | "devDependencies": { 43 | "@types/inquirer": "^6.5.0", 44 | "@types/node": "^12.6.9", 45 | "@types/node-fetch": "^2.5.0", 46 | "@types/prettier": "^1.18.1", 47 | "@types/react-dom": "^17.0.11", 48 | "gatsby": "^4.0.0", 49 | "prettier": "^1.18.2", 50 | "tslint": "^5.18.0", 51 | "tslint-config-prettier": "^1.18.0", 52 | "tslint-plugin-prettier": "^2.0.1", 53 | "typescript": "^4.0.0" 54 | }, 55 | "peerDependencies": { 56 | "gatsby": "^4.0.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/artist-list.ts: -------------------------------------------------------------------------------- 1 | import { Artist as RecentArtist } from './types/spotify-recent'; 2 | import { Artist } from './types/spotify-top-tracks'; 3 | 4 | export const generateArtistString = (artists: Artist[] | RecentArtist[]) => { 5 | if (artists.length === 1) { 6 | return artists[0].name; 7 | } 8 | 9 | const additionalArtists = 10 | artists 11 | .slice(1, artists.length > 2 ? -1 : undefined) 12 | .map(a => a.name) 13 | .join(', ') + 14 | (artists.length > 2 ? ` and ${artists[artists.length - 1].name}` : ''); 15 | 16 | return `${artists[0].name} feat. ${additionalArtists}`; 17 | }; 18 | -------------------------------------------------------------------------------- /src/gatsby-node.ts: -------------------------------------------------------------------------------- 1 | import { createFileNodeFromBuffer } from 'gatsby-source-filesystem'; 2 | import { createNodeHelpers } from 'gatsby-node-helpers'; 3 | import fetch from 'node-fetch'; 4 | 5 | import { generateArtistString } from './artist-list'; 6 | import { getUserData, TimeRange } from './spotify-api'; 7 | 8 | export interface PluginOptions { 9 | // Auth 10 | clientId: string; 11 | clientSecret: string; 12 | refreshToken: string; 13 | 14 | // Config 15 | timeRanges?: TimeRange[]; 16 | fetchPlaylists?: boolean; 17 | fetchRecent?: boolean; 18 | } 19 | 20 | const referenceRemoteFile = async ( 21 | id: string, 22 | url: string, 23 | { cache, createNode, createNodeId, touchNode, store }, 24 | ) => { 25 | const cachedResult = await cache.get(url); 26 | 27 | if (cachedResult) { 28 | touchNode({ nodeId: cachedResult }); 29 | return { localFile___NODE: cachedResult }; 30 | } 31 | 32 | const testRes = await fetch(url); 33 | 34 | if (!testRes.ok) { 35 | console.warn(`[${id}] Image could not be loaded. Skipping...`); 36 | return null; 37 | } 38 | 39 | const fileNode = await createFileNodeFromBuffer({ 40 | buffer: await testRes.buffer(), 41 | store, 42 | cache, 43 | createNode, 44 | createNodeId, 45 | name: id.replace(/[^a-z0-9]+/gi, '-'), 46 | ext: '.jpg', 47 | }); 48 | 49 | if (fileNode) { 50 | cache.set(url, fileNode.id); 51 | return { localFile___NODE: fileNode.id }; 52 | } 53 | 54 | return null; 55 | }; 56 | 57 | export const sourceNodes = async ( 58 | { actions, createNodeId, store, cache, createContentDigest }, 59 | pluginOptions: PluginOptions, 60 | ) => { 61 | const { createNodeFactory } = createNodeHelpers({ 62 | typePrefix: 'Spotify', 63 | createContentDigest, 64 | createNodeId, 65 | }); 66 | 67 | const TopArtistNode = createNodeFactory('TopArtist'); 68 | const TopTrackNode = createNodeFactory('TopTrack'); 69 | const PlaylistNode = createNodeFactory('Playlist'); 70 | const RecentTrackNode = createNodeFactory('RecentTrack'); 71 | 72 | const { createNode, touchNode } = actions; 73 | const helpers = { cache, createNode, createNodeId, store, touchNode }; 74 | 75 | const { tracks, artists, playlists, recentTracks } = await getUserData( 76 | pluginOptions, 77 | ); 78 | 79 | await Promise.all([ 80 | ...tracks.map(async (track, index) => { 81 | createNode( 82 | TopTrackNode({ 83 | ...track, 84 | id: `${track.time_range}__${track.id}`, 85 | order: index, 86 | artistString: generateArtistString(track.artists), 87 | image: 88 | track.album && track.album.images && track.album.images.length 89 | ? await referenceRemoteFile( 90 | track.album.uri, 91 | track.album.images[0].url, 92 | helpers, 93 | ) 94 | : null, 95 | }), 96 | ); 97 | }), 98 | ...artists.map(async (artist, index) => { 99 | createNode( 100 | TopArtistNode({ 101 | ...artist, 102 | id: `${artist.time_range}__${artist.id}`, 103 | order: index, 104 | image: 105 | artist.images && artist.images.length 106 | ? await referenceRemoteFile( 107 | artist.uri, 108 | artist.images[0].url, 109 | helpers, 110 | ) 111 | : null, 112 | }), 113 | ); 114 | }), 115 | ...playlists.map(async (playlist, index) => { 116 | createNode( 117 | PlaylistNode({ 118 | ...playlist, 119 | order: index, 120 | image: 121 | playlist.images && playlist.images.length 122 | ? await referenceRemoteFile( 123 | playlist.uri, 124 | playlist.images[0].url, 125 | helpers, 126 | ) 127 | : null, 128 | }), 129 | ); 130 | }), 131 | ...recentTracks.map(async (track, index) => { 132 | createNode( 133 | RecentTrackNode({ 134 | ...track, 135 | id: String(track.played_at), 136 | order: index, 137 | track: { 138 | ...track.track, 139 | artistString: generateArtistString(track.track.artists), 140 | image: 141 | track.track.album && 142 | track.track.album.images && 143 | track.track.album.images.length 144 | ? await referenceRemoteFile( 145 | track.track.uri, 146 | track.track.album.images[0].url, 147 | helpers, 148 | ) 149 | : null, 150 | }, 151 | }), 152 | ); 153 | }), 154 | ]); 155 | 156 | return; 157 | }; 158 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // no-op 2 | -------------------------------------------------------------------------------- /src/spotify-api.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | import { PluginOptions } from './gatsby-node'; 4 | import { PlaylistsResponse } from './types/spotify-playlists'; 5 | import { RecentResponse } from './types/spotify-recent'; 6 | import { TokenResponse } from './types/spotify-token'; 7 | import { Artist, TopArtistsResponse } from './types/spotify-top-artists'; 8 | import { TopTracksResponse, Track } from './types/spotify-top-tracks'; 9 | 10 | export type Scope = 11 | | 'playlist-read-private' 12 | | 'user-modify-playback-state' 13 | | 'user-top-read' 14 | | 'user-read-recently-played' 15 | | 'user-read-currently-playing' 16 | | 'playlist-modify-private' 17 | | 'app-remote-control' 18 | | 'playlist-modify-public' 19 | | 'user-read-birthdate' 20 | | 'user-read-playback-state' 21 | | 'user-follow-read' 22 | | 'user-read-email' 23 | | 'streaming' 24 | | 'playlist-read-collaborative' 25 | | 'user-library-modify' 26 | | 'user-read-private' 27 | | 'user-follow-modify' 28 | | 'user-library-read'; 29 | 30 | export type TimeRange = 'long_term' | 'medium_term' | 'short_term'; 31 | 32 | export const SPOTIFY_ACCOUNT_URL = 'https://accounts.spotify.com'; 33 | export const SPOTIFY_API_URL = 'https://api.spotify.com/v1'; 34 | export const REDIRECT_URL = 'http://localhost:5071/spotify'; 35 | 36 | export const generateAuthUrl = ( 37 | clientId: string, 38 | scopes: Scope[] = ['user-top-read', 'user-read-recently-played'], 39 | ) => { 40 | const base = new URL(`${SPOTIFY_ACCOUNT_URL}/authorize`); 41 | base.searchParams.append('response_type', 'code'); 42 | base.searchParams.append('redirect_uri', REDIRECT_URL); 43 | base.searchParams.append('client_id', clientId); 44 | base.searchParams.append('scope', scopes.join(' ')); 45 | return String(base); 46 | }; 47 | 48 | export const getTokens = async ( 49 | clientId: string, 50 | clientSecret: string, 51 | code: string, 52 | grantType: 'authorization_code' | 'refresh_token', 53 | ) => { 54 | const body = new URLSearchParams(); 55 | 56 | body.append('grant_type', grantType); 57 | body.append('redirect_uri', REDIRECT_URL); 58 | body.append(grantType === 'refresh_token' ? 'refresh_token' : 'code', code); 59 | body.append('client_id', clientId); 60 | body.append('client_secret', clientSecret); 61 | 62 | const response = await fetch(`${SPOTIFY_ACCOUNT_URL}/api/token`, { 63 | method: 'POST', 64 | body: body as any, // Typing seems to be off here 65 | }); 66 | 67 | if (!response.ok) { 68 | throw new Error(`${response.statusText}: ${await response.text()}`); 69 | } 70 | 71 | return (await response.json()) as TokenResponse; 72 | }; 73 | 74 | const getTop = async ( 75 | accessToken: string, 76 | type: 'artists' | 'tracks', 77 | timeRange: TimeRange = 'medium_term', 78 | limit: number = 20, 79 | ) => { 80 | const url = new URL(`${SPOTIFY_API_URL}/me/top/${type}`); 81 | url.searchParams.append('time_range', timeRange); 82 | url.searchParams.append('limit', String(Math.min(limit, 50))); 83 | 84 | const response = await fetch(String(url), { 85 | headers: { 86 | Authorization: `Bearer ${accessToken}`, 87 | }, 88 | }); 89 | 90 | if (!response.ok) { 91 | throw new Error( 92 | `[${url} / ${accessToken}] ${ 93 | response.statusText 94 | }: ${await response.text()}`, 95 | ); 96 | } 97 | 98 | const result: TopArtistsResponse | TopTracksResponse = await response.json(); 99 | return result.items; 100 | }; 101 | 102 | export const getPlaylists = async (accessToken: string, limit: number = 50) => { 103 | const url = new URL(`${SPOTIFY_API_URL}/me/playlists`); 104 | url.searchParams.append('limit', String(Math.min(limit, 50))); 105 | 106 | const response = await fetch(String(url), { 107 | headers: { 108 | Authorization: `Bearer ${accessToken}`, 109 | }, 110 | }); 111 | 112 | if (!response.ok) { 113 | throw new Error(`${response.statusText}: ${await response.text()}`); 114 | } 115 | 116 | const result: PlaylistsResponse = await response.json(); 117 | return result.items; 118 | }; 119 | 120 | export const getRecentTracks = async ( 121 | accessToken: string, 122 | limit: number = 50, 123 | ) => { 124 | const url = new URL(`${SPOTIFY_API_URL}/me/player/recently-played`); 125 | url.searchParams.append('limit', String(Math.min(limit, 50))); 126 | 127 | const response = await fetch(String(url), { 128 | headers: { 129 | Authorization: `Bearer ${accessToken}`, 130 | }, 131 | }); 132 | 133 | if (!response.ok) { 134 | throw new Error(`${response.statusText}: ${await response.text()}`); 135 | } 136 | 137 | const result: RecentResponse = await response.json(); 138 | return result.items; 139 | }; 140 | 141 | export const getUserData = async ({ 142 | clientId, 143 | clientSecret, 144 | refreshToken, 145 | timeRanges = ['short_term', 'medium_term', 'long_term'], 146 | fetchPlaylists = true, 147 | fetchRecent = true, 148 | }: PluginOptions) => { 149 | const { access_token } = await getTokens( 150 | clientId, 151 | clientSecret, 152 | refreshToken, 153 | 'refresh_token', 154 | ); 155 | 156 | const playlists = fetchPlaylists ? await getPlaylists(access_token) : []; 157 | const recentTracks = fetchRecent ? await getRecentTracks(access_token) : []; 158 | 159 | const artists = await Promise.all( 160 | timeRanges.map(async t => { 161 | const artists = (await getTop(access_token, 'artists', t)) as Artist[]; 162 | return artists.map(artist => ({ ...artist, time_range: t })); 163 | }), 164 | ); 165 | 166 | const tracks = await Promise.all( 167 | timeRanges.map(async t => { 168 | const tracks = (await getTop(access_token, 'tracks', t)) as Track[]; 169 | return tracks.map(track => ({ ...track, time_range: t })); 170 | }), 171 | ); 172 | 173 | return { 174 | playlists, 175 | recentTracks, 176 | artists: [].concat(...artists) as (Artist & { time_range: TimeRange })[], 177 | tracks: [].concat(...tracks) as (Track & { time_range: TimeRange })[], 178 | }; 179 | }; 180 | -------------------------------------------------------------------------------- /src/token-tool.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import program from 'commander'; 4 | import http from 'http'; 5 | import open from 'open'; 6 | 7 | program.description('Spotify Refresh Token Tool'); 8 | 9 | import { generateAuthUrl, getTokens } from './spotify-api'; 10 | 11 | program 12 | .command('token ') 13 | .alias('t') 14 | .description('Start Spotify OAuth Flow') 15 | .action((clientId, clientSecret) => { 16 | console.log('Starting HTTP server to receive OAuth data from Spotify...'); 17 | 18 | http 19 | .createServer(async (req, res) => { 20 | const url = new URL(`http://localhost${req.url}`); 21 | const code = url.searchParams.get('code'); 22 | 23 | if (!code) { 24 | return; 25 | } 26 | 27 | console.log('Got the code. Getting the refresh token now...'); 28 | 29 | const tokens = await getTokens( 30 | clientId, 31 | clientSecret, 32 | code, 33 | 'authorization_code', 34 | ); 35 | 36 | console.log(`Here's your refresh token:`); 37 | console.log(tokens.refresh_token); 38 | 39 | res.write(`Your refresh token is:\n${tokens.refresh_token}`); 40 | res.end(); 41 | setTimeout(() => process.exit(0), 1000); 42 | }) 43 | .listen(5071); 44 | 45 | const authUrl = generateAuthUrl(clientId); 46 | console.log( 47 | 'I will open a browser window for you.', 48 | 'Please log in using your Spotify credentials.', 49 | ); 50 | console.log(); 51 | console.log("In case your browser doesn't open, here's the link:", authUrl); 52 | 53 | try { 54 | open(authUrl); 55 | } catch (e) {} 56 | }); 57 | 58 | program.parse(process.argv); 59 | -------------------------------------------------------------------------------- /src/types/spotify-playlists.ts: -------------------------------------------------------------------------------- 1 | export interface PlaylistsResponse { 2 | href: string; 3 | items: Playlist[]; 4 | limit: number; 5 | next: string; 6 | offset: number; 7 | previous: null; 8 | total: number; 9 | } 10 | 11 | export interface Playlist { 12 | collaborative: boolean; 13 | external_urls: ExternalUrls; 14 | href: string; 15 | id: string; 16 | images: Image[]; 17 | name: string; 18 | owner: Owner; 19 | primary_color: null; 20 | public: boolean; 21 | snapshot_id: string; 22 | tracks: Tracks; 23 | type: 'playlist'; 24 | uri: string; 25 | } 26 | 27 | export interface ExternalUrls { 28 | spotify: string; 29 | } 30 | 31 | export interface Image { 32 | height: number | null; 33 | url: string; 34 | width: number | null; 35 | } 36 | 37 | export interface Owner { 38 | display_name: string; 39 | external_urls: ExternalUrls; 40 | href: string; 41 | id: string; 42 | type: 'user'; 43 | uri: string; 44 | } 45 | 46 | export interface Tracks { 47 | href: string; 48 | total: number; 49 | } 50 | -------------------------------------------------------------------------------- /src/types/spotify-recent.ts: -------------------------------------------------------------------------------- 1 | export interface RecentResponse { 2 | items: Item[]; 3 | next: string; 4 | cursors: Cursors; 5 | limit: number; 6 | href: string; 7 | } 8 | 9 | export interface Cursors { 10 | after: string; 11 | before: string; 12 | } 13 | 14 | export interface Item { 15 | track: Track; 16 | played_at: Date; 17 | context: Context | null; 18 | } 19 | 20 | export interface Context { 21 | uri: string; 22 | external_urls: ExternalUrls; 23 | href: string; 24 | type: AlbumTypeEnum; 25 | } 26 | 27 | export interface ExternalUrls { 28 | spotify: string; 29 | } 30 | 31 | export enum AlbumTypeEnum { 32 | Album = 'album', 33 | Compilation = 'compilation', 34 | Single = 'single', 35 | } 36 | 37 | export interface Track { 38 | album: Album; 39 | artists: Artist[]; 40 | available_markets: string[]; 41 | disc_number: number; 42 | duration_ms: number; 43 | explicit: boolean; 44 | external_ids: ExternalIDS; 45 | external_urls: ExternalUrls; 46 | href: string; 47 | id: string; 48 | is_local: boolean; 49 | name: string; 50 | popularity: number; 51 | preview_url: string; 52 | track_number: number; 53 | type: 'track'; 54 | uri: string; 55 | } 56 | 57 | export interface Album { 58 | album_type: AlbumTypeEnum; 59 | artists: Artist[]; 60 | available_markets: string[]; 61 | external_urls: ExternalUrls; 62 | href: string; 63 | id: string; 64 | images: Image[]; 65 | name: string; 66 | release_date: Date; 67 | release_date_precision: string; 68 | total_tracks: number; 69 | type: AlbumTypeEnum; 70 | uri: string; 71 | } 72 | 73 | export interface Artist { 74 | external_urls: ExternalUrls; 75 | href: string; 76 | id: string; 77 | name: string; 78 | type: 'artist'; 79 | uri: string; 80 | } 81 | 82 | export interface Image { 83 | height: number; 84 | url: string; 85 | width: number; 86 | } 87 | 88 | export interface ExternalIDS { 89 | isrc: string; 90 | } 91 | -------------------------------------------------------------------------------- /src/types/spotify-token.ts: -------------------------------------------------------------------------------- 1 | export interface TokenResponse { 2 | access_token: string; 3 | token_type: string; 4 | scope: string; 5 | expires_in: number; 6 | refresh_token: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/types/spotify-top-artists.ts: -------------------------------------------------------------------------------- 1 | export interface TopArtistsResponse { 2 | items: Artist[]; 3 | total: number; 4 | limit: number; 5 | offset: number; 6 | previous: null; 7 | href: string; 8 | next: string; 9 | } 10 | 11 | export interface Artist { 12 | external_urls: ExternalUrls; 13 | followers: Followers; 14 | genres: string[]; 15 | href: string; 16 | id: string; 17 | images: Image[]; 18 | name: string; 19 | popularity: number; 20 | type: Type; 21 | uri: string; 22 | } 23 | 24 | export interface ExternalUrls { 25 | spotify: string; 26 | } 27 | 28 | export interface Followers { 29 | href: null; 30 | total: number; 31 | } 32 | 33 | export interface Image { 34 | height: number; 35 | url: string; 36 | width: number; 37 | } 38 | 39 | export enum Type { 40 | Artist = 'artist', 41 | } 42 | -------------------------------------------------------------------------------- /src/types/spotify-top-tracks.ts: -------------------------------------------------------------------------------- 1 | export interface TopTracksResponse { 2 | items: Track[]; 3 | total: number; 4 | limit: number; 5 | offset: number; 6 | previous: null; 7 | href: string; 8 | next: string; 9 | } 10 | 11 | export interface Track { 12 | album: Album; 13 | artists: Artist[]; 14 | available_markets: string[]; 15 | disc_number: number; 16 | duration_ms: number; 17 | explicit: boolean; 18 | external_ids: ExternalIDS; 19 | external_urls: ExternalUrls; 20 | href: string; 21 | id: string; 22 | is_local: boolean; 23 | name: string; 24 | popularity: number; 25 | preview_url: null | string; 26 | track_number: number; 27 | type: ItemType; 28 | uri: string; 29 | } 30 | 31 | export interface Album { 32 | album_type: AlbumType; 33 | artists: Artist[]; 34 | available_markets: string[]; 35 | external_urls: ExternalUrls; 36 | href: string; 37 | id: string; 38 | images: Image[]; 39 | name: string; 40 | release_date: string; 41 | release_date_precision: ReleaseDatePrecision; 42 | total_tracks: number; 43 | type: AlbumTypeEnum; 44 | uri: string; 45 | } 46 | 47 | export enum AlbumType { 48 | Album = 'ALBUM', 49 | Compilation = 'COMPILATION', 50 | Single = 'SINGLE', 51 | } 52 | 53 | export interface Artist { 54 | external_urls: ExternalUrls; 55 | href: string; 56 | id: string; 57 | name: string; 58 | type: ArtistType; 59 | uri: string; 60 | } 61 | 62 | export interface ExternalUrls { 63 | spotify: string; 64 | } 65 | 66 | export enum ArtistType { 67 | Artist = 'artist', 68 | } 69 | 70 | export interface Image { 71 | height: number; 72 | url: string; 73 | width: number; 74 | } 75 | 76 | export enum ReleaseDatePrecision { 77 | Day = 'day', 78 | Year = 'year', 79 | } 80 | 81 | export enum AlbumTypeEnum { 82 | Album = 'album', 83 | } 84 | 85 | export interface ExternalIDS { 86 | isrc: string; 87 | } 88 | 89 | export enum ItemType { 90 | Track = 'track', 91 | } 92 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "outDir": "./" 8 | }, 9 | "files": [ 10 | "src/gatsby-node.ts", 11 | "src/index.ts", 12 | "src/token-tool.ts" 13 | ] 14 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:latest", "tslint-config-prettier"], 4 | "jsRules": {}, 5 | "rules": { 6 | "array-type": false, 7 | "no-unsafe-any": false, 8 | "curly": true, 9 | "interface-name": [true, "never-prefix"], 10 | "jsx-no-multiline-js": false, 11 | "max-classes-per-file": [true, 5, "exclude-class-expressions"], 12 | "member-access": [true, "no-public"], 13 | "no-console": false, 14 | "no-empty": false, 15 | "no-object-literal-type-assertion": false, 16 | "no-shadowed-variable": false, 17 | "no-submodule-imports": false, 18 | "no-unsafe-finally": false, 19 | "no-unused-expression": false, 20 | "object-literal-key-quotes": false, 21 | "object-literal-sort-keys": false, 22 | "ordered-imports": [ 23 | true, 24 | { 25 | "grouped-imports": true, 26 | "named-imports-order": "lowercase-first" 27 | } 28 | ], 29 | "prefer-template": [true, "allow-single-concat"], 30 | "radix": false, 31 | "space-before-function-paren": false, 32 | "prettier": true 33 | }, 34 | "rulesDirectory": ["tslint-plugin-prettier"] 35 | } 36 | --------------------------------------------------------------------------------