Commit Graph
37 Commits
Author SHA1 Message Date
Kayashov.SMandClaude Sonnet 5 513eac8f80 Fall back to title+year matching for Sonarr tvdbId - the tmdb: prefix isn't recognized
Build and Deploy / build-and-deploy (push) Successful in 2m3s
Diagnostic logging showed lookupByTmdbId (GET /series/lookup?term=tmdb:{id})
returns zero results even for a well-known show on this Sonarr
instance - the "tmdb:" term prefix simply isn't recognized here, not a
missing TheTVDB cross-reference. Falls back to a plain keyword search
by title, picking the first candidate whose year is within 1 of ours
(title alone is unreliable since Sonarr's TheTVDB-backed provider may
not carry the same localized title we store).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 00:49:30 +04:00
Kayashov.SMandClaude Sonnet 5 035e4c911e Log Sonarr's tmdbId lookup response for diagnosis
Build and Deploy / build-and-deploy (push) Successful in 2m14s
The new lookup returned nothing for at least one real series, which
its 422 message correctly reported - but with no way to tell from the
logs whether Sonarr genuinely has no TheTVDB match or the term/response
shape assumption is wrong. Logs the raw first result (or lack of one)
and no longer lets a lookup-request exception propagate as an
unrelated 500.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 00:43:44 +04:00
Kayashov.SMandClaude Sonnet 5 e14f3c3dc2 Fix Sonarr add failing with "Tvdb Id must be greater than 0"
Build and Deploy / build-and-deploy (push) Successful in 2m5s
addToLibrary always passed null for a series' tvdbId (we only ever
capture tmdbId), and Sonarr's add endpoint hard-rejects a missing/
zero tvdbId with a 400 - so every single series add was failing.
Sonarr's own /series/lookup?term=tmdb:{id} resolves our tmdbId
through its TheTVDB-keyed metadata provider first; if it finds
nothing, the request fails fast with a clear message instead of
silently sending tvdbId=0 again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 00:38:07 +04:00
Kayashov.SMandClaude Sonnet 5 e7463c71b2 Generalize detail modal action buttons beyond the blacklisted-only case
Build and Deploy / build-and-deploy (push) Successful in 2m11s
The previous "orphaned" condition (blacklisted && not in library) was
too narrow - a title reached by drilling into another release's
recommends list, or via manual search, is very often not blacklisted
at all, just not something you've acted on yet, and still had no way
to act on it without leaving the modal. Now mirrors MediaCard's own
logic generally, by status/watched combination:
 - IN_LIBRARY: badge only, same as everywhere else.
 - not in library, already watched: "Добавить в Radarr/Sonarr" +
   blacklist toggle, but no "Уже смотрел" (already true).
 - not in library, not watched: all three actions.

