diff --git a/app/(auth)/(tabs)/(home,libraries,search)/albums/[albumId].tsx b/app/(auth)/(tabs)/(home,libraries,search)/albums/[albumId].tsx
index 3e17610d..565f84c8 100644
--- a/app/(auth)/(tabs)/(home,libraries,search)/albums/[albumId].tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search)/albums/[albumId].tsx
@@ -1,7 +1,9 @@
import { Chromecast } from "@/components/Chromecast";
+import { ItemImage } from "@/components/common/ItemImage";
import { Text } from "@/components/common/Text";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import { SongsList } from "@/components/music/SongsList";
+import { ParallaxScrollView } from "@/components/ParallaxPage";
import ArtistPoster from "@/components/posters/ArtistPoster";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
@@ -11,6 +13,7 @@ import { router, useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai";
import { useEffect, useState } from "react";
import { ScrollView, TouchableOpacity, View } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function page() {
const searchParams = useLocalSearchParams();
@@ -88,30 +91,31 @@ export default function page() {
enabled: !!api && !!user?.Id,
});
+ const insets = useSafeAreaInsets();
+
if (!album) return null;
return (
-
-
-
-
-
-
-
- {album?.Name}
- {album?.ProductionYear}
-
-
- {album.AlbumArtists?.map((a) => (
-
-
- {album?.AlbumArtist}
-
-
- ))}
-
-
-
+
+ }
+ >
+
+ {album?.Name}
+
+ {songs?.TotalRecordCount} songs
+
+
+
-
+
);
}
diff --git a/app/(auth)/(tabs)/(home,libraries,search)/artists/[artistId].tsx b/app/(auth)/(tabs)/(home,libraries,search)/artists/[artistId].tsx
index 4a60fb06..8d82d205 100644
--- a/app/(auth)/(tabs)/(home,libraries,search)/artists/[artistId].tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search)/artists/[artistId].tsx
@@ -8,6 +8,10 @@ import { router, useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai";
import { useEffect, useState } from "react";
import { FlatList, ScrollView, TouchableOpacity, View } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { ItemImage } from "@/components/common/ItemImage";
+import { ParallaxScrollView } from "@/components/ParallaxPage";
+import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
export default function page() {
const searchParams = useLocalSearchParams();
@@ -82,50 +86,45 @@ export default function page() {
enabled: !!api && !!user?.Id,
});
- useEffect(() => {
- navigation.setOptions({
- title: albums?.Items?.[0]?.AlbumArtist || "",
- });
- }, [albums]);
+ const insets = useSafeAreaInsets();
if (!artist || !albums) return null;
return (
-
-
-
-
- Albums
-
- }
- nestedScrollEnabled
- data={albums.Items}
- numColumns={3}
- columnWrapperStyle={{
- justifyContent: "space-between",
- }}
- renderItem={({ item, index }) => (
- {
- router.push(`/albums/${item.Id}`);
+
-
-
- {item.Name}
- {item.ProductionYear}
-
-
- )}
- keyExtractor={(item) => item.Id || ""}
- />
+ />
+ }
+ >
+
+ {artist?.Name}
+
+ {albums.TotalRecordCount} albums
+
+
+
+ {albums.Items.map((item, idx) => (
+
+
+
+ {item.Name}
+ {item.ProductionYear}
+
+
+ ))}
+
+
);
}
diff --git a/app/(auth)/(tabs)/(home,libraries,search)/songs/[songId].tsx b/app/(auth)/(tabs)/(home,libraries,search)/songs/[songId].tsx
deleted file mode 100644
index 8a93d33d..00000000
--- a/app/(auth)/(tabs)/(home,libraries,search)/songs/[songId].tsx
+++ /dev/null
@@ -1,271 +0,0 @@
-import { AudioTrackSelector } from "@/components/AudioTrackSelector";
-import { Bitrate, BitrateSelector } from "@/components/BitrateSelector";
-import { Chromecast } from "@/components/Chromecast";
-import { Text } from "@/components/common/Text";
-import { DownloadItem } from "@/components/DownloadItem";
-import { Loader } from "@/components/Loader";
-import { MoviesTitleHeader } from "@/components/movies/MoviesTitleHeader";
-import { ParallaxScrollView } from "@/components/ParallaxPage";
-import { PlayButton } from "@/components/PlayButton";
-import { NextEpisodeButton } from "@/components/series/NextEpisodeButton";
-import { SimilarItems } from "@/components/SimilarItems";
-import { SubtitleTrackSelector } from "@/components/SubtitleTrackSelector";
-import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
-import { usePlayback } from "@/providers/PlaybackProvider";
-import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
-import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
-import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
-import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
-import { chromecastProfile } from "@/utils/profiles/chromecast";
-import ios from "@/utils/profiles/ios";
-import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
-import { useQuery } from "@tanstack/react-query";
-import { Image } from "expo-image";
-import { useLocalSearchParams, useNavigation } from "expo-router";
-import { useAtom } from "jotai";
-import { useCallback, useEffect, useMemo, useState } from "react";
-import { ScrollView, View } from "react-native";
-import CastContext, {
- PlayServicesState,
- useCastDevice,
- useRemoteMediaClient,
-} from "react-native-google-cast";
-
-const page: React.FC = () => {
- const local = useLocalSearchParams();
- const { songId: id } = local as { songId: string };
-
- const [api] = useAtom(apiAtom);
- const [user] = useAtom(userAtom);
-
- const { setCurrentlyPlayingState } = usePlayback();
-
- const castDevice = useCastDevice();
- const navigation = useNavigation();
-
- useEffect(() => {
- navigation.setOptions({
- headerRight: () => (
-
-
-
- ),
- });
- });
-
- const chromecastReady = useMemo(() => !!castDevice?.deviceId, [castDevice]);
- const [selectedAudioStream, setSelectedAudioStream] = useState(-1);
- const [selectedSubtitleStream, setSelectedSubtitleStream] =
- useState(0);
- const [maxBitrate, setMaxBitrate] = useState({
- key: "Max",
- value: undefined,
- });
-
- const { data: item, isLoading: l1 } = useQuery({
- queryKey: ["item", id],
- queryFn: async () =>
- await getUserItemData({
- api,
- userId: user?.Id,
- itemId: id,
- }),
- enabled: !!id && !!api,
- staleTime: 60 * 1000,
- });
-
- const backdropUrl = useMemo(
- () =>
- getBackdropUrl({
- api,
- item,
- quality: 90,
- width: 1000,
- }),
- [item]
- );
-
- const logoUrl = useMemo(
- () => (item?.Type === "Movie" ? getLogoImageUrlById({ api, item }) : null),
- [item]
- );
-
- const { data: sessionData } = useQuery({
- queryKey: ["sessionData", item?.Id],
- queryFn: async () => {
- if (!api || !user?.Id || !item?.Id) return null;
- const playbackData = await getMediaInfoApi(api!).getPlaybackInfo({
- itemId: item?.Id,
- userId: user?.Id,
- });
-
- return playbackData.data;
- },
- enabled: !!item?.Id && !!api && !!user?.Id,
- staleTime: 0,
- });
-
- const { data: playbackUrl } = useQuery({
- queryKey: [
- "playbackUrl",
- item?.Id,
- maxBitrate,
- castDevice,
- selectedAudioStream,
- selectedSubtitleStream,
- ],
- queryFn: async () => {
- if (!api || !user?.Id || !sessionData) return null;
-
- const url = await getStreamUrl({
- api,
- userId: user.Id,
- item,
- startTimeTicks: item?.UserData?.PlaybackPositionTicks || 0,
- maxStreamingBitrate: maxBitrate.value,
- sessionData,
- deviceProfile: castDevice?.deviceId ? chromecastProfile : ios,
- audioStreamIndex: selectedAudioStream,
- subtitleStreamIndex: selectedSubtitleStream,
- });
-
- console.log("Transcode URL: ", url);
-
- return url;
- },
- enabled: !!sessionData,
- staleTime: 0,
- });
-
- const client = useRemoteMediaClient();
-
- const onPressPlay = useCallback(
- async (type: "device" | "cast" = "device") => {
- if (!playbackUrl || !item) return;
-
- if (type === "cast" && client) {
- await CastContext.getPlayServicesState().then((state) => {
- if (state && state !== PlayServicesState.SUCCESS)
- CastContext.showPlayServicesErrorDialog(state);
- else {
- client.loadMedia({
- mediaInfo: {
- contentUrl: playbackUrl,
- contentType: "video/mp4",
- metadata: {
- type: item.Type === "Episode" ? "tvShow" : "movie",
- title: item.Name || "",
- subtitle: item.Overview || "",
- },
- },
- startTime: 0,
- });
- }
- });
- } else {
- setCurrentlyPlayingState({
- item,
- url: playbackUrl,
- });
- }
- },
- [playbackUrl, item]
- );
-
- if (l1)
- return (
-
-
-
- );
-
- if (!item?.Id || !backdropUrl) return null;
-
- return (
-
- }
- logo={
- <>
- {logoUrl ? (
-
- ) : null}
- >
- }
- >
-
-
-
- {item?.ProductionYear}
-
-
-
- {playbackUrl ? (
-
- ) : (
-
- )}
-
-
-
-
- setMaxBitrate(val)}
- selected={maxBitrate}
- />
-
-
-
-
-
-
-
-
-
-
-
-
- Audio
-
-
-
- {item.MediaStreams?.find((i) => i.Type === "Audio")?.DisplayTitle}
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default page;
diff --git a/components/music/SongsListItem.tsx b/components/music/SongsListItem.tsx
index d2688b5d..76ed9f73 100644
--- a/components/music/SongsListItem.tsx
+++ b/components/music/SongsListItem.tsx
@@ -71,7 +71,10 @@ export const SongsListItem: React.FC = ({
};
const play = async (type: "device" | "cast") => {
- if (!user?.Id || !api || !item.Id) return;
+ if (!user?.Id || !api || !item.Id) {
+ console.warn("No user, api or item", user, api, item.Id);
+ return;
+ }
const response = await getMediaInfoApi(api!).getPlaybackInfo({
itemId: item?.Id,
@@ -87,9 +90,13 @@ export const SongsListItem: React.FC = ({
startTimeTicks: item?.UserData?.PlaybackPositionTicks || 0,
sessionData,
deviceProfile: castDevice?.deviceId ? chromecastProfile : ios,
+ mediaSourceId: item.Id,
});
- if (!url || !item) return;
+ if (!url || !item) {
+ console.warn("No url or item", url, item.Id);
+ return;
+ }
if (type === "cast" && client) {
await CastContext.getPlayServicesState().then((state) => {
@@ -111,6 +118,7 @@ export const SongsListItem: React.FC = ({
}
});
} else {
+ console.log("Playing on device", url, item.Id);
setCurrentlyPlayingState({
item,
url,
diff --git a/components/stacks/NestedTabPageStack.tsx b/components/stacks/NestedTabPageStack.tsx
index 930c4bfc..32caef76 100644
--- a/components/stacks/NestedTabPageStack.tsx
+++ b/components/stacks/NestedTabPageStack.tsx
@@ -17,7 +17,6 @@ const routes = [
"artists/[artistId]",
"collections/[collectionId]",
"items/page",
- "songs/[songId]",
"series/[id]",
];