Skip to content

[DRAFT] feat: API#924

Merged
FuzzyGrim merged 87 commits intoFuzzyGrim:feat/add-apifrom
66Bunz:feat/add-api
Apr 18, 2026
Merged

[DRAFT] feat: API#924
FuzzyGrim merged 87 commits intoFuzzyGrim:feat/add-apifrom
66Bunz:feat/add-api

Conversation

@66Bunz
Copy link
Copy Markdown
Contributor

@66Bunz 66Bunz commented Oct 21, 2025

API Implementation

Roadmap

The roadmap I have in mind for this project is this:

  • develop API based on current app implementations (this PR)

  • move all implementations from the app to the API making them more "agnostic" (not so view driven)

  • change app to use API backend manipulating its data

  • since API is available, more "plugins", "integrations", "providers" can be implemented between various services (Jellyfin, seer, Trakt, ecc ecc)


Fixes #488
Fixes #543
Fixes #582
Fixes #698

To-Dos

  • Basic Media Endpoints
    • /api/v1/calendar/
      • GET: List upcoming calendar events
    • /api/v1/calendar/update/
      • POST: Update calendar events
    • /api/v1/changes_history/{media_type}/{history_id}:
      • DELETE: Delete specific changes history entry
      • GET: Get specific changes history entry
    • /api/v1/lists/
      • GET: List all user & shared lists
      • POST: Create new list
    • /api/v1/lists/{list_id}/
      • DELETE: Delete specific list
      • GET: Get specific list
      • PATCH: Edit list properties (maybe also content)
    • /api/v1/lists/{list_id}/items/
      • GET: List items in list
    • /api/v1/lists/{list_id}/items/{item_id}/
      • DELETE: Delete item from list
      • GET: Get tracked item
    • /api/v1/media/
      • GET: List tracked media
    • /api/v1/media/{media_type}/
      • GET: List tracked media of type
      • POST: Track media of type
    • /api/v1/media/{media_type}/{source}/{media_id}/
      • DELETE: Delete tracked item
      • GET: Get tracked item
      • PATCH: Edit last consumption of tracked item
    • /api/v1/media/{media_type}/{source}/{media_id}/changes_history/
      • GET: Get changes history of tracked item
    • /api/v1/media/{media_type}/{source}/{media_id}/history/
      • GET: Get consumption history of tracked item
    • /api/v1/media/{media_type}/{source}/{media_id}/history/{consumption_id}/
      • DELETE: Delete specific consumption entry
      • GET: Get specific consumption entry
      • PATCH: Edit specific consumption entry
    • /api/v1/media/{media_type}/{source}/{media_id}/lists/
      • GET: Lists where the tracked item is in
    • /api/v1/media/{media_type}/{source}/{media_id}/lists/{list_id}
      • DELETE: Delete tracked item from specific list
      • PUT: Add tracked item to specific list
    • /api/v1/media/{media_type}/{source}/{media_id}/recommendations/
      • GET: Get recommendations based on tracked item
    • /api/v1/media/{media_type}/{source}/{media_id}/seasons/
      • GET: List seasons of tracked tv serie if media is tv serie
    • /api/v1/media/{media_type}/{source}/{media_id}/sync/
      • POST: Update metadata of tracked item
    • /api/v1/media/{media_type}/{source}/{media_id}/{season}/
      • DELETE: Delete tracked season if media is tv serie
      • GET: Get tracked season if media is tv serie
      • PATCH: Edit last consumption of tracked season
    • /api/v1/media/{media_type}/{source}/{media_id}/{season}/episodes/
      • GET: List episodes of tracked season if media is tv serie
    • /api/v1/media/{media_type}/{source}/{media_id}/{season}/changes_history/
      • GET: Get changes history of tracked season
    • /api/v1/media/{media_type}/{source}/{media_id}/{season}/history/
      • GET: Get consumption history of tracked season
    • /api/v1/media/{media_type}/{source}/{media_id}/{season}/history/{consumption_id}/
      • DELETE: Delete specific consumption entry
      • GET: Get specific consumption entry
      • PATCH: Edit specific consumption entry
    • /api/v1/media/{media_type}/{source}/{media_id}/{season}/lists/
      • GET: Lists where the tracked item is in
    • /api/v1/media/{media_type}/{source}/{media_id}/{season}/lists/{list_id}
      • DELETE: Delete tracked item from specific list
      • PUT: Add tracked item to specific list
    • /api/v1/media/{media_type}/{source}/{media_id}/{season}/sync/
      • POST: Update metadata of tracked season
    • /api/v1/media/{media_type}/{source}/{media_id}/{season}/{episode}/
      • DELETE: Delete tracked episode if media is tv serie
      • GET: Get tracked episode if media is tv serie
      • PATCH: Edit last consumption of tracked episode
    • /api/v1/media/{media_type}/{source}/{media_id}/{season}/{episode}/changes_history/
      • GET: Get changes history of tracked episode
    • /api/v1/media/{media_type}/{source}/{media_id}/{season}/{episode}/history/
      • GET: Get consumption history of tracked episode
    • /api/v1/media/{media_type}/{source}/{media_id}/{season}/{episode}/history/{consumption_id}/
      • DELETE: Delete specific consumption entry
      • GET: Get specific consumption entry
      • PATCH: Edit specific consumption entry
    • /api/v1/media/{media_type}/{source}/{media_id}/{season}/{episode}/lists/
      • GET: Lists where the tracked item is in
    • /api/v1/media/{media_type}/{source}/{media_id}/{season}/{episode}/lists/{list_id}
      • DELETE: Delete tracked item from specific list
      • PUT: Add tracked item to specific list
    • /api/v1/media/{media_type}/{source}/{media_id}/{season}/{episode}/sync/
      • POST: Update metadata of tracked episode (season)
    • /api/v1/search/{media_type}/
      • GET: Search for media using the specified provider
    • /api/v1/statistics/
      • GET: Get user statistics
  • Yamtrack Endpoints
    • /api/v1/health/
      • GET: Health check endpoint
    • /api/v1/info/
      • GET: Get application info and version
  • Merge latest changes from feat/add-api branch
  • Merge latest changes from dev branch
  • Error handling
  • Tests
  • Documentation

