Technical Details - MiCat-S/Mi302 GitHub Wiki

繁體中文 | 简体中文 | English

This page is for people who want to know how Mi302 works inside or want to contribute: the request flow, path matching, the Emby endpoints that are implemented, the admin API, the code layout, running the tests, and the projects Mi302 builds on.

Request flow

Mi302 is a FastAPI application run by uvicorn. Its data lives in SQLite (data/library.db).

  1. Scan: .strm files, regular video files, nfo files and images in the library folders are written to the database, see Library and Scanning.
  2. Browse: players call the Emby API (/Users/{id}/Views, /Items, …) and Mi302 builds Emby BaseItemDto objects from the database.
  3. Playback info: POST /Items/{id}/PlaybackInfo. strm items come back with Protocol=Http and IsRemote=true, direct play only (transcoding off), and a DirectStreamUrl pointing to /videos/{id}/stream.{container}.
  4. Stream: the player requests /Videos/{id}/stream, /Videos/{id}/original.xxx or /Items/{id}/Download:
    • strm items: Mi302 reads the strm and applies the path rules. With a pickcode it gets a 115 direct link using the player's User-Agent; otherwise it can first follow the upstream redirects. It answers 302 Location: <real URL>.
    • Regular video files are served directly, with Range support.
  5. Progress: /Sessions/Playing… stores the resume position and watched state, and feeds intro/credits learning, see Intro and Credits.

See Playback for the details. Mi302 reports itself as Emby Server 4.8.11.0.

Paths and parameters

Every incoming HTTP request has its path normalised before routing:

  • ASCII letters in the path are lower-cased and other characters are left alone (the É in /Persons/Émilie stays), so paths are case-insensitive.
  • A leading /emby or /mediabrowser is removed, so both prefixed and bare paths work.
  • A trailing / is removed.
  • Query parameter names are case-insensitive (ParentId and parentid both work).

Also:

  • Error responses are plain text, as in Emby.
  • CORS is open to all origins.
  • FastAPI's generated API docs are at /api-docs.

Authentication

The token is looked for in this order:

  1. The header X-Emby-Token or X-MediaBrowser-Token.
  2. Token="…" inside the header X-Emby-Authorization (or Authorization), in the form MediaBrowser Client="…", Device="…", DeviceId="…", Version="…", Token="…".
  3. The query parameter api_key, X-Emby-Token, ApiKey or X-MediaBrowser-Token.

Tokens are issued by POST /Users/AuthenticateByName. When the same user logs in again from the same device (DeviceId), the old token is revoked. API keys (from api_keys in the config file, or created in the web admin page) act as an admin (the first admin by name) and are meant for programs such as MoviePilot; they cannot call /web/api/… or /p115/….

When the path names a user (/Users/{id}/…) or the request has a UserId parameter, watch history, resume points and favourites are read and written for that user. A regular user can only name themselves and gets 403 otherwise; admins and API keys can name another user and get 404 if that user does not exist. The id is case-insensitive, and the GUID form with dashes is accepted. Without one, the logged-in user is used.

Implemented Emby endpoints

Paths are written in Emby's usual casing but matched case-insensitively. Everything not marked "no login" needs a token.

System and users

Endpoint Notes
GET /System/Info/Public Server name, version and id; no login
GET /System/Info Full server info
GET, POST /System/Ping Returns Emby Server; no login
GET /System/Endpoint Always reports "local network"; no login
GET /Branding/Configuration Empty branding; no login
GET /Users/Public Users for the login screen (empty when server.public_users is off); no login
POST /Users/AuthenticateByName Log in with JSON or a form; the password field is Pw or Password; no login
GET /Users All users; admin only
GET /Users/Query The same list as {Items, TotalRecordCount} (MoviePilot counts users with it); admin only
GET /Users/{id} A user; only yourself unless you are an admin
POST /Sessions/Logout Log out and revoke the token
POST /Sessions/Capabilities, /Sessions/Capabilities/Full Accepted and ignored
GET /Sessions Empty list
GET, POST /DisplayPreferences/{id} Fixed defaults, nothing is stored; no login
GET /Users/{id}/GroupingOptions, /Plugins, /Localization/{kind} Empty lists; no login
GET /Items/{id}/RemoteImages Always empty (Mi302 does not look up remote images; MoviePilot then uses /Items/{id}/Images/{type}); no login
WebSocket /embywebsocket Only answers KeepAlive

