const fetch = require('node-fetch'); const md5 = require('md5'); const open = require("open"); const { setOptions } = require('../../config/plugins'); const getSongInfo = require('../../providers/song-info'); const defaultConfig = require('../../config/defaults'); const cleanupArtistName = (config, artist) => { // removes the suffixes of the artist name for more recognition by last.fm const { suffixesToRemove } = config; if (suffixesToRemove === undefined) return artist; for (suffix of suffixesToRemove) { artist = artist.replace(suffix, ''); } return artist; } const createFormData = params => { // creates the body for in the post request const formData = new URLSearchParams(); for (key in params) { formData.append(key, params[key]); } return formData; } const createQueryString = (params, api_sig) => { // creates a querystring const queryData = []; params.api_sig = api_sig; for (key in params) { queryData.push(`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`); } return '?'+queryData.join('&'); } const createApiSig = (params, secret) => { // this function creates the api signature, see: https://www.last.fm/api/authspec const keys = []; for (key in params) { keys.push(key); } keys.sort(); let sig = ''; for (key of keys) { if (String(key) === 'format') continue sig += `${key}${params[key]}`; } sig += secret; sig = md5(sig); return sig; } const createToken = async ({ api_key, api_root, secret }) => { // creates and stores the auth token const data = { method: 'auth.gettoken', api_key: api_key, format: 'json' }; const api_sig = createApiSig(data, secret); let response = await fetch(`${api_root}${createQueryString(data, api_sig)}`); response = await response.json(); return response?.token; } const authenticate = async config => { // asks the user for authentication config.token = await createToken(config); setOptions('last-fm', config); open(`https://www.last.fm/api/auth/?api_key=${config.api_key}&token=${config.token}`); return config; } const getAndSetSessionKey = async config => { // get and store the session key const data = { api_key: config.api_key, format: 'json', method: 'auth.getsession', token: config.token, }; const api_sig = createApiSig(data, config.secret); let res = await fetch(`${config.api_root}${createQueryString(data, api_sig)}`); res = await res.json(); if (res.error) await authenticate(config); config.session_key = res?.session?.key; setOptions('last-fm', config); return config; } const postSongDataToAPI = async (songInfo, config, data) => { // this sends a post request to the api, and adds the common data if (!config.session_key) await getAndSetSessionKey(config); const postData = { track: songInfo.title, duration: songInfo.songDuration, artist: songInfo.artist, api_key: config.api_key, sk: config.session_key, format: 'json', ...data, }; postData.api_sig = createApiSig(postData, config.secret); fetch('https://ws.audioscrobbler.com/2.0/', {method: 'POST', body: createFormData(postData)}) .catch(res => { if (res.response.data.error == 9) { // session key is invalid, so remove it from the config and reauthenticate config.session_key = undefined; setOptions('last-fm', config); authenticate(config); } }); } const addScrobble = (songInfo, config) => { // this adds one scrobbled song to last.fm const data = { method: 'track.scrobble', timestamp: ~~((Date.now() - songInfo.elapsedSeconds) / 1000), }; postSongDataToAPI(songInfo, config, data); } const setNowPlaying = (songInfo, config) => { // this sets the now playing status in last.fm const data = { method: 'track.updateNowPlaying', }; postSongDataToAPI(songInfo, config, data); } // this will store the timeout that will trigger addScrobble let scrobbleTimer = undefined; const lastfm = async (win, config) => { const registerCallback = getSongInfo(win); if (!config.api_root || !config.suffixesToRemove) { // settings are not present, creating them with the default values config = defaultConfig.plugins['last-fm']; config.enabled = true; setOptions('last-fm', config); } if (!config.session_key) { // not authenticated config = await getAndSetSessionKey(config); } registerCallback( songInfo => { // set remove the old scrobble timer clearTimeout(scrobbleTimer); // make the artist name a bit cleaner songInfo.artist = cleanupArtistName(config, songInfo.artist); if (!songInfo.isPaused) { setNowPlaying(songInfo, config); // scrobble when the song is half way through, or has passed the 4 minute mark const scrobbleTime = Math.min(Math.ceil(songInfo.songDuration / 2), 4 * 60); if (scrobbleTime > songInfo.elapsedSeconds) { // scrobble still needs to happen const timeToWait = (scrobbleTime - songInfo.elapsedSeconds) * 1000; scrobbleTimer = setTimeout(addScrobble, timeToWait, songInfo, config); } } }); } module.exports = lastfm;