Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
34009f7
feat: Cleanups, refactors
gallayl Mar 10, 2026
6e544c7
Merge branch 'develop' into feat/cleanups-refactors
gallayl Mar 10, 2026
a8c48b4
entity sync scope fix
gallayl Mar 10, 2026
fd5cce9
TMDB integration
gallayl Mar 11, 2026
40d6fde
eslint ignore glob updates
gallayl Mar 11, 2026
31ad5f1
fs update, eslint config fix, file list fixes
gallayl Mar 11, 2026
814fe7f
legacy navigation fixes
gallayl Mar 11, 2026
3a07b93
encoding fix
gallayl Mar 11, 2026
a7f246b
fixes and improvements
gallayl Mar 11, 2026
d7e6764
ffmpeg session teardown fix
gallayl Mar 11, 2026
971d043
movie duration fix
gallayl Mar 11, 2026
e9fbffc
improved playback quality controls
gallayl Mar 11, 2026
09e2037
improvements on quality switching
gallayl Mar 11, 2026
98d6900
seeking improvements
gallayl Mar 11, 2026
479c772
cleanup, refactorings
gallayl Mar 11, 2026
31720ed
code review fixes
gallayl Mar 11, 2026
d4e0e34
code review fixes, version bumps, changelogs
gallayl Mar 11, 2026
78bff72
playback fixes
gallayl Mar 11, 2026
1bfcc5f
added fast build option without monaco
gallayl Mar 12, 2026
44888fc
fire-and-forget fixes
gallayl Mar 12, 2026
ccac2b1
Native Shades video controls, code review fixes
gallayl Mar 12, 2026
38779d5
code review fixes
gallayl Mar 12, 2026
ddcad84
video player fixes
gallayl Mar 12, 2026
5bbe976
folder panel fixes
gallayl Mar 12, 2026
e5c0d6f
config watcher and metadata service fixes
gallayl Mar 12, 2026
d8e4e81
metadata getter fixes
gallayl Mar 12, 2026
a2584cf
progress
gallayl Mar 27, 2026
8e384ca
code review fixes
gallayl Mar 27, 2026
afcce07
cr fixes #2
gallayl Mar 27, 2026
9eabc27
e2e fix
gallayl Mar 27, 2026
03706a0
typo fix
gallayl Mar 27, 2026
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
88 changes: 88 additions & 0 deletions .cursor/rules/ASYNC_PATTERNS.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
alwaysApply: false
---
# Async Patterns

## Fire-and-Forget Promises Must Handle Errors

Every fire-and-forget `void` promise call must include a `.catch()` that logs the error. An unhandled async rejection is a silent bug.

```typescript
// ✅ Good — error is logged
void this.evictByDiskUsage().catch((error) => {
void this.logger.error({ message: 'Failed to evict by disk usage', data: { error } })
})

// ✅ Good — singleton init pattern (see SINGLETON_CONCURRENCY.md)
void this.initAsync().catch((error) => {
void this.logger.error({ message: 'Failed to initialize service', data: { error } })
})

// ❌ Bad — error silently lost
void this.evictByDiskUsage()

// ❌ Bad — empty catch swallows errors
void this.evictByDiskUsage().catch(() => {})
```

### Exception: Browser `video.play()`

Browser `HTMLMediaElement.play()` rejects with `AbortError` when navigation interrupts playback. This is benign and expected. An empty `.catch(() => {})` is acceptable **only** with an explanatory comment.

```typescript
// ✅ Acceptable — benign browser rejection documented
// play() can reject with AbortError when navigation interrupts playback; this is benign
void video.play().catch(() => {})
```

### When `void` logger calls are fine

Logger methods return promises but are fire-and-forget by design. `void this.logger.verbose(...)` is the correct pattern — do not `await` logger calls in hot paths.

### When `void` in frontend event handlers is fine

Sync DOM callbacks (`onclick`, `onsubmit`, `ondrop`) cannot be `async`. Using `void asyncFn()` is correct when the called function handles its own errors internally (e.g., shows user-facing error notifications).

