Compare commits

...
38 Commits
Author SHA1 Message Date
FoxxMD f856f79e51 Merge branch 'develop' 2023-07-17 15:41:00 -04:00
FoxxMD fbea08dded chore: Bump version 2023-07-17 15:40:44 -04:00
FoxxMD dc52376395 docs: Use external links for config examples
Docusaurus treats the files as assets and forces them to download when relative links. Instead, use external links to github so the user can stay in browser
2023-07-17 15:39:24 -04:00
FoxxMD 1e2bc0ca85 Merge branch 'docsite' into develop 2023-07-14 11:27:56 -04:00
FoxxMD 7849d6098a docs: Fix docker heading 2023-07-13 16:29:55 -04:00
FoxxMD 0b101b758e feat(tautulli): Support empty user to enable source with all users #84 2023-07-13 16:13:16 -04:00
FoxxMD 756ff0e0c1 chore(github): Update trigger branch for docs deploy to master 2023-07-13 16:12:31 -04:00
FoxxMD 2ddeca7d29 Use working directory without cd command 2023-07-13 15:58:12 -04:00
FoxxMD 1de934e30a Use working directory 2023-07-13 15:57:26 -04:00
FoxxMD 0409c526ee Assume we are still in docsite dir? 2023-07-13 15:54:25 -04:00
FoxxMD a7c340188f Fix run step typo 2023-07-13 15:52:42 -04:00
FoxxMD 5dc54849cc chore(github): Add action for build/deploy docusaurus docsite 2023-07-13 15:51:20 -04:00
FoxxMD 058fbee0c8 Migrate all docs to docsite 2023-07-13 15:40:48 -04:00
FoxxMD daea76eef3 feat: Implement somewhat-working docusaurus site 2023-07-13 15:08:00 -04:00
FoxxMD b81b1d94a3 docs: Update docker registry options 2023-07-13 11:21:27 -04:00
FoxxMD 226b7c741c feat: Implement filter logging controls
For jellyfin/plex/tautulli -- `logFilterFailure` option in config allows controlling if and what log LEVEL user/server filter failures are logged. #85

* Updated schema
* Updated examples
* Also added expected/found detailed logging for filters to jellyfin #84
2023-07-13 11:07:12 -04:00
FoxxMD e452bd761c chore(github): Publish to GHCR
Also pin actions at versions instead of commits
2023-07-13 09:58:07 -04:00
FoxxMD 19c3d97a46 chore(docker): Remove armv7 buildx platform
It's legacy and not supported in most places! Don't need to build for this anymore.
2023-07-13 09:46:22 -04:00
FoxxMD 64164b28de fix(plex): More config data normalization
* Split user value on comma to align with what config describes as acceptable (comma-delim)
* Trim found values to fix any whitespace typos
* On disallowed event logging add expected/found values to help with debugging

Fixes #84
2023-07-13 09:42:21 -04:00
FoxxMD b03cbe7a0a Merge branch 'develop' 2023-06-12 14:20:09 -04:00
FoxxMD 91bd702deb docs: Add multiple artist scrobbling limitation to FAQ 2023-06-12 12:23:49 -04:00
Matt Foxx ede22e10e9 Merge pull request #82 from Whhoesj/lastfm-fix 2023-06-12 12:10:05 -04:00
Wouter Habets c648ad4ae3 fix(lastfm): only scrobble primary artist to last.fm
When a track contains multiple artist, only the first artist should be send to Last.fm since it doesn't support multiple artists per track.
Previous behaviour was to combine all artists in a single string.

Issue: #81
2023-06-12 17:56:37 +02:00
FoxxMD a0fbd1129a chore: Add git-cliff template 2023-06-07 10:01:30 -04:00
FoxxMD b90ea03924 Merge branch 'develop' 2023-06-07 09:23:51 -04:00
FoxxMD 88efe2ad87 Bump versions for release 2023-06-07 09:23:40 -04:00
FoxxMD 39f933418e fix(ntfy): Include authorization object correctly in publish request data #80 2023-05-30 10:32:20 -04:00
FoxxMD cc8c7d84e2 fix(kodi): Fix hardcoded string #79
Probably leftover from debugging, oops
2023-05-25 12:18:29 -04:00
FoxxMD 6d9783c5f5 fix(tautulli): More response formatting safeguards #77
* Log warning if payload is empty
* Destructure request body with default empty object
2023-05-25 11:46:00 -04:00
FoxxMD c10d46b774 fix: Passing wrong argument to tautulli payload formatter #77 2023-05-24 11:47:29 -04:00
FoxxMD a5417277a8 feat: Use pre/post auth hooks for polling start to simplify logic
* Removes need to override poll() in child class and better defines when logic should be performed
* Fixes app crash when spotify credentials are present but invalid #76
2023-05-24 11:04:51 -04:00
FoxxMD 5a1c931513 fix: Re-initialize auth state after receiving auth callback
So we don't have to restart after authenticating in order to start polling

