Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,60 @@
# Changelog

## v0.73.0 — 2026-10-16

**A series with no description, genres or author can have them filled in, a whole source can be rated Adult only,
AniList sync waits when AniList asks instead of giving up, and a new Unraid extension engine asks for a password.**
The first two began as pull requests from **@Squeaks72**
([#257](https://github.com/AngeloSha/uchiyomi/pull/257), [#245](https://github.com/AngeloSha/uchiyomi/pull/245),
[#256](https://github.com/AngeloSha/uchiyomi/pull/256)), rebuilt on the app's own parts. Thank you.

### Missing details, filled in

- **Admin → Settings → Library housekeeping → Fill in missing details** ([#257](https://github.com/AngeloSha/uchiyomi/pull/257),
[#245](https://github.com/AngeloSha/uchiyomi/pull/245)). It is off by default. Once a night it fills in a description,
genres or an author for a series that has none of its own.
- It asks AniList first, for series linked there, in English, in libraries that look AniList up. Fifty series go in
one request, by id.
- Then it asks the series' own source and the sources it follows, in its language, for up to 15 series a night that
you keep updated. A source that is switched off, failing or cooling down is never asked.
- A series with no chapters yet gets a cover too.
- **Nothing is replaced.** A series' own details win as soon as a scan brings them, and anything you set in Edit
details wins over both. Genres you cleared on purpose stay cleared.
- **The 18+ filter sees the genres it adds**, like any others.
- **A series it could not describe is asked again after a month.** Following a source by hand asks that source at once.
- **Switching it off takes back everything it filled in**, covers included.

### A whole source can be rated Adult only

- **Admin → Sources**, open a source, **Age rating** ([#256](https://github.com/AngeloSha/uchiyomi/pull/256)): **Automatic**
leaves it to the source's extension, and **Adult only** makes the whole source adult whatever the extension says.
That helps with built-in sources and sites added by address, which can never say.
- **Accounts limited below 18 cannot use an Adult only source at all**, by any route, just as with a source whose
extension marks itself 18+. With Show 18+ off, it also leaves Discover, search and the latest and popular lists.
- **No rating opens a source to a younger account.** A source whose extension marks it 18+ stays 18+.
- **The 18+ filter's "Sources to treat as adult" chips are gone.** Those sources are now rated Adult only (see Upgrading).

### Everywhere else

- **AniList sync waits when AniList asks, instead of giving up.** AniList limits how many requests a server may make
a minute, and every AniList request Uchiyomi makes now shares that one limit: everyone's progress, ratings, art,
Discover and the new missing details. A push that AniList turns away is sent again after the wait AniList asks for,
so connecting AniList and pushing everything you have finished goes slowly instead of stopping after a dozen series.
Profile → Connections says **Syncing slowly** meanwhile, not an error, and Health's Tracker sync card leaves it out.
- **A new Unraid install of the extension engine asks for a password**, and Uchiyomi's template has the matching user
name. An engine installed before keeps its settings. [A password on the engine](docs/extensions.md#a-password-on-the-engine)
says how to add one.
- **The desktop app's extension engine gets a new password every time the app starts.** Nothing changes on screen.

### Upgrading

- **Sources you had ticked under Settings → 18+ filter → "Sources to treat as adult" are now rated Adult only, and
accounts limited below 18 can no longer use them.** Before, they only left the lists while Show 18+ was off. To let
such accounts use one again, set its Age rating back to Automatic under Admin → Sources.
- **Database:** one new table (`series_auto_details`) and one new setting (`fill_details`, off). Both are additive.
- **Optional:** `REPAIR_DETAILS_MAX` (1 to 200, 15 by default) sets how many series a night are asked about their
sources.

## v0.72.0 — 2026-10-16

**Health turns around: one card per series, with everything wrong with it. Three new cards say when the downloads
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ sites' terms and your local law.
in rows that fold.
- **Library management** — several libraries, each over as many folders as you like, *Select all* with bulk
fetch and removal, editable metadata that survives a rescan, merge (each chapter kept once), delete, restore
and forget, and *Rescan everything* for files changed behind its back.
and forget, and *Rescan everything* for files changed behind its back. Optionally, a series with no description,
genres or author gets them filled in overnight from AniList or another of its sources, never over its own.
- **Banners made from the series itself** — a series with no banner on AniList gets one built from striking panels of
its own pages.
- **Language editions** — the same series in English and in Spanish is one card, with chips to switch between
Expand Down Expand Up @@ -179,7 +180,8 @@ sites' terms and your local law.

- **Multi-user** — accounts, per-user progress and favourites, streaks and a leaderboard.
- **Age ratings and an 18+ library** that stays off the home screen, the grid and search until somebody asks
for it, with a per-member rating cap and a per-member permission to add series at all.
for it, with a per-member rating cap, a whole source rated *Adult only* when its extension does not say so, and a
per-member permission to add series at all.
- **Security** — 2FA, lockout, active sessions, an audit log, and **OIDC single sign-on** against Authentik,
Authelia or Keycloak.
- **Scoped API tokens** — read, write or admin, revocable, with an opt-in for 18+ libraries.
Expand Down
110 changes: 96 additions & 14 deletions bff/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
openapi: 3.1.0
info:
title: Uchiyomi API
version: 0.72.0
version: 0.73.0
description: "Everything the web app does, it does over this API. Session JWTs are for the browser; scripts
use a personal API token (Profile -> Connections -> API tokens), sent as `Authorization: Bearer uy_...`.
Scopes: read (every token; GET plus the library search POST, which is a query), write (anything that changes data), admin (the /api/admin surface, which also
Expand Down Expand Up @@ -5794,7 +5794,7 @@ paths:
type: array
items:
type: object
required: [provider, label, tokenHelp, connected, accountName, expiresAt, expiringSoon, lastSyncAt, lastError, needsReconnect]
required: [provider, label, tokenHelp, connected, accountName, expiresAt, expiringSoon, lastSyncAt, lastError, needsReconnect, slowedDown]
properties:
provider: { type: string, enum: [anilist, myanimelist, kitsu, mangaupdates] }
label: { type: string }
Expand All @@ -5816,6 +5816,13 @@ paths:
Since v0.71.0 (#246): only a new token mends this connection -- its token was rejected (the
connection is off), has lapsed, or can no longer be read. A rate limit, an outage or a refused
lower number also leave `lastError`, but never set this.
slowedDown:
type: boolean
description: >-
Since v0.73.0: the service asked us to wait (a 429 that outlasted the retries), so pushes go
slowly for now. A pace, not an error: `lastError` is null for it, and the next push that lands
clears it. AniList's pushes share the server's one AniList quota with every other AniList
request, spaced by the limit AniList states, and a 429 is waited out and the push made again.
"401":
description: Not authenticated
/api/trackers/anilist:
Expand Down Expand Up @@ -7077,7 +7084,7 @@ paths:
type: array
items:
type: string
description: The source ids the 18+ filter treats as adult. See adultSources on PATCH.
description: The source ids rated Adult only, lowercased. See adultSources on PATCH.
source_prefs:
$ref: "#/components/schemas/SourceOrder"
group_upgrade:
Expand All @@ -7086,6 +7093,9 @@ paths:
borrow_names:
type: boolean
description: Whether the nightly repair borrows chapter names from another source. See borrowNames on PATCH.
fill_details:
type: boolean
description: Whether the nightly repair fills in missing descriptions, genres, authors and covers. See fillDetails on PATCH.
archive_paused: { type: boolean, description: "The slow archive (#117) is paused server-wide. See archivePaused on PATCH." }
archive_per_hour: { type: integer, minimum: 1, maximum: 30 }
archive_window_from: { type: [integer, "null"], minimum: 0, maximum: 23 }
Expand Down Expand Up @@ -7265,10 +7275,14 @@ paths:
maxLength: 60
adultSources:
description: >-
Source ids to treat as adult on top of the ones whose extension declares itself NSFW,
replaced whole. While a request does not carry adult=1 they drop out of the routes that
list sources or fan out over them (GET /api/sources, search-all, latest, popular). By-id
routes are unaffected. Same normalisation as adultGenres. Empty by default.
The source ids rated Adult only, on top of the ones whose extension declares itself NSFW,
replaced whole (PUT /api/admin/sources/{id}/age-rating rates one at a time). Since v0.73.0 a
permission: an account limited below 18 is refused these sources everywhere, by-id routes
included (latest, popular, detail, add, preview), as it is refused a self-declared adult
one. And while a request does not carry adult=1 they drop out of the routes that list
sources or fan out over them (GET /api/sources, search-all, latest, popular). Ids are
lowercased and trimmed; anything outside letters, digits and _ . : - is dropped. Empty by
default.
type: array
maxItems: 200
items:
Expand Down Expand Up @@ -7306,6 +7320,19 @@ paths:
this off clears exactly the names it wrote for every series that follows this setting
(a series switched on for itself keeps its own).
type: boolean
fillDetails:
description: >-
Fill in missing details (since v0.73.0), the nightly repair's ninth step. Off by default.
A series with no description, genres or author of its own is given them from AniList (in one
batched request by id, for series linked there with a checked link, in libraries whose
AniList lookups are on, and in English only), else from its own source or one it follows in
its language -- only sources that are not off, failing or cooling down, for at most
REPAIR_DETAILS_MAX (15) series a night that are kept updated; a series with no chapters yet is
given a cover too. What it fills in is kept in a layer of its own below the scan: a series'
own details always win, an admin's edit wins over both, and the 18+ filter judges the genres
a series shows. A series asked is asked again after 30 days. A manual follow asks at once.
Switching this off deletes everything it filled in, and puts back any cover it set.
type: boolean
archivePaused:
type: boolean
description: >-
Expand Down Expand Up @@ -7478,7 +7505,7 @@ paths:
any kind -- how a page that pressed a one-row fix learns that ITS run (the id the run route answered)
ended, even one that took 5 ms; `recent` the five newest finished runs {id, finishedAt, status, kind,
target}; `lastFull` {id, at, ms, origin, result} the newest full run. `limits` is every bound the
running process uses (env overrides applied): shortMax, gapsMax, groupsMax, namesMax, countMax,
running process uses (env overrides applied): shortMax, gapsMax, groupsMax, namesMax, detailsMax, countMax,
directionsMax, huntBudget, shortHuntMax, gapChapters, retrySeries, shortCopies, failuresMax, retryCap,
paceMs, pageListMs, listingRefreshMs, listTimeoutMs, huntWallMs, huntMaxSources, solverBudgetMs,
expiredBlockHours, repairHours. `estimates` maps a run kind to {typicalMs (median of its last five
Expand Down Expand Up @@ -7726,7 +7753,7 @@ paths:
schedule. Audit: `task.run` {task: 'verify'} at the press and `library.verify` {checked, missing,
readLibraryMissing, unmounted, ms} when the walk ends.
`repair` (since v0.41.0) is the only task that takes a body, and it is also detached. With no body it
runs all eight steps over the whole library; `only` narrows it to named steps, and the three targets
runs all nine steps over the whole library; `only` narrows it to named steps, and the three targets
each belong to exactly one step -- `seriesId` to `gaps` ("Fill now" on one series), `bookId` to
`short` ("Fix" on one chapter), `sourceId` to `failures` ("Retry now" on one source) -- so a target
sent without `only: ["<its step>"]` is refused with 400 rather than quietly widened into a full run.
Expand Down Expand Up @@ -7777,16 +7804,18 @@ paths:
only:
type: array
minItems: 1
maxItems: 8
maxItems: 9
description: >-
Which steps to run, each at most once. Omitted means all eight, in this order. `groups`
and `names` (since v0.47.0) do nothing unless group upgrades, or chapter-name borrowing,
are switched on (groupUpgrade and borrowNames on PATCH /api/admin/settings).
Which steps to run, each at most once. Omitted means all nine, in this order. `groups`,
`names` (since v0.47.0) and `details` (since v0.73.0) do nothing unless group upgrades,
chapter-name borrowing or Fill in missing details are switched on (groupUpgrade,
borrowNames and fillDetails on PATCH /api/admin/settings) -- `details` then reports
`details: {asked, filled}`, and `details: {off: true, asked: 0, filled: 0}` while it is off.
`directions` (since v0.48.0) asks MangaDex and AniList which way series with no known
reading direction read, at most REPAIR_DIRECTIONS_MAX (500) series per service.
items:
type: string
enum: [solver, count, failures, short, gaps, groups, names, directions]
enum: [solver, count, failures, short, gaps, groups, names, directions, details]
seriesId:
type: string
description: >-
Expand Down Expand Up @@ -12146,6 +12175,8 @@ paths:
followed: { type: integer, minimum: 0 }
withBackup: { type: integer, minimum: 0 }
lastTestedAt: { type: [string, "null"], format: date-time, description: The last deliberate live check (Test or the daily check). }
ageRating: { type: [integer, "null"], enum: [18, null], description: "Since v0.73.0: 18 when the admin rated the whole source Adult only (PUT /api/admin/sources/{id}/age-rating), else null." }
defaultAgeRating: { type: [integer, "null"], enum: [18, null], description: "What the source is held to without that rating: 18 for an extension that declares itself adult, else null. Either 18 refuses the source to an account limited below 18." }
icon: { type: boolean, description: "GET /img/sources/icon/{id} serves an extension's logo." }
address: { type: string, description: A site added by address only. }
overLimit:
Expand Down Expand Up @@ -12265,6 +12296,57 @@ paths:
description: "Forbidden: token scope, account permission or age limit"
"404":
description: Not found, or not visible to this account
/api/admin/sources/{id}/age-rating:
put:
tags:
- admin
summary: Rate a whole source Adult only, or take the rating off.
operationId: putApiAdminSourcesIdAgeRating
description: >-
Since v0.73.0. Requires an admin account and a token carrying the admin scope. `ageRating` 18 rates the
source Adult only: an account limited below 18 is refused it everywhere -- the by-id routes included
(latest, popular, detail, add, preview) -- as it is refused a source whose extension declares itself adult,
and while a request does not carry adult=1 it leaves Discover, search and the latest and popular lists.
`null` takes the rating off, so the extension's own flag decides again. Nothing rates a source below its
extension's flag. Stored in the settings' adultSources, so the two are one list. The id need not be loaded
now (an extension's source is registered only while the engine is up). Audited as `source.age_rating`
{source, ageRating}.
parameters:
- name: id
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
required: [ageRating]
properties:
ageRating: { type: [integer, "null"], enum: [18, null], description: "18 = Adult only; null = the source's default." }
security:
- bearer: []
responses:
"200":
description: Saved.
content:
application/json:
schema:
type: object
required: [ok, ageRating]
properties:
ok: { type: boolean }
ageRating: { type: [integer, "null"] }
"400":
description: "`bad_request`: not 18 or null, or an id that is not a source id"
"401":
description: Not authenticated
"403":
description: "Forbidden: token scope, account permission or age limit"
"409":
description: "`too_many`: 200 sources are rated Adult only already. With `message` and `messageSaid` (`rating.tooMany`)."
/api/admin/sources/{id}/{action}:
post:
tags:
Expand Down
4 changes: 2 additions & 2 deletions bff/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion bff/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "yomi-bff",
"version": "0.72.0",
"version": "0.73.0",
"private": true,
"type": "commonjs",
"engines": {
Expand Down
Loading
Loading