Libraries and items

Endpoint Notes
GET /Users/{id}/Views, /Library/MediaFolders Library list
GET /Library/VirtualFolders, /Library/VirtualFolders/Query, /Library/SelectableMediaFolders Libraries and their folders (used by MoviePilot)
POST /Library/Refresh Rescan everything; admin only
POST /Library/Media/Updated Scan only the paths in the notification; admin only
POST /Items/{id}/Refresh Rescan only the series or movie containing this item; admin only
GET /Users/{id}/Items, /Items Item queries, parameters below
GET /Users/{id}/Items/{itemId}, /Items/{id} Full details of one item, including MediaSources, People and Chapters; returns a person for a person id
GET /Users/{id}/Items/Latest Recently added (Limit, ParentId, IncludeItemTypes; episodes are grouped by series)
GET /Users/{id}/Items/Resume Continue watching
GET /Items/{id}/Ancestors Parent items
GET /Items/Counts Numbers of movies, series and episodes
GET /Shows/{id}/Seasons Seasons
GET /Shows/{id}/Episodes Episodes (SeasonId, Season, StartIndex, Limit, Fields)
GET /Shows/NextUp Next up (SeriesId, Limit)
GET /Genres All genres
GET /Persons, /Persons/{name} Search people, look up a person by name (original or Chinese)
GET /Persons/{name}/Images/{type} Person photo, a 302 to the URL from the nfo; no login
POST /Users/{id}/PlayedItems/{itemId}; DELETE (or POST …/Delete) Mark watched or unwatched; for a series or season, all its episodes
POST /Users/{id}/FavoriteItems/{itemId}; DELETE (or POST …/Delete) Add or remove a favourite

Query parameters for /Items: ParentId, Recursive, IncludeItemTypes (including Person also searches people), ExcludeItemTypes, Ids, PersonIds, Person, IsFolder, SearchTerm, NameStartsWith, Years, Genres, Filters (IsPlayed, IsUnplayed, IsFavorite, IsResumable), IsPlayed, IsFavorite, SortBy, SortOrder, StartIndex, Limit, Fields (MediaSources, People, Chapters), UserId.

Negative or unreadable StartIndex and Limit values are ignored. Without Limit, /Items and /Shows/{id}/Episodes return everything, as Emby does. Latest, resume and next up default to 20 items and people search to 50, with at most 500 for these. Limit=0 returns only the total count.

SortBy accepts SortName, Name, DateCreated, DateLastContentAdded, PremiereDate, ProductionYear, CommunityRating, CriticRating, Runtime, Random, DatePlayed, PlayCount, IndexNumber, ParentIndexNumber and AiredEpisodeOrder.

Endpoints that return empty results so players do not fail: /Users/{id}/Items/{id}/Intros, /Items/{id}/Similar, /Movies/{id}/Similar, /Shows/{id}/Similar, /Videos/{id}/AdditionalParts, /Items/{id}/CriticReviews, /Users/{id}/Items/{id}/LocalTrailers, /Users/{id}/Items/{id}/SpecialFeatures, /Items/{id}/SpecialFeatures, /Items/{id}/ThemeMedia.

Images

Endpoint Notes
GET /Items/{id}/Images/{type}[/{index}] Read an image; type is Primary, Backdrop, Thumb, Logo or Art; no login
POST /Items/{id}/Images/{type}[/{index}] Upload an image (base64 text or the raw image, up to 30 MB), stored in data/images/; admin only
DELETE /Items/{id}/Images/{type}[/{index}] Delete the uploaded image and go back to the folder image; admin only
GET /Users/{id}/Images/{type} User avatar; always 404

Playback

