├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── babel.config.js ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── icon.png ├── index.html └── windows_runtime_scripts │ └── getTrack_iTunes.ps1 ├── src ├── App.vue ├── assets │ ├── logo.png │ └── logo.svg ├── background.js ├── components │ └── DividingDot.vue ├── main.js ├── players │ ├── Handler.js │ ├── Spotify │ │ ├── SpotifyHandler_darwin.js │ │ ├── SpotifyHandler_win32.js │ │ └── index.js │ ├── Web │ │ └── index.js │ ├── foobar2000 │ │ ├── foobarHandler_win32.js │ │ └── index.js │ ├── gpmdp │ │ ├── gpmdpHandler_win32.js │ │ └── index.js │ ├── iTunes │ │ ├── iTunesHandler_darwin.js │ │ ├── iTunesHandler_win32.js │ │ └── index.js │ ├── index.js │ ├── mpc-hc │ │ ├── index.js │ │ └── mpc-hc_win32.js │ ├── mpc-qt │ │ ├── index.js │ │ └── mpc-qt_win32.js │ ├── mpv │ │ ├── index.js │ │ └── mpv_win32.js │ └── vlc │ │ ├── index.js │ │ └── vlc_win32.js ├── plugins │ └── vuetify.js ├── router.js ├── store │ ├── index.js │ └── modules │ │ ├── index.js │ │ └── now-playing │ │ ├── actions.js │ │ ├── index.js │ │ └── mutations.js ├── util │ └── index.js └── views │ ├── NowPlaying.vue │ └── Settings.vue ├── vue.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | globals: { 7 | '__static': true 8 | }, 9 | extends: ["plugin:vue/essential", "@vue/prettier"], 10 | rules: { 11 | "quotes": [2, "single", { "avoidEscape": true }], 12 | "no-console": process.env.NODE_ENV === "production" ? "error" : "off", 13 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off", 14 | "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] 15 | }, 16 | parserOptions: { 17 | parser: "babel-eslint" 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | 23 | #Electron-builder output 24 | /dist_electron 25 | 26 | /build -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "arrowParens": "always" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Flying Lawnmower Development 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Universal Now Playing 2 | A now playing tool intended to be used with OBS. 3 | 4 | Spiritual successor to [EssentialNowPlaying](https://github.com/pendo324/EssentialNowPlaying) 5 | 6 | My reason for rewriting EssentialNowPlaying is because I still wanted to support it, but I no longer had much interest in C#. I know that Electron apps are somewhat controversial (this app takes up ~40MB whereas the C# app took ~10MB), but this is the version that I would rather maintain long-term. There were some nasty bugs in the C# version that I was just not willing to invest the time needed to fix. 7 | 8 | The usage of the program remains very similar. 9 | 10 | Currently supported: 11 | - Desktop (Windows [tested], *NIX [untested]): 12 | * Spotify 13 | * iTunes 14 | * VLC 15 | * MPC-HC 16 | * MPC-QT (rip) 17 | * foobar2000 18 | * mpv 19 | * Google Play Music Desktop Player 20 | - WebApps: 21 | * Mixcloud 22 | * Spotify web player (play.spotify.com) 23 | * Soundcloud 24 | * Tunein 25 | * YouTube 26 | * Pandora 27 | * Google Play (play.google.com) 28 | * Deezer 29 | * Bandcamp 30 | * Hype Machine 31 | * Plex 32 | * YouTube Music 33 | * Tidal 34 | 35 | Pull requests welcome to add player support. I could also use some Mac/Linux testers. 36 | 37 | ### Usage 38 | Download the installer from the Releases page. Double click and away you go. 39 | 40 | To use the WebApps, you'll need install the companion extension from the web store. Here's a link to it: https://chrome.google.com/webstore/detail/universal-now-playing-com/lljahlkpnhdopaegadghfjhhkcpdlijg. 41 | 42 | Extension repo here: https://github.com/pendo324/universal-np-extension 43 | 44 | To use the WebApps you need to do the following: 45 | 46 | 1. Open Universal Now Playing 47 | 2. Select the Player and set the file path in the Settings tab 48 | 3. Head to any supported web player 49 | 4. Click on the Now Playing Companion Extension and "Use on this page" 50 | 5. On the web pop-up click "Start" 51 | 6. Go back to Universal Now Playing app and hit "Start" 52 | 53 | Be sure that the desktop application is running before activating the extension. 54 | 55 | ### Contribute 56 | To build, just install the dependencies (`npm i`/`yarn`) and then run the build script, e.g. `npm run electron:build`. 57 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/app'] 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-np", 3 | "version": "0.4.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "electron:build": "vue-cli-service electron:build", 10 | "electron:generate-icon": "electron-icon-builder --input=./public/icon.png --output=./build --flatten", 11 | "electron:serve": "vue-cli-service electron:serve", 12 | "postinstall": "electron-builder install-app-deps", 13 | "postuninstall": "electron-builder install-app-deps" 14 | }, 15 | "main": "background.js", 16 | "dependencies": { 17 | "@mdi/font": "^3.6.95", 18 | "@pendo324/get-process-by-name": "^1.0.1", 19 | "body-parser": "^1.19.0", 20 | "core-js": "^2.6.5", 21 | "cors": "^2.8.5", 22 | "express": "^4.17.1", 23 | "mousetrap": "^1.6.3", 24 | "roboto-fontface": "*", 25 | "run-applescript": "^3.2.0", 26 | "vue": "^2.6.10", 27 | "vue-router": "^3.0.3", 28 | "vuetify": "^2.1.0", 29 | "vuex": "^3.0.1", 30 | "vuex-persistedstate": "^2.5.4", 31 | "winreg": "^1.2.4" 32 | }, 33 | "devDependencies": { 34 | "@vue/cli-plugin-babel": "^3.11.0", 35 | "@vue/cli-plugin-eslint": "^3.11.0", 36 | "@vue/cli-service": "^3.11.0", 37 | "@vue/eslint-config-prettier": "^5.0.0", 38 | "babel-eslint": "^10.0.1", 39 | "electron": "^8.2.3", 40 | "electron-icon-builder": "^1.0.1", 41 | "electron-rebuild": "^1.8.6", 42 | "eslint": "^5.16.0", 43 | "eslint-plugin-prettier": "^3.1.0", 44 | "eslint-plugin-vue": "^5.0.0", 45 | "prettier": "^1.18.2", 46 | "sass": "^1.18.0", 47 | "sass-loader": "^7.1.0", 48 | "vue-cli-plugin-electron-builder": "^2.0.0", 49 | "vue-cli-plugin-vuetify": "^1.0.0", 50 | "vue-template-compiler": "^2.6.10", 51 | "vuetify-loader": "^1.2.2" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pendo324/universal-np/aab5469fb563eb9046e5441c7651008061b3af4f/public/favicon.ico -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pendo324/universal-np/aab5469fb563eb9046e5441c7651008061b3af4f/public/icon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | universal-np 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/windows_runtime_scripts/getTrack_iTunes.ps1: -------------------------------------------------------------------------------- 1 | # I don't know why this works, and I don't care. It fixes Unicode (espeically CJK) so yoloooooo 2 | $OutputEncoding = [console]::InputEncoding = [console]::OutputEncoding = New-Object System.Text.UTF8Encoding; 3 | 4 | $iTunes = New-Object -ComObject iTunes.Application; 5 | 6 | $properties = @{ 7 | name = $iTunes."CurrentTrack"."Name"; 8 | artist = $iTunes."CurrentTrack"."Artist"; 9 | playerState = if ([System.Convert]::ToBoolean($iTunes."PlayerState")) { "playing" } else { "stopped" }; 10 | }; 11 | $out = New-Object PSObject -Property $properties; 12 | 13 | Write-Output ($out | ConvertTo-Json); 14 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 128 | 129 | 141 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pendo324/universal-np/aab5469fb563eb9046e5441c7651008061b3af4f/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 46 2 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { app, protocol, BrowserWindow, Menu, Tray } from 'electron'; 4 | import { 5 | createProtocol, 6 | installVueDevtools 7 | } from 'vue-cli-plugin-electron-builder/lib'; 8 | 9 | const path = require('path'); 10 | 11 | const isDevelopment = process.env.NODE_ENV !== 'production'; 12 | 13 | // Keep a global reference of the window object, if you don't, the window will 14 | // be closed automatically when the JavaScript object is garbage collected. 15 | let win; 16 | 17 | // since the close button doesn't actually close the application, we need to keep track 18 | // of when the application should actually close 19 | let isQuitting = false; 20 | 21 | app.on('before-quit', () => { 22 | isQuitting = true; 23 | }); 24 | 25 | app.allowRendererProcessReuse = true; 26 | 27 | // Scheme must be registered before the app is ready 28 | protocol.registerSchemesAsPrivileged([ 29 | { scheme: 'app', privileges: { secure: true, standard: true } } 30 | ]); 31 | 32 | function createWindow() { 33 | // Create the browser window. 34 | win = new BrowserWindow({ 35 | width: 800, 36 | height: 600, 37 | webPreferences: { 38 | // Use pluginOptions.nodeIntegration, leave this alone 39 | // See https://github.com/nklayman/vue-cli-plugin-electron-builder/blob/v2/docs/guide/configuration.md#node-integration for more info 40 | nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION 41 | }, 42 | darkTheme: true 43 | }); 44 | 45 | if (process.env.WEBPACK_DEV_SERVER_URL) { 46 | // Load the url of the dev server if in development mode 47 | win.loadURL(process.env.WEBPACK_DEV_SERVER_URL); 48 | if (!process.env.IS_TEST) win.webContents.openDevTools(); 49 | } else { 50 | createProtocol('app'); 51 | // Load the index.html when not in development 52 | win.loadURL('app://./index.html'); 53 | } 54 | 55 | win.on('close', function(event) { 56 | if (!isQuitting) { 57 | event.preventDefault(); 58 | win.hide(); 59 | } 60 | 61 | return false; 62 | }); 63 | 64 | win.on('closed', () => { 65 | win = null; 66 | }); 67 | 68 | return win; 69 | } 70 | 71 | // Quit when all windows are closed. 72 | app.on('window-all-closed', () => { 73 | // On macOS it is common for applications and their menu bar 74 | // to stay active until the user quits explicitly with Cmd + Q 75 | if (process.platform !== 'darwin') { 76 | app.quit(); 77 | } 78 | }); 79 | 80 | app.on('activate', () => { 81 | // On macOS it's common to re-create a window in the app when the 82 | // dock icon is clicked and there are no other windows open. 83 | if (win === null) { 84 | createWindow(); 85 | } 86 | }); 87 | 88 | // This method will be called when Electron has finished 89 | // initialization and is ready to create browser windows. 90 | // Some APIs can only be used after this event occurs. 91 | let tray = null; 92 | app.on('ready', async () => { 93 | if (isDevelopment && !process.env.IS_TEST) { 94 | // Install Vue Devtools 95 | // Devtools extensions are broken in Electron 6.0.0 and greater 96 | // See https://github.com/nklayman/vue-cli-plugin-electron-builder/issues/378 for more info 97 | // Electron will not launch with Devtools extensions installed on Windows 10 with dark mode 98 | // If you are not using Windows 10 dark mode, you may uncomment these lines 99 | // In addition, if the linked issue is closed, you can upgrade electron and uncomment these lines 100 | try { 101 | await installVueDevtools(); 102 | } catch (e) { 103 | console.error('Vue Devtools failed to install:', e.toString()); 104 | } 105 | } 106 | 107 | tray = new Tray(path.join(__static, 'icon.png')); 108 | tray.setToolTip('Universal Now Playing'); 109 | 110 | const mainWindow = createWindow(); 111 | 112 | const contextMenu = Menu.buildFromTemplate([ 113 | { 114 | label: 'Universal NP', 115 | type: 'normal', 116 | enabled: false 117 | }, 118 | { 119 | label: 'Quit', 120 | type: 'normal', 121 | click() { 122 | isQuitting = true; 123 | app.quit(); 124 | } 125 | } 126 | ]); 127 | 128 | tray.setContextMenu(contextMenu); 129 | 130 | tray.on('click', () => { 131 | mainWindow.show(); 132 | }); 133 | }); 134 | 135 | // Exit cleanly on request from parent process in development mode. 136 | if (isDevelopment) { 137 | if (process.platform === 'win32') { 138 | process.on('message', (data) => { 139 | if (data === 'graceful-exit') { 140 | app.quit(); 141 | } 142 | }); 143 | } else { 144 | process.on('SIGTERM', () => { 145 | app.quit(); 146 | }); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/components/DividingDot.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | import router from './router'; 4 | import store from './store'; 5 | import vuetify from './plugins/vuetify'; 6 | import 'roboto-fontface/css/roboto/roboto-fontface.css'; 7 | import '@mdi/font/css/materialdesignicons.css'; 8 | 9 | const Mousetrap = require('mousetrap'); 10 | const express = require('express'); 11 | const bodyParser = require('body-parser'); 12 | const cors = require('cors'); 13 | 14 | Vue.config.productionTip = false; 15 | 16 | const app = express(); 17 | app.use( 18 | cors({ 19 | origin: '*' 20 | }) 21 | ); 22 | app.use(bodyParser.urlencoded({ extended: false })); 23 | app.use(bodyParser.json()); 24 | const server = app.listen(47565); 25 | 26 | Vue.mixin({ 27 | data() { 28 | return { express: app, server }; 29 | } 30 | }); 31 | 32 | new Vue({ 33 | router, 34 | store, 35 | vuetify, 36 | render: (h) => h(App) 37 | }).$mount('#app'); 38 | 39 | if (process.env.NODE_ENV === 'production') { 40 | // temporary, since refreshing causes a few bugs atm, need to fix 41 | Mousetrap.bind(['command+r', 'control+r'], () => { 42 | return false; 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /src/players/Handler.js: -------------------------------------------------------------------------------- 1 | class PlayerHandler { 2 | constructor({ name, os, source, id }) { 3 | this.name = name; 4 | this.os = os; 5 | this.source = source; 6 | this.id = id; 7 | } 8 | 9 | getTrack() { 10 | throw new TypeError('Must override method.'); 11 | } 12 | } 13 | 14 | export default PlayerHandler; 15 | -------------------------------------------------------------------------------- /src/players/Spotify/SpotifyHandler_darwin.js: -------------------------------------------------------------------------------- 1 | import Handler from './../Handler'; 2 | 3 | import runApplescript from 'run-applescript'; 4 | 5 | const getSpotifyNowPlayingMac = async () => { 6 | return await runApplescript(` 7 | if application "Spotify" is running then 8 | tell application "Spotify" 9 | set currentArtist to artist of current track as string 10 | set currentTrack to name of current track as string 11 | 12 | return currentArtist & " - " & currentTrack 13 | end tell 14 | end if 15 | return "" 16 | `); 17 | }; 18 | 19 | class SpotifyHandler extends Handler { 20 | constructor() { 21 | super({ os: 'darwin', source: 'Desktop', id: 'Spotify', name: 'Spotify' }); 22 | } 23 | 24 | async getTrack() { 25 | const track = await getSpotifyNowPlayingMac(); 26 | return track; 27 | } 28 | } 29 | 30 | export default SpotifyHandler; 31 | -------------------------------------------------------------------------------- /src/players/Spotify/SpotifyHandler_win32.js: -------------------------------------------------------------------------------- 1 | import Handler from './../Handler'; 2 | import { getProcessByName } from '@pendo324/get-process-by-name'; 3 | 4 | class SpotifyHandler extends Handler { 5 | constructor() { 6 | super({ os: 'win32', source: 'Desktop', id: 'Spotify', name: 'Spotify' }); 7 | } 8 | 9 | async getTrack() { 10 | const processes = (await getProcessByName('Spotify.exe')).filter( 11 | (t) => 12 | t.windowTitle && 13 | t.windowTitle.length > 0 && 14 | t.windowTitle !== 'AngleHiddenWindow' 15 | ); 16 | 17 | if (!processes.length === 0) { 18 | alert('Tool needs updating.'); 19 | } 20 | 21 | if ( 22 | processes[0].windowTitle === 'Spotify' || 23 | processes[0].windowTitle === 'Spotify Premium' 24 | ) { 25 | return ''; 26 | } 27 | 28 | return processes[0].windowTitle; 29 | } 30 | } 31 | 32 | export default SpotifyHandler; 33 | -------------------------------------------------------------------------------- /src/players/Spotify/index.js: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron'; 2 | const { platform } = remote.require('os'); 3 | 4 | export const supportedPlatforms = ['darwin', 'win32']; 5 | 6 | const platformHandler = supportedPlatforms.find((p) => platform() === p); 7 | 8 | if (typeof platformHandler === 'undefined') { 9 | throw new Error('Platform not supported.'); 10 | } 11 | 12 | export default require(`./SpotifyHandler_${platformHandler}`).default; 13 | -------------------------------------------------------------------------------- /src/players/Web/index.js: -------------------------------------------------------------------------------- 1 | import Handler from './../Handler'; 2 | 3 | const defaultOptions = { 4 | source: 'Web', 5 | os: '' 6 | }; 7 | 8 | export class DeezerHandler extends Handler { 9 | constructor() { 10 | super({ 11 | ...defaultOptions, 12 | id: 'deezer', 13 | name: 'deezer' 14 | }); 15 | } 16 | } 17 | 18 | export class GooglePlayHandler extends Handler { 19 | constructor() { 20 | super({ 21 | ...defaultOptions, 22 | id: 'Google Play', 23 | name: 'Google Play' 24 | }); 25 | } 26 | } 27 | 28 | export class HypeMachineHandler extends Handler { 29 | constructor() { 30 | super({ 31 | ...defaultOptions, 32 | id: 'hypem', 33 | name: 'Hype Machine' 34 | }); 35 | } 36 | } 37 | 38 | export class MixcloudHandler extends Handler { 39 | constructor() { 40 | super({ 41 | ...defaultOptions, 42 | id: 'Mixcloud', 43 | name: 'Mixcloud' 44 | }); 45 | } 46 | } 47 | 48 | export class PandoraHandler extends Handler { 49 | constructor() { 50 | super({ 51 | ...defaultOptions, 52 | id: 'Pandora', 53 | name: 'Pandora' 54 | }); 55 | } 56 | } 57 | 58 | export class PlexHandler extends Handler { 59 | constructor() { 60 | super({ 61 | ...defaultOptions, 62 | id: 'Plex', 63 | name: 'Plex' 64 | }); 65 | } 66 | } 67 | 68 | export class SoundcloudHandler extends Handler { 69 | constructor() { 70 | super({ 71 | ...defaultOptions, 72 | id: 'Soundcloud', 73 | name: 'Soundcloud' 74 | }); 75 | } 76 | } 77 | 78 | export class SpotifyHandler extends Handler { 79 | constructor() { 80 | super({ 81 | ...defaultOptions, 82 | id: 'Spotify', 83 | name: 'Spotify (Web)' 84 | }); 85 | } 86 | } 87 | 88 | export class tuneInHandler extends Handler { 89 | constructor() { 90 | super({ 91 | ...defaultOptions, 92 | id: 'tunein', 93 | name: 'tunein' 94 | }); 95 | } 96 | } 97 | 98 | export class YouTubeHandler extends Handler { 99 | constructor() { 100 | super({ 101 | ...defaultOptions, 102 | id: 'YouTube', 103 | name: 'YouTube' 104 | }); 105 | } 106 | } 107 | 108 | export class bandcampHandler extends Handler { 109 | constructor() { 110 | super({ 111 | ...defaultOptions, 112 | id: 'bandcamp', 113 | name: 'bandcamp' 114 | }); 115 | } 116 | } 117 | 118 | export class youtubeMusicHandler extends Handler { 119 | constructor() { 120 | super({ 121 | ...defaultOptions, 122 | id: 'youtubeMusic', 123 | name: 'YouTube Music' 124 | }); 125 | } 126 | } 127 | 128 | export class tidalHandler extends Handler { 129 | constructor() { 130 | super({ 131 | ...defaultOptions, 132 | id: 'tidal', 133 | name: 'Tidal' 134 | }); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/players/foobar2000/foobarHandler_win32.js: -------------------------------------------------------------------------------- 1 | import Handler from './../Handler'; 2 | import { getProcessByName } from '@pendo324/get-process-by-name'; 3 | 4 | class FoobarHandler extends Handler { 5 | constructor() { 6 | super({ 7 | os: 'win32', 8 | source: 'Desktop', 9 | id: 'foobar2000', 10 | name: 'foobar2000' 11 | }); 12 | } 13 | 14 | async getTrack() { 15 | const processes = (await getProcessByName('foobar2000.exe')).filter( 16 | (t) => t.windowTitle && t.windowTitle.length > 0 17 | ); 18 | 19 | if (!processes.length) { 20 | // TODO: add better check/error handling here 21 | // alert('Tool needs updating.'); 22 | return ''; 23 | } 24 | 25 | const windowTitle = processes[0].windowTitle; 26 | 27 | if (windowTitle === 'foobar2000') { 28 | return ''; 29 | } 30 | 31 | // remove the default [foobar2000] from the end of the window title 32 | const defaultSuffix = '[foobar2000]'; 33 | if (windowTitle.endsWith(defaultSuffix)) { 34 | return windowTitle.split(defaultSuffix)[0].trim(); 35 | } 36 | 37 | return windowTitle; 38 | } 39 | } 40 | 41 | export default FoobarHandler; 42 | -------------------------------------------------------------------------------- /src/players/foobar2000/index.js: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron'; 2 | const { platform } = remote.require('os'); 3 | 4 | export const supportedPlatforms = ['win32']; 5 | 6 | const platformHandler = supportedPlatforms.find((p) => platform() === p); 7 | 8 | if (typeof platformHandler === 'undefined') { 9 | throw new Error('Platform not supported.'); 10 | } 11 | 12 | export default require(`./foobarHandler_${platformHandler}`).default; 13 | -------------------------------------------------------------------------------- /src/players/gpmdp/gpmdpHandler_win32.js: -------------------------------------------------------------------------------- 1 | import Handler from './../Handler'; 2 | import { getProcessByName } from '@pendo324/get-process-by-name'; 3 | 4 | class GpmdpHandler extends Handler { 5 | constructor() { 6 | super({ 7 | os: 'win32', 8 | source: 'Desktop', 9 | id: 'gpmdp', 10 | name: 'Google Play Desktop Player' 11 | }); 12 | } 13 | 14 | async getTrack() { 15 | const processes = ( 16 | await getProcessByName('Google Play Music Desktop Player.exe') 17 | ).filter((t) => t.windowTitle && t.windowTitle.length > 0); 18 | 19 | if (!processes.length) { 20 | // TODO: add better check/error handling here 21 | // alert('Tool needs updating.'); 22 | return ''; 23 | } 24 | 25 | const windowTitle = processes[0].windowTitle; 26 | 27 | if (windowTitle.startsWith('(Paused)')) { 28 | return 'Paused.'; 29 | } 30 | 31 | if ( 32 | windowTitle === 'Google Play Music Desktop Player' || 33 | windowTitle === 'crash service' 34 | ) { 35 | return ''; 36 | } 37 | 38 | return windowTitle; 39 | } 40 | } 41 | 42 | export default GpmdpHandler; 43 | -------------------------------------------------------------------------------- /src/players/gpmdp/index.js: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron'; 2 | const { platform } = remote.require('os'); 3 | 4 | export const supportedPlatforms = ['win32']; 5 | 6 | const platformHandler = supportedPlatforms.find((p) => platform() === p); 7 | 8 | if (typeof platformHandler === 'undefined') { 9 | throw new Error('Platform not supported.'); 10 | } 11 | 12 | export default require(`./gpmdpHandler_${platformHandler}`).default; 13 | -------------------------------------------------------------------------------- /src/players/iTunes/iTunesHandler_darwin.js: -------------------------------------------------------------------------------- 1 | import Handler from './../Handler'; 2 | 3 | import runApplescript from 'run-applescript'; 4 | 5 | const getiTunesNowPlayingMac = async () => { 6 | return await runApplescript(` 7 | if application "iTunes" is running then 8 | tell application "iTunes" 9 | set currentArtist to the artist of the current track as string 10 | set currentTrack to the name of the current track as string 11 | 12 | return currentArtist & " - " & currentTrack 13 | end tell 14 | end if 15 | return "" 16 | `); 17 | }; 18 | 19 | class iTunesHandler extends Handler { 20 | constructor() { 21 | super({ os: 'darwin', source: 'Desktop', id: 'iTunes', name: 'iTunes' }); 22 | } 23 | 24 | async getTrack() { 25 | const track = await getiTunesNowPlayingMac(); 26 | return track; 27 | } 28 | } 29 | 30 | export default iTunesHandler; 31 | -------------------------------------------------------------------------------- /src/players/iTunes/iTunesHandler_win32.js: -------------------------------------------------------------------------------- 1 | import Handler from './../Handler'; 2 | import { remote } from 'electron'; 3 | const { exec } = require('child_process'); 4 | const { join } = require('path'); 5 | 6 | import { copyWindowsScripts } from '@/util'; 7 | import { getProcessByName } from '@pendo324/get-process-by-name'; 8 | 9 | (async () => { 10 | await copyWindowsScripts(); 11 | })(); 12 | 13 | const getCurrentTrack = () => { 14 | return new Promise((resolve, reject) => { 15 | const scriptPath = `powershell ${join( 16 | remote.app.getPath('userData'), 17 | 'windows_runtime_scripts', 18 | 'getTrack_iTunes.ps1' 19 | )}`; 20 | exec(scriptPath, (err, stdout) => { 21 | if (err) { 22 | return reject(err); 23 | } 24 | 25 | const parsed = stdout.trim(); 26 | resolve(parsed); 27 | }); 28 | }); 29 | }; 30 | 31 | class iTunesHandler extends Handler { 32 | constructor() { 33 | super({ os: 'darwin', source: 'Desktop', id: 'iTunes', name: 'iTunes' }); 34 | } 35 | 36 | async getTrack() { 37 | try { 38 | const processes = await getProcessByName('iTunes.exe'); 39 | 40 | if (processes.length > 0) { 41 | const track = JSON.parse(await getCurrentTrack()); 42 | if (track.playerState === 'stopped') { 43 | return 'Paused.'; 44 | } 45 | return `${track.artist} - ${track.name}`; 46 | } 47 | 48 | return ''; 49 | } catch (e) { 50 | return ''; 51 | } 52 | } 53 | } 54 | 55 | export default iTunesHandler; 56 | -------------------------------------------------------------------------------- /src/players/iTunes/index.js: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron'; 2 | const { platform } = remote.require('os'); 3 | 4 | export const supportedPlatforms = ['darwin', 'win32']; 5 | 6 | const platformHandler = supportedPlatforms.find((p) => platform() === p); 7 | 8 | if (typeof platformHandler === 'undefined') { 9 | throw new Error('Platform not supported.'); 10 | } 11 | 12 | export default require(`./iTunesHandler_${platformHandler}`).default; 13 | -------------------------------------------------------------------------------- /src/players/index.js: -------------------------------------------------------------------------------- 1 | // const Spotify = require('./Spotify'); 2 | // const iTunes = require('./iTunes'); 3 | 4 | import * as Spotify from './Spotify'; 5 | import * as iTunes from './iTunes'; 6 | import * as foobar from './foobar2000'; 7 | import * as mpcQt from './mpc-qt'; 8 | import * as mpcHc from './mpc-hc'; 9 | import * as mpv from './mpv'; 10 | import * as vlc from './vlc'; 11 | import * as gpmdp from './gpmdp'; 12 | 13 | import * as Web from './Web'; 14 | 15 | export const desktop = { 16 | Spotify, 17 | iTunes, 18 | foobar, 19 | mpcQt, 20 | mpcHc, 21 | mpv, 22 | vlc, 23 | gpmdp 24 | }; 25 | 26 | export const web = { 27 | ...Web 28 | }; 29 | 30 | // export default { 31 | // desktop: { 32 | // }, 33 | // web: { 34 | 35 | // } 36 | // }; 37 | -------------------------------------------------------------------------------- /src/players/mpc-hc/index.js: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron'; 2 | const { platform } = remote.require('os'); 3 | 4 | export const supportedPlatforms = ['win32']; 5 | 6 | const platformHandler = supportedPlatforms.find((p) => platform() === p); 7 | 8 | if (typeof platformHandler === 'undefined') { 9 | throw new Error('Platform not supported.'); 10 | } 11 | 12 | export default require(`./mpc-hc_${platformHandler}`).default; 13 | -------------------------------------------------------------------------------- /src/players/mpc-hc/mpc-hc_win32.js: -------------------------------------------------------------------------------- 1 | import Handler from './../Handler'; 2 | import { getProcessByName } from '@pendo324/get-process-by-name'; 3 | 4 | class MpcHcHandler extends Handler { 5 | constructor() { 6 | super({ os: 'win32', source: 'Desktop', id: 'mpcHc', name: 'mpc-hc' }); 7 | } 8 | 9 | async getTrack() { 10 | let processes = null; 11 | 12 | // favor mpc-hc64 because I'm lazy 13 | processes = await getProcessByName('mpc-hc64.exe'); 14 | console.log(processes); 15 | if (!processes.length) { 16 | processes = await getProcessByName('mpc-hc.exe'); 17 | } 18 | 19 | processes = processes.filter( 20 | (t) => t.windowTitle && t.windowTitle.length > 0 21 | ); 22 | console.log(processes); 23 | 24 | if (!processes.length) { 25 | // TODO: add better check/error handling here 26 | // alert('Tool needs updating.'); 27 | return ''; 28 | } 29 | 30 | if (processes[0].windowTitle === 'Media Player Classic Home Cinema') { 31 | return ''; 32 | } 33 | 34 | return processes[0].windowTitle; 35 | } 36 | } 37 | 38 | export default MpcHcHandler; 39 | -------------------------------------------------------------------------------- /src/players/mpc-qt/index.js: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron'; 2 | const { platform } = remote.require('os'); 3 | 4 | export const supportedPlatforms = ['win32']; 5 | 6 | const platformHandler = supportedPlatforms.find((p) => platform() === p); 7 | 8 | if (typeof platformHandler === 'undefined') { 9 | throw new Error('Platform not supported.'); 10 | } 11 | 12 | export default require(`./mpc-qt_${platformHandler}`).default; 13 | -------------------------------------------------------------------------------- /src/players/mpc-qt/mpc-qt_win32.js: -------------------------------------------------------------------------------- 1 | import Handler from './../Handler'; 2 | import { getProcessByName } from '@pendo324/get-process-by-name'; 3 | 4 | const baseTitle = 'Media Player Classic Qute Theater'; 5 | 6 | class FoobarHandler extends Handler { 7 | constructor() { 8 | super({ os: 'win32', source: 'Desktop', id: 'mpcQt', name: 'mpc-qt' }); 9 | } 10 | 11 | async getTrack() { 12 | const processes = (await getProcessByName('mpc-qt.exe')).filter( 13 | (t) => t.windowTitle && t.windowTitle.length > 0 14 | ); 15 | 16 | if (!processes.length) { 17 | // TODO: add better check/error handling here 18 | // alert('Tool needs updating.'); 19 | return ''; 20 | } 21 | 22 | if (processes[0].windowTitle === baseTitle) { 23 | return ''; 24 | } 25 | 26 | return processes[0].windowTitle.split(`${baseTitle} - `)[1]; 27 | } 28 | } 29 | 30 | export default FoobarHandler; 31 | -------------------------------------------------------------------------------- /src/players/mpv/index.js: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron'; 2 | const { platform } = remote.require('os'); 3 | 4 | export const supportedPlatforms = ['win32']; 5 | 6 | const platformHandler = supportedPlatforms.find((p) => platform() === p); 7 | 8 | if (typeof platformHandler === 'undefined') { 9 | throw new Error('Platform not supported.'); 10 | } 11 | 12 | export default require(`./mpv_${platformHandler}`).default; 13 | -------------------------------------------------------------------------------- /src/players/mpv/mpv_win32.js: -------------------------------------------------------------------------------- 1 | import Handler from './../Handler'; 2 | import { getProcessByName } from '@pendo324/get-process-by-name'; 3 | 4 | class MpvHandler extends Handler { 5 | constructor() { 6 | super({ os: 'win32', source: 'Desktop', id: 'mpv', name: 'mpv' }); 7 | } 8 | 9 | async getTrack() { 10 | const processes = (await getProcessByName('mpv.exe')).filter( 11 | (t) => t.windowTitle && t.windowTitle.length > 0 12 | ); 13 | 14 | if (!processes.length) { 15 | // TODO: add better check/error handling here 16 | // alert('Tool needs updating.'); 17 | return ''; 18 | } 19 | 20 | if (processes[0].windowTitle === 'mpv') { 21 | return ''; 22 | } 23 | 24 | return processes[0].windowTitle.split(' - mpv')[0]; 25 | } 26 | } 27 | 28 | export default MpvHandler; 29 | -------------------------------------------------------------------------------- /src/players/vlc/index.js: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron'; 2 | const { platform } = remote.require('os'); 3 | 4 | export const supportedPlatforms = ['win32']; 5 | 6 | const platformHandler = supportedPlatforms.find((p) => platform() === p); 7 | 8 | if (typeof platformHandler === 'undefined') { 9 | throw new Error('Platform not supported.'); 10 | } 11 | 12 | export default require(`./vlc_${platformHandler}`).default; 13 | -------------------------------------------------------------------------------- /src/players/vlc/vlc_win32.js: -------------------------------------------------------------------------------- 1 | import Handler from './../Handler'; 2 | import { getProcessByName } from '@pendo324/get-process-by-name'; 3 | 4 | const baseTitle = 'VLC media player'; 5 | 6 | class VlcHandler extends Handler { 7 | constructor() { 8 | super({ os: 'win32', source: 'Desktop', id: 'vlc', name: 'vlc' }); 9 | } 10 | 11 | async getTrack() { 12 | const processes = (await getProcessByName('vlc.exe')).filter( 13 | (t) => t.windowTitle && t.windowTitle.length > 0 14 | ); 15 | 16 | if (!processes.length) { 17 | // TODO: add better check/error handling here 18 | // alert('Tool needs updating.'); 19 | return ''; 20 | } 21 | 22 | if (processes[0].windowTitle === baseTitle) { 23 | return ''; 24 | } 25 | 26 | return processes[0].windowTitle.split(` - ${baseTitle}`)[0]; 27 | } 28 | } 29 | 30 | export default VlcHandler; 31 | -------------------------------------------------------------------------------- /src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuetify from 'vuetify/lib'; 3 | 4 | Vue.use(Vuetify); 5 | 6 | export default new Vuetify({ 7 | theme: { dark: true }, 8 | icons: { 9 | iconfont: 'mdi' 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import NowPlaying from './views/NowPlaying'; 4 | import Settings from './views/Settings'; 5 | 6 | Vue.use(Router); 7 | 8 | export default new Router({ 9 | base: '/', 10 | routes: [ 11 | { 12 | path: '/', 13 | name: 'home', 14 | component: NowPlaying 15 | }, 16 | { 17 | path: '/settings', 18 | name: 'settings', 19 | component: Settings 20 | } 21 | ] 22 | }); 23 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import createPersistedState from 'vuex-persistedstate'; 4 | 5 | import modules from './modules'; 6 | 7 | Vue.use(Vuex); 8 | 9 | export default new Vuex.Store({ 10 | modules, 11 | plugins: [ 12 | createPersistedState({ 13 | paths: [ 14 | 'now-playing.browser', 15 | 'now-playing.saveLocation', 16 | 'now-playing.prefix', 17 | 'now-playing.suffix' 18 | ] 19 | }) 20 | ], 21 | strict: true 22 | }); 23 | -------------------------------------------------------------------------------- /src/store/modules/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The file enables `@/store/index.js` to import all vuex modules 3 | * in a one-shot manner. There should not be any reason to edit this file. 4 | */ 5 | 6 | import nowPlaying from './now-playing'; 7 | 8 | // const files = require.context('.', false, /\.js$/); 9 | // const modules = {}; 10 | 11 | // files.keys().forEach((key) => { 12 | // if (key === './index.js') return; 13 | // modules[key.replace(/(\.\/|\.js)/g, '')] = files(key).default; 14 | // }); 15 | 16 | // export default modules; 17 | 18 | const modules = { 19 | 'now-playing': nowPlaying 20 | }; 21 | 22 | export default modules; 23 | -------------------------------------------------------------------------------- /src/store/modules/now-playing/actions.js: -------------------------------------------------------------------------------- 1 | const { writeFileSync } = require('fs'); 2 | 3 | const actions = { 4 | async setTrack(context, { preview } = { preview: false }) { 5 | const { 6 | state: { player, saveLocation }, 7 | commit, 8 | getters: { nowPlaying } 9 | } = context; 10 | if (player !== null) { 11 | if (player.source === 'Desktop') { 12 | try { 13 | const track = await player.getTrack(); 14 | commit('SET_TRACK', { 15 | track 16 | }); 17 | if (!preview) { 18 | writeFileSync(saveLocation, nowPlaying, { 19 | encoding: 'utf8' 20 | }); 21 | } 22 | } catch (e) { 23 | console.log(e); 24 | return commit('SET_TRACK', { 25 | track: '' 26 | }); 27 | } 28 | } else if (player.source === 'Web') { 29 | if (!preview) { 30 | writeFileSync(saveLocation, nowPlaying); 31 | } 32 | } 33 | } 34 | } 35 | }; 36 | 37 | export default actions; 38 | -------------------------------------------------------------------------------- /src/store/modules/now-playing/index.js: -------------------------------------------------------------------------------- 1 | import mutations, { getters, state } from './mutations'; 2 | import actions from './actions'; 3 | 4 | export default { 5 | state, 6 | getters, 7 | mutations, 8 | actions, 9 | namespaced: true 10 | }; 11 | -------------------------------------------------------------------------------- /src/store/modules/now-playing/mutations.js: -------------------------------------------------------------------------------- 1 | export const state = { 2 | player: null, 3 | track: null, 4 | browser: null, 5 | saveLocation: null, 6 | prefix: '', 7 | suffix: '', 8 | polling: false 9 | }; 10 | 11 | const mutations = { 12 | SET_PLAYER(state, player) { 13 | state.player = player; 14 | }, 15 | CLEAR_PLAYER(state) { 16 | state.player = null; 17 | }, 18 | SET_TRACK(state, { track }) { 19 | state.track = track; 20 | }, 21 | CLEAR_TRACK(state) { 22 | state.track = null; 23 | }, 24 | SET_BROWSER(state, { browser }) { 25 | state.browser = browser; 26 | }, 27 | SET_SAVE_LOCATION(state, { saveLocation }) { 28 | state.saveLocation = saveLocation; 29 | }, 30 | SET_PREFIX(state, { prefix }) { 31 | state.prefix = prefix; 32 | }, 33 | CLEAR_PREFIX(state) { 34 | state.prefix = ''; 35 | }, 36 | SET_SUFFIX(state, { suffix }) { 37 | state.suffix = suffix; 38 | }, 39 | CLEAR_SUFFIX(state) { 40 | state.suffix = ''; 41 | }, 42 | SET_POLLING(state) { 43 | state.polling = true; 44 | }, 45 | CLEAR_POLLING(state) { 46 | state.polling = false; 47 | } 48 | }; 49 | 50 | export const getters = { 51 | webPlayer(state) { 52 | return state.source === 'Web'; 53 | }, 54 | nowPlaying(state) { 55 | if (state.track === null) { 56 | return 'No song playing.'; 57 | } 58 | return `${state.prefix}${state.track}${state.suffix}`; 59 | } 60 | }; 61 | 62 | export default mutations; 63 | -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron'; 2 | 3 | const mkdirp = require('mkdirp'); 4 | 5 | const { join } = require('path'); 6 | const { readFile, writeFile } = require('fs').promises; 7 | 8 | export const copyWindowsScripts = () => { 9 | return new Promise((resolve, reject) => { 10 | const path = join( 11 | remote.app.getPath('userData'), 12 | 'windows_runtime_scripts' 13 | ); 14 | 15 | mkdirp(path, async (e) => { 16 | if (e) { 17 | console.log(e); 18 | return reject(e); 19 | } 20 | 21 | await Promise.all( 22 | ['getTrack_iTunes.ps1'].map(async (f) => { 23 | return await writeFile( 24 | join(path, f), 25 | await readFile(join(__static, '/windows_runtime_scripts/', f)) 26 | ); 27 | }) 28 | ); 29 | 30 | resolve(); 31 | }); 32 | }); 33 | }; 34 | 35 | export const createDefaultConfig = () => { 36 | return new Promise((resolve, reject) => { 37 | const dataDirPath = join(remote.app.getPath('userData'), 'data'); 38 | const defaultConfigFilePath = join(dataDirPath, 'nowplaying.txt'); 39 | 40 | mkdirp(dataDirPath, async (e) => { 41 | if (e) { 42 | console.log(e); 43 | return reject(e); 44 | } 45 | 46 | try { 47 | await writeFile(defaultConfigFilePath, '', { 48 | flag: 'wx' 49 | }); 50 | } catch (e) { 51 | console.log( 52 | "Couldn't write default config file. It probably already exists: ", 53 | e 54 | ); 55 | } finally { 56 | resolve(defaultConfigFilePath); 57 | } 58 | }); 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /src/views/NowPlaying.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 174 | 175 | 180 | -------------------------------------------------------------------------------- /src/views/Settings.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lintOnSave: false, 3 | transpileDependencies: ['vuetify'], 4 | pluginOptions: { 5 | electronBuilder: { 6 | externals: ['body-parser'], 7 | nodeIntegration: true, 8 | builderOptions: { 9 | win: { 10 | publisherName: 'Flying Lawnmower Development' 11 | } 12 | } 13 | } 14 | } 15 | }; 16 | --------------------------------------------------------------------------------