Disclaimer

This is my first time working with Django, so I'm not really familiar with all the best practices, I tried to follow the existing code style.

If someone has suggestions on how to improve something let me know!

Main changes

Implementations

Since all of the features of the api are already implemented for the WebApp, I just referenced basically all the existing methods from the app views copying the implementations, or referencing directly the methods used in the app implementations.

Basically nothing was duplicated, only the changes_history_processor, because there were some things that were implemented to be human-readable, but not as api-friendly.

In the future, if there will be the decision that the WebApp should use the API directly, all the implementations should be moved from the app to the api.

Urls

For urls I used the prefix /api/v1/ just to be on the safe side in case of future updates of the api.

I choose to use the re_path in the urls.py to allow the ending with or without a trailing slash (both /api/v1/media/ and /api/v1/media are accepted). This makes the paths a bit more complex to write, but I think it's more friendly for the end user.

/media endpoint

The /media endpoint by default excludes seasons and episodes from the list of tracked items, to declutter the results.

Specific media endpoints

The main change I did in structuring the API routes for media items is inverting the media type and the source, because I think it makes more sense to group by media type first. So instead of /{source}/{media_type}/{media_id}/ as in the WebApp, I used /{media_type}/{source}/{media_id}/.

It's true that both the media type can have different sources and a source can have differrent media types, but mainly the media type is the primary categorization of tracked items (e.g., when listing tracked items, filtering, searching, etc.), so it makes more sense to have it first in the path.