## Prefer `fs/promises` in Async Contexts

When inside an `async` function, use `fs/promises` instead of sync `fs` methods to avoid blocking the Node.js event loop.

```typescript
// ✅ Good — non-blocking in async context
import { mkdir, stat, readdir, rm } from 'fs/promises'
import { existsAsync } from '../../../utils/exists-async.js'

async function ensureDir(dir: string) {
if (!(await existsAsync(dir))) {
await mkdir(dir, { recursive: true })
}
}

// ❌ Bad — blocks event loop in async context
import { existsSync, mkdirSync } from 'fs'

async function ensureDir(dir: string) {
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true })
}
}
```

### Mapping sync → async

| Sync | Async equivalent |
| ------------------------ | -------------------------------------------------- |
| `existsSync(p)` | `existsAsync(p)` (from `service/src/utils/exists-async.ts`) |
| `statSync(p)` | `stat(p)` from `fs/promises` |
| `mkdirSync(p, opts)` | `mkdir(p, opts)` from `fs/promises` |
| `readdirSync(p, opts)` | `readdir(p, opts)` from `fs/promises` |
| `rmSync(p, opts)` | `rm(p, opts)` from `fs/promises` |
| `readFileSync(p, enc)` | `readFile(p, enc)` from `fs/promises` |
| `writeFileSync(p, data)` | `writeFile(p, data)` from `fs/promises` |

### When sync FS is acceptable

- **Sync-only API contracts:** Vite middleware, Vite build hooks, Express middleware
- **Test setup/teardown:** `beforeEach` / `afterEach` with small temp directories
- **One-time cached helpers:** Sync helper that runs once and caches the result (e.g., `getBaseDir()`)
- **Synchronous process cleanup:** `destroySession()` where the caller needs the directory removed before the reference is dropped
33 changes: 33 additions & 0 deletions .cursor/rules/BACKEND_PATTERNS.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,39 @@ service/src/app-models/[module]/
- **Clean up**: Remove partial state on failures
- **Log appropriately**: Use scoped loggers with meaningful context

### Provider / Adapter Fallback Chains

When building fallback chains where multiple providers are tried in priority order:

- **Use a narrow internal result type** for provider helpers, not the top-level result type
- Providers should only return what they can produce (e.g., an `imdbId`), not dummy values for fields they don't have
- The orchestrator function creates the full result from the provider's narrow output

```typescript
// ✅ Good - narrow type for internal helpers
type ProviderResult =
| { status: 'skip' }
| { status: 'rate-limited' }
| { status: 'linked'; imdbId: string }

const tryProvider = async (...): Promise<ProviderResult> => {
// ...
return { status: 'linked', imdbId: result.imdbID }
}

// The orchestrator builds the full result
for (const provider of priority) {
const result = await providers[provider](injector, params)
if (result.status === 'linked') {
linkedImdbId = result.imdbId
break
}
}

// ❌ Bad - reusing the top-level type forces dummy values
return { status: 'linked', movieFile: undefined as unknown as MovieFile, movie }
```

### Service Layer

```typescript
Expand Down
7 changes: 7 additions & 0 deletions .cursor/rules/SCRIPT_EXECUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ yarn create-schemas # Generate schemas from API definitions
yarn clean # Clean build artifacts
```

> **WARNING -- never run `tsc`, `tsc -b`, or `tsc --build` directly.**
> The project uses `"composite": true` with project references, so bare
> `tsc -b` emits `.js`, `.d.ts`, and `.js.map` files **into the source
> tree**. Always use `yarn build` instead. If you only need a type check
> without emitting, use the built-in linter tools (ReadLints) or run a
> targeted `vitest` suite -- do NOT run tsc in any form.

**Testing:**

