Security hardening and first-run/portability improvements

Security (P0):
- Remove committed session-secret default; auto-generate and persist a
  random secret to the config volume when SESSION_SECRET is unset
  (prevents forgeable session cookies / auth bypass).
- Validate playlist names to a safe charset and render sldl configs via
  literal Python substitution instead of sed (closes a command-injection
  and path-traversal path through playlist names).
- shlex-quote credential values written to shell-sourced env files, and
  strip newlines from values patched into .conf files.
- Render playlist .conf files 0600; warn at startup if the master key is
  co-located with the config volume; document keeping it separate.

Portability:
- Configurable timezone via TZ (default UTC) instead of hardcoded Edmonton.
- Remove personal defaults (navidrome user "andrew", ephemeral.club URLs).
- Ship generic example seeds; move the cross-album dedup keep-list and the
  legacy playlist import to editable config files; drop the personal
  _upgrade.csv.
- Generic VPN reference in docker-compose.snippet.yml.

First-run experience:
- Redirect to /setup instead of 500 when OIDC is unconfigured; surface a
  missing master key inline; entrypoint exits with an actionable message
  when the config folder is not writable.
- Add unauthenticated /health (JSON) and /setup (checklist) diagnostics.

Docs:
- Write docs/ARCHITECTURE.md and docs/MIGRATION.md (previously referenced
  but missing); expand README with ownership, backups, advanced settings,
  and migration guidance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
