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:
FoxxMD
2020-11-25 14:09:56 -05:00
parent a49212ea24
commit 8a46890b20
3 changed files with 152 additions and 42 deletions
+86 -11
View File
@@ -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;
}
+25 -7
View File
@@ -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
View File
@@ -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 = () => {