Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,13 @@ WAVEFLOW_METADATA_LASTFM_KEY=… # your own Last.fm API application's key

| Variable | Default | What it does |
| --- | --- | --- |
| `WAVEFLOW_METADATA_PROVIDERS` | empty | The switch: an ordered, comma-separated list. Empty is off. The providers are `lastfm` (biographies and similar artists) and `deezer` (artist portraits, no key) |
| `WAVEFLOW_METADATA_PROVIDERS` | empty | The switch: an ordered, comma-separated list. Empty is off. The providers are `lastfm` (biographies and similar artists), `deezer` (artist portraits, no key) and `theaudiodb` (artist banners only — not its biographies) |
| `WAVEFLOW_METADATA_LASTFM_KEY` | — | Required when `lastfm` is listed; the server refuses to start without it. No key ships in the binary |
| `WAVEFLOW_METADATA_IMAGES` | on | `off` keeps the lookups and refuses artist image downloads; `deezer` is then not asked at all, since portraits are all it is asked for |
| `WAVEFLOW_METADATA_THEAUDIODB_KEY` | — | Required when `theaudiodb` is listed; the server refuses to start without it. TheAudioDB's public test key is not used |
| `WAVEFLOW_METADATA_IMAGES` | on | `off` keeps the lookups and refuses artist image downloads; `deezer` and `theaudiodb` are then not asked at all, since images are all they are asked for |
| `WAVEFLOW_METADATA_TTL_DAYS` | 30 | How long an answer with content is kept before it is asked again |
| `WAVEFLOW_METADATA_MISS_TTL_DAYS` | 7 | How long "the provider knows nothing about this artist" is kept |
| `WAVEFLOW_METADATA_RATE_PER_SEC` | 4 | Requests per second, per provider |
| `WAVEFLOW_METADATA_RATE_PER_SEC` | 4 | Requests per second, per provider. TheAudioDB is never asked faster than once every two seconds, its free tier's ceiling, whatever this says |
| `WAVEFLOW_METADATA_REQUEST_TIMEOUT_SECS` | 10 | How long one request may take before it counts as unanswered |

**Scrobbling's Last.fm key does not switch this on.** An application you
Expand All @@ -209,9 +210,21 @@ PNG or WebP by its own bytes, and at most 10 MiB. A picture that fails any of
these is not fetched and the artist is remembered as having none until the
short TTL runs out.

With `theaudiodb` listed, also `www.theaudiodb.com`, with your key in the
request path, and then the artist's **fanart** — the wide photograph the
artist page draws behind its header when no banner was chosen by hand —
under the same rules, from TheAudioDB's image hosts (`r2.theaudiodb.com`,
`www.theaudiodb.com`). Its fanarts are tried in order and the first real one
is kept; the 1000×185 strips TheAudioDB calls banners are never asked for.
A banner chosen by hand always wins over it, and the page credits TheAudioDB
whenever its fanart is the one shown. If a later answer no longer offers the
fanart, the artist goes back to having no automatic banner; a request that
fails changes nothing.

**What it stores.** In `waveflow.db`, for each provider and each artist name
asked about: the biography, the ranked list of similar artists by name, the
portrait's hash, whether the provider knew the artist, and when to ask again.
portrait's and the banner's hashes, whether the provider knew the artist, and
when to ask again.
The portrait itself goes in the artwork store beside the covers, and every
artist with that name shows it through the `coverArt` it already carries —
**back it up with the rest of `artwork_dir`**: unlike a cover, it cannot be
Expand Down
4 changes: 4 additions & 0 deletions docs/rfcs/RFC-011-metadata-enrichment.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ Settings, under `WAVEFLOW_METADATA_*`, in `src/config.rs` with the rest:
widening: an empty provider list means no outbound request of any kind, and
this setting cannot contradict it.
- `WAVEFLOW_METADATA_LASTFM_KEY` — the operator's own application key.
- `WAVEFLOW_METADATA_THEAUDIODB_KEY` — added by
[RFC-014](RFC-014-automatic-artist-banner.md) for the `theaudiodb`
provider, which is asked for artist banners only, under the same rule: no
key ships in the binary.
- `WAVEFLOW_METADATA_TTL_DAYS`, `WAVEFLOW_METADATA_MISS_TTL_DAYS`,
`WAVEFLOW_METADATA_RATE_PER_SEC`, `WAVEFLOW_METADATA_REQUEST_TIMEOUT_SECS`.

