forked from Ninjalama/streamyfin_mirror
Compare commits
19 Commits
fix/playba
...
feature/se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
343a2cb3b5 | ||
|
|
49d1be6bdf | ||
|
|
32687332b3 | ||
|
|
7c96443532 | ||
|
|
bf8728eeab | ||
|
|
d7ae41907e | ||
|
|
446439c2e0 | ||
|
|
a5463d783d | ||
|
|
640db35456 | ||
|
|
caa4b765c1 | ||
|
|
9c6aebe66a | ||
|
|
ef42510383 | ||
|
|
5273dfd22b | ||
|
|
00bc4232fb | ||
|
|
35c9258062 | ||
|
|
89bf51c3cc | ||
|
|
f64c5a02db | ||
|
|
cf284eb3d8 | ||
|
|
b581a077e1 |
4
app.json
4
app.json
@@ -33,7 +33,9 @@
|
||||
"jsEngine": "hermes",
|
||||
"versionCode": 53,
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/images/adaptive_icon.png"
|
||||
"foregroundImage": "./assets/images/adaptive_icon.png",
|
||||
"backgroundColor": "#464646"
|
||||
|
||||
},
|
||||
"package": "com.fredrikburmester.streamyfin",
|
||||
"permissions": [
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FlashList } from "@shopify/flash-list";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { Loader } from "@/components/Loader";
|
||||
import { SessionInfoDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { HardwareAccelerationType, SessionInfoDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import Poster from "@/components/posters/Poster";
|
||||
@@ -186,6 +186,7 @@ const TranscodingBadges = ({ properties }: TranscodingBadgesProps) => {
|
||||
resolution: <Ionicons name="film-outline" size={12} color="white" />,
|
||||
language: <Ionicons name="language-outline" size={12} color="white" />,
|
||||
audioChannels: <Ionicons name="mic-outline" size={12} color="white" />,
|
||||
hwType: <Ionicons name="hardware-chip-outline" size={12} color="white" />,
|
||||
} as const;
|
||||
|
||||
const icon = (val: string) => {
|
||||
@@ -200,6 +201,8 @@ const TranscodingBadges = ({ properties }: TranscodingBadgesProps) => {
|
||||
switch (key) {
|
||||
case "bitrate":
|
||||
return formatBitrate(val);
|
||||
case "hwType":
|
||||
return val === HardwareAccelerationType.None ? "sw" : "hw";
|
||||
default:
|
||||
return val;
|
||||
}
|
||||
@@ -219,6 +222,7 @@ const TranscodingBadges = ({ properties }: TranscodingBadgesProps) => {
|
||||
};
|
||||
|
||||
interface StreamProps {
|
||||
hwType?: HardwareAccelerationType | null | undefined;
|
||||
resolution?: string | null | undefined;
|
||||
language?: string | null | undefined;
|
||||
codec?: string | null | undefined;
|
||||
@@ -296,8 +300,8 @@ const TranscodingView = ({ session }: SessionCardProps) => {
|
||||
}, [session.PlayState?.SubtitleStreamIndex]);
|
||||
|
||||
const isTranscoding = useMemo(() => {
|
||||
return session.PlayState?.PlayMethod == "Transcode";
|
||||
}, [session.PlayState?.PlayMethod]);
|
||||
return session.PlayState?.PlayMethod == "Transcode" && session.TranscodingInfo;
|
||||
}, [session.PlayState?.PlayMethod, session.TranscodingInfo]);
|
||||
|
||||
const videoStreamTitle = () => {
|
||||
return videoStream?.DisplayTitle?.split(" ")[0];
|
||||
@@ -313,6 +317,7 @@ const TranscodingView = ({ session }: SessionCardProps) => {
|
||||
codec: videoStream?.Codec,
|
||||
}}
|
||||
transcodeProperties={{
|
||||
hwType: session.TranscodingInfo?.HardwareAccelerationType,
|
||||
bitrate: session.TranscodingInfo?.Bitrate,
|
||||
codec: session.TranscodingInfo?.VideoCodec,
|
||||
}}
|
||||
@@ -332,7 +337,6 @@ const TranscodingView = ({ session }: SessionCardProps) => {
|
||||
audioChannels: audioStream?.ChannelLayout,
|
||||
}}
|
||||
transcodeProperties={{
|
||||
bitrate: session.TranscodingInfo?.Bitrate,
|
||||
codec: session.TranscodingInfo?.AudioCodec,
|
||||
audioChannels: session.TranscodingInfo?.AudioChannels?.toString(),
|
||||
}}
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function page() {
|
||||
});
|
||||
|
||||
return await getStatistics({
|
||||
url: settings?.optimizedVersionsServerUrl,
|
||||
url: updatedUrl,
|
||||
authHeader: api?.accessToken,
|
||||
deviceId: getOrSetDeviceId(),
|
||||
});
|
||||
|
||||
@@ -29,7 +29,14 @@ import { useSharedValue } from "react-native-reanimated";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { BaseItemDto, MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client";
|
||||
import {
|
||||
BaseItemDto,
|
||||
MediaSourceInfo,
|
||||
PlaybackOrder,
|
||||
PlaybackProgressInfo,
|
||||
PlaybackStartInfo,
|
||||
RepeatMode,
|
||||
} from "@jellyfin/sdk/lib/generated-client";
|
||||
|
||||
export default function page() {
|
||||
const videoRef = useRef<VlcPlayerViewRef>(null);
|
||||
@@ -175,16 +182,15 @@ export default function page() {
|
||||
fetchStreamData();
|
||||
}, [itemId, mediaSourceId, bitrateValue, api, item, user?.Id]);
|
||||
|
||||
const togglePlay = useCallback(async () => {
|
||||
if (!api) return;
|
||||
|
||||
const togglePlay = async () => {
|
||||
lightHapticFeedback();
|
||||
setIsPlaying(!isPlaying);
|
||||
if (isPlaying) {
|
||||
await videoRef.current?.pause();
|
||||
} else {
|
||||
videoRef.current?.play();
|
||||
}
|
||||
}, [isPlaying, api, item, stream, videoRef, audioIndex, subtitleIndex, mediaSourceId, offline, progress]);
|
||||
};
|
||||
|
||||
const reportPlaybackStopped = useCallback(async () => {
|
||||
if (offline) return;
|
||||
@@ -212,6 +218,23 @@ export default function page() {
|
||||
};
|
||||
}, [navigation, stop]);
|
||||
|
||||
const currentPlayStateInfo = () => {
|
||||
return {
|
||||
itemId: item?.Id!,
|
||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
||||
mediaSourceId: mediaSourceId,
|
||||
positionTicks: msToTicks(progress.get()),
|
||||
isPaused: !isPlaying,
|
||||
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: stream.sessionId,
|
||||
isMuted: false,
|
||||
canSeek: true,
|
||||
repeatMode: RepeatMode.RepeatNone,
|
||||
playbackOrder: PlaybackOrder.Default,
|
||||
};
|
||||
};
|
||||
|
||||
const onProgress = useCallback(
|
||||
async (data: ProgressUpdatePayload) => {
|
||||
if (isSeeking.get() || isPlaybackStopped) return;
|
||||
@@ -225,20 +248,9 @@ export default function page() {
|
||||
|
||||
if (offline) return;
|
||||
|
||||
const currentTimeInTicks = msToTicks(currentTime);
|
||||
|
||||
if (!item?.Id || !stream) return;
|
||||
|
||||
await getPlaystateApi(api!).onPlaybackProgress({
|
||||
itemId: item.Id,
|
||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
||||
mediaSourceId: mediaSourceId,
|
||||
positionTicks: Math.floor(currentTimeInTicks),
|
||||
isPaused: !isPlaying,
|
||||
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: stream.sessionId,
|
||||
});
|
||||
reportPlaybackProgress();
|
||||
},
|
||||
[item?.Id, audioIndex, subtitleIndex, mediaSourceId, isPlaying, stream, isSeeking, isPlaybackStopped, isBuffering]
|
||||
);
|
||||
@@ -248,49 +260,18 @@ export default function page() {
|
||||
setIsPipStarted(pipStarted);
|
||||
}, []);
|
||||
|
||||
const changePlaybackState = useCallback(
|
||||
async (isPlaying: boolean) => {
|
||||
if (!api || offline || !stream) return;
|
||||
await getPlaystateApi(api).onPlaybackProgress({
|
||||
itemId: item?.Id!,
|
||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
||||
mediaSourceId: mediaSourceId,
|
||||
positionTicks: msToTicks(progress.get()),
|
||||
isPaused: !isPlaying,
|
||||
playMethod: stream?.url.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: stream.sessionId,
|
||||
});
|
||||
},
|
||||
[api, offline, stream, item?.Id, audioIndex, subtitleIndex, mediaSourceId, progress]
|
||||
);
|
||||
const reportPlaybackProgress = useCallback(async () => {
|
||||
if (!api || offline || !stream) return;
|
||||
await getPlaystateApi(api).reportPlaybackProgress({
|
||||
playbackProgressInfo: currentPlayStateInfo() as PlaybackProgressInfo,
|
||||
});
|
||||
}, [api, isPlaying, offline, stream, item?.Id, audioIndex, subtitleIndex, mediaSourceId, progress]);
|
||||
|
||||
const startPosition = useMemo(() => {
|
||||
if (offline) return 0;
|
||||
return item?.UserData?.PlaybackPositionTicks ? ticksToSeconds(item.UserData.PlaybackPositionTicks) : 0;
|
||||
}, [item]);
|
||||
|
||||
const reportPlaybackStart = useCallback(async () => {
|
||||
if (offline || !stream) return;
|
||||
await getPlaystateApi(api!).onPlaybackStart({
|
||||
itemId: item?.Id!,
|
||||
audioStreamIndex: audioIndex ? audioIndex : undefined,
|
||||
subtitleStreamIndex: subtitleIndex ? subtitleIndex : undefined,
|
||||
mediaSourceId: mediaSourceId,
|
||||
playMethod: stream.url?.includes("m3u8") ? "Transcode" : "DirectStream",
|
||||
playSessionId: stream?.sessionId ? stream?.sessionId : undefined,
|
||||
});
|
||||
hasReportedRef.current = true;
|
||||
}, [api, item, stream]);
|
||||
|
||||
const hasReportedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (stream && !hasReportedRef.current) {
|
||||
reportPlaybackStart();
|
||||
hasReportedRef.current = true; // Mark as reported
|
||||
}
|
||||
}, [stream]);
|
||||
|
||||
useWebSocket({
|
||||
isPlaying: isPlaying,
|
||||
togglePlay: togglePlay,
|
||||
@@ -301,17 +282,16 @@ export default function page() {
|
||||
const onPlaybackStateChanged = useCallback(
|
||||
async (e: PlaybackStatePayload) => {
|
||||
const { state, isBuffering, isPlaying } = e.nativeEvent;
|
||||
|
||||
if (state === "Playing") {
|
||||
setIsPlaying(true);
|
||||
await changePlaybackState(true);
|
||||
reportPlaybackProgress();
|
||||
if (!Platform.isTV) await activateKeepAwakeAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === "Paused") {
|
||||
setIsPlaying(false);
|
||||
await changePlaybackState(false);
|
||||
reportPlaybackProgress();
|
||||
if (!Platform.isTV) await deactivateKeepAwake();
|
||||
return;
|
||||
}
|
||||
@@ -323,7 +303,7 @@ export default function page() {
|
||||
setIsBuffering(true);
|
||||
}
|
||||
},
|
||||
[changePlaybackState]
|
||||
[reportPlaybackProgress]
|
||||
);
|
||||
|
||||
const allAudio = stream?.mediaSource.MediaStreams?.filter((audio) => audio.Type === "Audio") || [];
|
||||
|
||||
108
app/_layout.tsx
108
app/_layout.tsx
@@ -2,30 +2,26 @@ import "@/augmentations";
|
||||
import { Platform } from "react-native";
|
||||
import i18n from "@/i18n";
|
||||
import { DownloadProvider } from "@/providers/DownloadProvider";
|
||||
import {
|
||||
getOrSetDeviceId,
|
||||
getTokenFromStorage,
|
||||
JellyfinProvider,
|
||||
} from "@/providers/JellyfinProvider";
|
||||
import { getOrSetDeviceId, getTokenFromStorage, JellyfinProvider, apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { JobQueueProvider } from "@/providers/JobQueueProvider";
|
||||
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
|
||||
import { WebSocketProvider } from "@/providers/WebSocketProvider";
|
||||
import { Settings, useSettings } from "@/utils/atoms/settings";
|
||||
import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks";
|
||||
import {
|
||||
BACKGROUND_FETCH_TASK,
|
||||
BACKGROUND_FETCH_TASK_SESSIONS,
|
||||
registerBackgroundFetchAsyncSessions,
|
||||
} from "@/utils/background-tasks";
|
||||
import { LogProvider, writeToLog } from "@/utils/log";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
|
||||
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
|
||||
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
const BackGroundDownloader = !Platform.isTV
|
||||
? require("@kesha-antonov/react-native-background-downloader")
|
||||
: null;
|
||||
const BackGroundDownloader = !Platform.isTV ? require("@kesha-antonov/react-native-background-downloader") : null;
|
||||
import { DarkTheme, ThemeProvider } from "@react-navigation/native";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
const BackgroundFetch = !Platform.isTV
|
||||
? require("expo-background-fetch")
|
||||
: null;
|
||||
const BackgroundFetch = !Platform.isTV ? require("expo-background-fetch") : null;
|
||||
import * as FileSystem from "expo-file-system";
|
||||
const Notifications = !Platform.isTV ? require("expo-notifications") : null;
|
||||
import { router, Stack } from "expo-router";
|
||||
@@ -41,6 +37,10 @@ import { SystemBars } from "react-native-edge-to-edge";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import "react-native-reanimated";
|
||||
import { Toaster } from "sonner-native";
|
||||
import { useAtom } from "jotai";
|
||||
import { userAtom } from "@/providers/JellyfinProvider";
|
||||
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
|
||||
import { store } from "@/utils/store";
|
||||
|
||||
if (!Platform.isTV) {
|
||||
Notifications.setNotificationHandler({
|
||||
@@ -74,20 +74,16 @@ function useNotificationObserver() {
|
||||
}
|
||||
}
|
||||
|
||||
Notifications.getLastNotificationResponseAsync().then(
|
||||
(response: { notification: any }) => {
|
||||
if (!isMounted || !response?.notification) {
|
||||
return;
|
||||
}
|
||||
redirect(response?.notification);
|
||||
Notifications.getLastNotificationResponseAsync().then((response: { notification: any }) => {
|
||||
if (!isMounted || !response?.notification) {
|
||||
return;
|
||||
}
|
||||
);
|
||||
redirect(response?.notification);
|
||||
});
|
||||
|
||||
const subscription = Notifications.addNotificationResponseReceivedListener(
|
||||
(response: { notification: any }) => {
|
||||
redirect(response.notification);
|
||||
}
|
||||
);
|
||||
const subscription = Notifications.addNotificationResponseReceivedListener((response: { notification: any }) => {
|
||||
redirect(response.notification);
|
||||
});
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
@@ -97,6 +93,22 @@ function useNotificationObserver() {
|
||||
}
|
||||
|
||||
if (!Platform.isTV) {
|
||||
TaskManager.defineTask(BACKGROUND_FETCH_TASK_SESSIONS, async () => {
|
||||
console.log("TaskManager ~ sessions trigger");
|
||||
|
||||
const api = store.get(apiAtom);
|
||||
if (api === null || api === undefined) return;
|
||||
|
||||
const response = await getSessionApi(api).getSessions({
|
||||
activeWithinSeconds: 360,
|
||||
});
|
||||
|
||||
const result = response.data.filter((s) => s.NowPlayingItem);
|
||||
Notifications.setBadgeCountAsync(result.length);
|
||||
|
||||
return BackgroundFetch.BackgroundFetchResult.NewData;
|
||||
});
|
||||
|
||||
TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
|
||||
console.log("TaskManager ~ trigger");
|
||||
|
||||
@@ -109,15 +121,13 @@ if (!Platform.isTV) {
|
||||
const settings: Partial<Settings> = JSON.parse(settingsData);
|
||||
const url = settings?.optimizedVersionsServerUrl;
|
||||
|
||||
if (!settings?.autoDownload || !url)
|
||||
return BackgroundFetch.BackgroundFetchResult.NoData;
|
||||
if (!settings?.autoDownload || !url) return BackgroundFetch.BackgroundFetchResult.NoData;
|
||||
|
||||
const token = getTokenFromStorage();
|
||||
const deviceId = getOrSetDeviceId();
|
||||
const baseDirectory = FileSystem.documentDirectory;
|
||||
|
||||
if (!token || !deviceId || !baseDirectory)
|
||||
return BackgroundFetch.BackgroundFetchResult.NoData;
|
||||
if (!token || !deviceId || !baseDirectory) return BackgroundFetch.BackgroundFetchResult.NoData;
|
||||
|
||||
const jobs = await getAllJobsByDeviceId({
|
||||
deviceId,
|
||||
@@ -194,9 +204,7 @@ if (!Platform.isTV) {
|
||||
|
||||
const checkAndRequestPermissions = async () => {
|
||||
try {
|
||||
const hasAskedBefore = storage.getString(
|
||||
"hasAskedForNotificationPermission"
|
||||
);
|
||||
const hasAskedBefore = storage.getString("hasAskedForNotificationPermission");
|
||||
|
||||
if (hasAskedBefore !== "true") {
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
@@ -214,11 +222,7 @@ const checkAndRequestPermissions = async () => {
|
||||
console.log("Already asked for notification permissions before.");
|
||||
}
|
||||
} catch (error) {
|
||||
writeToLog(
|
||||
"ERROR",
|
||||
"Error checking/requesting notification permissions:",
|
||||
error
|
||||
);
|
||||
writeToLog("ERROR", "Error checking/requesting notification permissions:", error);
|
||||
console.error("Error checking/requesting notification permissions:", error);
|
||||
}
|
||||
};
|
||||
@@ -253,12 +257,11 @@ const queryClient = new QueryClient({
|
||||
|
||||
function Layout() {
|
||||
const [settings] = useSettings();
|
||||
const [user] = useAtom(userAtom);
|
||||
const appState = useRef(AppState.currentState);
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(
|
||||
settings?.preferedLanguage ?? getLocales()[0].languageCode ?? "en"
|
||||
);
|
||||
i18n.changeLanguage(settings?.preferedLanguage ?? getLocales()[0].languageCode ?? "en");
|
||||
}, [settings?.preferedLanguage, i18n]);
|
||||
|
||||
if (!Platform.isTV) {
|
||||
@@ -266,6 +269,11 @@ function Layout() {
|
||||
|
||||
useEffect(() => {
|
||||
checkAndRequestPermissions();
|
||||
(async () => {
|
||||
if (!Platform.isTV && user && user.Policy?.IsAdministrator) {
|
||||
registerBackgroundFetchAsyncSessions();
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -275,24 +283,16 @@ function Layout() {
|
||||
ScreenOrientation.unlockAsync();
|
||||
} else {
|
||||
// If the user has auto rotate disabled, lock the orientation to portrait
|
||||
ScreenOrientation.lockAsync(
|
||||
ScreenOrientation.OrientationLock.PORTRAIT_UP
|
||||
);
|
||||
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = AppState.addEventListener(
|
||||
"change",
|
||||
(nextAppState) => {
|
||||
if (
|
||||
appState.current.match(/inactive|background/) &&
|
||||
nextAppState === "active"
|
||||
) {
|
||||
BackGroundDownloader.checkForExistingDownloads();
|
||||
}
|
||||
const subscription = AppState.addEventListener("change", (nextAppState) => {
|
||||
if (appState.current.match(/inactive|background/) && nextAppState === "active") {
|
||||
BackGroundDownloader.checkForExistingDownloads();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
BackGroundDownloader.checkForExistingDownloads();
|
||||
|
||||
@@ -369,9 +369,7 @@ function Layout() {
|
||||
function saveDownloadedItemInfo(item: BaseItemDto) {
|
||||
try {
|
||||
const downloadedItems = storage.getString("downloadedItems");
|
||||
let items: BaseItemDto[] = downloadedItems
|
||||
? JSON.parse(downloadedItems)
|
||||
: [];
|
||||
let items: BaseItemDto[] = downloadedItems ? JSON.parse(downloadedItems) : [];
|
||||
|
||||
const existingItemIndex = items.findIndex((i) => i.Id === item.Id);
|
||||
if (existingItemIndex !== -1) {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 79 KiB |
@@ -3,20 +3,19 @@ import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { useAtom } from "jotai";
|
||||
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
|
||||
import { userAtom } from "@/providers/JellyfinProvider";
|
||||
import { Platform } from "react-native";
|
||||
const Notifications = !Platform.isTV ? require("expo-notifications") : null;
|
||||
|
||||
export interface useSessionsProps {
|
||||
refetchInterval: number;
|
||||
activeWithinSeconds: number;
|
||||
}
|
||||
|
||||
export const useSessions = ({
|
||||
refetchInterval = 5 * 1000,
|
||||
activeWithinSeconds = 360,
|
||||
}: useSessionsProps) => {
|
||||
export const useSessions = ({ refetchInterval = 5 * 1000, activeWithinSeconds = 360 }: useSessionsProps) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["sessions"],
|
||||
queryFn: async () => {
|
||||
if (!api || !user || !user.Policy?.IsAdministrator) {
|
||||
@@ -25,11 +24,15 @@ export const useSessions = ({
|
||||
const response = await getSessionApi(api).getSessions({
|
||||
activeWithinSeconds: activeWithinSeconds,
|
||||
});
|
||||
return response.data.filter((s) => s.NowPlayingItem);
|
||||
|
||||
const result = response.data
|
||||
.filter((s) => s.NowPlayingItem)
|
||||
.sort((a, b) => (b.NowPlayingItem?.Name ?? "").localeCompare(a.NowPlayingItem?.Name ?? ""));
|
||||
|
||||
Notifications.setBadgeCountAsync(result.length);
|
||||
return result
|
||||
},
|
||||
refetchInterval: refetchInterval,
|
||||
//enabled: !!user || !!user.Policy?.IsAdministrator,
|
||||
//cacheTime: 0
|
||||
});
|
||||
|
||||
return { sessions: data, isLoading };
|
||||
|
||||
@@ -402,7 +402,7 @@ class VlcPlayerView: ExpoView {
|
||||
}
|
||||
|
||||
private func updateVideoProgress() {
|
||||
guard let media = self.vlc.player.media else { return }
|
||||
guard self.vlc.player.media != nil else { return }
|
||||
|
||||
let currentTimeMs = self.vlc.player.time.intValue
|
||||
let durationMs = self.vlc.player.media?.length.intValue ?? 0
|
||||
@@ -459,7 +459,7 @@ extension VlcPlayerView: SimpleAppLifecycleListener {
|
||||
}
|
||||
|
||||
// Current solution to fixing black screen when re-entering application
|
||||
if let videoTrack = self.vlc.player.videoTracks.first { $0.isSelected == true },
|
||||
if let videoTrack = self.vlc.player.videoTracks.first(where: { $0.isSelected == true }),
|
||||
!self.vlc.isMediaPlaying()
|
||||
{
|
||||
videoTrack.isSelected = false
|
||||
@@ -479,6 +479,7 @@ extension VLCMediaPlayerState {
|
||||
case .paused: return "Paused"
|
||||
case .stopped: return "Stopped"
|
||||
case .error: return "Error"
|
||||
case .stopping: return "Stopping"
|
||||
@unknown default: return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"android:tv": "EXPO_TV=1 expo run:android",
|
||||
"prebuild": "EXPO_TV=0 bun run clean",
|
||||
"prebuild:tv": "EXPO_TV=1 bun run clean",
|
||||
"prebuild:tv-new": "EXPO_TV=1 node ./scripts/symlink-native-dirs.js; bun run prebuild:tv",
|
||||
"test": "jest --watchAll",
|
||||
"lint": "expo lint",
|
||||
"postinstall": "patch-package"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useInterval } from "@/hooks/useInterval";
|
||||
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { storage } from "@/utils/mmkv";
|
||||
import { store } from "@/utils/store";
|
||||
import { Api, Jellyfin } from "@jellyfin/sdk";
|
||||
import { UserDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getUserApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
@@ -165,6 +166,10 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
await refreshStreamyfinPluginSettings();
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
store.set(apiAtom, api);
|
||||
}, [api]);
|
||||
|
||||
useInterval(pollQuickConnect, isPolling ? 1000 : null);
|
||||
useInterval(refreshStreamyfinPluginSettings, 60 * 5 * 1000); // 5 min
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
"login_button": "Aanmelden",
|
||||
"quick_connect": "Snel Verbinden",
|
||||
"enter_code_to_login": "Vul code {{code}} in om aan te melden",
|
||||
"failed_to_initiate_quick_connect": "Gefaald om Snel Verbinden op te starten",
|
||||
"failed_to_initiate_quick_connect": "Mislukt om Snel Verbinden op te starten",
|
||||
"got_it": "Begrepen",
|
||||
"connection_failed": "Verbinding gefaald",
|
||||
"connection_failed": "Verbinding mislukt",
|
||||
"could_not_connect_to_server": "Kon niet verbinden met de server. Controleer de URL en je netwerkverbinding.",
|
||||
"an_unexpected_error_occured": "Er is een onverwachte fout opgetreden",
|
||||
"change_server": "Verander server",
|
||||
"invalid_username_or_password": "Ongeldige gebruikersnaam of wachtwoord",
|
||||
"change_server": "Server wijzigen",
|
||||
"invalid_username_or_password": "Onjuiste gebruikersnaam of wachtwoord",
|
||||
"user_does_not_have_permission_to_log_in": "Gebruiker heeft geen rechten om aan te melden",
|
||||
"server_is_taking_too_long_to_respond_try_again_later": "De server doet er te lang over om te antwoorden, probeer later opnieuw",
|
||||
"server_received_too_many_requests_try_again_later": "De server heeft te veel aanvragen ontvangen, probeer later opnieuw",
|
||||
@@ -42,7 +42,7 @@
|
||||
"continue_watching": "Verder Kijken",
|
||||
"next_up": "Volgende",
|
||||
"recently_added_in": "Recent toegevoegd in {{libraryName}}",
|
||||
"suggested_movies": "Voorgestelde Films",
|
||||
"suggested_movies": "Voorgestelde films",
|
||||
"suggested_episodes": "Voorgestelde Afleveringen",
|
||||
"intro": {
|
||||
"welcome_to_streamyfin": "Welkom bij Streamyfin",
|
||||
@@ -56,7 +56,7 @@
|
||||
"centralised_settings_plugin_title": "Plugin voor gecentraliseerde instellingen",
|
||||
"centralised_settings_plugin_description": "Configureer instellingen vanaf een centrale locatie op je Jellyfin server. Alle clientinstellingen voor alle gebruikers worden automatisch gesynchroniseerd.",
|
||||
"done_button": "Gedaan",
|
||||
"go_to_settings_button": "Go naar instellingen",
|
||||
"go_to_settings_button": "Ga naar instellingen",
|
||||
"read_more": "Lees meer"
|
||||
},
|
||||
"settings": {
|
||||
@@ -82,7 +82,7 @@
|
||||
"media_controls": {
|
||||
"media_controls_title": "Media Bedieningen",
|
||||
"forward_skip_length": "Duur voorwaarts overslaan",
|
||||
"rewind_length": "Duur terugspeolen",
|
||||
"rewind_length": "Duur terugspoelen",
|
||||
"seconds_unit": "s"
|
||||
},
|
||||
"audio": {
|
||||
@@ -96,7 +96,7 @@
|
||||
"subtitles": {
|
||||
"subtitle_title": "Ondertitels",
|
||||
"subtitle_language": "Ondertitel taal",
|
||||
"subtitle_mode": "Ondertitle Modus",
|
||||
"subtitle_mode": "Ondertitelmodus",
|
||||
"set_subtitle_track": "Gebruik Ondertitel Track Van Vorig Item",
|
||||
"subtitle_size": "Ondertitel Grootte",
|
||||
"subtitle_hint": "Stel ondertitel voorkeuren in.",
|
||||
@@ -108,7 +108,7 @@
|
||||
"Smart": "Slim",
|
||||
"Always": "Altijd",
|
||||
"None": "Geen",
|
||||
"OnlyForced": "Alleen Geforceeerd"
|
||||
"OnlyForced": "Alleen Geforceerd"
|
||||
}
|
||||
},
|
||||
"other": {
|
||||
@@ -131,18 +131,18 @@
|
||||
"safe_area_in_controls": "Veilig gebied in bedieningen",
|
||||
"show_custom_menu_links": "Aangepaste menulinks tonen",
|
||||
"hide_libraries": "Verberg Bibliotheken",
|
||||
"select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheek tab en hoofdpagina onderdelen.",
|
||||
"select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.",
|
||||
"disable_haptic_feedback": "Haptische feedback uitschakelen",
|
||||
"default_quality": "Standaard kwaliteit"
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Downloads",
|
||||
"download_method": "Download methode",
|
||||
"remux_max_download": "Remux max download",
|
||||
"remux_max_download": "Maximale Remux-download",
|
||||
"auto_download": "Auto download",
|
||||
"optimized_versions_server": "Geoptimaliseerde server versies",
|
||||
"save_button": "Opslaan",
|
||||
"optimized_server": "Geoptimailseerde Server",
|
||||
"optimized_server": "Geoptimaliseerde Server",
|
||||
"optimized": "Geoptimaliseerd",
|
||||
"default": "Standaard",
|
||||
"optimized_version_hint": "Vul de URL van de optimalisatieserver in. De URL moet http of https bevatten en eventueel de poort.",
|
||||
@@ -161,7 +161,7 @@
|
||||
"password_placeholder": "Voeg het wachtwoord in voor de Jellyfin gebruiker {{username}}",
|
||||
"save_button": "Opslaan",
|
||||
"clear_button": "Wissen",
|
||||
"login_button": "Aannmelden",
|
||||
"login_button": "Aanmelden",
|
||||
"total_media_requests": "Totaal aantal mediaverzoeken",
|
||||
"movie_quota_limit": "Limiet filmquota",
|
||||
"movie_quota_days": "Filmquota dagen",
|
||||
@@ -171,7 +171,7 @@
|
||||
"unlimited": "Ongelimiteerd"
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Marlin Search inschakeln ",
|
||||
"enable_marlin_search": "Marlin Search inschakelen ",
|
||||
"url": "URL",
|
||||
"server_url_placeholder": "http(s)://domein.org:poort",
|
||||
"marlin_search_hint": "Vul de URL van de Marlin Search server in. De URL moet http of https bevatten en eventueel de poort.",
|
||||
@@ -205,7 +205,7 @@
|
||||
"system": "Systeem"
|
||||
},
|
||||
"toasts": {
|
||||
"error_deleting_files": "Fout bij het verwijden van bestanden",
|
||||
"error_deleting_files": "Fout bij het verwijderen van bestanden",
|
||||
"background_downloads_enabled": "Downloads op de achtergrond ingeschakeld",
|
||||
"background_downloads_disabled": "Downloads op de achtergrond uitgeschakeld",
|
||||
"connected": "Verbonden",
|
||||
@@ -237,7 +237,7 @@
|
||||
"methods": "Methoden",
|
||||
"toasts": {
|
||||
"you_are_not_allowed_to_download_files": "Je mag geen bestanden downloaden.",
|
||||
"deleted_all_movies_successfully": "Alle filns succesvol verwijderd!",
|
||||
"deleted_all_movies_successfully": "Alle films succesvol verwijderd!",
|
||||
"failed_to_delete_all_movies": "Alle films zijn niet verwijderd",
|
||||
"deleted_all_tvseries_successfully": "Alle series succesvol verwijderd!",
|
||||
"failed_to_delete_all_tvseries": "Alle series zijn niet verwijderd",
|
||||
@@ -280,18 +280,18 @@
|
||||
"recent_requests": "Recent Aangevraagd",
|
||||
"plex_watchlist": "Plex Kijklijst",
|
||||
"trending": "Trending",
|
||||
"popular_movies": "Populaire Films",
|
||||
"popular_movies": "Populaire films",
|
||||
"movie_genres": "Film Genres",
|
||||
"upcoming_movies": "Aankomende Movies",
|
||||
"upcoming_movies": "Aankomende films",
|
||||
"studios": "Studios",
|
||||
"popular_tv": "Populaire TV",
|
||||
"tv_genres": "TV Genres",
|
||||
"upcoming_tv": "Opkomend TV",
|
||||
"upcoming_tv": "Aankomende TV",
|
||||
"networks": "Netwerken",
|
||||
"tmdb_movie_keyword": "TMDB Film Trefwoord",
|
||||
"tmdb_movie_genre": "TMDB Film Genre",
|
||||
"tmdb_movie_genre": "TMDB Filmgenres",
|
||||
"tmdb_tv_keyword": "TMDB TV Trefwoord",
|
||||
"tmdb_tv_genre": "TMDB TV Genre",
|
||||
"tmdb_tv_genre": "TMDB TV-Genres",
|
||||
"tmdb_search": "TMDB Zoeken",
|
||||
"tmdb_studio": "TMDB Studio",
|
||||
"tmdb_network": "TMDB Netwerk",
|
||||
@@ -303,9 +303,9 @@
|
||||
"no_results": "Geen resultaten",
|
||||
"no_libraries_found": "Geen bibliotheken gevonden",
|
||||
"item_types": {
|
||||
"movies": "films",
|
||||
"series": "series",
|
||||
"boxsets": "box sets",
|
||||
"movies": "Films",
|
||||
"series": "Series",
|
||||
"boxsets": "Boxsets",
|
||||
"items": "items"
|
||||
},
|
||||
"options": {
|
||||
@@ -345,7 +345,7 @@
|
||||
"could_not_create_stream_for_chromecast": "Kon geen stream maken voor Chromecast",
|
||||
"message_from_server": "Bericht van de server",
|
||||
"video_has_finished_playing": "Video is gedaan met spelen!",
|
||||
"no_video_source": "Geen video bron...",
|
||||
"no_video_source": "Geen videobron...",
|
||||
"next_episode": "Volgende Aflevering",
|
||||
"refresh_tracks": "Tracks verversen",
|
||||
"subtitle_tracks": "Ondertitel Tracks:",
|
||||
@@ -372,7 +372,7 @@
|
||||
"audio": "Audio",
|
||||
"subtitles": "Ondertitel",
|
||||
"show_more": "Toon meer",
|
||||
"show_less": "Toon minden",
|
||||
"show_less": "Toon minder",
|
||||
"appeared_in": "Verschenen in",
|
||||
"could_not_load_item": "Kon item niet laden",
|
||||
"none": "Geen",
|
||||
@@ -417,7 +417,7 @@
|
||||
"details": "Details",
|
||||
"status": "Status",
|
||||
"original_title": "Originele titel",
|
||||
"series_type": "Serie Type",
|
||||
"series_type": "Serietype",
|
||||
"release_dates": "Verschijningsdatums",
|
||||
"first_air_date": "Eerste uitzenddatum",
|
||||
"next_air_date": "Volgende uitzenddatum",
|
||||
@@ -440,12 +440,12 @@
|
||||
"appearances": "Verschijningen",
|
||||
"toasts": {
|
||||
"jellyseer_does_not_meet_requirements": "Jellyseerr server voldoet niet aan de minimale versievereisten! Update naar minimaal 2.0.0",
|
||||
"jellyseerr_test_failed": "Jellyseerr test gefaald. Probeer opnieuw.",
|
||||
"jellyseerr_test_failed": "Jellyseerr test mislukt. Probeer opnieuw.",
|
||||
"failed_to_test_jellyseerr_server_url": "Mislukt bij het testen van jellyseerr server url",
|
||||
"issue_submitted": "Probleem ingediend!",
|
||||
"requested_item": "{{item}} aangevraagd!",
|
||||
"you_dont_have_permission_to_request": "Je hebt geen toestemming om aanvragen te doen!",
|
||||
"something_went_wrong_requesting_media": "Er ging iets iets mis met het aavragen van media!"
|
||||
"something_went_wrong_requesting_media": "Er ging iets mis met het aanvragen van media!"
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
|
||||
@@ -24,3 +24,26 @@ export async function unregisterBackgroundFetchAsync() {
|
||||
console.log("Error unregistering background fetch task", error);
|
||||
}
|
||||
}
|
||||
|
||||
export const BACKGROUND_FETCH_TASK_SESSIONS =
|
||||
"background-fetch-sessions";
|
||||
|
||||
export async function registerBackgroundFetchAsyncSessions() {
|
||||
try {
|
||||
BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK_SESSIONS, {
|
||||
minimumInterval: 1 * 60, // 1 minutes
|
||||
stopOnTerminate: false, // android only,
|
||||
startOnBoot: true, // android only
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error registering background fetch task", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function unregisterBackgroundFetchAsyncSessions() {
|
||||
try {
|
||||
BackgroundFetch.unregisterTaskAsync(BACKGROUND_FETCH_TASK_SESSIONS);
|
||||
} catch (error) {
|
||||
console.log("Error unregistering background fetch task", error);
|
||||
}
|
||||
}
|
||||
3
utils/store.ts
Normal file
3
utils/store.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { createStore } from 'jotai';
|
||||
|
||||
export const store = createStore();
|
||||
Reference in New Issue
Block a user