mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-09-03 05:10:00 +03:00
feat: Implement Kodi as a Source
This commit is contained in:
@@ -18,6 +18,7 @@ A javascript app to scrobble music you listened to, to [Maloja](https://github.c
|
||||
* [MPRIS (Linux Desktop)](/docs/configuration.md#mpris)
|
||||
* [Mopidy](/docs/configuration.md#mopidy)
|
||||
* [JRiver](/docs/configuration.md#jriver)
|
||||
* [Kodi](/docs/configuration.md#kodi)
|
||||
* Supports scrobbling to many **Clients**
|
||||
* [Maloja](/docs/configuration.md#maloja)
|
||||
* [Last.fm](/docs/configuration.md#lastfm)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"name": "MyKodi",
|
||||
"data": {
|
||||
"url": "http://localhost:8080/jsonrpc",
|
||||
"username": "myUser",
|
||||
"password": "myPassword"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -16,6 +16,7 @@
|
||||
* [MPRIS (Linux Desktop)](#mpris)
|
||||
* [Mopidy](#mopidy)
|
||||
* [JRiver](#jriver)
|
||||
* [Kodi](#kodi)
|
||||
* [Client Configurations](#client-configurations)
|
||||
* [Maloja](#maloja)
|
||||
* [Last.fm](#lastfm)
|
||||
@@ -532,6 +533,74 @@ MS transforms this to: `http://mydomain.com:80/jriverReverse/MCWS/v1/`
|
||||
|
||||
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)
|
||||
|
||||
## [Kodi](https://kodi.tv/)
|
||||
|
||||
In order for multi-scrobbler to communicate with JRiver you must have the [Web Interface](https://kodi.wiki/view/Web_interface) enabled. This can can be in the Kodi GUI:
|
||||
|
||||
* Settings -> Services -> Control
|
||||
* Check `Allow remote control via HTTP`
|
||||
* Ensure you have a **Username** and **Password** set, you will need to provide them 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:8080/jsonrpc`
|
||||
|
||||
* Make sure the port number matches what is found in **Port** in the [Control](#kodi) section mentioned above.
|
||||
* 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 => `8080`
|
||||
* Path => `/jsonrpc`
|
||||
|
||||
<details>
|
||||
<summary>URL Transform Examples</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "kodi.mydomain.com"
|
||||
}
|
||||
```
|
||||
|
||||
MS transforms this to: `http://kodi.mydomain.com:8080/jsonrpc`
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "192.168.0.101:3456"
|
||||
}
|
||||
```
|
||||
|
||||
MS transforms this to: `http://192.168.0.101:3456/jsonprc`
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "mydomain.com:80/kodiReverse/jsonrpc"
|
||||
}
|
||||
```
|
||||
|
||||
MS transforms this to: `http://mydomain.com:80/kodiReverse/jsonrpc`
|
||||
|
||||
</details>
|
||||
|
||||
### ENV-Based
|
||||
|
||||
|
||||
| Environmental Variable | Required | Default | Description |
|
||||
|------------------------|----------|-------------------------------|----------------------------|
|
||||
| KODI_URL | Yes | http://localhost:8080/jsonrpc | The URL of the Kodi server |
|
||||
| KODI_USERNAME | No | | The username set |
|
||||
| KODI_PASSWORD | No | | The password set |
|
||||
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`kodi.json.example`](/config/kodi.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FKodiSourceConfig/%23%2Fdefinitions%2FKodiData?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
# Client Configurations
|
||||
|
||||
## [Maloja](https://github.com/krateng/maloja)
|
||||
|
||||
Generated
+154
-4
@@ -26,6 +26,7 @@
|
||||
"gotify": "^1.1.0",
|
||||
"iti": "^0.6.0",
|
||||
"json5": "^2.2.3",
|
||||
"kodi-api": "^0.2.1",
|
||||
"lastfm-node-client": "^2.2.0",
|
||||
"mopidy": "^1.3.0",
|
||||
"normalize-url": "^6.1.0",
|
||||
@@ -240,7 +241,6 @@
|
||||
"version": "3.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||
"integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -410,6 +410,14 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.2.tgz",
|
||||
"integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g=="
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "7.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz",
|
||||
"integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/xml2js": {
|
||||
"version": "0.4.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.11.tgz",
|
||||
@@ -891,6 +899,11 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"node_modules/compare-versions": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-4.1.4.tgz",
|
||||
@@ -1064,6 +1077,17 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/delay": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz",
|
||||
"integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -1270,6 +1294,19 @@
|
||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="
|
||||
},
|
||||
"node_modules/es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||
},
|
||||
"node_modules/es6-promisify": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
|
||||
"integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==",
|
||||
"dependencies": {
|
||||
"es6-promise": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
@@ -1425,6 +1462,14 @@
|
||||
],
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/eyes": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
|
||||
"integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==",
|
||||
"engines": {
|
||||
"node": "> 0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -2049,6 +2094,45 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/jayson": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jayson/-/jayson-3.7.0.tgz",
|
||||
"integrity": "sha512-tfy39KJMrrXJ+mFcMpxwBvFDetS8LAID93+rycFglIQM4kl3uNR3W4lBLE/FFhsoUCEox5Dt2adVpDm/XtebbQ==",
|
||||
"dependencies": {
|
||||
"@types/connect": "^3.4.33",
|
||||
"@types/node": "^12.12.54",
|
||||
"@types/ws": "^7.4.4",
|
||||
"commander": "^2.20.3",
|
||||
"delay": "^5.0.0",
|
||||
"es6-promisify": "^5.0.0",
|
||||
"eyes": "^0.1.8",
|
||||
"isomorphic-ws": "^4.0.1",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"JSONStream": "^1.3.5",
|
||||
"lodash": "^4.17.20",
|
||||
"uuid": "^8.3.2",
|
||||
"ws": "^7.4.5"
|
||||
},
|
||||
"bin": {
|
||||
"jayson": "bin/jayson.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jayson/node_modules/@types/node": {
|
||||
"version": "12.20.55",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
|
||||
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="
|
||||
},
|
||||
"node_modules/jayson/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/jsbi": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/jsbi/-/jsbi-2.0.5.tgz",
|
||||
@@ -2079,8 +2163,7 @@
|
||||
"node_modules/json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||
"optional": true
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
@@ -2093,6 +2176,37 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonparse": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
|
||||
"integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
|
||||
"engines": [
|
||||
"node >= 0.2.0"
|
||||
]
|
||||
},
|
||||
"node_modules/jsonschema": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz",
|
||||
"integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/JSONStream": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
|
||||
"integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
|
||||
"dependencies": {
|
||||
"jsonparse": "^1.2.0",
|
||||
"through": ">=2.2.7 <3"
|
||||
},
|
||||
"bin": {
|
||||
"JSONStream": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/jsprim": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
|
||||
@@ -2116,6 +2230,25 @@
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kodi-api": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/kodi-api/-/kodi-api-0.2.1.tgz",
|
||||
"integrity": "sha512-FcPJyisjyYZCvtGRLaO/s+gwGgZMdDrowan5/axI43PDB5fxE8OyrLpiwMrSDJ1Fs0ImaSO8UEoeGP9zcKstrQ==",
|
||||
"dependencies": {
|
||||
"jayson": "^3.4.4",
|
||||
"jsonschema": "^1.4.0",
|
||||
"rpc-websocket-client": "^1.1.4",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/kodi-api/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/kuler": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
|
||||
@@ -2129,6 +2262,11 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.mergewith": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
|
||||
@@ -2871,6 +3009,19 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/rpc-websocket-client": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/rpc-websocket-client/-/rpc-websocket-client-1.1.4.tgz",
|
||||
"integrity": "sha512-UDMVcsNDJ3WET+0EFdmseYX+60MOCA3BpgpZ5dbtiGsqHo+9uWfgNbPZ4iGDtNcX+EUhCt/Y7C0fTmUU3CkkWA==",
|
||||
"dependencies": {
|
||||
"isomorphic-ws": "^4.0.1",
|
||||
"uuid": "^3.3.3",
|
||||
"ws": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -3596,7 +3747,6 @@
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"gotify": "^1.1.0",
|
||||
"iti": "^0.6.0",
|
||||
"json5": "^2.2.3",
|
||||
"kodi-api": "^0.2.1",
|
||||
"lastfm-node-client": "^2.2.0",
|
||||
"mopidy": "^1.3.0",
|
||||
"normalize-url": "^6.1.0",
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import AbstractApiClient from "./AbstractApiClient.js";
|
||||
import {ErrorWithCause} from "pony-cause";
|
||||
import {KodiData} from "../common/infrastructure/config/source/kodi.js";
|
||||
import { KodiClient } from 'kodi-api'
|
||||
import normalizeUrl from "normalize-url";
|
||||
import {URL} from "url";
|
||||
import {RecentlyPlayedOptions} from "../sources/AbstractSource.js";
|
||||
import {FormatPlayObjectOptions, PlayObject} from "../common/infrastructure/Atomic.js";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
interface KodiDuration {
|
||||
hours: number
|
||||
milliseconds: number
|
||||
minutes: number
|
||||
seconds: number
|
||||
}
|
||||
interface ActivePlayer {
|
||||
playerid: number
|
||||
playertype: string
|
||||
type: string
|
||||
}
|
||||
|
||||
interface PlayerInfo {
|
||||
position: -1 | 0
|
||||
time: KodiDuration
|
||||
totaltime: KodiDuration
|
||||
type: string
|
||||
}
|
||||
|
||||
interface PlayerItem {
|
||||
album: string
|
||||
albumartist: string[]
|
||||
artist: string[]
|
||||
duration: number
|
||||
id: number
|
||||
label: string
|
||||
title: string
|
||||
type: string
|
||||
}
|
||||
|
||||
export class KodiApiClient extends AbstractApiClient {
|
||||
|
||||
declare config: KodiData
|
||||
|
||||
url: URL;
|
||||
|
||||
token?: string;
|
||||
|
||||
client: KodiClient;
|
||||
|
||||
constructor(name: any, config: KodiData, options = {}) {
|
||||
super('Kodi', name, config, options);
|
||||
const {
|
||||
url = 'http://localhost:8080/jsonrpc'
|
||||
} = config;
|
||||
this.url = KodiApiClient.parseConnectionUrl('http://localhost:8080');
|
||||
const auth = new Buffer(`${config.username}:${config.password}`).toString('base64');
|
||||
this.client = new KodiClient({
|
||||
clientType: this.url.protocol.replace(':', '') as ('http' | 'https'),
|
||||
//parameterMode: 'object',
|
||||
clientOptions: {
|
||||
host: this.url.hostname,
|
||||
port: this.url.port,
|
||||
path: this.url.pathname,
|
||||
headers: {
|
||||
Authorization: `Basic ${auth}`
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static parseConnectionUrl(val: string) {
|
||||
const normal = normalizeUrl(val, {removeTrailingSlash: true, normalizeProtocol: true})
|
||||
const url = new URL(normal);
|
||||
|
||||
if (url.port === null || url.port === '') {
|
||||
url.port = '8080';
|
||||
}
|
||||
if (url.pathname === '/') {
|
||||
url.pathname = '/jsonrpc';
|
||||
} else if (url.pathname === '/jsonrpc/') {
|
||||
url.pathname = '/jsonrpc';
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
static formatPlayObj(obj: (PlayerItem & Partial<PlayerInfo> & { playerid?: number }), options: FormatPlayObjectOptions = {}): PlayObject {
|
||||
const {newFromSource = true} = options;
|
||||
|
||||
const {
|
||||
album: albumVal,
|
||||
albumartist,
|
||||
artist: artistVal,
|
||||
duration,
|
||||
id,
|
||||
title,
|
||||
time,
|
||||
playerid,
|
||||
} = obj;
|
||||
|
||||
let artists = artistVal === null || artistVal === undefined ? [] : artistVal;
|
||||
let album = albumVal === null || albumVal === '' ? undefined : albumVal;
|
||||
const trackProgressPosition = time !== undefined ? Math.round(dayjs.duration(time).asSeconds()) : undefined;
|
||||
|
||||
return {
|
||||
data: {
|
||||
track: title,
|
||||
album: album,
|
||||
artists,
|
||||
duration,
|
||||
playDate: dayjs()
|
||||
},
|
||||
meta: {
|
||||
source: 'kodi',
|
||||
trackId: id.toString(),
|
||||
newFromSource,
|
||||
trackProgressPosition,
|
||||
deviceId: playerid !== undefined ? `Player${playerid}` : undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testConnection = async () => {
|
||||
return true;
|
||||
}
|
||||
|
||||
testAuth = async () => {
|
||||
try {
|
||||
|
||||
await this.client.connect();
|
||||
// https://kodi.wiki/view/JSON-RPC_API/v12#JSONRPC.Version
|
||||
const jsonInfo = await this.client.JSONRPC.Version();
|
||||
|
||||
// https://kodi.wiki/view/JSON-RPC_API/v12#Application.GetProperties
|
||||
const applicationInfo = await this.client.Application.GetProperties(['version']);
|
||||
|
||||
this.logger.info(`Found Kodi v${applicationInfo.version.major}.${applicationInfo.version.minor} (JSONRPC v${jsonInfo.version.major}.${jsonInfo.version.minor})`);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
getPlayerInfo = async (id: number): Promise<PlayerInfo> => {
|
||||
// https://kodi.wiki/view/JSON-RPC_API/v12#Player.GetProperties
|
||||
// @ts-ignore
|
||||
const playerInfo = await this.client.Player.GetProperties(0, ["position","type","time","totaltime"])
|
||||
return playerInfo;
|
||||
}
|
||||
|
||||
getPlayerItem = async (id: number): Promise<{item: PlayerItem}> => {
|
||||
// https://kodi.wiki/view/JSON-RPC_API/v12#Player.GetItem
|
||||
// @ts-ignore
|
||||
const itemInfo = await this.client.Player.GetItem(0, ["title","artist","album","albumartist","starttime","endtime","duration","streamdetails","uniqueid"]);
|
||||
return itemInfo as {item: PlayerItem};
|
||||
}
|
||||
|
||||
getActivePlayers = async (): Promise<ActivePlayer[]> => {
|
||||
// https://kodi.wiki/view/JSON-RPC_API/v12#Player.GetActivePlayers
|
||||
const players = await this.client.Player.GetActivePlayers();
|
||||
return players as ActivePlayer[];
|
||||
}
|
||||
|
||||
getRecentlyPlayed = async (options: RecentlyPlayedOptions = {}): Promise<PlayObject[]> => {
|
||||
const active = await this.getActivePlayers();
|
||||
if(active.length === 0) {
|
||||
return [];
|
||||
}
|
||||
// get first active playing audio
|
||||
const audioActive = active.find(x => x.type === 'audio');
|
||||
if(audioActive === undefined) {
|
||||
return [];
|
||||
}
|
||||
const playerInfo = await this.getPlayerInfo(audioActive.playerid);
|
||||
|
||||
const itemInfo = await this.getPlayerItem(audioActive.playerid);
|
||||
|
||||
const play = KodiApiClient.formatPlayObj({...itemInfo.item, ...playerInfo, ...audioActive});
|
||||
|
||||
return [play];
|
||||
}
|
||||
}
|
||||
@@ -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' | 'jriver';
|
||||
export const sourceTypes: SourceType[] = ['spotify', 'plex', 'tautulli', 'subsonic', 'jellyfin', 'lastfm', 'deezer', 'ytmusic', 'mpris', 'mopidy', 'listenbrainz', 'jriver'];
|
||||
export type SourceType = 'spotify' | 'plex' | 'tautulli' | 'subsonic' | 'jellyfin' | 'lastfm' | 'deezer' | 'ytmusic' | 'mpris' | 'mopidy' | 'listenbrainz' | 'jriver' | 'kodi';
|
||||
export const sourceTypes: SourceType[] = ['spotify', 'plex', 'tautulli', 'subsonic', 'jellyfin', 'lastfm', 'deezer', 'ytmusic', 'mpris', 'mopidy', 'listenbrainz', 'jriver', 'kodi'];
|
||||
|
||||
export const lowGranularitySources: SourceType[] = ['subsonic','ytmusic'];
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import {CommonSourceConfig, CommonSourceData} from "./index.js";
|
||||
import {PollingOptions} from "../common.js";
|
||||
|
||||
|
||||
export interface KodiData extends CommonSourceData, PollingOptions {
|
||||
/**
|
||||
* URL of the Kodi HTTP server to connect to
|
||||
*
|
||||
* multi-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:8080/jsonrpc`
|
||||
*
|
||||
* The URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.
|
||||
*
|
||||
* Parts => [default value]
|
||||
*
|
||||
* * Protocol => `http://`
|
||||
* * Hostname => `localhost`
|
||||
* * Port => `8080`
|
||||
* * Path => `/jsonrpc`
|
||||
*
|
||||
*
|
||||
* @examples ["http://localhost:8080/jsonrpc"]
|
||||
* @default "http://localhost:8080/jsonrpc"
|
||||
* */
|
||||
url: string
|
||||
|
||||
/**
|
||||
* The username set for Remote Control via Web Sever
|
||||
* */
|
||||
username: string
|
||||
|
||||
/**
|
||||
* The password set for Remote Control via Web Sever
|
||||
* */
|
||||
password: string
|
||||
}
|
||||
export interface KodiSourceConfig extends CommonSourceConfig {
|
||||
data: KodiData
|
||||
}
|
||||
|
||||
export interface KodiSourceAIOConfig extends KodiSourceConfig {
|
||||
type: 'kodi'
|
||||
}
|
||||
@@ -10,7 +10,8 @@ import {MPRISSourceAIOConfig, MPRISSourceConfig} from "./mpris.js";
|
||||
import {MopidySourceAIOConfig, MopidySourceConfig} from "./mopidy.js";
|
||||
import {ListenBrainzSourceAIOConfig, ListenBrainzSourceConfig} from "./listenbrainz.js";
|
||||
import {JRiverSourceAIOConfig, JRiverSourceConfig} from "./jriver.js";
|
||||
import {KodiSourceAIOConfig, KodiSourceConfig} from "./kodi.js";
|
||||
|
||||
export type SourceConfig = SpotifySourceConfig | PlexSourceConfig | TautulliSourceConfig | DeezerSourceConfig | SubSonicSourceConfig | JellySourceConfig | LastfmSourceConfig | YTMusicSourceConfig | MPRISSourceConfig | MopidySourceConfig | ListenBrainzSourceConfig | JRiverSourceConfig;
|
||||
export type SourceConfig = SpotifySourceConfig | PlexSourceConfig | TautulliSourceConfig | DeezerSourceConfig | SubSonicSourceConfig | JellySourceConfig | LastfmSourceConfig | YTMusicSourceConfig | MPRISSourceConfig | MopidySourceConfig | ListenBrainzSourceConfig | JRiverSourceConfig | KodiSourceConfig;
|
||||
|
||||
export type SourceAIOConfig = SpotifySourceAIOConfig | PlexSourceAIOConfig | TautulliSourceAIOConfig | DeezerSourceAIOConfig | SubsonicSourceAIOConfig | JellySourceAIOConfig | LastFmSouceAIOConfig | YTMusicSourceAIOConfig | MPRISSourceAIOConfig | MopidySourceAIOConfig | ListenBrainzSourceAIOConfig | JRiverSourceAIOConfig;
|
||||
export type SourceAIOConfig = SpotifySourceAIOConfig | PlexSourceAIOConfig | TautulliSourceAIOConfig | DeezerSourceAIOConfig | SubsonicSourceAIOConfig | JellySourceAIOConfig | LastFmSouceAIOConfig | YTMusicSourceAIOConfig | MPRISSourceAIOConfig | MopidySourceAIOConfig | ListenBrainzSourceAIOConfig | JRiverSourceAIOConfig | KodiSourceAIOConfig;
|
||||
|
||||
@@ -322,6 +322,110 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"KodiData": {
|
||||
"properties": {
|
||||
"interval": {
|
||||
"default": 30,
|
||||
"description": "How long to wait before polling the source API for new tracks (in seconds)",
|
||||
"examples": [
|
||||
30
|
||||
],
|
||||
"type": "number"
|
||||
},
|
||||
"maxInterval": {
|
||||
"default": 60,
|
||||
"description": "When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)",
|
||||
"examples": [
|
||||
60
|
||||
],
|
||||
"type": "number"
|
||||
},
|
||||
"maxPollRetries": {
|
||||
"default": 5,
|
||||
"description": "default # of automatic polling restarts on error",
|
||||
"examples": [
|
||||
5
|
||||
],
|
||||
"type": "number"
|
||||
},
|
||||
"maxRequestRetries": {
|
||||
"default": 1,
|
||||
"description": "default # of http request retries a source can make before error is thrown",
|
||||
"examples": [
|
||||
1
|
||||
],
|
||||
"type": "number"
|
||||
},
|
||||
"options": {
|
||||
"$ref": "#/definitions/Record<string,any>"
|
||||
},
|
||||
"password": {
|
||||
"description": "The password set for Remote Control via Web Sever",
|
||||
"type": "string"
|
||||
},
|
||||
"retryMultiplier": {
|
||||
"default": 1.5,
|
||||
"description": "default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)",
|
||||
"examples": [
|
||||
1.5
|
||||
],
|
||||
"type": "number"
|
||||
},
|
||||
"url": {
|
||||
"default": "http://localhost:8080/jsonrpc",
|
||||
"description": "URL of the Kodi HTTP server to connect to\n\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:8080/jsonrpc`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `http://`\n* Hostname => `localhost`\n* Port => `8080`\n* Path => `/jsonrpc`",
|
||||
"examples": [
|
||||
"http://localhost:8080/jsonrpc"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"description": "The username set for Remote Control via Web Sever",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"password",
|
||||
"url",
|
||||
"username"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"KodiSourceAIOConfig": {
|
||||
"properties": {
|
||||
"clients": {
|
||||
"description": "Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.",
|
||||
"examples": [
|
||||
[
|
||||
"MyMalojaConfigName",
|
||||
"MyLastFMConfigName"
|
||||
]
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/definitions/KodiData"
|
||||
},
|
||||
"name": {
|
||||
"description": "Unique identifier for this source.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"kodi"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"LastFmSouceAIOConfig": {
|
||||
"properties": {
|
||||
"clients": {
|
||||
@@ -925,6 +1029,9 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/JRiverSourceAIOConfig"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/KodiSourceAIOConfig"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -429,6 +429,110 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"KodiData": {
|
||||
"properties": {
|
||||
"interval": {
|
||||
"default": 30,
|
||||
"description": "How long to wait before polling the source API for new tracks (in seconds)",
|
||||
"examples": [
|
||||
30
|
||||
],
|
||||
"type": "number"
|
||||
},
|
||||
"maxInterval": {
|
||||
"default": 60,
|
||||
"description": "When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)",
|
||||
"examples": [
|
||||
60
|
||||
],
|
||||
"type": "number"
|
||||
},
|
||||
"maxPollRetries": {
|
||||
"default": 5,
|
||||
"description": "default # of automatic polling restarts on error",
|
||||
"examples": [
|
||||
5
|
||||
],
|
||||
"type": "number"
|
||||
},
|
||||
"maxRequestRetries": {
|
||||
"default": 1,
|
||||
"description": "default # of http request retries a source can make before error is thrown",
|
||||
"examples": [
|
||||
1
|
||||
],
|
||||
"type": "number"
|
||||
},
|
||||
"options": {
|
||||
"$ref": "#/definitions/Record<string,any>"
|
||||
},
|
||||
"password": {
|
||||
"description": "The password set for Remote Control via Web Sever",
|
||||
"type": "string"
|
||||
},
|
||||
"retryMultiplier": {
|
||||
"default": 1.5,
|
||||
"description": "default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)",
|
||||
"examples": [
|
||||
1.5
|
||||
],
|
||||
"type": "number"
|
||||
},
|
||||
"url": {
|
||||
"default": "http://localhost:8080/jsonrpc",
|
||||
"description": "URL of the Kodi HTTP server to connect to\n\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:8080/jsonrpc`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `http://`\n* Hostname => `localhost`\n* Port => `8080`\n* Path => `/jsonrpc`",
|
||||
"examples": [
|
||||
"http://localhost:8080/jsonrpc"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"description": "The username set for Remote Control via Web Sever",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"password",
|
||||
"url",
|
||||
"username"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"KodiSourceAIOConfig": {
|
||||
"properties": {
|
||||
"clients": {
|
||||
"description": "Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.",
|
||||
"examples": [
|
||||
[
|
||||
"MyMalojaConfigName",
|
||||
"MyLastFMConfigName"
|
||||
]
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/definitions/KodiData"
|
||||
},
|
||||
"name": {
|
||||
"description": "Unique identifier for this source.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"kodi"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data",
|
||||
"type"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"LastFmSouceAIOConfig": {
|
||||
"properties": {
|
||||
"clients": {
|
||||
@@ -1504,6 +1608,9 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/JRiverSourceAIOConfig"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/KodiSourceAIOConfig"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/JRiverSourceConfig"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/KodiSourceConfig"
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
@@ -339,6 +342,103 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"KodiData": {
|
||||
"properties": {
|
||||
"interval": {
|
||||
"default": 30,
|
||||
"description": "How long to wait before polling the source API for new tracks (in seconds)",
|
||||
"examples": [
|
||||
30
|
||||
],
|
||||
"type": "number"
|
||||
},
|
||||
"maxInterval": {
|
||||
"default": 60,
|
||||
"description": "When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)",
|
||||
"examples": [
|
||||
60
|
||||
],
|
||||
"type": "number"
|
||||
},
|
||||
"maxPollRetries": {
|
||||
"default": 5,
|
||||
"description": "default # of automatic polling restarts on error",
|
||||
"examples": [
|
||||
5
|
||||
],
|
||||
"type": "number"
|
||||
},
|
||||
"maxRequestRetries": {
|
||||
"default": 1,
|
||||
"description": "default # of http request retries a source can make before error is thrown",
|
||||
"examples": [
|
||||
1
|
||||
],
|
||||
"type": "number"
|
||||
},
|
||||
"options": {
|
||||
"$ref": "#/definitions/Record<string,any>"
|
||||
},
|
||||
"password": {
|
||||
"description": "The password set for Remote Control via Web Sever",
|
||||
"type": "string"
|
||||
},
|
||||
"retryMultiplier": {
|
||||
"default": 1.5,
|
||||
"description": "default retry delay multiplier (retry attempt * multiplier = # of seconds to wait before retrying)",
|
||||
"examples": [
|
||||
1.5
|
||||
],
|
||||
"type": "number"
|
||||
},
|
||||
"url": {
|
||||
"default": "http://localhost:8080/jsonrpc",
|
||||
"description": "URL of the Kodi HTTP server to connect to\n\nmulti-scrobbler connects to the Web Service Interface endpoint that ultimately looks like this => `http://yourDomain:8080/jsonrpc`\n\nThe URL you provide here will have all parts not explicitly defined filled in for you so if these are not the default you must define them.\n\nParts => [default value]\n\n* Protocol => `http://`\n* Hostname => `localhost`\n* Port => `8080`\n* Path => `/jsonrpc`",
|
||||
"examples": [
|
||||
"http://localhost:8080/jsonrpc"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"description": "The username set for Remote Control via Web Sever",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"password",
|
||||
"url",
|
||||
"username"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"KodiSourceConfig": {
|
||||
"properties": {
|
||||
"clients": {
|
||||
"description": "Restrict scrobbling tracks played from this source to Clients with names from this list. If list is empty is not present Source scrobbles to all configured Clients.",
|
||||
"examples": [
|
||||
[
|
||||
"MyMalojaConfigName",
|
||||
"MyLastFMConfigName"
|
||||
]
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/definitions/KodiData"
|
||||
},
|
||||
"name": {
|
||||
"description": "Unique identifier for this source.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"LastFmSourceData": {
|
||||
"properties": {
|
||||
"apiKey": {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import MemorySource from "./MemorySource.js";
|
||||
import {FormatPlayObjectOptions, InternalConfig, PlayObject} from "../common/infrastructure/Atomic.js";
|
||||
import {EventEmitter} from "events";
|
||||
import {RecentlyPlayedOptions} from "./AbstractSource.js";
|
||||
import {KodiSourceConfig} from "../common/infrastructure/config/source/kodi.js";
|
||||
import {KodiApiClient} from "../apis/KodiApiClient.js";
|
||||
|
||||
export class KodiSource extends MemorySource {
|
||||
declare config: KodiSourceConfig;
|
||||
|
||||
client: KodiApiClient;
|
||||
clientReady: boolean = false;
|
||||
|
||||
constructor(name: any, config: KodiSourceConfig, internal: InternalConfig, emitter: EventEmitter) {
|
||||
const {
|
||||
data,
|
||||
} = config;
|
||||
const {
|
||||
interval = 10,
|
||||
maxInterval = 30,
|
||||
...rest
|
||||
} = data || {};
|
||||
super('kodi', name, {...config, data: {interval, maxInterval, ...rest}}, internal, emitter);
|
||||
|
||||
this.client = new KodiApiClient(name, data);
|
||||
this.requiresAuth = true;
|
||||
this.canPoll = true;
|
||||
this.multiPlatform = true;
|
||||
}
|
||||
|
||||
initialize = async () => {
|
||||
const {
|
||||
data: {
|
||||
url
|
||||
} = {}
|
||||
} = this.config;
|
||||
this.logger.debug(`Config URL: '${url ?? '(None Given)'}' => Normalized: '${this.client.url.toString()}'`)
|
||||
this.initialized = true;
|
||||
return true;
|
||||
|
||||
/*const connected = await this.client.testConnection();
|
||||
if(connected) {
|
||||
//this.logger.info('Connection OK');
|
||||
this.initialized = true;
|
||||
return true;
|
||||
} else {
|
||||
this.logger.error(`Could not connect.`);
|
||||
this.initialized = false;
|
||||
return false;
|
||||
}*/
|
||||
}
|
||||
|
||||
testAuth = async () => {
|
||||
const resp = await this.client.testAuth();
|
||||
this.authed = resp;
|
||||
this.clientReady = this.authed;
|
||||
return this.authed;
|
||||
}
|
||||
|
||||
static formatPlayObj(obj: any, options: FormatPlayObjectOptions = {}): PlayObject {
|
||||
return KodiApiClient.formatPlayObj(obj, options);
|
||||
}
|
||||
|
||||
getRecentlyPlayed = async (options: RecentlyPlayedOptions = {}) => {
|
||||
if (!this.clientReady) {
|
||||
this.logger.warn('Cannot actively poll since client is not connected.');
|
||||
return [];
|
||||
}
|
||||
|
||||
let play = await this.client.getRecentlyPlayed(options);
|
||||
|
||||
return this.processRecentPlays(play);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,6 +33,8 @@ import ListenbrainzSource from "./ListenbrainzSource.js";
|
||||
import {ListenBrainzSourceConfig} from "../common/infrastructure/config/source/listenbrainz.js";
|
||||
import {JRiverSource} from "./JRiverSource.js";
|
||||
import {JRiverData, JRiverSourceConfig} from "../common/infrastructure/config/source/jriver.js";
|
||||
import {KodiSource} from "./KodiSource.js";
|
||||
import {KodiData, KodiSourceConfig} from "../common/infrastructure/config/source/kodi.js";
|
||||
|
||||
type groupedNamedConfigs = {[key: string]: ParsedConfig[]};
|
||||
|
||||
@@ -278,6 +280,23 @@ export default class ScrobbleSources {
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'kodi':
|
||||
const ko = {
|
||||
url: process.env.KODI_URL,
|
||||
username: process.env.KODI_USER,
|
||||
password: process.env.KODI_PASSWORD
|
||||
}
|
||||
if (!Object.values(ko).every(x => x === undefined)) {
|
||||
configs.push({
|
||||
type: 'kodi',
|
||||
name: 'unnamed',
|
||||
source: 'ENV',
|
||||
mode: 'single',
|
||||
configureAs: defaultConfigureAs,
|
||||
data: ko as KodiData
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -444,6 +463,9 @@ export default class ScrobbleSources {
|
||||
case 'jriver':
|
||||
newSource = await new JRiverSource(name, compositeConfig as JRiverSourceConfig, internal, this.emitter);
|
||||
break;
|
||||
case 'kodi':
|
||||
newSource = await new KodiSource(name, compositeConfig as KodiSourceConfig, internal, this.emitter);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user