```bash
Expand Down
2 changes: 2 additions & 0 deletions .cursor/rules/SINGLETON_CONCURRENCY.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ Apply this pattern when **all** of the following are true:

## Fire-and-Forget Async Initialization

> See also [ASYNC_PATTERNS.mdc](./ASYNC_PATTERNS.mdc) for the broader rule on fire-and-forget promises and `fs/promises` usage.

Singleton services that need async initialization (e.g., loading config from a database, connecting to external APIs) should **not** block startup. Use a synchronous `init()` that kicks off the async work and logs errors.

### The Problem
Expand Down
24 changes: 24 additions & 0 deletions .cursor/rules/TYPESCRIPT_GUIDELINES.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,30 @@ const value = 'hello' as string;
const value = unknownValue as User; // Prefer type guard
```

### Double-Cast Anti-Pattern (`as unknown as T`)

**NEVER** use `undefined as unknown as T` or `null as unknown as T` to satisfy a type contract.
This hides a design flaw — if a function doesn't have the value, the return type should reflect that.

```typescript
// ❌ FORBIDDEN - double-cast to satisfy type
return { status: 'linked', movieFile: undefined as unknown as MovieFile, movie };

// ✅ Good - use a narrower return type that doesn't require the value
type ProviderResult =
| { status: 'skip' }
| { status: 'rate-limited' }
| { status: 'linked'; imdbId: string };

return { status: 'linked', imdbId: added.imdbID };

// ✅ Good - if the caller needs different shapes, use discriminated unions
type InternalResult = { status: 'linked'; imdbId: string };
type FullResult = { status: 'linked'; movieFile: MovieFile; movie: Movie };
```

If you encounter `as unknown as T`, refactor the types so the cast is unnecessary.

### Non-Null Assertion Operator

- Avoid using `!` operator when possible
Expand Down
3 changes: 3 additions & 0 deletions .cursor/rules/rules-index.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ This file contains a list of helpful information and context that the agent can
- [REST action Validate() wrappers and explicit param checking before system calls](./REST_ACTION_VALIDATION.mdc)
- [Singleton concurrency guards, getOrCreate race prevention, fire-and-forget init, dispose-before-reinit, and safe Map iteration](./SINGLETON_CONCURRENCY.md)
- [MFE runtime type boundaries, duplicate type contracts, and cross-package type sync](./MFE_TYPE_CONTRACTS.md)
- [Double-cast anti-pattern (`as unknown as T`) and non-null assertion avoidance](./TYPESCRIPT_GUIDELINES.mdc)
- [Provider/adapter fallback chain patterns with narrow internal result types](./BACKEND_PATTERNS.mdc)
- [Fire-and-forget error handling, `fs/promises` in async contexts, and `void` promise patterns](./ASYNC_PATTERNS.mdc)
39 changes: 39 additions & 0 deletions .yarn/changelogs/common.90cc3afc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<!-- version-type: minor -->

# common

## ✨ Features

### TMDB Integration Models

- Added `TmdbMovieMetadata` model with fields for TMDB movie details including genres, vote data, production info, and multi-language support
- Added `TmdbSeriesMetadata` model with fields for TMDB series details including season/episode counts and language info
- Added `TmdbConfig` configuration type for TMDB API key, default language, and additional language preferences
- Added `MetadataProviderConfig` type to define an ordered priority list of metadata providers (`omdb` | `tmdb`)

### Localized Metadata Models

- Added `MovieMetadataLocalized` model for per-language movie metadata (title, plot, poster, genre) with source tracking (`omdb` | `tmdb`)
- Added `SeriesMetadataLocalized` model for per-language series metadata with source tracking

### Media API Endpoints

- Added REST endpoints for `TmdbMovieMetadata`, `TmdbSeriesMetadata`, `MovieMetadataLocalized`, and `SeriesMetadataLocalized` entity browsing
- Added `audioTrack` and `startTime` query parameters to HLS master, stream, segment, init, and teardown endpoints to support mid-stream seeking and audio track selection

### Other

- Added `HLS_SEGMENT_DURATION` constant (6 seconds) shared between frontend and service
- Extended `isMovieFile()` to recognize `.mp4` and `.mov` extensions
- Added `tmdb` field to `ServiceStatusResponse` for TMDB API availability checks

