mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
fix: separate item and show bar state
This commit is contained in:
@@ -1,10 +1,5 @@
|
|||||||
import { AudioTrackSelector } from "@/components/AudioTrackSelector";
|
import { AudioTrackSelector } from "@/components/AudioTrackSelector";
|
||||||
import { Bitrate, BitrateSelector } from "@/components/BitrateSelector";
|
import { Bitrate, BitrateSelector } from "@/components/BitrateSelector";
|
||||||
import {
|
|
||||||
currentlyPlayingItemAtom,
|
|
||||||
fullScreenAtom,
|
|
||||||
playingAtom,
|
|
||||||
} from "@/components/CurrentlyPlayingBar";
|
|
||||||
import { DownloadItem } from "@/components/DownloadItem";
|
import { DownloadItem } from "@/components/DownloadItem";
|
||||||
import { Loader } from "@/components/Loader";
|
import { Loader } from "@/components/Loader";
|
||||||
import { OverviewText } from "@/components/OverviewText";
|
import { OverviewText } from "@/components/OverviewText";
|
||||||
@@ -42,6 +37,12 @@ import CastContext, {
|
|||||||
useRemoteMediaClient,
|
useRemoteMediaClient,
|
||||||
} from "react-native-google-cast";
|
} from "react-native-google-cast";
|
||||||
import { ParallaxScrollView } from "../../../components/ParallaxPage";
|
import { ParallaxScrollView } from "../../../components/ParallaxPage";
|
||||||
|
import {
|
||||||
|
currentlyPlayingItemAtom,
|
||||||
|
fullScreenAtom,
|
||||||
|
playingAtom,
|
||||||
|
showCurrentlyPlayingBarAtom,
|
||||||
|
} from "@/utils/atoms/playState";
|
||||||
|
|
||||||
const page: React.FC = () => {
|
const page: React.FC = () => {
|
||||||
const local = useLocalSearchParams();
|
const local = useLocalSearchParams();
|
||||||
@@ -55,6 +56,7 @@ const page: React.FC = () => {
|
|||||||
const castDevice = useCastDevice();
|
const castDevice = useCastDevice();
|
||||||
|
|
||||||
const [, setCurrentlyPlying] = useAtom(currentlyPlayingItemAtom);
|
const [, setCurrentlyPlying] = useAtom(currentlyPlayingItemAtom);
|
||||||
|
const [, setShowCurrentlyPlayingBar] = useAtom(showCurrentlyPlayingBarAtom);
|
||||||
const [, setPlaying] = useAtom(playingAtom);
|
const [, setPlaying] = useAtom(playingAtom);
|
||||||
const [, setFullscreen] = useAtom(fullScreenAtom);
|
const [, setFullscreen] = useAtom(fullScreenAtom);
|
||||||
|
|
||||||
@@ -168,6 +170,8 @@ const page: React.FC = () => {
|
|||||||
playbackUrl,
|
playbackUrl,
|
||||||
});
|
});
|
||||||
setPlaying(true);
|
setPlaying(true);
|
||||||
|
setShowCurrentlyPlayingBar(true);
|
||||||
|
|
||||||
if (settings?.openFullScreenVideoPlayerByDefault === true) {
|
if (settings?.openFullScreenVideoPlayerByDefault === true) {
|
||||||
setFullscreen(true);
|
setFullscreen(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||||
|
import {
|
||||||
|
currentlyPlayingItemAtom,
|
||||||
|
fullScreenAtom,
|
||||||
|
playingAtom,
|
||||||
|
showCurrentlyPlayingBarAtom,
|
||||||
|
} from "@/utils/atoms/playState";
|
||||||
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
|
||||||
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
|
||||||
import { reportPlaybackProgress } from "@/utils/jellyfin/playstate/reportPlaybackProgress";
|
import { reportPlaybackProgress } from "@/utils/jellyfin/playstate/reportPlaybackProgress";
|
||||||
@@ -6,12 +12,11 @@ import { reportPlaybackStopped } from "@/utils/jellyfin/playstate/reportPlayback
|
|||||||
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
|
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
|
||||||
import { writeToLog } from "@/utils/log";
|
import { writeToLog } from "@/utils/log";
|
||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
|
||||||
import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { BlurView } from "expo-blur";
|
import { BlurView } from "expo-blur";
|
||||||
import { useRouter, useSegments } from "expo-router";
|
import { useRouter, useSegments } from "expo-router";
|
||||||
import { atom, useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Alert, Platform, TouchableOpacity, View } from "react-native";
|
import { Alert, Platform, TouchableOpacity, View } from "react-native";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
@@ -23,17 +28,9 @@ import Video, { OnProgressData, VideoRef } from "react-native-video";
|
|||||||
import { Text } from "./common/Text";
|
import { Text } from "./common/Text";
|
||||||
import { Loader } from "./Loader";
|
import { Loader } from "./Loader";
|
||||||
|
|
||||||
export const currentlyPlayingItemAtom = atom<{
|
|
||||||
item: BaseItemDto;
|
|
||||||
playbackUrl: string;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
export const playingAtom = atom(false);
|
|
||||||
export const fullScreenAtom = atom(false);
|
|
||||||
|
|
||||||
export const CurrentlyPlayingBar: React.FC = () => {
|
export const CurrentlyPlayingBar: React.FC = () => {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const segments = useSegments();
|
const segments = useSegments();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [api] = useAtom(apiAtom);
|
const [api] = useAtom(apiAtom);
|
||||||
const [user] = useAtom(userAtom);
|
const [user] = useAtom(userAtom);
|
||||||
@@ -42,6 +39,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
currentlyPlayingItemAtom
|
currentlyPlayingItemAtom
|
||||||
);
|
);
|
||||||
const [fullScreen, setFullScreen] = useAtom(fullScreenAtom);
|
const [fullScreen, setFullScreen] = useAtom(fullScreenAtom);
|
||||||
|
const [show, setShow] = useAtom(showCurrentlyPlayingBarAtom);
|
||||||
|
|
||||||
const videoRef = useRef<VideoRef | null>(null);
|
const videoRef = useRef<VideoRef | null>(null);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
@@ -120,21 +118,26 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
|
|
||||||
const onProgress = useCallback(
|
const onProgress = useCallback(
|
||||||
({ currentTime }: OnProgressData) => {
|
({ currentTime }: OnProgressData) => {
|
||||||
if (
|
if (!sessionData?.PlaySessionId || !api || !currentlyPlaying?.item.Id)
|
||||||
!currentTime ||
|
|
||||||
!sessionData?.PlaySessionId ||
|
|
||||||
!playing ||
|
|
||||||
!api ||
|
|
||||||
!currentlyPlaying?.item.Id
|
|
||||||
)
|
|
||||||
return;
|
return;
|
||||||
const newProgress = currentTime * 10000000;
|
const newProgress = currentTime * 10000000;
|
||||||
setProgress(newProgress);
|
setProgress(newProgress);
|
||||||
|
|
||||||
reportPlaybackProgress({
|
reportPlaybackProgress({
|
||||||
api,
|
api,
|
||||||
itemId: currentlyPlaying?.item.Id,
|
itemId: currentlyPlaying?.item.Id,
|
||||||
positionTicks: newProgress,
|
positionTicks: newProgress,
|
||||||
sessionId: sessionData.PlaySessionId,
|
sessionId: sessionData.PlaySessionId,
|
||||||
|
IsPaused: !playing,
|
||||||
|
});
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["nextUp", item?.SeriesId],
|
||||||
|
refetchType: "all",
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["episodes"],
|
||||||
|
refetchType: "all",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[sessionData?.PlaySessionId, api, playing, currentlyPlaying?.item.Id]
|
[sessionData?.PlaySessionId, api, playing, currentlyPlaying?.item.Id]
|
||||||
@@ -147,13 +150,6 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
videoRef.current?.resume();
|
videoRef.current?.resume();
|
||||||
} else {
|
} else {
|
||||||
videoRef.current?.pause();
|
videoRef.current?.pause();
|
||||||
if (progress > 0 && sessionData?.PlaySessionId)
|
|
||||||
reportPlaybackStopped({
|
|
||||||
api,
|
|
||||||
itemId: item?.Id,
|
|
||||||
positionTicks: progress,
|
|
||||||
sessionId: sessionData?.PlaySessionId,
|
|
||||||
});
|
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["nextUp", item?.SeriesId],
|
queryKey: ["nextUp", item?.SeriesId],
|
||||||
@@ -174,6 +170,17 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [fullScreen]);
|
}, [fullScreen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!show && currentlyPlaying && item && sessionData && api) {
|
||||||
|
reportPlaybackStopped({
|
||||||
|
api,
|
||||||
|
itemId: item?.Id,
|
||||||
|
sessionId: sessionData?.PlaySessionId,
|
||||||
|
positionTicks: progress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [show]);
|
||||||
|
|
||||||
const startPosition = useMemo(
|
const startPosition = useMemo(
|
||||||
() =>
|
() =>
|
||||||
item?.UserData?.PlaybackPositionTicks
|
item?.UserData?.PlaybackPositionTicks
|
||||||
@@ -193,7 +200,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
[item]
|
[item]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!currentlyPlaying || !api) return null;
|
if (show === false || !api) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
@@ -223,7 +230,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
${item?.Type === "Audio" ? "aspect-square" : "aspect-video"}
|
${item?.Type === "Audio" ? "aspect-square" : "aspect-video"}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{currentlyPlaying.playbackUrl && (
|
{currentlyPlaying?.playbackUrl && (
|
||||||
<Video
|
<Video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
allowsExternalPlayback
|
allowsExternalPlayback
|
||||||
@@ -272,7 +279,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
setPlaying(false);
|
setPlaying(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
progressUpdateInterval={1000}
|
progressUpdateInterval={2000}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
writeToLog(
|
writeToLog(
|
||||||
@@ -347,7 +354,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setCurrentlyPlaying(null);
|
setShow(false);
|
||||||
}}
|
}}
|
||||||
className="aspect-square rounded flex flex-col items-center justify-center p-2"
|
className="aspect-square rounded flex flex-col items-center justify-center p-2"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
import {
|
||||||
|
currentlyPlayingItemAtom,
|
||||||
|
playingAtom,
|
||||||
|
showCurrentlyPlayingBarAtom,
|
||||||
|
} from "@/utils/atoms/playState";
|
||||||
import { Api, Jellyfin } from "@jellyfin/sdk";
|
import { Api, Jellyfin } from "@jellyfin/sdk";
|
||||||
import { UserDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import { UserDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { isLoaded } from "expo-font";
|
|
||||||
import { router, useSegments } from "expo-router";
|
import { router, useSegments } from "expo-router";
|
||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import React, {
|
import React, {
|
||||||
@@ -21,6 +25,7 @@ interface Server {
|
|||||||
|
|
||||||
export const apiAtom = atom<Api | null>(null);
|
export const apiAtom = atom<Api | null>(null);
|
||||||
export const userAtom = atom<UserDto | null>(null);
|
export const userAtom = atom<UserDto | null>(null);
|
||||||
|
export const wsAtom = atom<WebSocket | null>(null);
|
||||||
|
|
||||||
interface JellyfinContextValue {
|
interface JellyfinContextValue {
|
||||||
discoverServers: (url: string) => Promise<Server[]>;
|
discoverServers: (url: string) => Promise<Server[]>;
|
||||||
@@ -31,7 +36,7 @@ interface JellyfinContextValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const JellyfinContext = createContext<JellyfinContextValue | undefined>(
|
const JellyfinContext = createContext<JellyfinContextValue | undefined>(
|
||||||
undefined,
|
undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
const getOrSetDeviceId = async () => {
|
const getOrSetDeviceId = async () => {
|
||||||
@@ -49,6 +54,15 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const [jellyfin, setJellyfin] = useState<Jellyfin | undefined>(undefined);
|
const [jellyfin, setJellyfin] = useState<Jellyfin | undefined>(undefined);
|
||||||
|
const [deviceId, setDeviceId] = useState<string | undefined>(undefined);
|
||||||
|
const [isConnected, setIsConnected] = useState<boolean>(false);
|
||||||
|
const [playing, setPlaying] = useAtom(playingAtom);
|
||||||
|
const [currentlyPlaying, setCurrentlyPlaying] = useAtom(
|
||||||
|
currentlyPlayingItemAtom
|
||||||
|
);
|
||||||
|
const [showCurrentlyPlayingBar, setShowCurrentlyPlayingBar] = useAtom(
|
||||||
|
showCurrentlyPlayingBarAtom
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -56,19 +70,85 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
setJellyfin(
|
setJellyfin(
|
||||||
() =>
|
() =>
|
||||||
new Jellyfin({
|
new Jellyfin({
|
||||||
clientInfo: { name: "Streamyfin", version: "0.6.1" },
|
clientInfo: { name: "Streamyfin", version: "0.6.2" },
|
||||||
deviceInfo: { name: Platform.OS === "ios" ? "iOS" : "Android", id },
|
deviceInfo: { name: Platform.OS === "ios" ? "iOS" : "Android", id },
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
setDeviceId(id);
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [api, setApi] = useAtom(apiAtom);
|
const [api, setApi] = useAtom(apiAtom);
|
||||||
const [user, setUser] = useAtom(userAtom);
|
const [user, setUser] = useAtom(userAtom);
|
||||||
|
const [ws, setWs] = useAtom(wsAtom);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!deviceId || !api) return;
|
||||||
|
|
||||||
|
const url = `wss://${api?.basePath
|
||||||
|
.replace("https://", "")
|
||||||
|
.replace("http://", "")}/socket?api_key=${
|
||||||
|
api?.accessToken
|
||||||
|
}&deviceId=${deviceId}`;
|
||||||
|
|
||||||
|
console.log("WS", url);
|
||||||
|
|
||||||
|
const newWebSocket = new WebSocket(url);
|
||||||
|
|
||||||
|
let keepAliveInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
newWebSocket.onopen = () => {
|
||||||
|
setIsConnected(true);
|
||||||
|
// Start sending "KeepAlive" message every 30 seconds
|
||||||
|
keepAliveInterval = setInterval(() => {
|
||||||
|
if (newWebSocket.readyState === WebSocket.OPEN) {
|
||||||
|
newWebSocket.send(JSON.stringify({ MessageType: "KeepAlive" }));
|
||||||
|
console.log("KeepAlive message sent");
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
};
|
||||||
|
|
||||||
|
newWebSocket.onmessage = (e) => {
|
||||||
|
const json = JSON.parse(e.data);
|
||||||
|
const command = json?.Data?.Command;
|
||||||
|
|
||||||
|
// On PlayPause
|
||||||
|
if (command === "PlayPause") {
|
||||||
|
console.log("Command ~ PlayPause");
|
||||||
|
setPlaying((state) => !state);
|
||||||
|
} else if (command === "Stop") {
|
||||||
|
console.log("Command ~ Stop");
|
||||||
|
setPlaying(false);
|
||||||
|
setShowCurrentlyPlayingBar(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
newWebSocket.onerror = (e) => {
|
||||||
|
console.error("WebSocket error:", e);
|
||||||
|
setIsConnected(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
newWebSocket.onclose = (e) => {
|
||||||
|
console.log("WebSocket connection closed:", e.reason);
|
||||||
|
if (keepAliveInterval) {
|
||||||
|
clearInterval(keepAliveInterval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setWs(newWebSocket);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (keepAliveInterval) {
|
||||||
|
clearInterval(keepAliveInterval);
|
||||||
|
}
|
||||||
|
newWebSocket.close();
|
||||||
|
};
|
||||||
|
}, [api, deviceId]);
|
||||||
|
|
||||||
const discoverServers = async (url: string): Promise<Server[]> => {
|
const discoverServers = async (url: string): Promise<Server[]> => {
|
||||||
const servers =
|
const servers = await jellyfin?.discovery.getRecommendedServerCandidates(
|
||||||
await jellyfin?.discovery.getRecommendedServerCandidates(url);
|
url
|
||||||
|
);
|
||||||
return servers?.map((server) => ({ address: server.address })) || [];
|
return servers?.map((server) => ({ address: server.address })) || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -144,7 +224,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
const token = await AsyncStorage.getItem("token");
|
const token = await AsyncStorage.getItem("token");
|
||||||
const serverUrl = await AsyncStorage.getItem("serverUrl");
|
const serverUrl = await AsyncStorage.getItem("serverUrl");
|
||||||
const user = JSON.parse(
|
const user = JSON.parse(
|
||||||
(await AsyncStorage.getItem("user")) as string,
|
(await AsyncStorage.getItem("user")) as string
|
||||||
) as UserDto;
|
) as UserDto;
|
||||||
|
|
||||||
if (serverUrl && token && user.Id && jellyfin) {
|
if (serverUrl && token && user.Id && jellyfin) {
|
||||||
|
|||||||
10
utils/atoms/playState.ts
Normal file
10
utils/atoms/playState.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
|
import { atom } from "jotai";
|
||||||
|
|
||||||
|
export const playingAtom = atom(false);
|
||||||
|
export const fullScreenAtom = atom(false);
|
||||||
|
export const showCurrentlyPlayingBarAtom = atom(false);
|
||||||
|
export const currentlyPlayingItemAtom = atom<{
|
||||||
|
item: BaseItemDto;
|
||||||
|
playbackUrl: string;
|
||||||
|
} | null>(null);
|
||||||
@@ -10,6 +10,8 @@ type Settings = {
|
|||||||
deviceProfile?: "Expo" | "Native" | "Old";
|
deviceProfile?: "Expo" | "Native" | "Old";
|
||||||
forceDirectPlay?: boolean;
|
forceDirectPlay?: boolean;
|
||||||
mediaListCollectionIds?: string[];
|
mediaListCollectionIds?: string[];
|
||||||
|
searchEngine: "Marlin" | "Jellyfin";
|
||||||
|
marlinServerUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,6 +35,8 @@ const loadSettings = async (): Promise<Settings> => {
|
|||||||
deviceProfile: "Expo",
|
deviceProfile: "Expo",
|
||||||
forceDirectPlay: false,
|
forceDirectPlay: false,
|
||||||
mediaListCollectionIds: [],
|
mediaListCollectionIds: [],
|
||||||
|
searchEngine: "Jellyfin",
|
||||||
|
marlinServerUrl: "",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Api } from "@jellyfin/sdk";
|
import { Api } from "@jellyfin/sdk";
|
||||||
import { AxiosError } from "axios";
|
|
||||||
import { getAuthHeaders } from "../jellyfin";
|
import { getAuthHeaders } from "../jellyfin";
|
||||||
|
import { postCapabilities } from "../session/capabilities";
|
||||||
|
|
||||||
interface ReportPlaybackProgressParams {
|
interface ReportPlaybackProgressParams {
|
||||||
api: Api;
|
api?: Api | null;
|
||||||
sessionId: string;
|
sessionId?: string | null;
|
||||||
itemId: string;
|
itemId?: string | null;
|
||||||
positionTicks: number;
|
positionTicks?: number | null;
|
||||||
|
IsPaused?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,25 +21,39 @@ export const reportPlaybackProgress = async ({
|
|||||||
sessionId,
|
sessionId,
|
||||||
itemId,
|
itemId,
|
||||||
positionTicks,
|
positionTicks,
|
||||||
|
IsPaused = false,
|
||||||
}: ReportPlaybackProgressParams): Promise<void> => {
|
}: ReportPlaybackProgressParams): Promise<void> => {
|
||||||
console.info(
|
if (!api || !sessionId || !itemId || !positionTicks) {
|
||||||
"Reporting playback progress:",
|
console.error("Missing required parameter");
|
||||||
sessionId,
|
return;
|
||||||
itemId,
|
}
|
||||||
positionTicks,
|
|
||||||
);
|
console.info("reportPlaybackProgress ~ IsPaused", IsPaused);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await postCapabilities({
|
||||||
|
api,
|
||||||
|
itemId,
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to post capabilities.", error);
|
||||||
|
throw new Error("Failed to post capabilities.");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.axiosInstance.post(
|
await api.axiosInstance.post(
|
||||||
`${api.basePath}/Sessions/Playing/Progress`,
|
`${api.basePath}/Sessions/Playing/Progress`,
|
||||||
{
|
{
|
||||||
ItemId: itemId,
|
ItemId: itemId,
|
||||||
PlaySessionId: sessionId,
|
PlaySessionId: sessionId,
|
||||||
IsPaused: false,
|
IsPaused,
|
||||||
PositionTicks: Math.round(positionTicks),
|
PositionTicks: Math.round(positionTicks),
|
||||||
CanSeek: true,
|
CanSeek: true,
|
||||||
MediaSourceId: itemId,
|
MediaSourceId: itemId,
|
||||||
|
EventName: "timeupdate",
|
||||||
},
|
},
|
||||||
{ headers: getAuthHeaders(api) },
|
{ headers: getAuthHeaders(api) }
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -41,12 +41,15 @@ export const reportPlaybackStopped = async ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("reportPlaybackStopped ~", { sessionId, itemId });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = `${api.basePath}/PlayingItems/${itemId}`;
|
const url = `${api.basePath}/PlayingItems/${itemId}`;
|
||||||
const params = {
|
const params = {
|
||||||
playSessionId: sessionId,
|
playSessionId: sessionId,
|
||||||
positionTicks: Math.round(positionTicks),
|
positionTicks: Math.round(positionTicks),
|
||||||
mediaSourceId: itemId,
|
MediaSourceId: itemId,
|
||||||
|
IsPaused: true,
|
||||||
};
|
};
|
||||||
const headers = getAuthHeaders(api);
|
const headers = getAuthHeaders(api);
|
||||||
|
|
||||||
@@ -58,7 +61,7 @@ export const reportPlaybackStopped = async ({
|
|||||||
console.error(
|
console.error(
|
||||||
"Failed to report playback progress",
|
"Failed to report playback progress",
|
||||||
error.message,
|
error.message,
|
||||||
error.response?.data,
|
error.response?.data
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to report playback progress", error);
|
console.error("Failed to report playback progress", error);
|
||||||
|
|||||||
48
utils/jellyfin/session/capabilities.ts
Normal file
48
utils/jellyfin/session/capabilities.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Api } from "@jellyfin/sdk";
|
||||||
|
import {
|
||||||
|
SessionApi,
|
||||||
|
SessionApiPostCapabilitiesRequest,
|
||||||
|
} from "@jellyfin/sdk/lib/generated-client/api/session-api";
|
||||||
|
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import { getAuthHeaders } from "../jellyfin";
|
||||||
|
|
||||||
|
interface PostCapabilitiesParams {
|
||||||
|
api: Api | null | undefined;
|
||||||
|
itemId: string | null | undefined;
|
||||||
|
sessionId: string | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks a media item as not played for a specific user.
|
||||||
|
*
|
||||||
|
* @param params - The parameters for marking an item as not played
|
||||||
|
* @returns A promise that resolves to true if the operation was successful, false otherwise
|
||||||
|
*/
|
||||||
|
export const postCapabilities = async ({
|
||||||
|
api,
|
||||||
|
itemId,
|
||||||
|
sessionId,
|
||||||
|
}: PostCapabilitiesParams): Promise<void> => {
|
||||||
|
if (!api || !itemId || !sessionId) {
|
||||||
|
throw new Error("Missing required parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await api.axiosInstance.post(
|
||||||
|
api.basePath + "/Sessions/Capabilities/Full",
|
||||||
|
{
|
||||||
|
playableMediaTypes: ["Audio", "Video", "Audio"],
|
||||||
|
supportedCommands: ["PlayState", "Play"],
|
||||||
|
supportsMediaControl: true,
|
||||||
|
id: sessionId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: getAuthHeaders(api),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error: any | AxiosError) {
|
||||||
|
console.log("Failed to mark as not played", error);
|
||||||
|
throw new Error("Failed to mark as not played");
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user