andrew
2026-07-09 14:25:55 -06:00
parent 2a19f84575
commit b135f11557
40 changed files with 759 additions and 600 deletions
+1
View File
@@ -5,3 +5,4 @@ __pycache__/
*.db-wal *.db-wal
*.db-shm *.db-shm
.env .env
pipeline/configs/_upgrade.csv
+51 -10
View File
@@ -82,29 +82,40 @@ This takes a few minutes the first time. You don't need to touch anything inside
### 2. Create your folders ### 2. Create your folders
alembic needs exactly two places to store things: alembic needs three places to store things:
- **A music folder** where your actual songs live. If you already have a music library, point at it directly. - **A music folder**, where your actual songs live. If you already have a music library, point at it directly.
- **A config folder** where alembic keeps its own database, your saved credentials, and logs. - **A config folder**, where alembic keeps its own database, your saved credentials (encrypted), and logs.
- **A secrets folder**, which holds just one file: the master key described in the next step. This is kept separate from the config folder on purpose (see below).
```bash ```bash
mkdir -p /path/to/your/music mkdir -p /path/to/your/music
mkdir -p /path/to/alembic-config mkdir -p /path/to/alembic-config
mkdir -p /path/to/alembic-secrets
``` ```
Everything else (which playlists you follow, your credentials, scheduling) is stored inside that config folder, so if you ever need to move alembic to a new machine, copying those two folders is all it takes. alembic runs inside the container as a non-root user (user and group id 1000). If you created these folders as yourself or as root, the container will not be able to write to them and will stop on startup with a clear message telling you this. Give that user ownership of the config and music folders:
```bash
sudo chown -R 1000:1000 /path/to/alembic-config
sudo chown -R 1000:1000 /path/to/your/music
```
Everything about how alembic runs (which playlists you follow, your credentials, scheduling) is stored inside the config folder, so moving alembic to a new machine is a matter of copying it across. Back up the secrets folder separately, and do not keep it in the same backup as the config folder. The reason is in the next step.
### 3. Generate a master key ### 3. Generate a master key
alembic encrypts every credential you give it (Spotify keys, Soulseek password, and so on) before storing it. It needs a key to do that: alembic encrypts every credential you give it (Spotify keys, Soulseek password, and so on) before storing it in the config folder. It needs a key to do that:
```bash ```bash
openssl rand -base64 32 > /path/to/alembic-config/master.key openssl rand -base64 32 > /path/to/alembic-secrets/master.key
chmod 600 /path/to/alembic-config/master.key chmod 600 /path/to/alembic-secrets/master.key
``` ```
Keep this file safe. If you lose it, alembic can no longer read your saved credentials and you'll need to re-enter them. Keep this file safe. If you lose it, alembic can no longer read your saved credentials and you'll need to re-enter them.
Keep it separate from your config folder, which is why it goes in its own folder above. The config folder holds the encrypted credentials. If the key sat in there too, then anyone who got hold of one backup would have both the locked box and its key, and the encryption would not protect you. Storing the key on its own keeps the two apart.
### 4. Set up login (OIDC) ### 4. Set up login (OIDC)
Register alembic as an application in your OIDC provider (Pocket ID, Authentik, whichever you use). You'll need to tell it: Register alembic as an application in your OIDC provider (Pocket ID, Authentik, whichever you use). You'll need to tell it:
@@ -145,12 +156,12 @@ services:
secrets: secrets:
alembic_master_key: alembic_master_key:
file: /path/to/alembic-config/master.key file: /path/to/alembic-secrets/master.key
``` ```
A few notes on the environment values: A few notes on the environment values:
- `SESSION_SECRET` can be any long random string, generate one with `openssl rand -base64 32`. - `SESSION_SECRET` signs your login session. You can leave it out and alembic will generate one on first start and save it inside the config folder for next time. If you would rather set it yourself, use any long random string, for example from `openssl rand -base64 32`.
- `ALLOWED_EMAIL` is optional but recommended. Even if your OIDC provider only has your account today, this makes sure only that one email can ever log in. - `ALLOWED_EMAIL` is optional but recommended. Even if your OIDC provider only has your account today, this makes sure only that one email can ever log in.
- If you want to route Soulseek traffic through a VPN container instead of exposing alembic's port directly, see the comments in `docker-compose.snippet.yml` in this repo for the `network_mode: "service:<vpn-container>"` pattern. Most people can skip this and use the simple port mapping above. - If you want to route Soulseek traffic through a VPN container instead of exposing alembic's port directly, see the comments in `docker-compose.snippet.yml` in this repo for the `network_mode: "service:<vpn-container>"` pattern. Most people can skip this and use the simple port mapping above.
@@ -218,10 +229,40 @@ docker build -t alembic:latest .
docker compose up -d --force-recreate alembic docker compose up -d --force-recreate alembic
``` ```
Your library, credentials, and settings all live in your two mounted folders, so updating the image never touches your data. Your library, credentials, and settings all live in your mounted folders, so updating the image never touches your data.
## Backups
Everything worth keeping is in the folders you already created:
- **The config folder** holds the app database, the beets library database, your rendered settings, and your encrypted credentials. Backing this up captures your playlists, schedules, and history.
- **The secrets folder** holds `master.key`. Back this up too, but keep it in a separate backup from the config folder. The config folder holds your credentials in encrypted form, and the key is what unlocks them. Keeping the two apart means one leaked backup is not enough to read your credentials.
- **The music folder** is your library itself. Back it up however you already back up files.
To restore on a new machine: put the config and secrets folders back where they were, point your compose file at them, and start the container. If you ever lose `master.key`, alembic can still start, but it can no longer read your saved credentials, so you would re-enter them once under Settings then Credentials.
## Advanced settings
Most people never touch these. They are environment variables you can add to your compose file's `environment:` block.
- `TZ` sets the timezone used for scheduling and for times shown in the app, for example `TZ=America/Toronto`. Defaults to UTC. This is the standard Docker timezone variable, so if you already set it for other containers, alembic follows it too.
- `ALLOWED_EMAIL` locks login to a single email address (recommended for a personal server).
- `MUSIC_DATA_DIR`, `ALEMBIC_CONFIG_DIR`, `ALEMBIC_PORT`, and `ENCRYPTION_KEY_FILE` let you change the in-container paths and port. The defaults match everything in this README, so you only need these for unusual setups.
You can confirm your setup at any time by visiting `/setup` in your browser (no login needed), which lists what is configured and what is missing. There is also a `/health` endpoint that returns the same information as JSON for uptime monitors.
## Migrating from the older script version
If you used the earlier version of this project, when it was a set of scripts rather than this container, there is a short guide for bringing your old playlists and credentials across in [docs/MIGRATION.md](docs/MIGRATION.md).
## Troubleshooting ## Troubleshooting
**Something isn't working and I'm not sure what.**
Open `/setup` in your browser. It checks the things a fresh install commonly gets wrong (login not configured, master key missing, config folder not writable) and tells you exactly what to fix. You do not need to be logged in to see it.
**The container won't start / exits immediately.**
Check the logs with `docker compose logs alembic`. The most common cause is folder ownership: the container runs as user id 1000, so the config and music folders must be owned by it. Run `sudo chown -R 1000:1000 <folder>` on each, as described in step 2.
**I can't log in / get redirected in a loop.** **I can't log in / get redirected in a loop.**
Double check the redirect URI registered with your OIDC provider exactly matches `https://<your-host>/auth/callback`, including `http` vs `https`. Double check the redirect URI registered with your OIDC provider exactly matches `https://<your-host>/auth/callback`, including `http` vs `https`.
+29 -2
View File
@@ -1,3 +1,4 @@
import logging
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
@@ -5,13 +6,38 @@ from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
from app.db import enable_beets_db_wal, init_db from app.db import enable_beets_db_wal, init_db
from app.routers import artist_casing, auth, credentials, dashboard, dedup, genres, import_, jobs, library, playlists from app.routers import artist_casing, auth, credentials, dashboard, dedup, genres, health, import_, jobs, library, playlists
from app.security.session import resolve_session_secret
from app.services import scheduler_service from app.services import scheduler_service
from app.settings import settings from app.settings import settings
log = logging.getLogger("alembic")
def _warn_if_key_colocated_with_config() -> None:
"""The Fernet master key decrypts every stored credential. If it lives
inside the config volume, one leaked volume or backup exposes both the
key and the encrypted database together, which defeats the encryption.
Warn loudly rather than fail, since some users deliberately accept this."""
try:
key_path = settings.encryption_key_file.resolve()
config_dir = settings.alembic_config_dir.resolve()
except OSError:
return
if config_dir == key_path or config_dir in key_path.parents:
log.warning(
"Master key %s is inside the config directory %s. Anyone with a "
"copy of that volume or backup gets both the key and the encrypted "
"credentials. Mount the key from a separate location kept out of "
"the config backup. See docker-compose.snippet.yml.",
key_path,
config_dir,
)
@asynccontextmanager @asynccontextmanager
async def lifespan(_app: FastAPI): async def lifespan(_app: FastAPI):
_warn_if_key_colocated_with_config()
init_db() init_db()
enable_beets_db_wal() enable_beets_db_wal()
@@ -27,10 +53,11 @@ async def lifespan(_app: FastAPI):
def create_app() -> FastAPI: def create_app() -> FastAPI:
app = FastAPI(title="alembic", lifespan=lifespan) app = FastAPI(title="alembic", lifespan=lifespan)
app.add_middleware(SessionMiddleware, secret_key=settings.session_secret) app.add_middleware(SessionMiddleware, secret_key=resolve_session_secret())
app.mount("/static", StaticFiles(directory="app/static"), name="static") app.mount("/static", StaticFiles(directory="app/static"), name="static")
app.include_router(health.router)
app.include_router(auth.router) app.include_router(auth.router)
app.include_router(dashboard.router) app.include_router(dashboard.router)
app.include_router(library.router) app.include_router(library.router)
+6
View File
@@ -7,12 +7,18 @@ from sqlalchemy import select
from app.db import SessionLocal from app.db import SessionLocal
from app.models import User from app.models import User
from app.security.oidc import oauth from app.security.oidc import oauth
from app.services import diagnostics
router = APIRouter(prefix="/auth", tags=["auth"]) router = APIRouter(prefix="/auth", tags=["auth"])
@router.get("/login") @router.get("/login")
async def login(request: Request): async def login(request: Request):
# If OIDC isn't configured, the client was never registered and calling it
# would raise an opaque 500. Send the operator to the setup check instead,
# which spells out exactly which variables are missing.
if not diagnostics.oidc_configured():
return RedirectResponse(url="/setup")
redirect_uri = request.url_for("auth_callback") redirect_uri = request.url_for("auth_callback")
return await oauth.pocketid.authorize_redirect(request, redirect_uri) return await oauth.pocketid.authorize_redirect(request, redirect_uri)
+9 -1
View File
@@ -3,6 +3,7 @@ from fastapi.responses import RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from app.db import get_db from app.db import get_db
from app.security import crypto
from app.security.deps import require_auth from app.security.deps import require_auth
from app.services import credential_service from app.services import credential_service
@@ -31,6 +32,7 @@ async def credentials_index(request: Request, user: dict = Depends(require_auth)
"core_scopes": credential_service.CORE_SCOPES, "core_scopes": credential_service.CORE_SCOPES,
"configured": configured, "configured": configured,
"enabled": enabled, "enabled": enabled,
"key_present": crypto.key_present(),
}, },
) )
@@ -47,7 +49,13 @@ async def save_credentials(
for field, value in form.items() for field, value in form.items()
if field in credential_service.SCOPE_FIELDS[scope] and value != "" if field in credential_service.SCOPE_FIELDS[scope] and value != ""
} }
credential_service.set_credentials(db, scope, values) try:
credential_service.set_credentials(db, scope, values)
except RuntimeError:
# Almost always the encryption key is missing, so the value can't be
# encrypted. Redirect back; the page shows a key-missing banner with
# exactly what to do.
return RedirectResponse(url="/settings/credentials", status_code=303)
return RedirectResponse(url="/settings/credentials", status_code=303) return RedirectResponse(url="/settings/credentials", status_code=303)
+3 -1
View File
@@ -8,7 +8,7 @@ from sqlalchemy import select
from app.db import get_db from app.db import get_db
from app.models import JobRun from app.models import JobRun
from app.security.deps import require_auth from app.security.deps import require_auth
from app.services import beets_service, credential_service, network_service, playlist_service, scheduler_service from app.services import beets_service, credential_service, diagnostics, network_service, playlist_service, scheduler_service
router = APIRouter(tags=["dashboard"]) router = APIRouter(tags=["dashboard"])
templates = Jinja2Templates(directory="app/templates") templates = Jinja2Templates(directory="app/templates")
@@ -34,11 +34,13 @@ async def dashboard(request: Request, user: dict = Depends(require_auth), db=Dep
) )
recent_tracks = beets_service.recently_added(time.time() - _RECENT_TRACKS_WINDOW_SECONDS) recent_tracks = beets_service.recently_added(time.time() - _RECENT_TRACKS_WINDOW_SECONDS)
public_ip = await asyncio.to_thread(network_service.public_ip) public_ip = await asyncio.to_thread(network_service.public_ip)
setup_warnings = [c for c in diagnostics.checks() if not c["ok"] and (c["critical"] or c.get("warn"))]
return templates.TemplateResponse( return templates.TemplateResponse(
request, request,
"dashboard.html", "dashboard.html",
{ {
"user": user, "user": user,
"setup_warnings": setup_warnings,
"stats": stats, "stats": stats,
"total_size_human": _human_bytes(stats.get("total_bytes", 0)), "total_size_human": _human_bytes(stats.get("total_bytes", 0)),
"playlist_count": len(playlists), "playlist_count": len(playlists),
+30
View File
@@ -0,0 +1,30 @@
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse
from fastapi.templating import Jinja2Templates
from app.services import diagnostics
router = APIRouter(tags=["health"])
templates = Jinja2Templates(directory="app/templates")
@router.get("/health")
async def health():
"""Unauthenticated JSON health check. 200 when everything critical is
configured, 503 when something critical is missing so an uptime monitor
notices. The body always lists every check either way."""
result = diagnostics.summary()
status_code = 200 if result["status"] == "ok" else 503
return JSONResponse(result, status_code=status_code)
@router.get("/setup")
async def setup(request: Request):
"""Unauthenticated, human-readable version of the same checks, so a fresh
operator can see what still needs configuring before login works."""
result = diagnostics.summary()
return templates.TemplateResponse(
request,
"setup.html",
{"status": result["status"], "checks": result["checks"]},
)
+10 -7
View File
@@ -28,13 +28,16 @@ async def create_playlist(
user: dict = Depends(require_auth), user: dict = Depends(require_auth),
db=Depends(get_db), db=Depends(get_db),
): ):
playlist_service.create( try:
db, playlist_service.create(
name=name, db,
spotify_url=spotify_url, name=name,
cron_expr=playlist_service.time_to_cron(sync_time), spotify_url=spotify_url,
no_m3u=no_m3u, cron_expr=playlist_service.time_to_cron(sync_time),
) no_m3u=no_m3u,
)
except ValueError as exc:
raise HTTPException(400, str(exc))
return RedirectResponse(url="/playlists", status_code=303) return RedirectResponse(url="/playlists", status_code=303)
+4
View File
@@ -17,6 +17,10 @@ def _fernet() -> Fernet:
return Fernet(key_path.read_bytes().strip()) return Fernet(key_path.read_bytes().strip())
def key_present() -> bool:
return settings.encryption_key_file.exists()
def encrypt(value: str) -> bytes: def encrypt(value: str) -> bytes:
return _fernet().encrypt(value.encode()) return _fernet().encrypt(value.encode())
+37
View File
@@ -0,0 +1,37 @@
import secrets
from app.settings import settings
# A leftover from an earlier build that shipped this as the default. Still
# rejected explicitly so anyone who copied it into their env doesn't end up
# with a publicly-known signing key.
_KNOWN_PLACEHOLDER = "changeme-dev-only-override-in-production"
def resolve_session_secret() -> str:
"""Return the secret used to sign session cookies.
Preference order:
1. SESSION_SECRET from the environment, if set to a real value.
2. A random secret persisted to the config volume, generated once on
first boot and reused on every restart after that.
This means a user who never sets SESSION_SECRET still gets a strong,
stable secret instead of the app silently falling back to a constant
that would make every session cookie forgeable.
"""
env_value = settings.session_secret.strip()
if env_value and env_value != _KNOWN_PLACEHOLDER:
return env_value
secret_path = settings.alembic_config_dir / "session.secret"
if secret_path.exists():
stored = secret_path.read_text().strip()
if stored:
return stored
generated = secrets.token_urlsafe(48)
secret_path.parent.mkdir(parents=True, exist_ok=True)
secret_path.write_text(generated + "\n")
secret_path.chmod(0o600)
return generated
+11 -3
View File
@@ -1,4 +1,5 @@
import re import re
import shlex
import time import time
from pathlib import Path from pathlib import Path
@@ -30,7 +31,7 @@ SCOPE_DESCRIPTIONS = {
"navidrome": "Triggers a library rescan after downloads/imports/edits, and lets the daily status report check Navidrome's scan health. Required.", "navidrome": "Triggers a library rescan after downloads/imports/edits, and lets the daily status report check Navidrome's scan health. Required.",
"bandcamp": "Pulls new purchases from your Bandcamp collection, imports them automatically, and stamps a buy link into the file tag.", "bandcamp": "Pulls new purchases from your Bandcamp collection, imports them automatically, and stamps a buy link into the file tag.",
"qobuz": "A buy-link source (alongside Bandcamp) for tracks you didn't purchase there -- DRM-free hi-res links when available, written directly into the file tag.", "qobuz": "A buy-link source (alongside Bandcamp) for tracks you didn't purchase there -- DRM-free hi-res links when available, written directly into the file tag.",
"azuracast": "Not a buy-link source itself (that's Bandcamp/Qobuz, written straight to the file tag) -- this lets alembic tell AzuraCast (ephemeral.club's backend) to immediately reprocess touched files or fix a playlist assignment, instead of waiting for its periodic scan.", "azuracast": "Only relevant if you run an AzuraCast radio station off the same library. Not a buy-link source itself (that's Bandcamp/Qobuz, written straight to the file tag) -- this lets alembic tell AzuraCast to immediately reprocess touched files or fix a playlist assignment, instead of waiting for its periodic scan.",
"telegram": "Sends the daily pipeline health digest to a chat instead of you having to check the dashboard.", "telegram": "Sends the daily pipeline health digest to a chat instead of you having to check the dashboard.",
} }
@@ -99,6 +100,9 @@ def _patch_conf_field(path: Path, key: str, value: str) -> None:
every other line (including PLAYLIST_NAME/SPOTIFY_URL substitutions every other line (including PLAYLIST_NAME/SPOTIFY_URL substitutions
regen.sh already applied) untouched.""" regen.sh already applied) untouched."""
text = path.read_text() text = path.read_text()
# A newline in the value would inject an extra `key = value` line into the
# sldl config. Credentials never legitimately contain one, so strip any.
value = value.replace("\r", "").replace("\n", "")
pattern = re.compile(rf"^{re.escape(key)}\s*=.*$", re.MULTILINE) pattern = re.compile(rf"^{re.escape(key)}\s*=.*$", re.MULTILINE)
new_line = f"{key} = {value}" new_line = f"{key} = {value}"
if pattern.search(text): if pattern.search(text):
@@ -114,7 +118,11 @@ def _every_playlist_conf() -> list[Path]:
def _write_env_file(path: Path, values: dict[str, str]) -> None: def _write_env_file(path: Path, values: dict[str, str]) -> None:
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
lines = [f"{k}='{v}'" for k, v in values.items()] # shlex.quote so a credential value containing a quote, space, or shell
# metacharacter (e.g. a password like `p'a$(id)ss`) stays a single literal
# value when a pipeline script `source`s this file, instead of breaking out
# and executing. shlex.quote also handles the empty string correctly ('').
lines = [f"{k}={shlex.quote(v)}" for k, v in values.items()]
path.write_text("\n".join(lines) + "\n") path.write_text("\n".join(lines) + "\n")
path.chmod(0o600) path.chmod(0o600)
@@ -147,7 +155,7 @@ def render_scope(db: Session, scope: str) -> None:
settings.pipeline_config_dir / "navidrome" / "admin.env", settings.pipeline_config_dir / "navidrome" / "admin.env",
{ {
"ND_BASE": values.get("base_url", "http://navidrome:4533"), "ND_BASE": values.get("base_url", "http://navidrome:4533"),
"ND_USER": values.get("admin_user", "andrew"), "ND_USER": values.get("admin_user", ""),
"ND_PASS": values.get("admin_pass", ""), "ND_PASS": values.get("admin_pass", ""),
}, },
) )
+105
View File
@@ -0,0 +1,105 @@
"""Setup and health checks.
These answer "is this install configured enough to work?" without needing a
login, so a fresh operator can see what is missing before the point where a
missing piece would otherwise surface as an opaque 500. Nothing here reads or
exposes a secret value; it only reports presence and reachability.
"""
import os
from app.settings import settings
# Checks whose failure means the app cannot function (can't log in, can't
# persist state). Everything else is informational.
CRITICAL = {"oidc", "master_key", "config_writable"}
def _config_writable() -> bool:
probe = settings.alembic_config_dir / ".write-probe"
try:
probe.write_text("ok")
probe.unlink()
return True
except OSError:
return False
def oidc_configured() -> bool:
return bool(
settings.pocketid_issuer
and settings.pocketid_client_id
and settings.pocketid_client_secret
)
def master_key_present() -> bool:
return settings.encryption_key_file.exists()
def _key_colocated() -> bool:
try:
key = settings.encryption_key_file.resolve()
cfg = settings.alembic_config_dir.resolve()
except OSError:
return False
return cfg == key or cfg in key.parents
def checks() -> list[dict]:
"""Return one dict per check: key, ok, critical, detail."""
results = [
{
"key": "oidc",
"ok": oidc_configured(),
"detail": "Login provider configured."
if oidc_configured()
else "Set POCKETID_ISSUER, POCKETID_CLIENT_ID and POCKETID_CLIENT_SECRET. Without them, login cannot work.",
},
{
"key": "master_key",
"ok": master_key_present(),
"detail": f"Encryption key found at {settings.encryption_key_file}."
if master_key_present()
else f"No encryption key at {settings.encryption_key_file}. Generate one with `openssl rand -base64 32` and mount it as the alembic_master_key secret. Saving any credential will fail until then.",
},
{
"key": "config_writable",
"ok": _config_writable(),
"detail": f"Config directory {settings.alembic_config_dir} is writable."
if _config_writable()
else f"Cannot write to {settings.alembic_config_dir}. Fix ownership on the host so the container user (uid 1000) can write: chown -R 1000:1000 <your config folder>.",
},
{
"key": "music_dir",
"ok": os.path.isdir(settings.music_data_dir),
"detail": f"Music directory {settings.music_data_dir} is present."
if os.path.isdir(settings.music_data_dir)
else f"Music directory {settings.music_data_dir} is not mounted.",
},
{
"key": "beets_library",
"ok": True, # absence is normal before the first import
"detail": "Library database present."
if settings.beets_db_path.exists()
else "No library database yet. This is normal until your first playlist sync or import.",
},
]
if master_key_present() and _key_colocated():
results.append(
{
"key": "key_location",
"ok": True,
"detail": "Note: the master key sits inside the config directory. A single leaked backup would expose both the key and the encrypted credentials. Consider mounting it from a separate location.",
"warn": True,
}
)
for r in results:
r["critical"] = r["key"] in CRITICAL
return results
def summary() -> dict:
"""Overall status plus the per-check list, for /health."""
results = checks()
degraded = any(c["critical"] and not c["ok"] for c in results)
return {"status": "degraded" if degraded else "ok", "checks": results}
+46 -30
View File
@@ -1,4 +1,5 @@
import json import json
import re
import subprocess import subprocess
import time import time
@@ -8,6 +9,26 @@ from sqlalchemy.orm import Session
from app.models import Playlist from app.models import Playlist
from app.settings import settings from app.settings import settings
# A playlist name becomes a filename (<name>.conf), a directory under the
# Soulseek dropbox, an M3U path, and a log path, and it is substituted into
# rendered config files. Restrict it to a small safe charset so it can never
# escape those paths (../) or inject config/shell content. Kept deliberately
# tight; if a user wants a fancier display name that's a separate field to add
# later, not a reason to loosen the on-disk identifier.
_VALID_NAME = re.compile(r"^[A-Za-z0-9 _-]{1,64}$")
def validate_name(name: str) -> str:
"""Return the name unchanged if it is a safe on-disk identifier, else
raise ValueError with a message suitable for showing to the user."""
cleaned = (name or "").strip()
if not _VALID_NAME.match(cleaned):
raise ValueError(
"Playlist name must be 1-64 characters using only letters, "
"numbers, spaces, hyphens and underscores."
)
return cleaned
def cron_to_time(cron_expr: str | None) -> str: def cron_to_time(cron_expr: str | None) -> str:
"""'30 1 * * *' -> '01:30'. Every playlist schedule today is a simple """'30 1 * * *' -> '01:30'. Every playlist schedule today is a simple
@@ -41,32 +62,6 @@ def time_to_cron(time_str: str | None) -> str | None:
return None return None
return f"{minute} {hour} * * *" return f"{minute} {hour} * * *"
# The 17-entry array this project is migrating off of (was hardcoded in
# /opt/sldl/configs/regen.sh). Used once by seed_legacy() during Stage 1/2
# of the migration; the DB is the source of truth from then on. cron_expr
# values are the exact staggered slots from /etc/cron.d/sldl-maintenance,
# preserved so cutover doesn't change anyone's download schedule.
LEGACY_PLAYLISTS = {
"digicore": ("https://open.spotify.com/playlist/6tWHtPBECZTkZHPKFA3Fq4", "0 0 * * *", False),
"techno": ("https://open.spotify.com/playlist/5GlAkczmLWJFIJO4FvkLEZ", "0 1 * * *", False),
"house": ("https://open.spotify.com/playlist/2XF8ye6O5xqzT4NSfIXdfJ", "30 1 * * *", False),
"lofi": ("https://open.spotify.com/playlist/0qnyF6TAmuT3U6hFH2fiF1", "0 2 * * *", False),
"y2k": ("https://open.spotify.com/playlist/0iaA7ZJS00aRxhMwZn2GCU", "30 2 * * *", False),
"weeb": ("https://open.spotify.com/playlist/0599AbpsKRjsp2rlJI8jhf", "0 3 * * *", False),
"shoegaze": ("https://open.spotify.com/playlist/3R4S3tmbVKjXQ1RPAmryC5", "30 3 * * *", False),
"bass": ("https://open.spotify.com/playlist/2zkF4S16efaMmmzdIOe5w2", "0 4 * * *", False),
"hiphop": ("https://open.spotify.com/playlist/42JxTtOLJ7gG1ybfyy7Vmi", "30 4 * * *", False),
"modular": ("https://open.spotify.com/playlist/2kOfP2EM8dzdMYC6fXvlLb", "0 5 * * *", False),
"hotdog": ("https://open.spotify.com/playlist/53HrOL0qw47G9ywYZz3kgX", "30 5 * * *", False),
"kpop": ("https://open.spotify.com/playlist/31e2V512TIqr5JIfgkyseo", "0 6 * * *", False),
"jungle": ("https://open.spotify.com/playlist/4LjLeXg6ElneQiTaIcAHhE", "30 6 * * *", False),
"goldenera": ("https://open.spotify.com/playlist/57vArhigysJfgwB6CY4VXR", "0 7 * * *", False),
"botanica": ("https://open.spotify.com/playlist/5KgQT9YWz3EqhIN4Jh69IU", "30 7 * * *", False),
"hardcore": ("https://open.spotify.com/playlist/1J1lbzlQKligzr2LWaq9Ex", "0 23 * * *", False),
"liked": ("https://open.spotify.com/playlist/4Z3qCYuU1sjNeNYO3Amzeo", "30 23 * * *", True),
}
def list_all(db: Session) -> list[Playlist]: def list_all(db: Session) -> list[Playlist]:
return list(db.execute(select(Playlist).order_by(Playlist.name)).scalars()) return list(db.execute(select(Playlist).order_by(Playlist.name)).scalars())
@@ -88,6 +83,7 @@ def create(
notes: str | None = None, notes: str | None = None,
cron_expr: str | None = None, cron_expr: str | None = None,
) -> Playlist: ) -> Playlist:
name = validate_name(name)
now = time.time() now = time.time()
playlist = Playlist( playlist = Playlist(
name=name, name=name,
@@ -141,13 +137,33 @@ def delete(db: Session, playlist_id: int) -> None:
def seed_legacy(db: Session) -> int: def seed_legacy(db: Session) -> int:
"""One-time import of the legacy hardcoded array (migration Stage 1/2). """One-time import of playlists from an optional legacy-playlists.json in
Skips any name that already exists. Returns the number of rows created.""" the config dir (migration helper). This is not personal data baked into
the code: point it at your own export. Each entry is an object with
"name", "spotify_url", and optional "cron_expr" and "no_m3u". Skips any
name that already exists. Returns the number of rows created, or 0 if the
file is absent.
Example legacy-playlists.json:
[{"name": "techno", "spotify_url": "https://open.spotify.com/playlist/...",
"cron_expr": "0 1 * * *", "no_m3u": false}]
"""
legacy_path = settings.pipeline_config_dir / "legacy-playlists.json"
if not legacy_path.exists():
return 0
entries = json.loads(legacy_path.read_text())
created = 0 created = 0
for name, (url, cron_expr, no_m3u) in LEGACY_PLAYLISTS.items(): for entry in entries:
name = entry["name"]
if get_by_name(db, name) is not None: if get_by_name(db, name) is not None:
continue continue
create(db, name=name, spotify_url=url, no_m3u=no_m3u, cron_expr=cron_expr) create(
db,
name=name,
spotify_url=entry["spotify_url"],
no_m3u=entry.get("no_m3u", False),
cron_expr=entry.get("cron_expr"),
)
created += 1 created += 1
return created return created
+18 -1
View File
@@ -1,4 +1,6 @@
import logging
import time import time
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
@@ -9,7 +11,22 @@ from app.models import Playlist, ScheduledJob
from app.services import dedup_review_service, genre_review_service, pipeline_runner from app.services import dedup_review_service, genre_review_service, pipeline_runner
from app.settings import settings from app.settings import settings
TIMEZONE = "America/Edmonton" log = logging.getLogger("alembic")
def _resolve_timezone(name: str) -> str:
"""Validate the configured timezone, falling back to UTC if it isn't a
real IANA zone. Returned as a string because that's what APScheduler and
CronTrigger accept directly."""
try:
ZoneInfo(name)
return name
except (ZoneInfoNotFoundError, ValueError):
log.warning("Unknown timezone %r, falling back to UTC.", name)
return "UTC"
TIMEZONE = _resolve_timezone(settings.timezone)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Maintenance jobs, ported 1:1 from /etc/cron.d/sldl-maintenance. Each entry # Maintenance jobs, ported 1:1 from /etc/cron.d/sldl-maintenance. Each entry
+16 -1
View File
@@ -1,5 +1,6 @@
from pathlib import Path from pathlib import Path
from pydantic import AliasChoices, Field
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -21,7 +22,11 @@ class Settings(BaseSettings):
encryption_key_file: Path = Path("/run/secrets/alembic_master_key") encryption_key_file: Path = Path("/run/secrets/alembic_master_key")
# Session cookie signing secret for Starlette's SessionMiddleware. # Session cookie signing secret for Starlette's SessionMiddleware.
session_secret: str = "changeme-dev-only-override-in-production" # Left empty by default on purpose: an empty/placeholder value would let
# anyone forge a signed session cookie and skip login entirely. When this
# is unset, resolve_session_secret() generates and persists a random one
# to the config volume instead of falling back to a known constant.
session_secret: str = ""
# Pocket ID OIDC client, registered once in the Pocket ID admin UI # Pocket ID OIDC client, registered once in the Pocket ID admin UI
# (redirect URI: https://<alembic-host>/auth/callback). # (redirect URI: https://<alembic-host>/auth/callback).
@@ -33,6 +38,16 @@ class Settings(BaseSettings):
# may complete login even if Pocket ID ever grows a second account. # may complete login even if Pocket ID ever grows a second account.
allowed_email: str = "" allowed_email: str = ""
# IANA timezone (e.g. "America/Edmonton") used for all scheduling and for
# times shown in the UI and the status report. Defaults to UTC so a fresh
# install behaves predictably anywhere. Reads the standard Docker `TZ`
# variable, or `ALEMBIC_TIMEZONE` if you'd rather be explicit. An
# unrecognized value falls back to UTC (see scheduler_service).
timezone: str = Field(
default="UTC",
validation_alias=AliasChoices("TZ", "ALEMBIC_TIMEZONE", "TIMEZONE"),
)
@property @property
def beets_dir(self) -> Path: def beets_dir(self) -> Path:
return self.alembic_config_dir / "beets" return self.alembic_config_dir / "beets"
+7
View File
@@ -4,6 +4,13 @@
<h2 style="margin-top:0;">Credentials</h2> <h2 style="margin-top:0;">Credentials</h2>
<p class="muted" style="margin-top:-0.75rem;">Write-only: values are encrypted at rest and never sent back to the browser. Leave a field blank to keep it unchanged.</p> <p class="muted" style="margin-top:-0.75rem;">Write-only: values are encrypted at rest and never sent back to the browser. Leave a field blank to keep it unchanged.</p>
{% if not key_present %}
<div class="panel" style="border:1px solid #b91c1c;margin-bottom:1.5rem;">
<strong>No encryption key found.</strong>
<p class="muted" style="margin:0.5rem 0 0;">Credentials cannot be saved until the master key is in place. Generate one with <code>openssl rand -base64 32</code> and mount it as the <code>alembic_master_key</code> secret, then restart. See the <a href="/setup">setup check</a>.</p>
</div>
{% endif %}
<div class="cred-grid"> <div class="cred-grid">
{% for scope, fields in scope_fields.items() %} {% for scope, fields in scope_fields.items() %}
{% set is_core = scope in core_scopes %} {% set is_core = scope in core_scopes %}
+9
View File
@@ -1,6 +1,15 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Dashboard — alembic{% endblock %} {% block title %}Dashboard — alembic{% endblock %}
{% block content %} {% block content %}
{% if setup_warnings %}
<div class="panel" style="border:1px solid #b91c1c;margin-bottom:1.5rem;">
<strong>Setup needs attention</strong>
<ul class="muted" style="margin:0.5rem 0 0;padding-left:1.2rem;">
{% for c in setup_warnings %}<li>{{ c.detail }}</li>{% endfor %}
</ul>
<p class="muted" style="margin:0.5rem 0 0;">Full checklist on the <a href="/setup">setup page</a>.</p>
</div>
{% endif %}
<div class="page-header"> <div class="page-header">
<div> <div>
<h1>Welcome back, {{ user.name or user.email }}</h1> <h1>Welcome back, {{ user.name or user.email }}</h1>
+38
View File
@@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block title %}Setup check · alembic{% endblock %}
{% block content %}
<section class="container" style="max-width:760px;margin:2rem auto;">
<div class="panel">
<h1 style="margin-top:0;">Setup check</h1>
{% if status == "ok" %}
<p class="muted">Everything needed to run is configured. You can log in from the
<a href="/">dashboard</a>.</p>
{% else %}
<p class="muted">Something needed to run is missing. Fix the items marked below,
then reload this page. You do not need to be logged in to see this.</p>
{% endif %}
<ul style="list-style:none;padding:0;margin:1.5rem 0 0;">
{% for c in checks %}
<li style="display:flex;gap:0.75rem;align-items:flex-start;padding:0.75rem 0;border-top:1px solid rgba(255,255,255,0.08);">
<span style="flex:0 0 auto;margin-top:0.15rem;">
{% if c.ok and c.get("warn") %}
<span class="badge badge-info" title="advisory">note</span>
{% elif c.ok %}
<span class="badge badge-success" title="ok">ok</span>
{% elif c.critical %}
<span class="badge" style="background:#b91c1c;color:#fff;" title="must fix">needs setup</span>
{% else %}
<span class="badge badge-muted" title="optional">optional</span>
{% endif %}
</span>
<span>
<strong>{{ c.key.replace("_", " ") }}</strong><br>
<span class="muted">{{ c.detail }}</span>
</span>
</li>
{% endfor %}
</ul>
</div>
</section>
{% endblock %}
+34 -35
View File
@@ -1,52 +1,51 @@
# Paste this block into /home/andrew/booksandtunes/docker-compose.yml. # Optional: route Soulseek traffic through a VPN container (gluetun).
# #
# Also required, one-time, in that same file: # This is a reference, not a drop-in file. Most people can ignore it and use
# 1. gluetun's environment: add/extend FIREWALL_OUTBOUND_SUBNETS to include # the plain `ports:` mapping shown in the README. Use this pattern only if you
# the bridge network subnet (default 172.19.0.0/16 in this compose # want alembic's outbound traffic to leave through a VPN.
# project — confirm with `docker network inspect booksandtunes_default`)
# so alembic can still reach navidrome:4533 etc:
# FIREWALL_OUTBOUND_SUBNETS=192.168.0.0/24,172.19.0.0/16
# 2. gluetun's ports: add the alembic web port (default 8420, verify free
# on the host first):
# - 8420:8420
# 3. Pin the bridge subnet so it can't silently drift on network recreate:
# networks:
# default:
# ipam:
# config:
# - subnet: 172.19.0.0/16
# 4. Remove the old on-demand `sldl:` service block entirely (superseded by
# the vendored binary running as an alembic subprocess) once cutover to
# alembic scheduling is complete (Migration Stage 3 in docs/MIGRATION.md).
# 5. Remove the `soulbeet:` service block once alembic has feature parity
# (Migration Stage 4).
# #
# Pangolin: point the new route at gluetun:8420, NOT alembic:8420 — alembic # The idea: instead of giving alembic its own network, you attach it to a
# has no independent container DNS identity once it rides gluetun's netns. # gluetun container's network with `network_mode: "service:gluetun"`. All of
# alembic's traffic then exits through the VPN gluetun maintains.
#
# When you do this, three things change compared to the README example:
# 1. alembic no longer publishes its own port. Publish 8420 on the gluetun
# service instead (add `- 8420:8420` to gluetun's `ports:`).
# 2. gluetun's firewall must allow alembic to reach your other containers
# (Navidrome, etc). Add your Docker bridge subnet to gluetun's
# FIREWALL_OUTBOUND_SUBNETS, for example:
# FIREWALL_OUTBOUND_SUBNETS=192.168.0.0/24,172.18.0.0/16
# Find your subnet with: docker network inspect <your-compose-project>_default
# 3. If you use a reverse proxy, point it at gluetun's host/port, not
# alembic's. Once alembic shares gluetun's network it has no container
# hostname of its own.
alembic: alembic:
image: git.kretzer.club/andrew/alembic:latest image: alembic:latest
container_name: alembic container_name: alembic
network_mode: "service:gluetun" network_mode: "service:gluetun"
depends_on: depends_on:
gluetun: gluetun:
condition: service_healthy condition: service_healthy
labels: env_file:
# Manual pull/up only — an auto-recreate mid-scheduled-job is exactly # Your OIDC settings (POCKETID_ISSUER, POCKETID_CLIENT_ID,
# the kind of surprise this project's "no unnecessary automation" # POCKETID_CLIENT_SECRET) and any other env vars.
# preference wants to avoid, and it would also orphan the - /path/to/alembic-config/alembic.env
# network_mode: service:gluetun reference the same way gluetun's own
# watchtower exclusion comment (above, in the gluetun service) explains.
- "com.centurylinklabs.watchtower.enable=false"
volumes: volumes:
- ${MUSIC_DATA_DIR:-/mnt/qnap/Music}:/data/music:rw - /path/to/your/music:/data/music:rw
- ${ALEMBIC_CONFIG_DIR:-/configs/alembicconfig}:/config:rw - /path/to/alembic-config:/config:rw
secrets: secrets:
- alembic_master_key - alembic_master_key
restart: unless-stopped restart: unless-stopped
# Add alongside the existing top-level `secrets:` block (create one if this # Add alongside the existing top-level `secrets:` block (create one if this
# compose file doesn't have one yet): # compose file doesn't have one yet).
#
# Keep master.key OUTSIDE the config volume. The key decrypts every stored
# credential, and the encrypted database lives in the config volume, so if the
# key sits in there too then a single leaked backup exposes both halves and the
# encryption buys you nothing. Put it in its own directory that your config
# backup does not include:
# secrets: # secrets:
# alembic_master_key: # alembic_master_key:
# file: /configs/alembicconfig/master.key # file: /path/to/alembic-secrets/master.key
+70
View File
@@ -0,0 +1,70 @@
# Architecture
This is a short tour of how alembic is put together, for anyone who wants to
understand or modify it. If you only want to run it, the README is enough.
## One container, everything inside
alembic ships as a single Docker image. The image is code only. It holds no
playlists, no credentials, and no music. Everything that changes over time
lives in mounted folders, so you can rebuild or upgrade the image without
touching your data.
There are two data mounts, plus one small secret:
- `/data/music` is your library. alembic writes tagged files here in a normal
`Artist/Album/Title.ext` layout that Navidrome (or any folder-based music
server) can read directly.
- `/config` holds everything else: the app database, the beets database, the
rendered pipeline config and credential files, and job logs.
- `master.key` is mounted as a Docker secret at `/run/secrets/alembic_master_key`.
It is the key that encrypts your saved credentials, and it is kept separate
from `/config` on purpose (see the README on why).
## The pieces
- **The web app** is FastAPI. It serves the UI, handles login through your
OIDC provider, and owns all the settings. It reads and writes two SQLite
databases: its own (`/config/alembic.db`, playlists, jobs, encrypted
credentials, dedup and genre review state) and the beets library database.
- **The scheduler** is APScheduler, running in the same process as the web app.
It fires playlist syncs and maintenance jobs on their cron schedules. It
keeps its schedule in memory and rebuilds it from code plus the database on
every startup, so there is no separate scheduler process or job store to
manage.
- **The pipeline** is a set of shell and Python scripts under `pipeline/`. The
app runs them as subprocesses. They do the actual downloading, tagging,
importing, deduplicating, and so on. `sldl` (the Soulseek downloader) is
vendored in the image and also run as a subprocess.
A single lock serializes pipeline work. Only one pipeline job runs at a time;
if a second is triggered while one is running, it is recorded as skipped rather
than run concurrently. This mirrors the old single-lock behavior and avoids two
jobs fighting over the same files.
## What happens on a playlist sync
1. The app writes the current playlist list to `/config/pipeline/playlists.json`
and renders one sldl config file per playlist from a template. Your Spotify
and Soulseek credentials are patched into those config files, decrypted from
the database just before use.
2. `sldl` reads a playlist's config, fetches the tracklist from Spotify, and
downloads each track from Soulseek into a per-playlist dropbox folder.
3. The downloaded files are tagged. Spotify is treated as the source of truth
for artist, title, album, and genre.
4. beets imports the tagged files, moving them into `/data/music` in the
canonical folder layout.
5. A matching `.m3u8` playlist file is written so the playlist plays back in
order.
6. Navidrome is asked to rescan so the new tracks show up.
Every run gets a row in the job history and its own log file, both visible in
the UI under Settings then Jobs.
## A note on the name
"alembic" here is this project. It is not the Python database-migration tool
also called Alembic. This project uses plain SQL files under `schema/` for its
own tiny migrations and does not depend on that tool. The name is a nod to a
still, which distills a raw input into something refined, which is roughly what
this does to a Spotify playlist.
+76
View File
@@ -0,0 +1,76 @@
# Migrating from the legacy scripts
This is only for people moving from the earlier version of this project, when
it was a collection of scripts under `/opt/sldl` with credentials scattered in
separate config folders. If you are setting up alembic fresh, ignore this file
and follow the README.
The migration does two things: bring your old playlists in, and bring your old
credentials in. Order matters, so do them in this order.
## 1. Import your playlists
`seed_legacy()` reads a JSON file at `/config/pipeline/legacy-playlists.json`
and creates a playlist row for each entry. The file is not shipped with your
playlists in it; you provide it. There is an example to copy from at
`pipeline/configs/legacy-playlists.json.example`.
Each entry looks like this:
```json
[
{
"name": "techno",
"spotify_url": "https://open.spotify.com/playlist/your-id-here",
"cron_expr": "0 1 * * *",
"no_m3u": false
}
]
```
Put your file at `/config/pipeline/legacy-playlists.json`, then run the seed
once:
```bash
docker exec alembic python -c "from app.db import SessionLocal; from app.services import playlist_service; db=SessionLocal(); print('created', playlist_service.seed_legacy(db)); db.close()"
```
It is safe to run more than once. It skips any playlist name that already
exists.
## 2. Import your credentials
Do this after step 1, not before. The credential import re-renders every
existing playlist config file with your Soulseek and Spotify details. If no
playlists exist yet, that render has nothing to write to and the step quietly
does nothing.
`scripts/import_legacy_credentials.py` reads your old plaintext credentials and
saves them into alembic's encrypted store. It only reads the legacy files, it
never changes them, and it is safe to run more than once.
It expects your old config folders bind-mounted read-only at `/legacy/*`. Run
it as a one-off container against the same image and config volume, for example:
```bash
docker run --rm \
-v /path/to/alembic-config:/config \
-v /path/to/old/opt-sldl:/legacy/opt-sldl:ro \
-v /path/to/old/bandcampconfig:/legacy/bandcampconfig:ro \
-v /path/to/old/azuracastconfig:/legacy/azuracastconfig:ro \
-v /path/to/old/qobuzconfig:/legacy/qobuzconfig:ro \
-v /path/to/old/telegramconfig:/legacy/telegramconfig:ro \
--secret ... \
--entrypoint python \
alembic:latest /app/scripts/import_legacy_credentials.py
```
Leave off any `-v /legacy/...` mount for a service you never used. The importer
skips a legacy folder that is not present.
## 3. Retire the old services
Once alembic is running your syncs and you have confirmed a playlist downloads
and imports correctly, you can remove the old on-demand download service and
the old import service from your compose file. Keep your old config folders
backed up until you are confident, since the importer only reads them.
+12 -2
View File
@@ -7,10 +7,19 @@ set -euo pipefail
ALEMBIC_CONFIG_DIR="${ALEMBIC_CONFIG_DIR:-/config}" ALEMBIC_CONFIG_DIR="${ALEMBIC_CONFIG_DIR:-/config}"
PIPELINE_DIR="${PIPELINE_DIR:-/app/pipeline}" PIPELINE_DIR="${PIPELINE_DIR:-/app/pipeline}"
mkdir -p \ # The container runs as uid 1000. If the mounted config folder is owned by
# root on the host (the usual result of `mkdir` as your normal user), the
# first write fails and the container would otherwise exit with a bare
# "Permission denied". Catch that here and say exactly how to fix it.
if ! mkdir -p \
"$ALEMBIC_CONFIG_DIR/beets/data" \ "$ALEMBIC_CONFIG_DIR/beets/data" \
"$ALEMBIC_CONFIG_DIR/pipeline" \ "$ALEMBIC_CONFIG_DIR/pipeline" \
"$ALEMBIC_CONFIG_DIR/logs" "$ALEMBIC_CONFIG_DIR/logs" 2>/dev/null; then
echo "[entrypoint] ERROR: cannot write to $ALEMBIC_CONFIG_DIR" >&2
echo "[entrypoint] The container runs as uid 1000 but that folder is not writable by it." >&2
echo "[entrypoint] On the host, run: chown -R 1000:1000 <the folder mounted at /config>" >&2
exit 1
fi
seed_if_absent() { seed_if_absent() {
local src="$1" dst="$2" local src="$1" dst="$2"
@@ -24,6 +33,7 @@ seed_if_absent "$PIPELINE_DIR/beets/config.yaml.example" "$ALEMBIC_CONFIG_DIR/be
seed_if_absent "$PIPELINE_DIR/configs/artist-canonical.list.example" "$ALEMBIC_CONFIG_DIR/pipeline/artist-canonical.list" seed_if_absent "$PIPELINE_DIR/configs/artist-canonical.list.example" "$ALEMBIC_CONFIG_DIR/pipeline/artist-canonical.list"
seed_if_absent "$PIPELINE_DIR/configs/djmix-albums.txt.example" "$ALEMBIC_CONFIG_DIR/pipeline/djmix-albums.txt" seed_if_absent "$PIPELINE_DIR/configs/djmix-albums.txt.example" "$ALEMBIC_CONFIG_DIR/pipeline/djmix-albums.txt"
seed_if_absent "$PIPELINE_DIR/configs/genres-whitelist.txt.example" "$ALEMBIC_CONFIG_DIR/pipeline/genres-whitelist.txt" seed_if_absent "$PIPELINE_DIR/configs/genres-whitelist.txt.example" "$ALEMBIC_CONFIG_DIR/pipeline/genres-whitelist.txt"
seed_if_absent "$PIPELINE_DIR/configs/cross-album-keep.list.example" "$ALEMBIC_CONFIG_DIR/pipeline/cross-album-keep.list"
export BEETSDIR="$ALEMBIC_CONFIG_DIR/beets" export BEETSDIR="$ALEMBIC_CONFIG_DIR/beets"
-413
View File
@@ -1,413 +0,0 @@
Artist,Title,Album,Length
,,,508:29
32ki; Hatsune Miku; 重音テト,メズマライザー (feat. 初音ミク&重音テト),メズマライザー,2:37
33 Below; DRIIA,LOYAL,LOYAL,4:15
[KRTM]; TWAN,Black Widow,Gum EP,6:21
\berhaupt & Au_erdem,Late Breakfast (Original),"Badarchin, Vol. 04",7:08
a r u k a.; DOVVE; echoed.; flawguri; Stardew,Aquarius,STORYBOOK: Chapter 2,5:09
A*S*Y*S,Bassface,Bassface EP,6:30
Above & Beyond/Oceanlab,Satellite,Super8 & Tab Presents: 20 Years Of Anjunabeats,3:29
Above & Beyond; Marty Longstaff,Flying By Candlelight - Above & Beyond Club Mix,Flying By Candlelight,4:02
Above & Beyond; Richard Bedford,Northern Soul,Common Ground,5:35
Adam Beyer; Kölsch,What You Need - Kölsch Remix,What You Need (Kölsch Remix),6:40
Aiko , ETHERIA,TECHNIKA [memory system],TECHNIKA [memory system]
AIROD,Universe of 90's Techno Parties [Molekul 007],Universe of 90's Techno Parties [Molekul 007],7:56
Alaskan Tapes,All We Can't See,All We Can't See / An Image,1:56
Alegant/Tube & Berger,Get Down (Short Edit),False Gods EP (Short Edits),3:34
Alesso; Tove Lo,Heroes (we could be),Forever,3:31
Alex Luciano,Under the Rain,Under the Rain,3:35
Alex Preston,Love You Better - Extended Mix,Love You Better,5:29
Alignment,Interference - Luca Agnelli Re,Interference,7:18
Alignment; Regal,Interference - Regal Remix,Interference,6:24
Alphazone,Alphazone Sunrise - Original C,Alphazone Sunrise,7:08
alt-J; Danny Brown; The Alchemist,Deadcrush (feat. Danny Brown) - Alchemist x Trooko Version,Deadcrush (feat. Danny Brown) [Alchemist x Trooko Version],4:03
altrice; Blue Dust Archive,bda creature,compciter ep,4:53
Amos & Riot Night; Ren Faye,Different View,Different View,4:13
Amos & Riot Night; Ren Faye,Different View,Different View,8:42
Amy Axegale,In Your Section,In Your Section,5:33
Andain; Myon; Shane 54,Promises - Myon & Shane 54 Sum,Promises,3:48
Andrea Kaeri/Phonez/BEKIMACHINE,Nothing for You,Dreamlike,5:26
Anetha; Sugar,Candy from Strangers,Don't Rush to Grow Up,8:07
ANNA; Miss Kittin,Forever Ravers,Speicher 112,6:00
Anton Kling,Mareld,Mareld,7:32
Apashe; VOLAC,Distance - Volac Remix,Distance (Volac Remix),3:35
Armin van Buuren; Tempo Giusto; i_o,Mr. Navigator - i_o Remix,"Balance (Remixes, Pt. 1)",5:54
ARTY; Brina Knauss,Hope - Brina Knauss Remix,Hope (Brina Knauss Remix),5:58
ARXMANE,'98 RACE,'98 RACE,1:40
Asagaoaudio,ring and portrait,the next game EP,5:52
At Dawn,Bare Essentials (Vip Edit),Routine Espresso 002: Cafe Dreams,5:36
Avicii,Silhouettes - Original Mix,Silhouettes,7:00
Avicii; Sandro Cavazza,Without You (feat. Sandro Cavazza),AVĪCI (01),2:58
Baalti,Aame,Baalti,2:28
Baalti,Ustad,Thandai - Flavours of the East,6:11
BABii; Pholo,EMBER (crashed) (crashed)_PHOLO,EMBER (crashed) (crashed)_PHOLO,3:46
Baby Billy Freeman,Bible Bonkers (Theme Song),The Righteous Gemstones: Season 3 (HBO Original Series Soundtrack),0:48
Baile,Dazed,Siren,6:46
Barry Can't Swim; somedeadbeat,Deadbeat Gospel,When Will We Land?,4:14
Basstakil,What If We Are Not Alone,What If We Are Not Alone,6:30
Beatenberg,The Lighthouse of Alexandria,On the way to Beatenberg,2:39
Belljack,불행한,Searching Within,6:03
Benny Bridges,Lucky Penny (Black Loops Remix),Lucky Penny (Black Loops Remix),5:33
biskuwi,Veil,Two Minds,5:23
Bjorn Salvador/Middle Aged Dad,Steina,Steina,8:08
Black Eyed Peas,Pump It (GWE,Pump It (GWE - Single,5:31
Bladee,Be Nice 2 Me (Euphorizer Remix),Be Nice 2 Me (Euphorizer Remix) - Single,3:21
BLCKLGHT,Away From You,Away From You,4:21
Bodysync; Ryan Hemsworth; Giraffage,Rhythm,Rhythm,2:32
Bomat,Doing All Right,Doing All Right,7:17
Boris Brejcha,Lieblingsmensch - Edit,Lieblingsmensch (Edit),3:40
Boris Brejcha,Never Look Back - Edit,Never Look Back (Edit),3:25
Boris Brejcha; Ginger,Happinezz - Edit,Happinezz (Edit),4:05
Bou; Trigga,Veteran,Scorpio,4:29
Brendan Park,Relic 19,Melt EP,4:33
Brooks Aleksander,You,You,4:11
Bruce Hornsby,5.Song C - Instrumental,Spirit Trail,2:47
Bug; Boyskin,Wait,Wait,6:31
Buku,TiiGA,TiiGA,3:48
BUSDRIVER; Aesop Rock; Danny Brown,Ego Death,Perfect Hair,6:00
Catz 'n Dogz; Jono McCleery,Would You Believe,Friendship,4:30
CBDJ,towards you,towards you,6:08
Chambray,Anew,Work That,7:42
Chance the Rapper; Future,Smoke Break (feat. Future),Coloring Book,3:46
CHANEY,Blank Space,Blank Space,5:17
Chicago Loop,Supernova 303,Memories Made / Supernova 303,3:36
Chris Luno,Better Try Again,Better Try Again,5:18
Chris Luno/Beron,The Hike,The Hike,7:46
Chris Luno/Beron,Friday,The Hike,6:43
Chris Luno/Beron,Saturday,The Hike,7:37
CJ Bolland; The Advent; Enrico Sangiuliano,Camargue 2019 - Enrico Sangiuliano Remix,Camargue 2019,8:14
Clams Casino; Vince Staples,All Nite (feat. Vince Staples),32 Levels (Deluxe),2:47
clipping.; Cocc Pistol Cree,Work Work,CLPPNG,3:43
Coastal,New Life,Cohesion,2:21
Cosmo's Midnight; Sarah Bonito,Hurt (feat. Sarah Bonito),Moments - EP,3:30
Crimsen/Feyln/Oliver Wickham,Grey Skies (Gorge Remix),Grey Skies (Gorge Remix),6:47
Cynthoni; Sewerslvt; AgonyOST,"Quitting Is Hard, Not Quitting Is Harder",SMOKE IT TO THE BUTT,5:49
dUO (Sakura Fujiwara),DdK _ ;_Dk (Itsuka Mita Eiga Mitai Ni),AIRPORT,3:19
D-Jastic,Up To No Good - Extended Mix,Up To No Good (Extended Mix),5:11
D-Mad; ARTY,She Gave Happiness - Arty Remix,"Digital Society International, Volume Three - Mixed by Ashley Wallbridge & Activa",6:05
Daijiro Nakagawa,Concentration,Loop Lullaby,2:44
Dan be; Alex Bone; hej motti,Ooh - hej motti Remix,Ooh (hej motti Remix),2:58
Danny Brown; Kendrick Lamar; Ab-Soul; Earl Sweatshirt,Really Doe,Atrocity Exhibition,5:19
Danny Brown; Purity Ring,25 Bucks (feat. Purity Ring),Old,3:30
Daphni; Caribou,Waiting So Long,Waiting So Long,3:54
Darby; OOTORO,PARTY,PARTY,2:08
Darc Marc,Dirty Rocking Bass - Dirty Rocking Bassline,Dirty Rocking Bass (Dirty Rocking Bassline),7:48
dark cat; juu; Cinders,BUBBLE TEA,BUBBLE TEA,4:03
Dash Berlin; Emma Hewitt,Waiting,The New Daylight,9:28
Data_haven,Utopia - Reprise,Data_haven [re-constructed],5:23
Davey Asprey,Catch,Catch,3:20
David Guetta; Sia,Titanium (feat. Sia),Nothing but the Beat 2.0,4:05
David Guetta; USHER,Without You (feat. Usher),Nothing but the Beat 2.0,3:28
Deadmau5,The Veldt V Porter Robinson - Language,The Veldt V Porter Robinson - Language - Single,2:07
deadmau5; i_o,Imaginary Friends - i_o Remix,"We Are Friends, Vol. 8",6:16
deadmau5; Rinzen,Monophobia - Rinzen Remix,mau5ville: Level 1,6:41
Death Grips,Beware,Exmilitary,5:52
Deborah de Luca; Marika Rossa,In Hypnose,In Hypnose,6:35
Diamond Dallas Tex; Lil Texas,Work My Body,The Afters,3:35
Diffrent,A Little Closer (Extended)145,A Little Closer (Extended)145 - Single,5:18
Diveo,Ferris Wheel,Singles Club Executive Lounge: Celebrating 50 Years of Activia Benz,3:05
DJ BORING,Like Water - Edit,Like Water,6:40
DJ Dextro,Sideral,Sideral,6:11
dj ignorant,i don't want to lose you,i don't want to lose you,5:17
DJ Koze,"Pick Up - 12"" Extended Disco Version",Pick Up,10:01
DJ PP,Desert Night (Extended Mix),Desert Night,6:37
DJ Seinfeld; ARY,Of Joy,Everything U,3:19
DJ_Dave; kitty ray,Still Miss U,Still Miss U,3:47
doechii,nissan altima (wheth,nissan altima (wheth - Single,2:10
Double Touch/Reigan,Greatest Day,Greatest Day,3:49
DRT; Alex K,Take Me Away - Alex K Remix,Take Me Away,2:27
Drunken Kong,Realize,Repeat,7:27
dublon; Deza,Sweet Nothings,Sweet Nothings,2:22
Duke Hugh,Greenleaf,Canvas,4:29
DV-i,Genconfinal,Metronomicon Audio 6.0,1:00
Eclipse,24-7 - Original Mix,24-7,8:04
Ed Balloon; Open Mike Eagle; They Hate Change,Missed Call,Missed Call,3:55
Effy; Mall Grab,iluv,iluv,4:17
Ejeca,U Gave Me Love,U Gave Me Love,2:21
Eleni Foureira; ARCADE,Disco-Tech,Disco-Tech,3:19
EMBRZ,Don't Look Back,Don't Look Back,3:27
EMBRZ; Montgomery,Count To Three,Count To Three,3:18
Eric Prydz,Pjanoo - Club Mix,Pjanoo,7:30
Ettu Brute,Know,Concrete,3:15
Ezekiel,help_urself (breakcore),help_urself (Alternates),1:58
F-Rontal,Inner Souls,Inner Souls,6:58
FallenLights.,You Are Enough,For Her,1:51
Farkey/Hayedeh,Flashback,Flashback,3:13
Felix Cartal; Hanne Mjøen,My Last Song (Felix Cartal's After Hours Mix),My Last Song (Felix Cartal's After Hours Mix),3:58
FenixForce,Domestic Violence,Inexcusable,5:52
Fhase 87,Redemption,Redemption EP,5:24
Flying Lotus; Kendrick Lamar,Never Catch Me,You're Dead!,3:54
Flywright,Thinkin' Bout You Lately,Hush Your Fuss,2:42
Frankyeffe; Teenage Mutants,Polymath,Polymath,6:11
Fred again.. & Anderson .Paak & Chika,places to be (CLIPZ remix),places to be (remixes),3:04
Fred again..; Anderson .Paak; CHIKA; CLIPZ,places to be - CLIPZ remix,places to be (remixes),3:04
Fred again..; Baby Keem; Nia Archives,leavemealone - Nia Archives Remix,USB,2:59
FROOTS,Papaya,Papaya,3:12
Frost Children,Satellites,Tweaker Poem,3:31
Gabriel & Dresden; Sub Teal; C,Only Road - Cosmic Gate Remix,Only Road (Cosmic Gate Remix),3:45
Gensuke Kanki,Vertige,Durée,3:54
Gilles Bernies,Knowing,Knowing,6:01
Gilmer Galibard,Bougarabou,A Baker's Shit Ton,5:36
Glen Campbell,Wichita Lineman - Remastered 2001,Wichita Lineman (Remastered),3:06
Goncalo M,Infinite Black,Best Weapons,6:08
Good Times Ahead; Vince Staples,Little Bit of This (feat. Vince Staples),Good Times Ahead,2:40
Greg Gonzalez,Country of the Mind (Edit),Country Of The Mind,6:25
Hadone,How To Fake Success - Original mix,Defining Persuasion (Original),6:27
Hailaker; S. Carey,Wavepool,Wavepool,3:38
harmony haven; Phusis,Violet Flash,Healing Visions,2:48
Helios,Bless This Morning Year,Eingya (Remastered),6:01
HNNY,Noth...,Nothing,5:12
Hope Tala,Tiptoeing - Tommy Villiers Remix,Tiptoeing (Tommy Villiers Remix),3:08
Hypsidia/Altriparty,Viral (Notre Dame Remix),Viral (The Remixes),4:14
HYUKOH; 落日飛車 Sunset Rollercoaster,Aaaannnnteeeeennnaaaaaa,AAA,6:50
"i_o, Raito",Rivolution,rivolution,5:12
i_o; Lights,Annihilation,Annihilation,4:07
i_o; Raito,Come with Me,House of God,6:05
i_o; Raito,Don't Stop,révolution,5:24
Ian van Dahl; Marsha,Castles In The Sky (featuring,Castles In The Sky,6:46
Ilja Alexander,Someday (LCAW Remix),Someday (LCAW Remix),6:38
Illiya Korniyenko,Ithori - Original,The Collapse Of Future Vol.3,6:34
Inafekt; Big Jon,Blaze (Extended Mix),Blaze,5:35
Infeed; INNERGATED,Freedom Inside Your Illness - LS41 Remix,Resurrected in the Hell,5:00
Isambard Khroustaliov,Atoll Song,"This Is My Private Beach, This Is My Jetsam",6:43
ISOxo; Ninajirachi,SHYPOP,kidsgonemad!,3:28
J.Shore; Mango; Orion; Shingo Nakamura,Raining In Osaka - Shingo Nakamura Remix,Raining In Osaka,7:44
Jacana People,Imari,Imari,4:23
jahjaylee,"Right, Right. Right?",jah,6:43
Jake Kaiser,Kestrel,Winter Selections 06,3:21
jamesjamesjames,"Its Not You, Its Me - Club Mix","It's Not You, It's Me (Club Mix)",3:44
jamesjamesjames; snctm.,homesoon.,homesoon.,3:55
jamesjamesjames; Spring Breaker,ibiza is waiting for me,ibiza is waiting for me,5:27
Japano File,Holy.Place,Holy.Place,3:50
Jaques Raupé; Felix Harrer,3 Haselnüsse,3 Haselnüsse,2:28
Jason Ross; Fiora,Through It All,Through It All,3:12
Jauz; i_o,Truth,Truth,5:45
Jayda G,Both Of Us - Edit,Both Of Us,3:49
Jazzanova/Edward Vanzet,I'm Still Here (Winnie & Somow Remix),I'm Still Here - Best's Friends Remixes,6:03
Jend,Forever,Forever,2:38
Jensen Interceptor; Assembler Code,Runner,6th Element,5:17
Jeshi; Ross from Friends,3210 - Ross from Friends Remix,3210 (Ross from Friends Remix),5:52
jizue,photograph feat.中嶋イッキュウ,shiori,5:56
Johnny Jungle,Johnny '94 - Dillinja Remix,Johnny '94 (Dillinja And Noise Of Art & Double D Remixes),6:28
Joy Anonymous,JOY (Up The Street),Cult Classics,3:58
JUKO; robo; wishlane; Mistrrr; The Litehouse; Foli,The Guardian,STORYBOOK,4:56
Julius Abel,Lost,Lost,3:39
Kacy Hill,I Believe In You (feat. Francis and the Lights),Is It Selfish If We Talk About Me Again,0:29
KAISUI,Cyclone,Cyclone EP,4:13
Kay Cee,Escape - Club Mix,Escape,6:51
Kayestone,Atmosphere - Angelic Remix,Atmosphere,8:27
Keiju(JP),Only U Know,Only U Know,6:18
Kesha,Die Young (Lil Texas Edit),Die Young (Lil Texas Edit) - Single,2:15
Kevin Flum,Mansion Tears,Mansion Tears,2:20
kita kouhei,Botanical Specimens,Neospecies,2:58
Kito; Chrome Sparks; IS U IS U,Melter,Melter,4:14
Kloyd,Sur,Night Tales,5:34
KOAN Sound,Shimmer,Moments of Opening,4:00
KOAN Sound,Colour Dance,Moments of Opening,4:29
KOAN Sound,Allure,Moments of Opening,2:51
KOAN Sound,Phantom Sight,Moments of Opening,4:48
Kobe JT,Lately,Lately,3:52
Koriass,Zombies,Love suprême,3:38
Kumi Takahara; aus,Tide,Tide,6:02
Kwengface; Joy Orbison; Overmono,Freedom 2,Freedom 2,3:26
Kyau & Albert; Jeza,Bring You Back - Original Mix,Matching Stories,4:35
Kyuuwaii,sora,bedroom,2:46
Lana Del Rey; Cedric Gervais,Summertime Sadness (Lana Del Rey Vs. Cedric Gervais) - Cedric Gervais Remix,Summertime Sadness (Lana Del Rey Vs. Cedric Gervais) [Cedric Gervais Remix],3:34
Landhouse/Sima Aava,Tales from the Swallow (Pandhora Remix),Tales from the Swallow (Remixes),4:10
Lauren Mia,Colours,Colours,7:26
Le Youth; Lane 8; Jyll,I Will Leave a Light On,About Us,4:00
Les Petits Pilous,Russian Forest,Bielle,3:44
Lil Texas; Irradiate,Highs In My Dreams,Highs In My Dreams,2:26
Lilly Palmer,Shift,All Things Fall,6:52
"Local Nomad, Nihil Young & Paige/Local Nomad/Nihil Young/Paige",Gates (Paige & Nihil Young Remix) - Extended,Gates (Paige & Nihil Young Remix),5:33
Logic1000; yunè pinku,What You Like,What You Like (feat. yunè pinku),3:13
Longo; Marco Lollis,Love Again,Love Again,2:59
longstoryshort,HER,HER,2:36
Loods,Need U,Need U,3:22
Loods; KLP,Freedom,Freedom,2:54
Lord RAJA; Jeremiah Jae,Van Go (feat. Jeremiah Jae),A Constant Moth,2:04
Lorenzo Raganzini; Paolo Ferrara; OGUZ,Raving in Paris - Oguz Journey Mix,Raving in Paris (Oguz Journey Mix),11:18
Luciid,Mob,Mob / D.O.L.L.A,8:12
Luciid,D.O.L.L.A,Mob / D.O.L.L.A,6:38
Luisa Marion,The Kite (Campfire Session) [Live],The Kite (Campfire Session) [Live],3:55
LUV HRTS,CAN WE DO BETTER?,CAN WE DO BETTER?,3:07
LUV HRTS,GONNA GET THRU,GONNA GET THRU,3:20
"LUV HRTS, Mild Minds",U REALLY HURT ME,U REALLY HURT ME,2:42
LYNY; FLY,Pulse,Pulse,2:27
M Field,Andrew,Andrew,2:44
Madeon,GoodFaith Radio (Ep. 1),GoodFaith Radio,59:29
Mall Grab; Skin On Skin,Strangers,Strangers,6:09
"Mango,Orion, J.Shore",When All the Ships Are Gone (Album Mix],Citylanes Airplanes,9:30
Mannequin; prod. DTM,Lay It On Me,Lay It On Me,2:17
Manuel Di Martino,Impact,Alpha Sides Vol. IV,5:10
Mark Michael,Solar Storm - Original Mix,Solar Storm,4:48
Martin Solveig; Faouzia; TRYM,Now Or Never - TRYM Remix,Now Or Never (Remixes),5:17
Masakatsu Takagi,Marginalia #131,Marginalia #131,7:10
Masakatsu Takagi,Marginalia #139,Marginalia #139,2:22
Masakatsu Takagi,Marginalia #143,Marginalia #143,2:53
Masakatsu Takagi,Marginalia #153,Marginalia #153,2:53
Masakatsu Takagi; 坂本美雨,Field Fjord,sorato,1:00
Matteo Vitanza,Acid? Why Not?,We Overcome,6:57
Mees Javois,DSTOP,DSTOP,5:27
Melé,And 1 (Extended Mix),And 1 (Extended Mix) - Single,4:30
Michael Klein,Yuzu,Yuzu,6:47
Middle School,remember,remember,1:23
midnight; ylxr,Promethium Reprise,Lo-Fi_house.Zip,4:00
Mike Posner,I Took a Pill in Ibiza (SeeB r,"At Night, Alone.",3:19
Miksu / Macloud/Makko,Nachts wach (Lila Wolken Bootleg),BRAVO Hits 2023,2:38
Milo; Hemlock Ernst,Souvenir,So The Flies Don't Come,3:45
Murder He Wrote; Idris Miles,Rave Out,Rave Out,3:16
My Sister's Fugazi Shirt,ONE MORE FINAL: I need you,"Man Fears the Darkness, and So He Scrapes Away at the Edges of It With Fire",4:29
Namatjira,Sounds Of Sunday Morning (RIGOONI Remix),Moons Of Yesterday (Remix Project 1),7:53
Nhato; Kenji Sekiguchi,Tokyo Blue Pipe - Kenji Sekigu,Tokyo Blue Pipe (Kenji Sekiguc,6:26
Ninajirachi; daine,It's You,I Love My Computer,2:49
Ninajirachi; daine; underscores,"It's You - underscores' ""Its U"" Remix","It's You (underscores' ""Its U"" Remix)",3:31
Ninajirachi; Effy,CSIRAC - Effy Remix,CSIRAC (Effy Remix),4:33
Ninajirachi; horsegiirL,Delete - horsegiirL Remix,Delete (horsegiirL Remix),2:54
No Buses,Eyes,Eyes,3:20
"NOTION, Chrystal",The Days (NOTION Remix) djsoundtop.com,djsoundtop.com,3:53
Odagled,Time Travel (Monikaze Remix),Time Travel,5:23
Onaisit,"the bit you were here was fun, maybe that makes it all worth it","the bit you were here was fun, maybe that makes it all worth it",2:28
Oncill; lillybug; evangeline; nshvll; Ridoy; clothespin.,Journey Across The Mountain,STORYBOOK: Chapter 2,7:10
Open Mike Eagle; Gold Panda,Ziggy Starfish (Anxiety Raps) [feat. Gold Panda],A Special Episode - EP,2:55
orbe; haruka nakamura; 田辺玄,ship,orbe ,3:50
Otira,Pill,Pill,2:09
Otira,Take Me,Take Me,3:50
Pablo Caballero; Tankhamun,Journey,Journey EP,6:12
Pandhora/Sanguinello,Memories (Rasi Z Remix),Imaginary Crossroads,6:58
Pansy Alert,Larry Fisherman,Wither EP,5:51
Paper Planes,Forever,Forever,2:02
Parvenu,Perfect Now,Perfect Now,3:26
Pasocom Music Club,hikari,Night Flow,4:07
Patrick Holland,Closer,Beaubien Dream,6:57
Pete Heller's Big Love; Eat Me,Big Love - Eat Me Edit,Big Love,4:01
Peter Palace,Lovahman,Lost And Found,5:47
Phil Weeks,The Grind,"Essential Beats, Vol. 5",7:14
PinkPantheress; Anz,All My Friends Know - Anz Remix,to hell with it (Remixes),5:11
piri; Tommy Villiers,soft spot,soft spot,3:39
Pleasurekraft; YellowHeads,Prison Planet,Pleasurekraft presents: Binaries,7:34
Polymod; Sasha,Full Circle,Full Circle,6:00
Porter Robinson, Everything Goes On (Assertive Hardcore Bootleg), Everything Goes On (Assertive Hardcore Bootleg) - Single,4:28
Porter Robinson,Language (Alex H Remix),Language (Alex H Remix) - Single,8:30
Porter Robinson,Look at the Sky (AZURA Remix),Look at the Sky (AZURA Remix),4:01
Porter Robinson,Musician (Synthion Hardcore Edit),Musician (Synthion Hardcore Edit),3:31
Porter Robinson,Something Comforting (Live),Secret Sky 2021 (Live),6:32
Porter Robinson,Sea of Voices (Remix) [Live],Secret Sky 2021 (Live),3:52
Porter Robinson,Goodbye To a World (Remix) [Live],Secret Sky 2021 (Live),3:01
Porter Robinson & Madeon,evil shelter,madeon edit,2:18
Porter Robinson & Madeon,Shelter (Live),Secret Sky 2021 (Live),0:05
Porter Robinson & Mat Zo,Easy (Pixel Terror DNB Flip),Pixel Terror Treats [Volume Four],2:40
Porter Robinson & Totally Enormous Extinct Dinosaurs,Unfold (Live),Secret Sky 2021 (Live),0:06
Porter Robinson; Madeon,Intro (Shelter Edit),Shelter Live,2:11
Porter Robinson; Madeon,Shelter (Shelter Edit),Shelter Live,3:37
Porter Robinson; Madeon,Pay No Mind x Easy (Shelter Edit),Shelter Live,3:57
Porter Robinson; Madeon,Sad Machine (Shelter Edit),Shelter Live,4:25
Porter Robinson; Madeon,You're On (Shelter Edit),Shelter Live,3:15
Porter Robinson; Madeon,OK x Lionhearted (Shelter Edit),Shelter Live,4:19
Porter Robinson; Madeon,Flicker (Shelter Edit),Shelter Live,2:57
Porter Robinson; Madeon,Finale x Cut The Kid (Shelter Edit),Shelter Live,4:14
Porter Robinson; Madeon,Imperium (Shelter Edit),Shelter Live,4:10
Porter Robinson; Madeon,Pop Culture (Shelter Edit),Shelter Live,1:20
Porter Robinson; Madeon,Divinity x Innocence (Shelter Edit),Shelter Live,6:20
Porter Robinson; Madeon,La Lune x Sea Of Voices (Shelter Edit),Shelter Live,2:22
Porter Robinson; Madeon,Fresh Static Snow (Shelter Edit),Shelter Live,2:22
Porter Robinson; Madeon,Home (Shelter Edit),Shelter Live,3:47
Porter Robinson; Madeon,Pixel Empire (Shelter Edit),Shelter Live,2:43
Porter Robinson; Madeon,Icarus x Fellow Feeling (Shelter Edit),Shelter Live,3:42
Porter Robinson; Madeon,Goodbye to a World (Shelter Edit),Shelter Live,5:01
Porter Robinson; Madeon,Encore (Shelter Edit),Shelter Live,5:51
projektbau,Easy 2 Love,Easy 2 Love,1:59
Qplex/Maurice Kaar,Mountains (Yannek Maunz Remix),Mountains (Yannek Maunz Remix),6:15
Raito,Deep Down Inside,Deep Down Inside - EP,5:49
RAY VOLPE,LASERBEAM (ANDEREX,LASERBEAM (ANDEREX - Single,3:12
Remco Beekwilder,LSD - Original Mix,LSD EP,5:21
Roald Velden & EcueD,Moments,Gallery:01 (Compiled By Lessov & Roald Velden),9:15
Romina; Franck Dona; Tone Depth,No Education - Tone Depth Remix,No Education (Tone Depth Remix),3:51
Rosbeh/Paulii,Oceans,Oceans,4:33
Ross from Friends,Talk to Me You'll Understand,You'll Understand,6:57
Rudosa,Smash The System,No Man's Land EP,7:01
Run The Jewels; Zack De La Rocha,Close Your Eyes (And Count to Fuck) [feat. Zack De La Rocha],Run the Jewels 2,3:54
rxflxct,Dreaming,Dreaming,4:03
salute; なかむらみなみ,go!,TRUE MAGIC,2:11
salute; なかむらみなみ; George Daniel,go! - George Daniel Remix,go! (George Daniel Remixes),3:41
Salyu; haruka nakamura,星のクズ α,星のクズ α,2:56
Sam Silver; ALRT; Yona; Leap96,Downtime,Downtime,2:36
Sans Soucis/Jacana People,Air (feat. Jacana People) (Jacana People Remix),Air (Remixes),3:25
Sasson (FR)/idd aziz/FNX OMAR/Taamy,Zani (FNX Omar Remix),Zani,6:25
Sebastian Davidson/Cir:cle,River Battles,River Battles,4:40
Seigen Tokuzawa; Masaki Hayashi,Iambic 9 Poetry,Drift,3:07
Shadow Child; Huxley,Err,Err,5:26
Shaolin Cowboy,Last Train to Peckham,2Be with You,6:00
Shaolin Cowboy,All Night All Summer,All Night All Summer,4:46
SHEE,Close My Eyes (Extended Mix),Close My Eyes,6:06
SHEE,Am I Just Living,Miami Ug 2019,6:01
Shermanology,Boyz N Da Club,Boyz N Da Club,6:17
Shire T,London. Paris. Berlin.,Tomorrows People,4:05
"skrillex, porter robinson",still here (nuphory x luna lenta lightmode mix),still here (nuphory x luna lenta lightmode mix) - Single,3:56
SLIM HUSTLA,2100,Compilation 2,5:48
Some Me,Engage,Engage,6:52
Staggr [257757361],hit me + ! [1419656392],cloudwire [2082910431],1:24
Static,feel,feel,2:54
Stereos,Summer Girl,Stereos (Deluxe Version),2:45
Steve Reich; Kristjan Järvi; Waltraut Wächter; Andreas Hartmann; MDR Leipzig Radio Symphony Orchestra,Duet for two Solo Violins and String Orchestra (Dedicated to and written for Yehudi Menuhin),Steve Reich - Duet,5:34
Sultan + Shepard; Elderbrook,I'll Be Here,"Endless, Dawn",4:52
sumthin sumthin,BEEN THRU,Growing Pains,3:26
sunflwr,you got your own thing going,you got your own thing going,4:03
Sunny Lax; Aneym,Everything's A Lie,Everything's A Lie,3:40
Suulo vs. Curl,Navida (Original Mix),"Kittball Konspiracy, Vol. 19",7:16
Svetec,Ruka Hore,Ruka Hore,5:16
Swimming Paul; Beaux Neptune,Good Girl,Good Girl,3:14
t0ni; Broosnica,keepsake Flip,keepsake Flip,3:04
T78,Ikarus,Ikarus,5:05
T78; Raito,Acid People,Acid People / Make Me Feel,6:59
tai hirose,sink,aquatic recitation,3:05
tai hirose,rain's footsteps,deepwater / gauze,2:05
tai hirose,magnify,magnify,1:02
Tame Impala,Borderline - Blood Orange Remix,Borderline,0:12
Taylor Swift,Love Story (Jake Silva Remix),Love Story (Jake Silva Remix) - Single,4:34
TDJ,Trpaslik Island,SPF INFINI 2,5:11
TDJ; fknsyd,Euphoria,Euphoria,4:14
TDJ; Peterparker69,ただdance in the dark,ただdance in the dark,4:12
Ted Poley; Tony Harnell,Escape From The City ...for City Escape,SONIC ADVENTURE 2 Original Soundtrack (20th Anniversary Edition),2:19
Tentendo; Jordan Dennis,Function,Function,3:18
Tenth Planet; Vincent de Moor,Ghosts - Vincent De Moor Mix,Ghosts,6:46
Terror Jr; AOBeats,Be Some Body,Be Some Body,3:01
The Knocks; TEED,Walking On Water (feat. TEED),HISTORY,3:36
thomfjord,Intrastellar,Intrastellar EP,5:21
thomfjord,Terminal Velocity,Intrastellar EP,5:56
thomfjord,Day , Nite,Intrastellar EP
Tom Jarmey; Ed Hodge,Deeper Pacific,Amber Glass,6:42
Tommie Sunshine; MureKian; Sabrina Signs; Raito,Save the Rave - Raito Remix,Save the Rave,4:30
Tommy Holohan; Clouds,South Beach Burnin Bins (Clouds Remix),RVE002,5:32
Too $hort,You Came to Party,You Came to Party,3:53
Tribz/Calixte,Sweet Disposition (Vijay & Sofia Edit),Sweet Disposition (Vijay & Sofia Edit),3:28
Tritonal; Lourdiz; Kyau & Albe,Love U Right - Kyau & Albert R,"Love U Right (Remixes, Pt. 1)",2:49
Truncate,Our Bodies,TRUNCEI8HT EP,5:12
TWONSKi,DiAMONDS,DiAMONDS,3:08
UMEK,Double the Lust - Original Mix,Double the Lust,7:40
V. Christie,Talk To Me,Talk To Me,3:53
Vanessa Sukowski; ABYSSVM,Lucanus Cervus - Abyssvm Remix,Lucanus Cervus,7:08
Varg²™; Bladee; Skrillex,Is there a place in heaven for boys like me?,"Nordic Flora Series, Pt. 6: Outlaw Music",3:41
Varra,Catto Observatory - Remix,Catto Observatory (Remix),2:47
Veerus,Heavy,FOUR ON JAM 01,6:52
Village,C-R-E-W,C-R-E-W,3:25
Vincent,Love Me Now (Extended),Love Me Now (Extended) - Single,4:07
Vincent de Moor,Fly Away - Extended Vocal Mix,Fly Away,8:21
VTSS,Sober Raving,HELLCAT VOL.1,5:38
Wassu,Escape,Tales of Romance I,7:12
water feature,Pond Bather,Various Birds,1:11
wes mills; meli,this time,this time,3:12
Willaris. K; jamesjamesjames,Silversun,Silversun,5:37
Willo,Past Time,Past Time,5:40
Worakls,Detached Motion (Patrice Bdumel Remix),Hortari & Detached Motion Remixes,6:33
Worakls/Ben Bvhmer/eivxr,Red Dressed (Ben Bvhmer Remix - Short Version),Red Dressed (Ben Bvhmer Remix),3:46
X-COAST,Party Time - Extended,Pianissimo,5:46
X-COAST,The Ultimate - Extended,Pianissimo,6:09
Yeek,Long Time No See,Long Time No See,1:51
yuragi,soon,nightlife,2:46
yuragi,night is young,nightlife,5:44
Zigan Aldi,Heppnar (Iorie Remix),Heppnar (Iorie Remix),7:22
Zmi,Shigatsu ame,Fu-ne,2:50
zodivk,yours (+),yours (+),2:55
adri; dossyx; Feathervane; Stardreams; wintercolor,Atonement,STORYBOOK: Chapter 2,6:12
⣎⡇ꉺლ༽இ•̛)ྀ◞ ༎ຶ ༽ৣৢ؞ৢ؞ؖ ꉺლ,ƪ. ◖ƪ❍⊁◞.,◗щ (*ㅇ△₊⁎❝᷀ົཽ*ೃ:(꒡͡ ❝᷀ົཽ ꉺ¨.·*:・✧⃛(ཽ๑,)✧⃛*
1 Artist Title Album Length
2 508:29
3 32ki; Hatsune Miku; 重音テト メズマライザー (feat. 初音ミク&重音テト) メズマライザー 2:37
4 33 Below; DRIIA LOYAL LOYAL 4:15
5 [KRTM]; TWAN Black Widow Gum EP 6:21
6 \berhaupt & Au_erdem Late Breakfast (Original) Badarchin, Vol. 04 7:08
7 a r u k a.; DOVVE; echoed.; flawguri; Stardew Aquarius STORYBOOK: Chapter 2 5:09
8 A*S*Y*S Bassface Bassface EP 6:30
9 Above & Beyond/Oceanlab Satellite Super8 & Tab Presents: 20 Years Of Anjunabeats 3:29
10 Above & Beyond; Marty Longstaff Flying By Candlelight - Above & Beyond Club Mix Flying By Candlelight 4:02
11 Above & Beyond; Richard Bedford Northern Soul Common Ground 5:35
12 Adam Beyer; Kölsch What You Need - Kölsch Remix What You Need (Kölsch Remix) 6:40
13 Aiko ETHERIA TECHNIKA [memory system] TECHNIKA [memory system]
14 AIROD Universe of 90's Techno Parties [Molekul 007] Universe of 90's Techno Parties [Molekul 007] 7:56
15 Alaskan Tapes All We Can't See All We Can't See / An Image 1:56
16 Alegant/Tube & Berger Get Down (Short Edit) False Gods EP (Short Edits) 3:34
17 Alesso; Tove Lo Heroes (we could be) Forever 3:31
18 Alex Luciano Under the Rain Under the Rain 3:35
19 Alex Preston Love You Better - Extended Mix Love You Better 5:29
20 Alignment Interference - Luca Agnelli Re Interference 7:18
21 Alignment; Regal Interference - Regal Remix Interference 6:24
22 Alphazone Alphazone Sunrise - Original C Alphazone Sunrise 7:08
23 alt-J; Danny Brown; The Alchemist Deadcrush (feat. Danny Brown) - Alchemist x Trooko Version Deadcrush (feat. Danny Brown) [Alchemist x Trooko Version] 4:03
24 altrice; Blue Dust Archive bda creature compciter ep 4:53
25 Amos & Riot Night; Ren Faye Different View Different View 4:13
26 Amos & Riot Night; Ren Faye Different View Different View 8:42
27 Amy Axegale In Your Section In Your Section 5:33
28 Andain; Myon; Shane 54 Promises - Myon & Shane 54 Sum Promises 3:48
29 Andrea Kaeri/Phonez/BEKIMACHINE Nothing for You Dreamlike 5:26
30 Anetha; Sugar Candy from Strangers Don't Rush to Grow Up 8:07
31 ANNA; Miss Kittin Forever Ravers Speicher 112 6:00
32 Anton Kling Mareld Mareld 7:32
33 Apashe; VOLAC Distance - Volac Remix Distance (Volac Remix) 3:35
34 Armin van Buuren; Tempo Giusto; i_o Mr. Navigator - i_o Remix Balance (Remixes, Pt. 1) 5:54
35 ARTY; Brina Knauss Hope - Brina Knauss Remix Hope (Brina Knauss Remix) 5:58
36 ARXMANE '98 RACE '98 RACE 1:40
37 Asagaoaudio ring and portrait the next game EP 5:52
38 At Dawn Bare Essentials (Vip Edit) Routine Espresso 002: Cafe Dreams 5:36
39 Avicii Silhouettes - Original Mix Silhouettes 7:00
40 Avicii; Sandro Cavazza Without You (feat. Sandro Cavazza) AVĪCI (01) 2:58
41 Baalti Aame Baalti 2:28
42 Baalti Ustad Thandai - Flavours of the East 6:11
43 BABii; Pholo EMBER (crashed) (crashed)_PHOLO EMBER (crashed) (crashed)_PHOLO 3:46
44 Baby Billy Freeman Bible Bonkers (Theme Song) The Righteous Gemstones: Season 3 (HBO Original Series Soundtrack) 0:48
45 Baile Dazed Siren 6:46
46 Barry Can't Swim; somedeadbeat Deadbeat Gospel When Will We Land? 4:14
47 Basstakil What If We Are Not Alone What If We Are Not Alone 6:30
48 Beatenberg The Lighthouse of Alexandria On the way to Beatenberg 2:39
49 Belljack 불행한 Searching Within 6:03
50 Benny Bridges Lucky Penny (Black Loops Remix) Lucky Penny (Black Loops Remix) 5:33
51 biskuwi Veil Two Minds 5:23
52 Bjorn Salvador/Middle Aged Dad Steina Steina 8:08
53 Black Eyed Peas Pump It (GWE Pump It (GWE - Single 5:31
54 Bladee Be Nice 2 Me (Euphorizer Remix) Be Nice 2 Me (Euphorizer Remix) - Single 3:21
55 BLCKLGHT Away From You Away From You 4:21
56 Bodysync; Ryan Hemsworth; Giraffage Rhythm Rhythm 2:32
57 Bomat Doing All Right Doing All Right 7:17
58 Boris Brejcha Lieblingsmensch - Edit Lieblingsmensch (Edit) 3:40
59 Boris Brejcha Never Look Back - Edit Never Look Back (Edit) 3:25
60 Boris Brejcha; Ginger Happinezz - Edit Happinezz (Edit) 4:05
61 Bou; Trigga Veteran Scorpio 4:29
62 Brendan Park Relic 19 Melt EP 4:33
63 Brooks Aleksander You You 4:11
64 Bruce Hornsby 5.Song C - Instrumental Spirit Trail 2:47
65 Bug; Boyskin Wait Wait 6:31
66 Buku TiiGA TiiGA 3:48
67 BUSDRIVER; Aesop Rock; Danny Brown Ego Death Perfect Hair 6:00
68 Catz 'n Dogz; Jono McCleery Would You Believe Friendship 4:30
69 CBDJ towards you towards you 6:08
70 Chambray Anew Work That 7:42
71 Chance the Rapper; Future Smoke Break (feat. Future) Coloring Book 3:46
72 CHANEY Blank Space Blank Space 5:17
73 Chicago Loop Supernova 303 Memories Made / Supernova 303 3:36
74 Chris Luno Better Try Again Better Try Again 5:18
75 Chris Luno/Beron The Hike The Hike 7:46
76 Chris Luno/Beron Friday The Hike 6:43
77 Chris Luno/Beron Saturday The Hike 7:37
78 CJ Bolland; The Advent; Enrico Sangiuliano Camargue 2019 - Enrico Sangiuliano Remix Camargue 2019 8:14
79 Clams Casino; Vince Staples All Nite (feat. Vince Staples) 32 Levels (Deluxe) 2:47
80 clipping.; Cocc Pistol Cree Work Work CLPPNG 3:43
81 Coastal New Life Cohesion 2:21
82 Cosmo's Midnight; Sarah Bonito Hurt (feat. Sarah Bonito) Moments - EP 3:30
83 Crimsen/Feyln/Oliver Wickham Grey Skies (Gorge Remix) Grey Skies (Gorge Remix) 6:47
84 Cynthoni; Sewerslvt; AgonyOST Quitting Is Hard, Not Quitting Is Harder SMOKE IT TO THE BUTT 5:49
85 dUO (Sakura Fujiwara) DdK _ ;_Dk (Itsuka Mita Eiga Mitai Ni) AIRPORT 3:19
86 D-Jastic Up To No Good - Extended Mix Up To No Good (Extended Mix) 5:11
87 D-Mad; ARTY She Gave Happiness - Arty Remix Digital Society International, Volume Three - Mixed by Ashley Wallbridge & Activa 6:05
88 Daijiro Nakagawa Concentration Loop Lullaby 2:44
89 Dan be; Alex Bone; hej motti Ooh - hej motti Remix Ooh (hej motti Remix) 2:58
90 Danny Brown; Kendrick Lamar; Ab-Soul; Earl Sweatshirt Really Doe Atrocity Exhibition 5:19
91 Danny Brown; Purity Ring 25 Bucks (feat. Purity Ring) Old 3:30
92 Daphni; Caribou Waiting So Long Waiting So Long 3:54
93 Darby; OOTORO PARTY PARTY 2:08
94 Darc Marc Dirty Rocking Bass - Dirty Rocking Bassline Dirty Rocking Bass (Dirty Rocking Bassline) 7:48
95 dark cat; juu; Cinders BUBBLE TEA BUBBLE TEA 4:03
96 Dash Berlin; Emma Hewitt Waiting The New Daylight 9:28
97 Data_haven Utopia - Reprise Data_haven [re-constructed] 5:23
98 Davey Asprey Catch Catch 3:20
99 David Guetta; Sia Titanium (feat. Sia) Nothing but the Beat 2.0 4:05
100 David Guetta; USHER Without You (feat. Usher) Nothing but the Beat 2.0 3:28
101 Deadmau5 The Veldt V Porter Robinson - Language The Veldt V Porter Robinson - Language - Single 2:07
102 deadmau5; i_o Imaginary Friends - i_o Remix We Are Friends, Vol. 8 6:16
103 deadmau5; Rinzen Monophobia - Rinzen Remix mau5ville: Level 1 6:41
104 Death Grips Beware Exmilitary 5:52
105 Deborah de Luca; Marika Rossa In Hypnose In Hypnose 6:35
106 Diamond Dallas Tex; Lil Texas Work My Body The Afters 3:35
107 Diffrent A Little Closer (Extended)145 A Little Closer (Extended)145 - Single 5:18
108 Diveo Ferris Wheel Singles Club Executive Lounge: Celebrating 50 Years of Activia Benz 3:05
109 DJ BORING Like Water - Edit Like Water 6:40
110 DJ Dextro Sideral Sideral 6:11
111 dj ignorant i don't want to lose you i don't want to lose you 5:17
112 DJ Koze Pick Up - 12" Extended Disco Version Pick Up 10:01
113 DJ PP Desert Night (Extended Mix) Desert Night 6:37
114 DJ Seinfeld; ARY Of Joy Everything U 3:19
115 DJ_Dave; kitty ray Still Miss U Still Miss U 3:47
116 doechii nissan altima (wheth nissan altima (wheth - Single 2:10
117 Double Touch/Reigan Greatest Day Greatest Day 3:49
118 DRT; Alex K Take Me Away - Alex K Remix Take Me Away 2:27
119 Drunken Kong Realize Repeat 7:27
120 dublon; Deza Sweet Nothings Sweet Nothings 2:22
121 Duke Hugh Greenleaf Canvas 4:29
122 DV-i Genconfinal Metronomicon Audio 6.0 1:00
123 Eclipse 24-7 - Original Mix 24-7 8:04
124 Ed Balloon; Open Mike Eagle; They Hate Change Missed Call Missed Call 3:55
125 Effy; Mall Grab iluv iluv 4:17
126 Ejeca U Gave Me Love U Gave Me Love 2:21
127 Eleni Foureira; ARCADE Disco-Tech Disco-Tech 3:19
128 EMBRZ Don't Look Back Don't Look Back 3:27
129 EMBRZ; Montgomery Count To Three Count To Three 3:18
130 Eric Prydz Pjanoo - Club Mix Pjanoo 7:30
131 Ettu Brute Know Concrete 3:15
132 Ezekiel help_urself (breakcore) help_urself (Alternates) 1:58
133 F-Rontal Inner Souls Inner Souls 6:58
134 FallenLights. You Are Enough For Her 1:51
135 Farkey/Hayedeh Flashback Flashback 3:13
136 Felix Cartal; Hanne Mjøen My Last Song (Felix Cartal's After Hours Mix) My Last Song (Felix Cartal's After Hours Mix) 3:58
137 FenixForce Domestic Violence Inexcusable 5:52
138 Fhase 87 Redemption Redemption EP 5:24
139 Flying Lotus; Kendrick Lamar Never Catch Me You're Dead! 3:54
140 Flywright Thinkin' Bout You Lately Hush Your Fuss 2:42
141 Frankyeffe; Teenage Mutants Polymath Polymath 6:11
142 Fred again.. & Anderson .Paak & Chika places to be (CLIPZ remix) places to be (remixes) 3:04
143 Fred again..; Anderson .Paak; CHIKA; CLIPZ places to be - CLIPZ remix places to be (remixes) 3:04
144 Fred again..; Baby Keem; Nia Archives leavemealone - Nia Archives Remix USB 2:59
145 FROOTS Papaya Papaya 3:12
146 Frost Children Satellites Tweaker Poem 3:31
147 Gabriel & Dresden; Sub Teal; C Only Road - Cosmic Gate Remix Only Road (Cosmic Gate Remix) 3:45
148 Gensuke Kanki Vertige Durée 3:54
149 Gilles Bernies Knowing Knowing 6:01
150 Gilmer Galibard Bougarabou A Baker's Shit Ton 5:36
151 Glen Campbell Wichita Lineman - Remastered 2001 Wichita Lineman (Remastered) 3:06
152 Goncalo M Infinite Black Best Weapons 6:08
153 Good Times Ahead; Vince Staples Little Bit of This (feat. Vince Staples) Good Times Ahead 2:40
154 Greg Gonzalez Country of the Mind (Edit) Country Of The Mind 6:25
155 Hadone How To Fake Success - Original mix Defining Persuasion (Original) 6:27
156 Hailaker; S. Carey Wavepool Wavepool 3:38
157 harmony haven; Phusis Violet Flash Healing Visions 2:48
158 Helios Bless This Morning Year Eingya (Remastered) 6:01
159 HNNY Noth... Nothing 5:12
160 Hope Tala Tiptoeing - Tommy Villiers Remix Tiptoeing (Tommy Villiers Remix) 3:08
161 Hypsidia/Altriparty Viral (Notre Dame Remix) Viral (The Remixes) 4:14
162 HYUKOH; 落日飛車 Sunset Rollercoaster Aaaannnnteeeeennnaaaaaa AAA 6:50
163 i_o, Raito Rivolution rivolution 5:12
164 i_o; Lights Annihilation Annihilation 4:07
165 i_o; Raito Come with Me House of God 6:05
166 i_o; Raito Don't Stop révolution 5:24
167 Ian van Dahl; Marsha Castles In The Sky (featuring Castles In The Sky 6:46
168 Ilja Alexander Someday (LCAW Remix) Someday (LCAW Remix) 6:38
169 Illiya Korniyenko Ithori - Original The Collapse Of Future Vol.3 6:34
170 Inafekt; Big Jon Blaze (Extended Mix) Blaze 5:35
171 Infeed; INNERGATED Freedom Inside Your Illness - LS41 Remix Resurrected in the Hell 5:00
172 Isambard Khroustaliov Atoll Song This Is My Private Beach, This Is My Jetsam 6:43
173 ISOxo; Ninajirachi SHYPOP kidsgonemad! 3:28
174 J.Shore; Mango; Orion; Shingo Nakamura Raining In Osaka - Shingo Nakamura Remix Raining In Osaka 7:44
175 Jacana People Imari Imari 4:23
176 jahjaylee Right, Right. Right? jah 6:43
177 Jake Kaiser Kestrel Winter Selections 06 3:21
178 jamesjamesjames It’s Not You, It’s Me - Club Mix It's Not You, It's Me (Club Mix) 3:44
179 jamesjamesjames; snctm. homesoon. homesoon. 3:55
180 jamesjamesjames; Spring Breaker ibiza is waiting for me ibiza is waiting for me 5:27
181 Japano File Holy.Place Holy.Place 3:50
182 Jaques Raupé; Felix Harrer 3 Haselnüsse 3 Haselnüsse 2:28
183 Jason Ross; Fiora Through It All Through It All 3:12
184 Jauz; i_o Truth Truth 5:45
185 Jayda G Both Of Us - Edit Both Of Us 3:49
186 Jazzanova/Edward Vanzet I'm Still Here (Winnie & Somow Remix) I'm Still Here - Best's Friends Remixes 6:03
187 Jend Forever Forever 2:38
188 Jensen Interceptor; Assembler Code Runner 6th Element 5:17
189 Jeshi; Ross from Friends 3210 - Ross from Friends Remix 3210 (Ross from Friends Remix) 5:52
190 jizue photograph feat.中嶋イッキュウ shiori 5:56
191 Johnny Jungle Johnny '94 - Dillinja Remix Johnny '94 (Dillinja And Noise Of Art & Double D Remixes) 6:28
192 Joy Anonymous JOY (Up The Street) Cult Classics 3:58
193 JUKO; robo; wishlane; Mistrrr; The Litehouse; Foli The Guardian STORYBOOK 4:56
194 Julius Abel Lost Lost 3:39
195 Kacy Hill I Believe In You (feat. Francis and the Lights) Is It Selfish If We Talk About Me Again 0:29
196 KAISUI Cyclone Cyclone EP 4:13
197 Kay Cee Escape - Club Mix Escape 6:51
198 Kayestone Atmosphere - Angelic Remix Atmosphere 8:27
199 Keiju(JP) Only U Know Only U Know 6:18
200 Kesha Die Young (Lil Texas Edit) Die Young (Lil Texas Edit) - Single 2:15
201 Kevin Flum Mansion Tears Mansion Tears 2:20
202 kita kouhei Botanical Specimens Neospecies 2:58
203 Kito; Chrome Sparks; IS U IS U Melter Melter 4:14
204 Kloyd Sur Night Tales 5:34
205 KOAN Sound Shimmer Moments of Opening 4:00
206 KOAN Sound Colour Dance Moments of Opening 4:29
207 KOAN Sound Allure Moments of Opening 2:51
208 KOAN Sound Phantom Sight Moments of Opening 4:48
209 Kobe JT Lately Lately 3:52
210 Koriass Zombies Love suprême 3:38
211 Kumi Takahara; aus Tide Tide 6:02
212 Kwengface; Joy Orbison; Overmono Freedom 2 Freedom 2 3:26
213 Kyau & Albert; Jeza Bring You Back - Original Mix Matching Stories 4:35
214 Kyuuwaii sora bedroom 2:46
215 Lana Del Rey; Cedric Gervais Summertime Sadness (Lana Del Rey Vs. Cedric Gervais) - Cedric Gervais Remix Summertime Sadness (Lana Del Rey Vs. Cedric Gervais) [Cedric Gervais Remix] 3:34
216 Landhouse/Sima Aava Tales from the Swallow (Pandhora Remix) Tales from the Swallow (Remixes) 4:10
217 Lauren Mia Colours Colours 7:26
218 Le Youth; Lane 8; Jyll I Will Leave a Light On About Us 4:00
219 Les Petits Pilous Russian Forest Bielle 3:44
220 Lil Texas; Irradiate Highs In My Dreams Highs In My Dreams 2:26
221 Lilly Palmer Shift All Things Fall 6:52
222 Local Nomad, Nihil Young & Paige/Local Nomad/Nihil Young/Paige Gates (Paige & Nihil Young Remix) - Extended Gates (Paige & Nihil Young Remix) 5:33
223 Logic1000; yunè pinku What You Like What You Like (feat. yunè pinku) 3:13
224 Longo; Marco Lollis Love Again Love Again 2:59
225 longstoryshort HER HER 2:36
226 Loods Need U Need U 3:22
227 Loods; KLP Freedom Freedom 2:54
228 Lord RAJA; Jeremiah Jae Van Go (feat. Jeremiah Jae) A Constant Moth 2:04
229 Lorenzo Raganzini; Paolo Ferrara; OGUZ Raving in Paris - Oguz Journey Mix Raving in Paris (Oguz Journey Mix) 11:18
230 Luciid Mob Mob / D.O.L.L.A 8:12
231 Luciid D.O.L.L.A Mob / D.O.L.L.A 6:38
232 Luisa Marion The Kite (Campfire Session) [Live] The Kite (Campfire Session) [Live] 3:55
233 LUV HRTS CAN WE DO BETTER? CAN WE DO BETTER? 3:07
234 LUV HRTS GONNA GET THRU GONNA GET THRU 3:20
235 LUV HRTS, Mild Minds U REALLY HURT ME U REALLY HURT ME 2:42
236 LYNY; FLY Pulse Pulse 2:27
237 M Field Andrew Andrew 2:44
238 Madeon GoodFaith Radio (Ep. 1) GoodFaith Radio 59:29
239 Mall Grab; Skin On Skin Strangers Strangers 6:09
240 Mango,Orion, J.Shore When All the Ships Are Gone (Album Mix] Citylanes Airplanes 9:30
241 Mannequin; prod. DTM Lay It On Me Lay It On Me 2:17
242 Manuel Di Martino Impact Alpha Sides Vol. IV 5:10
243 Mark Michael Solar Storm - Original Mix Solar Storm 4:48
244 Martin Solveig; Faouzia; TRYM Now Or Never - TRYM Remix Now Or Never (Remixes) 5:17
245 Masakatsu Takagi Marginalia #131 Marginalia #131 7:10
246 Masakatsu Takagi Marginalia #139 Marginalia #139 2:22
247 Masakatsu Takagi Marginalia #143 Marginalia #143 2:53
248 Masakatsu Takagi Marginalia #153 Marginalia #153 2:53
249 Masakatsu Takagi; 坂本美雨 Field Fjord sorato 1:00
250 Matteo Vitanza Acid? Why Not? We Overcome 6:57
251 Mees Javois DSTOP DSTOP 5:27
252 Melé And 1 (Extended Mix) And 1 (Extended Mix) - Single 4:30
253 Michael Klein Yuzu Yuzu 6:47
254 Middle School remember remember 1:23
255 midnight; ylxr Promethium Reprise Lo-Fi_house.Zip 4:00
256 Mike Posner I Took a Pill in Ibiza (SeeB r At Night, Alone. 3:19
257 Miksu / Macloud/Makko Nachts wach (Lila Wolken Bootleg) BRAVO Hits 2023 2:38
258 Milo; Hemlock Ernst Souvenir So The Flies Don't Come 3:45
259 Murder He Wrote; Idris Miles Rave Out Rave Out 3:16
260 My Sister's Fugazi Shirt ONE MORE FINAL: I need you Man Fears the Darkness, and So He Scrapes Away at the Edges of It With Fire 4:29
261 Namatjira Sounds Of Sunday Morning (RIGOONI Remix) Moons Of Yesterday (Remix Project 1) 7:53
262 Nhato; Kenji Sekiguchi Tokyo Blue Pipe - Kenji Sekigu Tokyo Blue Pipe (Kenji Sekiguc 6:26
263 Ninajirachi; daine It's You I Love My Computer 2:49
264 Ninajirachi; daine; underscores It's You - underscores' "It’s U" Remix It's You (underscores' "It’s U" Remix) 3:31
265 Ninajirachi; Effy CSIRAC - Effy Remix CSIRAC (Effy Remix) 4:33
266 Ninajirachi; horsegiirL Delete - horsegiirL Remix Delete (horsegiirL Remix) 2:54
267 No Buses Eyes Eyes 3:20
268 NOTION, Chrystal The Days (NOTION Remix) djsoundtop.com djsoundtop.com 3:53
269 Odagled Time Travel (Monikaze Remix) Time Travel 5:23
270 Onaisit the bit you were here was fun, maybe that makes it all worth it the bit you were here was fun, maybe that makes it all worth it 2:28
271 Oncill; lillybug; evangeline; nshvll; Ridoy; clothespin. Journey Across The Mountain STORYBOOK: Chapter 2 7:10
272 Open Mike Eagle; Gold Panda Ziggy Starfish (Anxiety Raps) [feat. Gold Panda] A Special Episode - EP 2:55
273 orbe; haruka nakamura; 田辺玄 ship orbe Ⅰ 3:50
274 Otira Pill Pill 2:09
275 Otira Take Me Take Me 3:50
276 Pablo Caballero; Tankhamun Journey Journey EP 6:12
277 Pandhora/Sanguinello Memories (Rasi Z Remix) Imaginary Crossroads 6:58
278 Pansy Alert Larry Fisherman Wither EP 5:51
279 Paper Planes Forever Forever 2:02
280 Parvenu Perfect Now Perfect Now 3:26
281 Pasocom Music Club hikari Night Flow 4:07
282 Patrick Holland Closer Beaubien Dream 6:57
283 Pete Heller's Big Love; Eat Me Big Love - Eat Me Edit Big Love 4:01
284 Peter Palace Lovahman Lost And Found 5:47
285 Phil Weeks The Grind Essential Beats, Vol. 5 7:14
286 PinkPantheress; Anz All My Friends Know - Anz Remix to hell with it (Remixes) 5:11
287 piri; Tommy Villiers soft spot soft spot 3:39
288 Pleasurekraft; YellowHeads Prison Planet Pleasurekraft presents: Binaries 7:34
289 Polymod; Sasha Full Circle Full Circle 6:00
290 Porter Robinson Everything Goes On (Assertive Hardcore Bootleg) Everything Goes On (Assertive Hardcore Bootleg) - Single 4:28
291 Porter Robinson Language (Alex H Remix) Language (Alex H Remix) - Single 8:30
292 Porter Robinson Look at the Sky (AZURA Remix) Look at the Sky (AZURA Remix) 4:01
293 Porter Robinson Musician (Synthion Hardcore Edit) Musician (Synthion Hardcore Edit) 3:31
294 Porter Robinson Something Comforting (Live) Secret Sky 2021 (Live) 6:32
295 Porter Robinson Sea of Voices (Remix) [Live] Secret Sky 2021 (Live) 3:52
296 Porter Robinson Goodbye To a World (Remix) [Live] Secret Sky 2021 (Live) 3:01
297 Porter Robinson & Madeon evil shelter madeon edit 2:18
298 Porter Robinson & Madeon Shelter (Live) Secret Sky 2021 (Live) 0:05
299 Porter Robinson & Mat Zo Easy (Pixel Terror DNB Flip) Pixel Terror Treats [Volume Four] 2:40
300 Porter Robinson & Totally Enormous Extinct Dinosaurs Unfold (Live) Secret Sky 2021 (Live) 0:06
301 Porter Robinson; Madeon Intro (Shelter Edit) Shelter Live 2:11
302 Porter Robinson; Madeon Shelter (Shelter Edit) Shelter Live 3:37
303 Porter Robinson; Madeon Pay No Mind x Easy (Shelter Edit) Shelter Live 3:57
304 Porter Robinson; Madeon Sad Machine (Shelter Edit) Shelter Live 4:25
305 Porter Robinson; Madeon You're On (Shelter Edit) Shelter Live 3:15
306 Porter Robinson; Madeon OK x Lionhearted (Shelter Edit) Shelter Live 4:19
307 Porter Robinson; Madeon Flicker (Shelter Edit) Shelter Live 2:57
308 Porter Robinson; Madeon Finale x Cut The Kid (Shelter Edit) Shelter Live 4:14
309 Porter Robinson; Madeon Imperium (Shelter Edit) Shelter Live 4:10
310 Porter Robinson; Madeon Pop Culture (Shelter Edit) Shelter Live 1:20
311 Porter Robinson; Madeon Divinity x Innocence (Shelter Edit) Shelter Live 6:20
312 Porter Robinson; Madeon La Lune x Sea Of Voices (Shelter Edit) Shelter Live 2:22
313 Porter Robinson; Madeon Fresh Static Snow (Shelter Edit) Shelter Live 2:22
314 Porter Robinson; Madeon Home (Shelter Edit) Shelter Live 3:47
315 Porter Robinson; Madeon Pixel Empire (Shelter Edit) Shelter Live 2:43
316 Porter Robinson; Madeon Icarus x Fellow Feeling (Shelter Edit) Shelter Live 3:42
317 Porter Robinson; Madeon Goodbye to a World (Shelter Edit) Shelter Live 5:01
318 Porter Robinson; Madeon Encore (Shelter Edit) Shelter Live 5:51
319 projektbau Easy 2 Love Easy 2 Love 1:59
320 Qplex/Maurice Kaar Mountains (Yannek Maunz Remix) Mountains (Yannek Maunz Remix) 6:15
321 Raito Deep Down Inside Deep Down Inside - EP 5:49
322 RAY VOLPE LASERBEAM (ANDEREX LASERBEAM (ANDEREX - Single 3:12
323 Remco Beekwilder LSD - Original Mix LSD EP 5:21
324 Roald Velden & EcueD Moments Gallery:01 (Compiled By Lessov & Roald Velden) 9:15
325 Romina; Franck Dona; Tone Depth No Education - Tone Depth Remix No Education (Tone Depth Remix) 3:51
326 Rosbeh/Paulii Oceans Oceans 4:33
327 Ross from Friends Talk to Me You'll Understand You'll Understand 6:57
328 Rudosa Smash The System No Man's Land EP 7:01
329 Run The Jewels; Zack De La Rocha Close Your Eyes (And Count to Fuck) [feat. Zack De La Rocha] Run the Jewels 2 3:54
330 rxflxct Dreaming Dreaming 4:03
331 salute; なかむらみなみ go! TRUE MAGIC 2:11
332 salute; なかむらみなみ; George Daniel go! - George Daniel Remix go! (George Daniel Remixes) 3:41
333 Salyu; haruka nakamura 星のクズ α 星のクズ α 2:56
334 Sam Silver; ALRT; Yona; Leap96 Downtime Downtime 2:36
335 Sans Soucis/Jacana People Air (feat. Jacana People) (Jacana People Remix) Air (Remixes) 3:25
336 Sasson (FR)/idd aziz/FNX OMAR/Taamy Zani (FNX Omar Remix) Zani 6:25
337 Sebastian Davidson/Cir:cle River Battles River Battles 4:40
338 Seigen Tokuzawa; Masaki Hayashi Iambic 9 Poetry Drift 3:07
339 Shadow Child; Huxley Err Err 5:26
340 Shaolin Cowboy Last Train to Peckham 2Be with You 6:00
341 Shaolin Cowboy All Night All Summer All Night All Summer 4:46
342 SHEE Close My Eyes (Extended Mix) Close My Eyes 6:06
343 SHEE Am I Just Living Miami Ug 2019 6:01
344 Shermanology Boyz N Da Club Boyz N Da Club 6:17
345 Shire T London. Paris. Berlin. Tomorrow’s People 4:05
346 skrillex, porter robinson still here (nuphory x luna lenta lightmode mix) still here (nuphory x luna lenta lightmode mix) - Single 3:56
347 SLIM HUSTLA 2100 Compilation 2 5:48
348 Some Me Engage Engage 6:52
349 Staggr [257757361] hit me + ! [1419656392] cloudwire [2082910431] 1:24
350 Static feel feel 2:54
351 Stereos Summer Girl Stereos (Deluxe Version) 2:45
352 Steve Reich; Kristjan Järvi; Waltraut Wächter; Andreas Hartmann; MDR Leipzig Radio Symphony Orchestra Duet for two Solo Violins and String Orchestra (Dedicated to and written for Yehudi Menuhin) Steve Reich - Duet 5:34
353 Sultan + Shepard; Elderbrook I'll Be Here Endless, Dawn 4:52
354 sumthin sumthin BEEN THRU Growing Pains 3:26
355 sunflwr you got your own thing going you got your own thing going 4:03
356 Sunny Lax; Aneym Everything's A Lie Everything's A Lie 3:40
357 Suulo vs. Curl Navida (Original Mix) Kittball Konspiracy, Vol. 19 7:16
358 Svetec Ruka Hore Ruka Hore 5:16
359 Swimming Paul; Beaux Neptune Good Girl Good Girl 3:14
360 t0ni; Broosnica keepsake Flip keepsake Flip 3:04
361 T78 Ikarus Ikarus 5:05
362 T78; Raito Acid People Acid People / Make Me Feel 6:59
363 tai hirose sink aquatic recitation 3:05
364 tai hirose rain's footsteps deepwater / gauze 2:05
365 tai hirose magnify magnify 1:02
366 Tame Impala Borderline - Blood Orange Remix Borderline 0:12
367 Taylor Swift Love Story (Jake Silva Remix) Love Story (Jake Silva Remix) - Single 4:34
368 TDJ Trpaslik Island SPF INFINI 2 5:11
369 TDJ; fknsyd Euphoria Euphoria 4:14
370 TDJ; Peterparker69 ただdance in the dark ただdance in the dark 4:12
371 Ted Poley; Tony Harnell Escape From The City ...for City Escape SONIC ADVENTURE 2 Original Soundtrack (20th Anniversary Edition) 2:19
372 Tentendo; Jordan Dennis Function Function 3:18
373 Tenth Planet; Vincent de Moor Ghosts - Vincent De Moor Mix Ghosts 6:46
374 Terror Jr; AOBeats Be Some Body Be Some Body 3:01
375 The Knocks; TEED Walking On Water (feat. TEED) HISTORY 3:36
376 thomfjord Intrastellar Intrastellar EP 5:21
377 thomfjord Terminal Velocity Intrastellar EP 5:56
378 thomfjord Day Nite Intrastellar EP
379 Tom Jarmey; Ed Hodge Deeper Pacific Amber Glass 6:42
380 Tommie Sunshine; MureKian; Sabrina Signs; Raito Save the Rave - Raito Remix Save the Rave 4:30
381 Tommy Holohan; Clouds South Beach Burnin Bins (Clouds Remix) RVE002 5:32
382 Too $hort You Came to Party You Came to Party 3:53
383 Tribz/Calixte Sweet Disposition (Vijay & Sofia Edit) Sweet Disposition (Vijay & Sofia Edit) 3:28
384 Tritonal; Lourdiz; Kyau & Albe Love U Right - Kyau & Albert R Love U Right (Remixes, Pt. 1) 2:49
385 Truncate Our Bodies TRUNCEI8HT EP 5:12
386 TWONSKi DiAMONDS DiAMONDS 3:08
387 UMEK Double the Lust - Original Mix Double the Lust 7:40
388 V. Christie Talk To Me Talk To Me 3:53
389 Vanessa Sukowski; ABYSSVM Lucanus Cervus - Abyssvm Remix Lucanus Cervus 7:08
390 Varg²™; Bladee; Skrillex Is there a place in heaven for boys like me? Nordic Flora Series, Pt. 6: Outlaw Music 3:41
391 Varra Catto Observatory - Remix Catto Observatory (Remix) 2:47
392 Veerus Heavy FOUR ON JAM 01 6:52
393 Village C-R-E-W C-R-E-W 3:25
394 Vincent Love Me Now (Extended) Love Me Now (Extended) - Single 4:07
395 Vincent de Moor Fly Away - Extended Vocal Mix Fly Away 8:21
396 VTSS Sober Raving HELLCAT VOL.1 5:38
397 Wassu Escape Tales of Romance I 7:12
398 water feature Pond Bather Various Birds 1:11
399 wes mills; meli this time this time 3:12
400 Willaris. K; jamesjamesjames Silversun Silversun 5:37
401 Willo Past Time Past Time 5:40
402 Worakls Detached Motion (Patrice Bdumel Remix) Hortari & Detached Motion Remixes 6:33
403 Worakls/Ben Bvhmer/eivxr Red Dressed (Ben Bvhmer Remix - Short Version) Red Dressed (Ben Bvhmer Remix) 3:46
404 X-COAST Party Time - Extended Pianissimo 5:46
405 X-COAST The Ultimate - Extended Pianissimo 6:09
406 Yeek Long Time No See Long Time No See 1:51
407 yuragi soon nightlife 2:46
408 yuragi night is young nightlife 5:44
409 Zigan Aldi Heppnar (Iorie Remix) Heppnar (Iorie Remix) 7:22
410 Zmi Shigatsu ame Fu-ne 2:50
411 zodivk yours (+) yours (+) 2:55
412 ​adri; dossyx; Feathervane; Stardreams; wintercolor Atonement STORYBOOK: Chapter 2 6:12
413 ⣎⡇ꉺლ༽இ•̛)ྀ◞ ༎ຶ ༽ৣৢ؞ৢ؞ؖ ꉺლ ƪ. ◖ƪ❍⊁◞. ◗щ (*ㅇ△₊⁎❝᷀ົཽ*ೃ:(꒡͡ ❝᷀ົཽ ꉺ¨.·*:・✧⃛(ཽ๑ )✧⃛*
+23 -39
View File
@@ -1,46 +1,30 @@
# /opt/sldl/configs/artist-canonical.list # artist-canonical.list
# #
# Canonical artist-name casing. One name per line; '#' starts a comment. # Canonical artist-name casing. One name per line; '#' starts a comment.
# Names here are written exactly as Spotify (or the artist) stylizes them. # Write each name exactly as you want it stored (usually how Spotify or the
# artist stylizes it).
# #
# normalize-artist-casing.py looks up every distinct $albumartist value in the # The normalize-artist-casing job looks up every distinct album-artist value in
# beets library; for any value that matches a name here case-insensitively but # your library. For any value that matches a name here case-insensitively but
# differs in casing, it runs `beet modify` to rewrite the tag to the canonical # differs in casing, it rewrites the tag to the form below, and beets then moves
# form. beets then moves the album folder to the canonical path. # the album folder to the canonical path.
# #
# Purpose: prevents LinuxWindows Syncthing collisions like `Mall Grab/` vs # Why this matters: it prevents Linux-to-Windows Syncthing collisions like
# `MALL GRAB/` (same path on Windows, different on Linux). # `Mall Grab/` versus `MALL GRAB/` (the same folder on Windows, two folders on
# Linux), and keeps one artist from being split across differently-cased folders.
# #
# To add an artist: # To add an artist:
# 1. Look them up on Spotify, copy their displayed name exactly. # 1. Look them up and copy their displayed name exactly.
# 2. Append a new line below. # 2. Add a line below.
# 3. Run /opt/sldl/lib/normalize-artist-casing.py to preview, then --apply. # 3. Preview from Settings then Artist Casing, or run the job from Settings
# then Jobs.
# #
# Characters illegal on Windows (`<>:"\?*|`) can appear here — beets's `replace:` # Characters that are illegal in Windows filenames (< > : " \ ? * |) are allowed
# rule substitutes them with `_` when generating the on-disk path, so the tag # here. beets substitutes them with `_` when building the on-disk path, so the
# stays Spotify-correct while the folder stays Windows-safe (e.g. `il:lo` tag # tag stays correct while the folder stays Windows-safe.
# maps to `il_lo/` folder). #
# This file ships empty. Add your own names below, for example:
Bicep #
bôa # Mall Grab
Charli xcx # Charli xcx
Charlotte de Witte # deadmau5
Crush3d
deadmau5
DJ HEARTSTRING
DjRUM
Doechii
Eprom
FM Forest
il:lo
KETTAMA
Mall Grab
nanobii
Pasocom Music Club
Porter Robinson
Ross from Friends
Shades
Svetec
UMEK
yuragi
YOTTO
@@ -0,0 +1,22 @@
# cross-album-keep.list
#
# Artists to protect from the cross-album duplicate pass (dedup Pass 4).
#
# Pass 4 looks for the same track title appearing under one artist across two
# or more different albums, which usually means a genuine duplicate. But some
# artists legitimately have the same title on several albums: compilation and
# "Various Artists" entries, prolific remixers, and artists with many live
# recordings. List those artists here and Pass 4 will leave them alone.
#
# One artist per line; '#' starts a comment. Casing and punctuation do not
# matter (names are compared with everything but letters and numbers removed),
# so "Porter Robinson", "porterrobinson", and "PORTER ROBINSON" are the same.
#
# This file ships empty, which means Pass 4 considers every artist. Add names
# only if you find Pass 4 repeatedly flagging an artist's legitimate versions.
# Nothing is ever deleted without your review in the Dedup page regardless.
#
# Example:
#
# Various Artists
# Porter Robinson
+12 -27
View File
@@ -4,32 +4,17 @@
# are passed as a single beet query argument. Lines containing | are split on | # are passed as a single beet query argument. Lines containing | are split on |
# and each part is passed as a separate beet query term (ANDed together). # and each part is passed as a separate beet query term (ANDed together).
# #
# Use | to combine albumartist and album constraints, e.g.: # Use | to combine an album-artist and an album constraint, for example:
# albumartist::Porter Robinson | album:Secret Sky 2021 # albumartist::Some Artist | album:Some Live Set 2024
# #
# Single-field lines with spaces work fine as one arg beet treats the whole # Single-field lines with spaces work fine as one argument, because beet treats
# string after the colon as the value. Do NOT word-split commas-in-names. # the whole string after the colon as the value. Do not split names on commas.
# #
# To add an album, find the exact name first: # To find the exact album name in your own library first:
# docker exec soulbeet beet ls -f '$albumartist|$album' 'artist:Name' | sort -u # beet ls -f '$albumartist|$album' 'artist:Name' | sort -u
# Then add a line and run: sudo /opt/sldl/lib/gen-djmix-playlist.sh # Then add a line and run the DJ-mix playlist job.
#
# ---- Fred again.. — Live DJ sets ---- # This file ships empty. Add your own albums below, for example:
album:Apple Music Live: Fred again.. #
album:Live from Lyon, France, Oct 24, 2025 # album:Some Live Set 2024
album:Alexandra Palace, London, Feb 27, 2026 # albumartist::Some Artist | album:Secret Show 2023
album:Live from Toronto, Canada, Nov 14, 2025
# ---- Porter Robinson — Live performances / DJ sets ----
albumartist::Porter Robinson | album:Secret Sky 2021
albumartist::Porter Robinson | album:Worlds Live at Second Sky 2019
albumartist::Porter Robinson | album:Shelter Live
# ---- Virtual Self b2b G Jones ----
albumartist::Virtual Self | album:Second Sky 2022
# ---- Air2Earth ----
albumartist::^Air2Earth$ | album:Splash House
# ---- G Jones — add more DJ mix albums below ----
# Find album names with: docker exec soulbeet beet ls -f '$albumartist|$album' 'artist:"G Jones"' | sort -u
@@ -0,0 +1,14 @@
[
{
"name": "techno",
"spotify_url": "https://open.spotify.com/playlist/REPLACE_WITH_YOUR_PLAYLIST_ID",
"cron_expr": "0 1 * * *",
"no_m3u": false
},
{
"name": "liked",
"spotify_url": "https://open.spotify.com/playlist/REPLACE_WITH_YOUR_PLAYLIST_ID",
"cron_expr": "30 23 * * *",
"no_m3u": true
}
]
+36 -13
View File
@@ -32,18 +32,41 @@ fi
mkdir -p "$CONFIG_OUT_DIR" mkdir -p "$CONFIG_OUT_DIR"
# One "name<TAB>spotify_url" line per playlist, active or not (see header). # Render each playlist's .conf entirely in Python. The playlist name and URL
python3 -c " # are substituted as LITERAL text (single-pass regex replace, never re-scanned),
import json, sys # not fed to `sed` — an earlier sed-based version let a crafted playlist name
with open(sys.argv[1]) as f: # execute shell via GNU sed's `e` flag. Names are also re-validated here as
# defense in depth even though the app validates them before writing the JSON.
python3 - "$PLAYLISTS_JSON" "$TEMPLATE" "$CONFIG_OUT_DIR" "$SLDL_DROPBOX_ROOT" <<'PY'
import json, os, re, sys
playlists_json, template_path, out_dir, dropbox_root = sys.argv[1:5]
with open(playlists_json) as f:
playlists = json.load(f) playlists = json.load(f)
with open(template_path) as f:
template = f.read()
SAFE_NAME = re.compile(r"^[A-Za-z0-9 _-]{1,64}$")
for p in playlists: for p in playlists:
print(p['name'] + '\t' + p['spotify_url']) name = p.get("name", "")
" "$PLAYLISTS_JSON" | while IFS=$'\t' read -r name url; do url = p.get("spotify_url", "")
[[ -z "$name" ]] && continue if not SAFE_NAME.match(name):
sed -e "s|PLAYLIST_NAME|${name}|g" \ print(f"WARNING: skipping playlist with unsafe name: {name!r}", file=sys.stderr)
-e "s|SPOTIFY_URL|${url}|g" \ continue
-e "s|SLDL_DROPBOX_ROOT|${SLDL_DROPBOX_ROOT}|g" \ mapping = {
"$TEMPLATE" > "$CONFIG_OUT_DIR/${name}.conf" "PLAYLIST_NAME": name,
echo "Generated $CONFIG_OUT_DIR/${name}.conf" "SPOTIFY_URL": url,
done "SLDL_DROPBOX_ROOT": dropbox_root,
}
pattern = re.compile("|".join(re.escape(k) for k in mapping))
rendered = pattern.sub(lambda m: mapping[m.group(0)], template)
out_path = os.path.join(out_dir, name + ".conf")
with open(out_path, "w") as f:
f.write(rendered)
# Holds the Soulseek password and Spotify secret once credentials are
# patched in, so keep it owner-only from the moment it is created.
os.chmod(out_path, 0o600)
print(f"Generated {out_path}")
PY
+1 -1
View File
@@ -146,7 +146,7 @@ def main():
ap.add_argument("--force", action="store_true", ap.add_argument("--force", action="store_true",
help="overwrite an existing buy-link tag") help="overwrite an existing buy-link tag")
ap.add_argument("--azuracast-key", default=None) ap.add_argument("--azuracast-key", default=None)
ap.add_argument("--azuracast-base", default="https://admin.ephemeral.club") ap.add_argument("--azuracast-base", default=os.environ.get("AZURACAST_BASE", ""))
ap.add_argument("--station", type=int, default=1) ap.add_argument("--station", type=int, default=1)
ap.add_argument("--username", default=None, ap.add_argument("--username", default=None,
help="Bandcamp username; default read from config.env if present") help="Bandcamp username; default read from config.env if present")
+1 -1
View File
@@ -62,7 +62,7 @@ beet move 2>&1 | tail -5 >> "$LOG"
NAVIDROME_ENV="${ALEMBIC_CONFIG_DIR:-/config}/pipeline/navidrome/admin.env" NAVIDROME_ENV="${ALEMBIC_CONFIG_DIR:-/config}/pipeline/navidrome/admin.env"
[ -f "$NAVIDROME_ENV" ] && source "$NAVIDROME_ENV" [ -f "$NAVIDROME_ENV" ] && source "$NAVIDROME_ENV"
ND_BASE="${ND_BASE:-http://navidrome:4533}" ND_BASE="${ND_BASE:-http://navidrome:4533}"
ND_USER="${ND_USER:-andrew}" ND_USER="${ND_USER:-}"
ND_PASS="${ND_PASS:-}" ND_PASS="${ND_PASS:-}"
echo "[$(date -Iseconds)] Triggering Navidrome full scan" | tee -a "$LOG" echo "[$(date -Iseconds)] Triggering Navidrome full scan" | tee -a "$LOG"
+17 -2
View File
@@ -407,8 +407,23 @@ log "[$(date -Iseconds)] === Pass 4: cross-album dedup (artist+title-base) ==="
beet ls -f "\$id${SEP}\$albumartist${SEP}\$album${SEP}\$title${SEP}\$path" \ beet ls -f "\$id${SEP}\$albumartist${SEP}\$album${SEP}\$title${SEP}\$path" \
2>/dev/null > /tmp/all-tracks.txt 2>/dev/null > /tmp/all-tracks.txt
# Pass artist + album allowlists into awk via -v # Pass artist + album allowlists into awk via -v.
CROSS_ALBUM_KEEP_RE='^(porterrobinson|madeon|variousartists)$' # The artist keep-list (artists whose same-title tracks across albums are NOT
# treated as duplicates) is user-editable config, not baked in here. Build the
# anchored regex from cross-album-keep.list: normalize each name the same way
# the awk norm() does (lowercase, letters+digits only) and OR them together.
# An empty/absent list means "protect nobody" -> use a pattern that can never
# match a normalized name (which only ever contains [a-z0-9], never '_').
CROSS_ALBUM_KEEP_FILE="${ALEMBIC_CONFIG_DIR:-/config}/pipeline/cross-album-keep.list"
if [[ -f "$CROSS_ALBUM_KEEP_FILE" ]]; then
_keep_names=$(grep -vE '^[[:space:]]*(#|$)' "$CROSS_ALBUM_KEEP_FILE" \
| tr 'A-Z' 'a-z' | sed -E 's/[^a-z0-9]//g' | grep -v '^$' | paste -sd'|' -)
fi
if [[ -n "${_keep_names:-}" ]]; then
CROSS_ALBUM_KEEP_RE="^(${_keep_names})\$"
else
CROSS_ALBUM_KEEP_RE='_never_'
fi
# Album-name patterns that indicate "intentional alt version" — these rows are # Album-name patterns that indicate "intentional alt version" — these rows are
# filtered out before grouping, so any group needing 2+ matches will only form # filtered out before grouping, so any group needing 2+ matches will only form
# from items whose albums DON'T look like remix/live/single-version releases. # from items whose albums DON'T look like remix/live/single-version releases.
+1 -1
View File
@@ -391,7 +391,7 @@ def main():
"re-matching — converts by album id. Skips links that " "re-matching — converts by album id. Skips links that "
"aren't purchasable (leaves them intact).") "aren't purchasable (leaves them intact).")
ap.add_argument("--azuracast-key", default=None) ap.add_argument("--azuracast-key", default=None)
ap.add_argument("--azuracast-base", default="https://admin.ephemeral.club") ap.add_argument("--azuracast-base", default=os.environ.get("AZURACAST_BASE", ""))
ap.add_argument("--station", type=int, default=1) ap.add_argument("--station", type=int, default=1)
args = ap.parse_args() args = ap.parse_args()
+1 -1
View File
@@ -82,7 +82,7 @@ def _load_navidrome_env():
_nd_env = _load_navidrome_env() _nd_env = _load_navidrome_env()
ND_BASE = _nd_env.get("ND_BASE", "http://navidrome:4533") ND_BASE = _nd_env.get("ND_BASE", "http://navidrome:4533")
ND_USER = _nd_env.get("ND_USER", "andrew") ND_USER = _nd_env.get("ND_USER", "")
ND_PASS = _nd_env.get("ND_PASS", "") ND_PASS = _nd_env.get("ND_PASS", "")
ND_API_VERSION = "1.16.0" ND_API_VERSION = "1.16.0"
ND_CLIENT = "dj-export" ND_CLIENT = "dj-export"
+1 -1
View File
@@ -22,7 +22,7 @@ mkdir -p "$(dirname "$LOG")"
NAVIDROME_ENV="${ALEMBIC_CONFIG_DIR:-/config}/pipeline/navidrome/admin.env" NAVIDROME_ENV="${ALEMBIC_CONFIG_DIR:-/config}/pipeline/navidrome/admin.env"
[ -f "$NAVIDROME_ENV" ] && source "$NAVIDROME_ENV" [ -f "$NAVIDROME_ENV" ] && source "$NAVIDROME_ENV"
ND_BASE="${ND_BASE:-http://navidrome:4533}" ND_BASE="${ND_BASE:-http://navidrome:4533}"
ND_USER="${ND_USER:-andrew}" ND_USER="${ND_USER:-}"
ND_PASS="${ND_PASS:-}" ND_PASS="${ND_PASS:-}"
log() { echo "[$(date -Iseconds)] $*" | tee -a "$LOG"; } log() { echo "[$(date -Iseconds)] $*" | tee -a "$LOG"; }
+1 -1
View File
@@ -68,7 +68,7 @@ def _load_navidrome_env():
_nd_env = _load_navidrome_env() _nd_env = _load_navidrome_env()
NAVIDROME_URL = f"{_nd_env.get('ND_BASE', 'http://navidrome:4533')}/rest/startScan.view" NAVIDROME_URL = f"{_nd_env.get('ND_BASE', 'http://navidrome:4533')}/rest/startScan.view"
NAVIDROME_USER = _nd_env.get("ND_USER", "andrew") NAVIDROME_USER = _nd_env.get("ND_USER", "")
NAVIDROME_PASS = _nd_env.get("ND_PASS", "") NAVIDROME_PASS = _nd_env.get("ND_PASS", "")
VORBIS_LIKE = (FLAC, OggOpus, OggVorbis) VORBIS_LIKE = (FLAC, OggOpus, OggVorbis)
+1 -1
View File
@@ -97,7 +97,7 @@ _nd_env = _load_navidrome_env()
# Subsonic # Subsonic
ND_BASE = _nd_env.get("ND_BASE", "http://navidrome:4533") ND_BASE = _nd_env.get("ND_BASE", "http://navidrome:4533")
ND_USER = _nd_env.get("ND_USER", "andrew") ND_USER = _nd_env.get("ND_USER", "")
ND_PASS = _nd_env.get("ND_PASS", "") ND_PASS = _nd_env.get("ND_PASS", "")
ND_API_VERSION = "1.16.0" ND_API_VERSION = "1.16.0"
ND_CLIENT = "dj-import" ND_CLIENT = "dj-import"
+1 -1
View File
@@ -23,7 +23,7 @@ LIB="$MUSIC_DATA_DIR/Library"
CANARY="$LIB/.navidrome-canary" CANARY="$LIB/.navidrome-canary"
MIN_ARTIST_DIRS=500 # library has ~1500; 500 is "clearly not wiped" MIN_ARTIST_DIRS=500 # library has ~1500; 500 is "clearly not wiped"
ND="${ND_BASE:-http://navidrome:4533}" ND="${ND_BASE:-http://navidrome:4533}"
ND_USER="${ND_USER:-andrew}" ND_USER="${ND_USER:-}"
ND_PASS="${ND_PASS:-}" ND_PASS="${ND_PASS:-}"
log() { echo "[$(date -Iseconds)] navidrome-scan: $*"; } log() { echo "[$(date -Iseconds)] navidrome-scan: $*"; }
+2 -2
View File
@@ -28,7 +28,7 @@ NOW_TS=$(date +%s)
NAVIDROME_ENV="${ALEMBIC_CONFIG_DIR:-/config}/pipeline/navidrome/admin.env" NAVIDROME_ENV="${ALEMBIC_CONFIG_DIR:-/config}/pipeline/navidrome/admin.env"
[ -f "$NAVIDROME_ENV" ] && source "$NAVIDROME_ENV" [ -f "$NAVIDROME_ENV" ] && source "$NAVIDROME_ENV"
ND_BASE="${ND_BASE:-http://navidrome:4533}" ND_BASE="${ND_BASE:-http://navidrome:4533}"
ND_USER="${ND_USER:-andrew}" ND_USER="${ND_USER:-}"
ND_PASS="${ND_PASS:-}" ND_PASS="${ND_PASS:-}"
# Tally counters for the header banner. Body collected first, then prepended. # Tally counters for the header banner. Body collected first, then prepended.
@@ -366,7 +366,7 @@ except Exception as e:
# ---- Build header banner and prepend ---- # ---- Build header banner and prepend ----
HOSTNAME_SHORT=$(hostname -s) HOSTNAME_SHORT=$(hostname -s)
DATE_HUMAN=$(TZ=America/Edmonton date '+%a %Y-%m-%d %H:%M %Z') DATE_HUMAN=$(TZ="${TZ:-UTC}" date '+%a %Y-%m-%d %H:%M %Z')
BANNER_TITLE="🎵 Music pipeline · $HOSTNAME_SHORT · $DATE_HUMAN" BANNER_TITLE="🎵 Music pipeline · $HOSTNAME_SHORT · $DATE_HUMAN"
BANNER_TALLY=" $OK OK · $WARN warn · $SKIP skip" BANNER_TALLY=" $OK OK · $WARN warn · $SKIP skip"
sed -i "1c\\ sed -i "1c\\
+2 -2
View File
@@ -14,7 +14,7 @@ Default is dry-run. Pass --apply to actually write tags + assign.
Env / args: Env / args:
--azuracast-key KEY AzuraCast API key (required) --azuracast-key KEY AzuraCast API key (required)
--azuracast-base URL default https://admin.ephemeral.club --azuracast-base URL default from AZURACAST_BASE env (empty if unset)
--station N default 1 --station N default 1
--apply actually act (default is dry-run) --apply actually act (default is dry-run)
--only PLAYLIST limit recovery to one playlist (testing) --only PLAYLIST limit recovery to one playlist (testing)
@@ -157,7 +157,7 @@ def beet_update(container_paths: list[str], log) -> None:
def main() -> int: def main() -> int:
ap = argparse.ArgumentParser() ap = argparse.ArgumentParser()
ap.add_argument("--azuracast-key", required=True) ap.add_argument("--azuracast-key", required=True)
ap.add_argument("--azuracast-base", default="https://admin.ephemeral.club") ap.add_argument("--azuracast-base", default=os.environ.get("AZURACAST_BASE", ""))
ap.add_argument("--station", type=int, default=1) ap.add_argument("--station", type=int, default=1)
ap.add_argument("--apply", action="store_true") ap.add_argument("--apply", action="store_true")
ap.add_argument("--only", default=None, help="limit recovery to one playlist name") ap.add_argument("--only", default=None, help="limit recovery to one playlist name")
+1 -1
View File
@@ -197,7 +197,7 @@ _IMAGE_EXTS = (".jpg", ".jpeg", ".png", ".webp")
# --- Buy-link tagging ------------------------------------------------------- # --- Buy-link tagging -------------------------------------------------------
# We stamp the release's Bandcamp page into the file so AzuraCast can show a # We stamp the release's Bandcamp page into the file so AzuraCast can show a
# "Buy" button on ephemeral.club. AzuraCast only auto-assigns custom fields # "Buy" button on your station. AzuraCast only auto-assigns custom fields
# from its *known* tag enum, so a bespoke "BUY_URL" tag would be ignored # from its *known* tag enum, so a bespoke "BUY_URL" tag would be ignored
# (it lands in extraTags). Instead we use the "Commercial Information" tag — # (it lands in extraTags). Instead we use the "Commercial Information" tag —
# ID3 frame WCOM, getID3 key `commercial_information`, semantically "a webpage # ID3 frame WCOM, getID3 key `commercial_information`, semantically "a webpage