forked from Ninjalama/streamyfin_mirror
feat: Better Jellyseerr search results #586
- fetch 4 pages at once to maximize search results - add local sorting options
This commit is contained in:
@@ -1,12 +1,8 @@
|
||||
import {router, useLocalSearchParams, useSegments,} from "expo-router";
|
||||
import {useLocalSearchParams} from "expo-router";
|
||||
import React, {useMemo,} from "react";
|
||||
import {TouchableOpacity} from "react-native";
|
||||
import {useInfiniteQuery} from "@tanstack/react-query";
|
||||
import {Endpoints, useJellyseerr} from "@/hooks/useJellyseerr";
|
||||
import {Text} from "@/components/common/Text";
|
||||
import {Image} from "expo-image";
|
||||
import Poster from "@/components/posters/Poster";
|
||||
import JellyseerrMediaIcon from "@/components/jellyseerr/JellyseerrMediaIcon";
|
||||
import {DiscoverSliderType} from "@/utils/jellyseerr/server/constants/discover";
|
||||
import ParallaxSlideShow from "@/components/jellyseerr/ParallaxSlideShow";
|
||||
import {MovieResult, Results, TvResult} from "@/utils/jellyseerr/server/models/Search";
|
||||
|
||||
@@ -1,39 +1,30 @@
|
||||
import { Input } from "@/components/common/Input";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
|
||||
import {Text} from "@/components/common/Text";
|
||||
import {TouchableItemRouter} from "@/components/common/TouchableItemRouter";
|
||||
import ContinueWatchingPoster from "@/components/ContinueWatchingPoster";
|
||||
import { Tag } from "@/components/GenreTags";
|
||||
import { ItemCardText } from "@/components/ItemCardText";
|
||||
import { JellyserrIndexPage } from "@/components/jellyseerr/JellyseerrIndexPage";
|
||||
import {Tag} from "@/components/GenreTags";
|
||||
import {ItemCardText} from "@/components/ItemCardText";
|
||||
import {JellyseerrSearchSort, JellyserrIndexPage} from "@/components/jellyseerr/JellyseerrIndexPage";
|
||||
import MoviePoster from "@/components/posters/MoviePoster";
|
||||
import SeriesPoster from "@/components/posters/SeriesPoster";
|
||||
import { LoadingSkeleton } from "@/components/search/LoadingSkeleton";
|
||||
import { SearchItemWrapper } from "@/components/search/SearchItemWrapper";
|
||||
import { useJellyseerr } from "@/hooks/useJellyseerr";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import {
|
||||
BaseItemDto,
|
||||
BaseItemKind,
|
||||
} from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getItemsApi, getSearchApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {LoadingSkeleton} from "@/components/search/LoadingSkeleton";
|
||||
import {SearchItemWrapper} from "@/components/search/SearchItemWrapper";
|
||||
import {useJellyseerr} from "@/hooks/useJellyseerr";
|
||||
import {apiAtom, userAtom} from "@/providers/JellyfinProvider";
|
||||
import {useSettings} from "@/utils/atoms/settings";
|
||||
import {BaseItemDto, BaseItemKind,} from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import {getItemsApi, getSearchApi} from "@jellyfin/sdk/lib/utils/api";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import { Href, router, useLocalSearchParams, useNavigation } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { eventBus } from "@/utils/eventBus";
|
||||
import {router, useLocalSearchParams, useNavigation} from "expo-router";
|
||||
import {useAtom} from "jotai";
|
||||
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,} from "react";
|
||||
import {Platform, ScrollView, TouchableOpacity, View} from "react-native";
|
||||
import {useSafeAreaInsets} from "react-native-safe-area-context";
|
||||
import {useDebounce} from "use-debounce";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {eventBus} from "@/utils/eventBus";
|
||||
import {sortOrderOptions} from "@/utils/atoms/filters";
|
||||
import {FilterButton} from "@/components/filters/FilterButton";
|
||||
|
||||
type SearchType = "Library" | "Discover";
|
||||
|
||||
@@ -64,6 +55,8 @@ export default function search() {
|
||||
|
||||
const [settings] = useSettings();
|
||||
const { jellyseerrApi } = useJellyseerr();
|
||||
const [jellyseerrOrderBy, setJellyseerrOrderBy] = useState<JellyseerrSearchSort>(JellyseerrSearchSort.DEFAULT)
|
||||
const [jellyseerrSortOrder, setJellyseerrSortOrder] = useState<"asc" | "desc">("desc")
|
||||
|
||||
const searchEngine = useMemo(() => {
|
||||
return settings?.searchEngine || "Jellyfin";
|
||||
@@ -241,26 +234,52 @@ export default function search() {
|
||||
}}
|
||||
>
|
||||
{jellyseerrApi && (
|
||||
<View className="flex flex-row flex-wrap space-x-2 px-4 mb-2">
|
||||
<TouchableOpacity onPress={() => setSearchType("Library")}>
|
||||
<Tag
|
||||
text={t("search.library")}
|
||||
textClass="p-1"
|
||||
className={
|
||||
searchType === "Library" ? "bg-purple-600" : undefined
|
||||
}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setSearchType("Discover")}>
|
||||
<Tag
|
||||
text={t("search.discover")}
|
||||
textClass="p-1"
|
||||
className={
|
||||
searchType === "Discover" ? "bg-purple-600" : undefined
|
||||
}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<>
|
||||
<ScrollView horizontal className="flex flex-row flex-wrap space-x-2 px-4 mb-2">
|
||||
<TouchableOpacity onPress={() => setSearchType("Library")}>
|
||||
<Tag
|
||||
text={t("search.library")}
|
||||
textClass="p-1"
|
||||
className={
|
||||
searchType === "Library" ? "bg-purple-600" : undefined
|
||||
}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={() => setSearchType("Discover")}>
|
||||
<Tag
|
||||
text={t("search.discover")}
|
||||
textClass="p-1"
|
||||
className={
|
||||
searchType === "Discover" ? "bg-purple-600" : undefined
|
||||
}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
{!loading && noResults && debouncedSearch.length > 0 && (
|
||||
<View className="flex flex-row justify-end items-center space-x-1">
|
||||
<FilterButton
|
||||
collectionId="search"
|
||||
queryKey="jellyseerr_search"
|
||||
queryFn={async () => Object.keys(JellyseerrSearchSort).filter(v => isNaN(Number(v)))}
|
||||
set={value => setJellyseerrOrderBy(value[0])}
|
||||
values={[jellyseerrOrderBy]}
|
||||
title={t("library.filters.sort_by")}
|
||||
renderItemLabel={(item) => t(`home.settings.plugins.jellyseerr.order_by.${item}`)}
|
||||
showSearch={false}
|
||||
/>
|
||||
<FilterButton
|
||||
collectionId="order"
|
||||
queryKey="jellysearr_search"
|
||||
queryFn={async () => ["asc", "desc"]}
|
||||
set={value => setJellyseerrSortOrder(value[0])}
|
||||
values={[jellyseerrSortOrder]}
|
||||
title={t("library.filters.sort_order")}
|
||||
renderItemLabel={(item) => t(`library.filters.${item}`)}
|
||||
showSearch={false}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</>
|
||||
)}
|
||||
|
||||
<View className="mt-2">
|
||||
@@ -353,7 +372,11 @@ export default function search() {
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<JellyserrIndexPage searchQuery={debouncedSearch} />
|
||||
<JellyserrIndexPage
|
||||
searchQuery={debouncedSearch}
|
||||
sortType={jellyseerrOrderBy}
|
||||
order={jellyseerrSortOrder}
|
||||
/>
|
||||
)}
|
||||
|
||||
{searchType === "Library" && (
|
||||
|
||||
@@ -13,7 +13,7 @@ interface FilterButtonProps<T> extends ViewProps {
|
||||
title: string;
|
||||
set: (value: T[]) => void;
|
||||
queryFn: (params: any) => Promise<any>;
|
||||
searchFilter: (item: T, query: string) => boolean;
|
||||
searchFilter?: (item: T, query: string) => boolean;
|
||||
renderItemLabel: (item: T) => React.ReactNode;
|
||||
icon?: "filter" | "sort";
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ interface Props<T> extends ViewProps {
|
||||
values: T[];
|
||||
set: (value: T[]) => void;
|
||||
title: string;
|
||||
searchFilter: (item: T, query: string) => boolean;
|
||||
searchFilter?: (item: T, query: string) => boolean;
|
||||
renderItemLabel: (item: T) => React.ReactNode;
|
||||
showSearch?: boolean;
|
||||
}
|
||||
@@ -88,7 +88,7 @@ export const FilterSheet = <T,>({
|
||||
if (!search) return _data;
|
||||
const results = [];
|
||||
for (let i = 0; i < (_data?.length || 0); i++) {
|
||||
if (_data && searchFilter(_data[i], search)) {
|
||||
if (_data && searchFilter?.(_data[i], search)) {
|
||||
results.push(_data[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
TvResult,
|
||||
} from "@/utils/jellyseerr/server/models/Search";
|
||||
import { useReactNavigationQuery } from "@/utils/useReactNavigationQuery";
|
||||
import React, { useMemo } from "react";
|
||||
import React, {useMemo, useState} from "react";
|
||||
import { View, ViewProps } from "react-native";
|
||||
import {
|
||||
useAnimatedReaction,
|
||||
@@ -21,17 +21,32 @@ import { LoadingSkeleton } from "../search/LoadingSkeleton";
|
||||
import { SearchItemWrapper } from "../search/SearchItemWrapper";
|
||||
import PersonPoster from "./PersonPoster";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {uniqBy} from "lodash";
|
||||
import {orderBy, uniqBy} from "lodash";
|
||||
import {useInfiniteQuery} from "@tanstack/react-query";
|
||||
|
||||
interface Props extends ViewProps {
|
||||
searchQuery: string;
|
||||
sortType?: JellyseerrSearchSort;
|
||||
order?: "asc" | "desc";
|
||||
}
|
||||
|
||||
export const JellyserrIndexPage: React.FC<Props> = ({ searchQuery }) => {
|
||||
export enum JellyseerrSearchSort {
|
||||
DEFAULT,
|
||||
VOTE_COUNT_AND_AVERAGE,
|
||||
POPULARITY
|
||||
}
|
||||
|
||||
export const JellyserrIndexPage: React.FC<Props> = ({
|
||||
searchQuery,
|
||||
sortType,
|
||||
order
|
||||
}) => {
|
||||
const { jellyseerrApi } = useJellyseerr();
|
||||
const opacity = useSharedValue(1);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [loadInitialPages, setLoadInitialPages] = useState<Boolean>(false)
|
||||
|
||||
const {
|
||||
data: jellyseerrDiscoverSettings,
|
||||
isFetching: f1,
|
||||
@@ -43,30 +58,33 @@ export const JellyserrIndexPage: React.FC<Props> = ({ searchQuery }) => {
|
||||
});
|
||||
|
||||
const {
|
||||
data: jellyseerrResults,
|
||||
data: jellyseerrResultPages,
|
||||
isFetching: f2,
|
||||
isLoading: l2,
|
||||
} = useReactNavigationQuery({
|
||||
isFetchingNextPage: n2,
|
||||
hasNextPage,
|
||||
fetchNextPage
|
||||
} = useInfiniteQuery({
|
||||
queryKey: ["search", "jellyseerr", "results", searchQuery],
|
||||
queryFn: async () => {
|
||||
const response = await jellyseerrApi?.search({
|
||||
query: new URLSearchParams(searchQuery).toString(),
|
||||
page: 1,
|
||||
language: "en",
|
||||
});
|
||||
return response?.results;
|
||||
},
|
||||
queryFn: async ({pageParam}) =>
|
||||
jellyseerrApi?.search({
|
||||
query: new URLSearchParams(searchQuery || "").toString(),
|
||||
page: Number(pageParam),
|
||||
}),
|
||||
enabled: !!jellyseerrApi && searchQuery.length > 0,
|
||||
});
|
||||
staleTime: 0,
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage, pages) => {
|
||||
const firstPage = pages?.[0]
|
||||
const mostRecentPage = lastPage || pages?.[pages?.length - 1]
|
||||
const currentPage = mostRecentPage?.page || 1
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: opacity.value,
|
||||
};
|
||||
return Math.min(currentPage + 1, firstPage?.totalPages || 1)
|
||||
},
|
||||
});
|
||||
|
||||
useAnimatedReaction(
|
||||
() => f1 || f2 || l1 || l2,
|
||||
() => f1 || f2 || l1 || l2 || n2,
|
||||
(isLoading) => {
|
||||
if (isLoading) {
|
||||
opacity.value = withTiming(1, { duration: 200 });
|
||||
@@ -76,31 +94,63 @@ export const JellyserrIndexPage: React.FC<Props> = ({ searchQuery }) => {
|
||||
}
|
||||
);
|
||||
|
||||
const sortingType = useMemo(
|
||||
() => {
|
||||
if (!sortType) return;
|
||||
switch (Number(JellyseerrSearchSort[sortType])) {
|
||||
case JellyseerrSearchSort.VOTE_COUNT_AND_AVERAGE:
|
||||
return ["voteCount", "voteAverage"];
|
||||
case JellyseerrSearchSort.POPULARITY:
|
||||
return ["voteCount", "popularity"]
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
[sortType, order]
|
||||
)
|
||||
|
||||
const jellyseerrResults = useMemo(
|
||||
() => {
|
||||
const lastPage = jellyseerrResultPages?.pages?.[jellyseerrResultPages?.pages?.length - 1]
|
||||
|
||||
if ((lastPage?.page || 0) % 5 !== 0 && hasNextPage && !loadInitialPages) {
|
||||
fetchNextPage()
|
||||
setLoadInitialPages(lastPage?.page === 4 || (lastPage !== undefined && lastPage.totalPages == lastPage.page))
|
||||
}
|
||||
|
||||
return uniqBy(jellyseerrResultPages?.pages?.flatMap?.(page => page?.results || []), "id")
|
||||
},
|
||||
[jellyseerrResultPages, fetchNextPage, hasNextPage]
|
||||
);
|
||||
|
||||
const jellyseerrMovieResults = useMemo(
|
||||
() =>
|
||||
uniqBy(
|
||||
orderBy(
|
||||
jellyseerrResults?.filter((r) => r.mediaType === MediaType.MOVIE) as MovieResult[],
|
||||
"id"
|
||||
sortingType || [m => m.title.toLowerCase() == searchQuery.toLowerCase()],
|
||||
order || "desc"
|
||||
),
|
||||
[jellyseerrResults]
|
||||
[jellyseerrResults, sortingType, order]
|
||||
);
|
||||
|
||||
const jellyseerrTvResults = useMemo(
|
||||
() =>
|
||||
uniqBy(
|
||||
orderBy(
|
||||
jellyseerrResults?.filter((r) => r.mediaType === MediaType.TV) as TvResult[],
|
||||
"id"
|
||||
sortingType || [t => t.originalName.toLowerCase() == searchQuery.toLowerCase()],
|
||||
order || "desc"
|
||||
),
|
||||
[jellyseerrResults]
|
||||
[jellyseerrResults, sortingType, order]
|
||||
);
|
||||
|
||||
const jellyseerrPersonResults = useMemo(
|
||||
() =>
|
||||
uniqBy(
|
||||
orderBy(
|
||||
jellyseerrResults?.filter((r) => r.mediaType === "person") as PersonResult[],
|
||||
"id"
|
||||
sortingType || [p => p.name.toLowerCase() == searchQuery.toLowerCase()],
|
||||
order || "desc"
|
||||
),
|
||||
[jellyseerrResults]
|
||||
[jellyseerrResults, sortingType, order]
|
||||
);
|
||||
|
||||
if (!searchQuery.length)
|
||||
@@ -112,7 +162,7 @@ export const JellyserrIndexPage: React.FC<Props> = ({ searchQuery }) => {
|
||||
|
||||
return (
|
||||
<View>
|
||||
<LoadingSkeleton isLoading={f1 || f2 || l1 || l2} />
|
||||
<LoadingSkeleton isLoading={(f1 || f2 || l1 || l2) && !loadInitialPages} />
|
||||
|
||||
{!jellyseerrMovieResults?.length &&
|
||||
!jellyseerrTvResults?.length &&
|
||||
@@ -120,7 +170,8 @@ export const JellyserrIndexPage: React.FC<Props> = ({ searchQuery }) => {
|
||||
!f1 &&
|
||||
!f2 &&
|
||||
!l1 &&
|
||||
!l2 && (
|
||||
!l2 &&
|
||||
!loadInitialPages && (
|
||||
<View>
|
||||
<Text className="text-center text-lg font-bold mt-4">
|
||||
{t("search.no_results_found_for")}
|
||||
@@ -131,7 +182,7 @@ export const JellyserrIndexPage: React.FC<Props> = ({ searchQuery }) => {
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className={f1 || f2 || l1 || l2 ? "opacity-0" : "opacity-100"}>
|
||||
<View className={(f1 || f2 || l1 || l2) && !loadInitialPages ? "opacity-0" : "opacity-100"}>
|
||||
<SearchItemWrapper
|
||||
header={t("search.request_movies")}
|
||||
items={jellyseerrMovieResults}
|
||||
|
||||
@@ -3,15 +3,16 @@ import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAtom } from "jotai";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { ScrollView } from "react-native";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { Text } from "../common/Text";
|
||||
import {FlashList} from "@shopify/flash-list";
|
||||
|
||||
type SearchItemWrapperProps<T> = {
|
||||
ids?: string[] | null;
|
||||
items?: T[];
|
||||
renderItem: (item: any) => React.ReactNode;
|
||||
header?: string;
|
||||
onEndReached?: (() => void) | null | undefined;
|
||||
};
|
||||
|
||||
export const SearchItemWrapper = <T extends unknown>({
|
||||
@@ -19,6 +20,7 @@ export const SearchItemWrapper = <T extends unknown>({
|
||||
items,
|
||||
renderItem,
|
||||
header,
|
||||
onEndReached
|
||||
}: PropsWithChildren<SearchItemWrapperProps<T>>) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
@@ -54,17 +56,22 @@ export const SearchItemWrapper = <T extends unknown>({
|
||||
return (
|
||||
<>
|
||||
<Text className="font-bold text-lg px-4 mb-2">{header}</Text>
|
||||
<ScrollView
|
||||
<FlashList
|
||||
horizontal
|
||||
className="px-4 mb-2"
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
>
|
||||
{data && data?.length > 0
|
||||
? data.map((item) => renderItem(item))
|
||||
: items && items?.length > 0
|
||||
? items.map((i) => renderItem(i))
|
||||
: undefined}
|
||||
</ScrollView>
|
||||
keyExtractor={(_, index) => index.toString()}
|
||||
estimatedItemSize={250}
|
||||
/*@ts-ignore */
|
||||
data={data || items}
|
||||
onEndReachedThreshold={1}
|
||||
onEndReached={onEndReached}
|
||||
//@ts-ignore
|
||||
renderItem={({item, index}) => item ? renderItem(item) : <></>}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
interface SearchParams {
|
||||
query: string;
|
||||
page: number;
|
||||
language: string;
|
||||
// language: string;
|
||||
}
|
||||
|
||||
interface SearchResults {
|
||||
@@ -214,11 +214,10 @@ export class JellyseerrApi {
|
||||
}
|
||||
|
||||
async search(params: SearchParams): Promise<SearchResults> {
|
||||
const response = await this.axios?.get<SearchResults>(
|
||||
return this.axios?.get<SearchResults>(
|
||||
Endpoints.API_V1 + Endpoints.SEARCH,
|
||||
{ params }
|
||||
);
|
||||
return response?.data;
|
||||
).then(({ data }) => data)
|
||||
}
|
||||
|
||||
async request(request: MediaRequestBody): Promise<MediaRequest> {
|
||||
@@ -467,7 +466,7 @@ export const useJellyseerr = () => {
|
||||
|
||||
const getTitle = (item: TvResult | TvDetails | MovieResult | MovieDetails) => {
|
||||
return isJellyseerrResult(item)
|
||||
? (item.mediaType == MediaType.MOVIE ? item?.originalTitle : item?.name)
|
||||
? (item.mediaType == MediaType.MOVIE ? item?.title : item?.name)
|
||||
: (item.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.title : (item as TvDetails)?.name)
|
||||
};
|
||||
|
||||
|
||||
@@ -174,7 +174,12 @@
|
||||
"tv_quota_days": "TV-Anfragetage",
|
||||
"reset_jellyseerr_config_button": "Setze Jellyseerr-Konfiguration zurück",
|
||||
"unlimited": "Unlimitiert",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"plus_n_more": "+{{n}} more",
|
||||
"order_by": {
|
||||
"DEFAULT": "Default",
|
||||
"VOTE_COUNT_AND_AVERAGE": "Vote count and average",
|
||||
"POPULARITY": "Popularity"
|
||||
}
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Aktiviere Marlin Search",
|
||||
@@ -329,6 +334,8 @@
|
||||
"years": "Jahre",
|
||||
"sort_by": "Sortieren nach",
|
||||
"sort_order": "Sortierreihenfolge",
|
||||
"asc": "Ascending",
|
||||
"desc": "Descending",
|
||||
"tags": "Tags"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -174,7 +174,12 @@
|
||||
"tv_quota_days": "TV quota days",
|
||||
"reset_jellyseerr_config_button": "Reset Jellyseerr config",
|
||||
"unlimited": "Unlimited",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"plus_n_more": "+{{n}} more",
|
||||
"order_by": {
|
||||
"DEFAULT": "Default",
|
||||
"VOTE_COUNT_AND_AVERAGE": "Vote count and average",
|
||||
"POPULARITY": "Popularity"
|
||||
}
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Enable Marlin Search ",
|
||||
@@ -333,6 +338,8 @@
|
||||
"years": "Years",
|
||||
"sort_by": "Sort By",
|
||||
"sort_order": "Sort Order",
|
||||
"asc": "Ascending",
|
||||
"desc": "Descending",
|
||||
"tags": "Tags"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -174,7 +174,12 @@
|
||||
"tv_quota_days": "Días de cuota de series",
|
||||
"reset_jellyseerr_config_button": "Restablecer configuración de Jellyseerr",
|
||||
"unlimited": "Ilimitado",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"plus_n_more": "+{{n}} more",
|
||||
"order_by": {
|
||||
"DEFAULT": "Default",
|
||||
"VOTE_COUNT_AND_AVERAGE": "Vote count and average",
|
||||
"POPULARITY": "Popularity"
|
||||
}
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Habilitar búsqueda de Marlin",
|
||||
@@ -329,6 +334,8 @@
|
||||
"years": "Años",
|
||||
"sort_by": "Ordenar por",
|
||||
"sort_order": "Ordenar",
|
||||
"asc": "Ascending",
|
||||
"desc": "Descending",
|
||||
"tags": "Etiquetas"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -175,7 +175,12 @@
|
||||
"tv_quota_days": "Jours de quota TV",
|
||||
"reset_jellyseerr_config_button": "Réinitialiser la configuration Jellyseerr",
|
||||
"unlimited": "Illimité",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"plus_n_more": "+{{n}} more",
|
||||
"order_by": {
|
||||
"DEFAULT": "Default",
|
||||
"VOTE_COUNT_AND_AVERAGE": "Vote count and average",
|
||||
"POPULARITY": "Popularity"
|
||||
}
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Activer Marlin Search ",
|
||||
@@ -330,6 +335,8 @@
|
||||
"years": "Années",
|
||||
"sort_by": "Trier par",
|
||||
"sort_order": "Ordre de tri",
|
||||
"asc": "Ascending",
|
||||
"desc": "Descending",
|
||||
"tags": "Tags"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -174,7 +174,12 @@
|
||||
"tv_quota_days": "Giorni di quota per le serie TV",
|
||||
"reset_jellyseerr_config_button": "Ripristina la configurazione di Jellyseerr",
|
||||
"unlimited": "Illimitato",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"plus_n_more": "+{{n}} more",
|
||||
"order_by": {
|
||||
"DEFAULT": "Default",
|
||||
"VOTE_COUNT_AND_AVERAGE": "Vote count and average",
|
||||
"POPULARITY": "Popularity"
|
||||
}
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Abilita la ricerca Marlin ",
|
||||
@@ -329,6 +334,8 @@
|
||||
"years": "Anni",
|
||||
"sort_by": "Ordina per",
|
||||
"sort_order": "Criterio di ordinamento",
|
||||
"asc": "Ascending",
|
||||
"desc": "Descending",
|
||||
"tags": "Tag"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -173,7 +173,12 @@
|
||||
"tv_quota_days": "テレビのクオータ日数",
|
||||
"reset_jellyseerr_config_button": "Jellyseerrの設定をリセット",
|
||||
"unlimited": "無制限",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"plus_n_more": "+{{n}} more",
|
||||
"order_by": {
|
||||
"DEFAULT": "Default",
|
||||
"VOTE_COUNT_AND_AVERAGE": "Vote count and average",
|
||||
"POPULARITY": "Popularity"
|
||||
}
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "マーリン検索を有効にする ",
|
||||
@@ -328,6 +333,8 @@
|
||||
"years": "年",
|
||||
"sort_by": "ソート",
|
||||
"sort_order": "ソート順",
|
||||
"asc": "Ascending",
|
||||
"desc": "Descending",
|
||||
"tags": "タグ"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -174,7 +174,12 @@
|
||||
"tv_quota_days": "Serie Quota dagen",
|
||||
"reset_jellyseerr_config_button": "Jellyseerr opnieuw instellen",
|
||||
"unlimited": "Ongelimiteerd",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"plus_n_more": "+{{n}} more",
|
||||
"order_by": {
|
||||
"DEFAULT": "Default",
|
||||
"VOTE_COUNT_AND_AVERAGE": "Vote count and average",
|
||||
"POPULARITY": "Popularity"
|
||||
}
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Marlin Search inschakelen ",
|
||||
@@ -329,6 +334,8 @@
|
||||
"years": "Jaren",
|
||||
"sort_by": "Sorteren op",
|
||||
"sort_order": "Sorteer volgorde",
|
||||
"asc": "Ascending",
|
||||
"desc": "Descending",
|
||||
"tags": "Labels"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -173,7 +173,12 @@
|
||||
"tv_quota_days": "TV kota günleri",
|
||||
"reset_jellyseerr_config_button": "Jellyseerr yapılandırmasını sıfırla",
|
||||
"unlimited": "Sınırsız",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"plus_n_more": "+{{n}} more",
|
||||
"order_by": {
|
||||
"DEFAULT": "Default",
|
||||
"VOTE_COUNT_AND_AVERAGE": "Vote count and average",
|
||||
"POPULARITY": "Popularity"
|
||||
}
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "Marlin Aramasını Etkinleştir ",
|
||||
@@ -328,6 +333,8 @@
|
||||
"years": "Yıllar",
|
||||
"sort_by": "Sırala",
|
||||
"sort_order": "Sıralama düzeni",
|
||||
"asc": "Ascending",
|
||||
"desc": "Descending",
|
||||
"tags": "Etiketler"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -173,7 +173,12 @@
|
||||
"tv_quota_days": "剧集配额天数",
|
||||
"reset_jellyseerr_config_button": "重置 Jellyseerr 设置",
|
||||
"unlimited": "无限制",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"plus_n_more": "+{{n}} more",
|
||||
"order_by": {
|
||||
"DEFAULT": "Default",
|
||||
"VOTE_COUNT_AND_AVERAGE": "Vote count and average",
|
||||
"POPULARITY": "Popularity"
|
||||
}
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "启用 Marlin 搜索",
|
||||
@@ -328,6 +333,8 @@
|
||||
"years": "年份",
|
||||
"sort_by": "排序依据",
|
||||
"sort_order": "排序顺序",
|
||||
"asc": "Ascending",
|
||||
"desc": "Descending",
|
||||
"tags": "标签"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -173,7 +173,12 @@
|
||||
"tv_quota_days": "電視配額天數",
|
||||
"reset_jellyseerr_config_button": "重置 Jellyseerr 配置",
|
||||
"unlimited": "無限制",
|
||||
"plus_n_more": "+{{n}} more"
|
||||
"plus_n_more": "+{{n}} more",
|
||||
"order_by": {
|
||||
"DEFAULT": "Default",
|
||||
"VOTE_COUNT_AND_AVERAGE": "Vote count and average",
|
||||
"POPULARITY": "Popularity"
|
||||
}
|
||||
},
|
||||
"marlin_search": {
|
||||
"enable_marlin_search": "啟用 Marlin 搜索",
|
||||
@@ -328,6 +333,8 @@
|
||||
"years": "年份",
|
||||
"sort_by": "排序依據",
|
||||
"sort_order": "排序順序",
|
||||
"asc": "Ascending",
|
||||
"desc": "Descending",
|
||||
"tags": "標籤"
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user