Repository navigation
Conversation
Audiobookshelf stores a podcast's releaseDate as free text: the web UI saves "" for podcasts added from an RSS URL, OPML imports save "", and admins can type anything. Decoding it as an Instant failed the whole personalized-shelves, library-items and recent-episodes responses, so Home, Shows and Latest never loaded for those libraries. Decode releaseDate as a String, map a blank value to null, and keep sending only ISO-8601 dates when creating a podcast. Closes #1224
One undecodable field in one item fails a whole shelf, list or item response. ABS v2.37.1 can send several values Campfire's models reject: - AudioFile duration, bitRate, codec, timeBase, channels and channelLayout are null when ffprobe doesn't report them, and the episode's AudioTrack copies duration and codec. Books share these. - PodcastEpisode.Enclosure.length is the feed's raw length attribute, which isn't always a number. Campfire never reads it. - Podcast maxEpisodesToKeep and maxNewEpisodesToDownload accept any number, fractions included. Make those fields nullable (or String/Double) on the network models and fill the server's own defaults (duration || 0, 0 and "") when mapping into the non-null domain models and DB columns, so no migration is needed.
Now that audio files without a probed duration decode, their episodes and tracks carry a duration of 0, and several places treated that as a real length. Playing such an episode marked it finished: Session isFinished (currentTime >= duration) was true at once, so progress sync sent isFinished to the server and the server session closed on its first sync. - Session.isFinished needs a known duration; timeRemaining never goes negative. - MediaProgress.actualProgress falls back to the reported progress instead of dividing by zero (infinite bars, a 2147483647% label). - iOS lock-screen scrubbing ignores a zero duration instead of seeking to an infinite position, and desktop seeks no longer clamp to 0. - Episode cards, the episode and description sheets, the queue and list items hide a zero duration instead of showing "0s" or "0 ns"; the audio track row uses progressOver.
Generated by 🚫 Danger Kotlin against 34fc9ee |
Collaborator
r0adkll
added a commit
that referenced
this pull request
Oct 10, 2026
…1225) <!-- pr-stack:start --> ## Stack - [This PR] #1225 - #1226 - #1227 <!-- pr-stack:end --> Podcast libraries could fail to load in the Home, Shows and Latest tabs. Audiobookshelf stores a podcast's release date as free text, and Campfire decoded it as an ISO-8601 `Instant`. The web UI saves `""` for podcasts added from an RSS URL, OPML imports save `""`, and admins can type anything ("2019"). One such podcast throws `kotlin.time.InstantFormatException`, failing the whole personalized-shelves, library-items or recent-episodes response. Fetch errors with nothing cached are swallowed, so Home keeps the previous library's shelves or spins, and Shows spins with a "--" count. Closes #1224 ## What changed - `PodcastMetadata.releaseDate` (`data/network/api`) is now `String?`, matching the server's `STRING` column. - `NetworkPodcastMetadata.asDomainModel()` (`PodcastMapping.kt`) maps a blank release date to null. The domain model and DB already held it as a `String?`. - `PodcastDraft.asCreateMetadata()` (`PodcastSearchMapping.kt`) still sends only an ISO-8601 release date when creating a podcast, now as `Instant.toString()`. That is the same wire format the `Instant` serializer produced. - New tests: - `PodcastMetadataTest` decodes a podcast item with a blank or non-ISO release date, standalone and inside a personalized podcast shelf. - `PodcastMappingTest` and `PodcastSearchMappingTest` cover both mapping changes. This adds `commonTest` source sets to `:data:db:mapping` and `:features:podcasts:impl`. ## Verification - **Root cause:** on unmodified `main`, a test decoding a minified podcast item with `"releaseDate": ""` failed with `kotlin.time.InstantFormatException`. - **Writers:** confirmed in ABS v2.37.1 that `NewModal.vue` (web RSS add) and `PodcastManager.js` (OPML import) write `''`. - **Before/after on the testbed emulator** (local ABS, Podcasts library). One podcast was set to `releaseDate: ""` and another to `"2019"`. - `main`: Home kept showing the audiobook shelves, and Shows and Latest stayed empty. Logcat showed decode failures on `/personalized`, `/items` and `/recent-episodes` (`An empty string is not a valid Instant`, `The input string is too short ... "2019"`). - This branch: Home shows Newest Episodes and Recently Added, including both affected podcasts. Shows lists all 12 podcasts. The `""` podcast's detail loads with its episodes. Latest loads, and switching back to Audiobooks works. No decode errors in logcat. - **Real data:** podcast responses from a real 2.37.1 server decode cleanly with these models: library items, personalized shelves, recent episodes, and 8 expanded item details. - **Checks:** `./scripts/ktlint --check`, `./gradlew --continue jvmTest test`, and compiles for `:app:desktop:compileKotlin`, `:app:android:compileAlphaDebugKotlin` and `:app:ios:compileKotlinIosSimulatorArm64`. All pass. - Excluded: `:infra:audioplayer:engine-tests:test`. Its FFmpeg engine tests can't load the native library on this machine (`UnsatisfiedLinkError`), and this PR doesn't touch audio code. ## Follow-ups An audit of the other podcast models against ABS v2.37.1 found more fields of the same kind: - non-null `AudioFile` probe fields that ffprobe can leave null; - `PodcastEpisode.Enclosure.length` typed `Long?`, though the server sends the feed's raw text; - `maxEpisodesToKeep` typed `Int?`, though the server accepts any number; - `numEpisodes`, which is never decoded, so every podcast from a list or shelf is stored with 0 episodes. These come next in their own PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
r0adkll
marked this pull request as draft
October 13, 2026 14:51
Owner
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack
An audit of Campfire's podcast models against Audiobookshelf v2.37.1 found more values the server can send that the models reject. Like the release date in the PR below, one such value fails a whole shelf, list or item-page response. This PR makes those fields tolerant. Unknown durations then reach playback as 0, so it also guards every place that treated 0 as a real length. Part of #1224.
What changed
Network models (
data/network/api)AudioFile:duration,bitRate,codec,timeBase,channelsandchannelLayoutare nullable. The server stores null when ffprobe doesn't report them, and the download path keeps the file anyway (prober.js,PodcastManager.probeAudioFile). Books share this model.AudioTrack:durationandcodecare nullable, since an episode's track is a copy of its audio file.PodcastEpisode.Enclosure.lengthis aString?. The server passes the feed's rawlengthattribute through as text, and Campfire never reads it.Podcast.maxEpisodesToKeep/maxNewEpisodesToDownloadareDouble?.Podcast.updateFromRequestsaves any number, fractions included.Mapping (
data/db/mapping)AudioFileMapping,AudioTrackMappingandPodcastMappingfill the server's own defaults into the non-null domain models and DB columns: duration 0 (as ABS'saudioFile?.duration || 0), 0 and"". No migration is needed.PodcastMappingtruncates the episode limits toInt.Unknown (zero) duration
Session.isFinishedrequires a known duration. Before this, playing such an episode marked it finished at once: the progress sync sentisFinished: trueto the server, andRemoteSessionsUpdaterclosed the server session on its first sync.timeRemainingno longer goes negative.MediaProgress.actualProgressfalls back to the reported progress instead of dividing by zero, which drew infinite bars and a "2147483647%" label.IosAudioPlayerignores lock-screen scrubbing when the duration is 0, instead of seeking to an infinite position.DesktopAudioPlayer.seekAbsoluteno longer clamps every seek to 0.EpisodeListItem,PodcastEpisodeBottomSheet,EpisodeDescriptionBottomSheet,QueueItemandLibraryItemListItemhide a zero duration instead of showing "0s" or "0 ns".AudioTrackSlotusesprogressOver.Verification
Tests
PodcastEpisodeTest: 5 decode cases (an unprobed audio file and track, a non-numeric enclosure length, fractional episode limits, and a recent-episodes page containing such an episode). They fail withJsonDecodingExceptionagainst the parent's models and pass here.AudioFileMappingTestandPodcastMappingTestcover the defaults and the truncation.UnknownDurationTestcoversisFinished,timeRemainingandactualProgress.Testbed emulator, before vs after
enclosureSize = 'unknown', its podcast gotmaxEpisodesToKeep = 2.5, and a book's first audio file got null probe fields. The API returned those exact values.Failed to parse literal '2.5' as an int value,'"unknown"' as a long valueand'null' as a float value.dumpsys media_sessionPLAYING, position advancing).Testbed emulator, unknown-duration guards
"duration": 0, "currentTime": 77.554, "isFinished": false)."isFinished": true).Checks
./scripts/ktlint --check,./gradlew --continue jvmTest test,:app:desktop:compileKotlin,:app:android:compileAlphaDebugKotlinand:app:ios:compileKotlinIosSimulatorArm64all pass.:infra:audioplayer:engine-tests:test, whose FFmpeg tests can't load the native library on this machine (UnsatisfiedLinkError).Follow-ups
IosPlayermultiplies the target by the item's metadata duration, so fixing it means using the player's own duration./api/mefails on an old progress row with nolibraryItemId.deviceInfo: {}.🤖 Generated with Claude Code