#76
2023-05-24 10:45:07 -04:00
FoxxMD 09f4503593 fix: Handle null/non-string values when truncating
#76
2023-05-19 12:25:22 -04:00
FoxxMD 045cae66f5 fix: Respect maloja response status and throw on error
Don't assume track scrobble correctly. Check body status prop and throw on error or log warnings if they are present

Maybe fixes #57
2023-04-18 10:51:25 -04:00
FoxxMD e67ef5fb0f fix: try to limit number of plays returned by deezer 2023-04-03 16:10:58 -04:00
FoxxMD 1677c0021f Update maloja track parsing in preparation for Maloja 3.2
Album structure changes
2023-04-03 15:59:29 -04:00
FoxxMD 3400e747a8 Fix listenbrainz example
Thanks @captainnapalm
2023-04-03 15:46:43 -04:00
FoxxMD c08ff4843c Bump flatpak build source 2023-03-27 10:02:49 -04:00
74 changed files with 19497 additions and 448 deletions
+2 -2
View File
@@ -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.
+42
View File
@@ -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
+21 -21
View File
@@ -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
+67
View File
@@ -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"
+2 -2
View File
@@ -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
+2 -1
View File
@@ -6,7 +6,8 @@
"users": ["FoxxMD"],
"servers": ["myServer","anotherServer"],
"options": {
"logPayload": false
"logPayload": false,
"logFilterFailure": "warn"
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
[
{
"name": "brainz",
"configureAs": "client"
"configureAs": "client",
"data": {
"token": "029b081ba-9156-4pe7-88e5-3be671f5ea2b",
"username": "FoxxMD"
+4 -1
View File
@@ -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"
}
}
}
]
+4 -1
View File
@@ -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"
}
}
}
]
+20
View File
@@ -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*
+41
View File
@@ -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.
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};
+18 -4
View File
@@ -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
![Google Headers](/docs/google-header.jpg)
![Google Headers](google-header.jpg)
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.
![Notification Tray](/assets/mpris.jpg)
![Notification Tray](mpris.jpg)
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:
Binary file not shown.

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]
+155
View File
@@ -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;
+12912
View File
File diff suppressed because it is too large Load Diff
+48
View File
@@ -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"
}
}
+40
View File
@@ -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;
+44
View File
@@ -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
+30
View File
@@ -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);
}
+100
View File
@@ -0,0 +1,100 @@
---
title: Multi-Scrobbler
---
# multi-scrobbler
![Latest Release](https://img.shields.io/github/v/release/foxxmd/multi-scrobbler)
![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
![Docker Pulls](https://img.shields.io/docker/pulls/foxxmd/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
+38
View File
@@ -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>
)
}
View File
File diff suppressed because it is too large Load Diff
+59
View File
@@ -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
}
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+12
View File
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

File diff suppressed because it is too large Load Diff
+10
View File
@@ -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 -1
View File
@@ -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
+12
View File
@@ -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>
+3 -1
View File
@@ -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
+102 -2
View File
@@ -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",
+9 -7
View File
@@ -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": {
+1 -1
View File
@@ -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'),
+82
View File
@@ -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[]
}
+10 -1
View File
@@ -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,
+103 -46
View File
@@ -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}` : ''}`;
}
-54
View File
@@ -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 {
+68 -8
View File
@@ -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"
}
},
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+57 -6
View File
@@ -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"
}
}
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -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;
+10 -10
View File
@@ -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}`)
+17 -3
View File
@@ -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;
+1 -1
View File
@@ -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);
}
+48 -8
View File
@@ -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
})
+1 -1
View File
@@ -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;
+53 -20
View File
@@ -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
})
+4 -1
View File
@@ -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',
+7 -4
View File
@@ -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;
}
}
+23 -21
View File
@@ -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);
+3 -3
View File
@@ -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.');
}
+7 -1
View File
@@ -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;
+2 -1
View File
@@ -23,6 +23,7 @@
"node_modules",
"coverage",
"tests/*.ts",
"_site"
"_site",
"docsite"
]
}
+14
View File
@@ -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"
}
}