mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-12 17:00:23 +01:00
Merge branch 'master' of https://github.com/fredrikburmester/streamyfin
This commit is contained in:
@@ -34,6 +34,11 @@ import {useJellyseerr} from "@/hooks/useJellyseerr";
|
|||||||
import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search";
|
import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search";
|
||||||
import {MediaType} from "@/utils/jellyseerr/server/constants/media";
|
import {MediaType} from "@/utils/jellyseerr/server/constants/media";
|
||||||
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
|
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
|
||||||
|
import {Tag} from "@/components/GenreTags";
|
||||||
|
import DiscoverSlide from "@/components/jellyseerr/DiscoverSlide";
|
||||||
|
import {sortBy} from "lodash";
|
||||||
|
|
||||||
|
type SearchType = 'Library' | 'Discover';
|
||||||
|
|
||||||
const exampleSearches = [
|
const exampleSearches = [
|
||||||
"Lord of the rings",
|
"Lord of the rings",
|
||||||
@@ -50,6 +55,7 @@ export default function search() {
|
|||||||
|
|
||||||
const { q, prev } = params as { q: string; prev: Href<string> };
|
const { q, prev } = params as { q: string; prev: Href<string> };
|
||||||
|
|
||||||
|
const [searchType, setSearchType] = useState<SearchType>("Library");
|
||||||
const [search, setSearch] = useState<string>("");
|
const [search, setSearch] = useState<string>("");
|
||||||
|
|
||||||
const [debouncedSearch] = useDebounce(search, 500);
|
const [debouncedSearch] = useDebounce(search, 500);
|
||||||
@@ -138,10 +144,10 @@ export default function search() {
|
|||||||
query: debouncedSearch,
|
query: debouncedSearch,
|
||||||
types: ["Movie"],
|
types: ["Movie"],
|
||||||
}),
|
}),
|
||||||
enabled: debouncedSearch.length > 0,
|
enabled: searchType === "Library" && debouncedSearch.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: jellyseerrResults, isFetching: r1 } = useQuery({
|
const { data: jellyseerrResults, isFetching: j1 } = useQuery({
|
||||||
queryKey: ["search", "jellyseerrResults", debouncedSearch],
|
queryKey: ["search", "jellyseerrResults", debouncedSearch],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await jellyseerrApi?.search({
|
const response = await jellyseerrApi?.search({
|
||||||
@@ -152,7 +158,13 @@ export default function search() {
|
|||||||
|
|
||||||
return response?.results;
|
return response?.results;
|
||||||
},
|
},
|
||||||
enabled: !!jellyseerrApi && debouncedSearch.length > 0,
|
enabled: !!jellyseerrApi && searchType === "Discover" && debouncedSearch.length > 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: jellyseerrDiscoverSettings, isFetching: j2 } = useQuery({
|
||||||
|
queryKey: ["search", "jellyseerrDiscoverSettings", debouncedSearch],
|
||||||
|
queryFn: async () => jellyseerrApi?.discoverSettings(),
|
||||||
|
enabled: !!jellyseerrApi && searchType === "Discover" && debouncedSearch.length == 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const jellyseerrMovieResults: MovieResult[] | undefined = useMemo(() =>
|
const jellyseerrMovieResults: MovieResult[] | undefined = useMemo(() =>
|
||||||
@@ -172,7 +184,7 @@ export default function search() {
|
|||||||
query: debouncedSearch,
|
query: debouncedSearch,
|
||||||
types: ["Series"],
|
types: ["Series"],
|
||||||
}),
|
}),
|
||||||
enabled: debouncedSearch.length > 0,
|
enabled: searchType === "Library" && debouncedSearch.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: episodes, isFetching: l3 } = useQuery({
|
const { data: episodes, isFetching: l3 } = useQuery({
|
||||||
@@ -182,7 +194,7 @@ export default function search() {
|
|||||||
query: debouncedSearch,
|
query: debouncedSearch,
|
||||||
types: ["Episode"],
|
types: ["Episode"],
|
||||||
}),
|
}),
|
||||||
enabled: debouncedSearch.length > 0,
|
enabled: searchType === "Library" && debouncedSearch.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: collections, isFetching: l7 } = useQuery({
|
const { data: collections, isFetching: l7 } = useQuery({
|
||||||
@@ -192,7 +204,7 @@ export default function search() {
|
|||||||
query: debouncedSearch,
|
query: debouncedSearch,
|
||||||
types: ["BoxSet"],
|
types: ["BoxSet"],
|
||||||
}),
|
}),
|
||||||
enabled: debouncedSearch.length > 0,
|
enabled: searchType === "Library" && debouncedSearch.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: actors, isFetching: l8 } = useQuery({
|
const { data: actors, isFetching: l8 } = useQuery({
|
||||||
@@ -202,7 +214,7 @@ export default function search() {
|
|||||||
query: debouncedSearch,
|
query: debouncedSearch,
|
||||||
types: ["Person"],
|
types: ["Person"],
|
||||||
}),
|
}),
|
||||||
enabled: debouncedSearch.length > 0,
|
enabled: searchType === "Library" && debouncedSearch.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: artists, isFetching: l4 } = useQuery({
|
const { data: artists, isFetching: l4 } = useQuery({
|
||||||
@@ -212,7 +224,7 @@ export default function search() {
|
|||||||
query: debouncedSearch,
|
query: debouncedSearch,
|
||||||
types: ["MusicArtist"],
|
types: ["MusicArtist"],
|
||||||
}),
|
}),
|
||||||
enabled: debouncedSearch.length > 0,
|
enabled: searchType === "Library" && debouncedSearch.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: albums, isFetching: l5 } = useQuery({
|
const { data: albums, isFetching: l5 } = useQuery({
|
||||||
@@ -222,7 +234,7 @@ export default function search() {
|
|||||||
query: debouncedSearch,
|
query: debouncedSearch,
|
||||||
types: ["MusicAlbum"],
|
types: ["MusicAlbum"],
|
||||||
}),
|
}),
|
||||||
enabled: debouncedSearch.length > 0,
|
enabled: searchType === "Library" && debouncedSearch.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: songs, isFetching: l6 } = useQuery({
|
const { data: songs, isFetching: l6 } = useQuery({
|
||||||
@@ -232,7 +244,7 @@ export default function search() {
|
|||||||
query: debouncedSearch,
|
query: debouncedSearch,
|
||||||
types: ["Audio"],
|
types: ["Audio"],
|
||||||
}),
|
}),
|
||||||
enabled: debouncedSearch.length > 0,
|
enabled: searchType === "Library" && debouncedSearch.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const noResults = useMemo(() => {
|
const noResults = useMemo(() => {
|
||||||
@@ -251,8 +263,8 @@ export default function search() {
|
|||||||
}, [artists, episodes, albums, songs, movies, series, collections, actors, jellyseerrResults]);
|
}, [artists, episodes, albums, songs, movies, series, collections, actors, jellyseerrResults]);
|
||||||
|
|
||||||
const loading = useMemo(() => {
|
const loading = useMemo(() => {
|
||||||
return l1 || l2 || l3 || l4 || l5 || l6 || l7 || l8;
|
return l1 || l2 || l3 || l4 || l5 || l6 || l7 || l8 || j1 || j2;
|
||||||
}, [l1, l2, l3, l4, l5, l6, l7, l8]);
|
}, [l1, l2, l3, l4, l5, l6, l7, l8, j1, j2]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -277,6 +289,18 @@ export default function search() {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
{jellyseerrApi && (
|
||||||
|
<View className="flex flex-row flex-wrap space-x-2 px-4">
|
||||||
|
<TouchableOpacity onPress={() => setSearchType('Library')}>
|
||||||
|
<Tag text="Library" textClass="p-1"
|
||||||
|
className={searchType === "Library" ? "bg-neutral-600" : undefined}/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity onPress={() => setSearchType('Discover')}>
|
||||||
|
<Tag text="Discover" textClass="p-1"
|
||||||
|
className={searchType === "Discover" ? "bg-neutral-600" : undefined}/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
{!!q && (
|
{!!q && (
|
||||||
<View className="px-4 flex flex-col space-y-2">
|
<View className="px-4 flex flex-col space-y-2">
|
||||||
<Text className="text-neutral-500 ">
|
<Text className="text-neutral-500 ">
|
||||||
@@ -284,144 +308,153 @@ export default function search() {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<SearchItemWrapper
|
{searchType === "Library" && (
|
||||||
header="Movies"
|
<>
|
||||||
ids={movies?.map((m) => m.Id!)}
|
<SearchItemWrapper
|
||||||
renderItem={(item: BaseItemDto) => (
|
header="Movies"
|
||||||
<TouchableItemRouter
|
ids={movies?.map((m) => m.Id!)}
|
||||||
key={item.Id}
|
renderItem={(item: BaseItemDto) => (
|
||||||
className="flex flex-col w-28 mr-2"
|
<TouchableItemRouter
|
||||||
item={item}
|
key={item.Id}
|
||||||
>
|
className="flex flex-col w-28 mr-2"
|
||||||
<MoviePoster item={item} key={item.Id} />
|
item={item}
|
||||||
<Text numberOfLines={2} className="mt-2">
|
>
|
||||||
{item.Name}
|
<MoviePoster item={item} key={item.Id}/>
|
||||||
</Text>
|
<Text numberOfLines={2} className="mt-2">
|
||||||
<Text className="opacity-50 text-xs">
|
{item.Name}
|
||||||
{item.ProductionYear}
|
</Text>
|
||||||
</Text>
|
<Text className="opacity-50 text-xs">
|
||||||
</TouchableItemRouter>
|
{item.ProductionYear}
|
||||||
)}
|
</Text>
|
||||||
/>
|
</TouchableItemRouter>
|
||||||
<SearchItemWrapper
|
)}
|
||||||
header="Request Movies"
|
/>
|
||||||
items={jellyseerrMovieResults}
|
<SearchItemWrapper
|
||||||
renderItem={(item: MovieResult) => (
|
ids={series?.map((m) => m.Id!)}
|
||||||
<JellyseerrPoster item={item} key={item.id} />
|
header="Series"
|
||||||
)}
|
renderItem={(item: BaseItemDto) => (
|
||||||
/>
|
<TouchableItemRouter
|
||||||
<SearchItemWrapper
|
key={item.Id}
|
||||||
ids={series?.map((m) => m.Id!)}
|
item={item}
|
||||||
header="Series"
|
className="flex flex-col w-28 mr-2"
|
||||||
renderItem={(item: BaseItemDto) => (
|
>
|
||||||
<TouchableItemRouter
|
<SeriesPoster item={item} key={item.Id}/>
|
||||||
key={item.Id}
|
<Text numberOfLines={2} className="mt-2">
|
||||||
item={item}
|
{item.Name}
|
||||||
className="flex flex-col w-28 mr-2"
|
</Text>
|
||||||
>
|
<Text className="opacity-50 text-xs">
|
||||||
<SeriesPoster item={item} key={item.Id} />
|
{item.ProductionYear}
|
||||||
<Text numberOfLines={2} className="mt-2">
|
</Text>
|
||||||
{item.Name}
|
</TouchableItemRouter>
|
||||||
</Text>
|
)}
|
||||||
<Text className="opacity-50 text-xs">
|
/>
|
||||||
{item.ProductionYear}
|
<SearchItemWrapper
|
||||||
</Text>
|
ids={episodes?.map((m) => m.Id!)}
|
||||||
</TouchableItemRouter>
|
header="Episodes"
|
||||||
)}
|
renderItem={(item: BaseItemDto) => (
|
||||||
/>
|
<TouchableItemRouter
|
||||||
<SearchItemWrapper
|
item={item}
|
||||||
header="Request Series"
|
key={item.Id}
|
||||||
items={jellyseerrTvResults}
|
className="flex flex-col w-44 mr-2"
|
||||||
renderItem={(item: TvResult) => (
|
>
|
||||||
<JellyseerrPoster item={item} key={item.id} />
|
<ContinueWatchingPoster item={item}/>
|
||||||
)}
|
<ItemCardText item={item}/>
|
||||||
/>
|
</TouchableItemRouter>
|
||||||
<SearchItemWrapper
|
)}
|
||||||
ids={episodes?.map((m) => m.Id!)}
|
/>
|
||||||
header="Episodes"
|
<SearchItemWrapper
|
||||||
renderItem={(item: BaseItemDto) => (
|
ids={collections?.map((m) => m.Id!)}
|
||||||
<TouchableItemRouter
|
header="Collections"
|
||||||
item={item}
|
renderItem={(item: BaseItemDto) => (
|
||||||
key={item.Id}
|
<TouchableItemRouter
|
||||||
className="flex flex-col w-44 mr-2"
|
key={item.Id}
|
||||||
>
|
item={item}
|
||||||
<ContinueWatchingPoster item={item} />
|
className="flex flex-col w-28 mr-2"
|
||||||
<ItemCardText item={item} />
|
>
|
||||||
</TouchableItemRouter>
|
<MoviePoster item={item} key={item.Id}/>
|
||||||
)}
|
<Text numberOfLines={2} className="mt-2">
|
||||||
/>
|
{item.Name}
|
||||||
<SearchItemWrapper
|
</Text>
|
||||||
ids={collections?.map((m) => m.Id!)}
|
</TouchableItemRouter>
|
||||||
header="Collections"
|
)}
|
||||||
renderItem={(item: BaseItemDto) => (
|
/>
|
||||||
<TouchableItemRouter
|
<SearchItemWrapper
|
||||||
key={item.Id}
|
ids={actors?.map((m) => m.Id!)}
|
||||||
item={item}
|
header="Actors"
|
||||||
className="flex flex-col w-28 mr-2"
|
renderItem={(item: BaseItemDto) => (
|
||||||
>
|
<TouchableItemRouter
|
||||||
<MoviePoster item={item} key={item.Id} />
|
item={item}
|
||||||
<Text numberOfLines={2} className="mt-2">
|
key={item.Id}
|
||||||
{item.Name}
|
className="flex flex-col w-28 mr-2"
|
||||||
</Text>
|
>
|
||||||
</TouchableItemRouter>
|
<MoviePoster item={item}/>
|
||||||
)}
|
<ItemCardText item={item}/>
|
||||||
/>
|
</TouchableItemRouter>
|
||||||
<SearchItemWrapper
|
)}
|
||||||
ids={actors?.map((m) => m.Id!)}
|
/>
|
||||||
header="Actors"
|
<SearchItemWrapper
|
||||||
renderItem={(item: BaseItemDto) => (
|
ids={artists?.map((m) => m.Id!)}
|
||||||
<TouchableItemRouter
|
header="Artists"
|
||||||
item={item}
|
renderItem={(item: BaseItemDto) => (
|
||||||
key={item.Id}
|
<TouchableItemRouter
|
||||||
className="flex flex-col w-28 mr-2"
|
item={item}
|
||||||
>
|
key={item.Id}
|
||||||
<MoviePoster item={item} />
|
className="flex flex-col w-28 mr-2"
|
||||||
<ItemCardText item={item} />
|
>
|
||||||
</TouchableItemRouter>
|
<AlbumCover id={item.Id}/>
|
||||||
)}
|
<ItemCardText item={item}/>
|
||||||
/>
|
</TouchableItemRouter>
|
||||||
<SearchItemWrapper
|
)}
|
||||||
ids={artists?.map((m) => m.Id!)}
|
/>
|
||||||
header="Artists"
|
<SearchItemWrapper
|
||||||
renderItem={(item: BaseItemDto) => (
|
ids={albums?.map((m) => m.Id!)}
|
||||||
<TouchableItemRouter
|
header="Albums"
|
||||||
item={item}
|
renderItem={(item: BaseItemDto) => (
|
||||||
key={item.Id}
|
<TouchableItemRouter
|
||||||
className="flex flex-col w-28 mr-2"
|
item={item}
|
||||||
>
|
key={item.Id}
|
||||||
<AlbumCover id={item.Id} />
|
className="flex flex-col w-28 mr-2"
|
||||||
<ItemCardText item={item} />
|
>
|
||||||
</TouchableItemRouter>
|
<AlbumCover id={item.Id}/>
|
||||||
)}
|
<ItemCardText item={item}/>
|
||||||
/>
|
</TouchableItemRouter>
|
||||||
<SearchItemWrapper
|
)}
|
||||||
ids={albums?.map((m) => m.Id!)}
|
/>
|
||||||
header="Albums"
|
<SearchItemWrapper
|
||||||
renderItem={(item: BaseItemDto) => (
|
ids={songs?.map((m) => m.Id!)}
|
||||||
<TouchableItemRouter
|
header="Songs"
|
||||||
item={item}
|
renderItem={(item: BaseItemDto) => (
|
||||||
key={item.Id}
|
<TouchableItemRouter
|
||||||
className="flex flex-col w-28 mr-2"
|
item={item}
|
||||||
>
|
key={item.Id}
|
||||||
<AlbumCover id={item.Id} />
|
className="flex flex-col w-28 mr-2"
|
||||||
<ItemCardText item={item} />
|
>
|
||||||
</TouchableItemRouter>
|
<AlbumCover id={item.AlbumId}/>
|
||||||
)}
|
<ItemCardText item={item}/>
|
||||||
/>
|
</TouchableItemRouter>
|
||||||
<SearchItemWrapper
|
)}
|
||||||
ids={songs?.map((m) => m.Id!)}
|
/>
|
||||||
header="Songs"
|
</>
|
||||||
renderItem={(item: BaseItemDto) => (
|
)}
|
||||||
<TouchableItemRouter
|
{searchType === "Discover" && (
|
||||||
item={item}
|
<>
|
||||||
key={item.Id}
|
<SearchItemWrapper
|
||||||
className="flex flex-col w-28 mr-2"
|
header="Request Movies"
|
||||||
>
|
items={jellyseerrMovieResults}
|
||||||
<AlbumCover id={item.AlbumId} />
|
renderItem={(item: MovieResult) => (
|
||||||
<ItemCardText item={item} />
|
<JellyseerrPoster item={item} key={item.id}/>
|
||||||
</TouchableItemRouter>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
<SearchItemWrapper
|
||||||
|
header="Request Series"
|
||||||
|
items={jellyseerrTvResults}
|
||||||
|
renderItem={(item: TvResult) => (
|
||||||
|
<JellyseerrPoster item={item} key={item.id}/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<View className="mt-4 flex justify-center items-center">
|
<View className="mt-4 flex justify-center items-center">
|
||||||
<Loader />
|
<Loader />
|
||||||
@@ -435,7 +468,7 @@ export default function search() {
|
|||||||
"{debouncedSearch}"
|
"{debouncedSearch}"
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : debouncedSearch.length === 0 ? (
|
) : debouncedSearch.length === 0 && searchType === 'Library' ? (
|
||||||
<View className="mt-4 flex flex-col items-center space-y-2">
|
<View className="mt-4 flex flex-col items-center space-y-2">
|
||||||
{exampleSearches.map((e) => (
|
{exampleSearches.map((e) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -447,6 +480,12 @@ export default function search() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
) : debouncedSearch.length === 0 && searchType === 'Discover' ? (
|
||||||
|
<View className="mt-4 flex flex-col space-y-2 px-2">
|
||||||
|
{sortBy?.(jellyseerrDiscoverSettings?.filter(s => s.enabled), 'order')
|
||||||
|
.map((slide) => <DiscoverSlide key={slide.id} slide={slide}/>)
|
||||||
|
}
|
||||||
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from "./number";
|
|
||||||
export * from "./mmkv";
|
export * from "./mmkv";
|
||||||
|
export * from "./number";
|
||||||
|
export * from "./string";
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
declare global {
|
declare global {
|
||||||
interface Number {
|
interface Number {
|
||||||
bytesToReadable(): string;
|
bytesToReadable(): string;
|
||||||
|
secondsToMilliseconds(): number
|
||||||
|
minutesToMilliseconds(): number
|
||||||
|
hoursToMilliseconds(): number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,4 +22,16 @@ Number.prototype.bytesToReadable = function () {
|
|||||||
return `${bytes.toFixed(2)} B`;
|
return `${bytes.toFixed(2)} B`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Number.prototype.secondsToMilliseconds = function () {
|
||||||
|
return this.valueOf() * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
Number.prototype.minutesToMilliseconds = function () {
|
||||||
|
return this.valueOf() * (60).secondsToMilliseconds()
|
||||||
|
}
|
||||||
|
|
||||||
|
Number.prototype.hoursToMilliseconds = function () {
|
||||||
|
return this.valueOf() * (60).minutesToMilliseconds()
|
||||||
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
16
augmentations/string.ts
Normal file
16
augmentations/string.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
declare global {
|
||||||
|
interface String {
|
||||||
|
toTitle(): string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String.prototype.toTitle = function () {
|
||||||
|
return this
|
||||||
|
.replaceAll("_", " ")
|
||||||
|
.replace(
|
||||||
|
/\w\S*/g,
|
||||||
|
text => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -8,14 +8,26 @@ interface TagProps {
|
|||||||
textClass?: ViewProps["className"]
|
textClass?: ViewProps["className"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Tag: React.FC<{ text: string, textClass?: ViewProps["className"]} & ViewProps> = ({
|
||||||
|
text,
|
||||||
|
textClass,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<View className="bg-neutral-800 rounded-full px-2 py-1" {...props}>
|
||||||
|
<Text className={textClass}>{text}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const Tags: React.FC<TagProps & ViewProps> = ({ tags, textClass = "text-xs", ...props }) => {
|
export const Tags: React.FC<TagProps & ViewProps> = ({ tags, textClass = "text-xs", ...props }) => {
|
||||||
if (!tags || tags.length === 0) return null;
|
if (!tags || tags.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className={`flex flex-row flex-wrap gap-1 ${props.className}`} {...props}>
|
<View className={`flex flex-row flex-wrap gap-1 ${props.className}`} {...props}>
|
||||||
{tags.map((genre, idx) => (
|
{tags.map((tag, idx) => (
|
||||||
<View key={idx} className="bg-neutral-800 rounded-full px-2 py-1">
|
<View>
|
||||||
<Text className={textClass}>{genre}</Text>
|
<Tag key={idx} textClass={textClass} text={tag}/>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -55,7 +55,9 @@ export const JellyserrRatings: React.FC<{result: MovieResult | TvResult}> = ({ r
|
|||||||
? jellyseerrApi?.movieRatings(result.id)
|
? jellyseerrApi?.movieRatings(result.id)
|
||||||
: jellyseerrApi?.tvRatings(result.id)
|
: jellyseerrApi?.tvRatings(result.id)
|
||||||
},
|
},
|
||||||
enabled: !!jellyseerrApi
|
staleTime: (5).minutesToMilliseconds(),
|
||||||
|
retry: false,
|
||||||
|
enabled: !!jellyseerrApi,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (isLoading || !!result.voteCount ||
|
return (isLoading || !!result.voteCount ||
|
||||||
|
|||||||
75
components/jellyseerr/DiscoverSlide.tsx
Normal file
75
components/jellyseerr/DiscoverSlide.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import React, {useMemo} from "react";
|
||||||
|
import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
|
||||||
|
import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover";
|
||||||
|
import {DiscoverEndpoint, Endpoints, useJellyseerr} from "@/hooks/useJellyseerr";
|
||||||
|
import {useInfiniteQuery} from "@tanstack/react-query";
|
||||||
|
import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search";
|
||||||
|
import JellyseerrPoster from "@/components/posters/JellyseerrPoster";
|
||||||
|
import {Text} from "@/components/common/Text";
|
||||||
|
import {FlashList} from "@shopify/flash-list";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
slide: DiscoverSlider
|
||||||
|
}
|
||||||
|
const DiscoverSlide: React.FC<Props> = ({slide}) => {
|
||||||
|
const {jellyseerrApi} = useJellyseerr();
|
||||||
|
|
||||||
|
const {data, isFetching, fetchNextPage, hasNextPage} = useInfiniteQuery({
|
||||||
|
queryKey: ["jellyseerr", "discover", slide.id],
|
||||||
|
queryFn: async ({ pageParam }) => {
|
||||||
|
let endpoint: DiscoverEndpoint | undefined = undefined;
|
||||||
|
let params: any = {
|
||||||
|
page: Number(pageParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (slide.type) {
|
||||||
|
case DiscoverSliderType.TRENDING:
|
||||||
|
endpoint = Endpoints.DISCOVER_TRENDING;
|
||||||
|
break;
|
||||||
|
case DiscoverSliderType.POPULAR_MOVIES:
|
||||||
|
case DiscoverSliderType.UPCOMING_MOVIES:
|
||||||
|
endpoint = Endpoints.DISCOVER_MOVIES
|
||||||
|
if (slide.type === DiscoverSliderType.UPCOMING_MOVIES)
|
||||||
|
params = { ...params, primaryReleaseDateGte: new Date().toISOString().split('T')[0]}
|
||||||
|
break;
|
||||||
|
case DiscoverSliderType.POPULAR_TV:
|
||||||
|
case DiscoverSliderType.UPCOMING_TV:
|
||||||
|
endpoint = Endpoints.DISCOVER_TV
|
||||||
|
if (slide.type === DiscoverSliderType.UPCOMING_TV)
|
||||||
|
params = {...params, firstAirDateGte: new Date().toISOString().split('T')[0]}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoint ? jellyseerrApi?.discover(endpoint, params) : null;
|
||||||
|
},
|
||||||
|
initialPageParam: 1,
|
||||||
|
getNextPageParam: (lastPage, pages) => ((lastPage?.page || pages?.findLast(p => p?.results.length)?.page) || 1) + 1,
|
||||||
|
enabled: !!jellyseerrApi,
|
||||||
|
staleTime: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const flatData = useMemo(() => data?.pages?.filter(p => p?.results.length).flatMap(p => p?.results), [data])
|
||||||
|
|
||||||
|
return (
|
||||||
|
(flatData && flatData?.length > 0) && <>
|
||||||
|
<Text className="font-bold text-lg mb-2">{DiscoverSliderType[slide.type].toString().toTitle()}</Text>
|
||||||
|
<FlashList
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
keyExtractor={item => item!!.id.toString()}
|
||||||
|
estimatedItemSize={250}
|
||||||
|
data={flatData}
|
||||||
|
onEndReachedThreshold={1}
|
||||||
|
onEndReached={() => {
|
||||||
|
if (hasNextPage)
|
||||||
|
fetchNextPage()
|
||||||
|
}}
|
||||||
|
renderItem={({item}) =>
|
||||||
|
(item ? <JellyseerrPoster item={item as MovieResult | TvResult} /> : <></>)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DiscoverSlide;
|
||||||
@@ -160,6 +160,9 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
setJellyseerrUser(user);
|
setJellyseerrUser(user);
|
||||||
updateSettings({jellyseerrServerUrl})
|
updateSettings({jellyseerrServerUrl})
|
||||||
})
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Failed to login to jellyseerr!")
|
||||||
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setJellyseerrPassword(undefined);
|
setJellyseerrPassword(undefined);
|
||||||
})
|
})
|
||||||
@@ -725,6 +728,8 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
|
|||||||
<Text className="text-xs opacity-50">
|
<Text className="text-xs opacity-50">
|
||||||
Set the URL for your jellyseerr instance.
|
Set the URL for your jellyseerr instance.
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text className="text-xs text-gray-600">Example: http(s)://your-host.url</Text>
|
||||||
|
<Text className="text-xs text-gray-600 mb-1">(add port if required)</Text>
|
||||||
<Text className="text-xs text-red-600">This integration is in its early stages. Expect things to change.</Text>
|
<Text className="text-xs text-red-600">This integration is in its early stages. Expect things to change.</Text>
|
||||||
</View>
|
</View>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {SeasonWithEpisodes, TvDetails} from "@/utils/jellyseerr/server/models/Tv
|
|||||||
import {IssueStatus, IssueType} from "@/utils/jellyseerr/server/constants/issue";
|
import {IssueStatus, IssueType} from "@/utils/jellyseerr/server/constants/issue";
|
||||||
import Issue from "@/utils/jellyseerr/server/entity/Issue";
|
import Issue from "@/utils/jellyseerr/server/entity/Issue";
|
||||||
import {RTRating} from "@/utils/jellyseerr/server/api/rating/rottentomatoes";
|
import {RTRating} from "@/utils/jellyseerr/server/api/rating/rottentomatoes";
|
||||||
|
import {writeErrorLog} from "@/utils/log";
|
||||||
|
import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
|
||||||
|
|
||||||
interface SearchParams {
|
interface SearchParams {
|
||||||
query: string,
|
query: string,
|
||||||
@@ -26,8 +28,8 @@ interface SearchParams {
|
|||||||
|
|
||||||
interface SearchResults {
|
interface SearchResults {
|
||||||
page: number,
|
page: number,
|
||||||
total_pages: number,
|
totalPages: number,
|
||||||
total_results: number;
|
totalResults: number;
|
||||||
results: Results[];
|
results: Results[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ export const clearJellyseerrStorageData = () => {
|
|||||||
storage.delete(JELLYSEERR_COOKIES);
|
storage.delete(JELLYSEERR_COOKIES);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Endpoints {
|
export enum Endpoints {
|
||||||
STATUS = "/status",
|
STATUS = "/status",
|
||||||
API_V1 = "/api/v1",
|
API_V1 = "/api/v1",
|
||||||
SEARCH = "/search",
|
SEARCH = "/search",
|
||||||
@@ -48,9 +50,16 @@ enum Endpoints {
|
|||||||
RATINGS = "/ratings",
|
RATINGS = "/ratings",
|
||||||
ISSUE = "/issue",
|
ISSUE = "/issue",
|
||||||
TV = "/tv",
|
TV = "/tv",
|
||||||
|
SETTINGS = "/settings",
|
||||||
|
DISCOVER = "/discover",
|
||||||
|
DISCOVER_TRENDING = DISCOVER + "/trending",
|
||||||
|
DISCOVER_MOVIES = DISCOVER + "/movies",
|
||||||
|
DISCOVER_TV = DISCOVER + TV,
|
||||||
AUTH_JELLYFIN = "/auth/jellyfin",
|
AUTH_JELLYFIN = "/auth/jellyfin",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DiscoverEndpoint = Endpoints.DISCOVER_TRENDING | Endpoints.DISCOVER_MOVIES | Endpoints.DISCOVER_TV;
|
||||||
|
|
||||||
export type TestResult = {
|
export type TestResult = {
|
||||||
isValid: true;
|
isValid: true;
|
||||||
requiresPass: boolean;
|
requiresPass: boolean;
|
||||||
@@ -102,13 +111,21 @@ export class JellyseerrApi {
|
|||||||
requiresPass: true
|
requiresPass: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
toast.error(`Jellyseerr test failed. Please try again.`);
|
||||||
|
writeErrorLog(
|
||||||
|
`Jellyseerr returned a ${status} for url:\n` +
|
||||||
|
response.config.url + '\n' +
|
||||||
|
JSON.stringify(response.data)
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
isValid: false,
|
||||||
requiresPass: false
|
requiresPass: false
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error("Failed to test jellyseerr server url", e)
|
const msg = "Failed to test jellyseerr server url";
|
||||||
|
toast.error(msg)
|
||||||
|
console.error(msg, e)
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
isValid: false,
|
||||||
requiresPass: false
|
requiresPass: false
|
||||||
@@ -122,12 +139,24 @@ export class JellyseerrApi {
|
|||||||
password,
|
password,
|
||||||
email: username
|
email: username
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
const user = response.data;
|
const user = response?.data;
|
||||||
|
if (!user)
|
||||||
|
throw Error("Login failed")
|
||||||
storage.setAny(JELLYSEERR_USER, user);
|
storage.setAny(JELLYSEERR_USER, user);
|
||||||
return user
|
return user
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async discoverSettings(): Promise<DiscoverSlider[]> {
|
||||||
|
return this.axios?.get<DiscoverSlider[]>(Endpoints.API_V1 + Endpoints.SETTINGS + Endpoints.DISCOVER)
|
||||||
|
.then(({data}) => data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async discover(endpoint: DiscoverEndpoint, params: any): Promise<SearchResults> {
|
||||||
|
return this.axios?.get<SearchResults>(Endpoints.API_V1 + endpoint, { params })
|
||||||
|
.then(({data}) => data)
|
||||||
|
}
|
||||||
|
|
||||||
async search(params: SearchParams): Promise<SearchResults> {
|
async search(params: SearchParams): Promise<SearchResults> {
|
||||||
const response = await this.axios?.get<SearchResults>(Endpoints.API_V1 + Endpoints.SEARCH, {params})
|
const response = await this.axios?.get<SearchResults>(Endpoints.API_V1 + Endpoints.SEARCH, {params})
|
||||||
return response?.data
|
return response?.data
|
||||||
@@ -186,6 +215,31 @@ export class JellyseerrApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setInterceptors() {
|
private setInterceptors() {
|
||||||
|
this.axios.interceptors.response.use(
|
||||||
|
async (response) => {
|
||||||
|
const cookies = response.headers["set-cookie"];
|
||||||
|
if (cookies) {
|
||||||
|
storage.setAny(JELLYSEERR_COOKIES, response.headers["set-cookie"]?.flatMap(c => c.split("; ")));
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error: AxiosError) => {
|
||||||
|
const errorMsg = "Jellyseerr response error";
|
||||||
|
console.error(errorMsg, error, error.response?.data);
|
||||||
|
writeErrorLog(
|
||||||
|
errorMsg + `\n` +
|
||||||
|
`error: ${error.toString()}\n` +
|
||||||
|
`url: ${error?.config?.url}\n` +
|
||||||
|
`data:\n` +
|
||||||
|
JSON.stringify(error.response?.data)
|
||||||
|
);
|
||||||
|
if (error.status === 403) {
|
||||||
|
clearJellyseerrStorageData()
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.axios.interceptors.request.use(
|
this.axios.interceptors.request.use(
|
||||||
async (config) => {
|
async (config) => {
|
||||||
const cookies = storage.get<string[]>(JELLYSEERR_COOKIES);
|
const cookies = storage.get<string[]>(JELLYSEERR_COOKIES);
|
||||||
@@ -204,22 +258,6 @@ export class JellyseerrApi {
|
|||||||
console.error("Jellyseerr request error", error)
|
console.error("Jellyseerr request error", error)
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
this.axios.interceptors.response.use(
|
|
||||||
async (response) => {
|
|
||||||
const cookies = response.headers["set-cookie"];
|
|
||||||
if (cookies) {
|
|
||||||
storage.setAny(JELLYSEERR_COOKIES, response.headers["set-cookie"]?.flatMap(c => c.split("; ")));
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
},
|
|
||||||
(error: AxiosError) => {
|
|
||||||
console.error("Jellyseerr response error:", error,error.response?.data)
|
|
||||||
if (error.status === 403) {
|
|
||||||
clearJellyseerrStorageData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user