mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-11 00:10:24 +01:00
Compare commits
13 Commits
renovate/s
...
feat/andro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3e6f6311e | ||
|
|
2597b4af49 | ||
|
|
2ad9753957 | ||
|
|
c2c6bf0b45 | ||
|
|
1685571406 | ||
|
|
7f68506ceb | ||
|
|
36ed7539a2 | ||
|
|
ac41fa7863 | ||
|
|
8f82ac481a | ||
|
|
a242ff69fd | ||
|
|
cd5300e4ba | ||
|
|
326956dfda | ||
|
|
7528274249 |
29
.gitattributes
vendored
29
.gitattributes
vendored
@@ -1 +1,28 @@
|
||||
.modules/vlc-player/Frameworks/*.xcframework filter=lfs diff=lfs merge=lfs -text
|
||||
# Normalise line endings to LF for everyone. Files are stored as LF in git and
|
||||
# checked out as LF on every OS, so Windows clones stop producing CRLF churn
|
||||
# (no more "LF will be replaced by CRLF" warnings) regardless of core.autocrlf.
|
||||
* text=auto eol=lf
|
||||
|
||||
# Windows-only scripts must stay CRLF
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
|
||||
# Binary assets — never touched / never normalised
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.webp binary
|
||||
*.ico binary
|
||||
*.icns binary
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.mp3 binary
|
||||
*.mp4 binary
|
||||
*.mov binary
|
||||
*.pdf binary
|
||||
*.keystore binary
|
||||
*.jks binary
|
||||
*.p12 binary
|
||||
|
||||
102
.github/workflows/crowdin.yml
vendored
102
.github/workflows/crowdin.yml
vendored
@@ -1,51 +1,51 @@
|
||||
name: 🌐 Translation Sync
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
paths:
|
||||
- "translations/**"
|
||||
- "crowdin.yml"
|
||||
- "i18n.ts"
|
||||
- ".github/workflows/crowdin.yml"
|
||||
# Run weekly to pull new translations
|
||||
schedule:
|
||||
- cron: "0 2 * * 1" # Every Monday at 2 AM UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
sync-translations:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 📥 Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🌐 Sync Translations with Crowdin
|
||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: true
|
||||
download_translations: true
|
||||
localization_branch_name: I10n_crowdin_translations
|
||||
create_pull_request: true
|
||||
pull_request_title: "feat: New Crowdin Translations"
|
||||
pull_request_body: "New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)"
|
||||
pull_request_base_branch_name: "develop"
|
||||
pull_request_labels: "🌐 translation"
|
||||
# Quality control options
|
||||
skip_untranslated_strings: false
|
||||
skip_untranslated_files: false
|
||||
export_only_approved: false
|
||||
# Commit customization
|
||||
commit_message: "feat(i18n): update translations from Crowdin"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
name: 🌐 Translation Sync
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
paths:
|
||||
- "translations/**"
|
||||
- "crowdin.yml"
|
||||
- "i18n.ts"
|
||||
- ".github/workflows/crowdin.yml"
|
||||
# Run weekly to pull new translations
|
||||
schedule:
|
||||
- cron: "0 2 * * 1" # Every Monday at 2 AM UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
sync-translations:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 📥 Checkout Repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🌐 Sync Translations with Crowdin
|
||||
uses: crowdin/github-action@8868a33591d21088edfc398968173a3b98d51706 # v2.16.2
|
||||
with:
|
||||
upload_sources: true
|
||||
upload_translations: true
|
||||
download_translations: true
|
||||
localization_branch_name: I10n_crowdin_translations
|
||||
create_pull_request: true
|
||||
pull_request_title: "feat: New Crowdin Translations"
|
||||
pull_request_body: "New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)"
|
||||
pull_request_base_branch_name: "develop"
|
||||
pull_request_labels: "🌐 translation"
|
||||
# Quality control options
|
||||
skip_untranslated_strings: false
|
||||
skip_untranslated_files: false
|
||||
export_only_approved: false
|
||||
# Commit customization
|
||||
commit_message: "feat(i18n): update translations from Crowdin"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
60
.github/workflows/trivy-scan.yml
vendored
Normal file
60
.github/workflows/trivy-scan.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: 🛡️ Trivy Security Scan
|
||||
|
||||
# Filesystem scan (Streamyfin ships no container image): finds vulnerable dependencies,
|
||||
# leaked secrets and misconfigurations, and reports them to GitHub code scanning.
|
||||
# Runs post-merge + weekly (not on PRs — dependency-review already gates PRs, and SARIF
|
||||
# upload needs a write token that fork PRs don't get).
|
||||
on:
|
||||
push:
|
||||
branches: [develop, master]
|
||||
schedule:
|
||||
- cron: "50 7 * * 5" # Weekly, Friday 07:50 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: trivy-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
trivy:
|
||||
name: 🔎 Filesystem scan
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write # upload SARIF to code scanning
|
||||
steps:
|
||||
- name: 📥 Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
# Rotate the DB cache weekly (matches the scheduled scan): cache hits within the week
|
||||
# instead of a fresh immutable entry per run, still refreshing the DB every week.
|
||||
- name: 🗓️ Compute weekly Trivy cache key
|
||||
id: trivy-cache-key
|
||||
run: echo "value=trivy-db-${{ runner.os }}-$(date -u +%G-%V)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: 💾 Cache Trivy vulnerability DB
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.cache/trivy
|
||||
key: ${{ steps.trivy-cache-key.outputs.value }}
|
||||
restore-keys: trivy-db-${{ runner.os }}-
|
||||
|
||||
- name: 🔎 Run Trivy filesystem scan
|
||||
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
|
||||
with:
|
||||
scan-type: fs
|
||||
scan-ref: .
|
||||
scanners: vuln,secret,misconfig
|
||||
ignore-unfixed: true
|
||||
severity: CRITICAL,HIGH
|
||||
format: sarif
|
||||
output: trivy-results.sarif
|
||||
|
||||
- name: 📤 Upload results to code scanning
|
||||
uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
with:
|
||||
sarif_file: trivy-results.sarif
|
||||
category: trivy-fs
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,6 +1,5 @@
|
||||
# Dependencies and Package Managers
|
||||
node_modules/
|
||||
bun.lock
|
||||
bun.lockb
|
||||
package-lock.json
|
||||
|
||||
@@ -21,10 +20,8 @@ web-build/
|
||||
# Gradle caches (top-level + per-module native projects)
|
||||
**/.gradle/
|
||||
|
||||
# Module-specific Builds
|
||||
modules/mpv-player/android/build
|
||||
modules/player/android
|
||||
modules/hls-downloader/android/build
|
||||
# Native module build outputs (any module)
|
||||
modules/*/android/build/
|
||||
|
||||
# Generated Applications
|
||||
Streamyfin.app
|
||||
@@ -69,10 +66,6 @@ certs/
|
||||
|
||||
# Version and Backup Files
|
||||
/version-backup-*
|
||||
/modules/sf-player/android/build
|
||||
/modules/music-controls/android/build
|
||||
modules/background-downloader/android/build/*
|
||||
/modules/mpv-player/android/build
|
||||
|
||||
# ios:unsigned-build Artifacts
|
||||
build/
|
||||
|
||||
@@ -161,9 +161,7 @@ export default function FavoritesSeeAllScreen() {
|
||||
/>
|
||||
{!itemType ? (
|
||||
<View className='flex-1 items-center justify-center px-6'>
|
||||
<Text className='text-neutral-500'>
|
||||
{t("favorites.noData", { defaultValue: "No items found." })}
|
||||
</Text>
|
||||
<Text className='text-neutral-500'>{t("favorites.noData")}</Text>
|
||||
</View>
|
||||
) : isLoading ? (
|
||||
<View className='justify-center items-center h-full'>
|
||||
@@ -194,7 +192,7 @@ export default function FavoritesSeeAllScreen() {
|
||||
ListEmptyComponent={
|
||||
<View className='flex flex-col items-center justify-center h-full py-12'>
|
||||
<Text className='font-bold text-xl text-neutral-500'>
|
||||
{t("home.no_items", { defaultValue: "No items" })}
|
||||
{t("home.no_items")}
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
|
||||
@@ -137,12 +137,12 @@ export default function DownloadsPage() {
|
||||
deleteFileByType("Episode")
|
||||
.then(() =>
|
||||
toast.success(
|
||||
t("home.downloads.toasts.deleted_all_tvseries_successfully"),
|
||||
t("home.downloads.toasts.deleted_all_series_successfully"),
|
||||
),
|
||||
)
|
||||
.catch((reason) => {
|
||||
writeToLog("ERROR", reason);
|
||||
toast.error(t("home.downloads.toasts.failed_to_delete_all_tvseries"));
|
||||
toast.error(t("home.downloads.toasts.failed_to_delete_all_series"));
|
||||
});
|
||||
const deleteOtherMedia = () =>
|
||||
Promise.all(
|
||||
@@ -207,7 +207,7 @@ export default function DownloadsPage() {
|
||||
<View className='mb-4'>
|
||||
<View className='flex flex-row items-center justify-between mb-2 px-4'>
|
||||
<Text className='text-lg font-bold'>
|
||||
{t("home.downloads.tvseries")}
|
||||
{t("home.downloads.series")}
|
||||
</Text>
|
||||
<View className='bg-purple-600 rounded-full h-6 w-6 flex items-center justify-center'>
|
||||
<Text className='text-xs font-bold'>
|
||||
@@ -288,7 +288,7 @@ export default function DownloadsPage() {
|
||||
{t("home.downloads.delete_all_movies_button")}
|
||||
</Button>
|
||||
<Button color='purple' onPress={deleteShows}>
|
||||
{t("home.downloads.delete_all_tvseries_button")}
|
||||
{t("home.downloads.delete_all_series_button")}
|
||||
</Button>
|
||||
{otherMedia.length > 0 && (
|
||||
<Button color='purple' onPress={deleteOtherMedia}>
|
||||
|
||||
@@ -179,18 +179,15 @@ export default function SettingsTV() {
|
||||
// Handle clearing all cache in the entire app
|
||||
const handleClearCache = async () => {
|
||||
Alert.alert(
|
||||
t("home.settings.storage.clear_all_cache_confirm", "Clear All Cache?"),
|
||||
t(
|
||||
"home.settings.storage.clear_all_cache_confirm_desc",
|
||||
"Are you sure you want to clear all cached data? This will clear all cached images, music files, subtitles, and query caches. Your settings and login session will be kept.",
|
||||
),
|
||||
t("home.settings.storage.clear_all_cache_confirm"),
|
||||
t("home.settings.storage.clear_all_cache_confirm_desc"),
|
||||
[
|
||||
{
|
||||
text: t("common.cancel", "Cancel"),
|
||||
text: t("common.cancel"),
|
||||
style: "cancel",
|
||||
},
|
||||
{
|
||||
text: t("common.ok", "OK"),
|
||||
text: t("common.ok"),
|
||||
onPress: async () => {
|
||||
try {
|
||||
// 1. Clear React Query Cache (memory & MMKV)
|
||||
@@ -243,11 +240,8 @@ export default function SettingsTV() {
|
||||
} catch (error) {
|
||||
console.error("Failed to clear cache:", error);
|
||||
Alert.alert(
|
||||
t("home.settings.toasts.error_deleting_files", "Error"),
|
||||
t(
|
||||
"home.settings.storage.clear_all_cache_error_desc",
|
||||
"An error occurred while clearing the cache.",
|
||||
),
|
||||
t("home.settings.toasts.error_deleting_files"),
|
||||
t("home.settings.storage.clear_all_cache_error_desc"),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,16 +3,24 @@ import {
|
||||
type NativeBottomTabNavigationEventMap,
|
||||
type NativeBottomTabNavigationOptions,
|
||||
} from "@bottom-tabs/react-navigation";
|
||||
import { withLayoutContext } from "expo-router";
|
||||
import { Stack, useSegments, withLayoutContext } from "expo-router";
|
||||
import type {
|
||||
ParamListBase,
|
||||
TabNavigationState,
|
||||
} from "expo-router/react-navigation";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, View } from "react-native";
|
||||
import { SystemBars } from "react-native-edge-to-edge";
|
||||
import type { TVNavBarTab } from "@/components/tv/TVNavBar";
|
||||
import { TVNavBar } from "@/components/tv/TVNavBar";
|
||||
import { Colors } from "@/constants/Colors";
|
||||
import { useTVHomeBackHandler } from "@/hooks/useTVBackHandler";
|
||||
import useRouter from "@/hooks/useAppRouter";
|
||||
import {
|
||||
isTabRoute,
|
||||
useTVHomeBackHandler,
|
||||
useTVTabRootBackHandler,
|
||||
} from "@/hooks/useTVBackHandler";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { eventBus } from "@/utils/eventBus";
|
||||
|
||||
@@ -33,13 +41,107 @@ export const NativeTabs = withLayoutContext<
|
||||
NativeBottomTabNavigationEventMap
|
||||
>(Navigator);
|
||||
|
||||
const IS_ANDROID_TV = Platform.isTV && Platform.OS === "android";
|
||||
|
||||
function TVTabLayout() {
|
||||
const { settings } = useSettings();
|
||||
const { t } = useTranslation();
|
||||
const segments = useSegments();
|
||||
const router = useRouter();
|
||||
|
||||
const currentTab = segments.find(isTabRoute);
|
||||
const atTabRoot = isTabRoute(segments[segments.length - 1] ?? "");
|
||||
|
||||
const tabs: TVNavBarTab[] = useMemo(
|
||||
() =>
|
||||
[
|
||||
{ key: "(home)", label: t("tabs.home") },
|
||||
{ key: "(search)", label: t("tabs.search") },
|
||||
{ key: "(favorites)", label: t("tabs.favorites") },
|
||||
!settings?.streamyStatsServerUrl || settings?.hideWatchlistsTab
|
||||
? null
|
||||
: { key: "(watchlists)", label: t("watchlists.title") },
|
||||
{ key: "(libraries)", label: t("tabs.library") },
|
||||
!settings?.showCustomMenuLinks
|
||||
? null
|
||||
: { key: "(custom-links)", label: t("tabs.custom_links") },
|
||||
{ key: "(settings)", label: t("tabs.settings") },
|
||||
].filter((tab): tab is TVNavBarTab => tab !== null),
|
||||
[
|
||||
settings?.streamyStatsServerUrl,
|
||||
settings?.hideWatchlistsTab,
|
||||
settings?.showCustomMenuLinks,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
const activeTabKey = currentTab ?? "(home)";
|
||||
|
||||
const visibleKeys = useMemo(
|
||||
() => new Set(tabs.map((tab) => tab.key)),
|
||||
[tabs],
|
||||
);
|
||||
|
||||
const handleTabChange = useCallback(
|
||||
(key: string) => {
|
||||
if (key === currentTab) return;
|
||||
|
||||
if (key === "(home)") eventBus.emit("scrollToTop");
|
||||
if (key === "(search)") eventBus.emit("searchTabPressed");
|
||||
|
||||
router.replace(`/(auth)/(tabs)/${key}`);
|
||||
},
|
||||
[currentTab, router],
|
||||
);
|
||||
|
||||
const navigateHome = useCallback(() => {
|
||||
router.replace("/(auth)/(tabs)/(home)");
|
||||
}, [router]);
|
||||
useTVTabRootBackHandler(navigateHome, atTabRoot, currentTab);
|
||||
|
||||
// If current tab is no longer visible (setting changed), navigate to home
|
||||
useEffect(() => {
|
||||
if (!visibleKeys.has(activeTabKey) && activeTabKey !== "(home)") {
|
||||
router.replace("/(auth)/(tabs)/(home)");
|
||||
}
|
||||
}, [visibleKeys, activeTabKey, router]);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<SystemBars hidden={false} style='light' />
|
||||
<Stack
|
||||
screenOptions={{ headerShown: false, animation: "none" }}
|
||||
initialRouteName='(home)'
|
||||
>
|
||||
<Stack.Screen name='index' redirect />
|
||||
</Stack>
|
||||
<TVNavBar
|
||||
tabs={tabs}
|
||||
activeTabKey={activeTabKey}
|
||||
onTabChange={handleTabChange}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TabLayout() {
|
||||
const { settings } = useSettings();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Handle TV back button - prevent app exit when at root
|
||||
// Must be called before any conditional return (rules of hooks)
|
||||
useTVHomeBackHandler();
|
||||
|
||||
if (IS_ANDROID_TV) {
|
||||
return <TVTabLayout />;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<SystemBars hidden={false} style='light' />
|
||||
|
||||
@@ -63,7 +63,7 @@ export const TrackSheet: React.FC<Props> = ({
|
||||
<Text numberOfLines={1}>
|
||||
{selected === -1 && streamType === "Subtitle"
|
||||
? t("common.none")
|
||||
: selectedSteam?.DisplayTitle || t("common.select", "Select")}
|
||||
: selectedSteam?.DisplayTitle || t("common.select")}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
@@ -379,7 +379,7 @@ export const TVHeroCarousel: React.FC<TVHeroCarouselProps> = ({
|
||||
if (items.length === 0) return null;
|
||||
|
||||
// Extra top padding for tvOS to clear the menu bar
|
||||
const tvosTopPadding = Platform.OS === "ios" ? scaleSize(145) : 0;
|
||||
const tvosTopPadding = scaleSize(145);
|
||||
const heroHeight = SCREEN_HEIGHT * sizes.padding.heroHeight;
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { useAtom } from "jotai";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import { Platform, ScrollView, TextInput, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { TVDiscover } from "@/components/jellyseerr/discover/TVDiscover";
|
||||
@@ -231,26 +231,48 @@ export const TVSearchPage: React.FC<TVSearchPageProps> = ({
|
||||
paddingTop: insets.top + TOP_PADDING,
|
||||
}}
|
||||
>
|
||||
{/* Native tvOS search field (SwiftUI `.searchable`, our `tv-search`
|
||||
module). It renders the native search bar + grid keyboard and
|
||||
forwards typed text into the existing query pipeline via setSearch;
|
||||
our own results grid renders below. */}
|
||||
{/* No horizontal margin here: the native tvOS search bar centers itself
|
||||
and renders a trailing "Hold to Dictate in <Language>" hint. Extra
|
||||
margins squeeze the bar's width and clip that trailing hint, so let
|
||||
the native view span the full width and own its own insets. */}
|
||||
<View
|
||||
style={{
|
||||
marginBottom: 24,
|
||||
height: SEARCH_AREA_HEIGHT,
|
||||
}}
|
||||
>
|
||||
<TvSearchView
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
placeholder={t("search.search")}
|
||||
onChangeText={(e) => setSearch(e.nativeEvent.text)}
|
||||
/>
|
||||
</View>
|
||||
{/* Search bar: native tvOS SwiftUI `.searchable` on Apple TV, standard
|
||||
TextInput fallback on Android TV (the native module is Apple-only). */}
|
||||
{Platform.OS === "ios" ? (
|
||||
<View
|
||||
style={{
|
||||
marginBottom: 24,
|
||||
height: SEARCH_AREA_HEIGHT,
|
||||
}}
|
||||
>
|
||||
{/* No horizontal margin here: the native tvOS search bar centers
|
||||
itself and renders a trailing "Hold to Dictate" hint. */}
|
||||
<TvSearchView
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
placeholder={t("search.search")}
|
||||
onChangeText={(e) => setSearch(e.nativeEvent.text)}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
marginHorizontal: HORIZONTAL_PADDING,
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
style={{
|
||||
height: 56,
|
||||
width: "100%",
|
||||
backgroundColor: "#262626",
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: 20,
|
||||
fontSize: 28,
|
||||
color: "#fff",
|
||||
}}
|
||||
placeholder={t("search.search")}
|
||||
placeholderTextColor='rgba(255,255,255,0.4)'
|
||||
onChangeText={setSearch}
|
||||
defaultValue=''
|
||||
autoFocus={false}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
|
||||
148
components/tv/TVNavBar.tsx
Normal file
148
components/tv/TVNavBar.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import React from "react";
|
||||
import { Animated, Pressable, ScrollView, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Text } from "@/components/common/Text";
|
||||
import { TVPadding } from "@/constants/TVSizes";
|
||||
import { useScaledTVTypography } from "@/constants/TVTypography";
|
||||
import { scaleSize } from "@/utils/scaleSize";
|
||||
import { useTVFocusAnimation } from "./hooks/useTVFocusAnimation";
|
||||
|
||||
export interface TVNavBarTab {
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface TVNavBarProps {
|
||||
tabs: TVNavBarTab[];
|
||||
activeTabKey: string;
|
||||
onTabChange: (key: string) => void;
|
||||
style?: ViewStyleProp;
|
||||
}
|
||||
|
||||
const TVNavBarTabItem: React.FC<{
|
||||
label: string;
|
||||
isActive: boolean;
|
||||
onSelect: () => void;
|
||||
onLayout: (e: {
|
||||
nativeEvent: { layout: { x: number; width: number } };
|
||||
}) => void;
|
||||
hasTVPreferredFocus: boolean;
|
||||
}> = ({ label, isActive, onSelect, onLayout, hasTVPreferredFocus }) => {
|
||||
const typography = useScaledTVTypography();
|
||||
const { focused, handleFocus, handleBlur, animatedStyle } =
|
||||
useTVFocusAnimation({
|
||||
scaleAmount: 1.05,
|
||||
duration: 120,
|
||||
});
|
||||
|
||||
const bg = focused
|
||||
? "rgba(255, 255, 255, 0.95)"
|
||||
: isActive
|
||||
? "rgba(255, 255, 255, 0.15)"
|
||||
: "transparent";
|
||||
|
||||
const textColor = focused
|
||||
? "#000"
|
||||
: isActive
|
||||
? "#fff"
|
||||
: "rgba(255, 255, 255, 0.7)";
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onSelect}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
hasTVPreferredFocus={hasTVPreferredFocus}
|
||||
onLayout={onLayout}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
animatedStyle,
|
||||
{
|
||||
backgroundColor: bg,
|
||||
borderRadius: scaleSize(24),
|
||||
borderWidth: isActive && !focused ? 1 : 0,
|
||||
borderColor: "rgba(255, 255, 255, 0.3)",
|
||||
paddingHorizontal: scaleSize(28),
|
||||
paddingVertical: scaleSize(14),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: typography.heading,
|
||||
color: textColor,
|
||||
fontWeight: isActive || focused ? "600" : "400",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export const TVNavBar: React.FC<TVNavBarProps> = ({
|
||||
tabs,
|
||||
activeTabKey,
|
||||
onTabChange,
|
||||
style,
|
||||
}) => {
|
||||
const scrollRef = React.useRef<ScrollView>(null);
|
||||
const tabLayouts = React.useRef<Record<string, { x: number; width: number }>>(
|
||||
{},
|
||||
);
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const handleTabLayout = React.useCallback(
|
||||
(key: string) =>
|
||||
(e: { nativeEvent: { layout: { x: number; width: number } } }) => {
|
||||
tabLayouts.current[key] = e.nativeEvent.layout;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleTabChange = React.useCallback(
|
||||
(key: string) => {
|
||||
onTabChange(key);
|
||||
|
||||
const layout = tabLayouts.current[key];
|
||||
if (layout && scrollRef.current) {
|
||||
scrollRef.current.scrollTo({
|
||||
x: Math.max(0, layout.x - TVPadding.horizontal / 2),
|
||||
animated: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
[onTabChange],
|
||||
);
|
||||
|
||||
if (tabs.length === 0) return null;
|
||||
|
||||
return (
|
||||
<View style={[{ paddingTop: insets.top + 16, paddingBottom: 8 }, style]}>
|
||||
<ScrollView
|
||||
ref={scrollRef}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps='handled'
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
gap: scaleSize(12),
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<TVNavBarTabItem
|
||||
key={tab.key}
|
||||
label={tab.label}
|
||||
isActive={tab.key === activeTabKey}
|
||||
onSelect={() => handleTabChange(tab.key)}
|
||||
onLayout={handleTabLayout(tab.key)}
|
||||
hasTVPreferredFocus={tab.key === activeTabKey}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -35,6 +35,8 @@ export type { TVLanguageCardProps } from "./TVLanguageCard";
|
||||
export { TVLanguageCard } from "./TVLanguageCard";
|
||||
export type { TVMetadataBadgesProps } from "./TVMetadataBadges";
|
||||
export { TVMetadataBadges } from "./TVMetadataBadges";
|
||||
export type { TVNavBarProps, TVNavBarTab } from "./TVNavBar";
|
||||
export { TVNavBar } from "./TVNavBar";
|
||||
export type { TVNextEpisodeCountdownProps } from "./TVNextEpisodeCountdown";
|
||||
export { TVNextEpisodeCountdown } from "./TVNextEpisodeCountdown";
|
||||
export type { TVOptionButtonProps } from "./TVOptionButton";
|
||||
|
||||
@@ -4,41 +4,42 @@ import { Platform } from "react-native";
|
||||
import {
|
||||
disableTVMenuKeyInterception,
|
||||
enableTVMenuKeyInterception,
|
||||
useTVBackPress,
|
||||
} from "./useTVBackPress";
|
||||
|
||||
export { enableTVMenuKeyInterception } from "./useTVBackPress";
|
||||
|
||||
/** All tab route names used in the bottom tab navigator. */
|
||||
export const TAB_ROUTES = [
|
||||
"(home)",
|
||||
"(search)",
|
||||
"(favorites)",
|
||||
"(libraries)",
|
||||
"(watchlists)",
|
||||
"(custom-links)",
|
||||
"(settings)",
|
||||
] as const;
|
||||
|
||||
export type TabRoute = (typeof TAB_ROUTES)[number];
|
||||
|
||||
/** Check if a segment string is a tab route. */
|
||||
export function isTabRoute(s: string): s is TabRoute {
|
||||
return (TAB_ROUTES as readonly string[]).includes(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we're at the root of a tab
|
||||
*/
|
||||
function isAtTabRoot(segments: string[]): boolean {
|
||||
const lastSegment = segments[segments.length - 1];
|
||||
const tabNames = [
|
||||
"(home)",
|
||||
"(search)",
|
||||
"(favorites)",
|
||||
"(libraries)",
|
||||
"(watchlists)",
|
||||
"(settings)",
|
||||
"(custom-links)",
|
||||
];
|
||||
return tabNames.includes(lastSegment) || lastSegment === "index";
|
||||
return isTabRoute(lastSegment) || lastSegment === "index";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current tab name from segments
|
||||
*/
|
||||
function getCurrentTab(segments: string[]): string | undefined {
|
||||
return segments.find(
|
||||
(s) =>
|
||||
s === "(home)" ||
|
||||
s === "(search)" ||
|
||||
s === "(favorites)" ||
|
||||
s === "(libraries)" ||
|
||||
s === "(watchlists)" ||
|
||||
s === "(settings)" ||
|
||||
s === "(custom-links)",
|
||||
);
|
||||
function getCurrentTab(segments: string[]): TabRoute | undefined {
|
||||
return segments.find(isTabRoute);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +50,6 @@ function getCurrentTab(segments: string[]): string | undefined {
|
||||
export function useTVHomeBackHandler() {
|
||||
const segments = useSegments();
|
||||
|
||||
// Get current state
|
||||
const currentTab = getCurrentTab(segments);
|
||||
const atTabRoot = isAtTabRoot(segments);
|
||||
const isOnHomeRoot = atTabRoot && currentTab === "(home)";
|
||||
@@ -65,3 +65,24 @@ export function useTVHomeBackHandler() {
|
||||
enableTVMenuKeyInterception();
|
||||
}, [isOnHomeRoot]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles back press at a non-Home tab root on Android TV by navigating to Home.
|
||||
*
|
||||
* Without NativeTabs, the Stack navigator used for the Android TV nav bar has no
|
||||
* built-in tab-level back handling — pressing back at a tab root would pop the
|
||||
* Stack entirely and exit the tab navigator. This hook intercepts that and routes
|
||||
* to Home instead.
|
||||
*/
|
||||
export function useTVTabRootBackHandler(
|
||||
onNavigateHome: () => void,
|
||||
isAtTabRoot: boolean,
|
||||
currentTab: string | undefined,
|
||||
) {
|
||||
useTVBackPress(() => {
|
||||
if (!Platform.isTV || Platform.OS !== "android") return false;
|
||||
if (!isAtTabRoot || currentTab === "(home)") return false;
|
||||
onNavigateHome();
|
||||
return true;
|
||||
}, [isAtTabRoot, currentTab, onNavigateHome]);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<receiver
|
||||
android:name=".TvRecommendationsReceiver"
|
||||
android:exported="true">
|
||||
<receiver android:name=".TvRecommendationsReceiver" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.tv.action.INITIALIZE_PROGRAMS" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.media.tv.action.PREVIEW_PROGRAM_BROWSABLE_DISABLED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
|
||||
@@ -16,12 +16,13 @@ import androidx.tvprovider.media.tv.PreviewProgram
|
||||
import androidx.tvprovider.media.tv.TvContractCompat
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.security.MessageDigest
|
||||
|
||||
internal object TvRecommendationsPublisher {
|
||||
private const val TAG = "TvRecommendations"
|
||||
private const val PREFS_NAME = "StreamyfinTvRecommendations"
|
||||
private const val KEY_PAYLOAD = "payload"
|
||||
private const val KEY_CHANNEL_ID = "channelId"
|
||||
private const val KEY_CHANNEL_ID_PREFIX = "channelId_"
|
||||
private const val KEY_PROGRAM_IDS = "programIds"
|
||||
private const val DEFAULT_CHANNEL_NAME = "Continue and Next Up"
|
||||
|
||||
@@ -61,31 +62,61 @@ internal object TvRecommendationsPublisher {
|
||||
|
||||
fun clear(context: Context): Boolean {
|
||||
val prefs = preferences(context)
|
||||
val channelId = prefs.getLong(KEY_CHANNEL_ID, -1L)
|
||||
val programIds = prefs.getString(KEY_PROGRAM_IDS, null)?.let(::JSONObject)
|
||||
val contentResolver = context.contentResolver
|
||||
|
||||
if (programIds != null) {
|
||||
// KEY_PROGRAM_IDS is now { channelId: "{ providerId: programId }" }
|
||||
val allProgramIds = prefs.getString(KEY_PROGRAM_IDS, null)?.let(::JSONObject)
|
||||
if (allProgramIds != null) {
|
||||
var deletedPrograms = 0
|
||||
val keys = programIds.keys()
|
||||
while (keys.hasNext()) {
|
||||
val key = keys.next()
|
||||
val programId = programIds.optLong(key, -1L)
|
||||
if (programId > 0L) {
|
||||
contentResolver.delete(
|
||||
TvContractCompat.buildPreviewProgramUri(programId),
|
||||
null,
|
||||
null
|
||||
)
|
||||
deletedPrograms += 1
|
||||
val channelKeys = allProgramIds.keys()
|
||||
while (channelKeys.hasNext()) {
|
||||
val channelIdStr = channelKeys.next()
|
||||
val programIdsJson = allProgramIds.optString(channelIdStr)
|
||||
if (programIdsJson.isBlank()) continue
|
||||
|
||||
try {
|
||||
val programIds = JSONObject(programIdsJson)
|
||||
val keys = programIds.keys()
|
||||
while (keys.hasNext()) {
|
||||
val providerId = keys.next()
|
||||
val programId = programIds.optLong(providerId, -1L)
|
||||
if (programId > 0L) {
|
||||
deletePreviewProgram(contentResolver, programId)
|
||||
deletedPrograms += 1
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "clear(): failed to parse programIds for channel $channelIdStr", e)
|
||||
}
|
||||
|
||||
// Notify the channel
|
||||
val channelId = channelIdStr.toLongOrNull() ?: -1L
|
||||
if (channelId > 0L) {
|
||||
try {
|
||||
contentResolver.notifyChange(TvContractCompat.buildChannelUri(channelId), null)
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "clear(): lost provider permission, cannot notify channel $channelId", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove per-channel pref
|
||||
prefs.edit().remove("programIds_$channelIdStr").apply()
|
||||
}
|
||||
Log.d(TAG, "clear(): deleted $deletedPrograms preview program(s)")
|
||||
}
|
||||
|
||||
if (channelId > 0L) {
|
||||
contentResolver.notifyChange(TvContractCompat.buildChannelUri(channelId), null)
|
||||
Log.d(TAG, "clear(): notified channel $channelId")
|
||||
// Also handle legacy format (flat { providerId: programId }) for migration
|
||||
val legacyProgramIds = prefs.getString("legacy_" + KEY_PROGRAM_IDS, null)?.let(::JSONObject)
|
||||
if (legacyProgramIds != null) {
|
||||
val keys = legacyProgramIds.keys()
|
||||
while (keys.hasNext()) {
|
||||
val key = keys.next()
|
||||
val programId = legacyProgramIds.optLong(key, -1L)
|
||||
if (programId > 0L) {
|
||||
deletePreviewProgram(contentResolver, programId)
|
||||
}
|
||||
}
|
||||
prefs.edit().remove("legacy_" + KEY_PROGRAM_IDS).apply()
|
||||
}
|
||||
|
||||
prefs.edit()
|
||||
@@ -96,128 +127,274 @@ internal object TvRecommendationsPublisher {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a single preview program from the TvProvider.
|
||||
* Called by clear(), synchronize() (stale programs), and TvRecommendationsReceiver (user removed).
|
||||
*/
|
||||
fun deletePreviewProgram(context: Context, programId: Long) {
|
||||
try {
|
||||
context.contentResolver.delete(
|
||||
TvContractCompat.buildPreviewProgramUri(programId),
|
||||
null,
|
||||
null
|
||||
)
|
||||
Log.d(TAG, "deletePreviewProgram(): deleted programId=$programId")
|
||||
|
||||
// Also remove from stored programIds prefs
|
||||
removeProgramFromPrefs(context, programId)
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "deletePreviewProgram(): lost provider permission for programId=$programId", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deletePreviewProgram(contentResolver: android.content.ContentResolver, programId: Long) {
|
||||
try {
|
||||
contentResolver.delete(
|
||||
TvContractCompat.buildPreviewProgramUri(programId),
|
||||
null,
|
||||
null
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "deletePreviewProgram(): lost provider permission for programId=$programId", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeProgramFromPrefs(context: Context, programId: Long) {
|
||||
val prefs = preferences(context)
|
||||
val programIdsJson = prefs.getString(KEY_PROGRAM_IDS, null) ?: return
|
||||
try {
|
||||
val channelMap = JSONObject(programIdsJson)
|
||||
val channelKeys = channelMap.keys()
|
||||
while (channelKeys.hasNext()) {
|
||||
val channelId = channelKeys.next()
|
||||
val inner = channelMap.optJSONObject(channelId) ?: continue
|
||||
val providerKeys = inner.keys()
|
||||
while (providerKeys.hasNext()) {
|
||||
val providerId = providerKeys.next()
|
||||
if (inner.optLong(providerId, -1L) == programId) {
|
||||
inner.remove(providerId)
|
||||
if (inner.length() == 0) {
|
||||
channelMap.remove(channelId)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
prefs.edit().putString(KEY_PROGRAM_IDS, channelMap.toString()).apply()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "removeProgramFromPrefs(): failed to update prefs for programId=$programId", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun synchronize(context: Context, payload: JSONObject): Boolean {
|
||||
val sections = payload.optJSONArray("sections") ?: JSONArray()
|
||||
val firstSection = if (sections.length() > 0) sections.optJSONObject(0) else null
|
||||
val sectionTitle = firstSection?.optString("title")?.takeIf { it.isNotBlank() } ?: DEFAULT_CHANNEL_NAME
|
||||
val items = firstSection?.optJSONArray("items") ?: JSONArray()
|
||||
|
||||
Log.d(
|
||||
TAG,
|
||||
"synchronize(): using section \"$sectionTitle\" with ${items.length()} item(s)"
|
||||
)
|
||||
|
||||
val channelId = getOrCreateChannel(context, sectionTitle)
|
||||
if (channelId <= 0L) {
|
||||
Log.w(TAG, "synchronize(): failed to get or create preview channel")
|
||||
if (sections.length() == 0) {
|
||||
Log.w(TAG, "synchronize(): no sections in payload")
|
||||
return false
|
||||
}
|
||||
|
||||
Log.d(TAG, "synchronize(): publishing into channelId=$channelId")
|
||||
val prefs = preferences(context)
|
||||
val allNextProgramIds = JSONObject()
|
||||
var totalActive = 0
|
||||
var totalDeleted = 0
|
||||
|
||||
val previousProgramIds = preferences(context)
|
||||
.getString(KEY_PROGRAM_IDS, null)
|
||||
?.let(::JSONObject)
|
||||
?: JSONObject()
|
||||
val nextProgramIds = JSONObject()
|
||||
val activeProviderIds = mutableSetOf<String>()
|
||||
for (sectionIndex in 0 until sections.length()) {
|
||||
val section = sections.optJSONObject(sectionIndex) ?: continue
|
||||
val sectionTitle = section.optString("title")?.takeIf { it.isNotBlank() } ?: DEFAULT_CHANNEL_NAME
|
||||
val items = section.optJSONArray("items") ?: JSONArray()
|
||||
|
||||
for (index in 0 until items.length()) {
|
||||
val item = items.optJSONObject(index) ?: continue
|
||||
val providerId = item.optString("id")
|
||||
if (providerId.isBlank()) continue
|
||||
|
||||
val programId = upsertPreviewProgram(
|
||||
context = context,
|
||||
channelId = channelId,
|
||||
item = item,
|
||||
previousProgramId = previousProgramIds.optLong(providerId, -1L),
|
||||
weight = index
|
||||
Log.d(
|
||||
TAG,
|
||||
"synchronize(): section \"$sectionTitle\" ($sectionIndex/${sections.length()}) with ${items.length()} item(s)"
|
||||
)
|
||||
|
||||
if (programId > 0L) {
|
||||
activeProviderIds += providerId
|
||||
nextProgramIds.put(providerId, programId)
|
||||
Log.d(TAG, "synchronize(): upserted program for item=$providerId programId=$programId")
|
||||
val channelId = getOrCreateChannel(context, sectionTitle)
|
||||
if (channelId <= 0L) {
|
||||
Log.w(TAG, "synchronize(): failed to get or create channel for \"$sectionTitle\"")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var deletedPrograms = 0
|
||||
val previousKeys = previousProgramIds.keys()
|
||||
while (previousKeys.hasNext()) {
|
||||
val providerId = previousKeys.next()
|
||||
if (activeProviderIds.contains(providerId)) continue
|
||||
// Per Android docs: check channel.isBrowsable() and request if needed.
|
||||
if (!isChannelBrowsable(context, channelId)) {
|
||||
Log.d(TAG, "synchronize(): channel $channelId not browsable, requesting browsable")
|
||||
TvContractCompat.requestChannelBrowsable(context, channelId)
|
||||
}
|
||||
|
||||
val programId = previousProgramIds.optLong(providerId, -1L)
|
||||
if (programId > 0L) {
|
||||
context.contentResolver.delete(
|
||||
TvContractCompat.buildPreviewProgramUri(programId),
|
||||
null,
|
||||
null
|
||||
val prefKey = "programIds_$channelId"
|
||||
val previousProgramIds = prefs.getString(prefKey, null)
|
||||
?.let(::JSONObject)
|
||||
?: JSONObject()
|
||||
val nextProgramIds = JSONObject()
|
||||
val activeProviderIds = mutableSetOf<String>()
|
||||
|
||||
for (index in 0 until items.length()) {
|
||||
val item = items.optJSONObject(index) ?: continue
|
||||
val providerId = item.optString("id")
|
||||
if (providerId.isBlank()) continue
|
||||
|
||||
val programId = upsertPreviewProgram(
|
||||
context = context,
|
||||
channelId = channelId,
|
||||
item = item,
|
||||
previousProgramId = previousProgramIds.optLong(providerId, -1L),
|
||||
weight = index
|
||||
)
|
||||
deletedPrograms += 1
|
||||
Log.d(TAG, "synchronize(): deleted stale programId=$programId for item=$providerId")
|
||||
|
||||
if (programId > 0L) {
|
||||
activeProviderIds += providerId
|
||||
nextProgramIds.put(providerId, programId)
|
||||
Log.d(TAG, "synchronize(): upserted program for item=$providerId programId=$programId")
|
||||
}
|
||||
}
|
||||
|
||||
var deletedPrograms = 0
|
||||
val previousKeys = previousProgramIds.keys()
|
||||
while (previousKeys.hasNext()) {
|
||||
val providerId = previousKeys.next()
|
||||
if (activeProviderIds.contains(providerId)) continue
|
||||
|
||||
val programId = previousProgramIds.optLong(providerId, -1L)
|
||||
if (programId > 0L) {
|
||||
deletePreviewProgram(context, programId)
|
||||
deletedPrograms += 1
|
||||
Log.d(TAG, "synchronize(): deleted stale programId=$programId for item=$providerId")
|
||||
}
|
||||
}
|
||||
|
||||
allNextProgramIds.put(channelId.toString(), nextProgramIds.toString())
|
||||
prefs.edit().putString(prefKey, nextProgramIds.toString()).apply()
|
||||
totalActive += activeProviderIds.size
|
||||
totalDeleted += deletedPrograms
|
||||
|
||||
logProviderState(context, channelId)
|
||||
}
|
||||
|
||||
preferences(context)
|
||||
.edit()
|
||||
.putLong(KEY_CHANNEL_ID, channelId)
|
||||
.putString(KEY_PROGRAM_IDS, nextProgramIds.toString())
|
||||
.apply()
|
||||
|
||||
logProviderState(context, channelId)
|
||||
// Store all channel program IDs for clear() to use
|
||||
prefs.edit().putString(KEY_PROGRAM_IDS, allNextProgramIds.toString()).apply()
|
||||
|
||||
Log.d(
|
||||
TAG,
|
||||
"synchronize(): completed with ${activeProviderIds.size} active program(s), deleted $deletedPrograms stale program(s)"
|
||||
"synchronize(): completed across ${sections.length()} section(s), $totalActive active program(s), deleted $totalDeleted stale program(s)"
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Query provider to check if a channel is browsable.
|
||||
* Per Android docs: "check channel.isBrowsable() before updating programs."
|
||||
*/
|
||||
private fun isChannelBrowsable(context: Context, channelId: Long): Boolean {
|
||||
return try {
|
||||
context.contentResolver.query(
|
||||
TvContractCompat.buildChannelUri(channelId),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val browsableIndex = cursor.getColumnIndex(TvContractCompat.Channels.COLUMN_BROWSABLE)
|
||||
if (browsableIndex >= 0) cursor.getInt(browsableIndex) == 1 else true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} ?: false
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "isChannelBrowsable(): lost provider permission for channelId=$channelId", e)
|
||||
true // Assume browsable if we can't check, to avoid blocking updates
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query provider to verify a channel actually exists.
|
||||
* Prevents the channel-delete-recreate bug: if update() returns 0 rows
|
||||
* we must first check whether the channel was deleted by the system
|
||||
* or if the update simply failed for another reason.
|
||||
*/
|
||||
private fun channelExistsInProvider(context: Context, channelId: Long): Boolean {
|
||||
return try {
|
||||
context.contentResolver.query(
|
||||
TvContractCompat.buildChannelUri(channelId),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)?.use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
} ?: false
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "channelExistsInProvider(): lost provider permission for channelId=$channelId", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOrCreateChannel(context: Context, displayName: String): Long {
|
||||
val prefs = preferences(context)
|
||||
val existingChannelId = prefs.getLong(KEY_CHANNEL_ID, -1L)
|
||||
val channelKey = getChannelKey(displayName)
|
||||
val existingChannelId = prefs.getLong(channelKey, -1L)
|
||||
val contentResolver = context.contentResolver
|
||||
|
||||
if (existingChannelId > 0L) {
|
||||
val updated = Channel.Builder()
|
||||
.setType(TvContractCompat.Channels.TYPE_PREVIEW)
|
||||
.setDisplayName(displayName)
|
||||
.setAppLinkIntentUri(buildIntentUri(context, "streamyfin://"))
|
||||
.build()
|
||||
// Query provider first to verify channel actually exists (prevents recreate bug)
|
||||
val exists = channelExistsInProvider(context, existingChannelId)
|
||||
|
||||
val updatedRows = contentResolver.update(
|
||||
TvContractCompat.buildChannelUri(existingChannelId),
|
||||
updated.toContentValues(),
|
||||
null,
|
||||
null
|
||||
)
|
||||
if (exists) {
|
||||
// Channel exists — update it in place, never recreate
|
||||
val updated = Channel.Builder()
|
||||
.setType(TvContractCompat.Channels.TYPE_PREVIEW)
|
||||
.setDisplayName(displayName)
|
||||
.setAppLinkIntentUri(buildIntentUri(context, "streamyfin://"))
|
||||
.build()
|
||||
|
||||
if (updatedRows > 0) {
|
||||
TvContractCompat.requestChannelBrowsable(context, existingChannelId)
|
||||
storeChannelLogo(context, existingChannelId)
|
||||
Log.d(TAG, "getOrCreateChannel(): updated existing channelId=$existingChannelId and requested browsable")
|
||||
return existingChannelId
|
||||
try {
|
||||
val updatedRows = contentResolver.update(
|
||||
TvContractCompat.buildChannelUri(existingChannelId),
|
||||
updated.toContentValues(),
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
if (updatedRows > 0) {
|
||||
TvContractCompat.requestChannelBrowsable(context, existingChannelId)
|
||||
storeChannelLogo(context, existingChannelId)
|
||||
Log.d(TAG, "getOrCreateChannel(): updated existing channelId=$existingChannelId and requested browsable")
|
||||
return existingChannelId
|
||||
}
|
||||
|
||||
// Update returned 0 rows but channel exists — log and return existing ID, don't recreate
|
||||
Log.e(TAG, "getOrCreateChannel(): update returned 0 for existing channelId=$existingChannelId but channel exists — not recreating")
|
||||
return existingChannelId
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "getOrCreateChannel(): lost provider permission updating channelId=$existingChannelId", e)
|
||||
return existingChannelId
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(TAG, "getOrCreateChannel(): stored channelId=$existingChannelId was stale, recreating")
|
||||
prefs.edit().remove(KEY_CHANNEL_ID).apply()
|
||||
// Channel truly doesn't exist in provider — recreate
|
||||
Log.w(TAG, "getOrCreateChannel(): channelId=$existingChannelId not in provider, recreating")
|
||||
prefs.edit().remove(channelKey).apply()
|
||||
}
|
||||
|
||||
// Create a new channel
|
||||
val channel = Channel.Builder()
|
||||
.setType(TvContractCompat.Channels.TYPE_PREVIEW)
|
||||
.setDisplayName(displayName)
|
||||
.setAppLinkIntentUri(buildIntentUri(context, "streamyfin://"))
|
||||
.build()
|
||||
|
||||
val channelUri = contentResolver.insert(
|
||||
TvContractCompat.Channels.CONTENT_URI,
|
||||
channel.toContentValues()
|
||||
) ?: return -1L
|
||||
val channelUri = try {
|
||||
contentResolver.insert(
|
||||
TvContractCompat.Channels.CONTENT_URI,
|
||||
channel.toContentValues()
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
Log.e(TAG, "getOrCreateChannel(): lost provider permission, cannot create channel", e)
|
||||
null
|
||||
} ?: return -1L
|
||||
|
||||
val channelId = ContentUris.parseId(channelUri)
|
||||
prefs.edit().putLong(channelKey, channelId).apply()
|
||||
TvContractCompat.requestChannelBrowsable(context, channelId)
|
||||
storeChannelLogo(context, channelId)
|
||||
Log.d(TAG, "getOrCreateChannel(): created new channelId=$channelId displayName=\"$displayName\"")
|
||||
@@ -225,6 +402,10 @@ internal object TvRecommendationsPublisher {
|
||||
return channelId
|
||||
}
|
||||
|
||||
private fun getChannelKey(displayName: String): String {
|
||||
return KEY_CHANNEL_ID_PREFIX + displayName.hashCode()
|
||||
}
|
||||
|
||||
private fun upsertPreviewProgram(
|
||||
context: Context,
|
||||
channelId: Long,
|
||||
@@ -249,42 +430,67 @@ internal object TvRecommendationsPublisher {
|
||||
builder.setDescription(it)
|
||||
}
|
||||
|
||||
// Per Android docs: use unique URIs for all images to avoid stale cache
|
||||
imageUrl.takeIf { it.isNotBlank() }?.let {
|
||||
val imageUri = Uri.parse(it)
|
||||
val uniqueImageUrl = appendCacheBuster(it)
|
||||
val imageUri = Uri.parse(uniqueImageUrl)
|
||||
builder.setPosterArtUri(imageUri)
|
||||
builder.setThumbnailUri(imageUri)
|
||||
}
|
||||
|
||||
|
||||
val contentValues = builder.build().toContentValues()
|
||||
val contentResolver = context.contentResolver
|
||||
|
||||
if (previousProgramId > 0L) {
|
||||
val updatedRows = contentResolver.update(
|
||||
TvContractCompat.buildPreviewProgramUri(previousProgramId),
|
||||
contentValues,
|
||||
null,
|
||||
null
|
||||
)
|
||||
try {
|
||||
val updatedRows = contentResolver.update(
|
||||
TvContractCompat.buildPreviewProgramUri(previousProgramId),
|
||||
contentValues,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
if (updatedRows > 0) {
|
||||
Log.d(TAG, "upsertPreviewProgram(): updated existing programId=$previousProgramId")
|
||||
return previousProgramId
|
||||
if (updatedRows > 0) {
|
||||
Log.d(TAG, "upsertPreviewProgram(): updated existing programId=$previousProgramId")
|
||||
return previousProgramId
|
||||
}
|
||||
|
||||
Log.w(TAG, "upsertPreviewProgram(): existing programId=$previousProgramId was stale, inserting new row")
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "upsertPreviewProgram(): lost provider permission for programId=$previousProgramId", e)
|
||||
}
|
||||
|
||||
Log.w(TAG, "upsertPreviewProgram(): existing programId=$previousProgramId was stale, inserting new row")
|
||||
}
|
||||
|
||||
val insertedUri = contentResolver.insert(
|
||||
TvContractCompat.PreviewPrograms.CONTENT_URI,
|
||||
contentValues
|
||||
) ?: return -1L
|
||||
val insertedUri = try {
|
||||
contentResolver.insert(
|
||||
TvContractCompat.PreviewPrograms.CONTENT_URI,
|
||||
contentValues
|
||||
)
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "upsertPreviewProgram(): lost provider permission, cannot insert program", e)
|
||||
null
|
||||
} ?: return -1L
|
||||
|
||||
val programId = ContentUris.parseId(insertedUri)
|
||||
Log.d(TAG, "upsertPreviewProgram(): inserted new programId=$programId")
|
||||
return programId
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a stable cache key derived from the image URL.
|
||||
* The Jellyfin image URLs already include a `tag=` query param (etag)
|
||||
* that changes whenever the image content changes, so a deterministic
|
||||
* hash of the URL is sufficient — the param only changes when the URL
|
||||
* (and therefore the image) actually changes, avoiding unnecessary
|
||||
* re-downloads on every sync.
|
||||
*/
|
||||
private fun appendCacheBuster(imageUrl: String): String {
|
||||
val digest = MessageDigest.getInstance("MD5").digest(imageUrl.toByteArray())
|
||||
val hash = digest.joinToString("") { "%02x".format(it) }.substring(0, 16)
|
||||
val separator = if (imageUrl.contains("?")) "&" else "?"
|
||||
return "$imageUrl${separator}_v=$hash"
|
||||
}
|
||||
|
||||
private fun buildIntentUri(context: Context, deepLink: String): Uri {
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse(deepLink)
|
||||
@@ -306,13 +512,17 @@ internal object TvRecommendationsPublisher {
|
||||
|
||||
private fun storeChannelLogo(context: Context, channelId: Long) {
|
||||
val bitmap = applicationIconBitmap(context) ?: return
|
||||
val outputStream = context.contentResolver.openOutputStream(
|
||||
TvContractCompat.buildChannelLogoUri(channelId)
|
||||
) ?: return
|
||||
try {
|
||||
val outputStream = context.contentResolver.openOutputStream(
|
||||
TvContractCompat.buildChannelLogoUri(channelId)
|
||||
) ?: return
|
||||
|
||||
outputStream.use { stream ->
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
||||
stream.flush()
|
||||
outputStream.use { stream ->
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
||||
stream.flush()
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "storeChannelLogo(): lost provider permission for channelId=$channelId", e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,9 +551,14 @@ internal object TvRecommendationsPublisher {
|
||||
return bitmap
|
||||
}
|
||||
|
||||
fun getChannelId(context: Context, displayName: String = DEFAULT_CHANNEL_NAME): Long {
|
||||
return preferences(context).getLong(getChannelKey(displayName), -1L)
|
||||
}
|
||||
|
||||
private fun preferences(context: Context): SharedPreferences {
|
||||
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
private fun logProviderState(context: Context, channelId: Long) {
|
||||
val contentResolver = context.contentResolver
|
||||
|
||||
@@ -372,8 +587,10 @@ internal object TvRecommendationsPublisher {
|
||||
Log.w(TAG, "logProviderState(): channelId=$channelId exists=false")
|
||||
}
|
||||
} ?: Log.w(TAG, "logProviderState(): channel query returned null for channelId=$channelId")
|
||||
} catch (error: Exception) {
|
||||
Log.w(TAG, "logProviderState(): failed to query channelId=$channelId", error)
|
||||
}
|
||||
} catch (error: SecurityException) {
|
||||
Log.w(TAG, "logProviderState(): lost provider permission for channelId=$channelId", error)
|
||||
} catch (error: Exception) {
|
||||
Log.w(TAG, "logProviderState(): failed to query channelId=$channelId", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,24 @@ package expo.modules.tvrecommendations
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ContentUris
|
||||
import android.util.Log
|
||||
import androidx.tvprovider.media.tv.TvContractCompat
|
||||
|
||||
class TvRecommendationsReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != TvContractCompat.ACTION_INITIALIZE_PROGRAMS) {
|
||||
return
|
||||
when (intent.action) {
|
||||
TvContractCompat.ACTION_INITIALIZE_PROGRAMS -> {
|
||||
Log.d("TvRecommendations", "Handling INITIALIZE_PROGRAMS broadcast")
|
||||
TvRecommendationsPublisher.refreshFromCache(context)
|
||||
}
|
||||
"android.media.tv.action.PREVIEW_PROGRAM_BROWSABLE_DISABLED" -> {
|
||||
val programId = intent.data?.let { ContentUris.parseId(it) } ?: -1L
|
||||
if (programId > 0L) {
|
||||
Log.d("TvRecommendations", "Handling PREVIEW_PROGRAM_BROWSABLE_DISABLED for programId=$programId")
|
||||
TvRecommendationsPublisher.deletePreviewProgram(context, programId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.d("TvRecommendations", "Handling INITIALIZE_PROGRAMS broadcast")
|
||||
TvRecommendationsPublisher.refreshFromCache(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { requireNativeView } from "expo";
|
||||
import * as React from "react";
|
||||
import type { View } from "react-native";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
import type { TvSearchViewProps } from "./TvSearchView.types";
|
||||
|
||||
// The native TvSearchModule is Apple-only (tvOS SwiftUI `.searchable`).
|
||||
// On Android the component is never rendered, but we must avoid calling
|
||||
// `requireNativeView` at module-scope because it would crash on import.
|
||||
const NativeView: React.ComponentType<
|
||||
TvSearchViewProps & React.RefAttributes<View>
|
||||
> = requireNativeView("TvSearchModule");
|
||||
> =
|
||||
Platform.OS === "ios"
|
||||
? requireNativeView("TvSearchModule")
|
||||
: ((() => null) as any);
|
||||
|
||||
/**
|
||||
* Forwards its ref to the underlying native view so it can be used as a
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "التنزيلات",
|
||||
"tvseries": "مسلسلات",
|
||||
"series": "مسلسلات",
|
||||
"movies": "أفلام",
|
||||
"queue": "قائمة الانتظار",
|
||||
"other_media": "وسائط أخرى",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "لا توجد عناصر في قائمة الانتظار",
|
||||
"no_downloaded_items": "لا توجد عناصر تم تنزيلها",
|
||||
"delete_all_movies_button": "حذف جميع الأفلام",
|
||||
"delete_all_tvseries_button": "حذف جميع المسلسلات",
|
||||
"delete_all_series_button": "حذف جميع المسلسلات",
|
||||
"delete_all_button": "حذف الكل",
|
||||
"delete_all_other_media_button": "حذف الوسائط الأخرى",
|
||||
"active_download": "التنزيل الجاري",
|
||||
@@ -542,8 +542,8 @@
|
||||
"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": "فشل حذف جميع المسلسلات",
|
||||
"deleted_all_series_successfully": "تم حذف جميع المسلسلات بنجاح!",
|
||||
"failed_to_delete_all_series": "فشل حذف جميع المسلسلات",
|
||||
"deleted_media_successfully": "تم حذف الوسائط الأخرى بنجاح!",
|
||||
"failed_to_delete_media": "فشل حذف الوسائط الأخرى",
|
||||
"download_deleted": "تم حذف التنزيل",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Descàrregues",
|
||||
"tvseries": "Sèries",
|
||||
"series": "Sèries",
|
||||
"movies": "Pel·lícules",
|
||||
"queue": "Cua",
|
||||
"other_media": "Other media",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "No hi ha elements a la cua",
|
||||
"no_downloaded_items": "No hi ha elements descarregats",
|
||||
"delete_all_movies_button": "Suprimeix totes les pel·lícules",
|
||||
"delete_all_tvseries_button": "Suprimeix totes les sèries",
|
||||
"delete_all_series_button": "Suprimeix totes les sèries",
|
||||
"delete_all_button": "Suprimeix-ho tot",
|
||||
"delete_all_other_media_button": "Delete other media",
|
||||
"active_download": "Descàrrega activa",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "No teniu permís per descarregar fitxers.",
|
||||
"deleted_all_movies_successfully": "S'han suprimit totes les pel·lícules correctament!",
|
||||
"failed_to_delete_all_movies": "No s'han pogut suprimir totes les pel·lícules",
|
||||
"deleted_all_tvseries_successfully": "S'han suprimit totes les sèries correctament!",
|
||||
"failed_to_delete_all_tvseries": "No s'han pogut suprimir totes les sèries",
|
||||
"deleted_all_series_successfully": "S'han suprimit totes les sèries correctament!",
|
||||
"failed_to_delete_all_series": "No s'han pogut suprimir totes les sèries",
|
||||
"deleted_media_successfully": "Deleted other media Successfully!",
|
||||
"failed_to_delete_media": "Failed to Delete other media",
|
||||
"download_deleted": "Download Deleted",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Stahování",
|
||||
"tvseries": "Televizní série",
|
||||
"series": "Televizní série",
|
||||
"movies": "Filmy",
|
||||
"queue": "Fronta",
|
||||
"other_media": "Ostatní média",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Žádné položky ve frontě",
|
||||
"no_downloaded_items": "Žádné stažené položky",
|
||||
"delete_all_movies_button": "Odstranit všechny filmy",
|
||||
"delete_all_tvseries_button": "Odstranit všechny TV-série",
|
||||
"delete_all_series_button": "Odstranit všechny TV-série",
|
||||
"delete_all_button": "Smazat vše",
|
||||
"delete_all_other_media_button": "Odstranit ostatní média",
|
||||
"active_download": "Aktivní stahování",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Nemáte oprávnění stahovat soubory.",
|
||||
"deleted_all_movies_successfully": "Všechny filmy byly úspěšně smazány!",
|
||||
"failed_to_delete_all_movies": "Nepodařilo se odstranit všechny filmy",
|
||||
"deleted_all_tvseries_successfully": "Všechny série televizorů byly úspěšně smazány!",
|
||||
"failed_to_delete_all_tvseries": "Nepodařilo se odstranit všechny TV-série",
|
||||
"deleted_all_series_successfully": "Všechny série televizorů byly úspěšně smazány!",
|
||||
"failed_to_delete_all_series": "Nepodařilo se odstranit všechny TV-série",
|
||||
"deleted_media_successfully": "Ostatní média úspěšně smazána!",
|
||||
"failed_to_delete_media": "Nepodařilo se odstranit ostatní média",
|
||||
"download_deleted": "Stahování smazáno",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Downloads",
|
||||
"tvseries": "TV-serier",
|
||||
"series": "TV-serier",
|
||||
"movies": "Film",
|
||||
"queue": "Kø",
|
||||
"other_media": "Andre medier",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Ingen elementer i køen",
|
||||
"no_downloaded_items": "Ingen downloadede elementer",
|
||||
"delete_all_movies_button": "Slet alle film",
|
||||
"delete_all_tvseries_button": "Slet alle TV-serier",
|
||||
"delete_all_series_button": "Slet alle TV-serier",
|
||||
"delete_all_button": "Slet alle",
|
||||
"delete_all_other_media_button": "Slet andre medier",
|
||||
"active_download": "Aktiv download",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Du har ikke tilladelse til at downloade filer.",
|
||||
"deleted_all_movies_successfully": "Alle film er slettet med succes!",
|
||||
"failed_to_delete_all_movies": "Kunne ikke slette alle film",
|
||||
"deleted_all_tvseries_successfully": "Alle TV-serier er slettet med succes!",
|
||||
"failed_to_delete_all_tvseries": "Kunne ikke slette alle TV-serier",
|
||||
"deleted_all_series_successfully": "Alle TV-serier er slettet med succes!",
|
||||
"failed_to_delete_all_series": "Kunne ikke slette alle TV-serier",
|
||||
"deleted_media_successfully": "Slettede andre medier med succes!",
|
||||
"failed_to_delete_media": "Kunne ikke slette andre medier",
|
||||
"download_deleted": "Download Slettet",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Downloads",
|
||||
"tvseries": "Serien",
|
||||
"series": "Serien",
|
||||
"movies": "Filme",
|
||||
"queue": "Warteschlange",
|
||||
"other_media": "Andere Medien",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Keine Elemente in der Warteschlange",
|
||||
"no_downloaded_items": "Keine heruntergeladenen Elemente",
|
||||
"delete_all_movies_button": "Alle Filme löschen",
|
||||
"delete_all_tvseries_button": "Alle Serien löschen",
|
||||
"delete_all_series_button": "Alle Serien löschen",
|
||||
"delete_all_button": "Alles löschen",
|
||||
"delete_all_other_media_button": "Alle anderen Medien löschen",
|
||||
"active_download": "Aktiver Download",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Du hast keine Berechtigung, Dateien herunterzuladen",
|
||||
"deleted_all_movies_successfully": "Alle Filme erfolgreich gelöscht!",
|
||||
"failed_to_delete_all_movies": "Fehler beim Löschen aller Filme",
|
||||
"deleted_all_tvseries_successfully": "Alle Serien erfolgreich gelöscht!",
|
||||
"failed_to_delete_all_tvseries": "Fehler beim Löschen aller Serien",
|
||||
"deleted_all_series_successfully": "Alle Serien erfolgreich gelöscht!",
|
||||
"failed_to_delete_all_series": "Fehler beim Löschen aller Serien",
|
||||
"deleted_media_successfully": "Andere Medien erfolgreich gelöscht!",
|
||||
"failed_to_delete_media": "Fehler beim Löschen anderer Medien",
|
||||
"download_deleted": "Download gelöscht",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Λήψεις",
|
||||
"tvseries": "Τηλεόραση-Σειρά",
|
||||
"series": "Τηλεόραση-Σειρά",
|
||||
"movies": "Ταινίες",
|
||||
"queue": "Ουρά",
|
||||
"other_media": "Άλλα μέσα",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Δεν υπάρχουν αντικείμενα στην ουρά",
|
||||
"no_downloaded_items": "Δεν Έχουν Ληφθεί Αντικείμενα",
|
||||
"delete_all_movies_button": "Διαγραφή Όλων Των Ταινιών",
|
||||
"delete_all_tvseries_button": "Διαγραφή Όλων Των Τηλεοπτικών Σειρών",
|
||||
"delete_all_series_button": "Διαγραφή Όλων Των Τηλεοπτικών Σειρών",
|
||||
"delete_all_button": "Διαγραφή Όλων",
|
||||
"delete_all_other_media_button": "Διαγραφή άλλων μέσων",
|
||||
"active_download": "Ενεργή Λήψη",
|
||||
@@ -542,8 +542,8 @@
|
||||
"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": "Αποτυχία διαγραφής Όλων των TV-Series",
|
||||
"deleted_all_series_successfully": "Διαγράφηκε Όλη Η Τηλεόραση-Σειρά Επιτυχία!",
|
||||
"failed_to_delete_all_series": "Αποτυχία διαγραφής Όλων των TV-Series",
|
||||
"deleted_media_successfully": "Διαγράφηκε άλλο μέσο επιτυχώς!",
|
||||
"failed_to_delete_media": "Αποτυχία διαγραφής άλλων πολυμέσων",
|
||||
"download_deleted": "Η Λήψη Διαγράφηκε",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -229,14 +229,14 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Elŝutoj",
|
||||
"tvseries": "Televidaj serioj",
|
||||
"series": "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_series_button": "Forigi ĉiujn Televidajn Seriojn",
|
||||
"delete_all_button": "Forigi ĉion",
|
||||
"active_download": "Aktiva elŝuto",
|
||||
"no_active_downloads": "Neniuj aktivaj elŝutoj",
|
||||
@@ -253,8 +253,8 @@
|
||||
"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",
|
||||
"deleted_all_series_successfully": "Sukcese forigis ĉiujn Televidajn Seriojn!",
|
||||
"failed_to_delete_all_series": "Malsukcesis forigi ĉiujn Televidajn Seriojn",
|
||||
"download_cancelled": "Elŝuto nuligita",
|
||||
"could_not_cancel_download": "Ne povis nuligi elŝuton",
|
||||
"download_completed": "Elŝuto finita",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Descargas",
|
||||
"tvseries": "Series",
|
||||
"series": "Series",
|
||||
"movies": "Películas",
|
||||
"queue": "Cola",
|
||||
"other_media": "Otros medios",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "No hay ítems en la cola",
|
||||
"no_downloaded_items": "No hay ítems descargados",
|
||||
"delete_all_movies_button": "Eliminar todas las películas",
|
||||
"delete_all_tvseries_button": "Eliminar todas las series",
|
||||
"delete_all_series_button": "Eliminar todas las series",
|
||||
"delete_all_button": "Eliminar todo",
|
||||
"delete_all_other_media_button": "Eliminar otros medios",
|
||||
"active_download": "Descarga activa",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "No tienes permiso para descargar archivos.",
|
||||
"deleted_all_movies_successfully": "¡Todas las películas eliminadas con éxito!",
|
||||
"failed_to_delete_all_movies": "Error al eliminar todas las películas",
|
||||
"deleted_all_tvseries_successfully": "¡Todas las series eliminadas con éxito!",
|
||||
"failed_to_delete_all_tvseries": "Error al eliminar todas las series",
|
||||
"deleted_all_series_successfully": "¡Todas las series eliminadas con éxito!",
|
||||
"failed_to_delete_all_series": "Error al eliminar todas las series",
|
||||
"deleted_media_successfully": "¡Otros medios eliminados con éxito!",
|
||||
"failed_to_delete_media": "Error al eliminar otros medios",
|
||||
"download_deleted": "Descarga eliminada",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Lataukset",
|
||||
"tvseries": "TV-sarjat",
|
||||
"series": "TV-sarjat",
|
||||
"movies": "Elokuvat",
|
||||
"queue": "Jonot",
|
||||
"other_media": "Muu media",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Ei kohteita jonossa",
|
||||
"no_downloaded_items": "Ei ladattuja kohteita",
|
||||
"delete_all_movies_button": "Poista kaikki elokuvat",
|
||||
"delete_all_tvseries_button": "Poista kaikki TV-sarjat",
|
||||
"delete_all_series_button": "Poista kaikki TV-sarjat",
|
||||
"delete_all_button": "Poista kaikki",
|
||||
"delete_all_other_media_button": "Poista muu media",
|
||||
"active_download": "Aktiivinen lataus",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Sinulla ei ole lupaa ladata tiedostoja.",
|
||||
"deleted_all_movies_successfully": "Kaikki elokuvat poistettu onnistuneesti!",
|
||||
"failed_to_delete_all_movies": "Kaikkien elokuvien poistaminen epäonnistui",
|
||||
"deleted_all_tvseries_successfully": "Kaikki TV-sarjat poistettu onnistuneesti!",
|
||||
"failed_to_delete_all_tvseries": "Kaikkien TV-sarjojen poistaminen epäonnistui",
|
||||
"deleted_all_series_successfully": "Kaikki TV-sarjat poistettu onnistuneesti!",
|
||||
"failed_to_delete_all_series": "Kaikkien TV-sarjojen poistaminen epäonnistui",
|
||||
"deleted_media_successfully": "Muu media poistettu onnistuneesti!",
|
||||
"failed_to_delete_media": "Muiden medioiden poistaminen epäonnistui",
|
||||
"download_deleted": "Lataus Poistettu",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Téléchargements",
|
||||
"tvseries": "Séries",
|
||||
"series": "Séries",
|
||||
"movies": "Films",
|
||||
"queue": "File d'attente",
|
||||
"other_media": "Autres médias",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Aucun téléchargement de média dans la file d'attente",
|
||||
"no_downloaded_items": "Aucun média téléchargé",
|
||||
"delete_all_movies_button": "Supprimer tous les films",
|
||||
"delete_all_tvseries_button": "Supprimer toutes les séries",
|
||||
"delete_all_series_button": "Supprimer toutes les séries",
|
||||
"delete_all_button": "Supprimer tous les médias",
|
||||
"delete_all_other_media_button": "Supprimer un autre média",
|
||||
"active_download": "Téléchargement actif",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Vous n'êtes pas autorisé à télécharger des fichiers.",
|
||||
"deleted_all_movies_successfully": "Tous les films ont été supprimés avec succès !",
|
||||
"failed_to_delete_all_movies": "Échec de la suppression de tous les films",
|
||||
"deleted_all_tvseries_successfully": "Toutes les séries ont été supprimées avec succès !",
|
||||
"failed_to_delete_all_tvseries": "Échec de la suppression de toutes les séries",
|
||||
"deleted_all_series_successfully": "Toutes les séries ont été supprimées avec succès !",
|
||||
"failed_to_delete_all_series": "Échec de la suppression de toutes les séries",
|
||||
"deleted_media_successfully": "Les autres médias ont été supprimés avec succès !",
|
||||
"failed_to_delete_media": "Échec de la suppression d'un autre média",
|
||||
"download_deleted": "Téléchargement supprimé",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "הורדות",
|
||||
"tvseries": "סדרות",
|
||||
"series": "סדרות",
|
||||
"movies": "סרטים",
|
||||
"queue": "תוֹר",
|
||||
"other_media": "תוכן אחר",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "אין פרטים בתור",
|
||||
"no_downloaded_items": "אין פריטים שהורדו",
|
||||
"delete_all_movies_button": "מחק את כל הסרטים",
|
||||
"delete_all_tvseries_button": "מחק את כל הסדרות",
|
||||
"delete_all_series_button": "מחק את כל הסדרות",
|
||||
"delete_all_button": "מחק הכל",
|
||||
"delete_all_other_media_button": "מחק שאר תוכן",
|
||||
"active_download": "הורדה פעילה",
|
||||
@@ -542,8 +542,8 @@
|
||||
"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": "נכשל במחיקת כל הסדרות",
|
||||
"deleted_all_series_successfully": "כל הסדרות נמחקו בהצלחה!",
|
||||
"failed_to_delete_all_series": "נכשל במחיקת כל הסדרות",
|
||||
"deleted_media_successfully": "כל שאר התוכן נמחק בהצלחה!",
|
||||
"failed_to_delete_media": "נכשל במחיקת שאר התוכן",
|
||||
"download_deleted": "ההורדה נמחקה",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Letöltések",
|
||||
"tvseries": "Sorozatok",
|
||||
"series": "Sorozatok",
|
||||
"movies": "Filmek",
|
||||
"queue": "Sor",
|
||||
"other_media": "Other media",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Nincs Elem a Sorban",
|
||||
"no_downloaded_items": "Nincsenek Letöltött Elemek",
|
||||
"delete_all_movies_button": "Összes Film Törlése",
|
||||
"delete_all_tvseries_button": "Összes Sorozat Törlése",
|
||||
"delete_all_series_button": "Összes Sorozat Törlése",
|
||||
"delete_all_button": "Összes Törlése",
|
||||
"delete_all_other_media_button": "Delete other media",
|
||||
"active_download": "Aktív Letöltés",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Nem engedélyezett a fájlok letöltése.",
|
||||
"deleted_all_movies_successfully": "Az Összes Film Sikeresen Törölve!",
|
||||
"failed_to_delete_all_movies": "Nem Sikerült Törölni Az Összes Filmet",
|
||||
"deleted_all_tvseries_successfully": "Az Összes Sorozat Sikeresen Törölve!",
|
||||
"failed_to_delete_all_tvseries": "Nem Sikerült Törölni Az Összes Sorozatot",
|
||||
"deleted_all_series_successfully": "Az Összes Sorozat Sikeresen Törölve!",
|
||||
"failed_to_delete_all_series": "Nem Sikerült Törölni Az Összes Sorozatot",
|
||||
"deleted_media_successfully": "Deleted other media Successfully!",
|
||||
"failed_to_delete_media": "Failed to Delete other media",
|
||||
"download_deleted": "Letöltés Törölve",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Scaricati",
|
||||
"tvseries": "Serie TV",
|
||||
"series": "Serie TV",
|
||||
"movies": "Film",
|
||||
"queue": "Coda",
|
||||
"other_media": "Altri supporti",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Nessun elemento in coda",
|
||||
"no_downloaded_items": "Nessun elemento scaricato",
|
||||
"delete_all_movies_button": "Cancella tutti i film",
|
||||
"delete_all_tvseries_button": "Cancella tutte le serie TV",
|
||||
"delete_all_series_button": "Cancella tutte le serie TV",
|
||||
"delete_all_button": "Cancella tutti",
|
||||
"delete_all_other_media_button": "Elimina altri supporti",
|
||||
"active_download": "Scaricamento in corso",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Non è consentito scaricare file.",
|
||||
"deleted_all_movies_successfully": "Cancellati tutti i film con successo!",
|
||||
"failed_to_delete_all_movies": "Impossibile eliminare tutti i film",
|
||||
"deleted_all_tvseries_successfully": "Eliminate tutte le serie TV con successo!",
|
||||
"failed_to_delete_all_tvseries": "Impossibile eliminare tutte le serie TV",
|
||||
"deleted_all_series_successfully": "Eliminate tutte le serie TV con successo!",
|
||||
"failed_to_delete_all_series": "Impossibile eliminare tutte le serie TV",
|
||||
"deleted_media_successfully": "Eliminato altri supporti con successo!",
|
||||
"failed_to_delete_media": "Impossibile eliminare altri media",
|
||||
"download_deleted": "Download Eliminato",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "ダウンロード",
|
||||
"tvseries": "TVシリーズ",
|
||||
"series": "TVシリーズ",
|
||||
"movies": "映画",
|
||||
"queue": "キュー",
|
||||
"other_media": "その他のメディア",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "キューにアイテムがありません",
|
||||
"no_downloaded_items": "ダウンロードしたアイテムはありません",
|
||||
"delete_all_movies_button": "すべての映画を削除",
|
||||
"delete_all_tvseries_button": "すべてのシリーズを削除",
|
||||
"delete_all_series_button": "すべてのシリーズを削除",
|
||||
"delete_all_button": "すべて削除",
|
||||
"delete_all_other_media_button": "他のメディアを削除する",
|
||||
"active_download": "アクティブなダウンロード",
|
||||
@@ -542,8 +542,8 @@
|
||||
"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": "すべてのシリーズを削除できませんでした",
|
||||
"deleted_all_series_successfully": "すべてのシリーズを正常に削除しました!",
|
||||
"failed_to_delete_all_series": "すべてのシリーズを削除できませんでした",
|
||||
"deleted_media_successfully": "他のメディアを削除しました!",
|
||||
"failed_to_delete_media": "他のメディアの削除に失敗しました",
|
||||
"download_deleted": "ダウンロードが削除されました",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Downloads",
|
||||
"tvseries": "TV-Series",
|
||||
"series": "TV-Series",
|
||||
"movies": "Movies",
|
||||
"queue": "Queue",
|
||||
"other_media": "Other media",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "No Items in Queue",
|
||||
"no_downloaded_items": "No Downloaded Items",
|
||||
"delete_all_movies_button": "Delete All Movies",
|
||||
"delete_all_tvseries_button": "Delete All TV-Series",
|
||||
"delete_all_series_button": "Delete All TV-Series",
|
||||
"delete_all_button": "Delete All",
|
||||
"delete_all_other_media_button": "Delete other media",
|
||||
"active_download": "Active Download",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "You are not allowed to download files.",
|
||||
"deleted_all_movies_successfully": "Deleted All Movies Successfully!",
|
||||
"failed_to_delete_all_movies": "Failed to Delete All Movies",
|
||||
"deleted_all_tvseries_successfully": "Deleted All TV-Series Successfully!",
|
||||
"failed_to_delete_all_tvseries": "Failed to Delete All TV-Series",
|
||||
"deleted_all_series_successfully": "Deleted All TV-Series Successfully!",
|
||||
"failed_to_delete_all_series": "Failed to Delete All TV-Series",
|
||||
"deleted_media_successfully": "Deleted other media Successfully!",
|
||||
"failed_to_delete_media": "Failed to Delete other media",
|
||||
"download_deleted": "Download Deleted",
|
||||
|
||||
@@ -229,14 +229,14 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Nedlastinger",
|
||||
"tvseries": "TV-serier",
|
||||
"series": "TV-serier",
|
||||
"movies": "Filmer",
|
||||
"queue": "Kø",
|
||||
"queue_hint": "Kø og nedlastinger vil gå tapt ved omstart av appen",
|
||||
"no_items_in_queue": "Ingen elementer i køen",
|
||||
"no_downloaded_items": "Ingen nedlastede elementer",
|
||||
"delete_all_movies_button": "Slett alle filmer",
|
||||
"delete_all_tvseries_button": "Slett alle TV-serier",
|
||||
"delete_all_series_button": "Slett alle TV-serier",
|
||||
"delete_all_button": "Slett alt",
|
||||
"active_download": "Aktiv nedlasting",
|
||||
"no_active_downloads": "Ingen aktive nedlastinger",
|
||||
@@ -253,8 +253,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Du har ikke tillatelse til å laste ned filer.",
|
||||
"deleted_all_movies_successfully": "Alle filmer ble slettet!",
|
||||
"failed_to_delete_all_movies": "Kunne ikke slette alle filmer",
|
||||
"deleted_all_tvseries_successfully": "Alle TV-serier ble slettet!",
|
||||
"failed_to_delete_all_tvseries": "Kunne ikke slette alle TV-serier",
|
||||
"deleted_all_series_successfully": "Alle TV-serier ble slettet!",
|
||||
"failed_to_delete_all_series": "Kunne ikke slette alle TV-serier",
|
||||
"download_cancelled": "Nedlasting avbrutt",
|
||||
"could_not_cancel_download": "Kunne ikke avbryte nedlastingen",
|
||||
"download_completed": "Nedlasting fullført",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Downloads",
|
||||
"tvseries": "Series",
|
||||
"series": "Series",
|
||||
"movies": "Films",
|
||||
"queue": "Wachtrij",
|
||||
"other_media": "Andere media",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Geen items in wachtrij",
|
||||
"no_downloaded_items": "Geen gedownloade items",
|
||||
"delete_all_movies_button": "Verwijder alle films",
|
||||
"delete_all_tvseries_button": "Verwijder alle Series",
|
||||
"delete_all_series_button": "Verwijder alle Series",
|
||||
"delete_all_button": "Verwijder alles",
|
||||
"delete_all_other_media_button": "Andere media verwijderen",
|
||||
"active_download": "Actieve download",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Je mag geen bestanden downloaden.",
|
||||
"deleted_all_movies_successfully": "Alle films succesvol verwijderd!",
|
||||
"failed_to_delete_all_movies": "Alle films zijn niet verwijderd",
|
||||
"deleted_all_tvseries_successfully": "Alle series succesvol verwijderd!",
|
||||
"failed_to_delete_all_tvseries": "Alle series zijn niet verwijderd",
|
||||
"deleted_all_series_successfully": "Alle series succesvol verwijderd!",
|
||||
"failed_to_delete_all_series": "Alle series zijn niet verwijderd",
|
||||
"deleted_media_successfully": "Andere media succesvol verwijderd!",
|
||||
"failed_to_delete_media": "Verwijderen van andere media mislukt",
|
||||
"download_deleted": "Download verwijderd",
|
||||
|
||||
@@ -229,14 +229,14 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Nedlastingar",
|
||||
"tvseries": "TV-seriar",
|
||||
"series": "TV-seriar",
|
||||
"movies": "Filmar",
|
||||
"queue": "Kø",
|
||||
"queue_hint": "Kø og nedlastingar vil gå tapt ved omstart av appen",
|
||||
"no_items_in_queue": "Ingen element i køen",
|
||||
"no_downloaded_items": "Ingen nedlasta element",
|
||||
"delete_all_movies_button": "Slett alle filmar",
|
||||
"delete_all_tvseries_button": "Slett alle TV-seriar",
|
||||
"delete_all_series_button": "Slett alle TV-seriar",
|
||||
"delete_all_button": "Slett alt",
|
||||
"active_download": "Aktiv nedlasting",
|
||||
"no_active_downloads": "Ingen aktive nedlastingar",
|
||||
@@ -253,8 +253,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Du har ikkje løyve til å lasta ned filer.",
|
||||
"deleted_all_movies_successfully": "Alle filmar vart sletta!",
|
||||
"failed_to_delete_all_movies": "Kunne ikkje sletta alle filmar",
|
||||
"deleted_all_tvseries_successfully": "Alle TV-seriar vart sletta!",
|
||||
"failed_to_delete_all_tvseries": "Kunne ikkje sletta alle TV-seriar",
|
||||
"deleted_all_series_successfully": "Alle TV-seriar vart sletta!",
|
||||
"failed_to_delete_all_series": "Kunne ikkje sletta alle TV-seriar",
|
||||
"download_cancelled": "Nedlasting avbroten",
|
||||
"could_not_cancel_download": "Kunne ikkje avbryta nedlastinga",
|
||||
"download_completed": "Nedlasting fullført",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Nedlastinger",
|
||||
"tvseries": "TV-Serier",
|
||||
"series": "TV-Serier",
|
||||
"movies": "Filmer",
|
||||
"queue": "Kø",
|
||||
"other_media": "Andre medier",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Ingen elementer i køen",
|
||||
"no_downloaded_items": "Ingen nedlastede elementer",
|
||||
"delete_all_movies_button": "Slett alle filmer",
|
||||
"delete_all_tvseries_button": "Slett alle TV-Serier",
|
||||
"delete_all_series_button": "Slett alle TV-Serier",
|
||||
"delete_all_button": "Slett alle",
|
||||
"delete_all_other_media_button": "Slett andre media",
|
||||
"active_download": "Aktiv nedlasting",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Du har ikke lov til å laste ned filer.",
|
||||
"deleted_all_movies_successfully": "Slettet alle filmer vellykket!",
|
||||
"failed_to_delete_all_movies": "Kunne ikke slette alle filmer",
|
||||
"deleted_all_tvseries_successfully": "Alle TV-Serier ble slettet!",
|
||||
"failed_to_delete_all_tvseries": "Kunne ikke slette alle TV-Serier",
|
||||
"deleted_all_series_successfully": "Alle TV-Serier ble slettet!",
|
||||
"failed_to_delete_all_series": "Kunne ikke slette alle TV-Serier",
|
||||
"deleted_media_successfully": "Slettet andre media vellykket!",
|
||||
"failed_to_delete_media": "Kunne ikke slette andre medier",
|
||||
"download_deleted": "Nedlasting slettet",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Pobrane",
|
||||
"tvseries": "Seriale",
|
||||
"series": "Seriale",
|
||||
"movies": "Filmy",
|
||||
"queue": "Kolejka",
|
||||
"other_media": "Inne media",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Brak elementów w kolejce",
|
||||
"no_downloaded_items": "Brak pobranych elementów",
|
||||
"delete_all_movies_button": "Usuń wszystkie filmy",
|
||||
"delete_all_tvseries_button": "Usuń wszystkie seriale",
|
||||
"delete_all_series_button": "Usuń wszystkie seriale",
|
||||
"delete_all_button": "Usuń wszystko",
|
||||
"delete_all_other_media_button": "Usuń inne media",
|
||||
"active_download": "Aktywne pobieranie",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Nie masz uprawnień do pobierania plików.",
|
||||
"deleted_all_movies_successfully": "Wszystkie filmy zostały pomyślnie usunięte!",
|
||||
"failed_to_delete_all_movies": "Nie udało się usunąć wszystkich filmów",
|
||||
"deleted_all_tvseries_successfully": "Wszystkie seriale zostały pomyślnie usunięte!",
|
||||
"failed_to_delete_all_tvseries": "Nie udało się usunąć wszystkich seriali",
|
||||
"deleted_all_series_successfully": "Wszystkie seriale zostały pomyślnie usunięte!",
|
||||
"failed_to_delete_all_series": "Nie udało się usunąć wszystkich seriali",
|
||||
"deleted_media_successfully": "Pomyślnie usunięto inne media!",
|
||||
"failed_to_delete_media": "Nie udało się usunąć innych mediów",
|
||||
"download_deleted": "Pobieranie usunięte",
|
||||
|
||||
@@ -227,14 +227,14 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Downloads",
|
||||
"tvseries": "TV/Séries",
|
||||
"series": "TV/Séries",
|
||||
"movies": "Filmes",
|
||||
"queue": "Fila",
|
||||
"queue_hint": "A fila e os downloads serão perdidos ao reiniciar o aplicativo",
|
||||
"no_items_in_queue": "Nenhum item na fila",
|
||||
"no_downloaded_items": "Nenhum item baixado",
|
||||
"delete_all_movies_button": "Remover todos os filmes",
|
||||
"delete_all_tvseries_button": "Remover todos as TV/Séries",
|
||||
"delete_all_series_button": "Remover todos as TV/Séries",
|
||||
"delete_all_button": "Remover tudo",
|
||||
"active_download": "Downloads ativos",
|
||||
"no_active_downloads": "Nenhum download ativo",
|
||||
@@ -251,8 +251,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Você não tem permissão para baixar arquivos.",
|
||||
"deleted_all_movies_successfully": "Todos os filmes foram removidos com sucesso!",
|
||||
"failed_to_delete_all_movies": "Falha ao remover todos os filmes",
|
||||
"deleted_all_tvseries_successfully": "Todos as TV/Séries foram removidos com sucesso!",
|
||||
"failed_to_delete_all_tvseries": "Falha ao remover todos as TV/Séries",
|
||||
"deleted_all_series_successfully": "Todos as TV/Séries foram removidos com sucesso!",
|
||||
"failed_to_delete_all_series": "Falha ao remover todos as TV/Séries",
|
||||
"download_cancelled": "Download cancelado",
|
||||
"could_not_cancel_download": "Não foi possível cancelar o download",
|
||||
"download_completed": "Download completo",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Downloads",
|
||||
"tvseries": "TV-Séries",
|
||||
"series": "TV-Séries",
|
||||
"movies": "Filmes",
|
||||
"queue": "Fila",
|
||||
"other_media": "Outras mídias",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Nenhum item na fila",
|
||||
"no_downloaded_items": "Nenhum item baixado",
|
||||
"delete_all_movies_button": "Excluir todos os filmes",
|
||||
"delete_all_tvseries_button": "Excluir todas as séries",
|
||||
"delete_all_series_button": "Excluir todas as séries",
|
||||
"delete_all_button": "Excluir todos os",
|
||||
"delete_all_other_media_button": "Excluir outras mídias",
|
||||
"active_download": "Download ativo",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Você não tem permissão para baixar arquivos.",
|
||||
"deleted_all_movies_successfully": "Todos os filmes excluídos com sucesso!",
|
||||
"failed_to_delete_all_movies": "Falha ao excluir todos os filmes",
|
||||
"deleted_all_tvseries_successfully": "Todas as TV-Series excluídas com sucesso!",
|
||||
"failed_to_delete_all_tvseries": "Falha ao excluir todas as séries",
|
||||
"deleted_all_series_successfully": "Todas as TV-Series excluídas com sucesso!",
|
||||
"failed_to_delete_all_series": "Falha ao excluir todas as séries",
|
||||
"deleted_media_successfully": "Outras mídias excluídas com sucesso!",
|
||||
"failed_to_delete_media": "Falha ao excluir outras mídias",
|
||||
"download_deleted": "Download Excluído",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Descărcări",
|
||||
"tvseries": "Seriale",
|
||||
"series": "Seriale",
|
||||
"movies": "Filme",
|
||||
"queue": "Coadă",
|
||||
"other_media": "Alte suporturi",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Niciun articol în coadă",
|
||||
"no_downloaded_items": "Niciun element descărcat",
|
||||
"delete_all_movies_button": "Șterge toate filmele",
|
||||
"delete_all_tvseries_button": "Șterge toate serialele",
|
||||
"delete_all_series_button": "Șterge toate serialele",
|
||||
"delete_all_button": "Șterge tot",
|
||||
"delete_all_other_media_button": "Șterge alte fișiere media",
|
||||
"active_download": "Descărcare activă",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Nu aveți voie să descărcați fișiere.",
|
||||
"deleted_all_movies_successfully": "Toate filmele au fost șterse cu succes!",
|
||||
"failed_to_delete_all_movies": "Nu s-au putut șterge toate filmele",
|
||||
"deleted_all_tvseries_successfully": "Toate serialele au fost șterse cu succes!",
|
||||
"failed_to_delete_all_tvseries": "Nu s-au putut șterge toate serialele",
|
||||
"deleted_all_series_successfully": "Toate serialele au fost șterse cu succes!",
|
||||
"failed_to_delete_all_series": "Nu s-au putut șterge toate serialele",
|
||||
"deleted_media_successfully": "Alte fișiere șterse cu succes!",
|
||||
"failed_to_delete_media": "Ștergerea altor fișiere media a eșuat",
|
||||
"download_deleted": "Descărcare ştearsă",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Загрузки",
|
||||
"tvseries": "Сериалы",
|
||||
"series": "Сериалы",
|
||||
"movies": "Фильмы",
|
||||
"queue": "Очередь",
|
||||
"other_media": "Прочие файлы",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Нет элементов в очереди",
|
||||
"no_downloaded_items": "Нет загруженных файлов",
|
||||
"delete_all_movies_button": "Удалить все фильмы",
|
||||
"delete_all_tvseries_button": "Удалить все сериалы",
|
||||
"delete_all_series_button": "Удалить все сериалы",
|
||||
"delete_all_button": "Удалить все",
|
||||
"delete_all_other_media_button": "Удалить прочие файлы",
|
||||
"active_download": "Загружается",
|
||||
@@ -542,8 +542,8 @@
|
||||
"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": "Возникла ошибка при удалении всех сериалов",
|
||||
"deleted_all_series_successfully": "Все сериалы были успешно удалены!",
|
||||
"failed_to_delete_all_series": "Возникла ошибка при удалении всех сериалов",
|
||||
"deleted_media_successfully": "Остальные медиафайлы успешно удалены!",
|
||||
"failed_to_delete_media": "Не удалось удалить остальные медиафайлы",
|
||||
"download_deleted": "Загруженный контент удалён",
|
||||
|
||||
@@ -229,14 +229,14 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Shkarkimet",
|
||||
"tvseries": "Seriale TV",
|
||||
"series": "Seriale TV",
|
||||
"movies": "Filma",
|
||||
"queue": "Rradhë",
|
||||
"queue_hint": "Rradhat dhe shkarkimet do të humbasin pas genstartit të aplikacionit",
|
||||
"no_items_in_queue": "Nuk ka elemente në rradhë",
|
||||
"no_downloaded_items": "Nuk ka shkarkime",
|
||||
"delete_all_movies_button": "Fshijë të gjithë filmat",
|
||||
"delete_all_tvseries_button": "Fshijë të gjitha serialet TV",
|
||||
"delete_all_series_button": "Fshijë të gjitha serialet TV",
|
||||
"delete_all_button": "Fshijë të gjitha",
|
||||
"active_download": "Shkarkim aktiv",
|
||||
"no_active_downloads": "Nuk ka shkarkime aktive",
|
||||
@@ -253,8 +253,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Nuk keni të drejtë të shkarkoni skedarë.",
|
||||
"deleted_all_movies_successfully": "Të gjithë filmat u fshinë me sukses!",
|
||||
"failed_to_delete_all_movies": "Dështojë fshirja e të gjithë filmave",
|
||||
"deleted_all_tvseries_successfully": "Të gjitha serialet TV u fshinë me sukses!",
|
||||
"failed_to_delete_all_tvseries": "Dështojë fshirja e të gjitha serialeve TV",
|
||||
"deleted_all_series_successfully": "Të gjitha serialet TV u fshinë me sukses!",
|
||||
"failed_to_delete_all_series": "Dështojë fshirja e të gjitha serialeve TV",
|
||||
"download_cancelled": "Shkarkimi u anulua",
|
||||
"could_not_cancel_download": "Nuk mundet të anulohet shkarkimi",
|
||||
"download_completed": "Shkarkimi u përfundua",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Nedladdningar",
|
||||
"tvseries": "TV-Serier",
|
||||
"series": "TV-Serier",
|
||||
"movies": "Filmer",
|
||||
"queue": "Kö",
|
||||
"other_media": "Annan media",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Inga objekt i Kön",
|
||||
"no_downloaded_items": "Inga Nedladdade Objekt",
|
||||
"delete_all_movies_button": "Ta Bort Alla Filmer",
|
||||
"delete_all_tvseries_button": "Ta Bort Alla TV-Serier",
|
||||
"delete_all_series_button": "Ta Bort Alla TV-Serier",
|
||||
"delete_all_button": "Radera Allt",
|
||||
"delete_all_other_media_button": "Ta Bort Andra Videor",
|
||||
"active_download": "Aktiv Nedladdning",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Du har inte behörighet att ladda ner filer.",
|
||||
"deleted_all_movies_successfully": "Alla Filmer Har Tagits Bort!",
|
||||
"failed_to_delete_all_movies": "Det Gick Inte Att Ta Bort Alla Filmer",
|
||||
"deleted_all_tvseries_successfully": "Alla TV-Serier Har Tagits Bort!",
|
||||
"failed_to_delete_all_tvseries": "Det Gick Inte Att Ta Bort Alla TV-Serier",
|
||||
"deleted_all_series_successfully": "Alla TV-Serier Har Tagits Bort!",
|
||||
"failed_to_delete_all_series": "Det Gick Inte Att Ta Bort Alla TV-Serier",
|
||||
"deleted_media_successfully": "Andra Medier Har Tagits Bort!",
|
||||
"failed_to_delete_media": "Kunde Inte Ta Bort Andra Medier",
|
||||
"download_deleted": "Nedladdning Borttagen",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Downloads",
|
||||
"tvseries": "TV-Series",
|
||||
"series": "TV-Series",
|
||||
"movies": "Movies",
|
||||
"queue": "Queue",
|
||||
"other_media": "Other media",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "No Items in Queue",
|
||||
"no_downloaded_items": "No Downloaded Items",
|
||||
"delete_all_movies_button": "Delete All Movies",
|
||||
"delete_all_tvseries_button": "Delete All TV-Series",
|
||||
"delete_all_series_button": "Delete All TV-Series",
|
||||
"delete_all_button": "Delete All",
|
||||
"delete_all_other_media_button": "Delete other media",
|
||||
"active_download": "Active Download",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "You are not allowed to download files.",
|
||||
"deleted_all_movies_successfully": "Deleted All Movies Successfully!",
|
||||
"failed_to_delete_all_movies": "Failed to Delete All Movies",
|
||||
"deleted_all_tvseries_successfully": "Deleted All TV-Series Successfully!",
|
||||
"failed_to_delete_all_tvseries": "Failed to Delete All TV-Series",
|
||||
"deleted_all_series_successfully": "Deleted All TV-Series Successfully!",
|
||||
"failed_to_delete_all_series": "Failed to Delete All TV-Series",
|
||||
"deleted_media_successfully": "Deleted other media Successfully!",
|
||||
"failed_to_delete_media": "Failed to Delete other media",
|
||||
"download_deleted": "Download Deleted",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Qaw' Doch",
|
||||
"tvseries": "TV Hem",
|
||||
"series": "TV Hem",
|
||||
"movies": "DIS",
|
||||
"queue": "ghom",
|
||||
"other_media": "Other media",
|
||||
@@ -524,7 +524,7 @@
|
||||
"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_series_button": "Hoch TV Hem yIQaw'",
|
||||
"delete_all_button": "Hoch yIQaw'",
|
||||
"delete_all_other_media_button": "Delete other media",
|
||||
"active_download": "chu' Qaw'",
|
||||
@@ -542,8 +542,8 @@
|
||||
"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'",
|
||||
"deleted_all_series_successfully": "Hoch TV Hem Qaw' Qapla'!",
|
||||
"failed_to_delete_all_series": "Hoch TV Hem Qaw'laHbe'",
|
||||
"deleted_media_successfully": "Deleted other media Successfully!",
|
||||
"failed_to_delete_media": "Failed to Delete other media",
|
||||
"download_deleted": "Download Deleted",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "İndirilenler",
|
||||
"tvseries": "Diziler",
|
||||
"series": "Diziler",
|
||||
"movies": "Filmler",
|
||||
"queue": "Sıra",
|
||||
"other_media": "Diğer medya",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Sırada öğe yok",
|
||||
"no_downloaded_items": "İndirilen öğe yok",
|
||||
"delete_all_movies_button": "Tüm Filmleri Sil",
|
||||
"delete_all_tvseries_button": "Tüm Dizileri Sil",
|
||||
"delete_all_series_button": "Tüm Dizileri Sil",
|
||||
"delete_all_button": "Tümünü Sil",
|
||||
"delete_all_other_media_button": "Diğer medyayı sil",
|
||||
"active_download": "Aktif indirme",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Dosyaları indirme izniniz yok.",
|
||||
"deleted_all_movies_successfully": "Tüm filmler başarıyla silindi!",
|
||||
"failed_to_delete_all_movies": "Filmler silinemedi",
|
||||
"deleted_all_tvseries_successfully": "Tüm diziler başarıyla silindi!",
|
||||
"failed_to_delete_all_tvseries": "Diziler silinemedi",
|
||||
"deleted_all_series_successfully": "Tüm diziler başarıyla silindi!",
|
||||
"failed_to_delete_all_series": "Diziler silinemedi",
|
||||
"deleted_media_successfully": "Diğer medya başarıyla silindi!",
|
||||
"failed_to_delete_media": "Failed to Delete other media",
|
||||
"download_deleted": "İndirme silindi",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Завантаження",
|
||||
"tvseries": "ТБ-Серіали",
|
||||
"series": "ТБ-Серіали",
|
||||
"movies": "Фільми",
|
||||
"queue": "Черга",
|
||||
"other_media": "Other media",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Нема елементів в черзі",
|
||||
"no_downloaded_items": "Нема завантажених елементів",
|
||||
"delete_all_movies_button": "Видалити всі Фільми",
|
||||
"delete_all_tvseries_button": "Видалити всі ТБ-Серіали",
|
||||
"delete_all_series_button": "Видалити всі ТБ-Серіали",
|
||||
"delete_all_button": "Видалити Все",
|
||||
"delete_all_other_media_button": "Delete other media",
|
||||
"active_download": "Активне завантаження",
|
||||
@@ -542,8 +542,8 @@
|
||||
"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": "Не вдалося видалити всі телесеріали",
|
||||
"deleted_all_series_successfully": "Успішно видалено всі серіали!",
|
||||
"failed_to_delete_all_series": "Не вдалося видалити всі телесеріали",
|
||||
"deleted_media_successfully": "Deleted other media Successfully!",
|
||||
"failed_to_delete_media": "Failed to Delete other media",
|
||||
"download_deleted": "Download Deleted",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Tải xuống",
|
||||
"tvseries": "Chương trình TV",
|
||||
"series": "Chương trình TV",
|
||||
"movies": "Phim",
|
||||
"queue": "Hàng đợi",
|
||||
"other_media": "Other media",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "Không có mục trong hàng đợi",
|
||||
"no_downloaded_items": "Không có mục đã tải",
|
||||
"delete_all_movies_button": "Xóa tất cả phim",
|
||||
"delete_all_tvseries_button": "Xóa tất cả chương trình TV",
|
||||
"delete_all_series_button": "Xóa tất cả chương trình TV",
|
||||
"delete_all_button": "Xóa tất cả",
|
||||
"delete_all_other_media_button": "Delete other media",
|
||||
"active_download": "Đang tải xuống",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "Bạn không có quyền tải nội dung.",
|
||||
"deleted_all_movies_successfully": "Đã xóa tất cả phim thành công!",
|
||||
"failed_to_delete_all_movies": "Xóa phim thất bại",
|
||||
"deleted_all_tvseries_successfully": "Đã xóa tất cả chương trình TV thành công!",
|
||||
"failed_to_delete_all_tvseries": "Xóa chương trình TV thất bại",
|
||||
"deleted_all_series_successfully": "Đã xóa tất cả chương trình TV thành công!",
|
||||
"failed_to_delete_all_series": "Xóa chương trình TV thất bại",
|
||||
"deleted_media_successfully": "Deleted other media Successfully!",
|
||||
"failed_to_delete_media": "Failed to Delete other media",
|
||||
"download_deleted": "Download Deleted",
|
||||
|
||||
@@ -224,14 +224,14 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "下载",
|
||||
"tvseries": "剧集",
|
||||
"series": "剧集",
|
||||
"movies": "电影",
|
||||
"queue": "队列",
|
||||
"queue_hint": "应用重启后队列和下载将会丢失",
|
||||
"no_items_in_queue": "队列中无项目",
|
||||
"no_downloaded_items": "无已下载项目",
|
||||
"delete_all_movies_button": "删除所有电影",
|
||||
"delete_all_tvseries_button": "删除所有剧集",
|
||||
"delete_all_series_button": "删除所有剧集",
|
||||
"delete_all_button": "删除全部",
|
||||
"active_download": "活跃下载",
|
||||
"no_active_downloads": "无活跃下载",
|
||||
@@ -248,8 +248,8 @@
|
||||
"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": "删除所有剧集失败",
|
||||
"deleted_all_series_successfully": "成功删除所有剧集!",
|
||||
"failed_to_delete_all_series": "删除所有剧集失败",
|
||||
"download_cancelled": "下载已取消",
|
||||
"could_not_cancel_download": "无法取消下载",
|
||||
"download_completed": "下载完成",
|
||||
|
||||
@@ -231,14 +231,14 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "下載",
|
||||
"tvseries": "電視劇",
|
||||
"series": "電視劇",
|
||||
"movies": "電影",
|
||||
"queue": "隊列",
|
||||
"queue_hint": "應用重啟後隊列和下載將會丟失",
|
||||
"no_items_in_queue": "隊列中無項目",
|
||||
"no_downloaded_items": "無已下載項目",
|
||||
"delete_all_movies_button": "刪除所有電影",
|
||||
"delete_all_tvseries_button": "刪除所有電視劇",
|
||||
"delete_all_series_button": "刪除所有電視劇",
|
||||
"delete_all_button": "刪除全部",
|
||||
"active_download": "活動下載",
|
||||
"no_active_downloads": "無活動下載",
|
||||
@@ -255,8 +255,8 @@
|
||||
"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": "刪除所有電視劇失敗",
|
||||
"deleted_all_series_successfully": "成功刪除所有電視劇!",
|
||||
"failed_to_delete_all_series": "刪除所有電視劇失敗",
|
||||
"download_cancelled": "下載已取消",
|
||||
"could_not_cancel_download": "無法取消下載",
|
||||
"download_completed": "下載完成",
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
},
|
||||
"downloads": {
|
||||
"downloads_title": "Downloads",
|
||||
"tvseries": "TV-Series",
|
||||
"series": "TV-Series",
|
||||
"movies": "Movies",
|
||||
"queue": "Queue",
|
||||
"other_media": "Other media",
|
||||
@@ -524,7 +524,7 @@
|
||||
"no_items_in_queue": "No Items in Queue",
|
||||
"no_downloaded_items": "No Downloaded Items",
|
||||
"delete_all_movies_button": "Delete All Movies",
|
||||
"delete_all_tvseries_button": "Delete All TV-Series",
|
||||
"delete_all_series_button": "Delete All TV-Series",
|
||||
"delete_all_button": "Delete All",
|
||||
"delete_all_other_media_button": "Delete other media",
|
||||
"active_download": "Active Download",
|
||||
@@ -542,8 +542,8 @@
|
||||
"you_are_not_allowed_to_download_files": "You are not allowed to download files.",
|
||||
"deleted_all_movies_successfully": "Deleted All Movies Successfully!",
|
||||
"failed_to_delete_all_movies": "Failed to Delete All Movies",
|
||||
"deleted_all_tvseries_successfully": "Deleted All TV-Series Successfully!",
|
||||
"failed_to_delete_all_tvseries": "Failed to Delete All TV-Series",
|
||||
"deleted_all_series_successfully": "Deleted All TV-Series Successfully!",
|
||||
"failed_to_delete_all_series": "Failed to Delete All TV-Series",
|
||||
"deleted_media_successfully": "Deleted other media Successfully!",
|
||||
"failed_to_delete_media": "Failed to Delete other media",
|
||||
"download_deleted": "Download Deleted",
|
||||
|
||||
@@ -50,7 +50,7 @@ export const getDownloadUrl = async ({
|
||||
if (maxBitrate.key === "Max" && !streamDetails?.mediaSource?.TranscodingUrl) {
|
||||
console.log("Downloading item directly");
|
||||
return {
|
||||
url: `${api.basePath}/Items/${item.Id}/Download?api_key=${api.accessToken}`,
|
||||
url: `${api.basePath}/Items/${mediaSource.Id}/Download?api_key=${api.accessToken}`,
|
||||
mediaSource: streamDetails?.mediaSource ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user