mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
fix: cache issues with songs, partial downloads, component rerenders, etc
This commit is contained in:
@@ -7,7 +7,11 @@ import TrackPlayer, {
|
||||
usePlaybackState,
|
||||
useProgress,
|
||||
} from "react-native-track-player";
|
||||
import { audioStorageEvents, getLocalPath } from "@/providers/AudioStorage";
|
||||
import {
|
||||
audioStorageEvents,
|
||||
deleteTrack,
|
||||
getLocalPath,
|
||||
} from "@/providers/AudioStorage";
|
||||
import { useMusicPlayer } from "@/providers/MusicPlayerProvider";
|
||||
|
||||
export const MusicPlaybackEngine: React.FC = () => {
|
||||
@@ -168,6 +172,51 @@ export const MusicPlaybackEngine: React.FC = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Listen for playback errors (corrupted cache files)
|
||||
useEffect(() => {
|
||||
const subscription = TrackPlayer.addEventListener(
|
||||
Event.PlaybackError,
|
||||
async (event) => {
|
||||
const activeTrack = await TrackPlayer.getActiveTrack();
|
||||
if (!activeTrack?.url) return;
|
||||
|
||||
// Only handle local file errors
|
||||
const url = activeTrack.url as string;
|
||||
if (!url.startsWith("file://")) return;
|
||||
|
||||
console.warn(
|
||||
`[MusicPlayer] Playback error for cached file: ${activeTrack.id}`,
|
||||
event,
|
||||
);
|
||||
|
||||
// Delete corrupted cache file
|
||||
if (activeTrack.id) {
|
||||
try {
|
||||
await deleteTrack(activeTrack.id);
|
||||
console.log(
|
||||
`[MusicPlayer] Deleted corrupted cache file: ${activeTrack.id}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[MusicPlayer] Failed to delete corrupted file:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip to next track
|
||||
try {
|
||||
await TrackPlayer.skipToNext();
|
||||
} catch {
|
||||
// No next track available, stop playback
|
||||
await TrackPlayer.stop();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return () => subscription.remove();
|
||||
}, []);
|
||||
|
||||
// No visual component needed - TrackPlayer is headless
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useNetworkStatus } from "@/hooks/useNetworkStatus";
|
||||
import {
|
||||
audioStorageEvents,
|
||||
getLocalPath,
|
||||
isCached,
|
||||
isPermanentDownloading,
|
||||
isPermanentlyDownloaded,
|
||||
} from "@/providers/AudioStorage";
|
||||
@@ -52,20 +53,20 @@ export const MusicTrackItem: React.FC<Props> = ({
|
||||
const isTrackLoading = loadingTrackId === track.Id;
|
||||
|
||||
// Track download status with reactivity to completion events
|
||||
// Only track permanent downloads - we don't show UI for auto-caching
|
||||
const [downloadStatus, setDownloadStatus] = useState<
|
||||
"none" | "downloading" | "downloaded"
|
||||
"none" | "downloading" | "downloaded" | "cached"
|
||||
>(() => {
|
||||
if (isPermanentlyDownloaded(track.Id)) return "downloaded";
|
||||
if (isPermanentDownloading(track.Id)) return "downloading";
|
||||
if (isCached(track.Id)) return "cached";
|
||||
return "none";
|
||||
});
|
||||
|
||||
// Listen for download completion/error events (only for permanent downloads)
|
||||
// Listen for download completion/error events
|
||||
useEffect(() => {
|
||||
const onComplete = (event: { itemId: string; permanent: boolean }) => {
|
||||
if (event.itemId === track.Id && event.permanent) {
|
||||
setDownloadStatus("downloaded");
|
||||
if (event.itemId === track.Id) {
|
||||
setDownloadStatus(event.permanent ? "downloaded" : "cached");
|
||||
}
|
||||
};
|
||||
const onError = (event: { itemId: string }) => {
|
||||
@@ -83,12 +84,18 @@ export const MusicTrackItem: React.FC<Props> = ({
|
||||
};
|
||||
}, [track.Id]);
|
||||
|
||||
// Also check periodically if permanent download started (for when download is triggered externally)
|
||||
// Re-check status when track changes (for list item recycling)
|
||||
useEffect(() => {
|
||||
if (downloadStatus === "none" && isPermanentDownloading(track.Id)) {
|
||||
if (isPermanentlyDownloaded(track.Id)) {
|
||||
setDownloadStatus("downloaded");
|
||||
} else if (isPermanentDownloading(track.Id)) {
|
||||
setDownloadStatus("downloading");
|
||||
} else if (isCached(track.Id)) {
|
||||
setDownloadStatus("cached");
|
||||
} else {
|
||||
setDownloadStatus("none");
|
||||
}
|
||||
});
|
||||
}, [track.Id]);
|
||||
|
||||
const _isDownloaded = downloadStatus === "downloaded";
|
||||
// Check if available locally (either cached or permanently downloaded)
|
||||
@@ -184,7 +191,7 @@ export const MusicTrackItem: React.FC<Props> = ({
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Download status indicator */}
|
||||
{/* Download/cache status indicator */}
|
||||
{downloadStatus === "downloading" && (
|
||||
<ActivityIndicator
|
||||
size={14}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { Text } from "@/components/common/Text";
|
||||
import { useFavorite } from "@/hooks/useFavorite";
|
||||
import {
|
||||
audioStorageEvents,
|
||||
deleteTrack,
|
||||
downloadTrack,
|
||||
isCached,
|
||||
isPermanentDownloading,
|
||||
@@ -80,6 +81,11 @@ export const TrackOptionsSheet: React.FC<Props> = ({
|
||||
};
|
||||
}, [track?.Id]);
|
||||
|
||||
// Force re-evaluation of cache status when track changes
|
||||
useEffect(() => {
|
||||
setStorageUpdateCounter((c) => c + 1);
|
||||
}, [track?.Id]);
|
||||
|
||||
// Use a placeholder item for useFavorite when track is null
|
||||
const { isFavorite, toggleFavorite } = useFavorite(
|
||||
track ?? ({ Id: "", UserData: { IsFavorite: false } } as BaseItemDto),
|
||||
@@ -180,6 +186,13 @@ export const TrackOptionsSheet: React.FC<Props> = ({
|
||||
setOpen(false);
|
||||
}, [track?.Id, api, user?.Id, isAlreadyDownloaded, setOpen]);
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
if (!track?.Id) return;
|
||||
await deleteTrack(track.Id);
|
||||
setStorageUpdateCounter((c) => c + 1);
|
||||
setOpen(false);
|
||||
}, [track?.Id, setOpen]);
|
||||
|
||||
const handleGoToArtist = useCallback(() => {
|
||||
const artistId = track?.ArtistItems?.[0]?.Id;
|
||||
if (artistId) {
|
||||
@@ -388,6 +401,23 @@ export const TrackOptionsSheet: React.FC<Props> = ({
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(isAlreadyDownloaded || isOnlyCached) && (
|
||||
<>
|
||||
<View style={styles.separator} />
|
||||
<TouchableOpacity
|
||||
onPress={handleDelete}
|
||||
className='flex-row items-center px-4 py-3.5'
|
||||
>
|
||||
<Ionicons name='trash-outline' size={22} color='#ef4444' />
|
||||
<Text className='text-red-500 ml-4 text-base'>
|
||||
{isAlreadyDownloaded
|
||||
? t("music.track_options.delete_download")
|
||||
: t("music.track_options.delete_cache")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Navigation Options */}
|
||||
|
||||
Reference in New Issue
Block a user