From 4747050b60b2a6c35253123c8bcd106ed478c677 Mon Sep 17 00:00:00 2001 From: Sem Visscher Date: Fri, 19 Mar 2021 21:32:33 +0100 Subject: [PATCH] Added working lastfm support currently only played songs are added --- config/defaults.js | 6 ++ package.json | 3 + plugins/discord/back.js | 5 +- plugins/last-fm/back.js | 135 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 plugins/last-fm/back.js diff --git a/config/defaults.js b/config/defaults.js index 6238e114..b81d1d2e 100644 --- a/config/defaults.js +++ b/config/defaults.js @@ -35,6 +35,12 @@ const defaultConfig = { ffmpegArgs: [], // e.g. ["-b:a", "192k"] for an audio bitrate of 192kb/s downloadFolder: undefined, // Custom download folder (absolute path) }, + "last-fm": { + enabled: false, + api_root: "http://ws.audioscrobbler.com/2.0/", + api_key: "04d76faaac8726e60988e14c105d421a", // api key registered by @semvis123 + secret: "a5d2a36fdf64819290f6982481eaffa2", + } }, }; diff --git a/package.json b/package.json index f5524a37..df4725e1 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@ffmpeg/core": "^0.8.5", "@ffmpeg/ffmpeg": "^0.9.7", "YoutubeNonStop": "git://github.com/lawfx/YoutubeNonStop.git#v0.8.1", + "axios": "^0.21.1", "browser-id3-writer": "^4.4.0", "discord-rpc": "^3.2.0", "downloads-folder": "^3.0.1", @@ -76,7 +77,9 @@ "electron-unhandled": "^3.0.2", "electron-updater": "^4.3.6", "filenamify": "^4.2.0", + "md5": "^2.3.0", "node-fetch": "^2.6.1", + "open": "^8.0.3", "ytdl-core": "^4.4.5" }, "devDependencies": { diff --git a/plugins/discord/back.js b/plugins/discord/back.js index 5c53193b..1d7e8e21 100644 --- a/plugins/discord/back.js +++ b/plugins/discord/back.js @@ -45,9 +45,6 @@ module.exports = (win) => { }); // Startup the rpc client - rpc.login({ - clientId, - }) - .catch(console.error); + rpc.login({ clientId }).catch(console.error); }); }; diff --git a/plugins/last-fm/back.js b/plugins/last-fm/back.js new file mode 100644 index 00000000..9a88d4fb --- /dev/null +++ b/plugins/last-fm/back.js @@ -0,0 +1,135 @@ +const fetch = require('node-fetch'); +const md5 = require('md5'); +const open = require("open"); +const axios = require('axios'); +const { setOptions } = require('../../config/plugins'); +const getSongInfo = require('../../providers/song-info'); + +const createFormData = (params) => { + // creates the body for in the post request + let 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(`${key}=${params[key]}`) + } + return '?'+queryData.join('&') +} + +const createApiSig = (params, secret) => { + // this function creates the api signature, see: https://www.last.fm/api/authspec + let 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 an auth token + data = { + method: 'auth.gettoken', + api_key: api_key, + format: 'json' + } + let 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 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 set the session key + data = { + api_key: config.api_key, + format: 'json', + method: 'auth.getsession', + token: config.token, + } + api_sig = createApiSig(data, config.secret); + 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 addScrobble = async (songInfo, config) => { + // this adds one scrobbled song + if (!config.session_key) + await getAndSetSessionKey(config); + data = { + track: songInfo.title, + artist: songInfo.artist, + api_key: config.api_key, + sk: config.session_key, + format: 'json', + method: 'track.scrobble', + timestamp: ~~((Date.now() - songInfo.elapsedSeconds)/1000), + duration: songInfo.songDuration, + } + data.api_sig = createApiSig(data, config.secret) + axios.post('https://ws.audioscrobbler.com/2.0/', createFormData(data)) + .then(res => res.data.scrobbles) + .catch(res => { + if (res.response.data.error == 9){ + // session key is invalid + config.session_key = undefined; + setOptions('last-fm', config); + authenticate(config); + } + }); +} + +// this will store the timeout that will trigger addScrobble +let scrobbleTimer = undefined; + +const lastfm = async (win, config) => { + const registerCallback = getSongInfo(win); + + if (!config.session_key) { + // not authenticated + config = await getAndSetSessionKey(config); + } + + registerCallback((songInfo)=> { + clearTimeout(scrobbleTimer); + if (!songInfo.isPaused) { + let scrobbleTime = Math.min(Math.ceil(songInfo.songDuration/2), 4*60); + if (scrobbleTime > songInfo.elapsedSeconds) { + // scrobble still needs to happen + timeToWait = (scrobbleTime-songInfo.elapsedSeconds)*1000; + scrobbleTimer = setTimeout(addScrobble, timeToWait, songInfo, config); + } + } + }) +} + +module.exports = lastfm; \ No newline at end of file