-
Notifications
You must be signed in to change notification settings - Fork 568
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #196 from semvis123/lastfm
Last.fm support
- Loading branch information
Showing
6 changed files
with
233 additions
and
6 deletions.
There are no files selected for viewing
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
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
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; |
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
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