LastFM Scrobbling

This commit is contained in:
vapormusic 2021-12-28 22:20:47 +07:00
parent df6ba93242
commit 130cfed2b4
7 changed files with 331 additions and 22 deletions

View file

@ -1,5 +1,7 @@
require('v8-compile-cache'); require('v8-compile-cache');
const { app } = require('electron'); const { app } = require('electron'),
{resolve} = require("path"),
CiderBase = require ('./src/main/cider-base');
// Analytics for debugging. // Analytics for debugging.
const ElectronSentry = require("@sentry/electron"); const ElectronSentry = require("@sentry/electron");
@ -33,9 +35,11 @@ const configSchema = {
"enable_yt": false, "enable_yt": false,
}, },
"lastfm": { "lastfm": {
"enabled": true, "enabled": false,
"scrobble_after": 30, "scrobble_after": 30,
"auth_token": "" "auth_token": "",
"enabledRemoveFeaturingArtists" : true,
"NowPlaying": "true"
} }
} }
@ -119,4 +123,60 @@ app.on('widevine-update-pending', (currentVersion, pendingVersion) => {
app.on('widevine-error', (error) => { app.on('widevine-error', (error) => {
console.log('[Cider][Widevine] Widevine installation encountered an error: ' + error) console.log('[Cider][Widevine] Widevine installation encountered an error: ' + error)
app.exit() app.exit()
}) })
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient('cider', process.execPath, [resolve(process.argv[1])])
app.setAsDefaultProtocolClient('ame', process.execPath, [resolve(process.argv[1])])
app.setAsDefaultProtocolClient('itms', process.execPath, [resolve(process.argv[1])])
app.setAsDefaultProtocolClient('itmss', process.execPath, [resolve(process.argv[1])])
app.setAsDefaultProtocolClient('musics', process.execPath, [resolve(process.argv[1])])
app.setAsDefaultProtocolClient('music', process.execPath, [resolve(process.argv[1])])
}
} else {
app.setAsDefaultProtocolClient('cider') // Custom AME Protocol
app.setAsDefaultProtocolClient('ame') // Custom AME Protocol
app.setAsDefaultProtocolClient('itms') // iTunes HTTP Protocol
app.setAsDefaultProtocolClient('itmss') // iTunes HTTPS Protocol
app.setAsDefaultProtocolClient('musics') // macOS Client Protocol
app.setAsDefaultProtocolClient('music') // macOS Client Protocol
}
app.on('open-url', (event, url) => {
event.preventDefault()
if (url.includes('ame://') || url.includes('itms://') || url.includes('itmss://') || url.includes('musics://') || url.includes('music://')) {
CiderBase.LinkHandler(url)
}
})
app.on('second-instance', (_e, argv) => {
console.warn(`[InstanceHandler][SecondInstanceHandler] Second Instance Started with args: [${argv.join(', ')}]`)
// Checks if first instance is authorized and if second instance has protocol args
argv.forEach((value) => {
if (value.includes('ame://') || value.includes('itms://') || value.includes('itmss://') || value.includes('musics://') || value.includes('music://')) {
console.warn(`[InstanceHandler][SecondInstanceHandler] Found Protocol!`)
CiderBase.LinkHandler(value);
}
})
if (argv.includes("--force-quit")) {
console.warn('[InstanceHandler][SecondInstanceHandler] Force Quit found. Quitting App.');
// app.isQuiting = true
app.quit()
} else if (CiderBase.win && true) { // If a Second Instance has Been Started
console.warn('[InstanceHandler][SecondInstanceHandler] Showing window.');
app.win.show()
app.win.focus()
}
})
if (!app.requestSingleInstanceLock() && true) {
console.warn("[InstanceHandler] Existing Instance is Blocking Second Instance.");
app.quit();
// app.isQuiting = true
}

View file

@ -31,6 +31,7 @@
"electron-window-state": "^5.0.3", "electron-window-state": "^5.0.3",
"express": "^4.17.2", "express": "^4.17.2",
"get-port": "^5.1.1", "get-port": "^5.1.1",
"lastfmapi": "^0.1.1",
"mpris-service": "^2.1.2", "mpris-service": "^2.1.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",

View file

@ -0,0 +1,4 @@
{
"key": "174905d201451602407b428a86e8344d",
"secret": "be61d4081f6adec150f0130939f854bb"
}

View file

