Skip to main content

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. It is recommended to 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
git checkout --track origin/develop
nvm use # optional, to set correct Node version
npm install
npm run start

Use the develop branch as the target for any Pull Requests. The master branch is reserved for releases and minor documentation updates only.

Common Development

info

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.

info

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 like data.baseUrl: 'SOME_IP' and then store this in the class config
  • Validate that config data contains required properties user 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.

info

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 - (default false) Set to true if MS should check/test Auth for this Component
  • requiresAuthInteraction - (default false) Set to true 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 succeeded
  • false 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.

info

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

Creating Clients and Sources