mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-09-03 05:10:00 +03:00
Compare commits
209
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46883384b6 | ||
|
|
7bc5ab3d0f | ||
|
|
fd15d15124 | ||
|
|
14da0795f1 | ||
|
|
3faf0ee7f9 | ||
|
|
78ddb15dcf | ||
|
|
760373c227 | ||
|
|
6bb7b17e97 | ||
|
|
231156d452 | ||
|
|
d85628b4c9 | ||
|
|
db0e74aec3 | ||
|
|
a918a96d34 | ||
|
|
bb51915085 | ||
|
|
7c177dfe03 | ||
|
|
cf5e7ca1fe | ||
|
|
fd43ff86c8 | ||
|
|
194a0e78ff | ||
|
|
04027e87e7 | ||
|
|
a998060bca | ||
|
|
5b16e8857e | ||
|
|
c53ce7c517 | ||
|
|
7897dc9503 | ||
|
|
160ddac3e4 | ||
|
|
59b4f27294 | ||
|
|
21f42bf45b | ||
|
|
3e8f170f9a | ||
|
|
15fbed09a5 | ||
|
|
52b301e729 | ||
|
|
0bef197521 | ||
|
|
f16750609a | ||
|
|
c39ef29597 | ||
|
|
72639616b7 | ||
|
|
effe870104 | ||
|
|
ee753389f7 | ||
|
|
5f4a5cb815 | ||
|
|
0607aedf04 | ||
|
|
1f165096b3 | ||
|
|
fb6f6a18c8 | ||
|
|
03f8b20704 | ||
|
|
c49cdba718 | ||
|
|
de67879af5 | ||
|
|
9d0cf8f845 | ||
|
|
8d9fd11c79 | ||
|
|
745343c589 | ||
|
|
e5935c38da | ||
|
|
cac13fa3da | ||
|
|
ca4803713e | ||
|
|
e53dd55ad8 | ||
|
|
1f2772353a | ||
|
|
78b48107a0 | ||
|
|
bd00055ae5 | ||
|
|
b174b23b3b | ||
|
|
9e6319ee73 | ||
|
|
6b5b3990cd | ||
|
|
f2465f0f88 | ||
|
|
c68ec8dbf6 | ||
|
|
6e1cdba58c | ||
|
|
df4bb27d3a | ||
|
|
de7707b45a | ||
|
|
c884b4776c | ||
|
|
0893b62f3b | ||
|
|
7d071c763d | ||
|
|
664395a0ee | ||
|
|
007d553d59 | ||
|
|
7c8c185c4c | ||
|
|
972eccd0bf | ||
|
|
034cfe7a8e | ||
|
|
7a8cf7c004 | ||
|
|
30cdc9f553 | ||
|
|
3f19abb899 | ||
|
|
a601757759 | ||
|
|
fe03bcd651 | ||
|
|
1f319cb9f6 | ||
|
|
a0ac0f8e46 | ||
|
|
06945fda52 | ||
|
|
306c4c858e | ||
|
|
81885632a0 | ||
|
|
23467481c1 | ||
|
|
e716de2160 | ||
|
|
dda06a0873 | ||
|
|
eca64d68e7 | ||
|
|
1065643b23 | ||
|
|
d92153f85f | ||
|
|
22d76c459a | ||
|
|
ec87ab96d6 | ||
|
|
f62fe270fb | ||
|
|
677474beed | ||
|
|
cb9dd0fbc5 | ||
|
|
0685d4b06d | ||
|
|
82c0a4faec | ||
|
|
ccbd5e304a | ||
|
|
4f81a192fa | ||
|
|
22cf95e58c | ||
|
|
358355c612 | ||
|
|
c5f9600047 | ||
|
|
703731e654 | ||
|
|
0dfa4c7aad | ||
|
|
68e473ec02 | ||
|
|
a8d20734aa | ||
|
|
ebdb76cf6c | ||
|
|
e62e1ac2dd | ||
|
|
e68b46f647 | ||
|
|
7605cd3f1d | ||
|
|
2735c283ce | ||
|
|
0daaec81ca | ||
|
|
2085df4406 | ||
|
|
9b8664d3a3 | ||
|
|
e027a33e8d | ||
|
|
f04a19d34d | ||
|
|
4b27671a91 | ||
|
|
5f4943e025 | ||
|
|
66cdb94d57 | ||
|
|
5ebb0e6a9c | ||
|
|
dfb7f43c00 | ||
|
|
ed85d35117 | ||
|
|
7d69e77ac6 | ||
|
|
03973d215b | ||
|
|
1b182d89cd | ||
|
|
3d5c2c7450 | ||
|
|
d6264dd994 | ||
|
|
4c18123207 | ||
|
|
71e7f41ea5 | ||
|
|
60cf5d1019 | ||
|
|
3977057dfa | ||
|
|
943bdc3e88 | ||
|
|
44710048f4 | ||
|
|
9f9f5655b1 | ||
|
|
e621811364 | ||
|
|
685bc846e8 | ||
|
|
3a9d58fae9 | ||
|
|
33639c6e4c | ||
|
|
7c235e3179 | ||
|
|
559d749207 | ||
|
|
53feae0f8c | ||
|
|
0941ccc0ac | ||
|
|
82ecd96ba4 | ||
|
|
e118dc5db1 | ||
|
|
1a23491ac2 | ||
|
|
d916f167a5 | ||
|
|
0102249549 | ||
|
|
2ca96dab42 | ||
|
|
345e60b7a3 | ||
|
|
b9fb6c9d35 | ||
|
|
5d0ccd8867 | ||
|
|
b36f484f3b | ||
|
|
d2f7f986c7 | ||
|
|
9137d03b9f | ||
|
|
f6c0d13a8b | ||
|
|
c472b4e33a | ||
|
|
f6c3fc6b50 | ||
|
|
8ba146495e | ||
|
|
68c46fefb2 | ||
|
|
eddda41559 | ||
|
|
5945fdc2ef | ||
|
|
d110dcbdcf | ||
|
|
ebc0aa8a4e | ||
|
|
d4113b464a | ||
|
|
753dfe1fb3 | ||
|
|
013cccaac5 | ||
|
|
504f8ffbeb | ||
|
|
81d1eb2553 | ||
|
|
c9327b0e92 | ||
|
|
36aa236d82 | ||
|
|
3c37aa4449 | ||
|
|
f1bf3cbe64 | ||
|
|
17a9356040 | ||
|
|
97c799bf2c | ||
|
|
99c02daec7 | ||
|
|
4cb792641d | ||
|
|
fbc2950854 | ||
|
|
08e0bdb437 | ||
|
|
73dcab4de1 | ||
|
|
00594a14ed | ||
|
|
52d5bab0f5 | ||
|
|
a41f4c56f7 | ||
|
|
8ea83962d6 | ||
|
|
9d97cc6e4c | ||
|
|
023579de9a | ||
|
|
214d99f1c3 | ||
|
|
d6fc6b9b10 | ||
|
|
cbdab93f92 | ||
|
|
abf37f0687 | ||
|
|
e993a43399 | ||
|
|
bc79969fd0 | ||
|
|
801f9ed403 | ||
|
|
3d9f6f8f58 | ||
|
|
c2676fb620 | ||
|
|
cdb4ab23e9 | ||
|
|
b3217f2d9c | ||
|
|
7b61385ad5 | ||
|
|
b5c86e74ad | ||
|
|
a3031a7eb9 | ||
|
|
7c49a0535f | ||
|
|
797e5b0794 | ||
|
|
bc6cd78c86 | ||
|
|
98a5b40be5 | ||
|
|
87c174fe0f | ||
|
|
0cbb45e4ad | ||
|
|
b11226af94 | ||
|
|
27dc5b5e19 | ||
|
|
d892b83110 | ||
|
|
c28984524a | ||
|
|
5d4fb63baa | ||
|
|
ca2574d1e5 | ||
|
|
758038e278 | ||
|
|
cb803b1a24 | ||
|
|
c985af5722 | ||
|
|
1ede54acbb | ||
|
|
a73ca6a95d |
@@ -11,7 +11,7 @@ services:
|
||||
# context: .
|
||||
# dockerfile: .devcontainer/Dockerfile
|
||||
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:1-20-bookworm
|
||||
image: mcr.microsoft.com/devcontainers/javascript-node:24-bookworm
|
||||
|
||||
volumes:
|
||||
# Update this to wherever you want VS Code to mount the folder of your project
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"forwardPorts": [9078,3000],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "npm install",
|
||||
"postCreateCommand": ".devcontainer/postCreate.sh",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo 'Adding node_modules to path'
|
||||
echo 'export PATH=/workspaces/node_modules/.bin:/workspaces/docsite/node_modules/.bin:$PATH' >> /home/node/.bashrc
|
||||
|
||||
echo 'Updating npm...'
|
||||
npm install -g npm@11.12.1
|
||||
|
||||
echo 'Installing concurrently...'
|
||||
npm install -g concurrently
|
||||
|
||||
echo 'If you have freshly cloned this project or node_modules folder does not exist then you should run "npm run install:parallel" now'
|
||||
+3
-1
@@ -28,4 +28,6 @@ build
|
||||
**/act/*
|
||||
tmp-*
|
||||
.env
|
||||
*.env
|
||||
*.env
|
||||
.storybook
|
||||
.stories
|
||||
@@ -0,0 +1,19 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directories:
|
||||
- "/"
|
||||
- "/docsite"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "docker"
|
||||
# Look for a `Dockerfile` in the `root` directory
|
||||
directory: "/"
|
||||
# Check for updates once a week
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -12,12 +12,12 @@ jobs:
|
||||
name: Deploy to GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #4.4.0
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #6.3.0
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
cache: npm
|
||||
|
||||
- name: Install app dev dependencies
|
||||
@@ -28,13 +28,11 @@ jobs:
|
||||
- name: Install docusaurus dependencies
|
||||
run: npm ci
|
||||
working-directory: ./docsite
|
||||
|
||||
- name: Build website
|
||||
env:
|
||||
ANALYTICS: ${{ vars.ANALYTICS }}
|
||||
ANALYTICS_DOMAIN: ${{ vars.ANALYTICS_DOMAIN }}
|
||||
RY_ANALYTICS: ${{ vars.RY_ANALYTICS }}
|
||||
RY_ANALYTICS_SITEID: ${{ vars.RY_ANALYTICS_SITEID }}
|
||||
RY_ANALYTICS_REPLAY: ${{ vars.RY_ANALYTICS_REPLAY }}
|
||||
GSITEVERIFICATION: ${{ vars.GSITEVERIFICATION }}
|
||||
DOCS_BASE: '/multi-scrobbler'
|
||||
run: npm run build
|
||||
@@ -43,7 +41,7 @@ jobs:
|
||||
# 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@373f7f263a76c20808c831209c920827a82a2847 #3.9.3
|
||||
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e #4.0.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Build output to publish to the `gh-pages` branch:
|
||||
@@ -55,3 +53,18 @@ jobs:
|
||||
# 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
|
||||
|
||||
- name: Trigger dependent build
|
||||
if: ${{ vars.DOCS_REPO != ''}}
|
||||
run: |
|
||||
# Trigger builds in dependent repositories
|
||||
curl -X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.DOCS_DISPATCH_TOKEN }}" \
|
||||
"https://api.github.com/repos/${{ vars.DOCS_REPO }}/dispatches" \
|
||||
-d "{
|
||||
\"event_type\": \"release\",
|
||||
\"client_payload\": {
|
||||
\"commit\": \"${{ github.sha }}\"
|
||||
}
|
||||
}"
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -19,12 +19,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 #v4.3.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #4.4.0
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #6.3.0
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
@@ -134,6 +134,7 @@ src/backend/common/schema/*.json
|
||||
!src/backend/common/schema/aio.json
|
||||
!src/backend/common/schema/client.json
|
||||
!src/backend/common/schema/source.json
|
||||
docsite/static/schemas/*.json
|
||||
|
||||
|
||||
*.bak
|
||||
@@ -147,3 +148,7 @@ build
|
||||
**/generated-sources.*
|
||||
.github/act/out
|
||||
tmp-*
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
lib
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"extends": "@istanbuljs/nyc-config-typescript",
|
||||
"exclude": [
|
||||
"node_modules/",
|
||||
"**/src/backend/common/schema/**",
|
||||
"**/src/backend/tests/**",
|
||||
"register.js",
|
||||
"**/src/server/**/*.d.ts",
|
||||
"**/src/client/**"
|
||||
],
|
||||
"include": [
|
||||
"**/src/backend/**/*.ts",
|
||||
"**/src/**/backend/*.js",
|
||||
"**/src/**/backend/*.js.map"
|
||||
],
|
||||
"extension": [
|
||||
".ts"
|
||||
],
|
||||
"reporter": [
|
||||
"text-summary",
|
||||
"html"
|
||||
],
|
||||
"report-dir": "./coverage"
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { defineMain } from "@storybook/react-vite/node";
|
||||
import { mergeConfig } from "vite";
|
||||
|
||||
|
||||
export default defineMain({
|
||||
"stories": [
|
||||
"../src/**/*.mdx",
|
||||
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
|
||||
],
|
||||
"addons": [
|
||||
"@chromatic-com/storybook",
|
||||
// "@storybook/addon-vitest",
|
||||
"@storybook/addon-a11y",
|
||||
"@storybook/addon-docs",
|
||||
// "@storybook/addon-onboarding"
|
||||
],
|
||||
features: {
|
||||
// https://github.com/storybookjs/storybook/discussions/33279
|
||||
sidebarOnboardingChecklist: false,
|
||||
},
|
||||
"framework": "@storybook/react-vite",
|
||||
viteFinal: async (config) => {
|
||||
return mergeConfig(config, {
|
||||
optimizeDeps: {
|
||||
include: ["@storybook/addon-docs"],
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: "fix-mdx-react-shim",
|
||||
enforce: "pre",
|
||||
resolveId(source: any) {
|
||||
if (
|
||||
source.startsWith("file://") &&
|
||||
source.includes("mdx-react-shim.js")
|
||||
) {
|
||||
// Convert file:///... path to normal filesystem path for Vite
|
||||
return new URL(source).pathname;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { addons } from 'storybook/manager-api';
|
||||
import { themes } from 'storybook/theming';
|
||||
|
||||
addons.setConfig({
|
||||
theme: themes.dark,
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import addonDocs from "@storybook/addon-docs";
|
||||
import addonA11y from "@storybook/addon-a11y";
|
||||
import { definePreview } from "@storybook/react-vite";
|
||||
|
||||
import { themes } from 'storybook/theming';
|
||||
|
||||
export default definePreview({
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
|
||||
a11y: {
|
||||
// 'todo' - show a11y violations in the test UI only
|
||||
// 'error' - fail CI on a11y violations
|
||||
// 'off' - skip a11y checks entirely
|
||||
test: 'todo'
|
||||
},
|
||||
docs: {
|
||||
theme: themes.dark,
|
||||
},
|
||||
},
|
||||
|
||||
addons: [addonA11y(), addonDocs()]
|
||||
});
|
||||
Vendored
+2
-1
@@ -4,7 +4,8 @@
|
||||
"mochaExplorer.exit": true,
|
||||
"search.exclude": {
|
||||
"**/.docusaurus": true,
|
||||
"**/build": true
|
||||
"**/build": true,
|
||||
"**/schema": true
|
||||
},
|
||||
"tailwindCSS.files.exclude": [
|
||||
"docsite/**"
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
coc@foxxmd.dev.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
+3
-1
@@ -1,7 +1,7 @@
|
||||
FROM ghcr.io/linuxserver/baseimage-debian:bookworm AS base
|
||||
|
||||
ENV TZ=Etc/GMT
|
||||
ENV NODE_VERSION=20.18.1
|
||||
ENV NODE_VERSION=24.14.0
|
||||
|
||||
# borrowing openssl header removal trick from offical docker-node
|
||||
# https://github.com/nodejs/docker-node/blob/main/18/bookworm-slim/Dockerfile#L8
|
||||
@@ -49,6 +49,8 @@ RUN \
|
||||
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
RUN echo "Node: $(node -v)\nNPM: $(npm -v)"
|
||||
|
||||
RUN npm install -g concurrently
|
||||
|
||||
ARG data_dir=/config
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
[](https://github.com/FoxxMD/multi-scrobbler/releases)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://hub.docker.com/r/foxxmd/multi-scrobbler)
|
||||
[](https://foxxmd.github.io/multi-scrobbler/)
|
||||
[](https://docs.multi-scrobbler.app/)
|
||||
|
||||
|
||||
<img src="/assets/icon.png" align="right"
|
||||
@@ -11,54 +11,56 @@ alt="multi-scrobbler logo" width="180" height="180">
|
||||
|
||||
A dockerized app that monitors your music listening activity from *everywhere* and scrobbles it *anywhere*.
|
||||
|
||||
* Supports scrobbling from many [**Sources**](https://foxxmd.github.io/multi-scrobbler/configuration/sources)
|
||||
* [Azuracast](https://foxxmd.github.io/multi-scrobbler/configuration/sources/azuracast)
|
||||
* [Deezer](https://foxxmd.github.io/multi-scrobbler/configuration/sources/deezer)
|
||||
* [Google Cast (Chromecast)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/google-cast)
|
||||
* [Icecast](https://foxxmd.github.io/multi-scrobbler/configuration/sources/icecast)
|
||||
* [Jellyfin](https://foxxmd.github.io/multi-scrobbler/configuration/sources/jellyfin)
|
||||
* [JRiver](https://foxxmd.github.io/multi-scrobbler/configuration/sources/jriver)
|
||||
* [Kodi](https://foxxmd.github.io/multi-scrobbler/configuration/sources/kodi)
|
||||
* [Koito](https://foxxmd.github.io/multi-scrobbler/configuration/sources/koito-source)
|
||||
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-source)
|
||||
* [Last.fm (Endpoint)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-endpoint)
|
||||
* [Libre.fm](https://foxxmd.github.io/multi-scrobbler/configuration/sources/librefm-source)
|
||||
* [ListenBrainz](https://foxxmd.github.io/multi-scrobbler/configuration/sources/listenbrainz-source)
|
||||
* [ListenBrainz (Endpoint)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/listenbrainz-endpoint)
|
||||
* [Maloja](https://foxxmd.github.io/multi-scrobbler/configuration/sources/maloja)
|
||||
* [Mopidy](https://foxxmd.github.io/multi-scrobbler/configuration/sources/mopidy)
|
||||
* [MPD (Music Player Daemon)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/mpd)
|
||||
* [MPRIS (Linux Desktop)](https://foxxmd.github.io/multi-scrobbler/configuration/sources/mpris)
|
||||
* [Musikcube](https://foxxmd.github.io/multi-scrobbler/configuration/sources/musikcube)
|
||||
* [Plex](https://foxxmd.github.io/multi-scrobbler/configuration/sources/plex)
|
||||
* [Rocksky](https://foxxmd.github.io/multi-scrobbler/configuration/sources/rocksky-source)
|
||||
* [Sonos](https://foxxmd.github.io/multi-scrobbler/configuration/sources/sonos)
|
||||
* [Spotify](https://foxxmd.github.io/multi-scrobbler/configuration/sources/spotify)
|
||||
* [Subsonic-compatible APIs](https://foxxmd.github.io/multi-scrobbler/configuration/sources/subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
|
||||
* [teal.fm](https://foxxmd.github.io/multi-scrobbler/configuration/sources/tealfm-source)
|
||||
* [WebScrobbler](https://foxxmd.github.io/multi-scrobbler/configuration/sources/webscrobbler)
|
||||
* [VLC](https://foxxmd.github.io/multi-scrobbler/configuration/sources/vlc)
|
||||
* [Yamaha MusicCast](https://foxxmd.github.io/multi-scrobbler/configuration/sources/yamaha-musiccast)
|
||||
* [Youtube Music](https://foxxmd.github.io/multi-scrobbler/configuration/sources/youtube-music)
|
||||
* Supports scrobbling to many [**Clients**](https://foxxmd.github.io/multi-scrobbler/configuration/clients)
|
||||
* [Koito](https://foxxmd.github.io/multi-scrobbler/configuration/clients/koito)
|
||||
* [Last.fm](https://foxxmd.github.io/multi-scrobbler/configuration/clients/lastfm)
|
||||
* [Libre.fm](https://foxxmd.github.io/multi-scrobbler/configuration/clients/librefm)
|
||||
* [ListenBrainz](https://foxxmd.github.io/multi-scrobbler/configuration/clients/listenbrainz)
|
||||
* [Maloja](https://foxxmd.github.io/multi-scrobbler/configuration/clients/maloja)
|
||||
* [Rocksky](https://foxxmd.github.io/multi-scrobbler/configuration/clients/rocksky)
|
||||
* [teal.fm](https://foxxmd.github.io/multi-scrobbler/configuration/clients/tealfm)
|
||||
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](https://foxxmd.github.io/multi-scrobbler/configuration#webhook-configurations), [healthcheck endpoints](https://foxxmd.github.io/multi-scrobbler/configuration#health-endpoint), or [prometheus metrics](https://foxxmd.github.io/multi-scrobbler/configuration/#prometheus).
|
||||
* Supports [Now Playing](https://foxxmd.github.io/multi-scrobbler/configuration/clients#now-playing) for scrobble Clients
|
||||
* Supports scrobbling from many [**Sources**](https://docs.multi-scrobbler.app/configuration/sources)
|
||||
* [Azuracast](https://docs.multi-scrobbler.app/configuration/sources/azuracast)
|
||||
* [Deezer](https://docs.multi-scrobbler.app/configuration/sources/deezer)
|
||||
* [Google Cast (Chromecast)](https://docs.multi-scrobbler.app/configuration/sources/google-cast)
|
||||
* [Icecast](https://docs.multi-scrobbler.app/configuration/sources/icecast)
|
||||
* [Jellyfin](https://docs.multi-scrobbler.app/configuration/sources/jellyfin)
|
||||
* [JRiver](https://docs.multi-scrobbler.app/configuration/sources/jriver)
|
||||
* [Kodi](https://docs.multi-scrobbler.app/configuration/sources/kodi)
|
||||
* [Koito](https://docs.multi-scrobbler.app/configuration/sources/koito-source)
|
||||
* [Last.fm](https://docs.multi-scrobbler.app/configuration/sources/lastfm-source)
|
||||
* [Last.fm (Endpoint)](https://docs.multi-scrobbler.app/configuration/sources/lastfm-endpoint)
|
||||
* [Libre.fm](https://docs.multi-scrobbler.app/configuration/sources/librefm-source)
|
||||
* [ListenBrainz](https://docs.multi-scrobbler.app/configuration/sources/listenbrainz-source)
|
||||
* [ListenBrainz (Endpoint)](https://docs.multi-scrobbler.app/configuration/sources/listenbrainz-endpoint)
|
||||
* [Maloja](https://docs.multi-scrobbler.app/configuration/sources/maloja)
|
||||
* [Mopidy](https://docs.multi-scrobbler.app/configuration/sources/mopidy)
|
||||
* [MPD (Music Player Daemon)](https://docs.multi-scrobbler.app/configuration/sources/mpd)
|
||||
* [MPRIS (Linux Desktop)](https://docs.multi-scrobbler.app/configuration/sources/mpris)
|
||||
* [Musikcube](https://docs.multi-scrobbler.app/configuration/sources/musikcube)
|
||||
* [Music Assistant](https://docs.multi-scrobbler.app/configuration/sources/listenbrainz-endpoint#music-assistant)
|
||||
* [Plex](https://docs.multi-scrobbler.app/configuration/sources/plex)
|
||||
* [Rocksky](https://docs.multi-scrobbler.app/configuration/sources/rocksky-source)
|
||||
* [Sonos](https://docs.multi-scrobbler.app/configuration/sources/sonos)
|
||||
* [Spotify](https://docs.multi-scrobbler.app/configuration/sources/spotify)
|
||||
* [Subsonic-compatible APIs](https://docs.multi-scrobbler.app/configuration/sources/subsonic) (like [Airsonic](https://airsonic.github.io/) and [Navidrome](https://www.navidrome.org/))
|
||||
* [teal.fm](https://docs.multi-scrobbler.app/configuration/sources/tealfm-source)
|
||||
* [WebScrobbler](https://docs.multi-scrobbler.app/configuration/sources/webscrobbler)
|
||||
* [VLC](https://docs.multi-scrobbler.app/configuration/sources/vlc)
|
||||
* [Yamaha MusicCast](https://docs.multi-scrobbler.app/configuration/sources/yamaha-musiccast)
|
||||
* [Youtube Music](https://docs.multi-scrobbler.app/configuration/sources/youtube-music)
|
||||
* Supports scrobbling to many [**Clients**](https://docs.multi-scrobbler.app/configuration/clients)
|
||||
* [Discord](https://docs.multi-scrobbler.app/configuration/clients/discord) (Now Playing)
|
||||
* [Koito](https://docs.multi-scrobbler.app/configuration/clients/koito)
|
||||
* [Last.fm](https://docs.multi-scrobbler.app/configuration/clients/lastfm)
|
||||
* [Libre.fm](https://docs.multi-scrobbler.app/configuration/clients/librefm)
|
||||
* [ListenBrainz](https://docs.multi-scrobbler.app/configuration/clients/listenbrainz)
|
||||
* [Maloja](https://docs.multi-scrobbler.app/configuration/clients/maloja)
|
||||
* [Rocksky](https://docs.multi-scrobbler.app/configuration/clients/rocksky)
|
||||
* [teal.fm](https://docs.multi-scrobbler.app/configuration/clients/tealfm)
|
||||
* Monitor status of Sources and Clients using [webhooks (Gotify, Ntfy, Apprise)](https://docs.multi-scrobbler.app/configuration#webhook-configurations), [healthcheck endpoints](https://docs.multi-scrobbler.app/configuration#health-endpoint), or [prometheus metrics](https://docs.multi-scrobbler.app/configuration/#prometheus).
|
||||
* Supports [Now Playing](https://docs.multi-scrobbler.app/configuration/clients#now-playing) for scrobble Clients
|
||||
* Supports configuring for single or multiple users (scrobbling for your friends and family!)
|
||||
* Web server interface for stats, basic control, and detailed logs
|
||||
* Graceful network and client failure handling (queued scrobbles that auto-retry)
|
||||
* Smart handling of credentials (persistent, authorization through app)
|
||||
* Easy configuration through [ENVs or JSON](ttps://foxxmd.github.io/multi-scrobbler/configuration#configuration-types)
|
||||
* Modify data before scrobbling with [regular expression or search patterns](https://foxxmd.github.io/multi-scrobbler/configuration/transforms)
|
||||
* Install using [Docker images for x86/ARM](https://foxxmd.github.io/multi-scrobbler/installation#docker) or [locally with NodeJS](https://foxxmd.github.io/multi-scrobbler/installation#nodejs)
|
||||
* Modify data before scrobbling with [regular expression or search patterns](https://docs.multi-scrobbler.app/configuration/transforms)
|
||||
* Install using [Docker images for x86/ARM](https://docs.multi-scrobbler.app/installation#docker) or [locally with NodeJS](https://docs.multi-scrobbler.app/installation#nodejs)
|
||||
|
||||
[**Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler//quickstart)
|
||||
[**Quick Start Guide**](https://docs.multi-scrobbler.app//quickstart)
|
||||
|
||||
<img src="/docsite/static/img/status-ui.png" width="800">
|
||||
|
||||
@@ -71,15 +73,15 @@ A dockerized app that monitors your music listening activity from *everywhere* a
|
||||
|
||||
**But I already scrobble my music to Last.fm/ListenBrainz, is multi-scrobbler for me?**
|
||||
|
||||
Yes! You can use [Last.fm as a **Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources/lastfm-source) or [Listenbrainz as a **Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources/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**](https://docs.multi-scrobbler.app/configuration/sources/lastfm-source) or [Listenbrainz as a **Source**](https://docs.multi-scrobbler.app/configuration/sources/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.
|
||||
|
||||
## How Does multi-scrobbler (MS) Work?
|
||||
|
||||
You set up [configurations](https://foxxmd.github.io/multi-scrobbler/configuration) for one or more [**Sources**](https://foxxmd.github.io/multi-scrobbler/configuration/sources) and one or more [**Clients**](https://foxxmd.github.io/multi-scrobbler/configuration/clients). MS monitors all of your configured **Sources**. When new tracks are played by a Source MS grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to forward to.
|
||||
You set up [configurations](https://docs.multi-scrobbler.app/configuration) for one or more [**Sources**](https://docs.multi-scrobbler.app/configuration/sources) and one or more [**Clients**](https://docs.multi-scrobbler.app/configuration/clients). MS monitors all of your configured **Sources**. When new tracks are played by a Source MS grabs that information and then sends it (scrobbles it) to all **Clients** that Source is configured to forward to.
|
||||
|
||||
### Source
|
||||
|
||||
A [**Source**](https://foxxmd.github.io/multi-scrobbler/configuration/sources) is a data source that contains information about music you are playing or have listened to, such as: a desktop player, web music player, or cloud music service. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Navidrome**, etc...
|
||||
A [**Source**](https://docs.multi-scrobbler.app/configuration/sources) is a data source that contains information about music you are playing or have listened to, such as: a desktop player, web music player, or cloud music service. Examples are **Spotify, Jellyfin, Plex, Youtube Music, Navidrome**, etc...
|
||||
|
||||
Source configurations consist of:
|
||||
|
||||
@@ -89,7 +91,7 @@ Source configurations consist of:
|
||||
|
||||
### Client
|
||||
|
||||
A [**Client**](https://foxxmd.github.io/multi-scrobbler/configuration/clients) is an application that stores the historical information about what music you have played (scrobbles). Examples are **Koito, Last.fm, Listenbrainz**...
|
||||
A [**Client**](https://docs.multi-scrobbler.app/configuration/clients) is an application that stores the historical information about what music you have played (scrobbles). Examples are **Koito, Last.fm, Listenbrainz**...
|
||||
|
||||
Client configurations consist of:
|
||||
|
||||
@@ -98,15 +100,15 @@ Client configurations consist of:
|
||||
|
||||
## Quick Start
|
||||
|
||||
[See the **Quick Start Guide**](https://foxxmd.github.io/multi-scrobbler/quickstart)
|
||||
[See the **Quick Start Guide**](https://docs.multi-scrobbler.app/quickstart)
|
||||
|
||||
## Installation
|
||||
|
||||
[See the **Installation** documentation](https://foxxmd.github.io/multi-scrobbler/installation)
|
||||
[See the **Installation** documentation](https://docs.multi-scrobbler.app/installation)
|
||||
|
||||
## Configuration
|
||||
|
||||
[See the **Configuration** documentation](https://foxxmd.github.io/multi-scrobbler/configuration)
|
||||
[See the **Configuration** documentation](https://docs.multi-scrobbler.app/configuration)
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -121,11 +123,11 @@ 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](https://foxxmd.github.io/multi-scrobbler/FAQ) before creating an issue!
|
||||
Having issues with connections or configuration? Check the [FAQ](https://docs.multi-scrobbler.app/FAQ) before creating an issue!
|
||||
|
||||
## Development
|
||||
|
||||
[Detailed architecture and development guides for Sources/Clients](https://foxxmd.github.io/multi-scrobbler/development/dev-common)
|
||||
[Detailed architecture and development guides for Sources/Clients](https://docs.multi-scrobbler.app/development/dev-common)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"librariesBlock": ["BadMusic"],
|
||||
"additionalAllowedLibraryTypes": ["musicvideos"],
|
||||
"allowMediaTypes": ["audio"],
|
||||
"frontendUrlOverride": "https://myJellyfinExternal.tld"
|
||||
},
|
||||
"options": {
|
||||
"logPayload": true,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"name": "Yandex Music",
|
||||
"type": "ymbridge",
|
||||
"data": {
|
||||
"url": "http://192.168.1.161:9980",
|
||||
"apiKey": "change-me",
|
||||
"interval": 5
|
||||
}
|
||||
}
|
||||
]
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
---
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
title: FAQ
|
||||
---
|
||||
|
||||
## Connection Issues
|
||||
@@ -85,7 +86,7 @@ Error: Could not send the specified request to browse. Status code: 401
|
||||
|
||||
then YTM has invalidated your authentication.
|
||||
|
||||
First, ensure you are NOT using [YoutubeTV authentication.](/configuration/sources/youtube-music?ytmAuth=ytt#authentication) If you completed authentication by entering a "User Code" you are using YoutubeTV which has stopped working. You should reauthenticate using **Cookies** or **Custom OAuth.**
|
||||
First, ensure you are NOT using [YoutubeTV authentication.](/configuration/sources/youtube-music?ytmAuth=ytt#ytm-auth) If you completed authentication by entering a "User Code" you are using YoutubeTV which has stopped working. You should reauthenticate using **Cookies** or **Custom OAuth.**
|
||||
|
||||
#### When using OAuth Client Authentication
|
||||
|
||||
@@ -95,7 +96,7 @@ Refresh your authentication by using the **(Re)authenticate** link from MS's web
|
||||
|
||||
The library MS uses relies on scraping the YTM site by using cookies from your actual browser to pretend it is a browser. It does its best to keep these up to date but since this is not an official way to access the service YTM may invalidate your access _to the authenticated session_ at any time. How this is triggered is unknown and not something multi-scrobbler can control. You can help limit the chance of your session being invalidated by [getting the cookie from an Incognito/Private Session](https://github.com/LuanRT/YouTube.js/issues/803#issuecomment-2504032666) and then immediately closing the browser afterwards.
|
||||
|
||||
To re-authenticate MS [follow the YTM instructions to retrieve a new set of cookies for multi-scrobbler](/configuration/sources/youtube-music?ytmAuth=cookie#authentication) and then restart MS to potentially resolve the problem.
|
||||
To re-authenticate MS [follow the YTM instructions to retrieve a new set of cookies for multi-scrobbler](/configuration/sources/youtube-music?ytmAuth=cookie#ytm-auth) and then restart MS to potentially resolve the problem.
|
||||
|
||||
## Configuration Issues
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Overview
|
||||
description: Scrobble Clients
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
@@ -23,6 +24,71 @@ A **Client** is an application that stores the historical information about what
|
||||
|
||||
## Features
|
||||
|
||||
### Duplicate Detection
|
||||
|
||||
Before a Play is scrobbled to a client MS checks existing scrobbles from the Client's API to see if the Play has already been scrobbled.
|
||||
|
||||
For each Play, MS fetches (cached) scrobbles from the Client in a time range inclusive of the Play's listening timestamp and then scores existing scrobbles against the Play based on:
|
||||
|
||||
* Similarity of Title, Artists, and Album
|
||||
* Temporal closeness of the Play's timestamp to the existing scrobble's timestamp
|
||||
* Whether MS detected the Play as a repeat (applicable to Sources that report realtime Player data)
|
||||
|
||||
Detailed scoring breakdowns against each existing scrobble are logged at the `TRACE` level.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Detailed Explanation</summary>
|
||||
|
||||
A **candidate** (to be scrobbled) Play is first transformed using the configured [`compare.candidate` Hook](/configuration/transforms/#hook), if any exists.
|
||||
|
||||
Next, MS checks (up to) the last 100 scrobbles *in-memory* scrobbles that it has made. These are *not* from the Client but the actual scrobbles MS made while it has been running. The data from these scrobbles is much richer than what is usually parsed from the Client which makes it easier to detect duplicates from.
|
||||
|
||||
If no in-memory scrobble matches then MS starts comparing the candidate against historical scrobbles fetched from an inclusive time range of the candidate's timestamp.
|
||||
|
||||
<h3>Matching Title/Artist/Album</h3>
|
||||
|
||||
For these string-based values MS uses an [token-order-invariant](https://github.com/FoxxMD/multi-scrobbler/blob/0dfa4c7aad6df98e13aee6d395827665c2414adb/src/backend/utils/StringUtils.ts#L299) method that scores string similarity based on a (token count) weighted average of two [similarity](https://foxxmd.github.io/string-sameness/#md:strategies) algorithms, [Levenshtien Distance](https://en.wikipedia.org/wiki/Levenshtein_distance) and [Dice's Coefficient](https://en.wikipedia.org/wiki/S%C3%B8rensen%E2%80%93Dice_coefficient). The weighted average ensures that very long strings (Track titles) are scored with a confidence proportional to their length.
|
||||
|
||||
<h3>Matching Timestamp</h3>
|
||||
|
||||
Timestamps are scored on how temporally close they are. There are four possible scores with decreasing value:
|
||||
|
||||
* **Exact** - Timestamps are within 1 second of each other
|
||||
* **Close** - Timestamps are within `threshold` seconds of each other
|
||||
* This is determined by the smallest update interval of Source
|
||||
* Some Sources (subsonic) only update every 60 seconds so this is the smallest "close" value possible. Most are 10 seconds. Signified by `(Needed <10s)` in the breakdown example below.
|
||||
* **Fuzzy** - One timestamp is within `threshold` seconds of the *end* of the other timestamp
|
||||
* Sources can set the scrobble timestamp at different times. Some do it when the track is started listening to, some when it ends, some when the *player* stops.
|
||||
* Where possible, MS knows and keeps track of when this timestamp *should* be, for each Source. If it's not possible then Fuzzy may be allowed.
|
||||
* **None** - There is no correlation between timestamps
|
||||
|
||||
<h3>Scoring and Breakdows</h3>
|
||||
|
||||
A candidate Play must score >= 1 to be detected as a duplicate of an existing scrobble.
|
||||
|
||||
Each score and a breakdown of the scores for its individual components can be see at the `TRACE` logging level or in the [debug data](/help#copy-play-debug-data) for a scrobble. An example:
|
||||
|
||||
```
|
||||
* Artist: 0.06 * 0.3 = 0.02
|
||||
* Title: 0.04 * 0.4 = 0.02
|
||||
* Time: (Exact) 1 * 0.5 = 0.50
|
||||
* Existing: 19:13:33-04:00 - Candidate: 19:13:33-04:00
|
||||
* Temporal Sameness: Exact
|
||||
* Play Diff: 0s (Needed <10s)
|
||||
* Range Comparison N/A
|
||||
Score 0.54 => No Match
|
||||
```
|
||||
|
||||
In each component equation, the first number is the similarity (or temporal closeness).
|
||||
|
||||
* 0 = no correlation
|
||||
* 1 = exactly the same
|
||||
|
||||
The second number is the *weight* of that component in the final score.
|
||||
|
||||
</details>
|
||||
|
||||
### Dead Scrobbles
|
||||
|
||||
If multi-scrobbler is unable to submit a scrobble to a Client then it places the scrobble into a queue which is retried every 5 minutes for a number of times before it gives up.
|
||||
|
||||
@@ -47,7 +47,7 @@ This integration uses an existing, running Discord application to communicate wi
|
||||
|
||||
<img src={require('/img/discord_kasm.png').default} height="400"/>
|
||||
|
||||
This containerized approach can be used instead of configuring MS to use a [Native Discord](./?discordType=native#dicord-connection) application on a specific machine.
|
||||
This containerized approach can be used instead of configuring MS to use a [Native Discord](./?discordType=native#discord-connection) application on a specific machine.
|
||||
|
||||
**Advantages**
|
||||
|
||||
@@ -434,7 +434,7 @@ After [finding your IPC connection path](#find-ipc-connection) you will need to
|
||||
|
||||
:::note
|
||||
|
||||
Due to the limitations of the Discord local API the other listening activity detection and online status customizations found in [Headless](/configuration/discord/discord-headless#optional-setup) are not available for the Local integration.
|
||||
Due to the limitations of the Discord local API the other listening activity detection and online status customizations found in [Headless](/configuration/clients/discord/discord-headless#optional-setup) are not available for the Local integration.
|
||||
|
||||
:::
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Discord
|
||||
description: Discord Client Overview
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ You will need a [Last.fm](https://www.last.fm) account to use the Last.fm API.
|
||||
|
||||
After creating your account, [register for an API account here.](https://www.last.fm/api/account/create)
|
||||
|
||||
The callback URL in the API account form is not used, but to keep it consistent use the same value you set using the [Callback/Redirect URL](#callback-redirect-url) section.
|
||||
The callback URL in the API account form is not used, but to keep it consistent use the same value you set using the [Callback/Redirect URL](#callbackredirect-url) section.
|
||||
|
||||
**Take note of your API Key and Secret after account creation.**
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ On your [profile page](https://listenbrainz.org/profile/) find your **User Token
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="ListenbrainzClientConfig" fileContent={JsonConfig} client name="listenbrainz">
|
||||
<Config config="ListenBrainzClientConfig" fileContent={JsonConfig} client name="listenbrainz">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| ---------------------- | --------- | ----------------------------- | ------------------------------- |
|
||||
| `LZ_TOKEN` | Yes | | User token from your LZ profile |
|
||||
|
||||
@@ -64,7 +64,7 @@ The handle used with multi-scrobbler should be your **full** ATProto handle, inc
|
||||
|
||||
If you do not include a TLD then multi-scrobbler will automatically append `.bsky.social` to your handle value.
|
||||
|
||||
<Config config="RockskyClientConfig" fileContent={JsonConfig} client name="rocksky">
|
||||
<Config config="RockSkyClientConfig" fileContent={JsonConfig} client name="rocksky">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | ------- | ----------------------------------------------------------------------------------- |
|
||||
| `ROCKSKY_HANDLE` | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` |
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Overview
|
||||
description: Configuration
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
import Tabs from '@theme/Tabs';
|
||||
@@ -168,7 +169,7 @@ Each entry for a Source/Client includes a **Configuration** section that describ
|
||||
|
||||
## Secrets Interpolation
|
||||
|
||||
When using [File](./?configType=file#configuration-types) or [AIO](./?configType=aio#configuration-types) Configuration, Multi-Scrobbler can interpolate Environmental Variables into your json files. This can be used, for example, to keep sensitive data (like [Last.fm Client/Secret](/configurations/clients/lastfm#configuration)) out of your configuration files so that they can be committed to git.
|
||||
When using [File](./?configType=file#configuration-types) or [AIO](./?configType=aio#configuration-types) Configuration, Multi-Scrobbler can interpolate Environmental Variables into your json files. This can be used, for example, to keep sensitive data (like [Last.fm Client/Secret](/configuration/clients/lastfm#configuration)) out of your configuration files so that they can be committed to git.
|
||||
|
||||
Multi-scrobbler will look for patterns in text fields within _all_ of your json files:
|
||||
|
||||
@@ -179,7 +180,7 @@ Multi-scrobbler will look for patterns in text fields within _all_ of your json
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
Given this [Last.fm](/configurations/clients/lastfm#configuration) [File](./?configType=file#configuration-types) config:
|
||||
Given this [Last.fm](/configuration/clients/lastfm#configuration) [File](./?configType=file#configuration-types) config:
|
||||
|
||||
```json title="lastfm.json"
|
||||
[
|
||||
@@ -294,7 +295,7 @@ All of the activities below are **always** cached **in-memory** with an optional
|
||||
|
||||
By default, this data use a [Secondary](#secondary-caching-configuration) [File](./?cacheType=file#secondary-caching-configuration) store, configured for you automatically.
|
||||
|
||||
If you have configured a [persisted volume/bind mount](/installation#storage) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)) then you are already done. If you are not persisting this directory then you should consider setting up [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration) for this.
|
||||
If you have configured a [persisted volume/bind mount](/installation?dockerSetting=storage#recommended-settings) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)) then you are already done. If you are not persisting this directory then you should consider setting up [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration) for this.
|
||||
|
||||
:::
|
||||
|
||||
@@ -337,7 +338,7 @@ Authentication sessions/tokens/etc... are cached for quicker requests and for pe
|
||||
|
||||
By default, this data use a [Secondary](#secondary-caching-configuration) [File](./?cacheType=file#secondary-caching-configuration) store, configured for you automatically.
|
||||
|
||||
If you have configured a [persisted volume/bind mount](/installation#storage) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)) then you are already done. If you are not persisting this directory then you should consider setting up [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration) for this.
|
||||
If you have configured a [persisted volume/bind mount](/installation?dockerSetting=storage#recommended-settings) for configuration (`/config` is mounted in [docker compose](/quickstart#create-docker-compose-file)) then you are already done. If you are not persisting this directory then you should consider setting up [Valkey Cache](./?cacheType=valkey#secondary-caching-configuration) for this.
|
||||
|
||||
:::
|
||||
|
||||
@@ -555,7 +556,7 @@ If you do not need the dashboard and/or ingress sources, or have security concer
|
||||
|
||||
:::warning
|
||||
|
||||
Any **[ingress-based sources](/configuration/sources#ingress) will be unusable** (Webscrobbler, etc...) if this is disabled.
|
||||
Any **[ingress-based sources](/configuration/sources?sourceComm=ingress#by-communication-method) will be unusable** (Webscrobbler, etc...) if this is disabled.
|
||||
|
||||
:::
|
||||
|
||||
@@ -725,5 +726,37 @@ It includes metrics for:
|
||||
* Count of Queued/Scrobbled/Deadletter scrobbles per [Client](/configuration/clients)
|
||||
* Number of issues per Source/Client
|
||||
* If any of these metrics is > 0 it means your Source/Client is not operating normally
|
||||
* Caching metrics
|
||||
|
||||
Additionally, general process metrics like cpu and memory usage can be enabled with the env `PROMETHEUS_FULL=true`
|
||||
Additionally, general process metrics like cpu and memory usage can be enabled with the env `PROMETHEUS_FULL=true`
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example Usage</summary>
|
||||
|
||||
Update your [Prometheus Configuration file](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#configuration-file) (usually at `/etc/prometheus.yml`) to add a job to the `scrape_configs`[section](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config) like this:
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
#
|
||||
# ...your other jobs are here
|
||||
#
|
||||
- job_name: multi-scrobbler
|
||||
scrape_interval: 30s
|
||||
metrics_path: '/api/metrics'
|
||||
static_configs:
|
||||
- targets: ['myMultiscrobblerIp:9078']
|
||||
```
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
<DetailsAdmo status="tip" summary="Grafana Dashboard">
|
||||
|
||||
Github user [4rft5](https://github.com/FoxxMD/multi-scrobbler/issues/507#issuecomment-4144982295) created a Grafana dashboard that uses the Prometheus metrics from above.
|
||||
|
||||
<img src={require('/img/grafana.png').default} height="800"/>
|
||||
|
||||
Download the json dashboard here: [`multiscrobbler-dashboard.json`](/multiscrobbler-dashboard.json)
|
||||
|
||||
</DetailsAdmo>
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
title: Kitchen Sink
|
||||
description: Kitchen Sink Example
|
||||
---
|
||||
|
||||
# Example Complex Scenario
|
||||
@@ -11,13 +12,13 @@ Scenario:
|
||||
* Each person has their own [Maloja](/configuration/clients/maloja) server
|
||||
* Each person has their own Spotify account
|
||||
* You have your own Airsonic ([subsonic](/configuration/sources/subsonic)) server you to scrobble from
|
||||
* You have your own [Youtube Music](/configuration/soures/youtube-mysic) account you want to scrobble from
|
||||
* You have your own [Youtube Music](/configuration/sources/youtube-music) account you want to scrobble from
|
||||
* Mary has her own [Last.fm](/configuration/clients/lastfm) account she also wants to scrobble to
|
||||
* Fred has his own [Spotify](/configuration/sources/spotify) application and provides you with just his access and refresh token because he doesn't trust you (wtf Fred)
|
||||
* Fred has a Plex server and wants to scrobble everything he plays
|
||||
* Mary uses Fred's Plex server but only wants to scrobble her plays from the `podcast` library
|
||||
* The three of you have a shared library on [Plex](/configuration/sources/plex) called `party` that you only play when you are hanging out. You want plays from that library to be scrobbled to everyone's servers.
|
||||
* Fred also has his own [Jellyfin server](/configurion/sources/jellyfin) and wants to scrobble everything he plays
|
||||
* Fred also has his own [Jellyfin server](/configuration/sources/jellyfin) and wants to scrobble everything he plays
|
||||
* You have an android music app that can scrobble to a [custom listenbrainz server](/configuration/sources/listenbrainz-endpoint)
|
||||
|
||||
### All-in-one Config
|
||||
|
||||
@@ -49,28 +49,11 @@ If neither is configured automatic scrobble defaults to **off** and you should e
|
||||
|
||||
## Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
<Config config="AzuracastSourceConfig" fileContent={AzuracastConfig} name="azuracast">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | :-------- | :------ | ---------------------------------------------------------------------------------------------- |
|
||||
| `AZ_URL` | Yes | | Azuracast URL *without station name* |
|
||||
| `AZ_STATION` | Yes | | The station name shown on the public page |
|
||||
| `AZURA_LIVE` | No | Yes | Only scrobble when station status is ONLINE |
|
||||
| `AZURA_LISTENERS_NUM` | No | `true` | Only scrobble if station has any listeners (`true`) or listeners are equal-to/greater-than `X` |
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/azuracast.json" language="json5">{AzuracastConfig}</CodeBlock>
|
||||
|
||||
|
||||
or <SchemaLink lower objectName="AzuracastSourceConfig"/>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
<AIOExample data={AzuracastConfig} name="azuracast"/>
|
||||
|
||||
|
||||
or <SchemaLink lower objectName="AzuracastSourceConfig"/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</Config>
|
||||
@@ -44,28 +44,6 @@ import DeezerDeprecatedConfig from '!!raw-loader!@site/../config/deezer-deprecat
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | ------- | ------------------------------------------------ |
|
||||
| `DEEZER_ARL` | Yes | | The ARL cookie value retrieved from your browser |
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/deezer.json" language="json5">{DeezerConfig}</CodeBlock>
|
||||
|
||||
or <SchemaLink lower objectName="DeezerSourceConfig"/>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
<AIOExample data={DeezerConfig} name="deezer"/>
|
||||
|
||||
or <SchemaLink lower objectName="DeezerSourceConfig"/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Duplicate detection
|
||||
|
||||
Third party integrations with Deezer (Sonos) may cause Deezer to [report the same track many times in listening history.](https://github.com/FoxxMD/multi-scrobbler/pull/296#issuecomment-2922374738) If you experience this issue modify your Deezer Source config (file-based only) to include the option `"fuzzyDiscoveryIgnore": "aggressive"`
|
||||
@@ -153,7 +131,13 @@ import DeezerDeprecatedConfig from '!!raw-loader!@site/../config/deezer-deprecat
|
||||
:::
|
||||
</details>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="DeezerCompatConfig" fileContent={DeezerConfig} name="deezer">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | --------- | ------- | ------------------------------------------------ |
|
||||
| `DEEZER_ARL` | Yes | | The ARL cookie value retrieved from your browser |
|
||||
</Config>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="official" label="Official API">
|
||||
|
||||
@@ -144,8 +144,7 @@ To diagnose bad/incomplete track information or strange MS player behavior pleas
|
||||
|
||||
## Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
<Config config="ChromecastSourceConfig" fileContent={ChromecastConfig} name="chromecast">
|
||||
:::note
|
||||
[Manually configuring cast device connections](#connecting-devices) is only available through file-based config.
|
||||
:::
|
||||
@@ -157,28 +156,4 @@ To diagnose bad/incomplete track information or strange MS player behavior pleas
|
||||
| CC_BLACKLIST_DEVICES | No | | Do not scrobble from these Cast devices. Comma-delimited list |
|
||||
| CC_WHITELIST_APPS | No | | Only scrobble from these casted Apps. Comma-delimited list. EX spotify, pandora |
|
||||
| CC_BLACKLIST_APPS | No | | Do not scrobble from these casted Apps. Comma-delimited list |
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/chromecast.json" language="json5">{ChromecastConfig}</CodeBlock>
|
||||
|
||||
</details>
|
||||
|
||||
or <SchemaLink lower objectName="ChromecastSourceConfig"/>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
<AIOExample data={ChromecastConfig} name="chromecast"/>
|
||||
|
||||
</details>
|
||||
|
||||
or <SchemaLink lower objectName="ChromecastSourceConfig"/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</Config>
|
||||
@@ -2,6 +2,7 @@
|
||||
title: Icecast
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
description: Icecast Source Configuration
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
@@ -62,32 +63,8 @@ If no `sources` are configured for multi-scrobbler it will automatically try to
|
||||
|
||||
## Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
<Config config="IcecastSourceConfig" fileContent={IcecastConfig} name="icecast">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | :-------- | :------ | ----------- |
|
||||
| `ICECAST_URL` | Yes | | Icecast URL |
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/icecast.json" language="json5">{IcecastConfig}</CodeBlock>
|
||||
|
||||
</details>
|
||||
|
||||
or <SchemaLink lower objectName="IcecastSourceConfig"/>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
<AIOExample data={IcecastConfig} name="icecast"/>
|
||||
|
||||
</details>
|
||||
|
||||
or <SchemaLink lower objectName="IcecastSourceConfig"/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</Config>
|
||||
@@ -35,36 +35,35 @@ By default...
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
<DetailsAdmo type="tip" summary="External Asset URL">
|
||||
|
||||
If you use MS and Jellyfin on an internal or otherwise inaccessible network but still want album art assets used in the UI or for [Discord](/configuration/clients/discord) to be accessible you can specify a URL to replace your internal url.
|
||||
|
||||
Use (file) `frontendUrlOverride` or (env) `JELLYFIN_FRONTEND_URL_OVERRIDE` to replace `url`/`JELLYFIN_URL`, respectively, when rendering links for the dashboard UI or other resources.
|
||||
|
||||
Example:
|
||||
|
||||
Your `JELLYFIN_URL` is `http://jellyfin:8096` but your Jellyfin instance is externally accessible (by MS or in general) at `https://jellyfin.mydomain.com`.
|
||||
|
||||
Use `JELLYFIN_FRONTEND_URL_OVERRIDE=https://jellyfin.mydomain.com` in order for the MS dashboard to use `https://jellyfin.mydomain.com/art/1234.jpg` when linking to album art.
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
#### Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| --------------------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `JELLYFIN_URL` | **Yes** | | The URL of the Jellyfin server IE `http://localhost:8096` |
|
||||
| `JELLYFIN_USER` | **Yes** | | The user to authenticate with the API |
|
||||
| `JELLYFIN_APIKEY` | No | | The API Key to use for authentication **(Must provide either apikey or password)** |
|
||||
| `JELLYFIN_PASSWORD` | No | | The password of the user to authenticate for. **(Must provide either apikey or password)** |
|
||||
| `JELLYFIN_USERS_ALLOW` | No | | Comma-separated list of usernames (from Jellyfin) to scrobble for |
|
||||
| `JELLYFIN_USERS_BLOCK` | No | | Comma-separated list of usernames (from Jellyfin) to disallow scrobble for |
|
||||
| `JELLYFIN_DEVICES_ALLOW` | No | | Comma-separated list of devices to scrobble from |
|
||||
| `JELLYFIN_DEVICES_BLOCK` | No | | Comma-separated list of devices to disallow scrobbles from |
|
||||
| `JELLYFIN_LIBRARIES_ALLOW` | No | | Comma-separated list of libraries to allow scrobbles from |
|
||||
| `JELLYFIN_LIBRARIES_BLOCK` | No | | Comma-separated list of libraries to disallow scrobbles from |
|
||||
| `JELLYFIN_MEDIATYPES_ALLOW` | No | | Comma-separated list of [media types](https://github.com/jellyfin/jellyfin-sdk-typescript/blob/master/src/generated-client/models/media-type.ts#L22) to allow scrobbling. Defaults to `audio` |
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
|
||||
<CodeBlock title="CONFIG_DIR/jellyfin.json" language="json5">{JellyfinConfig}</CodeBlock>
|
||||
|
||||
or <SchemaLink lower objectName="JellyApiSourceConfig"/>
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
|
||||
<AIOExample data={JellyfinConfig} name="jellyfin"/>
|
||||
|
||||
|
||||
or <SchemaLink lower objectName="JellyApiSourceConfig"/>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
<Config config="JellyApiSourceConfig" fileContent={JellyfinConfig} name="jellyfin">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| -------------------------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `JELLYFIN_URL` | **Yes** | | The URL of the Jellyfin server IE `http://localhost:8096` |
|
||||
| `JELLYFIN_USER` | **Yes** | | The user to authenticate with the API |
|
||||
| `JELLYFIN_APIKEY` | No | | The API Key to use for authentication **(Must provide either apikey or password)** |
|
||||
| `JELLYFIN_PASSWORD` | No | | The password of the user to authenticate for. **(Must provide either apikey or password)** |
|
||||
| `JELLYFIN_USERS_ALLOW` | No | | Comma-separated list of usernames (from Jellyfin) to scrobble for |
|
||||
| `JELLYFIN_USERS_BLOCK` | No | | Comma-separated list of usernames (from Jellyfin) to disallow scrobble for |
|
||||
| `JELLYFIN_DEVICES_ALLOW` | No | | Comma-separated list of devices to scrobble from |
|
||||
| `JELLYFIN_DEVICES_BLOCK` | No | | Comma-separated list of devices to disallow scrobbles from |
|
||||
| `JELLYFIN_LIBRARIES_ALLOW` | No | | Comma-separated list of libraries to allow scrobbles from |
|
||||
| `JELLYFIN_LIBRARIES_BLOCK` | No | | Comma-separated list of libraries to disallow scrobbles from |
|
||||
| `JELLYFIN_MEDIATYPES_ALLOW` | No | | Comma-separated list of [media types](https://github.com/jellyfin/jellyfin-sdk-typescript/blob/master/src/generated-client/models/media-type.ts#L22) to allow scrobbling. Defaults to `audio` |
|
||||
| `JELLYFIN_FRONTEND_URL_OVERRIDE` | No | | The external URL to use for album art assets instead of `JELLYFIN_URL` |
|
||||
</Config>
|
||||
@@ -49,28 +49,10 @@ If any part of this URL is missing multi-scrobbler will use a default value. Thi
|
||||
|
||||
## Configuration
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
<EnvType>
|
||||
<Config config="JRiverSourceConfig" fileContent={JriverConfig} name="jriver">
|
||||
| Environmental Variable | Required | Default | Description |
|
||||
|------------------------|----------|---------------------------------|------------------------------------------------|
|
||||
| JRIVER_URL | Yes | http://localhost:52199/MCWS/v1/ | The URL of the JRiver server |
|
||||
| JRIVER_USERNAME | No | | If authentication is enabled, the username set |
|
||||
| JRIVER_PASSWORD | No | | If authenticated is enabled, the password set |
|
||||
</EnvType>
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
<FileType>
|
||||
<CodeBlock title="CONFIG_DIR/jriver.json" language="json5">{JriverConfig}</CodeBlock>
|
||||
|
||||
or <SchemaLink lower objectName="JRiverSourceConfig"/>
|
||||
</FileType>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
<AIOType>
|
||||
<AIOExample data={JriverConfig} name="jriver"/>
|
||||
|
||||
or <SchemaLink lower objectName="JRiverSourceConfig"/>
|
||||
</AIOType>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</Config>
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Koito
|
||||
description: Koito (Source)
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Last.fm
|
||||
description: Last.fm (Source)
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Libre.fm
|
||||
description: Libre.fm (Source)
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
@@ -9,7 +9,9 @@ import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import ListenbrainzEndpointConfig from '!!raw-loader!@site/../config/endpointlz.json.example';
|
||||
|
||||
This Source enables multi-scrobbler to accept scrobbles from outside applications as if it was a Listenbrainz server.
|
||||
This Source enables multi-scrobbler to accept scrobbles from outside applications **as if it was a Listenbrainz server.**
|
||||
|
||||
If your service/player has a "Scrobble to Listenbrainz" feature and there is an option to set the Listenbrainz URL then multi-scrobbler can accept scrobbles from that service/player by imitating a real Listenbrainz server.
|
||||
|
||||
:::tip[Other Uses]
|
||||
|
||||
@@ -18,44 +20,91 @@ This Source enables multi-scrobbler to accept scrobbles from outside application
|
||||
|
||||
:::
|
||||
|
||||
## Setup
|
||||
|
||||
### URL
|
||||
|
||||
If a **slug** is **not** provided in configuration then multi-scrobbler will accept Listenbrainz scrobbles at
|
||||
In general, you should use one of the following as the **base URL** when configuring your application to scrobble to multi-scrobbler:
|
||||
|
||||
* `http://myMultiScrobblerIP:9078`
|
||||
* `http://myMultiScrobblerIP:9078/1/`
|
||||
|
||||
<DetailsAdmo type="tip" summary="Default Scrobble Submit URL">
|
||||
|
||||
If you are not using a [slug for multiple endpoints](#multiple-endpoints) then multi-scrobbler will accept Listenbrainz scrobbles at
|
||||
|
||||
```
|
||||
http://myMultiScrobblerIP:9078/1/submit-listens
|
||||
```
|
||||
|
||||
which is the "standard" Listenbrainz server path for scrobbling.
|
||||
which is the **standard** Listenbrainz server path for scrobbling.
|
||||
|
||||
In general, you should use `http://myMultiScrobblerIP:9078/1/` as the **base path** when configuring your application to scrobble to multi-scrobbler.
|
||||
</DetailsAdmo>
|
||||
|
||||
<details>
|
||||
<DetailsAdmo type="note" summary="Some Services Require HTTPS">
|
||||
|
||||
<summary>URL Setup for Popular Apps</summary>
|
||||
Some services that can scrobble to a custom Listenbrainz URL may require the URL to have a real domain (`example.com`) and/or use SSL (`https://`). In this case you should setup multi-scrobbler behind a reverse proxy to support this functionality. It is out of the scope of this project to do this solely within multi-scrobbler.
|
||||
|
||||
##### Navidrome
|
||||
|
||||
Set the [**ListenBrainz.BaseURL**](https://www.navidrome.org/docs/usage/configuration-options/#advanced-configuration) (env `ND_LISTENBRAINZ_BASEURL`) to `http://myMultiScrobblerIP:9078/1/`
|
||||
|
||||
</details>
|
||||
|
||||
Use a slug only if you need to setup multiple Listenbrainz Endpoint sources and cannot use different tokens.
|
||||
|
||||
If a slug is used then the URL will be:
|
||||
|
||||
```
|
||||
http://myMultiScrobblerIP:9078/api/listenbrainz/mySlug
|
||||
```
|
||||
|
||||
:::note
|
||||
Some Listenbrainz applications may require custom Listenbrainz URLs to be a real domain (`example.com`) and/or use SSL (`https://`). In this case you should setup multi-scrobbler behind a reverse proxy to support this functionality. It is out of the scope of this project to do this solely within multi-scrobbler.
|
||||
:::
|
||||
</DetailsAdmo>
|
||||
|
||||
### Token
|
||||
|
||||
Most Listenbrainz clients require a token (Authentication Token) to be provided during setup. This value can be anything you want, just make sure to use the same value for `token` in your multi-scrobbler configuration for the endpoint.
|
||||
|
||||
### Multiple Endpoints
|
||||
|
||||
If you are setting up multiple [Listenbrainz (Endpoint) Sources](/configuration/sources/listenbrainz-endpoint) MS can differentiate scrobbles based on [Token](#token) so that you can use the same endpoint for many users.
|
||||
|
||||
However, if you cannot configure multiple tokens for your scenario you may also use a URL **Slug** to tell MS which Source belongs to a specific user.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Using a Slug</summary>
|
||||
|
||||
When a **slug** is defined in ENV/File config MS will listen for listenbrainz routes using the slug as a the route path. Example:
|
||||
|
||||
```
|
||||
LZE_SLUG=foobar
|
||||
```
|
||||
|
||||
MS will accept calls to
|
||||
|
||||
```
|
||||
http://myMultiScrobblerIP:9078/api/listenbrainz/foobar
|
||||
```
|
||||
|
||||
for scrobble submission.
|
||||
|
||||
</details>
|
||||
|
||||
## Setup for Popular Apps
|
||||
|
||||
### Navidrome
|
||||
|
||||
<details>
|
||||
|
||||
Set the [**ListenBrainz.BaseURL**](https://www.navidrome.org/docs/usage/configuration-options/#advanced-configuration) (env `ND_LISTENBRAINZ_BASEURL`) to `http://myMultiScrobblerIP:9078/1/`
|
||||
|
||||
</details>
|
||||
|
||||
### Music Assistant
|
||||
|
||||
<details>
|
||||
|
||||
[Music Assistant Docs](https://www.music-assistant.io/plugins/listenbrainz_scrobble/)
|
||||
|
||||
* In your Music Assistant UI navigate to **Settings** -> **Plugins**
|
||||
* Click **Add a plugin**
|
||||
* Add **ListenBrainz Scrobbler**
|
||||
* In the ListenBrainz Scrobbler configuration screen:
|
||||
* User Token is the [Token](#token) you configured
|
||||
* Toggle **Show advanced settings**
|
||||
* Base URL is the [URL](#url) you can configured, or the equivalent of `http://myMultiScrobblerIP:9078` for your MS instance
|
||||
* Save the configuration and you are ready to scrobble
|
||||
|
||||
To troubleshoot any errors, and assuming you are using Home Assistant, view the Logs in the Music Assistant Add-ons. Please include these logs in any reported issue.
|
||||
|
||||
</details>
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Listenbrainz
|
||||
description: Listenbrainz (Source)
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
@@ -32,7 +32,7 @@ MPRIS communication requires multi-scrobbler to have access to the host's dbus-d
|
||||
|
||||
##### Local Unix Socket
|
||||
|
||||
The most secure way is to **run a multi-scrobbler container on the same host as MPRIS.** In this scenario you can simply mount the **user session** dbus unix socket into the container. Make sure [`PUID` and `PGID` ENVs are configured](/installation/#linux-host) so the container has permissions to access the socket. Then, modify your compose stack with these additional values:
|
||||
The most secure way is to **run a multi-scrobbler container on the same host as MPRIS.** In this scenario you can simply mount the **user session** dbus unix socket into the container. Make sure [`PUID` and `PGID` ENVs are configured](/installation?dockerSetting=linuxHost#recommended-settings) so the container has permissions to access the socket. Then, modify your compose stack with these additional values:
|
||||
|
||||
```yaml title="docker-compose.yaml"
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Rocksky
|
||||
description: Rocksky (Source)
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
@@ -31,7 +32,7 @@ See the [Rocksky (Client)](/configuration/clients/rocksky#setup) configuration f
|
||||
|
||||
* [File/AIO config](/configuration?configType=file#configuration-types) must include `"configureAs": "source"`
|
||||
|
||||
<Config config="RockskyClientConfig" fileContent={JsonConfig} name="rocksky">
|
||||
<Config config="RockskySourceConfig" fileContent={JsonConfig} name="rocksky">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :---------------------- | --------- | ------- | ----------------------------------------------------------------------------------- |
|
||||
| `SOURCE_ROCKSKY_HANDLE` | Yes | | Your **full** ATProto handle. For Bluesky account this is like `myUser.bsky.social` |
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: Overview
|
||||
description: Scrobble Sources
|
||||
toc_max_heading_level: 4
|
||||
---
|
||||
|
||||
@@ -40,6 +41,7 @@ A **Source** is a data source that contains information about tracks you are pla
|
||||
| [WebScrobbler](/configuration/sources/webscrobbler) | [Ingress](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
|
||||
| [VLC](/configuration/sources/vlc) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
|
||||
| [Yamaha MusicCast](/configuration/sources/yamaha-musiccast) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | ❌ | ✅ | ✅ | ❌ |
|
||||
| [Yandex Music](/configuration/sources/yandex-music) | [Active](./?sourceComm=active#by-communication-method) | [Activity](./?sot=activity#by-data-source-of-truth) | | ✅ | ✅ | ❌ |
|
||||
| [Youtube Music](/configuration/sources/youtube-music) | [Active](./?sourceComm=active#by-communication-method) | [History](./?sot=history#by-data-source-of-truth) | ❌ | ✅ | ❌ | ❌ |
|
||||
|
||||
## Features
|
||||
|
||||
@@ -16,6 +16,23 @@ If your Spotify player has [Automix](https://community.spotify.com/t5/FAQs/What-
|
||||
|
||||
#### Authenticate Spotify with Multi-Scrobbler
|
||||
|
||||
<DetailsAdmo type="warning" summary="Premium Account Required">
|
||||
|
||||
Starting [March 9th 2026](https://developer.spotify.com/blog/2026-02-06-update-on-developer-access-and-platform-security) all applications on the Spotify Developer platform will only continue to work if they are **owned** by a **Spotify Premium** account.
|
||||
|
||||
If your App is not created by a Premium account you will likely recieve a 403 error when trying to use MS with Spotify.
|
||||
|
||||
If you do not have a Spotify Premium account *you can still use Spotify with MS* but it will require you to coordinate with a (friend?) user who *does* have Spotify Premium:
|
||||
|
||||
* Follow the steps below **logged in as the Spotify Premium account user**
|
||||
* After completing App creation (step 4 or 5), navigate to the **User Management** page for the created App
|
||||
* Add **your** (non-premium) account here using your account email/name
|
||||
* Complete the rest of the authentication/configuration process normally
|
||||
|
||||
Assure your friend: **Creating a Spotify Developer app does not give anyone access to the owner's account.** The authentication process within MS is where authorization is given: it requires clicking the link in MS's dashboard and following a separate auth flow. This auth flow only authorizes access to the specific account used in the flow, unrelated to the owner of the developer app.
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
To access your Spotify history you must [create a Spotify App](https://developer.spotify.com/dashboard) to get a
|
||||
Client ID/Secret.
|
||||
|
||||
@@ -48,7 +65,7 @@ Use **one** of the following methods to specify a valid Redirect URI and complet
|
||||
|
||||
In this scenario you can use `http://127.0.0.1:9078/callback` as the Spotify Redirect URI and multi-scrobbler `SPOTIFY_REDIRECT_URI` ENV.
|
||||
|
||||
After completing authentication, copy the contents of local multi-scrobbler's [configuration folder](/installation#storage) to the final location of where multi-scrobbler will be run.
|
||||
After completing authentication, copy the contents of local multi-scrobbler's [configuration folder](/installation?dockerSetting=storage#recommended-settings) to the final location of where multi-scrobbler will be run.
|
||||
</TabItem>
|
||||
<TabItem value="echo" label="Echo">
|
||||
**Use a docker container on the same machine your browser is on to "intercept" the callback URL from Spotify authentication, then manually finish the process by changing the domain to your multi-scrobbler instance.**
|
||||
|
||||
@@ -30,7 +30,7 @@ Use the optional `usersAllow` property with **File** or **AIO** configuration to
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="SubsonicSourceConfig" fileContent={SubsonicConfig} name="subsonic">
|
||||
<Config config="SubSonicSourceConfig" fileContent={SubsonicConfig} name="subsonic">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|---------|--------------------------------------|
|
||||
| `SUBSONIC_USER` | Yes | | |
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Teal.fm
|
||||
description: Teal.fm (Source)
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
@@ -86,7 +86,7 @@ This affects **only** MS's behavior and does not affect the general connector be
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="WebscrobblerSourceConfig" fileContent={JsonConfig} name="webscrobbler">
|
||||
<Config config="WebScrobblerSourceConfig" fileContent={JsonConfig} name="webscrobbler">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
|------------------------|-----------|---------|--------------------------------------------------------------------------|
|
||||
| WS_ENABLE | No | | Set to 'true' to enable WS without needing to define other ENVs |
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: Yandex Music
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import JsonConfig from '!!raw-loader!@site/../config/ymbridge.json.example';
|
||||
|
||||
Monitor your [**Yandex Music**](https://music.yandex.com) listening activity in real-time.
|
||||
|
||||
This Source requires a third party docker container to communicate with Yandex Music, [`yandex-music-bridge`](https://github.com/Druidblack/yandex-music-bridge).
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example Docker Compose with `yandex-music-bridge`</summary>
|
||||
|
||||
```yaml title="~/msData/docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
container_name: multi-scrobbler
|
||||
environment:
|
||||
- TZ=Etc/GMT
|
||||
# ...your other sources/clients here
|
||||
|
||||
# add for yandex music
|
||||
// highlight-start
|
||||
- YMBRIDGE_URL=http://yandex-music-bridge:9980
|
||||
- YMBRIDGE_API_KEY=change-me
|
||||
// highlight-end
|
||||
|
||||
volumes:
|
||||
- "./config:/config"
|
||||
ports:
|
||||
- "9078:9078"
|
||||
restart: unless-stopped
|
||||
|
||||
// highlight-start
|
||||
yandex-music-bridge:
|
||||
image: ghcr.io/druidblack/yandex-music-bridge:latest
|
||||
environment:
|
||||
- TZ=Europe/Moscow
|
||||
- YM_TOKEN=AgAAAAACO3_345345
|
||||
- YM_API_KEY=change-me
|
||||
- YM_PORT=9980
|
||||
- YM_LANGUAGE=ru
|
||||
- YM_ENABLE_YNISON=true
|
||||
- YM_PUSH_TTL=45
|
||||
- YM_QUEUE_CACHE_TTL=15
|
||||
- YM_LOG_LEVEL=INFO
|
||||
ports:
|
||||
- "9980:9980"
|
||||
restart: unless-stopped
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
You will need to acquire your own token for `YM_TOKEN` used in [`yandex-music-bridge`](https://github.com/Druidblack/yandex-music-bridge) using the instructions provided there.
|
||||
|
||||
</details>
|
||||
|
||||
<DetailsAdmo type="tip" summary="Need Help?">
|
||||
|
||||
If your issue is specifically related to [`yandex-music-bridge`](https://github.com/Druidblack/yandex-music-bridge) (errors in the container, usage instructions, general Yandex setup like token etc...) please open an issue on **that repository** instead of Multi-Scrobbler's.
|
||||
|
||||
If you have issues specifically with the MS Source please mention [**Druidblack**](https://github.com/Druidblack) (`@Druidblack`) when opening an [issue](https://github.com/FoxxMD/multi-scrobbler/issues/new/choose) or [discussion](https://github.com/FoxxMD/multi-scrobbler/discussions/new/choose). Yandex Music is [not available](https://en.wikipedia.org/wiki/Yandex_Music) in the country of the main Multi-Scrobbler developer (FoxxMD) so they can only help troubleshooting general MS issues related to Yandex Music.
|
||||
|
||||
</DetailsAdmo>
|
||||
|
||||
## Configuration
|
||||
|
||||
<Config config="YandexMusicBridgeSourceConfig" fileContent={JsonConfig} name="ymbridge">
|
||||
| Environmental Variable | Required? | Default | Description |
|
||||
| :--------------------- | :-------- | ------- | :------------------------------------------------------------------------ |
|
||||
| `YMBRIDGE_URL` | Yes | | `http://URL:PORT` for the `yandex-music-bridge` container |
|
||||
| `YMBRIDGE_API_KEY` | No | | The same key used for `YM_API_KEY` on the `yandex-music-bridge` container |
|
||||
</Config>
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Musicbrainz Stage
|
||||
description: Enhancing Scrobbles with Musicbrainz
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
@@ -50,7 +51,7 @@ Set up [Valkey Caching](/configuration?cachedThings=metadata#caching) to cache M
|
||||
|
||||
### API Setup
|
||||
|
||||
To avoid rate limiting, MusicBrainz requires API users to identify themself using a [User-Agent that contains an email or URL.](https://wiki.musicbrainz.org/MusicBrainz_API/Rate_Limiting#How_can_I_be_a_good_citizen_and_be_smart_about_using_the_Web_Service?) You must set this in the `data` for your [stage configuration](http://localhost:3000/docs/configuration/transforms/#configuring-stages) in your [AIO Config](/configuration?configType=aio#configuration-types):
|
||||
To avoid rate limiting, MusicBrainz requires API users to identify themself using a [User-Agent that contains an email or URL.](https://wiki.musicbrainz.org/MusicBrainz_API/Rate_Limiting#How_can_I_be_a_good_citizen_and_be_smart_about_using_the_Web_Service?) You must set this in the `data` for your [stage configuration](/configuration/transforms/#configuring-stages) in your [AIO Config](/configuration?configType=aio#configuration-types):
|
||||
|
||||
```json5 title="config.json"
|
||||
{
|
||||
@@ -139,7 +140,7 @@ Example:
|
||||
|
||||
### Rules and Hooks
|
||||
|
||||
[Add your Stage](https://foxxmd.github.io/multi-scrobbler/configuration/transforms/#stage) to a Source or Client by specifying it in a [Hook](/configuration/transforms/#hook):
|
||||
[Add your Stage](/configuration/transforms/#stage) to a Source or Client by specifying it in a [Hook](/configuration/transforms/#hook):
|
||||
|
||||
```json5 title="subsonic.json"
|
||||
[
|
||||
@@ -160,7 +161,7 @@ Example:
|
||||
]
|
||||
```
|
||||
|
||||
Each [**Stage Rule**](/configuration/transforms#stage-rules) should be either a boolean, specifying if the transformed data should be used for this field, or a [`when` condition](/configuration/transforms#conditional-moditication):
|
||||
Each [**Stage Rule**](/configuration/transforms#stage-rules) should be either a boolean, specifying if the transformed data should be used for this field, or a [`when` condition](/configuration/transforms#conditional-modification):
|
||||
|
||||
<details>
|
||||
|
||||
@@ -247,11 +248,11 @@ To configure [stage defaults](#stage-configuration) use `MB_PRESETS` with a comm
|
||||
* `fields` - Applies [Field Scoring](#field-scoring) to match sorting so match text is baised towards similarity to original scrobble
|
||||
* `sensible` - Applies the [Sensible Default](#sensible-default) configuration for [sorting releases](#sorting-releases)
|
||||
|
||||
Finally, use ENV `*_TRANSFORMS=musicbrainz` on each Source/Client you wish to apply this stage to. This applies the stage in the [`preTransform` Hook](/configuration/transforms#lifecycle-hooks) with all [Rules](#rules) enabled.
|
||||
Finally, use ENV `*_TRANSFORMS=musicbrainz` on each Source/Client you wish to apply this stage to. This applies the stage in the [`preTransform` Hook](/configuration/transforms#lifecycle-hooks) with all [Rules](#rules-and-hooks) enabled.
|
||||
|
||||
The `*` stands for the prefix used for each Source/Client's ENV keys. Refer to the individual Source/Client Configuration sections to find this. Example:
|
||||
|
||||
* All [Subsonic Source](http://localhost:3000/docs/configuration/sources/subsonic/) ENVs look like `SUBSONIC_USER=myuser` etc...
|
||||
* All [Subsonic Source](/configuration/sources/subsonic) ENVs look like `SUBSONIC_USER=myuser` etc...
|
||||
* Use `SUBSONIC_TRANSFORMS=musicbrainz`
|
||||
|
||||
<details>
|
||||
@@ -706,7 +707,7 @@ Sorting can be used instead of filters, or in conjunction with filters, it's you
|
||||
|
||||
#### Release Attributes {#sorting-releases}
|
||||
|
||||
Each [Release Attribute](#release-attribute) has one additional property that can be used to **rank** releases based on the order of the values you give it. This is the `priority` property.
|
||||
Each [Release Attribute](#release-attributes) has one additional property that can be used to **rank** releases based on the order of the values you give it. This is the `priority` property.
|
||||
|
||||
In configuration:
|
||||
|
||||
@@ -777,6 +778,10 @@ In the final sorting of Recordings, title score + artist score is combined with
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Caching
|
||||
|
||||
You **should** setup [metadata caching](/configuration/transforms#caching) to reduce API calls, improve transform performance, and reduce memory usage when using this stage.
|
||||
|
||||
### Sensible Default
|
||||
|
||||
Generally, the Musicbrainz Stage can be used without any of the [optional configuration](#configuration) and you should still see good results from matches. The top [scored](#score) match is, anecdotally, good enough for correcting and filling in surface-level play data like Title and Artist names.
|
||||
@@ -818,7 +823,7 @@ Consider adding [Artist Extraction (`artist`)](#search-methods) in **Native Mode
|
||||
|
||||
How complete, and accurate, your scrobble data is from a certain Source should dictate what [Search Methods](#search-methods) you should use for matching.
|
||||
|
||||
Musicbrainz agressively normalizes **artist, album, and title** fields in its database which means that if your Scrobble data contains major innaccuracies or phrases fields in a non-normal way, it's possible Musicbrainz won't find any matches. This is a more common occurrence when trying to match Scrobble data from Sources that don't support multiple artist data, like [Subsonic](/configuration/sources/subsonic) and [Last.fm](/configuration/sources/lastfm).
|
||||
Musicbrainz agressively normalizes **artist, album, and title** fields in its database which means that if your Scrobble data contains major innaccuracies or phrases fields in a non-normal way, it's possible Musicbrainz won't find any matches. This is a more common occurrence when trying to match Scrobble data from Sources that don't support multiple artist data, like [Subsonic](/configuration/sources/subsonic) and [Last.fm](/configuration/sources/lastfm-source).
|
||||
|
||||
<details>
|
||||
|
||||
@@ -869,7 +874,7 @@ When using your own filters consider:
|
||||
|
||||
### Using Partial Match
|
||||
|
||||
Use [Rules](#rules) to apply MusicBrainz match data selectively.
|
||||
Use [Rules](#rules-and-hooks) to apply MusicBrainz match data selectively.
|
||||
|
||||
If you know that your music collection is well organized and you do not want to change the artists/title/album etc... sent to a [Client](/configuration/clients), you can still benefit from matches by only applying MBIDs using `meta` so that any Client that supports Musicbrainz data ([Koito](/configuration/clients/koito), [Tealfm](/configuration/clients/tealfm), [Listenbrainz](/configuration/clients/listenbrainz), [Rocksky](/configuration/clients/rocksky)) can still get that data.
|
||||
|
||||
@@ -1086,7 +1091,7 @@ Your [AIO Config](/configuration?configType=aio#configuration-types):
|
||||
}
|
||||
```
|
||||
|
||||
In a [Jellyfin](/configuration/clients/jellyfin) [File Config](/configuration?configType=file#configuration-types):
|
||||
In a [Jellyfin](/configuration/sources/jellyfin) [File Config](/configuration?configType=file#configuration-types):
|
||||
|
||||
```json5 title="jellyfin.json"
|
||||
[
|
||||
@@ -1119,7 +1124,7 @@ In a [Jellyfin](/configuration/clients/jellyfin) [File Config](/configuration?co
|
||||
* [Sensible Defaults](#sensible-default)
|
||||
* Fallback search with [Artist Extraction](#search-methods)
|
||||
* [Default Native Stage](/configuration/transforms/native/#default-stage)
|
||||
* Uses [Flow Control](/configurations/transforms#flow-control) to run Native only if there are no Musicbrainz matches
|
||||
* Uses [Flow Control](/configuration/transforms#flow-control) to run Native only if there are no Musicbrainz matches
|
||||
|
||||
<details>
|
||||
|
||||
@@ -1153,7 +1158,7 @@ Your [AIO Config](/configuration?configType=aio#configuration-types):
|
||||
}
|
||||
```
|
||||
|
||||
In a [Jellyfin](/configuration/clients/jellyfin) [File Config](/configuration?configType=file#configuration-types):
|
||||
In a [Jellyfin](/configuration/sources/jellyfin) [File Config](/configuration?configType=file#configuration-types):
|
||||
|
||||
```json5 title="jellyfin.json"
|
||||
[
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Native Stage
|
||||
description: Enhancing Scrobbles with Built-in Parsing
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
@@ -75,7 +76,7 @@ Config => extracted artists
|
||||
|
||||
### Rules
|
||||
|
||||
Each [Rule](/configuration/transforms#stage-rules) should be either a boolean, specifying if the transformed data should be used for this field, or a [`when` condition.](/configuration/transforms#conditional-moditication):
|
||||
Each [Rule](/configuration/transforms#stage-rules) should be either a boolean, specifying if the transformed data should be used for this field, or a [`when` condition.](/configuration/transforms#conditional-modification):
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -96,7 +97,7 @@ If a rule is not present then multi-scrobbler defaults it to `true`.
|
||||
|
||||
For your convenience, if you do not define any Native [Stage Configurations](/configuration/transforms#configuring-stages) then multi-scrobbler automatically builds one using the defaults shown in [configuration](#configuration).
|
||||
|
||||
This can be used by omitting the `name` in your [modification Stage](/configuration/transforms#modification-stage).
|
||||
This can be used by omitting the `name` in your [modification Stage](/configuration/transforms#stage).
|
||||
|
||||
<details>
|
||||
|
||||
@@ -132,7 +133,7 @@ Use ENV `*_TRANSFORMS=native` on each Source/Client you wish to apply this stage
|
||||
|
||||
The `*` stands for the prefix used for each Source/Client's ENV keys. Refer to the individual Source/Client Configuration sections to find this. Example
|
||||
|
||||
* All [Subsonic Source](http://localhost:3000/docs/configuration/sources/subsonic/) ENVs look like `SUBSONIC_USER=myuser` etc...
|
||||
* All [Subsonic Source](/configuration/sources/subsonic/) ENVs look like `SUBSONIC_USER=myuser` etc...
|
||||
* Use `SUBSONIC_TRANSFORMS=native`
|
||||
|
||||
<details>
|
||||
|
||||
@@ -36,7 +36,7 @@ Scrobble **Clients** listen for discovered Plays from Sources, then...
|
||||
* The Client **compares** this Play to all the other scrobbles it has already seen, if the Play is unique (title/artist/album/listened datetime) then...
|
||||
* The Client **scrobbles** the Play downstream to the scrobble service and adds it as a Scrobble it has seen already
|
||||
|
||||
## Lifecyle Hooks
|
||||
## Lifecycle Hooks
|
||||
|
||||
You'll notice there is a pattern above that looks like this:
|
||||
|
||||
@@ -376,7 +376,7 @@ Specifying these Rules is **not** the same as [configuring the Stage](#configuri
|
||||
|
||||
Stages may be optionally configured to **continue** to the next Stage or **stop** (end early) all subsequent Stages, based on the outcome of the currently running Stage.
|
||||
|
||||
These three properties can be added to the [Modification Stage](#modification-stage) data, alongside [Rules](#rules-for-play-data):
|
||||
These three properties can be added to the [Modification Stage](#stage) data, alongside [Rules](#stage-rules):
|
||||
|
||||
* `onSuccess` (default `continue`) - If the Stage successfully finishes processing
|
||||
* `onFailure` (default `stop`) - If the Stage encounters an error while processing, or otherwise fails to achieve the transformation result
|
||||
@@ -585,6 +585,47 @@ The output shows the diff between the previous stage (or original Play) and the
|
||||
|
||||
</details>
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Caching
|
||||
|
||||
MS uses [caching](/configuration/#caching) to reduce the number of API calls needed for stages like [Musicbrainz](/configuration/transforms/musicbrainz) and to speed up all transforms by caching steps and results. However, the default caching strategy uses a small cache size and very short [TTLs](https://en.wikipedia.org/wiki/Time_to_live) because it is *in-memory*.
|
||||
|
||||
**If you are using any Transform stages you should configure [secondary caching with Valkey for Metadata](/configuration/?cacheType=valkey&cachedThings=metadata#secondary-caching-configuration)** to increase the cache size and lifetime of cached items. This will also reduce memory usage in MS.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
Add valkey service to your [multi-scrobbler docker compose stack](/installation?runType=docker-compose#docker) and configure MS to use it for metadata:
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
# ...
|
||||
environment:
|
||||
# ...
|
||||
// highlight-start
|
||||
- CACHE_METADATA=valkey
|
||||
- CACHE_METADATA_CONN=redis://valkey:6379
|
||||
// highlight-end
|
||||
# ...
|
||||
|
||||
// highlight-start
|
||||
valkey:
|
||||
image: valkey/valkey
|
||||
volumes:
|
||||
- valkeydata:/data
|
||||
|
||||
volumes:
|
||||
valkeydata:
|
||||
driver: local
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Examples
|
||||
|
||||
See **Examples** sections in specific Stage docs (also in the sidebar):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
title: User Stage
|
||||
description: Enhancing Scrobbles with Regex Parsing
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
---
|
||||
@@ -16,7 +17,7 @@ The user [Stage `type`](/configuration/transforms#stage) is `user`.
|
||||
|
||||
## Configuration
|
||||
|
||||
All [Stage Configuration](/configuration/transforms#configuring-stage) is done using [**search-and-replace** expressions](#search-and-replace-expression) inside individual [rules](#rules). Default configuration for all Rules can still be done using Stage `defaults`.
|
||||
All [Stage Configuration](/configuration/transforms#configuring-stages) is done using [**search-and-replace** expressions](#search-and-replace-expression) inside individual [rules](#rules). Default configuration for all Rules can still be done using Stage `defaults`.
|
||||
|
||||
<details>
|
||||
|
||||
@@ -46,9 +47,9 @@ All [Stage Configuration](/configuration/transforms#configuring-stage) is done u
|
||||
|
||||
#### Default Stage
|
||||
|
||||
For your convenience, if you do not define any User [Stage Configurations](/configuration/transforms#configuration) then multi-scrobbler automatically builds an empty one for you.
|
||||
For your convenience, if you do not define any User [Stage Configurations](/configuration/transforms#configuring-stages) then multi-scrobbler automatically builds an empty one for you.
|
||||
|
||||
This can be used by omitting the `name` in your [modification Stage](/configuration/transforms#modification-stage).
|
||||
This can be used by omitting the `name` in your [modification Stage](/configuration/transforms#stage).
|
||||
|
||||
<details>
|
||||
|
||||
@@ -126,7 +127,7 @@ The `replace` property uses javascript's [`replace()` function and so can use an
|
||||
|
||||
### Rules
|
||||
|
||||
Each [Rule](/configuration/transforms#stage-rules) must be an array of [search-and-replace expressions](#search-and-replace-expressions):
|
||||
Each [Rule](/configuration/transforms#stage-rules) must be an array of [search-and-replace expressions](#search-and-replace-expression):
|
||||
|
||||
```json5 title="lastfm.json"
|
||||
[
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
sidebar_position: 3
|
||||
title: Client Development/Tutorial
|
||||
---
|
||||
|
||||
To do...
|
||||
@@ -3,7 +3,7 @@ toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
sidebar_position: 1
|
||||
title: Common Development
|
||||
description: Start here for MS development
|
||||
description: Common Development Knowledge
|
||||
---
|
||||
|
||||
# Development
|
||||
@@ -177,7 +177,7 @@ For more refer to the TS documentation for `PlayObject` or [`AmbPlayObject`](htt
|
||||
## Creating Clients and Sources
|
||||
|
||||
* [Source Development and Tutorial](dev-source.md)
|
||||
* [Client Development and Tutorial](dev-client.md)
|
||||
* Client Development and Tutorial (TBD)
|
||||
|
||||
## Profiling
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
sidebar_position: 2
|
||||
title: Source Development/Tutorial
|
||||
description: Developing a new Source
|
||||
---
|
||||
|
||||
This document will provide a step-by-step guide for creating a (trivial) new Source in MS alongside describing what aspects of the Source need to be implemented based on the service you use. Before using this document you should review [Common Development](dev-common.md#common-development).
|
||||
|
||||
@@ -3,7 +3,7 @@ toc_min_heading_level: 2
|
||||
toc_max_heading_level: 5
|
||||
sidebar_position: 4
|
||||
title: Flatpak
|
||||
description: Building Flatpak App locally
|
||||
description: Building Flatpak App
|
||||
---
|
||||
|
||||
## Install Requirements
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: 'Help'
|
||||
title: Help
|
||||
---
|
||||
|
||||
## Getting Useful Data
|
||||
@@ -54,7 +54,7 @@ Thank you for your time.
|
||||
|
||||
### FAQ
|
||||
|
||||
Check the [FAQ](/faq) to see if your issue is a known issue or a commonly-experienced issue with Multi-Scrobbler. The FAQ also includes fixes or ways to help troubleshoot these common issues.
|
||||
Check the [FAQ](/FAQ) to see if your issue is a known issue or a commonly-experienced issue with Multi-Scrobbler. The FAQ also includes fixes or ways to help troubleshoot these common issues.
|
||||
|
||||
### Existing Issues
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ A dockerized app that monitors your music listening activity from *everywhere* a
|
||||
* [MPD (Music Player Daemon)](/configuration/sources/mpd)
|
||||
* [MPRIS (Linux Desktop)](/configuration/sources/mpris)
|
||||
* [Musikcube](/configuration/sources/musikcube)
|
||||
* [Music Assistant](/configuration/sources/listenbrainz-endpoint#setup-for-popular-apps)
|
||||
* [Plex](/configuration/sources/plex)
|
||||
* [Rocksky](/configuration/sources/rocksky-source)
|
||||
* [Sonos](/configuration/sources/sonos)
|
||||
@@ -40,6 +41,7 @@ A dockerized app that monitors your music listening activity from *everywhere* a
|
||||
* [WebScrobbler](/configuration/sources/webscrobbler)
|
||||
* [VLC](/configuration/sources/vlc)
|
||||
* [Yamaha MusicCast](/configuration/sources/yamaha-musiccast)
|
||||
* [Yandex Music](/configuration/sources/yandex-music)
|
||||
* [Youtube Music](/configuration/sources/youtube-music)
|
||||
* Supports scrobbling to many [**Clients**](/configuration/clients)
|
||||
* [Discord](/configuration/clients/discord) (Now Playing)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: 'Overview'
|
||||
title: Overview
|
||||
description: Installation
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
@@ -37,81 +38,39 @@ Cross-platform images are built for x86 (Intel/AMD) and ARM64 (IE Raspberry Pi)
|
||||
|
||||
:::
|
||||
|
||||
Or use the example `docker-compose.yml`, found in the repo, after modifying it to fit your configuration:
|
||||
### Recommended Settings
|
||||
|
||||
<details>
|
||||
<Tabs groupId="dockerSetting" queryString>
|
||||
<TabItem value="storage" label="Storage">
|
||||
You **should** bind a host directory into the container for storing configurations and credentials. Otherwise, these will be lost when the container is updated.
|
||||
|
||||
<summary>`docker-compose.yml`</summary>
|
||||
|
||||
<CodeBlock language="yaml">{ComposeStack}</CodeBlock>
|
||||
|
||||
</details>
|
||||
|
||||
Recommended configuration steps for docker or docker-compose usage:
|
||||
|
||||
#### Storage
|
||||
|
||||
You should bind a host directory into the container for storing configurations and credentials. Otherwise, these will be lost when the container is updated.
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
<Tabs groupId="runType" queryString>
|
||||
<TabItem value="docker" label="Docker">
|
||||
[Using `-v` method for docker](https://docs.docker.com/storage/bind-mounts/#start-a-container-with-a-bind-mount):
|
||||
```shell
|
||||
docker run ... -v "$(pwd)/config:/config" foxxmd/multi-scrobbler
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="docker-compose" label="Docker Compose" default>
|
||||
[Using docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-3):
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
volumes:
|
||||
- "./config:/config"
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</details>
|
||||
|
||||
#### Networking
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
// highlight-start
|
||||
volumes:
|
||||
- "./config:/config"
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="networking" label="Networking">
|
||||
If you are using a [bridge network](https://www.appsdeveloperblog.com/docker-networking-bridging-host-and-overlay/) (default docker setup) you must map a port to the container in order to access the dashboard and use MS with some sources (Webscrobbler, LFM/LZ Endpoints). The default container port is `9078`.
|
||||
|
||||
<details>
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
// highlight-start
|
||||
ports:
|
||||
- "9078:9078"
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
<summary>Example</summary>
|
||||
|
||||
<Tabs groupId="runType" queryString>
|
||||
<TabItem value="docker" label="Docker">
|
||||
[Docker `run` publish options](https://docs.docker.com/engine/reference/commandline/run/#publish):
|
||||
```shell
|
||||
docker run ... -p 9078:9078 foxxmd/multi-scrobbler
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="docker-compose" label="Docker Compose" default>
|
||||
[docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-1):
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
ports:
|
||||
- "9078:9078"
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
</details>
|
||||
|
||||
##### Base URL
|
||||
|
||||
Optionally, when
|
||||
</TabItem>
|
||||
<TabItem value="baseUrl" label="Base URL">
|
||||
**Optionally**, when
|
||||
|
||||
* using a [Source or Client](../configuration/configuration.mdx) that has a "Redirect URI" that you have not explicitly defined
|
||||
* and
|
||||
@@ -120,63 +79,89 @@ Optionally, when
|
||||
|
||||
set the [Base URL](../configuration/configuration.mdx#base-url) as the IP of the host machine. (This is the IP you would use to view the dashboard in a browser)
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Example</summary>
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
environment:
|
||||
// highlight-start
|
||||
- BASE_URL=http://hostMachineIP
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
<Tabs groupId="runType" queryString>
|
||||
<TabItem value="docker" label="Docker">
|
||||
```shell
|
||||
docker run ... -e BASE_URL="http://hostMachineIP" foxxmd/multi-scrobbler
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="docker-compose" label="Docker Compose" default>
|
||||
[docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#short-syntax-1):
|
||||
</TabItem>
|
||||
<TabItem value="tz" label="Timezone">
|
||||
Set the [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for the container using the environmental variable `TZ`.
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
environment:
|
||||
- BASE_URL=http://hostMachineIP
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
# ...
|
||||
environment:
|
||||
# Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
// highlight-start
|
||||
- TZ=America/New_York
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### Other
|
||||
|
||||
* (Optionally) set the [timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for the container using the environmental variable `TZ` ([docker](https://docs.docker.com/engine/reference/commandline/run/#env)) ([docker-compose](https://docs.docker.com/compose/compose-file/compose-file-v3/#environment))
|
||||
|
||||
#### Linux Host
|
||||
|
||||
::::info
|
||||
|
||||
If you are running this container with **Docker** on a **Linux Host** you must specify `user:group` permissions of the user who owns the **configuration directory** on the host to avoid [docker file permission problems.](https://ikriv.com/blog/?p=4698) These can be specified using the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid)
|
||||
</TabItem>
|
||||
<TabItem value="linuxHost" label="Linux Host">
|
||||
If you are running this container with **Docker** on a **Linux Host** you **should** specify `user:group` permissions of the user who owns the **configuration directory** on the host to avoid [docker file permission problems.](https://ikriv.com/blog/?p=4698) These can be specified using the [environmental variables **PUID** and **PGID**.](https://docs.linuxserver.io/general/understanding-puid-and-pgid)
|
||||
|
||||
To get the UID and GID for the current user run these commands from a terminal:
|
||||
|
||||
* `id -u` -- prints UID
|
||||
* `id -g` -- prints GID
|
||||
|
||||
::::
|
||||
|
||||
#### Read Only
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="ro" label="Read Only">
|
||||
MS can be hardended by setting the file system as read only with [`read_only`](https://docs.docker.com/reference/compose-file/services/#read_only), with a few exceptions:
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
# ...
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /run:exec
|
||||
- /tmp:noexec
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
# ...
|
||||
// highlight-start
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /run:exec
|
||||
- /tmp:noexec
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="caching" label="Caching">
|
||||
**Optionally**, add a [Valkey](https://valkey.io/) service to your stack for [secondary caching](/configuration/?cacheType=valkey#secondary-caching-configuration) to take advantage of faster performance and reduced memory usage.
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
# ...
|
||||
environment:
|
||||
# ...
|
||||
// highlight-start
|
||||
CACHE_METADATA=valkey
|
||||
CACHE_METADATA_CONN=redis://valkey:6379
|
||||
// highlight-end
|
||||
# ...
|
||||
|
||||
// highlight-start
|
||||
valkey:
|
||||
image: valkey/valkey
|
||||
volumes:
|
||||
- valkeydata:/data
|
||||
|
||||
volumes:
|
||||
valkeydata:
|
||||
driver: local
|
||||
// highlight-end
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Docker Usage Example
|
||||
|
||||
:::tip
|
||||
@@ -193,49 +178,43 @@ The example scenario:
|
||||
* Docker container located on a different IP (`192.168.0.100`) so use [Base URL](/configuration#base-url)
|
||||
* Config/data directory on host machine in a directory next to `docker-compose.yml`
|
||||
* Linux uid/gid is `1000:1000`
|
||||
* Optional caching using valkey (after uncommenting)
|
||||
|
||||
<Tabs groupId="runType" queryString>
|
||||
<TabItem value="docker" label="Docker">
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
```bash
|
||||
docker run --name multi-scrobbler -e "JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56bd6540c" -e "JELLYFIN_URL=192.168.0.101:8096" -e "JELLYFIN_USER=MyUser" -e "BASE_URL=192.168.0.100" -e "MALOJA_URL=http://domain.tld" -e "MALOJA_API_KEY=1234" -e "PUID=1000" -e "PGID=1000" -p 9078:9078 -v /home/myUser/ms:/config foxxmd/multi-scrobbler
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
```bash
|
||||
docker run --name multi-scrobbler -e "PUID=1000" -e "PGID=1000" -e "BASE_URL=192.168.0.100" -p 9078:9078 -v "$(pwd)/config:/config" foxxmd/multi-scrobbler
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</TabItem>
|
||||
<TabItem value="docker-compose" label="Docker Compose" default>
|
||||
See [`docker-compose.yml`](#docker) sample above for more options and annotations.
|
||||
See [`docker-compose.yml`](#docker) sample above for more options and annotations.
|
||||
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
container_name: multi-scrobbler
|
||||
environment:
|
||||
- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
- JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56bd6540c
|
||||
- JELLYFIN_URL=192.168.0.101:8096
|
||||
- JELLYFIN_USER=MyUser
|
||||
- BASE_URL=http://192.168.0.100:9078
|
||||
- MALOJA_URL=http://domain.tld:42010
|
||||
- MALOJA_API_KEY=1234
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
volumes:
|
||||
- "./config:/config"
|
||||
ports:
|
||||
- "9078:9078"
|
||||
restart: unless-stopped
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
```yaml title="docker-compose.yml"
|
||||
services:
|
||||
multi-scrobbler:
|
||||
image: foxxmd/multi-scrobbler
|
||||
container_name: multi-scrobbler
|
||||
environment:
|
||||
- TZ=Etc/GMT # Specify timezone from TZ Database name found here https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
- JELLYFIN_APIKEY=c9fae8756fbf481ebd9c5bb56bd6540c
|
||||
- JELLYFIN_URL=192.168.0.101:8096
|
||||
- JELLYFIN_USER=MyUser
|
||||
- BASE_URL=http://192.168.0.100:9078
|
||||
- MALOJA_URL=http://domain.tld:42010
|
||||
- MALOJA_API_KEY=1234
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
# uncomment along with valkey service/volume below for better caching
|
||||
#- CACHE_METADATA=valkey
|
||||
#- CACHE_METADATA_CONN=redis://valkey:6379
|
||||
volumes:
|
||||
- ./config:/config
|
||||
ports:
|
||||
- 9078:9078
|
||||
restart: unless-stopped
|
||||
|
||||
#valkey:
|
||||
# image: valkey/valkey
|
||||
# volumes:
|
||||
# - valkeydata:/data
|
||||
|
||||
#volumes:
|
||||
# valkeydata:
|
||||
# driver: local
|
||||
```
|
||||
## Local Installation
|
||||
|
||||
After installation see [service.md](service.md) to configure multi-scrobbler to run automatically in the background.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
title: 'As a Service'
|
||||
title: As a Service
|
||||
description: Local Install as a Service
|
||||
---
|
||||
|
||||
If you have multi-scrobbler installed [locally](installation.mdx#nodejs) you can enable it to run as a background service when you login.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: 'Quickstart'
|
||||
title: Quickstart
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
@@ -10,7 +10,7 @@ This guide will get you up and running with multi-scrobbler using [Docker](https
|
||||
|
||||
* the dashboard served on port `9078` of a host machine which has an IP of `192.168.0.100`
|
||||
* data saved to the same directory as the `docker-compose.yml` file
|
||||
* multi-scrobbler monitoring [Spotify](/configuration/sources/spotify), [Jellyfin](/configuration/sources/jellyfin), Navidrome, [Plex](/configuration/sources/plex), and/or [Last.fm](/configurations/sources/lastfm) for listening activity
|
||||
* multi-scrobbler monitoring [Spotify](/configuration/sources/spotify), [Jellyfin](/configuration/sources/jellyfin), Navidrome, [Plex](/configuration/sources/plex), and/or [Last.fm](/configuration/sources/lastfm-source) for listening activity
|
||||
* multi-scrobbler scrobbling to [Lastfm](/configuration/clients/lastfm), [Maloja](/configuration/clients/maloja), and/or [Koito](/configuration/clients/koito)
|
||||
|
||||
:::note
|
||||
@@ -166,7 +166,7 @@ Add these value to the `environment` section in the [`docker-compose.yml` you cr
|
||||
- MB_PRESETS=default,sensible,native
|
||||
```
|
||||
|
||||
**Presets** are found [here](http://localhost:3000/docs/configuration/transforms/musicbrainz/#env-configuration).
|
||||
**Presets** are found [here](/configuration/transforms/musicbrainz/#env-configuration).
|
||||
|
||||
<h3>Enable Musicbrainz For A Source</h3>
|
||||
|
||||
@@ -314,7 +314,7 @@ Modify the `lastfm.json` file you created in the [Setup Sources](#setup-sources)
|
||||
|
||||
:::tip
|
||||
|
||||
If you are running your multi-scrobbler container on a Linux host see [these instructions for setting proper file permissions.](/installation#linux-host)
|
||||
If you are running your multi-scrobbler container on a Linux host see [these instructions for setting proper file permissions.](/installation?dockerSetting=linuxHost#recommended-settings)
|
||||
|
||||
:::
|
||||
|
||||
@@ -336,6 +336,7 @@ Visit `http://192.168.0.100:9078` to see the dashboard where
|
||||
## Next Steps
|
||||
|
||||
* See more advanced docker options as well as other install methods in the [**Installation**](/installation#docker) docs
|
||||
* Setup [secondary caching](/configuration/?cacheType=valkey#secondary-caching-configuration) with [valkey](/installation?dockerSetting=caching#recommended-settings) for increased performance and reduced memory usage
|
||||
* Review the [**Configuration**](/configuration) docs
|
||||
* Learn about how to configure multi-scrobbler using files for more complicated Source/Client scenarios
|
||||
* See all available [**Sources**](/configuration/sources) and [**Clients**](/configuration/clients) alongside configuration examples
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
title: 'Overview'
|
||||
description: Updating
|
||||
---
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
import type * as Preset from '@docusaurus/preset-classic';
|
||||
import * as Preset from '@docusaurus/preset-classic';
|
||||
import type { Config } from '@docusaurus/types';
|
||||
import * as themes from 'prism-react-renderer';
|
||||
import * as Renderers from './lib/socialCard/ImageRenderers.js';
|
||||
//import sidebars from './sidebars';
|
||||
|
||||
let baseSite = 'https://foxxmd.github.io';
|
||||
// https://docs.netlify.com/build/configure-builds/environment-variables/#deploy-urls-and-metadata
|
||||
const netlifyDeploy = process.env.DEPLOY_PRIME_URL;
|
||||
if(netlifyDeploy !== undefined && netlifyDeploy !== null && netlifyDeploy.trim() !== '') {
|
||||
baseSite = netlifyDeploy;
|
||||
} else {
|
||||
const baseSiteEnv = process.env.BASE_SITE;
|
||||
if(baseSiteEnv !== undefined && baseSiteEnv !== null && baseSiteEnv.trim() !== '') {
|
||||
baseSite = baseSiteEnv;
|
||||
}
|
||||
}
|
||||
|
||||
const config: Config = {
|
||||
future: {
|
||||
v4: {
|
||||
@@ -19,7 +32,7 @@ const config: Config = {
|
||||
favicon: 'img/favicon.ico',
|
||||
|
||||
// Set the production url of your site here
|
||||
url: 'https://foxxmd.github.io',
|
||||
url: baseSite,
|
||||
// Set the /<baseUrl>/ pathname under which your site is served
|
||||
// For GitHub pages deployment, it is often '/<projectName>/'
|
||||
baseUrl: process.env.DOCS_BASE !== undefined && process.env.DOCS_BASE !== '' ? process.env.DOCS_BASE : '/docs',
|
||||
@@ -31,8 +44,12 @@ const config: Config = {
|
||||
|
||||
trailingSlash: true,
|
||||
|
||||
onBrokenLinks: 'warn',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
|
||||
markdown: {
|
||||
hooks: {
|
||||
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
|
||||
@@ -71,7 +88,7 @@ const config: Config = {
|
||||
],
|
||||
],
|
||||
themes: [
|
||||
"docusaurus-json-schema-plugin",
|
||||
//"docusaurus-json-schema-plugin",
|
||||
[
|
||||
"@easyops-cn/docusaurus-search-local",
|
||||
/** @type {import("@easyops-cn/docusaurus-search-local").PluginOptions} */
|
||||
@@ -90,6 +107,7 @@ const config: Config = {
|
||||
'docusaurus-theme-github-codeblock'
|
||||
],
|
||||
plugins: [
|
||||
'docusaurus-plugin-sass',
|
||||
[
|
||||
'@docusaurus/plugin-client-redirects',
|
||||
{
|
||||
@@ -121,6 +139,16 @@ const config: Config = {
|
||||
},
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
'@bony_chops/docusaurus-og',
|
||||
{
|
||||
path: './preview-images', // relative to the build directory
|
||||
imageRenderers: {
|
||||
'docusaurus-plugin-content-docs': Renderers.docs,
|
||||
'docusaurus-plugin-content-pages': Renderers.docs,
|
||||
},
|
||||
},
|
||||
]
|
||||
],
|
||||
themeConfig:
|
||||
@@ -153,14 +181,14 @@ const config: Config = {
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
href: 'https://foxxmd.github.io/multi-scrobbler/',
|
||||
href: 'https://multi-scrobbler.app',
|
||||
label: 'Website',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: 'dark',
|
||||
|
||||
links: [
|
||||
{
|
||||
title: 'Docs',
|
||||
@@ -192,7 +220,7 @@ const config: Config = {
|
||||
},
|
||||
{
|
||||
label: 'Website',
|
||||
href: 'https://foxxmd.github.io/multi-scrobbler/',
|
||||
href: 'https://multi-scrobbler.app',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -215,28 +243,22 @@ const config: Config = {
|
||||
showRunmeLink: false,
|
||||
runmeLinkLabel: 'Checkout via Runme'
|
||||
}
|
||||
} satisfies Preset.ThemeConfig,
|
||||
headTags: [
|
||||
{
|
||||
tagName: 'meta',
|
||||
attributes: {
|
||||
name: 'google-site-verification',
|
||||
content: process.env.GSITEVERIFICATION ?? 'none'
|
||||
}
|
||||
}
|
||||
]
|
||||
} satisfies Preset.ThemeConfig
|
||||
};
|
||||
|
||||
if (process.env.ANALYTICS !== undefined && process.env.ANALYTICS !== '') {
|
||||
const script = {
|
||||
src: process.env.ANALYTICS,
|
||||
defer: true
|
||||
}
|
||||
if (process.env.ANALYTICS_DOMAIN !== undefined && process.env.ANALYTICS_DOMAIN !== '') {
|
||||
script['data-domain'] = process.env.ANALYTICS_DOMAIN;
|
||||
}
|
||||
config.scripts.push(script)
|
||||
const gverify = process.env.GSITEVERIFICATION;
|
||||
if(gverify !== undefined && gverify !== null && gverify.trim() !== '') {
|
||||
config.headTags = [
|
||||
{
|
||||
tagName: 'meta',
|
||||
attributes: {
|
||||
name: 'google-site-verification',
|
||||
content: gverify
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (process.env.RY_ANALYTICS !== undefined && process.env.RY_ANALYTICS !== '') {
|
||||
const script = {
|
||||
src: process.env.RY_ANALYTICS,
|
||||
@@ -245,10 +267,23 @@ if (process.env.RY_ANALYTICS !== undefined && process.env.RY_ANALYTICS !== '') {
|
||||
if (process.env.RY_ANALYTICS_SITEID !== undefined && process.env.RY_ANALYTICS_SITEID !== '') {
|
||||
script['data-site-id'] = process.env.RY_ANALYTICS_SITEID;
|
||||
}
|
||||
if (process.env.RY_ANALYTICS_REPLAY !== undefined && process.env.RY_ANALYTICS_REPLAY !== '') {
|
||||
script['data-session-replay'] = process.env.RY_ANALYTICS_REPLAY;
|
||||
}
|
||||
config.scripts.push(script)
|
||||
}
|
||||
|
||||
if(process.env.NETLIFY === 'true') {
|
||||
// @ts-ignore
|
||||
config.themeConfig.footer.links.push({
|
||||
title: 'Docs Powered By',
|
||||
items: [
|
||||
{
|
||||
html: `
|
||||
<a href="https://www.netlify.com">
|
||||
<img src="https://www.netlify.com/assets/badges/netlify-badge-color-accent.svg" alt="Deploys by Netlify" />
|
||||
</a>
|
||||
`
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
export default config;
|
||||
|
||||
Generated
+5535
-1733
File diff suppressed because it is too large
Load Diff
+31
-15
@@ -1,40 +1,51 @@
|
||||
{
|
||||
"name": "my-website",
|
||||
"version": "0.0.0",
|
||||
"name": "multi-scrobbler-docs",
|
||||
"version": "0.12.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "docusaurus start --host 0.0.0.0",
|
||||
"build": "docusaurus build",
|
||||
"build": "npm run -s prebuild && 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"
|
||||
"typecheck": "tsc",
|
||||
"postinstall": "patch-package",
|
||||
"previews:build-renderer": "tsc --project tsconfig.client.json || exit 0",
|
||||
"prebuild": "npm run -s previews:build-renderer"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.8.1",
|
||||
"@docusaurus/faster": "3.8.1",
|
||||
"@docusaurus/plugin-client-redirects": "^3.8.1",
|
||||
"@docusaurus/preset-classic": "3.8.1",
|
||||
"@bony_chops/docusaurus-og": "^1.0.3-beta.3",
|
||||
"@docusaurus/core": "3.9.2",
|
||||
"@docusaurus/faster": "3.9.2",
|
||||
"@docusaurus/plugin-client-redirects": "^3.9.2",
|
||||
"@docusaurus/preset-classic": "3.9.2",
|
||||
"@easyops-cn/docusaurus-search-local": "0.51.1",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"ajv": "^8.18.0",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"cf-json-schema-viz": "^0.3.0",
|
||||
"clsx": "^2.0.0",
|
||||
"docusaurus-json-schema-plugin": "^1.14.0",
|
||||
"docusaurus-plugin-sass": "^0.2.6",
|
||||
"docusaurus-theme-github-codeblock": "^2.0.2",
|
||||
"json5": "^2.2.3",
|
||||
"micromark-extension-directive": "^3.0.1",
|
||||
"modern-react-json-editor": "^1.0.2",
|
||||
"patch-package": "^8.0.1",
|
||||
"prism-react-renderer": "^2.3.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"sass": "^1.97.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.6.3",
|
||||
"@docusaurus/tsconfig": "^3.6.3",
|
||||
"@docusaurus/types": "^3.6.3",
|
||||
"@types/react": "^18.2.29",
|
||||
"@docusaurus/module-type-aliases": "^3.9.2",
|
||||
"@docusaurus/tsconfig": "^3.9.2",
|
||||
"@docusaurus/types": "^3.9.2",
|
||||
"@types/react": "^19.2.4",
|
||||
"typescript": "~5.6.2"
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -50,6 +61,11 @@
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
"node": ">=24.14.0",
|
||||
"npm": ">=11.12.1"
|
||||
},
|
||||
"overrides": {
|
||||
"lodash": "4.18.1",
|
||||
"satori": "0.25.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
diff --git a/node_modules/@bony_chops/docusaurus-og/lib/server/docs.plugin.js b/node_modules/@bony_chops/docusaurus-og/lib/server/docs.plugin.js
|
||||
index b308c02..e0d291a 100644
|
||||
--- a/node_modules/@bony_chops/docusaurus-og/lib/server/docs.plugin.js
|
||||
+++ b/node_modules/@bony_chops/docusaurus-og/lib/server/docs.plugin.js
|
||||
@@ -93,7 +93,16 @@ class DocsPlugin {
|
||||
await document.write();
|
||||
}
|
||||
};
|
||||
- getHtmlPath = (doc) => doc.metadata?.permalink &&
|
||||
- path.join(this.context.outDir, doc.metadata.permalink, 'index.html');
|
||||
+ getHtmlPath = (doc) => {
|
||||
+ if(doc.metadata?.permalink !== undefined) {
|
||||
+ let relativePerma = doc.metadata.permalink;
|
||||
+ if(this.context.baseUrl !== undefined && this.context.baseUrl !== '/') {
|
||||
+ relativePerma = path.join('/', relativePerma.replace(this.context.baseUrl, ''));
|
||||
+ }
|
||||
+ const p = path.join(this.context.outDir, relativePerma, 'index.html');
|
||||
+ return p;
|
||||
+ }
|
||||
+ return undefined;
|
||||
+ }
|
||||
}
|
||||
exports.DocsPlugin = DocsPlugin;
|
||||
diff --git a/node_modules/@bony_chops/docusaurus-og/lib/server/pages.plugin.js b/node_modules/@bony_chops/docusaurus-og/lib/server/pages.plugin.js
|
||||
index 4676f56..03f03a5 100644
|
||||
--- a/node_modules/@bony_chops/docusaurus-og/lib/server/pages.plugin.js
|
||||
+++ b/node_modules/@bony_chops/docusaurus-og/lib/server/pages.plugin.js
|
||||
@@ -101,6 +101,13 @@ class PagesPlugin {
|
||||
await document.write();
|
||||
}
|
||||
};
|
||||
- getHtmlPath = (permalink) => path.join(this.context.outDir, permalink, 'index.html');
|
||||
+ getHtmlPath = (permalink) => {
|
||||
+ let relativePerma = permalink;
|
||||
+ if(this.context.baseUrl !== undefined && this.context.baseUrl !== '/') {
|
||||
+ relativePerma = path.join('/', relativePerma.replace(this.context.baseUrl, ''));
|
||||
+ }
|
||||
+ const p = path.join(this.context.outDir, relativePerma, 'index.html');
|
||||
+ return p;
|
||||
+ }
|
||||
}
|
||||
exports.PagesPlugin = PagesPlugin;
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,86 @@
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import Link from '@docusaurus/Link';
|
||||
|
||||
// Define the Button type to control the props that can be passed to the Button component.
|
||||
export type ButtonProps = {
|
||||
// The size prop can be one of the following values: 'sm', 'lg', 'small', 'medium', 'large', or null.
|
||||
// We'll convert 'small' to 'sm' and 'large' to 'lg' in the component. 'medium' will be considered null.
|
||||
size?: 'sm' | 'lg' | 'small' | 'medium' | 'large' | null;
|
||||
// The outline prop is a boolean that determines if the button should be an outline button.
|
||||
outline?: boolean;
|
||||
// The variant prop is a string that determines the color of the button.
|
||||
// It can be one of the following values: 'primary', 'secondary', 'danger', 'warning', 'success', 'info', 'link', or any other string.
|
||||
// The default value is 'primary'.
|
||||
variant: 'primary' | 'secondary' | 'danger' | 'warning' | 'success' | 'info' | 'link' | string;
|
||||
// The block prop is a boolean that determines if the button should be a block-level button.
|
||||
block?: boolean;
|
||||
// The disabled prop is a boolean that determines if the button should be disabled.
|
||||
disabled?: boolean;
|
||||
// The className prop is a string that allows you to add custom classes to the button.
|
||||
className?: string;
|
||||
// The style prop is an object that allows you to add custom styles to the button.
|
||||
style?: CSSProperties;
|
||||
// The link prop is a string that determines the URL the button should link to.
|
||||
link: string;
|
||||
// The label prop is a string that determines the text of the button.
|
||||
label: string;
|
||||
onClick?: () => any
|
||||
}
|
||||
|
||||
// Button component that accepts the specified props.
|
||||
export default function Button ({
|
||||
size = null,
|
||||
outline = false,
|
||||
variant = 'primary',
|
||||
block = false,
|
||||
disabled = false,
|
||||
className,
|
||||
style = {},
|
||||
link,
|
||||
label,
|
||||
onClick = () => null,
|
||||
...rest
|
||||
}: ButtonProps) {
|
||||
// Map the size prop values to corresponding CSS classes.
|
||||
const sizeMap = {
|
||||
sm: 'sm',
|
||||
small: 'sm',
|
||||
lg: 'lg',
|
||||
large: 'lg',
|
||||
medium: null,
|
||||
};
|
||||
const buttonSize = size ? sizeMap[size] : '';
|
||||
const sizeClass = buttonSize ? `button--${buttonSize}` : '';
|
||||
const outlineClass = outline ? 'button--outline' : '';
|
||||
const variantClass = variant ? `button--${variant}` : '';
|
||||
const blockClass = block ? 'button--block' : '';
|
||||
const disabledClass = disabled ? 'disabled' : '';
|
||||
// If the button is disabled, set the destination to null.
|
||||
const destination = disabled ? null : link;
|
||||
return (
|
||||
<Link to={destination}>
|
||||
<button
|
||||
className={clsx(
|
||||
'button',
|
||||
sizeClass,
|
||||
outlineClass,
|
||||
variantClass,
|
||||
blockClass,
|
||||
disabledClass,
|
||||
className
|
||||
)}
|
||||
style={style}
|
||||
role='button'
|
||||
aria-disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick()
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { ReactNode, CSSProperties } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import Link from '@docusaurus/Link';
|
||||
import Button, { ButtonProps } from './Button';
|
||||
|
||||
const leftButton = {
|
||||
borderRadius: 'var(--ifm-button-border-radius) 0 0 var(--ifm-button-border-radius)'
|
||||
}
|
||||
|
||||
const rightButton = {
|
||||
borderRadius: '0 var(--ifm-button-border-radius) var(--ifm-button-border-radius) 0'
|
||||
}
|
||||
|
||||
const middleButton = {
|
||||
borderRadius: '0'
|
||||
}
|
||||
|
||||
// Define the Button type to control the props that can be passed to the Button component.
|
||||
type ButtonGroup = {
|
||||
options: [string, string][]
|
||||
value?: string
|
||||
defaultValue?: string
|
||||
// The size prop can be one of the following values: 'sm', 'lg', 'small', 'medium', 'large', or null.
|
||||
// We'll convert 'small' to 'sm' and 'large' to 'lg' in the component. 'medium' will be considered null.
|
||||
size?: 'sm' | 'lg' | 'small' | 'medium' | 'large' | null;
|
||||
|
||||
variant: 'primary' | 'secondary' | 'danger' | 'warning' | 'success' | 'info' | 'link' | string;
|
||||
// The block prop is a boolean that determines if the button should be a block-level button.
|
||||
block?: boolean;
|
||||
// The disabled prop is a boolean that determines if the button should be disabled.
|
||||
disabled?: boolean;
|
||||
// The className prop is a string that allows you to add custom classes to the button.
|
||||
className?: string;
|
||||
// The style prop is an object that allows you to add custom styles to the button.
|
||||
style?: CSSProperties;
|
||||
onChange?: (val: string) => any
|
||||
}
|
||||
|
||||
// Button component that accepts the specified props.
|
||||
export default function ButtonGroup ({
|
||||
size = null,
|
||||
variant = 'primary',
|
||||
block = false,
|
||||
disabled = false,
|
||||
className,
|
||||
style,
|
||||
options,
|
||||
value,
|
||||
defaultValue,
|
||||
onChange = () => null
|
||||
}: ButtonGroup) {
|
||||
|
||||
if(options === undefined || options.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const buttons = options.map((opt, index) => {
|
||||
const props: ButtonProps = {
|
||||
style: middleButton,
|
||||
outline: true,
|
||||
size,
|
||||
variant,
|
||||
label: opt[1],
|
||||
link: '#',
|
||||
onClick: () => onChange(opt[0])
|
||||
};
|
||||
if(index === 0) {
|
||||
props.style = leftButton;
|
||||
} else if(index === options.length - 1) {
|
||||
props.style = rightButton;
|
||||
}
|
||||
|
||||
if((value ?? defaultValue) === opt[0]) {
|
||||
props.outline = false;
|
||||
}
|
||||
|
||||
return <Button key={index} {...props}/>
|
||||
|
||||
});
|
||||
|
||||
return <span>{buttons}</span>;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { Fragment, useMemo } from "react"
|
||||
import CodeBlock, {Props as CodeBlockProps} from '@theme/CodeBlock';
|
||||
import Admonition from '@theme/Admonition';
|
||||
import ErrorBoundary from "@docusaurus/ErrorBoundary"
|
||||
import Error from "@theme/Error"
|
||||
import { Simulate } from "react-dom/test-utils";
|
||||
import error = Simulate.error;
|
||||
import json5 from 'json5';
|
||||
import FileExample, { FileProps } from "./FileExample";
|
||||
|
||||
export interface FileConfigProps extends FileProps {
|
||||
data: string
|
||||
name: string
|
||||
configureAs?: 'client' | 'source'
|
||||
aio?: boolean
|
||||
filePathName?: string
|
||||
}
|
||||
|
||||
const FileConfigExample = (props: FileConfigProps) => {
|
||||
const {
|
||||
data,
|
||||
configureAs = 'source',
|
||||
aio = false,
|
||||
name,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
let configObj;
|
||||
// eslint-disable-next-line prefer-const
|
||||
try {
|
||||
configObj = json5.parse(data) as object[];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return <Admonition type="danger" title="Unexpected Error">
|
||||
<p>Example component crashed because of error!</p>
|
||||
<CodeBlock>{e.message}</CodeBlock>
|
||||
</Admonition>
|
||||
}
|
||||
|
||||
const transformed = useMemo(() => {
|
||||
let content: object;
|
||||
if(typeof data === 'string') {
|
||||
content = json5.parse(data);
|
||||
} else {
|
||||
content = data;
|
||||
}
|
||||
if(configureAs !== undefined && Array.isArray(content)) {
|
||||
const filtered = content.filter(x => x.configureAs === undefined || x.configureAs === configureAs);
|
||||
if(aio) {
|
||||
const configType = configureAs === 'client' ? 'clients' : 'sources';
|
||||
content = {[configType]: filtered.map(x => ({...x, type: name}))}
|
||||
} else {
|
||||
content = filtered;
|
||||
}
|
||||
}
|
||||
return content;
|
||||
},[data, configureAs, aio, name])
|
||||
|
||||
configObj = configObj.filter(x => x.configureAs === undefined || x.configureAs === configureAs);
|
||||
|
||||
return <FileExample data={transformed} {...rest}/>
|
||||
}
|
||||
|
||||
const WrappedFileConfigExample = (props: FileConfigProps) => {
|
||||
return <ErrorBoundary
|
||||
fallback={({error}) => (
|
||||
<div>
|
||||
<p>Example component crashed because of error: {error.message}.</p>
|
||||
</div>
|
||||
)}
|
||||
><FileConfigExample {...props} /></ErrorBoundary>
|
||||
}
|
||||
|
||||
export default WrappedFileConfigExample;
|
||||
@@ -1,44 +1,90 @@
|
||||
import React, { Fragment } from "react"
|
||||
import CodeBlock, {Props as CodeBlockProps} from '@theme/CodeBlock';
|
||||
import React, { Fragment, useMemo } from "react"
|
||||
import CodeBlock, { Props as CodeBlockProps } from '@theme/CodeBlock';
|
||||
import Admonition from '@theme/Admonition';
|
||||
import ErrorBoundary from "@docusaurus/ErrorBoundary"
|
||||
import Error from "@theme/Error"
|
||||
import { Simulate } from "react-dom/test-utils";
|
||||
import error = Simulate.error;
|
||||
import json5 from 'json5';
|
||||
import { useTypedLocalStorage } from "./useLocalStorage";
|
||||
import ButtonGroup from "./ButtonGroup";
|
||||
import EditorFetch from '@site/src/components/Schema/SchemaEditorFetch';
|
||||
import {useWindowSize} from '@docusaurus/theme-common';
|
||||
|
||||
export interface FileProps extends Omit<CodeBlockProps, 'children'> {
|
||||
data: string
|
||||
client?: boolean
|
||||
data: string | object
|
||||
filePathName?: string
|
||||
schemaName?: string
|
||||
interactive?: boolean
|
||||
defaultDisplay?: 'plain' | 'interactive'
|
||||
readOnlyAtWidth?: number
|
||||
}
|
||||
|
||||
const FileExample = (props: FileProps) => {
|
||||
const {
|
||||
data,
|
||||
client = false,
|
||||
interactive = true,
|
||||
defaultDisplay = 'interactive',
|
||||
schemaName,
|
||||
filePathName,
|
||||
readOnlyAtWidth = 1,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
let configObj;
|
||||
// eslint-disable-next-line prefer-const
|
||||
try {
|
||||
configObj = json5.parse(data) as object[];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return <Admonition type="danger" title="Unexpected Error">
|
||||
<p>Example component crashed because of error!</p>
|
||||
<CodeBlock>{e.message}</CodeBlock>
|
||||
</Admonition>
|
||||
const [fileExampleShow, setFileExampleShow] = useTypedLocalStorage('docusaurus.tab.fileExampleShow', defaultDisplay, false);
|
||||
|
||||
const windowSize = useWindowSize({desktopBreakpoint: readOnlyAtWidth});
|
||||
|
||||
let displaySwitch = null;
|
||||
|
||||
const isInteractive = interactive && windowSize === 'desktop'
|
||||
|
||||
if (isInteractive) {
|
||||
displaySwitch = (
|
||||
<ButtonGroup
|
||||
options={[['plain', 'Plain'], ['interactive', 'Interactive']]}
|
||||
defaultValue={defaultDisplay}
|
||||
value={fileExampleShow}
|
||||
variant="primary"
|
||||
size="medium"
|
||||
onChange={(val) => setFileExampleShow(val as 'interactive' | 'plain')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
configObj = configObj.filter(x => x.configureAs === undefined || x.configureAs === (client ? 'client' : 'source'));
|
||||
let fileNameContent = null;
|
||||
if(filePathName !== undefined && (isInteractive && fileExampleShow === 'interactive')) {
|
||||
fileNameContent = <code style={{float: 'right', paddingRight: '0.5em', paddingLeft: '0.5em'}}>{filePathName}</code>
|
||||
}
|
||||
|
||||
return <CodeBlock {...rest} language="json5">{JSON.stringify(configObj, null, 2)}</CodeBlock>
|
||||
let header = null;
|
||||
if(displaySwitch !== null || fileNameContent !== null) {
|
||||
header = (
|
||||
<div style={{marginBottom: '1em'}}>
|
||||
{displaySwitch}{fileNameContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const codeBlockContent = useMemo(() =>{
|
||||
if(isInteractive && fileExampleShow === 'interactive') {
|
||||
return null;
|
||||
}
|
||||
if(typeof data !== 'string') {
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
return data;
|
||||
}, [interactive, fileExampleShow, data]);
|
||||
|
||||
return (<Fragment>
|
||||
{header}
|
||||
{!isInteractive || fileExampleShow === 'plain' ? <CodeBlock title={filePathName} language="json5">{codeBlockContent}</CodeBlock> : null}
|
||||
{isInteractive && fileExampleShow === 'interactive' ? <EditorFetch configContent={data} schemaName={schemaName}/> : null}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const WrappedFileExample = (props: FileProps) => {
|
||||
return <ErrorBoundary
|
||||
fallback={({error}) => (
|
||||
fallback={({ error }) => (
|
||||
<div>
|
||||
<p>Example component crashed because of error: {error.message}.</p>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
import ExplorerFetch from '@site/src/components/Schema/ExplorerFetch';
|
||||
import EditorFetch from '@site/src/components/Schema/SchemaEditorFetch';
|
||||
import './GenericConfiguration.scss';
|
||||
import FileConfigExample from './FileConfigExample';
|
||||
import {useWindowSize} from '@docusaurus/theme-common';
|
||||
|
||||
<Tabs groupId="configType" queryString>
|
||||
<TabItem value="env" label="ENV">
|
||||
@@ -10,19 +15,96 @@ import CodeBlock from '@theme/CodeBlock';
|
||||
</TabItem>
|
||||
<TabItem value="file" label="File">
|
||||
<FileType>
|
||||
<div
|
||||
className="schemaWrapper"
|
||||
style={{
|
||||
display: "flex"
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '0 1em 0 1em', flexBasis: 'min-content', flexGrow: 2}}>
|
||||
<DetailsAdmo type="note" summary="Config Structure">
|
||||
This displays the **structure** of the [File Configuration](/configuration?configType=aio#configuration-types) for a <strong children={props.name}/> type <strong children={props.client ? 'Client' : 'Source'}/> with all possible properties, their shape, and descriptions/types. Use this to understand how to write a valid config.
|
||||
</DetailsAdmo>
|
||||
<ExplorerFetch name={`schemas/${props.config}`} />
|
||||
</div>
|
||||
<div style={{ flexBasis: 'auto', flexGrow: 3 }}>
|
||||
<div className="fileEditor">
|
||||
<DetailsAdmo type="note" summary="Config Example">
|
||||
This displays an **example config file** of a <strong children={props.name}/> <span children={props.client ? 'Client' : 'Source'}/> [File Configuration](/configuration?configType=aio#configuration-types) that adheres to the shown **Config Structure**.
|
||||
|
||||
<FileExample title={`CONFIG/${props.name}.json`} client={props.client ?? false} data={props.fileContent}/>
|
||||
* **text** mode lets you edit the JSON directly.
|
||||
* **tree** mode gives you a guided point-and-click editing experience that always keeps the JSON *syntax* valid.
|
||||
|
||||
or <SchemaLink lower objectName={props.config}/>
|
||||
Both modes **validate** that the configuraion is correct. Any errors show up as squiggly lines.
|
||||
|
||||
After you finish editing, switch to **text** and then copy all text to get a completed config.
|
||||
</DetailsAdmo>
|
||||
</div>
|
||||
<div className="fileEditorMobile">
|
||||
<DetailsAdmo type="warning" summary="Config Example Mobile Experience">
|
||||
This displays an **example config file** of a <strong children={props.name}/> <span children={props.client ? 'Client' : 'Source'}/> [File Configuration](/configuration?configType=aio#configuration-types) that adheres to the shown **Config Structure**.
|
||||
|
||||
The example config file editor is meant for a larger screen experience so **only the read-only example is shown.** Please use this site on a tablet/laptop/desktop to enable file editor features.
|
||||
</DetailsAdmo>
|
||||
</div>
|
||||
<FileConfigExample
|
||||
filePathName={`CONFIG/${props.name}.json`}
|
||||
configureAs={props.client ? 'client' : 'source'}
|
||||
data={props.fileContent}
|
||||
schemaName={props.config}
|
||||
readOnlyAtWidth={500}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FileType>
|
||||
</TabItem>
|
||||
<TabItem value="aio" label="AIO">
|
||||
<AIOType>
|
||||
<div
|
||||
className="schemaWrapper"
|
||||
style={{
|
||||
display: "flex"
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '0 1em 0 1em', flexBasis: 'min-content', flexGrow: 2}}>
|
||||
<DetailsAdmo type="note" summary="Config Structure">
|
||||
This displays the **structure** of the [all-in-one (`config.json`) configuration](/configuration?configType=aio#configuration-types) with all possible properties, their shape, and descriptions/types. Use this to understand how to write a valid config.
|
||||
|
||||
<AIOExample data={props.fileContent} client={props.client ?? false} name={props.name}/>
|
||||
Select the <strong children={props.name}/> type from the respective <code children={props.client ? 'clients' : 'sources'}/> node below, then expand it.
|
||||
</DetailsAdmo>
|
||||
<ExplorerFetch name={`schemas/aio`} />
|
||||
</div>
|
||||
<div style={{ flexBasis: 'auto', flexGrow: 3 }}>
|
||||
<div className="fileEditor">
|
||||
<DetailsAdmo type="note" summary="Config Example">
|
||||
This displays an **example config file** of a [all-in-one (`config.json`) configuration](/configuration?configType=aio#configuration-types) that includes a <strong children={props.name}/> <span children={props.client ? 'Client' : 'Source'}/> that adheres to the shown **Config Structure**.
|
||||
|
||||
* **text** mode lets you edit the JSON directly.
|
||||
* **tree** mode gives you a guided point-and-click editing experience that always keeps the JSON *syntax* valid.
|
||||
|
||||
or <SchemaLink lower objectName={props.config}/>
|
||||
Both modes **validate** that the configuraion is correct. Any errors show up as squiggly lines.
|
||||
|
||||
After you finish editing, switch to **text** and then copy all text to get a completed config.
|
||||
</DetailsAdmo>
|
||||
</div>
|
||||
<div className="fileEditorMobile">
|
||||
<DetailsAdmo type="warning" summary="Config Example Mobile Experience">
|
||||
This displays an **example config file** of a [all-in-one (`config.json`) configuration](/configuration?configType=aio#configuration-types) that includes a <strong children={props.name}/> <span children={props.client ? 'Client' : 'Source'}/> that adheres to the shown **Config Structure**.
|
||||
|
||||
The example config file editor is meant for a larger screen experience so **only the read-only example is shown.** Please use this site on a tablet/laptop/desktop to enable file editor features.
|
||||
</DetailsAdmo>
|
||||
</div>
|
||||
<FileConfigExample
|
||||
filePathName={`CONFIG/config.json`}
|
||||
configureAs={props.client ? 'client' : 'source'}
|
||||
data={props.fileContent}
|
||||
schemaName="aio"
|
||||
name={props.name}
|
||||
readOnlyAtWidth={500}
|
||||
aio
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AIOType>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
</Tabs>
|
||||
@@ -0,0 +1,26 @@
|
||||
.schemaWrapper {
|
||||
margin-top: 1em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
row-gap: 1em;
|
||||
column-gap: 1em;
|
||||
}
|
||||
|
||||
.fileEditorMobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1378px) {
|
||||
.schemaWrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
.fileEditor {
|
||||
display: none;
|
||||
}
|
||||
.fileEditorMobile {
|
||||
display: block
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
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,26 @@
|
||||
.schemaWrapper2 {
|
||||
margin: 1em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
row-gap: 1em;
|
||||
column-gap: 1em;
|
||||
}
|
||||
|
||||
.fileEditorMobile2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1280px) {
|
||||
.schemaWrapper2 {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
.fileEditor2 {
|
||||
display: none;
|
||||
}
|
||||
.fileEditorMobile2 {
|
||||
display: block
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,104 @@
|
||||
import React from "react"
|
||||
import React, {useEffect, useState} from "react"
|
||||
import BrowserOnly from "@docusaurus/BrowserOnly"
|
||||
import {useColorMode, useWindowSize} from '@docusaurus/theme-common';
|
||||
import CodeBlock from '@theme/CodeBlock';
|
||||
|
||||
import Schema from "@site/static/aio.json";
|
||||
import Schema from "@site/static/schemas/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")
|
||||
import './Playground.scss';
|
||||
import { JSONSchema, JsonSchemaViewer } from 'cf-json-schema-viz';
|
||||
import "./json-schema-viewer-styles.css";
|
||||
import "./modern-json-react-styles.css";
|
||||
import { Mode, ReactJsonEditor, createAjvValidator, type Content } from 'modern-react-json-editor';
|
||||
import f from "ajv-formats"
|
||||
import DetailsAdmo from "./AdmonitionDetails";
|
||||
|
||||
const validator = createAjvValidator({
|
||||
schema: Schema,
|
||||
ajvOptions: {
|
||||
strict: "log",
|
||||
strictSchema: "log",
|
||||
allowUnionTypes: true,
|
||||
},
|
||||
onCreateAjv: (ajv) => {
|
||||
ajv.addKeyword('deprecationMessage');
|
||||
f.default(ajv);
|
||||
return ajv;
|
||||
}
|
||||
});
|
||||
|
||||
// based on https://github.com/jy95/docusaurus-json-schema-plugin/blob/main/testsite/src/components/PlaygroundInner.tsx
|
||||
function PlaygroundInner(): JSX.Element {
|
||||
|
||||
const { colorMode } = useColorMode()
|
||||
const {
|
||||
colorMode, // the "effective" color mode, never null
|
||||
// colorModeChoice, // the color mode chosen by the user, can be null
|
||||
// setColorMode, // set the color mode chosen by the user
|
||||
} = useColorMode();
|
||||
const windowSize = useWindowSize({desktopBreakpoint: 500});
|
||||
|
||||
const [data, setData] = useState<Content>({json: ConfigExample });
|
||||
|
||||
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 className="schemaWrapper2">
|
||||
<div style={{ padding: '0 1em 0 1em', flexBasis: 'min-content', flexGrow: 2}}>
|
||||
<DetailsAdmo type="note" summary="Config Structure">
|
||||
<p>This displays the <strong>structure</strong> of the <a href="/configuration?configType=aio#configuration-types">all-in-one (<code>config.json</code>) configuration</a> with all possible properties, their shape, and descriptions/types.</p>
|
||||
|
||||
<p>Use this to understand how to write a valid config.</p>
|
||||
</DetailsAdmo>
|
||||
<JsonSchemaViewer
|
||||
schema={Schema as JSONSchema}
|
||||
// expanded={false}
|
||||
// hideTopBar={false}
|
||||
renderRootTreeLines={true}
|
||||
emptyText="No schema defined"
|
||||
defaultExpandedDepth={0}
|
||||
markup
|
||||
/>
|
||||
</div>
|
||||
<div style={{ boxSizing: "border-box", width: "50%" }}>
|
||||
<JSONSchemaEditor
|
||||
value={STRINGIFY_JSON(ConfigExample)}
|
||||
theme={colorMode === "dark" ? "vs-dark" : "vs"}
|
||||
schema={Schema}
|
||||
height={"70vh"}
|
||||
/>
|
||||
<div style={{ flexBasis: 'auto', flexGrow: 3 }}>
|
||||
<div className="fileEditor2">
|
||||
<DetailsAdmo type="note" summary="Config Example">
|
||||
<p>
|
||||
This displays an <strong>example config file</strong> of a <a href="/configuration?configType=aio#configuration-types">all-in-one (<code>config.json</code>) configuration</a> that adheres to the shown <strong>Config Structure.</strong>
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<strong>text</strong> mode lets you edit the JSON directly.
|
||||
</li>
|
||||
<li>
|
||||
<strong>tree</strong> mode gives you a guided point-and-click editing experience that always keeps the JSON <i>syntax</i> valid.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>Both modes <strong>validate</strong> that the configuraion is correct. Any errors show up as squiggly lines.</p>
|
||||
|
||||
<p>After you finish editing, switch to <strong>text</strong> and then copy all text to get a completed config.</p>
|
||||
</DetailsAdmo>
|
||||
</div>
|
||||
<div className="fileEditorMobile2">
|
||||
<DetailsAdmo type="warning" summary="Config Example Mobile Experience">
|
||||
<p>
|
||||
This displays an <strong>example config file</strong> of a <a href="/configuration?configType=aio#configuration-types">all-in-one (<code>config.json</code>) configuration</a> that adheres to the shown <strong>Config Structure.</strong>
|
||||
</p>
|
||||
<p>
|
||||
The example config file editor is meant for a larger screen experience so <strong>only the read-only example is shown.</strong> Please use this site on a tablet/laptop/desktop to enable file editor features.
|
||||
</p>
|
||||
</DetailsAdmo>
|
||||
</div>
|
||||
{windowSize === 'mobile' ? (<CodeBlock title={`CONFIG/config.json`} language="json5">{JSON.stringify(data, null, 2)}</CodeBlock>) : (
|
||||
<ReactJsonEditor
|
||||
validator={validator}
|
||||
theme={colorMode}
|
||||
content={data}
|
||||
mode={"text" as Mode.text}
|
||||
style={{height: 'initial'}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, {useEffect, useState} from "react"
|
||||
import BrowserOnly from "@docusaurus/BrowserOnly"
|
||||
|
||||
//import './Explorer.scss';
|
||||
import { JsonSchemaViewer } from 'cf-json-schema-viz';
|
||||
import '../json-schema-viewer-styles.css'
|
||||
|
||||
export interface ExplorerProps {
|
||||
schema: object
|
||||
}
|
||||
|
||||
// based on https://github.com/jy95/docusaurus-json-schema-plugin/blob/main/testsite/src/components/PlaygroundInner.tsx
|
||||
export default function Explorer(props: ExplorerProps): JSX.Element {
|
||||
|
||||
//injectStyles();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<JsonSchemaViewer
|
||||
schema={props.schema}
|
||||
renderRootTreeLines={true}
|
||||
emptyText="No schema defined"
|
||||
defaultExpandedDepth={0}
|
||||
markup
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// export default function PlaygroundComponent(): JSX.Element {
|
||||
// return (
|
||||
// <BrowserOnly fallback={<div>Loading...</div>}>
|
||||
// {() => {
|
||||
// return <PlaygroundInner />
|
||||
// }}
|
||||
// </BrowserOnly>
|
||||
// )
|
||||
// }
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, {useEffect, useState, Fragment} from "react"
|
||||
//import BrowserOnly from "@docusaurus/BrowserOnly"
|
||||
|
||||
import Explorer from './Explorer';
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
export interface ExplorerFetchProps {
|
||||
name: string
|
||||
}
|
||||
|
||||
export default function ExplorerFetchFunc(props: ExplorerFetchProps): JSX.Element {
|
||||
|
||||
const [schema, setSchema] = useState(undefined as undefined | Error | object);
|
||||
|
||||
const url = useBaseUrl(`/${props.name}.json`);
|
||||
|
||||
useEffect( () => {
|
||||
fetch(
|
||||
url,
|
||||
{
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
}
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((data) => setSchema(data))
|
||||
.catch( (err) => setSchema(err) )
|
||||
}, [setSchema, url])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{schema === undefined && <div>Loading ...</div>}
|
||||
{schema !== undefined && schema instanceof Error && <div>Houston we have a problem : {schema.message}</div>}
|
||||
{schema !== undefined && !(schema instanceof Error) && <Explorer schema={schema}/>}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
// export default function ExplorerFetch(props: ExplorerFetchProps): JSX.Element {
|
||||
// return (
|
||||
// <BrowserOnly fallback={<div>Loading...</div>}>
|
||||
// {() => {
|
||||
// return <ExplorerFetchFunc {...props} />
|
||||
// }}
|
||||
// </BrowserOnly>
|
||||
// )
|
||||
// }
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useEffect, useState, useMemo } from "react"
|
||||
import BrowserOnly from "@docusaurus/BrowserOnly"
|
||||
import { useColorMode } from '@docusaurus/theme-common';
|
||||
|
||||
const STRINGIFY_JSON = (json: unknown) => JSON.stringify(json, null, "\t");
|
||||
import "../modern-json-react-styles.css";
|
||||
import { JSONSchema, Mode, ReactJsonEditor, createAjvValidator, type Content, type Validator } from 'modern-react-json-editor';
|
||||
import f from "ajv-formats"
|
||||
import json5 from 'json5';
|
||||
|
||||
export interface SchemaEditorProps {
|
||||
config: object | string
|
||||
schema?: object
|
||||
height?: string
|
||||
}
|
||||
|
||||
// based on https://github.com/jy95/docusaurus-json-schema-plugin/blob/main/testsite/src/components/PlaygroundInner.tsx
|
||||
function SchemaEditorInner(props: SchemaEditorProps): JSX.Element {
|
||||
|
||||
const validator: Validator | undefined = useMemo(() => {
|
||||
if(props.schema === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return createAjvValidator({
|
||||
schema: props.schema as JSONSchema,
|
||||
ajvOptions: {
|
||||
strict: "log",
|
||||
strictSchema: "log",
|
||||
allowUnionTypes: true,
|
||||
},
|
||||
onCreateAjv: (ajv) => {
|
||||
ajv.addKeyword('deprecationMessage');
|
||||
f.default(ajv);
|
||||
return ajv;
|
||||
}
|
||||
});
|
||||
}, [props.schema]);
|
||||
|
||||
const initialData = useMemo(() => {
|
||||
let content: object;
|
||||
if(typeof props.config === 'string') {
|
||||
content = json5.parse(props.config);
|
||||
} else {
|
||||
content = props.config;
|
||||
}
|
||||
return {json: content};
|
||||
},[props.config])
|
||||
|
||||
|
||||
// const [data, setData] = useState({ json: props.config });
|
||||
|
||||
const {
|
||||
colorMode, // the "effective" color mode, never null
|
||||
} = useColorMode();
|
||||
|
||||
//const val = typeof props.config === 'object' ? STRINGIFY_JSON(props.config) : props.config;
|
||||
return (
|
||||
<ReactJsonEditor
|
||||
validator={validator}
|
||||
theme={colorMode}
|
||||
mode={"text" as Mode.text}
|
||||
content={initialData}
|
||||
style={{height: 'initial'}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SchemaEditor(props: SchemaEditorProps): JSX.Element {
|
||||
return (
|
||||
<BrowserOnly fallback={<div>Loading...</div>}>
|
||||
{() => {
|
||||
return <SchemaEditorInner {...props} />
|
||||
}}
|
||||
</BrowserOnly>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, {useEffect, useState, Fragment} from "react"
|
||||
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import SchemaEditor from "./SchemaEditor";
|
||||
import json5 from 'json5';
|
||||
|
||||
export interface SchemaEditorFetchProps {
|
||||
configContent: string | object
|
||||
schemaName?: string
|
||||
}
|
||||
|
||||
export default function ExplorerFetchFunc(props: SchemaEditorFetchProps): JSX.Element {
|
||||
|
||||
const {
|
||||
schemaName
|
||||
} = props;
|
||||
|
||||
const [schema, setSchema] = useState(undefined as undefined | Error | object | false);
|
||||
const [config, setConfig] = useState(undefined as undefined | Error | object);
|
||||
|
||||
const schemaUrl = schemaName === undefined ? undefined : useBaseUrl(`schemas/${props.schemaName}.json`);
|
||||
//const configUrl = props.configName !== undefined ? useBaseUrl(`/${props.configName}.json`) : undefined;
|
||||
useEffect( () => {
|
||||
if(schemaUrl === undefined) {
|
||||
setSchema(false);
|
||||
} else {
|
||||
fetch(
|
||||
schemaUrl,
|
||||
{
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
}
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((data) => setSchema(data))
|
||||
.catch( (err) => setSchema(err) )
|
||||
}
|
||||
}, [setSchema, schemaUrl])
|
||||
|
||||
// useEffect( () => {
|
||||
// if(configUrl !== undefined) {
|
||||
// fetch(
|
||||
// configUrl,
|
||||
// {
|
||||
// headers: {
|
||||
// 'Accept': 'application/json',
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
// .then((response) => response.json())
|
||||
// .then((data) => setConfig(data))
|
||||
// .catch( (err) => setConfig(err) )
|
||||
// } else if(props.configContent !== undefined) {
|
||||
// if(typeof props.configContent === 'string') {
|
||||
// setConfig(json5.parse(props.configContent));
|
||||
// } else {
|
||||
// setConfig(props.configContent);
|
||||
// }
|
||||
// }
|
||||
// }, [setConfig, configUrl, props.configContent])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{schema === undefined && <div>Loading ...</div>}
|
||||
{schema !== undefined && schema instanceof Error && <div>Houston we have a problem : {schema.message}</div>}
|
||||
{schema !== undefined && !(schema instanceof Error) &&
|
||||
<SchemaEditor
|
||||
schema={schema === false ? undefined : schema}
|
||||
config={props.configContent}
|
||||
/>}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
/* JSON Schema Viewer - Pure CSS with CSS Custom Properties */
|
||||
|
||||
/* ========================================
|
||||
Theme Variables - Light Mode (default)
|
||||
Define at :root so portaled elements inherit them
|
||||
======================================== */
|
||||
:root {
|
||||
--jsv-font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
--jsv-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||
--jsv-font-size: 0.875rem;
|
||||
|
||||
/* Colors - Light Mode defaults */
|
||||
--jsv-color-body: #1a1a1a;
|
||||
--jsv-color-muted: #6b7280;
|
||||
--jsv-color-border: #e5e7eb;
|
||||
--jsv-color-canvas: #f4f4f5;
|
||||
--jsv-color-canvas-tint: #f4f4f5;
|
||||
--jsv-color-bg: #ffffff;
|
||||
|
||||
/* Semantic colors - Cloudflare orange as primary */
|
||||
--jsv-color-primary: #f6821f;
|
||||
--jsv-color-danger: #dc2626;
|
||||
--jsv-color-warning: #f97316;
|
||||
--jsv-color-success: #16a34a;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Theme Variables - Dark Mode
|
||||
Cloudflare docs dark theme colors
|
||||
======================================== */
|
||||
:root[data-theme="dark"],
|
||||
[data-theme="dark"] {
|
||||
--jsv-color-body: #e4e4e7;
|
||||
--jsv-color-muted: #a1a1aa;
|
||||
--jsv-color-border: #3f3f46;
|
||||
--jsv-color-canvas: #27272a;
|
||||
--jsv-color-canvas-tint: #3f3f46;
|
||||
--jsv-color-bg: #18181b;
|
||||
|
||||
--jsv-color-primary: #f6821f;
|
||||
--jsv-color-danger: #f87171;
|
||||
--jsv-color-warning: #fb923c;
|
||||
--jsv-color-success: #4ade80;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Base Styles
|
||||
======================================== */
|
||||
.jsv-root {
|
||||
font-size: var(--jsv-font-size);
|
||||
font-family: var(--jsv-font-sans);
|
||||
color: var(--jsv-color-body);
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.jsv-content {
|
||||
padding-left: 2rem;
|
||||
padding-right: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-top: 0.375rem;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Schema Row
|
||||
======================================== */
|
||||
.jsv-row {
|
||||
position: relative;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.jsv-row-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
max-width: 100%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.jsv-row-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
/* Nubbin - horizontal connector from left border to property */
|
||||
.jsv-nubbin {
|
||||
width: 0.75rem;
|
||||
height: 0;
|
||||
border-top: 1px solid var(--jsv-color-border);
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
margin-left: -1rem;
|
||||
}
|
||||
|
||||
.jsv-nubbin-short {
|
||||
width: 0.25rem;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Child Stack (nested content with left border)
|
||||
======================================== */
|
||||
.jsv-child-stack {
|
||||
font-size: var(--jsv-font-size);
|
||||
}
|
||||
|
||||
.jsv-child-stack-nested {
|
||||
border-left: 1px solid var(--jsv-color-border);
|
||||
margin-left: 0.5rem;
|
||||
padding-left: 1rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Property Name
|
||||
======================================== */
|
||||
.jsv-property-name {
|
||||
font-family: var(--jsv-font-mono);
|
||||
font-weight: 600;
|
||||
margin-right: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Type Display
|
||||
======================================== */
|
||||
.jsv-type {
|
||||
color: var(--jsv-color-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.jsv-types-container {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
font-size: var(--jsv-font-size);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Badges (required, deprecated, read-only)
|
||||
======================================== */
|
||||
.jsv-badges {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.jsv-badge {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.jsv-badge-required {
|
||||
color: var(--jsv-color-warning);
|
||||
}
|
||||
|
||||
.jsv-badge-deprecated {
|
||||
color: var(--jsv-color-warning);
|
||||
}
|
||||
|
||||
.jsv-badge-readonly {
|
||||
color: var(--jsv-color-muted);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Divider (horizontal line on hover)
|
||||
======================================== */
|
||||
.jsv-divider {
|
||||
height: 1px;
|
||||
flex: 1;
|
||||
margin-left: 0.75rem;
|
||||
margin-right: 0.75rem;
|
||||
min-width: 1rem;
|
||||
background-color: transparent;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.jsv-divider-visible {
|
||||
background-color: var(--jsv-color-border);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Description
|
||||
======================================== */
|
||||
.jsv-description {
|
||||
font-size: var(--jsv-font-size);
|
||||
color: var(--jsv-color-muted);
|
||||
margin-top: 0.0625rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Validations
|
||||
======================================== */
|
||||
.jsv-validations {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.jsv-validations-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.jsv-validation {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.375rem;
|
||||
color: var(--jsv-color-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.jsv-validation-label {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.jsv-validation-values {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.jsv-validation-value {
|
||||
padding: 0.0625rem 0.375rem;
|
||||
background-color: var(--jsv-color-canvas-tint);
|
||||
color: var(--jsv-color-muted);
|
||||
border: 1px solid var(--jsv-color-border);
|
||||
border-radius: 0.25rem;
|
||||
font-family: var(--jsv-font-mono);
|
||||
font-size: 0.75rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Caret (expand/collapse icon)
|
||||
======================================== */
|
||||
.jsv-caret {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-left: -1.5rem;
|
||||
margin-right: 0.25rem;
|
||||
color: var(--jsv-color-muted);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.jsv-caret:hover {
|
||||
color: var(--jsv-color-body);
|
||||
}
|
||||
|
||||
.jsv-caret-expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Error Display
|
||||
======================================== */
|
||||
.jsv-error {
|
||||
color: var(--jsv-color-danger);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Change Annotations (added/removed/modified)
|
||||
======================================== */
|
||||
.jsv-annotation {
|
||||
position: absolute;
|
||||
width: 0.25rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.125rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.jsv-annotation-added {
|
||||
background-color: var(--jsv-color-success);
|
||||
}
|
||||
|
||||
.jsv-annotation-removed {
|
||||
background-color: var(--jsv-color-danger);
|
||||
}
|
||||
|
||||
.jsv-annotation-modified {
|
||||
background-color: var(--jsv-color-warning);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Path Crumbs (sticky breadcrumb header)
|
||||
Wrapper is sticky with height:0 (no layout impact)
|
||||
Inner crumbs div is absolute, overlays content
|
||||
======================================== */
|
||||
.jsv-crumbs {
|
||||
height: 2rem;
|
||||
padding-left: 2rem;
|
||||
padding-right: 1rem;
|
||||
font-family: var(--jsv-font-mono);
|
||||
font-size: var(--jsv-font-size);
|
||||
line-height: 1;
|
||||
z-index: 20;
|
||||
background-color: var(--sl-color-bg, var(--jsv-color-bg, #fff));
|
||||
color: var(--jsv-color-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
border-bottom: 1px solid var(--jsv-color-border);
|
||||
border-top-left-radius: 0.5rem;
|
||||
border-top-right-radius: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
/* Hidden by default */
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .jsv-crumbs,
|
||||
:root[data-theme="dark"] .jsv-crumbs {
|
||||
border-bottom-color: var(--sl-color-gray-5, #52525b);
|
||||
}
|
||||
|
||||
.jsv-crumbs[data-visible="true"] {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.jsv-crumb-current {
|
||||
color: var(--jsv-color-body);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Select / Menu Dropdown
|
||||
======================================== */
|
||||
.jsv-select-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.125rem 0.5rem;
|
||||
font-size: var(--jsv-font-size);
|
||||
font-family: var(--jsv-font-sans);
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--jsv-color-border);
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
color: var(--jsv-color-muted);
|
||||
line-height: 1.4;
|
||||
transition: background-color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.jsv-select-trigger:hover {
|
||||
background-color: var(--jsv-color-canvas);
|
||||
border-color: var(--jsv-color-muted);
|
||||
}
|
||||
|
||||
.jsv-select-trigger:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px var(--jsv-color-primary);
|
||||
}
|
||||
|
||||
.jsv-select-positioner {
|
||||
z-index: 9999 !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.jsv-select-popup {
|
||||
background-color: var(--jsv-color-bg);
|
||||
border: 1px solid var(--jsv-color-border);
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
padding: 0.25rem 0;
|
||||
min-width: 10rem;
|
||||
overflow: hidden;
|
||||
font-family: var(--jsv-font-sans);
|
||||
font-size: var(--jsv-font-size);
|
||||
}
|
||||
|
||||
.jsv-select-item {
|
||||
padding: 0.5rem 0.75rem;
|
||||
padding-left: 2rem;
|
||||
font-size: var(--jsv-font-size);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
outline: none;
|
||||
color: var(--jsv-color-body);
|
||||
background-color: var(--jsv-color-bg);
|
||||
position: relative;
|
||||
transition: background-color 0.1s ease;
|
||||
}
|
||||
|
||||
.jsv-select-item:hover,
|
||||
.jsv-select-item:focus,
|
||||
.jsv-select-item[data-highlighted] {
|
||||
background-color: var(--jsv-color-canvas);
|
||||
}
|
||||
|
||||
.jsv-select-item[data-selected] {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.jsv-select-item-indicator {
|
||||
position: absolute;
|
||||
left: 0.625rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--jsv-color-primary);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Top Level Styles
|
||||
======================================== */
|
||||
.jsv-top-label {
|
||||
font-family: var(--jsv-font-mono);
|
||||
font-weight: 600;
|
||||
font-size: var(--jsv-font-size);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.jsv-top-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.jsv-combiner-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--jsv-color-muted);
|
||||
font-size: var(--jsv-font-size);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Link Styles
|
||||
======================================== */
|
||||
.jsv-link {
|
||||
color: var(--jsv-color-primary);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.jsv-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Misc Helpers
|
||||
======================================== */
|
||||
.jsv-cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.jsv-ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
.jse-theme-dark{--jse-theme: dark;--jse-theme-color: #2f6dd0;--jse-theme-color-highlight: #467cd2;--jse-background-color: #1e1e1e;--jse-text-color: #d4d4d4;--jse-text-color-inverse: #4d4d4d;--jse-main-border: 1px solid #4f4f4f;--jse-menu-color: #fff;--jse-modal-background: #2f2f2f;--jse-modal-overlay-background: rgba(0, 0, 0, .5);--jse-modal-code-background: #2f2f2f;--jse-tooltip-color: var(--jse-text-color);--jse-tooltip-background: #4b4b4b;--jse-tooltip-border: 1px solid #737373;--jse-tooltip-action-button-color: inherit;--jse-tooltip-action-button-background: #737373;--jse-panel-background: #333333;--jse-panel-background-border: 1px solid #464646;--jse-panel-color: var(--jse-text-color);--jse-panel-color-readonly: #737373;--jse-panel-border: 1px solid #3c3c3c;--jse-panel-button-color-highlight: #e5e5e5;--jse-panel-button-background-highlight: #464646;--jse-navigation-bar-background: #656565;--jse-navigation-bar-background-highlight: #7e7e7e;--jse-navigation-bar-dropdown-color: var(--jse-text-color);--jse-context-menu-background: #4b4b4b;--jse-context-menu-background-highlight: #595959;--jse-context-menu-separator-color: #595959;--jse-context-menu-color: var(--jse-text-color);--jse-context-menu-pointer-background: #737373;--jse-context-menu-pointer-background-highlight: #818181;--jse-context-menu-pointer-color: var(--jse-context-menu-color);--jse-key-color: #9cdcfe;--jse-value-color: var(--jse-text-color);--jse-value-color-number: #b5cea8;--jse-value-color-boolean: #569cd6;--jse-value-color-null: #569cd6;--jse-value-color-string: #ce9178;--jse-value-color-url: #ce9178;--jse-delimiter-color: #949494;--jse-edit-outline: 2px solid var(--jse-text-color);--jse-selection-background-color: #464646;--jse-selection-background-inactive-color: #333333;--jse-hover-background-color: #343434;--jse-active-line-background-color: rgba(255, 255, 255, .06);--jse-search-match-background-color: #343434;--jse-collapsed-items-background-color: #333333;--jse-collapsed-items-selected-background-color: #565656;--jse-collapsed-items-link-color: #b2b2b2;--jse-collapsed-items-link-color-highlight: #ec8477;--jse-search-match-color: #724c27;--jse-search-match-outline: 1px solid #966535;--jse-search-match-active-color: #9f6c39;--jse-search-match-active-outline: 1px solid #bb7f43;--jse-tag-background: #444444;--jse-tag-color: #bdbdbd;--jse-table-header-background: #333333;--jse-table-header-background-highlight: #424242;--jse-table-row-odd-background: rgba(255, 255, 255, .1);--jse-input-background: #3d3d3d;--jse-input-border: var(--jse-main-border);--jse-button-background: #808080;--jse-button-background-highlight: #7a7a7a;--jse-button-color: #e0e0e0;--jse-button-secondary-background: #494949;--jse-button-secondary-background-highlight: #5d5d5d;--jse-button-secondary-background-disabled: #9d9d9d;--jse-button-secondary-color: var(--jse-text-color);--jse-a-color: #55abff;--jse-a-color-highlight: #4387c9;--jse-svelte-select-background: #3d3d3d;--jse-svelte-select-border: 1px solid #4f4f4f;--list-background: #3d3d3d;--item-hover-bg: #505050;--multi-item-bg: #5b5b5b;--input-color: #d4d4d4;--multi-clear-bg: #8a8a8a;--multi-item-clear-icon-color: #d4d4d4;--multi-item-outline: 1px solid #696969;--list-shadow: 0 2px 8px 0 rgba(0, 0, 0, .4);--jse-color-picker-background: #656565;--jse-color-picker-border-box-shadow: #8c8c8c 0 0 0 1px}
|
||||
@@ -1,2 +1 @@
|
||||
[**AIO** Config Type](/configuration?configType=aio#configuration-types)
|
||||
{props.children}
|
||||
@@ -1,3 +1,5 @@
|
||||
[**ENV** Config Type](/configuration?configType=env#configuration-types)
|
||||
:::note[Configuration Type]
|
||||
This is configuration for the **ENV** [Config Type](/configuration?configType=env#configuration-types).
|
||||
:::
|
||||
|
||||
{props.children}
|
||||
@@ -1,3 +1 @@
|
||||
[**File** Config Type](/configuration?configType=file#configuration-types)
|
||||
|
||||
{props.children}
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Inspired by typings from https://github.com/michal-worwag/hooks-ts/blob/main/packages/hooks-ts/src/useLocalStorage/useLocalStorage.ts
|
||||
* and global updates/dedup from https://github.com/kaansey/react-localstorage-hook/blob/master/src/useLocalStorage/useLocalStorage.ts
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||
|
||||
const localTypedStorageHook = <T>(key: string, initialValue: T, json: boolean = true): [T, (value: T) => void] => {
|
||||
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
const item = window.localStorage.getItem(key);
|
||||
if (json) {
|
||||
try {
|
||||
return item ? (JSON.parse(item) as T) : initialValue;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return initialValue;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if (null === item) {
|
||||
return initialValue;
|
||||
}
|
||||
if (item === 'true') {
|
||||
return true as T;
|
||||
}
|
||||
if (item === 'false') {
|
||||
return false as T;
|
||||
}
|
||||
if (isNumeric(item)) {
|
||||
if (item.includes('.')) {
|
||||
return parseFloat(item) as T;
|
||||
}
|
||||
return parseInt(item) as T;
|
||||
}
|
||||
return item as T;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return initialValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const updateValue = useCallback(
|
||||
newValue => {
|
||||
setStoredValue(() => {
|
||||
if(json) {
|
||||
localStorage.setItem(key, JSON.stringify(newValue))
|
||||
} else {
|
||||
localStorage.setItem(key, newValue)
|
||||
}
|
||||
return newValue
|
||||
})
|
||||
},
|
||||
[key]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const onStorage = (event: any) => {
|
||||
if (event.storageArea === localStorage && event.key === key) {
|
||||
setStoredValue(
|
||||
event.newValue === null ? initialValue : event.newValue
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('storage', onStorage)
|
||||
|
||||
return () => window.removeEventListener('storage', onStorage)
|
||||
});
|
||||
|
||||
return [storedValue, updateValue];
|
||||
|
||||
}
|
||||
|
||||
const isNumeric = (num: any) => (typeof(num) === 'number' || typeof(num) === "string" && num.trim() !== '') && !isNaN(num as number);
|
||||
|
||||
const createTypedLocalStorageHook = <T>(key: string, defaultValue: T, json?: boolean) => {
|
||||
const updates: Array<any> = []
|
||||
|
||||
return () => {
|
||||
const [value, setValue] = localTypedStorageHook<T>(key, defaultValue, json)
|
||||
const updateValue = useCallback((newValue: T) => {
|
||||
for (const update of updates) {
|
||||
update(newValue)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
updates.push(setValue)
|
||||
return () => {
|
||||
updates.splice(updates.indexOf(setValue), 1)
|
||||
}
|
||||
}, [setValue])
|
||||
|
||||
return [value, updateValue]
|
||||
}
|
||||
}
|
||||
|
||||
const hookInstances: Record<string, any> = {}
|
||||
|
||||
export const useTypedLocalStorage = <T>(key: string, defaultValue: T, json?: boolean): [T, (value: T) => void] => {
|
||||
if (key in hookInstances) {
|
||||
return hookInstances[key]()
|
||||
}
|
||||
|
||||
// prevents usage during server static rendering
|
||||
if(!ExecutionEnvironment.canUseDOM) {
|
||||
return [defaultValue, () => null];
|
||||
}
|
||||
|
||||
hookInstances[key] = createTypedLocalStorageHook(key, defaultValue, json)
|
||||
return hookInstances[key]()
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
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
|
||||
@@ -32,7 +32,7 @@ export default function Playground(): JSX.Element {
|
||||
return (
|
||||
<Layout
|
||||
title={`Playground`}
|
||||
description="Playground of docusaurus-json-schema-plugin"
|
||||
description="Playground"
|
||||
>
|
||||
<PlaygroundComponent />
|
||||
</Layout>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,92 @@
|
||||
import type { DocsPageData, ImageRenderer } from '@bony_chops/docusaurus-og'
|
||||
import { readFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
// const logo = (<svg
|
||||
// id="Layer_1"
|
||||
// data-name="Layer 1"
|
||||
// xmlns="http://www.w3.org/2000/svg"
|
||||
// width="128"
|
||||
// height="128"
|
||||
// viewBox="0 0 128 128"
|
||||
// fill="#c34a36"
|
||||
// >
|
||||
// <path className="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 className="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>);
|
||||
|
||||
const DocContent = (props: { data: DocsPageData }) => {
|
||||
const { data } = props;
|
||||
return (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
textAlign: 'center',
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: "#1b1b1d",
|
||||
color: 'white',
|
||||
justifyContent: 'flex-start',
|
||||
flexDirection: 'column',
|
||||
flexWrap: 'wrap',
|
||||
padding: '3rem 3rem 0 3rem',
|
||||
}}>
|
||||
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '30px'
|
||||
}}>
|
||||
<img src="https://docs.multi-scrobbler.app/img/icon.png" width={120} height={120} />
|
||||
<div style={{
|
||||
fontSize: 125,
|
||||
//fontWeight: 600
|
||||
}}>Multi-Scrobbler</div>
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 50,
|
||||
color: "#c8c8c8",
|
||||
marginTop: '1rem'
|
||||
}}
|
||||
>Listen everywhere, Scrobble anywhere</div>
|
||||
<div style={{
|
||||
marginTop: '4rem',
|
||||
display: 'flex',
|
||||
alignContent: 'flex-start',
|
||||
justifyContent: 'flex-start',
|
||||
flexDirection: 'column',
|
||||
gap: '10px',
|
||||
paddingLeft: '0.5em',
|
||||
fontSize: 75
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
textWrap: 'balance',
|
||||
textAlign: 'left'
|
||||
}}>{data.metadata.frontMatter?.description ?? data.metadata.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const docs: ImageRenderer<DocsPageData> = (data, context) => [
|
||||
<DocContent data={data} />,
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Inter',
|
||||
data: readFileSync(
|
||||
// must be a STATIC (not variable) font due to a bug in satori
|
||||
// https://github.com/vercel/satori/issues/162
|
||||
// use static-font.sh to convert a variable font to static
|
||||
join(__dirname, '../../src/socialCard/AdwaitaSans-Regular-StaticSubset.ttf'),
|
||||
),
|
||||
weight: 400,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# based on https://github.com/Kikobeats/og.kikobeats.com/blob/master/static-font.sh
|
||||
|
||||
# 1) Freeze SF Pro to Bold (700) and make it fully static
|
||||
fonttools varLib.instancer AdwaitaSans-Regular.ttf \
|
||||
wght=400 \
|
||||
--static \
|
||||
-o AdwaitaSans-Regular-Static.ttf
|
||||
|
||||
# 2) Subset to only the glyphs you actually need (Satori-friendly)
|
||||
fonttools subset AdwaitaSans-Regular-Static.ttf \
|
||||
--text="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#\$%^&*()-_=+[]{};:'\",.<>/? " \
|
||||
--no-hinting \
|
||||
--name-legacy \
|
||||
--output-file=AdwaitaSans-Regular-StaticSubset.ttf
|
||||
|
||||
@@ -9,6 +9,8 @@ import EnvType from "@site/src/components/snippets/_env-config.mdx";
|
||||
import FileType from "@site/src/components/snippets/_file-config.mdx";
|
||||
import AIOType from "@site/src/components/snippets/_aio-config.mdx";
|
||||
import Config from "@site/src/components/GenericConfiguration.mdx";
|
||||
import Button from '../components/Button';
|
||||
import ButtonGroup from '../components/ButtonGroup';
|
||||
|
||||
export default {
|
||||
// Re-use the default mapping
|
||||
@@ -20,5 +22,7 @@ export default {
|
||||
FileType,
|
||||
AIOType,
|
||||
Config,
|
||||
DetailsAdmo
|
||||
DetailsAdmo,
|
||||
Button,
|
||||
ButtonGroup
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 240 KiB |
@@ -0,0 +1,925 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": { "type": "grafana", "uid": "-- Grafana --" },
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "red", "value": 1 }
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 4, "x": 0, "y": 0 },
|
||||
"id": 1,
|
||||
"options": {
|
||||
"colorMode": "background",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" },
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "10.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "sum(multiscrobbler_source_issues)",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Total Source Issues",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "red", "value": 1 }
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 4, "x": 4, "y": 0 },
|
||||
"id": 2,
|
||||
"options": {
|
||||
"colorMode": "background",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" },
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "10.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "sum(multiscrobbler_client_issues)",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Total Client Issues",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 300000000 },
|
||||
{ "color": "red", "value": 500000000 }
|
||||
]
|
||||
},
|
||||
"unit": "decbytes"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 4, "x": 8, "y": 0 },
|
||||
"id": 25,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" },
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "10.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "process_resident_memory_bytes{job=\"multi-scrobbler\"}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Memory (RSS)",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"mappings": [],
|
||||
"max": 1,
|
||||
"min": 0,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 0.5 },
|
||||
{ "color": "red", "value": 0.8 }
|
||||
]
|
||||
},
|
||||
"unit": "percentunit"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 4, "x": 12, "y": 0 },
|
||||
"id": 26,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" },
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "10.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "rate(process_cpu_seconds_total{job=\"multi-scrobbler\"}[1m])",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "CPU Usage",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 0.05 },
|
||||
{ "color": "red", "value": 0.1 }
|
||||
]
|
||||
},
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 4, "x": 16, "y": 0 },
|
||||
"id": 27,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" },
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "10.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "nodejs_eventloop_lag_seconds{job=\"multi-scrobbler\"}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Event Loop Lag",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": {
|
||||
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
|
||||
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
|
||||
"fillOpacity": 10, "gradientMode": "none",
|
||||
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
|
||||
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
|
||||
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
|
||||
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
|
||||
"thresholdsStyle": { "mode": "off" }
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 4, "x": 20, "y": 0 },
|
||||
"id": 3,
|
||||
"options": {
|
||||
"legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": false },
|
||||
"tooltip": { "mode": "single", "sort": "none" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "sum(rate(multiscrobbler_source_discovered[5m]))",
|
||||
"legendFormat": "Discoveries/sec",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Discovery Rate",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 4 },
|
||||
"id": 5,
|
||||
"panels": [],
|
||||
"title": "Sources",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": {
|
||||
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
|
||||
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
|
||||
"fillOpacity": 10, "gradientMode": "none",
|
||||
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
|
||||
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
|
||||
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
|
||||
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
|
||||
"thresholdsStyle": { "mode": "off" }
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 5 },
|
||||
"id": 6,
|
||||
"options": {
|
||||
"legend": { "calcs": ["last"], "displayMode": "table", "placement": "right", "showLegend": true },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "multiscrobbler_source_discovered",
|
||||
"legendFormat": "{{name}} ({{type}})",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Plays Discovered by Source",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": {
|
||||
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
|
||||
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
|
||||
"fillOpacity": 10, "gradientMode": "none",
|
||||
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
|
||||
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
|
||||
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
|
||||
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
|
||||
"thresholdsStyle": { "mode": "off" }
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [{ "color": "green", "value": null }, { "color": "red", "value": 1 }]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 5 },
|
||||
"id": 7,
|
||||
"options": {
|
||||
"legend": { "calcs": ["last"], "displayMode": "table", "placement": "right", "showLegend": true },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "multiscrobbler_source_issues",
|
||||
"legendFormat": "{{name}} ({{type}})",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Source Issues",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 13 },
|
||||
"id": 8,
|
||||
"panels": [],
|
||||
"title": "Clients",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": {
|
||||
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
|
||||
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
|
||||
"fillOpacity": 10, "gradientMode": "none",
|
||||
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
|
||||
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
|
||||
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
|
||||
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
|
||||
"thresholdsStyle": { "mode": "off" }
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 8, "x": 0, "y": 14 },
|
||||
"id": 9,
|
||||
"options": {
|
||||
"legend": { "calcs": ["last"], "displayMode": "table", "placement": "bottom", "showLegend": true },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "multiscrobbler_client_scrobbled",
|
||||
"legendFormat": "{{name}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Total Scrobbles by Client",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": {
|
||||
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
|
||||
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
|
||||
"fillOpacity": 10, "gradientMode": "none",
|
||||
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
|
||||
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
|
||||
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
|
||||
"spanNulls": false, "stacking": { "group": "A", "mode": "normal" },
|
||||
"thresholdsStyle": { "mode": "off" }
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 8, "x": 8, "y": 14 },
|
||||
"id": 10,
|
||||
"options": {
|
||||
"legend": { "calcs": ["last"], "displayMode": "table", "placement": "bottom", "showLegend": true },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "multiscrobbler_client_queued",
|
||||
"legendFormat": "{{name}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Queued Plays by Client",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": {
|
||||
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
|
||||
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
|
||||
"fillOpacity": 10, "gradientMode": "none",
|
||||
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
|
||||
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
|
||||
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
|
||||
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
|
||||
"thresholdsStyle": { "mode": "off" }
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [{ "color": "green", "value": null }, { "color": "red", "value": 1 }]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 8, "x": 16, "y": 14 },
|
||||
"id": 11,
|
||||
"options": {
|
||||
"legend": { "calcs": ["last"], "displayMode": "table", "placement": "bottom", "showLegend": true },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "multiscrobbler_client_issues",
|
||||
"legendFormat": "{{name}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Client Issues",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 22 },
|
||||
"id": 12,
|
||||
"panels": [],
|
||||
"title": "Node.js Performance",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": {
|
||||
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
|
||||
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
|
||||
"fillOpacity": 10, "gradientMode": "none",
|
||||
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
|
||||
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
|
||||
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
|
||||
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
|
||||
"thresholdsStyle": { "mode": "off" }
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
|
||||
"unit": "decbytes"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 8, "x": 0, "y": 23 },
|
||||
"id": 17,
|
||||
"options": {
|
||||
"legend": { "calcs": ["last", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "process_resident_memory_bytes{job=\"multi-scrobbler\"}",
|
||||
"legendFormat": "Resident Memory (RSS)",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "nodejs_heap_size_total_bytes{job=\"multi-scrobbler\"}",
|
||||
"legendFormat": "Heap Total",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "nodejs_heap_size_used_bytes{job=\"multi-scrobbler\"}",
|
||||
"legendFormat": "Heap Used",
|
||||
"refId": "C"
|
||||
}
|
||||
],
|
||||
"title": "Memory Usage",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": {
|
||||
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
|
||||
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
|
||||
"fillOpacity": 10, "gradientMode": "none",
|
||||
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
|
||||
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
|
||||
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
|
||||
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
|
||||
"thresholdsStyle": { "mode": "off" }
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 0.05 },
|
||||
{ "color": "red", "value": 0.1 }
|
||||
]
|
||||
},
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 8, "x": 8, "y": 23 },
|
||||
"id": 18,
|
||||
"options": {
|
||||
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "nodejs_eventloop_lag_seconds{job=\"multi-scrobbler\"}",
|
||||
"legendFormat": "Current Lag",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "nodejs_eventloop_lag_mean_seconds{job=\"multi-scrobbler\"}",
|
||||
"legendFormat": "Mean Lag",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "nodejs_eventloop_lag_p99_seconds{job=\"multi-scrobbler\"}",
|
||||
"legendFormat": "P99 Lag",
|
||||
"refId": "C"
|
||||
}
|
||||
],
|
||||
"title": "Event Loop Lag",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": {
|
||||
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
|
||||
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
|
||||
"fillOpacity": 10, "gradientMode": "none",
|
||||
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
|
||||
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
|
||||
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
|
||||
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
|
||||
"thresholdsStyle": { "mode": "off" }
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
|
||||
"unit": "percentunit"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 8, "x": 16, "y": 23 },
|
||||
"id": 19,
|
||||
"options": {
|
||||
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "bottom", "showLegend": true },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "rate(process_cpu_user_seconds_total{job=\"multi-scrobbler\"}[5m])",
|
||||
"legendFormat": "User CPU",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "rate(process_cpu_system_seconds_total{job=\"multi-scrobbler\"}[5m])",
|
||||
"legendFormat": "System CPU",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "CPU Usage",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"mappings": [],
|
||||
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 4, "x": 0, "y": 31 },
|
||||
"id": 20,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" },
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "10.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "nodejs_active_handles_total{job=\"multi-scrobbler\"}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Active Handles",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "thresholds" },
|
||||
"mappings": [],
|
||||
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 4, "x": 4, "y": 31 },
|
||||
"id": 21,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": { "values": false, "calcs": ["lastNotNull"], "fields": "" },
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "10.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "process_open_fds{job=\"multi-scrobbler\"}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Open File Descriptors",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": {
|
||||
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
|
||||
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
|
||||
"fillOpacity": 10, "gradientMode": "none",
|
||||
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
|
||||
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
|
||||
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
|
||||
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
|
||||
"thresholdsStyle": { "mode": "off" }
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 8, "x": 8, "y": 31 },
|
||||
"id": 22,
|
||||
"options": {
|
||||
"legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "rate(nodejs_gc_duration_seconds_sum{job=\"multi-scrobbler\"}[5m])",
|
||||
"legendFormat": "{{kind}} GC",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Garbage Collection Time",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": {
|
||||
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
|
||||
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
|
||||
"fillOpacity": 10, "gradientMode": "none",
|
||||
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
|
||||
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
|
||||
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
|
||||
"spanNulls": false, "stacking": { "group": "A", "mode": "normal" },
|
||||
"thresholdsStyle": { "mode": "off" }
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
|
||||
"unit": "decbytes"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 8, "x": 16, "y": 31 },
|
||||
"id": 23,
|
||||
"options": {
|
||||
"legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "nodejs_heap_space_size_used_bytes{job=\"multi-scrobbler\"}",
|
||||
"legendFormat": "{{space}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Heap Space Breakdown",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 35 },
|
||||
"id": 24,
|
||||
"panels": [],
|
||||
"title": "Cache Performance",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": {
|
||||
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
|
||||
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
|
||||
"fillOpacity": 10, "gradientMode": "none",
|
||||
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
|
||||
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
|
||||
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
|
||||
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
|
||||
"thresholdsStyle": { "mode": "off" }
|
||||
},
|
||||
"mappings": [],
|
||||
"max": 100,
|
||||
"min": 0,
|
||||
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
|
||||
"unit": "percent"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 36 },
|
||||
"id": 13,
|
||||
"options": {
|
||||
"legend": { "calcs": ["mean", "last"], "displayMode": "table", "placement": "right", "showLegend": true },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "100 * multiscrobbler_cache_hits / (multiscrobbler_cache_hits + multiscrobbler_cache_misses)",
|
||||
"legendFormat": "{{cacheType}} ({{tier}})",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Cache Hit Rate",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": {
|
||||
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
|
||||
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
|
||||
"fillOpacity": 10, "gradientMode": "none",
|
||||
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
|
||||
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
|
||||
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
|
||||
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
|
||||
"thresholdsStyle": { "mode": "off" }
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
|
||||
"unit": "ops"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 36 },
|
||||
"id": 14,
|
||||
"options": {
|
||||
"legend": { "calcs": ["mean", "last"], "displayMode": "table", "placement": "right", "showLegend": true },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "rate(multiscrobbler_cache_sets[5m])",
|
||||
"legendFormat": "{{cacheType}} ({{tier}})",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Cache Set Rate",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": { "mode": "palette-classic" },
|
||||
"custom": {
|
||||
"axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "",
|
||||
"axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line",
|
||||
"fillOpacity": 10, "gradientMode": "none",
|
||||
"hideFrom": { "tooltip": false, "viz": false, "legend": false },
|
||||
"lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5,
|
||||
"scaleDistribution": { "type": "linear" }, "showPoints": "auto",
|
||||
"spanNulls": false, "stacking": { "group": "A", "mode": "none" },
|
||||
"thresholdsStyle": { "mode": "off" }
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] }
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": { "id": "byRegexp", "options": ".*hits.*" },
|
||||
"properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }]
|
||||
},
|
||||
{
|
||||
"matcher": { "id": "byRegexp", "options": ".*misses.*" },
|
||||
"properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 44 },
|
||||
"id": 16,
|
||||
"options": {
|
||||
"legend": { "calcs": ["last"], "displayMode": "table", "placement": "right", "showLegend": true },
|
||||
"tooltip": { "mode": "multi", "sort": "none" }
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "multiscrobbler_cache_hits",
|
||||
"legendFormat": "{{cacheType}} hits ({{tier}})",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
|
||||
"expr": "multiscrobbler_cache_misses",
|
||||
"legendFormat": "{{cacheType}} misses ({{tier}})",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Cache Hits vs Misses",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"refresh": "10s",
|
||||
"schemaVersion": 38,
|
||||
"style": "dark",
|
||||
"tags": ["multiscrobbler", "scrobbling", "music"],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": { "selected": false, "text": "Prometheus", "value": "Prometheus" },
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"label": "Datasource",
|
||||
"multi": false,
|
||||
"name": "DS_PROMETHEUS",
|
||||
"options": [],
|
||||
"query": "prometheus",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"type": "datasource"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": { "from": "now-6h", "to": "now" },
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Multiscrobbler Dashboard",
|
||||
"uid": "multiscrobbler",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
Sitemap: https://foxxmd.github.io/multi-scrobbler/sitemap.xml
|
||||
Sitemap: https://docs.multi-scrobbler.app/sitemap.xml
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user