From bd9467b09ec8222d359fde3d72c36ded64f80428 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Fri, 16 Jan 2026 08:32:02 +0100 Subject: [PATCH] fix: remove music provider for tv --- providers/MusicPlayerProvider.tsx | 132 ++++++++++++++++++++++++++---- 1 file changed, 114 insertions(+), 18 deletions(-) diff --git a/providers/MusicPlayerProvider.tsx b/providers/MusicPlayerProvider.tsx index 63871a22..71bce6e1 100644 --- a/providers/MusicPlayerProvider.tsx +++ b/providers/MusicPlayerProvider.tsx @@ -15,12 +15,7 @@ import React, { useRef, useState, } from "react"; -import TrackPlayer, { - Capability, - type Progress, - RepeatMode as TPRepeatMode, - type Track, -} from "react-native-track-player"; +import { Platform } from "react-native"; import { downloadTrack, getLocalPath, @@ -34,6 +29,22 @@ import { settingsAtom } from "@/utils/atoms/settings"; import { getAudioStreamUrl } from "@/utils/jellyfin/audio/getAudioStreamUrl"; import { storage } from "@/utils/mmkv"; +// Conditionally import TrackPlayer only on non-TV platforms +// This prevents the native module from being loaded on TV where it doesn't exist +const TrackPlayer = Platform.isTV + ? null + : require("react-native-track-player").default; + +const TrackPlayerModule = Platform.isTV + ? null + : require("react-native-track-player"); + +// Extract types and enums from the module (only available on non-TV) +const Capability = TrackPlayerModule?.Capability; +const TPRepeatMode = TrackPlayerModule?.RepeatMode; +type Track = NonNullable["Track"]; +type Progress = NonNullable["Progress"]; + // Storage keys const STORAGE_KEYS = { QUEUE: "music_player_queue", @@ -116,6 +127,28 @@ interface MusicPlayerContextType extends MusicPlayerState { triggerLookahead: () => void; } +const defaultState: MusicPlayerState = { + currentTrack: null, + queue: [], + originalQueue: [], + queueIndex: 0, + isPlaying: false, + isLoading: false, + loadingTrackId: null, + progress: 0, + duration: 0, + streamUrl: null, + playSessionId: null, + repeatMode: "off", + shuffleEnabled: false, + mediaSource: null, + isTranscoding: false, + trackMediaInfoMap: {}, +}; + +// No-op function for TV stub +const noop = () => {}; + const MusicPlayerContext = createContext( undefined, ); @@ -132,6 +165,48 @@ interface MusicPlayerProviderProps { children: ReactNode; } +// Stub provider for tvOS - music playback is not supported +const TVMusicPlayerProvider: React.FC = ({ + children, +}) => { + const value: MusicPlayerContextType = { + ...defaultState, + playTrack: noop, + playQueue: noop, + playAlbum: noop, + playPlaylist: noop, + pause: noop, + resume: noop, + togglePlayPause: noop, + next: noop, + previous: noop, + seek: noop, + stop: noop, + addToQueue: noop, + playNext: noop, + removeFromQueue: noop, + moveInQueue: noop, + reorderQueue: noop, + clearQueue: noop, + jumpToIndex: noop, + setRepeatMode: noop, + toggleShuffle: noop, + setProgress: noop, + setDuration: noop, + setIsPlaying: noop, + reportProgress: noop, + onTrackEnd: noop, + syncFromTrackPlayer: noop, + triggerLookahead: noop, + }; + + return ( + + {children} + + ); +}; + // Persistence helpers const saveQueueToStorage = (queue: BaseItemDto[], queueIndex: number) => { try { @@ -272,7 +347,8 @@ const itemToTrack = ( }; }; -export const MusicPlayerProvider: React.FC = ({ +// Full implementation for non-TV platforms +const MobileMusicPlayerProvider: React.FC = ({ children, }) => { const api = useAtomValue(apiAtom); @@ -306,6 +382,8 @@ export const MusicPlayerProvider: React.FC = ({ // Setup TrackPlayer and AudioStorage useEffect(() => { + if (!TrackPlayer) return; + const setupPlayer = async () => { if (playerSetupRef.current) return; @@ -354,19 +432,21 @@ export const MusicPlayerProvider: React.FC = ({ // Sync repeat mode to TrackPlayer useEffect(() => { + if (!TrackPlayer) return; + const syncRepeatMode = async () => { if (!playerSetupRef.current) return; - let tpRepeatMode: TPRepeatMode; + let tpRepeatMode: typeof TPRepeatMode; switch (state.repeatMode) { case "one": - tpRepeatMode = TPRepeatMode.Track; + tpRepeatMode = TPRepeatMode?.Track; break; case "all": - tpRepeatMode = TPRepeatMode.Queue; + tpRepeatMode = TPRepeatMode?.Queue; break; default: - tpRepeatMode = TPRepeatMode.Off; + tpRepeatMode = TPRepeatMode?.Off; } await TrackPlayer.setRepeatMode(tpRepeatMode); }; @@ -553,14 +633,13 @@ export const MusicPlayerProvider: React.FC = ({ // Load remaining tracks in the background without blocking playback const loadRemainingTracksInBackground = useCallback( async (queue: BaseItemDto[], startIndex: number, preferLocal: boolean) => { - if (!api || !user?.Id) return; + if (!api || !user?.Id || !TrackPlayer) return; const mediaInfoMap: Record = {}; const failedItemIds: string[] = []; // Track items that failed to prepare // Process tracks BEFORE the start index (insert at position 0, pushing current track forward) const beforeTracks: Track[] = []; - const beforeSuccessIds: string[] = []; // Track successful IDs to maintain order for (let i = 0; i < startIndex; i++) { const item = queue[i]; if (!item.Id) continue; @@ -568,7 +647,6 @@ export const MusicPlayerProvider: React.FC = ({ const prepared = await prepareTrack(item, preferLocal); if (prepared) { beforeTracks.push(prepared.track); - beforeSuccessIds.push(item.Id); if (prepared.mediaInfo) { mediaInfoMap[item.Id] = prepared.mediaInfo; } @@ -641,7 +719,7 @@ export const MusicPlayerProvider: React.FC = ({ const loadAndPlayQueue = useCallback( async (queue: BaseItemDto[], startIndex: number) => { - if (!api || !user?.Id || queue.length === 0) return; + if (!api || !user?.Id || queue.length === 0 || !TrackPlayer) return; const preferLocal = settings?.preferLocalAudio ?? true; @@ -856,11 +934,13 @@ export const MusicPlayerProvider: React.FC = ({ ); const pause = useCallback(async () => { + if (!TrackPlayer) return; await TrackPlayer.pause(); setState((prev) => ({ ...prev, isPlaying: false })); }, []); const resume = useCallback(async () => { + if (!TrackPlayer) return; if (!state.streamUrl && state.currentTrack && api && user?.Id) { // Need to load the track first (e.g., after app restart) const result = await getAudioStreamUrl( @@ -905,6 +985,7 @@ export const MusicPlayerProvider: React.FC = ({ }, [state.isPlaying, pause, resume]); const next = useCallback(async () => { + if (!TrackPlayer) return; const currentIndex = await TrackPlayer.getActiveTrackIndex(); const queueLength = (await TrackPlayer.getQueue()).length; @@ -964,6 +1045,7 @@ export const MusicPlayerProvider: React.FC = ({ ]); const previous = useCallback(async () => { + if (!TrackPlayer) return; const position = await TrackPlayer.getProgress().then( (p: Progress) => p.position, ); @@ -1033,11 +1115,13 @@ export const MusicPlayerProvider: React.FC = ({ ]); const seek = useCallback(async (position: number) => { + if (!TrackPlayer) return; await TrackPlayer.seekTo(position); setState((prev) => ({ ...prev, progress: position })); }, []); const stop = useCallback(async () => { + if (!TrackPlayer) return; if (state.currentTrack && state.playSessionId) { reportPlaybackStopped( state.currentTrack, @@ -1087,7 +1171,7 @@ export const MusicPlayerProvider: React.FC = ({ // Queue management const addToQueue = useCallback( async (tracks: BaseItemDto | BaseItemDto[]) => { - if (!api || !user?.Id) return; + if (!api || !user?.Id || !TrackPlayer) return; const tracksArray = Array.isArray(tracks) ? tracks : [tracks]; const preferLocal = settings?.preferLocalAudio ?? true; @@ -1120,7 +1204,7 @@ export const MusicPlayerProvider: React.FC = ({ const playNext = useCallback( async (tracks: BaseItemDto | BaseItemDto[]) => { - if (!api || !user?.Id) return; + if (!api || !user?.Id || !TrackPlayer) return; const tracksArray = Array.isArray(tracks) ? tracks : [tracks]; const currentIndex = await TrackPlayer.getActiveTrackIndex(); @@ -1168,6 +1252,7 @@ export const MusicPlayerProvider: React.FC = ({ ); const removeFromQueue = useCallback(async (index: number) => { + if (!TrackPlayer) return; const queueLength = (await TrackPlayer.getQueue()).length; const currentIndex = await TrackPlayer.getActiveTrackIndex(); @@ -1201,6 +1286,7 @@ export const MusicPlayerProvider: React.FC = ({ const moveInQueue = useCallback( async (fromIndex: number, toIndex: number) => { + if (!TrackPlayer) return; const queue = await TrackPlayer.getQueue(); if ( fromIndex < 0 || @@ -1241,6 +1327,7 @@ export const MusicPlayerProvider: React.FC = ({ // Reorder queue with a new array (used by drag-to-reorder UI) const reorderQueue = useCallback( async (newQueue: BaseItemDto[]) => { + if (!TrackPlayer) return; // Find where the current track ended up in the new order const currentTrackId = state.currentTrack?.Id; const newIndex = currentTrackId @@ -1253,7 +1340,7 @@ export const MusicPlayerProvider: React.FC = ({ // Create a map of trackId -> current TrackPlayer index const currentPositions = new Map(); - tpQueue.forEach((track, idx) => { + tpQueue.forEach((track: Track, idx: number) => { currentPositions.set(track.id, idx); }); @@ -1296,6 +1383,7 @@ export const MusicPlayerProvider: React.FC = ({ ); const clearQueue = useCallback(async () => { + if (!TrackPlayer) return; const currentIndex = await TrackPlayer.getActiveTrackIndex(); const queue = await TrackPlayer.getQueue(); @@ -1325,6 +1413,7 @@ export const MusicPlayerProvider: React.FC = ({ const jumpToIndex = useCallback( async (index: number) => { + if (!TrackPlayer) return; if ( index < 0 || index >= state.queue.length || @@ -1460,6 +1549,7 @@ export const MusicPlayerProvider: React.FC = ({ // Sync state from TrackPlayer (called when active track changes) // Uses ID-based lookup instead of index to handle queue mismatches const syncFromTrackPlayer = useCallback(async () => { + if (!TrackPlayer) return; const activeTrack = await TrackPlayer.getActiveTrack(); if (!activeTrack?.id) return; @@ -1476,6 +1566,7 @@ export const MusicPlayerProvider: React.FC = ({ // Called by playback engine when track ends const onTrackEnd = useCallback(() => { + if (!TrackPlayer) return; if (state.repeatMode === "one") { TrackPlayer.seekTo(0); TrackPlayer.play(); @@ -1485,6 +1576,7 @@ export const MusicPlayerProvider: React.FC = ({ // Look-ahead cache: pre-cache upcoming N tracks (excludes current track to avoid bandwidth competition) const triggerLookahead = useCallback(async () => { + if (!TrackPlayer) return; // Check if caching is enabled in settings if (settings?.audioLookaheadEnabled === false) return; if (!api || !user?.Id) return; @@ -1598,3 +1690,7 @@ export const MusicPlayerProvider: React.FC = ({ ); }; + +// Export the appropriate provider based on platform +export const MusicPlayerProvider: React.FC = + Platform.isTV ? TVMusicPlayerProvider : MobileMusicPlayerProvider;