I decided that for the media types to follow a parent-child relationship, so seasons and episodes are sub-resources of tv series, so media_type in the request URL cannot be season or episode, but only tv, movie ecc ecc. This was made to simplify the API structure and make it more intuitive to use. So to query seasons or episodes, the user has to specify the parent tv serie first (e.g., /api/v1/media/tv/{source}/{media_id}/{season}/{episode}/).

Handling multiple consumptions

A single consumption entry is identified by /history/{consumption_id}/ subpath, so to get/edit/delete a specific consumption entry, the user has to specify the consumption id.

The consumption id is the id of the media in the database, so it's incremental between all medias of the same type, and it's not related to the media id of the tracked item, because a tracked item can have multiple consumption entries, so they need to be identified by a unique id.

When listing the consumption history of an item, all the consumption entries are returned with their consumption id, so the user can use that id to get/edit/delete a specific consumption entry.

Operations on specific media items

GET on /media/{media_type}/{source}/{media_id}/ returns the tracked item with all its data.
DELETE on /media/{media_type}/{source}/{media_id}/ deletes the tracked item completely, with all its consumptions.
PATCH on /media/{media_type}/{source}/{media_id}/ edits the last consumption of the tracked item.

All of these operations are available for single consumptions, to get/delete/edit a specific consumption entry, the user has to use the /history/{consumption_id}/ subpath.

All of the previous operations are available for seasons and episodes as well.

Changes history endpoints

A thing to note is that history entries of children can appear in the changes history of the parent item when querying the parent changes history endpoint. For example, when getting a changes history entry of a season by id, that same id is present when querying the changes history entry of the tv serie, but this means that the item id of the entry is not the correct one because it was built for the parent and not the child.

Lists

Since the current implementation of lists doesn't have a field to identify the element in the list (0 for the first element added to the list, 1 for the second one and so on), I added a new field list_item_id to the CustomListItem model, which is the id of the item in the list, so it can be used to identify the item in the list and perform operations on it (e.g., delete a specific item from the list).

I implemented also a logic to update the list_item_id of the items when items are removed, to not have "gaps" in the numeration. This change could be leveraged in the future to implement custom sorting for lists (out of scope for this PR).

Sorting

Sorting for media items was implemented only partially, because there already are some ways to sort items in the existing codebase functions, so I just referenced those functions.

For lists, I had to implement them from scratch, because there were no existing implementations. They are implemented in the helpers.py file.

Probably in the future it could be useful to re-implement sorting for media items too, to have more control over it. At that point probably a new sorting.py file could be created to handle all the sorting logic in one place.

Search

The search is only implemented for providers by type, so the user has to specify the media type in the url, and cannot search only between already tracked items.

Search should be done globally, returning tracked and not tracked elements, maybe adding a tracked boolean field in the element, it'll come in the future.

Also Search should support more filters in the future, and also not be binded to media type, but a global search with elements from all providers could be implemented.

Statistics

Right now the statistics endpoint returns exactly the same data as the WebApp statistics page, without any modification. This is not really the best thing as they should be computed specifically for the API, so they can be computed as the API implementer wants. I wasn't really sure what to include, so I just left it as is for now.

Episode sync

Since in the app the episode sync method was not implemented, I just referenced the season sync method for now. In the future, if there will be the need to have a specific episode sync implementation, it can be changed easily.

Authentication

The API uses two authentication methods implemented in authentication.py:

  • BearerAuthentication: Authorization: Bearer <token> - Standard OAuth-style bearer tokens
  • APIKeyAuthentication: X-API-Key: <token> - Custom header for simpler API key auth

Both methods look up the user by the token field in the User model. The user's existing token (from the users app) is reused for API authentication. No separate API key generation was implemented yet.

Note: Currently there's no way to generate or regenerate API tokens via the API itself - users must use the web interface.

Pagination

All list endpoints use a consistent pagination approach via parse_limit_offset and paginate_data helpers in helpers.py:

  • Query params: limit (default 20, max 200) and offset (default 0)
  • Response includes pagination object with total, limit, offset, next, and previous URLs
  • The next and previous URLs preserve all original query params (filters, sort, search)

