Releases: AlyShmahell/MatchMedia
Release list
v0.0.9
Changelog
MatchMedia v0.0.9
- Implemented the XDG Base Directory Specification, relative to current user's
$HOME - Implemented an XDG enabled and config-driven
browse_root, limited to$XDG_VIDEOS_DIR,$XDG_MUSIC_DIR,/mnt,/media - Moved the dev runtime to a container
- Limited the default http addr to
localhost
v0.0.8
Changelog
MatchMedia v0.0.8
POST /v1/scantakesmode: omit or"rescan"is the 0.0.7 full match;"changes"groups the current tree and only rematches incomplete titles.202is{"session","files","mode"}. Unknownmodeor a non-empty path that is not absolute underbrowse_rootis400.- Changes completeness is title identity:
tvshow.nfoplus a show poster (poster.jpg/folder.jpg) or a season-folder poster. Movies still need that item’s NFO + poster. Episode stills (-thumb.jpg) never rematch.uniqueidis optional (empty provider/id is fine). require_episode_nfois Changes-only (*bool, defaulttrue): every owned video (including Specials/extras on the parent) needsepisode.nfoor<basename>.nfo."false"skips that. Rescan ignores the field.- Complete Changes titles leave grouping as
status: matched, one Score1.0candidate from the sidecar,catalog: [], and never enter the provider worker. Gone or renamed-away paths are omitted; a rename without sidecar ispendingat the new path. - Ingest, ranker,
POST /v1/match,POST /v1/retry, select, and catalog HTTP are unchanged.
v0.0.7
Changelog
MatchMedia v0.0.7
match.prefer.animeis a typed-job auto-match hint only. Untyped scan/ingest never prefer, andjob.candidatesis always the full ranked list.- Displayed Score is scaled into
[0, 1]:(J + title_lift · max(func, parentCov) + exact_lift · exact) / (1 + title_lift + exact_lift). Title-in-synopsis no longer fills Score to1.0. - Auto-match adds a Score-leader path and a unique full-query-coverage path (nicknames and long official titles).
solo_min_scoreis ignored. - Alias GETs run when any job title token is missing from the primary name (
Spy×FamilyloadsSpy x Family). - When Jaccard is already
1, synopsis/parent lift is omitted so identical titles are not reordered by a blurb ([Oshi no Ko]anime vs live action). - HTTP routes ServeMedia pins (
POST /v1/scan, untypedPOST /v1/ingest, pollGET /v1/jobs?session=) andskipDefer(Jaccard) are unchanged. - The release tarball is still binary,
config/,public/, andLICENSE.
Prefer
prefer.anime(language: Japanese,kind: Animation) runs only whenjob.typeis a key undermatch.prefer(ingesttype: anime).- Rank always scores every provider hit. Prefer never deletes rows, so
selectcan still pick a Scripted TVMaze title next to a Japanese Animation hit (The 100 vs The 100 Girlfriends). - Jikan still ships static
attrs: { language: Japanese, kind: Animation }so typed anime prefer can see those hits. - A single prefer-shaped hit does not auto-match.
Ranking
Score is no longer J + (1 − J) · plot. After the same titleNorm (lowercase, strip non-alnum, no stemming):
- Jaccard
Jis still the best|q ∩ t| / |q ∪ t|over the primary title and eachtitle*attr. - Title-in-synopsis
funcis coverage of job title content tokens in the candidate synopsis (not plot-to-plot). Shared tokens in this result list are down-weighted (0.25). exactis1whentitleNormof the job equalstitleNormof the primary title (aliases do not count).- Shipped
title_lift/exact_liftare0.15/0.25. Divide by1 + title_lift + exact_liftso Score stays in[0, 1]without clamping. - When
J == 1,funcandparentCovare not added. Primary exact is(1 + exact_lift) / den(~0.89), not1.0from a synopsis token.
Sort is unchanged: content-token overlap, then Score, then title prefix, then Jaccard, then year.
Auto-match
job.candidates stays the full list. Auto-matched if:
- best
max(Jaccard, Score) >= match.min_scoreand (one candidate, or that strength gap to second is>= match.min_margin), or - best Score
>= match.min_scoreand the Score gap to second is>= match.min_margin, or - typed prefer, at least two prefer hits, best of those has
queryCov == 1, and the Score gap to the next prefer hit is>= match.min_margin, or - the job has a content token and exactly one work has
queryCov == 1and that row is ranked first.
Path 4 is for short nicknames (Anohana) and solo official titles with extra words (Apothecary Diaries, Berserk Memorial Edition). Provider clones that share the covering title (best-Jaccard primary/alias) and year count as one work (TVMaze + Jikan Banished). Season 2 or The Office US/UK stay manual. Girls vs Girls und Panzer still two works.
defer: true providers still run only if the fast pass is thin (fewer than match.min_hits or best Jaccard below match.min_score). A Jaccard auto-match on the fast pass skips defer. Unique queryCov is applied on the final ranked list so a weak TVMaze substring does not lock in before Jikan.
Aliases
- Skip the extra
titlesGET only when the primary title already covers every job title token, not only content tokens (plot_stop/ length < 3). - Stored aliases still must share a content token. Bookworm / Erased subset skips are unchanged.
Spy X Familyvs TVMazeSpy×Familynow fetches AKASpy x Family, so Jaccard can be1via the alias.
Config
version:is0.0.7.- New tunables:
match.title_lift(shipped0.15),match.exact_lift(shipped0.25). Unset still defaults to those values. match.solo_min_scoreis removed from shipped YAML and ignored if an overlay still has it.- Leave
match.min_score/min_margin/match.workers, cooldown YAML, and providerdefer/min_interval_ms/provider_timeout_msas shipped in 0.0.6.
Upgrade notes
- HTTP contract is unchanged. Rematch existing sessions if you want the new Score, prefer-not-filtering, and unique
queryCovon old jobs (ranker: setis the same key; Score numbers are not). - Overlay
data/config.yamlmust not rely on prefer deleting non-anime rows, or on Score1.0from title-in-synopsis. Dropsolo_min_scoreif present. - Scan jobs still have empty
type. Western TV + anime-shaped hits stay on the same picker (The 100). Typed CSVtype: animestill uses prefer for auto-match only. - Exact primary titles display ~0.89, not
1.0. Auto-match still usesmax(Jaccard, Score), so Jaccard1still matches. - Unique
queryCovcan auto-match a unique official title belowmin_score. Two seasons or two years of the same covering title staymanual.
v0.0.6
Changelog
MatchMedia v0.0.6
- Scan jobs carry numbered
files[]. - Immediate
group.kinds/group.extrasfolders stay on the parent show (season0) instead of minting a second title. - A cooldown or timeout on one provider no longer zeros hits from another; all-cooldown with zero candidates stays
status: error. - HTTP routes ServeMedia pins (
POST /v1/scan, untypedPOST /v1/ingest, pollGET /v1/jobs?session=) andskipDeferare unchanged. - The release tarball is still binary,
config/,public/, andLICENSE.
Scan files
- Every scan job now has
files[]: video files only (group.video_ext), never a directory, under that job’spath. - Longest-prefix ownership so a named sibling keeps its own tree (
Show/Spin Off/does not leak ontoShow/).
Numbers when the tree implies them:
- season-shaped folder → that number (
Season 01→1) - folder classified
kindorextras(YAML lists) → season0 - filename
sxxexx/episode N(same patterns grouping already used) → those numbers; filename wins over folder - remaining files in a known season, sorted by path → sequential unused episode numbers
- Loose files with no season folder and no episode pattern still appear as
{path}with season/episode omitted (MapFilesOntoCatalogalready skips those). - Catalog GET / select / worker backfill still call
MapFilesOntoCatalogas before.
Kinds and extras
- Immediate children classified
kindorextrasno longer emit inner scan jobs. - The parent is treated as a series; those videos land on it via
files[]with season0. - Named siblings stay a separate title with
parentset as today.
- Title clustering still uses
keepVideo; file attachment usescollectVideos. - Word lists stay in YAML (
group.kinds/group.extras); Go does not special-case folder names.
Cooldown
- A cooldown skip does not wipe candidates from other providers.
- If any wanted provider returned hits, the job is ranked (
matchedormanual) anderrorstays empty — even if a deferred provider is cooling or timed out.
- Zero candidates with
ok == 0and at least one provider error (including all-cooldown) staystatus: error. job.Erroriskey: messagejoined by"; "(YAML provider keys).- Empty 200s still increment
okand becomeunmatchedwhen there are no rows. - No new status. No worker auto-retry.
defer: trueproviders still run only if the fast pass is thin (fewer thanmatch.min_hitsor best Jaccard belowmatch.min_score).- A high-Jaccard
manualfast pass still skips defer.
Config
version:is0.0.6.- No new tunables.
- Leave
match.min_score/min_margin/match.workers, cooldown YAML, and providerdefer/min_interval_ms/provider_timeout_msas shipped in 0.0.5.
Upgrade notes
- HTTP contract is unchanged. Rematch is not required for ranking; Score /
ranker: setare the same as 0.0.5. - Scan jobs now populate
files[]. ServeMedia 0.0.3 may double-count if it still walksjob.pathas well asfiles[]. Preferfiles[]when present. Specials// OVA-stylegroup.kindsfolders and extras no longer mint a second job of the same show. Those videos are season0on the parent. Named spin-off folders are still separate titles.- Mixed “one provider hit, another cooling” is no longer
error. All-cooldown with zero candidates stays a rederrorbadge (POST /v1/retrystill rematcheserrorandunmatched). - CSV / JSON ingest is unchanged except for that cooldown ranking (ingest does not go through the grouper;
files[]stays empty unless the client sent season/episode columns ontojob.season/job.episode).
v0.0.5
Changelog
MatchMedia v0.0.5
SequenceMatcher stays on disk grouping only. Candidate ranking is token-set Jaccard plus a residual plot/parent score. Jobs record ranker: set. The release tarball is still binary, config/, public/, and LICENSE.
Ranking
Score is no longer a SequenceMatcher ratio (and no longer +0.15 for year). After the same titleNorm used for grouping (lowercase, strip non-alnum, no stemming):
- Jaccard
Jis the best|q ∩ t| / |q ∪ t|over the job title and each candidate title string (primary title plus every attr whose key starts withtitle). Parent is not inJ. - Plot
funcis length-weighted coverage of content tokens from the job title in the synopsis. Tokens inmatch.plot_stopor shorter than 3 are dropped. A token that appears in two or more synopses in this result list is down-weighted (0.25). parentCovis coverage of those same content tokens from a different parent folder in the candidate title set.- Displayed
Score=J + (1 − J) · max(func, parentCov). Exact title (J = 1) is always Score1. - Year is a sort key only when the job has a year.
Sort: content-token overlap with the job title beats none; then Score; then job title tokens as a prefix of the candidate primary title; then Jaccard; then year.
That stops short near-misses (Frieden, Crashed, Arifureta Kiseki) from beating the long official title, keeps Wednesday / Limitless above suffixed shows, and lets plot lift English disk titles that only have a Japanese TVMaze show.name (Explosion → Bakuen when aliases are absent). Parent residual lifts Magi/Sinbad and Scarlet Bond without giving Adventures a free hit on the / of.
Aliases
Optional provider titles block. TVMaze ships GET /shows/{id}/akas with max: 3. After search, candidates whose primary title already covers every content token of the job skip the extra GET. At most max of the rest are fetched, paced with min_interval_ms. Failed GETs do not fail the search. Only aliases that share a content token with the job are stored as title_aka / title_aka_2 / … so an English AKA can be Jaccard 1 (Erased).
Two franchise AKAs that both copy the same English name still tie at Score 1 and stay manual (I Shall Survive Using Potions! vs Potions Will Save Me; KonoSuba main vs Explosion spin-off when TVMaze lists the spin-off title on both shows).
Auto-match and defer
match.prefer.anime (language: Japanese, kind: Animation) runs before rank. Typed anime jobs use it as before. Untyped scan jobs use the same filter when any candidate matches.
Auto-matched if:
- best Jaccard
>= match.min_scoreand the Jaccard gap is>= match.min_margin, or - prefer actually filtered, and either a single remaining candidate’s Score meets
solo_min_score(shipped0.01), orqueryCov == 1with the usual margin on Score.
A weak solo hit does not use solo_min_score unless prefer filtered. defer: true providers still run only if the fast pass is thin (fewer than match.min_hits or best Jaccard below min_score). A prefer-filtered auto-match on the fast pass skips Jikan.
Config
version: is 0.0.5. New / now-used tunables in config/default.yaml:
match.plot_stop— stop list for plot, parent residual, and content overlapmatch.synopsis_limit— search/detail/catalog clip (shipped 4000 runes; was 400)match.solo_min_score— prefer-solo floormatch.wait_cap— in-memory wait log cap (500)scan.sample_videos— videos sampled per child (5)session.ttl_max_ms— clamp onsession.ttl_ms(shipped 86400000)
match.weights is gone. Score is the convex residual above, not a weighted sum.
Upgrade notes
- Rematch existing sessions if you want set ranking, aliases, and the new Score on old jobs (
ranker: seqrows stay as stored). - Overlay
data/config.yamlmust not rely onmatch.weights. Specials/is still agroup.kindsfolder, not extras. A Specials tree whose files repeat the series title can emit a second job of the same show (Arifureta). Addspecial/specialstogroup.extrasif you want those folded into the parent.- Scan jobs still have empty
type. Movies without OMDb/TMDB keys stayunmatched.
v0.0.4
Changelog
Matchora 0.0.4
- No local LLM, embedder, or llama.cpp. 🎉
- Scan grouping and candidate ranking are SequenceMatcher plus YAML.
- The release tarball is binary,
config/,public/, andLICENSE.
Scan grouping
Scan no longer calls an instruct model (prompt.md is gone). Each library child is classified from disk: seasons and extras fold into the walk-root title; named sibling folders become their own titles; leftover loose videos cluster with SequenceMatcher.
Word lists live under group: in config/default.yaml (video_ext, extras, release, kinds, seq_threshold). Years are taken only from a (YYYY) suffix or a trailing year token already on the name.
Ranking
Jobs record ranker: seq. Score is SequenceMatcher on normalized titles only:
- Exact title match is
1.0. - Otherwise title ratio, with a second try against
title + year(helpsGirls (2012)vsGirls). - Matching year still adds
0.15. - Synopsis is not used. A one-word query whose word appeared in a synopsis no longer scores
1.0(Wednesday vs Wednesday Club, Limitless vs Limitless with Chris Hemsworth, and similar).
True same-name collisions with no job year (Dark Matter 2015 vs 2024) stay manual.
Ingest
Unknown CSV headers are mapped with ingest.aliases, then SequenceMatcher against canonical fields when the ratio is at least group.seq_threshold. The instruct ingest.md path is gone.
Config
version: in config/default.yaml is the only version string (0.0.4). There is no root VERSION file and no compile-time version.
Grouping lists and match/http numbers come only from YAML. Missing or invalid required keys fail start. POST /v1/config that would clear a required list is 400 and is not written.
Removed: llama: block, ranker: embed, GGUF URLs, --prepare, and the llama admin form.
Packaging and docs
- One archive:
matchora-<version>-linux-amd64.tar.gz(no-llamatarball, no vendored llama.cpp). - Packaging fails if
versionis missing from the shipped YAML. - README is written for the release tarball: unpack,
./matchora, listen onhttp.addr.
Upgrade notes
- Delete any leftover
vendor/llama.cpptree; it is unused. - Overlay
data/config.yamlmust not rely onllama:keys. - Rematch existing sessions if you want title-only ranking on old jobs.
- Scan jobs still have empty
type. Jikan (types: [anime]) does not run on untyped scan unless the TVMaze pass is empty. English folder titles that TVMaze stores under a Japanese name (e.g. Erased → Boku Dake ga Inai Machi) can sit below other exact-title hits; the admin UI shows the top 5 candidates.
v0.0.3
Changelog
0.0.3 - 2026-08-29
Scan/ingest are isolated per request. Title-finding is back to the August 17 loop (one child, one instruct call, match kicked immediately), plus a title folder path.
Breaking
POST /v1/scanreturns202 {"session","files"}.POST /v1/ingestreturns202 {"session","jobs"}(jobs are no longer a bare array).GET/DELETE /v1/jobs,GET /v1/scan/status,GET /v1/match/log,POST /v1/match,POST /v1/retry, select/catalog-on-job, and catalog reads require?session=. Missing id is400; unknown or expired is404.- Jobs persist as
{data_dir}/jobs-{session}.json, notjobs.json. - Scan no longer attaches video files to jobs or maps them onto catalog episodes (that 0.0.2 experiment is reverted).
Changed (title-finding realignment)
Restored the August 17 grouping engine (bb1cdb1 / 0692b2d), not the later Units / batched Group / LLM SxxExx experiments.
- Immediate children only (
scan.Children). Onematch.Groupper child. Compact listing, ~5 filename samples,max_tokens: 256. Append+Kickinside the child loop so matching overlaps later grouping.- Prompt is the August 17 rules again (folder as title, one object per series, no SxxExx, extras are not shows).
- Added
Path:on the listing and{title,year,path}so a mixed folder can use{Path}/{child}(movie/OVA next to seasons). Parent:on folder listings; fallback title prefers Parent whenFolder:is Season N.- Same title from two children is still two jobs (no end-of-scan merge).
Added
- Server-minted session ids:
<UTC 20060102T150405Z>-<16 hex>. GET /v1/sessions— unexpired ids, newest first.session.ttl_ms(default and max 24h). Expired job files are purged; catalog on disk is not.GET /v1/catalog?session=lists only titles that session matched.DELETE /v1/catalogandDELETE /v1/catalog/{provider}/{id}— no session;409if any live session still has that match.- Admin UI stores the last session and sends
?session=on job/scan/catalog calls.
Fixed
- Image
Fetchno longer shares the provider API pacer (tvmaze/posterno longer queues for minutes behind search). - Admin scan poller no longer dies on the previous session while
POST /v1/scanis in flight; poller restarts aftersetSession. POST /v1/scandoes not walk the whole tree before202;filesis the sum of each child’s video count.ListVideos/ samples follow symlink library roots.
Removed
Units/FolderClass/ batchedGroupUnit/ LLM file S/E labels.- Unused
MergeJobs/ emptyGrouped.Filesleftovers.
v0.0.2
0.0.2
Scan jobs now carry video files and map them onto catalog episodes. Grouping stays one instruct call per library child (prompt.md); season/episode labels are only taken from explicit SxxExx in the filename.
Added
- Job
files(path, optionalseason/episode) filled during scan from videos under each child. - Catalog episodes get
path/pathswhen file S/E matches the catalog numbers (01=1). - Same
title+yearin one scan merge into one job (Season 1 + Season 2 of one show), withpathset to the show folder. - Season-folder listings include
Parent:so a failed group falls back to the show name, notSeason 1. - Vendored
llama-serveris started withPR_SET_PDEATHSIGSIGTERM so it exits if Matchora’s PID disappears. CleanStop()still SIGTERMs the process group.
Changed
prompt.mdmay returnfileson each show. Paths must be copied from the listing; do not guess S/E from01.mkvor aSeason Nfolder.browse_root: /treats absolute paths as inside the root.
Notes
- Ingest is unchanged (
ingest.md). Catalog join is still numeric, not a second LLM call. share/config/default.yamlversionis still0.0.1; bump it if the shipped YAML should matchVERSION.
v0.0.1
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning.
[0.0.1] - 2026-08-28
First public release. Matchora is a Go service that ingests title rows or scans a
video library, searches YAML-defined metadata APIs, and ranks candidates with a
local llama.cpp embedder or instruct model.
Added
Matching and providers
- Generic GET + JSON-path provider engine. Provider names are not hardcoded in Go.
- Shipped providers: TVMaze, Jikan v4, OMDb (movies, keyed), TMDB movie and TMDB TV (keyed).
- Type allowlists so a typed job only calls providers that list that type.
- Fast pass plus deferred providers (
defer: true) when hit count or best score is too low. - Parallel provider GETs per title; per-job HTTP clocks so a slow deferred call does not stall the batch.
- Per-provider pacing (
min_interval_ms), retries, per-attempt timeouts, capped exponential backoff, andRetry-After. - Provider cooldown after consecutive GET failures (jittered skip, reset on success).
- Ranking: MiniLM embeddings (cosine) or instruct chat; lexical token overlap + year bonus if the embed server is down.
- Auto-
matchedvsmanualvsunmatchedfrommin_score/min_margin; user confirm via select. - Season/episode catalog fetch for TV providers (TVMaze, TMDB TV), including a catalog-only request that does not change the match.
- Optional provider
detailGET (OMDb plot) and poster downloads. skip_episode_posterson ingest, scan, rematch, retry, select, and catalog.
Ingest and scan
POST /v1/ingestfor CSV (header) or JSON arrays:title,year,type,season,episode,imdb.- CSV alias map (
ingest.aliases) and type rewrite (episode/season→tv). - Instruct fallback to map unknown CSV columns using
config/ingest.md. POST /v1/scanlists videos underbrowse_root, groups dirty names with a local instruct model (config/prompt.md), then matches in the worker.- Scan grouping progress via
GET /v1/scan/status. - Expected Plex / Jellyfin / Infuse trees (movies vs series roots, optional
{tmdb-…}/{imdb-tt…}ids).
Catalog
- Matched, selected, and cataloged titles written under
{data_dir}/catalogas[uniqueid-id] Title (Year)/trees. tvshow.nfo/movie.nfo, season folders, episode.nfo, and downloaded posters.- Distinct
uniqueidslugs so TMDB movie vs TV ids cannot collide (tmdb-movie,tmdb-tv, OMDbimdb). GET /v1/catalogandGET /v1/catalog/{provider}/{id}; posters at/poster.jpg(and season/episode variants).- Clearing jobs does not delete the catalog tree.
Runtime and llama.cpp
- HTTP admin server on
:7680(User-Agent: matchora/{version}). - Writable
{exeDir}/data:jobs.json, optionalconfig.yamloverlay,secrets(YAML map of API keys). GET/POST /v1/secretsreport which key slots are set and merge the secrets file (values never returned). Slots are provider API keys only.GET/POST /v1/configread and deep-merge{data_dir}/config.yaml(same shape asdefault.yaml). Never includes secrets.- Listen address is
llama.host/llama.port(defaults127.0.0.1/8080);llama.base_urlis derived ashttp://{host}:{port}/v1. - On start, probe that URL. A healthy listener is left alone. If it is down, install llama.cpp + GGUFs into
{exeDir}/vendor/llama.cppand spawn one llama-server on127.0.0.1at that port (embed + optional instruct). Distinctllm_base_url(e.g. a stub) is left alone. - After a successful secrets or config POST, return the JSON body, stop a spawned llama-server, and re-exec so the next
Load/llama.Startapplies the files. Setters write files only. --prepareruns that install, verifies models, stops the spawned server, and exits.- Dist is binary +
config/+public/only; llama.cpp is not in the slim tree.
Admin console
- Verification UI at
/(folder picker viaGET /v1/fs, ingest upload, scan, job cards). - Secrets panel for provider keys and llama panel for host/port; both wait for
/healthafter the process restart. - Live match wait log, sticky status chips, score heatmap, retry errors/unmatched, clear jobs.
- Manual candidate select and per-candidate seasons catalog.
- Skip-episode-posters checkbox persisted in
localStorage.
HTTP API
GET /,/health,/v1/fs,/v1/jobs,/v1/match/log,/v1/scan/status,/v1/catalog,/v1/catalog/{provider}/{id}GET/POST /v1/secrets,GET/POST /v1/configPOST /v1/ingest,/v1/scan,/v1/match,/v1/retry,/v1/jobs/{id}/select,/v1/jobs/{id}/catalogDELETE /v1/jobs(empty list and abort in-flight scan grouping)
Build, package, and tests
- Podman linux/amd64 dist builder (
./build/run: run, rebuild, prepare, package). - Slim tarball
matchora-*-linux-amd64.tar.gz(binary, config, public, LICENSE). - Bundled tarball
matchora-*-linux-amd64-llama.tar.gz(same plus llama.cpp, GGUFs, and fetched third-party licenses). - Podman-only unit, smoke (stub metadata + stub chat; waits for
/healthafter secrets POST), and optional live (MATCHORA_LIVE=1) harness. - BSD 3-Clause license.