mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-09-03 05:10:00 +03:00
Refactor/improve error handling for configuration in spotify and maloja
* Implement api wrapping for maloja and handle formatting error * Implement testing maloja connection to make sure configuration is valid (check server info and test endpoint) * Provide better defaults for maloja scrobbles list when empty (maybe fixes #5) * Better formatting for maloja scrobble api calls * Better handling of maloja and spotify configuration issues during initialization (And logging for it)
This commit is contained in:
+86
-11
@@ -31,13 +31,86 @@ export default class MalojaScrobbler extends AbstractScrobbleClient {
|
||||
}
|
||||
}
|
||||
|
||||
callApi = async (req) => {
|
||||
try {
|
||||
return await req;
|
||||
} catch (e) {
|
||||
const {
|
||||
message,
|
||||
response: {
|
||||
status,
|
||||
body,
|
||||
text,
|
||||
} = {},
|
||||
response,
|
||||
} = e;
|
||||
let msg = response !== undefined ? `API Call failed: Server Response => ${message}` : `API Call failed: ${message}`;
|
||||
const responseMeta = body ?? text;
|
||||
this.logger.error(msg, {status, response: responseMeta});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
testConnection = async () => {
|
||||
|
||||
const {url, apiKey} = this.config;
|
||||
try {
|
||||
const serverInfoResp = await this.callApi(request.get(`${url}/apis/mlj_1/serverinfo`));
|
||||
const {
|
||||
body: {
|
||||
version = [],
|
||||
versionstring = '',
|
||||
} = {},
|
||||
} = serverInfoResp;
|
||||
if (version.length === 0) {
|
||||
this.logger.error('Server did not respond with a version. Either the base URL is incorrect or this Maloja server is too old :(');
|
||||
return false;
|
||||
}
|
||||
this.logger.info(`Maloja Server Version: ${versionstring}`);
|
||||
if (version[0] < 2 || version[1] < 7) {
|
||||
this.logger.warn('Maloja Server Version is less than 2.7, please upgrade to ensure compatibility');
|
||||
}
|
||||
|
||||
const resp = await this.callApi(request
|
||||
.get(`${url}/apis/mlj_1/test`)
|
||||
.query({key: apiKey}));
|
||||
|
||||
const {
|
||||
status,
|
||||
body: {
|
||||
status: bodyStatus,
|
||||
} = {},
|
||||
body = {},
|
||||
text = '',
|
||||
} = resp;
|
||||
if (bodyStatus.toLocaleLowerCase() === 'ok') {
|
||||
this.logger.info('Test connection succeeded!');
|
||||
return true;
|
||||
}
|
||||
this.logger.error('Testing connection failed => Server Response body was malformed -- should have returned "status: ok"...is the URL correct?', {
|
||||
status,
|
||||
body,
|
||||
text: text.slice(0, 50)
|
||||
})
|
||||
return false;
|
||||
} catch (e) {
|
||||
this.logger.error('Testing connection failed');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
refreshScrobbles = async () => {
|
||||
if (this.refreshEnabled) {
|
||||
const {url} = this.config;
|
||||
const resp = await request.get(`${url}/apis/mlj_1/scrobbles?max=20`);
|
||||
this.recentScrobbles = resp.body.list.map(x => MalojaScrobbler.formatPlayObj(x)).sort(sortByPlayDate);
|
||||
const [{data: {playDate: newestScrobbleTime = dayjs()}} = {}] = this.recentScrobbles.slice(-1);
|
||||
const [{data: {playDate: oldestScrobbleTime = dayjs()}} = {}] = this.recentScrobbles.slice(0, 1);
|
||||
const resp = await this.callApi(request.get(`${url}/apis/mlj_1/scrobbles?max=20`));
|
||||
const {
|
||||
body: {
|
||||
list = [],
|
||||
} = {},
|
||||
} = resp;
|
||||
this.recentScrobbles = list.map(x => MalojaScrobbler.formatPlayObj(x)).sort(sortByPlayDate);
|
||||
const [{data: {playDate: newestScrobbleTime = dayjs()} = {}} = {}] = this.recentScrobbles.slice(-1);
|
||||
const [{data: {playDate: oldestScrobbleTime = dayjs()} = {}} = {}] = this.recentScrobbles.slice(0, 1);
|
||||
this.newestScrobbleTime = newestScrobbleTime;
|
||||
this.oldestScrobbleTime = oldestScrobbleTime;
|
||||
}
|
||||
@@ -49,7 +122,7 @@ export default class MalojaScrobbler extends AbstractScrobbleClient {
|
||||
}
|
||||
|
||||
existingScrobble = (playObj) => {
|
||||
if (false === this.checkExistingScrobbles) {
|
||||
if (false === this.checkExistingScrobbles || this.recentScrobbles.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -96,7 +169,7 @@ export default class MalojaScrobbler extends AbstractScrobbleClient {
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (existingScrobble && largeDiffs.length > 0) {
|
||||
if (existingScrobble === undefined && largeDiffs.length > 0) {
|
||||
this.logger.debug('Scrobbles with same name detected but play diff and scrobble diffs were too large to consider dups.');
|
||||
for (const diff of largeDiffs) {
|
||||
this.logger.debug(`Scrobble: ${diff.title} | Played At ${playDate.local().format()} | End Diff ${diff.endTimeDiff.toFixed(0)}s | Start Diff ${diff.startTimeDiff === undefined ? 'N/A' : `${diff.startTimeDiff.toFixed(0)}s`}`);
|
||||
@@ -121,8 +194,10 @@ export default class MalojaScrobbler extends AbstractScrobbleClient {
|
||||
} = {}
|
||||
} = playObj;
|
||||
|
||||
const sType = newFromSource ? 'New' : 'Backlog';
|
||||
|
||||
try {
|
||||
await request.post(`${url}/apis/mlj_1/newscrobble`)
|
||||
await this.callApi(request.post(`${url}/apis/mlj_1/newscrobble`)
|
||||
.type('json')
|
||||
.send({
|
||||
artist,
|
||||
@@ -130,14 +205,14 @@ export default class MalojaScrobbler extends AbstractScrobbleClient {
|
||||
album,
|
||||
key: apiKey,
|
||||
time: playDate.unix(),
|
||||
});
|
||||
}));
|
||||
if (newFromSource) {
|
||||
this.logger.info(`Scrobbled Newly Found Track (${source}): ${buildTrackString(playObj)}`);
|
||||
this.logger.info(`Scrobbled (New) => (${source}) ${buildTrackString(playObj)}`);
|
||||
} else {
|
||||
this.logger.info(`Scrobbled Backlogged Track (${source}): ${buildTrackString(playObj)}`);
|
||||
this.logger.info(`Scrobbled (Backlog) => (${source}) ${buildTrackString(playObj)}`);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error('Error while scrobbling', { playInfo: buildTrackString(playObj) });
|
||||
this.logger.error(`Scrobble Error (${sType})`, {playInfo: buildTrackString(playObj)});
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,19 +23,27 @@ export default class ScrobbleClients {
|
||||
|
||||
switch (clientType) {
|
||||
case 'maloja':
|
||||
clientConfig = clientConfigs.find(x => x.type === 'maloja') || {
|
||||
this.logger.debug('Attempting Maloja initialization...');
|
||||
const configObj = clientConfigs.find((x = {}) => x.type === 'maloja');
|
||||
const {data} = configObj || {};
|
||||
clientConfig = data || {
|
||||
url: process.env.MALOJA_URL,
|
||||
apiKey: process.env.MALOJA_API_KEY
|
||||
};
|
||||
|
||||
if (Object.values(clientConfig).every(x => x === undefined)) {
|
||||
const filePath = `${configDir}/maloja.json`;
|
||||
try {
|
||||
clientConfig = await readJson(`${configDir}/maloja.json`);
|
||||
clientConfig = await readJson(filePath, {throwOnNotFound: false});
|
||||
} catch (e) {
|
||||
// no config exists, skip this client
|
||||
this.logger.warn(`Maloja config file could not be read, skipping initialization`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (clientConfig === undefined) {
|
||||
this.logger.warn('No config data passed for Maloja and no config file could be found, skipping initialization');
|
||||
continue;
|
||||
}
|
||||
|
||||
const {
|
||||
url,
|
||||
@@ -43,14 +51,20 @@ export default class ScrobbleClients {
|
||||
} = clientConfig;
|
||||
|
||||
if (url === undefined) {
|
||||
this.logger.warn('Maloja url not found in config');
|
||||
this.logger.warn('Maloja url not found in config, not initializing');
|
||||
continue;
|
||||
}
|
||||
if (apiKey === undefined) {
|
||||
this.logger.warn('Maloja api key not found in config');
|
||||
continue;
|
||||
this.logger.warn('Maloja api key not found in config! Client will most likely fail when trying to scrobble');
|
||||
}
|
||||
const mj = new MalojaScrobbler(clientConfig);
|
||||
const testSuccess = await mj.testConnection();
|
||||
if (testSuccess === false) {
|
||||
this.logger.warn('Maloja client not initialized due to failure during connection testing');
|
||||
} else {
|
||||
this.logger.info('Maloja client initialized');
|
||||
clients.push(mj);
|
||||
}
|
||||
clients.push(new MalojaScrobbler(clientConfig));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -68,6 +82,10 @@ export default class ScrobbleClients {
|
||||
|
||||
const tracksScrobbled = [];
|
||||
|
||||
if (this.clients.length === 0) {
|
||||
this.logger.warn('Cannot scrobble! No clients are configured.');
|
||||
}
|
||||
|
||||
for (const client of this.clients) {
|
||||
try {
|
||||
if (forceRefresh || client.scrobblesLastCheckedAt().unix() < checkTime.unix()) {
|
||||
|
||||
+41
-24
@@ -75,19 +75,21 @@ export default class SpotifySource {
|
||||
|
||||
buildSpotifyApi = async (spotifyObj) => {
|
||||
|
||||
this.logger.debug('Initializing Spotify source');
|
||||
|
||||
let spotifyCreds = {};
|
||||
try {
|
||||
spotifyCreds = await readJson(this.workingCredsPath);
|
||||
spotifyCreds = await readJson(this.workingCredsPath, {throwOnNotFound: false});
|
||||
} catch (e) {
|
||||
this.logger.warn('Current spotify access token was not parsable or file does not exist (this could be normal)');
|
||||
this.logger.warn('Current spotify credentials file exists but could not be parsed');
|
||||
}
|
||||
|
||||
let spotifyConfig = spotifyObj;
|
||||
if (spotifyObj === undefined) {
|
||||
try {
|
||||
spotifyConfig = await readJson(`${this.configDir}/spotify.json`);
|
||||
spotifyConfig = await readJson(`${this.configDir}/spotify.json`, {throwOnNotFound: false});
|
||||
} catch (e) {
|
||||
this.logger.warn('No spotify config file or could not be read (normal if using ENV vars only)');
|
||||
this.logger.warn('Spotify config file exists but could not be parsed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,36 +110,51 @@ export default class SpotifySource {
|
||||
const rdUri = redirectUri || `${this.localUrl}/callback`;
|
||||
|
||||
|
||||
const {token = accessToken, refreshToken: rt = refreshToken} = spotifyCreds;
|
||||
const {token = accessToken, refreshToken: rt = refreshToken} = spotifyCreds || {};
|
||||
|
||||
const apiConfig = {
|
||||
clientId,
|
||||
clientSecret,
|
||||
accessToken: token,
|
||||
redirectUri: rdUri,
|
||||
refreshToken: rt,
|
||||
}
|
||||
|
||||
if (Object.values(apiConfig).every(x => x === undefined)) {
|
||||
this.logger.info('No values found for Spotify configuration, assuming user does not want to set it up');
|
||||
} else {
|
||||
if (token === undefined) {
|
||||
let ready = true;
|
||||
if (clientId === undefined) {
|
||||
this.logger.warn('No access token exists and clientId is not defined');
|
||||
ready = false;
|
||||
}
|
||||
if (clientSecret === undefined) {
|
||||
this.logger.warn('No access token exists and clientSecret is not defined')
|
||||
ready = false;
|
||||
}
|
||||
if (ready === false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.spotifyApi = new SpotifyWebApi(apiConfig);
|
||||
this.logger.info('No values found for Spotify configuration, skipping initialization');
|
||||
return;
|
||||
}
|
||||
apiConfig.redirectUri = rdUri;
|
||||
|
||||
const validationErrors = [];
|
||||
|
||||
if (token === undefined) {
|
||||
if (clientId === undefined) {
|
||||
validationErrors.push('clientId must be defined when access token is not present');
|
||||
}
|
||||
if (clientSecret === undefined) {
|
||||
validationErrors.push('clientSecret must be defined when access token is not present');
|
||||
}
|
||||
if (rdUri === undefined) {
|
||||
validationErrors.push('redirectUri must be defined when access token is not present');
|
||||
}
|
||||
if (validationErrors.length !== 0) {
|
||||
validationErrors.unshift('no access token is defined');
|
||||
}
|
||||
} else if (rt === undefined && (
|
||||
clientId === undefined ||
|
||||
clientSecret === undefined ||
|
||||
rdUri === undefined
|
||||
)) {
|
||||
this.logger.warn('Access token is present but no refresh token is defined and remaining configuration is not sufficient to re-authorize. Without a refresh token API calls will fail after current token is expired.');
|
||||
}
|
||||
|
||||
if (validationErrors.length !== 0) {
|
||||
this.logger.warn(`Spotify configuration was not valid:\n*${validationErrors.join('\n')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info('Spotify source initialized');
|
||||
this.spotifyApi = new SpotifyWebApi(apiConfig);
|
||||
}
|
||||
|
||||
createAuthUrl = () => {
|
||||
|
||||
Reference in New Issue
Block a user