@ -7,6 +7,7 @@ const windowStateKeeper = require("electron-window-state");
const os = require('os'); const os = require('os');
const yt = require('youtube-search-without-api-key'); const yt = require('youtube-search-without-api-key');
const discord = require('./discordrpc'); const discord = require('./discordrpc');
const lastfm = require('./lastfm');
const mpris = require('./mpris'); const mpris = require('./mpris');
// Analytics for debugging. // Analytics for debugging.
@ -14,9 +15,10 @@ const ElectronSentry = require("@sentry/electron");
ElectronSentry.init({dsn: "https://68c422bfaaf44dea880b86aad5a820d2@o954055.ingest.sentry.io/6112214"}); ElectronSentry.init({dsn: "https://68c422bfaaf44dea880b86aad5a820d2@o954055.ingest.sentry.io/6112214"});
const CiderBase = { const CiderBase = {
win: null,
async Start() { async Start() {
this.clientPort = await getPort({port: 9000}); this.clientPort = await getPort({port: 9000});
this.CreateBrowserWindow() this.win = this.CreateBrowserWindow()
}, },
clientPort: 0, clientPort: 0,
CreateBrowserWindow() { CreateBrowserWindow() {
@ -204,15 +206,22 @@ const CiderBase = {
mpris.connect(win) mpris.connect(win)
lastfm.authenticate()
// Discord // Discord
discord.connect((app.cfg.get("general.discord_rpc") == 1) ? '911790844204437504' : '886578863147192350'); discord.connect((app.cfg.get("general.discord_rpc") == 1) ? '911790844204437504' : '886578863147192350');
ipcMain.on('playbackStateDidChange', (_event, a) => { ipcMain.on('playbackStateDidChange', (_event, a) => {
app.media = a;
discord.updateActivity(a) discord.updateActivity(a)
mpris.updateState(a) mpris.updateState(a)
lastfm.scrobbleSong(a)
lastfm.updateNowPlayingSong(a)
}); });
ipcMain.on('nowPlayingItemDidChange', (_event, a) => { ipcMain.on('nowPlayingItemDidChange', (_event, a) => {
app.media = a;
discord.updateActivity(a) discord.updateActivity(a)
mpris.updateAttributes(a) mpris.updateAttributes(a)
lastfm.scrobbleSong(a)
lastfm.updateNowPlayingSong(a)
}); });
return win return win
@ -224,6 +233,31 @@ const CiderBase = {
dev: app.isPackaged dev: app.isPackaged
} }
}, },
LinkHandler: (startArgs) => {
if (!startArgs) return;
console.log("lfmtoken",String(startArgs))
if (String(startArgs).includes('auth')) {
let authURI = String(startArgs).split('/auth/')[1]
if (authURI.startsWith('lastfm')) { // If we wanted more auth options
const authKey = authURI.split('lastfm?token=')[1];
app.cfg.set('lastfm.enabled', true);
app.cfg.set('lastfm.auth_token', authKey);
CiderBase.win.webContents.send('LastfmAuthenticated', authKey);
lastfm.authenticate()
}
} else {
const formattedSongID = startArgs.replace('ame://', '').replace('/', '');
console.warn(`[LinkHandler] Attempting to load song id: ${formattedSongID}`);
// setQueue can be done with album, song, url, playlist id
this.win.webContents.executeJavaScript(`
MusicKit.getInstance().setQueue({ song: '${formattedSongID}'}).then(function(queue) {
MusicKit.getInstance().play();
});
`).catch((err) => console.error(err));
}
},
async InitWebServer() { async InitWebServer() {
const webapp = express(); const webapp = express();

153
src/main/lastfm.js Normal file
View file

@ -0,0 +1,153 @@
const {app, Notification} = require('electron'),
fs = require('fs'),
{resolve} = require('path'),
sessionPath = resolve(app.getPath('userData'), 'session.json'),
apiCredentials = require('../../resources/lfmApiCredentials.json'),
LastfmAPI = require('lastfmapi');
const lfm = {
authenticateFromFile: function () {
let sessionData = require(sessionPath)
console.log("[LastFM][authenticateFromFile] Logging in with Session Info.")
app.lastfm.setSessionCredentials(sessionData.name, sessionData.key)
console.log("[LastFM][authenticateFromFile] Logged in.")
},
authenticate: function () {
if (app.cfg.get('lastfm.auth_token')) {
app.cfg.set('lastfm.enabled', true);
}
if (!app.cfg.get('lastfm.enabled') || !app.cfg.get('lastfm.auth_token')) {
app.cfg.set('lastfm.enabled', false);
return
}
const lfmAPI = new LastfmAPI({
'api_key': apiCredentials.key,
'secret': apiCredentials.secret
});
app.lastfm = Object.assign(lfmAPI, {cachedAttributes: false, cachedNowPlayingAttributes: false});
fs.stat(sessionPath, function (err) {
if (err) {
console.error("[LastFM][Session] Session file couldn't be opened or doesn't exist,", err)
console.log("[LastFM][Auth] Beginning authentication from configuration")
app.lastfm.authenticate(app.cfg.get('lastfm.auth_token'), function (err, session) {
if (err) {
throw err;
}
console.log("[LastFM] Successfully obtained LastFM session info,", session); // {"name": "LASTFM_USERNAME", "key": "THE_USER_SESSION_KEY"}
console.log("[LastFM] Saving session info to disk.")
let tempData = JSON.stringify(session)
fs.writeFile(sessionPath, tempData, (err) => {
if (err)
console.log("[LastFM][fs]", err)
else {
console.log("[LastFM][fs] File was written successfully.")
lfm.authenticateFromFile()
new Notification({
title: app.getName(),
body: "Successfully logged into LastFM using Authentication Key."
}).show()
}
})
});
} else {
lfm.authenticateFromFile()
}
})
},
scrobbleSong: async function (attributes) {
await new Promise(resolve => setTimeout(resolve, app.cfg.get('lastfm.scrobble_after') * 1000));
const currentAttributes = app.media;
if (!app.lastfm || app.lastfm.cachedAttributes === attributes ) {
return
}
if (app.lastfm.cachedAttributes) {
if (app.lastfm.cachedAttributes.playParams.id === attributes.playParams.id) return;
}
if (currentAttributes.status && currentAttributes === attributes) {
if (fs.existsSync(sessionPath)) {
// Scrobble playing song.
if (attributes.status === true) {
app.lastfm.track.scrobble({
'artist': lfm.filterArtistName(attributes.artistName),
'track': attributes.name,
'album': attributes.albumName,
'albumArtist': this.filterArtistName(attributes.artistName),
'timestamp': new Date().getTime() / 1000
}, function (err, scrobbled) {
if (err) {
return console.error('[LastFM] An error occurred while scrobbling', err);
}
console.log('[LastFM] Successfully scrobbled: ', scrobbled);
});
app.lastfm.cachedAttributes = attributes
}
} else {
this.authenticate();
}
} else {
return console.log('[LastFM] Did not add ', attributes.name , '-' , lfm.filterArtistName(attributes.artistName), 'because now playing a other song.');
}
},
filterArtistName: function (artist) {
if (!app.cfg.get('lastfm.enabledRemoveFeaturingArtists')) return artist;
artist = artist.split(' ');
if (artist.includes('&')) {
artist.length = artist.indexOf('&');
}
if (artist.includes('and')) {
artist.length = artist.indexOf('and');
}
artist = artist.join(' ');
if (artist.includes(',')) {
artist = artist.split(',')
artist = artist[0]
}
return artist.charAt(0).toUpperCase() + artist.slice(1);
},
updateNowPlayingSong: function (attributes) {
if (!app.lastfm ||app.lastfm.cachedNowPlayingAttributes === attributes | !app.cfg.get('lastfm.NowPlaying')) {
return
}
if (app.lastfm.cachedNowPlayingAttributes) {
if (app.lastfm.cachedNowPlayingAttributes.playParams.id === attributes.playParams.id) return;
}
if (fs.existsSync(sessionPath)) {
// update Now Playing
if (attributes.status === true) {
app.lastfm.track.updateNowPlaying({
'artist': lfm.filterArtistName(attributes.artistName),
'track': attributes.name,
'album': attributes.albumName,
'albumArtist': this.filterArtistName(attributes.artistName)
}, function (err, nowPlaying) {
if (err) {
return console.error('[LastFM] An error occurred while updating nowPlayingSong', err);
}
console.log('[LastFM] Successfully updated nowPlayingSong', nowPlaying);
});
app.lastfm.cachedNowPlayingAttributes = attributes
}
} else {
this.authenticate()
}
}
}
module.exports = lfm;

View file

@ -2425,6 +2425,37 @@ const app = new Vue({
} }
CiderContextMenu.Create(event, menus[useMenu]) CiderContextMenu.Create(event, menus[useMenu])
}, },
LastFMDeauthorize() {
ipcRenderer.invoke('setStoreValue', 'lastfm.enabled', false).catch((e) => console.error(e));
ipcRenderer.invoke('setStoreValue', 'lastfm.auth_token', '').catch((e) => console.error(e));
app.cfg.lastfm.auth_token = "";
app.cfg.lastfm.enabled = false;
const element = document.getElementById('lfmConnect');
element.innerHTML = 'Connect';
element.onclick = app.LastFMAuthenticate;
},
LastFMAuthenticate() {
console.log("wag")
const element = document.getElementById('lfmConnect');
window.open('https://www.last.fm/api/auth?api_key=174905d201451602407b428a86e8344d&cb=ame://auth/lastfm');
element.innerText = 'Connecting...';
/* Just a timeout for the button */
setTimeout(() => {
if (element.innerText === 'Connecting...') {
element.innerText = 'Connect';
console.warn('[LastFM] Attempted connection timed out.');
}
}, 20000);
ipcRenderer.on('LastfmAuthenticated', function (_event, lfmAuthKey) {
app.cfg.lastfm.auth_token = lfmAuthKey;
app.cfg.lastfm.enabled = true;
element.innerHTML = `Disconnect\n<p style="font-size: 8px"><i>(Authed: ${lfmAuthKey})</i></p>`;
element.onclick = app.LastFMDeauthorize;
});
}
} }
}) })

