processedSources was only incremented during the TMDB phase, so once
that finished the progress bar showed 100% while the Kinopoisk phase
(which can take a while due to per-request rate limiting) kept running
underneath, making the "Пересчитать" button look stuck. The Kinopoisk
candidate list is now computed upfront and folded into the total so
100% isn't reached until the whole recompute is actually done.
Also add an independent "Найдено через" filter (TMDB/Kinopoisk) on the
Recommendations page, AND'ed with the existing type filter, so it's
possible to check whether Kinopoisk found anything at all.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Matches the header logo's light chip so the navy strokes stay legible
in the browser tab too, instead of floating on transparent/dark tab UI.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Recreated the provided logo as SVG (navy concentric play-button rings
branching into orange distribution nodes) - used as the browser tab
favicon and in the header brand, the latter on a small light chip so
the navy strokes stay legible on the dark theme.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Kinopoisk's /similars endpoint is queried per source alongside TMDB's
recommendations/similar, resolved back to MediaEntity by title+year and
cached (kinopoiskId/kinopoiskCheckedAt) to respect its free-tier daily
quota (configurable per-integration requestLimitPerDay, throttled by
ScoringConfig.kinopoiskRefreshDays). A pair confirmed by both providers
upgrades its single Intersection row to via=BOTH instead of duplicating
it, and gets a relevance boost (ScoringConfig.multiSourceBoost) plus a
"found via" badge on the card.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- TmdbClient.searchMulti() hits /search/multi (language=ru-RU), per-item
media_type distinguishes movie/tv so each result gets its own MediaType
(with Animation-genre detection same as recompute).
- ScoringService.search(): local title-substring match (new
MediaEntityRepository.findByTitleContainingIgnoreCase) merged with new TMDB
hits, lazily persisted via the existing findOrCreateFromTmdb cache so every
result is a real MediaEntity and all existing card actions just work.
Extracted the DTO-mapping body of getWatched() into a shared toBasicCard()
used by both.
- New GET /api/search?q=... (SearchController) and pages/Search.jsx (debounced
input, reuses MediaCard). MediaCard now shows a plain "В библиотеке" badge
instead of Add/Blacklist buttons when a search hit is already owned.
- Просмотрено page: replaced the single ALL/IN/OUT radio with three
independent checkbox filter groups (type; Скачано/В библиотеке/Не в
библиотеке; Есть оценка/Нет оценки) that AND together, plus a "Сбросить
фильтры" button - sort and "Показать скрытые" are left untouched by reset.
- MediaService now actually populates MediaEntity.imdbId from Radarr/Sonarr's
own API responses (both already return it for free) - foundation for a
future IMDB/Kinopoisk pass, no behavior change yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Hibernate 6 auto-generates a DB CHECK constraint listing an enum's values the
first time an @Enumerated(STRING) column is created. ddl-auto=update never
refreshes that constraint when the Java enum gains a value later, so it goes
stale. This bit two columns already:
- media_entity_type_check still only allowed MOVIE/CARTOON/SERIES, so every
insert of a CARTOON_SERIES row (i.e. any Sonarr series with the Animation
genre) violated it and aborted the whole Sonarr sync loop silently
(caught by RecomputeService's per-source try/catch) - explains why newly
added animated shows, and everything after them in the sync, never made
it into media_entity.
- media_entity_status_check still only allowed the old WATCHED_REMOVED
value, so every attempt to set status=REMOVED (webhook-driven removals,
markRemovedIfMissing) has been silently failing since that rename - zero
REMOVED rows existed in the live database.
Manually dropped both stale constraints on the live DB (data-safe, purely
removes an incorrect restriction). Added explicit columnDefinition to every
@Enumerated column in the codebase so Hibernate stops synthesizing these
constraints at all - ddl-auto has no mechanism to keep them in sync with the
Java enum, so it must never create one to begin with.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RecomputeService now tracks running/processed/total sources and the last
run's new-intersection count via atomics (recompute() is already
synchronized to one run at a time, so plain shared state is enough). New
GET /api/recompute/status exposes it. The header polls this every second
while a recompute is in flight (whether triggered by the button or an
already-running scheduled job picked up on page load) and shows a progress
bar + "processed / total" counter instead of just a static "Пересчёт..."
label.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same issue as the earlier title/overview refresh: findOrCreateFromTmdb only
sets type on brand-new rows, so titles discovered before CARTOON_SERIES
existed (or whose animation genre wasn't recorded yet) stayed SERIES/MOVIE
forever even after the type-detection fix landed. Reclassify on every
recompute alongside title/overview, skipping anything the user has manually
overridden.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Previously every Sonarr-sourced title (and every found-via-TMDB series) was
typed plain SERIES with no way to flag it as animated, and any "is this a
series" check only tested `== MediaType.SERIES`. Add MediaType.CARTOON_SERIES
(mirrors CARTOON's Radarr-side "Animation" genre detection, but for Sonarr)
plus MediaType.isSeries() so every add-to-library/quality-profile check
treats CARTOON_SERIES the same as SERIES (Sonarr), never Radarr. Enum values
map to a varchar column, so no ddl-auto migration risk here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rating an unwatched candidate doesn't make sense - personal ratings now only
live on the "Просмотрено" page, for things actually in the library or
already watched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Previously a title stayed NEVER_HAD until the next full recompute's library
sync, so it kept showing in Recommendations right after being added. Now
RadarrClient.addMovie/SonarrClient.addSeries return the created resource (its
new id), and MediaActionController stores that id and sets status=IN_LIBRARY
right on the 2xx response - RestClient throws on non-2xx, so this only
happens on genuine success. Drops the title out of Recommendations and into
"Просмотрено" instantly, no recompute needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
findOrCreateFromTmdb only sets title/overview on brand-new rows, so titles
found before ru-RU localization was added stayed in English forever and
never picked up an overview even after a recompute. Now refresh both on
every recompute for non-IN_LIBRARY rows (library items keep their
Radarr/Sonarr-sourced title, already refreshed by the sync step this same
recompute run).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- MediaEntity gains overview + hasFile (informational only, doesn't affect
IN_LIBRARY status); Snooze gains a permanent `blacklisted` flag alongside
the existing temporary snoozedUntil; ScoringConfig gains
rewatchInsertInterval. All new NOT NULL columns use @ColumnDefault to avoid
repeating the watched_boost startup crash on the already-populated tables.
- TmdbClient now captures `overview` and requests language=ru-RU so found
titles get Russian title/overview when TMDB has a translation.
- ScoringService: getRecommendations() now excludes already-watched titles
from the main feed and instead splices one rewatch suggestion (a watched
title no longer in the library) in every rewatchInsertInterval items,
flagged for a distinguishing badge. Replaced getWatchAgain() with
getWatched(includeHidden) for the new "Просмотрено" page: everything
currently in the library (even unwatched - it can't be re-added anyway)
plus everything ever watched, with blacklisted titles surfaced only when
includeHidden is requested (for the "Вернуть" undo action).
- MediaActionController: new /watched (mark watched, no status change),
/blacklist and /unblacklist endpoints.
- Frontend: removed the separate "Посмотреть ещё раз" page/route in favor of
the rewatch-suggestion badge inline in Recommendations; added the
"Просмотрено" page (library/in-library filter, sort, hidden-blacklist
toggle); MediaCard/LibraryCard get a click-to-open MediaDetailModal showing
the overview text; hid the inherited-rating badge when it's 0; added an
intersection-count badge; added a type filter on Recommendations.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ddl-auto=update generated "ALTER TABLE scoring_config ADD COLUMN
watched_boost ... NOT NULL" with no default, which Postgres rejects on the
already-populated singleton config row ("column contains null values").
That aborted the migration, so every later query against scoring_config
failed with "column watched_boost does not exist" - including
/api/recommendations. @ColumnDefault backfills the existing row instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Previously "watched >=90%" was a hard gate for being a source (in addition
to library items), and Radarr sync only marked something IN_LIBRARY when
hasFile was true - both wrong per the corrected spec:
- anything ever added to Radarr/Sonarr is a source forever, regardless of
whether the file has finished downloading
- removal from the library flips it to REMOVED (renamed from
WATCHED_REMOVED) but it stays a source and keeps feeding intersections -
REMOVED only drives the "Watch Again" feed now
- being watched (and rewatch count) no longer gates source membership at
all; it's purely a scoring weight boost via the new watchedBoost config,
stacked with the existing rewatch amplifier
Also added MediaService.markRemovedIfMissing(), called from
RecomputeService's library sync, so a title that drops out of Radarr/Sonarr's
listing gets flagged REMOVED even if its delete webhook was missed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This server (Jellyfin 12.0.0) rejects X-Emby-Token, X-MediaBrowser-Token,
a raw Authorization value, and Authorization: Bearer <token> - all with
401 - even against a freshly generated API key confirmed valid via curl.
Its OpenAPI spec names Authorization as the apiKey header, but the only
value it actually accepts is the legacy 'MediaBrowser Token="<key>"'
scheme, confirmed working via curl against the live server.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
buildx runs its own BuildKit daemon with a separate insecure-registry
config that act-runner-docker-config doesn't reach, so the push failed
with "server gave HTTP response to HTTPS client" even though the image
built fine and login succeeded. The classic docker engine in the dind
runner already trusts this registry, so use it directly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
git.kayashov.keenetic.pro is the KeenDNS HTTPS proxy in front of Gitea's web
UI - its cert only covers one subdomain level and it doesn't route /v2/...
registry API traffic, so docker login got back the proxy's own 404 HTML page.
The real registry address is the NodePort 192.168.1.100:30008, already set as
insecure-registry in act-runner-docker-config (dind push) and
/etc/rancher/k3s/registries.yaml (containerd pull) - use that everywhere.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Workflow referenced GITEA_REGISTRY_USER/GITEA_REGISTRY_TOKEN/KUBECONFIG_DATA,
but the account's global secrets are named REGISTRY_USER/REGISTRY_TOKEN/
KUBE_CONFIG, so docker/login-action got empty credentials. Also drop the
base64 decode for KUBE_CONFIG since it's stored as plain kubeconfig YAML.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Gitea Actions runner image has no system Maven (setup-node/setup-java
only install those tools, not Maven), so the pipeline failed at 'mvn:
command not found'. Switch the build step to ./mvnw and pin the wrapper's
line endings/executable bit via .gitattributes so it survives Windows
checkouts intact.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Spring Boot 3.3 backend + React frontend bundled into one jar, Postgres via
Hibernate ddl-auto, Radarr/Sonarr/Jellyfin/Plex/TMDB integrations with
runtime-editable settings, webhook-driven batch recompute, and k8s/Gitea CI-CD
for deployment into the arr namespace on k3s.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>