Compare commits

...
14 Commits
Author SHA1 Message Date
FoxxMD ce9e09c3a6 Add examples and update readme 2020-11-16 11:55:09 -05:00
FoxxMD 7b45ad6e9e More cleanup on readme 2020-11-16 11:31:48 -05:00
FoxxMD 44ea06020b Use whole numbers for scrobble time diffs for better readability 2020-11-16 11:30:56 -05:00
FoxxMD 89ea2e5ee1 Add badges to readme 2020-11-16 11:29:46 -05:00
FoxxMD 9fb915067e Use max param for returning maloja scrobbles
Still keep since/to in case user is running an older server version

krateng/maloja#53
2020-11-16 11:14:06 -05:00
FoxxMD 55ca8762f2 Improve datetime handling
* Switch to dayjs from date-fns
* Format timestamps and play time to local timezone
2020-11-16 11:12:06 -05:00
FoxxMD ca8589891a Implement index route to display polling status and buffered log data
* (Crude) detection of running poll loop
* Show auth url if not detected (from initialization)
* Output last 50 buffered log statements in descending order

Closes #4
2020-11-16 10:58:03 -05:00
FoxxMD 5383fb8280 Implement back off behavior if listening behavior is idle 2020-11-15 00:03:10 -05:00
FoxxMD 569778d69f Fix path 2020-11-14 20:38:17 -05:00
FoxxMD 9fd769513d Add some docker env var documentation 2020-11-14 20:36:55 -05:00
FoxxMD aa782d3bf7 Move spotify creds file into CONFIG_DIR
* Easier to keep track of all files in one folder in code
* Keeping in CONFIG_DIR means creds file will persist after docker container rebuild on image update
2020-11-14 20:31:14 -05:00
FoxxMD b0fd0c6959 Refactor client config building so that a json file is not required 2020-11-14 20:19:25 -05:00
FoxxMD f05ddd2e3e Improve documentation 2020-11-14 19:44:29 -05:00
FoxxMD 63b73a5ea4 Simplify configuration options and improve configuration documentation
* Removed individual config file location vars in favor of using just CONFIG_DIR or json objects to reduce complexity
* Justified json example comments and removed unused variables
* Tables for env variable documentation in readme (and remove unused)
* Implement LOG_DIR docker volume
* Implement LOG_LEVEL environment for setting global log level
2020-11-14 19:36:02 -05:00
13 changed files with 370 additions and 144 deletions
+5 -1
View File
@@ -5,5 +5,9 @@ Dockerfile
.dockerignore
.gitignore
.git
spotifyCreds.json
config/currentCreds.json
*.log
config/maloja.json
config/spotify.json
config/config.json
/examples
+10 -1
View File
@@ -1,5 +1,9 @@
FROM node:fermium-alpine3.10
ENV TZ=Etc/GMT
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node
WORKDIR /home/node/app
@@ -8,7 +12,7 @@ COPY package*.json ./
USER node
RUN npm install
RUN npm install --production
COPY --chown=node:node . .
@@ -19,6 +23,11 @@ RUN mkdir -p $config_dir
VOLUME $config_dir
ENV CONFIG_DIR=$config_dir
ARG log_dir=/home/node/logs
RUN mkdir -p $log_dir
VOLUME $log_dir
ENV LOG_DIR=$log_dir
ARG webPort=9078
ENV PORT=$webPort
EXPOSE $PORT
+60 -27
View File
@@ -1,65 +1,99 @@
# spotify-scrobbler
[![Latest Release](https://img.shields.io/github/v/release/foxxmd/spotify-scrobbler)](https://github.com/FoxxMD/spotify-scrobbler/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker Pulls](https://img.shields.io/docker/pulls/foxxmd/spotify-scrobbler)](https://hub.docker.com/repository/docker/foxxmd/spotify-scrobbler)
A single-user, javascript app to scrobble your recent plays to [Maloja](https://github.com/krateng/maloja) (and other clients, eventually)
* Includes convenience web server for authorizing your spotify app
* Persists obtained credentials to file
* Automatically refreshes authorization for unattended use
* Implements back off behavior if no listening activity is detected after an interval (after 10 minutes of idle it will back off to a maximum of 5 minutes between checks)
* Displays running status and buffered log through web server
## Installation
### Locally
Clone this repository somewhere and then install from the working directory
```bash
npm install
```
## Setup/Configuration
### [Dockerhub](https://hub.docker.com/repository/docker/foxxmd/spotify-scrobbler)
```
foxxmd/spotify-scrobbler:latest
```
## Setup App and Spotify
All configuration is done through json files or environment variables. Reference the [examples in the config folder](https://github.com/FoxxMD/spotify-scrobbler/tree/master/config) more detailed explanations and structure.
**A property from a json config will override the environmental variable.**
**A property from a json config will override the corresponding environmental variable.**
### General
[JSON config example](https://github.com/FoxxMD/spotify-scrobbler/blob/master/config/config.json.example)
Environment Variables
* CONFIG_DIR - Default `./config` - Sets configuration directory to look for all other configuration files (if they are not specified)
* CONFIG_PATH - Default `CONFIG_DIR/config.json`
* LOG_PATH - Default `true` - If `false` no logs will be written. If `string` will be the directory logs are written to
* PORT - Default 9078 - Port to run web server on (for authentication callbacks)
These environmental variables do not have a config file equivalent (to make Docker configuration easier)
| Environmental Variable | Required? | Default | Description |
|----------------------------|-----------|--------------|-------------------------------------------------------------------------------------------|
| `CONFIG_DIR` | - | `CWD/config` | Directory to look for all other configuration files |
| `LOG_PATH` | - | `CWD/logs` | If `false` no logs will be written. If `string` will be the directory logs are written to |
| `PORT` | - | 9078 | Port to run web server on |
**The app must have permission to write to `CONFIG_DIR` in order to store the current spotify access token.**
### Spotify
To access your Spotify history you must [register an application](https://developer.spotify.com/dashboard) to get a Client ID/Secret. Make sure to also whitelist your redirect URI in the application settings.
[Spotify config example](https://github.com/FoxxMD/spotify-scrobbler/blob/master/config/spotify.json.example)
Environment Variables
* SPOTIFY_CONFIG_PATH - Optional, defaults to `CONFIG_DIR/spotify.json`
* SPOTIFY_CLIENT_ID - **Required**
* SPOTIFY_CLIENT_SECRET - **Required**
* SPOTIFY_ACCESS_TOKEN - Optional if client/secret provided
* SPOTIFY_REFRESH_TOKEN - Optional if client/secret provided
* SPOTIFY_REDIRECT_URI - Optional, default is `http://localhost:{port}/callback`
All variables have a config file equivalent which will overwrite the ENV variable if present (so config file is not required if ENVs present)
| Environmental Variable | Required? | Default | Description |
|----------------------------|-----------|----------------------------------|----------------------------------------------------|
| `SPOTIFY_CLIENT_ID` | Yes | | |
| `SPOTIFY_CLIENT_SECRET` | Yes | | |
| `SPOTIFY_ACCESS_TOKEN` | - | | Must include either this token or client id/secret |
| `SPOTIFY_REFRESH_TOKEN` | - | | |
| `SPOTIFY_REDIRECT_URI` | - | `http://localhost:{PORT}/callback` | URI must end in `callback` |
The app will automatically obtain new access/refresh token if needed and possible. These will override values from configuration.
## Setup Scrobble Clients
At least one client (the only one right now...) must be setup in order for the app to work. Client configurations can alternatively be configred in the main `config.json` configuration (see configuration example linked in **General** setup)
### Maloja
[Maloja config example](https://github.com/FoxxMD/spotify-scrobbler/blob/master/config/maloja.json.example)
Environment Variables
* MALOJA_CONFIG_PATH - Optional, defaults to `CONFIG_DIR/maloja.json`
* MALOJA_URL - **Required** - Base Url of your Maloja installation
* MALOJA_API_KEY - **Required** - Api Key for scrobbling
All variables have a config file equivalent which will overwrite the ENV variable if present (so config file is not required if ENVs present)
| Environmental Variable | Required? | Default | Description |
|----------------------------|-----------|---------|-------------------------------|
| `MALOJA_URL` | Yes | | Base URL of your installation |
| `MALOJA_API_KEY` | Yes | | Api Key |
## Usage
Output is provided to stdout/stderr as well as file if specified in configuration.
On first startup you may need to authroize Spotify by visiting a callback URL. The default url to open is:
On first startup you may need to authorize Spotify by visiting a callback URL. The default url to open is:
```
https://localhost:9078/authSpotify
```
Connection status and a buffered log of the last 50 events can be viewed at the root url: `https://localhost:9078`
### Running Directly
```
@@ -68,16 +102,15 @@ node index.js
### Docker
[Docker repository](https://hub.docker.com/repository/docker/foxxmd/spotify-scrobbler)
```
foxxmd/spotify-scrobbler:latest
```
| Environmental Variable | Type | Default |
|------------------------|--------|-------------------------|
| `CONFIG_DIR` | Volume | `/home/node/app/config` |
| `LOG_DIR` | Volume | `/home/node/app/logs` |
| `PORT` | Port | 9078 |
Minimal configuration requires you to bind a host directory for the configuration directory in the container:
## Examples
```
docker run ... -v /path/on/host/config:/home/node/config ...
```
[See minimal configuration examples in the examples folder](https://github.com/FoxxMD/spotify-scrobbler/tree/master/examples)
## License
+4 -3
View File
@@ -1,6 +1,6 @@
import ScrobbleClient from "./ScrobbleClient.js";
import request from 'superagent';
import format from 'date-fns/format/index.js';
import dayjs from 'dayjs';
export default class MalojaScrobbler extends ScrobbleClient {
@@ -8,7 +8,8 @@ export default class MalojaScrobbler extends ScrobbleClient {
refreshScrobbles = async () => {
const {url} = this.config;
const resp = await request.get(`${url}/apis/mlj_1/scrobbles?since=${format(new Date(), 'yyyy/MM/dd')}&to=${format(new Date(), 'yyyy/MM/dd')}`)
const today = dayjs().format('YYYY/MM/DD');
const resp = await request.get(`${url}/apis/mlj_1/scrobbles?since=${today}&to=${today}&max=15`);
this.recentScrobbles = resp.body.list.slice(0, 10);
this.lastScrobbleCheck = new Date();
}
@@ -32,7 +33,7 @@ export default class MalojaScrobbler extends ScrobbleClient {
this.logger.debug(`Scrobble with same name found and the play (start time) vs. scrobble time diff was smaller than 10 seconds`, {label: this.name});
return true;
}
this.logger.debug(`Scrobble with same name found but the start/finish times vs scrobble time diffs were too large to consider dups (Start Diff ${scrobblePlayStartDiff}s) (End Diff ${scrobblePlayDiff}s)`, {label: this.name});
this.logger.debug(`Scrobble with same name found but the start/finish times vs scrobble time diffs were too large to consider dups (Start Diff ${scrobblePlayStartDiff.toFixed(0)}s) (End Diff ${scrobblePlayDiff.toFixed(0)}s)`, {label: this.name});
return false;
}
return false;
+6 -17
View File
@@ -1,23 +1,12 @@
{
// string specifies a directory location to log to.
// boolean FALSE means do not log to file
// boolean TRUE means log to working directory
"logPath": true, // optional
"interval": 60, // optional, number of seconds to wait before checking spotify for new tracks
"port": 9078, // optional, port for server to start on
"spotify": 'object or string', // can specify full spotify config object here OR a string location of json file OR do not include to use config dir location
// array of client configurations
"clients": [
"interval": 60, // optional, number of seconds to wait before checking spotify for new tracks
"spotify": {}, // optional, may specify config here, or in CONFIG_DIR/spotify.json, or as ENV vars
"clients": [ // may specify clients as objects, or in CONFIG_DIR/{clientType}.json, or as ENV vars
// EX CONFIG_DIR/maloja.json
{
"type": "maloja", // client name
"data": "/someLocation/aFile.json" // location to look for json configuration in
"data": {} // config data
},
{
"type": "maloja", // client name
"data": {
// client configuration can also be an object
}
},
"maloja" // client can also be just the type if the configuration is located in the default configuration directory
]
}
+1 -1
View File
@@ -1,4 +1,4 @@
{
"url": "https://domain.tld", // the base url of your maloja installation
"apiKey": "string" // your maloja api key
"apiKey": "string" // your maloja api key
}
+3 -4
View File
@@ -1,6 +1,5 @@
{
"clientId": "string", // spotify client id
"clientSecret": "string", // spotify client secret
"redirectUri": "http://localhost:9078/callback", // optional, spotify callback url. useful if this installation is behind a reverse proxy
"callbackPath": "callback" // optional, if redirectUri does not end with /callback then must specify path here
"clientId": "string", // spotify client id
"clientSecret": "string", // spotify client secret
"redirectUri": "http://localhost:9078/callback", // optional, spotify redirect URI. Specify only if not the default. URI must end in "callback"
}
+36
View File
@@ -0,0 +1,36 @@
# Minimal Configuration
Examples assume you have registered a Spotify application with the default callback url of `http://localhost:9078/callback`.
If you use another callback url or domain name you will need to specify at a minimum `SPOTIFY_REDIRECT_URI`.
## Using Environmental Variables
### Local
```
SPOTIFY_CLIENT_ID=yourId SPOTIFY_CLIENT_SECRET=yourSecret MALOJA_URL=http://domain.tld MALOJA_API_KEY=1234 node index.js
```
### Dockerhub
Note: I do not recommend running a container without a `config` volume specified or you will need to reauthorize the app everytime the container is rebuilt.
```
docker run -e "SPOTIFY_CLIENT_ID=yourId" -e "SPOTIFY_CLIENT_SECRET=yourSecret" -e "MALOJA_URL=http://domain.tld" -e "MALOJA_API_KEY=1234" foxxmd/spotify-scrobbler
```
## Using Configuration
Reference the [example json configs.](https://github.com/FoxxMD/spotify-scrobbler/tree/master/config)
### Local
```
node index.js
```
### Docker
```
docker run -v /path/on/host/config:/home/node/app/config foxxmd/spotify-scrobbler
```
+124 -84
View File
@@ -2,46 +2,75 @@ import fs from "fs";
import {addAsync} from '@awaitjs/express';
import express from 'express';
import winston from 'winston';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
import {Writable} from 'stream';
import 'winston-daily-rotate-file';
import {readJson, sleep, writeFile, buildTrackString} from "./utils.js";
import SpotifyWebApi from "spotify-web-api-node";
import MalojaScrobbler from "./clients/MalojaScrobbler.js";
dayjs.extend(utc)
const {format, createLogger, transports} = winston;
const {combine, printf, timestamp} = format;
let output = []
const stream = new Writable()
stream._write = (chunk, encoding, next) => {
output.unshift(chunk.toString());
output.unshift(chunk.toString().replace('\n', ''));
output = output.slice(0, 51);
next()
}
const streamTransport = new winston.transports.Stream({stream})
const streamTransport = new winston.transports.Stream({
stream,
level: process.env.LOG_LEVEL || 'info',
})
const logPath = process.env.LOG_DIR || `${process.cwd()}/logs`;
const port = process.env.PORT ?? 9078;
const localUrl = `http://localhost:${port}`;
const myFormat = printf(({level, message, label = 'App', timestamp}) => {
return `${timestamp} [${label}] ${level}: ${message}`;
});
const logger = createLogger({
level: 'debug',
level: process.env.LOG_LEVEL || 'info',
format: combine(
timestamp(),
timestamp(
{
format: () => dayjs().local().format(),
}
),
myFormat
),
transports: [
new transports.Console(),
new transports.Console({
level: process.env.LOG_LEVEL || 'info',
}),
streamTransport,
]
});
if (typeof logPath === 'string') {
logger.add(new winston.transports.DailyRotateFile({
level: process.env.LOG_LEVEL || 'info',
dirname: logPath,
createSymlink: true,
symlinkName: 'scrobble-current.log',
filename: 'scrobble-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '5m'
}))
}
const scopes = ['user-read-recently-played', 'user-read-currently-playing'];
const state = 'random';
let lastTrackPlayedAt = new Date();
let lastTrackPlayedAt = undefined;
const configDir = process.env.CONFIG_DIR || `${process.cwd()}/config`;
const configLocation = process.env.CONFIG_PATH || `${configDir}/config.json`;
const workingCredentialsPath = `${configDir}/currentCreds.json`;
const app = addAsync(express());
@@ -53,58 +82,35 @@ try {
// try to read a configuration file
let config = {};
try {
config = await readJson(configLocation);
config = await readJson(`${configDir}/config.json`);
} catch (e) {
logger.warn('Could not read config file');
logger.error(e);
logger.info('No config file or could not be read (normal if using ENV vars only)');
}
// setup defaults for other configs and general config
const {
logPath: logPathRaw = process.env.LOG_PATH || true,
interval = 60,
port = process.env.PORT ?? 9078,
spotify: spotifyConfigRaw = process.env.SPOTIFY_CONFIG_PATH || `${configDir}/spotify.json`,
spotify,
clients = [],
} = config || {};
const localUrl = `http://localhost:${port}`;
// first thing, if user wants to log to file set it up now
if (logPathRaw !== false) {
let logPath = `${process.cwd()}/logs`;
if (typeof logPathRaw === 'string') {
logPath = logPathRaw;
}
logger.add(new winston.transports.DailyRotateFile({
level: 'info', // don't need to add a bunch of noise to files
dirname: logPath,
createSymlink: true,
symlinkName: 'scrobble-current.log',
filename: 'scrobble-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: '5m'
}))
}
if (interval < 15) {
console.warn('Interval should be above 30 seconds...😬');
}
let spotifyCreds = {};
try {
spotifyCreds = await readJson('./spotifyCreds.json');
spotifyCreds = await readJson(workingCredentialsPath);
} catch (e) {
logger.warn('Current spotify access token was not parsable or file does not exist (this could be normal)');
}
let spotifyConfig = spotifyConfigRaw;
if (typeof spotifyConfigRaw === 'string') {
let spotifyConfig = spotify;
if (spotify === undefined) {
try {
spotifyConfig = await readJson(spotifyConfigRaw);
spotifyConfig = await readJson(`${configDir}/spotify.json`);
} catch (e) {
logger.warn('Could not read spotify config file');
logger.error(e);
logger.warn('No spotify config file or could not be read (normal if using ENV vars only)');
}
}
@@ -143,6 +149,13 @@ try {
throw new Error('No scrobble clients were configured');
}
app.getAsync('/', async function (req, res) {
res.render('status', {
status: spotifyAsyncFunc !== null ? 'Connected' : 'Awaiting Authorization',
authUrl: spotifyAsyncFunc !== null ? null : `${localUrl}/authSpotify`,
logs: output
});
})
app.getAsync('/authSpotify', async function (req, res) {
logger.info('Redirecting to spotify authorization url');
@@ -154,13 +167,13 @@ try {
res.send('OK');
});
app.getAsync(`/callback`, async function (req, res, next) {
app.getAsync(/.*callback$/, async function (req, res, next) {
const {error, code} = req.query;
if (error === undefined) {
const tokenResponse = await spotifyApi.authorizationCodeGrant(code);
spotifyApi.setAccessToken(tokenResponse.body['access_token']);
spotifyApi.setRefreshToken(tokenResponse.body['refresh_token']);
await writeFile('spotifyCreds.json', JSON.stringify({
await writeFile(workingCredentialsPath, JSON.stringify({
token: tokenResponse.body['access_token'],
refreshToken: tokenResponse.body['refresh_token']
}));
@@ -180,6 +193,8 @@ try {
logger.info(`Server started at ${localUrl}`);
}
app.set('views', './views');
app.set('view engine', 'ejs');
const server = await app.listen(port)
}());
} catch (e) {
@@ -190,6 +205,7 @@ try {
const pollSpotify = async function (spotifyApi, interval = 60, clients = []) {
logger.info('Starting spotify polling', {label: 'Spotify'});
try {
let checkCount = 0;
while (true) {
let data = {};
logger.debug('Refreshing recently played', {label: 'Spotify'})
@@ -199,31 +215,45 @@ const pollSpotify = async function (spotifyApi, interval = 60, clients = []) {
});
} catch (e) {
if (e.statusCode === 401) {
logger.info('Access token was not valid, attempting to refresh', {label: 'Spotify'});
const tokenResponse = await spotifyApi.refreshAccessToken();
const {body: {
access_token,
// spotify may return a new refresh token
// if it doesn't then continue to use the last refresh token we received
refresh_token = spotifyApi.getRefreshToken(),
} = {}} = tokenResponse;
spotifyApi.setAccessToken(access_token);
await writeFile('spotifyCreds.json', JSON.stringify({
token: access_token,
refreshToken: refresh_token,
}));
data = await spotifyApi.getMyRecentlyPlayedTracks({
limit: 20
});
if (spotifyApi.getRefreshToken() === undefined) {
throw new Error('Access token was not valid and no refresh token was present, bailing out of polling')
}
logger.debug('Access token was not valid, attempting to refresh', {label: 'Spotify'});
try {
const tokenResponse = await spotifyApi.refreshAccessToken();
const {
body: {
access_token,
// spotify may return a new refresh token
// if it doesn't then continue to use the last refresh token we received
refresh_token = spotifyApi.getRefreshToken(),
} = {}
} = tokenResponse;
spotifyApi.setAccessToken(access_token);
await writeFile(workingCredentialsPath, JSON.stringify({
token: access_token,
refreshToken: refresh_token,
}));
data = await spotifyApi.getMyRecentlyPlayedTracks({
limit: 20
});
} catch (err) {
logger.error('Refreshing access token encountered an error', {label: 'Spotify'});
throw err;
}
} else {
throw e;
}
}
checkCount++;
let newLastPLayedAt = undefined;
const now = new Date();
for (const playObj of data.body.items) {
const {track: {name: trackName, duration_ms }, played_at} = playObj;
const {track: {name: trackName, duration_ms}, played_at} = playObj;
const playDate = new Date(played_at);
if (lastTrackPlayedAt === undefined) {
lastTrackPlayedAt = playDate;
}
// compare play time to most recent track played_at scrobble
if (playDate.getTime() > lastTrackPlayedAt.getTime()) {
logger.info(`New Track => ${buildTrackString(playObj)}`, {label: 'Spotify'});
@@ -254,9 +284,22 @@ const pollSpotify = async function (spotifyApi, interval = 60, clients = []) {
lastTrackPlayedAt = newLastPLayedAt;
}
}
let sleepTime = interval;
// don't need to do back off calc if interval is 10 minutes or greater since its already pretty light on API calls
// and don't want to back off if we just started the app
if (checkCount > 5 && sleepTime < 600) {
const lastPlayToNowSecs = Math.abs(now.getTime() - lastTrackPlayedAt.getTime()) / 1000;
// back off if last play was longer than 10 minutes ago
const backoffThreshold = Math.min((interval * 10), 600);
if (lastPlayToNowSecs >= backoffThreshold) {
// back off to a maximum of 5 minutes
sleepTime = Math.min(interval * 5, 300);
}
}
// sleep for interval
logger.debug(`Sleeping for interval (${interval}s)`, {label: 'Spotify'});
await sleep(interval * 1000);
logger.debug(`Sleeping for interval (${sleepTime}s)`, {label: 'Spotify'});
await sleep(sleepTime * 1000);
}
} catch (e) {
logger.error('Error occurred while in spotify polling loop', {label: 'Spotify'});
@@ -267,37 +310,34 @@ const pollSpotify = async function (spotifyApi, interval = 60, clients = []) {
const createClients = async function (clientConfigs = [], configDir = '.') {
const clients = [];
for (const config of clientConfigs) {
const dataType = typeof config;
if (!['object', 'string'].includes(dataType)) {
throw new Error('All client configs must be objects or strings');
}
const clientType = dataType === 'string' ? config : config.type;
if (!clientConfigs.every(x => typeof x === 'object')) {
throw new Error('All client from config json must be objects');
}
for (const clientType of ['maloja']) {
let clientConfig = {};
switch (clientType) {
case 'maloja':
let data = `${configDir}/maloja.json`;
if (dataType === 'object') {
const {data: dataProp = process.env.MALOJA_CONFIG_PATH || `${configDir}/maloja.json`} = config;
data = dataProp;
}
let malojaConfig;
if (typeof data === 'string') {
clientConfig = clientConfigs.find(x => x.type === 'maloja') || {
url: process.env.MALOJA_URL,
apiKey: process.env.MALOJA_API_KEY
};
if (Object.values(clientConfig).every(x => x === undefined)) {
try {
malojaConfig = await readJson(data);
clientConfig = await readJson(`${configDir}/maloja.json`);
} catch (e) {
logger.warn('Maloja config was not parsable or file does not exist');
// no config exists, skip this client
continue;
}
} else {
malojaConfig = data;
}
const {
url = process.env.MALOJA_URL,
apiKey = process.env.MALOJA_API_KEY
} = malojaConfig;
if (url === undefined && apiKey === undefined) {
// the user probably didn't set anything up for this client at all, don't log
continue;
}
url,
apiKey
} = clientConfig;
if (url === undefined) {
logger.warn('Maloja url not found in config');
continue;
@@ -306,7 +346,7 @@ const createClients = async function (clientConfigs = [], configDir = '.') {
logger.warn('Maloja api key not found in config');
continue;
}
clients.push(new MalojaScrobbler(logger, malojaConfig));
clients.push(new MalojaScrobbler(logger, clientConfig));
break;
default:
break;
+101 -4
View File
@@ -28,6 +28,14 @@
"negotiator": "0.6.2"
}
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"requires": {
"color-convert": "^1.9.0"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -43,6 +51,11 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
@@ -80,11 +93,30 @@
}
}
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"color": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz",
@@ -143,6 +175,11 @@
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg=="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
@@ -183,10 +220,10 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"date-fns": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.16.1.tgz",
"integrity": "sha512-sAJVKx/FqrLYHAQeN7VpJrPhagZc9R4ImZIWYRFZaaohR3KzmuK88touwsSwSVT8Qcbd4zoDsnGfX4GFB4imyQ=="
"dayjs": {
"version": "1.9.6",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.9.6.tgz",
"integrity": "sha512-HngNLtPEBWRo8EFVmHFmSXAjtCX8rGNqeXQI0Gh7wCTSqwaKgPIDqu9m07wABVopNwzvOeCb+2711vQhDlcIXw=="
},
"debug": {
"version": "4.2.0",
@@ -216,6 +253,14 @@
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"ejs": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.5.tgz",
"integrity": "sha512-dldq3ZfFtgVTJMLjOe+/3sROTzALlL9E34V4/sDtUd/KlBSS0s6U1/+WPE1B4sj9CXHJpL1M6rhNJnc9Wbal9w==",
"requires": {
"jake": "^10.6.1"
}
},
"enabled": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
@@ -231,6 +276,11 @@
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -316,6 +366,14 @@
"moment": "^2.11.2"
}
},
"filelist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.1.tgz",
"integrity": "sha512-8zSK6Nu0DQIC08mUC46sWGXi+q3GGpKydAG36k+JDba6VRpkevvOWUW5a/PhShij4+vHT9M+ghgG7eM+a9JDUQ==",
"requires": {
"minimatch": "^3.0.4"
}
},
"finalhandler": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
@@ -375,6 +433,11 @@
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
@@ -427,6 +490,24 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"jake": {
"version": "10.8.2",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz",
"integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==",
"requires": {
"async": "0.9.x",
"chalk": "^2.4.2",
"filelist": "^1.0.1",
"minimatch": "^3.0.4"
},
"dependencies": {
"async": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz",
"integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0="
}
}
},
"kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
@@ -477,6 +558,14 @@
"mime-db": "1.44.0"
}
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
@@ -698,6 +787,14 @@
"semver": "^7.3.2"
}
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"requires": {
"has-flag": "^3.0.0"
}
},
"text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
+2 -1
View File
@@ -23,7 +23,8 @@
"homepage": "https://github.com/FoxxMD/maloja-spotify-scrobbler#readme",
"dependencies": {
"@awaitjs/express": "^0.6.3",
"date-fns": "^2.16.1",
"dayjs": "^1.9.6",
"ejs": "^3.1.5",
"express": "^4.17.1",
"spotify-web-api-node": "^5.0.0",
"superagent": "^6.1.0",
+5 -1
View File
@@ -1,4 +1,8 @@
import fs, {promises, constants} from "fs";
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
dayjs.extend(utc);
export async function readJson(path) {
await promises.access(path, constants.R_OK);
@@ -62,5 +66,5 @@ export const buildTrackString = (obj) => {
played_at
} = obj;
let artistString = artists.reduce((acc, curr) => acc.concat(curr.name), []).join(' / ');
return `${artistString} - ${name}, played at ${played_at}`
return `${artistString} - ${name}, played at ${dayjs(played_at).local().format()}`
}
+13
View File
@@ -0,0 +1,13 @@
<html>
<body>
<h2>Status: <%= status %><b></b></h2>
<% if (authUrl) { %>
Visit <a target="_blank" href="<%= authUrl %>">Redirect URL to start polling</a>
<% } %>
<h2>Log (Last 50, most recent first)</h2>
<pre>
<% logs.forEach(function (logEntry){ %>
<%=logEntry%><% }) %>
</pre>
</body>
</html>