View file

@ -396,7 +396,7 @@
</select> </select>
</div> </div>
</div> </div>
<div class="md-option-line"> <div class="md-option-line" v-show="app.cfg.general.discord_rpc != 0">
<div class="md-option-segment"> <div class="md-option-segment">
Clear Discord RPC on Pause Clear Discord RPC on Pause
</div> </div>
@ -407,6 +407,39 @@
</select> </select>
</div> </div>
</div> </div>
<div class="md-option-line">
<div class="md-option-segment">
LastFM Scrobbling
</div>
<div class="md-option-segment md-option-segment_auto">
<label class="list-button list-element" id="lfmConnect" ref="lfmConnect"
onclick="app.LastFMAuthenticate()">Connect</label>
</div>
</div>
<div class="md-option-line" v-show ="app.cfg.lastfm.enabled">
<div class="md-option-segment">
LastFM Scrobble Delay
</div>
<div class="md-option-segment md-option-segment_auto" >
<input type="number" v-model="app.cfg.lastfm.scrobble_after"/>
</div>
</div>
<div class="md-option-line" v-show ="app.cfg.lastfm.enabled">
<div class="md-option-segment">
Enable LastFM Now Playing
</div>
<div class="md-option-segment md-option-segment_auto" >
<input type="checkbox" v-model="app.cfg.lastfm.NowPlaying" switch/>
</div>
</div>
<div class="md-option-line" v-show ="app.cfg.lastfm.enabled">
<div class="md-option-segment">
Remove featuring artists from song title (LastFM)
</div>
<div class="md-option-segment md-option-segment_auto" >
<input type="checkbox" v-model="app.cfg.lastfm.enabledRemoveFeaturingArtists" switch/>
</div>
</div>
<div class="md-option-header"> <div class="md-option-header">
<span>Unfinished / Non Functional</span> <span>Unfinished / Non Functional</span>
</div> </div>
@ -420,22 +453,6 @@
<input type="checkbox" switch/> <input type="checkbox" switch/>
</div> </div>
</div> </div>
<div class="md-option-line">
<div class="md-option-segment">
LastFM Scrobbling
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" switch/>
</div>
</div>
<div class="md-option-line">
<div class="md-option-segment">
LastFM Scrobble Delay
</div>
<div class="md-option-segment md-option-segment_auto">
<input type="checkbox" switch/>
</div>
</div>
<div class="md-option-line"> <div class="md-option-line">
<div class="md-option-segment"> <div class="md-option-segment">
Theme Theme
@ -520,6 +537,15 @@
app: this.$root app: this.$root
} }
}, },
mounted: function () {
if (app.cfg.lastfm.enabled){
const element = document.getElementById('lfmConnect');
if (element){
element.innerHTML = `Disconnect\n<p style="font-size: 8px"><i>(Authed: ${app.cfg.lastfm.auth_token})</i></p>`;
element.onclick = app.LastFMDeauthorize;
}
}
},
methods: { methods: {
} }