Commit Graph
22 Commits
Author SHA1 Message Date
Kayashov.SMandClaude Sonnet 5 fcb7ea0d8b Fix recompute progress bar stalling at 100%, add source filter to Recommendations
Build and Deploy / build-and-deploy (push) Successful in 2m20s
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>
2026-09-15 22:10:11 +04:00
Kayashov.SMandClaude Sonnet 5 9587d0c7cf Add light backing to the favicon
Build and Deploy / build-and-deploy (push) Successful in 1m56s
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>
2026-09-15 22:04:28 +04:00
Kayashov.SMandClaude Sonnet 5 5aa15800ef Add app logo/favicon
Build and Deploy / build-and-deploy (push) Successful in 2m1s
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>
2026-09-15 21:57:06 +04:00
Kayashov.SMandClaude Sonnet 5 47429e00fd Add Kinopoisk as a second discovery source for Intersection, with provider badges
Build and Deploy / build-and-deploy (push) Successful in 2m38s
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>
2026-09-15 21:47:58 +04:00
Kayashov.SMandClaude Sonnet 5 92dc2fe527 Add combined local+TMDB search, independent filters on Просмотрено, imdbId foundation
Build and Deploy / build-and-deploy (push) Successful in 2m34s
- 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>
2026-09-15 19:45:14 +04:00
Kayashov.SMandClaude Sonnet 5 3c58006bde Fix silent Sonarr sync failures: stale Hibernate CHECK constraints on enum columns
Build and Deploy / build-and-deploy (push) Successful in 2m6s
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>
2026-09-15 19:20:17 +04:00
Kayashov.SMandClaude Sonnet 5 6997dfb4e0 Add recompute progress bar in the UI header
Build and Deploy / build-and-deploy (push) Successful in 2m2s
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>
2026-09-15 18:41:19 +04:00
Kayashov.SMandClaude Sonnet 5 316d21341e Reclassify existing found media type on recompute too
Build and Deploy / build-and-deploy (push) Successful in 1m56s
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>
2026-09-15 13:54:03 +04:00
Kayashov.SMandClaude Sonnet 5 df8e7b731d Add CARTOON_SERIES media type so animated shows route to Sonarr, not Radarr
Build and Deploy / build-and-deploy (push) Successful in 3m38s
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>
2026-09-15 13:34:12 +04:00
Kayashov.SMandClaude Sonnet 5 5f98be9a46 Remove personal rating input from Recommendations cards
Build and Deploy / build-and-deploy (push) Successful in 2m22s
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>
2026-09-15 12:43:10 +04:00
Kayashov.SMandClaude Sonnet 5 99cfcb1b5a Flip to IN_LIBRARY immediately on successful Radarr/Sonarr add
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>
2026-09-15 12:42:09 +04:00
Kayashov.SMandClaude Sonnet 5 5aa363713a Fix: refresh title/overview on existing found media during recompute
Build and Deploy / build-and-deploy (push) Successful in 1m57s
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>
2026-09-15 12:24:11 +04:00
Kayashov.SMandClaude Sonnet 5 7dc2a4dbb6 Add watched/blacklist actions, Просмотрено page, rewatch insertion, overview modal
Build and Deploy / build-and-deploy (push) Successful in 1m55s
- 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>
2026-09-15 12:04:41 +04:00
Kayashov.SMandClaude Sonnet 5 e31bc87c4f Fix startup crash: add DEFAULT to new watched_boost column
Build and Deploy / build-and-deploy (push) Successful in 1m52s
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>
2026-09-15 10:11:16 +04:00
Kayashov.SMandClaude Sonnet 5 e8c795aebc Rework source model: library membership drives sources, watched is a weight only
Build and Deploy / build-and-deploy (push) Successful in 1m56s
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>
2026-09-15 10:04:40 +04:00
Kayashov.SMandClaude Sonnet 5 faec2a3463 Fix Jellyfin auth: use MediaBrowser Authorization scheme, not X-Emby-Token
Build and Deploy / build-and-deploy (push) Successful in 1m56s
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>
2026-09-14 19:31:45 +04:00
Kayashov.SMandClaude Sonnet 5 5fb05d8bec Fix CI: use plain docker build/push instead of buildx
Build and Deploy / build-and-deploy (push) Successful in 4m37s
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>
2026-09-14 18:56:35 +04:00
Kayashov.SMandClaude Sonnet 5 12df47cf98 Fix CI: point at Gitea's actual insecure registry address
Build and Deploy / build-and-deploy (push) Failing after 4m23s
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>
2026-09-14 18:48:04 +04:00
Kayashov.SM 57d32bb50c Fix CI: KUBE_CONFIG secret is base64-encoded, restore decode step
Build and Deploy / build-and-deploy (push) Failing after 40s
2026-09-14 18:25:44 +04:00
Kayashov.SMandClaude Sonnet 5 7946b7727b Fix CI: use actual user-level secret names
Build and Deploy / build-and-deploy (push) Failing after 40s
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>
2026-09-14 18:25:21 +04:00
Kayashov.SMandClaude Sonnet 5 e530d37dab Fix CI: add Maven Wrapper so build-deploy doesn't need a preinstalled mvn
Build and Deploy / build-and-deploy (push) Failing after 1m57s
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>
2026-09-14 18:11:54 +04:00
Kayashov.SMandClaude Sonnet 5 915be9100b Initial MVP: recommendarr recommendation service
Build and Deploy / build-and-deploy (push) Failing after 1m50s
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>
2026-09-14 18:06:54 +04:00