Consideration: This is offset-based pagination which can have performance issues with big lists of items.

Error Handling

A custom middleware ApiJsonErrorMiddleware converts Django HTML error responses to JSON for all /api/ paths:

  • Intercepts 4xx/5xx responses and ensures they return JSON with {"detail": "message"} structure
  • Also has process_exception to catch unhandled exceptions and return JSON errors
  • In DEBUG mode, includes additional debug information in the response

This is because Django by default returns HTML error pages, but APIs shouldn't return HTML data.

Serializers

I added custom serializers in serializers.py to format the API responses consistently, each resource (Media, History, Season, Episode, List, etc.) has its own serializer class, nested relationships (e.g., Media with Seasons and Episodes) are handled correctly.

Testing

Still missing

Known Issues & Considerations

  1. Statistics endpoint: Returns exactly what the web statistics page needs, not necessarily what an API consumer would want. Should be redesigned with API-first thinking.
  2. No API token management: Users can't generate/regenerate/revoke API tokens via API - must use web interface.
  3. Search limitations: Can't search seasons/episodes (intentionally). Can't search already tracked media (only provider search).
  4. No rate limiting: Currently no rate limiting implemented, could lead to abuse.
  5. For children items, errors should shown for non existing season/episode (for example if it's requested season 4 of a 2 season show)
  6. Should the list of all media contain all the episodes and seasons? They can be hidden by default and only activated by a flag?

Future Improvements

  1. Field filtering: Allow clients to request only specific fields (?fields=title,score)

  2. Batch operations: Add support for bulk operations (e.g., update multiple items in one request)

  3. Webhooks: Event subscription for calendar updates, new releases

  4. Rate limiting: Proper per-user rate limiting with headers (X-RateLimit-*)

  5. Expand filters: More granular filtering (score range, date range, tags)

  6. Sorting: More sort options and multi-field sorting directly on the db query

  7. Provider integrations: Endpoints to import tracked media from external providers (#1069)

  8. Improve media user fields: Allow setting notes, progress, status, score and dates to every media, because not every media supports it, see table below.

  9. Custom lists ordering: Allow users to create a custom order to the lists.

  10. Administration commands: Add administration commands

    table

    Media Type score status progress start_date end_date notes
    Movie
    TV
    Season
    Episode
    Anime
    Manga
    Game
    Book
    Comic

  11. Implement admin commands to manage the server

Dependencies

Only djangorestframework was added as a dependency, no additional dependencies were needed.

@66Bunz 66Bunz marked this pull request as draft October 21, 2025 20:35
@66Bunz
Copy link
Copy Markdown
Contributor Author

66Bunz commented Oct 21, 2025

@FuzzyGrim is it possible to create a branch feat/add-api?

@codecov
Copy link
Copy Markdown

codecov Bot commented Oct 23, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.83%. Comparing base (4b846d6) to head (47b6369).
⚠️ Report is 6 commits behind head on dev.

Additional details and impacted files
@@           Coverage Diff           @@
##              dev     #924   +/-   ##
=======================================
  Coverage   82.83%   82.83%           
=======================================
  Files          69       69           
  Lines        6923     6923           
=======================================
  Hits         5735     5735           
  Misses       1188     1188           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@FuzzyGrim FuzzyGrim changed the base branch from dev to feat/add-api October 23, 2025 21:38
@FuzzyGrim
Copy link
Copy Markdown
Owner

Hi @66Bunz,

Thanks for working on this!

The spec file looks good to me. I've created the feat/add-api branch.

Also, I'd prefer a separate api app. Do you plan to use the Django REST framework for the implementation?

@66Bunz
Copy link
Copy Markdown
Contributor Author

66Bunz commented Oct 23, 2025

Thanks!
Yes, that's what I've been using

66Bunz and others added 5 commits November 4, 2025 21:20
…ion and pagination

- Added Bearer and API Key authentication classes for secure access
- Created serializers for media and item details
- Started implementing API endpoints copying `app` implementations (could be useful to change some implementations in the future)
@66Bunz
Copy link
Copy Markdown
Contributor Author

66Bunz commented Dec 10, 2025

I've got an open questions:

Looking at this list, is it ok to have the /api/v1/media/[media_type]/[source]/[media_id]/history/[history_id] route to delete an history record as it's now, or it's better to have it at /api/v1/history/[history_id]?

The history_id is not bound to the specific media

- Introduced new `EpisodeSerializer` for handling episode-specific data
- Updated `MediaSerializer` to include `parent_id` for nested relationships (tv serie -> season -> episode)
- Updated `helpers.py` to start to support episodes
- Added `history_processor.py` for managing history entries and processing timeline data
- Continued implementation of routes
- Modified URL patterns to accommodate new endpoints for media recommendations and history details
- Improved pagination and serialization logic for media and history responses
@FuzzyGrim
Copy link
Copy Markdown
Owner

Another option would be /api/v1/history/{media_type}/{history_id}. Since history_id is unique within its media type.

@66Bunz
Copy link
Copy Markdown
Contributor Author

66Bunz commented Dec 16, 2025

@FuzzyGrim I've got a question: Is it possible to get the media_id that is related to the history entry?

I looked around but I wasn't able to find anything useful...

@FuzzyGrim
Copy link
Copy Markdown
Owner

Something like this:

historical_model = apps.get_model("app", f"historical{media_type}")
record = historical_model.objects.get(history_id=history_id)

media_model = apps.get_model("app", media_type)
media_instance = media_model.objects.get(id=record.id)
media_id = media_instance.item.media_id

@66Bunz
Copy link
Copy Markdown
Contributor Author

66Bunz commented Dec 18, 2025

Something like this:

historical_model = apps.get_model("app", f"historical{media_type}")
record = historical_model.objects.get(history_id=history_id)

media_model = apps.get_model("app", media_type)
media_instance = media_model.objects.get(id=record.id)
media_id = media_instance.item.media_id

@FuzzyGrim 1 thing I noticed is that if the history entry is of a specific season/episode, I can only get the serie out of it, I wasn't able to get the specific season/episode. Am I missing something or this is it?

@FuzzyGrim
Copy link
Copy Markdown
Owner

It should work for both seasons and episodes. The media_type wouldbe episode or season. But tv shows, seasons and episodes all share the same media_id. TMDB doesn't have a separate unique identifier for each, but the media_instance.item will include season_number and episode_number in it.

@66Bunz
Copy link
Copy Markdown
Contributor Author

66Bunz commented Dec 19, 2025

Yes, nevermind, I was doing it wrong

@66Bunz 66Bunz marked this pull request as ready for review April 15, 2026 17:51
Copilot AI review requested due to automatic review settings April 15, 2026 17:51
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new /api/v1/ Django REST Framework API layer on top of the existing Yamtrack app implementations, plus supporting model changes (notably list item addressing) and a new API test suite.

Changes:

  • Add a DRF-powered REST API app (src/api/*) with endpoints for media, lists, calendar, changes history, search, health/info, and statistics.
  • Extend lists to support per-list sequential list_item_id with backfill migration and updated toggle behavior.
  • Add manual-provider search support with limit/offset/user inputs and wire up CI to run API tests.

Reviewed changes

Copilot reviewed 32 out of 37 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/lists/views.py Switch list toggle to delete/create CustomListItem (to support list-scoped IDs).
src/lists/models.py Add list_item_id, sequential assignment, renumber-on-delete, and list lookup helpers.
src/lists/migrations/0004_customlistitem_list_item_id_and_more.py Backfill list_item_id for existing list items and add uniqueness constraint.
src/lists/tests/test_views.py Update view tests to create CustomListItem directly.
src/lists/tests/test_models.py Add tests for sequential IDs, bulk create behavior, renumbering, and list search.
src/config/urls.py Register api/v1/ URL include.
src/config/settings.py Add DRF + API app, default auth/permissions, JSON renderer, and API JSON error middleware.
src/app/providers/services.py Extend provider search() signature and route manual searches to a new handler.
src/app/providers/manual.py Add manual tracked-item search with pagination + caching; adjust manual metadata handling.
src/app/models.py Add helper methods for seasons/episodes and list-membership mapping.
src/app/helpers.py Add MODEL_MAP used for manual provider search across media types.
src/api/urls.py Define API endpoint routing (optional trailing slash support).
src/api/* (serializers/helpers/middleware/auth) Implement API serialization, pagination/sorting/helpers, JSON error conversion, and auth.
src/api/tests/* Add endpoint contract tests and auth matrix tests for the new API.
requirements.txt Add djangorestframework.
pyproject.toml Ruff: ignore additional TODO rules.
.github/workflows/app-tests.yml Run API tests in CI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/app/providers/manual.py Outdated
Comment thread src/api/urls.py Outdated
Comment thread src/api/helpers.py
Comment thread src/api/helpers.py
Comment thread src/api/serializers.py Outdated
Comment thread src/lists/models.py
Comment on lines +175 to +182
def get_next_list_item_id(self, custom_list_id):
"""Return the next sequential id for an item within a custom list."""
current_max = (
self.filter(custom_list_id=custom_list_id)
.aggregate(max_id=Max("list_item_id"))
.get("max_id")
)
return 0 if current_max is None else current_max + 1
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_next_list_item_id() uses MAX(list_item_id)+1 without any locking. With concurrent inserts into the same list, two transactions can compute the same next id and then hit the (custom_list, list_item_id) unique constraint on save/bulk_create. Consider locking the list (e.g., select_for_update() on the parent CustomList) or retrying on IntegrityError so concurrent adds don’t sporadically fail.

Copilot uses AI. Check for mistakes.
Comment thread src/app/providers/manual.py
@FuzzyGrim FuzzyGrim merged commit dcec8bf into FuzzyGrim:feat/add-api Apr 18, 2026
0 of 2 checks passed
Breezyslasher pushed a commit to Breezyslasher/komikku that referenced this pull request Apr 19, 2026
Adds a new self-hosted tracker for Yamtrack using the new REST API
introduced in FuzzyGrim/Yamtrack#924.

- Bearer-token authentication with user-supplied host URL + API token
- Search via /api/v1/search/manga/ with manual-entry fallback so users
  can track items that don't appear in upstream providers
- Add / update / refresh / delete media via /api/v1/media/manga/...
- Status mapping: Planning, In progress, Paused, Completed, Dropped
- 0.0-10.0 score in 0.1 increments
- Extended TrackingLoginDialog with optional pwStringRes so the token
  field reads "API token" instead of "Password"
Breezyslasher pushed a commit to Breezyslasher/komikku that referenced this pull request Apr 20, 2026
The previous integration sent `status` as a display string (e.g. "In progress")
and parsed the detail response as if `status`/`progress`/`score` lived at the
top level. The actual REST API (FuzzyGrim/Yamtrack#924) expects `status` as an
integer 0-4, keeps user tracking data inside `consumptions[0]`, and uses
`max_progress`/`tracked` at the top level. This caused 400 errors on every
PATCH and kept `bind()` from ever POSTing new items (GET returned 200 with
provider metadata for any media, which we treated as "already tracked").

- Send `status` as Int (via Yamtrack.statusToApi) so validate_body accepts it.
- Parse `tracked`, `max_progress`, and nested consumptions in YTMediaItem.
- In bind()/refresh(), trigger add-to-library only when `tracked` is false.
@mbuet2ner
Copy link
Copy Markdown

Really love the API! Makes a lot of things possible, e. g. mobile apps. Created a (yes, fully vibe-coded) iOS app here. Swift 6, iOS 26, Liquid Glass, decent test coverage.

Wanted to have an easier way to add media "on-the-go" and tried I far I can get with codex. Looking forward to the official release of the API!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants