Compare commits

..

3 Commits

Author SHA1 Message Date
lance chant
d9677cbece Merge branch 'develop' into fix/subtitles-ordering-fix 2026-06-30 10:16:04 +02:00
lance chant
97ef9b5ee7 Merge branch 'develop' into fix/subtitles-ordering-fix 2026-06-29 07:50:18 +02:00
Lance Chant
d6980cfc8e fix: subtitle ordering
Fixed an issue where external and subrip subtitles were not ordered
correctly

Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
2026-06-23 12:00:11 +02:00
10 changed files with 337 additions and 396 deletions

View File

@@ -30,7 +30,6 @@ jobs:
actions: write # dispatch artifact-comment.yml to refresh the PR comment
steps:
- parallel:
- name: 🗑️ Free Disk Space
uses: BRAINSia/free-disk-space@7048ffbf50819342ac964ef3998a51c2564a8a75 # v2.1.3
with:
@@ -57,6 +56,19 @@ jobs:
# renovate: datasource=npm depName=bun
bun-version: "1.3.14"
- name: 💾 Cache Bun dependencies
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: 📦 Install dependencies and reload submodules
run: |
bun install --frozen-lockfile
bun run submodule-reload
- name: ☕ Set up JDK 17
# ubuntu-26.04 defaults to JDK 25, which breaks the RN/AGP native build
# (Kotlin falls back to JVM_23, the foojay toolchain + CMake configure
@@ -66,15 +78,6 @@ jobs:
distribution: temurin
java-version: "17"
- parallel:
- name: 💾 Cache Bun dependencies
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: 💾 Cache Gradle global
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
@@ -85,11 +88,6 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
- name: 📦 Install dependencies and reload submodules
run: |
bun install --frozen-lockfile
bun run submodule-reload
- name: 🛠️ Generate project files
run: bun run prebuild
@@ -132,7 +130,6 @@ jobs:
actions: write # dispatch artifact-comment.yml to refresh the PR comment
steps:
- parallel:
- name: 🗑️ Free Disk Space
uses: BRAINSia/free-disk-space@7048ffbf50819342ac964ef3998a51c2564a8a75 # v2.1.3
with:
@@ -159,6 +156,19 @@ jobs:
# renovate: datasource=npm depName=bun
bun-version: "1.3.14"
- name: 💾 Cache Bun dependencies
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: 📦 Install dependencies and reload submodules
run: |
bun install --frozen-lockfile
bun run submodule-reload
- name: ☕ Set up JDK 17
# ubuntu-26.04 defaults to JDK 25, which breaks the RN/AGP native build
# (Kotlin falls back to JVM_23, the foojay toolchain + CMake configure
@@ -168,15 +178,6 @@ jobs:
distribution: temurin
java-version: "17"
- parallel:
- name: 💾 Cache Bun dependencies
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: 💾 Cache Gradle global
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
@@ -187,11 +188,6 @@ jobs:
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-gradle-
- name: 📦 Install dependencies and reload submodules
run: |
bun install --frozen-lockfile
bun run submodule-reload
- name: 🛠️ Generate project files
run: bun run prebuild:tv
@@ -233,7 +229,6 @@ jobs:
actions: write # dispatch artifact-comment.yml to refresh the PR comment
steps:
- parallel:
- name: 📥 Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
@@ -248,19 +243,6 @@ jobs:
# renovate: datasource=npm depName=bun
bun-version: "1.3.14"
- name: 🔧 Setup Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with:
# renovate: datasource=custom.xcode depName=xcode versioning=loose
xcode-version: "26.5"
- name: 🏗️ Setup EAS
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
eas-cache: true
- name: 💾 Cache Bun dependencies
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
@@ -277,6 +259,19 @@ jobs:
- name: 🛠️ Generate project files
run: bun run prebuild
- name: 🔧 Setup Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with:
# renovate: datasource=custom.xcode depName=xcode versioning=loose
xcode-version: "26.5"
- name: 🏗️ Setup EAS
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
eas-cache: true
- name: 🚀 Build iOS app
env:
EXPO_TV: 0
@@ -306,7 +301,6 @@ jobs:
actions: write # dispatch artifact-comment.yml to refresh the PR comment
steps:
- parallel:
- name: 📥 Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
@@ -321,12 +315,6 @@ jobs:
# renovate: datasource=npm depName=bun
bun-version: "1.3.14"
- name: 🔧 Setup Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with:
# renovate: datasource=custom.xcode depName=xcode versioning=loose
xcode-version: "26.5"
- name: 💾 Cache Bun dependencies
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
@@ -343,6 +331,12 @@ jobs:
- name: 🛠️ Generate project files
run: bun run prebuild
- name: 🔧 Setup Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with:
# renovate: datasource=custom.xcode depName=xcode versioning=loose
xcode-version: "26.5"
- name: 🚀 Build iOS app
env:
EXPO_TV: 0
@@ -374,7 +368,6 @@ jobs:
actions: write # dispatch artifact-comment.yml to refresh the PR comment
steps:
- parallel:
- name: 📥 Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
@@ -389,19 +382,6 @@ jobs:
# renovate: datasource=npm depName=bun
bun-version: "1.3.14"
- name: 🔧 Setup Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with:
# renovate: datasource=custom.xcode depName=xcode versioning=loose
xcode-version: "26.5"
- name: 🏗️ Setup EAS
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
eas-cache: true
- name: 💾 Cache Bun dependencies
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
@@ -418,6 +398,19 @@ jobs:
- name: 🛠️ Generate project files
run: bun run prebuild:tv
- name: 🔧 Setup Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with:
# renovate: datasource=custom.xcode depName=xcode versioning=loose
xcode-version: "26.5"
- name: 🏗️ Setup EAS
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
eas-cache: true
- name: 🚀 Build iOS app
env:
EXPO_TV: 1
@@ -445,7 +438,6 @@ jobs:
actions: write # dispatch artifact-comment.yml to refresh the PR comment
steps:
- parallel:
- name: 📥 Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
@@ -460,12 +452,6 @@ jobs:
# renovate: datasource=npm depName=bun
bun-version: "1.3.14"
- name: 🔧 Setup Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with:
# renovate: datasource=custom.xcode depName=xcode versioning=loose
xcode-version: "26.5"
- name: 💾 Cache Bun dependencies
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
@@ -482,6 +468,12 @@ jobs:
- name: 🛠️ Generate project files
run: bun run prebuild:tv
- name: 🔧 Setup Xcode
uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1
with:
# renovate: datasource=custom.xcode depName=xcode versioning=loose
xcode-version: "26.5"
- name: 🚀 Build iOS app
env:
EXPO_TV: 1

View File

@@ -18,13 +18,13 @@ jobs:
contents: read
steps:
- parallel:
- name: 📥 Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
show-progress: false
submodules: recursive
fetch-depth: 0
- name: 🍞 Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0

View File

@@ -20,7 +20,6 @@ jobs:
issues: write
contents: read
steps:
- parallel:
- name: 📥 Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

View File

@@ -2,7 +2,7 @@ name: 🚦 Security & Quality Gate
on:
pull_request:
types: [opened, synchronize, reopened]
types: [opened, edited, synchronize, reopened]
branches: [develop, master]
workflow_dispatch:
push:
@@ -11,11 +11,39 @@ on:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
validate_pr_title:
name: "📝 Validate PR Title"
if: github.event_name == 'pull_request'
runs-on: ubuntu-26.04
permissions:
pull-requests: write
contents: read
steps:
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: always() && (steps.lint_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
message: |
Hey there and thank you for opening this pull request! 👋🏼
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/).
**Error details:**
```
${{ steps.lint_pr_title.outputs.error_message }}
```
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
with:
header: pr-title-lint-error
delete: true
dependency-review:
name: 🔍 Vulnerable Dependencies
runs-on: ubuntu-26.04
@@ -39,12 +67,12 @@ jobs:
name: 🚑 Expo Doctor Check
runs-on: ubuntu-26.04
steps:
- parallel:
- name: 🛒 Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
submodules: recursive
fetch-depth: 0
- name: 🍞 Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
@@ -52,14 +80,6 @@ jobs:
# renovate: datasource=npm depName=bun
bun-version: "1.3.14"
- name: 💾 Cache Bun dependencies
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: 📦 Install dependencies (bun)
run: bun install --frozen-lockfile
@@ -83,12 +103,18 @@ jobs:
- "i18n:check"
steps:
- parallel:
- name: "📥 Checkout PR code"
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
submodules: recursive
fetch-depth: 0
- name: "🟢 Setup Node.js"
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
# renovate: datasource=node-version depName=node versioning=node
node-version: "24.18.0"
- name: "🍞 Setup Bun"
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
@@ -96,14 +122,6 @@ jobs:
# renovate: datasource=npm depName=bun
bun-version: "1.3.14"
- name: 💾 Cache Bun dependencies
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-bun-
- name: "📦 Install dependencies"
run: bun install --frozen-lockfile

View File

@@ -1,45 +0,0 @@
name: 📝 PR Title
on:
pull_request:
types: [opened, edited, synchronize, reopened]
branches: [develop, master]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
validate_pr_title:
name: "📝 Validate PR Title"
runs-on: ubuntu-26.04
permissions:
pull-requests: write
contents: read
steps:
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
if: always() && (steps.lint_pr_title.outputs.error_message != null)
with:
header: pr-title-lint-error
message: |
Hey there and thank you for opening this pull request! 👋🏼
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/).
**Error details:**
```
${{ steps.lint_pr_title.outputs.error_message }}
```
- if: ${{ steps.lint_pr_title.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4
with:
header: pr-title-lint-error
delete: true

View File

@@ -13,8 +13,6 @@ name: 🚀 Release (EAS build + submit)
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
# Queue successive releases in order instead of dropping the extra pending run.
queue: max
on:
push:
@@ -65,7 +63,6 @@ jobs:
artifact_name: streamyfin-android-tv-apk
steps:
- parallel:
- name: 📥 Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
@@ -79,13 +76,6 @@ jobs:
# renovate: datasource=npm depName=bun
bun-version: "1.3.14"
- name: 🏗️ Setup EAS
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
eas-cache: true
- name: 💾 Cache Bun dependencies
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
@@ -99,6 +89,13 @@ jobs:
bun install --frozen-lockfile
bun run submodule-reload
- name: 🏗️ Setup EAS
uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # main
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
eas-cache: true
# tvOS uses credentialsSource: local — restore the gitignored
# credentials.json + cert + provisioning profiles from secrets.
- name: 🔐 Restore tvOS signing credentials

View File

@@ -25,7 +25,6 @@ jobs:
contents: write
pull-requests: write
steps:
- parallel:
- name: 📥 Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:

View File

@@ -12,16 +12,11 @@ import {
import { FlashList } from "@shopify/flash-list";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import {
useFocusEffect,
useLocalSearchParams,
useNavigation,
} from "expo-router";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai";
import React, { useCallback, useEffect, useMemo, useRef } from "react";
import React, { useCallback, useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import {
BackHandler,
FlatList,
Platform,
ScrollView,
@@ -85,9 +80,8 @@ const Page = () => {
sortBy?: string;
sortOrder?: string;
filterBy?: string;
fromSeeAll?: string;
};
const { libraryId, fromSeeAll } = searchParams;
const { libraryId } = searchParams;
const typography = useScaledTVTypography();
const posterSizes = useScaledTVPosterSizes();
@@ -118,22 +112,6 @@ const Page = () => {
const { t } = useTranslation();
const router = useRouter();
const { showOptions } = useTVOptionModal();
// When this library detail was opened from the home "See All" button, its
// libraries stack is just [detail], so the default TV Back would exit to home.
// Intercept Back (scoped to while this screen is focused via useFocusEffect) and
// route to the library list instead, so the user can switch libraries. Normal
// entries from the list keep their native pop-to-list behavior.
useFocusEffect(
useCallback(() => {
if (!Platform.isTV || fromSeeAll !== "true") return;
const sub = BackHandler.addEventListener("hardwareBackPress", () => {
router.replace("/(auth)/(tabs)/(libraries)");
return true;
});
return () => sub.remove();
}, [fromSeeAll, router]),
);
const { showItemActions } = useTVItemActionModal();
// TV Filter queries
@@ -291,23 +269,6 @@ const Page = () => {
});
}, [library]);
// If this See-All detail was deep-linked on top of the libraries index, collapse
// the libraries stack to just this screen. Otherwise the stack is [index, detail],
// which the native bottom tab reliably auto-pops back to the index (the detail
// "bounces" to the library list ~0.5s after opening). With [detail] alone it stays
// put, and Back is handled explicitly by the fromSeeAll interceptor above.
const didCollapseRef = useRef(false);
useEffect(() => {
if (!Platform.isTV || fromSeeAll !== "true" || didCollapseRef.current)
return;
const state = navigation.getState();
if (state?.routes && state.routes.length > 1) {
didCollapseRef.current = true;
const top = state.routes[state.routes.length - 1];
navigation.reset({ index: 0, routes: [top] } as any);
}
}, [navigation, fromSeeAll]);
const fetchItems = useCallback(
async ({
pageParam,

View File

@@ -201,18 +201,12 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
const handleSeeAllPress = useCallback(() => {
if (!parentId) return;
// Navigate into the library detail (lives in the libraries tab) sorted by most
// recently added. The `fromSeeAll` flag tells the detail page to (a) collapse
// the libraries stack so the native tab can't auto-pop it back to the list, and
// (b) intercept Back to route to the library list so the user can switch
// libraries. See app/(auth)/(tabs)/(libraries)/[libraryId].tsx.
router.push({
pathname: "/[libraryId]",
pathname: "/(auth)/(tabs)/(libraries)/[libraryId]",
params: {
libraryId: parentId,
sortBy: SortByOption.DateCreated,
sortOrder: SortOrderOption.Descending,
fromSeeAll: "true",
},
} as any);
}, [router, parentId]);
@@ -354,14 +348,11 @@ export const InfiniteScrollingCollectionList: React.FC<Props> = ({
// contentOffset={{ x: -sizes.padding.horizontal, y: 0 }}
// contentContainerStyle={{ paddingVertical: SCALE_PADDING }}
ListFooterComponent={
// No fixed width: the footer must size to the "See All" card so the
// FlatList's scrollable content extends to fully reveal it. A fixed
// (narrow) width clipped the card at the right edge. Trailing space is
// provided by contentContainerStyle.paddingRight.
<View
style={{
flexDirection: "row",
alignItems: "center",
width: sizes.padding.horizontal,
}}
>
{isFetchingNextPage && (

View File

@@ -44,9 +44,22 @@ export const isSubtitleInMpv = (
/**
* Calculate the MPV track ID for a given Jellyfin subtitle index.
*
* MPV track IDs are 1-based and only count subtitles that are actually in MPV.
* We iterate through all subtitles, counting only those in MPV, until we find
* the one matching the Jellyfin index.
* MPV track IDs are 1-based, but MPV's track list is NOT in MediaStreams order:
* 1. Embedded/HLS subs are enumerated from the container (or HLS playlist)
* first, in MediaStreams order.
* 2. External subs are appended via `sub-add` AFTER the file loads, in the
* order they are passed to MPV (here, also MediaStreams order — see
* direct-player.tsx where the externalSubtitles array is built by
* filtering MediaStreams).
*
* Iterating in pure MediaStreams order produces the wrong MPV ID whenever an
* External sub is listed before an Embed sub in MediaStreams (common when
* Jellyfin prepends a converted SRT/VTT ahead of an original PGS/ASS track),
* causing e.g. English to select Spanish. We therefore count in two phases
* that mirror MPV's actual ordering.
*
* Image-based subs (PGS/VOBSUB) during transcoding are burned into the video
* and absent from MPV's track list; they are skipped in both phases.
*
* @param mediaSource - The media source containing subtitle streams
* @param jellyfinSubtitleIndex - The Jellyfin server-side subtitle index (-1 = disabled)
@@ -74,15 +87,31 @@ export const getMpvSubtitleId = (
return undefined;
}
// Count MPV track position (1-based)
const isExternal = (sub: MediaStream) =>
sub.DeliveryMethod === SubtitleDeliveryMethod.External;
let mpvIndex = 0;
// Phase 1: embedded / HLS subs — these occupy MPV track IDs first because
// they come from the container or HLS playlist.
for (const sub of allSubs) {
if (isSubtitleInMpv(sub, isTranscoding)) {
if (isExternal(sub)) continue;
if (!isSubtitleInMpv(sub, isTranscoding)) continue;
mpvIndex++;
if (sub.Index === jellyfinSubtitleIndex) {
return mpvIndex;
}
}
// Phase 2: external subs — appended via `sub-add` after the file loads,
// so they come last in MPV's track list.
for (const sub of allSubs) {
if (!isExternal(sub)) continue;
if (!isSubtitleInMpv(sub, isTranscoding)) continue;
mpvIndex++;
if (sub.Index === jellyfinSubtitleIndex) {
return mpvIndex;
}
}
return undefined;