mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00: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:
|
||||
name: 🚑 Expo Doctor Check
|
||||
if: false
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: 🛒 Checkout repository
|
||||
|
||||
2
app.json
2
app.json
@@ -122,7 +122,7 @@
|
||||
[
|
||||
"expo-splash-screen",
|
||||
{
|
||||
"backgroundColor": "#2e2e2e",
|
||||
"backgroundColor": "#010101",
|
||||
"image": "./assets/images/icon-ios-plain.png",
|
||||
"imageWidth": 100
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
type BaseItemDto,
|
||||
type MediaSourceInfo,
|
||||
PlaybackOrder,
|
||||
PlaybackProgressInfo,
|
||||
PlaybackStartInfo,
|
||||
RepeatMode,
|
||||
} from "@jellyfin/sdk/lib/generated-client";
|
||||
@@ -271,12 +272,7 @@ export default function page() {
|
||||
if (isPlaying) {
|
||||
await videoRef.current?.pause();
|
||||
playbackManager.reportPlaybackProgress(
|
||||
item?.Id!,
|
||||
msToTicks(progress.get()),
|
||||
{
|
||||
AudioStreamIndex: audioIndex ?? -1,
|
||||
SubtitleStreamIndex: subtitleIndex ?? -1,
|
||||
},
|
||||
currentPlayStateInfo() as PlaybackProgressInfo,
|
||||
);
|
||||
} else {
|
||||
videoRef.current?.play();
|
||||
@@ -394,12 +390,7 @@ export default function page() {
|
||||
if (!item?.Id) return;
|
||||
|
||||
playbackManager.reportPlaybackProgress(
|
||||
item.Id,
|
||||
msToTicks(progress.get()),
|
||||
{
|
||||
AudioStreamIndex: audioIndex ?? -1,
|
||||
SubtitleStreamIndex: subtitleIndex ?? -1,
|
||||
},
|
||||
currentPlayStateInfo() as PlaybackProgressInfo,
|
||||
);
|
||||
},
|
||||
[
|
||||
@@ -506,12 +497,7 @@ export default function page() {
|
||||
setIsPlaying(true);
|
||||
if (item?.Id) {
|
||||
playbackManager.reportPlaybackProgress(
|
||||
item.Id,
|
||||
msToTicks(progress.get()),
|
||||
{
|
||||
AudioStreamIndex: audioIndex ?? -1,
|
||||
SubtitleStreamIndex: subtitleIndex ?? -1,
|
||||
},
|
||||
currentPlayStateInfo() as PlaybackProgressInfo,
|
||||
);
|
||||
}
|
||||
if (!Platform.isTV) await activateKeepAwakeAsync();
|
||||
@@ -522,12 +508,7 @@ export default function page() {
|
||||
setIsPlaying(false);
|
||||
if (item?.Id) {
|
||||
playbackManager.reportPlaybackProgress(
|
||||
item.Id,
|
||||
msToTicks(progress.get()),
|
||||
{
|
||||
AudioStreamIndex: audioIndex ?? -1,
|
||||
SubtitleStreamIndex: subtitleIndex ?? -1,
|
||||
},
|
||||
currentPlayStateInfo() as PlaybackProgressInfo,
|
||||
);
|
||||
}
|
||||
if (!Platform.isTV) await deactivateKeepAwake();
|
||||
|
||||
4
bun.lock
4
bun.lock
@@ -65,7 +65,7 @@
|
||||
"react-native-ios-utilities": "5.1.8",
|
||||
"react-native-mmkv": "2.12.2",
|
||||
"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-safe-area-context": "5.4.0",
|
||||
"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-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=="],
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
||||
userId: user.Id,
|
||||
seasonId: seasonId || undefined,
|
||||
seriesId: item.SeriesId,
|
||||
enableUserData: true,
|
||||
fields: [
|
||||
"ItemCounts",
|
||||
"PrimaryImageAspectRatio",
|
||||
@@ -70,48 +71,6 @@ export const SeasonEpisodesCarousel: React.FC<Props> = ({
|
||||
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(() => {
|
||||
if (item?.Type === "Episode" && item.Id) {
|
||||
const index = episodes?.findIndex((ep) => ep.Id === item.Id);
|
||||
|
||||
@@ -87,7 +87,7 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
|
||||
seasonId: selectedSeasonId,
|
||||
enableUserData: true,
|
||||
// Note: Including trick play is necessary to enable trick play downloads
|
||||
fields: ["MediaSources", "MediaStreams", "Overview", "Trickplay"],
|
||||
fields: ["Overview", "Trickplay"],
|
||||
});
|
||||
|
||||
if (res.data.TotalRecordCount === 0)
|
||||
@@ -102,23 +102,6 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
|
||||
});
|
||||
|
||||
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
|
||||
const [nrOfEpisodes, setNrOfEpisodes] = useState(0);
|
||||
|
||||
@@ -74,7 +74,12 @@ export const HomeIndex = () => {
|
||||
|
||||
const { getDownloadedItems, cleanCacheDirectory } = useDownload();
|
||||
const prevIsConnected = useRef<boolean | null>(false);
|
||||
const { isConnected, loading: retryLoading, retryCheck } = useNetworkStatus();
|
||||
const {
|
||||
isConnected,
|
||||
serverConnected,
|
||||
loading: retryLoading,
|
||||
retryCheck,
|
||||
} = useNetworkStatus();
|
||||
const invalidateCache = useInvalidatePlaybackProgressCache();
|
||||
useEffect(() => {
|
||||
// Only invalidate cache when transitioning from offline to online
|
||||
@@ -358,13 +363,28 @@ export const HomeIndex = () => {
|
||||
|
||||
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 (
|
||||
<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-center opacity-70'>
|
||||
{t("home.no_internet_message")}
|
||||
</Text>
|
||||
<Text className='text-3xl font-bold mb-2'>{title}</Text>
|
||||
<Text className='text-center opacity-70'>{subtitle}</Text>
|
||||
|
||||
<View className='mt-4'>
|
||||
{!Platform.isTV && (
|
||||
<Button
|
||||
@@ -378,6 +398,7 @@ export const HomeIndex = () => {
|
||||
{t("home.go_to_downloads")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
color='black'
|
||||
onPress={retryCheck}
|
||||
@@ -390,9 +411,9 @@ export const HomeIndex = () => {
|
||||
}
|
||||
>
|
||||
{retryLoading ? (
|
||||
<ActivityIndicator size={"small"} color={"white"} />
|
||||
<ActivityIndicator size='small' color='white' />
|
||||
) : (
|
||||
"Retry"
|
||||
t("home.retry")
|
||||
)}
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
@@ -1,30 +1,58 @@
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import { useAtom } from "jotai";
|
||||
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() {
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [serverConnected, setServerConnected] = useState<boolean | null>(null);
|
||||
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 () => {
|
||||
setLoading(true);
|
||||
const state = await NetInfo.fetch();
|
||||
setIsConnected(!!state.isConnected && !!state.isInternetReachable);
|
||||
await validateConnection();
|
||||
setLoading(false);
|
||||
}, []);
|
||||
}, [validateConnection]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = NetInfo.addEventListener((state) => {
|
||||
setIsConnected(!!state.isConnected && !!state.isInternetReachable);
|
||||
const unsubscribe = NetInfo.addEventListener(async (state) => {
|
||||
setIsConnected(!!state.isConnected);
|
||||
if (state.isConnected) {
|
||||
await validateConnection();
|
||||
} else {
|
||||
setServerConnected(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Initial state
|
||||
// Initial check: wait for NetInfo first
|
||||
NetInfo.fetch().then((state) => {
|
||||
setIsConnected(!!state.isConnected && !!state.isInternetReachable);
|
||||
if (state.isConnected) {
|
||||
validateConnection();
|
||||
} else {
|
||||
setServerConnected(false);
|
||||
}
|
||||
});
|
||||
|
||||
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 { useQuery } from "@tanstack/react-query";
|
||||
import { useAtomValue } from "jotai";
|
||||
@@ -141,13 +144,10 @@ export const usePlaybackManager = ({
|
||||
* @param positionTicks The current playback position in ticks.
|
||||
*/
|
||||
const reportPlaybackProgress = async (
|
||||
itemId: string,
|
||||
positionTicks: number,
|
||||
metadata?: {
|
||||
AudioStreamIndex: number;
|
||||
SubtitleStreamIndex: number;
|
||||
},
|
||||
playbackProgressInfo: PlaybackProgressInfo,
|
||||
) => {
|
||||
const positionTicks = playbackProgressInfo.PositionTicks || 0;
|
||||
const itemId = playbackProgressInfo.ItemId!;
|
||||
const localItem = getDownloadedItemById(itemId);
|
||||
|
||||
// Handle local state update for downloaded items
|
||||
@@ -192,14 +192,7 @@ export const usePlaybackManager = ({
|
||||
if (isOnline && api) {
|
||||
try {
|
||||
await getPlaystateApi(api).reportPlaybackProgress({
|
||||
playbackProgressInfo: {
|
||||
ItemId: itemId,
|
||||
PositionTicks: Math.floor(positionTicks),
|
||||
...(metadata && { AudioStreamIndex: metadata.AudioStreamIndex }),
|
||||
...(metadata && {
|
||||
SubtitleStreamIndex: metadata.SubtitleStreamIndex,
|
||||
}),
|
||||
},
|
||||
playbackProgressInfo,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to report playback progress", error);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"android:tv": "cross-env EXPO_TV=1 expo run:android",
|
||||
"build:android:local": "cd android && cross-env NODE_ENV=production ./gradlew assembleRelease",
|
||||
"prepare": "husky",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit | grep -v \"utils/jellyseerr\"",
|
||||
"typecheck": "node scripts/typecheck.js",
|
||||
"check": "biome check . --max-diagnostics 1000",
|
||||
"lint": "biome check --write --unsafe --max-diagnostics 1000",
|
||||
"format": "biome format --write .",
|
||||
@@ -83,7 +83,7 @@
|
||||
"react-native-ios-utilities": "5.1.8",
|
||||
"react-native-mmkv": "2.12.2",
|
||||
"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-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "~4.11.1",
|
||||
|
||||
@@ -374,7 +374,7 @@ function useProtectedRoute(user: UserDto | null, loaded = false) {
|
||||
useEffect(() => {
|
||||
if (loaded === false) return;
|
||||
|
||||
const inAuthGroup = segments[0] === "(auth)";
|
||||
const inAuthGroup = segments.length > 1 && segments[0] === "(auth)";
|
||||
|
||||
if (!user?.Id && inAuthGroup) {
|
||||
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"
|
||||
},
|
||||
"home": {
|
||||
"checking_server_connection": "Checking server connection...",
|
||||
"no_internet": "No Internet",
|
||||
"no_items": "No items",
|
||||
"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",
|
||||
"retry": "Retry",
|
||||
"server_unreachable": "Server Unreachable",
|
||||
"server_unreachable_message": "Could not reach the server.\nPlease check your network connection.",
|
||||
"oops": "Oops!",
|
||||
"error_message": "Something went wrong.\nPlease log out and in again.",
|
||||
"continue_watching": "Continue Watching",
|
||||
|
||||
Submodule utils/jellyseerr updated: 4401b16414...fc6a9e952c
Reference in New Issue
Block a user