Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
FoxxMD committed Mar 15, 2023
2 parents 7a0a23b + 1b0212f commit 8b3f4c6
Show file tree
Hide file tree
Showing 19 changed files with 887 additions and 81 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ A javascript app to scrobble music you listened to, to [Maloja](https://github.c
* [Deezer](/docs/configuration.md#deezer)
* [MPRIS (Linux Desktop)](/docs/configuration.md#mpris)
* [Mopidy](/docs/configuration.md#mopidy)
* [JRiver](/docs/configuration.md#jriver)
* Supports scrobbling to many **Clients**
* [Maloja](/docs/configuration.md#maloja)
* [Last.fm](/docs/configuration.md#lastfm)
Expand Down
10 changes: 10 additions & 0 deletions config/jriver.json.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
"name": "MyJriver",
"data": {
"url": "0.0.0.0",
"username": "auser",
"password": "apassword"
}
}
]
76 changes: 74 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* [Youtube Music](#youtube-music)
* [MPRIS (Linux Desktop)](#mpris)
* [Mopidy](#mopidy)
* [JRiver](#jriver)
* [Client Configurations](#client-configurations)
* [Maloja](#maloja)
* [Last.fm](#lastfm)
Expand Down Expand Up @@ -270,7 +271,7 @@ No support for ENV based for Last.fm as a client (only source)

See [`lastfm.json.example`](/config/lastfm.json.example), change `configureAs` to `source`. Or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FLastfmSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)

# [Listenbrainz (Source)](https://listenbrainz.org)
## [Listenbrainz (Source)](https://listenbrainz.org)

You will need to run your own Listenbrainz server or have an account [on the official instance](https://listenbrainz.org/login/)

Expand Down Expand Up @@ -404,7 +405,8 @@ Part => Default Value
* Port => `6680`
* Path => `/mopidy/ws/`

EX
<details>
<summary>URL Transform Examples</summary>

```json
{
Expand All @@ -430,6 +432,8 @@ MS transforms this to: `ws://192.168.0.101:3456/mopidy/ws/`

MS transforms this to: `ws://mopidy.mydomain.com:80/MOPWS`

</details>


#### URI Blacklist/Whitelist

Expand Down Expand Up @@ -460,6 +464,74 @@ EX:
If a track would be scrobbled like `Album: Soundcloud, Track: My Cool Track, Artist: A Cool Artist`
then multi-scrobbler will instead scrobble `Track: My Cool Track, Artist: A Cool Artist`

## [JRiver](https://jriver.com/)

In order for multi-scrobbler to communicate with JRiver you must have [Web Server Interface](https://wiki.jriver.com/index.php/Web_Service_Interface#Documentation_of_Functions) enabled. This can can be in the JRiver GUI:

* Tools -> Options -> Media Network
* Check `Use Media Network to share this library...`
* If you have `Authentication` checked you will need to provide the **Username** and **Password** in the ENV/File configuration below.

#### URL

If you do not provide a URL then a default is used which assumes JRiver is installed on the same server as multi-scrobbler: `http://localhost:52199/MCWS/v1/`

* Make sure the port number matches what is found in `Advanced` section in the [Media Network](#jriver) options.
* If your installation is on the same machine but you cannot connect using `localhost` try `0.0.0.0` instead.

The URL used to connect ultimately must be formed like this: `[protocol]://[hostname]:[port]/[path]`
If any part of this URL is missing multi-scrobbler will use a default value, for your convenience. This also means that if any part of your URL is **not** standard you must explicitly define it.

Part => Default Value

* Protocol => `http://`
* Hostname => `localhost`
* Port => `52199`
* Path => `/MCWS/v1/`

<details>
<summary>URL Transform Examples</summary>

```json
{
"url": "jriver.mydomain.com"
}
```

MS transforms this to: `http://jriver.mydomain.com:52199/MCWS/v1/`

```json
{
"url": "192.168.0.101:3456"
}
```

MS transforms this to: `http://192.168.0.101:3456/MCWS/v1/`

```json
{
"url": "mydomain.com:80/jriverReverse/MCWS/v1/"
}
```

MS transforms this to: `http://mydomain.com:80/jriverReverse/MCWS/v1/`

</details>

### ENV-Based


| Environmental Variable | Required | Default | Description |
|------------------------|----------|---------------------------------|------------------------------------------------|
| JRIVER_URL | Yes | http://localhost:52199/MCWS/v1/ | The URL of the JRiver server |
| JRIVER_USERNAME | No | | If authentication is enabled, the username set |
| JRIVER_PASSWORD | No | | If authenticated is enabled, the password set |


### File-Based

See [`jriver.json.example`](/config/jriver.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FJRiverSourceConfig/%23%2Fdefinitions%2FJRiverData?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)

# Client Configurations

## [Maloja](https://github.com/krateng/maloja)
Expand Down
17 changes: 17 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"homepage": "https://github.com/FoxxMD/multi-scrobbler#readme",
"dependencies": {
"@awaitjs/express": "^0.6.3",
"@kenyip/backoff-strategies": "^1.0.4",
"ajv": "^7.2.4",
"body-parser": "^1.19.0",
"compare-versions": "^4.1.2",
Expand Down Expand Up @@ -69,6 +70,7 @@
"winston-duplex": "^0.1.1",
"winston-null": "^2.0.0",
"winston-transport": "^4.4.0",
"xml2js": "^0.4.23",
"youtube-music-ts-api": "^1.4.1"
},
"devDependencies": {
Expand All @@ -82,6 +84,7 @@
"@types/spotify-web-api-node": "^5.0.7",
"@types/superagent": "^4.1.16",
"@types/triple-beam": "^1.3.2",
"@types/xml2js": "^0.4.11",
"ts-essentials": "^9.1.2",
"ts-node": "^10.7.0",
"tsconfig-paths": "^3.13.0",
Expand Down
167 changes: 167 additions & 0 deletions src/apis/JRiverApiClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import AbstractApiClient from "./AbstractApiClient.js";
import {JRiverData} from "../common/infrastructure/config/source/jriver.js";
import request, {Request, Response} from 'superagent';
import xml2js from 'xml2js';
import {ErrorWithCause} from "pony-cause";

const parser = new xml2js.Parser({'async': true});

export const PLAYER_STATE: Record<string, PLAYER_STATE> = {
STOPPED: '0',
PAUSED: '1',
PLAYING: '2'
}

export type PLAYER_STATE = '0' | '1' | '2';

interface JRiverResponseItem {
_: string
$: {
Name: string
}
}

interface JRiverResponse {
Response: {
'$': {
Status: string
},
Item: JRiverResponseItem[]
}
}

export interface JRiverTransformedResponse<T> {
status: string
data?: T
}

export interface Alive {
RuntimeGUID: string
LibraryVersion: string
ProgramName: string
ProgramVersion: string
FriendlyName: string
AccessKey: string
ProductVersion: string
Platform: string
}
// state 0 = nothing?
// 2 = playing
// 1 = paused

export interface Authenticate {
Token: string
ReadOnly: number
PreLicensed: boolean
}

export interface Info {
ZoneID: string
ZoneName: string
State: PLAYER_STATE
PositionMS: number
DurationMS: number
Artist: string
Album: string
Name: string
Status: string
FileKey: string
}

export interface Zones {
NumberZones: number
CurrentZoneID: string
CurrentZoneIndex: string
}

const jriverResponseTransform = <T>(val: JRiverResponse): JRiverTransformedResponse<T> => {
const status = val.Response.$.Status;
const items = val.Response.Item === undefined ? undefined : val.Response.Item.map(x => {
return [x.$.Name, x._];
});
return {
status,
data: items.reduce((acc, curr) => {
acc[curr[0]] = curr[1];
return acc;
}, {}) as T
};
}

export class JRiverApiClient extends AbstractApiClient {

declare config: JRiverData

url: string;

token?: string;

constructor(name: any, config: JRiverData, options = {}) {
super('JRiver', name, config, options);
const {
url = 'http://localhost:52199/MCWS/v1/'
} = config;
this.url = url;
}

callApi = async <T>(req: Request, retries = 0): Promise<Response & {body: T}> => {
const {
maxRequestRetries = 2,
retryMultiplier = 1.5
} = this.config;

if (this.token !== undefined) {
req.query({token: this.token});
}

try {
const resp = await req as Response;
if (resp.text !== '') {
const rawBody = await parser.parseStringPromise(resp.text);
resp.body = <T>jriverResponseTransform(rawBody);
}
return resp;
} catch (e) {
throw e;
}
}

testConnection = async () => {
try {
const resp = await this.callApi<Alive>(request.get(`${this.url}Alive`));
const {body: { data } = {}} = resp;
this.logger.verbose(`Found ${data.ProgramName} ${data.ProgramVersion} (${data.FriendlyName})`);
return true;
} catch (e) {
this.logger.error(new ErrorWithCause('Could not communicate with JRiver server. Verify your server URL is correct.', {cause: e}));
return false;
}
}

testAuth = async () => {
try {
let req = request.get(`${this.url}Authenticate`);
if (this.config.username !== undefined) {
req.auth(this.config.username, this.config.password);
}
const resp = await this.callApi<Authenticate>(req);
this.token = resp.body.data.Token;
return true;
} catch (e) {
let msg = 'Authentication failed.';
if(this.config.username === undefined || this.config.password === undefined) {
msg = 'Authentication failed. No username/password was provided in config! Did you mean to do this?';
}
this.logger.error(new ErrorWithCause(msg, {cause: e}));
return false;
}
}

getInfo = async (zoneId: string = '-1') => {
return await this.callApi<Info>(request.get(`${this.url}Playback/Info`).query({Zone: zoneId}));
}

getZones = async () => {
return await this.callApi<Zones>(request.get(`${this.url}Playback/Zones`));
}
}
2 changes: 1 addition & 1 deletion src/clients/AbstractScrobbleClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default abstract class AbstractScrobbleClient {
constructor(type: any, name: any, config: CommonClientConfig, notifier: Notifiers, logger: Logger) {
this.type = type;
this.name = name;
const identifier = `Client ${capitalize(this.type)} - ${name}`;
const identifier = `${capitalize(this.type)} - ${name}`;
this.logger = logger.child({labels: [identifier]}, mergeArr);
this.notifier = notifier;

Expand Down
4 changes: 2 additions & 2 deletions src/common/infrastructure/Atomic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {FixedSizeList} from 'fixed-size-list';
import {MESSAGE} from 'triple-beam';
import {Logger} from "winston";

export type SourceType = 'spotify' | 'plex' | 'tautulli' | 'subsonic' | 'jellyfin' | 'lastfm' | 'deezer' | 'ytmusic' | 'mpris' | 'mopidy' | 'listenbrainz';
export const sourceTypes: SourceType[] = ['spotify', 'plex', 'tautulli', 'subsonic', 'jellyfin', 'lastfm', 'deezer', 'ytmusic', 'mpris', 'mopidy', 'listenbrainz'];
export type SourceType = 'spotify' | 'plex' | 'tautulli' | 'subsonic' | 'jellyfin' | 'lastfm' | 'deezer' | 'ytmusic' | 'mpris' | 'mopidy' | 'listenbrainz' | 'jriver';
export const sourceTypes: SourceType[] = ['spotify', 'plex', 'tautulli', 'subsonic', 'jellyfin', 'lastfm', 'deezer', 'ytmusic', 'mpris', 'mopidy', 'listenbrainz', 'jriver'];

export const lowGranularitySources: SourceType[] = ['subsonic','ytmusic'];

Expand Down
Loading

0 comments on commit 8b3f4c6

Please sign in to comment.