## ♻️ Refactoring

- Moved language-dependent fields (`title`, `plot`, `genre`, `thumbnailImageUrl`) from `Movie` and `Series` models into the new localized metadata models
- Renamed `omdb-not-configured` / `omdb-error` link statuses to `provider-not-configured` / `provider-error` to reflect multi-provider support
- Renamed `omdbNotConfigured` / `omdbError` scan progress fields to `providerNotConfigured` / `providerError`

## ⚠️ Breaking Changes

- `LinkMovie` response status strings `omdb-not-configured` and `omdb-error` have been renamed to `provider-not-configured` and `provider-error`
- `ScanProgress` fields `omdbNotConfigured` and `omdbError` have been renamed to `providerNotConfigured` and `providerError`
51 changes: 51 additions & 0 deletions .yarn/changelogs/frontend.90cc3afc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!-- version-type: patch -->

# frontend

## ✨ Features

### TMDB Settings Page

Added admin settings page at `/settings/tmdb` for configuring TMDB API credentials, default language, and additional languages for metadata fetching.

### Localized Metadata Service

Added `LocalizedMetadataService` with caches for fetching `MovieMetadataLocalized` and `SeriesMetadataLocalized` by IMDB ID and language. Movie and series overview pages now display localized titles, plots, posters, and genres from this service.

### Movie Player Seeking and Quality Switching

- Added server-side seek support via `startTime` parameter — the player requests a new HLS session starting at the desired position when seeking outside the buffered range
- Added `switchResolution()` for changing playback quality mid-stream with optional full session reload
- Added `isSwitching` guard to avoid progress updates during resolution/audio/seek transitions

### Entity Browser Pages

- Added entity browser pages for `TmdbMovieMetadata` and `TmdbSeriesMetadata`

## 🐛 Bug Fixes

- Fixed movie duration not displaying correctly by preferring `playbackInfo.duration` over stream duration in seek bar
- Fixed legacy navigation issues by relocating route utilities to `utils/` directory

## ♻️ Refactoring

- Replaced `media-chrome` and `hls.js` dependencies with modular native player controls (`PlayButton`, `SeekBar`, `VolumeControl`, `SettingsMenu`, etc.) under `controls/` directory
- Moved video event binding and playback state (play/pause, volume, duration, buffered) into `MoviePlayerService` observables
- Extracted file context menu items into a pure `getContextMenuItems()` function, replacing the Shade-based `FileContextMenu` component
- Extracted file drag-and-drop upload logic into `handleFileDrop()` utility
- Extracted `SessionUserUnavailableError`, `getUser()`, and `hasRole()` into `utils/session-helpers.ts` for reusable session access
- Relocated `environment-options.ts`, `navigate-to-route.ts`, `trigger-download.ts`, and `theme-switch-cheat.tsx` into `utils/` directory
- Simplified `SessionService.currentUser` to store the full `User` object instead of a partial pick

## 🧪 Tests

- Added tests for `LocalizedMetadataService` cache behavior
- Added tests for `TmdbSettings` page form validation and config persistence
- Added tests for `session-helpers` utilities
- Updated `MoviePlayerService` tests for seeking, resolution switching, and start-time support
- Updated `MoviePlayerV2Component` tests for the new seeking and resolution switching behavior

## ⬆️ Dependencies

- Removed `media-chrome`
- Re-added `hls.js` (^1.6.15) for cross-browser HLS playback support
11 changes: 11 additions & 0 deletions .yarn/changelogs/pi-rat.90cc3afc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!-- version-type: patch -->

# pi-rat

## 📦 Build

- Updated ESLint configuration with refined ignore globs for generated schemas and build artifacts

## ⬆️ Dependencies

- Updated FuryStack framework dependencies
68 changes: 68 additions & 0 deletions .yarn/changelogs/service.90cc3afc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<!-- version-type: patch -->

# service

## ✨ Features

### TMDB Client Service

Added `TmdbClientService` with support for searching and fetching movie/series details from the TMDB API, including multi-language metadata retrieval based on the configured `TmdbConfig` languages.

### Configurable Metadata Provider Chain

The movie-linking pipeline now supports a configurable provider priority (`omdb`, `tmdb`) via `MetadataProviderConfig`. Providers are tried in order; the first one to return a result wins.

### IMDB ID Extraction from Files

- Added `extractImdbIdFromNfoFiles()` — scans `.nfo` files in the parent directory for IMDB IDs (e.g. `tt1234567`), enabling direct metadata lookup without a search API call
- Added `extractImdbIdFromFfprobeTags()` — extracts IMDB IDs from ffprobe format tags (`imdb_id`, `imdb-id`, `imdb`, etc.)

### Localized Metadata Storage

- Added `ensureMovieLocalizedMetadataExists()` and `ensureSeriesLocalizedMetadataExists()` for upserting per-language metadata records
- Added `mapOmdbMovieToLocalized()` / `mapOmdbSeriesToLocalized()` to convert OMDB metadata into localized format (English only)
- Added `mapTmdbMovieToLocalized()` / `mapTmdbSeriesToLocalized()` to convert TMDB responses into localized format for any language

### HLS Start-Time Seeking

- Transcoding sessions now accept a `startTime` parameter, using FFmpeg input seeking (`-ss` before `-i`) to start transcoding from an arbitrary position
- Added `padPlaylistToFullDuration()` to pad HLS playlists for correct total duration when segments don't cover the full file

### Other

- Added `removeAllSessionsForFile()` to tear down every active transcoding session for a given file path
- Added 4K (3840x2160) resolution variant to the HLS manifest generator
- Added `TmdbMovieMetadata`, `TmdbSeriesMetadata`, `MovieMetadataLocalized`, and `SeriesMetadataLocalized` data sets and API endpoints
- Added TMDB status check to the service status endpoint

## 🐛 Bug Fixes

- Fixed HLS session teardown to remove all sessions for a file instead of requiring exact mode/resolution/audioTrack match

## ♻️ Refactoring

### `setup-media.ts` Split

Extracted the monolithic `setup-media.ts` into focused modules:

- `media-sequelize-models.ts` — Sequelize model definitions for all media entities
- `media-data-sets.ts` — Repository data set registration
- `media-schema-setup.ts` — Database schema setup and sync
- `announce-movie-file-added.ts` — WebSocket notification when movie files are added

### Link-Movie Provider Architecture

Refactored `link-movie.ts` from a single OMDB-only flow into a provider-based architecture with `tryOmdbProvider()` and `tryTmdbProvider()` functions, plus an `enrichMetadataFromProviders()` step that fetches additional localized metadata after initial linking.

## 🧪 Tests

- Added tests for `TmdbClientService` covering search, detail fetching, multi-language support, and error/rate-limit handling
- Added tests for `extractImdbIdFromNfoFiles()` and `extractImdbIdFromFfprobeTags()`
- Added tests for `ensureMovieLocalizedMetadataExists()` and `ensureSeriesLocalizedMetadataExists()`
- Added tests for `mapOmdbMovieToLocalized()` and `mapTmdbMovieToLocalized()` / `mapTmdbSeriesToLocalized()`
- Added tests for `ensureTmdbMovieExists()` and `ensureTmdbSeriesExists()`
- Added tests for `HlsManifestGenerator` including 4K variant and `startTime` / `audioTrack` pass-through
- Added tests for `HlsStreamAction` start-time parameter handling
- Updated `TranscodingSession` tests for start-time seeking and playlist padding
- Updated `link-movie` tests for provider chain, NFO extraction, and TMDB fallback
- Updated `HlsSessionTeardownAction` tests for `removeAllSessionsForFile()` behavior
5 changes: 5 additions & 0 deletions .yarn/versions/90cc3afc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
releases:
common: minor
frontend: patch
pi-rat: patch
service: patch
Loading
Loading