Compare commits

...

27 Commits

Author SHA1 Message Date
sarendsen
bd10daf9ac fix: unwanted
detail calls on search
2025-05-22 17:00:17 +02:00
retardgerman
3adc4d2a21 fix(lang): uk.json 2025-05-19 13:44:19 +02:00
Chris
185524c06c feat(lang): add Klingon and Esperanto localization support (#672) 2025-05-19 12:39:02 +02:00
Simon Eklundh
6a208ee201 fix: improve readme to reduce the questions we tend to receive rather… (#699) 2025-05-18 20:36:08 +02:00
Ahmed Sbai
99938ddf5a feat: add "Are you still watching" modal overlay with configurable options (#663)
Co-authored-by: Fredrik Burmester <fredrik.burmester@gmail.com>
2025-05-18 09:21:50 +02:00
lostb1t
963a54a36c fix: cancel for direct downloads 2025-05-14 21:18:48 +02:00
sarendsen
e939c9b933 Revert "style: horizontal width"
This reverts commit 31f662a582.
2025-05-14 21:07:18 +02:00
lostb1t
2ffd569bba chore: fix build 2025-05-14 19:42:56 +02:00
storm1er
c8ea494d6f fix: downgrade expo-sharing version until expo 53 (#688) 2025-05-14 19:18:46 +02:00
Danylo Kozhushko
577a61a452 fix: Fixed Ukrainian translation filename and also some typos (#682) 2025-05-14 19:09:54 +02:00
retardgerman
a731c4eebd fix: remove Feature that doesn’t exist. 2025-05-12 20:55:37 +02:00
Fredrik Burmester
8a664757b8 fix: android popup crash patch 2025-05-05 11:19:06 +02:00
sarendsen
655a78900d fix: try to enable ios background downloads plugin 2025-05-04 18:38:27 +02:00
sarendsen
87a33af8d1 fix: restore downloads if missing 2025-05-04 18:12:16 +02:00
sarendsen
36b1c48fdd fix: use ts for downloads 2025-05-04 12:50:21 +02:00
lostb1t
0454ba9f29 Update DownloadProvider.tsx 2025-05-04 12:01:51 +02:00
lostb1t
b55ed6349c Update DownloadProvider.tsx 2025-05-04 11:56:47 +02:00
Ryan
0c34add45a fix: update search functionality to set text in search bar on press (#669) 2025-05-04 11:48:53 +02:00
lostb1t
1c1345a3b7 feat: move to custom download handler with background download support (#675) 2025-05-04 11:46:34 +02:00
Chris
9f706a348e chore: Update README.md - Sessions View (#673) 2025-05-03 17:29:51 +02:00
sarendsen
f4750e781d refactor: getstreamurl 2025-05-02 19:02:35 +02:00
lance chant
0b574cc047 fix: dolby vision on supported devices, specifically profile 5 (#660) 2025-05-01 12:11:29 +02:00
Alec Warren
4a816470d1 feat: improve jellyseer item page buttons (#634) 2025-04-29 18:40:43 +02:00
Ryan
0d43b57f55 fix: improve empty state layout in library view (#665) 2025-04-28 18:11:24 +02:00
sarendsen
31f662a582 style: horizontal width 2025-04-21 12:28:12 +02:00
Alex
23e0ec9774 Remove Alamofire (#656) 2025-04-20 00:25:51 +10:00
Alex
d6ac8569a8 Fix/external subtitle support vlc3 (#655) 2025-04-20 00:25:35 +10:00
52 changed files with 2758 additions and 2127 deletions

View File

@@ -2,7 +2,7 @@
<a href="https://www.buymeacoffee.com/fredrikbur3" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
Welcome to Streamyfin, a simple and user-friendly Jellyfin client built with Expo. If you're looking for an alternative to other Jellyfin clients, we hope you'll find Streamyfin to be a useful addition to your media streaming toolbox.
Welcome to Streamyfin, a simple and user-friendly Jellyfin video streaming client built with Expo. If you're looking for an alternative to other Jellyfin clients, we hope you'll find Streamyfin to be a useful addition to your media streaming toolbox.
<div style="display: flex; flex-direction: row; gap: 8px">
<img width=150 src="./assets/images/screenshots/screenshot1.png" />
@@ -15,11 +15,11 @@ Welcome to Streamyfin, a simple and user-friendly Jellyfin client built with Exp
- 🚀 **Skip Intro / Credits Support**
- 🖼️ **Trickplay images**: The new golden standard for chapter previews when seeking.
- 🔊 **Background audio**: Stream music in the background, even when locking the phone.
- 📥 **Download media** (Experimental): Save your media locally and watch it offline.
- 📡 **Chromecast** (Experimental): Cast your media to any Chromecast-enabled device.
- 📡 **Settings management** (Experimental): Manage app settings for all your users with a JF plugin.
- 🤖 **Jellyseerr integration**: Request media directly in the app.
- 👁️ **Sessions View:** View all active sessions currently streaming on your server.
## 🧪 Experimental Features
@@ -31,7 +31,7 @@ Downloading works by using ffmpeg to convert an HLS stream into a video file on
### Chromecast
Chromecast support is still in development, and we're working on improving it. Currently, it supports casting videos and audio, but we're working on adding support for subtitles and other features.
Chromecast support is still in development, and we're working on improving it. Currently, it supports casting videos, but we're working on adding support for subtitles and other features.
### Streamyfin Plugin
@@ -118,6 +118,13 @@ If you have questions or need support, feel free to reach out:
- GitHub Issues: Report bugs or request features here.
- Email: [fredrik.burmester@gmail.com](mailto:fredrik.burmester@gmail.com)
## FAQ
1. Q: Why can't I see my libraries in Streamyfin?
A: Make sure your server is running one of the latest versions and that you have at least one library that isn't audio only.
2. Q: Why can't I see my music library?
A: We don't currently support music and are unlikely to support music in the near future.
## 📝 Credits
Streamyfin is developed by [Fredrik Burmester](https://github.com/fredrikburmester) and is not affiliated with Jellyfin. The app is built with Expo, React Native, and other open-source libraries.

View File

@@ -48,7 +48,6 @@
"@react-native-tvos/config-tv",
"expo-router",
"expo-font",
"@config-plugins/ffmpeg-kit-react-native",
[
"react-native-video",
{
@@ -113,6 +112,7 @@
["./plugins/withAndroidManifest.js"],
["./plugins/withTrustLocalCerts.js"],
["./plugins/withGradleProperties.js"],
["./plugins/withRNBackgroundDownloader.js"],
[
"expo-splash-screen",
{

View File

@@ -30,7 +30,7 @@ import {
} from "@gorhom/bottom-sheet";
import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { useLocalSearchParams, useNavigation } from "expo-router";
import { useLocalSearchParams, useNavigation, useRouter } from "expo-router";
import type React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -46,6 +46,7 @@ const Page: React.FC = () => {
const insets = useSafeAreaInsets();
const params = useLocalSearchParams();
const { t } = useTranslation();
const router = useRouter();
const { mediaTitle, releaseYear, posterSrc, mediaType, ...result } =
params as unknown as {
@@ -236,30 +237,65 @@ const Page: React.FC = () => {
}}
/>
</View>
<View className='mb-4'>
<View>
<GenreTags genres={details?.genres?.map((g) => g.name) || []} />
</View>
{isLoading || isFetching ? (
<Button loading={true} disabled={true} color='purple' />
<Button
loading={true}
disabled={true}
color='purple'
className='mt-4'
/>
) : canRequest ? (
<Button color='purple' onPress={request}>
<Button color='purple' onPress={request} className='mt-4'>
{t("jellyseerr.request_button")}
</Button>
) : (
<Button
className='bg-yellow-500/50 border-yellow-400 ring-yellow-400 text-yellow-100'
color='transparent'
onPress={() => bottomSheetModalRef?.current?.present()}
iconLeft={
<Ionicons name='warning-outline' size={24} color='white' />
}
style={{
borderWidth: 1,
borderStyle: "solid",
}}
>
{t("jellyseerr.report_issue_button")}
</Button>
details?.mediaInfo?.jellyfinMediaId && (
<View className='flex flex-row space-x-2 mt-4'>
<Button
className='flex-1 bg-yellow-500/50 border-yellow-400 ring-yellow-400 text-yellow-100'
color='transparent'
onPress={() => bottomSheetModalRef?.current?.present()}
iconLeft={
<Ionicons
name='warning-outline'
size={20}
color='white'
/>
}
style={{
borderWidth: 1,
borderStyle: "solid",
}}
>
<Text className='text-sm'>
{t("jellyseerr.report_issue_button")}
</Text>
</Button>
<Button
className='flex-1 bg-purple-600/50 border-purple-400 ring-purple-400 text-purple-100'
onPress={() => {
const url =
mediaType === MediaType.MOVIE
? `/(auth)/(tabs)/(search)/items/page?id=${details?.mediaInfo.jellyfinMediaId}`
: `/(auth)/(tabs)/(search)/series/${details?.mediaInfo.jellyfinMediaId}`;
// @ts-expect-error
router.push(url);
}}
iconLeft={
<Ionicons name='play-outline' size={20} color='white' />
}
style={{
borderWidth: 1,
borderStyle: "solid",
}}
>
<Text className='text-sm'>Play</Text>
</Button>
</View>
)
)}
<OverviewText text={result.overview} className='mt-4' />
</View>

View File

@@ -433,15 +433,6 @@ const Page = () => {
</View>
);
if (flatData.length === 0)
return (
<View className='h-full w-full flex justify-center items-center'>
<Text className='text-lg text-neutral-500'>
{t("library.no_items_found")}
</Text>
</View>
);
return (
<FlashList
key={orientation}

View File

@@ -331,7 +331,7 @@ export default function search() {
<View className={l1 || l2 ? "opacity-0" : "opacity-100"}>
<SearchItemWrapper
header={t("search.movies")}
ids={movies?.map((m) => m.Id!)}
items={movies}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
key={item.Id}
@@ -349,7 +349,7 @@ export default function search() {
)}
/>
<SearchItemWrapper
ids={series?.map((m) => m.Id!)}
items={series}
header={t("search.series")}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
@@ -368,7 +368,7 @@ export default function search() {
)}
/>
<SearchItemWrapper
ids={episodes?.map((m) => m.Id!)}
items={episodes}
header={t("search.episodes")}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
@@ -382,7 +382,7 @@ export default function search() {
)}
/>
<SearchItemWrapper
ids={collections?.map((m) => m.Id!)}
items={collections}
header={t("search.collections")}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
@@ -398,7 +398,7 @@ export default function search() {
)}
/>
<SearchItemWrapper
ids={actors?.map((m) => m.Id!)}
items={actors}
header={t("search.actors")}
renderItem={(item: BaseItemDto) => (
<TouchableItemRouter
@@ -434,7 +434,10 @@ export default function search() {
<View className='mt-4 flex flex-col items-center space-y-2'>
{exampleSearches.map((e) => (
<TouchableOpacity
onPress={() => setSearch(e)}
onPress={() => {
setSearch(e);
searchBarRef.current?.setText(e);
}}
key={e}
className='mb-2'
>

View File

@@ -17,7 +17,7 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { writeToLog } from "@/utils/log";
import native from "@/utils/profiles/native";
import generateDeviceProfile from "@/utils/profiles/native";
import { msToTicks, ticksToSeconds } from "@/utils/time";
import {
type BaseItemDto,
@@ -159,6 +159,7 @@ export default function page() {
useEffect(() => {
const fetchStreamData = async () => {
const native = await generateDeviceProfile();
try {
let result: Stream | null = null;
if (offline && !Platform.isTV) {

View File

@@ -5,7 +5,6 @@
"name": "streamyfin",
"dependencies": {
"@bottom-tabs/react-navigation": "0.8.6",
"@config-plugins/ffmpeg-kit-react-native": "^9.0.0",
"@expo/config-plugins": "~9.0.15",
"@expo/react-native-action-sheet": "^4.1.0",
"@expo/vector-icons": "^14.0.4",
@@ -51,7 +50,6 @@
"expo-task-manager": "~12.0.5",
"expo-updates": "~0.26.17",
"expo-web-browser": "~14.0.2",
"ffmpeg-kit-react-native": "^6.0.2",
"i18next": "^24.2.2",
"jotai": "^2.11.3",
"lodash": "^4.17.21",
@@ -400,8 +398,6 @@
"@bottom-tabs/react-navigation": ["@bottom-tabs/react-navigation@0.8.6", "", { "dependencies": { "color": "^4.2.3" }, "peerDependencies": { "@react-navigation/native": ">=7", "react": "*", "react-native": "*", "react-native-bottom-tabs": "*" } }, "sha512-hLlyBAUz4ahaVK2Op2VcJeAkCSpm3KKho4IojkPyXsos4WEHtO44EYWC71TDbVGeOP5HQ9k7FSwAW3IiZs0wHw=="],
"@config-plugins/ffmpeg-kit-react-native": ["@config-plugins/ffmpeg-kit-react-native@9.0.0", "", { "dependencies": { "semver": "^7.3.5" }, "peerDependencies": { "expo": "^52" } }, "sha512-04bXwdq7pmUPoGqYV0YGsrW/8Db+TNicn2Hznb5t+Dl740z9QkNGP4A38y1Mdz7mCU2EW0riASwl/JTH+6rBvw=="],
"@dominicstop/ts-event-emitter": ["@dominicstop/ts-event-emitter@1.1.0", "", {}, "sha512-CcxmJIvUb1vsFheuGGVSQf4KdPZC44XolpUT34+vlal+LyQoBUOn31pjFET5M9ctOxEpt8xa0M3/2M7uUiAoJw=="],
"@egjs/hammerjs": ["@egjs/hammerjs@2.0.17", "", { "dependencies": { "@types/hammerjs": "^2.0.36" } }, "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A=="],
@@ -1250,8 +1246,6 @@
"fetch-retry": ["fetch-retry@4.1.1", "", {}, "sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA=="],
"ffmpeg-kit-react-native": ["ffmpeg-kit-react-native@6.0.2", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-r9uSmahq8TeyIb7fXf3ft+uUXyoeWRFa99+khjo0TAzWO9y0z9wU7eGnab9JLw1MmCB9v64o4yojNluJhVm9nQ=="],
"file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
@@ -2318,8 +2312,6 @@
"@babel/runtime/regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
"@config-plugins/ffmpeg-kit-react-native/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
"@expo/bunyan/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
"@expo/cli/arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],

View File

@@ -1,4 +1,3 @@
//import { useRemuxHlsToMp4 } from "@/hooks/useRemuxHlsToMp4";
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { queueActions, queueAtom } from "@/utils/atoms/queue";
@@ -152,18 +151,7 @@ export const DownloadItems: React.FC<DownloadProps> = ({
}
closeModal();
if (usingOptimizedServer) initiateDownload(...itemsNotDownloaded);
else {
queueActions.enqueue(
queue,
setQueue,
...itemsNotDownloaded.map((item) => ({
id: item.Id!,
execute: async () => await initiateDownload(item),
item,
})),
);
}
initiateDownload(...itemsNotDownloaded);
} else {
toast.error(
t("home.downloads.toasts.you_are_not_allowed_to_download_files"),
@@ -203,7 +191,6 @@ export const DownloadItems: React.FC<DownloadProps> = ({
mediaSource = defaults.mediaSource;
audioIndex = defaults.audioIndex;
subtitleIndex = defaults.subtitleIndex;
// Keep using the selected bitrate for consistency across all downloads
}
const res = await getStreamUrl({
@@ -216,6 +203,8 @@ export const DownloadItems: React.FC<DownloadProps> = ({
mediaSourceId: mediaSource?.Id,
subtitleStreamIndex: subtitleIndex,
deviceProfile: download,
download: true,
// deviceId: mediaSource?.Id,
});
if (!res) {
@@ -230,12 +219,8 @@ export const DownloadItems: React.FC<DownloadProps> = ({
if (!url || !source) throw new Error("No url");
if (usingOptimizedServer) {
saveDownloadItemInfoToDiskTmp(item, source, url);
await startBackgroundDownload(url, item, source);
} else {
//await startRemuxing(item, url, source);
}
saveDownloadItemInfoToDiskTmp(item, source, url);
await startBackgroundDownload(url, item, source, maxBitrate);
}
},
[
@@ -249,7 +234,6 @@ export const DownloadItems: React.FC<DownloadProps> = ({
maxBitrate,
usingOptimizedServer,
startBackgroundDownload,
//startRemuxing,
],
);

View File

@@ -7,7 +7,6 @@ import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { chromecast } from "@/utils/profiles/chromecast";
import { chromecasth265 } from "@/utils/profiles/chromecasth265";
import ios from "@/utils/profiles/ios";
import { runtimeTicksToMinutes } from "@/utils/time";
import { useActionSheet } from "@expo/react-native-action-sheet";
import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
@@ -67,11 +66,14 @@ export const PlayButton: React.FC<Props> = ({
const startColor = useSharedValue(colorAtom);
const widthProgress = useSharedValue(0);
const colorChangeProgress = useSharedValue(0);
const [settings] = useSettings();
const [settings, updateSettings] = useSettings();
const lightHapticFeedback = useHaptic("light");
const goToPlayer = useCallback(
(q: string) => {
if (settings.maxAutoPlayEpisodeCount.value !== -1) {
updateSettings({ autoPlayEpisodeCount: 0 });
}
router.push(`/player/direct-player?${q}`);
},
[router],

View File

@@ -23,9 +23,6 @@ import { Button } from "../Button";
const BackGroundDownloader = !Platform.isTV
? require("@kesha-antonov/react-native-background-downloader")
: null;
//const FFmpegKitProvider = !Platform.isTV
// ? require("ffmpeg-kit-react-native")
// : null;
interface Props extends ViewProps {}
@@ -72,23 +69,18 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
mutationFn: async (id: string) => {
if (!process) throw new Error("No active download");
if (settings?.downloadMethod === DownloadMethod.Optimized) {
try {
const tasks = await BackGroundDownloader.checkForExistingDownloads();
for (const task of tasks) {
if (task.id === id) {
task.stop();
}
try {
const tasks = await BackGroundDownloader.checkForExistingDownloads();
for (const task of tasks) {
if (task.id === id) {
task.stop();
}
} finally {
await removeProcess(id);
}
} finally {
await removeProcess(id);
if (settings?.downloadMethod === DownloadMethod.Optimized) {
await queryClient.refetchQueries({ queryKey: ["jobs"] });
}
} else {
//FFmpegKitProvider.FFmpegKit.cancel(Number(id));
setProcesses((prev: any[]) =>
prev.filter((p: { id: string }) => p.id !== id),
);
}
},
onSuccess: () => {

View File

@@ -9,7 +9,6 @@ import type { PropsWithChildren } from "react";
import { Text } from "../common/Text";
type SearchItemWrapperProps<T> = {
ids?: string[] | null;
items?: T[];
renderItem: (item: any) => React.ReactNode;
header?: string;
@@ -17,7 +16,6 @@ type SearchItemWrapperProps<T> = {
};
export const SearchItemWrapper = <T,>({
ids,
items,
renderItem,
header,
@@ -26,33 +24,7 @@ export const SearchItemWrapper = <T,>({
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { data, isLoading: l1 } = useQuery({
queryKey: ["items", ids],
queryFn: async () => {
if (!user?.Id || !api || !ids || ids.length === 0) {
return [];
}
const itemPromises = ids.map((id) =>
getUserItemData({
api,
userId: user.Id,
itemId: id,
}),
);
const results = await Promise.all(itemPromises);
// Filter out null items
return results.filter(
(item) => item !== null,
) as unknown as BaseItemDto[];
},
enabled: !!ids && ids.length > 0 && !!api && !!user?.Id,
staleTime: Number.POSITIVE_INFINITY,
});
if (!data && (!items || items.length === 0)) return null;
if (!items || items.length === 0) return null;
return (
<>
@@ -67,7 +39,7 @@ export const SearchItemWrapper = <T,>({
keyExtractor={(_, index) => index.toString()}
estimatedItemSize={250}
/*@ts-ignore */
data={data || items}
data={items}
onEndReachedThreshold={1}
onEndReached={onEndReached}
//@ts-ignore

View File

@@ -10,6 +10,7 @@ import {
} from "@/utils/background-tasks";
import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
import i18n, { TFunction } from "i18next";
import type React from "react";
import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
@@ -251,7 +252,46 @@ export const OtherSettings: React.FC = () => {
}
/>
</ListItem>
<ListItem title={t("home.settings.other.max_auto_play_episode_count")}>
<Dropdown
data={AUTOPLAY_EPISODES_COUNT(t)}
keyExtractor={(item) => item.key}
titleExtractor={(item) => item.key}
title={
<TouchableOpacity className='flex flex-row items-center justify-between py-3 pl-3'>
<Text className='mr-1 text-[#8E8D91]'>
{t(settings?.maxAutoPlayEpisodeCount.key)}
</Text>
<Ionicons
name='chevron-expand-sharp'
size={18}
color='#5A5960'
/>
</TouchableOpacity>
}
label={t("home.settings.other.max_auto_play_episode_count")}
onSelected={(maxAutoPlayEpisodeCount) =>
updateSettings({ maxAutoPlayEpisodeCount })
}
/>
</ListItem>
</ListGroup>
</DisabledSetting>
);
};
const AUTOPLAY_EPISODES_COUNT = (
t: TFunction<"translation", undefined>,
): {
key: string;
value: number;
}[] => [
{ key: t("home.settings.other.disabled"), value: -1 },
{ key: "1", value: 1 },
{ key: "2", value: 2 },
{ key: "3", value: 3 },
{ key: "4", value: 4 },
{ key: "5", value: 5 },
{ key: "6", value: 6 },
{ key: "7", value: 7 },
];

View File

@@ -0,0 +1,49 @@
import { Button } from "@/components/Button";
import { Text } from "@/components/common/Text";
import { useSettings } from "@/utils/atoms/settings";
import { useRouter } from "expo-router";
import { t } from "i18next";
import React from "react";
import { View } from "react-native";
export interface ContinueWatchingOverlayProps {
goToNextItem: (options: {
isAutoPlay: boolean;
resetWatchCount: boolean;
}) => void;
}
const ContinueWatchingOverlay: React.FC<ContinueWatchingOverlayProps> = ({
goToNextItem,
}) => {
const [settings] = useSettings();
const router = useRouter();
return settings.autoPlayEpisodeCount >=
settings.maxAutoPlayEpisodeCount.value ? (
<View
className={
"absolute top-0 bottom-0 left-0 right-0 flex flex-col px-4 items-center justify-center bg-[#000000B3]"
}
>
<Text className='text-2xl font-bold text-white py-4 '>
Are you still watching ?
</Text>
<Button
onPress={() => {
goToNextItem({ isAutoPlay: false, resetWatchCount: true });
}}
color={"purple"}
className='my-4 w-2/3'
>
{t("player.continue_watching")}
</Button>
<Button onPress={router.back} color={"transparent"} className='w-2/3'>
{t("player.go_back")}
</Button>
</View>
) : null;
};
export default ContinueWatchingOverlay;

View File

@@ -1,5 +1,6 @@
import { Loader } from "@/components/Loader";
import { Text } from "@/components/common/Text";
import ContinueWatchingOverlay from "@/components/video-player/controls/ContinueWatchingOverlay";
import { useAdjacentItems } from "@/hooks/useAdjacentEpisodes";
import { useCreditSkipper } from "@/hooks/useCreditSkipper";
import { useHaptic } from "@/hooks/useHaptic";
@@ -28,7 +29,7 @@ import { Image } from "expo-image";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useAtom } from "jotai";
import { debounce } from "lodash";
import {
import React, {
type Dispatch,
type FC,
type MutableRefObject,
@@ -121,7 +122,7 @@ export const Controls: FC<Props> = ({
enableTrickplay = true,
isVlc = false,
}) => {
const [settings] = useSettings();
const [settings, updateSettings] = useSettings();
const router = useRouter();
const insets = useSafeAreaInsets();
const [api] = useAtom(apiAtom);
@@ -236,15 +237,76 @@ export const Controls: FC<Props> = ({
goToItemCommon(previousItem);
}, [previousItem, goToItemCommon]);
const goToNextItem = useCallback(() => {
if (!nextItem) return;
goToItemCommon(nextItem);
}, [nextItem, goToItemCommon]);
const goToNextItem = useCallback(
({
isAutoPlay,
resetWatchCount,
}: { isAutoPlay?: boolean; resetWatchCount?: boolean }) => {
if (!nextItem) {
return;
}
if (!isAutoPlay) {
// if we are not autoplaying, we won't update anything, we just go to the next item
goToItemCommon(nextItem);
if (resetWatchCount) {
updateSettings({
autoPlayEpisodeCount: 0,
});
}
return;
}
// Skip autoplay logic if maxAutoPlayEpisodeCount is -1
if (settings.maxAutoPlayEpisodeCount.value === -1) {
goToItemCommon(nextItem);
return;
}
if (
settings.autoPlayEpisodeCount + 1 <
settings.maxAutoPlayEpisodeCount.value
) {
goToItemCommon(nextItem);
}
// Check if the autoPlayEpisodeCount is less than maxAutoPlayEpisodeCount for the autoPlay
if (
settings.autoPlayEpisodeCount < settings.maxAutoPlayEpisodeCount.value
) {
// update the autoPlayEpisodeCount in settings
updateSettings({
autoPlayEpisodeCount: settings.autoPlayEpisodeCount + 1,
});
}
},
[nextItem, goToItemCommon],
);
// Add a memoized handler for autoplay next episode
const handleNextEpisodeAutoPlay = useCallback(() => {
goToNextItem({ isAutoPlay: true });
}, [goToNextItem]);
// Add a memoized handler for manual next episode
const handleNextEpisodeManual = useCallback(() => {
goToNextItem({ isAutoPlay: false });
}, [goToNextItem]);
// Add a memoized handler for ContinueWatchingOverlay
const handleContinueWatching = useCallback(
(options: { isAutoPlay?: boolean; resetWatchCount?: boolean }) => {
goToNextItem(options);
},
[goToNextItem],
);
const goToItem = useCallback(
async (itemId: string) => {
const gotoItem = await getItemById(api, itemId);
if (!gotoItem) return;
if (!gotoItem) {
return;
}
goToItemCommon(gotoItem);
},
[goToItemCommon, api],
@@ -300,7 +362,9 @@ export const Controls: FC<Props> = ({
};
const handleSliderStart = useCallback(() => {
if (!showControls) return;
if (!showControls) {
return;
}
setIsSliding(true);
wasPlayingRef.current = isPlaying;
@@ -339,7 +403,9 @@ export const Controls: FC<Props> = ({
);
const handleSkipBackward = useCallback(async () => {
if (!settings?.rewindSkipTime) return;
if (!settings?.rewindSkipTime) {
return;
}
wasPlayingRef.current = isPlaying;
lightHapticFeedback();
try {
@@ -371,7 +437,9 @@ export const Controls: FC<Props> = ({
? curr + secondsToMs(settings.forwardSkipTime)
: ticksToSeconds(curr) + settings.forwardSkipTime;
seek(Math.max(0, newTime));
if (wasPlayingRef.current) play();
if (wasPlayingRef.current) {
play();
}
}
} catch (error) {
writeToLog("ERROR", "Error seeking video forwards", error);
@@ -546,7 +614,7 @@ export const Controls: FC<Props> = ({
{nextItem && !offline && (
<TouchableOpacity
onPress={goToNextItem}
onPress={() => goToNextItem({ isAutoPlay: false })}
className='aspect-square flex flex-col rounded-xl items-center justify-center p-2'
>
<Ionicons name='play-skip-forward' size={24} color='white' />
@@ -741,17 +809,21 @@ export const Controls: FC<Props> = ({
onPress={skipCredit}
buttonText='Skip Credits'
/>
<NextEpisodeCountDownButton
show={
!nextItem
? false
: isVlc
? remainingTime < 10000
: remainingTime < 10
}
onFinish={goToNextItem}
onPress={goToNextItem}
/>
{(settings.maxAutoPlayEpisodeCount.value === -1 ||
settings.autoPlayEpisodeCount <
settings.maxAutoPlayEpisodeCount.value) && (
<NextEpisodeCountDownButton
show={
!nextItem
? false
: isVlc
? remainingTime < 10000
: remainingTime < 10
}
onFinish={handleNextEpisodeAutoPlay}
onPress={handleNextEpisodeManual}
/>
)}
</View>
</View>
<View
@@ -799,6 +871,9 @@ export const Controls: FC<Props> = ({
</View>
</>
)}
{settings.maxAutoPlayEpisodeCount.value !== -1 && (
<ContinueWatchingOverlay goToNextItem={handleContinueWatching} />
)}
</ControlProvider>
);
};

View File

@@ -79,7 +79,7 @@ const NextEpisodeCountDownButton: React.FC<NextEpisodeCountDownButtonProps> = ({
>
<Animated.View style={animatedStyle} />
<View className='px-3 py-3'>
<Text className='text-center font-bold'>
<Text numberOfLines={1} className='text-center font-bold'>
{t("player.next_episode")}
</Text>
</View>

View File

@@ -1,4 +1,5 @@
import type { TrackInfo } from "@/modules/VlcPlayer.types";
import { VideoPlayer, useSettings } from "@/utils/atoms/settings";
import { router, useLocalSearchParams } from "expo-router";
import type React from "react";
import {
@@ -47,6 +48,7 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
}) => {
const [audioTracks, setAudioTracks] = useState<Track[] | null>(null);
const [subtitleTracks, setSubtitleTracks] = useState<Track[] | null>(null);
const [settings] = useSettings();
const ControlContext = useControlContext();
const isVideoLoaded = ControlContext?.isVideoLoaded;
@@ -132,7 +134,7 @@ export const VideoProvider: React.FC<VideoProviderProps> = ({
);
// Step 2: Apply VLC indexing logic
let textSubIndex = 0;
let textSubIndex = settings.defaultPlayer === VideoPlayer.VLC_4 ? 0 : 1;
const processedSubs: Track[] = sortedSubs?.map((sub) => {
// Always increment for non-transcoding subtitles
// Only increment for text-based subtitles when transcoding

View File

@@ -1,231 +0,0 @@
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getItemImage } from "@/utils/getItemImage";
import { writeErrorLog, writeInfoLog } from "@/utils/log";
import type {
BaseItemDto,
MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client/models";
import { useQueryClient } from "@tanstack/react-query";
import * as FileSystem from "expo-file-system";
import { useRouter } from "expo-router";
// import { FFmpegKit, FFmpegSession, Statistics } from "ffmpeg-kit-react-native";
const FFMPEGKitReactNative = !Platform.isTV
? require("ffmpeg-kit-react-native")
: null;
import { useSettings } from "@/utils/atoms/settings";
import useDownloadHelper from "@/utils/download";
import type { JobStatus } from "@/utils/optimize-server";
import type { Api } from "@jellyfin/sdk";
import { useAtomValue } from "jotai";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
import { toast } from "sonner-native";
import useImageStorage from "./useImageStorage";
type FFmpegSession = typeof FFMPEGKitReactNative.FFmpegSession;
type Statistics = typeof FFMPEGKitReactNative.Statistics;
const FFmpegKit = Platform.isTV
? null
: (FFMPEGKitReactNative.FFmpegKit as typeof FFMPEGKitReactNative.FFmpegKit);
const createFFmpegCommand = (url: string, output: string) => [
"-y", // overwrite output files without asking
"-thread_queue_size 512", // https://ffmpeg.org/ffmpeg.html#toc-Advanced-options
// region ffmpeg protocol commands // https://ffmpeg.org/ffmpeg-protocols.html
"-protocol_whitelist file,http,https,tcp,tls,crypto", // whitelist
"-multiple_requests 1", // http
"-tcp_nodelay 1", // http
// endregion ffmpeg protocol commands
"-fflags +genpts", // format flags
`-i ${url}`, // infile
"-map 0:v -map 0:a", // select all streams for video & audio
"-c copy", // streamcopy, preventing transcoding
"-bufsize 25M", // amount of data processed before calculating current bitrate
"-max_muxing_queue_size 4096", // sets the size of stream buffer in packets for output
output,
];
/**
* Custom hook for remuxing HLS to MP4 using FFmpeg.
*
* @param url - The URL of the HLS stream
* @param item - The BaseItemDto object representing the media item
* @returns An object with remuxing-related functions
*/
export const useRemuxHlsToMp4 = () => {
const api = useAtomValue(apiAtom);
const router = useRouter();
const queryClient = useQueryClient();
const { t } = useTranslation();
const [settings] = useSettings();
const { saveImage } = useImageStorage();
const { saveSeriesPrimaryImage } = useDownloadHelper();
const {
saveDownloadedItemInfo,
setProcesses,
processes,
APP_CACHE_DOWNLOAD_DIRECTORY,
} = useDownload();
const onSaveAssets = async (api: Api, item: BaseItemDto) => {
await saveSeriesPrimaryImage(item);
const itemImage = getItemImage({
item,
api,
variant: "Primary",
quality: 90,
width: 500,
});
await saveImage(item.Id, itemImage?.uri);
};
const completeCallback = useCallback(
async (session: FFmpegSession, item: BaseItemDto) => {
try {
console.log("completeCallback");
const returnCode = await session.getReturnCode();
if (returnCode.isValueSuccess()) {
const stat = await session.getLastReceivedStatistics();
await FileSystem.moveAsync({
from: `${APP_CACHE_DOWNLOAD_DIRECTORY}${item.Id}.mp4`,
to: `${FileSystem.documentDirectory}${item.Id}.mp4`,
});
await queryClient.invalidateQueries({
queryKey: ["downloadedItems"],
});
saveDownloadedItemInfo(item, stat.getSize());
toast.success(t("home.downloads.toasts.download_completed"));
}
setProcesses((prev: any[]) => {
return prev.filter(
(process: { itemId: string | undefined }) =>
process.itemId !== item.Id,
);
});
} catch (e) {
console.error(e);
}
console.log("completeCallback ~ end");
},
[processes, setProcesses],
);
const statisticsCallback = useCallback(
(statistics: Statistics, item: BaseItemDto) => {
const videoLength =
(item.MediaSources?.[0]?.RunTimeTicks || 0) / 10000000; // In seconds
const fps = item.MediaStreams?.[0]?.RealFrameRate || 25;
const totalFrames = videoLength * fps;
const processedFrames = statistics.getVideoFrameNumber();
const speed = statistics.getSpeed();
const percentage =
totalFrames > 0 ? Math.floor((processedFrames / totalFrames) * 100) : 0;
if (!item.Id) throw new Error("Item is undefined");
setProcesses((prev: JobStatus[]) => {
return prev.map((process: JobStatus) => {
if (process.itemId === item.Id) {
return {
...process,
id: statistics.getSessionId().toString(),
progress: percentage,
speed: Math.max(speed, 0),
};
}
return process;
});
});
},
[setProcesses, completeCallback],
);
const startRemuxing = useCallback(
async (item: BaseItemDto, url: string, mediaSource: MediaSourceInfo) => {
const cacheDir = await FileSystem.getInfoAsync(
APP_CACHE_DOWNLOAD_DIRECTORY,
);
if (!cacheDir.exists) {
await FileSystem.makeDirectoryAsync(APP_CACHE_DOWNLOAD_DIRECTORY, {
intermediates: true,
});
}
const output = `${APP_CACHE_DOWNLOAD_DIRECTORY}${item.Id}.mp4`;
if (!api) throw new Error("API is not defined");
if (!item.Id) throw new Error("Item must have an Id");
// First lets save any important assets we want to present to the user offline
await onSaveAssets(api, item);
toast.success(
t("home.downloads.toasts.download_started_for", { item: item.Name }),
{
action: {
label: "Go to download",
onClick: () => {
router.push("/downloads");
toast.dismiss();
},
},
},
);
try {
const job: JobStatus = {
id: "",
deviceId: "",
inputUrl: url,
item: item,
itemId: item.Id!,
outputPath: output,
progress: 0,
status: "downloading",
timestamp: new Date(),
};
writeInfoLog(`useRemuxHlsToMp4 ~ startRemuxing for item ${item.Name}`);
setProcesses((prev: any) => [...prev, job]);
await FFmpegKit.executeAsync(
createFFmpegCommand(url, output).join(" "),
(session: any) => completeCallback(session, item),
undefined,
(s: any) => statisticsCallback(s, item),
);
} catch (e) {
const error = e as Error;
console.error("Failed to remux:", error);
writeErrorLog(
`useRemuxHlsToMp4 ~ remuxing failed for item: ${item.Name},
Error: ${error.message}, Stack: ${error.stack}`,
);
setProcesses((prev: any[]) => {
return prev.filter(
(process: { itemId: string | undefined }) =>
process.itemId !== item.Id,
);
});
throw error; // Re-throw the error to propagate it to the caller
}
},
[settings, processes, setProcesses, completeCallback, statisticsCallback],
);
const cancelRemuxing = useCallback(() => {
FFmpegKit.cancel();
setProcesses([]);
}, []);
return { startRemuxing, cancelRemuxing };
};

15
i18n.ts
View File

@@ -5,16 +5,18 @@ import { getLocales } from "expo-localization";
import de from "./translations/de.json";
import en from "./translations/en.json";
import es from "./translations/es.json";
import eo from "./translations/eo.json";
import fr from "./translations/fr.json";
import it from "./translations/it.json";
import ja from "./translations/ja.json";
import nl from "./translations/nl.json";
import pl from "./translations/pl.json";
import ptBR from "./translations/pt-BR.json";
import sv from "./translations/sv.json";
import ru from "./translations/ru.json";
import sv from "./translations/sv.json";
import tr from "./translations/tr.json";
import ua from "./translations/ua.json";
import tlh from "./translations/tlh.json";
import uk from "./translations/uk.json";
import zhCN from "./translations/zh-CN.json";
import zhTW from "./translations/zh-TW.json";
@@ -22,16 +24,19 @@ export const APP_LANGUAGES = [
{ label: "Deutsch", value: "de" },
{ label: "English", value: "en" },
{ label: "Español", value: "es" },
{ label: "Esperanto", value: "eo" },
{ label: "Français", value: "fr" },
{ label: "Italiano", value: "it" },
{ label: "日本語", value: "ja" },
{ label: "Klingon", value: "tlh" },
{ label: "Türkçe", value: "tr" },
{ label: "Nederlands", value: "nl" },
{ label: "Polski", value: "pl" },
{ label: "Português (Brasil)", value: "pt-BR" },
{ label: "Svenska", value: "sv" },
{ label: "Русский", value: "ru" },
{ label: "Українська", value: "ua" },
{ label: "Українська", value: "uk" },
{ label: "Українська", value: "uk" },
{ label: "简体中文", value: "zh-CN" },
{ label: "繁體中文", value: "zh-TW" },
];
@@ -42,6 +47,7 @@ i18n.use(initReactI18next).init({
de: { translation: de },
en: { translation: en },
es: { translation: es },
eo: { translation: eo },
fr: { translation: fr },
it: { translation: it },
ja: { translation: ja },
@@ -51,7 +57,8 @@ i18n.use(initReactI18next).init({
sv: { translation: sv },
ru: { translation: ru },
tr: { translation: tr },
ua: { translation: ua },
tlh: { translation: tlh },
uk: { translation: uk },
"zh-CN": { translation: zhCN },
"zh-TW": { translation: zhTW },
},

View File

@@ -1,10 +1,10 @@
import ExpoModulesCore
#if os(tvOS)
import TVVLCKit
import TVVLCKit
#else
import MobileVLCKit
import MobileVLCKit
#endif
import UIKit
class VlcPlayer3View: ExpoView {
private var mediaPlayer: VLCMediaPlayer?
@@ -16,7 +16,7 @@ class VlcPlayer3View: ExpoView {
private var lastReportedIsPlaying: Bool?
private var customSubtitles: [(internalName: String, originalName: String)] = []
private var startPosition: Int32 = 0
private var isMediaReady: Bool = false
private var externalSubtitles: [[String: String]]?
private var externalTrack: [String: String]?
private var progressTimer: DispatchSourceTimer?
private var isStopping: Bool = false // Define isStopping here
@@ -61,7 +61,7 @@ class VlcPlayer3View: ExpoView {
}
// MARK: - Public Methods
func startPictureInPicture() { }
func startPictureInPicture() {}
@objc func play() {
self.mediaPlayer?.play()
@@ -109,6 +109,7 @@ class VlcPlayer3View: ExpoView {
self.externalTrack = source["externalTrack"] as? [String: String]
var initOptions = source["initOptions"] as? [Any] ?? []
self.startPosition = source["startPosition"] as? Int32 ?? 0
self.externalSubtitles = source["externalSubtitles"] as? [[String: String]]
initOptions.append("--start-time=\(self.startPosition)")
guard let uri = source["uri"] as? String, !uri.isEmpty else {
@@ -143,8 +144,8 @@ class VlcPlayer3View: ExpoView {
media.addOptions(mediaOptions)
self.mediaPlayer?.media = media
self.setInitialExternalSubtitles()
self.hasSource = true
if autoplay {
print("Playing...")
self.play()
@@ -182,9 +183,9 @@ class VlcPlayer3View: ExpoView {
return
}
let result = self.mediaPlayer?.addPlaybackSlave(url, type: .subtitle, enforce: true)
let result = self.mediaPlayer?.addPlaybackSlave(url, type: .subtitle, enforce: false)
if let result = result {
let internalName = "Track \(self.customSubtitles.count + 1)"
let internalName = "Track \(self.customSubtitles.count)"
print("Subtitle added with result: \(result) \(internalName)")
self.customSubtitles.append((internalName: internalName, originalName: name))
} else {
@@ -192,6 +193,19 @@ class VlcPlayer3View: ExpoView {
}
}
private func setInitialExternalSubtitles() {
if let externalSubtitles = self.externalSubtitles {
for subtitle in externalSubtitles {
if let subtitleName = subtitle["name"],
let subtitleURL = subtitle["DeliveryUrl"]
{
print("Setting external subtitle: \(subtitleName) \(subtitleURL)")
self.setSubtitleURL(subtitleURL, name: subtitleName)
}
}
}
}
@objc func getSubtitleTracks() -> [[String: Any]]? {
guard let mediaPlayer = self.mediaPlayer else {
return nil
@@ -276,16 +290,6 @@ class VlcPlayer3View: ExpoView {
print("Debug: Current time: \(currentTimeMs)")
if currentTimeMs >= 0 && currentTimeMs < durationMs {
if player.isPlaying && !self.isMediaReady {
self.isMediaReady = true
// Set external track subtitle when starting.
if let externalTrack = self.externalTrack {
if let name = externalTrack["name"], !name.isEmpty {
let deliveryUrl = externalTrack["DeliveryUrl"] ?? ""
self.setSubtitleURL(deliveryUrl, name: name)
}
}
}
self.onVideoProgress?([
"currentTime": currentTimeMs,
"duration": durationMs,

View File

@@ -12,7 +12,6 @@ Pod::Spec.new do |s|
s.dependency 'ExpoModulesCore'
s.ios.dependency 'VLCKit', s.version
s.tvos.dependency 'VLCKit', s.version
s.dependency 'Alamofire', '~> 5.10'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {

View File

@@ -18,7 +18,6 @@
},
"dependencies": {
"@bottom-tabs/react-navigation": "0.8.6",
"@config-plugins/ffmpeg-kit-react-native": "^9.0.0",
"@expo/config-plugins": "~9.0.15",
"@expo/react-native-action-sheet": "^4.1.0",
"@expo/vector-icons": "^14.0.4",
@@ -57,7 +56,7 @@
"expo-router": "~4.0.17",
"expo-screen-orientation": "~8.0.4",
"expo-sensors": "~14.0.2",
"expo-sharing": "~13.1.0",
"expo-sharing": "~13.0.1",
"expo-splash-screen": "~0.29.22",
"expo-status-bar": "~2.0.1",
"expo-system-ui": "~4.0.8",
@@ -71,7 +70,7 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-i18next": "^15.4.0",
"react-native": "npm:react-native-tvos@~0.77.0-0",
"react-native": "npm:react-native-tvos@~0.77.2-0",
"react-native-awesome-slider": "^2.9.0",
"react-native-bottom-tabs": "0.8.6",
"react-native-circular-progress": "^1.4.1",

0
patches/.gitkeep Normal file
View File

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,6 @@
import { useHaptic } from "@/hooks/useHaptic";
import useImageStorage from "@/hooks/useImageStorage";
import { useInterval } from "@/hooks/useInterval";
import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
import { getOrSetDeviceId } from "@/utils/device";
import useDownloadHelper from "@/utils/download";
@@ -18,6 +19,7 @@ import type {
BaseItemDto,
MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client/models";
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
import BackGroundDownloader from "@kesha-antonov/react-native-background-downloader";
import { focusManager, useQuery, useQueryClient } from "@tanstack/react-query";
import axios from "axios";
@@ -38,6 +40,7 @@ import {
import { useTranslation } from "react-i18next";
import { AppState, type AppStateStatus, Platform } from "react-native";
import { toast } from "sonner-native";
import { Bitrate } from "../components/BitrateSelector";
import { apiAtom } from "./JellyfinProvider";
export type DownloadedItem = {
@@ -66,7 +69,7 @@ function useDownloadProvider() {
const { saveSeriesPrimaryImage } = useDownloadHelper();
const { saveImage } = useImageStorage();
const [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
let [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
const successHapticFeedback = useHaptic("success");
@@ -74,6 +77,17 @@ function useDownloadProvider() {
return api?.accessToken;
}, [api]);
const usingOptimizedServer = useMemo(
() => settings?.downloadMethod === DownloadMethod.Optimized,
[settings],
);
const getDownloadUrl = (process: JobStatus) => {
return usingOptimizedServer
? `${settings.optimizedVersionsServerUrl}download/${process.id}`
: process.inputUrl;
};
const { data: downloadedFiles, refetch } = useQuery({
queryKey: ["downloadedItems"],
queryFn: getAllDownloadedItems,
@@ -164,6 +178,64 @@ function useDownloadProvider() {
enabled: settings?.downloadMethod === DownloadMethod.Optimized,
});
/// Cant use the background downloader callback. As its not triggered if size is unknown.
const updateProgress = async () => {
if (settings?.downloadMethod === DownloadMethod.Optimized) {
return;
}
// const response = await getSessionApi(api).getSessions({
// activeWithinSeconds: 300,
// });
const tasks = await BackGroundDownloader.checkForExistingDownloads();
// check if processes are missing
const missingProcesses = tasks
.filter((t) => !processes.some((p) => p.id === t.id))
.map((t) => {
return t.metadata;
});
processes = [...processes, ...missingProcesses];
const updatedProcesses = processes.map((p) => {
// const result = response.data.find((s) => s.Id == p.sessionId);
// if (result) {
// return {
// ...p,
// progress: result.TranscodingInfo?.CompletionPercentage,
// };
// }
// fallback. Doesn't really work for transcodes as they may be a lot smaller.
// We make an wild guess by comparing bitrates
const task = tasks.find((s) => s.id === p.id);
if (task) {
let progress = p.progress;
let size = p.mediaSource.Size;
const maxBitrate = p.maxBitrate.value;
if (maxBitrate && maxBitrate < p.mediaSource.Bitrate) {
size = (size / p.mediaSource.Bitrate) * maxBitrate;
}
progress = (100 / size) * task.bytesDownloaded;
if (progress >= 100) {
progress = 99;
}
return {
...p,
progress,
};
}
return p;
});
setProcesses(updatedProcesses);
};
useInterval(updateProgress, 2000);
useEffect(() => {
const checkIfShouldStartDownload = async () => {
if (processes.length === 0) return;
@@ -176,18 +248,25 @@ function useDownloadProvider() {
const removeProcess = useCallback(
async (id: string) => {
const deviceId = await getOrSetDeviceId();
if (!deviceId || !authHeader || !settings?.optimizedVersionsServerUrl)
return;
if (!deviceId || !authHeader) return;
try {
await cancelJobById({
authHeader,
id,
url: settings?.optimizedVersionsServerUrl,
});
} catch (error) {
console.error(error);
if (usingOptimizedServer) {
try {
await cancelJobById({
authHeader,
id,
url: settings?.optimizedVersionsServerUrl,
});
} catch (error) {
console.error(error);
}
}
setProcesses((prev: any[]) => {
return prev.filter(
(process: { itemId: string | undefined }) => process.id !== id,
);
});
},
[settings?.optimizedVersionsServerUrl, authHeader],
);
@@ -238,8 +317,9 @@ function useDownloadProvider() {
BackGroundDownloader?.download({
id: process.id,
url: `${settings?.optimizedVersionsServerUrl}download/${process.id}`,
url: getDownloadUrl(process),
destination: `${baseDirectory}/${process.item.Id}.mp4`,
metadata: process,
})
.begin(() => {
setProcesses((prev) =>
@@ -256,6 +336,9 @@ function useDownloadProvider() {
);
})
.progress((data) => {
if (!usingOptimizedServer) {
return;
}
const percent = (data.bytesDownloaded / data.bytesTotal) * 100;
setProcesses((prev) =>
prev.map((p) =>
@@ -328,7 +411,12 @@ function useDownloadProvider() {
);
const startBackgroundDownload = useCallback(
async (url: string, item: BaseItemDto, mediaSource: MediaSourceInfo) => {
async (
url: string,
item: BaseItemDto,
mediaSource: MediaSourceInfo,
maxBitrate?: Bitrate,
) => {
if (!api || !item.Id || !authHeader)
throw new Error("startBackgroundDownload ~ Missing required params");
@@ -345,26 +433,42 @@ function useDownloadProvider() {
width: 500,
});
await saveImage(item.Id, itemImage?.uri);
const response = await axios.post(
`${settings?.optimizedVersionsServerUrl}optimize-version`,
{
url,
fileExtension,
deviceId,
itemId: item.Id,
item,
},
{
headers: {
"Content-Type": "application/json",
Authorization: authHeader,
if (usingOptimizedServer) {
const response = await axios.post(
`${settings?.optimizedVersionsServerUrl}optimize-version`,
{
url,
fileExtension,
deviceId,
itemId: item.Id,
item,
},
},
);
{
headers: {
"Content-Type": "application/json",
Authorization: authHeader,
},
},
);
if (response.status !== 201) {
throw new Error("Failed to start optimization job");
if (response.status !== 201) {
throw new Error("Failed to start optimization job");
}
} else {
const job: JobStatus = {
id: item.Id!,
deviceId: deviceId,
inputUrl: url,
item: item,
itemId: item.Id!,
mediaSource,
progress: 0,
maxBitrate,
status: "downloading",
timestamp: new Date(),
};
setProcesses([...processes, job]);
startDownload(job);
}
toast.success(

View File

@@ -1,108 +0,0 @@
import { storage } from "@/utils/mmkv";
import type { JobStatus } from "@/utils/optimize-server";
import type {
BaseItemDto,
MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client/models";
import * as Application from "expo-application";
import * as FileSystem from "expo-file-system";
import { atom, useAtom } from "jotai";
import type React from "react";
import { createContext, useCallback, useContext, useMemo } from "react";
export type DownloadedItem = {
item: Partial<BaseItemDto>;
mediaSource: MediaSourceInfo;
};
export const processesAtom = atom<JobStatus[]>([]);
const DownloadContext = createContext<ReturnType<
typeof useDownloadProvider
> | null>(null);
/**
* Dummy download provider for tvOS
*/
function useDownloadProvider() {
const [processes, setProcesses] = useAtom<JobStatus[]>(processesAtom);
const downloadedFiles: DownloadedItem[] = [];
const removeProcess = useCallback(async (id: string) => {}, []);
const startDownload = useCallback(async (process: JobStatus) => {
return null;
}, []);
const startBackgroundDownload = useCallback(
async (url: string, item: BaseItemDto, mediaSource: MediaSourceInfo) => {
return null;
},
[],
);
const deleteAllFiles = async (): Promise<void> => {};
const deleteFile = async (id: string): Promise<void> => {};
const deleteItems = async (items: BaseItemDto[]) => {};
const cleanCacheDirectory = async () => {};
const deleteFileByType = async (type: BaseItemDto["Type"]) => {};
const appSizeUsage = useMemo(async () => {
return 0;
}, []);
function getDownloadedItem(itemId: string): DownloadedItem | null {
return null;
}
function saveDownloadedItemInfo(item: BaseItemDto, size = 0) {}
function getDownloadedItemSize(itemId: string): number {
const size = storage.getString(`downloadedItemSize-${itemId}`);
return size ? Number.parseInt(size) : 0;
}
const APP_CACHE_DOWNLOAD_DIRECTORY = `${FileSystem.cacheDirectory}${Application.applicationId}/Downloads/`;
return {
processes,
startBackgroundDownload,
downloadedFiles,
deleteAllFiles,
deleteFile,
deleteItems,
saveDownloadedItemInfo,
removeProcess,
setProcesses,
startDownload,
getDownloadedItem,
deleteFileByType,
appSizeUsage,
getDownloadedItemSize,
APP_CACHE_DOWNLOAD_DIRECTORY,
cleanCacheDirectory,
};
}
export function DownloadProvider({ children }: { children: React.ReactNode }) {
const downloadProviderValue = useDownloadProvider();
return (
<DownloadContext.Provider value={downloadProviderValue}>
{children}
</DownloadContext.Provider>
);
}
export function useDownload() {
const context = useContext(DownloadContext);
if (context === null) {
throw new Error("useDownload must be used within a DownloadProvider");
}
return context;
}

View File

@@ -1,7 +1,7 @@
import type { Bitrate } from "@/components/BitrateSelector";
import { settingsAtom } from "@/utils/atoms/settings";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import native from "@/utils/profiles/native";
import generateDeviceProfile from "@/utils/profiles/native";
import type {
BaseItemDto,
MediaSourceInfo,
@@ -84,6 +84,7 @@ export const PlaySettingsProvider: React.FC<{ children: React.ReactNode }> = ({
}
try {
const native = await generateDeviceProfile();
const data = await getStreamUrl({
api,
deviceProfile: native,

View File

@@ -138,7 +138,8 @@
"hide_libraries": "Bibliotheken ausblenden",
"select_liraries_you_want_to_hide": "Wähl die Bibliotheken aus, die du im Bibliothekstab und auf der Startseite ausblenden möchtest.",
"disable_haptic_feedback": "Haptisches Feedback deaktivieren",
"default_quality": "Standardqualität"
"default_quality": "Standardqualität",
"disabled": "Deaktiviert"
},
"downloads": {
"downloads_title": "Downloads",
@@ -370,7 +371,9 @@
"audio_tracks": "Audiospuren:",
"playback_state": "Wiedergabestatus:",
"no_data_available": "Keine Daten verfügbar",
"index": "Index:"
"index": "Index:",
"continue_watching": "Weiterschauen",
"go_back": "Zurück"
},
"item_card": {
"next_up": "Als Nächstes",

View File

@@ -138,7 +138,9 @@
"hide_libraries": "Hide Libraries",
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
"disable_haptic_feedback": "Disable Haptic Feedback",
"default_quality": "Default quality"
"default_quality": "Default quality",
"max_auto_play_episode_count": "Max auto play episode count",
"disabled": "Disabled"
},
"downloads": {
"downloads_title": "Downloads",
@@ -374,7 +376,9 @@
"audio_tracks": "Audio Tracks:",
"playback_state": "Playback State:",
"no_data_available": "No data available",
"index": "Index:"
"index": "Index:",
"continue_watching": "Continue Watching",
"go_back": "Go back"
},
"item_card": {
"next_up": "Next up",

480
translations/eo.json Normal file
View File

@@ -0,0 +1,480 @@
{
"login": {
"username_required": "Uzantnomo estas deviga",
"error_title": "Eraro",
"login_title": "Ensaluti",
"login_to_title": "Ensaluti al",
"username_placeholder": "Uzantnomo",
"password_placeholder": "Pasvorto",
"login_button": "Ensaluti",
"quick_connect": "Rapida Konekto",
"enter_code_to_login": "Enigu kodon {{code}} por ensaluti",
"failed_to_initiate_quick_connect": "Malsukcesis iniciati Rapidan Konekton",
"got_it": "Komprenita",
"connection_failed": "Konekto malsukcesis",
"could_not_connect_to_server": "Ne povis konekti al la servilo. Bonvolu kontroli la URL-on kaj vian retan konekton.",
"an_unexpected_error_occured": "Neatendita eraro okazis",
"change_server": "Ŝanĝi servilon",
"invalid_username_or_password": "Nevalida uzantnomo aŭ pasvorto",
"user_does_not_have_permission_to_log_in": "Uzanto ne havas permeson ensaluti",
"server_is_taking_too_long_to_respond_try_again_later": "Servilo respondas tro malrapide, provu denove poste",
"server_received_too_many_requests_try_again_later": "Servilo ricevis tro multajn petojn, provu denove poste.",
"there_is_a_server_error": "Estas servila eraro",
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Neatendita eraro okazis. Ĉu vi enigis la ĝustan servilan URL-on?"
},
"server": {
"enter_url_to_jellyfin_server": "Enigu la URL-on al via Jellyfin-servilo",
"server_url_placeholder": "http(s)://via-servilo.com",
"connect_button": "Konekti",
"previous_servers": "antaŭaj serviloj",
"clear_button": "Forviŝi",
"search_for_local_servers": "Serĉi lokajn servilojn",
"searching": "Serĉante...",
"servers": "Serviloj"
},
"home": {
"no_internet": "Neniu Interreto",
"no_items": "Neniuj eroj",
"no_internet_message": "Ne zorgu, vi ankoraŭ povas spekti\nelsŝutitan enhavon.",
"go_to_downloads": "Iri al elŝutoj",
"oops": "Ho ve!",
"error_message": "Io misfunkciis.\nBonvolu elsaluti kaj reensaluti.",
"continue_watching": "Daŭrigi Spektadon",
"next_up": "Sekva",
"recently_added_in": "Ĵus Aldonita en {{libraryName}}",
"suggested_movies": "Sugestitaj Filmoj",
"suggested_episodes": "Sugestitaj Epizodoj",
"intro": {
"welcome_to_streamyfin": "Bonvenon al Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Senpaga kaj malfermfonta kliento por Jellyfin.",
"features_title": "Trajtoj",
"features_description": "Streamyfin havas multajn trajtojn kaj integriĝas kun vasta gamo de programaroj, kiujn vi povas trovi en la agorda menuo, tiuj inkluzivas:",
"jellyseerr_feature_description": "Konekti al via Jellyseerr-instanco kaj peti filmojn rekte en la aplikaĵo.",
"downloads_feature_title": "Elŝutoj",
"downloads_feature_description": "Elŝutu filmojn kaj televidajn seriojn por vidi senkonekte. Uzu aŭ la defaŭltan metodon aŭ instalu la optimumigan servilon por elŝuti dosierojn en la fono.",
"chromecast_feature_description": "Ĵetu filmojn kaj televidajn seriojn al viaj Chromecast-aparatoj.",
"centralised_settings_plugin_title": "Centralizita Agorda Kromprogramo",
"centralised_settings_plugin_description": "Agordu agordojn de centralizita loko sur via Jellyfin-servilo. Ĉiuj klientaj agordoj por ĉiuj uzantoj estos sinkronigitaj aŭtomate.",
"done_button": "Farite",
"go_to_settings_button": "Iri al agordoj",
"read_more": "Legu pli"
},
"settings": {
"settings_title": "Agordoj",
"log_out_button": "Elsaluti",
"user_info": {
"user_info_title": "Uzantaj Informoj",
"user": "Uzanto",
"server": "Servilo",
"token": "Ĵetono",
"app_version": "Aplikaĵa Versio"
},
"quick_connect": {
"quick_connect_title": "Rapida Konekto",
"authorize_button": "Aŭtorizi Rapidan Konekton",
"enter_the_quick_connect_code": "Enigu la rapidan konektan kodon...",
"success": "Sukceso",
"quick_connect_autorized": "Rapida Konekto aŭtorizita",
"error": "Eraro",
"invalid_code": "Nevalida kodo",
"authorize": "Aŭtorizi"
},
"media_controls": {
"media_controls_title": "Mediaj Kontroloj",
"forward_skip_length": "Antaŭensalta longeco",
"rewind_length": "Rebobena longeco",
"seconds_unit": "s"
},
"audio": {
"audio_title": "Audio",
"set_audio_track": "Agordi Aŭdian Trakon De Antaŭa Ero",
"audio_language": "Aŭdia lingvo",
"audio_hint": "Elektu defaŭltan aŭdian lingvon.",
"none": "Neniu",
"language": "Lingvo"
},
"subtitles": {
"subtitle_title": "Subtekstoj",
"subtitle_language": "Subteksta lingvo",
"subtitle_mode": "Subteksta Reĝimo",
"set_subtitle_track": "Agordi Subtekstan Trakon De Antaŭa Ero",
"subtitle_size": "Subteksta Grandeco",
"subtitle_hint": "Agordu subtekstan preferon.",
"none": "Neniu",
"language": "Lingvo",
"loading": "Ŝarĝante",
"modes": {
"Default": "Defaŭlta",
"Smart": "Inteligenta",
"Always": "Ĉiam",
"None": "Neniu",
"OnlyForced": "NurDevigita"
}
},
"other": {
"other_title": "Alia",
"follow_device_orientation": "Aŭtomata rotacio",
"video_orientation": "Video-orientiĝo",
"orientation": "Orientiĝo",
"orientations": {
"DEFAULT": "Defaŭlta",
"ALL": "Ĉiuj",
"PORTRAIT": "Portreta",
"PORTRAIT_UP": "Portreta Supren",
"PORTRAIT_DOWN": "Portreta Malsupren",
"LANDSCAPE": "Pejzaĝa",
"LANDSCAPE_LEFT": "Pejzaĝa Maldekstren",
"LANDSCAPE_RIGHT": "Pejzaĝa Dekstren",
"OTHER": "Alia",
"UNKNOWN": "Nekonata"
},
"safe_area_in_controls": "Sekura areo en kontroloj",
"video_player": "Video-ludilo",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Eksperimenta + PiP)"
},
"show_custom_menu_links": "Montri Proprajn Menuajn Ligilojn",
"hide_libraries": "Kaŝi Bibliotekojn",
"select_liraries_you_want_to_hide": "Elektu la bibliotekojn, kiujn vi volas kaŝi de la Biblioteka langeto kaj hejmpaĝaj sekcioj.",
"disable_haptic_feedback": "Malŝalti Haptan Rimarkon",
"default_quality": "Defaŭlta kvalito"
},
"downloads": {
"downloads_title": "Elŝutoj",
"download_method": "Elŝuta metodo",
"remux_max_download": "Remux maksimuma elŝuto",
"auto_download": "Aŭtomata elŝuto",
"optimized_versions_server": "Optimumigitaj versioj servilo",
"save_button": "Konservi",
"optimized_server": "Optimumigita Servilo",
"optimized": "Optimumigita",
"default": "Defaŭlta",
"optimized_version_hint": "Enigu la URL-on por la optimumiga servilo. La URL devus inkluzivi http aŭ https kaj laŭvole la pordon.",
"read_more_about_optimized_server": "Legu pli pri la optimumiga servilo.",
"url": "URL",
"server_url_placeholder": "http(s)://domajno.org:pordo"
},
"plugins": {
"plugins_title": "Kromprogramoj",
"jellyseerr": {
"jellyseerr_warning": "Ĉi tiu integriĝo estas en siaj fruaj stadioj. Atendu ŝanĝojn.",
"server_url": "Servila URL",
"server_url_hint": "Ekzemplo: http(s)://via-gastiganto.url\n(aldonu pordon se necese)",
"server_url_placeholder": "Jellyseerr URL...",
"password": "Pasvorto",
"password_placeholder": "Enigu pasvorton por Jellyfin-uzanto {{username}}",
"save_button": "Konservi",
"clear_button": "Forviŝi",
"login_button": "Ensaluti",
"total_media_requests": "Totalaj mediaj petoj",
"movie_quota_limit": "Filma kvota limo",
"movie_quota_days": "Filmaj kvotaj tagoj",
"tv_quota_limit": "Televida kvota limo",
"tv_quota_days": "Televidaj kvotaj tagoj",
"reset_jellyseerr_config_button": "Restarigi Jellyseerr-agordon",
"unlimited": "Senlima",
"plus_n_more": "+{{n}} pli",
"order_by": {
"DEFAULT": "Defaŭlta",
"VOTE_COUNT_AND_AVERAGE": "Voĉdonkalkulo kaj mezumo",
"POPULARITY": "Populareco"
}
},
"marlin_search": {
"enable_marlin_search": "Ebligi Marlin Serĉon ",
"url": "URL",
"server_url_placeholder": "http(s)://domajno.org:pordo",
"marlin_search_hint": "Enigu la URL-on por la Marlin-servilo. La URL devus inkluzivi http aŭ https kaj laŭvole la pordon.",
"read_more_about_marlin": "Legu pli pri Marlin.",
"save_button": "Konservi",
"toasts": {
"saved": "Konservita"
}
}
},
"storage": {
"storage_title": "Stokado",
"app_usage": "Aplikaĵo {{usedSpace}}%",
"device_usage": "Aparato {{availableSpace}}%",
"size_used": "{{used}} el {{total}} uzata",
"delete_all_downloaded_files": "Forigi Ĉiujn Elŝutitajn Dosierojn"
},
"intro": {
"show_intro": "Montri enkondukon",
"reset_intro": "Restarigi enkondukon"
},
"logs": {
"logs_title": "Protokoloj",
"export_logs": "Eksporti protokolojn",
"click_for_more_info": "Klaku por pli da informoj",
"level": "Nivelo",
"no_logs_available": "Neniuj protokoloj disponeblaj",
"delete_all_logs": "Forigi ĉiujn protokolojn"
},
"languages": {
"title": "Lingvoj",
"app_language": "Aplikaĵa lingvo",
"app_language_description": "Elektu la lingvon por la aplikaĵo.",
"system": "Sistemo"
},
"toasts": {
"error_deleting_files": "Eraro forigante dosierojn",
"background_downloads_enabled": "Fonaj elŝutoj ebligitaj",
"background_downloads_disabled": "Fonaj elŝutoj malŝaltitaj",
"connected": "Konektita",
"could_not_connect": "Ne povis konekti",
"invalid_url": "Nevalida URL"
}
},
"sessions": {
"title": "Sesioj",
"no_active_sessions": "Neniuj aktivaj sesioj"
},
"downloads": {
"downloads_title": "Elŝutoj",
"tvseries": "Televidaj serioj",
"movies": "Filmoj",
"queue": "Vico",
"queue_hint": "Vico kaj elŝutoj perdiĝos ĉe aplikaĵa rekomenco",
"no_items_in_queue": "Neniuj eroj en vico",
"no_downloaded_items": "Neniuj elŝutitaj eroj",
"delete_all_movies_button": "Forigi ĉiujn Filmojn",
"delete_all_tvseries_button": "Forigi ĉiujn Televidajn Seriojn",
"delete_all_button": "Forigi ĉion",
"active_download": "Aktiva elŝuto",
"no_active_downloads": "Neniuj aktivaj elŝutoj",
"active_downloads": "Aktivaj elŝutoj",
"new_app_version_requires_re_download": "Nova aplikaĵa versio postulas re-elŝuton",
"new_app_version_requires_re_download_description": "La nova ĝisdatigo postulas, ke enhavo estu elŝutita denove. Bonvolu forigi ĉian elŝutitan enhavon kaj provi denove.",
"back": "Reen",
"delete": "Forigi",
"something_went_wrong": "Io misfunkciis",
"could_not_get_stream_url_from_jellyfin": "Ne povis akiri la fluan URL-on de Jellyfin",
"eta": "ETA {{eta}}",
"methods": "Metodoj",
"toasts": {
"you_are_not_allowed_to_download_files": "Vi ne rajtas elŝuti dosierojn.",
"deleted_all_movies_successfully": "Sukcese forigis ĉiujn filmojn!",
"failed_to_delete_all_movies": "Malsukcesis forigi ĉiujn filmojn",
"deleted_all_tvseries_successfully": "Sukcese forigis ĉiujn Televidajn Seriojn!",
"failed_to_delete_all_tvseries": "Malsukcesis forigi ĉiujn Televidajn Seriojn",
"download_cancelled": "Elŝuto nuligita",
"could_not_cancel_download": "Ne povis nuligi elŝuton",
"download_completed": "Elŝuto finita",
"download_started_for": "Elŝuto komenciĝis por {{item}}",
"item_is_ready_to_be_downloaded": "{{item}} estas preta por esti elŝutita",
"download_stated_for_item": "Elŝuto komenciĝis por {{item}}",
"download_failed_for_item": "Elŝuto malsukcesis por {{item}} - {{error}}",
"download_completed_for_item": "Elŝuto finita por {{item}}",
"queued_item_for_optimization": "Envicigis {{item}} por optimumigo",
"failed_to_start_download_for_item": "Malsukcesis komenci elŝutadon por {{item}}: {{message}}",
"server_responded_with_status_code": "Servilo respondis kun statuskodo {{statusCode}}",
"no_response_received_from_server": "Neniu respondo ricevita de la servilo",
"error_setting_up_the_request": "Eraro starigante la peton",
"failed_to_start_download_for_item_unexpected_error": "Malsukcesis komenci elŝutadon por {{item}}: Neatendita eraro",
"all_files_folders_and_jobs_deleted_successfully": "Ĉiuj dosieroj, dosierujoj kaj taskoj sukcese forigitaj",
"an_error_occured_while_deleting_files_and_jobs": "Eraro okazis dum forigo de dosieroj kaj taskoj",
"go_to_downloads": "Iri al elŝutoj"
}
}
},
"search": {
"search_here": "Serĉu ĉi tie...",
"search": "Serĉi...",
"x_items": "{{count}} eroj",
"library": "Biblioteko",
"discover": "Malkovri",
"no_results": "Neniuj rezultoj",
"no_results_found_for": "Neniuj rezultoj trovitaj por",
"movies": "Filmoj",
"series": "Serioj",
"episodes": "Epizodoj",
"collections": "Kolektoj",
"actors": "Aktoroj",
"request_movies": "Peti Filmojn",
"request_series": "Peti Seriojn",
"recently_added": "Ĵus Aldonita",
"recent_requests": "Lastatempaj Petoj",
"plex_watchlist": "Plex Spektolisto",
"trending": "Tendencaj",
"popular_movies": "Popularaj Filmoj",
"movie_genres": "Filmaj Ĝenroj",
"upcoming_movies": "Venontaj Filmoj",
"studios": "Studioj",
"popular_tv": "Populara Televido",
"tv_genres": "Televidaj Ĝenroj",
"upcoming_tv": "Venonta Televido",
"networks": "Retoj",
"tmdb_movie_keyword": "TMDB Filma Ŝlosilvorto",
"tmdb_movie_genre": "TMDB Filma Ĝenro",
"tmdb_tv_keyword": "TMDB Televida Ŝlosilvorto",
"tmdb_tv_genre": "TMDB Televida Ĝenro",
"tmdb_search": "TMDB Serĉo",
"tmdb_studio": "TMDB Studio",
"tmdb_network": "TMDB Reto",
"tmdb_movie_streaming_services": "TMDB Filmaj Fluservoj",
"tmdb_tv_streaming_services": "TMDB Televidaj Fluservoj"
},
"library": {
"no_items_found": "Neniuj eroj trovitaj",
"no_results": "Neniuj rezultoj",
"no_libraries_found": "Neniuj bibliotekoj trovitaj",
"item_types": {
"movies": "filmoj",
"series": "serioj",
"boxsets": "skatolaj aroj",
"items": "eroj"
},
"options": {
"display": "Vidigi",
"row": "Vico",
"list": "Listo",
"image_style": "Bildostilo",
"poster": "Afiŝo",
"cover": "Kovrilo",
"show_titles": "Montri titolojn",
"show_stats": "Montri statistikojn"
},
"filters": {
"genres": "Ĝenroj",
"years": "Jaroj",
"sort_by": "Ordigi laŭ",
"sort_order": "Orda ordo",
"asc": "Supreniranta",
"desc": "Malsupreniranta",
"tags": "Etikedoj"
}
},
"favorites": {
"series": "Serioj",
"movies": "Filmoj",
"episodes": "Epizodoj",
"videos": "Videoj",
"boxsets": "Skatolaj aroj",
"playlists": "Ludlistoj",
"noDataTitle": "Ankoraŭ neniuj favoratoj",
"noData": "Marku erojn kiel favoratojn por vidi ilin aperi ĉi tie por rapida aliro."
},
"custom_links": {
"no_links": "Neniuj ligiloj"
},
"player": {
"error": "Eraro",
"failed_to_get_stream_url": "Malsukcesis akiri la fluan URL-on",
"an_error_occured_while_playing_the_video": "Eraro okazis dum ludado de la video. Kontrolu protokolojn en agordoj.",
"client_error": "Klienta eraro",
"could_not_create_stream_for_chromecast": "Ne povis krei fluon por Chromecast",
"message_from_server": "Mesaĝo de servilo: {{message}}",
"video_has_finished_playing": "Video finis ludi!",
"no_video_source": "Neniu video-fonto...",
"next_episode": "Sekva Epizodo",
"refresh_tracks": "Refreŝigi Trakojn",
"subtitle_tracks": "Subtekstaj Trakoj:",
"audio_tracks": "Aŭdiaj Trakoj:",
"playback_state": "Ludada Stato:",
"no_data_available": "Neniuj datumoj disponeblaj",
"index": "Indekso:"
},
"item_card": {
"next_up": "Sekva",
"no_items_to_display": "Neniuj eroj por montri",
"cast_and_crew": "Rolantaro & Skiparo",
"series": "Serioj",
"seasons": "Sezonoj",
"season": "Sezono",
"no_episodes_for_this_season": "Neniuj epizodoj por ĉi tiu sezono",
"overview": "Superrigardo",
"more_with": "Pli kun {{name}}",
"similar_items": "Similaj eroj",
"no_similar_items_found": "Neniuj similaj eroj trovitaj",
"video": "Video",
"more_details": "Pli da detaloj",
"quality": "Kvalito",
"audio": "Audio",
"subtitles": "Subteksto",
"show_more": "Montri pli",
"show_less": "Montri malpli",
"appeared_in": "Aperis en",
"could_not_load_item": "Ne povis ŝarĝi eron",
"none": "Neniu",
"download": {
"download_season": "Elŝuti Sezonon",
"download_series": "Elŝuti Serion",
"download_episode": "Elŝuti Epizodon",
"download_movie": "Elŝuti Filmon",
"download_x_item": "Elŝuti {{item_count}} erojn",
"download_button": "Elŝuti",
"using_optimized_server": "Uzante optimumigitan servilon",
"using_default_method": "Uzante defaŭltan metodon"
}
},
"live_tv": {
"next": "Sekva",
"previous": "Antaŭa",
"live_tv": "Viva Televido",
"coming_soon": "Baldaŭ",
"on_now": "Nun",
"shows": "Spektakloj",
"movies": "Filmoj",
"sports": "Sportoj",
"for_kids": "Por Infanoj",
"news": "Novaĵoj"
},
"jellyseerr": {
"confirm": "Konfirmi",
"cancel": "Nuligi",
"yes": "Jes",
"whats_wrong": "Kio estas malĝusta?",
"issue_type": "Problema tipo",
"select_an_issue": "Elektu problemon",
"types": "Tipoj",
"describe_the_issue": "(laŭvola) Priskribu la problemon...",
"submit_button": "Sendi",
"report_issue_button": "Raporti problemon",
"request_button": "Peti",
"are_you_sure_you_want_to_request_all_seasons": "Ĉu vi certas, ke vi volas peti ĉiujn sezonojn?",
"failed_to_login": "Malsukcesis ensaluti",
"cast": "Rolantaro",
"details": "Detaloj",
"status": "Stato",
"original_title": "Originala Titolo",
"series_type": "Seria Tipo",
"release_dates": "Eldondatoj",
"first_air_date": "Unua Elsendo-dato",
"next_air_date": "Sekva Elsendo-dato",
"revenue": "Enspezo",
"budget": "Buĝeto",
"original_language": "Originala Lingvo",
"production_country": "Produktada Lando",
"studios": "Studioj",
"network": "Reto",
"currently_streaming_on": "Nuntempe Flusanta ĉe",
"advanced": "Altnivela",
"request_as": "Peti Kiel",
"tags": "Etikedoj",
"quality_profile": "Kvalita Profilo",
"root_folder": "Radika Dosierujo",
"season_all": "Sezono (ĉiuj)",
"season_number": "Sezono {{season_number}}",
"number_episodes": "{{episode_number}} Epizodoj",
"born": "Naskiĝis",
"appearances": "Aperoj",
"toasts": {
"jellyseer_does_not_meet_requirements": "Jellyseerr-servilo ne plenumas minimumajn versiajn postulojn! Bonvolu ĝisdatigi al almenaŭ 2.0.0",
"jellyseerr_test_failed": "Jellyseerr-testo malsukcesis. Bonvolu provi denove.",
"failed_to_test_jellyseerr_server_url": "Malsukcesis testi jellyseerr-servilan url-on",
"issue_submitted": "Problemo sendita!",
"requested_item": "Petis {{item}}!",
"you_dont_have_permission_to_request": "Vi ne havas permeson peti!",
"something_went_wrong_requesting_media": "Io misfunkciis petante medion!"
}
},
"tabs": {
"home": "Hejmo",
"search": "Serĉi",
"library": "Biblioteko",
"custom_links": "Propraj Ligiloj",
"favorites": "Favoratoj"
}
}

View File

@@ -138,7 +138,8 @@
"hide_libraries": "Ocultar bibliotecas",
"select_liraries_you_want_to_hide": "Selecciona las bibliotecas que quieres ocultar de la pestaña Bibliotecas y de Inicio.",
"disable_haptic_feedback": "Desactivar feedback háptico",
"default_quality": "Calidad por defecto"
"default_quality": "Calidad por defecto",
"disabled": "Deshabilitado"
},
"downloads": {
"downloads_title": "Descargas",
@@ -370,7 +371,9 @@
"audio_tracks": "Pistas de audio:",
"playback_state": "Estado de la reproducción:",
"no_data_available": "No hay datos disponibles",
"index": "Índice:"
"index": "Índice:",
"continue_watching": "Continuar viendo",
"go_back": "Volver"
},
"item_card": {
"next_up": "A continuación",

View File

@@ -138,7 +138,8 @@
"hide_libraries": "Cacher des bibliothèques",
"select_liraries_you_want_to_hide": "Sélectionnez les bibliothèques que vous souhaitez masquer dans l'onglet Bibliothèque et les sections de la page d'accueil.",
"disable_haptic_feedback": "Désactiver le retour haptique",
"default_quality": "Qualité par défaut"
"default_quality": "Qualité par défaut",
"disabled": "Désactivé"
},
"downloads": {
"downloads_title": "Téléchargements",
@@ -370,7 +371,9 @@
"audio_tracks": "Pistes audio:",
"playback_state": "État de lecture:",
"no_data_available": "Aucune donnée disponible",
"index": "Index:"
"index": "Index :",
"continue_watching": "Continuer à regarder",
"go_back": "Retour"
},
"item_card": {
"next_up": "À suivre",

View File

@@ -138,7 +138,8 @@
"hide_libraries": "Nascondi Librerie",
"select_liraries_you_want_to_hide": "Selezionate le librerie che volete nascondere dalla scheda Libreria e dalle sezioni della pagina iniziale.",
"disable_haptic_feedback": "Disabilita il feedback aptico",
"default_quality": "Qualità predefinita"
"default_quality": "Qualità predefinita",
"disabled": "Disabilitato"
},
"downloads": {
"downloads_title": "Scaricamento",
@@ -370,7 +371,9 @@
"audio_tracks": "Tracce audio:",
"playback_state": "Stato della riproduzione:",
"no_data_available": "Nessun dato disponibile",
"index": "Indice:"
"index": "Indice:",
"continue_watching": "Continua a guardare",
"go_back": "Indietro"
},
"item_card": {
"next_up": "Il prossimo",

View File

@@ -152,7 +152,9 @@
"optimized_version_hint": "OptimizeサーバーのURLを入力します。URLにはhttpまたはhttpsを含め、オプションでポートを指定します。",
"read_more_about_optimized_server": "Optimizeサーバーの詳細をご覧ください。",
"url": "URL",
"server_url_placeholder": "http(s)://domain.org:ポート"
"server_url_placeholder": "http(s)://domain.org:ポート",
"default_quality": "デフォルトの品質",
"disabled": "無効"
},
"plugins": {
"plugins_title": "プラグイン",
@@ -369,7 +371,9 @@
"audio_tracks": "音声トラック:",
"playback_state": "再生状態:",
"no_data_available": "データなし",
"index": "インデックス:"
"index": "インデックス:",
"continue_watching": "視聴を続ける",
"go_back": "戻る"
},
"item_card": {
"next_up": "次",

View File

@@ -138,7 +138,8 @@
"hide_libraries": "Verberg Bibliotheken",
"select_liraries_you_want_to_hide": "Selecteer de bibliotheken die je wil verbergen van de Bibliotheektab en hoofdpagina onderdelen.",
"disable_haptic_feedback": "Haptische feedback uitschakelen",
"default_quality": "Standaard kwaliteit"
"default_quality": "Standaard kwaliteit",
"disabled": "Uitgeschakeld"
},
"downloads": {
"downloads_title": "Downloads",
@@ -370,7 +371,9 @@
"audio_tracks": "Audio Tracks:",
"playback_state": "Afspeelstatus:",
"no_data_available": "Geen data beschikbaar",
"index": "Index:"
"index": "Index:",
"continue_watching": "Verder kijken",
"go_back": "Terug"
},
"item_card": {
"next_up": "Volgende",

View File

@@ -138,7 +138,8 @@
"hide_libraries": "Ukryj biblioteki",
"select_liraries_you_want_to_hide": "Wybierz biblioteki, które chcesz ukryć na karcie Biblioteka i w sekcjach strony głównej.",
"disable_haptic_feedback": "Wyłącz wibracje",
"default_quality": "Domyślna jakość"
"default_quality": "Domyślna jakość",
"disabled": "Wyłączone"
},
"downloads": {
"downloads_title": "Pobieranie",
@@ -374,7 +375,9 @@
"audio_tracks": "Ścieżki audio:",
"playback_state": "Stan odtwarzania:",
"no_data_available": "Brak dostępnych danych",
"index": "Indeks:"
"index": "Indeks:",
"continue_watching": "Kontynuuj oglądanie",
"go_back": "Wstecz"
},
"item_card": {
"next_up": "Następne",

View File

@@ -138,7 +138,8 @@
"hide_libraries": "Ocultar bibliotecas",
"select_liraries_you_want_to_hide": "Selecione as bibliotecas que você deseja ocultar das abas Biblioteca e Início.",
"disable_haptic_feedback": "Desativar o feedback háptico",
"default_quality": "Qualidade padrão"
"default_quality": "Qualidade padrão",
"disabled": "Desativado"
},
"downloads": {
"downloads_title": "Downloads",
@@ -371,7 +372,9 @@
"audio_tracks": "Faixas do áudio:",
"playback_state": "Playback State:",
"no_data_available": "Nenhum dado disponível",
"index": "Índice:"
"index": "Índice:",
"continue_watching": "Continuar assistindo",
"go_back": "Voltar"
},
"item_card": {
"next_up": "Próximo em",

View File

@@ -1,478 +1,480 @@
{
"login": {
"username_required": "Имя пользователя обязательно",
"error_title": "Ошибка",
"login_title": "Вход",
"login_to_title": "Вход в",
"username_placeholder": "Имя пользователя",
"password_placeholder": "Пароль",
"login_button": "Войти",
"quick_connect": "Быстрое подключение",
"enter_code_to_login": "Введите код {{code}} чтобы войти",
"failed_to_initiate_quick_connect": "Не удалось инициировать быстрое подключение",
"got_it": "Принято",
"connection_failed": "Соединение не удалось",
"could_not_connect_to_server": "Не удалось подключиться к серверу. Пожалуйста проверьте URL и ваше интернет соединение.",
"an_unexpected_error_occured": "Возникла непредвиденная ошибка",
"change_server": "Поменять сервер",
"invalid_username_or_password": "Неправильное имя пользователя или пароль",
"user_does_not_have_permission_to_log_in": "Пользователь не имеет прав на вход",
"server_is_taking_too_long_to_respond_try_again_later": "Сервер долго не отвечает, попробуйте позже.",
"server_received_too_many_requests_try_again_later": "Сервер получил слишком много запросов, попробуйте позже.",
"there_is_a_server_error": "Возникла ошибка сервера",
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Возникла непредвиденная ошибка. Вы правильно ввели URL?"
"login": {
"username_required": "Имя пользователя обязательно",
"error_title": "Ошибка",
"login_title": "Вход",
"login_to_title": "Вход в",
"username_placeholder": "Имя пользователя",
"password_placeholder": "Пароль",
"login_button": "Войти",
"quick_connect": "Быстрое подключение",
"enter_code_to_login": "Введите код {{code}} чтобы войти",
"failed_to_initiate_quick_connect": "Не удалось инициировать быстрое подключение",
"got_it": "Принято",
"connection_failed": "Соединение не удалось",
"could_not_connect_to_server": "Не удалось подключиться к серверу. Пожалуйста проверьте URL и ваше интернет соединение.",
"an_unexpected_error_occured": "Возникла непредвиденная ошибка",
"change_server": "Поменять сервер",
"invalid_username_or_password": "Неправильное имя пользователя или пароль",
"user_does_not_have_permission_to_log_in": "Пользователь не имеет прав на вход",
"server_is_taking_too_long_to_respond_try_again_later": "Сервер долго не отвечает, попробуйте позже.",
"server_received_too_many_requests_try_again_later": "Сервер получил слишком много запросов, попробуйте позже.",
"there_is_a_server_error": "Возникла ошибка сервера",
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Возникла непредвиденная ошибка. Вы правильно ввели URL?"
},
"server": {
"enter_url_to_jellyfin_server": "Укажите URL на ваш Jellyfin сервер",
"server_url_placeholder": "http(s)://your-server.com",
"connect_button": "Подключиться",
"previous_servers": "предыдущие серверы",
"clear_button": "Очистить",
"search_for_local_servers": "Поиск локальных серверов",
"searching": "Поиск...",
"servers": "Сервера"
},
"home": {
"no_internet": "Нет интернета",
"no_items": "Нет элементов",
"no_internet_message": "Не переживайте, Вы всё ещё можете смотреть\nскачанный контент.",
"go_to_downloads": "В загрузки",
"oops": "Упс!",
"error_message": "Что-то пошло не так.\nПожалуйста выйдите и зайдите снова.",
"continue_watching": "Продолжить просмотр",
"next_up": "Следующее",
"recently_added_in": "Недавно добавлено в {{libraryName}}",
"suggested_movies": "Предложенные фильмы",
"suggested_episodes": "Предложенные серии",
"intro": {
"welcome_to_streamyfin": "Добро пожаловать в Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Бесплатный клиент для Jellyfin с открытым кодом",
"features_title": "Функции",
"features_description": "Streamyfin имеет множество функций и интегрируется с широким спектром программ, которое вы можете найти в меню настроек:",
"jellyseerr_feature_description": "Подключитесь к Jellyseerr и запрашивайте фильмы прямо в приложении.",
"downloads_feature_title": "Загрузки",
"downloads_feature_description": "Скачивайте фильмы и сериалы для просмотра без интернета. Используйте стандартный способ или установите сервер оптимизации для загрузки файлов в фоновом режиме.",
"chromecast_feature_description": "Транслируйте фильмы и сериалы на ваши устройста с поддержкой Chromecast.",
"centralised_settings_plugin_title": "Плагин для централизованной настройки",
"centralised_settings_plugin_description": "Настраивайте параметры из централизованного места на сервере Jellyfin. Все настройки клиента для всех пользователей будут синхронизированы автоматически.",
"done_button": "Готово",
"go_to_settings_button": "Перейти в настройки",
"read_more": "Узнать больше"
},
"server": {
"enter_url_to_jellyfin_server": "Укажите URL на ваш Jellyfin сервер",
"server_url_placeholder": "http(s)://your-server.com",
"connect_button": "Подключиться",
"previous_servers": "предыдущие серверы",
"clear_button": "Очистить",
"search_for_local_servers": "Поиск локальных серверов",
"searching": "Поиск...",
"servers": "Сервера"
},
"home": {
"no_internet": "Нет интернета",
"no_items": "Нет элементов",
"no_internet_message": "Не переживайте, Вы всё ещё можете смотреть\nскачанный контент.",
"go_to_downloads": "В загрузки",
"oops": "Упс!",
"error_message": "Что-то пошло не так.\nПожалуйста выйдите и зайдите снова.",
"continue_watching": "Продолжить просмотр",
"next_up": "Следующее",
"recently_added_in": "Недавно добавлено в {{libraryName}}",
"suggested_movies": "Предложенные фильмы",
"suggested_episodes": "Предложенные серии",
"intro": {
"welcome_to_streamyfin": "Добро пожаловать в Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Бесплатный клиент для Jellyfin с открытым кодом",
"features_title": "Функции",
"features_description": "Streamyfin имеет множество функций и интегрируется с широким спектром программ, которое вы можете найти в меню настроек:",
"jellyseerr_feature_description": "Подключитесь к Jellyseerr и запрашивайте фильмы прямо в приложении.",
"downloads_feature_title": "Загрузки",
"downloads_feature_description": "Скачивайте фильмы и сериалы для просмотра без интернета. Используйте стандартный способ или установите сервер оптимизации для загрузки файлов в фоновом режиме.",
"chromecast_feature_description": "Транслируйте фильмы и сериалы на ваши устройста с поддержкой Chromecast.",
"centralised_settings_plugin_title": "Плагин для централизованной настройки",
"centralised_settings_plugin_description": "Настраивайте параметры из централизованного места на сервере Jellyfin. Все настройки клиента для всех пользователей будут синхронизированы автоматически.",
"done_button": "Готово",
"go_to_settings_button": "Перейти в настройки",
"read_more": "Узнать больше"
"settings": {
"settings_title": "Настройки",
"log_out_button": "Выйти",
"user_info": {
"user_info_title": "Информация о пользователе",
"user": "Пользователь",
"server": "Сервер",
"token": "Токен",
"app_version": "Версия приложения"
},
"settings": {
"settings_title": "Настройки",
"log_out_button": "Выйти",
"user_info": {
"user_info_title": "Информация о пользователе",
"user": "Пользователь",
"server": "Сервер",
"token": "Токен",
"app_version": "Версия приложения"
},
"quick_connect": {
"quick_connect_title": "Быстрое подключение",
"authorize_button": "Авторизировать через быстрое подключение",
"enter_the_quick_connect_code": "Введите код для быстрого подключения...",
"success": "Успех",
"quick_connect_autorized": "Быстрое подключение авторизовано",
"error": "Ошибка",
"invalid_code": "Неверный код",
"authorize": "Авторизировать"
},
"media_controls": {
"media_controls_title": "Медиа-контроль",
"forward_skip_length": "Длина пропуска вперед",
"rewind_length": "Длина перемотки",
"seconds_unit": "c"
},
"audio": {
"audio_title": "Аудио",
"set_audio_track": "Устанавливать аудио дорожку из предыдущего элемента",
"audio_language": "Язык аудио",
"audio_hint": "Выберите стандартный язык аудио.",
"none": "Отсутствует",
"language": "Язык"
},
"subtitles": {
"subtitle_title": "Субтитры",
"subtitle_language": "Язык субтитров",
"subtitle_mode": "Режим субтитров",
"set_subtitle_track": "Устанавливать субтитры из предыдущего элемента",
"subtitle_size": "Размер субтитров",
"subtitle_hint": "Настроить субтитры.",
"none": "Отсутствует",
"language": "Язык",
"loading": "Загрузка",
"modes": {
"Default": "Стандартный",
"Smart": "Умный",
"Always": "Всегда",
"None": "Отсутствует",
"OnlyForced": "Только принудительные"
}
},
"other": {
"other_title": "Другое",
"follow_device_orientation": "Авто-поворот",
"video_orientation": "Ориентация видео",
"orientation": "Ориентация",
"orientations": {
"DEFAULT": "Стандартный",
"ALL": "Все",
"PORTRAIT": "Портретный",
"PORTRAIT_UP": "Портрет вверх",
"PORTRAIT_DOWN": "Портрет вниз",
"LANDSCAPE": "Ландшафтный",
"LANDSCAPE_LEFT": "Ландшафтный слева",
"LANDSCAPE_RIGHT": "Ландшафтный справа",
"OTHER": "Другое",
"UNKNOWN": "Неизвестное"
},
"safe_area_in_controls": "Безопасная зона в элементах управления",
"video_player": "Видео прейер",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Экспериментальный + PiP)"
},
"show_custom_menu_links": "Показать ссылки кастомного меню",
"hide_libraries": "Скрыть библиотеки",
"select_liraries_you_want_to_hide": "Выберите Библиотеки, которое хотите спрятать из вкладки Библиотеки и домашней страницы.",
"disable_haptic_feedback": "Отключить тактильную обратную связь",
"default_quality": "Качество по умолчанию"
},
"downloads": {
"downloads_title": "Загрузки",
"download_method": "способ загрузки",
"remux_max_download": "Remux max скачать",
"auto_download": "Авто-загрузка",
"optimized_versions_server": "Оптимизированные версии сервера",
"save_button": "Сохранить",
"optimized_server": "Оптимизированный сервер",
"optimized": "Оптимизированный",
"default": "По умолчанию",
"optimized_version_hint": "Укажите URL на оптимизированный сервер. URL должен включать http or https и опционально порт.",
"read_more_about_optimized_server": "Узнать больше про оптимизацию сервера.",
"url": "URL",
"server_url_placeholder": "http(s)://domain.org:port"
},
"plugins": {
"plugins_title": "Плагины",
"jellyseerr": {
"jellyseerr_warning": "Эта интеграция находится на ранней стадии. Ожидайте изменений.",
"server_url": "URL сервера",
"server_url_hint": "Пример: http(s)://your-host.url\n(Добавьте порт если необходимо)",
"server_url_placeholder": "Jellyseerr URL...",
"password": "Пароль",
"password_placeholder": "Введите пароль для пользователя Jellyfin {{username}}",
"save_button": "Сохранить",
"clear_button": "Очистить",
"login_button": "Войти",
"total_media_requests": "Всего запросов на медиа",
"movie_quota_limit": "Ограничение квоты на фильмы",
"movie_quota_days": "Дни квоты на фильмы",
"tv_quota_limit": "Ограничение квоты на сериалы",
"tv_quota_days": "Дни квоты на сериалы",
"reset_jellyseerr_config_button": "Сбросить конфигурацию Jellyseerr",
"unlimited": "Неограниченно",
"plus_n_more": "+{{n}} больше",
"order_by": {
"DEFAULT": "По умолчанию",
"VOTE_COUNT_AND_AVERAGE": "Количеству голосов и среднему",
"POPULARITY": "Популярности"
}
},
"marlin_search": {
"enable_marlin_search": "Включить Marlin Search ",
"url": "URL",
"server_url_placeholder": "http(s)://domain.org:port",
"marlin_search_hint": "Введите URL для Marlin сервера. URL должен включать http or https и опционально порт.",
"read_more_about_marlin": "Узнать больше о Marlin.",
"save_button": "Сохранить",
"toasts": {
"saved": "Сохранено"
}
}
},
"storage": {
"storage_title": "Хранилище",
"app_usage": "Приложение {{usedSpace}}%",
"device_usage": "Устройство {{availableSpace}}%",
"size_used": "{{used}} из {{total}} использовано",
"delete_all_downloaded_files": "Удалить все загруженные файлы",
},
"intro": {
"show_intro": "Показать вступление",
"reset_intro": "Сбросить вступление"
},
"logs": {
"logs_title": "Логи",
"no_logs_available": "Логи не доступны",
"delete_all_logs": "Удалить все логи",
},
"languages": {
"title": "Языки",
"app_language": "Язык приложения",
"app_language_description": "Выберите язык для приложения.",
"system": "Системный"
},
"toasts": {
"error_deleting_files": "Ошибка при удалении файлов",
"background_downloads_enabled": "Фоновая загрузка включена",
"background_downloads_disabled": "Фоновая загрузка отключена",
"connected": "Подключено",
"could_not_connect": "Не удалось подключиться",
"invalid_url": "Неверный URL"
"quick_connect": {
"quick_connect_title": "Быстрое подключение",
"authorize_button": "Авторизировать через быстрое подключение",
"enter_the_quick_connect_code": "Введите код для быстрого подключения...",
"success": "Успех",
"quick_connect_autorized": "Быстрое подключение авторизовано",
"error": "Ошибка",
"invalid_code": "Неверный код",
"authorize": "Авторизировать"
},
"media_controls": {
"media_controls_title": "Медиа-контроль",
"forward_skip_length": "Длина пропуска вперед",
"rewind_length": "Длина перемотки",
"seconds_unit": "c"
},
"audio": {
"audio_title": "Аудио",
"set_audio_track": "Устанавливать аудио дорожку из предыдущего элемента",
"audio_language": "Язык аудио",
"audio_hint": "Выберите стандартный язык аудио.",
"none": "Отсутствует",
"language": "Язык"
},
"subtitles": {
"subtitle_title": "Субтитры",
"subtitle_language": "Язык субтитров",
"subtitle_mode": "Режим субтитров",
"set_subtitle_track": "Устанавливать субтитры из предыдущего элемента",
"subtitle_size": "Размер субтитров",
"subtitle_hint": "Настроить субтитры.",
"none": "Отсутствует",
"language": "Язык",
"loading": "Загрузка",
"modes": {
"Default": "Стандартный",
"Smart": "Умный",
"Always": "Всегда",
"None": "Отсутствует",
"OnlyForced": "Только принудительные"
}
},
"sessions": {
"title": "Сессии",
"no_active_sessions": "Нет активных сессий",
"other": {
"other_title": "Другое",
"follow_device_orientation": "Авто-поворот",
"video_orientation": "Ориентация видео",
"orientation": "Ориентация",
"orientations": {
"DEFAULT": "Стандартный",
"ALL": "Все",
"PORTRAIT": "Портретный",
"PORTRAIT_UP": "Портрет вверх",
"PORTRAIT_DOWN": "Портрет вниз",
"LANDSCAPE": "Ландшафтный",
"LANDSCAPE_LEFT": "Ландшафтный слева",
"LANDSCAPE_RIGHT": "Ландшафтный справа",
"OTHER": "Другое",
"UNKNOWN": "Неизвестное"
},
"safe_area_in_controls": "Безопасная зона в элементах управления",
"video_player": "Видео прейер",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Экспериментальный + PiP)"
},
"show_custom_menu_links": "Показать ссылки кастомного меню",
"hide_libraries": "Скрыть библиотеки",
"select_liraries_you_want_to_hide": "Выберите Библиотеки, которое хотите спрятать из вкладки Библиотеки и домашней страницы.",
"disable_haptic_feedback": "Отключить тактильную обратную связь",
"default_quality": "Качество по умолчанию",
"disabled": "Отключено"
},
"downloads": {
"downloads_title": "Загрузки",
"tvseries": "Сериалы",
"movies": "Фильмы",
"queue": "Очередь",
"queue_hint": "Очередь и загрузки будут удалены при перезагрузке приложения",
"no_items_in_queue": "Нет элементов в очереди",
"no_downloaded_items": "Нет загруженых предметов",
"delete_all_movies_button": "Удалить все фильмы",
"delete_all_tvseries_button": "Удалить все сериалы",
"delete_all_button": "Удалить все",
"active_download": "Активно загружается",
"no_active_downloads": "Нет активных загрузок",
"active_downloads": "Активные загрузки",
"new_app_version_requires_re_download": "Новая версия приложения требует повторной загрузки",
"new_app_version_requires_re_download_description": "Новая версия приложения требует повторной загрузки. Пожалуйста удалите всё и попробуйте заново.",
"back": "Назад",
"delete": "Удалить",
"something_went_wrong": "Что-то пошло не так",
"could_not_get_stream_url_from_jellyfin": "Не удалось получить ссылку трансляции из Jellyfin",
"eta": "ETA {{eta}}",
"methods": "Методы",
"toasts": {
"you_are_not_allowed_to_download_files": "Нет разрешения на скачивание файлов.",
"deleted_all_movies_successfully": "Все фильмы были успешно удалены!",
"failed_to_delete_all_movies": "Возникла ошибка при удалении всех фильмов",
"deleted_all_tvseries_successfully": "Все сериалы были успешно удалены!",
"failed_to_delete_all_tvseries": "Возникла ошибка при удалении всех сериалов",
"download_cancelled": "Загрузка отменена",
"could_not_cancel_download": "Не удалось отменить загрузку",
"download_completed": "Загрузка завершена",
"download_started_for": "Загрузка {{item}} началась",
"item_is_ready_to_be_downloaded": "{{item}} готов к загрузке",
"download_stated_for_item": "Загрузка {{item} началась",
"download_failed_for_item": "Загрузка {{item}} провалилась с ошибкой: {{error}}",
"download_completed_for_item": "{{item}} успешно загружен",
"queued_item_for_optimization": "{{item}} поставлен в очередь для оптимизации",
"failed_to_start_download_for_item": "Не удалось начать загрузку {{item}}: {{message}}",
"server_responded_with_status_code": "Сервер ответил со статусом {{statusCode}}",
"no_response_received_from_server": "Нет ответа от сервера",
"error_setting_up_the_request": "Ошибка при создании запроса",
"failed_to_start_download_for_item_unexpected_error": "Не удалось начать загрузку {{item}}: Неожиданная ошибка",
"all_files_folders_and_jobs_deleted_successfully": "Все файлы, папки, и задачи были успешно удалены",
"an_error_occured_while_deleting_files_and_jobs": "Возникла ошибка при удалении файлов и работ",
"go_to_downloads": "В загрузки"
"download_method": "способ загрузки",
"remux_max_download": "Remux max скачать",
"auto_download": "Авто-загрузка",
"optimized_versions_server": "Оптимизированные версии сервера",
"save_button": "Сохранить",
"optimized_server": "Оптимизированный сервер",
"optimized": "Оптимизированный",
"default": "По умолчанию",
"optimized_version_hint": "Укажите URL на оптимизированный сервер. URL должен включать http or https и опционально порт.",
"read_more_about_optimized_server": "Узнать больше про оптимизацию сервера.",
"url": "URL",
"server_url_placeholder": "http(s)://domain.org:port"
},
"plugins": {
"plugins_title": "Плагины",
"jellyseerr": {
"jellyseerr_warning": "Эта интеграция находится на ранней стадии. Ожидайте изменений.",
"server_url": "URL сервера",
"server_url_hint": "Пример: http(s)://your-host.url\n(Добавьте порт если необходимо)",
"server_url_placeholder": "Jellyseerr URL...",
"password": "Пароль",
"password_placeholder": "Введите пароль для пользователя Jellyfin {{username}}",
"save_button": "Сохранить",
"clear_button": "Очистить",
"login_button": "Войти",
"total_media_requests": "Всего запросов на медиа",
"movie_quota_limit": "Ограничение квоты на фильмы",
"movie_quota_days": "Дни квоты на фильмы",
"tv_quota_limit": "Ограничение квоты на сериалы",
"tv_quota_days": "Дни квоты на сериалы",
"reset_jellyseerr_config_button": "Сбросить конфигурацию Jellyseerr",
"unlimited": "Неограниченно",
"plus_n_more": "+{{n}} больше",
"order_by": {
"DEFAULT": "По умолчанию",
"VOTE_COUNT_AND_AVERAGE": "Количеству голосов и среднему",
"POPULARITY": "Популярности"
}
},
"marlin_search": {
"enable_marlin_search": "Включить Marlin Search ",
"url": "URL",
"server_url_placeholder": "http(s)://domain.org:port",
"marlin_search_hint": "Введите URL для Marlin сервера. URL должен включать http or https и опционально порт.",
"read_more_about_marlin": "Узнать больше о Marlin.",
"save_button": "Сохранить",
"toasts": {
"saved": "Сохранено"
}
}
}
},
"search": {
"search_here": "Искать здесь...",
"search": "Поиск...",
"x_items": "{{count}} предметов",
"library": "Библиотека",
"discover": "Найти новое",
"no_results": "Нет результатов",
"no_results_found_for": "Не было результатов при поиске",
"movies": "Фильмы",
"series": "Сериалы",
"episodes": "Серии",
"collections": "Коллекции",
"actors": "Актеры",
"request_movies": "Запросить фильмы",
"request_series": "Запросить сериалы",
"recently_added": "Недавно добавлено",
"recent_requests": "Недавно запрошено",
"plex_watchlist": "Список просмотра с Plex",
"trending": "В тренде",
"popular_movies": "Популярные фильмы",
"movie_genres": "Популярные жанры",
"upcoming_movies": "Предстоящие фильмы",
"studios": "Студии",
"popular_tv": "Популярные сериалы",
"tv_genres": "жанры сериалов",
"upcoming_tv": "Предстоящие сериалы",
"networks": "Сети",
"tmdb_movie_keyword": "TMDB Ключевые слова фильмов",
"tmdb_movie_genre": "TMDB Жанры фильмов",
"tmdb_tv_keyword": "TMDB Ключевые слова сериалов",
"tmdb_tv_genre": "TMDB Жанры сериалов",
"tmdb_search": "TMDB Поиск",
"tmdb_studio": "TMDB Студии",
"tmdb_network": "TMDB Сеть",
"tmdb_movie_streaming_services": "TMDB Потоковые сервисы фильмов",
"tmdb_tv_streaming_services": "TMDB Потоковые сервисы сериалов",
},
"library": {
"no_items_found": "элементы не найдены",
"no_results": "Нет результатов",
"no_libraries_found": "Библиотеки не найдены",
"item_types": {
"movies": "фильмы",
"series": "Сериалы",
"boxsets": "Коллекции",
"items": "элементы"
},
"options": {
"display": "Отображать",
"row": "Ряд",
"list": "Список",
"image_style": "Стиль изображения",
"poster": "Постер",
"cover": "Обложка",
"show_titles": "Показывать загаловки",
"show_stats": "Показывать статистику",
"storage": {
"storage_title": "Хранилище",
"app_usage": "Приложение {{usedSpace}}%",
"device_usage": "Устройство {{availableSpace}}%",
"size_used": "{{used}} из {{total}} использовано",
"delete_all_downloaded_files": "Удалить все загруженные файлы"
},
"intro": {
"show_intro": "Показать вступление",
"reset_intro": "Сбросить вступление"
},
"logs": {
"logs_title": "Логи",
"no_logs_available": "Логи не доступны",
"delete_all_logs": "Удалить все логи"
},
"languages": {
"title": "Языки",
"app_language": "Язык приложения",
"app_language_description": "Выберите язык для приложения.",
"system": "Системный"
},
"filters": {
"genres": "Жанры",
"years": "Года",
"sort_by": "Сортировать по",
"sort_order": "Порядок сортировки",
"asc": "По Возрастанию",
"desc": "По убыванию",
"tags": "Тэги"
}
},
"favorites": {
"series": "Сериалы",
"movies": "Фильмы",
"episodes": "Серии",
"videos": "Видео",
"boxsets": "Коллекции",
"playlists": "Плейлисты",
"noDataTitle": "Пока нет избранных",
"noData": "Отметьте элементы как избранные, чтобы они отображались здесь для быстрого доступа."
},
"custom_links": {
"no_links": "Нет ссылок"
},
"player": {
"error": "Ошибка",
"failed_to_get_stream_url": "Не удалось получить URL потока",
"an_error_occured_while_playing_the_video": "Возникла Неожиданная ошибка во время воспроизведения. Проверьте логи в настройках.",
"client_error": "Ошибка клиента",
"could_not_create_stream_for_chromecast": "Не удалось создать поток для Chromecast",
"message_from_server": "Сообщение от сервера: {{message}}",
"video_has_finished_playing": "Видео закончило воспроизводиться!",
"no_video_source": "Нет источника видео...",
"next_episode": "Следующая серия",
"refresh_tracks": "Обновить дорожки",
"subtitle_tracks": "Субтитры:",
"audio_tracks": "Аудио дорожки:",
"playback_state": "Состояние воспроизведения:",
"no_data_available": "Данные не доступны",
"index": "Индекс:"
},
"item_card": {
"next_up": "Следующее",
"no_items_to_display": "Нет элементов для отображения",
"cast_and_crew": "Актеры и съемочная группа",
"series": "Серии",
"seasons": "Сезоны",
"season": "Сезон",
"no_episodes_for_this_season": "В этом сезоне нет серий",
"overview": "Обзор",
"more_with": "Больше с {{name}}",
"similar_items": "Похожие элементы",
"no_similar_items_found": "Похожие элементы не найдены",
"video": "Видео",
"more_details": "Больше деталей",
"quality": "Качество",
"audio": "Звук",
"subtitles": "Субтитры",
"show_more": "Показать больше",
"show_less": "Показать меньше",
"appeared_in": "Появлялся в",
"could_not_load_item": "Не удалось загрузить элемент",
"none": "Отсутствует",
"download": {
"download_season": "Загрузить сезон",
"download_series": "Загрузить сериал",
"download_episode": "Загрузить серию",
"download_movie": "Скачать фильм",
"download_x_item": "Загрузить {{item_count}} элементов",
"download_button": "Загрузить",
"using_optimized_server": "Использовать оптимизированный сервер",
"using_default_method": "Использовать стандартный метод",
}
},
"live_tv": {
"next": "Следующая",
"previous": "Предыдущая",
"live_tv": "Прямой эфир ТВ",
"coming_soon": "Скоро",
"on_now": "Сейчас в эфире",
"shows": "Сериалы",
"movies": "Фильмы",
"sports": "Спорт",
"for_kids": "Для детей",
"news": "Новости"
},
"jellyseerr": {
"confirm": "Подтвердить",
"cancel": "Отменить",
"yes": "Да",
"whats_wrong": "В чем дело?",
"issue_type": "Вид проблемы",
"select_an_issue": "Выберите проблему",
"types": "Типы",
"describe_the_issue": "(опционально) Опишите проблему...",
"submit_button": "Подать",
"report_issue_button": "Сообщить о проблеме",
"request_button": "Запросить",
"are_you_sure_you_want_to_request_all_seasons": "Вы уверены, что хотите запросить все сезоны?",
"failed_to_login": "Не удалось войти",
"cast": "Транслировать",
"details": "Детали",
"status": "Статус",
"original_title": "Оригинальное название",
"series_type": "Тип сериала",
"release_dates": "Дата релиза",
"first_air_date": "Первая дата выхода в эфир",
"next_air_date": "Следующая дата выхода в эфир",
"revenue": "Прибыль",
"budget": "Бюджет",
"original_language": "Оригинальный язык",
"production_country": "Страна производства",
"studios": "Студия",
"network": "Сеть",
"currently_streaming_on": "Сейчас доступно на",
"advanced": "Продвинутое",
"request_as": "Запросить как",
"tags": "Тэги",
"quality_profile": "Профиль качества",
"root_folder": "Корневая папка",
"season_all": "Сезон (все)",
"season_number": "Сезон {{season_number}}",
"number_episodes": "{{episode_number}} серий",
"born": "Рожден",
"appearances": "Появления",
"toasts": {
"jellyseer_does_not_meet_requirements": "Сервер Jellyseerr не соответствует минимальным требованиям версии! Пожалуйста, обновите до версии не ниже 2.0.0",
"jellyseerr_test_failed": "Тест Jellyseerr не пройден. Попробуйте еще раз.",
"failed_to_test_jellyseerr_server_url": "Не удалось проверить URL-адрес сервера jellyseerr",
"issue_submitted": "Проблема отправлена!",
"requested_item": "Запрошено {{item}}!",
"you_dont_have_permission_to_request": "У вас нет разрешения на запрос!",
"something_went_wrong_requesting_media": "Что-то пошло не так при запросе медиафайлов!"
"error_deleting_files": "Ошибка при удалении файлов",
"background_downloads_enabled": "Фоновая загрузка включена",
"background_downloads_disabled": "Фоновая загрузка отключена",
"connected": "Подключено",
"could_not_connect": "Не удалось подключиться",
"invalid_url": "Неверный URL"
}
},
"tabs": {
"home": "Дом",
"search": "Поиск",
"library": "Библиотека",
"custom_links": "Кастомные ссылки",
"favorites": "Избранное"
"sessions": {
"title": "Сессии",
"no_active_sessions": "Нет активных сессий"
},
"downloads": {
"downloads_title": "Загрузки",
"tvseries": "Сериалы",
"movies": "Фильмы",
"queue": "Очередь",
"queue_hint": "Очередь и загрузки будут удалены при перезагрузке приложения",
"no_items_in_queue": "Нет элементов в очереди",
"no_downloaded_items": "Нет загруженых предметов",
"delete_all_movies_button": "Удалить все фильмы",
"delete_all_tvseries_button": "Удалить все сериалы",
"delete_all_button": "Удалить все",
"active_download": "Активно загружается",
"no_active_downloads": "Нет активных загрузок",
"active_downloads": "Активные загрузки",
"new_app_version_requires_re_download": "Новая версия приложения требует повторной загрузки",
"new_app_version_requires_re_download_description": "Новая версия приложения требует повторной загрузки. Пожалуйста удалите всё и попробуйте заново.",
"back": "Назад",
"delete": "Удалить",
"something_went_wrong": "Что-то пошло не так",
"could_not_get_stream_url_from_jellyfin": "Не удалось получить ссылку трансляции из Jellyfin",
"eta": "ETA {{eta}}",
"methods": "Методы",
"toasts": {
"you_are_not_allowed_to_download_files": "Нет разрешения на скачивание файлов.",
"deleted_all_movies_successfully": "Все фильмы были успешно удалены!",
"failed_to_delete_all_movies": "Возникла ошибка при удалении всех фильмов",
"deleted_all_tvseries_successfully": "Все сериалы были успешно удалены!",
"failed_to_delete_all_tvseries": "Возникла ошибка при удалении всех сериалов",
"download_cancelled": "Загрузка отменена",
"could_not_cancel_download": "Не удалось отменить загрузку",
"download_completed": "Загрузка завершена",
"download_started_for": "Загрузка {{item}} началась",
"item_is_ready_to_be_downloaded": "{{item}} готов к загрузке",
"download_stated_for_item": "Загрузка {{item} началась",
"download_failed_for_item": "Загрузка {{item}} провалилась с ошибкой: {{error}}",
"download_completed_for_item": "{{item}} успешно загружен",
"queued_item_for_optimization": "{{item}} поставлен в очередь для оптимизации",
"failed_to_start_download_for_item": "Не удалось начать загрузку {{item}}: {{message}}",
"server_responded_with_status_code": "Сервер ответил со статусом {{statusCode}}",
"no_response_received_from_server": "Нет ответа от сервера",
"error_setting_up_the_request": "Ошибка при создании запроса",
"failed_to_start_download_for_item_unexpected_error": "Не удалось начать загрузку {{item}}: Неожиданная ошибка",
"all_files_folders_and_jobs_deleted_successfully": "Все файлы, папки, и задачи были успешно удалены",
"an_error_occured_while_deleting_files_and_jobs": "Возникла ошибка при удалении файлов и работ",
"go_to_downloads": "В загрузки"
}
}
},
"search": {
"search_here": "Искать здесь...",
"search": "Поиск...",
"x_items": "{{count}} предметов",
"library": "Библиотека",
"discover": "Найти новое",
"no_results": "Нет результатов",
"no_results_found_for": "Не было результатов при поиске",
"movies": "Фильмы",
"series": "Сериалы",
"episodes": "Серии",
"collections": "Коллекции",
"actors": "Актеры",
"request_movies": "Запросить фильмы",
"request_series": "Запросить сериалы",
"recently_added": "Недавно добавлено",
"recent_requests": "Недавно запрошено",
"plex_watchlist": "Список просмотра с Plex",
"trending": "В тренде",
"popular_movies": "Популярные фильмы",
"movie_genres": "Популярные жанры",
"upcoming_movies": "Предстоящие фильмы",
"studios": "Студии",
"popular_tv": "Популярные сериалы",
"tv_genres": "жанры сериалов",
"upcoming_tv": "Предстоящие сериалы",
"networks": "Сети",
"tmdb_movie_keyword": "TMDB Ключевые слова фильмов",
"tmdb_movie_genre": "TMDB Жанры фильмов",
"tmdb_tv_keyword": "TMDB Ключевые слова сериалов",
"tmdb_tv_genre": "TMDB Жанры сериалов",
"tmdb_search": "TMDB Поиск",
"tmdb_studio": "TMDB Студии",
"tmdb_network": "TMDB Сеть",
"tmdb_movie_streaming_services": "TMDB Потоковые сервисы фильмов",
"tmdb_tv_streaming_services": "TMDB Потоковые сервисы сериалов"
},
"library": {
"no_items_found": "элементы не найдены",
"no_results": "Нет результатов",
"no_libraries_found": "Библиотеки не найдены",
"item_types": {
"movies": "фильмы",
"series": "Сериалы",
"boxsets": "Коллекции",
"items": "элементы"
},
"options": {
"display": "Отображать",
"row": "Ряд",
"list": "Список",
"image_style": "Стиль изображения",
"poster": "Постер",
"cover": "Обложка",
"show_titles": "Показывать загаловки",
"show_stats": "Показывать статистику"
},
"filters": {
"genres": "Жанры",
"years": "Года",
"sort_by": "Сортировать по",
"sort_order": "Порядок сортировки",
"asc": "По Возрастанию",
"desc": "По убыванию",
"tags": "Тэги"
}
},
"favorites": {
"series": "Сериалы",
"movies": "Фильмы",
"episodes": "Серии",
"videos": "Видео",
"boxsets": "Коллекции",
"playlists": "Плейлисты",
"noDataTitle": "Пока нет избранных",
"noData": "Отметьте элементы как избранные, чтобы они отображались здесь для быстрого доступа."
},
"custom_links": {
"no_links": "Нет ссылок"
},
"player": {
"error": "Ошибка",
"failed_to_get_stream_url": "Не удалось получить URL потока",
"an_error_occured_while_playing_the_video": "Возникла Неожиданная ошибка во время воспроизведения. Проверьте логи в настройках.",
"client_error": "Ошибка клиента",
"could_not_create_stream_for_chromecast": "Не удалось создать поток для Chromecast",
"message_from_server": "Сообщение от сервера: {{message}}",
"video_has_finished_playing": "Видео закончило воспроизводиться!",
"no_video_source": "Нет источника видео...",
"next_episode": "Следующая серия",
"refresh_tracks": "Обновить дорожки",
"subtitle_tracks": "Субтитры:",
"audio_tracks": "Аудио дорожки:",
"playback_state": "Состояние воспроизведения:",
"no_data_available": "Данные не доступны",
"index": "Индекс:",
"continue_watching": "Продолжить просмотр",
"go_back": "Назад"
},
"item_card": {
"next_up": "Следующее",
"no_items_to_display": "Нет элементов для отображения",
"cast_and_crew": "Актеры и съемочная группа",
"series": "Серии",
"seasons": "Сезоны",
"season": "Сезон",
"no_episodes_for_this_season": "В этом сезоне нет серий",
"overview": "Обзор",
"more_with": "Больше с {{name}}",
"similar_items": "Похожие элементы",
"no_similar_items_found": "Похожие элементы не найдены",
"video": "Видео",
"more_details": "Больше деталей",
"quality": "Качество",
"audio": "Звук",
"subtitles": "Субтитры",
"show_more": "Показать больше",
"show_less": "Показать меньше",
"appeared_in": "Появлялся в",
"could_not_load_item": "Не удалось загрузить элемент",
"none": "Отсутствует",
"download": {
"download_season": "Загрузить сезон",
"download_series": "Загрузить сериал",
"download_episode": "Загрузить серию",
"download_movie": "Скачать фильм",
"download_x_item": "Загрузить {{item_count}} элементов",
"download_button": "Загрузить",
"using_optimized_server": "Использовать оптимизированный сервер",
"using_default_method": "Использовать стандартный метод"
}
},
"live_tv": {
"next": "Следующая",
"previous": "Предыдущая",
"live_tv": "Прямой эфир ТВ",
"coming_soon": "Скоро",
"on_now": "Сейчас в эфире",
"shows": "Сериалы",
"movies": "Фильмы",
"sports": "Спорт",
"for_kids": "Для детей",
"news": "Новости"
},
"jellyseerr": {
"confirm": "Подтвердить",
"cancel": "Отменить",
"yes": "Да",
"whats_wrong": "В чем дело?",
"issue_type": "Вид проблемы",
"select_an_issue": "Выберите проблему",
"types": "Типы",
"describe_the_issue": "(опционально) Опишите проблему...",
"submit_button": "Подать",
"report_issue_button": "Сообщить о проблеме",
"request_button": "Запросить",
"are_you_sure_you_want_to_request_all_seasons": "Вы уверены, что хотите запросить все сезоны?",
"failed_to_login": "Не удалось войти",
"cast": "Транслировать",
"details": "Детали",
"status": "Статус",
"original_title": "Оригинальное название",
"series_type": "Тип сериала",
"release_dates": "Дата релиза",
"first_air_date": "Первая дата выхода в эфир",
"next_air_date": "Следующая дата выхода в эфир",
"revenue": "Прибыль",
"budget": "Бюджет",
"original_language": "Оригинальный язык",
"production_country": "Страна производства",
"studios": "Студия",
"network": "Сеть",
"currently_streaming_on": "Сейчас доступно на",
"advanced": "Продвинутое",
"request_as": "Запросить как",
"tags": "Тэги",
"quality_profile": "Профиль качества",
"root_folder": "Корневая папка",
"season_all": "Сезон (все)",
"season_number": "Сезон {{season_number}}",
"number_episodes": "{{episode_number}} серий",
"born": "Рожден",
"appearances": "Появления",
"toasts": {
"jellyseer_does_not_meet_requirements": "Сервер Jellyseerr не соответствует минимальным требованиям версии! Пожалуйста, обновите до версии не ниже 2.0.0",
"jellyseerr_test_failed": "Тест Jellyseerr не пройден. Попробуйте еще раз.",
"failed_to_test_jellyseerr_server_url": "Не удалось проверить URL-адрес сервера jellyseerr",
"issue_submitted": "Проблема отправлена!",
"requested_item": "Запрошено {{item}}!",
"you_dont_have_permission_to_request": "У вас нет разрешения на запрос!",
"something_went_wrong_requesting_media": "Что-то пошло не так при запросе медиафайлов!"
}
},
"tabs": {
"home": "Дом",
"search": "Поиск",
"library": "Библиотека",
"custom_links": "Кастомные ссылки",
"favorites": "Избранное"
}
}

View File

@@ -30,5 +30,24 @@
"home": "Hem",
"search": "Sök",
"library": "Bibliotek"
},
"player": {
"error": "Fel",
"failed_to_get_stream_url": "Kunde inte hämta stream-URL",
"an_error_occured_while_playing_the_video": "Ett fel uppstod vid uppspelning av videon. Kontrollera loggarna i inställningarna.",
"client_error": "Klientfel",
"could_not_create_stream_for_chromecast": "Kunde inte skapa stream för Chromecast",
"message_from_server": "Meddelande från servern: {{message}}",
"video_has_finished_playing": "Videon har spelat klart!",
"no_video_source": "Ingen videokälla...",
"next_episode": "Nästa avsnitt",
"refresh_tracks": "Uppdatera spår",
"subtitle_tracks": "Textspår:",
"audio_tracks": "Ljudspår:",
"playback_state": "Uppspelningsstatus:",
"no_data_available": "Inga data tillgängliga",
"index": "Index:",
"continue_watching": "Fortsätt titta",
"go_back": "Tillbaka"
}
}

480
translations/tlh.json Normal file
View File

@@ -0,0 +1,480 @@
{
"login": {
"username_required": "tlhIngan DaneH",
"error_title": "ghIq",
"login_title": "lut 'el",
"login_to_title": "lut 'el",
"username_placeholder": "tlhIngan",
"password_placeholder": "ngoq De'",
"login_button": "yI'el!",
"quick_connect": "parmaq ngoQ",
"enter_code_to_login": "yI'elDI' De' {{code}} yIlaD",
"failed_to_initiate_quick_connect": "parmaq ngoQ yIchu'laHbe'",
"got_it": "jIyaj",
"connection_failed": "ngoQlaHbe'",
"could_not_connect_to_server": "SeHlaw veS Ho'Do'laHbe'. URL 'ej ret ghun mej.",
"an_unexpected_error_occured": "num ghIq Doch",
"change_server": "Ho'Do' veS yIghoS",
"invalid_username_or_password": "tlhIngan pagh ngoq De' law'be'",
"user_does_not_have_permission_to_log_in": "tlhIngan lut 'el je'laHbe'",
"server_is_taking_too_long_to_respond_try_again_later": "Ho'Do' veS jachrup. pItlh yIHaD.",
"server_received_too_many_requests_try_again_later": "Ho'Do' veS lutlh ngeb petlh law'. pItlh yIHaD.",
"there_is_a_server_error": "Ho'Do' veS ghIq maS",
"an_unexpected_error_occured_did_you_enter_the_correct_url": "num ghIq Doch. URL mej Danej'a'?"
},
"server": {
"enter_url_to_jellyfin_server": "Jellyfin Ho'Do' veS URL yI'el",
"server_url_placeholder": "http(s)://HoDo-veS.com",
"connect_button": "yIngoq!",
"previous_servers": "namen Ho'Do' veS",
"clear_button": "yIQaw'",
"search_for_local_servers": "val Ho'Do' veS yISam",
"searching": "Sam...",
"servers": "Ho'Do' veS"
},
"home": {
"no_internet": "ret pagh",
"no_items": "Doch pagh",
"no_internet_message": "QublaHbe'.\nDoch Qaw'laHnIS SoH.",
"go_to_downloads": "Qaw' Doch yIghoS",
"oops": "QI'ya!",
"error_message": "Doch rurbe'.\nyIQo' 'ej yI'elqa'.",
"continue_watching": "tlhol yIHaDqa'",
"next_up": "wej",
"recently_added_in": "num tu'lu' {{libraryName}}",
"suggested_movies": "rutlh DIS",
"suggested_episodes": "rutlh Hem",
"intro": {
"welcome_to_streamyfin": "Streamyfin yI'el!",
"a_free_and_open_source_client_for_jellyfin": "Jellyfin lut 'el je'be' 'ej wang.",
"features_title": "mIw",
"features_description": "Streamyfin mIw law' tu'. men menuDaq yISam:",
"jellyseerr_feature_description": "Jellyseerr yIngoq 'ej DIS pe'vIl yISov.",
"downloads_feature_title": "Qaw' Doch",
"downloads_feature_description": "DIS 'ej Hem Qaw'laH. Qaw' mIw tu'lu'.",
"chromecast_feature_description": "DIS 'ej Hem Chromecast vI' ghoS.",
"centralised_settings_plugin_title": "wa'DIch men mIw",
"centralised_settings_plugin_description": "Jellyfin Ho'Do' veSDaq men yISeH. tlhIngan chIch.",
"done_button": "Qapla'",
"go_to_settings_button": "men yIghoS",
"read_more": "yIlaDqa'"
},
"settings": {
"settings_title": "men",
"log_out_button": "yIQo'",
"user_info": {
"user_info_title": "tlhIngan De'",
"user": "tlhIngan",
"server": "Ho'Do' veS",
"token": "per De'",
"app_version": "ghun wej",
},
"quick_connect": {
"quick_connect_title": "parmaq ngoQ",
"authorize_button": "parmaq ngoQ yIje'",
"enter_the_quick_connect_code": "parmaq ngoQ De' yI'el...",
"success": "Qapla'",
"quick_connect_autorized": "parmaq ngoQ je'laH",
"error": "ghIq",
"invalid_code": "De' law'be'",
"authorize": "yIje'"
},
"media_controls": {
"media_controls_title": "tlhol SeHlaw",
"forward_skip_length": "Du'Hom vum",
"rewind_length": "bavHom vum",
"seconds_unit": "tera' rep"
},
"audio": {
"audio_title": "QoQ",
"set_audio_track": "namen Doch QoQ ret yISeH",
"audio_language": "QoQ Hol",
"audio_hint": "QoQ Hol wa' yIwIv.",
"none": "pagh",
"language": "Hol"
},
"subtitles": {
"subtitle_title": "De' chu'",
"subtitle_language": "De' chu' Hol",
"subtitle_mode": "De' chu' mIw",
"set_subtitle_track": "namen Doch De' chu' ret yISeH",
"subtitle_size": "De' chu' qIt",
"subtitle_hint": "De' chu' wIvlaw' yISeH.",
"none": "pagh",
"language": "Hol",
"loading": "tlha'... ",
"modes": {
"Default": "wa'",
"Smart": "SonchIy",
"Always": "reH",
"None": "pagh",
"OnlyForced": "Dun je'"
}
},
"other": {
"other_title": "patlh",
"follow_device_orientation": "naDevvo' pegh",
"video_orientation": "mu'tlhegh pegh",
"orientation": "pegh",
"orientations": {
"DEFAULT": "wa'",
"ALL": "Hoch",
"PORTRAIT": "leng ret",
"PORTRAIT_UP": "leng ret Dung",
"PORTRAIT_DOWN": "leng ret nuq",
"LANDSCAPE": "leng yot",
"LANDSCAPE_LEFT": "leng yot poS",
"LANDSCAPE_RIGHT": "leng yot nIH",
"OTHER": "patlh",
"UNKNOWN": "Sovbe'"
},
"safe_area_in_controls": "SeHlawDaq yot QIH",
"video_player": "mu'tlhegh tlholwI'",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (PiP mIwHa')"
},
"show_custom_menu_links": "menuDaq ret teqlu' yInej",
"hide_libraries": "De'wI' bom yIQIj",
"select_liraries_you_want_to_hide": "De'wI' bom Danej QIj yIwIv.",
"disable_haptic_feedback": "Qub quvHa' yIQIj",
"default_quality": "wa' luj"
},
"downloads": {
"downloads_title": "Qaw' Doch",
"download_method": "Qaw' mIw",
"remux_max_download": "Remux Qaw' Dun",
"auto_download": "chIch Qaw'",
"optimized_versions_server": "luj wej Ho'Do' veS",
"save_button": "yIqIp",
"optimized_server": "luj Ho'Do' veS",
"optimized": "luj",
"default": "wa'",
"optimized_version_hint": "luj Ho'Do' veS URL yI'el.",
"read_more_about_optimized_server": "luj Ho'Do' veS latlh yIlaD",
"url": "URL",
"server_url_placeholder": "http(s)://domajn.org:pord"
},
"plugins": {
"plugins_title": "mIwHom",
"jellyseerr": {
"jellyseerr_warning": "mIwHomvam chu'. ghoSlaH.",
"server_url": "Ho'Do' veS URL",
"server_url_hint": "ghu': http(s)://HoDo-veS.url\n(pord yIbel)",
"server_url_placeholder": "Jellyseerr URL...",
"password": "ngoq De'",
"password_placeholder": "tlhIngan {{username}} ngoq De' yI'el",
"save_button": "yIqIp",
"clear_button": "yIQaw'",
"login_button": "yI'el!",
"total_media_requests": "Hoch tlhol petlh",
"movie_quota_limit": "DIS petlh Dun",
"movie_quota_days": "DIS petlh jaj",
"tv_quota_limit": "TV petlh Dun",
"tv_quota_days": "TV petlh jaj",
"reset_jellyseerr_config_button": "Jellyseerr men yIQaw'qa'",
"unlimited": "Dun pagh",
"plus_n_more": "+{{n}} latlh",
"order_by": {
"DEFAULT": "wa'",
"VOTE_COUNT_AND_AVERAGE": "nem chIm 'ej mev",
"POPULARITY": "ruch"
}
},
"marlin_search": {
"enable_marlin_search": "Marlin Sam yIchu'",
"url": "URL",
"server_url_placeholder": "http(s)://domajn.org:pord",
"marlin_search_hint": "Marlin Ho'Do' veS URL yI'el.",
"read_more_about_marlin": "Marlin latlh yIlaD",
"save_button": "yIqIp",
"toasts": {
"saved": "qIp"
}
}
},
"storage": {
"storage_title": "ram",
"app_usage": "ghun {{usedSpace}}%",
"device_usage": "naDev {{availableSpace}}%",
"size_used": "{{used}} / {{total}} ram",
"delete_all_downloaded_files": "Hoch Qaw' Doch yIQaw'",
},
"intro": {
"show_intro": "chu' Doch yIHoch",
"reset_intro": "chu' Doch yIQaw'qa'"
},
"logs": {
"logs_title": "De' qon",
"export_logs": "De' qon yISamqa'",
"click_for_more_info": "latlh De' yIchIch",
"level": "quv",
"no_logs_available": "De' qon pagh",
"delete_all_logs": "Hoch De' qon yIQaw'"
},
"languages": {
"title": "Holmey",
"app_language": "ghun Hol",
"app_language_description": "ghun Hol yIwIv.",
"system": "mIw'a'"
},
"toasts": {
"error_deleting_files": "Qaw' ghIq",
"background_downloads_enabled": "tlhegh Qaw' chu'",
"background_downloads_disabled": "tlhegh Qaw' QIj",
"connected": "ngoQ",
"could_not_connect": "ngoQlaHbe'",
"invalid_url": "URL law'be'"
}
},
"sessions": {
"title": "tlholrap",
"no_active_sessions": "tlholrap pagh chu'"
},
"downloads": {
"downloads_title": "Qaw' Doch",
"tvseries": "TV Hem",
"movies": "DIS",
"queue": "ghom",
"queue_hint": "ghun ghImDI' ghom Qaw'laH.",
"no_items_in_queue": "ghom Doch pagh",
"no_downloaded_items": "Qaw' Doch pagh",
"delete_all_movies_button": "Hoch DIS yIQaw'",
"delete_all_tvseries_button": "Hoch TV Hem yIQaw'",
"delete_all_button": "Hoch yIQaw'",
"active_download": "chu' Qaw'",
"no_active_downloads": "chu' Qaw' pagh",
"active_downloads": "chu' Qaw'",
"new_app_version_requires_re_download": "ghun wej chu' Qaw'qa' DaneH",
"new_app_version_requires_re_download_description": "wej chu' Doch Qaw'qa' DaneH. Hoch Qaw' Doch yIQaw' 'ej yIHaDqa'.",
"back": "yIbav",
"delete": "yIQaw'",
"something_went_wrong": "Doch rurbe'",
"could_not_get_stream_url_from_jellyfin": "Jellyfin tlhol ret URL tu'laHbe'",
"eta": "ETA {{eta}}",
"methods": "mIw",
"toasts": {
"you_are_not_allowed_to_download_files": "Doch Qaw' je'laHbe'.",
"deleted_all_movies_successfully": "Hoch DIS Qaw' Qapla'!",
"failed_to_delete_all_movies": "Hoch DIS Qaw'laHbe'",
"deleted_all_tvseries_successfully": "Hoch TV Hem Qaw' Qapla'!",
"failed_to_delete_all_tvseries": "Hoch TV Hem Qaw'laHbe'",
"download_cancelled": "Qaw' ghIm",
"could_not_cancel_download": "Qaw' ghImlaHbe'",
"download_completed": "Qaw' Qapla'",
"download_started_for": "{{item}} Qaw' vIlchu'",
"item_is_ready_to_be_downloaded": "{{item}} Qaw'laHnIS",
"download_stated_for_item": "{{item}} Qaw' vIlchu'",
"download_failed_for_item": "{{item}} Qaw'laHbe' - {{error}}",
"download_completed_for_item": "{{item}} Qaw' Qapla'",
"queued_item_for_optimization": "{{item}} luj ghom",
"failed_to_start_download_for_item": "{{item}} Qaw' vIlchu'laHbe': {{message}}",
"server_responded_with_status_code": "Ho'Do' veS jachrup {{statusCode}}",
"no_response_received_from_server": "Ho'Do' veS jachbe'",
"error_setting_up_the_request": "petlh SeH ghIq",
"failed_to_start_download_for_item_unexpected_error": "{{item}} Qaw' vIlchu'laHbe': num ghIq",
"all_files_folders_and_jobs_deleted_successfully": "Hoch De', ram 'ej vum Qaw' Qapla'",
"an_error_occured_while_deleting_files_and_jobs": "De', ram 'ej vum Qaw'DI' ghIq",
"go_to_downloads": "Qaw' Doch yIghoS"
}
}
},
"search": {
"search_here": "DaH yISam...",
"search": "yISam...",
"x_items": "{{count}} Doch",
"library": "De'wI' bom",
"discover": "yISamqa'",
"no_results": "Doch pagh tu'",
"no_results_found_for": "Doch pagh tu' <...>",
"movies": "DIS",
"series": "Hem",
"episodes": "HemHom",
"collections": "ghom",
"actors": "tlholwI'",
"request_movies": "DIS yIpetlh",
"request_series": "Hem yIpetlh",
"recently_added": "num tu'",
"recent_requests": "num petlh",
"plex_watchlist": "Plex tlhol ghom",
"trending": "chu' ruch",
"popular_movies": "ruch DIS",
"movie_genres": "DIS qorDu'",
"upcoming_movies": "DIS wej",
"studios": "DIS qonwI'",
"popular_tv": "ruch TV",
"tv_genres": "TV qorDu'",
"upcoming_tv": "TV wej",
"networks": "ret",
"tmdb_movie_keyword": "TMDB DIS De'",
"tmdb_movie_genre": "TMDB DIS qorDu'",
"tmdb_tv_keyword": "TMDB TV De'",
"tmdb_tv_genre": "TMDB TV qorDu'",
"tmdb_search": "TMDB Sam",
"tmdb_studio": "TMDB qonwI'",
"tmdb_network": "TMDB ret",
"tmdb_movie_streaming_services": "TMDB DIS tlhol mIw",
"tmdb_tv_streaming_services": "TMDB TV tlhol mIw"
},
"library": {
"no_items_found": "Doch pagh tu'",
"no_results": "Doch pagh tu'",
"no_libraries_found": "De'wI' bom pagh tu'",
"item_types": {
"movies": "DIS",
"series": "Hem",
"boxsets": "Hem ghom",
"items": "Doch"
},
"options": {
"display": "yIHoch",
"row": "ret",
"list": "ghom",
"image_style": "nagh bep",
"poster": "nagh",
"cover": "nagh chop",
"show_titles": "pab HoS yIHoch",
"show_stats": "chIm De' yIHoch"
},
"filters": {
"genres": "qorDu'",
"years": "DIS",
"sort_by": "yIwIv",
"sort_order": "wIv mIw",
"asc": "Dung",
"desc": "nuq",
"tags": "De'Hom"
}
},
"favorites": {
"series": "Hem",
"movies": "DIS",
"episodes": "HemHom",
"videos": "mu'tlhegh",
"boxsets": "Hem ghom",
"playlists": "bom ghom",
"noDataTitle": "wIv Doch pagh",
"noData": "Doch wIv DaneH. DaH tu'laH."
},
"custom_links": {
"no_links": "ret pagh"
},
"player": {
"error": "ghIq",
"failed_to_get_stream_url": "tlhol ret URL tu'laHbe'",
"an_error_occured_while_playing_the_video": "mu'tlhegh tlholDI' ghIq. menDaq De' qon mej.",
"client_error": "lut 'el ghIq",
"could_not_create_stream_for_chromecast": "Chromecast tlhol ret qonlaHbe'",
"message_from_server": "Ho'Do' veS jach: {{message}}",
"video_has_finished_playing": "mu'tlhegh tlhol Qapla'!",
"no_video_source": "mu'tlhegh wang pagh",
"next_episode": "wej HemHom",
"refresh_tracks": "ret yIchu'qa'",
"subtitle_tracks": "De' chu' ret:",
"audio_tracks": "QoQ ret:",
"playback_state": "tlhol mIw:",
"no_data_available": "De' pagh tu'",
"index": "nem:"
},
"item_card": {
"next_up": "wej",
"no_items_to_display": "Doch pagh HochlaH",
"cast_and_crew": "tlholwI' 'ej qonwI'",
"series": "Hem",
"seasons": "muv",
"season": "muv",
"no_episodes_for_this_season": "muvvam HemHom pagh",
"overview": "Hoch Sov",
"more_with": "{{name}} latlh",
"similar_items": "Doch rur",
"no_similar_items_found": "Doch rur pagh tu'",
"video": "mu'tlhegh",
"more_details": "latlh De'",
"quality": "luj",
"audio": "QoQ",
"subtitles": "De' chu'",
"show_more": "latlh yIHoch",
"show_less": "Hom yIHoch",
"appeared_in": "tlholvam",
"could_not_load_item": "Doch tlha'laHbe'",
"none": "pagh",
"download": {
"download_season": "muv yIQaw'",
"download_series": "Hem yIQaw'",
"download_episode": "HemHom yIQaw'",
"download_movie": "DIS yIQaw'",
"download_x_item": "{{item_count}} Doch yIQaw'",
"download_button": "yIQaw'",
"using_optimized_server": "luj Ho'Do' veS tu'lu'",
"using_default_method": "wa' mIw tu'lu'"
}
},
"live_tv": {
"next": "wej",
"previous": "namen",
"live_tv": "chu' TV",
"coming_soon": "wej lup",
"on_now": "DaH",
"shows": "tlhol",
"movies": "DIS",
"sports": "QI'",
"for_kids": "puqbeq",
"news": "De'"
},
"jellyseerr": {
"confirm": "yInej",
"cancel": "yIQo'",
"yes": "HIja'",
"whats_wrong": "Doch rurbe' 'Iv?",
"issue_type": "ghIq bep",
"select_an_issue": "ghIq yIwIv",
"types": "bep",
"describe_the_issue": "(num) ghIq yIqon...",
"submit_button": "yInejqa'",
"report_issue_button": "ghIq yIqon",
"request_button": "yIpetlh",
"are_you_sure_you_want_to_request_all_seasons": "Hoch muv Danej petlh'a'?",
"failed_to_login": "'ellaHbe'",
"cast": "tlholwI'",
"details": "De'",
"status": "mIw",
"original_title": "wa'DIch pab HoS",
"series_type": "Hem bep",
"release_dates": "Qaw' jaj",
"first_air_date": "wa'DIch tlhol jaj",
"next_air_date": "wej tlhol jaj",
"revenue": "boj De'",
"budget": "boj nem",
"original_language": "wa'DIch Hol",
"production_country": "qonwI' qo'",
"studios": "qonwI'",
"network": "ret",
"currently_streaming_on": "DaH tlhol <...>",
"advanced": "SonchIy",
"request_as": "yIpetlh <...>",
"tags": "De'Hom",
"quality_profile": "luj wIvlaw'",
"root_folder": "wa'DIch ram",
"season_all": "muv (Hoch)",
"season_number": "muv {{season_number}}",
"number_episodes": "{{episode_number}} HemHom",
"born": "poS",
"appearances": "tlholvam",
"toasts": {
"jellyseer_does_not_meet_requirements": "Jellyseerr Ho'Do' veS wej law'be'! 2.0.0 yIchu'!",
"jellyseerr_test_failed": "Jellyseerr nejlaHbe'. yIHaDqa'.",
"failed_to_test_jellyseerr_server_url": "Jellyseerr Ho'Do' veS URL nejlaHbe'",
"issue_submitted": "ghIq nejqa'!",
"requested_item": "{{item}} petlh!",
"you_dont_have_permission_to_request": "petlh je'laHbe'!",
"something_went_wrong_requesting_media": "tlhol petlhDI' Doch rurbe'!"
}
},
"tabs": {
"home": "juH",
"search": "Sam",
"library": "De'wI' bom",
"custom_links": "teqlu' ret",
"favorites": "wIv Doch"
}
}

View File

@@ -137,7 +137,9 @@
"show_custom_menu_links": "Özel Menü Bağlantılarını Göster",
"hide_libraries": "Kütüphaneleri Gizle",
"select_liraries_you_want_to_hide": "Kütüphane sekmesinden ve ana sayfa bölümlerinden gizlemek istediğiniz kütüphaneleri seçin.",
"disable_haptic_feedback": "Dokunsal Geri Bildirimi Devre Dışı Bırak"
"disable_haptic_feedback": "Dokunsal Geri Bildirimi Devre Dışı Bırak",
"default_quality": "Varsayılan kalite",
"disabled": "Devre dışı"
},
"downloads": {
"downloads_title": "İndirmeler",
@@ -369,7 +371,9 @@
"audio_tracks": "Ses Parçaları:",
"playback_state": "Oynatma Durumu:",
"no_data_available": "Veri bulunamadı",
"index": "İndeks:"
"index": "İndeks:",
"continue_watching": "İzlemeye devam et",
"go_back": "Geri"
},
"item_card": {
"next_up": "Sıradaki",

View File

@@ -1,478 +0,0 @@
{
"login": {
"username_required": "Імʼя користувача необхідне",
"error_title": "Помилка",
"login_title": "Вхід",
"login_to_title": "Увійти в",
"username_placeholder": "Імʼя користувача",
"password_placeholder": "Пароль",
"login_button": "Вхід",
"quick_connect": "Швидке Зʼєднання",
"enter_code_to_login": "Введіть код {{code}} для входу",
"failed_to_initiate_quick_connect": "Не вдалося ініціалізувати Швидке Зʼєднання",
"got_it": "Готово",
"connection_failed": "Помилка зʼєднання",
"could_not_connect_to_server": "Неможливо підʼєднатися до серверу. Будь ласка перевірте URL і ваше зʼєднання з мережею",
"an_unexpected_error_occured": "Сталася несподівана помилка",
"change_server": "Змінити сервер",
"invalid_username_or_password": "Неправильні імʼя користувача або пароль",
"user_does_not_have_permission_to_log_in": "Користувач не маю дозволу на вхід",
"server_is_taking_too_long_to_respond_try_again_later": "Сервер відповідає занадто довго, будь-ласка спробуйте пізніше",
"server_received_too_many_requests_try_again_later": "Server received too many requests, try again later.",
"there_is_a_server_error": "Відбулася помилка на стороні сервера",
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Відбулася несподівана помилка. Чи введений URL сервера правильний?"
},
"server": {
"enter_url_to_jellyfin_server": "Введіть URL вашого Jellyfin сервера",
"server_url_placeholder": "http(s)://your-server.com",
"connect_button": "Підʼєднатися",
"previous_servers": "попередні сервери",
"clear_button": "Очистити",
"search_for_local_servers": "Пошук локальних серверів",
"searching": "Пошук...",
"servers": "Сервери"
},
"home": {
"no_internet": "Інтернет відсутній",
"no_items": "Пусто",
"no_internet_message": "Не хвилюйтеся, ви все ще можете переглядати\nзавантажений контент.",
"go_to_downloads": "Перейти в завантаження",
"oops": "Упс!",
"error_message": "Щось пішло не так.\nБудь ласка вийдіть і увійдіть знов.",
"continue_watching": "Продовжити перегляд",
"next_up": "Далі",
"recently_added_in": "Нещодавно додане до медіатеки {{libraryName}}",
"suggested_movies": "Рекомендовані Фільми",
"suggested_episodes": "Рекомендовані Епізоди",
"intro": {
"welcome_to_streamyfin": "Вітаємо у Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Вільний і open-source клієнт для Jellyfin.",
"features_title": "Функції",
"features_description": "Streamyfin має безліч функцій та інтегрується з широким спектром програмного забезпечення, яке ви можете знайти в меню налаштувань, зокрема:",
"jellyseerr_feature_description": "Підключіться до вашого екземпляру Jellyseerr і запитуватуйте фільми безпосередньо в застосунку.",
"downloads_feature_title": "Завантаження",
"downloads_feature_description": "Завантажуйте фільми і серіали для перегляду офлайн. Використовуйте або метод за замовчуванням або встановіть оптимізований сервер для завантаження файлів у фоні.",
"chromecast_feature_description": "Транслюйте фільми і серіали но ваші Chromecast прилади.",
"centralised_settings_plugin_title": "Centralised Settings Plugin",
"centralised_settings_plugin_description": "Налаштуйте параметри з централізованої локації на вашому сервері Jellyfin. Всі налаштування клієнтів для всіх користувачів будуть синхронізовані автоматично.",
"done_button": "Готово",
"go_to_settings_button": "Перейти до параметрів",
"read_more": "Прочитати більше"
},
"settings": {
"settings_title": "Параметри",
"log_out_button": "Вихід",
"user_info": {
"user_info_title": "Інформація користувача",
"user": "Користувач",
"server": "Сервер",
"token": "Токен",
"app_version": "Версія Застосунку"
},
"quick_connect": {
"quick_connect_title": "Швидке Зʼєднання",
"authorize_button": "Авторизуйте Швидке Зʼєднання",
"enter_the_quick_connect_code": "Введіть код для швидкого зʼєднання...",
"success": "Успіх",
"quick_connect_autorized": "Швидке Зʼєднання авторизовано",
"error": "Помилка",
"invalid_code": "Не правильний код",
"authorize": "Авторизувати"
},
"media_controls": {
"media_controls_title": "Керування Медія",
"forward_skip_length": "Тривалість перемотування вперед",
"rewind_length": "Довжина перемотування назад",
"seconds_unit": "с"
},
"audio": {
"audio_title": "Аудіо",
"set_audio_track": "Виставити аудіо доріжку як в попередньому епізоду",
"audio_language": "Мова аудіо",
"audio_hint": "Вибрати мову аудіо за замовчуванням.",
"none": "Ніяка",
"language": "Мова"
},
"subtitles": {
"subtitle_title": "Субтитри",
"subtitle_language": "Мова субтитрів",
"subtitle_mode": "Режим субтитрів",
"set_subtitle_track": "Виставити доріжку субтитрів як в попередньому епізоду",
"subtitle_size": "Розмір субтитрів",
"subtitle_hint": "Налаштуйте параметри субтитрів.",
"none": "Ніякі",
"language": "Мова",
"loading": "Завантаження",
"modes": {
"Default": "За замовчування",
"Smart": "Smart",
"Always": "Завжди",
"None": "Някий",
"OnlyForced": "Виключно Форсовані"
}
},
"other": {
"other_title": "Інші",
"follow_device_orientation": "Дотримуйтесь орієнтації пристрою",
"video_orientation": "Орієнтація відео",
"orientation": "Orientation",
"orientations": {
"DEFAULT": "За змовчуванням",
"ALL": "Всі",
"PORTRAIT": "Портретна",
"PORTRAIT_UP": "Портретна Догори",
"PORTRAIT_DOWN": "Портретна Донизу",
"LANDSCAPE": "Альбомна",
"LANDSCAPE_LEFT": "Альбомна Ліва",
"LANDSCAPE_RIGHT": "Альбомна Права",
"OTHER": "Інше",
"UNKNOWN": "Невідомо"
},
"safe_area_in_controls": "Безпечна зона в елементах керування",
"video_player": "Відео плеєр",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)"
},
"show_custom_menu_links": "Показати посилання на користувацьке меню",
"hide_libraries": "Сховати медіатеки",
"select_liraries_you_want_to_hide": "Виберіть медіатеки, що бажаєте приховати з вкладки Медіатека і з секції на головній сторінці.",
"disable_haptic_feedback": "Вимкнути тактильний зворотний зв'язок",
"default_quality": "Якість за замовченням"
},
"downloads": {
"downloads_title": "Завантаження",
"download_method": "Метод завантаження",
"remux_max_download": "Remux max download",
"auto_download": "Авто-завантаження",
"optimized_versions_server": "Optimized versions server",
"save_button": "Зберегти",
"optimized_server": "Оптимізований Сервер",
"optimized": "Оптимізований",
"default": "За замовченням",
"optimized_version_hint": "Введіть URL-адресу сервера для оптимізації. URL-адреса має містити http або https і, за бажанням, порт.",
"read_more_about_optimized_server": "Дізнайтеся більше про сервер для оптимізації.",
"url": "URL",
"server_url_placeholder": "http(s)://domain.org:port"
},
"plugins": {
"plugins_title": "Плагіни",
"jellyseerr": {
"jellyseerr_warning": "Ця інтеграція перебуває на початковій стадії. Очікуйте, що все зміниться.",
"server_url": "URL Сервера",
"server_url_hint": "Наприклад: http(s)://your-host.url\n(додайте порт якщо необхідно)",
"server_url_placeholder": "Jellyseerr URL...",
"password": "Пароль",
"password_placeholder": "Введіть Jellyfin пароль для користувача {{username}}",
"save_button": "Зберегти",
"clear_button": "Очистити",
"login_button": "Вхід",
"total_media_requests": "Загальна кількість медіа запитів",
"movie_quota_limit": "Дні квоти на фільми",
"movie_quota_days": "Дні квоти на фільми",
"tv_quota_limit": "Дні квоти на серіали",
"tv_quota_days": "Дні квоти на серіали",
"reset_jellyseerr_config_button": "Скинути конфігурацію Jellyseerr",
"unlimited": "Необмежене",
"plus_n_more": "+{{n}} ще",
"order_by": {
"DEFAULT": "За замовченням",
"VOTE_COUNT_AND_AVERAGE": "Кількість голосів і середнє",
"POPULARITY": "Популярність"
}
},
"marlin_search": {
"enable_marlin_search": "Увімкнути Marlin Search ",
"url": "URL",
"server_url_placeholder": "http(s)://domain.org:port",
"marlin_search_hint": "Введіть URL-адресу сервера Marlin. Адреса повинна містити http або https і, за бажанням, порт.",
"read_more_about_marlin": "Дізнайтеся більше про Marlin.",
"save_button": "Зберегти",
"toasts": {
"saved": "Збережено"
}
}
},
"storage": {
"storage_title": "Сховище",
"app_usage": "Застосунок {{usedSpace}}%",
"device_usage": "Гаджет {{availableSpace}}%",
"size_used": "{{used}} з {{total}} використано",
"delete_all_downloaded_files": "Видалити усі завантаженні файли"
},
"intro": {
"show_intro": "Показати інтро",
"reset_intro": "Скинути інтро"
},
"logs": {
"logs_title": "Журнал",
"export_logs": "Export logs",
"click_for_more_info": "Click for more info",
"level": "Level",
"no_logs_available": "Нема доступних журналів",
"delete_all_logs": "Видалити усі журнали"
},
"languages": {
"title": "Мова",
"app_language": "Мова застосунку",
"app_language_description": "Виберіть мову застосунку.",
"system": "Системна"
},
"toasts": {
"error_deleting_files": "Помилка при видалені файлів",
"background_downloads_enabled": "Завантаження в фоні увімкнене",
"background_downloads_disabled": "Завантаження в фоні вимкнене",
"connected": "Зʼєднано",
"could_not_connect": "Неможливо зʼєднатися",
"invalid_url": "Неправльий URL"
}
},
"sessions": {
"title": "Сесії",
"no_active_sessions": "Нема активних сесій"
},
"downloads": {
"downloads_title": "Завантаження",
"tvseries": "ТБ-Серіали",
"movies": "Фільми",
"queue": "Черга",
"queue_hint": "Черга і завантаження буде втрачене при перезапуску застосунку",
"no_items_in_queue": "Нема елементів в черзі",
"no_downloaded_items": "Нема завантажених елементів",
"delete_all_movies_button": "Видалити всі Фільми",
"delete_all_tvseries_button": "Видалити всі ТБ-Серіали",
"delete_all_button": "Видалити Все",
"active_download": "Активне завантаження",
"no_active_downloads": "Нема активних завантажень",
"active_downloads": "Активні завантаження",
"new_app_version_requires_re_download": "Нова версія застосунку вимагає завантажити заново",
"new_app_version_requires_re_download_description": "Нове оновлення вимагає повторного завантаження вмісту. Будь ласка, видаліть весь завантажений вміст і повторіть спробу.",
"back": "Назад",
"delete": "Видалити",
"something_went_wrong": "Щось пішло не так",
"could_not_get_stream_url_from_jellyfin": "Не вдалося отримати URL-адресу потоку від Jellyfin",
"eta": "ETA {{eta}}",
"methods": "Методи",
"toasts": {
"you_are_not_allowed_to_download_files": "Вам не дозволено завантажувати файли.",
"deleted_all_movies_successfully": "Видалення всіх фільмів було успішне!",
"failed_to_delete_all_movies": "Не вдалося видалити усі фільми",
"deleted_all_tvseries_successfully": "Успішно видалено всі серіали!",
"failed_to_delete_all_tvseries": "Не вдалося видалити всі телесеріали",
"download_cancelled": "Завантаження скасоване",
"could_not_cancel_download": "Неможливо скасувати завантаження",
"download_completed": "Завантаження завершено",
"download_started_for": "Почалося завантаження {{item}}",
"item_is_ready_to_be_downloaded": "{{item}} вже завантажено",
"download_stated_for_item": "Почалося завантаження {{item}}",
"download_failed_for_item": "Не вдалося завантажити {{item}} - {{error}}",
"download_completed_for_item": "Завантаження завершено {{item}}",
"queued_item_for_optimization": "{{item}} в черзі на оптимізацію",
"failed_to_start_download_for_item": "Не вдалося почати завантаження {{item}}: {{message}}",
"server_responded_with_status_code": "Сервер відповів зі статусом {{statusCode}}",
"no_response_received_from_server": "Не отримано відповіді від сервера",
"error_setting_up_the_request": "Помилка налаштування запиту",
"failed_to_start_download_for_item_unexpected_error": "Не вдалося почати завантаження {{item}}: Несподівана помилка",
"all_files_folders_and_jobs_deleted_successfully": "Усі файли, папки та завдання успішно видалено",
"an_error_occured_while_deleting_files_and_jobs": "Виникла помилка під час видалення файлів і завдань",
"go_to_downloads": "Перейти до завантаження"
}
}
},
"search": {
"search_here": "Шукати тут...",
"search": "Шукати...",
"x_items": "{{count}} елементів",
"library": "Медіатека",
"discover": "Відкрийте для себе",
"no_results": "Без результатів",
"no_results_found_for": "Жодних результатів не знайдено для",
"movies": "Фільми",
"series": "Серіали",
"episodes": "Епізоди",
"collections": "Колекції",
"actors": "Актори",
"request_movies": "Запитати Фільми",
"request_series": "Запитати Серіали",
"recently_added": "Нещодавно Додане",
"recent_requests": "Нещодавні Запити",
"plex_watchlist": "Список перегляду Plex",
"trending": "У Тренді",
"popular_movies": "Популярні Фільми",
"movie_genres": "Жанри Кіно",
"upcoming_movies": "Майбутні Фільми",
"studios": "Студії",
"popular_tv": "Популярні Серіали",
"tv_genres": "Жанри Серіалів",
"upcoming_tv": "Майбутні Серіали",
"networks": "ТБ Канали",
"tmdb_movie_keyword": "TMDB Ключові слова Фільмів",
"tmdb_movie_genre": "TMDB Жанри Кіно",
"tmdb_tv_keyword": "TMDB ТБ Ключові слова",
"tmdb_tv_genre": "TMDB ТБ Жанри",
"tmdb_search": "TMDB Пошук",
"tmdb_studio": "TMDB Студії",
"tmdb_network": "TMDB ТБ Канали",
"tmdb_movie_streaming_services": "TMDB Стрімінгові Сервіси Фільмів",
"tmdb_tv_streaming_services": "TMDB Стрімінгові Сервіси Серіалів"
},
"library": {
"no_items_found": "Елементів не знайдено",
"no_results": "Без результатів",
"no_libraries_found": "Не знайдено медіатек",
"item_types": {
"movies": "фільми",
"series": "серіали",
"boxsets": "бокс-сети",
"items": "елементи"
},
"options": {
"display": "Показати",
"row": "Ряд",
"list": "Список",
"image_style": "Стиль зображення",
"poster": "Постер",
"cover": "Обкладинка",
"show_titles": "Показати заголовки",
"show_stats": "Показати статистику"
},
"filters": {
"genres": "Жанри",
"years": "Роки",
"sort_by": "Відсортувати за",
"sort_order": "Порядок сортування",
"asc": "За зростанням",
"desc": "За спаданням",
"tags": "Теги"
}
},
"favorites": {
"series": "Серіали",
"movies": "Фільми",
"episodes": "Епізоди",
"videos": "Відео",
"boxsets": "Бокс-сети",
"playlists": "Плейлісти"
},
"custom_links": {
"no_links": "Немає посилань"
},
"player": {
"error": "Помилка",
"failed_to_get_stream_url": "Не вдалося отримати URL-адресу потоку",
"an_error_occured_while_playing_the_video": "Під час відтворення відео сталася помилка. Перевірте журнали в налаштуваннях.",
"client_error": "Помилка клієнту",
"could_not_create_stream_for_chromecast": "Не вдалося створити потік для Chromecast",
"message_from_server": "Повідомлення від серверу: {{message}}",
"video_has_finished_playing": "Відтворення відео завершено!",
"no_video_source": "Немає джерела відео...",
"next_episode": "Наступний Епізод",
"refresh_tracks": "Оновити доріжки",
"subtitle_tracks": "Доріжки Субтитрів:",
"audio_tracks": "Аудіо-доріжки:",
"playback_state": "Стан відтворення:",
"no_data_available": "Дані відсутні",
"index": "Індекс:"
},
"item_card": {
"next_up": "Далі",
"no_items_to_display": "Немає елементів для відображення",
"cast_and_crew": "Акторський склад та команда",
"series": "Серіали",
"seasons": "Сезони",
"season": "Сезон",
"no_episodes_for_this_season": "У цьому сезоні немає епізодів",
"overview": "Огляд",
"more_with": "Більше з {{name}}",
"similar_items": "Схожі елементи",
"no_similar_items_found": "Не знайдено схожих елементів",
"video": "Відео",
"more_details": "Більше деталей",
"quality": "Якість",
"audio": "Аудіо",
"subtitles": "Субтитри",
"show_more": "Показати більше",
"show_less": "Показати менше",
"appeared_in": "Зʼявлявся у",
"could_not_load_item": "Неможливо завантажити елемент",
"none": "Нічого",
"download": {
"download_season": "Завантажити Сезон",
"download_series": "Завантажити Серіал",
"download_episode": "Завантажити Епізод",
"download_movie": "Завантажити Фільм",
"download_x_item": "Завантажено {{item_count}} елементів",
"download_button": "Завантажити",
"using_optimized_server": "Використовуючи сервер оптимізації",
"using_default_method": "Використовуючи метод за замовченням"
}
},
"live_tv": {
"next": "Наступний",
"previous": "Попередній",
"live_tv": "Live TV",
"coming_soon": "Скоро",
"on_now": "Просто зараз",
"shows": "Серіали",
"movies": "Фільми",
"sports": "Спорт",
"for_kids": "Для дітей",
"news": "Новини"
},
"jellyseerr": {
"confirm": "Підтвердити",
"cancel": "Скасувати",
"yes": "Так",
"whats_wrong": "Щось сталося?",
"issue_type": "Тип проблеми",
"select_an_issue": "Виберіть проблему",
"types": "Типи",
"describe_the_issue": "(опціонально) Опишіть проблему...",
"submit_button": "Надіслати",
"report_issue_button": "Звіт про проблему",
"request_button": "Запити",
"are_you_sure_you_want_to_request_all_seasons": "Ви впевнені, що хочете запросити всі сезони?",
"failed_to_login": "Не вдалося увійти",
"cast": "Акторський склад",
"details": "Деталі",
"status": "Статус",
"original_title": "Оригінальна Назва",
"series_type": "Тип Серіалу",
"release_dates": "Дата Виходу",
"first_air_date": "Дата першого етеру",
"next_air_date": "Дата наступного етеру",
"revenue": "Збори",
"budget": "Бюджет",
"original_language": "Мова Оригіналу",
"production_country": "Країна Виробництва",
"studios": "Студії",
"network": "ТБ Канали",
"currently_streaming_on": "Наразі транслюється на",
"advanced": "Просунуте",
"request_as": "Запит Як",
"tags": "Теги",
"quality_profile": "Профіль якості",
"root_folder": "Корнева Тека",
"season_all": "Сезон (всі)",
"season_number": "Сезон {{season_number}}",
"number_episodes": "{{episode_number}} Епізодів",
"born": "Дата народження",
"appearances": "Зовнішній вигляд",
"toasts": {
"jellyseer_does_not_meet_requirements": "Сервер Jellyseerr не відповідає мінімальним вимогам до версії! Будь ласка, оновіть версію принаймні до 2.0.0",
"jellyseerr_test_failed": "Тест Jellyseerr завершився невдало. Спробуйте ще раз.",
"failed_to_test_jellyseerr_server_url": "Не вдалося перевірити URL-адресу сервера jellyseerr",
"issue_submitted": "Звіт про проблему відправлено",
"requested_item": "Запитано {{item}}!",
"you_dont_have_permission_to_request": "У вас нема дозволу на запит медіа!",
"something_went_wrong_requesting_media": "Щось пішло не так під час запиту медіа!"
}
},
"tabs": {
"home": "Головна",
"search": "Пошук",
"library": "Медіатека",
"custom_links": "Ваші Посилання",
"favorites": "Улюблене"
}
}

483
translations/uk.json Normal file
View File

@@ -0,0 +1,483 @@
{
"login": {
"username_required": "Імʼя користувача необхідне",
"error_title": "Помилка",
"login_title": "Вхід",
"login_to_title": "Увійти в",
"username_placeholder": "Імʼя користувача",
"password_placeholder": "Пароль",
"login_button": "Вхід",
"quick_connect": "Швидке Зʼєднання",
"enter_code_to_login": "Введіть код {{code}} для входу",
"failed_to_initiate_quick_connect": "Не вдалося ініціалізувати Швидке Зʼєднання",
"got_it": "Готово",
"connection_failed": "Помилка зʼєднання",
"could_not_connect_to_server": "Неможливо підʼєднатися до серверу. Будь ласка перевірте URL і ваше зʼєднання з мережею",
"an_unexpected_error_occured": "Сталася несподівана помилка",
"change_server": "Змінити сервер",
"invalid_username_or_password": "Неправильні імʼя користувача або пароль",
"user_does_not_have_permission_to_log_in": "Користувач не маю дозволу на вхід",
"server_is_taking_too_long_to_respond_try_again_later": "Сервер відповідає занадто довго, будь-ласка спробуйте пізніше",
"server_received_too_many_requests_try_again_later": "Сервер отримав забагато запитів, будь ласка спробуйте пізніше.",
"there_is_a_server_error": "Відбулася помилка на стороні сервера",
"an_unexpected_error_occured_did_you_enter_the_correct_url": "Відбулася несподівана помилка. Чи введений URL сервера правильний?"
},
"server": {
"enter_url_to_jellyfin_server": "Введіть URL вашого Jellyfin сервера",
"server_url_placeholder": "http(s)://your-server.com",
"connect_button": "Підʼєднатися",
"previous_servers": "попередні сервери",
"clear_button": "Очистити",
"search_for_local_servers": "Пошук локальних серверів",
"searching": "Пошук...",
"servers": "Сервери"
},
"home": {
"no_internet": "Інтернет відсутній",
"no_items": "Пусто",
"no_internet_message": "Не хвилюйтеся, ви все ще можете переглядати\nзавантажений контент.",
"go_to_downloads": "Перейти в завантаження",
"oops": "Упс!",
"error_message": "Щось пішло не так.\nБудь ласка вийдіть і увійдіть знов.",
"continue_watching": "Продовжити перегляд",
"next_up": "Далі",
"recently_added_in": "Нещодавно додане до \"{{libraryName}}\"",
"suggested_movies": "Рекомендовані Фільми",
"suggested_episodes": "Рекомендовані Епізоди",
"intro": {
"welcome_to_streamyfin": "Вітаємо у Streamyfin",
"a_free_and_open_source_client_for_jellyfin": "Вільний і open-source клієнт для Jellyfin.",
"features_title": "Функції",
"features_description": "Streamyfin має безліч функцій та інтегрується з широким спектром програмного забезпечення, яке ви можете знайти в меню налаштувань, зокрема:",
"jellyseerr_feature_description": "Підключіться до вашого екземпляру Jellyseerr і запитуватуйте фільми безпосередньо в застосунку.",
"downloads_feature_title": "Завантаження",
"downloads_feature_description": "Завантажуйте фільми і серіали для перегляду офлайн. Використовуйте або метод за замовчуванням або встановіть оптимізований сервер для завантаження файлів у фоні.",
"chromecast_feature_description": "Транслюйте фільми і серіали на ваші Chromecast прилади.",
"centralised_settings_plugin_title": "Centralised Settings Plugin",
"centralised_settings_plugin_description": "Налаштуйте параметри з централізованої локації на вашому сервері Jellyfin. Всі налаштування клієнтів для всіх користувачів будуть синхронізовані автоматично.",
"done_button": "Готово",
"go_to_settings_button": "Перейти до параметрів",
"read_more": "Прочитати більше"
},
"settings": {
"settings_title": "Параметри",
"log_out_button": "Вихід",
"user_info": {
"user_info_title": "Інформація користувача",
"user": "Користувач",
"server": "Сервер",
"token": "Токен",
"app_version": "Версія Застосунку"
},
"quick_connect": {
"quick_connect_title": "Швидке Зʼєднання",
"authorize_button": "Авторизуйте Швидке Зʼєднання",
"enter_the_quick_connect_code": "Введіть код для швидкого зʼєднання...",
"success": "Успіх",
"quick_connect_autorized": "Швидке Зʼєднання авторизовано",
"error": "Помилка",
"invalid_code": "Не правильний код",
"authorize": "Авторизувати"
},
"media_controls": {
"media_controls_title": "Керування Медіа",
"forward_skip_length": "Довжина перемотування вперед",
"rewind_length": "Довжина перемотування назад",
"seconds_unit": "с"
},
"audio": {
"audio_title": "Аудіо",
"set_audio_track": "Аудіо доріжка як в попередньому епізоді",
"audio_language": "Мова аудіо",
"audio_hint": "Вибрати мову аудіо за замовчуванням.",
"none": "Ніяка",
"language": "Мова"
},
"subtitles": {
"subtitle_title": "Субтитри",
"subtitle_language": "Мова субтитрів",
"subtitle_mode": "Режим субтитрів",
"set_subtitle_track": "Виставити доріжку субтитрів як в попередньому епізоду",
"subtitle_size": "Розмір субтитрів",
"subtitle_hint": "Налаштуйте параметри субтитрів.",
"none": "Ніякі",
"language": "Мова",
"loading": "Завантаження",
"modes": {
"Default": "За замовчування",
"Smart": "Smart",
"Always": "Завжди",
"None": "Някий",
"OnlyForced": "Виключно Форсовані"
}
},
"other": {
"other_title": "Інші",
"follow_device_orientation": "Дотримуйтесь орієнтації пристрою",
"video_orientation": "Орієнтація відео",
"orientation": "Orientation",
"orientations": {
"DEFAULT": "За змовчуванням",
"ALL": "Всі",
"PORTRAIT": "Портретна",
"PORTRAIT_UP": "Портретна Догори",
"PORTRAIT_DOWN": "Портретна Донизу",
"LANDSCAPE": "Альбомна",
"LANDSCAPE_LEFT": "Альбомна Ліва",
"LANDSCAPE_RIGHT": "Альбомна Права",
"OTHER": "Інше",
"UNKNOWN": "Невідомо"
},
"safe_area_in_controls": "Безпечна зона в елементах керування",
"video_player": "Відео плеєр",
"video_players": {
"VLC_3": "VLC 3",
"VLC_4": "VLC 4 (Experimental + PiP)"
},
"show_custom_menu_links": "Показати користувацькі посилання меню",
"hide_libraries": "Сховати медіатеки",
"select_liraries_you_want_to_hide": "Виберіть медіатеки, що бажаєте приховати з вкладки Медіатека і з секції на головній сторінці.",
"disable_haptic_feedback": "Вимкнути тактильний зворотний зв'язок",
"default_quality": "Якість за замовченням",
"disabled": "Вимкнено"
},
"downloads": {
"downloads_title": "Завантаження",
"download_method": "Метод завантаження",
"remux_max_download": "Remux max download",
"auto_download": "Авто-завантаження",
"optimized_versions_server": "Сервер оптимізованих версій",
"save_button": "Зберегти",
"optimized_server": "Оптимізований Сервер",
"optimized": "Оптимізований",
"default": "За замовченням",
"optimized_version_hint": "Введіть URL-адресу сервера для оптимізації. URL-адреса має містити http або https і, за бажанням, порт.",
"read_more_about_optimized_server": "Дізнайтеся більше про сервер для оптимізації.",
"url": "URL",
"server_url_placeholder": "http(s)://domain.org:port"
},
"plugins": {
"plugins_title": "Плагіни",
"jellyseerr": {
"jellyseerr_warning": "Ця інтеграція перебуває на початковій стадії. Очікуйте, що все зміниться.",
"server_url": "URL Сервера",
"server_url_hint": "Наприклад: http(s)://your-host.url\n(додайте порт якщо необхідно)",
"server_url_placeholder": "Jellyseerr URL...",
"password": "Пароль",
"password_placeholder": "Введіть Jellyfin пароль для користувача {{username}}",
"save_button": "Зберегти",
"clear_button": "Очистити",
"login_button": "Вхід",
"total_media_requests": "Загальна кількість медіа запитів",
"movie_quota_limit": "Дні квоти на фільми",
"movie_quota_days": "Дні квоти на фільми",
"tv_quota_limit": "Дні квоти на серіали",
"tv_quota_days": "Дні квоти на серіали",
"reset_jellyseerr_config_button": "Скинути конфігурацію Jellyseerr",
"unlimited": "Необмежене",
"plus_n_more": "+{{n}} ще",
"order_by": {
"DEFAULT": "За замовченням",
"VOTE_COUNT_AND_AVERAGE": "Кількість голосів і середнє",
"POPULARITY": "Популярність"
}
},
"marlin_search": {
"enable_marlin_search": "Увімкнути Marlin Search ",
"url": "URL",
"server_url_placeholder": "http(s)://domain.org:port",
"marlin_search_hint": "Введіть URL-адресу сервера Marlin. Адреса повинна містити http або https і, за бажанням, порт.",
"read_more_about_marlin": "Дізнайтеся більше про Marlin.",
"save_button": "Зберегти",
"toasts": {
"saved": "Збережено"
}
}
},
"storage": {
"storage_title": "Сховище",
"app_usage": "Застосунок {{usedSpace}}%",
"device_usage": "Гаджет {{availableSpace}}%",
"size_used": "{{used}} з {{total}} використано",
"delete_all_downloaded_files": "Видалити усі завантаженні файли"
},
"intro": {
"show_intro": "Показати інтро",
"reset_intro": "Скинути інтро"
},
"logs": {
"logs_title": "Журнал",
"export_logs": "Export logs",
"click_for_more_info": "Click for more info",
"level": "Level",
"no_logs_available": "Нема доступних журналів",
"delete_all_logs": "Видалити усі журнали"
},
"languages": {
"title": "Мова",
"app_language": "Мова застосунку",
"app_language_description": "Виберіть мову застосунку.",
"system": "Системна"
},
"toasts": {
"error_deleting_files": "Помилка при видалені файлів",
"background_downloads_enabled": "Завантаження в фоні увімкнене",
"background_downloads_disabled": "Завантаження в фоні вимкнене",
"connected": "Зʼєднано",
"could_not_connect": "Неможливо зʼєднатися",
"invalid_url": "Неправльий URL"
}
},
"sessions": {
"title": "Сесії",
"no_active_sessions": "Нема активних сесій"
},
"downloads": {
"downloads_title": "Завантаження",
"tvseries": "ТБ-Серіали",
"movies": "Фільми",
"queue": "Черга",
"queue_hint": "Черга і завантаження буде втрачене при перезапуску застосунку",
"no_items_in_queue": "Нема елементів в черзі",
"no_downloaded_items": "Нема завантажених елементів",
"delete_all_movies_button": "Видалити всі Фільми",
"delete_all_tvseries_button": "Видалити всі ТБ-Серіали",
"delete_all_button": "Видалити Все",
"active_download": "Активне завантаження",
"no_active_downloads": "Нема активних завантажень",
"active_downloads": "Активні завантаження",
"new_app_version_requires_re_download": "Нова версія застосунку вимагає завантажити заново",
"new_app_version_requires_re_download_description": "Нове оновлення вимагає повторного завантаження вмісту. Будь ласка, видаліть весь завантажений вміст і повторіть спробу.",
"back": "Назад",
"delete": "Видалити",
"something_went_wrong": "Щось пішло не так",
"could_not_get_stream_url_from_jellyfin": "Не вдалося отримати URL-адресу потоку від Jellyfin",
"eta": "ETA {{eta}}",
"methods": "Методи",
"toasts": {
"you_are_not_allowed_to_download_files": "Вам не дозволено завантажувати файли.",
"deleted_all_movies_successfully": "Видалення всіх фільмів було успішне!",
"failed_to_delete_all_movies": "Не вдалося видалити усі фільми",
"deleted_all_tvseries_successfully": "Успішно видалено всі серіали!",
"failed_to_delete_all_tvseries": "Не вдалося видалити всі телесеріали",
"download_cancelled": "Завантаження скасоване",
"could_not_cancel_download": "Неможливо скасувати завантаження",
"download_completed": "Завантаження завершено",
"download_started_for": "Почалося завантаження {{item}}",
"item_is_ready_to_be_downloaded": "{{item}} вже завантажено",
"download_stated_for_item": "Почалося завантаження {{item}}",
"download_failed_for_item": "Не вдалося завантажити {{item}} - {{error}}",
"download_completed_for_item": "Завантаження завершено {{item}}",
"queued_item_for_optimization": "{{item}} в черзі на оптимізацію",
"failed_to_start_download_for_item": "Не вдалося почати завантаження {{item}}: {{message}}",
"server_responded_with_status_code": "Сервер відповів зі статусом {{statusCode}}",
"no_response_received_from_server": "Не отримано відповіді від сервера",
"error_setting_up_the_request": "Помилка налаштування запиту",
"failed_to_start_download_for_item_unexpected_error": "Не вдалося почати завантаження {{item}}: Несподівана помилка",
"all_files_folders_and_jobs_deleted_successfully": "Усі файли, папки та завдання успішно видалено",
"an_error_occured_while_deleting_files_and_jobs": "Виникла помилка під час видалення файлів і завдань",
"go_to_downloads": "Перейти до завантаження"
}
}
},
"search": {
"search_here": "Шукати тут...",
"search": "Шукати...",
"x_items": "{{count}} елементів",
"library": "Медіатека",
"discover": "Відкрийте для себе",
"no_results": "Без результатів",
"no_results_found_for": "Жодних результатів не знайдено для",
"movies": "Фільми",
"series": "Серіали",
"episodes": "Епізоди",
"collections": "Колекції",
"actors": "Актори",
"request_movies": "Запитати Фільми",
"request_series": "Запитати Серіали",
"recently_added": "Нещодавно Додане",
"recent_requests": "Нещодавні Запити",
"plex_watchlist": "Список перегляду Plex",
"trending": "У Тренді",
"popular_movies": "Популярні Фільми",
"movie_genres": "Жанри Кіно",
"upcoming_movies": "Майбутні Фільми",
"studios": "Студії",
"popular_tv": "Популярні Серіали",
"tv_genres": "Жанри Серіалів",
"upcoming_tv": "Майбутні Серіали",
"networks": "ТБ Канали",
"tmdb_movie_keyword": "TMDB Ключові слова Фільмів",
"tmdb_movie_genre": "TMDB Жанри Кіно",
"tmdb_tv_keyword": "TMDB ТБ Ключові слова",
"tmdb_tv_genre": "TMDB ТБ Жанри",
"tmdb_search": "TMDB Пошук",
"tmdb_studio": "TMDB Студії",
"tmdb_network": "TMDB ТБ Канали",
"tmdb_movie_streaming_services": "TMDB Стрімінгові Сервіси Фільмів",
"tmdb_tv_streaming_services": "TMDB Стрімінгові Сервіси Серіалів"
},
"library": {
"no_items_found": "Елементів не знайдено",
"no_results": "Без результатів",
"no_libraries_found": "Не знайдено медіатек",
"item_types": {
"movies": "фільми",
"series": "серіали",
"boxsets": "бокс-сети",
"items": "елементи"
},
"options": {
"display": "Показати",
"row": "Ряд",
"list": "Список",
"image_style": "Стиль зображення",
"poster": "Постер",
"cover": "Обкладинка",
"show_titles": "Показати заголовки",
"show_stats": "Показати статистику"
},
"filters": {
"genres": "Жанри",
"years": "Роки",
"sort_by": "Відсортувати за",
"sort_order": "Порядок сортування",
"asc": "За зростанням",
"desc": "За спаданням",
"tags": "Теги"
}
},
"favorites": {
"series": "Серіали",
"movies": "Фільми",
"episodes": "Епізоди",
"videos": "Відео",
"boxsets": "Бокс-сети",
"playlists": "Плейлісти",
"noDataTitle": "Поки що нема обраного",
"noData": "Відмітьте як улюблене що би побачити це тут в швидкому доступі."
},
"custom_links": {
"no_links": "Немає посилань"
},
"player": {
"error": "Помилка",
"failed_to_get_stream_url": "Не вдалося отримати URL-адресу потоку",
"an_error_occured_while_playing_the_video": "Під час відтворення відео сталася помилка. Перевірте журнал в налаштуваннях.",
"client_error": "Помилка клієнту",
"could_not_create_stream_for_chromecast": "Не вдалося створити потік для Chromecast",
"message_from_server": "Повідомлення від серверу: {{message}}",
"video_has_finished_playing": "Відтворення відео завершено!",
"no_video_source": "Немає джерела відео...",
"next_episode": "Наступний Епізод",
"refresh_tracks": "Оновити доріжки",
"subtitle_tracks": "Доріжки Субтитрів:",
"audio_tracks": "Аудіо-доріжки:",
"playback_state": "Стан відтворення:",
"no_data_available": "Дані відсутні",
"index": "Індекс:",
"continue_watching": "Продовжити перегляд",
"go_back": "Назад"
},
"item_card": {
"next_up": "Далі",
"no_items_to_display": "Немає елементів для відображення",
"cast_and_crew": "Акторський склад та команда",
"series": "Серіали",
"seasons": "Сезони",
"season": "Сезон",
"no_episodes_for_this_season": "У цьому сезоні немає епізодів",
"overview": "Огляд",
"more_with": "Більше з {{name}}",
"similar_items": "Схожі елементи",
"no_similar_items_found": "Не знайдено схожих елементів",
"video": "Відео",
"more_details": "Більше деталей",
"quality": "Якість",
"audio": "Аудіо",
"subtitles": "Субтитри",
"show_more": "Показати більше",
"show_less": "Показати менше",
"appeared_in": "Зʼявлявся у",
"could_not_load_item": "Неможливо завантажити елемент",
"none": "Нічого",
"download": {
"download_season": "Завантажити Сезон",
"download_series": "Завантажити Серіал",
"download_episode": "Завантажити Епізод",
"download_movie": "Завантажити Фільм",
"download_x_item": "Завантажено {{item_count}} елементів",
"download_button": "Завантажити",
"using_optimized_server": "Використовуючи сервер оптимізації",
"using_default_method": "Використовуючи метод за замовченням"
}
},
"live_tv": {
"next": "Наступний",
"previous": "Попередній",
"live_tv": "Live TV",
"coming_soon": "Скоро",
"on_now": "Просто зараз",
"shows": "Серіали",
"movies": "Фільми",
"sports": "Спорт",
"for_kids": "Для дітей",
"news": "Новини"
},
"jellyseerr": {
"confirm": "Підтвердити",
"cancel": "Скасувати",
"yes": "Так",
"whats_wrong": "Щось сталося?",
"issue_type": "Тип проблеми",
"select_an_issue": "Виберіть проблему",
"types": "Типи",
"describe_the_issue": "(опціонально) Опишіть проблему...",
"submit_button": "Надіслати",
"report_issue_button": "Звіт про проблему",
"request_button": "Запити",
"are_you_sure_you_want_to_request_all_seasons": "Ви впевнені, що хочете запросити всі сезони?",
"failed_to_login": "Не вдалося увійти",
"cast": "Акторський склад",
"details": "Деталі",
"status": "Статус",
"original_title": "Оригінальна Назва",
"series_type": "Тип Серіалу",
"release_dates": "Дата Виходу",
"first_air_date": "Дата першого етеру",
"next_air_date": "Дата наступного етеру",
"revenue": "Збори",
"budget": "Бюджет",
"original_language": "Мова Оригіналу",
"production_country": "Країна Виробництва",
"studios": "Студії",
"network": "ТБ Канали",
"currently_streaming_on": "Наразі транслюється на",
"advanced": "Просунуте",
"request_as": "Запит Як",
"tags": "Теги",
"quality_profile": "Профіль якості",
"root_folder": "Корнева Тека",
"season_all": "Сезон (всі)",
"season_number": "Сезон {{season_number}}",
"number_episodes": "{{episode_number}} Епізодів",
"born": "Дата народження",
"appearances": "Зовнішній вигляд",
"toasts": {
"jellyseer_does_not_meet_requirements": "Версія Jellyseerr не відповідає мінімальним вимогам! Будь ласка, оновіться принаймні до 2.0.0",
"jellyseerr_test_failed": "Тест Jellyseerr завершився невдало. Спробуйте ще раз.",
"failed_to_test_jellyseerr_server_url": "Не вдалося перевірити URL-адресу сервера jellyseerr",
"issue_submitted": "Звіт про проблему відправлено",
"requested_item": "Запитано {{item}}!",
"you_dont_have_permission_to_request": "Ви не маєте дозволу на запит медіа!",
"something_went_wrong_requesting_media": "Щось пішло не так під час запиту медіа!"
}
},
"tabs": {
"home": "Головна",
"search": "Пошук",
"library": "Медіатека",
"custom_links": "Ваші Посилання",
"favorites": "Улюблене"
}
}

View File

@@ -369,7 +369,9 @@
"audio_tracks": "音频轨道:",
"playback_state": "播放状态:",
"no_data_available": "无可用数据",
"index": "索引:"
"index": "索引:",
"continue_watching": "继续观看",
"go_back": "返回"
},
"item_card": {
"next_up": "下一个",

View File

@@ -137,7 +137,9 @@
"show_custom_menu_links": "顯示自定義菜單鏈接",
"hide_libraries": "隱藏媒體庫",
"select_liraries_you_want_to_hide": "選擇您想從媒體庫頁面和主頁隱藏的媒體庫。",
"disable_haptic_feedback": "禁用觸覺回饋"
"disable_haptic_feedback": "禁用觸覺回饋",
"default_quality": "預設品質",
"disabled": "已停用"
},
"downloads": {
"downloads_title": "下載",
@@ -369,7 +371,9 @@
"audio_tracks": "音頻軌道:",
"playback_state": "播放狀態:",
"no_data_available": "無可用數據",
"index": "索引:"
"index": "索引:",
"continue_watching": "繼續觀看",
"go_back": "返回"
},
"item_card": {
"next_up": "下一個",

View File

@@ -114,6 +114,11 @@ export type HomeSectionNextUpResolver = {
enableRewatching?: boolean;
};
export interface MaxAutoPlayEpisodeCount {
key: string;
value: number;
}
export type HomeSectionLatestResolver = {
parentId?: string;
limit?: number;
@@ -163,6 +168,8 @@ export type Settings = {
hiddenLibraries?: string[];
enableH265ForChromecast: boolean;
defaultPlayer: VideoPlayer;
maxAutoPlayEpisodeCount: MaxAutoPlayEpisodeCount;
autoPlayEpisodeCount: number;
};
export interface Lockable<T> {
@@ -217,7 +224,9 @@ const defaultValues: Settings = {
jellyseerrServerUrl: undefined,
hiddenLibraries: [],
enableH265ForChromecast: false,
defaultPlayer: VideoPlayer.VLC_3, // ios only setting. does not matter what this is for android
defaultPlayer: VideoPlayer.VLC_3, // ios-only setting. does not matter what this is for android
maxAutoPlayEpisodeCount: { key: "3", value: 3 },
autoPlayEpisodeCount: 0,
};
const loadSettings = (): Partial<Settings> => {
@@ -236,11 +245,11 @@ const loadSettings = (): Partial<Settings> => {
const EXCLUDE_FROM_SAVE = ["home"];
const saveSettings = (settings: Settings) => {
Object.keys(settings).forEach((key) => {
for (const key of Object.keys(settings)) {
if (EXCLUDE_FROM_SAVE.includes(key)) {
delete settings[key as keyof Settings];
}
});
}
const jsonValue = JSON.stringify(settings);
storage.set("settings", jsonValue);
};
@@ -271,7 +280,9 @@ export const useSettings = () => {
);
const refreshStreamyfinPluginSettings = useCallback(async () => {
if (!api) return;
if (!api) {
return;
}
const settings = await api.getStreamyfinPluginConfig().then(
({ data }) => {
writeInfoLog("Got plugin settings", data?.settings);
@@ -284,7 +295,9 @@ export const useSettings = () => {
}, [api]);
const updateSettings = (update: Partial<Settings>) => {
if (!_settings) return;
if (!_settings) {
return;
}
const hasChanges = Object.entries(update).some(
([key, value]) => _settings[key as keyof Settings] !== value,
);
@@ -305,34 +318,31 @@ export const useSettings = () => {
// If admin sets locked to false but provides a value,
// use user settings first and fallback on admin setting if required.
const settings: Settings = useMemo(() => {
let unlockedPluginDefaults = {} as Settings;
const overrideSettings = Object.entries(pluginSettings || {}).reduce(
(acc, [key, setting]) => {
if (setting) {
const { value, locked } = setting;
const unlockedPluginDefaults = {} as Settings;
const overrideSettings = Object.entries(pluginSettings ?? {}).reduce<
Partial<Settings>
>((acc, [key, setting]) => {
if (setting) {
const { value, locked } = setting;
const settingsKey = key as keyof Settings;
// Make sure we override default settings with plugin settings when they are not locked.
// Admin decided what users defaults should be and grants them the ability to change them too.
if (
locked === false &&
value &&
_settings?.[key as keyof Settings] !== value
) {
unlockedPluginDefaults = Object.assign(unlockedPluginDefaults, {
[key as keyof Settings]: value,
});
}
acc = Object.assign(acc, {
[key]: locked
? value
: (_settings?.[key as keyof Settings] ?? value),
// Make sure we override default settings with plugin settings when they are not locked.
if (
!locked &&
value !== undefined &&
_settings?.[settingsKey] !== value
) {
Object.assign(unlockedPluginDefaults, {
[settingsKey]: value,
});
}
return acc;
},
{} as Settings,
);
Object.assign(acc, {
[settingsKey]: locked ? value : (_settings?.[settingsKey] ?? value),
});
}
return acc;
}, {});
return {
...defaultValues,

View File

@@ -1,4 +1,4 @@
import native from "@/utils/profiles/native";
import generateDeviceProfile from "@/utils/profiles/native";
import type { Api } from "@jellyfin/sdk";
import type {
BaseItemDto,
@@ -14,23 +14,27 @@ export const getStreamUrl = async ({
userId,
startTimeTicks = 0,
maxStreamingBitrate,
sessionData,
deviceProfile = native,
playSessionId,
deviceProfile = generateDeviceProfile(),
audioStreamIndex = 0,
subtitleStreamIndex = undefined,
mediaSourceId,
download = false,
deviceId,
}: {
api: Api | null | undefined;
item: BaseItemDto | null | undefined;
userId: string | null | undefined;
startTimeTicks: number;
maxStreamingBitrate?: number;
sessionData?: PlaybackInfoResponse | null;
playSessionId?: string | null;
deviceProfile?: any;
audioStreamIndex?: number;
subtitleStreamIndex?: number;
height?: number;
mediaSourceId?: string | null;
download?: bool;
deviceId?: string | null;
}): Promise<{
url: string | null;
sessionId: string | null;
@@ -44,111 +48,84 @@ export const getStreamUrl = async ({
let mediaSource: MediaSourceInfo | undefined;
let sessionId: string | null | undefined;
if (item.Type === "Program") {
console.log("Item is of type program...");
const res0 = await getMediaInfoApi(api).getPlaybackInfo(
{
userId,
itemId: item.ChannelId!,
},
{
method: "POST",
params: {
startTimeTicks: 0,
isPlayback: true,
autoOpenLiveStream: true,
maxStreamingBitrate,
audioStreamIndex,
},
data: {
deviceProfile,
},
},
);
const transcodeUrl = res0.data.MediaSources?.[0].TranscodingUrl;
sessionId = res0.data.PlaySessionId || null;
if (transcodeUrl) {
return {
url: `${api.basePath}${transcodeUrl}`,
sessionId,
mediaSource: res0.data.MediaSources?.[0],
};
}
}
const itemId = item.Id;
const res2 = await getMediaInfoApi(api).getPlaybackInfo(
const res = await getMediaInfoApi(api).getPlaybackInfo(
{
itemId: item.Id!,
},
{
method: "POST",
data: {
deviceProfile,
userId,
maxStreamingBitrate,
startTimeTicks,
autoOpenLiveStream: true,
mediaSourceId,
audioStreamIndex,
deviceProfile,
subtitleStreamIndex,
startTimeTicks,
isPlayback: true,
autoOpenLiveStream: true,
maxStreamingBitrate,
audioStreamIndex,
mediaSourceId,
},
},
);
if (res2.status !== 200) {
console.error("Error getting playback info:", res2.status, res2.statusText);
if (res.status !== 200) {
console.error("Error getting playback info:", res.status, res.statusText);
}
sessionId = res2.data.PlaySessionId || null;
sessionId = res.data.PlaySessionId || null;
mediaSource = res.data.MediaSources[0];
let transcodeUrl = mediaSource.TranscodingUrl;
mediaSource = res2.data.MediaSources?.find(
(source: MediaSourceInfo) => source.Id === mediaSourceId,
);
if (item.MediaType === "Video") {
if (mediaSource?.TranscodingUrl) {
const urlObj = new URL(api.basePath + mediaSource?.TranscodingUrl); // Create a URL object
// Get the updated URL
const transcodeUrl = urlObj.toString();
console.log("Video has transcoding URL:", `${transcodeUrl}`);
return {
url: transcodeUrl,
sessionId: sessionId,
mediaSource,
};
if (transcodeUrl) {
if (download) {
transcodeUrl = transcodeUrl.replace("master.m3u8", "stream");
}
const searchParams = new URLSearchParams({
playSessionId: sessionData?.PlaySessionId || "",
mediaSourceId: mediaSource?.Id || "",
static: "true",
subtitleStreamIndex: subtitleStreamIndex?.toString() || "",
audioStreamIndex: audioStreamIndex?.toString() || "",
deviceId: api.deviceInfo.id,
api_key: api.accessToken,
startTimeTicks: startTimeTicks.toString(),
maxStreamingBitrate: maxStreamingBitrate?.toString() || "",
userId: userId || "",
});
const directPlayUrl = `${
api.basePath
}/Videos/${itemId}/stream.mp4?${searchParams.toString()}`;
console.log("Video is being direct played:", directPlayUrl);
console.log("Video is being transcoded:", transcodeUrl);
return {
url: directPlayUrl,
sessionId: sessionId,
url: `${api.basePath}${transcodeUrl}`,
sessionId,
mediaSource,
};
}
Alert.alert("Error", "Could not play this item");
let downloadParams = {};
return null;
if (download) {
// We need to disable static so we can have a remux with subtitle.
downloadParams = {
subtitleMethod: "Embed",
enableSubtitlesInManifest: true,
static: "false",
allowVideoStreamCopy: true,
allowAudioStreamCopy: true,
playSessionId: sessionId || "",
container: "ts",
};
}
const streamParams = new URLSearchParams({
static: "true",
container: "mp4",
mediaSourceId: mediaSource?.Id || "",
subtitleStreamIndex: subtitleStreamIndex?.toString() || "",
audioStreamIndex: audioStreamIndex?.toString() || "",
deviceId: deviceId || api.deviceInfo.id,
api_key: api.accessToken,
startTimeTicks: startTimeTicks.toString(),
maxStreamingBitrate: maxStreamingBitrate?.toString() || "",
userId: userId || "",
...downloadParams,
});
const directPlayUrl = `${
api.basePath
}/Videos/${item.Id}/stream?${streamParams.toString()}`;
console.log("Video is being direct played:", directPlayUrl);
return {
url: directPlayUrl,
sessionId: sessionId || playSessionId,
mediaSource,
};
};

View File

@@ -1,7 +1,5 @@
import { getOrSetDeviceId } from "@/providers/JellyfinProvider";
import type { Settings } from "@/utils/atoms/settings";
import ios from "@/utils/profiles/ios";
import native from "@/utils/profiles/native";
import old from "@/utils/profiles/old";
import type { Api } from "@jellyfin/sdk";
import { DeviceProfile } from "@jellyfin/sdk/lib/generated-client";

View File

@@ -1,5 +1,5 @@
import type { Settings } from "@/utils/atoms/settings";
import native from "@/utils/profiles/native";
import generateDeviceProfile from "@/utils/profiles/native";
import type { Api } from "@jellyfin/sdk";
import type { AxiosResponse } from "axios";
import { getAuthHeaders } from "../jellyfin";
@@ -43,7 +43,7 @@ export const postCapabilities = async ({
],
supportsMediaControl: true,
id: sessionId,
DeviceProfile: native,
DeviceProfile: generateDeviceProfile(),
},
{
headers: getAuthHeaders(api),

View File

@@ -1,143 +0,0 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import MediaTypes from "../../constants/MediaTypes";
/**
* Device profile for Native video player
*/
export default {
Name: "1. Native iOS Video Profile",
MaxStaticBitrate: 100000000,
MaxStreamingBitrate: 120000000,
MusicStreamingTranscodingBitrate: 384000,
CodecProfiles: [
{
Type: MediaTypes.Video,
Codec: "h264,h265,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1",
},
{
Type: MediaTypes.Audio,
Codec: "aac,mp3,flac,alac,opus,vorbis,pcm,wma",
},
],
DirectPlayProfiles: [
{
Type: MediaTypes.Video,
Container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp",
VideoCodec: "h264,h265,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1",
AudioCodec: "aac,mp3,flac,alac,opus,vorbis,wma",
},
{
Type: MediaTypes.Audio,
Container: "mp3,aac,flac,alac,wav,ogg,wma",
AudioCodec: "mp3,aac,flac,alac,opus,vorbis,wma,pcm",
},
],
TranscodingProfiles: [
{
Type: MediaTypes.Video,
Context: "Streaming",
Protocol: "hls",
Container: "ts",
VideoCodec: "h264",
AudioCodec: "aac,mp3,ac3",
MaxAudioChannels: "8",
MinSegments: "2",
BreakOnNonKeyFrames: true,
},
{
Type: MediaTypes.Audio,
Context: "Streaming",
Protocol: "http",
Container: "mp3",
AudioCodec: "mp3",
MaxAudioChannels: "2",
},
],
ResponseProfiles: [
{
Container: "mkv",
MimeType: "video/x-matroska",
Type: MediaTypes.Video,
},
{
Container: "mp4",
MimeType: "video/mp4",
Type: MediaTypes.Video,
},
],
SubtitleProfiles: [
{ Format: "srt", Method: "Embed" },
{ Format: "srt", Method: "External" },
{ Format: "srt", Method: "Encode" },
{ Format: "ass", Method: "Embed" },
{ Format: "ass", Method: "External" },
{ Format: "ass", Method: "Encode" },
{ Format: "ssa", Method: "Embed" },
{ Format: "ssa", Method: "External" },
{ Format: "ssa", Method: "Encode" },
{ Format: "sub", Method: "Embed" },
{ Format: "sub", Method: "External" },
{ Format: "sub", Method: "Encode" },
{ Format: "vtt", Method: "Embed" },
{ Format: "vtt", Method: "External" },
{ Format: "vtt", Method: "Encode" },
{ Format: "ttml", Method: "Embed" },
{ Format: "ttml", Method: "External" },
{ Format: "ttml", Method: "Encode" },
{ Format: "pgs", Method: "Embed" },
{ Format: "pgs", Method: "External" },
{ Format: "pgs", Method: "Encode" },
{ Format: "dvdsub", Method: "Embed" },
{ Format: "dvdsub", Method: "External" },
{ Format: "dvdsub", Method: "Encode" },
{ Format: "dvbsub", Method: "Embed" },
{ Format: "dvbsub", Method: "External" },
{ Format: "dvbsub", Method: "Encode" },
{ Format: "xsub", Method: "Embed" },
{ Format: "xsub", Method: "External" },
{ Format: "xsub", Method: "Encode" },
{ Format: "mov_text", Method: "Embed" },
{ Format: "mov_text", Method: "External" },
{ Format: "mov_text", Method: "Encode" },
{ Format: "scc", Method: "Embed" },
{ Format: "scc", Method: "External" },
{ Format: "scc", Method: "Encode" },
{ Format: "smi", Method: "Embed" },
{ Format: "smi", Method: "External" },
{ Format: "smi", Method: "Encode" },
{ Format: "teletext", Method: "Embed" },
{ Format: "teletext", Method: "External" },
{ Format: "teletext", Method: "Encode" },
{ Format: "microdvd", Method: "Embed" },
{ Format: "microdvd", Method: "External" },
{ Format: "microdvd", Method: "Encode" },
{ Format: "mpl2", Method: "Embed" },
{ Format: "mpl2", Method: "External" },
{ Format: "mpl2", Method: "Encode" },
{ Format: "pjs", Method: "Embed" },
{ Format: "pjs", Method: "External" },
{ Format: "pjs", Method: "Encode" },
{ Format: "realtext", Method: "Embed" },
{ Format: "realtext", Method: "External" },
{ Format: "realtext", Method: "Encode" },
{ Format: "stl", Method: "Embed" },
{ Format: "stl", Method: "External" },
{ Format: "stl", Method: "Encode" },
{ Format: "subrip", Method: "Embed" },
{ Format: "subrip", Method: "External" },
{ Format: "subrip", Method: "Encode" },
{ Format: "subviewer", Method: "Embed" },
{ Format: "subviewer", Method: "External" },
{ Format: "subviewer", Method: "Encode" },
{ Format: "text", Method: "Embed" },
{ Format: "text", Method: "External" },
{ Format: "text", Method: "Encode" },
{ Format: "vplayer", Method: "Embed" },
{ Format: "vplayer", Method: "External" },
{ Format: "vplayer", Method: "Encode" },
],
};

View File

@@ -1,86 +0,0 @@
import MediaTypes from "../../constants/MediaTypes";
export default {
Name: "Expo Base Video Profile",
MaxStaticBitrate: 100000000,
MaxStreamingBitrate: 120000000,
MusicStreamingTranscodingBitrate: 384000,
CodecProfiles: [
{
Codec: "h264",
Conditions: [
{
Condition: "NotEquals",
IsRequired: false,
Property: "IsAnamorphic",
Value: "true",
},
{
Condition: "EqualsAny",
IsRequired: false,
Property: "VideoProfile",
Value: "high|main|baseline|constrained baseline",
},
{
Condition: "LessThanEqual",
IsRequired: false,
Property: "VideoLevel",
Value: "51",
},
{
Condition: "NotEquals",
IsRequired: false,
Property: "IsInterlaced",
Value: "true",
},
],
Type: MediaTypes.Video,
},
{
Codec: "hevc",
Conditions: [
{
Condition: "NotEquals",
IsRequired: false,
Property: "IsAnamorphic",
Value: "true",
},
{
Condition: "EqualsAny",
IsRequired: false,
Property: "VideoProfile",
Value: "main|main 10",
},
{
Condition: "LessThanEqual",
IsRequired: false,
Property: "VideoLevel",
Value: "183",
},
{
Condition: "NotEquals",
IsRequired: false,
Property: "IsInterlaced",
Value: "true",
},
],
Type: MediaTypes.Video,
},
],
ContainerProfiles: [],
DirectPlayProfiles: [],
ResponseProfiles: [
{
Container: "m4v",
MimeType: "video/mp4",
Type: MediaTypes.Video,
},
],
SubtitleProfiles: [
{
Format: "vtt",
Method: "Hls",
},
],
TranscodingProfiles: [],
};

View File

@@ -1,149 +0,0 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import MediaTypes from "../../constants/MediaTypes";
import BaseProfile from "./base";
/**
* Device profile for Expo Video player on iOS 13+
*/
export default {
...BaseProfile,
Name: "Expo iOS Video Profile",
DirectPlayProfiles: [
{
AudioCodec: "aac,mp3,ac3,eac3,flac,alac",
Container: "mp4,m4v",
Type: MediaTypes.Video,
VideoCodec: "hevc,h264",
},
{
AudioCodec: "aac,mp3,ac3,eac3,flac,alac",
Container: "mov",
Type: MediaTypes.Video,
VideoCodec: "hevc,h264",
},
{
Container: "mp3",
Type: MediaTypes.Audio,
},
{
Container: "aac",
Type: MediaTypes.Audio,
},
{
AudioCodec: "aac",
Container: "m4a",
Type: MediaTypes.Audio,
},
{
AudioCodec: "aac",
Container: "m4b",
Type: MediaTypes.Audio,
},
{
Container: "flac",
Type: MediaTypes.Audio,
},
{
Container: "alac",
Type: MediaTypes.Audio,
},
{
AudioCodec: "alac",
Container: "m4a",
Type: MediaTypes.Audio,
},
{
AudioCodec: "alac",
Container: "m4b",
Type: MediaTypes.Audio,
},
{
Container: "wav",
Type: MediaTypes.Audio,
},
],
TranscodingProfiles: [
{
AudioCodec: "aac",
BreakOnNonKeyFrames: true,
Container: "aac",
Context: "Streaming",
MaxAudioChannels: "6",
MinSegments: "2",
Protocol: "hls",
Type: MediaTypes.Audio,
},
{
AudioCodec: "aac",
Container: "aac",
Context: "Streaming",
MaxAudioChannels: "6",
Protocol: "http",
Type: MediaTypes.Audio,
},
{
AudioCodec: "mp3",
Container: "mp3",
Context: "Streaming",
MaxAudioChannels: "6",
Protocol: "http",
Type: MediaTypes.Audio,
},
{
AudioCodec: "wav",
Container: "wav",
Context: "Streaming",
MaxAudioChannels: "6",
Protocol: "http",
Type: MediaTypes.Audio,
},
{
AudioCodec: "mp3",
Container: "mp3",
Context: "Static",
MaxAudioChannels: "6",
Protocol: "http",
Type: MediaTypes.Audio,
},
{
AudioCodec: "aac",
Container: "aac",
Context: "Static",
MaxAudioChannels: "6",
Protocol: "http",
Type: MediaTypes.Audio,
},
{
AudioCodec: "wav",
Container: "wav",
Context: "Static",
MaxAudioChannels: "6",
Protocol: "http",
Type: MediaTypes.Audio,
},
{
AudioCodec: "aac,mp3",
BreakOnNonKeyFrames: true,
Container: "ts",
Context: "Streaming",
MaxAudioChannels: "6",
MinSegments: "2",
Protocol: "hls",
Type: MediaTypes.Video,
VideoCodec: "h264",
},
{
AudioCodec: "aac,mp3,ac3,eac3,flac,alac",
Container: "mp4",
Context: "Static",
Protocol: "http",
Type: MediaTypes.Video,
VideoCodec: "h264",
},
],
};

View File

@@ -1,3 +1,5 @@
import { Platform } from "react-native";
import DeviceInfo from "react-native-device-info";
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -5,132 +7,190 @@
*/
import MediaTypes from "../../constants/MediaTypes";
/**
* Device profile for Native video player
*/
export default {
Name: "1. Vlc Player",
MaxStaticBitrate: 999_999_999,
MaxStreamingBitrate: 999_999_999,
CodecProfiles: [
{
Type: MediaTypes.Video,
Codec: "h264,h265,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1",
},
{
Type: MediaTypes.Audio,
Codec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,pcm,wma",
},
],
DirectPlayProfiles: [
{
Type: MediaTypes.Video,
Container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp,hls",
VideoCodec:
"h264,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1,avi,mpeg,mpeg2video",
AudioCodec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,wma,dts",
},
{
Type: MediaTypes.Audio,
Container: "mp3,aac,flac,alac,wav,ogg,wma",
AudioCodec:
"mp3,aac,flac,alac,opus,vorbis,wma,pcm,mpa,wav,ogg,oga,webma,ape",
},
],
TranscodingProfiles: [
{
Type: MediaTypes.Video,
Context: "Streaming",
Protocol: "hls",
Container: "fmp4",
VideoCodec: "h264, hevc",
AudioCodec: "aac,mp3,ac3,dts",
},
{
Type: MediaTypes.Audio,
Context: "Streaming",
Protocol: "http",
Container: "mp3",
AudioCodec: "mp3",
MaxAudioChannels: "2",
},
],
SubtitleProfiles: [
// Official formats
{ Format: "vtt", Method: "Embed" },
{ Format: "vtt", Method: "External" },
// Helper function to detect Dolby Vision support
const supportsDolbyVision = async () => {
if (Platform.OS === "ios") {
const deviceModel = await DeviceInfo.getModel();
// iPhone 12 and newer generally support Dolby Vision
const modelNumber = Number.parseInt(deviceModel.replace(/iPhone/, ""), 10);
return !Number.isNaN(modelNumber) && modelNumber >= 12;
}
{ Format: "webvtt", Method: "Embed" },
{ Format: "webvtt", Method: "External" },
if (Platform.OS === "android") {
const apiLevel = await DeviceInfo.getApiLevel();
const isHighEndDevice =
(await DeviceInfo.getTotalMemory()) > 4 * 1024 * 1024 * 1024; // >4GB RAM
// Very rough approximation - Android 10+ on higher-end devices may support it
return apiLevel >= 29 && isHighEndDevice;
}
{ Format: "srt", Method: "Embed" },
{ Format: "srt", Method: "External" },
{ Format: "subrip", Method: "Embed" },
{ Format: "subrip", Method: "External" },
{ Format: "ttml", Method: "Embed" },
{ Format: "ttml", Method: "External" },
{ Format: "dvbsub", Method: "Embed" },
{ Format: "dvdsub", Method: "Encode" },
{ Format: "ass", Method: "Embed" },
{ Format: "ass", Method: "External" },
{ Format: "idx", Method: "Embed" },
{ Format: "idx", Method: "Encode" },
{ Format: "pgs", Method: "Embed" },
{ Format: "pgs", Method: "Encode" },
{ Format: "pgssub", Method: "Embed" },
{ Format: "pgssub", Method: "Encode" },
{ Format: "ssa", Method: "Embed" },
{ Format: "ssa", Method: "External" },
// Other formats
{ Format: "microdvd", Method: "Embed" },
{ Format: "microdvd", Method: "External" },
{ Format: "mov_text", Method: "Embed" },
{ Format: "mov_text", Method: "External" },
{ Format: "mpl2", Method: "Embed" },
{ Format: "mpl2", Method: "External" },
{ Format: "pjs", Method: "Embed" },
{ Format: "pjs", Method: "External" },
{ Format: "realtext", Method: "Embed" },
{ Format: "realtext", Method: "External" },
{ Format: "scc", Method: "Embed" },
{ Format: "scc", Method: "External" },
{ Format: "smi", Method: "Embed" },
{ Format: "smi", Method: "External" },
{ Format: "stl", Method: "Embed" },
{ Format: "stl", Method: "External" },
{ Format: "sub", Method: "Embed" },
{ Format: "sub", Method: "External" },
{ Format: "subviewer", Method: "Embed" },
{ Format: "subviewer", Method: "External" },
{ Format: "teletext", Method: "Embed" },
{ Format: "teletext", Method: "Encode" },
{ Format: "text", Method: "Embed" },
{ Format: "text", Method: "External" },
{ Format: "vplayer", Method: "Embed" },
{ Format: "vplayer", Method: "External" },
{ Format: "xsub", Method: "Embed" },
{ Format: "xsub", Method: "External" },
],
return false;
};
export const generateDeviceProfile = async () => {
const dolbyVisionSupported = await supportsDolbyVision();
/**
* Device profile for Native video player
*/
const profile = {
Name: "1. Vlc Player",
MaxStaticBitrate: 999_999_999,
MaxStreamingBitrate: 999_999_999,
CodecProfiles: [
{
Type: MediaTypes.Video,
Codec: "h264,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1",
},
{
Type: MediaTypes.Video,
Codec: "hevc,h265",
Conditions: [
{
Condition: "LessThanEqual",
Property: "VideoLevel",
Value: "153",
IsRequired: false,
},
// We'll add Dolby Vision condition below if not supported
],
},
{
Type: MediaTypes.Audio,
Codec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,pcm,wma",
},
],
DirectPlayProfiles: [
{
Type: MediaTypes.Video,
Container: "mp4,mkv,avi,mov,flv,ts,m2ts,webm,ogv,3gp,hls",
VideoCodec:
"h264,hevc,mpeg4,divx,xvid,wmv,vc1,vp8,vp9,av1,avi,mpeg,mpeg2video",
AudioCodec: "aac,ac3,eac3,mp3,flac,alac,opus,vorbis,wma,dts",
},
{
Type: MediaTypes.Audio,
Container: "mp3,aac,flac,alac,wav,ogg,wma",
AudioCodec:
"mp3,aac,flac,alac,opus,vorbis,wma,pcm,mpa,wav,ogg,oga,webma,ape",
},
],
TranscodingProfiles: [
{
Type: MediaTypes.Video,
Context: "Streaming",
Protocol: "hls",
Container: "fmp4",
VideoCodec: "h264, hevc",
AudioCodec: "aac,mp3,ac3,dts",
},
{
Type: MediaTypes.Audio,
Context: "Streaming",
Protocol: "http",
Container: "mp3",
AudioCodec: "mp3",
MaxAudioChannels: "2",
},
],
SubtitleProfiles: [
// Official formats
{ Format: "vtt", Method: "Embed" },
{ Format: "vtt", Method: "External" },
{ Format: "webvtt", Method: "Embed" },
{ Format: "webvtt", Method: "External" },
{ Format: "srt", Method: "Embed" },
{ Format: "srt", Method: "External" },
{ Format: "subrip", Method: "Embed" },
{ Format: "subrip", Method: "External" },
{ Format: "ttml", Method: "Embed" },
{ Format: "ttml", Method: "External" },
{ Format: "dvbsub", Method: "Embed" },
{ Format: "dvdsub", Method: "Encode" },
{ Format: "ass", Method: "Embed" },
{ Format: "ass", Method: "External" },
{ Format: "idx", Method: "Embed" },
{ Format: "idx", Method: "Encode" },
{ Format: "pgs", Method: "Embed" },
{ Format: "pgs", Method: "Encode" },
{ Format: "pgssub", Method: "Embed" },
{ Format: "pgssub", Method: "Encode" },
{ Format: "ssa", Method: "Embed" },
{ Format: "ssa", Method: "External" },
// Other formats
{ Format: "microdvd", Method: "Embed" },
{ Format: "microdvd", Method: "External" },
{ Format: "mov_text", Method: "Embed" },
{ Format: "mov_text", Method: "External" },
{ Format: "mpl2", Method: "Embed" },
{ Format: "mpl2", Method: "External" },
{ Format: "pjs", Method: "Embed" },
{ Format: "pjs", Method: "External" },
{ Format: "realtext", Method: "Embed" },
{ Format: "realtext", Method: "External" },
{ Format: "scc", Method: "Embed" },
{ Format: "scc", Method: "External" },
{ Format: "smi", Method: "Embed" },
{ Format: "smi", Method: "External" },
{ Format: "stl", Method: "Embed" },
{ Format: "stl", Method: "External" },
{ Format: "sub", Method: "Embed" },
{ Format: "sub", Method: "External" },
{ Format: "subviewer", Method: "Embed" },
{ Format: "subviewer", Method: "External" },
{ Format: "teletext", Method: "Embed" },
{ Format: "teletext", Method: "Encode" },
{ Format: "text", Method: "Embed" },
{ Format: "text", Method: "External" },
{ Format: "vplayer", Method: "Embed" },
{ Format: "vplayer", Method: "External" },
{ Format: "xsub", Method: "Embed" },
{ Format: "xsub", Method: "External" },
],
};
// Add Dolby Vision restriction if not supported
if (!dolbyVisionSupported) {
const hevcProfile = profile.CodecProfiles.find(
(p) => p.Type === MediaTypes.Video && p.Codec.includes("hevc"),
);
if (hevcProfile) {
hevcProfile.Conditions.push({
Condition: "NotEquals",
Property: "VideoRangeType",
Value: "DOVI", //no dolby vision at all
IsRequired: true,
});
}
}
return profile;
};
export default async () => {
return await generateDeviceProfile();
};