Expand Down
8 changes: 6 additions & 2 deletions docs/rfcs/RFC-014-automatic-artist-banner.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
- **Status**: Proposed
- **Implemented by**: in progress, in the order of "The order of the work"
below: [#312](https://github.com/InstaZDLL/waveflow-server/pull/312) (this
RFC). The *Status* field above never flips in this project; this line is the
one that says what is running.
RFC) and [#314](https://github.com/InstaZDLL/waveflow-server/pull/314)
(steps 2 and 3 together: the `theaudiodb` provider and its key, the fill and
its pace, the fanart columns, linking and unlinking, the effective banner
and `enrichment.banner`, the sweep, and the credit in the web client). The
*Status* field above never flips in this project; this line is the one that
says what is running.
- **Date**: 2026-10-24
- **Authors**: the WaveFlow project
- **Depends on**: [RFC-011](RFC-011-metadata-enrichment.md) (the providers, the
Expand Down
17 changes: 17 additions & 0 deletions migrations-v2/20261024010000_artist_fanart.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- An automatic artist banner, from TheAudioDB's fanart (RFC-014 decision 3).
--
-- The cache records the fanart a provider answered, as `image_hash` records a
-- portrait, and loses it the same way when the sweeper takes the artwork row.
ALTER TABLE metadata_artist
ADD COLUMN fanart_hash TEXT REFERENCES artwork(hash) ON DELETE SET NULL;

-- Every artist row with the name carries it, in every library, written the way
-- `artwork_hash` is: by the fill, only where it changes, with the artist event.
-- It is the provider's and never a choice — a banner chosen by hand stays in
-- `artist_image_choice`. `ON DELETE SET NULL` as on `artwork_hash`: the sweep
-- keeps what an artist names, so this only ever clears a link to artwork that
-- is already gone.
ALTER TABLE artist
ADD COLUMN fanart_hash TEXT REFERENCES artwork(hash) ON DELETE SET NULL;

CREATE INDEX artist_fanart ON artist(fanart_hash);
46 changes: 36 additions & 10 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,8 @@ pub struct ScrobbleDestination {
#[derive(Clone)]
pub struct MetadataSettings {
/// `WAVEFLOW_METADATA_PROVIDERS`: an ordered, comma-separated list, empty
/// by default, of `lastfm` and `deezer`. The order is the order fields are
/// merged in when read.
/// by default, of `lastfm`, `deezer` and `theaudiodb`. The order is the
/// order fields are merged in when read.
pub providers: Vec<crate::enrichment::Provider>,
/// `WAVEFLOW_METADATA_IMAGES`, on by default, `off` to keep the lookups and
/// refuse decision 8's image fetch. A narrowing only: it switches nothing
Expand All @@ -210,6 +210,11 @@ pub struct MetadataSettings {
/// `WAVEFLOW_METADATA_LASTFM_KEY`, the operator's own application key.
/// Required when `providers` names `lastfm`: no key ships in this binary.
pub lastfm_key: Option<String>,
/// `WAVEFLOW_METADATA_THEAUDIODB_KEY`, the operator's own TheAudioDB key.
/// Required when `providers` names `theaudiodb` (RFC-014 decision 1): the
/// public test key `waveflow-core` uses for one desktop user is not one a
/// server may share with everyone else's.
pub theaudiodb_key: Option<String>,
/// `WAVEFLOW_METADATA_TTL_DAYS`: how long an answer with content stands.
pub ttl: Duration,
/// `WAVEFLOW_METADATA_MISS_TTL_DAYS`: how long "the provider knows nothing"
Expand All @@ -230,6 +235,7 @@ impl MetadataSettings {
providers: Vec::new(),
images: true,
lastfm_key: None,
theaudiodb_key: None,
ttl: Duration::from_secs(30 * 24 * 60 * 60),
miss_ttl: Duration::from_secs(7 * 24 * 60 * 60),
rate_per_sec: 4,
Expand All @@ -251,6 +257,10 @@ impl std::fmt::Debug for MetadataSettings {
"lastfm_key",
&self.lastfm_key.as_ref().map(|_| "[redacted]"),
)
.field(
"theaudiodb_key",
&self.theaudiodb_key.as_ref().map(|_| "[redacted]"),
)
.field("ttl", &self.ttl)
.field("miss_ttl", &self.miss_ttl)
.field("rate_per_sec", &self.rate_per_sec)
Expand Down Expand Up @@ -1056,9 +1066,11 @@ fn normalize_public_url(value: &str) -> anyhow::Result<String> {
fn parse_metadata_settings() -> anyhow::Result<MetadataSettings> {
let defaults = MetadataSettings::off();
let lastfm_key = read_trimmed_env("WAVEFLOW_METADATA_LASTFM_KEY");
let theaudiodb_key = read_trimmed_env("WAVEFLOW_METADATA_THEAUDIODB_KEY");
let providers = parse_metadata_providers(
&std::env::var("WAVEFLOW_METADATA_PROVIDERS").unwrap_or_default(),
lastfm_key.is_some(),
theaudiodb_key.is_some(),
)?;
let days = |name: &str, default: Duration| -> anyhow::Result<Duration> {
let days: u64 = parse_positive_env(name, default.as_secs() / (24 * 60 * 60))?;
Expand All @@ -1070,6 +1082,7 @@ fn parse_metadata_settings() -> anyhow::Result<MetadataSettings> {
providers,
images: parse_bool_env("WAVEFLOW_METADATA_IMAGES", defaults.images)?,
lastfm_key,
theaudiodb_key,
ttl: days("WAVEFLOW_METADATA_TTL_DAYS", defaults.ttl)?,
miss_ttl: days("WAVEFLOW_METADATA_MISS_TTL_DAYS", defaults.miss_ttl)?,
rate_per_sec: parse_positive_env("WAVEFLOW_METADATA_RATE_PER_SEC", defaults.rate_per_sec)?,
Expand All @@ -1090,6 +1103,7 @@ fn parse_metadata_settings() -> anyhow::Result<MetadataSettings> {
fn parse_metadata_providers(
list: &str,
has_lastfm_key: bool,
has_theaudiodb_key: bool,
) -> anyhow::Result<Vec<crate::enrichment::Provider>> {
let mut providers = Vec::new();
for name in list
Expand All @@ -1107,6 +1121,11 @@ fn parse_metadata_providers(
"WAVEFLOW_METADATA_PROVIDERS names lastfm without WAVEFLOW_METADATA_LASTFM_KEY"
);
}
if provider == crate::enrichment::Provider::TheAudioDb && !has_theaudiodb_key {
anyhow::bail!(
"WAVEFLOW_METADATA_PROVIDERS names theaudiodb without WAVEFLOW_METADATA_THEAUDIODB_KEY"
);
}
providers.push(provider);
}
Ok(providers)
Expand Down Expand Up @@ -1236,26 +1255,33 @@ mod tests {
fn metadata_providers_are_read_strictly() {
use crate::enrichment::Provider;

assert_eq!(parse_metadata_providers("", false).unwrap(), []);
assert_eq!(parse_metadata_providers(" , ", false).unwrap(), []);
assert_eq!(parse_metadata_providers("", false, false).unwrap(), []);
assert_eq!(parse_metadata_providers(" , ", false, false).unwrap(), []);
assert_eq!(
parse_metadata_providers(" lastfm ", true).unwrap(),
parse_metadata_providers(" lastfm ", true, false).unwrap(),
[Provider::LastFm]
);
// No key ships in the binary: naming the provider without one is a
// misconfiguration, not a quiet off.
assert!(parse_metadata_providers("lastfm", false).is_err());
assert!(parse_metadata_providers("lastfm,lastfm", true).is_err());
assert!(parse_metadata_providers("last.fm", true).is_err());
assert!(parse_metadata_providers("lastfm", false, false).is_err());
assert!(parse_metadata_providers("lastfm,lastfm", true, false).is_err());
assert!(parse_metadata_providers("last.fm", true, false).is_err());
// Deezer asks for no key, so none is required of it.
assert_eq!(
parse_metadata_providers("lastfm, deezer", true).unwrap(),
parse_metadata_providers("lastfm, deezer", true, false).unwrap(),
[Provider::LastFm, Provider::Deezer]
);
assert_eq!(
parse_metadata_providers("deezer", false).unwrap(),
parse_metadata_providers("deezer", false, false).unwrap(),
[Provider::Deezer]
);
// TheAudioDB asks for a key of its own (RFC-014 decision 1), and the
// Last.fm one does not stand in for it.
assert!(parse_metadata_providers("theaudiodb", true, false).is_err());
assert_eq!(
parse_metadata_providers("deezer, theaudiodb", false, true).unwrap(),
[Provider::Deezer, Provider::TheAudioDb]
);
}

#[test]
Expand Down
Loading
Loading