The blacklist toggle is now two-way ("Не предлагать" / "Добавить в
рекомендации" depending on current state), matching normal cards
instead of only ever offering the un-hide direction.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 00:30:52 +04:00
Kayashov.SMandClaude Sonnet 5 2188458646 Make watched titles full recompute sources, not just a scoring weight
Build and Deploy / build-and-deploy (push) Successful in 2m14s
The original design ("Rework source model: library membership drives
sources, watched is a weight only") meant marking something "Уже
смотрел" without owning it never seeded its own TMDB/Kinopoisk
discoveries at all - it only ever showed up as a candidate itself, or
boosted OTHER sources' weight if it happened to already be a source
via ownership. That contradicts the actual intent behind watched
tracking: it should build the recommendation graph, not just softly
exclude a title from Рекомендации. Sources are now IN_LIBRARY ∪
REMOVED ∪ every WatchSignal.watched=true title regardless of status.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 23:50:57 +04:00
Kayashov.SMandClaude Sonnet 5 e2caeab70f Show duplicate action buttons in the detail modal for orphaned (blacklisted) titles
Build and Deploy / build-and-deploy (push) Successful in 2m21s
Being blacklisted is the only way a title becomes invisible on both
Рекомендации (excluded there) and Просмотрено (excluded from its
normal view too) at once - such a title can still be reached via
modal navigation (clicking through another release's "recommends"
list), so it needs its own way back in. When the currently displayed
title is blacklisted and not already in the library, the modal now
shows the same three actions as a normal card: "Добавить в Radarr/
Sonarr" (flips to IN_LIBRARY on success, same as everywhere else),
"Добавить в рекомендации" (un-blacklists it), and "Уже смотрел".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 18:39:45 +04:00
Kayashov.SMandClaude Sonnet 5 6ba1424557 Add on-demand per-release recommendation fetch, clickable navigable detail modal
Build and Deploy / build-and-deploy (push) Successful in 2m0s
MediaEntity gets onDemandRecommendationsFetchedAt, gating a new
RecomputeService.fetchOnDemand(id) that runs the same TMDB+Kinopoisk
logic as a normal recompute for exactly one source (extracted the
TMDB per-source body out of the batch loop so both can share it),
persisting ordinary Intersection rows - so once found, a title's
recommendations behave identically everywhere (badges, scoring) with
no special-casing. Gated as a single flag regardless of outcome, so
re-opening the same detail modal never repeats the external calls.

New POST /api/media/{id}/fetch-recommends drives a button in the
detail modal that only appears when a release has no computed
recommendations yet. The modal's "this release recommends" list now
shows a small poster thumbnail per row and is clickable: clicking
re-renders the same modal for that title instead of closing it,
recursively applying the same empty-state/fetch-button logic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 17:31:53 +04:00
Kayashov.SMandClaude Sonnet 5 bc9909418e Add Просмотрено counter, make "hidden" an exclusive view, show what a release recommends
Build and Deploy / build-and-deploy (push) Successful in 2m6s
- Просмотрено gets the same filtered/total counter already on Рекомендации.
- "Показать скрытые" replaced its previous additive behavior (hidden titles
  layered on top of the normal list) with an exclusive "Только скрытые"
  toggle: off shows the normal list with blacklisted titles excluded, on
  replaces it with just the blacklisted titles so they can be reviewed and
  un-hidden without wading through everything else.
- The shared detail modal now shows what a release itself recommends (its
  own Intersection rows) with a provider badge per pair, via a new
  GET /api/media/{id}/recommends. Only meaningful for sources (Просмотрено
  items); Рекомендации cards aren't sources today, so this comes back empty
  for them until an on-demand per-card fetch (discussed, not yet built).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 16:38:14 +04:00
Kayashov.SMandClaude Sonnet 5 9aaced55e3 Add a filtered/total count to the Recommendations page
Build and Deploy / build-and-deploy (push) Successful in 4m35s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 12:33:50 +04:00
Kayashov.SMandClaude Sonnet 5 dfc58e0bdf Merge dual-source badge into one, split source filter into exclusive options
Build and Deploy / build-and-deploy (push) Successful in 2m8s
Three separate badges (TMDB, Kinopoisk, "found by both") for a title
confirmed by both providers looked cluttered - now it's a single badge
("TMDB + Кинопоиск") when there's more than one, or the lone provider's
badge otherwise.

The Recommendations source filter's "TMDB"/"Kinopoisk" buttons now mean
"found by this provider only", with a new third "Оба источника" button
for titles both providers agreed on - mutually exclusive and together
covering every discoverySources value, instead of the previous
inclusive filter that couldn't isolate single-provider matches.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 01:18:02 +04:00
Kayashov.SMandClaude Sonnet 5 f7ebe91fc1 Search Kinopoisk by imdbId directly instead of confirming via detail-fetch
Build and Deploy / build-and-deploy (push) Successful in 1m56s
Revised per feedback: Kinopoisk's "films by filter" endpoint
(/api/v2.2/films?imdbId=...) accepts a direct imdbId lookup and returns
imdbId on each result - no need to search by keyword and then spend
extra requests confirming candidates one by one. Resolution is now two
independent OR'd attempts (never one request ANDing both signals):
searchByImdbId first when the source has one, falling back to
searchByKeyword + the title/year heuristic only when there's no imdbId
or that search comes back empty. This is also cheaper (1-2 requests
per source instead of up to 4).

Per-source outcomes are logged at DEBUG (successes) and DEBUG (misses,
with a plain-English reason) rather than one INFO line per source -
meaningful per-request logging without burying the phase summary INFO
line under dozens/hundreds of lines every run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 00:56:56 +04:00
Kayashov.SMandClaude Sonnet 5 ce6dc533dc Confirm Kinopoisk candidates by imdbId before falling back to title matching
Build and Deploy / build-and-deploy (push) Failing after 1m34s
The unmatched-search diagnostics showed the real problem with pure
title matching: Kinopoisk is a Russian-language catalog, so comparing
only against nameRu missed titles we store in English/original form
(e.g. "The Legend of Korra" vs "Легенда о Корре" - same year, same
film, zero string overlap). We already capture imdbId from Radarr/
Sonarr, and Kinopoisk's own film-details endpoint (/films/{id}) returns
imdbId too - so up to 3 year-plausible search candidates now get a
details fetch to confirm by that real external key first, falling back
to the (now also more lenient: checks nameRu OR nameEn, tolerates a
missing/zero year, allows substring containment) title heuristic only
when no imdbId match is found.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 23:00:05 +04:00
Kayashov.SMandClaude Sonnet 5 5a730227b7 Sample unmatched Kinopoisk search results for diagnosis
Build and Deploy / build-and-deploy (push) Successful in 2m0s
The rate-limit fix revealed a new mystery: all 126 candidates spent
exactly one request each and zero matched, with no errors at all -
meaning Kinopoisk's search returned 200 OK but either empty results or
results our title+year heuristic rejected for literally every title.
Log a few samples of "search succeeded but nothing matched" (our title
vs its first hit) to tell which case it actually is, without needing
the raw API key.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 22:39:59 +04:00
Kayashov.SMandClaude Sonnet 5 238abc05ad Fix Kinopoisk 429s by pacing requests instead of treating them as daily quota
Build and Deploy / build-and-deploy (push) Successful in 2m4s
Diagnostic logging from the previous commit showed the real cause of
zero Kinopoisk intersections: HTTP 429 after only 14 unpaced requests,
not the 490/day budget being exhausted. 429 (a short per-second rate
limit) and 402 (Payment Required, the actual daily quota) were being
treated identically, aborting the whole phase on the very first burst.

Every Kinopoisk request is now paced (~1.1s apart), and a lone 429 gets
one backoff-and-retry before being treated like a real quota-exhausted
402 - so a brief rate-limit blip no longer kills the entire run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 22:31:00 +04:00
Kayashov.SMandClaude Sonnet 5 f7bff755c0 Log Kinopoisk request failures instead of swallowing them silently
Build and Deploy / build-and-deploy (push) Successful in 2m12s
KinopoiskClient degraded every non-quota error to an empty result with
no trace at all, making it impossible to tell from the logs why zero
intersections were ever recorded (bad key permissions on a specific
endpoint vs. no title/year match vs. something else). Also logs a
one-line summary of each recompute's Kinopoisk phase (candidates,
resolved ids, requests spent, new intersections).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 22:15:40 +04:00
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