mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-09-03 05:10:00 +03:00
feat: Simplify component state updates for start/stop/restart and replace error usage
This commit is contained in:
@@ -7,7 +7,7 @@ import {MONITORING_ORIGIN_SYSTEM, MONITORING_ORIGIN_USER, type ComponentType, ty
|
||||
import { buildTrackString } from "../../core/StringUtils.ts";
|
||||
import type {CommonClientConfig} from "./infrastructure/config/client/index.ts";
|
||||
import type {CommonSourceConfig} from "./infrastructure/config/source/index.ts";
|
||||
import { mergeSimpleError, SimpleError, SkipTransformStageError, StagePrerequisiteError, StageTransformError, TransformRulesError } from "./errors/MSErrors.ts";
|
||||
import { mergeSimpleError, SimpleError, SkipTransformStageError, StageChangeError, StagePrerequisiteError, StageTransformError, TransformRulesError } from "./errors/MSErrors.ts";
|
||||
import {
|
||||
FLOW_CONTROL_TERM,
|
||||
type PlayTransformRules,
|
||||
@@ -229,7 +229,9 @@ export default abstract class AbstractComponent extends AbstractInitializable {
|
||||
await this.stop(opts);
|
||||
await this.start(opts);
|
||||
} catch (e) {
|
||||
throw new Error('Failed to restart', { cause: e });
|
||||
const err = new StageChangeError('Failed to restart', { cause: e });
|
||||
this.replaceErrors(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,10 @@ export class PostInitError extends StageError {
|
||||
}
|
||||
addKnownErrorConstructor(PostInitError);
|
||||
|
||||
export class StageChangeError extends NamedError {
|
||||
override name = 'Stage Change';
|
||||
}
|
||||
|
||||
const STACK_AT_REGEX = new RegExp(/[\n\r]\s*at/);
|
||||
|
||||
export class SimpleError extends Error implements HasSimpleError {
|
||||
|
||||
@@ -61,7 +61,7 @@ import { existingScrobble, type ExistingScrobbleOpts } from "../utils/PlayCompar
|
||||
import { statefulInvariantTransform } from "../../core/PlayUtils.ts";
|
||||
import { normalizeStr } from "../utils/StringUtils.ts";
|
||||
import type { Counter, Gauge } from 'prom-client';
|
||||
import { generateLoggableAbortReason, ScrobbleSubmitError, SimpleError } from "../common/errors/MSErrors.ts";
|
||||
import { generateLoggableAbortReason, ScrobbleSubmitError, SimpleError, StageChangeError } from "../common/errors/MSErrors.ts";
|
||||
import {isErrorLike, serializeError} from 'serialize-error';
|
||||
import { DEFAULT_NEW_PADDING, groupPlaysToTimeRanges } from "../utils/ListenFetchUtils.ts";
|
||||
import { spawn, isAbortError, delay } from 'abort-controller-x';
|
||||
@@ -330,25 +330,31 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i
|
||||
}
|
||||
|
||||
public async start(opts: {forceInit?: boolean} = {}) {
|
||||
if(opts.forceInit) {
|
||||
if(!this.canAuthUnattended()) {
|
||||
this.logger.warn({labels: 'Heartbeat'}, 'Client is not ready but will not try to initialize because auth state is not good and cannot be corrected unattended.')
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await this.initialize({force: true, notify: true, notifyTitle: 'Could not initialize automatically'});
|
||||
} catch (e) {
|
||||
this.logger.error(new Error('Could not initialize automatically', {cause: e}));
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (opts.forceInit) {
|
||||
if (!this.canAuthUnattended()) {
|
||||
this.logger.warn({ labels: 'Heartbeat' }, 'Client is not ready but will not try to initialize because auth state is not good and cannot be corrected unattended.')
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await this.initialize({ force: true, notify: true, notifyTitle: 'Could not initialize automatically' });
|
||||
} catch (e) {
|
||||
this.logger.error(new Error('Could not initialize automatically', { cause: e }));
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!this.canAuthUnattended()) {
|
||||
this.logger.warn({label: 'Heartbeat'}, 'Should be monitoring scrobbles but will not attempt to start because auth state is not good and cannot be correct unattended.');
|
||||
return false;
|
||||
if (!this.canAuthUnattended()) {
|
||||
this.logger.warn({ label: 'Heartbeat' }, 'Should be monitoring scrobbles but will not attempt to start because auth state is not good and cannot be correct unattended.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.initTasks();
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw new StageChangeError('Failed to start', { cause: e });
|
||||
} finally {
|
||||
this.emitComponentUpdate<Partial<ComponentClientApiJson>>({ state: this.getRunningState() });
|
||||
}
|
||||
this.initTasks();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async stop(opts: { reason?: string | Error } = {}) {
|
||||
@@ -365,7 +371,8 @@ export default abstract class AbstractScrobbleClient extends AbstractComponent i
|
||||
this.setStatus('Stopped');
|
||||
this.emitComponentUpdate<Partial<ComponentClientApiJson>>({state: COMPONENT_STATE.STOPPED});
|
||||
} catch (e) {
|
||||
throw new Error('Failed to stop Client', { cause: e });
|
||||
this.emitComponentUpdate<Partial<ComponentClientApiJson>>({state: this.getRunningState()});
|
||||
throw new StageChangeError('Failed to stop Client', { cause: e });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -360,12 +360,11 @@ export const setupApi = (app: Express, logger: Logger, appLoggerStream: PassThro
|
||||
try {
|
||||
logger.verbose('User requested auth test');
|
||||
await component.testAuth(true);
|
||||
component.errors = component.errors.filter(x => !findAuthIssue(x));
|
||||
component.clearErrors({predicate: x => findAuthIssue(x) !== undefined});
|
||||
didAuth = true;
|
||||
return res.sendStatus(200);
|
||||
} catch (e) {
|
||||
component.errors = component.errors.filter(x => !findAuthIssue(x));
|
||||
component.errors.push(e);
|
||||
component.replaceErrors(e, {predicate: x => findAuthIssue(x) !== undefined});
|
||||
return res.status(500).json({error: serializeError(e)});
|
||||
} finally {
|
||||
const data = component.getApiData();
|
||||
|
||||
@@ -125,7 +125,7 @@ export const setupAuthRoutes = (app: Express, logger: Logger, sourceMiddle: Expr
|
||||
throw error;
|
||||
}
|
||||
await entity.api.authenticate(token);
|
||||
entity.errors = entity.errors.filter(x => !findAuthIssue(x));
|
||||
entity.clearErrors({predicate: x => findAuthIssue(x) !== undefined});
|
||||
if(entity instanceof AbstractSource) {
|
||||
entity.poll().catch((e) => logger.error(e));
|
||||
} else {
|
||||
@@ -167,22 +167,21 @@ export const setupAuthRoutes = (app: Express, logger: Logger, sourceMiddle: Expr
|
||||
try {
|
||||
const tokenResult = await source.handleAuthCodeCallback(req.query);
|
||||
if (tokenResult === true) {
|
||||
source.errors = source.errors.filter(x => !findAuthIssue(x));
|
||||
source.clearErrors({predicate: x => findAuthIssue(x) !== undefined});
|
||||
source.poll().catch((e) => logger.error(e));
|
||||
|
||||
} else {
|
||||
if (tokenResult instanceof Error) {
|
||||
source.errors.push(tokenResult);
|
||||
source.replaceErrors(tokenResult, {predicate: (x) => x.message === tokenResult.message});
|
||||
source.logger.error(tokenResult);
|
||||
} else if (typeof tokenResult === 'string') {
|
||||
const e = new SimpleError(`Token result was unexpected: ${tokenResult}`);
|
||||
source.errors.push(e);
|
||||
source.replaceErrors(e, {predicate: (x) => x.message === e.message});
|
||||
source.logger.error(e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const err = new SimpleError('Unexpected error while trying to authorize code, or save file', { cause: e });
|
||||
source.errors.push(err);
|
||||
source.replaceErrors(err, {predicate: (x) => err.message === x.message});
|
||||
source.logger.error(err);
|
||||
}
|
||||
return res.redirect('/next');
|
||||
|
||||
@@ -40,7 +40,7 @@ import pMap, {pMapIterable} from 'p-map';
|
||||
import type { Counter } from 'prom-client';
|
||||
import { normalizeStr } from '../utils/StringUtils.ts';
|
||||
import { spawn, isAbortError, delay, throwIfAborted } from 'abort-controller-x';
|
||||
import { generateLoggableAbortReason } from '../common/errors/MSErrors.ts';
|
||||
import { generateLoggableAbortReason, StageChangeError } from '../common/errors/MSErrors.ts';
|
||||
import { DrizzlePlayRepository, playToRepositoryCreatePlayOpts, type QueryPlaysOpts, type RequestPlayQuery, type WithPlayRelation } from '../common/database/drizzle/repositories/PlayRepository.ts';
|
||||
import { asPlay } from '../../core/PlayMarshalUtils.ts';
|
||||
import { AsyncTask, SimpleIntervalJob, ToadScheduler } from 'toad-scheduler';
|
||||
@@ -216,26 +216,33 @@ export default abstract class AbstractSource extends AbstractComponent implement
|
||||
}
|
||||
|
||||
public async start(opts: {forceInit?: boolean} = {}) {
|
||||
if(opts.forceInit) {
|
||||
if(!this.canAuthUnattended()) {
|
||||
this.logger.warn({labels: 'Heartbeat'}, 'Source is not ready but will not try to initialize because auth state is not good and cannot be corrected unattended.')
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
this.setStatus('Attempting to initialize...');
|
||||
await this.initialize({force: true, notify: true, notifyTitle: 'Could not initialize automatically'});
|
||||
} catch (e) {
|
||||
this.logger.error(new Error('Could not initialize automatically', {cause: e}));
|
||||
this.setStatus('Could not initialize automatically');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (opts.forceInit) {
|
||||
if (!this.canAuthUnattended()) {
|
||||
this.logger.warn({ labels: 'Heartbeat' }, 'Source is not ready but will not try to initialize because auth state is not good and cannot be corrected unattended.')
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
this.setStatus('Attempting to initialize...');
|
||||
await this.initialize({ force: true, notify: true, notifyTitle: 'Could not initialize automatically' });
|
||||
} catch (e) {
|
||||
this.logger.error(new Error('Could not initialize automatically', { cause: e }));
|
||||
this.setStatus('Could not initialize automatically');
|
||||
return false;
|
||||
}
|
||||
|
||||
if('discoverDevices' in this && typeof this.discoverDevices === 'function') {
|
||||
this.discoverDevices();
|
||||
if ('discoverDevices' in this && typeof this.discoverDevices === 'function') {
|
||||
this.discoverDevices();
|
||||
}
|
||||
}
|
||||
this.initTasks();
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw new StageChangeError('Failed to start', { cause: e });
|
||||
} finally {
|
||||
this.emitComponentUpdate({state: this.getRunningState()});
|
||||
}
|
||||
this.initTasks();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async stop(opts: { reason?: string | Error } = {}) {
|
||||
@@ -251,7 +258,8 @@ export default abstract class AbstractSource extends AbstractComponent implement
|
||||
this.setStatus('Stopped');
|
||||
this.emitComponentUpdate<Partial<ComponentSourceApiJson>>({state: COMPONENT_STATE.STOPPED});
|
||||
} catch (e) {
|
||||
throw new Error('Failed to stop Source', { cause: e });
|
||||
this.emitComponentUpdate<Partial<ComponentSourceApiJson>>({state: this.getRunningState()});
|
||||
throw new StageChangeError('Failed to stop', { cause: e });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,7 +291,7 @@ export default abstract class AbstractSource extends AbstractComponent implement
|
||||
const t = this.transformManager.getTransformerByStage({ type: hook.type, name: hook.name });
|
||||
pcInits.push(t.staggerOpts?.initialInterval ?? 0);
|
||||
pcMaxStagger.push(t.staggerOpts?.maxRandomStagger ?? 0)
|
||||
}
|
||||
}
|
||||
this.staggerMappers.preCompare = staggerMapper<PlayObject, PlayObject>({ initialInterval: Math.max(...pcInits), maxRandomStagger: Math.max(...pcMaxStagger), concurrency: 2 });
|
||||
}
|
||||
|
||||
@@ -568,7 +576,7 @@ export default abstract class AbstractSource extends AbstractComponent implement
|
||||
const err = new Error('Cannot start polling because Source is not ready', {cause: e});
|
||||
this.logger.error(err);
|
||||
this.setStatus('Polling Error');
|
||||
this.errors.push(err);
|
||||
this.replaceErrors(err, {predicate: (x) => x.message === err.message});
|
||||
this.emitComponentUpdate<Partial<ComponentSourceApiJson>>({errors: this.errors});
|
||||
if(notify) {
|
||||
await this.notify( {title: `Polling Error`, message: `Cannot start polling because Source is not ready: ${truncateStringToLength(500)(messageWithCausesTruncatedDefault(e))}`, priority: 'error'});
|
||||
|
||||
@@ -12,6 +12,7 @@ import { readJson } from '../utils/DataUtils.ts';
|
||||
import { joinedUrl } from "../utils/NetworkUtils.ts";
|
||||
import AbstractSource, { type RecentlyPlayedOptions } from "./AbstractSource.ts";
|
||||
import { baseFormatPlayObj } from "../utils/PlayTransformUtils.ts";
|
||||
import { SimpleError } from "../common/errors/MSErrors.ts";
|
||||
|
||||
export default class DeezerSource extends AbstractSource {
|
||||
workingCredsPath;
|
||||
@@ -243,7 +244,7 @@ export default class DeezerSource extends AbstractSource {
|
||||
return true;
|
||||
} else {
|
||||
this.logger.warn('Callback contained an error! User may have denied access?')
|
||||
this.errors = error;
|
||||
this.errors.push(error);
|
||||
this.logger.error(error);
|
||||
return error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user