Endpoint Notes
GET, POST /Items/{id}/PlaybackInfo Media source
GET, HEAD /Videos/{id}/{name} Stream (stream, stream.mkv, original.mkv, …); needs login when redirect.require_auth is on
GET, HEAD /Items/{id}/Download, /Items/{id}/File Download; always needs login, returns 403 when server.allow_download is off
POST /Sessions/Playing, /Sessions/Playing/Progress, /Sessions/Playing/Stopped Playback progress
POST /Sessions/Playing/Ping Accepted and ignored
POST /Users/{id}/PlayingItems/{itemId}, …/Progress; DELETE (or POST …/Delete) Older progress reporting API

Other endpoints

Endpoint Notes
GET /Episode/{id}/IntroTimestamps (also /v1), GET /Episode/{id}/Timestamps Intro and credits in the Jellyfin Intro Skipper plugin format
GET /MediaSegments/{id} Jellyfin 10.10 media segments
GET, HEAD /d/{pickcode}[.ext][/name] Short links used in Mi302's own strm files, a 302 to the 115 direct link; no login
GET, HEAD /p115/redirect?pickcode=…, /api/v1/plugin/p115strmhelper/redirect_url?pickcode=… Compatibility with other tools' strm files; no login
OPTIONS, PROPFIND, GET, HEAD /dav/{115 path} Read-only WebDAV (webdav.enabled): PROPFIND lists folders (Depth at most 1), GET on a file redirects (302) to the 115 direct link, HEAD returns only the size; HTTP Basic sign-in required, write methods return 405

Admin API

The API behind the web admin page /web. 115 file and folder IDs have 19 digits, more than JavaScript can represent exactly, so responses always carry them as strings; requests accept strings or numbers. Apart from /web/api/setup, it needs the token of a logged-in admin account (sent in the X-Emby-Token header); API keys are rejected. Requests and responses are JSON.

