Skip to content

[Enhancement]: Add dedicated media progress endpoint filtered by library item ID #5276

Open

Description

@GrakovNe

Type of Enhancement

Server Backend

Describe the Feature/Enhancement

Type of Enhancement

Server Backend / API

Describe the feature/enhancement

Please add a dedicated endpoint for fetching media progress for a specific library item, for example:

GET /api/progress?libraryItemId=<libraryItemId>

Currently, clients need to call GET /api/me, read the full authenticated user payload, and then filter mediaProgress locally for a single item. The API docs show that /api/me includes the full user object with mediaProgress, bookmarks, permissions, and other user/server-related data, so this is much heavier than necessary for item-specific progress loading. [page:1]

Current behavior

Client code currently has to do something like this:

val progress =
  dataRepository
    .fetchUserInfoResponse()
    .fold(
      onSuccess = { it.mediaProgress ?: emptyList() },
      onFailure = { emptyList() },
    )

if (progress.isEmpty()) {
  return@async null
}

progress
  .filter { it.libraryItemId == bookId }
  .filterNot { it.episodeId == null }
  .sortedByDescending { it.lastUpdate }
  .distinctBy { it.episodeId }

Under the hood this goes through:

@GET("api/me")
suspend fun fetchUserInfo(): Response<UserResponse>

So when opening a single item page, the client has to fetch all user progress entries and filter them locally for just one libraryItemId.

Problem

This is inefficient because:

  • the payload is much larger than needed for a single item;
  • client performance gets worse as mediaProgress grows;
  • item-specific UI depends on a general-purpose user info endpoint;
  • the client has to do extra filtering, sorting, and deduplication work after downloading unrelated data.

This is especially noticeable for users with large listening histories or many podcast episode progress entries.

Proposed API

A dedicated endpoint such as:

GET /api/progress?libraryItemId=<libraryItemId>

Possible behavior:

  • when libraryItemId is provided, return only progress entries for that item;
  • optionally support additional filtering such as episodeOnly=1 for podcast/item detail use cases;
  • optionally, without query params, return all progress entries for the authenticated user if that is useful for backward-compatible generic usage.

Example response:

{
  "mediaProgress": [
    {
      "id": "progress_id",
      "libraryItemId": "item_id",
      "episodeId": "episode_id",
      "duration": 1454.18449,
      "progress": 0.434998929881311,
      "currentTime": 632.568697,
      "isFinished": false,
      "hideFromContinueListening": false,
      "lastUpdate": 1668586015691,
      "startedAt": 1668120083771,
      "finishedAt": null
    }
  ]
}

Alternative

An item-scoped route would also work well, for example:

GET /api/items/{libraryItemId}/progress

That may be more REST-friendly, especially since the requested data is tied to a single library item.

Prior art in the API

Audiobookshelf already has a more specific progress-related endpoint, GET /api/me/items-in-progress, which suggests that splitting progress use cases out from /api/me is already an accepted API direction. [page:2]

Use case

In mobile clients, item detail screens may need only the progress entries for one book or podcast, often to show episode-level listening state sorted by most recent activity. Fetching the full /api/me response just to derive that subset is unnecessarily heavy.

Why would this be helpful?

  • reduces payload size for item detail requests;
  • improves load time for clients that only need progress for one item;
  • avoids coupling item progress UI to /api/me;
  • provides a clearer API contract for progress as a resource.

Future Implementation (Screenshot)

no UI

Audiobookshelf Server Version

v2.35.1

Current Implementation (Screenshot)

No response

Activity

  1. Vito0912 commented on May 30, 2026

    @Vito0912
    Contributor

    I am not sure if I understand your issue correctly, but that is well doable and also stated in the not so up to date API docs:

    https://api.audiobookshelf.org/#get-a-media-progress

    If you mean to get the progress for all users for a library item that is another topic, but as yous state

    Currently, clients need to call GET /api/me, read the full authenticated user payload, and then filter mediaProgress locally for a single item.

    This does not to seem your intent

  2. GrakovNe commented on May 30, 2026

    @GrakovNe
    ContributorAuthor

    The problem is with podcasts.

    If I call:
    GET /api/me/progress/{libraryItemId}

    I only get a single media progress entry, for example:

    {
      "id": "ee1d6453-835e-4e56-8029-19dade1d32b2",
      "userId": "f6891cb5-3527-46fb-bdb3-d690e279382d",
      "libraryItemId": "dc4d3bfb-b952-4154-a52f-aecde9ba4292",
      "episodeId": "0025b1fe-9645-402f-ba8e-1c57aa2ab769",
      "mediaItemId": "0025b1fe-9645-402f-ba8e-1c57aa2ab769",
      "mediaItemType": "podcastEpisode",
      "duration": 77.208,
      "progress": 0.0000129520,
      "currentTime": 0.001,
      "isFinished": false,
      "hideFromContinueListening": false,
      "ebookLocation": null,
      "ebookProgress": 0,
      "lastUpdate": 1780147091360,
      "startedAt": 1780147091360,
      "finishedAt": null
    }

    But if I call:
    GET /api/me

    I can see that the latest saved progress for the same libraryItemId is actually a different episode:

    {
      "id": "cfef04de-3ace-4387-a0ca-f78bb41b260c",
      "userId": "f6891cb5-3527-46fb-bdb3-d690e279382d",
      "libraryItemId": "dc4d3bfb-b952-4154-a52f-aecde9ba4292",
      "episodeId": "475fb32b-a3e2-4072-91a9-c0eb3720bfe5",
      "mediaItemId": "475fb32b-a3e2-4072-91a9-c0eb3720bfe5",
      "mediaItemType": "podcastEpisode",
      "duration": 1545.264,
      "progress": 0.31196049865912884,
      "currentTime": 482.061328,
      "isFinished": false,
      "hideFromContinueListening": false,
      "ebookLocation": null,
      "ebookProgress": 0,
      "lastUpdate": 1780153963627,
      "startedAt": 1772461678448,
      "finishedAt": null
    }

    Lissen does not show podcast episodes as separate sub-items with their own detail pages. Instead, it shows them as a continuous playback queue so the user can listen through the podcast sequentially.

    Because of this, I cannot afford to call the progress endpoint for every single episode – that would turn into an N+1 problem.

    Instead, right now I fetch the entire user progress history once via /api/me and then filter mediaProgress by the libraryItemId I currently need.

    That is why I want a dedicated endpoint that returns all progress entries for the authenticated user for a single libraryItemId (especially for podcasts), so I can avoid both downloading the full /api/me payload and doing one request per episode.

  3. Quaiyyum commented on Sep 29, 2026

    @Quaiyyum

    I'd like to work on this.

    The fix is straightforward: add an optional ?libraryItemId= query param to the existing GET /api/me/progress endpoint in MeController.getAllMediaProgress(). When the param is provided, filter req.user.mediaProgresses by mp.extraData?.libraryItemId before mapping to the old format. No new route needed — the existing endpoint stays backward-compatible when called without the param.

    Working on a PR now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions