Ein Song oder Album laesst sich per https://<host>/?song=<id> an jemanden auf
derselben Instanz schicken. Der Link traegt nur die Navidrome-Id, keine
Zugangsdaten — der Empfaenger sieht den Inhalt mit seinem eigenen Konto.
Web: Teilen ueber navigator.share() mit Rueckfall auf die Zwischenablage. Beim
Start wird ?song= / ?album= aufgeloest (nach der Anmeldung, vorher fehlen die
Zugangsdaten), das Album geoeffnet, der Song gestartet und der Parameter aus der
Adresszeile entfernt, damit ein Neuladen nicht erneut losspielt. Das Manifest
bekommt launch_handler=navigate-existing, damit der Link in einem bereits
offenen PWA-Fenster landet statt in einem neuen.
Android: Teilen ueber ACTION_SEND, Empfang ueber einen App-Link-Intent-Filter.
Der Host ist eine Build-Eigenschaft (-PnovasonicHost=…, verifiziert am
zusammengefuehrten Manifest), die Web-Adresse fuer ausgehende Links eine neue
Einstellung. Weil der NavController in der Compose-Hierarchie lebt und die
Activity nicht selbst navigieren kann, reicht DeepLinkState den Link durch;
onNewIntent ist mitbedacht, sonst passierte beim zweiten Link nichts.
assetlinks.json liegt als Vorlage in public/ und wird vom Frontend-Image
ausgeliefert (im Build verifiziert); ohne den Release-Fingerabdruck oeffnet der
Link die Web-App im Browser statt die App. Anleitung: docs/share-links.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drei Luecken derselben Familie, die dazu fuehrten, dass dieselben Songs ein
zweites Mal geladen wurden:
- Kuenstlernamen wurden zeichengenau verglichen: "Cécile Corbel" und "Cecile
Corbel" galten als zwei Personen. Welche Schreibweise ankommt, haengt an der
Quelle (Deezer vs. Spotify). Der Vergleich normalisiert jetzt Akzente weg —
nur der Vergleich, Tags und Dateinamen behalten das Original.
- Typografische Apostrophe: "Arrietty’s Song" (U+2019) und "Arrietty's Song"
fielen auseinander. Fiel beim Testen auf, betrifft viele Soundtracks.
- findExistingTrack durchsucht nur den Ordner des uebergebenen Kuenstlers. Bei
Soundtracks weicht der Album- vom Titel-Kuenstler ab, der Song landet je nach
Ladeweg unter dem einen oder anderen — geprueft wurde jeweils der falsche.
Weicht der Titel-Kuenstler ab, wird nun auch dessen Ordner geprueft (~0,6 ms
bei 28.801 Dateien, gegenueber 3-9 s pro Download).
Sieben neue Tests zielen gezielt auf Fehlalarme: aehnliche Namen (Cecil Taylor
vs. Cécile Corbel), gleicher Titel bei verschiedenen Kuenstlern, Remix gegen
Basistitel, fremder Titel im Soundtrack-Ordner — alle bleiben getrennt.
Gesamte Suite: 42 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Findet das Backend die von Navidrome gemeldete Datei nicht am angegebenen Pfad
(Subsonic liefert oft einen aus Tags gebauten virtuellen Pfad), sucht es ueber
Dateiname bzw. Kuenstler+Titel weiter. Liegt derselbe Song mehrfach auf der
Platte, konnte das die falsche Kopie treffen: die wurde getaggt, der bearbeitete
Song blieb unveraendert — die Aenderung "sprang zurueck". Das erklaert, warum es
bei Songs ohne Dublette immer ging und bei anderen nie.
Der ueber Aehnlichkeit gefundene Kandidat wird jetzt per ffprobe gegen die Tags
geprueft, die Navidrome fuer diesen Song meldet, und bei Widerspruch mit 409 und
erklaerender Meldung abgebrochen statt geraten. Zusaetzlich melden uebersprungene
Umbenennungen und Verschiebungen sich als Warnung, statt stumm zu bleiben; die
Antwort nennt den tatsaechlich bearbeiteten Pfad, der Editor zeigt beides an.
Ausserdem: -dontwarn org.slf4j.** — der Release-APK-Build brach seit laengerem an
R8 ab (Ktors Logging referenziert den slf4j-1.x-Binder, den es auf Android nicht
gibt). Vorbestehend, mit HEAD~1 verifiziert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Android holt zwei Sachen nach, die es im Web laengst gibt:
- Song von der Festplatte loeschen, im Kontextmenue unten abgesetzt und rot, mit
derselben Sicherheitsabfrage wie im Web. Laeuft ueber /api/delete-song und
erscheint nur mit konfiguriertem Backend — Navidrome allein loescht nichts.
- Playlist direkt aus dem Import anlegen: Haekchen plus Namensfeld ab zwei
Titeln, danach Scan anstossen, Titel suchen, Playlist aus den gefundenen.
Nicht gefundene werden gezaehlt statt die Erstellung scheitern zu lassen.
Dazu der Web-Fix zur Album-Position: die gespeicherte Scrollposition wurde in
genau einem requestAnimationFrame gesetzt. Solange Cover und Eintraege noch
nachluden, war scrollHeight kleiner als das Ziel und der Browser kappte den Wert
stillschweigend — man landete wieder oben. Jetzt wird er ueber bis zu ~0,5 s
erneut gesetzt, bis er sitzt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Top-Titel gruppierte nach song_id. Wird eine Datei beim Bearbeiten umbenannt,
vergibt Navidrome eine neue id — der Titel erschien danach zweimal mit geteilten
Zaehlern. Kuenstler/Alben/Genres gruppierten nach lower(), sodass schon ein
doppeltes Leerzeichen zwei Eintraege ergab.
Schluessel ist jetzt durchgehend normalisiert (lower + trim + Leerzeichen
gefaltet), Top-Titel gruppiert nach Titel+Kuenstler statt nach song_id. Die
Anzeigefelder kommen aus dem JUENGSTEN Play statt aus MAX(), sonst zeigte die
Statistik nach einer Korrektur weiter die alte Schreibweise. Am laufenden Dienst
geprueft: 40 Plays vor und 10 nach einer Umbenennung erscheinen als ein Eintrag
mit 50 Plays, neuer Schreibweise und neuer songId.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
album-back-btn war die einzige Detailseite, die per navigate() VORWAERTS auf die
Albenliste sprang statt einen Schritt im Verlauf zurueck. Dadurch landete man
nicht dort, wo man das Album angeklickt hatte (z. B. Home), und es kam ein
zusaetzlicher History-Eintrag auf den Stapel, durch den sich der Browser-Zurueck-
Knopf erst arbeiten musste. Jetzt _navGoBack() wie bei Artist, Genre, Deezer- und
Partial-Album; die Albenliste bleibt nur das Ziel, wenn es keinen Verlauf gibt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
getYtmVideoUrls wertete jedes /browse/UC in der Ausgabe als "yt-dlp hat einen
Artist-Channel aufgeklappt" und gab [] zurueck — samt der bereits gesammelten
Song-URLs. Bei "DAT ADAM UFO" stand der Kanal auf Platz 9 und riss sieben gute
Treffer mit, darunter den offiziellen Art Track auf Platz 1; uebrig blieb der
YouTube-Fallback mit einem Live-Mitschnitt. An 8 echten Suchen gemessen feuerte
das in 7 Faellen, Schritt 1 der Kaskade war also praktisch tot.
Mit --flat-playlist kann gar nichts aufgeklappt werden, ein Kanal ist EINE Zeile.
pickYtmVideoUrls() behaelt daher alle watch?v=-Zeilen und ueberspringt browse/UC
(Kanal) wie browse/MPREb_ (Album). Ueber 20k Zufallsausgaben geprueft: die neue
Funktion ist eine Obermenge der alten, kann also nur Kandidaten hinzufuegen.
Drei echte Downloads liefen end-to-end sauber durch (Dauer sekundengenau auf
Deezer, korrekte Tags), alle aus Schritt 1 statt aus dem Fallback.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
is_live meint nur "laeuft gerade als Stream" — eine Konzert-AUFNAHME kam
ungehindert durch und landete als Albumtrack in der Bibliothek ("Dat Adam - Ufo
Video Days 2015 Live" auf dem echten Kuenstlerkanal, Dauer im Fenster). Der
Variantenfilter bekommt daher eine live-Regel. Sie schaltet sich ab, wenn der
gesuchte Titel selbst live enthaelt; die Pruefung nutzt \blive\b statt includes,
sonst wuerde "deliver" die Regel aushebeln. Drei Tests dazu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Folgekorrektur zu 02b5905: "… - 2020 Remaster" steht auf der Veroeffentlichung,
aber nie im YouTube-Titel — als Pflicht-Keyword haette die Zahl den richtigen
Upload abgelehnt. Jahreszahlen neben einem Qualifier (remaster/version/edition/
anniversary/mono/stereo) fallen daher wieder raus; ein Titel, der selbst eine
Jahreszahl ist ("1999", "Berlin 2020"), behaelt sie. Gegen 29 Beispieltitel
gemessen verhalten sich 24 exakt wie vorher.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"700 Main Street" lud "777 Main Street" eines fremden Artists: buildTitleKeywords
behielt nur die zwei laengsten Woerter (Street, Main), die unterscheidende 700
fiel als zu kurz raus — und Schritt 3 der Kaskade prueft den Kuenstler-Channel
nicht mehr. Zahlen werden jetzt immer als \b<n>\b gefordert. Dazu eine kleine
Abkuerzungstabelle (street/st, saint/st, mount/mt, doctor/dr, versus/vs,
number/no), denn der korrekte Art Track heisst "700 Main st" und scheiterte
bisher an jedem Schritt. Tests in backend/test/ytdlp.test.js.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fasst die Aenderungen seit 2.2 als Release zusammen: image-basiertes
docker-compose.yml (Bauen wandert nach .dev.yml), der auf GITEA_URL
umgeschriebene APK-Asset-Download und der Alben-Tab-Fix, dazu das
veroeffentlichte versionCode 3. Am Handy bestaetigt: Update-Dialog,
In-Place-Install und die Scrollpositionen funktionieren.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
APK mit dem Debug-Key vom alten Rechner gebaut (Signatur 1039a00e…, passend zur
installierten App) und als Asset im Release apk-latest ersetzt, Body auf
versionCode=3. Dazu das ausfuehrbare Bit auf gradlew, das im Clone fehlte, und
.idea/ ignoriert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LaunchedEffect(state.albumSort) { scrollToItem(0) } feuerte nicht nur beim
Sortierwechsel, sondern auch beim ersten Compose nach der Rueckkehr aus einem
Album — die im ViewModel gehaltene Position wurde also sofort ueberschrieben.
Das Hochscrollen sitzt jetzt in setAlbumSort() und laeuft nur bei echtem
Wechsel; dazu zwei ungenutzte remember*State-Importe entfernt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gitea baut browser_download_url aus seiner eigenen ROOT_URL (hier
192.168.178.153:3000), die vom Backend-Container nicht erreichbar ist. Der
API-Call gelang, der Asset-Download nicht — der stille Fallback auf die nicht
vorhandene lokale Datei meldete dann "Keine APK hinterlegt". Asset- und
Redirect-URLs werden jetzt auf GITEA_URL umgeschrieben; scheitert der Weg
trotzdem, sagen Log und Fehlermeldung welcher der beiden Wege fehlt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Namen getauscht: das Standard-Compose zieht jetzt die fertigen Registry-Images
(vorher docker-compose.prod.yml), die Build-Variante heisst docker-compose.dev.yml.
Weniger verwirrend, weil der Produktivfall ohne -f auskommt. Kopfkommentare,
README, CLAUDE.md, .env.example und build-push-images.mjs entsprechend angepasst.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Produktivsystem baut nicht mehr selbst, sondern zieht die drei fertigen Images
(novasonic-frontend/-backend/-scrobble) aus der Gitea-Registry. Das bestehende
docker-compose.yml bleibt der Build-auf-dem-Host-Weg. Dazu .dockerignore fuers
Frontend-Image (android/, backend/, .git raus) und README/CLAUDE.md aktualisiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Chosen flow: build on the host from backend/scrobble Dockerfiles instead of
pulling from a registry (no registry / no Docker-on-dev needed). Heavy deps are
baked, so restarts are instant and rebuilds after git pull are cache-fast.
Removed the now-obsolete docker-compose.dev.yml (runtime-install variant); the
optional push-to-registry script stays for a future CI flow. Updated README,
CLAUDE.md, .env.example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Less confusion: docker-compose.yml is now the default (pulls pre-built images)
and the build-on-host variant is docker-compose.dev.yml. Updated all references
(CLAUDE.md, README, .env.example, build-push-images.mjs, compose header).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a pull-based production option so the host no longer builds/installs deps
on every restart:
- backend/Dockerfile + scrobble/Dockerfile bake the heavy deps (ffmpeg, Deno,
yt-dlp, node libs). Backend still refreshes yt-dlp on start (non-fatal).
- docker-compose.prod.yml pulls ${REGISTRY}/novasonic-*:${TAG}.
- scripts/build-push-images.mjs (npm run push-images) builds + pushes all three.
- .env.example documents every var incl. REGISTRY/TAG; .env gitignored.
The build-on-host docker-compose.yml stays as-is.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Android: the artist page's main list and the "Alle anzeigen" album overlay
(grid scroll + search query) are hoisted into the Artist/Explore ViewModels,
so drilling into an album/artist and back keeps position + query. Covers the
library artist and Deezer (Explore) artist screens.
- Web: the "Android App" download section now sits at the very bottom of
Settings, below the Navidrome-Server (account) section.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The app checks {backend}/api/apk/info on launch and, if a newer versionCode
than BuildConfig.VERSION_CODE is offered, shows an update dialog that
downloads {backend}/api/apk into a single wiped-each-time cache file and
hands it to the system installer (FileProvider + REQUEST_INSTALL_PACKAGES).
No APK hoarding; in-place install preserves data (stable signing key needed).
- backend /api/apk/info now returns versionCode (parsed from the release body).
- publish-apk.mjs auto-increments versionCode, sets versionName, and writes
versionCode/versionName into the Gitea release body.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Going into an album/artist and back now restores the scroll position and
keeps the typed query, instead of resetting to the top.
- Web: navigation.js snapshots each history entry's #main-content scroll and
any open local (header) search, restoring them on Back/Forward; a genuine
new navigation still starts at the top.
- Android: the Alben/Künstler lists (LibraryViewModel) and the global/local
search-results lists (ExploreViewModel/SearchViewModel) hoist their
LazyListState/LazyGridState, so scroll survives a drill-in/back for the
tab's lifetime.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
You can still enjoy an incomplete album: the downloaded-tracks section now has
Abspielen + Zufall buttons (play/shuffle the downloaded songs) and each
downloaded row gets the 3-dot context menu, threaded from both artist screens.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`npm run publish-apk` builds the APK (assembleDebug by default — the release
build type is unsigned) and uploads it to the apk-latest Gitea release,
replacing the old .apk asset, so the web download button always serves the
latest build. Config via env or a gitignored scripts/.env.release
(GITEA_URL/REPO/TOKEN, APK_GRADLE_TASK). Release title tracks versionName.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The /api/apk + /api/apk/info routes now fetch a fixed Gitea release
("apk-latest") server-side over the LAN and stream its .apk asset, falling
back to /data/novasonic.apk. Gitea can stay internal-only — the backend holds
the token, the browser only ever talks to the backend. Follows redirects,
constant ~30 MB (overwrite the one release's asset each build).
env: GITEA_URL, GITEA_REPO, GITEA_TOKEN (private repo only), GITEA_APK_TAG.
docker-compose documents them; unset = local-file fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend serves a staged APK so co-users self-serve the latest build:
- GET /api/apk streams APK_PATH (default /data/novasonic.apk) with the
android package content-type + attachment disposition; 404 if absent.
- GET /api/apk/info reports availability, size, mtime, and an optional
version from /data/novasonic.version.txt.
- Settings gets a "🤖 Android App" section that appears only when an APK is
present and shows version/size/date.
Maintainer updates the APK with:
docker cp app-release.apk novasonic-backend:/data/novasonic.apk
Android source is already tracked; only build artifacts stay out of git.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The header search overlay was anchored to the search bar's width, leaving a
dead strip on the right where scrolling hit the background page. Anchor it to
the sticky #mobile-header instead (which already sits right of the sidebar), so
it spans the full main-content width — sidebar stays visible, no dead strip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The album-name normaliser stripped everything but a-z0-9, so any JP/CJK
title collapsed to "". All of an artist's Japanese singles then normalised
identically, de-duplicated into one entry and "matched" each other — e.g.
inabakumori (稲葉曇) showed only 2 of 9 releases. Normalisers now keep
Unicode letters/numbers (\p{L}\p{N}) in:
- web _normAlbum (detail.js)
- android normAlbum (categorizeAlbums), normTrack (partial-album split),
the enrichPartialCounts helpers, and the Deezer artist-name match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- .artist-hero-name: add padding-right so the heavy/negative-letter-spacing
final glyph isn't cut by overflow:hidden.
- CHANGELOG: merge the 2.3 batch into a single 2.2 (2026-08-22) section.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Anchor #header-search-overlay to the search bar (position:absolute inside a
relative .header-search-wrap) instead of position:fixed full-viewport width,
so the results drop down at the bar's width and the left sidebar stays visible.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Downloads started from global search / Bibliothek now poll via the
app-lifetime NotificationStore, so they show success/failure like the
importer and artist page did (was a silent toast only).
- Swipe between tabs, synced with the tab bar, matching Erkunden:
* Now Playing: Player <-> Lyrics <-> Queue (cover's skip-track swipe
still wins as a child gesture).
* Bibliothek: Playlists <-> Alben <-> Künstler <-> Offline.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Importer: each resolved track gets a play/stop button that plays its
Deezer/Spotify preview via the app-level preview bar (pauses library
playback, highlights the row). Parity with the web importer.
- Artist "Unvollständig" now works: Deezer's /artist/{id}/albums omits
nb_tracks, so owned albums never registered as partial. The artist VM
now resolves the real total for matched albums via a per-album lookup
(like web), so the partial section + downloaded/missing split populate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Backend now caches Ticketmaster events per artist and prefers the larger
recent result, so a flaky partial/empty response can't shrink a good list;
every client (web/Android) stays consistent (1h TTL self-heals).
- Web SoundCloud importer preview: add the missing .gtrack-num grid cell so
the 4-column row no longer squeezes the cover/title (title was wrapping one
char per line).
- CHANGELOG [2.3] opened for this batch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "16. Sep. 2026" date wrapped inside the 52dp column, so rows with/without
a time ended up different heights and the divider/venue misaligned. The date
block is now a fixed 56dp calendar block: big day number, small month/year and
time, each single-line — every row is the same height and lines up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend sent the UTC `dateTime` (a 20:00 local show read as 08:00Z), and
Android's ZonedDateTime.parse choked on zone-less values → some rows had a
wrong time, others none at all.
- Backend: prefer localDate+localTime (venue-local, naive) over the UTC instant.
- Android: parse a naive LocalDateTime first, fall back to ZonedDateTime for
older "…Z" values. Web already renders the naive string as-is.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Events were fetched with runCatching{}.getOrDefault(emptyList()), so any
network/deserialization failure looked identical to "no concerts". The
backend + Ticketmaster work (verified), so the failure is client-side.
- EventsClient: expectSuccess + timeouts so a non-2xx (e.g. an SSO/HTML
redirect) becomes a real exception instead of an obscure body() failure.
- ArtistViewModel: capture the failure into eventsError (+ Logcat) rather
than defaulting to empty.
- ConcertsSheet: show "Konzerte konnten nicht geladen werden" + the reason
when the fetch failed, vs. "Keine Konzerte angekündigt" when genuinely none.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Pin ("Falscher Künstler?") is now a small icon-only ⚙ chip on both web
and Android — accent-blue when pinned, subtle white otherwise; no more
text banner over the hero.
- Android artist discography no longer filters to record_type=="album";
it now shows albums, EPs AND singles (ordered albums→EPs→singles),
matching Deezer's own artist page. Web already showed all.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The importer no longer has its own sidebar/bottom-nav entry. The Global
Suche field now handles both jobs: plain text runs the Deezer search;
a pasted Spotify/SoundCloud/YouTube link switches into importer mode
(badge + "Importieren" button + preview), matching the Android app and
tidying the left nav.
- New onGlobalInput/tryImporterResolve; importer DOM moved into
page-discover; old page-spotify-importer removed; routing cleaned up.
- Existing resolvers unchanged (still read the mirrored importer-link-input).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CHANGELOG: remove the accidentally duplicated Changed/Fixed/Investigated
block that sat orphaned before [0.8]; [0.9] now appears once.
- README: artist-pages bullet now mentions the same-name artist pin
(Deezer link/id) and that a pinned artist's similar list comes from Deezer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Downloads were 403ing on two fronts: (1) recent yt-dlp needs a JS runtime to
solve YouTube's signature/nsig challenge (Deno, added in cb7a3d7), and (2) the
current yt-dlp *stable* had a bug that 403'd the android_vr client on every video
(yt-dlp#17456, fixed in nightly). Switch the backend from the stable yt-dlp
binary to nightly via pip (`--pre 'yt-dlp[default]'`). Verified end-to-end incl.
a real frontend download. No cookies / PO-token service / proxy needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recent yt-dlp (which the backend re-pulls latest on every restart) needs an
external JavaScript runtime to solve YouTube's signature/nsig challenge — without
it, "Signature solving failed" → formats get skipped/HTTP 403. Install Deno in the
backend container (+ unzip for its installer) and put it on PATH for the node
process so spawned yt-dlp finds it. No extra container, no account, no proxy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
None of it got downloads working on the flagged datacenter IP (web_music media
403, then 429 rate-limiting), so revert the whole lot to keep things clean:
- config.js: remove potProviderUrl + ytdlpProxy
- ytdlp.js: remove potArgs/proxyArgs; player_client back to web_music,default
- novasonic-server.js: remove pot/proxy args from importer resolve
- docker-compose.yml: remove the bgutil-provider service + backend env/plugin/depends_on
- CHANGELOG: drop the [2.2] section
Kept the one genuine fix: a failed direct-link download now reports failure
(moved its note into [2.1]).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The PO token provider works (web_music gets a token) but web_music's media URLs
still 403 from a datacenter IP. Prefer the default player clients for the
DOWNLOAD (their URLs go through with a PO token), keeping web_music only as a
fallback for Art-Track extraction; the YTM search still leads with web_music.
Also add an optional YTDLP_PROXY (--proxy) — the durable fix if the IP stays
flagged (403/429) even with a PO token. Empty → unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the cookies-file stopgap (needs a throwaway account + manual refresh)
with a self-hosted PO token provider — the cookie-less way past YouTube's
datacenter-IP block (bot check + media 403).
- docker-compose: new bgutil-provider service (brainicism/bgutil-ytdlp-pot-provider)
+ install the bgutil-ytdlp-pot-provider plugin in the backend + POT_PROVIDER_URL
env + depends_on.
- ytdlp.js / server.js: when POT_PROVIDER_URL is set, pass
--extractor-args youtubepot-bgutilhttp:base_url=... to the download cascade, the
YTM search probe and the importer resolve. Empty → omitted (unchanged behaviour).
- Removed the cookiesFile config + --cookies wiring so there's no dead code.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The direct-URL (sourceUrl) path returned { success: true } even when no audio
file was produced (e.g. YouTube 403 / bot check), so the job was marked
"done/Fertig!" and web + Android both said the track was added. It now returns
{ failed: true } → the server sets status 'error' with a clear message, which
both frontends already render as a failure. Search-cascade path was already
correct.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
YouTube now returns "Sign in to confirm you're not a bot" for yt-dlp from
datacenter IPs, blocking downloads, search and link-resolve. When a cookies file
is present (YTDLP_COOKIES, default /data/youtube-cookies.txt) every yt-dlp call
passes --cookies so it authenticates. Checked at call time (drop the file in, no
code change); absent → unchanged behaviour. Applied to the download cascade, the
YTM search probe, and the importer resolve.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>