Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f856f79e51 | ||
|
|
fbea08dded | ||
|
|
dc52376395 | ||
|
|
1e2bc0ca85 | ||
|
|
7849d6098a | ||
|
|
0b101b758e | ||
|
|
756ff0e0c1 | ||
|
|
2ddeca7d29 | ||
|
|
1de934e30a | ||
|
|
0409c526ee | ||
|
|
a7c340188f | ||
|
|
5dc54849cc | ||
|
|
058fbee0c8 | ||
|
|
daea76eef3 | ||
|
|
b81b1d94a3 | ||
|
|
226b7c741c | ||
|
|
e452bd761c | ||
|
|
19c3d97a46 | ||
|
|
64164b28de | ||
|
|
b03cbe7a0a | ||
|
|
91bd702deb | ||
|
|
ede22e10e9 | ||
|
|
c648ad4ae3 | ||
|
|
a0fbd1129a | ||
|
|
b90ea03924 | ||
|
|
88efe2ad87 | ||
|
|
39f933418e | ||
|
|
cc8c7d84e2 | ||
|
|
6d9783c5f5 | ||
|
|
c10d46b774 | ||
|
|
a5417277a8 | ||
|
|
5a1c931513 | ||
|
|
09f4503593 | ||
|
|
045cae66f5 | ||
|
|
e67ef5fb0f | ||
|
|
1677c0021f | ||
|
|
3400e747a8 | ||
|
|
c08ff4843c |
@@ -7,7 +7,7 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
[Please check the FAQ](https://github.com/FoxxMD/multi-scrobbler/blob/master/docs/FAQ.md) before submitting a bug report.
|
||||
[Please check the FAQ](https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/FAQ.md) before submitting a bug report.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
@@ -23,7 +23,7 @@ Steps to reproduce the behavior:
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Logs**
|
||||
If possible reproduce the issue with [debug logging ON](https://github.com/FoxxMD/multi-scrobbler/blob/master/docs/FAQ.md#turn-on-debug-logging)
|
||||
If possible reproduce the issue with [debug logging ON](https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/FAQ.md#turn-on-debug-logging)
|
||||
|
||||
```
|
||||
Copy and paste as much log data as possible related to this issue here.
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
# Review gh actions docs if you want to further define triggers, paths, etc
|
||||
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
working-directory: ./docsite
|
||||
- name: Build website
|
||||
run: npm run build
|
||||
working-directory: ./docsite
|
||||
|
||||
# Popular action to deploy to GitHub Pages:
|
||||
# Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Build output to publish to the `gh-pages` branch:
|
||||
publish_dir: ./docsite/build
|
||||
# The following lines assign commit authorship to the official
|
||||
# GH-Actions bot for deploys to `gh-pages` branch:
|
||||
# https://github.com/actions/checkout/issues/13#issuecomment-724415212
|
||||
# The GH actions bot is used by default if you didn't specify the two fields.
|
||||
# You can swap them out with your own user credentials.
|
||||
user_name: github-actions[bot]
|
||||
user_email: 41898282+github-actions[bot]@users.noreply.github.com
|
||||
@@ -15,22 +15,36 @@ jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@dd4fa0671be5250ee6f50aedf4cb05514abda2c7
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@f2a13332ac1ce8c0a71aeac48a150dbb1838ab67
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: foxxmd/multi-scrobbler
|
||||
images: |
|
||||
foxxmd/multi-scrobbler
|
||||
ghcr.io/foxxmd/multi-scrobbler
|
||||
# generate Docker tags based on the following events/attributes
|
||||
tags: |
|
||||
type=raw,value=latest,enable=${{ endsWith(github.ref, 'master') }}
|
||||
@@ -40,16 +54,16 @@ jobs:
|
||||
latest=false
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@27d0a4f181a40b142cce983c5393082c365d1480
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@94ab11c41e45d028884a99163086648e898eed25
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
push: ${{ github.event_name != 'pull_request' && !env.ACT}}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -10,28 +10,28 @@ alt="multi-scrobbler logo" width="180" height="180">
|
||||
A javascript app to scrobble music you listened to, to [Maloja](https://github.com/krateng/maloja), [Last.fm](https://www.last.fm), and [ListenBrainz](https://listenbrainz.org)
|
||||
|
||||
* Supports scrobbling from many **Sources**
|
||||
* [Spotify](/docs/configuration.md#spotify)
|
||||
* [Plex](/docs/configuration.md#plex) or [Tautulli](/docs/configuration.md#tautulli)
|
||||
* [Subsonic-compatible APIs](/docs/configuration.md#subsonic) (like [Airsonic](https://airsonic.github.io/))
|
||||
* [Jellyfin](/docs/configuration.md#jellyfin)
|
||||
* [Youtube Music](/docs/configuration.md#youtube-music)
|
||||
* [Last.fm](/docs/configuration.md#lastfm-source)
|
||||
* [ListenBrainz](/docs/configuration.md#listenbrainz--source-)
|
||||
* [Deezer](/docs/configuration.md#deezer)
|
||||
* [MPRIS (Linux Desktop)](/docs/configuration.md#mpris)
|
||||
* [Mopidy](/docs/configuration.md#mopidy)
|
||||
* [JRiver](/docs/configuration.md#jriver)
|
||||
* [Kodi](/docs/configuration.md#kodi)
|
||||
* [Spotify](/docsite/docs/configuration/configuration.md#spotify)
|
||||
* [Plex](/docsite/docs/configuration/configuration.md#plex) or [Tautulli](/docsite/docs/configuration/configuration.md#tautulli)
|
||||
* [Subsonic-compatible APIs](/docsite/docs/configuration/configuration.md#subsonic) (like [Airsonic](https://airsonic.github.io/))
|
||||
* [Jellyfin](/docsite/docs/configuration/configuration.md#jellyfin)
|
||||
* [Youtube Music](/docsite/docs/configuration/configuration.md#youtube-music)
|
||||
* [Last.fm](/docsite/docs/configuration/configuration.md#lastfm-source)
|
||||
* [ListenBrainz](/docsite/docs/configuration/configuration.md#listenbrainz--source-)
|
||||
* [Deezer](/docsite/docs/configuration/configuration.md#deezer)
|
||||
* [MPRIS (Linux Desktop)](/docsite/docs/configuration/configuration.md#mpris)
|
||||
* [Mopidy](/docsite/docs/configuration/configuration.md#mopidy)
|
||||
* [JRiver](/docsite/docs/configuration/configuration.md#jriver)
|
||||
* [Kodi](/docsite/docs/configuration/configuration.md#kodi)
|
||||
* Supports scrobbling to many **Clients**
|
||||
* [Maloja](/docs/configuration.md#maloja)
|
||||
* [Last.fm](/docs/configuration.md#lastfm)
|
||||
* [ListenBrainz](/docs/configuration.md#listenbrainz)
|
||||
* Monitor status of Sources and Clients using [webhooks (Gotify or Ntfy)](/docs/configuration.md#webhook-configurations) or [healthcheck endpoint](/docs/configuration.md#health-endpoint)
|
||||
* [Maloja](/docsite/docs/configuration/configuration.md#maloja)
|
||||
* [Last.fm](/docsite/docs/configuration/configuration.md#lastfm)
|
||||
* [ListenBrainz](/docsite/docs/configuration/configuration.md#listenbrainz)
|
||||
* Monitor status of Sources and Clients using [webhooks (Gotify or Ntfy)](/docsite/docs/configuration/configuration.md#webhook-configurations) or [healthcheck endpoint](/docsite/docs/configuration/configuration.md#health-endpoint)
|
||||
* Supports configuring for single or multiple users (scrobbling for your friends and family!)
|
||||
* Web server interface for stats, basic control, and detailed logs
|
||||
* Smart handling of credentials (persistent, authorization through app)
|
||||
* Easy configuration through ENVs or JSON
|
||||
* Install using [Docker images for x86/ARM](/docs/installation.md#docker), [flatpak](/docs/installation.md#flatpak), or [locally with NodeJS](/docs/installation.md#nodejs)
|
||||
* Install using [Docker images for x86/ARM](/docsite/docs/installation/installation.md#docker), [flatpak](/docsite/docs/installation/installation.md#flatpak), or [locally with NodeJS](/docsite/docs/installation/installation.md#nodejs)
|
||||
|
||||
**Why should I use this over a browser extension and/or mobile app scrobbler?**
|
||||
|
||||
@@ -42,7 +42,7 @@ A javascript app to scrobble music you listened to, to [Maloja](https://github.c
|
||||
|
||||
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
|
||||
|
||||
Yes! You can use [Last.fm as a **Source**](/docs/configuration.md#lastfm--source-) or [Listenbrainz as a **Source**](/docs/configuration.md#listenbrainz--source-) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
|
||||
Yes! You can use [Last.fm as a **Source**](/docsite/docs/configuration/configuration.md#lastfm--source-) or [Listenbrainz as a **Source**](/docsite/docs/configuration/configuration.md#listenbrainz--source-) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
|
||||
|
||||
<img src="/assets/status-ui.jpg" width="800">
|
||||
|
||||
@@ -71,11 +71,11 @@ Client configurations consist of:
|
||||
|
||||
## Installation
|
||||
|
||||
[See the **Installation** documentation](/docs/installation.md)
|
||||
[See the **Installation** documentation](/docsite/docs/installation/installation.md)
|
||||
|
||||
## Configuration
|
||||
|
||||
[See the **Configuration** documentation](/docs/configuration.md)
|
||||
[See the **Configuration** documentation](/docsite/docs/configuration/configuration.md)
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -90,7 +90,7 @@ On first startup you may need to authorize Spotify and/or Last.fm by visiting th
|
||||
|
||||
## Help/FAQ
|
||||
|
||||
Having issues with connections or configuration? Check the [FAQ](/docs/FAQ.md) before creating an issue!
|
||||
Having issues with connections or configuration? Check the [FAQ](/docsite/docs/FAQ.md) before creating an issue!
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# configuration file for git-cliff (0.1.0)
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://tera.netlify.app/docs/#introduction
|
||||
body = """
|
||||
{% if version %}\
|
||||
## [{{ version | replace(from="v", to="") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
{% for commit in commits
|
||||
| filter(attribute="scope")
|
||||
| sort(attribute="scope") %}
|
||||
- *({{commit.scope}})* {{ commit.message | upper_first }}
|
||||
{%- if commit.breaking %}
|
||||
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- for commit in commits %}
|
||||
{%- if commit.scope -%}
|
||||
{% else -%}
|
||||
- *(No Category)* {{ commit.message | upper_first }}
|
||||
{% if commit.breaking -%}
|
||||
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
|
||||
{% endif -%}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
{% endfor %}
|
||||
"""
|
||||
# remove the leading and trailing whitespaces from the template
|
||||
trim = true
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
|
||||
[git]
|
||||
# allow only conventional commits
|
||||
# https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "Features"},
|
||||
{ message = "^fix", group = "Bug Fixes"},
|
||||
{ message = "^doc", group = "Documentation"},
|
||||
{ message = "^perf", group = "Performance"},
|
||||
{ message = "^refactor", group = "Refactor"},
|
||||
{ message = "^style", group = "Styling"},
|
||||
{ message = "^test", group = "Testing"},
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true},
|
||||
{ message = "^chore", group = "Miscellaneous Tasks"},
|
||||
{ body = ".*security", group = "Security"},
|
||||
]
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = "v0.1.0-beta.1"
|
||||
@@ -8,5 +8,5 @@ These are **NOT** exhaustive examples. You should consult the **configuration**
|
||||
|
||||
Documentation at
|
||||
|
||||
* [internal docs](/docs/configuration.md)
|
||||
* External Link: https://github.com/FoxxMD/multi-scrobbler/blob/master/docs/configuration.md
|
||||
* [internal docs](../docsite/docs/configuration/configuration.md)
|
||||
* External Link: https://github.com/FoxxMD/multi-scrobbler/blob/master/docsite/docs/configuration/configuration.md
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"users": ["FoxxMD"],
|
||||
"servers": ["myServer","anotherServer"],
|
||||
"options": {
|
||||
"logPayload": false
|
||||
"logPayload": false,
|
||||
"logFilterFailure": "warn"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "brainz",
|
||||
"configureAs": "client"
|
||||
"configureAs": "client",
|
||||
"data": {
|
||||
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
|
||||
"username": "FoxxMD"
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"data": {
|
||||
"user": ["username@gmail.com","anotherUser@gmail.com"],
|
||||
"libraries": ["music","my podcasts"],
|
||||
"servers": ["myServer","anotherServer"]
|
||||
"servers": ["myServer","anotherServer"],
|
||||
"options": {
|
||||
"logFilterFailure": "warn"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
"data": {
|
||||
"user": ["username@gmail.com","anotherUser@gmail.com"],
|
||||
"libraries": ["music","my podcasts"],
|
||||
"servers": ["myServer","anotherServer"]
|
||||
"servers": ["myServer","anotherServer"],
|
||||
"options": {
|
||||
"logFilterFailure": "warn"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -0,0 +1,41 @@
|
||||
# Website
|
||||
|
||||
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
|
||||
|
||||
### Installation
|
||||
|
||||
```
|
||||
$ yarn
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
```
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||
|
||||
### Deployment
|
||||
|
||||
Using SSH:
|
||||
|
||||
```
|
||||
$ USE_SSH=true yarn deploy
|
||||
```
|
||||
|
||||
Not using SSH:
|
||||
|
||||
```
|
||||
$ GIT_USER=<Your GitHub username> yarn deploy
|
||||
```
|
||||
|
||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
};
|
||||
@@ -4,12 +4,15 @@
|
||||
* [Spotify/Deezer/LastFM won't authenticate](#spotifydeezerlastfm-wont-authenticate)
|
||||
* [Configuration Issues](#configuration-issues)
|
||||
* [Config could not be parsed](#config-could-not-be-parsed)
|
||||
* [Scrobbling Issues](#scrobbling-issues)
|
||||
* [Last.fm does not scrobble tracks with multiple artists correctly](#lastfm-does-not-scrobble-tracks-with-multiple-artists-correctly)
|
||||
* [Jellyfin does not scrobble tracks with multiple artists correctly](#jellyfin-does-not-scrobble-tracks-with-multiple-artists-correctly)
|
||||
|
||||
# Connection Issues
|
||||
|
||||
## Plex/Tautulli/Jellyfin don't connect
|
||||
|
||||
These three [sources](/README.md#source) are **ingress-based** which means that multi-scrobbler waits for the Plex/Tautulli/Jellyfin server to contact multi-scrobbler, as opposed to multi-scrobbler contacting the server.
|
||||
These three [sources](/#source) are **ingress-based** which means that multi-scrobbler waits for the Plex/Tautulli/Jellyfin server to contact multi-scrobbler, as opposed to multi-scrobbler contacting the server.
|
||||
|
||||
multi-scrobbler will log information about any server that connects to it for these three services. In the logs it looks something like this:
|
||||
|
||||
@@ -19,7 +22,6 @@ multi-scrobbler will log information about any server that connects to it for th
|
||||
2023-02-22T10:55:56-05:00 warn : [Plex Request ] Received valid Plex webhook payload but no Plex sources are configured
|
||||
```
|
||||
It also logs if a server tries to connect to a URL that it does not recognize:
|
||||
|
||||
```
|
||||
2023-02-22T11:16:12-05:00 debug : [App ] Server received POST request from ::ffff:192.168.0.140 (UA: PlexMediaServer/1.24.5.5173-8dcc73a59) to unknown route: /plkex
|
||||
```
|
||||
@@ -42,7 +44,7 @@ Check the output for any additional information.
|
||||
|
||||
#### Check Host name and URL
|
||||
|
||||
The URLs examples in the [configuration](/docs/configuration.md) documentation assume you are running Plex/Tautulli/Jellyfin on the same server as multi-scrobbler. If these are not the same machine then you need to determine the IP address or domain name that multi-scrobbler is reachable at and use that instead of `localhost` when configuring these sources. **This is likely the same host name that you would use to access the web interface for multi-scrobbler.**
|
||||
The URLs examples in the [configuration](configuration/configuration.md) documentation assume you are running Plex/Tautulli/Jellyfin on the same server as multi-scrobbler. If these are not the same machine then you need to determine the IP address or domain name that multi-scrobbler is reachable at and use that instead of `localhost` when configuring these sources. **This is likely the same host name that you would use to access the web interface for multi-scrobbler.**
|
||||
|
||||
EX `http://localhost:9078/plex` -> `http://192.168.0.140:9078/plex`
|
||||
|
||||
@@ -69,7 +71,7 @@ Administration -> Dashboard -> Advanced -> Logs
|
||||
## Jellyfin has warnings about undefined or missing data
|
||||
|
||||
Make sure you have
|
||||
* [Configured the webhook plugin correctly](/docs/configuration.md#jellyfin)
|
||||
* [Configured the webhook plugin correctly](configuration/configuration.md#jellyfin)
|
||||
* Checked the **Send All Properties(ignores template)** option in the webhook settings and **Saved**
|
||||
|
||||
multi-scrobbler is known to work on Jellyfin `10.8.9` with Webhook version `11.0.0.0`.
|
||||
@@ -120,3 +122,15 @@ If you see something like this in your logs:
|
||||
```
|
||||
|
||||
It means the JSON in your configuration file is not valid. Copy and paste your configuration into a site like [JSONLint](https://jsonlint.com/) to find out where errors you have and fix them.
|
||||
|
||||
# Scrobbling Issues
|
||||
|
||||
## Last.fm does not scrobble tracks with multiple artists correctly
|
||||
|
||||
This is a limitation of the [Last.fm API](https://www.last.fm/api/show/track.scrobble) where the **artist** field is only one string and Last.fm does not recognize (play well) with "combined" artists.
|
||||
|
||||
Multi-scrobbler works the same was the official Spotify-Last.fm integration works -- it only scrobbles the **first** artist on a multi-artist track.
|
||||
|
||||
## Jellyfin does not scrobble tracks with multiple artists correctly
|
||||
|
||||
This is a limitation caused by the [Jellyfin webhook plugin](https://github.com/FoxxMD/multi-scrobbler/issues/70#issuecomment-1443804712) only sending the first artist to multi-scrobbler. This issues needs to be [fixed upstream on the Jellyfin webhook repository.](https://github.com/jellyfin/jellyfin-plugin-webhook/issues/166)
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"label": "Configuration",
|
||||
"position": 2,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Configuring Multi-Scrobbler and examples"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
* [Configuration Overview](#configuration-overview)
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Overview
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
* [Overview](#overview)
|
||||
* [ENV-Based Configuration](#env-based-configuration)
|
||||
* [File-Based Configuration](#file-based-configuration)
|
||||
* [All-in-One File Configuration](#all-in-one-file-configuration)
|
||||
@@ -25,13 +32,13 @@
|
||||
* [Webhooks](#webhook-configurations)
|
||||
* [Health Endpoint](#health-endpoint)
|
||||
|
||||
# Configuration Overview
|
||||
# Overview
|
||||
|
||||
[**Sources** and **Clients**](/README.md#how-does-multi-scrobbler-ms-work) are configured using environmental (ENV) variables and/or json files.
|
||||
[**Sources** and **Clients**](/#how-does-multi-scrobbler-ms-work) are configured using environmental (ENV) variables and/or json files.
|
||||
|
||||
**MS will parse configuration from both configuration types.** You can mix and match configurations but it is generally better to stick to one or the other.
|
||||
|
||||
TIP: Check the [**FAQ**](/docs/FAQ.md) if you have any issues after configuration!
|
||||
TIP: Check the [**FAQ**](../FAQ.md) if you have any issues after configuration!
|
||||
|
||||
## ENV-Based Configuration
|
||||
|
||||
@@ -43,18 +50,18 @@ This is done by passing environmental variables and so does not require any file
|
||||
Use ENV-based configuration if:
|
||||
|
||||
* You are the only person for whom MS is scrobbling for
|
||||
* You have a very simple setup for MS such as one scrobble [Client](/README.md#client) and one [Source](/README.md#source) IE Plex -> Maloja
|
||||
* You have a very simple setup for MS such as one scrobble [Client](/#client) and one [Source](/#source) IE Plex -> Maloja
|
||||
|
||||
## File-Based Configuration
|
||||
|
||||
MS will parse configuration files located in the directory specified by the `CONFIG_DIR` environmental variable. This variable defaults to:
|
||||
|
||||
* Local installation -> `PROJECT_DIR/config`
|
||||
* Docker -> `/config` (in the container) -- see the [install docs](/docs/installation.md#docker) for how to configure this correctly
|
||||
* Docker -> `/config` (in the container) -- see the [install docs](../installation/installation.md#docker) for how to configure this correctly
|
||||
|
||||
Use File-based configuration if:
|
||||
|
||||
* You have many [Sources](/README.md#source)
|
||||
* You have many [Sources](/#source)
|
||||
* You have many of each type of **Source** you want to scrobble from IE 2x Plex accounts, 3x Spotify accounts, 1x
|
||||
Funkwhale...
|
||||
* You have more than one scrobble **Client** you want to scrobble to IE multiple Maloja servers
|
||||
@@ -66,10 +73,10 @@ File-based configurations located in the `CONFIG_DIR` directory can be parsed fr
|
||||
* an **all-in-one** config file named `config.json` that contains information for all Sources and Clients and/or
|
||||
* many **specific** files named based on the client/source to configure IE `plex.json` `spotify.json`
|
||||
|
||||
There are **example configurations** for all Source/Client types and AIO config located in the [/config](/config) directory of this project. These can be used as-is by renaming them to `.json`.
|
||||
There are **example configurations** for all Source/Client types and AIO config located in the [/config](https://github.com/FoxxMD/multi-scrobbler/tree/master/config) directory of this project. These can be used as-is by renaming them to `.json`.
|
||||
For docker installations these examples are copied to your configuration directory on first-time use.
|
||||
|
||||
There is also a [**kitchensink example**](/docs/kitchensink.md) that provides examples of using all sources/clients in a complex configuration.
|
||||
There is also a [**kitchensink example**](kitchensink.md) that provides examples of using all sources/clients in a complex configuration.
|
||||
|
||||
### All-in-One File Configuration
|
||||
|
||||
@@ -85,8 +92,7 @@ Example directory structure:
|
||||
<details>
|
||||
<summary>Config Example</summary>
|
||||
|
||||
```json5
|
||||
// in config.json
|
||||
```json5 title="config.json"
|
||||
{
|
||||
//...
|
||||
"sources": [
|
||||
@@ -119,7 +125,7 @@ Example directory structure:
|
||||
|
||||
`config.json` can also be used to set default behavior for all sources/clients using `sourceDefaults` and `clientDefaults` properties.
|
||||
|
||||
See [config.json.example](/config/config.json.example) for an annotated example or check out [the kitchen sink example](kitchensink.md).
|
||||
See [config.json.example](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/config.json.example) for an annotated example or check out [the kitchen sink example](kitchensink.md).
|
||||
|
||||
### Specific File Configuration
|
||||
|
||||
@@ -160,7 +166,7 @@ Example directory structure:
|
||||
|
||||
</details>
|
||||
|
||||
See the [/config](/config) directory of this project for examples of each type of config file or reference specific files below.
|
||||
See the [/config](https://github.com/FoxxMD/multi-scrobbler/blob/master/config) directory of this project for examples of each type of config file or reference specific files below.
|
||||
|
||||
# Source Configurations
|
||||
|
||||
@@ -179,7 +185,7 @@ Client ID/Secret. Make sure to also whitelist your redirect URI in the applicati
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`spotify.json.example`](/config/spotify.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FSpotifySourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
See [`spotify.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/spotify.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FSpotifySourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Plex](https://plex.tv)
|
||||
|
||||
@@ -193,7 +199,7 @@ Check the [instructions](plex.md) on how to setup a [webhooks](https://support.p
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`plex.json.example`](/config/plex.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FPlexSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
See [`plex.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/plex.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FPlexSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Tautulli](https://tautulli.com)
|
||||
|
||||
@@ -207,7 +213,7 @@ Check the [instructions](plex.md) on how to setup a notification agent.
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`tautulli.json.example`](/config/tautulli.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FTautulliSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
See [`tautulli.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/tautulli.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FTautulliSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Subsonic](http://www.subsonic.org/)
|
||||
|
||||
@@ -230,7 +236,7 @@ Can use this source for any application that implements the [Subsonic API](http:
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`subsonic.json.example`](/config/subsonic.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FSubSonicSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
See [`subsonic.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/subsonic.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FSubSonicSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Jellyfin](https://jellyfin.org/)
|
||||
|
||||
@@ -258,7 +264,7 @@ Must be using Jellyfin 10.7 or greater
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`jellyfin.json.example`](/config/jellyfin.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FJellySourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
See [`jellyfin.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/jellyfin.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FJellySourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Last.fm (Source)](https://www.last.fm)
|
||||
|
||||
@@ -270,7 +276,7 @@ No support for ENV based for Last.fm as a client (only source)
|
||||
|
||||
### File-Based
|
||||
|
||||
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)
|
||||
See [`lastfm.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/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)
|
||||
|
||||
@@ -282,7 +288,7 @@ On your [profile page](https://listenbrainz.org/profile/) find your **User Token
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`listenbrainz.json.example`](/config/listenbrainz.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FListenBrainzSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
See [`listenbrainz.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/listenbrainz.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FListenBrainzSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
**Change `configureAs` to `source`**
|
||||
|
||||
@@ -314,7 +320,7 @@ After starting multi-scrobbler with credentials in-place open the dashboard (`ht
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`deezer.json.example`](/config/deezer.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FDeezerSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
See [`deezer.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/deezer.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FDeezerSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Youtube Music](https://music.youtube.com)
|
||||
|
||||
@@ -331,7 +337,7 @@ Then...
|
||||
3. In the **Request Headers** section find and copy the **entire value** found after `Cookie:` and use this as the `cookie` value in your multi-scrobbler config
|
||||
4. If present, in the **Request Headers** section find and copy the number found in `X-google-AuthUser` and use this as the value for `authUser` in your multi-scrobbler config
|
||||
|
||||

|
||||

|
||||
|
||||
NOTES:
|
||||
|
||||
@@ -340,7 +346,7 @@ NOTES:
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`ytmusic.json.example`](/config/ytmusic.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FYTMusicSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
See [`ytmusic.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/ytmusic.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FYTMusicSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [MPRIS](https://specifications.freedesktop.org/mpris-spec/latest/)
|
||||
|
||||
@@ -348,11 +354,11 @@ MPRIS is a standard interface for communicating with Music Players on **linux op
|
||||
|
||||
If you run Linux and have a notification tray that shows what media you are listening to, you likely have access to MPRIS.
|
||||
|
||||

|
||||

|
||||
|
||||
multi-scrobbler can listen to this interface and scrobble tracks played by **any media player** that communicates to the operating system with MPRIS.
|
||||
|
||||
**NOTE:** multi-scrobbler needs to be running as a [**Local Installation**](/docs/installation.md#local) in order to use MPRIS. This cannot be used from docker.
|
||||
**NOTE:** multi-scrobbler needs to be running as a [**Local Installation**](../installation/installation.md#local) in order to use MPRIS. This cannot be used from docker.
|
||||
|
||||
### ENV-Based
|
||||
|
||||
@@ -364,7 +370,7 @@ multi-scrobbler can listen to this interface and scrobble tracks played by **any
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`mpris.json.example`](/config/mpris.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FMPRISSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
See [`mpris.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/mpris.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FMPRISSourceConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
## [Mopidy](https://mopidy.com/)
|
||||
|
||||
@@ -374,7 +380,7 @@ multi-scrobbler can scrobble tracks played from any Mopidy backend source, regar
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`mopidy.json.example`](/config/mopidy.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FMopidySourceConfig/%23%2Fdefinitions%2FMopidyData?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
See [`mopidy.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/mopidy.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FMopidySourceConfig/%23%2Fdefinitions%2FMopidyData?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fsource.json)
|
||||
|
||||
Configuration Options:
|
||||
|
||||
@@ -531,7 +537,7 @@ MS transforms this to: `http://mydomain.com:80/jriverReverse/MCWS/v1/`
|
||||
|
||||
### 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)
|
||||
See [`jriver.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/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/)
|
||||
|
||||
@@ -599,7 +605,7 @@ MS transforms this to: `http://mydomain.com:80/kodiReverse/jsonrpc`
|
||||
|
||||
### 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)
|
||||
See [`kodi.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/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
|
||||
|
||||
@@ -614,7 +620,7 @@ See [`kodi.json.example`](/config/kodi.json.example) or [explore the schema with
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`maloja.json.example`](/config/maloja.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FMalojaClientConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fclient.json)
|
||||
See [`maloja.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/maloja.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FMalojaClientConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fclient.json)
|
||||
|
||||
## [Last.fm](https://www.last.fm)
|
||||
|
||||
@@ -637,7 +643,7 @@ or replace `localhost:9078` with your own base URL
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`lastfm.json.example`](/config/lastfm.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FLastfmClientConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fclient.json)
|
||||
See [`lastfm.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/lastfm.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23/%23%2Fdefinitions%2FLastfmClientConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fclient.json)
|
||||
|
||||
## [Listenbrainz](https://listenbrainz.org)
|
||||
|
||||
@@ -656,7 +662,7 @@ On your [profile page](https://listenbrainz.org/profile/) find your **User Token
|
||||
|
||||
### File-Based
|
||||
|
||||
See [`listenbrainz.json.example`](/config/listenbrainz.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FListenBrainzClientConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fclient.json)
|
||||
See [`listenbrainz.json.example`](https://github.com/FoxxMD/multi-scrobbler/blob/master/config/listenbrainz.json.example) or [explore the schema with an example and live editor/validator](https://json-schema.app/view/%23%2Fdefinitions%2FListenBrainzClientConfig?url=https%3A%2F%2Fraw.githubusercontent.com%2FFoxxMD%2Fmulti-scrobbler%2Fdevelop%2Fsrc%2Fcommon%2Fschema%2Fclient.json)
|
||||
|
||||
# Monitoring
|
||||
|
||||
@@ -673,13 +679,13 @@ Webhooks will **push** a notification to your configured servers on these events
|
||||
|
||||
Webhooks are configured in the main [config.json](#all-in-one-file-configuration) file under the `webhook` top-level property. Multiple webhooks may be configured for each webhook type. EX:
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"sources": [
|
||||
...
|
||||
//...
|
||||
],
|
||||
"clients": [
|
||||
...
|
||||
//...
|
||||
],
|
||||
"webhooks": [
|
||||
{
|
||||
@@ -691,14 +697,14 @@ Webhooks are configured in the main [config.json](#all-in-one-file-configuration
|
||||
{
|
||||
"name": "SecondGotifyServer",
|
||||
"type": "gotify",
|
||||
...
|
||||
//...
|
||||
},
|
||||
{
|
||||
"name": "NtfyServerOne",
|
||||
"type": "ntfy",
|
||||
...
|
||||
//...
|
||||
},
|
||||
...
|
||||
//...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 226 KiB |
@@ -1,3 +1,8 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
title: Kitchen Sink
|
||||
---
|
||||
|
||||
# Example Config using all Possible Features
|
||||
|
||||
Scenario:
|
||||
|
After Width: | Height: | Size: 36 KiB |
@@ -1,3 +1,8 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Plex/Tautulli
|
||||
---
|
||||
|
||||
Tracks played on [Plex](https://plex.tv/) can be scrobbled either by:
|
||||
* A [Tautulli](https://tautulli.com/) notification agent with a webhook.
|
||||
* Using Plex [Webhooks](https://support.plex.tv/articles/115002267687-webhooks) (restricted to Plex Pass users)
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"label": "Installation",
|
||||
"position": 1,
|
||||
"link": {
|
||||
"type": "generated-index",
|
||||
"description": "Way to install Multi-Scrobbler"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: 'Overview'
|
||||
---
|
||||
|
||||
# Installation
|
||||
|
||||
# Local
|
||||
|
||||
After installation see [service.md](/docs/service.md) to configure multi-scrobbler to run automatically in the background.
|
||||
After installation see [service.md](service.md) to configure multi-scrobbler to run automatically in the background.
|
||||
|
||||
## Nodejs
|
||||
|
||||
@@ -21,13 +26,13 @@ npm start
|
||||
|
||||
* The web UI is served on port `9078`. This can be modified using the `PORT` environmental variable.
|
||||
|
||||
#### Using [file-based](/docs/configuration.md#file-based-configuration) configuration
|
||||
#### Using [file-based](../configuration/configuration.md#file-based-configuration) configuration
|
||||
|
||||
```shell
|
||||
npm start
|
||||
```
|
||||
|
||||
#### Using [env-based](/docs/configuration.md#env-based-configuration) configuration
|
||||
#### Using [env-based](../configuration/configuration.md#env-based-configuration) configuration
|
||||
|
||||
```shell
|
||||
SPOTIFY_CLIENT_ID=yourId SPOTIFY_CLIENT_SECRET=yourSecret MALOJA_URL="http://domain.tld" node src/index.js
|
||||
@@ -43,7 +48,7 @@ flatpak install flathub io.github.foxxmd.multiscrobbler
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### Using [file-based](/docs/configuration.md#file-based-configuration) configuration
|
||||
#### Using [file-based](../configuration/configuration.md#file-based-configuration) configuration
|
||||
|
||||
The config directory for multi-scrobbler as a flatpak can be found under `/home/YourUser/.var/app/io.github.foxxmd.multiscrobbler/config`
|
||||
|
||||
@@ -51,7 +56,7 @@ The config directory for multi-scrobbler as a flatpak can be found under `/home/
|
||||
flatpak run io.github.multiscrobbler
|
||||
```
|
||||
|
||||
#### Using [env-based](/docs/configuration.md#env-based-configuration) configuration
|
||||
#### Using [env-based](../configuration/configuration.md#env-based-configuration) configuration
|
||||
|
||||
There are a few [options for running flatpak applications with temporary or permanent environmental variables.](https://ardasevinc.dev/launch-flatpak-apps-with-custom-args-and-environment-variables)
|
||||
|
||||
@@ -59,15 +64,21 @@ There are a few [options for running flatpak applications with temporary or perm
|
||||
flatpak run --env=SPOTIFY_CLIENT_ID=yourId --envSPOTIFY_CLIENT_SECRET=yourSecret --env=MALOJA_URL="http://domain.tld" io.github.foxxmd.multiscrobbler
|
||||
```
|
||||
|
||||
# [Docker](https://hub.docker.com/r/foxxmd/multi-scrobbler)
|
||||
## Docker
|
||||
|
||||
Cross-platform images are built for x86 (Intel/AMD) and ARM (IE Raspberry Pi)
|
||||
Cross-platform images are built for x86 (Intel/AMD) and ARM64 (IE Raspberry Pi)
|
||||
|
||||
[Dockerhub](https://hub.docker.com/r/foxxmd/multi-scrobbler)
|
||||
```
|
||||
foxxmd/multi-scrobbler:latest
|
||||
docker.io/foxxmd/multi-scrobbler:latest
|
||||
```
|
||||
|
||||
Or use the provided [docker-compose.yml](/docker-compose.yml) after modifying it to fit your configuration.
|
||||
[Github Packages](https://github.com/FoxxMD/multi-scrobbler/pkgs/container/multi-scrobbler)
|
||||
```
|
||||
ghcr.io/foxxmd/multi-scrobbler:latest
|
||||
```
|
||||
|
||||
Or use the provided [docker-compose.yml](../../../docker-compose.yml) after modifying it to fit your configuration.
|
||||
|
||||
Recommended configuration steps for docker or docker-compose usage:
|
||||
|
||||
@@ -97,16 +108,16 @@ To get the UID and GID for the current user run these commands from a terminal:
|
||||
|
||||
## Docker Usage Examples
|
||||
|
||||
### Using [env-based](/docs/configuration.md#env-based-configuration) configuration
|
||||
### Using [env-based](../configuration/configuration.md#env-based-configuration) configuration
|
||||
|
||||
```bash
|
||||
docker run -e "SPOTIFY_CLIENT_ID=yourId" -e "SPOTIFY_CLIENT_SECRET=yourSecret" -e "MALOJA_URL=http://domain.tld" -e "MALOJA_API_KEY=1234" -e "PUID=1000" -e "PGID=1000" -p 9078:9078 -v /path/on/host/config:/config foxxmd/multi-scrobbler
|
||||
```
|
||||
|
||||
### Using [file-based](/docs/configuration.md#file-based-configuration) configuration
|
||||
### Using [file-based](../configuration/configuration.md#file-based-configuration) configuration
|
||||
|
||||
```bash
|
||||
docker run -e "PUID=1000" -e "PGID=1000" -p 9078:9078 -v /path/on/host/config:/config foxxmd/multi-scrobbler
|
||||
```
|
||||
|
||||
See the [docker-compose.yml](/docker-compose.yml) file for how to use with docker-compose.
|
||||
See the [docker-compose.yml](../../../docker-compose.yml) file for how to use with docker-compose.
|
||||
@@ -1,4 +1,9 @@
|
||||
If you have multi-scrobbler installed [locally](/docs/installation.md#local) you can enable it to run as a background service when you login.
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: 'As a Service'
|
||||
---
|
||||
|
||||
If you have multi-scrobbler installed [locally](installation.md#local) you can enable it to run as a background service when you login.
|
||||
|
||||
Before running as a service you should run it at least once in the foreground to ensure it can start up correctly!
|
||||
|
||||
@@ -33,11 +38,11 @@ Restart=no
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
The above assumes you [installed multi-scrobbler using flatpak](/docs/installation.md#flatpak)
|
||||
The above assumes you [installed multi-scrobbler using flatpak](installation.md#flatpak)
|
||||
|
||||
### Node.js Installs
|
||||
|
||||
If you are running multi-scrobbler directly with [nodejs from a clone repository directory](/docs/installation.md#nodejs) you should modify the `[Service]`:
|
||||
If you are running multi-scrobbler directly with [nodejs from a clone repository directory](installation.md#nodejs) you should modify the `[Service]`:
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
@@ -0,0 +1,155 @@
|
||||
// @ts-check
|
||||
// Note: type annotations allow type checking and IDEs autocompletion
|
||||
|
||||
const lightCodeTheme = require('prism-react-renderer/themes/github');
|
||||
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
|
||||
|
||||
/** @type {import('@docusaurus/types').Config} */
|
||||
const config = {
|
||||
title: 'My Site',
|
||||
tagline: 'Dinosaurs are cool',
|
||||
favicon: 'img/favicon.ico',
|
||||
|
||||
// Set the production url of your site here
|
||||
url: 'https://foxxmd.github.io',
|
||||
// Set the /<baseUrl>/ pathname under which your site is served
|
||||
// For GitHub pages deployment, it is often '/<projectName>/'
|
||||
baseUrl: '/multi-scrobbler',
|
||||
|
||||
// GitHub pages deployment config.
|
||||
// If you aren't using GitHub pages, you don't need these.
|
||||
organizationName: 'foxxmd', // Usually your GitHub org/user name.
|
||||
projectName: 'multi-scrobbler', // Usually your repo name.
|
||||
|
||||
trailingSlash: false,
|
||||
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
|
||||
// Even if you don't use internalization, you can use this field to set useful
|
||||
// metadata like html lang. For example, if your site is Chinese, you may want
|
||||
// to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en'],
|
||||
},
|
||||
|
||||
presets: [
|
||||
[
|
||||
'classic',
|
||||
/** @type {import('@docusaurus/preset-classic').Options} */
|
||||
({
|
||||
docs: {
|
||||
sidebarPath: require.resolve('./sidebars.js'),
|
||||
// Please change this to your repo.
|
||||
// Remove this to remove the "edit this page" links.
|
||||
editUrl:
|
||||
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
|
||||
},
|
||||
// blog: {
|
||||
// showReadingTime: true,
|
||||
// // Please change this to your repo.
|
||||
// // Remove this to remove the "edit this page" links.
|
||||
// editUrl:
|
||||
// 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
|
||||
// },
|
||||
blog: false,
|
||||
|
||||
theme: {
|
||||
customCss: require.resolve('./src/css/custom.css'),
|
||||
},
|
||||
}),
|
||||
],
|
||||
],
|
||||
themes: ["docusaurus-json-schema-plugin"],
|
||||
plugins: [
|
||||
[
|
||||
require.resolve("@cmfcmf/docusaurus-search-local"),
|
||||
{
|
||||
indexBlog: false,
|
||||
},
|
||||
],
|
||||
],
|
||||
themeConfig:
|
||||
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
|
||||
({
|
||||
// Replace with your project's social card
|
||||
image: 'img/docusaurus-social-card.jpg',
|
||||
navbar: {
|
||||
title: 'Multi-Scrobbler',
|
||||
logo: {
|
||||
alt: 'Logo',
|
||||
src: 'img/icon.svg',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'docSidebar',
|
||||
sidebarId: 'tutorialSidebar',
|
||||
position: 'left',
|
||||
label: 'Docs',
|
||||
},
|
||||
{
|
||||
to: 'playground',
|
||||
position: 'left',
|
||||
label: 'Config Playground',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/foxxmd/multi-scrobbler',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: 'dark',
|
||||
links: [
|
||||
{
|
||||
title: 'Docs',
|
||||
items: [
|
||||
{
|
||||
label: 'Installation',
|
||||
to: '/docs/installation',
|
||||
},
|
||||
{
|
||||
label: 'Configuration',
|
||||
to: '/docs/configuration',
|
||||
},
|
||||
],
|
||||
},
|
||||
/* {
|
||||
title: 'Community',
|
||||
items: [
|
||||
{
|
||||
label: 'Stack Overflow',
|
||||
href: 'https://stackoverflow.com/questions/tagged/docusaurus',
|
||||
},
|
||||
{
|
||||
label: 'Discord',
|
||||
href: 'https://discordapp.com/invite/docusaurus',
|
||||
},
|
||||
{
|
||||
label: 'Twitter',
|
||||
href: 'https://twitter.com/docusaurus',
|
||||
},
|
||||
],
|
||||
},*/
|
||||
{
|
||||
title: 'More',
|
||||
items: [
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/foxxmd/multi-scrobbler',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
copyright: `Copyright © ${new Date().getFullYear()} Multi-Scrobbler. Built with Docusaurus.`,
|
||||
},
|
||||
prism: {
|
||||
theme: lightCodeTheme,
|
||||
darkTheme: darkCodeTheme,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "my-website",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cmfcmf/docusaurus-search-local": "^1.1.0",
|
||||
"@docusaurus/core": "2.4.1",
|
||||
"@docusaurus/preset-classic": "2.4.1",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"clsx": "^1.2.1",
|
||||
"docusaurus-json-schema-plugin": "^1.6.1",
|
||||
"prism-react-renderer": "^1.3.5",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "2.4.1",
|
||||
"@tsconfig/docusaurus": "^1.0.5",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.5%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.14"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Creating a sidebar enables you to:
|
||||
- create an ordered group of docs
|
||||
- render a sidebar for each doc of that group
|
||||
- provide next/previous navigation
|
||||
|
||||
The sidebars can be generated from the filesystem, or explicitly defined here.
|
||||
|
||||
Create as many sidebars as you want.
|
||||
*/
|
||||
|
||||
// @ts-check
|
||||
|
||||
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
|
||||
const sidebars = {
|
||||
// By default, Docusaurus generates a sidebar from the docs folder structure
|
||||
tutorialSidebar: [
|
||||
{type: 'autogenerated', dirName: '.'},
|
||||
{
|
||||
type: "link",
|
||||
label: "Config Playground",
|
||||
href: "/playground"
|
||||
}
|
||||
],
|
||||
|
||||
// But you can create a sidebar manually
|
||||
/*
|
||||
tutorialSidebar: [
|
||||
'intro',
|
||||
'hello',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Tutorial',
|
||||
items: ['tutorial-basics/create-a-document'],
|
||||
},
|
||||
],
|
||||
*/
|
||||
};
|
||||
|
||||
module.exports = sidebars;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from "react"
|
||||
import BrowserOnly from "@docusaurus/BrowserOnly"
|
||||
import { useColorMode } from "@docusaurus/theme-common"
|
||||
|
||||
import JSONSchemaEditor from "@theme/JSONSchemaEditor"
|
||||
// Context
|
||||
import { usePlaygroundContext } from "@site/src/contexts/PlaygroundContext"
|
||||
|
||||
// Common stringify of the JSON
|
||||
const STRINGIFY_JSON = (json: unknown) => JSON.stringify(json, null, "\t")
|
||||
|
||||
function JSONSchemaDataInner(): JSX.Element {
|
||||
const {
|
||||
state: { userSchema, editorRef, data: value },
|
||||
updateState,
|
||||
} = usePlaygroundContext()
|
||||
|
||||
const { colorMode } = useColorMode()
|
||||
|
||||
return (
|
||||
<div style={{ boxSizing: "border-box", width: "50%" }}>
|
||||
<JSONSchemaEditor
|
||||
value={value}
|
||||
schema={userSchema}
|
||||
theme={colorMode === "dark" ? "vs-dark" : "vs"}
|
||||
editorDidMount={(editor) => {
|
||||
updateState({ editorRef: editor })
|
||||
}}
|
||||
height={"70vh"}
|
||||
key={STRINGIFY_JSON(userSchema)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function JSONSchemaDataComponent(): JSX.Element {
|
||||
return (
|
||||
<BrowserOnly fallback={<div>Loading...</div>}>
|
||||
{() => {
|
||||
return <JSONSchemaDataInner />
|
||||
}}
|
||||
</BrowserOnly>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from "react"
|
||||
import BrowserOnly from "@docusaurus/BrowserOnly"
|
||||
|
||||
import Schema from "@site/static/aio.json";
|
||||
import ConfigExample from "@site/static/configExample.json";
|
||||
import JSONSchemaViewer from "@theme/JSONSchemaViewer"
|
||||
import JSONSchemaEditor from "@theme/JSONSchemaEditor";
|
||||
import {useColorMode} from "@docusaurus/theme-common";
|
||||
const STRINGIFY_JSON = (json: unknown) => JSON.stringify(json, null, "\t")
|
||||
|
||||
// based on https://github.com/jy95/docusaurus-json-schema-plugin/blob/main/testsite/src/components/PlaygroundInner.tsx
|
||||
function PlaygroundInner(): JSX.Element {
|
||||
|
||||
const { colorMode } = useColorMode()
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflowY: "hidden",
|
||||
overflowX: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div style={{width: '50%'}}>
|
||||
<JSONSchemaViewer
|
||||
schema={Schema}
|
||||
showExamples={true}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ boxSizing: "border-box", width: "50%" }}>
|
||||
<JSONSchemaEditor
|
||||
value={STRINGIFY_JSON(ConfigExample)}
|
||||
theme={colorMode === "dark" ? "vs-dark" : "vs"}
|
||||
schema={Schema}
|
||||
height={"70vh"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function PlaygroundComponent(): JSX.Element {
|
||||
return (
|
||||
<BrowserOnly fallback={<div>Loading...</div>}>
|
||||
{() => {
|
||||
return <PlaygroundInner />
|
||||
}}
|
||||
</BrowserOnly>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useContext, createContext } from "react"
|
||||
|
||||
// Type I need for useRef
|
||||
import type { MonacoEditorTypes } from "@theme/MonacoEditor"
|
||||
|
||||
export type State = {
|
||||
// The full schema
|
||||
// We might need to scope it with a JSON Pointer
|
||||
fullSchema: unknown
|
||||
// The current schema displayed (after the json pointer)
|
||||
userSchema: unknown
|
||||
// The current json pointer
|
||||
jsonPointer: string
|
||||
// Schema editor ref
|
||||
schemaRef: undefined | MonacoEditorTypes.IStandaloneCodeEditor
|
||||
// Data editor ref
|
||||
editorRef: undefined | MonacoEditorTypes.IStandaloneCodeEditor
|
||||
// Data on the data editor
|
||||
// When using "Infer schema from data", it would be annoying to lose our written
|
||||
data: string
|
||||
// Errors in schema
|
||||
schemaErrors: MonacoEditorTypes.IMarkerData[]
|
||||
// Errors in data
|
||||
dataErrors: MonacoEditorTypes.IMarkerData[]
|
||||
}
|
||||
|
||||
export type Playground = {
|
||||
// state
|
||||
state: State
|
||||
// update function
|
||||
updateState: (_: Partial<State>) => void
|
||||
}
|
||||
|
||||
export const PlaygroundContext = createContext<Playground>({
|
||||
state: {
|
||||
fullSchema: {},
|
||||
userSchema: {},
|
||||
jsonPointer: "",
|
||||
data: "{}",
|
||||
schemaRef: undefined,
|
||||
editorRef: undefined,
|
||||
schemaErrors: [],
|
||||
dataErrors: [],
|
||||
},
|
||||
updateState: () => {},
|
||||
})
|
||||
|
||||
export const usePlaygroundContext = () => useContext(PlaygroundContext)
|
||||
|
||||
export const PlaygroundContextProvider = PlaygroundContext.Provider
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Any CSS included here will be global. The classic template
|
||||
* bundles Infima by default. Infima is a CSS framework designed to
|
||||
* work well for content-centric websites.
|
||||
*/
|
||||
|
||||
/* You can override the default Infima variables here. */
|
||||
:root {
|
||||
--ifm-color-primary: #2e8555;
|
||||
--ifm-color-primary-dark: #29784c;
|
||||
--ifm-color-primary-darker: #277148;
|
||||
--ifm-color-primary-darkest: #205d3b;
|
||||
--ifm-color-primary-light: #33925d;
|
||||
--ifm-color-primary-lighter: #359962;
|
||||
--ifm-color-primary-lightest: #3cad6e;
|
||||
--ifm-code-font-size: 95%;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* For readability concerns, you should choose a lighter palette in dark mode. */
|
||||
[data-theme='dark'] {
|
||||
--ifm-color-primary: #25c2a0;
|
||||
--ifm-color-primary-dark: #21af90;
|
||||
--ifm-color-primary-darker: #1fa588;
|
||||
--ifm-color-primary-darkest: #1a8870;
|
||||
--ifm-color-primary-light: #29d5b0;
|
||||
--ifm-color-primary-lighter: #32d8b4;
|
||||
--ifm-color-primary-lightest: #4fddbf;
|
||||
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
title: Multi-Scrobbler
|
||||
---
|
||||
|
||||
# multi-scrobbler
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
<img src={require('/img/icon.png').default} align="right" alt="multi-scrobbler logo" width="180" height="180"/>
|
||||
|
||||
A javascript app to scrobble music you listened to, to [Maloja](https://github.com/krateng/maloja), [Last.fm](https://www.last.fm), and [ListenBrainz](https://listenbrainz.org)
|
||||
|
||||
* Supports scrobbling from many **Sources**
|
||||
* [Spotify](docs/configuration#spotify)
|
||||
* [Plex](docs/configuration#plex) or [Tautulli](docs/configuration#tautulli)
|
||||
* [Subsonic-compatible APIs](docs/configuration#subsonic) (like [Airsonic](https://airsonic.github.io/))
|
||||
* [Jellyfin](docs/configuration#jellyfin)
|
||||
* [Youtube Music](docs/configuration#youtube-music)
|
||||
* [Last.fm](docs/configuration#lastfm-source)
|
||||
* [ListenBrainz](docs/configuration#listenbrainz--source-)
|
||||
* [Deezer](docs/configuration#deezer)
|
||||
* [MPRIS (Linux Desktop)](docs/configuration#mpris)
|
||||
* [Mopidy](docs/configuration#mopidy)
|
||||
* [JRiver](docs/configuration#jriver)
|
||||
* [Kodi](docs/configuration#kodi)
|
||||
* Supports scrobbling to many **Clients**
|
||||
* [Maloja](docs/configuration#maloja)
|
||||
* [Last.fm](docs/configuration#lastfm)
|
||||
* [ListenBrainz](docs/configuration#listenbrainz)
|
||||
* Monitor status of Sources and Clients using [webhooks (Gotify or Ntfy)](docs/configuration#webhook-configurations) or [healthcheck endpoint](docs/configuration#health-endpoint)
|
||||
* Supports configuring for single or multiple users (scrobbling for your friends and family!)
|
||||
* Web server interface for stats, basic control, and detailed logs
|
||||
* Smart handling of credentials (persistent, authorization through app)
|
||||
* Easy configuration through ENVs or JSON
|
||||
* Install using [Docker images for x86/ARM](docs/installation#docker), [flatpak](docs/installation#flatpak), or [locally with NodeJS](docs/installation#nodejs)
|
||||
|
||||
**Why should I use this over a browser extension and/or mobile app scrobbler?**
|
||||
|
||||
* **Platform independent** -- Because multi-scrobbler communicates directly with service APIs it will scrobble everything you play regardless of where you play it. No more need for apps on every platform you use!
|
||||
* **Open-source** -- Get peace of mind knowing exactly how your personal data is being handled.
|
||||
* **Track your activity regardless of where you listen** -- Scrobble from many Sources to one Client with ease and without duplicating tracks.
|
||||
* **Manage scrobbling for others** -- Scrobble for your friends and family without any setup on their part. Easily silo sources to specific clients to keep plays separate.
|
||||
|
||||
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
|
||||
|
||||
Yes! You can use [Last.fm as a **Source**](docs/configuration#lastfm--source-) or [Listenbrainz as a **Source**](docs/configuration#listenbrainz--source-) to forward scrobbles from your profile to any other Client! That way you can keep your current scrobble setup as-is but still get the benefit of capturing your data to a self-hosted location.
|
||||
|
||||
<img src={require('/img/status-ui.jpg').default} width="800"/>
|
||||
|
||||
## How Does multi-scrobbler (MS) Work?
|
||||
|
||||
You set up configurations for one or more **Sources** and one or more **Clients**. MS monitors all of your configured **Sources**. When new tracks are played by a Source it grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to scrobble to.
|
||||
|
||||
### Source
|
||||
|
||||
A **Source** is a data source that contains information about tracks you are playing like a music player or platform. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Airsonic**, etc...
|
||||
|
||||
Source configurations consist of:
|
||||
|
||||
* A friendly name.
|
||||
* Any data needed to communicate or authenticate with the Source.
|
||||
* An optional list of Client names that the Source should scrobble to. If omitted the Source also scrobbles to all configured Clients.
|
||||
|
||||
### Client
|
||||
|
||||
A **Client** is an application that stores the historical information about what songs you have played (scrobbles). Examples are **Maloja, Last.fm, Listenbrainz**...
|
||||
|
||||
Client configurations consist of:
|
||||
|
||||
* A friendly name.
|
||||
* Any data needed to communicate or authenticate with the Client.
|
||||
|
||||
## Installation
|
||||
|
||||
[See the **Installation** documentation](docs/installation)
|
||||
|
||||
## Configuration
|
||||
|
||||
[See the **Configuration** documentation](docs/configuration)
|
||||
|
||||
## Usage
|
||||
|
||||
A status page with statistics, recent logs, and some runtime configuration options can be found at
|
||||
|
||||
```
|
||||
http://localhost:9078
|
||||
```
|
||||
Output is also provided to stdout/stderr as well as file if specified in configuration.
|
||||
|
||||
On first startup you may need to authorize Spotify and/or Last.fm by visiting the callback URL (which can also be accessed from the status page). Visit the status page above to find the applicable link to trigger this.
|
||||
|
||||
## Help/FAQ
|
||||
|
||||
Having issues with connections or configuration? Check the [FAQ](docs/FAQ) before creating an issue!
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from "react"
|
||||
import Layout from "@theme/Layout"
|
||||
import BrowserOnly from "@docusaurus/BrowserOnly"
|
||||
import ErrorBoundary from "@docusaurus/ErrorBoundary"
|
||||
// based on https://github.com/jy95/docusaurus-json-schema-plugin/blob/main/testsite/src/pages/playground.tsx
|
||||
function PlaygroundComponent(): JSX.Element {
|
||||
// No SSR for the live preview
|
||||
// See https://github.com/facebook/docusaurus/issues/5747
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={({ error, tryAgain }) => (
|
||||
<div>
|
||||
<p>Playground component crashed because of error: {error.message}.</p>
|
||||
<button onClick={tryAgain}>Try Again!</button>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<BrowserOnly fallback={<div>Loading...</div>}>
|
||||
{() => {
|
||||
const PlaygroundInnerComponent =
|
||||
require("@site/src/components/PlaygroundInner").default
|
||||
return <PlaygroundInnerComponent />
|
||||
}}
|
||||
</BrowserOnly>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Playground(): JSX.Element {
|
||||
return (
|
||||
<Layout
|
||||
title={`Playground`}
|
||||
description="Playground of docusaurus-json-schema-plugin"
|
||||
>
|
||||
<PlaygroundComponent />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"sourceDefaults": {
|
||||
"maxPollRetries": 0,
|
||||
"maxRequestRetries": 1,
|
||||
"retryMultiplier": 1.5
|
||||
},
|
||||
"clientDefaults": {
|
||||
"maxRequestRetries": 1,
|
||||
"retryMultiplier": 1.5
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"type": "spotify",
|
||||
"clients": ["myConfig"],
|
||||
"name": "mySpotifySource",
|
||||
"data": {
|
||||
"clientId": "a89cba1569901a0671d5a9875fed4be1",
|
||||
"clientSecret": "ec42e09d5ae0ee0f0816ca151008412a",
|
||||
"redirectUri": "http://localhost:9078/callback"
|
||||
}
|
||||
}
|
||||
],
|
||||
"clients": [
|
||||
{
|
||||
"type": "maloja",
|
||||
"name": "myConfig",
|
||||
"data": {
|
||||
"url": "http://localhost:42010",
|
||||
"apiKey": "myMalojaKey"
|
||||
}
|
||||
}
|
||||
],
|
||||
"webhooks": [
|
||||
{
|
||||
"name": "FirstGotifyServer",
|
||||
"type": "gotify",
|
||||
"url": "http://localhost:8070",
|
||||
"token": "MyGotifyToken",
|
||||
"priorities": {
|
||||
"info": 5,
|
||||
"warn": 7,
|
||||
"error": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "ntfy",
|
||||
"name": "MyNtfyFriendlyNameForLogs",
|
||||
"url": "http://localhost:9991",
|
||||
"topic": "MyMultiScrobblerTopic",
|
||||
"username": "Optional",
|
||||
"password": "Optional",
|
||||
"priorities": {
|
||||
"info": 3,
|
||||
"warn": 4,
|
||||
"error": 5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #231f20;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-1" d="M76.94,65.49c.87-7.85-5.27-14.51-12.94-14.51-4.56,0-8.57,2.36-10.9,5.91h-4.55c2.36-5.11,7.17-8.87,12.95-9.71V20c0-11.05-8.95-20-20-20H5C2.24,0,0,2.24,0,5V123c0,2.76,2.24,5,5,5H41.5c11.05,0,20-8.95,20-20v-27.17c-5.78-.84-10.59-4.6-12.95-9.72h4.55c2.59,3.96,7.27,6.43,12.46,5.82,6.08-.71,10.71-5.37,11.39-11.45ZM15.3,94.39c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v60.78Zm11.12,14.55c0,1.1-.9,2-2,2s-2-.9-2-2V19.06c0-1.1,.9-2,2-2s2,.9,2,2V108.94Zm37.58-39.53c-2.28,0-4.23-1.41-5.02-3.41h-21.43v28.39c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v28.39h21.43c.93-2.33,3.42-3.86,6.18-3.29,2.05,.42,3.7,2.08,4.13,4.12,.73,3.5-1.92,6.58-5.29,6.58Z"/>
|
||||
<path class="cls-1" d="M123,0h-36.5c-11.05,0-20,8.95-20,20v27.18c7.55,1.12,13.5,7.2,14.4,14.82h9.55v-28.28c0-1,.68-1.92,1.66-2.08,1.25-.2,2.34,.76,2.34,1.97v60.67c0,1-.68,1.92-1.66,2.08-1.25,.2-2.34-.76-2.34-1.97v-28.39h-9.55c-.9,7.62-6.84,13.7-14.4,14.82v27.18c0,11.05,8.95,20,20,20h36.5c2.76,0,5-2.24,5-5V5c0-2.76-2.24-5-5-5Zm-17.42,108.94c0,1.1-.9,2-2,2s-2-.9-2-2V19.06c0-1.1,.9-2,2-2s2,.9,2,2V108.94Zm11.12-14.55c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v60.78Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 191 KiB |
@@ -0,0 +1,10 @@
|
||||
{
|
||||
// This file is not used in compilation. It is here just for a nice editor experience.
|
||||
"extends": "@tsconfig/docusaurus/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"resolveJsonModule": true,
|
||||
// Extending "@tsconfig/docusaurus/tsconfig.json".types with "docusaurus-json-schema-plugin"
|
||||
"types": ["node", "@docusaurus/module-type-aliases", "@docusaurus/theme-classic", "docusaurus-json-schema-plugin"]
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
NOTE: This steps are for building the flatpak entirely locally, from source. If you want to install the application normally then [get it through flathub](/docs/installation.md#flatpak)
|
||||
|
||||
Flatpak build is a little convoluted until someone sets me straight...
|
||||
The final build repo for the flathub version can be found at [flathub/io.github.foxxmd.multiscrobbler](https://github.com/flathub/io.github.foxxmd.multiscrobbler)
|
||||
|
||||
# 1. Install Requirements
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #231f20;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-1" d="M76.94,65.49c.87-7.85-5.27-14.51-12.94-14.51-4.56,0-8.57,2.36-10.9,5.91h-4.55c2.36-5.11,7.17-8.87,12.95-9.71V20c0-11.05-8.95-20-20-20H5C2.24,0,0,2.24,0,5V123c0,2.76,2.24,5,5,5H41.5c11.05,0,20-8.95,20-20v-27.17c-5.78-.84-10.59-4.6-12.95-9.72h4.55c2.59,3.96,7.27,6.43,12.46,5.82,6.08-.71,10.71-5.37,11.39-11.45ZM15.3,94.39c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v60.78Zm11.12,14.55c0,1.1-.9,2-2,2s-2-.9-2-2V19.06c0-1.1,.9-2,2-2s2,.9,2,2V108.94Zm37.58-39.53c-2.28,0-4.23-1.41-5.02-3.41h-21.43v28.39c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v28.39h21.43c.93-2.33,3.42-3.86,6.18-3.29,2.05,.42,3.7,2.08,4.13,4.12,.73,3.5-1.92,6.58-5.29,6.58Z"/>
|
||||
<path class="cls-1" d="M123,0h-36.5c-11.05,0-20,8.95-20,20v27.18c7.55,1.12,13.5,7.2,14.4,14.82h9.55v-28.28c0-1,.68-1.92,1.66-2.08,1.25-.2,2.34,.76,2.34,1.97v60.67c0,1-.68,1.92-1.66,2.08-1.25,.2-2.34-.76-2.34-1.97v-28.39h-9.55c-.9,7.62-6.84,13.7-14.4,14.82v27.18c0,11.05,8.95,20,20,20h36.5c2.76,0,5-2.24,5-5V5c0-2.76-2.24-5-5-5Zm-17.42,108.94c0,1.1-.9,2-2,2s-2-.9-2-2V19.06c0-1.1,.9-2,2-2s2,.9,2,2V108.94Zm11.12-14.55c0,1.1-.9,2-2,2s-2-.9-2-2V33.61c0-1.1,.9-2,2-2s2,.9,2,2v60.78Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -41,6 +41,7 @@
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release version="0.4.6" date="2023-06-07"/>
|
||||
<release version="0.4.4" date="2023-03-20"/>
|
||||
</releases>
|
||||
<categories>
|
||||
|
||||
@@ -48,6 +48,7 @@ modules:
|
||||
|
||||
- install -Dm644 flatpak/${FLATPAK_ID}.desktop /app/share/applications/${FLATPAK_ID}.desktop
|
||||
- install -Dm644 flatpak/icon.png /app/share/icons/hicolor/512x512/apps/${FLATPAK_ID}.png
|
||||
- install -Dm644 assets/icon.svg /app/share/icons/hicolor/scalable/apps/${FLATPAK_ID}.svg
|
||||
- install -Dm644 flatpak/${FLATPAK_ID}.metainfo.xml /app/share/metainfo/${FLATPAK_ID}.metainfo.xml
|
||||
|
||||
- install -Dm755 ../run.sh /app/bin/multiscrobbler
|
||||
@@ -55,7 +56,8 @@ modules:
|
||||
sources:
|
||||
- type: git
|
||||
url: https://github.com/FoxxMD/multi-scrobbler
|
||||
commit: 9a578e54a89f87c82ff811bb04257c9b641b199a
|
||||
tag: 0.4.6
|
||||
commit: b90ea039245491767baeefc53966b0350016aa4d
|
||||
dest: main
|
||||
# Wrapper to launch the app
|
||||
- type: script
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "multi-scrobbler",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "multi-scrobbler",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@awaitjs/express": "^0.6.3",
|
||||
@@ -63,6 +63,7 @@
|
||||
"ts-essentials": "^9.1.2",
|
||||
"ts-node": "^10.7.0",
|
||||
"tsconfig-paths": "^3.13.0",
|
||||
"typedoc": "^0.23.28",
|
||||
"typescript": "^4.9.5",
|
||||
"typescript-json-schema": "~0.55"
|
||||
},
|
||||
@@ -510,6 +511,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-sequence-parser": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.0.tgz",
|
||||
"integrity": "sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
@@ -2188,6 +2195,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonc-parser": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
|
||||
"integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jsonparse": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
|
||||
@@ -2326,6 +2339,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/lunr": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
|
||||
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
@@ -2337,6 +2356,18 @@
|
||||
"resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
|
||||
"integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g=="
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
|
||||
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -3158,6 +3189,18 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/shiki": {
|
||||
"version": "0.14.3",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.3.tgz",
|
||||
"integrity": "sha512-U3S/a+b0KS+UkTyMjoNojvTgrBHjgp7L6ovhFVZsXmBGnVdQ4K4U9oK0z63w538S91ATngv1vXigHCSWOwnr+g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-sequence-parser": "^1.1.0",
|
||||
"jsonc-parser": "^3.2.0",
|
||||
"vscode-oniguruma": "^1.7.0",
|
||||
"vscode-textmate": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
@@ -3640,6 +3683,51 @@
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
|
||||
},
|
||||
"node_modules/typedoc": {
|
||||
"version": "0.23.28",
|
||||
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.28.tgz",
|
||||
"integrity": "sha512-9x1+hZWTHEQcGoP7qFmlo4unUoVJLB0H/8vfO/7wqTnZxg4kPuji9y3uRzEu0ZKez63OJAUmiGhUrtukC6Uj3w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lunr": "^2.3.9",
|
||||
"marked": "^4.2.12",
|
||||
"minimatch": "^7.1.3",
|
||||
"shiki": "^0.14.1"
|
||||
},
|
||||
"bin": {
|
||||
"typedoc": "bin/typedoc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/typedoc/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typedoc/node_modules/minimatch": {
|
||||
"version": "7.4.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz",
|
||||
"integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
@@ -3801,6 +3889,18 @@
|
||||
"extsprintf": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-oniguruma": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz",
|
||||
"integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vscode-textmate": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz",
|
||||
"integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "multi-scrobbler",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.8",
|
||||
"description": "scrobble plays from multiple sources to multiple clients",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
@@ -8,11 +8,12 @@
|
||||
"build": "tsc",
|
||||
"start": "node src/index.js",
|
||||
"schema": "npm run -s schema-aio & npm run -s schema-source & npm run -s schema-client & npm run -s schema-aiosource & npm run -s schema-aioclient",
|
||||
"schema-client": "typescript-json-schema tsconfig.json ClientConfig --out src/common/schema/client.json --required --tsNodeRegister --refs --validationKeywords deprecationMessage",
|
||||
"schema-source": "typescript-json-schema tsconfig.json SourceConfig --out src/common/schema/source.json --required --tsNodeRegister --refs --validationKeywords deprecationMessage",
|
||||
"schema-aio": "typescript-json-schema tsconfig.json AIOConfig --out src/common/schema/aio.json --required --tsNodeRegister --refs --validationKeywords deprecationMessage",
|
||||
"schema-aiosource": "typescript-json-schema tsconfig.json AIOSourceConfig --out src/common/schema/aio-source.json --required --tsNodeRegister --refs --validationKeywords deprecationMessage",
|
||||
"schema-aioclient": "typescript-json-schema tsconfig.json AIOClientConfig --out src/common/schema/aio-client.json --required --tsNodeRegister --refs --validationKeywords deprecationMessage"
|
||||
"schema-client": "typescript-json-schema tsconfig.json ClientConfig --out src/common/schema/client.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage",
|
||||
"schema-source": "typescript-json-schema tsconfig.json SourceConfig --out src/common/schema/source.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage",
|
||||
"schema-aio": "typescript-json-schema tsconfig.json AIOConfig --out src/common/schema/aio.json --required --titles --tsNodeRegister --refs --validationKeywords deprecationMessage",
|
||||
"schema-aiosource": "typescript-json-schema tsconfig.json AIOSourceConfig --out src/common/schema/aio-source.json --titles --required --tsNodeRegister --refs --validationKeywords deprecationMessage",
|
||||
"schema-aioclient": "typescript-json-schema tsconfig.json AIOClientConfig --out src/common/schema/aio-client.json --titles --required --tsNodeRegister --refs --validationKeywords deprecationMessage",
|
||||
"typedoc": "typedoc"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -90,7 +91,8 @@
|
||||
"ts-node": "^10.7.0",
|
||||
"tsconfig-paths": "^3.13.0",
|
||||
"typescript": "^4.9.5",
|
||||
"typescript-json-schema": "~0.55"
|
||||
"typescript-json-schema": "~0.55",
|
||||
"typedoc": "^0.23.28"
|
||||
},
|
||||
"overrides": {
|
||||
"spotify-web-api-node": {
|
||||
|
||||
@@ -53,7 +53,7 @@ export class KodiApiClient extends AbstractApiClient {
|
||||
const {
|
||||
url = 'http://localhost:8080/jsonrpc'
|
||||
} = config;
|
||||
this.url = KodiApiClient.parseConnectionUrl('http://localhost:8080');
|
||||
this.url = KodiApiClient.parseConnectionUrl(url);
|
||||
const auth = new Buffer(`${config.username}:${config.password}`).toString('base64');
|
||||
this.client = new KodiClient({
|
||||
clientType: this.url.protocol.replace(':', '') as ('http' | 'https'),
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
export interface MalojaV2ScrobbleData {
|
||||
artists: string[]
|
||||
title: string
|
||||
album: string
|
||||
/**
|
||||
* Length of the track
|
||||
* */
|
||||
duration: number
|
||||
/**
|
||||
* unix timestamp (seconds) scrobble was made at
|
||||
* */
|
||||
time: number
|
||||
}
|
||||
|
||||
export interface MalojaAlbumData {
|
||||
name?: string
|
||||
albumtitle?: string
|
||||
artists: string[]
|
||||
}
|
||||
|
||||
export interface MalojaV3ScrobbleData {
|
||||
/**
|
||||
* unix timestamp (seconds) scrobble was made at
|
||||
* */
|
||||
time: number
|
||||
track: {
|
||||
artists: string[]
|
||||
title: string
|
||||
album?: MalojaAlbumData | null
|
||||
/**
|
||||
* length of the track
|
||||
* */
|
||||
length: number
|
||||
}
|
||||
/**
|
||||
* how long the track was listened to before it was scrobbled
|
||||
* */
|
||||
duration: number
|
||||
}
|
||||
|
||||
export type MalojaScrobbleData = MalojaV2ScrobbleData | MalojaV3ScrobbleData;
|
||||
|
||||
export interface MalojaScrobbleRequestData {
|
||||
key: string
|
||||
title: string
|
||||
album: string
|
||||
time: number
|
||||
length: number
|
||||
}
|
||||
|
||||
export interface MalojaScrobbleV2RequestData extends MalojaScrobbleRequestData {
|
||||
artist: string
|
||||
}
|
||||
|
||||
export interface MalojaScrobbleV3RequestData extends MalojaScrobbleRequestData {
|
||||
artists: string[]
|
||||
}
|
||||
|
||||
interface MalojaScrobbleWarning {
|
||||
type: string
|
||||
value: string[] | string
|
||||
desc: string
|
||||
}
|
||||
|
||||
export interface MalojaResponseV3CommonData {
|
||||
status: 'failure' | 'error' | 'success' | 'ok'
|
||||
error?: {
|
||||
type: string
|
||||
value?: string | object
|
||||
desc: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface MalojaScrobbleV3ResponseData extends MalojaResponseV3CommonData {
|
||||
track?: {
|
||||
title: string
|
||||
artists: string[]
|
||||
album?: MalojaAlbumData | null
|
||||
}
|
||||
desc: string
|
||||
warnings?: MalojaScrobbleWarning[]
|
||||
}
|
||||
@@ -138,8 +138,17 @@ export default class LastfmScrobbler extends AbstractScrobbleClient {
|
||||
|
||||
const sType = newFromSource ? 'New' : 'Backlog';
|
||||
|
||||
// LFM does not support multiple artists in scrobble payload
|
||||
// https://www.last.fm/api/show/track.scrobble
|
||||
let artist: string;
|
||||
if (artists.length === 0) {
|
||||
artist = "";
|
||||
} else {
|
||||
artist = artists[0];
|
||||
}
|
||||
|
||||
const rawPayload = {
|
||||
artist: artists.join(', '),
|
||||
artist: artist,
|
||||
duration,
|
||||
track,
|
||||
album,
|
||||
|
||||
@@ -14,18 +14,18 @@ import {
|
||||
import {
|
||||
FormatPlayObjectOptions,
|
||||
INITIALIZING,
|
||||
MalojaScrobbleData,
|
||||
MalojaScrobbleRequestData,
|
||||
MalojaScrobbleV2RequestData,
|
||||
MalojaScrobbleV3RequestData,
|
||||
MalojaV2ScrobbleData,
|
||||
MalojaV3ScrobbleData,
|
||||
PlayObject,
|
||||
TrackStringOptions
|
||||
} from "../common/infrastructure/Atomic.js";
|
||||
import {MalojaClientConfig} from "../common/infrastructure/config/client/maloja.js";
|
||||
import {Notifiers} from "../notifier/Notifiers.js";
|
||||
import {Logger} from '@foxxmd/winston';
|
||||
import {
|
||||
MalojaScrobbleData,
|
||||
MalojaScrobbleRequestData,
|
||||
MalojaScrobbleV2RequestData,
|
||||
MalojaScrobbleV3RequestData, MalojaScrobbleV3ResponseData, MalojaV2ScrobbleData, MalojaV3ScrobbleData
|
||||
} from "../apis/maloja/interfaces";
|
||||
|
||||
const feat = ["ft.", "ft", "feat.", "feat", "featuring", "Ft.", "Ft", "Feat.", "Feat", "Featuring"];
|
||||
|
||||
@@ -65,10 +65,7 @@ export default class MalojaScrobbler extends AbstractScrobbleClient {
|
||||
track: {
|
||||
artists: mArtists,
|
||||
title: mTitle,
|
||||
album: {
|
||||
name: mAlbum,
|
||||
artists: albumArtists
|
||||
} = {},
|
||||
album: mAlbum,
|
||||
// length of the track
|
||||
length: mLength,
|
||||
} = {},
|
||||
@@ -79,7 +76,14 @@ export default class MalojaScrobbler extends AbstractScrobbleClient {
|
||||
time = mTime;
|
||||
title = mTitle;
|
||||
duration = mLength;
|
||||
album = mAlbum;
|
||||
if(mAlbum !== null) {
|
||||
const {
|
||||
albumtitle,
|
||||
name: mAlbumName,
|
||||
artists: albumArtists
|
||||
} = mAlbum || {};
|
||||
album = albumtitle ?? mAlbumName;
|
||||
}
|
||||
} else {
|
||||
// scrobble data structure for v2 and below
|
||||
const {
|
||||
@@ -384,6 +388,8 @@ export default class MalojaScrobbler extends AbstractScrobbleClient {
|
||||
length: duration,
|
||||
};
|
||||
|
||||
let responseBody: MalojaScrobbleV3ResponseData;
|
||||
|
||||
try {
|
||||
// 3.0.3 has a BC for something (maybe seconds => length ?) -- see #42 in repo
|
||||
if(this.serverVersion === undefined || compareVersions(this.serverVersion, '3.0.2') > 0) {
|
||||
@@ -398,53 +404,77 @@ export default class MalojaScrobbler extends AbstractScrobbleClient {
|
||||
.type('json')
|
||||
.send(scrobbleData));
|
||||
|
||||
let scrobbleResponse = {};
|
||||
let scrobbleResponse: any | undefined = undefined,
|
||||
scrobbledPlay: PlayObject;
|
||||
|
||||
if(this.serverVersion === undefined || compareVersions(this.serverVersion, '3.0.0') >= 0) {
|
||||
responseBody = response.body;
|
||||
const {
|
||||
track,
|
||||
status,
|
||||
warnings = [],
|
||||
} = responseBody;
|
||||
if(status === 'success') {
|
||||
if(track !== undefined) {
|
||||
scrobbleResponse = {
|
||||
time: playDate.unix(),
|
||||
track: {
|
||||
...track,
|
||||
length: duration
|
||||
},
|
||||
}
|
||||
if (album !== undefined) {
|
||||
const {
|
||||
album: malojaAlbum = {},
|
||||
} = track;
|
||||
scrobbleResponse.track.album = {
|
||||
...malojaAlbum,
|
||||
name: album
|
||||
}
|
||||
}
|
||||
}
|
||||
if(warnings.length > 0) {
|
||||
for(const w of warnings) {
|
||||
this.logger.warn(`Maloja Warning: ${w.desc} => ${JSON.stringify(w.value)}`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error(buildErrorString(response));
|
||||
}
|
||||
} else {
|
||||
const {
|
||||
body: {
|
||||
// @ts-expect-error TS(2525): Initializer provides no value for this binding ele... Remove this comment to see the full error message
|
||||
track,
|
||||
} = {}
|
||||
track: {
|
||||
time: mTime = playDate.unix(),
|
||||
duration: mDuration = duration,
|
||||
album: mAlbum = album,
|
||||
...rest
|
||||
} = {}
|
||||
} = {}
|
||||
} = response;
|
||||
scrobbleResponse = {
|
||||
time: playDate.unix(),
|
||||
track: {
|
||||
...track,
|
||||
length: duration
|
||||
},
|
||||
}
|
||||
if(album !== undefined) {
|
||||
const {
|
||||
album: malojaAlbum = {},
|
||||
} = track;
|
||||
// @ts-expect-error TS(2339): Property 'track' does not exist on type '{}'.
|
||||
scrobbleResponse.track.album = {
|
||||
...malojaAlbum,
|
||||
name: album
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const {body: {
|
||||
// @ts-expect-error TS(2525): Initializer provides no value for this binding ele... Remove this comment to see the full error message
|
||||
track: {
|
||||
time: mTime = playDate.unix(),
|
||||
duration: mDuration = duration,
|
||||
album: mAlbum = album,
|
||||
...rest
|
||||
}
|
||||
} = {}} = response;
|
||||
scrobbleResponse = {...rest, album: mAlbum, time: mTime, duration: mDuration};
|
||||
}
|
||||
this.addScrobbledTrack(playObj, this.formatPlayObj(scrobbleResponse));
|
||||
if (newFromSource) {
|
||||
this.logger.info(`Scrobbled (New) => (${source}) ${buildTrackString(playObj)}`);
|
||||
let warning = '';
|
||||
if(scrobbleResponse === undefined) {
|
||||
warning = `WARNING: Maloja did not return track data in scrobble response! Maybe it didn't scrobble correctly??`;
|
||||
scrobbledPlay = playObj;
|
||||
} else {
|
||||
this.logger.info(`Scrobbled (Backlog) => (${source}) ${buildTrackString(playObj)}`);
|
||||
scrobbledPlay = this.formatPlayObj(scrobbleResponse)
|
||||
}
|
||||
this.addScrobbledTrack(playObj, scrobbledPlay);
|
||||
const scrobbleInfo = `Scrobbled (${newFromSource ? 'New' : 'Backlog'}) => (${source}) ${buildTrackString(playObj)}`;
|
||||
if(warning !== '') {
|
||||
this.logger.warn(`${scrobbleInfo} | ${warning}`);
|
||||
this.logger.debug(`Response: ${this.logger.debug(JSON.stringify(response.body))}`);
|
||||
} else {
|
||||
this.logger.info(scrobbleInfo);
|
||||
}
|
||||
} catch (e) {
|
||||
await this.notifier.notify({title: `Client - ${capitalize(this.type)} - ${this.name} - Scrobble Error`, message: `Failed to scrobble => ${buildTrackString(playObj)} | Error: ${e.message}`, priority: 'error'});
|
||||
this.logger.error(`Scrobble Error (${sType})`, {playInfo: buildTrackString(playObj), payload: scrobbleData});
|
||||
if(responseBody !== undefined) {
|
||||
this.logger.error('Raw Response:', responseBody);
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
this.logger.debug('Raw Payload:', scrobbleData);
|
||||
@@ -453,3 +483,30 @@ export default class MalojaScrobbler extends AbstractScrobbleClient {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const buildErrorString = (body: MalojaScrobbleV3ResponseData) => {
|
||||
let valString: string | undefined = undefined;
|
||||
const {
|
||||
status,
|
||||
error: {
|
||||
type,
|
||||
value,
|
||||
desc
|
||||
} = {}
|
||||
} = body;
|
||||
if(value !== undefined && value !== null) {
|
||||
if(typeof value === 'string') {
|
||||
valString = value;
|
||||
} else if(Array.isArray(value)) {
|
||||
valString = value.map(x => {
|
||||
if(typeof x === 'string') {
|
||||
return x;
|
||||
}
|
||||
return JSON.stringify(x);
|
||||
}).join(', ');
|
||||
} else {
|
||||
valString = JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
return `Maloja API returned ${status} of type ${type} "${desc}"${valString !== undefined ? `: ${valString}` : ''}`;
|
||||
}
|
||||
|
||||
@@ -134,60 +134,6 @@ export interface ScrobbledPlayObject {
|
||||
scrobble: PlayObject
|
||||
}
|
||||
|
||||
export interface MalojaV2ScrobbleData {
|
||||
artists: string[]
|
||||
title: string
|
||||
album: string
|
||||
/**
|
||||
* Length of the track
|
||||
* */
|
||||
duration: number
|
||||
/**
|
||||
* unix timestamp (seconds) scrobble was made at
|
||||
* */
|
||||
time: number
|
||||
}
|
||||
|
||||
export interface MalojaV3ScrobbleData {
|
||||
/**
|
||||
* unix timestamp (seconds) scrobble was made at
|
||||
* */
|
||||
time: number
|
||||
track: {
|
||||
artists: string[]
|
||||
title: string
|
||||
album?: {
|
||||
name: string
|
||||
artists: string[]
|
||||
}
|
||||
/**
|
||||
* length of the track
|
||||
* */
|
||||
length: number
|
||||
}
|
||||
/**
|
||||
* how long the track was listened to before it was scrobbled
|
||||
* */
|
||||
duration: number
|
||||
}
|
||||
|
||||
export type MalojaScrobbleData = MalojaV2ScrobbleData | MalojaV3ScrobbleData;
|
||||
|
||||
export interface MalojaScrobbleRequestData {
|
||||
key: string
|
||||
title: string
|
||||
album: string
|
||||
time: number
|
||||
length: number
|
||||
}
|
||||
|
||||
export interface MalojaScrobbleV2RequestData extends MalojaScrobbleRequestData {
|
||||
artist: string
|
||||
}
|
||||
|
||||
export interface MalojaScrobbleV3RequestData extends MalojaScrobbleRequestData {
|
||||
artists: string[]
|
||||
}
|
||||
|
||||
export interface RemoteIdentityParts {
|
||||
host: string,
|
||||
|
||||
@@ -29,6 +29,20 @@ export interface JellyData extends CommonSourceData {
|
||||
* @examples [false]
|
||||
* */
|
||||
logPayload?: boolean
|
||||
|
||||
/**
|
||||
* How MS should log when a Jellyfin event fails a defined filter (users/servers)
|
||||
*
|
||||
* * `false` => do not log
|
||||
* * `debug` => log to DEBUG level
|
||||
* * `warn` => log to WARN level (default)
|
||||
*
|
||||
* Hint: This is useful if you are sure this source is setup correctly and you have multiple other Jellyfin sources. Set to `debug` or `false` to reduce log noise.
|
||||
*
|
||||
* @default warn
|
||||
* @examples ["warn"]
|
||||
* */
|
||||
logFilterFailure?: false | 'debug' | 'warn'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,26 @@ export interface PlexSourceData extends CommonSourceData {
|
||||
* @examples [["MyServerName"]]
|
||||
* */
|
||||
servers?: string | string[]
|
||||
|
||||
/**
|
||||
* Additional options for Plex/Tautulli logging and tuning
|
||||
* */
|
||||
options?: {
|
||||
|
||||
/**
|
||||
* How MS should log when a Plex/Tautulli event fails a defined filter (users/servers)
|
||||
*
|
||||
* * `false` => do not log
|
||||
* * `debug` => log to DEBUG level
|
||||
* * `warn` => log to WARN level (default)
|
||||
*
|
||||
* Hint: This is useful if you are sure this source is setup correctly and you have multiple other Plex/Tautulli sources. Set to `debug` or `false` to reduce log noise.
|
||||
*
|
||||
* @default warn
|
||||
* @examples ["warn"]
|
||||
* */
|
||||
logFilterFailure?: false | 'debug' | 'warn'
|
||||
}
|
||||
}
|
||||
|
||||
export interface PlexSourceConfig extends CommonSourceConfig {
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
{
|
||||
"$ref": "#/definitions/MalojaClientAIOConfig"
|
||||
}
|
||||
]
|
||||
],
|
||||
"title": "ClientAIOConfig"
|
||||
},
|
||||
"CommonClientData": {
|
||||
"properties": {
|
||||
@@ -22,6 +23,7 @@
|
||||
"examples": [
|
||||
1
|
||||
],
|
||||
"title": "maxRequestRetries",
|
||||
"type": "number"
|
||||
},
|
||||
"options": {
|
||||
@@ -32,6 +34,7 @@
|
||||
"examples": [
|
||||
true
|
||||
],
|
||||
"title": "checkExistingScrobbles",
|
||||
"type": "boolean"
|
||||
},
|
||||
"refreshEnabled": {
|
||||
@@ -40,18 +43,22 @@
|
||||
"examples": [
|
||||
true
|
||||
],
|
||||
"title": "refreshEnabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"verbose": {
|
||||
"description": "Options used for increasing verbosity of logging in MS (used for debugging)",
|
||||
"properties": {
|
||||
"match": {
|
||||
"$ref": "#/definitions/MatchLoggingOptions"
|
||||
"$ref": "#/definitions/MatchLoggingOptions",
|
||||
"title": "match"
|
||||
}
|
||||
},
|
||||
"title": "verbose",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"title": "options",
|
||||
"type": "object"
|
||||
},
|
||||
"retryMultiplier": {
|
||||
@@ -60,9 +67,11 @@
|
||||
"examples": [
|
||||
1.5
|
||||
],
|
||||
"title": "retryMultiplier",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"title": "CommonClientData",
|
||||
"type": "object"
|
||||
},
|
||||
"LastfmClientAIOConfig": {
|
||||
@@ -77,6 +86,7 @@
|
||||
"examples": [
|
||||
"client"
|
||||
],
|
||||
"title": "configureAs",
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
@@ -88,19 +98,22 @@
|
||||
"$ref": "#/definitions/LastfmData"
|
||||
}
|
||||
],
|
||||
"description": "Specific data required to configure this client"
|
||||
"description": "Specific data required to configure this client",
|
||||
"title": "data"
|
||||
},
|
||||
"name": {
|
||||
"description": "Unique identifier for this client. Used with sources to restrict where scrobbles are sent.",
|
||||
"examples": [
|
||||
"MyConfig"
|
||||
],
|
||||
"title": "name",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"lastfm"
|
||||
],
|
||||
"title": "type",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -109,6 +122,7 @@
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"title": "LastfmClientAIOConfig",
|
||||
"type": "object"
|
||||
},
|
||||
"LastfmData": {
|
||||
@@ -118,6 +132,7 @@
|
||||
"examples": [
|
||||
"787c921a2a2ab42320831aba0c8f2fc2"
|
||||
],
|
||||
"title": "apiKey",
|
||||
"type": "string"
|
||||
},
|
||||
"maxRequestRetries": {
|
||||
@@ -126,6 +141,7 @@
|
||||
"examples": [
|
||||
1
|
||||
],
|
||||
"title": "maxRequestRetries",
|
||||
"type": "number"
|
||||
},
|
||||
"redirectUri": {
|
||||
@@ -134,6 +150,7 @@
|
||||
"examples": [
|
||||
"http://localhost:9078/lastfm/callback"
|
||||
],
|
||||
"title": "redirectUri",
|
||||
"type": "string"
|
||||
},
|
||||
"retryMultiplier": {
|
||||
@@ -142,6 +159,7 @@
|
||||
"examples": [
|
||||
1.5
|
||||
],
|
||||
"title": "retryMultiplier",
|
||||
"type": "number"
|
||||
},
|
||||
"secret": {
|
||||
@@ -149,10 +167,12 @@
|
||||
"examples": [
|
||||
"ec42e09d5ae0ee0f0816ca151008412a"
|
||||
],
|
||||
"title": "secret",
|
||||
"type": "string"
|
||||
},
|
||||
"session": {
|
||||
"description": "Optional session id returned from a completed auth flow",
|
||||
"title": "session",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -160,6 +180,7 @@
|
||||
"apiKey",
|
||||
"secret"
|
||||
],
|
||||
"title": "LastfmData",
|
||||
"type": "object"
|
||||
},
|
||||
"ListenBrainzClientAIOConfig": {
|
||||
@@ -174,23 +195,27 @@
|
||||
"examples": [
|
||||
"client"
|
||||
],
|
||||
"title": "configureAs",
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/definitions/ListenBrainzClientData",
|
||||
"description": "Specific data required to configure this client"
|
||||
"description": "Specific data required to configure this client",
|
||||
"title": "data"
|
||||
},
|
||||
"name": {
|
||||
"description": "Unique identifier for this client. Used with sources to restrict where scrobbles are sent.",
|
||||
"examples": [
|
||||
"MyConfig"
|
||||
],
|
||||
"title": "name",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"listenbrainz"
|
||||
],
|
||||
"title": "type",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -199,6 +224,7 @@
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"title": "ListenBrainzClientAIOConfig",
|
||||
"type": "object"
|
||||
},
|
||||
"ListenBrainzClientData": {
|
||||
@@ -209,6 +235,7 @@
|
||||
"examples": [
|
||||
1
|
||||
],
|
||||
"title": "maxRequestRetries",
|
||||
"type": "number"
|
||||
},
|
||||
"options": {
|
||||
@@ -219,6 +246,7 @@
|
||||
"examples": [
|
||||
true
|
||||
],
|
||||
"title": "checkExistingScrobbles",
|
||||
"type": "boolean"
|
||||
},
|
||||
"refreshEnabled": {
|
||||
@@ -227,18 +255,22 @@
|
||||
"examples": [
|
||||
true
|
||||
],
|
||||
"title": "refreshEnabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"verbose": {
|
||||
"description": "Options used for increasing verbosity of logging in MS (used for debugging)",
|
||||
"properties": {
|
||||
"match": {
|
||||
"$ref": "#/definitions/MatchLoggingOptions"
|
||||
"$ref": "#/definitions/MatchLoggingOptions",
|
||||
"title": "match"
|
||||
}
|
||||
},
|
||||
"title": "verbose",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"title": "options",
|
||||
"type": "object"
|
||||
},
|
||||
"retryMultiplier": {
|
||||
@@ -247,6 +279,7 @@
|
||||
"examples": [
|
||||
1.5
|
||||
],
|
||||
"title": "retryMultiplier",
|
||||
"type": "number"
|
||||
},
|
||||
"token": {
|
||||
@@ -254,6 +287,7 @@
|
||||
"examples": [
|
||||
"6794186bf-1157-4de6-80e5-uvb411f3ea2b"
|
||||
],
|
||||
"title": "token",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
@@ -262,10 +296,12 @@
|
||||
"examples": [
|
||||
"https://api.listenbrainz.org/"
|
||||
],
|
||||
"title": "url",
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"description": "Username of the user to scrobble for",
|
||||
"title": "username",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -273,25 +309,29 @@
|
||||
"token",
|
||||
"username"
|
||||
],
|
||||
"title": "ListenBrainzClientData",
|
||||
"type": "object"
|
||||
},
|
||||
"MalojaClientAIOConfig": {
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/MalojaClientData",
|
||||
"description": "Specific data required to configure this client"
|
||||
"description": "Specific data required to configure this client",
|
||||
"title": "data"
|
||||
},
|
||||
"name": {
|
||||
"description": "Unique identifier for this client. Used with sources to restrict where scrobbles are sent.",
|
||||
"examples": [
|
||||
"MyConfig"
|
||||
],
|
||||
"title": "name",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"maloja"
|
||||
],
|
||||
"title": "type",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -300,6 +340,7 @@
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"title": "MalojaClientAIOConfig",
|
||||
"type": "object"
|
||||
},
|
||||
"MalojaClientData": {
|
||||
@@ -309,6 +350,7 @@
|
||||
"examples": [
|
||||
"myApiKey"
|
||||
],
|
||||
"title": "apiKey",
|
||||
"type": "string"
|
||||
},
|
||||
"maxRequestRetries": {
|
||||
@@ -317,6 +359,7 @@
|
||||
"examples": [
|
||||
1
|
||||
],
|
||||
"title": "maxRequestRetries",
|
||||
"type": "number"
|
||||
},
|
||||
"options": {
|
||||
@@ -327,6 +370,7 @@
|
||||
"examples": [
|
||||
true
|
||||
],
|
||||
"title": "checkExistingScrobbles",
|
||||
"type": "boolean"
|
||||
},
|
||||
"refreshEnabled": {
|
||||
@@ -335,18 +379,22 @@
|
||||
"examples": [
|
||||
true
|
||||
],
|
||||
"title": "refreshEnabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"verbose": {
|
||||
"description": "Options used for increasing verbosity of logging in MS (used for debugging)",
|
||||
"properties": {
|
||||
"match": {
|
||||
"$ref": "#/definitions/MatchLoggingOptions"
|
||||
"$ref": "#/definitions/MatchLoggingOptions",
|
||||
"title": "match"
|
||||
}
|
||||
},
|
||||
"title": "verbose",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"title": "options",
|
||||
"type": "object"
|
||||
},
|
||||
"retryMultiplier": {
|
||||
@@ -355,6 +403,7 @@
|
||||
"examples": [
|
||||
1.5
|
||||
],
|
||||
"title": "retryMultiplier",
|
||||
"type": "number"
|
||||
},
|
||||
"url": {
|
||||
@@ -362,6 +411,7 @@
|
||||
"examples": [
|
||||
"http://localhost:42010"
|
||||
],
|
||||
"title": "url",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -369,6 +419,7 @@
|
||||
"apiKey",
|
||||
"url"
|
||||
],
|
||||
"title": "MalojaClientData",
|
||||
"type": "object"
|
||||
},
|
||||
"MatchLoggingOptions": {
|
||||
@@ -380,6 +431,7 @@
|
||||
"examples": [
|
||||
false
|
||||
],
|
||||
"title": "confidenceBreakdown",
|
||||
"type": "boolean"
|
||||
},
|
||||
"onMatch": {
|
||||
@@ -388,6 +440,7 @@
|
||||
"examples": [
|
||||
false
|
||||
],
|
||||
"title": "onMatch",
|
||||
"type": "boolean"
|
||||
},
|
||||
"onNoMatch": {
|
||||
@@ -396,9 +449,11 @@
|
||||
"examples": [
|
||||
false
|
||||
],
|
||||
"title": "onNoMatch",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"title": "MatchLoggingOptions",
|
||||
"type": "object"
|
||||
},
|
||||
"RequestRetryOptions": {
|
||||
@@ -409,6 +464,7 @@
|
||||
"examples": [
|
||||
1
|
||||
],
|
||||
"title": "maxRequestRetries",
|
||||
"type": "number"
|
||||
},
|
||||
"retryMultiplier": {
|
||||
@@ -417,20 +473,24 @@
|
||||
"examples": [
|
||||
1.5
|
||||
],
|
||||
"title": "retryMultiplier",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"title": "RequestRetryOptions",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"clientDefaults": {
|
||||
"$ref": "#/definitions/RequestRetryOptions"
|
||||
"$ref": "#/definitions/RequestRetryOptions",
|
||||
"title": "clientDefaults"
|
||||
},
|
||||
"clients": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ClientAIOConfig"
|
||||
},
|
||||
"title": "clients",
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"examples": [
|
||||
1
|
||||
],
|
||||
"title": "maxRequestRetries",
|
||||
"type": "number"
|
||||
},
|
||||
"options": {
|
||||
@@ -30,6 +31,7 @@
|
||||
"examples": [
|
||||
true
|
||||
],
|
||||
"title": "checkExistingScrobbles",
|
||||
"type": "boolean"
|
||||
},
|
||||
"refreshEnabled": {
|
||||
@@ -38,18 +40,22 @@
|
||||
"examples": [
|
||||
true
|
||||
],
|
||||
"title": "refreshEnabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"verbose": {
|
||||
"description": "Options used for increasing verbosity of logging in MS (used for debugging)",
|
||||
"properties": {
|
||||
"match": {
|
||||
"$ref": "#/definitions/MatchLoggingOptions"
|
||||
"$ref": "#/definitions/MatchLoggingOptions",
|
||||
"title": "match"
|
||||
}
|
||||
},
|
||||
"title": "verbose",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"title": "options",
|
||||
"type": "object"
|
||||
},
|
||||
"retryMultiplier": {
|
||||
@@ -58,9 +64,11 @@
|
||||
"examples": [
|
||||
1.5
|
||||
],
|
||||
"title": "retryMultiplier",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"title": "CommonClientData",
|
||||
"type": "object"
|
||||
},
|
||||
"LastfmClientConfig": {
|
||||
@@ -75,6 +83,7 @@
|
||||
"examples": [
|
||||
"client"
|
||||
],
|
||||
"title": "configureAs",
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
@@ -86,13 +95,15 @@
|
||||
"$ref": "#/definitions/LastfmData"
|
||||
}
|
||||
],
|
||||
"description": "Specific data required to configure this client"
|
||||
"description": "Specific data required to configure this client",
|
||||
"title": "data"
|
||||
},
|
||||
"name": {
|
||||
"description": "Unique identifier for this client. Used with sources to restrict where scrobbles are sent.",
|
||||
"examples": [
|
||||
"MyConfig"
|
||||
],
|
||||
"title": "name",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -100,6 +111,7 @@
|
||||
"data",
|
||||
"name"
|
||||
],
|
||||
"title": "LastfmClientConfig",
|
||||
"type": "object"
|
||||
},
|
||||
"LastfmData": {
|
||||
@@ -109,6 +121,7 @@
|
||||
"examples": [
|
||||
"787c921a2a2ab42320831aba0c8f2fc2"
|
||||
],
|
||||
"title": "apiKey",
|
||||
"type": "string"
|
||||
},
|
||||
"maxRequestRetries": {
|
||||
@@ -117,6 +130,7 @@
|
||||
"examples": [
|
||||
1
|
||||
],
|
||||
"title": "maxRequestRetries",
|
||||
"type": "number"
|
||||
},
|
||||
"redirectUri": {
|
||||
@@ -125,6 +139,7 @@
|
||||
"examples": [
|
||||
"http://localhost:9078/lastfm/callback"
|
||||
],
|
||||
"title": "redirectUri",
|
||||
"type": "string"
|
||||
},
|
||||
"retryMultiplier": {
|
||||
@@ -133,6 +148,7 @@
|
||||
"examples": [
|
||||
1.5
|
||||
],
|
||||
"title": "retryMultiplier",
|
||||
"type": "number"
|
||||
},
|
||||
"secret": {
|
||||
@@ -140,10 +156,12 @@
|
||||
"examples": [
|
||||
"ec42e09d5ae0ee0f0816ca151008412a"
|
||||
],
|
||||
"title": "secret",
|
||||
"type": "string"
|
||||
},
|
||||
"session": {
|
||||
"description": "Optional session id returned from a completed auth flow",
|
||||
"title": "session",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -151,6 +169,7 @@
|
||||
"apiKey",
|
||||
"secret"
|
||||
],
|
||||
"title": "LastfmData",
|
||||
"type": "object"
|
||||
},
|
||||
"ListenBrainzClientConfig": {
|
||||
@@ -165,17 +184,20 @@
|
||||
"examples": [
|
||||
"client"
|
||||
],
|
||||
"title": "configureAs",
|
||||
"type": "string"
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/definitions/ListenBrainzClientData",
|
||||
"description": "Specific data required to configure this client"
|
||||
"description": "Specific data required to configure this client",
|
||||
"title": "data"
|
||||
},
|
||||
"name": {
|
||||
"description": "Unique identifier for this client. Used with sources to restrict where scrobbles are sent.",
|
||||
"examples": [
|
||||
"MyConfig"
|
||||
],
|
||||
"title": "name",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -183,6 +205,7 @@
|
||||
"data",
|
||||
"name"
|
||||
],
|
||||
"title": "ListenBrainzClientConfig",
|
||||
"type": "object"
|
||||
},
|
||||
"ListenBrainzClientData": {
|
||||
@@ -193,6 +216,7 @@
|
||||
"examples": [
|
||||
1
|
||||
],
|
||||
"title": "maxRequestRetries",
|
||||
"type": "number"
|
||||
},
|
||||
"options": {
|
||||
@@ -203,6 +227,7 @@
|
||||
"examples": [
|
||||
true
|
||||
],
|
||||
"title": "checkExistingScrobbles",
|
||||
"type": "boolean"
|
||||
},
|
||||
"refreshEnabled": {
|
||||
@@ -211,18 +236,22 @@
|
||||
"examples": [
|
||||
true
|
||||
],
|
||||
"title": "refreshEnabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"verbose": {
|
||||
"description": "Options used for increasing verbosity of logging in MS (used for debugging)",
|
||||
"properties": {
|
||||
"match": {
|
||||
"$ref": "#/definitions/MatchLoggingOptions"
|
||||
"$ref": "#/definitions/MatchLoggingOptions",
|
||||
"title": "match"
|
||||
}
|
||||
},
|
||||
"title": "verbose",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"title": "options",
|
||||
"type": "object"
|
||||
},
|
||||
"retryMultiplier": {
|
||||
@@ -231,6 +260,7 @@
|
||||
"examples": [
|
||||
1.5
|
||||
],
|
||||
"title": "retryMultiplier",
|
||||
"type": "number"
|
||||
},
|
||||
"token": {
|
||||
@@ -238,6 +268,7 @@
|
||||
"examples": [
|
||||
"6794186bf-1157-4de6-80e5-uvb411f3ea2b"
|
||||
],
|
||||
"title": "token",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
@@ -246,10 +277,12 @@
|
||||
"examples": [
|
||||
"https://api.listenbrainz.org/"
|
||||
],
|
||||
"title": "url",
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"description": "Username of the user to scrobble for",
|
||||
"title": "username",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -257,19 +290,22 @@
|
||||
"token",
|
||||
"username"
|
||||
],
|
||||
"title": "ListenBrainzClientData",
|
||||
"type": "object"
|
||||
},
|
||||
"MalojaClientConfig": {
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/MalojaClientData",
|
||||
"description": "Specific data required to configure this client"
|
||||
"description": "Specific data required to configure this client",
|
||||
"title": "data"
|
||||
},
|
||||
"name": {
|
||||
"description": "Unique identifier for this client. Used with sources to restrict where scrobbles are sent.",
|
||||
"examples": [
|
||||
"MyConfig"
|
||||
],
|
||||
"title": "name",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -277,6 +313,7 @@
|
||||
"data",
|
||||
"name"
|
||||
],
|
||||
"title": "MalojaClientConfig",
|
||||
"type": "object"
|
||||
},
|
||||
"MalojaClientData": {
|
||||
@@ -286,6 +323,7 @@
|
||||
"examples": [
|
||||
"myApiKey"
|
||||
],
|
||||
"title": "apiKey",
|
||||
"type": "string"
|
||||
},
|
||||
"maxRequestRetries": {
|
||||
@@ -294,6 +332,7 @@
|
||||
"examples": [
|
||||
1
|
||||
],
|
||||
"title": "maxRequestRetries",
|
||||
"type": "number"
|
||||
},
|
||||
"options": {
|
||||
@@ -304,6 +343,7 @@
|
||||
"examples": [
|
||||
true
|
||||
],
|
||||
"title": "checkExistingScrobbles",
|
||||
"type": "boolean"
|
||||
},
|
||||
"refreshEnabled": {
|
||||
@@ -312,18 +352,22 @@
|
||||
"examples": [
|
||||
true
|
||||
],
|
||||
"title": "refreshEnabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"verbose": {
|
||||
"description": "Options used for increasing verbosity of logging in MS (used for debugging)",
|
||||
"properties": {
|
||||
"match": {
|
||||
"$ref": "#/definitions/MatchLoggingOptions"
|
||||
"$ref": "#/definitions/MatchLoggingOptions",
|
||||
"title": "match"
|
||||
}
|
||||
},
|
||||
"title": "verbose",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"title": "options",
|
||||
"type": "object"
|
||||
},
|
||||
"retryMultiplier": {
|
||||
@@ -332,6 +376,7 @@
|
||||
"examples": [
|
||||
1.5
|
||||
],
|
||||
"title": "retryMultiplier",
|
||||
"type": "number"
|
||||
},
|
||||
"url": {
|
||||
@@ -339,6 +384,7 @@
|
||||
"examples": [
|
||||
"http://localhost:42010"
|
||||
],
|
||||
"title": "url",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -346,6 +392,7 @@
|
||||
"apiKey",
|
||||
"url"
|
||||
],
|
||||
"title": "MalojaClientData",
|
||||
"type": "object"
|
||||
},
|
||||
"MatchLoggingOptions": {
|
||||
@@ -357,6 +404,7 @@
|
||||
"examples": [
|
||||
false
|
||||
],
|
||||
"title": "confidenceBreakdown",
|
||||
"type": "boolean"
|
||||
},
|
||||
"onMatch": {
|
||||
@@ -365,6 +413,7 @@
|
||||
"examples": [
|
||||
false
|
||||
],
|
||||
"title": "onMatch",
|
||||
"type": "boolean"
|
||||
},
|
||||
"onNoMatch": {
|
||||
@@ -373,9 +422,11 @@
|
||||
"examples": [
|
||||
false
|
||||
],
|
||||
"title": "onNoMatch",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"title": "MatchLoggingOptions",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`)
|
||||
app.postAsync('/tautulli', async function(this: any, req, res) {
|
||||
tauIngress.trackIngress(req, false);
|
||||
|
||||
const payload = TautulliSource.formatPlayObj(req.body, {newFromSource: true});
|
||||
const payload = TautulliSource.formatPlayObj(req, {newFromSource: true});
|
||||
// try to get config name from payload
|
||||
if (req.body.scrobblerConfig !== undefined) {
|
||||
const source = scrobbleSources.getByName(req.body.scrobblerConfig);
|
||||
@@ -520,6 +520,7 @@ const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`)
|
||||
const tokenResult = await source.handleAuthCodeCallback(req.query);
|
||||
let responseContent = 'OK';
|
||||
if (tokenResult === true) {
|
||||
await source.testAuth();
|
||||
source.poll();
|
||||
} else {
|
||||
responseContent = tokenResult;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import {publish} from 'ntfy';
|
||||
import request from "superagent";
|
||||
import {Logger} from '@foxxmd/winston';
|
||||
import {Config} from "ntfy/interfaces";
|
||||
|
||||
export class NtfyWebhookNotifier extends AbstractWebhookNotifier {
|
||||
|
||||
@@ -54,21 +55,20 @@ export class NtfyWebhookNotifier extends AbstractWebhookNotifier {
|
||||
|
||||
doNotify = async (payload: WebhookPayload) => {
|
||||
try {
|
||||
let authorization = {};
|
||||
if (this.config.username !== undefined) {
|
||||
authorization = {
|
||||
username: this.config.username,
|
||||
password: this.config.password,
|
||||
}
|
||||
}
|
||||
await publish({
|
||||
const req: Config = {
|
||||
message: payload.message,
|
||||
topic: this.config.topic,
|
||||
title: payload.title,
|
||||
server: this.config.url,
|
||||
priority: this.priorities[payload.priority],
|
||||
...authorization,
|
||||
});
|
||||
};
|
||||
if (this.config.username !== undefined) {
|
||||
req.authorization = {
|
||||
username: this.config.username,
|
||||
password: this.config.password,
|
||||
}
|
||||
}
|
||||
await publish(req);
|
||||
this.logger.debug(`Pushed notification.`);
|
||||
} catch (e: any) {
|
||||
this.logger.error(`Failed to push notification: ${e.message}`)
|
||||
|
||||
@@ -159,11 +159,18 @@ export default abstract class AbstractSource {
|
||||
this.emitter.emit('notify', payload);
|
||||
}
|
||||
|
||||
poll = async () => {
|
||||
await this.startPolling();
|
||||
onPollPreAuthCheck = async (): Promise<boolean> => {
|
||||
return true;
|
||||
}
|
||||
|
||||
startPolling = async () => {
|
||||
onPollPostAuthCheck = async (): Promise<boolean> => {
|
||||
return true;
|
||||
}
|
||||
|
||||
poll = async () => {
|
||||
if(!await this.onPollPreAuthCheck()) {
|
||||
return;
|
||||
}
|
||||
if(this.requiresAuth && !this.authed) {
|
||||
if(this.requiresAuthInteraction) {
|
||||
this.notify({title: `${this.identifier} - Polling Error`, message: 'Cannot start polling because user interaction is required for authentication', priority: 'error'});
|
||||
@@ -174,6 +181,13 @@ export default abstract class AbstractSource {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(!await this.onPollPostAuthCheck()) {
|
||||
return;
|
||||
}
|
||||
await this.startPolling();
|
||||
}
|
||||
|
||||
startPolling = async () => {
|
||||
// reset poll attempts if already previously run
|
||||
this.pollRetries = 0;
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ export default class DeezerSource extends AbstractSource {
|
||||
}
|
||||
|
||||
getRecentlyPlayed = async (options: RecentlyPlayedOptions = {}) => {
|
||||
const resp = await this.callApi(request.get(`${this.baseUrl}/user/me/history`));
|
||||
const resp = await this.callApi(request.get(`${this.baseUrl}/user/me/history?limit=20`));
|
||||
return resp.data.map((x: any) => DeezerSource.formatPlayObj(x)).sort(sortByOldestPlayDate);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,28 +17,52 @@ export default class JellyfinSource extends MemorySource {
|
||||
|
||||
constructor(name: any, config: JellySourceConfig, internal: InternalConfig, emitter: EventEmitter) {
|
||||
super('jellyfin', name, config, internal, emitter);
|
||||
const {data: {users, servers} = {}} = config;
|
||||
const {
|
||||
data: {
|
||||
users,
|
||||
servers,
|
||||
options: {
|
||||
logFilterFailure = 'warn'
|
||||
} = {}
|
||||
} = {}
|
||||
} = config;
|
||||
|
||||
if(logFilterFailure !== false && !['debug', 'warn'].includes(logFilterFailure)) {
|
||||
this.logger.warn(`logFilterFailure value of '${logFilterFailure.toString()}' is NOT VALID. Logging will not occur if filters fail. You should fix this.`);
|
||||
}
|
||||
|
||||
if (users === undefined || users === null) {
|
||||
this.users = undefined;
|
||||
} else {
|
||||
if (!Array.isArray(users)) {
|
||||
this.users = users.split(',')
|
||||
if(users.trim() === '') {
|
||||
this.users = undefined;
|
||||
} else {
|
||||
this.users = users.split(',').map(x => x.trim());
|
||||
}
|
||||
} else {
|
||||
this.users = users;
|
||||
}
|
||||
this.users = this.users.map((x: any) => x.toLocaleLowerCase())
|
||||
if(this.users !== undefined) {
|
||||
this.users = this.users.map((x: any) => x.toLocaleLowerCase())
|
||||
}
|
||||
}
|
||||
|
||||
if (servers === undefined || servers === null) {
|
||||
this.servers = undefined;
|
||||
} else {
|
||||
if (!Array.isArray(servers)) {
|
||||
this.servers = servers.split(',')
|
||||
if(servers.trim() === '') {
|
||||
this.servers = undefined;
|
||||
} else {
|
||||
this.servers = servers.split(',').map(x => x.trim());
|
||||
}
|
||||
} else {
|
||||
this.servers = servers;
|
||||
}
|
||||
this.servers = this.servers.map((x: any) => x.toLocaleLowerCase())
|
||||
if(this.servers !== undefined) {
|
||||
this.servers = this.servers.map((x: any) => x.toLocaleLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
if (users === undefined && servers === undefined) {
|
||||
@@ -124,6 +148,22 @@ export default class JellyfinSource extends MemorySource {
|
||||
}
|
||||
}
|
||||
|
||||
protected logFilterFailure = (str: string, meta?: any) => {
|
||||
const {
|
||||
data: {
|
||||
options: {
|
||||
logFilterFailure = 'warn'
|
||||
} = {}
|
||||
} = {}
|
||||
} = this.config;
|
||||
|
||||
if(logFilterFailure === false || !['warn','debug'].includes(logFilterFailure)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.logger[logFilterFailure](str, meta);
|
||||
}
|
||||
|
||||
isValidEvent = (playObj: PlayObject) => {
|
||||
const {
|
||||
meta: {
|
||||
@@ -148,7 +188,7 @@ export default class JellyfinSource extends MemorySource {
|
||||
}
|
||||
|
||||
if (this.servers !== undefined && !this.servers.includes(server.toLocaleLowerCase())) {
|
||||
this.logger.warn(`Will not scrobble event because server was not on allowed list, found server: ${server}`, {
|
||||
this.logFilterFailure(`Will not scrobble event because server was not an allowed server. Expected: ${this.servers.map(x => `'${x}'`).join(' or ')} | Found: '${server.toLocaleLowerCase()}'`, {
|
||||
track
|
||||
})
|
||||
return false;
|
||||
@@ -156,10 +196,10 @@ export default class JellyfinSource extends MemorySource {
|
||||
|
||||
if (this.users !== undefined) {
|
||||
if (user === undefined) {
|
||||
this.logger.warn(`Will not scrobble event because config defined users but payload contained no user info`);
|
||||
this.logFilterFailure(`Will not scrobble event because config defined users but payload contained no user info`);
|
||||
return false;
|
||||
} else if (!this.users.includes(user.toLocaleLowerCase())) {
|
||||
this.logger.warn(`Will not scrobble event because author was not an allowed user: ${user}`, {
|
||||
this.logFilterFailure(`Will not scrobble event because author was not an allowed user. Expected: ${this.users.map(x => `'${x}'`).join(' or ')} | Found: '${user.toLocaleLowerCase()}'`, {
|
||||
artists,
|
||||
track
|
||||
})
|
||||
|
||||
@@ -22,7 +22,6 @@ export class KodiSource extends MemorySource {
|
||||
} = 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;
|
||||
@@ -34,6 +33,7 @@ export class KodiSource extends MemorySource {
|
||||
url
|
||||
} = {}
|
||||
} = this.config;
|
||||
this.client = new KodiApiClient(this.name, this.config.data);
|
||||
this.logger.debug(`Config URL: '${url ?? '(None Given)'}' => Normalized: '${this.client.url.toString()}'`)
|
||||
this.initialized = true;
|
||||
return true;
|
||||
|
||||
@@ -26,10 +26,27 @@ export default class PlexSource extends AbstractSource {
|
||||
|
||||
constructor(name: any, config: PlexSourceConfig, internal: InternalConfig, type: SourceType = 'plex',emitter: EventEmitter) {
|
||||
super(type, name, config, internal, emitter);
|
||||
const {data: {user = [], libraries = [], servers = []} = {}} = config
|
||||
const {
|
||||
data: {
|
||||
user = [],
|
||||
libraries = [],
|
||||
servers = [],
|
||||
options: {
|
||||
logFilterFailure = 'warn'
|
||||
} = {}
|
||||
} = {},
|
||||
} = config
|
||||
|
||||
if(logFilterFailure !== false && !['debug', 'warn'].includes(logFilterFailure)) {
|
||||
this.logger.warn(`logFilterFailure value of '${logFilterFailure.toString()}' is NOT VALID. Logging will not occur if filters fail. You should fix this.`);
|
||||
}
|
||||
|
||||
if (!Array.isArray(user)) {
|
||||
this.users = [user];
|
||||
if(user.trim() === '') {
|
||||
this.users = [];
|
||||
} else {
|
||||
this.users = user.split(',').map(x => x.trim());
|
||||
}
|
||||
} else {
|
||||
this.users = user;
|
||||
}
|
||||
@@ -106,6 +123,22 @@ export default class PlexSource extends AbstractSource {
|
||||
}
|
||||
}
|
||||
|
||||
protected logFilterFailure = (str: string, meta?: any) => {
|
||||
const {
|
||||
data: {
|
||||
options: {
|
||||
logFilterFailure = 'warn'
|
||||
} = {}
|
||||
} = {}
|
||||
} = this.config;
|
||||
|
||||
if(logFilterFailure === false || !['warn','debug'].includes(logFilterFailure)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.logger[logFilterFailure](str, meta);
|
||||
}
|
||||
|
||||
isValidEvent = (playObj: PlayObject) => {
|
||||
const {
|
||||
meta: {
|
||||
@@ -119,20 +152,8 @@ export default class PlexSource extends AbstractSource {
|
||||
|
||||
const hint = this.type === 'tautulli' ? ' (Check notification agent json data configuration)' : '';
|
||||
|
||||
if (this.users.length !== 0) {
|
||||
if (user === undefined) {
|
||||
this.logger.warn(`Config defined users but payload contained no user info${hint}`);
|
||||
} else if (!this.users.includes(user.toLocaleLowerCase())) {
|
||||
this.logger.verbose(`Will not scrobble event because author was not an allowed user: ${user}`, {
|
||||
artists,
|
||||
track
|
||||
})
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (event !== undefined && event !== 'media.scrobble') {
|
||||
this.logger.verbose(`Will not scrobble event because it is not media.scrobble (${event})`, {
|
||||
this.logger.debug(`Will not scrobble event because it is not media.scrobble (${event})`, {
|
||||
artists,
|
||||
track
|
||||
})
|
||||
@@ -140,18 +161,30 @@ export default class PlexSource extends AbstractSource {
|
||||
}
|
||||
|
||||
if (mediaType !== 'track') {
|
||||
this.logger.verbose(`Will not scrobble event because media type was not a track (${mediaType})`, {
|
||||
this.logger.debug(`Will not scrobble event because media type was not a track (${mediaType})`, {
|
||||
artists,
|
||||
track
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.users.length !== 0) {
|
||||
if (user === undefined) {
|
||||
this.logFilterFailure(`Config defined users but payload contained no user info${hint}`);
|
||||
} else if (!this.users.includes(user.toLocaleLowerCase())) {
|
||||
this.logFilterFailure(`Will not scrobble event because author was not an allowed user. Expected: ${this.users.map(x => `'${x}'`).join(' or ')} | Found: '${user.toLocaleLowerCase()}'`, {
|
||||
artists,
|
||||
track
|
||||
})
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.libraries.length !== 0) {
|
||||
if (library === undefined) {
|
||||
this.logger.warn(`Config defined libraries but payload contained no library info${hint}`);
|
||||
this.logFilterFailure(`Config defined libraries but payload contained no library info${hint}`);
|
||||
} else if (!this.libraries.includes(library.toLocaleLowerCase())) {
|
||||
this.logger.verbose(`Will not scrobble event because library was not on allowed list: ${library}`, {
|
||||
this.logFilterFailure(`Will not scrobble event because library was not an allowed library. Expected: ${this.libraries.map(x => `'${x}'`).join(' or ')} | Found: '${library.toLocaleLowerCase()}'`, {
|
||||
artists,
|
||||
track
|
||||
})
|
||||
@@ -161,9 +194,9 @@ export default class PlexSource extends AbstractSource {
|
||||
|
||||
if (this.servers.length !== 0) {
|
||||
if (server === undefined) {
|
||||
this.logger.warn(`Config defined server but payload contained no server info${hint}`);
|
||||
this.logFilterFailure(`Config defined server but payload contained no server info${hint}`);
|
||||
} else if (!this.servers.includes(server.toLocaleLowerCase())) {
|
||||
this.logger.verbose(`Will not scrobble event because server was not on allowed list: ${server}`, {
|
||||
this.logFilterFailure(`Will not scrobble event because server was not an allowed server. Expected: ${this.servers.map(x => `'${x}'`).join(' or ')} | Found: '${server.toLocaleLowerCase()}'`, {
|
||||
artists,
|
||||
track
|
||||
})
|
||||
|
||||
@@ -159,8 +159,11 @@ export default class ScrobbleSources {
|
||||
case 'tautulli':
|
||||
const t = {
|
||||
// support this for now
|
||||
user: process.env.TAUTULLI_USER || process.env.PLEX_USER
|
||||
user: process.env.TAUTULLI_USER
|
||||
};
|
||||
if(t.user === undefined) {
|
||||
t.user = process.env.PLEX_USER;
|
||||
}
|
||||
if (!Object.values(t).every(x => x === undefined)) {
|
||||
configs.push({
|
||||
type: 'tautulli',
|
||||
|
||||
@@ -364,12 +364,15 @@ export default class SpotifySource extends MemorySource {
|
||||
}
|
||||
}
|
||||
|
||||
poll = async () => {
|
||||
onPollPreAuthCheck = async () => {
|
||||
if (this.spotifyApi === undefined) {
|
||||
this.logger.warn('Cannot poll spotify without valid credentials configuration')
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
onPollPostAuthCheck = async () => {
|
||||
// test capabilities
|
||||
try {
|
||||
await this.getCurrentPlaybackState(false);
|
||||
@@ -378,12 +381,12 @@ export default class SpotifySource extends MemorySource {
|
||||
this.logger.warn('multi-scrobbler does not have sufficient permissions to access Spotify API "Get Playback State". MS will continue to work but accuracy for determining if/when a track played from a Spotify Connect device (smart device controlled through Spotify app) may be degraded. To fix this re-authenticate MS with Spotify and restart polling.');
|
||||
}
|
||||
|
||||
// and record backlogged tracks
|
||||
this.logger.info('Checking recently played API for tracks to backlog...');
|
||||
const backlogPlays = await this.getPlayHistory({formatted: true});
|
||||
this.scrobble(backlogPlays);
|
||||
this.logger.info('Backlog complete.');
|
||||
|
||||
await this.startPolling();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {TautulliSourceConfig} from "../common/infrastructure/config/source/tautu
|
||||
import {FormatPlayObjectOptions, InternalConfig, PlayObject} from "../common/infrastructure/Atomic.js";
|
||||
import {combinePartsToString, truncateStringToLength} from "../utils.js";
|
||||
import EventEmitter from "events";
|
||||
import {Request} from "express";
|
||||
|
||||
const shortDeviceId = truncateStringToLength(10, '');
|
||||
|
||||
@@ -13,29 +14,30 @@ export default class TautulliSource extends PlexSource {
|
||||
constructor(name: any, config: TautulliSourceConfig, internal: InternalConfig, emitter: EventEmitter) {
|
||||
super(name, config, internal, 'tautulli', emitter);
|
||||
}
|
||||
|
||||
static formatPlayObj(obj: any, options: FormatPlayObjectOptions = {}): PlayObject {
|
||||
static formatPlayObj(obj: Request, options: FormatPlayObjectOptions = {}): PlayObject {
|
||||
const {newFromSource = false} = options;
|
||||
const {
|
||||
artist_name,
|
||||
track_name,
|
||||
track_artist,
|
||||
album_name,
|
||||
media_type,
|
||||
title,
|
||||
library_name,
|
||||
server,
|
||||
version,
|
||||
duration,
|
||||
username,
|
||||
library,
|
||||
machine_id = '',
|
||||
session_key,
|
||||
action,
|
||||
platform,
|
||||
device,
|
||||
player,
|
||||
} = obj.body;
|
||||
body :{
|
||||
artist_name,
|
||||
track_name,
|
||||
track_artist,
|
||||
album_name,
|
||||
media_type,
|
||||
title,
|
||||
library_name,
|
||||
server,
|
||||
version,
|
||||
duration,
|
||||
username,
|
||||
library,
|
||||
machine_id = '',
|
||||
session_key,
|
||||
action,
|
||||
platform,
|
||||
device,
|
||||
player,
|
||||
} = {}
|
||||
} = obj;
|
||||
let artists = [artist_name];
|
||||
if (track_artist !== undefined && track_artist !== artist_name) {
|
||||
artists.push(track_artist);
|
||||
|
||||
@@ -180,8 +180,8 @@ export default class YTMusicSource extends AbstractSource {
|
||||
return this.authed;
|
||||
}
|
||||
|
||||
poll = async () => {
|
||||
if(this.authed && !this.polling) {
|
||||
onPollPostAuthCheck = async () => {
|
||||
if(!this.polling) {
|
||||
this.logger.verbose('Hydrating initial recently played tracks for reference.');
|
||||
const referencePlays = await this.getRecentlyPlayed();
|
||||
const reversedPlays = [...referencePlays];
|
||||
@@ -194,6 +194,6 @@ export default class YTMusicSource extends AbstractSource {
|
||||
}
|
||||
}
|
||||
}
|
||||
await this.startPolling();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@ export class TautulliNotifier extends IngressNotifier {
|
||||
|
||||
if(!this.seenServers.includes(playObj.meta.server)) {
|
||||
this.seenServers.push(playObj.meta.server);
|
||||
let msg = [`Received valid data from server ${playObj.meta.server} for the first time.`];
|
||||
let msg = [`Received data from server ${playObj.meta.server} for the first time.`];
|
||||
if(req.body === undefined) {
|
||||
msg.push('WARNING: Payload was empty.');
|
||||
}
|
||||
if(playObj.meta.library === undefined) {
|
||||
msg.push('WARNING: library was not defined in payload. If you want to filter plays by library this must be present in webhook payload.');
|
||||
}
|
||||
|
||||
@@ -77,7 +77,13 @@ export const truncateStringArrToLength = (length: any, truncStr = '...') => {
|
||||
const truncater = truncateStringToLength(length, truncStr);
|
||||
return (strings: any) => strings.map(truncater);
|
||||
}
|
||||
export const truncateStringToLength = (length: any, truncStr = '...') => (str: any) => str.length > length ? `${str.slice(0, length)}${truncStr}` : str;
|
||||
export const truncateStringToLength = (length: any, truncStr = '...') => (val: any = '') => {
|
||||
if(val === null) {
|
||||
return '';
|
||||
}
|
||||
const str = typeof val !== 'string' ? val.toString() : val;
|
||||
return str.length > length ? `${str.slice(0, length)}${truncStr}` : str;
|
||||
}
|
||||
|
||||
const defaultTransformer = (input: any) => input;
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"node_modules",
|
||||
"coverage",
|
||||
"tests/*.ts",
|
||||
"_site"
|
||||
"_site",
|
||||
"docsite"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://typedoc.org/schema.json",
|
||||
"name": "multi-scrobbler Docs",
|
||||
"entryPoints": ["./src/common/infrastructure/config/aioConfig.ts"],
|
||||
"sort": ["source-order"],
|
||||
"categorizeByGroup": false,
|
||||
"searchGroupBoosts": {
|
||||
"Functions": 1.5
|
||||
},
|
||||
"navigationLinks": {
|
||||
"Docs": "https://foxxmd.github.io/multi-scrobbler",
|
||||
"GitHub": "https://github.com/foxxmd/multi-scrobbler"
|
||||
}
|
||||
}
|
||||