Repository navigation
fix(playback): correct nine faults in progress sync - #6
Merged
Merged
Conversation
- ABS adds the timeListened of each sync to the session. The total time of the session in each sync stored one hour as about 61 hours. - A failed sync gives its time back, so the next sync sends it.
- A paused session sent its old position each 30 seconds. In ABS, that sync overwrites a newer position of a second device. - lastSyncPos is now the position that the server accepted. A session starts with the position of the server, not with the seek target.
- ABS has no open session after a restart, or after 36 hours with no change. Each later sync failed, so the position and the listening time did not reach the server. - The sync now opens a new session for the item and sends them there.
- ABS reads "isFinished": false as "mark as not finished". At the end of a book, the update of the stop removed the mark that the close of the session set, and it set the position to 0. - With no key, ABS applies its own rule for the position, as it does for the close. Only "mark as not finished" sends false.
- pine restores the last session at each start, paused. For a finished book, ABS starts that session at 0. The stop then sent the position 0, and ABS removed the finished mark of the book. - The stop now sends no progress update, and the close has no body, if the server has the position and the listening time.
- The close of the old session and the play request of the next item ran at the same time. ABS closes the open session of a device at a play request, and then answers 404 to the close of pine, so the last position and the listening time of the old item were lost. - The play request, or the series lookup before it, now runs after the close completes or fails.
- The progress update of the stop still carries the position. Only the listening time of a close that gets 404 is lost.
- A quit with q sent the close of the session and the progress update in commands, together with tea.Quit. Bubble Tea does not wait for a command at a quit, and Cleanup found a model with no session. So the process ended before the server got the last position and the listening time. - The quit now keeps the session in the model. Cleanup, which main calls on the final model, sends the close and the progress update and waits for the server for 5 seconds at most. Co-Authored-By: Claude Opus 5.5 <[email protected]>
- A quit with ctrl+c ran the commands of a stop, but it kept the model that still had the session. So Cleanup sent the close of the session and the progress update a second time, and the server added the listening time of the close two times. - The quit now sends no command for the session, as the quit with q. Cleanup sends the close and the progress update one time. Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The listening time was the change of the position. At the speed 2.0, the position moves two seconds in each second, so one hour of play counted as two hours. The web client of ABS counts the time of the clock (client/players/PlayerHandler.js). - The listening time is now the time between two position ticks of a player that plays. A paused player adds nothing. A tick that comes more than 5 seconds after the last tick adds nothing, for example after a suspend of the machine. - The tests of the listening time move the clock of the model. Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
What was wrong
A review of an ABS-compatible server found six faults in the progress sync of pine. Each fault is confirmed in the code of pine, and a test reproduces it. "ABS" below is Audiobookshelf 2.37.1. The statements about ABS come from its source:
server/models/MediaProgress.js,server/managers/PlaybackSessionManager.js, andserver/controllers/SessionController.js.RestorePlaySessionMsgininternal/app/model.go). For a finished book, ABS starts that session at the position 0. At a quit, and at the play of a different item,stopPlaybackandCleanupininternal/app/playback.gosentPATCH /api/me/progress/{id}with{"currentTime":0,"progress":0,"isFinished":false}, and a close with{"currentTime":0,"timeListened":0}. ABS stored the book as not finished, with no finish time, at the position 0. pine sends the same body for the action "mark as not finished", so a server cannot tell the two apart. The close has the same effect if the stored position is not 0, because ABS reads a position in a close as a progress update."isFinished": falsewith the position at the end and the progress 1 (stopPlayback). The close ran at the same time. If ABS handled the close first, it marked the book as finished. Then the progress update removed the mark, and ABS stored the position 0.handleSyncTick). ABS adds thetimeListenedof each sync to the session. For one hour of play, ABS stored about 61 hours.handleSyncTick:isPlayingis true while a session exists). ABS stored the old position again. That removed a newer position that a second device wrote.handleSyncTick). ABS has no open session after a restart, or after 36 hours with no change of the session. Each later sync failed. ABS stored no listening time for the rest of the playback, and it stored the position only at the stop.tea.BatchinhandlePlayCmd,handlePlayEpisodeCmd,startQueuedEntry, andhandlePlaybackCompleted). ABS closes the open session of a device at a play request, and it then answers 404 to the close of pine. ABS did not store the listening time that the close carried.The work on this pull request found three more faults: the faults 7 to 9. They are in "Faults found during the work on this pull request".
What changes
timeListenedis now the listening time that the server does not have. A sync takes it. If the sync fails,handleSessionSyncedgives it back, so the next sync sends it also. A close sends only the time since the last sync.lastSyncPosis now the position that the server accepted. A session starts with the position of the server (PlaySessionData.SyncedTime), not with the seek target. No sync goes out while the position and the listening time have a change of less than 1 second (syncMinChange).abs.Client.UpdatePosition, which sends noisFinishedkey. Only "mark as finished" and "mark as not finished" send the key.abs.Client.CloseSessionNoSync).stopPlaybackCmdsreturns the close apart from the other cleanup commands.closeThenruns the play request, or the series lookup before it, after the close completes or fails.Decisions:
isFinishedkey, not"isFinished": true. pine does not know the "mark as finished" setting of the library, and a stop near the end uses the same code as a stop at the end. With no key, ABS applies its own rule for the position, as it does for the close. The two requests of a stop then give the same result in each order.SessionController.closeof ABS reads an empty body as a close with no position.syncMinChangeis 1 second, because a paused player can report a position that is a fraction of a second away from the start position of the session.How it was tested
Each fault has a test in
internal/app/progress_sync_test.gothat failed before its correction. The tests use a fake ABS server (httptest) and the mock player of the app tests.TestStopOfUntouchedSessionSendsNoProgress(quit withq, quit with ctrl+c,Cleanup, play of a different item)TestStopSendsNoFinishedFlag(end of the book,Cleanup)TestSyncSendsListeningTimeSinceLastSync,TestSyncCarriesListeningTimeOfFailedSync,TestCloseSendsListeningTimeSinceLastSyncTestSyncTickSendsNothingWhenNothingChangedTestSyncOpensNewSessionAfter404(book, episode)TestOldSessionClosesBeforeNextPlay(book, episode, queue at the end of a book, skip in the queue, series, close that fails)The test for fault 6 runs the commands as Bubble Tea does: each command of a batch runs in its own goroutine. Its server needs 100 ms for the close.
Three more tests guard the corrections. They passed before the corrections also:
TestMarkFinishedSendsExplicitFlag: the two explicit actions keep theisFinishedkey.TestSyncSendsPositionOfSeekToOtherTrack: a seek to a different track of a paused book still reaches the server.TestSyncKeepsListeningTimeIfNewSessionFails: the listening time stays with the session if the new session cannot open.internal/abshas a test for each new client function:TestUpdatePositionHTTPandTestCloseSessionNoSyncHTTP.Three old tests asserted that
lastSyncPoschanges at the sync tick. They now assert the new contract:TestSyncTickWhenPlaying,TestPlaybackLifecycleIntegration, andTestPlaybackMultipleSyncCycles.Checks on macOS (Apple Silicon), on the last commit (
54cf382):gofmt -l .: no filego build ./...: passgo vet ./...: passgolangci-lint runwith version 2.14.0: 0 issuesPINE_REQUIRE_LIBMPV=1 PINE_REQUIRE_DBUS=1 go test ./... -count=1: 27 packages passgo test -race ./... -count=1: 27 packages passFaults found during the work on this pull request
The work on this pull request found three more faults. Each fault is confirmed in the code of pine, and a test reproduces it. Each fault has its own commit.
What was wrong
qcould lose the last position and the listening time. Theqkey ranstopPlaybackand returned its commands in a batch withtea.Quit(Updateininternal/app/model.go). Bubble Tea 1.3.10 runs each command of a batch in its own goroutine, and it does not wait for a command at a quit. The model then had no session, soCleanupsent nothing, and the process ended. The server got the close and the progress update only if the two requests left before the end of the process.stopPlayback, but it kept the model that still had the session.Cleanupthen sent the close and the progress update again. ABS adds thetimeListenedof each close to the session.handlePositionMsg). At the speed 2.0, the position moves two seconds in each second. For one hour of play, ABS stored two hours. The web client of ABS counts the time of the clock (client/players/PlayerHandler.js).What changes
tea.Quitand keep the session in the model.Cleanupsends the close and the progress update one time, andmainwaits for it.Cleanuphas a limit of 5 seconds for the two requests. The rule for an untouched session stays: no progress update, and a close with no body.timeListenedis the time of the clock between two position ticks of a player that plays (trackListeningTime). A tick of a paused player adds nothing, and the first tick after a pause adds nothing. A tick with an error of the player adds nothing. A tick that comes more than 5 seconds after the last tick adds nothing (maxListeningTick), for example after a suspend of the machine. A test moves the clock withModel.now.Decisions:
qended immediately and lost the requests.SetSpeed(internal/player/commands.go). Inference from the code: after the start of a new session, mpv plays at the speed 1.0 while the model can have a different speed. No real playback checked this.How it was tested
TestQuitWaitsForLastPosition(q, ctrl+c). Its server needs 100 ms for each request.TestQuitSendsLastPositionOnce(q, ctrl+c),TestStopOfUntouchedSessionSendsNoProgress(new case: quit with ctrl+c)TestListeningTimeIsTimeOfPlay(one minute at the speed 2.0, a pause of one minute, a tick after a suspend of the machine)The tests for the faults 7 and 8 do what the program does at a quit (
quitProgram): each command of the key runs in its own goroutine, the program returns at theQuitMsg, and thenCleanupruns on the final model.Old tests that now assert the new contracts:
Cleanup:TestE2E_FullJourneyandTestE2E_QQuitsAndStopsPlayback.TestPositionMsgUpdatesPlayer,TestPositionMsgTracksTimeListened,TestPositionMsgBackwardSeekAddsOnlyTimeOfPlay(before:TestPositionMsgBackwardSeekDoesNotAddTime),TestPlaybackLifecycleIntegration,TestPlaybackSeekBackwardThenForward,TestE2E_DetailToPlayback, andTestE2E_FullJourney.Not tested
isFinishedkey.🤖 Generated with Claude Code