Configuration¶
Just getting started?
The defaults work well out of the box. You only need to set your Last.fm credentials and YouTube Music auth - everything else is optional. Docker users can skip this page entirely and configure via the web dashboard.
Docker: Use the Settings modal in the web dashboard. All settings are editable from the UI and saved to .env automatically.
CLI: Copy .env.example to .env and edit it. The example file has inline comments for every setting.
Quick reference: the settings you'll actually touch¶
| Setting | Default | What it does |
|---|---|---|
LASTFM_USER |
(required) | Your Last.fm username |
LASTFM_API_KEY |
(required) | Your Last.fm API key |
PLAYLIST_NAME |
Last.fm Recents (auto) |
Name of the synced playlist |
PLAYLIST_PRIVACY |
PRIVATE |
PRIVATE, UNLISTED, or PUBLIC |
LIMIT |
100 |
Target number of tracks |
RECENCY_PLAY_WEIGHT |
0.7 |
0.0 = pure recency ↔ 1.0 = pure play count |
RECENCY_HALF_LIFE_HOURS |
48.0 |
How fast recency decays (lower = favor newer) |
WEEKLY_ENABLED |
true |
Create weekly snapshot playlists |
TIMEZONE |
UTC |
IANA timezone for week boundaries / session hours |
Keeping .env up to date¶
When you update to a newer version, new settings may be added to .env.example that your existing .env doesn't have yet. The tool detects this automatically:
- Startup warning: CLI and web processes log a one-line warning at startup listing any keys present in
.env.examplebut missing from your.env. Nothing is written automatically. - Dashboard banner: Open the Settings modal and a dismissible banner appears when keys are missing. Click Import defaults to add them.
Import defaults regenerates your .env from the .env.example template:
- Existing values are always preserved - nothing you've set is changed or removed.
- Missing keys are inserted in their correct position with the project's optimized defaults.
- Inline comments are refreshed to match the current template.
- Any custom keys not in the template are kept at the bottom under a preserved section.
- A timestamped backup (
runtime/backups/.env.bak-YYYYMMDD-HHMMSS) of your previous.envis always created first.
Imported defaults take effect after the next restart, so a restart banner follows the import.
Missing .env.example?
If the .env.example template itself is missing (e.g. a partial checkout), the banner offers a Download template button that fetches the correct .env.example for your installed version/channel from GitHub, plus a link to view it manually.
Required: Credentials¶
These two are the only settings you have to configure. Everything else has sensible defaults.
Last.fm¶
- Get an API key at https://www.last.fm/api/account/create
- Docker: Enter credentials in the setup wizard or Settings modal
- CLI: Set
LASTFM_API_KEYandLASTFM_USERin.env
YouTube Music (ytmusicapi)¶
- This tool uses browser-based authentication only (no OAuth)
- Docker: Use the built-in auth flow in the web dashboard - no terminal access needed
- CLI: Follow the ytmusicapi browser setup guide to export
browser.json
| Variable | Default | Description |
|---|---|---|
LASTFM_USER |
Your Last.fm username | |
LASTFM_API_KEY |
Last.fm API key | |
YTM_AUTH_PATH |
browser.json |
Path to ytmusicapi auth file |
Anonymous search
Anonymous search is supported (USE_ANON_SEARCH=true, the default) for finding tracks, but you still need valid YouTube Music auth to create or update playlists.
Common settings¶
General¶
| Variable | Default | Description |
|---|---|---|
TIMEZONE |
UTC |
Default IANA timezone (e.g. Europe/Berlin, America/New_York). Weekly-playlist week boundaries and session hours inherit this unless they set their own timezone. |
Main Playlist¶
| Variable | Default | Description |
|---|---|---|
PLAYLIST_NAME |
Last.fm Recents (auto) |
Name for the synced playlist on YouTube Music |
PLAYLIST_DESCRIPTION |
(auto-generated) | Custom description (empty = auto-generated) |
PLAYLIST_PRIVACY |
PRIVATE |
Playlist visibility. One of PRIVATE, UNLISTED, or PUBLIC. Preferred over MAKE_PUBLIC. |
MAKE_PUBLIC |
(unset) | Deprecated alias for PLAYLIST_PRIVACY. Still honoured when PLAYLIST_PRIVACY is unset. |
LIMIT |
100 |
Target number of tracks |
DEDUPLICATE |
true |
Remove duplicate tracks |
Naming heads-up: MAKE_PUBLIC is deprecated
Prefer PLAYLIST_PRIVACY (PRIVATE / UNLISTED / PUBLIC). The older MAKE_PUBLIC variable is still honoured when PLAYLIST_PRIVACY is unset, and accepts the same privacy strings. Legacy boolean values (true → PUBLIC, false → PRIVATE) continue to work for backward compatibility but log a deprecation warning - switch to PLAYLIST_PRIVACY to silence it.
Track Ranking¶
Just tuning?
Leave these at defaults. The two knobs worth adjusting:
RECENCY_PLAY_WEIGHT: lower (e.g.0.3) to favor what you've heard lately, higher (e.g.0.9) to favor what you play most.RECENCY_HALF_LIFE_HOURS: lower (e.g.24) to make the playlist react faster to new listens, higher (e.g.168) for a slower-moving feel.
For the exact scoring formulas and how much each knob actually moves the ranking, see Recency Weighting in How It Works.
| Variable | Default | Description |
|---|---|---|
USE_RECENCY_WEIGHTING |
true |
Rank by play count + recency (false = most recent first) |
RECENCY_HALF_LIFE_HOURS |
48.0 |
How fast recency decays (lower = favor newer tracks) |
RECENCY_PLAY_WEIGHT |
0.7 |
Balance: 0.0 = pure recency, 1.0 = pure play count |
RECENCY_MIN_PLAYS |
1 |
Minimum scrobbles within the fetched window for a track to qualify (1 = no gate). Acts as a hard filter: raise it (e.g. 3) to keep only songs you've actually revisited, which shrinks the playlist and can leave it below LIMIT if few tracks clear the bar. |
RECENCY_NORMALIZATION |
linear |
How play counts convert to a score. linear divides by the top track (a single 500-play outlier flattens everything below it). log applies log1p first to tame outliers so heavy hitters don't dominate. rank ignores absolute counts and scores each track by its percentile position - the most balanced spread. |
RECENCY_VELOCITY_WEIGHT |
0.0 |
Blend in a trending signal based on plays-per-day (how tightly a track's plays cluster in time). 0.0 = off. Raise it to favor tracks you're bingeing right now over ones played the same number of times spread across months. |
RECENCY_SESSION_WEIGHTING |
false |
Give extra weight to plays made during your preferred listening hours over background/late-night scrobbles. Only applies when scoring from the fetched scrobble window - it's a no-op with USE_LOCAL_LASTFM_DB (which keeps no per-scrobble timestamps). |
RECENCY_SESSION_HOURS |
9-23 |
Preferred listening window as START-END in 24-hour local time. Half-open [start, end) and may wrap past midnight (e.g. 22-4). Used only when session weighting is on. |
RECENCY_SESSION_TIMEZONE |
(TIMEZONE) | IANA timezone for the session window (e.g. America/New_York). Blank inherits the general TIMEZONE (which itself defaults to UTC). |
Weekly Playlists¶
| Variable | Default | Description |
|---|---|---|
WEEKLY_ENABLED |
true |
Create weekly playlist snapshots |
WEEKLY_WEEK_START |
MON |
Day the week starts (MON-SUN) |
WEEKLY_TIMEZONE |
(TIMEZONE) | Timezone for week boundary calculation. Blank inherits the general TIMEZONE. |
WEEKLY_KEEP_WEEKS |
2 |
How many weeks to keep (0 = keep all) |
WEEKLY_PLAYLIST_PREFIX |
(from playlist name) | Override name prefix (empty = derive from PLAYLIST_NAME) |
WEEKLY_PLAYLIST_PRIVACY |
(inherit) | PRIVATE/UNLISTED/PUBLIC (empty = inherit PLAYLIST_PRIVACY). Preferred over WEEKLY_MAKE_PUBLIC. |
WEEKLY_MAKE_PUBLIC |
(inherit) | Deprecated alias for WEEKLY_PLAYLIST_PRIVACY. Still honoured when the preferred var is unset. |
Auto-Sync (Web Dashboard)¶
These settings only apply when running the web dashboard.
| Variable | Default | Description |
|---|---|---|
AUTO_SYNC_ENABLED |
false |
Enable the built-in scheduler |
AUTO_SYNC_TYPE |
interval |
interval or cron |
AUTO_SYNC_INTERVAL_HOURS |
6 |
Hours between syncs (interval mode) |
AUTO_SYNC_START_TIME |
HH:MM anchor for interval start (e.g., 00:00) |
|
AUTO_SYNC_CRON |
0 */6 * * * |
Cron expression (cron mode) |
AUTO_TAG_SYNC_ENABLED |
false |
Also sync custom playlists (tags & artists) after each scheduled run |
AUTO_TAG_SYNC_FREQUENCY |
1 |
Run tag sync every N main syncs (1 = every time). Higher values (e.g. 4) cut API load but let custom playlists lag behind the main one - they only refresh every Nth scheduled run. |
USE_24_HOUR_CLOCK |
true |
Display times in 24-hour format |
DATE_FORMAT |
auto |
Date display format: auto (browser locale), DMY (31/12), or MDY (12/31) |
NOW_PLAYING_ENABLED |
true |
Show "Now Playing" from Last.fm in the header |
NOW_PLAYING_INTERVAL |
15 |
Seconds between Now Playing polls (5-120) |
DISPLAY_TIPS |
true |
Show the helper info banners at the top of each dashboard tab |
Webhooks¶
| Variable | Default | Description |
|---|---|---|
WEBHOOK_URL |
(empty) | Webhook endpoint URL (leave empty to disable) |
WEBHOOK_EVENTS |
error |
When to send: all (every sync) or error (failures only) |
WEBHOOK_ALLOW_PRIVATE |
false |
Allow webhook URLs that resolve to private/LAN/localhost addresses (enable only for self-hosted receivers) |
Advanced settings¶
These are rarely changed. Most users can ignore everything below.
Runtime directory¶
All persistent runtime state - the SQLite history databases plus the search,
tag, and playlist caches, run logs, and notification queue - lives under
runtime/ in the project root.
Renamed from cache/
This directory used to be called cache/. It was renamed to runtime/
because it holds persistent state (notably the opt-in SQLite history
databases), not disposable cache data. The migration is automatic and
non-breaking:
- On startup, if a legacy
cache/directory exists andruntime/does not, its contents are moved toruntime/for you. - The CLI and web entry points also rewrite the path variables below in
your
.envfromcache/...toruntime/...once - values pointing at a custom location or already atruntime/are left untouched. - The legacy
CACHE_DIRenvironment variable still works as an alias forRUNTIME_DIR, so existing setups keep working. When the Docker launcher (run-docker.sh) sees a legacycache/directory and noruntime/, it moves it across before starting the container.
| Variable | Default | Description |
|---|---|---|
RUNTIME_DIR |
runtime/ |
Directory for all persistent runtime state. Overrides the per-file paths' base directory. |
CACHE_DIR |
(unset) | Deprecated alias for RUNTIME_DIR. Still honoured for backward compatibility. |
The individual path variables (HISTORY_DB_FILE, CACHE_SEARCH_FILE, etc.)
listed in the sections below default to files inside runtime/. Set them
explicitly only if you need a file in a non-default location.
Search behavior
| Variable | Default | Description |
|---|---|---|
USE_ANON_SEARCH |
true |
Use anonymous YTM client for searches (recommended) |
EARLY_TERMINATION_SCORE |
0.9 |
Stop search early if match score exceeds this (0.0-1.0) |
SLEEP_BETWEEN_SEARCHES |
0.25 |
Delay between searches in seconds |
SEARCH_MAX_WORKERS |
2 |
Parallel search threads (higher = faster but more API load) |
Privacy: When USE_ANON_SEARCH=false, your YouTube Music searches appear in your YouTube search history. Set to true to keep searches private. Anonymous search may return slightly different results.
Backfill
| Variable | Default | Description |
|---|---|---|
MAX_RAW_SCROBBLES |
2000 |
Safety ceiling on scrobbles fetched (0 = unlimited). Fetching normally stops earlier - as soon as LIMIT unique tracks have been collected - so this cap only bites when your listening is repetitive (many plays across few unique tracks). When it does bite it limits how many plays accumulate per track, which affects play-count ranking and the RECENCY_MIN_PLAYS gate (both count plays only within the fetched scrobbles). |
BACKFILL_PASSES |
3 |
Additional fetch attempts if below LIMIT (0 = disabled). When the first pass resolves fewer than LIMIT tracks, backfill pulls in older scrobbles to fill the gap - so a sparse recent history yields a playlist padded with less-recent tracks rather than a short one. |
Caching
| Variable | Default | Description |
|---|---|---|
CACHE_PLAYLIST_FILE |
runtime/.playlist_cache.json |
Path to playlist cache |
CACHE_SEARCH_FILE |
runtime/.search_cache.json |
Path to search cache |
CACHE_OVERRIDES_FILE |
config/search_overrides.json |
Path to search overrides |
CACHE_SEARCH_TTL_DAYS |
30 |
Days before cached searches expire (0 = never). On expiry the track is re-searched, so a match can change if YTM now has a better upload (or the old video was removed). |
CACHE_NOTFOUND_TTL_DAYS |
7 |
Days before "not found" results are retried (0 = don't cache). After this, previously unmatched tracks get another chance and may newly appear in the playlist once they become findable. |
Custom playlists
| Variable | Default | Description |
|---|---|---|
CUSTOM_PLAYLISTS_FILE |
config/custom_playlists.json |
Path to custom playlist config |
CUSTOM_PLAYLISTS_PRIVACY |
(inherit) | Default privacy for custom playlists: PRIVATE/UNLISTED/PUBLIC (empty = inherit PLAYLIST_PRIVACY). Overridable per playlist via the config's privacy field |
TAG_CACHE_FILE |
runtime/.tag_cache.json |
Path to tag cache file |
TAG_CACHE_TTL_DAYS |
90 |
Days before cached tags expire |
TAG_MIN_COUNT |
10 |
Minimum Last.fm tag vote count to consider valid |
TAG_SLEEP_BETWEEN |
0.25 |
Delay between tag API calls in seconds |
TAG_OVERRIDES_FILE |
config/tag_overrides.json |
Manual tag overrides file |
See Custom Playlists for the JSON schema and examples.
History database
| Variable | Default | Description |
|---|---|---|
HISTORY_DB_ENABLED |
false |
Track all songs, syncs, and actions in a local SQLite DB |
HISTORY_DB_FILE |
runtime/history.db |
Path to the history database file |
HISTORY_MAX_SIZE_MB |
0 |
Auto-prune oldest records when exceeded (0 = unlimited). When it prunes, the oldest syncs/actions rows are deleted, so the History tab loses its earliest data (older trends and past syncs disappear). |
HISTORY_RETENTION_DAYS |
0 |
After each sync, delete syncs & actions rows older than N days (0 = keep forever). tracks are always retained. Lowering it keeps the History tab focused on recent activity but trims long-term trend charts. |
See History Database for what's tracked and the dashboard view.
Local Last.fm history database
| Variable | Default | Description |
|---|---|---|
USE_LOCAL_LASTFM_DB |
false |
Build the main playlist from your full local scrobble history (lifetime plays + recency) instead of recent tracks. Changes playlist behaviour. |
LASTFM_LOCAL_DB_FILE |
runtime/lastfm_history.db |
Path to the local Last.fm history database file |
LASTFM_LOCAL_DB_MAX_SCROBBLES |
0 |
Safety cap on scrobbles ingested per crawl (0 = unlimited). More than a safety valve: a non-zero cap scopes the playlist to a slice of your history - e.g. 10000 builds rankings from your 10k most recent lifetime scrobbles, so older favourites never enter the pool. |
DISCOVERY_REDISCOVER_DAYS |
0 |
Discovery playlists only: when a playlist excludes already-heard tracks, only treat tracks played within the last N days as "already heard" (0 = exclude your entire history). Lets older favourites resurface. Works with recent scrobbles and USE_LOCAL_LASTFM_DB. See Discovery Playlists. |
See Local Last.fm History for how the full crawl and incremental updates work.
Reliability & retries
| Variable | Default | Description |
|---|---|---|
API_MAX_RETRIES |
3 |
YTM API retry attempts (exponential backoff) |
LASTFM_MAX_RETRIES |
5 |
Last.fm API retry attempts |
LASTFM_MAX_CONSECUTIVE_EMPTY |
3 |
Stop after N pages with no new tracks |
LASTFM_FORCE_IPV4 |
true |
Force IPv4 (helps with flaky Last.fm IPv6) |
Logging
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL |
INFO |
CRITICAL, ERROR, WARNING, INFO, or DEBUG |
Docker-specific (host-side)
These are set on the host, not in .env:
| Variable | Default | Description |
|---|---|---|
YTMT_PORT |
2002 |
Port to expose the web dashboard |
YTMT_HEALTH_TIMEOUT |
30 |
Seconds to wait for health check |