Skip to content

feat(exporter): implement exporter namespace (resilient backup, checkpoints, ListenBrainz format) - #161

Merged
ansango merged 1 commit into
mainfrom
feat/157-exporter-namespace
Aug 28, 2026
Merged

ansango merged 1 commit into
mainfrom
feat/157-exporter-namespace

Conversation

@ansango

@ansango ansango commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Closes #157. Part of #153.

Summary

  • Implemented client.exporter namespace with 3 bulk backup & migration engines:
    • exportScrobbles: Streamed scrobbles export with UTS checkpointing for resumable backward scans, and multi-format output (json, jsonl, csv, listenbrainz).
    • exportLovedTracks: Loved tracks backup in JSON and CSV.
    • exportLibrary: Artist catalog export with playcounts and metadata.
  • Added full Zod schemas, unit tests, subpath exports (@ansango/lastfm-api/exporter), and Scalar OpenAPI registrations.

@ansango
ansango merged commit 384b57c into main Aug 28, 2026
1 check passed
@ansango
ansango deleted the feat/157-exporter-namespace branch August 28, 2026 23:24
@github-actions

Copy link
Copy Markdown

✅ Test Summary

250 passed · 0 failed · 0 skipped

Run: 33220299673

File ✅ ❌ ⏭️
track 27 0 0
inventory 6 0 0
insights 61 0 0
library 4 0 0
chart 6 0 0
geo 7 0 0
utils 4 0 0
tag 10 0 0
exporter 6 0 0
auth 9 0 0
user 23 0 0
artist 21 0 0
ci-summary 11 0 0
reports 5 0 0
album 15 0 0
pagination 5 0 0
transport 25 0 0
playlists 5 0 0
Total 250 0 0
All test names
Status Test Time
✅ track › track service > getInfo > routes to track.getInfo with artist and track, returns parsed payload 1ms
✅ track › track service > getInfo > passes optional mbid when provided 0ms
✅ track › track service > getSimilar > routes to track.getSimilar and returns parsed payload 0ms
✅ track › track service > getTags > routes to track.getTags with user, artist, and track, returns parsed payload 0ms
✅ track › track service > getTopTags > routes to track.getTopTags and returns parsed payload 0ms
✅ track › track service > search > routes to track.search with track query and returns parsed payload 0ms
✅ track › track service > getCorrection > routes to track.getCorrection unsigned with artist+track, returns the corrected identities 0ms
✅ track › track service > getCorrection > parses a no-correction response (empty correction list) 0ms
✅ track › track service > getCorrection > accepts absent optional corrected flags and @attr 0ms
✅ track › track service > scrobble (smoke through the service; transport behavior is in transport.test.ts) > scrobble succeeds end-to-end when session is on the config 1ms
✅ track › track service > scrobble (smoke through the service; transport behavior is in transport.test.ts) > scrobbleMany accepts a numeric timestamp per the updated schema 0ms
✅ track › track service > scrobble (smoke through the service; transport behavior is in transport.test.ts) > scrobbleMany rejects more than 50 tracks with a clear error 0ms
✅ track › track service > error handling > Last.fm error envelope becomes LastFmApiError 0ms
✅ track › track service > addTags > routes to track.addTags via signed POST with sk and comma-joined tags, returns void 0ms
✅ track › track service > addTags > fails before fetch when no session key is available 0ms
✅ track › track service > addTags > rejects more than 10 tags in the request schema 2ms
✅ track › track service > removeTag > routes to track.removeTag via signed POST with sk and single tag, returns void 0ms
✅ track › track service > removeTag > fails before fetch when no session key is available 0ms
✅ track › track service > love > routes to track.love via signed POST with sk, returns void 0ms
✅ track › track service > love > fails before fetch when no session key is available 0ms
✅ track › track service > unlove > routes to track.unlove via signed POST with sk, returns void 0ms
✅ track › track service > updateNowPlaying (regression — restored after #89 merge drop) > routes to track.updateNowPlaying with the minimum required body, returns the parsed nowplaying payload 0ms
✅ track › track service > updateNowPlaying (regression — restored after #89 merge drop) > forwards every optional field with exact wire casing and omits undefined 0ms
✅ track › track service > updateNowPlaying (regression — restored after #89 merge drop) > fails before fetch when no session key is available, with a sanitized error 0ms
✅ track › track service > updateNowPlaying (regression — restored after #89 merge drop) > parses an ignored now-playing response (code != 0) but does not throw 0ms
✅ track › track service > updateNowPlaying (regression — restored after #89 merge drop) > Last.fm error envelope becomes LastFmApiError 0ms
✅ track › track service > import coverage > track service is exposed from root, track entrypoint, and track.schemas entrypoint 0ms
✅ inventory › inventory: 56/56 canonical Last.fm methods > the inventory list itself contains exactly 56 entries 0ms
✅ inventory › inventory: 56/56 canonical Last.fm methods > all canonical methods exist on the LastFmClient as callable functions 0ms
✅ inventory › inventory: 56/56 canonical Last.fm methods > no duplicate canonical methods are listed 0ms
✅ inventory › inventory: 56/56 canonical Last.fm methods > every entry matches the namespace.method shape 0ms
✅ inventory › inventory: 56/56 canonical Last.fm methods > per-namespace breakdown matches the docs/api-coverage.md audit 0ms
✅ inventory › inventory: 56/56 canonical Last.fm methods > all canonical methods exist on LastFmCoreClient instance 0ms
✅ insights › insights service > pure algorithms: diversity > returns zeros for empty count map 0ms
✅ insights › insights service > pure algorithms: diversity > returns 0 entropy and 0 normalized for single item 0ms
✅ insights › insights service > pure algorithms: diversity > returns 1.0 normalized entropy for perfectly uniform distribution 0ms
✅ insights › insights service > pure algorithms: diversity > returns < 1.0 normalized entropy for skewed distribution 0ms
✅ insights › insights service > pure algorithms: diversity > topNShare calculates correct concentration shares 0ms
✅ insights › insights service > pure algorithms: periods > resolves overall period 0ms
✅ insights › insights service > pure algorithms: periods > resolves 7day and weekly periods 0ms
✅ insights › insights service > pure algorithms: periods > resolves monthly and 1month periods 0ms
✅ insights › insights service > pure algorithms: bio sanitization > stripWiki cleans wiki links and HTML tags 0ms
✅ insights › insights service > pure algorithms: bio sanitization > summarizeBio truncates cleanly on word boundary 0ms
✅ insights › insights service > pure algorithms: bio sanitization > summarizeBio returns full string if within maxChars 0ms
✅ insights › insights service > pure algorithms: hours histogram > bucketTimestamp maps UTC hour and ISO weekday (Mon=0..Sun=6) 0ms
✅ insights › insights service > pure algorithms: hours histogram > buildHourHistogram computes counts, peaks, and shares 0ms
✅ insights › insights service > pure algorithms: binges > findBinges detects consecutive plays of the same artist 0ms
✅ insights › insights service > pure algorithms: binges > breaks binge run when gap exceeds maxGapSeconds 0ms
✅ insights › insights service > pure algorithms: trends diff > diffRankings categorizes risers, fallers, newcomers, departures 0ms
✅ insights › insights service > pure algorithms: discoveries > findNewArtists filters out baseline and sorts by firstSeen 0ms
✅ insights › insights service > pure algorithms: mood classifier > classifies euphoric and energetic tags 1ms
✅ insights › insights service > pure algorithms: mood classifier > classifies ambient and melancholic tags 0ms
✅ insights › insights service > pure algorithms: personality archetypes > scores Devotee highest when top1Share is very high 0ms
✅ insights › insights service > pure algorithms: personality archetypes > scores Nocturnal highest when night listening dominates 0ms
✅ insights › insights service > pure algorithms: compare users > jaccard computes correct overlap fractions 0ms
✅ insights › insights service > pure algorithms: compare users > compareArtists computes mutual overlap and ranking 0ms
✅ insights › insights service > getSummary > fetches top artists, tracks, albums, tags in parallel and computes summary + diversity 3ms
✅ insights › insights service > getSummary > returns undefined diversity when fewer than 2 artists are returned 0ms
✅ insights › insights service > getSummary > propagates LastFmApiError if any sub-call fails 0ms
✅ insights › insights service > getNowPlaying > enriches currently playing track with artist bio and similar artists 2ms
✅ insights › insights service > getNowPlaying > handles empty track history gracefully 0ms
✅ insights › insights service > getHoursHistogram > paginates recenttracks and calculates diurnal histogram 1ms
✅ insights › insights service > getBinges > paginates recent tracks, sorts ascending, and identifies streaks 1ms
✅ insights › insights service > getTrends > calculates artist ranking diff between periods 1ms
✅ insights › insights service > getDiscoveries > identifies new artist discoveries against baseline roster 1ms
✅ insights › insights service > getMood > fetches user tags & top artist tags, returns classified mood profile 1ms
✅ insights › insights service > getPersonality > assembles features and returns listener archetype 2ms
✅ insights › insights service > compareUsers > fetches both users top artists and calculates Jaccard affinity and shared artists 1ms
✅ insights › insights service > getObscurityScore > evaluates top artists and computes weighted obscurity score, gems, and anchors 2ms
✅ insights › insights service > getObscurityScore > handles empty top artists gracefully with fallback score 0ms
✅ insights › insights service > getForgottenFavorites > identifies historical top artists absent from recent listening 1ms
✅ insights › insights service > getObsessions > detects high-density obsession window in recent track stream 2ms
✅ insights › insights service > getListeningStreaks > computes consecutive streak days and dry spells 1ms
✅ insights › insights service > getListeningHeatmap > generates calendar days with intensity levels 0..4 1ms
✅ insights › insights service > getAlbumHabits > analyzes sequential album tracks and classifies listener profile 2ms
✅ insights › insights service > getGenreBreakdown > filters noise tags and calculates HHI concentration index 1ms
✅ insights › insights service > getGenreEvolution > computes rising, fading, and new genres between periods 2ms
✅ insights › insights service > getSmartRecommendations > traverses similarity graph from user seeds and returns ranked candidates 2ms
✅ insights › insights service > getBridgeArtists > identifies artists appearing across two distinct genre charts 3ms
✅ insights › insights service > compareTasteGroup > computes pairwise matrix, consensus artists, and outlier for 3 users 2ms
✅ insights › insights service > schema exports and client wiring > factory createInsightsService instantiates InsightsService with all wired methods 0ms
✅ insights › insights service > schema exports and client wiring > exposes insights service via createClient helper 0ms
✅ insights › insights service > schema exports and client wiring > insightsCompareRequestSchema validates inputs 0ms
✅ insights › insights service > schema exports and client wiring > insightsObscurityRequestSchema validates inputs 0ms
✅ insights › insights service > schema exports and client wiring > insightsForgottenFavoritesRequestSchema validates inputs 0ms
✅ insights › insights service > schema exports and client wiring > insightsObsessionsRequestSchema validates inputs 0ms
✅ insights › insights service > schema exports and client wiring > insightsStreaksRequestSchema validates inputs 0ms
✅ insights › insights service > schema exports and client wiring > insightsHeatmapRequestSchema validates inputs 0ms
✅ insights › insights service > schema exports and client wiring > insightsAlbumHabitsRequestSchema validates inputs 0ms
✅ insights › insights service > schema exports and client wiring > insightsGenreBreakdownRequestSchema validates inputs 0ms
✅ insights › insights service > schema exports and client wiring > insightsGenreEvolutionRequestSchema validates inputs 0ms
✅ insights › insights service > schema exports and client wiring > insightsRecommendationsRequestSchema validates inputs 0ms
✅ insights › insights service > schema exports and client wiring > insightsBridgeArtistsRequestSchema validates inputs 0ms
✅ insights › insights service > schema exports and client wiring > insightsCompareTasteGroupRequestSchema validates inputs 0ms
✅ library › library service > getArtists > routes to library.getArtists with user and returns parsed payload 0ms
✅ library › library service > getArtists > passes limit and page when provided 0ms
✅ library › library service > error handling > Last.fm error envelope becomes LastFmApiError 0ms
✅ library › library service > import coverage > library service is exposed from root, library entrypoint, and library.schemas entrypoint 0ms
✅ chart › chart service > getTopArtists > routes to chart.getTopArtists and returns parsed payload 0ms
✅ chart › chart service > getTopArtists > passes limit and page when provided 0ms
✅ chart › chart service > getTopTags > routes to chart.getTopTags and returns parsed payload 0ms
✅ chart › chart service > getTopTracks > routes to chart.getTopTracks and returns parsed payload 0ms
✅ chart › chart service > error handling > Last.fm error envelope becomes LastFmApiError 0ms
✅ chart › chart service > import coverage > chart service is exposed from root, chart entrypoint, and chart.schemas entrypoint 0ms
✅ geo › geo service > getTopArtists > routes to geo.getTopArtists with country and returns parsed payload 0ms
✅ geo › geo service > getTopArtists > passes limit and page when provided 0ms
✅ geo › geo service > getTopArtists > uses GET (no body) and no Content-Type header 0ms
✅ geo › geo service > getTopTracks > routes to geo.getTopTracks with country and location 0ms
✅ geo › geo service > error handling > Last.fm error envelope becomes LastFmApiError with code and message 0ms
✅ geo › geo service > error handling > HTTP 500 with non-JSON body becomes LastFmApiError without exposing a fake api_sig 0ms
✅ geo › geo service > import coverage > geo service is exposed from root, geo entrypoint, and geo.schemas entrypoint 0ms
✅ utils › issue #58: default baseUrl > LastFmClient constructed without baseUrl + getInfo does not throw "Failed to parse URL from undefined" 0ms
✅ utils › issue #58: default baseUrl > buildUrl defaults baseUrl when config.baseUrl is undefined 0ms
✅ utils › issue #58: default baseUrl > buildAuthUrl defaults baseUrl when config.baseUrl is undefined 0ms
✅ utils › issue #58: default baseUrl > buildUrl honours an explicit baseUrl when provided 0ms
✅ tag › tag service > getInfo > routes to tag.getInfo with tag and returns parsed payload 0ms
✅ tag › tag service > getInfo > passes lang when provided 0ms
✅ tag › tag service > getSimilar > routes to tag.getSimilar and returns parsed payload 0ms
✅ tag › tag service > getTopAlbums > routes to tag.getTopAlbums and returns parsed payload 0ms
✅ tag › tag service > getTopArtists > routes to tag.getTopArtists and returns parsed payload 0ms
✅ tag › tag service > getTopTags > routes to tag.getTopTags and returns parsed payload 0ms
✅ tag › tag service > getTopTracks > routes to tag.getTopTracks and returns parsed payload 0ms
✅ tag › tag service > getWeeklyChartList > routes to tag.getWeeklyChartList and returns parsed payload 0ms
✅ tag › tag service > error handling > Last.fm error envelope becomes LastFmApiError 0ms
✅ tag › tag service > import coverage > tag service is exposed from root, tag entrypoint, and tag.schemas entrypoint 0ms
✅ exporter › exporter service > exportScrobbles > exports scrobbles to JSON, CSV, JSONL, and ListenBrainz with checkpoint metadata 1ms
✅ exporter › exporter service > exportScrobbles > exports to CSV format properly escaping columns 0ms
✅ exporter › exporter service > exportLovedTracks > exports loved tracks to CSV and JSON 1ms
✅ exporter › exporter service > exportLibrary > exports user artist library to CSV and JSON 1ms
✅ exporter › exporter service > schema exports and client wiring > factory createExporterService instantiates ExporterService with all wired methods 0ms
✅ exporter › exporter service > schema exports and client wiring > exposes exporter service via createClient helper 0ms
✅ auth › auth service > getSession (regression — unchanged by #72) > routes to auth.getSession with token and api_sig, returns parsed payload 0ms
✅ auth › auth service > getSession (regression — unchanged by #72) > throws LastFmApiError on auth failure 0ms
✅ auth › auth service > getToken > works without a request object and signs GET with no sk 0ms
✅ auth › auth service > getToken > response includes a pre-built authUrl pointing at last.fm/api/auth 0ms
✅ auth › auth service > getToken > authUrl is independent of the network roundtrip (built from the returned token) 0ms
✅ auth › auth service > getToken > signature is reproducible from a fixed vector (method + api_key only) 0ms
✅ auth › auth service > getToken > preserves caller-provided signal and safe headers 0ms
✅ auth › auth service > getToken > Last.fm error envelope becomes LastFmApiError 0ms
✅ auth › auth service > import coverage > auth service is exposed from root, auth entrypoint, and auth.schemas entrypoint 0ms
✅ user › user service > getInfo > routes to user.getInfo with user, returns parsed payload 0ms
✅ user › user service > getFriends > routes to user.getFriends with user, returns parsed payload 0ms
✅ user › user service > getFriends > passes limit and page when provided 0ms
✅ user › user service > getLovedTracks > routes to user.getLovedTracks with user, returns parsed payload 0ms
✅ user › user service > getRecentTracks > routes to user.getRecentTracks with user, returns parsed payload 0ms
✅ user › user service > getTopAlbums > routes to user.getTopAlbums and returns parsed payload 0ms
✅ user › user service > getTopAlbums > passes period when provided 0ms
✅ user › user service > getTopArtists > routes to user.getTopArtists and returns parsed payload 0ms
✅ user › user service > getTopTags > routes to user.getTopTags and returns parsed payload 0ms
✅ user › user service > getTopTracks > routes to user.getTopTracks and returns parsed payload 0ms
✅ user › user service > getWeeklyAlbumChart > routes to user.getWeeklyAlbumChart and returns parsed payload 0ms
✅ user › user service > getWeeklyArtistChart > routes to user.getWeeklyArtistChart and returns parsed payload 0ms
✅ user › user service > getWeeklyChartList > routes to user.getWeeklyChartList and returns parsed payload 0ms
✅ user › user service > getWeeklyTrackChart > routes to user.getWeeklyTrackChart and returns parsed payload 0ms
✅ user › user service > getPersonalTags > routes to user.getPersonalTags with user+tag+taggingtype, returns the artist variant 0ms
✅ user › user service > getPersonalTags > routes for the album variant and parses the album entity 0ms
✅ user › user service > getPersonalTags > routes for the track variant and parses the track entity 0ms
✅ user › user service > getPersonalTags > passes limit and page when provided 0ms
✅ user › user service > getPersonalTags > parses empty results (no personal tags) 0ms
✅ user › user service > getPersonalTags > rejects an invalid taggingtype at the request schema 1ms
✅ user › user service > error handling > Last.fm error envelope becomes LastFmApiError 0ms
✅ user › user service > import coverage > user service is exposed from root, user entrypoint, and user.schemas entrypoint 0ms
✅ user › user service > import coverage > generic narrowing: literal taggingtype resolves to the matching response 0ms
✅ artist › artist service > getInfo > routes to artist.getInfo with artist, returns parsed payload 0ms
✅ artist › artist service > getInfo > passes optional mbid, lang, and user when provided 0ms
✅ artist › artist service > getTags > routes to artist.getTags with artist, returns parsed payload 0ms
✅ artist › artist service > getTags > passes limit when provided 0ms
✅ artist › artist service > getSimilar > routes to artist.getSimilar and returns parsed payload 0ms
✅ artist › artist service > getSimilar > passes limit when provided 0ms
✅ artist › artist service > getTopAlbums > routes to artist.getTopAlbums and returns parsed payload 0ms
✅ artist › artist service > getTopTags > routes to artist.getTopTags and returns parsed payload 0ms
✅ artist › artist service > getTopTracks > routes to artist.getTopTracks and returns parsed payload 0ms
✅ artist › artist service > search > routes to artist.search with artist query and returns parsed payload 0ms
✅ artist › artist service > getCorrection > routes to artist.getCorrection unsigned with artist, returns the corrected identity 0ms
✅ artist › artist service > getCorrection > parses a no-correction response (empty correction list) 0ms
✅ artist › artist service > getCorrection > parses a response without optional @attr on the correction entry 0ms
✅ artist › artist service > error handling > Last.fm error envelope becomes LastFmApiError 0ms
✅ artist › artist service > addTags > routes to artist.addTags via signed POST with sk and comma-joined tags, returns void 0ms
✅ artist › artist service > addTags > per-request sk overrides config.sessionKey 0ms
✅ artist › artist service > addTags > fails before fetch when no session key is available, with a clear sanitized error 0ms
✅ artist › artist service > addTags > rejects more than 10 tags in the request schema 0ms
✅ artist › artist service > removeTag > routes to artist.removeTag via signed POST with sk and single tag, returns void 0ms
✅ artist › artist service > removeTag > fails before fetch when no session key is available 0ms
✅ artist › artist service > import coverage > artist service is exposed from root, artist entrypoint, and artist.schemas entrypoint 0ms
✅ ci-summary › parse() > parses bun output without group prefix (local default) 0ms
✅ ci-summary › parse() > accepts ::group:: prefix (real bytes bun writes in CI) 0ms
✅ ci-summary › parse() > accepts ##[group] prefix (how the runner renders the same bytes) 0ms
✅ ci-summary › parse() > returns empty array when no test files are present 0ms
✅ ci-summary › parse() > does not match nested paths that are not under src/tests/ 0ms
✅ ci-summary › buildMarkdown() > aggregates counts correctly and picks a ❌ head when any test failed 0ms
✅ ci-summary › buildMarkdown() > picks ✅ when everything passed and ⚠️ when only skipped remain 0ms
✅ ci-summary › buildMarkdown() > per-file table strips the src/tests/ prefix and .test.ts suffix 0ms
✅ ci-summary › buildMarkdown() > includes the run URL in the header sub-line 0ms
✅ ci-summary › buildMarkdown() > contains the dedup marker so a re-run can PATCH instead of POSTing again 0ms
✅ ci-summary › buildMarkdown() > collapsible section lists every test with its emoji and elapsed ms 0ms
✅ reports › reports service > getWrapped > generates yearly wrapped summary with seasonal profiles and top entities 2ms
✅ reports › reports service > getMilestones > detects milestones and calculates next milestone projection 1ms
✅ reports › reports service > getMonthlyDigest > compares current month to previous month and returns growth delta 1ms
✅ reports › reports service > schema exports and client wiring > factory createReportsService instantiates ReportsService with all wired methods 0ms
✅ reports › reports service > schema exports and client wiring > exposes reports service via createClient helper 0ms
✅ album › album service > getInfo > routes to album.getInfo with artist and album, returns parsed payload 0ms
✅ album › album service > getInfo > passes optional mbid, username, and lang when provided 0ms
✅ album › album service > getTags > routes to album.getTags with user, artist, and album, returns parsed payload 0ms
✅ album › album service > getTopTags > routes to album.getTopTags and returns parsed payload 0ms
✅ album › album service > search > routes to album.search with album query, returns parsed payload 0ms
✅ album › album service > search > passes limit and page when provided 0ms
✅ album › album service > error handling > Last.fm error envelope becomes LastFmApiError 0ms
✅ album › album service > addTags > routes to album.addTags via signed POST with sk and comma-joined tags, returns void 0ms
✅ album › album service > addTags > per-request sk overrides config.sessionKey 0ms
✅ album › album service > addTags > fails before fetch when no session key is available, with a clear sanitized error 0ms
✅ album › album service > addTags > rejects more than 10 tags in the request schema 0ms
✅ album › album service > addTags > Last.fm error envelope becomes LastFmApiError 0ms
✅ album › album service > removeTag > routes to album.removeTag via signed POST with sk and single tag, returns void 0ms
✅ album › album service > removeTag > fails before fetch when no session key is available 0ms
✅ album › album service > import coverage > album service is exposed from root, album entrypoint, and album.schemas entrypoint 0ms
✅ pagination › core/pagination > iterateItems streams items across multiple pages 0ms
✅ pagination › core/pagination > iterateItems respects maxPages limit 0ms
✅ pagination › core/pagination > iterateItems respects maxItems limit 0ms
✅ pagination › core/pagination > collectAll collects all items into an array 0ms
✅ pagination › core/pagination > iteratePages yields full page responses 0ms
✅ transport › signed POST transport (issue #68) > single track.scrobble > sends a signed POST with form body; nothing signed in the URL 0ms
✅ transport › signed POST transport (issue #68) > single track.scrobble > signature excludes format and api_sig and is reproducible from a fixed vector 0ms
✅ transport › signed POST transport (issue #68) > single track.scrobble > coerces numeric timestamp to string in the wire body 0ms
✅ transport › signed POST transport (issue #68) > single track.scrobble > passes through optional album 0ms
✅ transport › signed POST transport (issue #68) > batch track.scrobble > preserves indexed artist[0], track[0], timestamp[0], album[0] keys 0ms
✅ transport › signed POST transport (issue #68) > batch track.scrobble > does not put any write parameter in the URL 0ms
✅ transport › signed POST transport (issue #68) > session resolution and pre-fetch failures > request sk overrides config.sessionKey 0ms
✅ transport › signed POST transport (issue #68) > session resolution and pre-fetch failures > config.sessionKey is used when request does not provide sk 0ms
✅ transport › signed POST transport (issue #68) > session resolution and pre-fetch failures > missing session fails before fetch with a clear, sanitized error 0ms
✅ transport › signed POST transport (issue #68) > session resolution and pre-fetch failures > missing sharedSecret fails before fetch with a clear, sanitized error 0ms
✅ transport › signed POST transport (issue #68) > reserved field protection > caller cannot override method, api_key, api_sig, or format via params 0ms
✅ transport › signed POST transport (issue #68) > reserved field protection > caller cannot replace method, body, or Content-Type via init 0ms
✅ transport › signed POST transport (issue #68) > RequestInit preservation > preserves caller-provided signal 0ms
✅ transport › signed POST transport (issue #68) > RequestInit preservation > preserves safe caller-provided headers (besides Content-Type) 0ms
✅ transport › signed POST transport (issue #68) > error routing > HTTP failure with no JSON body surfaces as LastFmApiError 0ms
✅ transport › signed POST transport (issue #68) > error routing > Last.fm error envelope surfaces as LastFmApiError with code and message 0ms
✅ transport › signed POST transport (issue #68) > error routing > empty success body is returned, not turned into an error 0ms
✅ transport › signed POST transport (issue #68) > baseUrl handling > uses default baseUrl when config.baseUrl is undefined 0ms
✅ transport › signed POST transport (issue #68) > baseUrl handling > honours an explicit config.baseUrl 0ms
✅ transport › signed POST transport (issue #68) > session-less signed methods (forward-compat for #72) > requiresSession: false omits sk from signature and body 0ms
✅ transport › signed POST transport (issue #68) > session-less signed methods (forward-compat for #72) > requiresSession: false does not fail when config has no sessionKey 0ms
✅ transport › signed POST transport (issue #68) > integration through LastFmClient.track > scrobble and postTrackScrobble share the same implementation 0ms
✅ transport › signed POST transport (issue #68) > integration through LastFmClient.track > scrobbleMany and postBatchTrackScrobble share the same implementation 0ms
✅ transport › signed POST transport (issue #68) > integration through LastFmClient.track > scrobble routes a numeric timestamp through the service 0ms
✅ transport › signed POST transport (issue #68) > integration through LastFmClient.track > scrobbleMany preserves indexed batch keys through the service 0ms
✅ playlists › playlists service > generate > generates heavy-rotation playlist with M3U and CSV formats 2ms
✅ playlists › playlists service > generate > generates time-capsule playlist skipping recent tracks 0ms
✅ playlists › playlists service > export formats > exports custom tracks to M3U and CSV formats 0ms
✅ playlists › playlists service > schema exports and client wiring > factory createPlaylistsService instantiates PlaylistsService with all wired methods 0ms
✅ playlists › playlists service > schema exports and client wiring > exposes playlists service via createClient helper 0ms

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(exporter): implement exporter namespace (resilient backup, checkpoints, ListenBrainz format)

1 participant