[PR #5144] Add backend support for users to follow series #4445

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

📋 Pull Request Information

Original PR: https://github.com/advplyr/audiobookshelf/pull/5144
Author: @pdevito3
Created: 3/23/2026
Status: 🔄 Open

Base: masterHead: feature/follow-series-backend


📝 Commits (1)

  • 37b9558 Add backend support for users to follow series

📊 Changes

9 files changed (+744 additions, -5 deletions)

View changed files

📝 server/Database.js (+6 -0)
📝 server/controllers/MeController.js (+82 -0)
server/migrations/v2.34.0-create-user-series-follows.js (+110 -0)
📝 server/models/User.js (+17 -5)
server/models/UserSeriesFollow.js (+84 -0)
📝 server/routers/ApiRouter.js (+3 -0)
📝 server/scanner/LibraryScanner.js (+3 -0)
server/utils/followNotifications.js (+50 -0)
test/server/controllers/MeController.follows.test.js (+389 -0)

📄 Description

Brief summary

Adds backend-only support for users to follow book series and receive real-time socket notifications when new books are added to followed series. Three new API endpoints under /api/me/follows/ enable clients to manage follow state, and the user JSON response now includes a seriesFollowing array on login.

For context as to why users might want this, one example I have in my client is to lookup upcoming books in series a user follows. If you'd like I can expand this to include that, but I wanted to keep it simple for now as it could could get very client specific.

Note

This PR is intentionally backend-only. No UI changes are included. I saw y'all are actively working on a v2 UI, so rather than targeting the current frontend (which would conflict with that effort), this lays the groundwork for clients to start leveraging the feature now via the API. A follow-up PR can integrate the UI once the v2 frontend is ready to accept changes. Personally, I have a client I use that I'd love to have leverage this ASAP.

Which issue is fixed?

N/A — new feature. Enables client apps to sync followed series across devices instead of relying on local-only storage.

In-depth Description

Data model: A new UserSeriesFollow model backed by a dedicated userSeriesFollows join table (userId, seriesId, createdAt), following the existing BookSeries join table pattern used throughout the codebase. Indexes on userId (primary query path for "get all series a user follows"), UNIQUE(userId, seriesId) (prevents duplicates), and seriesId (for follower lookups during notifications). CASCADE deletes on both foreign keys ensure automatic cleanup when a user or series is removed.

API endpoints:

  • POST /api/me/follows/series/:id — follow a series (idempotent)
  • DELETE /api/me/follows/series/:id — unfollow a series
  • GET /api/me/follows?type=series — list followed series with name, libraryId, and timestamps

User JSON: toOldJSONForBrowser() now includes seriesFollowing: string[] so clients have follow state immediately on auth without a separate API call.

Socket events:

  • user_series_follows_updated — sent to the user on follow/unfollow with the updated list

  • followed_series_book_added — sent to all followers when the scanner detects a new book in a followed series (hooked into both the file watcher and batch scan paths in LibraryScanner)

    Cache: User cache is invalidated on follow/unfollow via seriesFollowChanged() to ensure fresh data on next fetch.

This approach was chosen over a polymorphic userFollows table (with followableId/followableType) because the codebase predominantly uses dedicated join tables (BookSeries, BookAuthor, CollectionBook), and the dedicated table provides real FK constraints with CASCADE deletes. If author follows are desired in the future, the pattern replicates trivially, but if you'd like me to refactor to this pattern that's totally fine, just let me know.

How have you tested this?

17 new tests added in test/server/controllers/MeController.follows.test.js using the existing pattern:

  • Follow a series returns 200, creates a record, and is idempotent (second follow still returns 200, only one record created)
  • Follow returns 404 for non-existent series
  • Unfollow returns 200 and removes the record
  • Unfollow returns 404 when not following
  • Both follow and unfollow emit user_series_follows_updated socket event with correct payload
  • getFollows returns all followed series for the requesting user with series name and libraryId
  • getFollows does not leak follows from other users
  • getFollows returns empty array when user has no follows
  • toOldJSONForBrowser() includes seriesFollowing array with correct series IDs
  • seriesFollowing is an empty array when user has no follows
  • CASCADE: deleting a series removes all associated follow records
  • CASCADE: deleting a user removes all associated follow records
  • getFollowedSeriesIdsForUser returns correct series ID array and empty array when none

Screenshots

N/A


🔄 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/5144 **Author:** [@pdevito3](https://github.com/pdevito3) **Created:** 3/23/2026 **Status:** 🔄 Open **Base:** `master` ← **Head:** `feature/follow-series-backend` --- ### 📝 Commits (1) - [`37b9558`](https://github.com/advplyr/audiobookshelf/commit/37b95582a21c573a618c5bf95e542e4db83555d7) Add backend support for users to follow series ### 📊 Changes **9 files changed** (+744 additions, -5 deletions) <details> <summary>View changed files</summary> 📝 `server/Database.js` (+6 -0) 📝 `server/controllers/MeController.js` (+82 -0) ➕ `server/migrations/v2.34.0-create-user-series-follows.js` (+110 -0) 📝 `server/models/User.js` (+17 -5) ➕ `server/models/UserSeriesFollow.js` (+84 -0) 📝 `server/routers/ApiRouter.js` (+3 -0) 📝 `server/scanner/LibraryScanner.js` (+3 -0) ➕ `server/utils/followNotifications.js` (+50 -0) ➕ `test/server/controllers/MeController.follows.test.js` (+389 -0) </details> ### 📄 Description ## Brief summary Adds backend-only support for users to follow book series and receive real-time socket notifications when new books are added to followed series. Three new API endpoints under `/api/me/follows/` enable clients to manage follow state, and the user JSON response now includes a seriesFollowing array on login. For context as to why users might want this, one example I have in my client is to lookup upcoming books in series a user follows. If you'd like I can expand this to include that, but I wanted to keep it simple for now as it could could get very client specific. > [!NOTE] > This PR is intentionally backend-only. No UI changes are included. I saw y'all are actively working on a v2 UI, so rather than targeting the current frontend (which would conflict with that effort), this lays the groundwork for clients to start leveraging the feature now via the API. A follow-up PR can integrate the UI once the v2 frontend is ready to accept changes. Personally, I have a client I use that I'd love to have leverage this ASAP. ## Which issue is fixed? N/A — new feature. Enables client apps to sync followed series across devices instead of relying on local-only storage. ## In-depth Description Data model: A new `UserSeriesFollow` model backed by a dedicated `userSeriesFollows` join table (userId, seriesId, createdAt), following the existing BookSeries join table pattern used throughout the codebase. Indexes on userId (primary query path for "get all series a user follows"), UNIQUE(userId, seriesId) (prevents duplicates), and seriesId (for follower lookups during notifications). CASCADE deletes on both foreign keys ensure automatic cleanup when a user or series is removed. ### API endpoints: - POST `/api/me/follows/series/:id` — follow a series (idempotent) - DELETE `/api/me/follows/series/:id` — unfollow a series - GET `/api/me/follows?type=series` — list followed series with name, libraryId, and timestamps User JSON: `toOldJSONForBrowser()` now includes `seriesFollowing: string[]` so clients have follow state immediately on auth without a separate API call. ### Socket events: - `user_series_follows_updated` — sent to the user on follow/unfollow with the updated list - `followed_series_book_added` — sent to all followers when the scanner detects a new book in a followed series (hooked into both the file watcher and batch scan paths in LibraryScanner) ### Cache: User cache is invalidated on follow/unfollow via seriesFollowChanged() to ensure fresh data on next fetch. This approach was chosen over a polymorphic userFollows table (with followableId/followableType) because the codebase predominantly uses dedicated join tables (BookSeries, BookAuthor, CollectionBook), and the dedicated table provides real FK constraints with CASCADE deletes. If author follows are desired in the future, the pattern replicates trivially, but if you'd like me to refactor to this pattern that's totally fine, just let me know. ## How have you tested this? 17 new tests added in test/server/controllers/MeController.follows.test.js using the existing pattern: - Follow a series returns 200, creates a record, and is idempotent (second follow still returns 200, only one record created) - Follow returns 404 for non-existent series - Unfollow returns 200 and removes the record - Unfollow returns 404 when not following - Both follow and unfollow emit user_series_follows_updated socket event with correct payload - getFollows returns all followed series for the requesting user with series name and libraryId - getFollows does not leak follows from other users - getFollows returns empty array when user has no follows - toOldJSONForBrowser() includes seriesFollowing array with correct series IDs - seriesFollowing is an empty array when user has no follows - CASCADE: deleting a series removes all associated follow records - CASCADE: deleting a user removes all associated follow records - getFollowedSeriesIdsForUser returns correct series ID array and empty array when none ## Screenshots N/A --- <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:19:46 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/audiobookshelf#4445