tests/fixtures/github_releases.json
Rama dev · réplica de lectura del repositorio original.
{
"_why": "GitHub `GET /repos/stevezau/shortlist/releases?per_page=100`, unauthenticated \u2014 the list the What's new dialog reads its notes from. Settles three things. The list is ordered by when a release was PUBLISHED, not by version: v1.2.1 was published after v1.3.0, so the dialog sorts by version itself. `body` is the release's own markdown, which is what an owner edits on GitHub after a release ships. And drafts/pre-releases are flagged per entry (none here; the API hides drafts from an unauthenticated caller).",
"recorded": "2026-09-14",
"reduced": "12 releases reduced to 5, and each entry to the 7 fields read or checked; values verbatim",
"releases": [
{
"tag_name": "v1.8.0",
"name": "v1.8.0",
"html_url": "https://github.com/stevezau/shortlist/releases/tag/v1.8.0",
"published_at": "2026-08-26T09:57:25Z",
"draft": false,
"prerelease": false,
"body": "\n### Added\n\n- **Shortlist can now tell you whether its picks actually get watched.** It only ever read one\n signal \u2014 Plex's binary watched flag \u2014 which cannot answer either question that matters. *When* did\n they watch it, since a rewatch overwrites the original date? And what about the film someone is\n two nights into, which the flag calls unwatched right up until the credits?\n\n Shortlist now watches playback itself. A pick is credited at the moment someone presses play, a\n part-watch is captured when playback stops rather than waiting for the next sync, and picks\n delivered through shared rows count too \u2014 previously they credited nothing at all, because a\n shared row writes no per-person pick record to match a play against. If tracking stops working,\n the dashboard says so rather than quietly reporting zero.\n\n A watch still in progress is no longer called an abandonment. \"Gave up part-way\" is the harshest\n thing this app says about a pick, and the rule behind it was invisible \u2014 so it now explains itself\n where the claim is made, instead of leaving you to conclude the tracking is broken.\n\n- **The dashboard leads with whether your setup is working.** It had grown to six stat tiles and\n eleven sections, with the one number that actually judges a setup \u2014 the share of delivered picks\n that got watched \u2014 nowhere near the top. It now opens with whether it is working and what isn't,\n and every person and row links through to its own page.\n\n- **Requests can be tagged with the person they were picked for.** Radarr and Sonarr have no field\n for \"who wanted this\", only tags, so the Requests inbox's why-line never reached them. Turn it on\n and each request carries the wanting person's name as a second tag, so you can tell in Radarr and\n Sonarr who a title was added for. Off by default, and any row can opt in or out on its own.\n\n- **Settings changes now record who made them.** The audit log said what changed and when, but\n never who \u2014 so a value that moved with nobody owning up to it was simply unanswerable. That\n happened on a live server: a request threshold shifted by 0.2 between two runs and no record could\n say what did it. Each `settings.change` event now carries the actor (a browser session or an API\n token, plus a short client hint). Deliberately not the client IP: these rows are immutable and the\n support bundle exports them, and a LAN address is the kind of environment detail that should not\n end up in something people paste into public issues.\n\n- **Prefer a language when requesting.** The request pool is by definition what your library\n _lacks_, so on a library that already holds the popular English titles what's left missing skews\n non-English before any setting applies \u2014 and the rating floor then favours it further, because\n TMDB's audience rates anime and K-drama generously. Nightly runs ended up mostly asking Sonarr and\n Radarr for subtitled titles.\n\n Settings \u2192 Requests \u2192 Guardrails now has a **Language** choice, and any per-person row can override\n it. **Any language** is the default and behaves exactly as before, so an upgrade changes nothing.\n **Prefer these** keeps your languages on the normal bars and makes everything else clear a higher\n rating to be sent on its own \u2014 below that bar a title waits in the inbox with the reason on it\n rather than being dropped, so a great foreign film is still one click away. **Only these** never\n asks for another language at all.\n\n The higher bar has no fixed default: it follows your own minimum rating plus 1.5 and keeps\n following it, so a permissive 6.0 server starts at 7.5 and a strict 8.0 server at 9.5. Type a\n number to pin it. The ranking is deliberately unchanged \u2014 a foreign title that clears the bar still\n competes on merit and usually wins, because it out-rates the English titles around it. This thins\n the middle; it doesn't exclude a language.\n\n The inbox now shows a language chip on any title outside your languages, so a held-back one\n explains itself without opening TMDB.\n\n- **Choose how much of a show to grab.** Every show Shortlist requested arrived with all seasons\n monitored, so one pick of a long-running series started the whole back catalogue downloading that\n night \u2014 enough to fill a disk, and enough to spend a day's worth of indexer queries on a show\n nobody had watched an episode of yet.\n\n Settings \u2192 Requests now has **How much of a show to grab** on the Sonarr card, and any per-person\n row can override it in the row editor. The choices are Sonarr's own Add Series _Monitor_ options,\n passed through unchanged, so they mean exactly what they mean in Sonarr: **All Episodes** (still\n the default), **First Season** for a taster you can extend later, **Last Season**, **Pilot\n Episode**, and **None** to file the show in Sonarr without downloading anything.\n\n Anything other than **All Episodes** also switches off Sonarr's separate **Monitor New Seasons**\n for that show, so \"first season only\" still means that when season eight airs. Without it a\n restriction on a still-running series quietly grows back a season at a time.\n\n It applies only to shows Shortlist adds. One Sonarr already tracks is left exactly as you have it,\n as before.\n\n### Fixed\n\n- **Moving your watching to a second account now copies your history exactly.** It was marking whole\n shows watched when you were part-way through one. If you were 400 episodes into One Piece, the new\n account said you had finished all 1,100 \u2014 and the same happened to any series you had not\n completed. On a real 50-account server, 342 of 535 watched shows were part-watched, so this was the\n normal outcome rather than an edge case.\n\n The cause was that Shortlist only ever knew _how many_ episodes you had watched, never _which_. It\n now reads your account episode by episode and copies that, so the new account matches yours:\n\n - the exact episodes of each show, not the whole show\n - how many times you have rewatched something, not just that you watched it\n - anything you are part-way through, at the same position, back in Continue Watching \u2014 these could\n not be carried across at all before\n\n It also **removes** anything watched on the new account that you have not watched, which is what\n makes the two match, and what repairs an account an earlier version over-marked. The preview lists\n what would be removed **by name** and asks you to confirm before anything is written. Your own\n account is never written to.\n\n It is now reversible: the new account's state is saved before the first write, and **Undo** puts it\n back exactly \u2014 rewatch counts and positions included. The one exception is an account that can't see\n all of your libraries, where a complete picture can't be saved; the preview tells you that before\n you agree to anything. Afterwards Shortlist re-reads the account and\n tells you what did not land, instead of reporting the writes it sent.\n\n Plex still records everything as watched _today_ \u2014 no Plex API accepts a date, and that has not\n changed. The writes now go oldest-first so Continue Watching still comes out in the right order,\n and Shortlist keeps your real dates itself, so your recommendations are unaffected.\n\n"
},
{
"tag_name": "v1.7.0",
"name": "v1.7.0",
"html_url": "https://github.com/stevezau/shortlist/releases/tag/v1.7.0",
"published_at": "2026-08-18T11:28:47Z",
"draft": false,
"prerelease": false,
"body": "\n### Added\n\n- **Each row can have its own Sonarr/Radarr settings.** Until now every row asked for titles under\n one set of rules, which is wrong the moment your rows are for different people. A kids' row and a\n film-buff row want different rating floors, different quality profiles, and different folders \u2014 and\n a run with three rows was really one pass that mixed them together.\n\n Open a row, go to **Requests**, and override what that row needs: rating and vote floors, year\n bounds, how many titles it may ask for, whether it sends automatically, and which quality profile\n and root folder it files into. Anything you leave alone is inherited from the global settings, so a\n row you never touch behaves exactly as it does today.\n\n Two limits stay global on purpose. **Max per run** is the ceiling for the whole run and a row can\n only ask for *less* than it, never more \u2014 otherwise the one setting protecting your download queue\n would be a suggestion. And the rating source (plus its API key) stays global, because it is one\n account shared by everything.\n\n When several rows want titles in the same run, the run's slots are split evenly between them, and\n each row is evaluated on its own rules. A row that wants fewer than its share hands the remainder\n back to the others rather than wasting it. **A title two rows both want costs one slot, not two** \u2014\n it is requested once, and the run says which row claimed it.\n\n- **A share filter that Plex stores but does not apply is now caught and reported**\n ([#88](https://github.com/stevezau/shortlist/discussions/88)). Shortlist keeps each row private by\n adding label exclusions to everyone else's Plex share, and every check it had said the same thing:\n the exclusions were written, plex.tv stored them, and reading them back confirmed it. None of that\n proves Plex is *applying* them. Six Home accounts saw all six rows anyway, and the first person to\n notice was a user rather than the owner.\n\n Each run now looks at one account's home screen through that account's own eyes and reports it if\n rows belonging to other people are showing. It never blocks anything \u2014 it measures and tells you,\n because nothing you change in Plex fixes it and the useful action is a bug report.\n\n### Fixed\n\n- **Nothing was reaching Sonarr or Radarr, and nothing said why.** Titles were being examined and\n discarded before any request could be made, and every screen reported a confident zero. The cause\n was a lookup budget spent on cached answers, so a run could exhaust it without ever asking about a\n new title. Rating lookups are now billed only when they actually cost something, and rejections are\n remembered rather than re-fetched nightly.\n- **The screens that said \"0 requested\" now explain their own zero** \u2014 whether nothing cleared the\n bar, or titles are sitting in the queue waiting for you to approve them, which the old wording\n reported as if none had been good enough.\n- **A row's delivery no longer collapses because one title was deleted from Plex.** A title that\n disappears between being picked and being delivered took the whole row down with it. Runs also now\n report what Plex actually accepted rather than what was asked for, so a partial delivery reads as\n partial instead of complete.\n- **Copying a watching account's history moved nothing**, because the owner's own history was never\n read.\n- Approvals for titles from different rows each get their own Radarr/Sonarr connection, so a title\n approved from one row can no longer be filed using another row's settings.\n\n"
},
{
"tag_name": "v1.6.1",
"name": "v1.6.1",
"html_url": "https://github.com/stevezau/shortlist/releases/tag/v1.6.1",
"published_at": "2026-08-17T07:44:42Z",
"draft": false,
"prerelease": false,
"body": "\n### Added\n\n- **Leave one person's Plex sharing settings alone**\n ([#92](https://github.com/stevezau/shortlist/discussions/92)). To keep each row private, Shortlist\n adds label exclusions to every other account's Plex restrictions \u2014 which is right until it collides\n with a restriction you set yourself. The case that surfaced it: a child's managed account with an\n **allow only** label list, where the whole point is that the account sees nothing but the labels you\n named, and Shortlist's exclusions were fighting it. Switching that person off in Shortlist did not\n help, because off means \"no row for them\", not \"stop touching their account\".\n\n Open the person, go to **Settings \u2192 Plex sharing**, and turn off **Manage their Plex sharing\n settings**. Shortlist takes back out the exclusions it added and never touches that account again;\n the Users list badges them **Sharing untouched** so it is visible at a glance, and Support \u2192 Sharing\n lists them separately rather than reporting them as a fault. Said plainly at the switch: that\n account can then see other people's rows, unless \u2014 as with an allow-only list \u2014 its own Plex\n restrictions already keep it away from them.\n\n Two things stay true regardless. Everyone else still hides *that* person's row, so leaving one\n account alone never makes their row public. And a **shared row you have limited to certain people**\n stays hidden from them: that exclusion is the only thing keeping the row away from people you did\n not pick, so removing it would undo a choice you made on the row itself. The trade-off to know is\n that later changes to who a shared row is for stop reaching a left-alone account \u2014 turn management\n back on if you need them picked up.\n\n### Changed\n\n- Frontend dependencies bumped (lucide-react, sonner, eslint, typescript-eslint, testing-library),\n and the CI checkout action brought in line with the rest of the workflow.\n\n"
},
{
"tag_name": "v1.3.0",
"name": "v1.3.0",
"html_url": "https://github.com/stevezau/shortlist/releases/tag/v1.3.0",
"published_at": "2026-08-11T07:03:03Z",
"draft": false,
"prerelease": false,
"body": "\n### Added\n\n- **Rows can prefer recent releases.** Ranking had no age term at all \u2014 a title's score was seed\n frequency x rating x seed weight x affinity \u2014 so a well-rated, highly-similar 1996 film beat a 2024\n one every time and rows filled with catalog titles. **Settings \u2192 Finding titles \u2192 Recent releases**\n is a weight, never a filter: an older title still reaches a row, it just has to be a better match.\n It applies to the pool CUT as well as the order, so a newer title below the line can still get in.\n **This applies to every install, existing servers included** \u2014 the default is 0.5 (\"leans towards\n recent releases\"), and no migration pins the old age-blind behaviour. Nothing is rewritten at\n upgrade: each row adopts it on its next refresh night, so rows shift towards newer titles over the\n following days rather than all at once. Set **Recent releases** to 0 to rank as before, globally or\n per row.\n\n- **Changing a setting rebuilds the row instead of waiting out freshness.** Freshness is a cadence\n for suppressing churn when nothing has changed \u2014 but nothing recorded WHICH settings a row's picks\n were chosen under, so it also delayed changes made on purpose. On a real server, raising \"Recent\n releases\" left 36 of 42 rows redelivering byte-identical picks for up to a fortnight, which from\n the outside is indistinguishable from the setting not working. Each row now stores a fingerprint of\n the settings that decide its CONTENTS and rebuilds on a mismatch, whatever the cadence says.\n\n- **`/api/support/surfaces`** \u2014 a read-only diagnostic that reports which Plex surfaces each row is\n actually on, included in the support bundle.\n\n- **A run says what year each pick is and how well rated it was.** \"Why is this row full of old\n films?\" was unanswerable on the one page built to answer it.\n\n\n- **Web search can now run entirely on your own hardware, via SearXNG.** The AI web-search source\n previously had two backends: your AI provider's own search (Claude, GPT and Gemini only) or the\n hosted Exa API. Neither suited a fully self-hosted server \u2014 a local Ollama model can't search on\n its own, so Exa was the only way to use the feature at all, and that meant a third-party account\n and sending queries off the box. **Settings \u2192 Finding titles \u2192 AI web search** now offers\n **SearXNG** as a third backend: point it at your own instance and the whole search stays local.\n It works with every AI provider, exactly as Exa does. ([#78](https://github.com/stevezau/shortlist/issues/78))\n\n Set it up in **Connections \u2192 Web search** \u2014 one card where you pick the backend and enter what it\n needs, replacing the separate per-vendor cards \u2014 or inline on the web-search card itself. One prerequisite,\n which the card and the **Test** button both state outright: SearXNG's JSON API is **off in a stock\n install**, so add `json` to `search.formats` in its `settings.yml` and restart. Without it SearXNG\n answers with a bare `403` that explains nothing \u2014 Shortlist translates that into the exact fix.\n A reverse-proxy login is supported via its own username/password fields, where the password is\n encrypted at rest. A login embedded in the address (`http://user:pass@host`) is refused outright,\n with a message pointing at those fields: the address itself is stored in the clear, returned by the\n API and written verbatim into the immutable `settings.change` audit event that support bundles\n export, so a password must never be allowed into it. A subpath deployment such as\n `https://example.com/searxng` is kept intact.\n\n Choosing a backend by name means only that backend \u2014 an unconfigured Exa or SearXNG never falls\n through to the other, which would have sent a self-hoster's queries to a paid vendor they never\n picked.\n\n### Changed\n\n- **The \"Auto\" search backend is gone.** It was the default, so it is what nearly everyone ran, and\n it meant \"the provider's own search UNIONED with whichever external is configured\" \u2014 a real\n behaviour that its name described not at all. You now pick exactly one backend, and it is the one\n used. Migration 0063 pins every install to what it was actually using: a configured SearXNG, else\n a configured Exa key, else the provider's own search. **A server that had both loses the combined\n search**, which did widen the candidate pool; raise a row's seed count if you want that reach back.\n\n- **Which backend the web search uses now lives in one place.** It was briefly both on the\n Connections card and on the Finding-titles card \u2014 the same control twice, with no way to tell\n which was authoritative. Connections \u2192 Web search owns the backend and its credentials (the AI\n provider already works that way); Settings \u2192 Finding titles owns whether the source runs, and\n names the backend with a link rather than repeating the form.\n\n- **\"Test\" on the provider's own web search now really searches.** Being Claude, GPT or Gemini says\n the provider offers a web-search tool; it does not say your plan or model may use it. When it may\n not, the call failed at run time, logged a warning and returned no titles \u2014 so the source quietly\n contributed nothing, every night, with nothing in the UI to say so. The Test button now performs\n one real search and tells you if the tool came back empty.\n\n- **The run trace names every candidate and what became of it.** \"What survived, and what release\n date did to it\" reported counts \u2014 \"40 candidates survived filtering\" \u2014 which is a summary, not a\n trace: it could not answer \"so why isn't X in my row\". Every candidate is now listed under what\n happened to it (made the shortlist / not in your libraries / lost the cut / already watched), each\n with the rating and the release-date multiplier actually applied to it. The per-title verdicts were\n already being recorded; they were simply never shown.\n\n- **The \"not in your libraries\" group says what Radarr/Sonarr did with it.** That group IS the\n request pool, and nothing on the page said so. Each title now reads \"requested \u2014 added to Sonarr\n and searching\" or the reason it is still waiting. Which exposed the gap behind it: the engine\n worked out per-title why a title stayed queued and then discarded it into an aggregate log line, so\n every pending row had an empty reason and the inbox's one question had no answer anywhere.\n\n- **\"How we ordered the shortlist\" shows the ordering instead of describing it.** The claim an owner\n most wants checked \u2014 that one heavily-watched favourite cannot swallow the row \u2014 was the one prose\n could not settle. The rank list now marks where each source's and each watched title's turn begins.\n\n- **Run stats say \"Web searches\" rather than \"Exa searches\".** The same counter now serves both\n external backends, so naming one vendor was simply wrong on a SearXNG server. A run's \"How we\n picked\" trace also records _which_ backend actually ran.\n\n### Fixed\n\n- **A share filter written by Plex Web is merged, not corrupted.** `parse_filter` split conditions on\n `|` and values on `,` only, but Plex Web writes a combined allow+exclude filter as\n `label=Age%200%2CAge%203&label!=X`. That parsed into one condition whose field swallowed the allow\n clause, so the existing exclude was invisible, the merge appended a second one, and the filter grew\n corrupt. This is the privacy machinery, so it is the most important fix in this release.\n\n **It stops the corruption; it does not undo it.** An account whose filter already carries two\n `label!=` clauses keeps both \u2014 future runs merge into the first and leave the second alone, and Plex\n Web is unhappy with the pair. If you hit this before upgrading, open **Plex \u2192 Settings \u2192 Users \u2192\n that account \u2192 Restrictions** and delete the duplicate exclude line; Shortlist rebuilds what it\n needs on the next run. Healing them automatically means rewriting a filter we only partly wrote,\n which is how they got corrupted in the first place, so it is deliberately left as a manual step.\n\n- **The broken-row sweep asks twice before deleting.** It is the only destructive Plex path, and an\n empty label read is indistinguishable from a genuinely unlabelled collection \u2014 so a collection is\n now re-read to confirm before removal, and a sweep where NOT ONE of our rows reads as labelled is\n treated as a failed read rather than a server full of orphans.\n\n- **A refresh night no longer collapses a row onto one taste.** The refresh branch merged survivors\n with newcomers and then truncated by pool order, re-applying the very ordering `diversify_by_seed`\n had just defeated \u2014 so a heavily-watched title's look-alikes took the row back over.\n\n- **Shared rows honour their display order** and stop offering dials they ignore: a shared row set to\n \"Shuffled\" or \"Highest rated\" was delivering ranking order, because the shared path never called\n the code those settings live in.\n\n- **Cold-start rows are full size and from the right library**, and \"unstarted only\" is rechecked \u2014\n three findings from the pipeline audit, all in paths whose victims are least likely to report them.\n\n- **An exclusion written URL-encoded is no longer reported as missing.** The sharing report compared\n filter values raw while the privacy code compares them decoded, so an encoded copy of a label read\n as absent \u2014 and the one question that report exists to answer named a leak that wasn't there.\n\n- **Cancel stops lying about what it did.** It worked on the first press and then answered \"this run\n isn't currently running\" to every press after, about a run that was. Asking a stopping run to stop\n is now a no-op rather than an error, and the accepted cancel is recorded on the run so a reloaded\n page still shows \"Stopping\u2026\" instead of a live-looking button that can only fail.\n\n- **A connection card says what it needs before what it is** \u2014 the part that actually blocks you was\n the easiest to skim past.\n\n"
},
{
"tag_name": "v1.2.1",
"name": "v1.2.1",
"html_url": "https://github.com/stevezau/shortlist/releases/tag/v1.2.1",
"published_at": "2026-08-12T00:18:46Z",
"draft": false,
"prerelease": false,
"body": "\n### Added\n\n- **A run now says what Plex ratings did to it.** Open a run \u2192 **How we picked** for a person, and\n the \"Watched recently\" step states the rating policy that run used in one line: off, on but nobody\n rated anything low enough, on but the account's ratings were disbelieved as tool-written, or how\n many titles a rating dropped. Until now all four looked identical \u2014 the trace listed dropped\n titles when there were any and said nothing at all when there weren't, so a run where the feature\n silently did nothing read exactly like a healthy one. The distrusted case is the one worth\n catching: a Kometa rating sync on that account means every rating is ignored, all run, invisibly.\n\n The line reports the setting **as it was when the run happened**, not as Settings reads today, so\n changing it later doesn't rewrite the history of a run you open a fortnight afterwards. Runs\n recorded before this release don't carry it and still show just the dropped titles.\n\n It also catches the half-case the account-level check is designed to let through: if a tool has\n written _some_ of someone's ratings but not enough to condemn the account, the counted ratings and\n the skipped ones are reported separately, rather than a single total that quietly speaks for values\n nothing ever looked at.\n\n### Fixed\n\n- **A pick's \"why\" said \"Because you liked\u2026\" when it only ever knew what someone watched.** Every\n pick with a genre in common with its seed claimed a preference nobody had expressed \u2014 seeds are\n weighted purely by how recently something was watched, and a Plex rating can only ever _remove_ a\n seed, never mark one as liked. It now reads \"Because you watched\u2026\", which is the fact we have.\n Reasons are written into each run as it happens, so runs already recorded keep the old wording.\n\n"
}
]
}