Skip to content

refactor: architecture cleanup — service layer, admin split, N+1 fix#89

Merged
arediss merged 7 commits intomainfrom
refactor/architecture-cleanup
Apr 7, 2026
Merged

refactor: architecture cleanup — service layer, admin split, N+1 fix#89
arediss merged 7 commits intomainfrom
refactor/architecture-cleanup

Conversation

@arediss
Copy link
Copy Markdown
Owner

@arediss arediss commented Apr 6, 2026

Summary

Major architecture refactor addressing #88. Transforms the codebase from patch-on-patch spaghetti into a clean, modular provider-driven architecture.

Phase 1 — Extract RequestService

  • Move all business logic from routes/requests.tsservices/requestService.ts
  • Fix inverted dependency: scheduler now imports from services/ not routes/

Phase 2 — Split admin.ts (1380 lines → 12 modules)

  • routes/admin/{settings,services,folderRules,users,logs,jobs,sync,notifications,quality,danger,roles,keywords}.ts
  • 55 route handlers across 12 domain files, RBAC paths preserved

Phase 3 — ArrClient provider abstraction

  • ArrClient interface with: getAllMedia, checkAvailability, findByExternalId, addMedia, searchMedia, getHistoryEntries, getEpisodesNormalized
  • RadarrClient and SonarrClient self-contained in providers/radarr/ and providers/sonarr/ (client.ts + types.ts + index.ts)
  • Registry: getArrClient(), getArrClientForService(), createArrClient(), getServiceTypeForMedia()
  • Zero references to RadarrClient/SonarrClient/RadarrMovie/SonarrSeries outside providers/
  • Adding a new *arr service = create one provider directory, zero changes in routes or business logic

Phase 4 — Generic sync pipeline

  • movieSync.ts + tvSync.ts merged into mediaSync.ts (generic via ArrClient)
  • availabilitySync.ts rewritten with generic syncServiceAvailability()
  • requestSync.ts rewritten — uses getAllMedia() with tags, no unsafe casts
  • keywordSync.ts moved into sync/ module

Phase 5 — MediaService + safeNotify + frontend hooks

  • mediaService.ts: performLiveCheck uses client.checkAvailability() via interface
  • safeNotify/safeUserNotify wrapper replaces 10+ inline .catch() patterns
  • sync.ts (656 lines) split into sync/{helpers,mediaSync,availabilitySync,keywordSync,index}.ts
  • MediaDetailPage.tsx (986→714 lines): extracted useMediaDetailData, useMediaRequestActions, useEpisodeModal hooks + CollectionSection component

Minor fixes

  • radarr-sonarr.ts: errors logged instead of silently swallowed
  • support.ts: parseId instead of parseInt
  • HomePage.tsx: DB-to-TMDB mapper extracted to utils/mediaMapper.ts
  • Dead imports cleaned, Prisma type-safety improved
  • TV sync: proper tvdbId/negative-tmdbId lookup for legacy rows

Documentation

  • docs/providers.md fully rewritten with ArrClient interface, registry API, and step-by-step Lidarr example

Closes #88

Test plan

  • Backend + frontend compile
  • Full sync (1456 series + 2036 Radarr history events) — no errors
  • Request creation → sent to Radarr successfully
  • Media detail page — status, episodes, languages all functional
  • Admin panel — services, profiles, rootfolders, jobs, settings
  • Zero references to provider-specific types outside providers/ directory
  • Code review: 3 rounds of fixes (tautological OR, legacy rows, unsafe casts)

arediss added 2 commits April 6, 2026 16:35
Move all business logic from routes/requests.ts into services/requestService.ts:
- sendToService, sendToRadarr, sendToSonarr, resolveServiceContext
- findOrCreateMedia, getUserTagName, requestCollectionMovie
- retryFailedRequests, promoteStaleStatuses, validateRequestBody

Routes file is now thin HTTP handlers only.
Fix inverted dependency: scheduler now imports from services/ not routes/.
…es 2-5)

Phase 2 — Split admin.ts (1380 lines) into 12 domain modules:
  routes/admin/{settings,services,folderRules,users,logs,jobs,sync,
  notifications,quality,danger,roles,keywords}.ts
  Each module registers routes on the same Fastify instance (RBAC preserved)

