mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-02-13 15:52:23 +00:00
Compare commits
1 Commits
feat/local
...
renovate/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7879fc7ea2 |
@@ -28,7 +28,6 @@ import {
|
||||
} from "@/components/video-player/controls/utils/playback-speed-settings";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import { useHaptic } from "@/hooks/useHaptic";
|
||||
import { useIntroPlayback } from "@/hooks/useIntroPlayback";
|
||||
import { useOrientation } from "@/hooks/useOrientation";
|
||||
import { usePlaybackManager } from "@/hooks/usePlaybackManager";
|
||||
import usePlaybackSpeed from "@/hooks/usePlaybackSpeed";
|
||||
@@ -56,7 +55,7 @@ import {
|
||||
} from "@/utils/jellyfin/subtitleUtils";
|
||||
import { writeToLog } from "@/utils/log";
|
||||
import { generateDeviceProfile } from "@/utils/profiles/native";
|
||||
import { msToTicks, ticksToMs, ticksToSeconds } from "@/utils/time";
|
||||
import { msToTicks, ticksToSeconds } from "@/utils/time";
|
||||
|
||||
export default function page() {
|
||||
const videoRef = useRef<MpvPlayerViewRef>(null);
|
||||
@@ -88,8 +87,6 @@ export default function page() {
|
||||
const progress = useSharedValue(0);
|
||||
const isSeeking = useSharedValue(false);
|
||||
const cacheProgress = useSharedValue(0);
|
||||
// Track whether we've already triggered completion for the current intro
|
||||
const introCompletionTriggered = useSharedValue(false);
|
||||
const VolumeManager = Platform.isTV
|
||||
? null
|
||||
: require("react-native-volume-manager");
|
||||
@@ -152,14 +149,6 @@ export default function page() {
|
||||
isError: false,
|
||||
});
|
||||
|
||||
// Intro playback hook - manages intro video playback before main content
|
||||
const { intros, currentIntro, isPlayingIntro, skipAllIntros } =
|
||||
useIntroPlayback({
|
||||
api,
|
||||
itemId: item?.Id || null,
|
||||
userId: user?.Id,
|
||||
});
|
||||
|
||||
// Resolve audio index: use URL param if provided, otherwise use stored index for offline playback
|
||||
const audioIndex = useMemo(() => {
|
||||
if (audioIndexFromUrl !== undefined) {
|
||||
@@ -258,9 +247,6 @@ export default function page() {
|
||||
isError: false,
|
||||
});
|
||||
|
||||
// Intro stream state - separate from main content stream
|
||||
const [introStream, setIntroStream] = useState<Stream | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStreamData = async () => {
|
||||
setStreamStatus({ isLoading: true, isError: false });
|
||||
@@ -341,57 +327,6 @@ export default function page() {
|
||||
downloadedItem,
|
||||
]);
|
||||
|
||||
// Fetch intro stream when current intro changes
|
||||
useEffect(() => {
|
||||
const fetchIntroStreamData = async () => {
|
||||
// Don't fetch intro stream if offline or no current intro
|
||||
if (offline || !currentIntro?.Id || !api || !user?.Id) {
|
||||
setIntroStream(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getStreamUrl({
|
||||
api,
|
||||
item: currentIntro,
|
||||
startTimeTicks: 0, // Always start from beginning for intros
|
||||
userId: user.Id,
|
||||
audioStreamIndex: audioIndex,
|
||||
maxStreamingBitrate: bitrateValue,
|
||||
mediaSourceId: undefined,
|
||||
subtitleStreamIndex: subtitleIndex,
|
||||
deviceProfile: generateDeviceProfile(),
|
||||
});
|
||||
if (!res) return;
|
||||
const { mediaSource, sessionId, url } = res;
|
||||
|
||||
if (!sessionId || !mediaSource || !url) {
|
||||
console.error("Failed to get intro stream URL");
|
||||
return;
|
||||
}
|
||||
setIntroStream({ mediaSource, sessionId, url });
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch intro stream:", error);
|
||||
}
|
||||
};
|
||||
fetchIntroStreamData();
|
||||
}, [
|
||||
currentIntro,
|
||||
api,
|
||||
user?.Id,
|
||||
audioIndex,
|
||||
bitrateValue,
|
||||
subtitleIndex,
|
||||
offline,
|
||||
]);
|
||||
|
||||
// Reset intro completion flag when a new intro starts playing
|
||||
useEffect(() => {
|
||||
if (isPlayingIntro) {
|
||||
introCompletionTriggered.value = false;
|
||||
}
|
||||
}, [isPlayingIntro, currentIntro]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!stream || !api || offline) return;
|
||||
const reportPlaybackStart = async () => {
|
||||
@@ -545,21 +480,6 @@ export default function page() {
|
||||
lastUrlUpdateTime.value = now;
|
||||
}
|
||||
|
||||
// Handle intro completion - check if intro has reached its end
|
||||
if (isPlayingIntro && currentIntro) {
|
||||
const introDuration = ticksToMs(currentIntro.RunTimeTicks || 0);
|
||||
// Check if we're near the end of the intro (within 1000ms buffer)
|
||||
// Use a larger buffer to ensure reliable detection even with short intros
|
||||
// or if MPV doesn't fire progress callbacks frequently
|
||||
if (currentTime >= introDuration - 1000) {
|
||||
// Only trigger once per intro to avoid multiple calls
|
||||
if (!introCompletionTriggered.value) {
|
||||
introCompletionTriggered.value = true;
|
||||
skipAllIntros();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!item?.Id) return;
|
||||
|
||||
playbackManager.reportPlaybackProgress(
|
||||
@@ -576,9 +496,6 @@ export default function page() {
|
||||
isSeeking,
|
||||
isPlaybackStopped,
|
||||
isBuffering,
|
||||
isPlayingIntro,
|
||||
currentIntro,
|
||||
skipAllIntros,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -589,11 +506,9 @@ export default function page() {
|
||||
|
||||
/** Build video source config for MPV */
|
||||
const videoSource = useMemo<MpvVideoSource | undefined>(() => {
|
||||
// Use intro stream if playing intro, otherwise use main content stream
|
||||
const activeStream = isPlayingIntro ? introStream : stream;
|
||||
if (!activeStream?.url) return undefined;
|
||||
if (!stream?.url) return undefined;
|
||||
|
||||
const mediaSource = activeStream.mediaSource;
|
||||
const mediaSource = stream.mediaSource;
|
||||
const isTranscoding = Boolean(mediaSource?.TranscodingUrl);
|
||||
|
||||
// Get external subtitle URLs
|
||||
@@ -629,17 +544,14 @@ export default function page() {
|
||||
);
|
||||
|
||||
// Calculate start position directly here to avoid timing issues
|
||||
// For intros, always start from 0
|
||||
const startTicks = isPlayingIntro
|
||||
? 0
|
||||
: playbackPositionFromUrl
|
||||
? Number.parseInt(playbackPositionFromUrl, 10)
|
||||
: (item?.UserData?.PlaybackPositionTicks ?? 0);
|
||||
const startTicks = playbackPositionFromUrl
|
||||
? Number.parseInt(playbackPositionFromUrl, 10)
|
||||
: (item?.UserData?.PlaybackPositionTicks ?? 0);
|
||||
const startPos = ticksToSeconds(startTicks);
|
||||
|
||||
// Build source config - headers only needed for online streaming
|
||||
const source: MpvVideoSource = {
|
||||
url: activeStream.url,
|
||||
url: stream.url,
|
||||
startPosition: startPos,
|
||||
autoplay: true,
|
||||
initialSubtitleId,
|
||||
@@ -662,8 +574,6 @@ export default function page() {
|
||||
}, [
|
||||
stream?.url,
|
||||
stream?.mediaSource,
|
||||
introStream?.url,
|
||||
introStream?.mediaSource,
|
||||
item?.UserData?.PlaybackPositionTicks,
|
||||
playbackPositionFromUrl,
|
||||
api?.basePath,
|
||||
@@ -671,7 +581,6 @@ export default function page() {
|
||||
subtitleIndex,
|
||||
audioIndex,
|
||||
offline,
|
||||
isPlayingIntro,
|
||||
]);
|
||||
|
||||
const volumeUpCb = useCallback(async () => {
|
||||
@@ -1084,9 +993,6 @@ export default function page() {
|
||||
getTechnicalInfo={getTechnicalInfo}
|
||||
playMethod={playMethod}
|
||||
transcodeReasons={transcodeReasons}
|
||||
isPlayingIntro={isPlayingIntro}
|
||||
skipAllIntros={skipAllIntros}
|
||||
intros={intros}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
13
bun.lock
13
bun.lock
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "streamyfin",
|
||||
@@ -87,7 +88,7 @@
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-volume-manager": "^2.0.8",
|
||||
"react-native-web": "^0.21.0",
|
||||
"react-native-worklets": "0.5.1",
|
||||
"react-native-worklets": "0.7.2",
|
||||
"sonner-native": "0.21.2",
|
||||
"tailwindcss": "3.3.2",
|
||||
"use-debounce": "^10.0.4",
|
||||
@@ -257,7 +258,7 @@
|
||||
|
||||
"@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q=="],
|
||||
|
||||
"@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ=="],
|
||||
"@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg=="],
|
||||
|
||||
"@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg=="],
|
||||
|
||||
@@ -1707,7 +1708,7 @@
|
||||
|
||||
"react-native-web": ["react-native-web@0.21.2", "", { "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", "fbjs": "^3.0.4", "inline-style-prefixer": "^7.0.1", "memoize-one": "^6.0.0", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "styleq": "^0.1.3" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg=="],
|
||||
|
||||
"react-native-worklets": ["react-native-worklets@0.5.1", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "semver": "7.7.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w=="],
|
||||
"react-native-worklets": ["react-native-worklets@0.7.2", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "7.27.1", "@babel/plugin-transform-class-properties": "7.27.1", "@babel/plugin-transform-classes": "7.28.4", "@babel/plugin-transform-nullish-coalescing-operator": "7.27.1", "@babel/plugin-transform-optional-chaining": "7.27.1", "@babel/plugin-transform-shorthand-properties": "7.27.1", "@babel/plugin-transform-template-literals": "7.27.1", "@babel/plugin-transform-unicode-regex": "7.27.1", "@babel/preset-typescript": "7.27.1", "convert-source-map": "2.0.0", "semver": "7.7.3" }, "peerDependencies": { "@babel/core": "*", "react": "*", "react-native": "*" } }, "sha512-DuLu1kMV/Uyl9pQHp3hehAlThoLw7Yk2FwRTpzASOmI+cd4845FWn3m2bk9MnjUw8FBRIyhwLqYm2AJaXDXsog=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
|
||||
|
||||
@@ -2215,6 +2216,8 @@
|
||||
|
||||
"@react-native/babel-preset/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||
|
||||
"@react-native/babel-preset/@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ=="],
|
||||
|
||||
"@react-native/babel-preset/@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||
|
||||
"@react-native/codegen/@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||
@@ -2437,7 +2440,9 @@
|
||||
|
||||
"react-native-web/memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="],
|
||||
|
||||
"react-native-worklets/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
"react-native-worklets/@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="],
|
||||
|
||||
"react-native-worklets/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"readable-web-to-node-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="],
|
||||
|
||||
|
||||
@@ -57,11 +57,6 @@ interface BottomControlsProps {
|
||||
minutes: number;
|
||||
seconds: number;
|
||||
};
|
||||
|
||||
// Intro playback props
|
||||
isPlayingIntro?: boolean;
|
||||
skipAllIntros?: () => void;
|
||||
intros?: BaseItemDto[];
|
||||
}
|
||||
|
||||
export const BottomControls: FC<BottomControlsProps> = ({
|
||||
@@ -92,9 +87,6 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
||||
trickPlayUrl,
|
||||
trickplayInfo,
|
||||
time,
|
||||
isPlayingIntro = false,
|
||||
skipAllIntros,
|
||||
intros = [],
|
||||
}) => {
|
||||
const { settings } = useSettings();
|
||||
const insets = useSafeAreaInsets();
|
||||
@@ -141,14 +133,6 @@ export const BottomControls: FC<BottomControlsProps> = ({
|
||||
)}
|
||||
</View>
|
||||
<View className='flex flex-row space-x-2 shrink-0'>
|
||||
{/* Skip Intro button - shows when playing intro videos */}
|
||||
{isPlayingIntro && intros.length > 0 && skipAllIntros && (
|
||||
<SkipButton
|
||||
showButton={true}
|
||||
onPress={skipAllIntros}
|
||||
buttonText='Skip Intro'
|
||||
/>
|
||||
)}
|
||||
<SkipButton
|
||||
showButton={showSkipButton}
|
||||
onPress={skipIntro}
|
||||
|
||||
@@ -72,10 +72,6 @@ interface Props {
|
||||
getTechnicalInfo?: () => Promise<TechnicalInfo>;
|
||||
playMethod?: "DirectPlay" | "DirectStream" | "Transcode";
|
||||
transcodeReasons?: string[];
|
||||
// Intro playback props
|
||||
isPlayingIntro?: boolean;
|
||||
skipAllIntros?: () => void;
|
||||
intros?: BaseItemDto[];
|
||||
}
|
||||
|
||||
export const Controls: FC<Props> = ({
|
||||
@@ -105,9 +101,6 @@ export const Controls: FC<Props> = ({
|
||||
getTechnicalInfo,
|
||||
playMethod,
|
||||
transcodeReasons,
|
||||
isPlayingIntro = false,
|
||||
skipAllIntros,
|
||||
intros = [],
|
||||
}) => {
|
||||
const offline = useOfflineMode();
|
||||
const { settings, updateSettings } = useSettings();
|
||||
@@ -561,9 +554,6 @@ export const Controls: FC<Props> = ({
|
||||
trickPlayUrl={trickPlayUrl}
|
||||
trickplayInfo={trickplayInfo}
|
||||
time={isSliding || showRemoteBubble ? time : remoteTime}
|
||||
isPlayingIntro={isPlayingIntro}
|
||||
skipAllIntros={skipAllIntros}
|
||||
intros={intros}
|
||||
/>
|
||||
</Animated.View>
|
||||
</>
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import type { Api } from "@jellyfin/sdk";
|
||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getIntros } from "@/utils/intros";
|
||||
|
||||
interface UseIntroPlaybackProps {
|
||||
api: Api | null;
|
||||
itemId: string | null;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export function useIntroPlayback({
|
||||
api,
|
||||
itemId,
|
||||
userId,
|
||||
}: UseIntroPlaybackProps) {
|
||||
const [intros, setIntros] = useState<BaseItemDto[]>([]);
|
||||
const [isPlayingIntro, setIsPlayingIntro] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchIntros() {
|
||||
if (!api || !itemId) return;
|
||||
|
||||
const introItems = await getIntros(api, itemId, userId);
|
||||
setIntros(introItems);
|
||||
// Set isPlayingIntro to true when intros are available
|
||||
setIsPlayingIntro(introItems.length > 0);
|
||||
}
|
||||
|
||||
fetchIntros();
|
||||
}, [api, itemId, userId]);
|
||||
|
||||
// Only play the first intro if intros are available.. might be nice to configure this at some point with tags or something 🤷♂️
|
||||
const currentIntro = intros.length > 0 ? intros[0] : null;
|
||||
|
||||
const skipAllIntros = () => {
|
||||
setIsPlayingIntro(false);
|
||||
};
|
||||
|
||||
return {
|
||||
intros,
|
||||
currentIntro,
|
||||
isPlayingIntro,
|
||||
skipAllIntros,
|
||||
};
|
||||
}
|
||||
@@ -108,7 +108,7 @@
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-volume-manager": "^2.0.8",
|
||||
"react-native-web": "^0.21.0",
|
||||
"react-native-worklets": "0.5.1",
|
||||
"react-native-worklets": "0.7.2",
|
||||
"sonner-native": "0.21.2",
|
||||
"tailwindcss": "3.3.2",
|
||||
"use-debounce": "^10.0.4",
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { Api } from "@jellyfin/sdk";
|
||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
|
||||
/**
|
||||
* Fetches intro items for a given media item using the Jellyfin SDK
|
||||
* @param api - The Jellyfin API instance
|
||||
* @param itemId - The ID of the media item
|
||||
* @param userId - Optional user ID
|
||||
* @returns Promise<BaseItemDto[]> - Array of intro items
|
||||
*/
|
||||
export async function getIntros(
|
||||
api: Api,
|
||||
itemId: string,
|
||||
userId?: string,
|
||||
): Promise<BaseItemDto[]> {
|
||||
try {
|
||||
const response = await getUserLibraryApi(api).getIntros({
|
||||
itemId,
|
||||
userId,
|
||||
});
|
||||
|
||||
return response.data.Items || [];
|
||||
} catch (error) {
|
||||
console.error("Error fetching intros:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user