Last active
September 14, 2020 21:06
-
-
Save ZackBoe/1085bcf3126b1650adc2188cb5fb37a9 to your computer and use it in GitHub Desktop.
Spotify Recently Played scrobbler for Maloja
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const fetch = require('node-fetch'); | |
const qs = require('querystring'); | |
const low = require('lowdb') | |
const FileSync = require('lowdb/adapters/FileSync') | |
const db = low(new FileSync('tracks.json', { | |
serialize: (obj) => JSON.stringify(obj), | |
deserialize: (data) => JSON.parse(data) | |
})) | |
db.defaults({ tracks: [] }).write() | |
require('dotenv').config() | |
const { | |
SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, SPOTIFY_REFRESH_TOKEN, MALOJA_KEY, HEALTHCHECK, MALOJA_SUBMIT | |
} = process.env | |
const SPOTIFY_BASIC = Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString('base64') | |
const SPOTIFY_SCOPES = ['user-read-recently-played', 'user-read-currently-playing'] | |
let after = Date.now() - (60000*15) | |
async function getAuthToken(){ | |
token = await fetch('https://accounts.spotify.com/api/token', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/x-www-form-urlencoded', | |
'Authorization': `Basic ${SPOTIFY_BASIC}` | |
}, | |
body: qs.stringify({ | |
grant_type: 'refresh_token', | |
refresh_token: SPOTIFY_REFRESH_TOKEN | |
}), | |
}) | |
.then(res => { | |
if (res.status === 200) return res.json() | |
else throw new Error(res.status) | |
}) | |
.catch(err => { | |
throw new Error(err) | |
}) | |
return token.access_token | |
} | |
async function getCurrentUserName(){ | |
const currentUser = await fetch(`https://api.spotify.com/v1/me`, { | |
method: 'GET', | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': `Bearer ${await getAuthToken()}` | |
}, | |
}) | |
.then(res => { | |
if (res.status === 200) return res.json() | |
else throw new Error(res.status) | |
}) | |
.catch(err => { | |
throw new Error(err) | |
}) | |
return currentUser.display_name | |
} | |
async function getRecentlyPlayed(){ | |
const recentlyPlayed = await fetch(`https://api.spotify.com/v1/me/player/recently-played?limit=50${after ? `&after=${after.toString()}` : ''}`, { | |
method: 'GET', | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': `Bearer ${await getAuthToken()}` | |
}, | |
}) | |
.then(res => { | |
if (res.status === 200) return res.json() | |
else throw new Error(res.status) | |
}) | |
.catch(err => { | |
throw new Error(err) | |
}) | |
return recentlyPlayed | |
} | |
async function scrobbleTrack(track){ | |
// Maloja's native api wasn't accepting scrobbles properly, so using ListenBrainz API endpoint | |
const maloja = await fetch(MALOJA_SUBMIT, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
'Authorization': MALOJA_KEY | |
}, | |
body: JSON.stringify({ | |
'listen_type': 'single', | |
'payload': [{'track_metadata': {'artist_name': track.artists,'track_name': track.title}}] | |
}) | |
}) | |
.then(res => { | |
if (res.status === 200) return res.json() | |
else throw new Error(res.status) | |
}) | |
.catch(err => { | |
throw new Error(err) | |
}) | |
db.get('tracks').push({ id: track.id, artists: track.artists, title: track.title, played_at: track.played_at }).write() | |
console.log(`Scrobbled '${track.artists} - ${track.title}' to Maloja`) | |
} | |
(async () => { | |
const tracks = await getRecentlyPlayed() | |
if(tracks?.items?.length > 0) { | |
console.log(`Got ${tracks.items.length} tracks from Spotify user '${await getCurrentUserName()}' at ${Date.now()}`) | |
after = new Date(tracks.items[0].played_at).getTime() | |
tracksToScrobble = tracks.items.map(item => { | |
return { | |
id: item.track.id, | |
played_at: new Date(item.played_at).getTime(), | |
artists: [...item.track.artists.map(artist => artist.name)].join(';'), | |
title: item.track.name | |
} | |
}) | |
// Spotify likes to count a single play, skipped tracks, or paused tracks as multiple plays sometimes? | |
// Remove duplicates https://stackoverflow.com/a/56757215/1810897 | |
tracksToScrobble = tracksToScrobble.filter((v,i,a)=>a.findIndex(t=>(t.title === v.title && t.artists === v.artists))===i) | |
tracksToScrobble.forEach(async (track) => { | |
let alreadyScrobbled = await db.get('tracks').find({ id: track.id, played_at: track.played_at }).value() | |
if(!alreadyScrobbled) scrobbleTrack(track) | |
}) | |
} else after = Date.now() | |
fetch(HEALTHCHECK).catch(err => { throw new Error(err)}) | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment