Skip to content

Add external subtitle playback#44

Merged
ModerRAS merged 2 commits into
masterfrom
feature/subtitle-support
Jul 13, 2026
Merged

Add external subtitle playback#44
ModerRAS merged 2 commits into
masterfrom
feature/subtitle-support

Conversation

@ModerRAS

@ModerRAS ModerRAS commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • discover and persist same-stem external subtitle sidecars
  • import MLIP v1/v2 media_subtitle entries without WebDAV directory scans
  • attach external subtitle tracks to ExoPlayer and embedded mpv playback
  • migrate Room index storage and bump the release baseline to 1.1

Validation

  • ./gradlew test --no-daemon
  • ./gradlew :app:assembleDebug --no-daemon
  • focused MlipLibraryIndexImporterTest

Note

./gradlew lint remains blocked by the pre-existing API 29 MediaStore.Downloads.EXTERNAL_CONTENT_URI issue in EarlyStartupDiagnostics.kt.

Summary by CodeRabbit

  • New Features

    • Automatically detects matching external subtitle files during library scans and imports them from supported libraries.
    • Identifies and normalizes subtitle languages from filenames.
    • Adds a “Subtitles off” option in the playback menu.
    • Loads discovered external subtitles during desktop and Android playback.
    • Improves subtitle track selection across playback engines.
  • Bug Fixes

    • Prevents similarly named episodes and unsupported files from being incorrectly matched as subtitles.
    • Preserves external subtitle information through library updates and database migrations.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ModerRAS, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 33 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e48366d3-1cb8-4abb-a687-ae09cfc38e02

📥 Commits

Reviewing files that changed from the base of the PR and between 4794f2f and 749ae42.

📒 Files selected for processing (4)
  • desktop-app/src/main/kotlin/com/miruplay/tv/desktop/MiruPlayDesktopComposeApp.kt
  • player-core/src/main/kotlin/com/miruplay/tv/player/ExoPlaybackController.kt
  • player-mpv-android/src/main/kotlin/is/xyz/mpv/MiruMpvSurfaceView.kt
  • scanner/src/main/kotlin/com/miruplay/tv/scanner/DefaultMediaScanner.kt
📝 Walkthrough

Walkthrough

The changes add external subtitle discovery, MLIP import, Room persistence, playback resolution, mpv/Exo handling, and a TV subtitle-off option. The database advances to version 8, and the application fallback version changes to 1.1.0.

Changes

External subtitle playback

Layer / File(s) Summary
Subtitle contracts and resolution
core/model/..., repository-api/...
Subtitle paths are matched by filename stem, normalized into language-aware tracks, persisted in repository models, and merged into indexed playback sources.
Database persistence and migration
app/..., data/...
Room schema version 8 stores external_subtitle_paths, wires migration 7→8, updates index mappings, and validates the migration and round trip.
Scanner and MLIP subtitle ingestion
scanner/..., scanner-desktop/...
Directory scanners associate matching subtitle siblings with videos, while MLIP imports subtitle paths and validates schema version 2 requirements.
Playback backend integration
desktop-app/..., player-core/..., player-mpv-android/...
Desktop, embedded mpv, and Exo playback consume external subtitle paths; track enumeration and nullable subtitle selection are updated.
Player resolution and subtitle controls
ui-tv/...
Playback resolves indexed subtitles before starting, and the TV subtitle menu adds a subtitle-off option.
Version fallback
app/build.gradle.kts
The default application base version changes from 1.0.0 to 1.1.0.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Scanner
  participant MediaIndexRepository
  participant PlayerViewModel
  participant PlaybackController
  participant EmbeddedMpv
  Scanner->>MediaIndexRepository: store externalSubtitlePaths
  PlayerViewModel->>MediaIndexRepository: resolve indexed playback subtitles
  MediaIndexRepository-->>PlayerViewModel: subtitle tracks
  PlayerViewModel->>PlaybackController: start resolved PlaybackSource
  PlaybackController->>EmbeddedMpv: loadMedia with external subtitle paths
  EmbeddedMpv-->>PlaybackController: subtitle track list
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding support for external subtitle playback.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/subtitle-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
player-core/src/main/kotlin/com/miruplay/tv/player/ExoPlaybackController.kt (2)

408-421: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Simplify the nullable-index resolution for embedded mpv subtitle selection.

The trackIndex?.let(embeddedSubtitleTrackIds::getOrNull) ?: if (trackIndex == null) null else return@withContext expression is correct but hard to follow. A when reads more clearly:

♻️ Suggested refactor
-            val nativeTrackId = trackIndex?.let(embeddedSubtitleTrackIds::getOrNull) ?: if (trackIndex == null) null else return@withContext
+            val nativeTrackId = when (trackIndex) {
+                null -> null
+                else -> embeddedSubtitleTrackIds.getOrNull(trackIndex) ?: return@withContext
+            }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@player-core/src/main/kotlin/com/miruplay/tv/player/ExoPlaybackController.kt`
around lines 408 - 421, In setSubtitleTrack, replace the complex nullable
trackIndex resolution for EXPERIMENTAL_MPV_EMBEDDED with an explicit when
expression: resolve null to null, return from the Main context when a non-null
index has no matching embedded subtitle ID, and otherwise use the mapped ID.
Preserve the existing setSubtitleTrack and selectedSubtitleTrackIndex behavior.

1264-1264: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Fixed 150ms delay before refreshing embedded mpv subtitle tracks is timing-dependent.

postDelayed(::refreshEmbeddedMpvSubtitleTracks, 150L) assumes mpv finishes processing the sub-add commands issued during MPV_EVENT_FILE_LOADED within 150ms. On slower storage/larger subtitle files this could race, leaving getAvailableSubtitles() briefly (or persistently, if never refreshed again) missing tracks. The file already wires observeProperty/eventProperty for time-pos/duration/full/pause/eof-reached; consider observing track-list/count the same way instead of a fixed delay.

Also applies to: 1298-1323

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@player-core/src/main/kotlin/com/miruplay/tv/player/ExoPlaybackController.kt`
at line 1264, Replace the fixed 150ms refresh scheduling around
refreshEmbeddedMpvSubtitleTracks, including the related logic at the referenced
later block, with an observeProperty/eventProperty handler for mpv’s
track-list/count property. Trigger subtitle-track refresh when the property
changes after sub-add processing, while preserving the existing time-pos,
duration/full, pause, and eof-reached observers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@desktop-app/src/main/kotlin/com/miruplay/tv/desktop/MiruPlayDesktopComposeApp.kt`:
- Around line 1045-1065: Update the subtitle selection logic in the
DesktopPlaybackLaunchRequest construction to preserve an explicit “no subtitle”
state separately from an absent subtitle value. Ensure a deliberately blank
subtitlePath does not trigger indexedSubtitlePaths fallback, while still
allowing automatic indexed subtitle discovery when no explicit override was
provided.

In `@scanner/src/main/kotlin/com/miruplay/tv/scanner/DefaultMediaScanner.kt`:
- Around line 99-101: Update siblingFilePaths in DefaultMediaScanner so it
applies shouldInclude(file.name, config) in addition to excluding directories
before matching subtitles. Ensure matchingExternalSubtitlePaths receives only
scan-approved sibling candidates, including the analogous construction at the
other referenced location, while preserving the existing video-file filtering
flow.

---

Nitpick comments:
In `@player-core/src/main/kotlin/com/miruplay/tv/player/ExoPlaybackController.kt`:
- Around line 408-421: In setSubtitleTrack, replace the complex nullable
trackIndex resolution for EXPERIMENTAL_MPV_EMBEDDED with an explicit when
expression: resolve null to null, return from the Main context when a non-null
index has no matching embedded subtitle ID, and otherwise use the mapped ID.
Preserve the existing setSubtitleTrack and selectedSubtitleTrackIndex behavior.
- Line 1264: Replace the fixed 150ms refresh scheduling around
refreshEmbeddedMpvSubtitleTracks, including the related logic at the referenced
later block, with an observeProperty/eventProperty handler for mpv’s
track-list/count property. Trigger subtitle-track refresh when the property
changes after sub-add processing, while preserving the existing time-pos,
duration/full, pause, and eof-reached observers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c9b9fcb0-5cd7-4feb-89d8-18fc78d2cc94

📥 Commits

Reviewing files that changed from the base of the PR and between 59eabe1 and 4794f2f.

📒 Files selected for processing (31)
  • app/build.gradle.kts
  • app/src/main/kotlin/com/miruplay/tv/di/AppModule.kt
  • app/src/test/kotlin/com/miruplay/tv/di/AppModuleTest.kt
  • core/model/src/main/kotlin/com/miruplay/tv/model/PlaybackUiConventions.kt
  • core/model/src/main/kotlin/com/miruplay/tv/model/SubtitleTracks.kt
  • core/model/src/test/kotlin/com/miruplay/tv/model/SubtitleTracksTest.kt
  • data/schemas/com.miruplay.tv.data.db.MiruPlayDatabase/8.json
  • data/src/main/kotlin/com/miruplay/tv/data/dao/IndexDao.kt
  • data/src/main/kotlin/com/miruplay/tv/data/db/MiruPlayDatabase.kt
  • data/src/main/kotlin/com/miruplay/tv/data/entity/IndexEntryEntity.kt
  • data/src/main/kotlin/com/miruplay/tv/data/repository/IndexRepositoryImpl.kt
  • data/src/test/kotlin/com/miruplay/tv/data/db/MiruPlayDatabaseMigrationTest.kt
  • data/src/test/kotlin/com/miruplay/tv/data/repository/IndexRepositoryImplTest.kt
  • desktop-app/src/main/kotlin/com/miruplay/tv/desktop/MiruPlayDesktopComposeApp.kt
  • player-core/src/main/kotlin/com/miruplay/tv/player/EmbeddedMpvSessionOptions.kt
  • player-core/src/main/kotlin/com/miruplay/tv/player/ExoPlaybackController.kt
  • player-core/src/main/kotlin/com/miruplay/tv/player/PlaybackController.kt
  • player-core/src/test/kotlin/com/miruplay/tv/player/EmbeddedMpvSessionOptionsTest.kt
  • player-core/src/test/kotlin/com/miruplay/tv/player/SubtitleMimeTypeTest.kt
  • player-mpv-android/src/main/kotlin/is/xyz/mpv/MiruMpvSurfaceView.kt
  • repository-api/src/main/kotlin/com/miruplay/tv/repository/MediaIndexRepository.kt
  • repository-api/src/main/kotlin/com/miruplay/tv/repository/PlaybackSubtitleResolver.kt
  • repository-api/src/test/kotlin/com/miruplay/tv/repository/PlaybackSubtitleResolverTest.kt
  • scanner-desktop/src/main/kotlin/com/miruplay/tv/scanner/desktop/DesktopMediaLibraryScanner.kt
  • scanner/src/main/kotlin/com/miruplay/tv/scanner/DefaultMediaScanner.kt
  • scanner/src/main/kotlin/com/miruplay/tv/scanner/MlipLibraryIndexImporter.kt
  • scanner/src/main/kotlin/com/miruplay/tv/scanner/ScanCoordinator.kt
  • scanner/src/test/kotlin/com/miruplay/tv/scanner/MlipLibraryIndexImporterTest.kt
  • scanner/src/test/kotlin/com/miruplay/tv/scanner/ScanCoordinatorTest.kt
  • ui-tv/src/main/kotlin/com/miruplay/tv/ui/player/PlayerScreen.kt
  • ui-tv/src/main/kotlin/com/miruplay/tv/ui/player/PlayerViewModel.kt

Comment thread desktop-app/src/main/kotlin/com/miruplay/tv/desktop/MiruPlayDesktopComposeApp.kt Outdated
Comment thread scanner/src/main/kotlin/com/miruplay/tv/scanner/DefaultMediaScanner.kt Outdated
@ModerRAS ModerRAS merged commit dbb59e2 into master Jul 13, 2026
11 checks passed
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.

1 participant