Phase 4 — Extract mediaService.ts from routes/media.ts:
  performLiveCheck, performLiveCheckWithTimeout, cacheLanguageData,
  promoteMediaToAvailable — all moved to service layer

Phase 5 — Rewrite requestSync.ts with bulk queries:
  Replace N+1 pattern (findUnique + findFirst per movie/series in nested loops)
  with bulk findMany + in-memory Set lookups + createMany batch insert
  Add utils/batch.ts chunk helper for SQLite parameter limit

Code review fixes:
  - Replace hardcoded status list with COMPLETABLE_REQUEST_STATUSES constant
  - Remove dead getCollection import from requestService
  - Add 'added' field to SonarrSeries interface, remove unsafe double-cast
}
return true;
} catch (err) {
console.error('Failed to send %s "%s" to service:', mediaType, media.title, err);
arediss added 3 commits April 6, 2026 17:42
…Phase A)

Frontend:
- Extract useMediaDetailData, useMediaRequestActions, useEpisodeModal hooks
- Extract CollectionSection into its own component
- MediaDetailPage reduced from 986 to 714 lines (pure JSX + derivation)

Backend:
- Create safeNotify/safeUserNotify wrapper, replace 10 inline .catch patterns
- Split sync.ts (656 lines) into sync/{helpers,movieSync,tvSync,availabilitySync,index}.ts
- Remove dead imports from requestService.ts
- Replace notifyRequesters with safeUserNotify in sync/helpers.ts
Consolidate all sync-related code under services/sync/.
Update imports in scheduler.ts and tmdb.ts routes.
Re-export from sync/index.ts for backwards compatibility.
- Create providers/radarr/{client,types,index}.ts — RadarrClient implements ArrClient
- Create providers/sonarr/{client,types,index}.ts — SonarrClient implements ArrClient
- Add ArrClient interface to providers/types.ts with shared methods
- Add getArrClient, getArrClientForService, createArrClient, getServiceTypeForMedia to registry
- Migrate all 13 consumers from services/radarr.ts and services/sonarr.ts to provider registry
- Eliminate if/else dispatch in admin/services.ts (profiles, rootfolders)
- Delete services/radarr.ts and services/sonarr.ts
}
return true;
} catch (err) {
console.error('Failed to send %s "%s" to service:', mediaType, media.title, err);
arediss added 2 commits April 7, 2026 15:10
…ces in business logic

ArrClient interface extended with:
- getAllMedia() with tags + hasFile fields for generic sync
- checkAvailability() for live check — language extraction moved into providers
- findByExternalId(), addMedia(), searchMedia() for request dispatch
- getHistoryEntries() for normalized availability sync
- getEpisodesNormalized() (optional, TV only)
- Metadata properties: mediaType, serviceType, dbIdField, defaultRootFolder

Sync pipeline unified:
- movieSync.ts + tvSync.ts merged into mediaSync.ts (generic)
- processSingleMedia handles both movie and TV via ArrClient interface
- TV lookup: sequential tvdbId then negative tmdbId fallback (legacy rows)
- Skip items with no valid externalId (tvdbId=0 guard)
- availabilitySync.ts rewritten with generic syncServiceAvailability

Request service unified:
- sendToRadarr + sendToSonarr merged into sendToArrService
- Uses client.findByExternalId/addMedia/searchMedia via interface
- requestSync rewritten — uses getAllMedia() with tags, no unsafe casts

Minor fixes:
- radarr-sonarr.ts: errors now logged instead of swallowed
- support.ts: parseId instead of parseInt
- HomePage.tsx: DB-to-TMDB mapper extracted to utils/mediaMapper.ts

Verification: zero grep results for RadarrClient/SonarrClient/RadarrMovie/SonarrSeries
outside providers/ directory.
…arr example

Complete rewrite covering:
- Provider architecture with client.ts/types.ts/index.ts structure
- Full ArrClient interface reference
- Registry API (getArrClient, createArrClient, getServiceTypeForMedia)
- Step-by-step guide to add a new *arr provider (Lidarr example)
- What works automatically vs what needs additional work
@arediss arediss merged commit 95998e9 into main Apr 7, 2026
6 checks passed
@arediss arediss deleted the refactor/architecture-cleanup branch April 8, 2026 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: codebase quality audit — split monoliths, centralize patterns

2 participants