Repository navigation
fix: correct bookmarks, playback faults, and the keys of text inputs - #8
Merged
Merged
Conversation
- The detail screen read the bookmarks from the key `bookmarks` of
GET /api/me/progress/{id}. ABS gives no such key in that response
(getOldMediaProgress in server/models/MediaProgress.js), and it
answers 404 for an item with no progress. So the detail screen showed
"No bookmarks yet" for an item that has bookmarks.
- GetBookmarks now reads GET /api/me/bookmarks/{id}, which ABS has from
2.36.0. An older ABS answers 404 for that route. GetBookmarks then
reads the bookmarks of the item from GET /api/me.
- The detail screen calls GetBookmarks of the cache client. The cache
key is `bookmarks:{id}`, which the bookmark commands already remove.
- The fake server of the E2E tests gave bookmarks with the progress. It
now has the bookmark route, as ABS 2.37.1.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Each launch makes a new mpv handle, and a new handle plays at the speed 1.0. Only the speed keys and the MPRIS rate set the speed of the player. So after the start of a session, and after a track rollover, the player played at the speed 1.0, while the footer showed the speed of the model. The speed of the config file had no effect on the player. - The model now sets its speed in the player when the player is ready, as spruce does. Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The listening time counted the time of the clock while the player reported no pause. mpv reports no pause while it fills its cache: the property `pause` stays false, the position does not move, and the property `core-idle` is true. So the time of a stalled stream counted as listening time. - A position tick now also reads `core-idle` (GetIdle, PositionMsg.Idle). A player that is paused or idle adds no listening time. The footer and MPRIS still show the pause state only. - The Player interface has the new method GetIdle. spruce gets the new field in its ticks and does not use it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The close of the terminal sends SIGHUP to the process. Go ends a process at SIGHUP if no code asked for the signal, and Bubble Tea asks only for SIGINT and SIGTERM. So the process ended in p.Run, and main did not call Cleanup: the last position and the listening time since the last sync were lost. - main now quits the program at SIGHUP. p.Run then returns the final model, and Cleanup sends the close of the session and the progress update. - SIGTERM needs no change: Bubble Tea quits the program at SIGTERM, and main calls Cleanup. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Thelost77
force-pushed
the
fix/playback-followups
branch
from
October 13, 2026 20:00
0ea2185 to
19629b1
Compare
- The keys n and N went to the next and the previous chapter during a playback. The chapter list (C) and the command palette do that job. - The config keys next_chapter and prev_chapter are gone. A config file that has them still loads: the two keys have no effect. - The command palette keeps "Next Chapter" and "Previous Chapter". The chapter tests use these actions, and one test checks that the palette offers them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The root model gave some keys a global function or a playback function before the screen got them. A guard held back these keys only while a list filter was active. So the bookmark title editor, the login screen, the metadata editor, and the confirmation dialog of the detail screen lost keys: q quit the program, ? opened the help overlay, a typed C had no effect, esc in the bookmark title editor went to the previous screen, and the playback keys did their function. - capturesKeys (before: isFiltering) is true in each of these cases. Update then gives the key to the screen, before the global keys, the chapter overlay, the error banner, and the playback keys. Before, the error banner took the first key, and the chapter overlay took j, k, H, L, and enter. - ? closes an open help overlay on each screen. - The hint line shows no global key and no playback key while the screen gets each key. - ctrl+p does not open the command palette while a text input or a dialog gets the keys. ctrl+c quits the program on each screen. Co-Authored-By: Claude Opus 5.5 <[email protected]>
- BookmarksUpdatedMsg had no item ID. The answer for an item can come after the user opened a different item. The detail screen then showed the bookmarks of the first item. enter on such a bookmark moved the second item to a time of the first item. - The message now has the ID of its item. The root model ignores the message if the detail screen shows a different item, as it does for the episodes and for the detail of a book. - The fault had no effect before the read of the bookmark route: the server gave no bookmarks. Co-Authored-By: Claude Opus 5.5 <[email protected]>
- For ABS before 2.36.0, each read of the bookmarks sent two requests: the bookmark route, which answers 404, and GET /api/me. Each read also wrote an error line for the 404 to the log. - The client now keeps the result of the first 404, and then reads GET /api/me directly. - ABS from 2.36.0 answers 404 only for an item that it does not have. After such a 404, the client also reads GET /api/me for the later reads. That response has the bookmarks of all items, so the result is correct. Co-Authored-By: Claude Opus 5.5 <[email protected]>
- TestHangupRunsCleanup called quitOnHangup, and then ran the program. main did the same in two lines of its own. So the test passed also when main did not call quitOnHangup. - runProgram now starts the handler for SIGHUP and runs the program. main and the child process of the test call runProgram. The test fails if runProgram has no handler. Co-Authored-By: Claude Opus 5.5 <[email protected]>
- b on the detail screen made a bookmark at the position of the player. handleAddBookmark did not compare the item of the detail screen with the item that plays. So b on a different item made a bookmark on that item, at a position of the item that plays. - handleAddBookmark now refuses the bookmark and shows the error that the command palette shows in this case. Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The bookmark title editor used the back keys of the detail screen to cancel the edit. These keys are esc and the left arrow. So the left arrow closed the editor, and the text was lost. - Only esc cancels the edit now. The left arrow moves the cursor. Co-Authored-By: Claude Opus 5.5 <[email protected]>
- SetPauseCmd, SetSpeedCmd, and SetVolumeCmd reported a failure of the player as a position tick with the generation 0. pine and spruce ignore a tick that is not of the current playback. So the app showed a speed that the player did not have, and showed no error. - The three commands now return CommandErrMsg. pine and spruce show its error in the error banner. - A player that is closed gets no banner: it has no state, and the next launch sets the speed and the volume. - The seek commands are as they were. After a seek that fails, the next position tick has the position of the player. Co-Authored-By: Claude Opus 5.5 <[email protected]>
- player.TickCmd and tickCmd of the app had the same body. They were different only in the interval. A change of the tick, such as the read of core-idle, had to go in the two functions. - player.TickAfterCmd has the body and gets the interval. TickCmd calls it with 500 ms. The app calls it for the longer wait while mpv has no position. tickCmd of the app is gone. - TestStalledPlayerAddsNoListeningTime had one case for each function. It now has one case. Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The comments of GetMediaProgress and MediaProgressWithBookmarks said that the progress has the bookmarks. ABS gives no bookmarks in that response. GetBookmarks reads them from the bookmark route. Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The main of spruce had no handler for SIGHUP. At the close of the terminal, the process ended in p.Run, and main did not call Cleanup. So spruce did not close its media cache and its players. - The function that runs the program and quits it at SIGHUP is now ui.RunProgram, in the shared package internal/ui. The main of pine and the main of spruce call it. - The test is now internal/ui/program_unix_test.go, with the name TestRunProgramReturnsAtHangup. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This was referenced Oct 13, 2026
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 named four suspected faults. Each fault is confirmed in the code of pine, and a test reproduces it. Each fault has its own commit. "ABS" below is Audiobookshelf 2.37.1. The statements about ABS come from its source in the Docker image
ghcr.io/advplyr/audiobookshelf:2.37.1.fetchBookmarksCmdininternal/app/bookmarks.goread the keybookmarksofGET /api/me/progress/{id}(MediaProgressWithBookmarksininternal/abs/types.go). ABS gives no such key in that response (getOldMediaProgressinserver/models/MediaProgress.js), and it answers 404 for an item with no progress (getMediaProgressinserver/controllers/MeController.js). pine read the 404 as "no bookmarks". So the detail screen showed "No bookmarks yet" in the two cases. A bookmark that the user added was in the list only until the user left the detail screen. The fake server of the E2E tests gave bookmarks with the progress, so the tests passed.Launchmakes a new mpv handle (launchininternal/player/mpv.go), andstartsets no speed option. Only the speed keys and the MPRIS rate calledSetSpeedCmd. So after the start of a session, and after a track rollover, mpv played at the speed 1.0. The keyspeedof the config file had no effect on the player.trackListeningTimeininternal/app/playback.gocounted the time of the clock while the player reported no pause. mpv reports no pause while it waits for data: the propertypausestays false,time-posdoes not move, and the propertiespaused-for-cacheandcore-idleare true.handleSignalsintea.go). So the process ended inp.Run, andmaindid not callCleanup. SIGTERM had no fault: Bubble Tea sendsQuitMsg,p.Runreturns the final model, andmaincallsCleanup.What changes
abs.Client.GetBookmarksreadsGET /api/me/bookmarks/{id}. The response is{"bookmarks": [...]}(getBookmarksForLibraryIteminMeController.js). ABS has this route from 2.36.0. An older ABS has no handler for the path and answers 404.GetBookmarksthen readsGET /api/meand keeps the bookmarks of the item.fetchBookmarksCmdcallsGetBookmarksof the cache client. Its cache key isbookmarks:{id}, which the bookmark commands already remove after each change. The special case for a 404 is gone.handlePlayerReadysendsSetSpeedCmdwith the speed of the model, as spruce does.Playerinterface has the new methodGetIdle.Mpv.GetIdlereads the mpv propertycore-idle. A position tick reads it into the new fieldPositionMsg.Idle. A tick of a player that is paused or idle adds no listening time. The footer and MPRIS still show the pause state only.maincalls the new functionui.RunProgram. At SIGHUP it callsp.Quit.p.Runthen returns the final model, andmaincallsCleanup.Decisions:
Player.Launch. The probe below shows that mpv accepts the speed before the file is open.core-idle, notpaused-for-cache.core-idleis also true while mpv starts or seeks, and in that time no audio plays. The mpv manual says aboutcore-idle: "it's only no/false if there's actually video playing". The probes below show the same for a file with audio only.ui.RunProgramusesp.Quit, so the end of the program is the end of a quit withq.How it was tested
Each fault has a test that failed before its correction.
TestGetBookmarksHTTP,TestGetBookmarksReadsUserBeforeABS236(internal/abs),TestE2E_BookmarkFetchOnDetailEntry(internal/app)bookmarks_test.go:41: expected 2 bookmarks, got 0ande2e_test.go:657: detail bookmarks = 0, want 2TestNewPlaybackGetsSpeedOfModel(internal/app): two sessionsplayback_test.go:324: speed of the player in the session sess-first = 1, want 1.5TestStalledPlayerAddsNoListeningTime(internal/app)progress_sync_test.go:808: timeListened = 1 after one second of a player that waits for data, want 0TestRunProgramReturnsAtHangup(internal/ui/program_unix_test.go). Its first name wasTestHangupRunsCleanupinmain_unix_test.go.main_unix_test.go:79: after SIGHUP, the process printed [] and ended with the error signal: hangup, want the line cleanup and no errorNotes on the tests:
TestGetBookmarksHTTPanswers as ABS does: the progress of the item has no bookmarks. The fake server of the E2E tests now has the bookmark route, and its progress route for a GET is gone. Three more E2E tests of the bookmarks failed with that server before the correction.TestClient_GetBookmarks_CacheHit(internal/cache) is new. The app did not use this cache path before, and the test checks it with a real store, also for an item with no bookmarks.Launchsets the speed 1.0, as a new mpv handle.player.TickAfterCmd, with a mock player that is idle and not paused.TestListeningTimeIsTimeOfPlayhas the new case "one minute of a player that waits for data".TestMpvIsIdleWhileItWaitsForData(internal/player) uses the real libmpv: its server sends 100 seconds of a file of 120 seconds, and then nothing. WithGetIdleon the propertypause, that test fails withpaused = false, idle = false at the position 99.6.maindoes, and prints a line at the place wheremaincallsCleanup. The test does not run on Windows.p.Runstarts, and the test sent the signal at the first line of the program. Inference: the signal came before that goroutine asked for it. The case is not in the test, because its result depends on that timing.Probes that are not in the repository:
ao=null, a stream that stops after 40 seconds of data:pause=false paused-for-cache=true core-idle=true, andtime-posstays at 39.936.core-idleis false while the file plays, and true after a pause.SetSpeed(1.5), and 1 after the nextLaunch.SetSpeed(8)directly afterLaunchgives the speed 8, whiletime-posis not available yet.pty), with an empty config folder, at the login screen. After the close of the pseudo-terminal, the program of279e34fended by SIGHUP, and its log has no linesession ended. The program of this branch ended with the exit code 0, and its log has the linesterminal closed, quittingandsession ended, 16 ms apart.Checks on macOS 27.0 (Apple Silicon), on the last commit (
19629b1):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: 28 packages passgo test -race ./... -count=1: 28 packages passNot tested
v2.35.1:server/routers/ApiRouter.jshas no route for the bookmarks, andserver/Server.jswith the default client has no handler for an unknown path under/api. The tagv2.36.0is the first tag whoseApiRouter.jshas the route.Cleanupto the server has the testsTestQuitWaitsForLastPositionandTestQuitSendsLastPositionOnce.TestE2E_CacheSurvivesRestartfailed on Windows withhome should not be loading after first init. This pull request does not change that test or the home screen. The test passed on Windows in the second run. Its cause was not examined. Inference: a command of the test needed more than the 200 ms thatfeedCmdwaits.Not changed
After the correction of fault 1, no code of the app calls
GetMediaProgress.GetMediaProgressofinternal/absand ofinternal/cache, the typeMediaProgressWithBookmarkswith its keybookmarks, the cache keyprogress:{id}, and their tests are still in the repository. Their removal is a separate change.Changes after the review
A review of this pull request found eight more faults. The commits below correct them. Three more commits remove two keys, remove a duplicate function, and correct two comments.
In the sections above, three names are new:
quitOnHangupis nowui.RunProgram, the test of fault 4 is nowTestRunProgramReturnsAtHangup, and the tick of the test of fault 3 comes fromplayer.TickAfterCmd.User-facing changes
nandN(next and previous chapter) are gone. The chapter list (C) and the command palette ("Next Chapter", "Previous Chapter") do that job.next_chapterandprev_chapterare gone. A config file that has them still loads, and the two keys have no effect. pine wrote the two keys to each config file that it made.bon the detail screen makes a bookmark only for the item that plays. For a different item, pine shows an error.ctrl+pdoes not open the command palette while a text input or a dialog gets the keys.Put these changes in the release notes.
What was wrong
Updateininternal/app/model.gogave some keys a global function or a playback function before the screen got them. A guard (isFiltering) held back these keys only while a list filter was active.qquit the program,?opened the help overlay,escwent to the previous screen, and the playback keys (n,l,h,p, space,S,+,-,[,],>) did their function. The correction of fault 1 made this editor usable: before, the list had no bookmarks from the server.?opened the help overlay, and a typedChad no effect.?opened the help overlay.qquit the program.j,k,H,L, andenter.BookmarksUpdatedMsghad no item ID. The answer for an item can come after the user opened a different item. Before the correction of fault 1, this fault had no effect: the server gave no bookmarks.abs.Clientwrote an error line for it to the log.maindid not start the handler for SIGHUP. The test calledquitOnHangupitself, andmaincalled it in a line of its own.bmade a bookmark on an item that did not play.handleAddBookmarkused the position of the player, and did not compare the item of the detail screen with the item that plays.escand the left arrow) to cancel the edit. The text was lost.SetPauseCmd,SetSpeedCmd, andSetVolumeCmdreported the failure as a position tick with the generation 0. pine and spruce ignore a tick that is not of the current playback.mainof spruce had no handler for SIGHUP, as themainof pine before fault 4. spruce did not close its media cache and its players.What changes
2aa3a4fnandNand their config keys are gone.b94a4a7capturesKeys(before:isFiltering) is true for the login screen, the metadata editor, a list filter, the bookmark title editor, and the confirmation dialog of the detail screen.Updatethen gives the key to the screen, before the global keys, the chapter overlay, the error banner, and the playback keys.?closes an open help overlay on each screen. The hint line shows no global key and no playback key while the screen gets each key.ctrl+cquits the program on each screen.bdaef5bEpisodesLoadedMsgandBookDetailLoadedMsg.da8bc75GET /api/medirectly. ABS from 2.36.0 answers 404 only for an item that it does not have. After such a 404, the client also readsGET /api/mefor the later reads. That response has the bookmarks of all items, so the result is correct.7f54726mainand the child process of the test call it.quitOnHangupis gone.b9c9388handleAddBookmarkrefuses the bookmark and shows the error that the command palette shows in this case.6ffd9efesccancels the edit. The left arrow moves the cursor.ab8d962player.CommandErrMsg. pine and spruce show its error in the error banner. A player that is closed gets no banner: it has no state, and the next launch sets the speed and the volume. The seek commands are as they were.a92ed6dplayer.TickCmdandtickCmdof the app had the same body.player.TickAfterCmdhas the body now, andtickCmdof the app is gone.c7aa399GetMediaProgressandMediaProgressWithBookmarkssaid that the progress has the bookmarks. They now say that it has none.1588e30ui.RunProgramin the shared packageinternal/ui. Themainof pine and themainof spruce call it.How it was tested
TestE2E_BookmarkTitleEditGetsEachKey,TestE2E_BookmarkTitleEditGetsKeysOfBannerAndChapterList,TestLoginCapturesGlobalKeys,TestMetadataEditorCapturesHelpKey,TestDetailConfirmCapturesQuitAndBackKeys,TestHelpKeyClosesHelpOnLoginScreen,TestHintsShowNoKeyThatTheScreenCaptures(internal/app)screen = Home, want Detail(escin the editor),server bookmark title = "Oldk", want Oldjk(error banner),server field = "", want "?Cq"(login screen),after ?, the help overlay is openTestE2E_DetailIgnoresBookmarksOfOtherItem(internal/app)detail of item-002 has the bookmarks [{... Title:Bookmark of the first item ...}], want item-002 and no bookmarksTestGetBookmarksReadsUserBeforeABS236(internal/abs)requests = map[/api/me:2 /api/me/bookmarks/li-001:2], want 1 for the bookmark route and 2 for the userTestRunProgramReturnsAtHangup(internal/ui)after SIGHUP, the process printed [] and ended with the error signal: hangupTestE2E_NoBookmarkForItemThatDoesNotPlay(internal/app)1 bookmark requests for an item that does not play, want 0: [{Method:POST Path:/api/me/item/item-002/bookmark Body:map[time:100 title:Bookmark at 1:40]}]TestLeftKey_MovesCursorWhileEditingBookmark(internal/screens/detail)after the left arrow, the bookmark editor is not openTestStateCommandsReportFailure(internal/player),TestPlayerCommandErrorShowsErrorBanner(internal/appandinternal/spruce/app)the command returned player.PositionMsg{...}, want CommandErrMsg with the error of the playerandthe error banner is not visibleTestRunProgramReturnsAtHangup(internal/ui)mainfunctions call. No test runs themainof spruce.Notes on the tests:
esccase ofTestDetailConfirmCapturesQuitAndBackKeysand thectrl+pcheck ofTestLoginCapturesGlobalKeyspassed before the correction. They cover behaviour that had no test.handleAddBookmarkhad a session and no item that plays. They now have the item.nandN: three chapter tests now use the palette actions.TestE2E_ChapterNavigationchecks that the palette offers the two actions.TestStalledPlayerAddsNoListeningTimehad one case for each of the two functions. It now has one case.Checks on Linux, on the last commit (
1588e30):gofmt -l .: no filego vet ./...: passgolangci-lint runwith version 2.14.0: 0 issuesPINE_REQUIRE_LIBMPV=1 PINE_REQUIRE_DBUS=1 go test ./... -count=1: 855 tests pass-race: passGOOS=windows go build ./...andGOOS=darwin go build ./...: passscripts/release.shfor the packages of each binary: passNot tested
Updateand read the view functions.mainof pine or themainof spruce: amainneeds a terminal. Amainthat callsp.Runand notui.RunProgramfails no test.7f54726,TestE2E_LoginFailurefailed on Windows withexpected login error after failed attempt, after 0.22 seconds. The job passed in the second run of the same commit. The test asks an address that has no server, andfeedCmdwaits 200 ms for the answer. Inference: the refusal came later than 200 ms. This pull request does not change that test.Not changed
SeekCmdandSeekRelativeCmdreport a failure as a position tick with the generation 0. After a seek that fails, the next position tick has the position of the player.🤖 Generated with Claude Code