Development
Architecture
Multi-scrobbler is written entirely in Typescript. It consists of a backend and frontend. The backend handles all Source/Client logic, mounts web server endpoints that listen for Auth callbacks and Source ingress using expressjs, and serves the frontend. The frontend is a standalone Vitejs app that communicates via API to the backend in order to render the dashboard.
Project Setup
Development requires Node v18.19.1 or higher is installed on your system.
When running locally (not with a devcontainer) you can use nvm to manage the installed node version.
Clone this repository somewhere and then install from the working directory
git clone https://github.com/FoxxMD/multi-scrobbler.git .
cd multi-scrobbler
nvm use # optional, sets correct node version when running without devcontainer
npm install
npm run start
VSCode
This repository contains workspace settings for development with VSCode. These include:
- Run/Debug Launch configurations for the application and tests
- Devcontainer for development with all dependencies already installed
- Useful extensions for linting and running tests
To use the Devcontainer simple open the repository in VSCode and "Use Devcontainer" when the notification is presented. npm install
will be run when a new container is created.
Common Development
In this document, when referring to aspects of Sources and Clients that are shared between both, the Source/Client will be referred to as a Component.
A Component is composed of two parts:
- Typescript interfaces describing structure of configuration for that Component
- A concrete class inheriting from a common "startup" abstract class that enforces how the Component is built and operates
In both parts Source/Clients share some common properties/behavior before diverging in how they operate.
Config
The configuration for a Component should always have this minimum shape, enforced respectively by the interfaces CommonSourceConfig and CommonClientConfig:
interface MyConfig {
name: string
data?: object
options?: object
}
data
contains data that is required for a Component to operate such as credentials, callback urls, api keys, endpoints, etc...options
are optional settings that can be used to fine-tune the usage of the Component but are not required or do not majorly affect behavior. EX additional logging toggles
Concrete Class
Components inherit from an abstract base class, AbstractComponent
, that defines different "stages" of how a Component is built and initialized when MS first starts as well as when restarting the Component in the event it stops due to an error/network failure/etc...
Stages
Stages below are invoked in the order listed. All stages are asynchronous to allow fetching network requests or reading files.
The stage function (described in each stage below) should return a value or throw:
- return
null
if the stage is not required - return
true
if the stage succeeded - return a
string
if the stage succeeded and you wish to append a result to the log output for this stage - throw an
Exception
if the stage failed for any reason and the Component should not continue to run/start up
Stage: Build Data
This stage should be used to validate user configuration, parse any additional data from async sources (file, network), and finalize the shape of any configuration/data needed for the Component to operate.
Implement doBuildInitData
in your child class to invoke this stage.
Examples
- Parse a full URL like
http://SOME_IP:7000/subfolder/api
from user config containing a base url likedata.baseUrl: 'SOME_IP'
and then store this in the class config - Validate that config
data
contains required propertiesuser
password
salt
- Read stored credentials from
${this.configDir}/currentCreds-MySource-${name}.json
;
Stage: Check Connection
This stage is used to validate that MS can communicate with the service the Component is interacting with. This stage is invoked on MS startup as well as any time the Component tries to restart after a failure.
If the Component depends on ingress (like Jellyfin/Plex webhook) this stage is not necessary.
Implement doCheckConnection
in your child class to invoke this stage.
Examples
- Make a
request
to the service's server to ensure it is accessible - Open a websocket connection and check for a ping-pong
Stage: Test Auth
MS determines if Auth is required for a Component based on two class properties. You should set these properties during constructor
initialization for your Component class:
requiresAuth
- (defaultfalse
) Set totrue
if MS should check/test Auth for this ComponentrequiresAuthInteraction
- (defaultfalse
) Set totrue
if user interaction is required to complete auth IE user needs to visit a callback URL
If the Component requires authentication in order to communicate with a service then any required data should be built in this stage and a request made to the service to ensure the authentication data is valid.
This stage should return:
true
if auth succeededfalse
if auth failed without unexpected errors- IE the authentication data is not valid and requires user interaction to resolve the failure
- throw an exception if network failure or unexpected error occurred
You should attempt to re-authenticate, if possible. Only throw an exception or return false
if there is no way to recover from an authentication failure.
Implement doAuthentication
in your child class to invoke this stage.
Examples
- Generate a Bearer Token for Basic Auth from user/password given in config and store in class properties
- Make a request to a known endpoint with Authorization token from read credentials file to see if succeeds or returns 403
- Catch a 403 and attempt to reauthenticate at an auth endpoint with user/password given in config
Play Object
The PlayObject is the standard data structure MS uses to store listen (track) information and data required for scrobbling. It consists of:
- Track Data -- a standard format for storing track, artists, album, track duration, the date the track was played at, etc...
- Listen Metadata -- Optional but useful data related to the specific play or specifics about the Source/Client context for this play such as
- Platform specific ID, web URL to track, device/user ID that played this track, etc...
Both Sources and Clients use the PlayObject interface. When a Component receives track info from its corresponding service it must transform this data into a PlayObject before it can be interacted with.
For more refer to the TS documentation for PlayObject
or AmbPlayObject
in your project