mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-12 08:50:25 +01:00
Merge branch 'develop' into feature/subtitle-customizations
This commit is contained in:
1
.github/workflows/linting.yml
vendored
1
.github/workflows/linting.yml
vendored
@@ -65,6 +65,7 @@ jobs:
|
|||||||
|
|
||||||
expo-doctor:
|
expo-doctor:
|
||||||
name: 🚑 Expo Doctor Check
|
name: 🚑 Expo Doctor Check
|
||||||
|
if: false
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: 🛒 Checkout repository
|
- name: 🛒 Checkout repository
|
||||||
|
|||||||
2
app.json
2
app.json
@@ -122,7 +122,7 @@
|
|||||||
[
|
[
|
||||||
"expo-splash-screen",
|
"expo-splash-screen",
|
||||||
{
|
{
|
||||||
"backgroundColor": "#2e2e2e",
|
"backgroundColor": "#010101",
|
||||||
"image": "./assets/images/icon-ios-plain.png",
|
"image": "./assets/images/icon-ios-plain.png",
|
||||||
"imageWidth": 100
|
"imageWidth": 100
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
type BaseItemDto,
|
type BaseItemDto,
|
||||||
type MediaSourceInfo,
|
type MediaSourceInfo,
|
||||||
PlaybackOrder,
|
PlaybackOrder,
|
||||||
|
PlaybackProgressInfo,
|
||||||
PlaybackStartInfo,
|
PlaybackStartInfo,
|
||||||
RepeatMode,
|
RepeatMode,
|
||||||
} from "@jellyfin/sdk/lib/generated-client";
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
@@ -271,12 +272,7 @@ export default function page() {
|
|||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
await videoRef.current?.pause();
|
await videoRef.current?.pause();
|
||||||
playbackManager.reportPlaybackProgress(
|
playbackManager.reportPlaybackProgress(
|
||||||
item?.Id!,
|
currentPlayStateInfo() as PlaybackProgressInfo,
|
||||||
msToTicks(progress.get()),
|
|
||||||
{
|
|
||||||
AudioStreamIndex: audioIndex ?? -1,
|
|
||||||
SubtitleStreamIndex: subtitleIndex ?? -1,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
videoRef.current?.play();
|
videoRef.current?.play();
|
||||||
@@ -394,12 +390,7 @@ export default function page() {
|
|||||||
if (!item?.Id) return;
|
if (!item?.Id) return;
|
||||||
|
|
||||||
playbackManager.reportPlaybackProgress(
|
playbackManager.reportPlaybackProgress(
|
||||||
item.Id,
|
currentPlayStateInfo() as PlaybackProgressInfo,
|
||||||
msToTicks(progress.get()),
|
|
||||||
{
|
|
||||||
AudioStreamIndex: audioIndex ?? -1,
|
|
||||||
SubtitleStreamIndex: subtitleIndex ?? -1,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -506,12 +497,7 @@ export default function page() {
|
|||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
if (item?.Id) {
|
if (item?.Id) {
|
||||||
playbackManager.reportPlaybackProgress(
|
playbackManager.reportPlaybackProgress(
|
||||||
item.Id,
|
currentPlayStateInfo() as PlaybackProgressInfo,
|
||||||
msToTicks(progress.get()),
|
|
||||||
{
|
|
||||||
AudioStreamIndex: audioIndex ?? -1,
|
|
||||||
SubtitleStreamIndex: subtitleIndex ?? -1,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!Platform.isTV) await activateKeepAwakeAsync();
|
if (!Platform.isTV) await activateKeepAwakeAsync();
|
||||||
@@ -522,12 +508,7 @@ export default function page() {
|
|||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
if (item?.Id) {
|
if (item?.Id) {
|
||||||
playbackManager.reportPlaybackProgress(
|
playbackManager.reportPlaybackProgress(
|
||||||
item.Id,
|
currentPlayStateInfo() as PlaybackProgressInfo,
|
||||||
msToTicks(progress.get()),
|
|
||||||
{
|
|
||||||
AudioStreamIndex: audioIndex ?? -1,
|
|
||||||
SubtitleStreamIndex: subtitleIndex ?? -1,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!Platform.isTV) await deactivateKeepAwake();
|
if (!Platform.isTV) await deactivateKeepAwake();
|
||||||
|
|||||||
4
bun.lock
4
bun.lock
@@ -65,7 +65,7 @@
|
|||||||
"react-native-ios-utilities": "5.1.8",
|
"react-native-ios-utilities": "5.1.8",
|
||||||
"react-native-mmkv": "2.12.2",
|
"react-native-mmkv": "2.12.2",
|
||||||
"react-native-pager-view": "^6.9.1",
|
"react-native-pager-view": "^6.9.1",
|
||||||
"react-native-reanimated": "~3.17.4",
|
"react-native-reanimated": "~3.19.1",
|
||||||
"react-native-reanimated-carousel": "4.0.2",
|
"react-native-reanimated-carousel": "4.0.2",
|
||||||
"react-native-safe-area-context": "5.4.0",
|
"react-native-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "~4.11.1",
|
"react-native-screens": "~4.11.1",
|
||||||
@@ -1637,7 +1637,7 @@
|
|||||||
|
|
||||||
"react-native-pager-view": ["react-native-pager-view@6.9.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-uUT0MMMbNtoSbxe9pRvdJJKEi9snjuJ3fXlZhG8F2vVMOBJVt/AFtqMPUHu9yMflmqOr08PewKzj9EPl/Yj+Gw=="],
|
"react-native-pager-view": ["react-native-pager-view@6.9.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-uUT0MMMbNtoSbxe9pRvdJJKEi9snjuJ3fXlZhG8F2vVMOBJVt/AFtqMPUHu9yMflmqOr08PewKzj9EPl/Yj+Gw=="],
|
||||||
|
|
||||||
"react-native-reanimated": ["react-native-reanimated@3.17.5", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "invariant": "^2.2.4", "react-native-is-edge-to-edge": "1.1.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-SxBK7wQfJ4UoWoJqQnmIC7ZjuNgVb9rcY5Xc67upXAFKftWg0rnkknTw6vgwnjRcvYThrjzUVti66XoZdDJGtw=="],
|
"react-native-reanimated": ["react-native-reanimated@3.19.1", "", { "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", "@babel/plugin-transform-classes": "^7.0.0-0", "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", "@babel/plugin-transform-optional-chaining": "^7.0.0-0", "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", "@babel/plugin-transform-template-literals": "^7.0.0-0", "@babel/plugin-transform-unicode-regex": "^7.0.0-0", "@babel/preset-typescript": "^7.16.7", "convert-source-map": "^2.0.0", "invariant": "^2.2.4", "react-native-is-edge-to-edge": "1.1.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", "react-native": "*" } }, "sha512-ILL0FSNzSVIg6WuawrsMBvNxk2yJFiTUcahimXDAeNiE/09eagVUlHhYWXAAmH0umvAOafBaGjO7YfBhUrf5ZQ=="],
|
||||||
|
|
||||||
"react-native-reanimated-carousel": ["react-native-reanimated-carousel@4.0.2", "", { "peerDependencies": { "react": ">=18.0.0", "react-native": ">=0.70.3", "react-native-gesture-handler": ">=2.9.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-vNpCfPlFoOVKHd+oB7B0luoJswp+nyz0NdJD8+LCrf25JiNQXfM22RSJhLaksBHqk3fm8R4fKWPNcfy5w7wL1Q=="],
|
"react-native-reanimated-carousel": ["react-native-reanimated-carousel@4.0.2", "", { "peerDependencies": { "react": ">=18.0.0", "react-native": ">=0.70.3", "react-native-gesture-handler": ">=2.9.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-vNpCfPlFoOVKHd+oB7B0luoJswp+nyz0NdJD8+LCrf25JiNQXfM22RSJhLaksBHqk3fm8R4fKWPNcfy5w7wL1Q=="],
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
|||||||
userId: user.Id,
|
userId: user.Id,
|
||||||
seasonId: seasonId || undefined,
|
seasonId: seasonId || undefined,
|
||||||
seriesId: item.SeriesId,
|
seriesId: item.SeriesId,
|
||||||
|
enableUserData: true,
|
||||||
fields: [
|
fields: [
|
||||||
"ItemCounts",
|
"ItemCounts",
|
||||||
"PrimaryImageAspectRatio",
|
"PrimaryImageAspectRatio",
|
||||||
@@ -70,48 +71,6 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
|||||||
enabled: !!api && !!user?.Id && !!seasonId,
|
enabled: !!api && !!user?.Id && !!seasonId,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Prefetch previous and next episode
|
|
||||||
*/
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
useEffect(() => {
|
|
||||||
if (!item?.Id || !item.IndexNumber || !episodes || episodes.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousId = episodes?.find(
|
|
||||||
(ep) => ep.IndexNumber === item.IndexNumber! - 1,
|
|
||||||
)?.Id;
|
|
||||||
if (previousId) {
|
|
||||||
queryClient.prefetchQuery({
|
|
||||||
queryKey: ["item", previousId],
|
|
||||||
queryFn: async () =>
|
|
||||||
await getUserItemData({
|
|
||||||
api,
|
|
||||||
userId: user?.Id,
|
|
||||||
itemId: previousId,
|
|
||||||
}),
|
|
||||||
staleTime: 60 * 1000 * 5,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextId = episodes?.find(
|
|
||||||
(ep) => ep.IndexNumber === item.IndexNumber! + 1,
|
|
||||||
)?.Id;
|
|
||||||
if (nextId) {
|
|
||||||
queryClient.prefetchQuery({
|
|
||||||
queryKey: ["item", nextId],
|
|
||||||
queryFn: async () =>
|
|
||||||
await getUserItemData({
|
|
||||||
api,
|
|
||||||
userId: user?.Id,
|
|
||||||
itemId: nextId,
|
|
||||||
}),
|
|
||||||
staleTime: 60 * 1000 * 5,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [episodes, api, user?.Id, item]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (item?.Type === "Episode" && item.Id) {
|
if (item?.Type === "Episode" && item.Id) {
|
||||||
const index = episodes?.findIndex((ep) => ep.Id === item.Id);
|
const index = episodes?.findIndex((ep) => ep.Id === item.Id);
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
|
|||||||
seasonId: selectedSeasonId,
|
seasonId: selectedSeasonId,
|
||||||
enableUserData: true,
|
enableUserData: true,
|
||||||
// Note: Including trick play is necessary to enable trick play downloads
|
// Note: Including trick play is necessary to enable trick play downloads
|
||||||
fields: ["MediaSources", "MediaStreams", "Overview", "Trickplay"],
|
fields: ["Overview", "Trickplay"],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.data.TotalRecordCount === 0)
|
if (res.data.TotalRecordCount === 0)
|
||||||
@@ -102,23 +102,6 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
useEffect(() => {
|
|
||||||
for (const e of episodes || []) {
|
|
||||||
queryClient.prefetchQuery({
|
|
||||||
queryKey: ["item", e.Id],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!e.Id) return;
|
|
||||||
const res = await getUserItemData({
|
|
||||||
api,
|
|
||||||
userId: user?.Id,
|
|
||||||
itemId: e.Id,
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
},
|
|
||||||
staleTime: 60 * 5 * 1000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [episodes]);
|
|
||||||
|
|
||||||
// Used for height calculation
|
// Used for height calculation
|
||||||
const [nrOfEpisodes, setNrOfEpisodes] = useState(0);
|
const [nrOfEpisodes, setNrOfEpisodes] = useState(0);
|
||||||
|
|||||||
@@ -74,7 +74,12 @@ export const HomeIndex = () => {
|
|||||||
|
|
||||||
const { getDownloadedItems, cleanCacheDirectory } = useDownload();
|
const { getDownloadedItems, cleanCacheDirectory } = useDownload();
|
||||||
const prevIsConnected = useRef<boolean | null>(false);
|
const prevIsConnected = useRef<boolean | null>(false);
|
||||||
const { isConnected, loading: retryLoading, retryCheck } = useNetworkStatus();
|
const {
|
||||||
|
isConnected,
|
||||||
|
serverConnected,
|
||||||
|
loading: retryLoading,
|
||||||
|
retryCheck,
|
||||||
|
} = useNetworkStatus();
|
||||||
const invalidateCache = useInvalidatePlaybackProgressCache();
|
const invalidateCache = useInvalidatePlaybackProgressCache();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only invalidate cache when transitioning from offline to online
|
// Only invalidate cache when transitioning from offline to online
|
||||||
@@ -358,13 +363,28 @@ export const HomeIndex = () => {
|
|||||||
|
|
||||||
const sections = settings?.home?.sections ? customSections : defaultSections;
|
const sections = settings?.home?.sections ? customSections : defaultSections;
|
||||||
|
|
||||||
if (isConnected === false) {
|
if (!isConnected || serverConnected !== true) {
|
||||||
|
let title: string;
|
||||||
|
let subtitle: string;
|
||||||
|
|
||||||
|
if (!isConnected) {
|
||||||
|
// No network connection
|
||||||
|
title = t("home.no_internet");
|
||||||
|
subtitle = t("home.no_internet_message");
|
||||||
|
} else if (serverConnected === null) {
|
||||||
|
// Network is up, but server is being checked
|
||||||
|
title = t("home.checking_server_connection");
|
||||||
|
subtitle = t("home.checking_server_connection_message");
|
||||||
|
} else if (!serverConnected) {
|
||||||
|
// Network is up, but server is unreachable
|
||||||
|
title = t("home.server_unreachable");
|
||||||
|
subtitle = t("home.server_unreachable_message");
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<View className='flex flex-col items-center justify-center h-full -mt-6 px-8'>
|
<View className='flex flex-col items-center justify-center h-full -mt-6 px-8'>
|
||||||
<Text className='text-3xl font-bold mb-2'>{t("home.no_internet")}</Text>
|
<Text className='text-3xl font-bold mb-2'>{title}</Text>
|
||||||
<Text className='text-center opacity-70'>
|
<Text className='text-center opacity-70'>{subtitle}</Text>
|
||||||
{t("home.no_internet_message")}
|
|
||||||
</Text>
|
|
||||||
<View className='mt-4'>
|
<View className='mt-4'>
|
||||||
{!Platform.isTV && (
|
{!Platform.isTV && (
|
||||||
<Button
|
<Button
|
||||||
@@ -378,6 +398,7 @@ export const HomeIndex = () => {
|
|||||||
{t("home.go_to_downloads")}
|
{t("home.go_to_downloads")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color='black'
|
color='black'
|
||||||
onPress={retryCheck}
|
onPress={retryCheck}
|
||||||
@@ -390,9 +411,9 @@ export const HomeIndex = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{retryLoading ? (
|
{retryLoading ? (
|
||||||
<ActivityIndicator size={"small"} color={"white"} />
|
<ActivityIndicator size='small' color='white' />
|
||||||
) : (
|
) : (
|
||||||
"Retry"
|
t("home.retry")
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,30 +1,58 @@
|
|||||||
import NetInfo from "@react-native-community/netinfo";
|
import NetInfo from "@react-native-community/netinfo";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||||
|
|
||||||
|
async function checkApiReachable(basePath?: string): Promise<boolean> {
|
||||||
|
if (!basePath) return false;
|
||||||
|
try {
|
||||||
|
const response = await fetch(basePath, { method: "HEAD" });
|
||||||
|
return response.ok;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function useNetworkStatus() {
|
export function useNetworkStatus() {
|
||||||
const [isConnected, setIsConnected] = useState(false);
|
const [isConnected, setIsConnected] = useState(false);
|
||||||
|
const [serverConnected, setServerConnected] = useState<boolean | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [api] = useAtom(apiAtom);
|
||||||
|
|
||||||
|
const validateConnection = useCallback(async () => {
|
||||||
|
if (!api?.basePath) return false;
|
||||||
|
const reachable = await checkApiReachable(api.basePath);
|
||||||
|
setServerConnected(reachable);
|
||||||
|
return reachable;
|
||||||
|
}, [api?.basePath]);
|
||||||
|
|
||||||
// Manual check (optional)
|
|
||||||
const retryCheck = useCallback(async () => {
|
const retryCheck = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const state = await NetInfo.fetch();
|
await validateConnection();
|
||||||
setIsConnected(!!state.isConnected && !!state.isInternetReachable);
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, []);
|
}, [validateConnection]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = NetInfo.addEventListener((state) => {
|
const unsubscribe = NetInfo.addEventListener(async (state) => {
|
||||||
setIsConnected(!!state.isConnected && !!state.isInternetReachable);
|
setIsConnected(!!state.isConnected);
|
||||||
|
if (state.isConnected) {
|
||||||
|
await validateConnection();
|
||||||
|
} else {
|
||||||
|
setServerConnected(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initial state
|
// Initial check: wait for NetInfo first
|
||||||
NetInfo.fetch().then((state) => {
|
NetInfo.fetch().then((state) => {
|
||||||
setIsConnected(!!state.isConnected && !!state.isInternetReachable);
|
if (state.isConnected) {
|
||||||
|
validateConnection();
|
||||||
|
} else {
|
||||||
|
setServerConnected(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => unsubscribe();
|
return () => unsubscribe();
|
||||||
}, []);
|
}, [validateConnection]);
|
||||||
|
|
||||||
return { isConnected, loading, retryCheck };
|
return { isConnected, serverConnected, loading, retryCheck };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
|
import type {
|
||||||
|
BaseItemDto,
|
||||||
|
PlaybackProgressInfo,
|
||||||
|
} from "@jellyfin/sdk/lib/generated-client";
|
||||||
import { getPlaystateApi, getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
|
import { getPlaystateApi, getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAtomValue } from "jotai";
|
import { useAtomValue } from "jotai";
|
||||||
@@ -141,13 +144,10 @@ export const usePlaybackManager = ({
|
|||||||
* @param positionTicks The current playback position in ticks.
|
* @param positionTicks The current playback position in ticks.
|
||||||
*/
|
*/
|
||||||
const reportPlaybackProgress = async (
|
const reportPlaybackProgress = async (
|
||||||
itemId: string,
|
playbackProgressInfo: PlaybackProgressInfo,
|
||||||
positionTicks: number,
|
|
||||||
metadata?: {
|
|
||||||
AudioStreamIndex: number;
|
|
||||||
SubtitleStreamIndex: number;
|
|
||||||
},
|
|
||||||
) => {
|
) => {
|
||||||
|
const positionTicks = playbackProgressInfo.PositionTicks || 0;
|
||||||
|
const itemId = playbackProgressInfo.ItemId!;
|
||||||
const localItem = getDownloadedItemById(itemId);
|
const localItem = getDownloadedItemById(itemId);
|
||||||
|
|
||||||
// Handle local state update for downloaded items
|
// Handle local state update for downloaded items
|
||||||
@@ -192,14 +192,7 @@ export const usePlaybackManager = ({
|
|||||||
if (isOnline && api) {
|
if (isOnline && api) {
|
||||||
try {
|
try {
|
||||||
await getPlaystateApi(api).reportPlaybackProgress({
|
await getPlaystateApi(api).reportPlaybackProgress({
|
||||||
playbackProgressInfo: {
|
playbackProgressInfo,
|
||||||
ItemId: itemId,
|
|
||||||
PositionTicks: Math.floor(positionTicks),
|
|
||||||
...(metadata && { AudioStreamIndex: metadata.AudioStreamIndex }),
|
|
||||||
...(metadata && {
|
|
||||||
SubtitleStreamIndex: metadata.SubtitleStreamIndex,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to report playback progress", error);
|
console.error("Failed to report playback progress", error);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"android:tv": "cross-env EXPO_TV=1 expo run:android",
|
"android:tv": "cross-env EXPO_TV=1 expo run:android",
|
||||||
"build:android:local": "cd android && cross-env NODE_ENV=production ./gradlew assembleRelease",
|
"build:android:local": "cd android && cross-env NODE_ENV=production ./gradlew assembleRelease",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit | grep -v \"utils/jellyseerr\"",
|
"typecheck": "node scripts/typecheck.js",
|
||||||
"check": "biome check . --max-diagnostics 1000",
|
"check": "biome check . --max-diagnostics 1000",
|
||||||
"lint": "biome check --write --unsafe --max-diagnostics 1000",
|
"lint": "biome check --write --unsafe --max-diagnostics 1000",
|
||||||
"format": "biome format --write .",
|
"format": "biome format --write .",
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
"react-native-ios-utilities": "5.1.8",
|
"react-native-ios-utilities": "5.1.8",
|
||||||
"react-native-mmkv": "2.12.2",
|
"react-native-mmkv": "2.12.2",
|
||||||
"react-native-pager-view": "^6.9.1",
|
"react-native-pager-view": "^6.9.1",
|
||||||
"react-native-reanimated": "~3.17.4",
|
"react-native-reanimated": "~3.19.1",
|
||||||
"react-native-reanimated-carousel": "4.0.2",
|
"react-native-reanimated-carousel": "4.0.2",
|
||||||
"react-native-safe-area-context": "5.4.0",
|
"react-native-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "~4.11.1",
|
"react-native-screens": "~4.11.1",
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ function useProtectedRoute(user: UserDto | null, loaded = false) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loaded === false) return;
|
if (loaded === false) return;
|
||||||
|
|
||||||
const inAuthGroup = segments[0] === "(auth)";
|
const inAuthGroup = segments.length > 1 && segments[0] === "(auth)";
|
||||||
|
|
||||||
if (!user?.Id && inAuthGroup) {
|
if (!user?.Id && inAuthGroup) {
|
||||||
console.log("Redirected to login");
|
console.log("Redirected to login");
|
||||||
|
|||||||
256
scripts/typecheck.js
Normal file
256
scripts/typecheck.js
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
const { execFileSync } = require("node:child_process");
|
||||||
|
const process = require("node:process");
|
||||||
|
|
||||||
|
// Enhanced ANSI color codes and styles
|
||||||
|
const colors = {
|
||||||
|
red: "\x1b[31m",
|
||||||
|
green: "\x1b[32m",
|
||||||
|
yellow: "\x1b[33m",
|
||||||
|
blue: "\x1b[34m",
|
||||||
|
magenta: "\x1b[35m",
|
||||||
|
cyan: "\x1b[36m",
|
||||||
|
white: "\x1b[37m",
|
||||||
|
gray: "\x1b[90m",
|
||||||
|
reset: "\x1b[0m",
|
||||||
|
bold: "\x1b[1m",
|
||||||
|
dim: "\x1b[2m",
|
||||||
|
underline: "\x1b[4m",
|
||||||
|
bg: {
|
||||||
|
red: "\x1b[41m",
|
||||||
|
green: "\x1b[42m",
|
||||||
|
yellow: "\x1b[43m",
|
||||||
|
blue: "\x1b[44m",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const border = "━".repeat(80);
|
||||||
|
|
||||||
|
// Center the title within the border
|
||||||
|
const title = "🔥 STREAMYFIN TYPESCRIPT CHECK";
|
||||||
|
const titlePadding = Math.floor((80 - title.length) / 2);
|
||||||
|
const centeredTitle = " ".repeat(titlePadding) + title;
|
||||||
|
|
||||||
|
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
||||||
|
|
||||||
|
function log(message, color = "") {
|
||||||
|
if (useColor && color) {
|
||||||
|
console.log(`${color}${message}${colors.reset}`);
|
||||||
|
} else {
|
||||||
|
console.log(String(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatError(errorLine) {
|
||||||
|
if (!useColor) return errorLine;
|
||||||
|
|
||||||
|
// Color file paths in cyan
|
||||||
|
let formatted = errorLine.replace(
|
||||||
|
/^([^(]+\([^)]+\):)/,
|
||||||
|
`${colors.cyan}$1${colors.reset}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Color error codes in red bold
|
||||||
|
formatted = formatted.replace(
|
||||||
|
/(error TS\d+:)/g,
|
||||||
|
`${colors.red}${colors.bold}$1${colors.reset}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Color type names in yellow
|
||||||
|
formatted = formatted.replace(
|
||||||
|
/(Type '[^']*')/g,
|
||||||
|
`${colors.yellow}$1${colors.reset}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Color property names in magenta
|
||||||
|
formatted = formatted.replace(
|
||||||
|
/(Property '[^']*')/g,
|
||||||
|
`${colors.magenta}$1${colors.reset}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseErrorsAndCreateSummary(errorOutput) {
|
||||||
|
const lines = errorOutput.split("\n").filter((line) => line.trim());
|
||||||
|
const errorsByFile = new Map();
|
||||||
|
const formattedErrors = [];
|
||||||
|
|
||||||
|
let currentError = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
if (!trimmedLine) continue;
|
||||||
|
|
||||||
|
// Check if this is the start of a new error (has file path and error code)
|
||||||
|
const errorMatch = line.match(/^([^(]+\([^)]+\):)\s*(error TS\d+:)/);
|
||||||
|
|
||||||
|
if (errorMatch) {
|
||||||
|
// If we have a previous error, add it to the list
|
||||||
|
if (currentError.length > 0) {
|
||||||
|
formattedErrors.push(currentError.join("\n"));
|
||||||
|
currentError = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract file info for summary
|
||||||
|
const filePath = errorMatch[1].split("(")[0];
|
||||||
|
if (!errorsByFile.has(filePath)) {
|
||||||
|
errorsByFile.set(filePath, 0);
|
||||||
|
}
|
||||||
|
errorsByFile.set(filePath, errorsByFile.get(filePath) + 1);
|
||||||
|
|
||||||
|
// Start new error
|
||||||
|
currentError.push(formatError(line));
|
||||||
|
} else if (currentError.length > 0) {
|
||||||
|
// This is a continuation of the current error
|
||||||
|
currentError.push(` ${colors.gray}${line}${colors.reset}`);
|
||||||
|
} else if (line.match(/Found \d+ errors? in \d+ files?/)) {
|
||||||
|
// Skip the summary line; no action needed for this line
|
||||||
|
} else {
|
||||||
|
// Standalone line
|
||||||
|
formattedErrors.push(formatError(line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the last error if exists
|
||||||
|
if (currentError.length > 0) {
|
||||||
|
formattedErrors.push(currentError.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { formattedErrors, errorsByFile };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createErrorSummaryTable(errorsByFile) {
|
||||||
|
if (errorsByFile.size === 0) return "";
|
||||||
|
|
||||||
|
const sortedFiles = Array.from(errorsByFile.entries()).sort(
|
||||||
|
(a, b) => b[1] - a[1],
|
||||||
|
); // Sort by error count descending
|
||||||
|
|
||||||
|
let table = `\n${colors.gray}${colors.bold}Errors Files${colors.reset}\n`;
|
||||||
|
|
||||||
|
for (const [file, count] of sortedFiles) {
|
||||||
|
const paddedCount = String(count).padStart(6);
|
||||||
|
table += `${colors.red}${paddedCount}${colors.reset} ${colors.cyan}${file}${colors.reset}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTypeCheck() {
|
||||||
|
const extraArgs = process.argv.slice(2);
|
||||||
|
|
||||||
|
// Prefer local TypeScript binary when available
|
||||||
|
const runnerArgs = ["-p", "tsconfig.json", "--noEmit", ...extraArgs];
|
||||||
|
let execArgs = null;
|
||||||
|
try {
|
||||||
|
const tscBin = require.resolve("typescript/bin/tsc");
|
||||||
|
execArgs = { cmd: process.execPath, args: [tscBin, ...runnerArgs] };
|
||||||
|
} catch {
|
||||||
|
// fallback to PATH tsc
|
||||||
|
execArgs = {
|
||||||
|
cmd: "tsc",
|
||||||
|
args: ["-p", "tsconfig.json", "--noEmit", ...extraArgs],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log(
|
||||||
|
`🔍 ${colors.bold}Running TypeScript type check...${colors.reset} ${colors.gray}${extraArgs.join(" ")}${colors.reset}`.trim(),
|
||||||
|
colors.blue,
|
||||||
|
);
|
||||||
|
|
||||||
|
const MAX_BUFFER_SIZE = 64 * 1024 * 1024; // 64MB
|
||||||
|
|
||||||
|
execFileSync(execArgs.cmd, execArgs.args, {
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
maxBuffer: MAX_BUFFER_SIZE,
|
||||||
|
env: { ...process.env, FORCE_COLOR: "0" },
|
||||||
|
});
|
||||||
|
|
||||||
|
log(
|
||||||
|
`✅ ${colors.bold}TypeScript check passed${colors.reset} - no errors found!`,
|
||||||
|
colors.green,
|
||||||
|
);
|
||||||
|
return { ok: true };
|
||||||
|
} catch (error) {
|
||||||
|
const errorOutput = (error && (error.stderr || error.stdout)) || "";
|
||||||
|
|
||||||
|
// Filter out jellyseerr utils errors - this is a third-party git submodule
|
||||||
|
// that generates a large volume of known type errors
|
||||||
|
const filteredLines = errorOutput.split("\n").filter((line) => {
|
||||||
|
const trimmedLine = line.trim();
|
||||||
|
return trimmedLine && !trimmedLine.includes("utils/jellyseerr");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filteredLines.length > 0) {
|
||||||
|
// Count TypeScript error occurrences (TS####)
|
||||||
|
const remainingMatches = (
|
||||||
|
filteredLines.join("\n").match(/\berror\s+TS\d+:/gi) || []
|
||||||
|
).length;
|
||||||
|
|
||||||
|
// Parse errors and create formatted output with summary
|
||||||
|
const { formattedErrors, errorsByFile } = parseErrorsAndCreateSummary(
|
||||||
|
filteredLines.join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Enhanced error header
|
||||||
|
log(
|
||||||
|
`\n${colors.bg.red} ERROR ${colors.reset} ${colors.red}${colors.bold}TypeScript errors found:${colors.reset}`,
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// Display errors with spacing between each error
|
||||||
|
for (let i = 0; i < formattedErrors.length; i++) {
|
||||||
|
console.log(formattedErrors[i]);
|
||||||
|
|
||||||
|
// Add spacing between errors (but not after the last one)
|
||||||
|
if (i < formattedErrors.length - 1) {
|
||||||
|
console.log(); // Empty line between errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and display summary table
|
||||||
|
const summaryTable = createErrorSummaryTable(errorsByFile);
|
||||||
|
if (summaryTable) {
|
||||||
|
console.log(summaryTable);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean summary - just the error count
|
||||||
|
const errorIcon = "🚨";
|
||||||
|
log(
|
||||||
|
`${errorIcon} ${colors.red}${colors.bold}${remainingMatches} TypeScript error${remainingMatches !== 1 ? "s" : ""}${colors.reset}`,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
log(
|
||||||
|
`✅ ${colors.bold}TypeScript check passed${colors.reset} ${colors.gray}(jellyseerr utils errors ignored)${colors.reset}`,
|
||||||
|
colors.green,
|
||||||
|
);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced header
|
||||||
|
console.log(`${colors.blue}${colors.bold}${border}${colors.reset}`);
|
||||||
|
console.log(`${colors.blue}${colors.bold}${centeredTitle}${colors.reset}`);
|
||||||
|
console.log(`${colors.blue}${colors.bold}${border}${colors.reset}`);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// Main execution
|
||||||
|
const result = runTypeCheck();
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
if (!result.ok) {
|
||||||
|
log(
|
||||||
|
`${colors.red}${colors.bold}🔥 Typecheck failed - please fix the errors above${colors.reset}`,
|
||||||
|
);
|
||||||
|
process.exitCode = 1;
|
||||||
|
} else {
|
||||||
|
log(
|
||||||
|
`${colors.green}${colors.bold}🎉 All checks passed! Ready to ship 🚀${colors.reset}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -35,10 +35,15 @@
|
|||||||
"servers": "Servers"
|
"servers": "Servers"
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
|
"checking_server_connection": "Checking server connection...",
|
||||||
"no_internet": "No Internet",
|
"no_internet": "No Internet",
|
||||||
"no_items": "No items",
|
"no_items": "No items",
|
||||||
"no_internet_message": "No worries, you can still watch\ndownloaded content.",
|
"no_internet_message": "No worries, you can still watch\ndownloaded content.",
|
||||||
|
"checking_server_connection_message": "Checking connection to server",
|
||||||
"go_to_downloads": "Go to downloads",
|
"go_to_downloads": "Go to downloads",
|
||||||
|
"retry": "Retry",
|
||||||
|
"server_unreachable": "Server Unreachable",
|
||||||
|
"server_unreachable_message": "Could not reach the server.\nPlease check your network connection.",
|
||||||
"oops": "Oops!",
|
"oops": "Oops!",
|
||||||
"error_message": "Something went wrong.\nPlease log out and in again.",
|
"error_message": "Something went wrong.\nPlease log out and in again.",
|
||||||
"continue_watching": "Continue Watching",
|
"continue_watching": "Continue Watching",
|
||||||
|
|||||||
Submodule utils/jellyseerr updated: 4401b16414...fc6a9e952c
Reference in New Issue
Block a user