This commit is contained in:
Fredrik Burmester
2024-08-04 14:01:48 +02:00
parent 7e9ccc38e6
commit c4ae33510d
5 changed files with 175 additions and 46 deletions

View File

@@ -85,6 +85,16 @@ export default function index() {
staleTime: 60, staleTime: 60,
}); });
if (isError)
return (
<View className="flex flex-col items-center justify-center h-full -mt-12">
<Text className="text-3xl font-bold mb-2">Oops!</Text>
<Text className="text-center opacity-70">
Something went wrong.{"\n"}Please log out and in again.
</Text>
</View>
);
if (isLoading) if (isLoading)
return ( return (
<View className="justify-center items-center h-full"> <View className="justify-center items-center h-full">
@@ -92,8 +102,6 @@ export default function index() {
</View> </View>
); );
if (isError) return <Text>Error loading items</Text>;
if (!data || data.length === 0) return <Text>No data...</Text>; if (!data || data.length === 0) return <Text>No data...</Text>;
return ( return (

View File

@@ -1,9 +1,9 @@
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { runningProcesses } from "@/components/DownloadItem";
import { ListItem } from "@/components/ListItem"; import { ListItem } from "@/components/ListItem";
import ProgressCircle from "@/components/ProgressCircle"; import ProgressCircle from "@/components/ProgressCircle";
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
import { runningProcesses } from "@/utils/atoms/downloads";
import { readFromLog } from "@/utils/log"; import { readFromLog } 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 { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
@@ -38,14 +38,11 @@ const deleteFile = async (id: string | null | undefined) => {
(err) => console.error(err) (err) => console.error(err)
); );
AsyncStorage.setItem( const currentFiles = JSON.parse(
"downloaded_files", (await AsyncStorage.getItem("downloaded_files")) ?? "[]"
JSON.stringify([
JSON.parse(
(await AsyncStorage.getItem("downloaded_files")) || "[]"
).filter((f: string) => f !== id),
])
); );
const updatedFiles = currentFiles.filter((f: string) => f !== id);
await AsyncStorage.setItem("downloaded_files", JSON.stringify(updatedFiles));
}; };
const listDownloadedFiles = async () => { const listDownloadedFiles = async () => {
@@ -81,6 +78,11 @@ export default function settings() {
(await AsyncStorage.getItem("downloaded_files")) || "[]" (await AsyncStorage.getItem("downloaded_files")) || "[]"
) as BaseItemDto[]; ) as BaseItemDto[];
console.log(
"Files",
data.map((i) => i.Name)
);
setFiles(data); setFiles(data);
})(); })();
}, [key]); }, [key]);
@@ -160,8 +162,8 @@ export default function settings() {
<Button <Button
className="mb-2" className="mb-2"
color="red" color="red"
onPress={() => { onPress={async () => {
deleteAllFiles(); await deleteAllFiles();
setKey((prevKey) => prevKey + 1); setKey((prevKey) => prevKey + 1);
}} }}
> >

View File

@@ -103,7 +103,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({ itemId }) => {
const { data: playbackURL } = useQuery({ const { data: playbackURL } = useQuery({
queryKey: ["playbackUrl", itemId, maxBitrate, forceTranscoding], queryKey: ["playbackUrl", itemId, maxBitrate, forceTranscoding],
queryFn: async () => { queryFn: async () => {
if (!api || !user?.Id) return null; if (!api || !user?.Id || !sessionData) return null;
const url = await getStreamUrl({ const url = await getStreamUrl({
api, api,
@@ -111,6 +111,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({ itemId }) => {
item, item,
startTimeTicks: item?.UserData?.PlaybackPositionTicks || 0, startTimeTicks: item?.UserData?.PlaybackPositionTicks || 0,
maxStreamingBitrate: maxBitrate, maxStreamingBitrate: maxBitrate,
sessionData,
forceTranscoding: forceTranscoding, forceTranscoding: forceTranscoding,
}); });
@@ -118,7 +119,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({ itemId }) => {
return url; return url;
}, },
enabled: !!itemId && !!api && !!user?.Id && !!item, enabled: !!itemId && !!api && !!user?.Id && !!item && !!sessionData,
staleTime: 0, staleTime: 0,
}); });

View File

@@ -1,5 +1,8 @@
import { Api } from "@jellyfin/sdk"; import { Api } from "@jellyfin/sdk";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import {
BaseItemDto,
PlaybackInfoResponse,
} from "@jellyfin/sdk/lib/generated-client/models";
import { import {
getMediaInfoApi, getMediaInfoApi,
getUserLibraryApi, getUserLibraryApi,
@@ -59,14 +62,17 @@ export const useDownloadMedia = (api: Api | null) => {
console.log("File downloaded to:", uri); console.log("File downloaded to:", uri);
const currentFiles: BaseItemDto[] = JSON.parse( const currentFiles: BaseItemDto[] = JSON.parse(
(await AsyncStorage.getItem("downloaded_files")) || "[]" (await AsyncStorage.getItem("downloaded_files")) ?? "[]"
); );
const otherItems = currentFiles.filter((i) => i.Id !== itemId); const updatedFiles = [
...currentFiles.filter((file) => file.Id !== itemId),
item,
];
await AsyncStorage.setItem( await AsyncStorage.setItem(
"downloaded_files", "downloaded_files",
JSON.stringify([...otherItems, item]) JSON.stringify(updatedFiles)
); );
setIsDownloading(false); setIsDownloading(false);
@@ -429,6 +435,7 @@ export const getStreamUrl = async ({
startTimeTicks = 0, startTimeTicks = 0,
maxStreamingBitrate = 140000000, maxStreamingBitrate = 140000000,
forceTranscoding = false, forceTranscoding = false,
sessionData,
}: { }: {
api: Api | null | undefined; api: Api | null | undefined;
item: BaseItemDto | null | undefined; item: BaseItemDto | null | undefined;
@@ -436,6 +443,7 @@ export const getStreamUrl = async ({
startTimeTicks: number; startTimeTicks: number;
maxStreamingBitrate?: number; maxStreamingBitrate?: number;
forceTranscoding?: boolean; forceTranscoding?: boolean;
sessionData: PlaybackInfoResponse;
}) => { }) => {
if (!api || !userId || !item?.Id) { if (!api || !userId || !item?.Id) {
return null; return null;
@@ -469,38 +477,72 @@ export const getStreamUrl = async ({
); );
const data = response.data; const data = response.data;
const mediaSource = item.MediaSources?.[0];
const sessionId = sessionData.PlaySessionId;
if (item.MediaSources?.[0].SupportsDirectPlay) { if (!mediaSource) throw new Error("no media source");
console.log("Direct play supported"); if (!sessionId) throw new Error("no sessionId");
}
if ( const streamParams = new URLSearchParams({
item.MediaSources?.[0].SupportsTranscoding && Static: "true",
data.MediaSources?.[0].TranscodingUrl api_key: api.accessToken,
) { playSessionId: sessionData.PlaySessionId || "",
console.log("Supports transcoding"); videoCodec: "hevc,h264",
} audioCodec: "aac,mp3,ac3,eac3,flac,alac",
maxAudioChannels: "6",
mediaSourceId: itemId,
Tag: mediaSource.ETag || "",
VideoBitrate: "324036",
TranscodingMaxAudioChannels: "2",
RequireAvc: "false",
SegmentContainer: "mp4",
MinSegments: "2",
BreakOnNonKeyFrames: "True",
"h264-level": "40",
"h264-videobitdepth": "8",
"h264-profile": "high",
"h264-audiochannels": "2",
"aac-profile": "lc",
"h264-rangetype": "SDR",
"h264-deinterlace": "true",
TranscodeReasons: "ContainerBitrateExceedsLimit",
});
if ( url = `${
item.MediaSources?.[0].SupportsTranscoding && api.basePath
!data.MediaSources?.[0].TranscodingUrl }/Videos/${itemId}/main.m3u8?${streamParams.toString()}`;
) {
console.log("Supports transcoding, but no URL found");
}
if (data.MediaSources?.[0].TranscodingUrl) { // if (item.MediaSources?.[0].SupportsDirectPlay) {
url = api.basePath + data.MediaSources?.[0].TranscodingUrl; // console.log("Direct play supported");
} else { // }
url = buildStreamUrl({
apiKey: api.accessToken || "", // if (
sessionId: "", // item.MediaSources?.[0].SupportsTranscoding &&
itemId: itemId, // data.MediaSources?.[0].TranscodingUrl
serverUrl: api.basePath || "", // ) {
deviceId: "unique-device-id", // console.log("Supports transcoding");
mediaSourceId: data.MediaSources?.[0].Id || "", // }
tag: data.MediaSources?.[0]?.ETag || "",
}).toString(); // if (
} // item.MediaSources?.[0].SupportsTranscoding &&
// !data.MediaSources?.[0].TranscodingUrl
// ) {
// console.log("Supports transcoding, but no URL found");
// }
// if (data.MediaSources?.[0].TranscodingUrl) {
// url = api.basePath + data.MediaSources?.[0].TranscodingUrl;
// } else {
// url = buildStreamUrl({
// apiKey: api.accessToken || "",
// sessionId: "",
// itemId: itemId,
// serverUrl: api.basePath || "",
// deviceId: "unique-device-id",
// mediaSourceId: data.MediaSources?.[0].Id || "",
// tag: data.MediaSources?.[0]?.ETag || "",
// }).toString();
// }
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }

View File

@@ -0,0 +1,76 @@
import { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models";
export function createVideoUrl(mediaSource: MediaSourceInfo): string {
const baseUrl = `/videos/${mediaSource.Id}/main.m3u8`;
const urlParams = new URLSearchParams();
// Extract query parameters from TranscodingUrl
const transcodingUrlParts = mediaSource.TranscodingUrl?.split("?") ?? [];
if (transcodingUrlParts.length > 1) {
const queryParams = new URLSearchParams(transcodingUrlParts[1]);
queryParams.forEach((value, key) => {
urlParams.append(key, value);
});
}
// Add or update specific parameters based on the mediaSource object
if (mediaSource.DefaultAudioStreamIndex !== undefined) {
urlParams.set(
"AudioStreamIndex",
mediaSource.DefaultAudioStreamIndex?.toString() || ""
);
}
if (mediaSource.DefaultSubtitleStreamIndex !== undefined) {
urlParams.set(
"SubtitleStreamIndex",
mediaSource.DefaultSubtitleStreamIndex?.toString() || ""
);
}
// Add information about available streams
if (mediaSource.MediaStreams) {
const videoStreams = mediaSource.MediaStreams.filter(
(stream) => stream.Type === "Video"
);
const audioStreams = mediaSource.MediaStreams.filter(
(stream) => stream.Type === "Audio"
);
const subtitleStreams = mediaSource.MediaStreams.filter(
(stream) => stream.Type === "Subtitle"
);
if (videoStreams.length > 0) {
urlParams.set(
"VideoStreamIndex",
videoStreams[0].Index?.toString() || ""
);
}
if (audioStreams.length > 0) {
const defaultAudioStream =
audioStreams.find((stream) => stream.IsDefault) || audioStreams[0];
urlParams.set(
"AudioStreamIndex",
defaultAudioStream.Index?.toString() || ""
);
urlParams.set("AudioCodec", defaultAudioStream.Codec || "");
}
if (subtitleStreams.length > 0) {
const defaultSubtitleStream = subtitleStreams.find(
(stream) => stream.IsDefault
);
if (defaultSubtitleStream?.Index) {
urlParams.set(
"SubtitleStreamIndex",
defaultSubtitleStream.Index.toString()
);
}
}
}
console.log("createVideoUrl ~", `${baseUrl}?${urlParams.toString()}`);
return `${baseUrl}?${urlParams.toString()}`;
}