diff --git a/index.js b/index.js index 6944f2fe..6688ecfc 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,7 @@ require('v8-compile-cache'); -const { app } = require('electron'); +const { app } = require('electron'), + {resolve} = require("path"), + CiderBase = require ('./src/main/cider-base'); // Analytics for debugging. const ElectronSentry = require("@sentry/electron"); @@ -33,9 +35,11 @@ const configSchema = { "enable_yt": false, }, "lastfm": { - "enabled": true, + "enabled": false, "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) => { console.log('[Cider][Widevine] Widevine installation encountered an error: ' + error) app.exit() -}) \ No newline at end of file +}) + +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 +} + + + \ No newline at end of file diff --git a/package.json b/package.json index 1a02b53c..a45419e1 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "electron-window-state": "^5.0.3", "express": "^4.17.2", "get-port": "^5.1.1", + "lastfmapi": "^0.1.1", "mpris-service": "^2.1.2", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/resources/lfmApiCredentials.json b/resources/lfmApiCredentials.json new file mode 100644 index 00000000..69af5902 --- /dev/null +++ b/resources/lfmApiCredentials.json @@ -0,0 +1,4 @@ +{ + "key": "174905d201451602407b428a86e8344d", + "secret": "be61d4081f6adec150f0130939f854bb" +} \ No newline at end of file diff --git a/src/main/cider-base.js b/src/main/cider-base.js index 18cdd1c4..52786201 100644 --- a/src/main/cider-base.js +++ b/src/main/cider-base.js @@ -7,6 +7,7 @@ const windowStateKeeper = require("electron-window-state"); const os = require('os'); const yt = require('youtube-search-without-api-key'); const discord = require('./discordrpc'); +const lastfm = require('./lastfm'); const mpris = require('./mpris'); // Analytics for debugging. @@ -14,9 +15,10 @@ const ElectronSentry = require("@sentry/electron"); ElectronSentry.init({dsn: "https://68c422bfaaf44dea880b86aad5a820d2@o954055.ingest.sentry.io/6112214"}); const CiderBase = { + win: null, async Start() { this.clientPort = await getPort({port: 9000}); - this.CreateBrowserWindow() + this.win = this.CreateBrowserWindow() }, clientPort: 0, CreateBrowserWindow() { @@ -204,15 +206,22 @@ const CiderBase = { mpris.connect(win) + lastfm.authenticate() // Discord discord.connect((app.cfg.get("general.discord_rpc") == 1) ? '911790844204437504' : '886578863147192350'); ipcMain.on('playbackStateDidChange', (_event, a) => { + app.media = a; discord.updateActivity(a) mpris.updateState(a) + lastfm.scrobbleSong(a) + lastfm.updateNowPlayingSong(a) }); ipcMain.on('nowPlayingItemDidChange', (_event, a) => { + app.media = a; discord.updateActivity(a) mpris.updateAttributes(a) + lastfm.scrobbleSong(a) + lastfm.updateNowPlayingSong(a) }); return win @@ -224,6 +233,31 @@ const CiderBase = { 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() { const webapp = express(); diff --git a/src/main/lastfm.js b/src/main/lastfm.js new file mode 100644 index 00000000..74dfa5b2 --- /dev/null +++ b/src/main/lastfm.js @@ -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; \ No newline at end of file diff --git a/src/renderer/index.js b/src/renderer/index.js index 9a8d7a18..429206ab 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -2425,6 +2425,37 @@ const app = new Vue({ } 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

(Authed: ${lfmAuthKey})

`; + element.onclick = app.LastFMDeauthorize; + }); + } + } }) diff --git a/src/renderer/views/pages/settings.ejs b/src/renderer/views/pages/settings.ejs index 5dbac871..5f773116 100644 --- a/src/renderer/views/pages/settings.ejs +++ b/src/renderer/views/pages/settings.ejs @@ -396,7 +396,7 @@ -
+
Clear Discord RPC on Pause
@@ -407,6 +407,39 @@
+
+
+ LastFM Scrobbling +
+
+ +
+
+
+
+ LastFM Scrobble Delay +
+
+ +
+
+
+
+ Enable LastFM Now Playing +
+
+ +
+
+
+
+ Remove featuring artists from song title (LastFM) +
+
+ +
+
Unfinished / Non Functional
@@ -420,22 +453,6 @@ -
-
- LastFM Scrobbling -
-
- -
-
-
-
- LastFM Scrobble Delay -
-
- -
-
Theme @@ -520,6 +537,15 @@ app: this.$root } }, + mounted: function () { + if (app.cfg.lastfm.enabled){ + const element = document.getElementById('lfmConnect'); + if (element){ + element.innerHTML = `Disconnect\n

(Authed: ${app.cfg.lastfm.auth_token})

`; + element.onclick = app.LastFMDeauthorize; + } + } + }, methods: { }