[PR #5173] feat: Add native podcast episode bookmarking #4453

Open
opened 2026-04-25 00:50:48 +02:00 by adam · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/advplyr/audiobookshelf/pull/5173
Author: @johneliott
Created: 4/8/2026
Status: 🔄 Open

Base: masterHead: feature/podcast-bookmarks


📝 Commits (1)

  • ca6c7d7 feat: Add native podcast episode bookmarking

📊 Changes

7 files changed (+54 additions, -29 deletions)

View changed files

📝 client/components/app/MediaPlayerContainer.vue (+2 -2)
📝 client/components/modals/BookmarksModal.vue (+4 -1)
📝 client/components/player/PlayerUi.vue (+1 -1)
📝 client/store/user.js (+9 -4)
📝 server/controllers/MeController.js (+7 -6)
📝 server/models/User.js (+20 -9)
📝 test/server/controllers/MeController.test.js (+11 -6)

📄 Description

Resolves #884

Changes

1. Backend Database Model (server/models/User.js)

  • Modified the createBookmark, updateBookmark, removeBookmark, and findBookmark methods to accept an optional episodeId parameter.
  • Instead of migrating every user's old JSON blob or making a massive breaking schema change, the system now simply attaches episodeId to the JSON payload for new podcast bookmarks while seamlessly falling back to standard libraryItemId behavior for audiobooks (or legacy data).

2. Backend API Endpoints (server/controllers/MeController.js)

  • Updated POST /api/me/item/:id/bookmark and PATCH /api/me/item/:id/bookmark to parse episodeId directly from the req.body.
  • Updated DELETE /api/me/item/:id/bookmark/:time to accept an episodeId via a query parameter (req.query.episode).

3. Frontend Vuex Store (client/store/user.js)

  • Updated the global state getter getUserBookmarksForItem to take an optional episodeId. If provided, it filters out all bookmarks that belong to different episodes, ensuring you only see the bookmarks for the episode you are currently listening to.

4. Frontend UI Components (client/components/...)

  • PlayerUi.vue: Removed the !isPodcast block that artificially hid the bookmark icon when playing a podcast.
  • MediaPlayerContainer.vue: Passed the active $store.state.streamEpisodeId down into the computed bookmarks() list and into the <modals-bookmarks-modal> prop.
  • BookmarksModal.vue: Added the episodeId prop. When creating a new bookmark, it injects the episodeId into the POST body. When deleting, it intelligently appends ?episode=${this.episodeId} to the DELETE URL.

Automated Test Plan: Podcast Episode Bookmarks

Prerequisites

  • A running instance of Audiobookshelf (e.g., http://localhost:13380).
  • A valid user authentication token (<AUTH_TOKEN>).
  • A Library Item ID representing a podcast (bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2).
  • An Episode ID belonging to that podcast (09690803-ae60-4b25-8f7d-f2bcccd6bb83).

Step 1: Create a Bookmark

Command:

curl -s -X POST \
  -H "Authorization: Bearer <AUTH_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"time": 420, "title": "Ziva Cooper talking about THC receptors", "episodeId": "09690803-ae60-4b25-8f7d-f2bcccd6bb83"}' \
  http://localhost:13380/api/me/item/bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2/bookmark

Response:

{
  "libraryItemId": "bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2",
  "time": 420,
  "title": "Ziva Cooper talking about THC receptors",
  "createdAt": 1775677075715,
  "episodeId": "09690803-ae60-4b25-8f7d-f2bcccd6bb83"
}

Step 2: Verify the Bookmark (Read)

Command:

curl -s -H "Authorization: Bearer <AUTH_TOKEN>" http://localhost:13380/api/me | grep "Ziva Cooper talking about THC receptors"

Response:

{
  "libraryItemId": "bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2",
  "time": 420,
  "title": "Ziva Cooper talking about THC receptors",
  "createdAt": 1775677075715,
  "episodeId": "09690803-ae60-4b25-8f7d-f2bcccd6bb83"
}

Step 3: Update the Bookmark

Command:

curl -s -X PATCH \
  -H "Authorization: Bearer <AUTH_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"time": 420, "title": "UPDATED: Ziva Cooper on THC vs CBD receptors", "episodeId": "09690803-ae60-4b25-8f7d-f2bcccd6bb83"}' \
  http://localhost:13380/api/me/item/bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2/bookmark

Response:

{
  "libraryItemId": "bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2",
  "time": 420,
  "title": "UPDATED: Ziva Cooper on THC vs CBD receptors",
  "createdAt": 1775677075715,
  "episodeId": "09690803-ae60-4b25-8f7d-f2bcccd6bb83"
}

Step 4: Delete the Bookmark

Command:

curl -s -X DELETE \
  -H "Authorization: Bearer <AUTH_TOKEN>" \
  "http://localhost:13380/api/me/item/bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2/bookmark/420?episode=09690803-ae60-4b25-8f7d-f2bcccd6bb83"

Response:

OK

Step 5: Validate Error & Warning Logging

This step ensures that the server correctly validates input and logs issues as expected.

5.1 Trigger "Invalid Time" Error

  • Command: curl -s -X POST -H "Authorization: Bearer <AUTH_TOKEN>" -H "Content-Type: application/json" -d '{"title": "Missing Time"}' http://localhost:13380/api/me/item/bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2/bookmark
  • Response: ERROR: [MeController] createBookmark invalid time undefined

5.2 Trigger "Invalid Title" Error

  • Command: curl -s -X POST -H "Authorization: Bearer <AUTH_TOKEN>" -H "Content-Type: application/json" -d '{"time": 123}' http://localhost:13380/api/me/item/bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2/bookmark
  • Response: ERROR: [MeController] createBookmark invalid title undefined

5.3 Trigger "Already Exists" Warning

  • Command: curl -s -X POST ... (Duplicate attempt)
  • Response: WARN: [User] Create Bookmark already exists for this time

5.4 Trigger "Update Not Found" Error

  • Command: curl -s -X PATCH ... (Time 9999)
  • Response: ERROR: [User] updateBookmark not found

5.5 Trigger "Remove Not Found" Error

  • Command: curl -s -X DELETE ... (Time 9999)
  • Response: ERROR: [MeController] removeBookmark not found

Manual UI Verification Plan

1. Preparation
Ensure the Audiobookshelf server is running and you are logged in to the web interface.

2. Verify UI Visibility

  • Navigate to a Podcast: Open any podcast series in your library.
  • Start Playback: Click play on any episode.
  • Check the Player: Look at the player controls (usually at the bottom or in the full-screen player).
  • Result: The Bookmark icon (looks like a bookmark ribbon) should now be visible. Previously, this was hidden for all podcasts.

3. Test Bookmark Creation

  • Seek to a timestamp: Move the playhead to a specific spot (e.g., 01:23).
  • Click the Bookmark icon: A modal should appear.
  • Save the Bookmark: Enter a title (or use the default) and click save.
  • Result: The bookmark should be saved. You can verify this by clicking the Bookmark icon again; your saved bookmark should appear in the list.

4. Test Episode Isolation (The Core Fix)
This verifies that bookmarks are now tied to specific episodes rather than the whole podcast series:

  • Switch Episodes: Stop playing Episode A and start playing Episode B from the same podcast.
  • Open Bookmarks: Click the Bookmark icon in the player.
  • Result: The list should be empty (or only show bookmarks created for Episode B). You should not see the bookmark you just created for Episode A.
  • Switch Back: Go back to Episode A.
  • Result: Your original bookmark at 01:23 should reappear.

5. Test Deletion

  • Delete the bookmark: Open the bookmarks list for Episode A and click the "Delete" (trash can) icon.
  • Result: The bookmark should be removed successfully.
Screenshot 2026-04-08 at 12 29 32 PM Screenshot 2026-04-08 at 12 30 13 PM Screenshot 2026-04-08 at 12 30 44 PM Screenshot 2026-04-08 at 12 31 59 PM Screenshot 2026-04-08 at 12 32 17 PM Screenshot 2026-04-08 at 12 32 46 PM Screenshot 2026-04-08 at 12 32 58 PM Screenshot 2026-04-08 at 12 33 18 PM Screenshot 2026-04-08 at 12 33 29 PM Screenshot 2026-04-08 at 12 35 07 PM Screenshot 2026-04-08 at 12 35 26 PM Screenshot 2026-04-08 at 12 35 45 PM

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/advplyr/audiobookshelf/pull/5173 **Author:** [@johneliott](https://github.com/johneliott) **Created:** 4/8/2026 **Status:** 🔄 Open **Base:** `master` ← **Head:** `feature/podcast-bookmarks` --- ### 📝 Commits (1) - [`ca6c7d7`](https://github.com/advplyr/audiobookshelf/commit/ca6c7d7958a5b83f95fdb18a60cdf7fe60279916) feat: Add native podcast episode bookmarking ### 📊 Changes **7 files changed** (+54 additions, -29 deletions) <details> <summary>View changed files</summary> 📝 `client/components/app/MediaPlayerContainer.vue` (+2 -2) 📝 `client/components/modals/BookmarksModal.vue` (+4 -1) 📝 `client/components/player/PlayerUi.vue` (+1 -1) 📝 `client/store/user.js` (+9 -4) 📝 `server/controllers/MeController.js` (+7 -6) 📝 `server/models/User.js` (+20 -9) 📝 `test/server/controllers/MeController.test.js` (+11 -6) </details> ### 📄 Description Resolves #884 ### Changes **1. Backend Database Model (`server/models/User.js`)** - Modified the `createBookmark`, `updateBookmark`, `removeBookmark`, and `findBookmark` methods to accept an optional `episodeId` parameter. - Instead of migrating every user's old JSON blob or making a massive breaking schema change, the system now simply attaches `episodeId` to the JSON payload for new podcast bookmarks while seamlessly falling back to standard `libraryItemId` behavior for audiobooks (or legacy data). **2. Backend API Endpoints (`server/controllers/MeController.js`)** - Updated `POST /api/me/item/:id/bookmark` and `PATCH /api/me/item/:id/bookmark` to parse `episodeId` directly from the `req.body`. - Updated `DELETE /api/me/item/:id/bookmark/:time` to accept an `episodeId` via a query parameter (`req.query.episode`). **3. Frontend Vuex Store (`client/store/user.js`)** - Updated the global state getter `getUserBookmarksForItem` to take an optional `episodeId`. If provided, it filters out all bookmarks that belong to different episodes, ensuring you only see the bookmarks for the episode you are currently listening to. **4. Frontend UI Components (`client/components/...`)** - **`PlayerUi.vue`:** Removed the `!isPodcast` block that artificially hid the bookmark icon when playing a podcast. - **`MediaPlayerContainer.vue`:** Passed the active `$store.state.streamEpisodeId` down into the computed `bookmarks()` list and into the `<modals-bookmarks-modal>` prop. - **`BookmarksModal.vue`:** Added the `episodeId` prop. When creating a new bookmark, it injects the `episodeId` into the `POST` body. When deleting, it intelligently appends `?episode=${this.episodeId}` to the `DELETE` URL. --- ### Automated Test Plan: Podcast Episode Bookmarks #### Prerequisites - A running instance of Audiobookshelf (e.g., `http://localhost:13380`). - A valid user authentication token (`<AUTH_TOKEN>`). - A Library Item ID representing a podcast (`bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2`). - An Episode ID belonging to that podcast (`09690803-ae60-4b25-8f7d-f2bcccd6bb83`). #### Step 1: Create a Bookmark **Command:** ```bash curl -s -X POST \ -H "Authorization: Bearer <AUTH_TOKEN>" \ -H "Content-Type: application/json" \ -d '{"time": 420, "title": "Ziva Cooper talking about THC receptors", "episodeId": "09690803-ae60-4b25-8f7d-f2bcccd6bb83"}' \ http://localhost:13380/api/me/item/bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2/bookmark ``` **Response:** ```json { "libraryItemId": "bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2", "time": 420, "title": "Ziva Cooper talking about THC receptors", "createdAt": 1775677075715, "episodeId": "09690803-ae60-4b25-8f7d-f2bcccd6bb83" } ``` #### Step 2: Verify the Bookmark (Read) **Command:** ```bash curl -s -H "Authorization: Bearer <AUTH_TOKEN>" http://localhost:13380/api/me | grep "Ziva Cooper talking about THC receptors" ``` **Response:** ```json { "libraryItemId": "bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2", "time": 420, "title": "Ziva Cooper talking about THC receptors", "createdAt": 1775677075715, "episodeId": "09690803-ae60-4b25-8f7d-f2bcccd6bb83" } ``` #### Step 3: Update the Bookmark **Command:** ```bash curl -s -X PATCH \ -H "Authorization: Bearer <AUTH_TOKEN>" \ -H "Content-Type: application/json" \ -d '{"time": 420, "title": "UPDATED: Ziva Cooper on THC vs CBD receptors", "episodeId": "09690803-ae60-4b25-8f7d-f2bcccd6bb83"}' \ http://localhost:13380/api/me/item/bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2/bookmark ``` **Response:** ```json { "libraryItemId": "bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2", "time": 420, "title": "UPDATED: Ziva Cooper on THC vs CBD receptors", "createdAt": 1775677075715, "episodeId": "09690803-ae60-4b25-8f7d-f2bcccd6bb83" } ``` #### Step 4: Delete the Bookmark **Command:** ```bash curl -s -X DELETE \ -H "Authorization: Bearer <AUTH_TOKEN>" \ "http://localhost:13380/api/me/item/bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2/bookmark/420?episode=09690803-ae60-4b25-8f7d-f2bcccd6bb83" ``` **Response:** ```text OK ``` #### Step 5: Validate Error & Warning Logging This step ensures that the server correctly validates input and logs issues as expected. **5.1 Trigger "Invalid Time" Error** - **Command:** `curl -s -X POST -H "Authorization: Bearer <AUTH_TOKEN>" -H "Content-Type: application/json" -d '{"title": "Missing Time"}' http://localhost:13380/api/me/item/bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2/bookmark` - **Response:** `ERROR: [MeController] createBookmark invalid time undefined` **5.2 Trigger "Invalid Title" Error** - **Command:** `curl -s -X POST -H "Authorization: Bearer <AUTH_TOKEN>" -H "Content-Type: application/json" -d '{"time": 123}' http://localhost:13380/api/me/item/bc57c7f0-08dc-4b7e-b570-2268dd7d6cc2/bookmark` - **Response:** `ERROR: [MeController] createBookmark invalid title undefined` **5.3 Trigger "Already Exists" Warning** - **Command:** `curl -s -X POST ...` (Duplicate attempt) - **Response:** `WARN: [User] Create Bookmark already exists for this time` **5.4 Trigger "Update Not Found" Error** - **Command:** `curl -s -X PATCH ...` (Time 9999) - **Response:** `ERROR: [User] updateBookmark not found` **5.5 Trigger "Remove Not Found" Error** - **Command:** `curl -s -X DELETE ...` (Time 9999) - **Response:** `ERROR: [MeController] removeBookmark not found` --- ### Manual UI Verification Plan **1. Preparation** Ensure the Audiobookshelf server is running and you are logged in to the web interface. **2. Verify UI Visibility** * **Navigate to a Podcast:** Open any podcast series in your library. * **Start Playback:** Click play on any episode. * **Check the Player:** Look at the player controls (usually at the bottom or in the full-screen player). * **Result:** The Bookmark icon (looks like a bookmark ribbon) should now be visible. Previously, this was hidden for all podcasts. **3. Test Bookmark Creation** * **Seek to a timestamp:** Move the playhead to a specific spot (e.g., 01:23). * **Click the Bookmark icon:** A modal should appear. * **Save the Bookmark:** Enter a title (or use the default) and click save. * **Result:** The bookmark should be saved. You can verify this by clicking the Bookmark icon again; your saved bookmark should appear in the list. **4. Test Episode Isolation (The Core Fix)** This verifies that bookmarks are now tied to specific episodes rather than the whole podcast series: * **Switch Episodes:** Stop playing Episode A and start playing Episode B from the same podcast. * **Open Bookmarks:** Click the Bookmark icon in the player. * **Result:** The list should be empty (or only show bookmarks created for Episode B). You should not see the bookmark you just created for Episode A. * **Switch Back:** Go back to Episode A. * **Result:** Your original bookmark at 01:23 should reappear. **5. Test Deletion** * **Delete the bookmark:** Open the bookmarks list for Episode A and click the "Delete" (trash can) icon. * **Result:** The bookmark should be removed successfully. <img width="1448" height="909" alt="Screenshot 2026-04-08 at 12 29 32 PM" src="https://github.com/user-attachments/assets/4f8240cf-86de-4783-b3a7-95d0ef3a708e" /> <img width="1448" height="913" alt="Screenshot 2026-04-08 at 12 30 13 PM" src="https://github.com/user-attachments/assets/8e66d12e-80d8-41f0-adcc-bef5969c756b" /> <img width="1441" height="854" alt="Screenshot 2026-04-08 at 12 30 44 PM" src="https://github.com/user-attachments/assets/800e1850-0905-45fe-871d-d6fa0629d1f7" /> <img width="1451" height="908" alt="Screenshot 2026-04-08 at 12 31 59 PM" src="https://github.com/user-attachments/assets/6763d591-f1c8-4a50-b888-d831f1897379" /> <img width="1446" height="911" alt="Screenshot 2026-04-08 at 12 32 17 PM" src="https://github.com/user-attachments/assets/5e43157a-609e-47ea-9367-4fbc5efcae9c" /> <img width="1448" height="911" alt="Screenshot 2026-04-08 at 12 32 46 PM" src="https://github.com/user-attachments/assets/1d33c01b-b59c-46c0-a6e1-9e9231d5bca4" /> <img width="1446" height="905" alt="Screenshot 2026-04-08 at 12 32 58 PM" src="https://github.com/user-attachments/assets/99af25f6-4cc5-40a7-85f4-c9b40b6e183c" /> <img width="1446" height="908" alt="Screenshot 2026-04-08 at 12 33 18 PM" src="https://github.com/user-attachments/assets/366f7957-cf6b-4c9f-a5fe-ed68b0e8b8ed" /> <img width="1448" height="909" alt="Screenshot 2026-04-08 at 12 33 29 PM" src="https://github.com/user-attachments/assets/4a5b4788-c37f-4666-8520-1cc0b01ebe25" /> <img width="1448" height="907" alt="Screenshot 2026-04-08 at 12 35 07 PM" src="https://github.com/user-attachments/assets/29b1e783-18a6-4f90-9bab-9549e4f3700e" /> <img width="1445" height="907" alt="Screenshot 2026-04-08 at 12 35 26 PM" src="https://github.com/user-attachments/assets/de4a1d2b-d083-475f-9148-f42471b35aad" /> <img width="1442" height="907" alt="Screenshot 2026-04-08 at 12 35 45 PM" src="https://github.com/user-attachments/assets/830dce3d-454b-4e84-9ec2-9e3663c2605b" /> --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
adam added the pull-request label 2026-04-25 00:50:48 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/audiobookshelf#4453