Path Purpose
GET, POST /web/api/setup Whether the first admin still has to be created; create it (only while no account exists)
GET, PUT /web/api/settings Read and save the web settings (written to the config file)
GET, POST /web/api/scan Scan progress and item counts per library; start a scan with {"library": name}, {"path": path}, or neither for everything; when no scan is running, last describes the previous one (scope, items processed, whether it was stopped)
GET /web/api/libraries/userdata How many watch-history records a library (name) has and for how many users (for the delete-library confirmation)
GET, POST /web/api/users; PUT, DELETE /web/api/users/{id} Users
GET /web/api/browse, /web/api/115/browse Pick a folder on the server, pick a 115 directory
GET /web/api/115/offline, POST /web/api/115/offline, POST /web/api/115/offline/delete, POST /web/api/115/offline/clear 115 cloud download: task list (page, with how many can still be added this month); add tasks (urls one per line, folder the 115 path to save to, empty for 115's default); delete tasks (hashes, files also deletes the downloaded files); clear finished or failed records (what: done/failed)
POST /web/api/115/offline/retry Add a failed cloud-download task again (hash, url, folder_id): removes that failed record first (not the files), then adds it back to the original folder
POST /web/api/115/share/read, POST /web/api/115/share/start, GET /web/api/115/share/status, POST /web/api/115/share/stop Transfer 115 shares (needs the cookie login): read shares (text, one link per line; returns each share's title, size and top-level items, with 115's own message in error when it cannot be read); transfer in the background (shares: [{code, receive_code, title, ids}], ids as strings; folder the 115 folder to save into; target: path organises into target_path, empty does not organise yet); progress; stop (after the share in hand)
POST /web/api/115/rapid/read, POST /web/api/115/rapid/start, GET /web/api/115/rapid/status, POST /web/api/115/rapid/stop Rapid upload from Aliyun Drive to 115 (needs the 115 cookie login and an Aliyun Drive login): read (source an Aliyun Drive path, media_only videos and subtitles only; returns the number of videos and subtitles, total size, files without a SHA1, files over 115's single-file limit, the first 200 files); upload in the background (source, folder the 115 folder to save into, target: path organises into target_path, empty does not organise yet, media_only); progress and the result of each file; stop (after the file in hand)
GET /web/api/aliyun/status, POST /web/api/aliyun/token, POST /web/api/aliyun/logout, GET /web/api/aliyun/dirs Aliyun Drive: account status; log in (refresh_token; the previous one is kept when it fails); log out; folder picker (path: subfolders and file count of that level; empty gives 資源庫 and 備份盤)
GET /web/api/115/recyclebin, POST /web/api/115/recyclebin/clean One page of the 115 recycle bin (offset, limit); emptying it (permanent; needs {"confirm": "清空"}, plus the password security key on the cookie channel)
GET /web/api/deleted What Mi302 sent to the 115 recycle bin, newest first (source filters by source, offset, limit); returns items, total, sources
GET /web/api/115/files Browse 115: subfolders and files of a folder (cid and path, or only path), with what the library made of each video (lib); files come 1000 at a time, offset for the next batch
POST /web/api/115/delete Delete what is ticked in Browse 115 (parent is the folder being viewed, ids): the folder is listed again and only items really in it are moved to the 115 recycle bin; for sync folders the local strm files and library entries are removed too
GET /web/api/115/organize, POST /web/api/115/organize/check, GET /web/api/115/organize/job Organise 115: folders to organise (name differs from MoviePilot's answer, or wrong episode numbers; q, kind as series/movie/episodes/held (held back), offset, limit; folders added from Browse 115 are in pinned; held on each entry says whether it is held back), with the background check progress job and the number not asked yet unchecked; ask MoviePilot in the background (refresh asks again for everything); progress of the reorganise job
POST /web/api/115/organize/folder, DELETE /web/api/115/organize/folder/{id} Pin a folder picked in Browse 115 (cid, path) at the top of the list; unpin it
POST /web/api/115/organize/preview, POST /web/api/115/organize/recommend, POST /web/api/115/organize/execute Preview one folder (id; per-part settings parts: {part: {type, tmdbid, season, format}}; target as parent/auto/path plus target_path), returning a token; episode format for a part (id, part, asking MoviePilot first); run previews by token (tokens; cleanup moves source folders with no videos left to the recycle bin)
POST /web/api/115/organize/all, POST /web/api/115/organize/all/stop, GET /web/api/115/organize/all Organise all: everything in the list matching q and kind (plus entries pinned from Browse 115) is previewed one by one in the background; entries whose preview has an empty review are organised, the rest are skipped with the reason (target, target_path, cleanup; entries held back and those that would send more than max_videos videos at once are left out, default 300, 0 = no limit; if MoviePilot cannot be reached it waits for it, up to 15 minutes); stop after the current entry; progress with the skipped and failed entries (at most 2000 are kept; results_total counts all of them and truncated says whether some were not kept; progress also appears as batch in GET /web/api/115/organize/job)
POST /web/api/115/organize/hold Hold back (id, hold): skipped by Organise all and moved to the end of the list; returns the entry
GET /web/api/115/organize/episodes, POST /web/api/115/organize/delete Deleting: every episode of a show and its folder (series; wrong numbers marked problem); delete ticked episodes (series_id, file_ids, remove_folder) or one list entry entirely (id), all to the 115 recycle bin, removing local strm files and library entries; {"ids": [...]} deletes ticked entries one by one in the background, progress in deleting of organize/job
GET /web/api/server, POST /web/api/server/check, POST /web/api/server/update, POST /web/api/server/restart Version and the last update check (boot differs on every start); check now; update to the latest version (runs in the background, progress in job); restart
GET /web/api/libraries/suggest?path= Bulk add libraries: list the subfolders with a suggested type and the reason, the video count (complete when fully counted), whether a library already uses the folder (used), and whether to tick it by default; partial means some folders were not fully read
POST /web/api/moviepilot/test, GET /web/api/moviepilot/status, POST /web/api/moviepilot/scrape MoviePilot connection test, status, scrape items missing metadata
GET /web/api/moviepilot/library-dirs MoviePilot library folders whose storage is 115 (dirs: name, path, type; enabled says whether MoviePilot is set up), for Organise to (整理到) when transferring shares; dirs is empty with the reason in error when MoviePilot is not set up or cannot be read
GET /web/api/series, POST /web/api/moviepilot/fill Series and episode-gap list; fill missing episodes
GET /web/api/moviepilot/subscriptions, POST /web/api/moviepilot/subscriptions/clear, GET /web/api/moviepilot/subscriptions/unsubscribed How many subscriptions MoviePilot has; cancel them all ({"confirm": "取消訂閱"}); the subscription lists saved before the last 5 cancellations (backups)
GET /web/api/intro/status, POST /web/api/intro/clear Intro and credits ({"season_id": id} clears one season's learned records)
GET /web/api/intro/seasons?q=, PUT /web/api/intro/seasons/{season id} Season list and search for intro and credits; manual settings for a season (all_seasons applies them to the whole series)
GET /web/api/people/status, POST /web/api/people/resolve Chinese names for cast and crew
GET, POST /web/api/backups; GET /web/api/backups/{name} List backups, back up now, download a backup
GET /web/api/mediainfo/status, GET /web/api/mediainfo/titles, POST /web/api/mediainfo/probe Media info: status; titles still missing media info (filterable, ordered); probing (optional filters, limit, ids)
POST /web/api/mediainfo/probe/limit, POST /web/api/mediainfo/stop Change how many the running batch probes ({"limit": N}); stop the running batch
GET /web/api/logs, /web/api/logs/download Logs, download the log file
GET, POST /web/api/apikeys; DELETE /web/api/apikeys/{key} API keys
GET /p115/status 115 account status
POST /p115/qrcode, GET /p115/qrcode/status, POST /p115/cookies, POST /p115/logout QR-code login, paste a cookie, log out
POST /p115/open/qrcode, GET /p115/open/qrcode/status, POST /p115/open/logout 115 open platform authorisation
POST /p115/strm/sync?mode=incremental (full sync without mode), GET /p115/strm/status, PUT /p115/strm/tasks Sync, sync status, sync tasks
POST /p115/strm/rewrite, POST /p115/strm/rewrite/stop Rewrite existing strm files to the current server address (local files only); stop it (what is rewritten stays)
GET /web/api/dupes, POST /web/api/dupes/scan, GET /web/api/dupes/groups, GET /web/api/dupes/shows, POST /web/api/dupes/delete, GET /web/api/dupes/log 115 duplicate files: status, find (paths), grouped list, delete (overrides; sha1 + size for one group; dry_run to only count); kind=versions for versions (grp for one group, show for one series or movie, use_suggestions to follow the suggestion; the web list uses GET /web/api/dupes/shows?kind=versions, one row per series, and expands one with groups?kind=versions&show=…; identical files work the same way with shows?kind=exact and groups?show=…, and deleting with show covers only that series), deletion log; kind=big for big files (filter with min_size in bytes and type as movie/episode/none; when deleting, use_suggestions means everything that matches, and nothing has to be kept); POST /web/api/dupes/prefer sets which resolution the versions suggestion keeps (prefer is 1080, 2160 or highest)
POST /web/api/scan/stop, POST /p115/strm/stop, POST /web/api/dupes/stop, POST /web/api/moviepilot/stop, POST /web/api/115/organize/check/stop, POST /web/api/115/organize/execute/stop, POST /web/api/115/organize/delete/stop Stop a scan, a 115 sync, a duplicate scan or delete, a MoviePilot scrape, fill or unsubscribe (what is scrape, fill or unsubscribe), or an Organise 115 check, run or delete of ticked entries; each stops at a safe point and leaves the rest for next time; returns stopped (false if nothing was running)

GET /web serves the admin page itself, and /web/115 redirects to /web#115.

Code layout

Each module under embyserver/:

Module Contents
__main__.py Command-line entry: python -m embyserver [-c config.yaml] [--scan] [--sync-115 [incremental]] [--reset-password USER NEW_PASSWORD]
app.py Builds the FastAPI app, normalises paths, starts background jobs
auth.py Users, tokens, Emby authorization headers
backup.py Daily backup of the database and config file
browse115.py Browsing 115: folder listings, videos matched to the library
config.py Config data classes and loading
config_file.py Writes the settings back to config.yaml
settings.py Settings editable in the web page: validation, applying, keeping the config file in step
db.py SQLite tables and access
dupes.py Duplicate files on 115: identical files, different versions, which one to keep; big files
dto.py Turns database rows into Emby BaseItemDto, UserDto and MediaSourceInfo
filetypes.py Shared file extension lists
organize115.py Organise 115: asks MoviePilot what each folder would be called (answers stored in organize_checks) and previews whole folders with it
http_util.py httpx client for external services that turns connection errors into readable reasons
intro.py Learns intros and credits from playback
library_suggest.py Bulk-adding libraries: guesses whether subfolders hold movies or shows
logs.py Logging to the console, the log file and memory, with credentials masked
mediainfo.py Media info: ffprobe output and X-mediainfo.json to Emby format
prober.py Probes the videos behind strm files with ffprobe
moviepilot.py Scraping and filling missing episodes through MoviePilot
moviepilot-plugin/ MoviePilot V3 plugin Mi302 Organizer (package.v3.json, plugins.v3/mi302organizer/): works out names with MoviePilot's own rules (naming.py) and renames in bulk from Mi302's list with MoviePilot's 115 authorisation; see MoviePilot
p115.py 115: QR-code login, direct links, directory listing, life events (115's activity log), circuit breaker
p115_open.py 115 open platform channel
probe_select.py Media info: picking titles to probe (filters, order)
people.py Cast and crew, Chinese names, Chinese genres
redirect.py Resolves the real URL behind a strm, path rules, redirect cache
reorganize.py Running reorganisations (sending previews to MoviePilot, cleaning up emptied folders, the incremental sync afterwards) and deletion; episode-format templates (fallback when MoviePilot cannot recommend one)
scanner.py Library scanning
share115.py Transferring 115 shares: reads shares, saves them into a temporary subfolder, waits for 115 to finish, then adds it to Organise 115 for MoviePilot
rapid115.py Rapid upload from Aliyun Drive to 115: 115's upload initialisation (encryption, second check), batch runs, adding the result to Organise 115
aliyun.py Aliyun Drive open platform: login and token refresh, listing, download links, reading a byte range
strm_sync.py Creates strm files from 115 and downloads metadata
textutil.py Pinyin sorting, pinyin search, Traditional/Simplified conversion
updater.py Checking for updates, updating and restarting from the web page
routes/system.py System, Users, Sessions and similar endpoints
routes/items.py Libraries, item queries, user data, images
routes/playback.py PlaybackInfo, streaming, progress reports, intro endpoints
routes/p115.py Admin API for 115 login and sync, pickcode short links
routes/web/ The web admin page and its API, split by page into setup, scan, intro, moviepilot, organize, p115 and server
routes/common.py Small helpers shared by the routes
web/admin.html The web admin page (one file with HTML, CSS and JavaScript)

Tables in library.db: meta (server id, 115 login state, sync progress and so on), users, tokens, items, user_data (watch history, favourites), p115_index (115 file id to local path), media_info, people, person_names, intro_obs, mp_no_image.

Development and tests

Python 3.10 or later is required. In a virtual environment:

pip install -r requirements.txt pytest ruff
python -m pytest
ruff check embyserver tests

The tests also check the admin page's JavaScript syntax with node --check (skipped without node) and the documentation: wiki links and anchors resolve, every page is in the sidebar, and the config reference in all three languages covers every key.

After changing install.sh:

shellcheck install.sh

Credits

  • DDSRem-Dev/MoviePilot-Plugins: the 302 playback flow follows the embyreverseproxy plugin, except that Mi302 needs no real Emby server behind it and implements the Emby API itself; the 115 QR-code login and direct-link flow follows p115strmhelper.
  • StrmAssistant (Emby 神醫助手): X-mediainfo.json uses its "media info persistence" format, and intro/credits learning follows its playback-behaviour intro detection.
  • xiao-vvv/emby-mediainfo (MIT licence): the ffprobe-to-Emby field mapping, the probing rate limits and the 115 circuit-breaker rules are adapted from it; its copyright notice is kept in embyserver/mediainfo.py.
⚠️ **GitHub.com Fallback** ⚠️