chore: Apply linting rules and add git hok (#611)

Co-authored-by: Fredrik Burmester <fredrik.burmester@gmail.com>
This commit is contained in:
lostb1t
2025-03-16 18:01:12 +01:00
committed by GitHub
parent 2688e1b981
commit 92513e234f
268 changed files with 9197 additions and 8394 deletions

View File

@@ -1,9 +1,9 @@
import { apiAtom } from "@/providers/JellyfinProvider";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { useAtomValue } from "jotai";
import { useMemo } from "react";
interface AdjacentEpisodesProps {
item?: BaseItemDto | null;

View File

@@ -5,11 +5,11 @@ import {
useSharedValue,
} from "react-native-reanimated";
export const useControlsVisibility = (timeout: number = 3000) => {
export const useControlsVisibility = (timeout = 3000) => {
const opacity = useSharedValue(1);
const hideControlsTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
null
null,
);
const showControls = useCallback(() => {

View File

@@ -1,10 +1,10 @@
import { useCallback, useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
import { writeToLog } from "@/utils/log";
import { msToSeconds, secondsToMs } from "@/utils/time";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { useCallback, useEffect, useState } from "react";
import { useHaptic } from "./useHaptic";
interface CreditTimestamps {
@@ -25,7 +25,7 @@ export const useCreditSkipper = (
currentTime: number,
seek: (time: number) => void,
play: () => void,
isVlc: boolean = false
isVlc = false,
) => {
const [api] = useAtom(apiAtom);
const [showSkipCreditButton, setShowSkipCreditButton] = useState(false);
@@ -54,7 +54,7 @@ export const useCreditSkipper = (
`${api.basePath}/Episode/${itemId}/Timestamps`,
{
headers: getAuthHeaders(api),
}
},
);
if (res?.status !== 200) {
@@ -71,7 +71,7 @@ export const useCreditSkipper = (
if (creditTimestamps) {
setShowSkipCreditButton(
currentTime > creditTimestamps.Credits.Start &&
currentTime < creditTimestamps.Credits.End
currentTime < creditTimestamps.Credits.End,
);
}
}, [creditTimestamps, currentTime]);

View File

@@ -1,7 +1,7 @@
import { Bitrate, BITRATES } from "@/components/BitrateSelector";
import { Settings } from "@/utils/atoms/settings";
import { BITRATES, Bitrate } from "@/components/BitrateSelector";
import type { Settings } from "@/utils/atoms/settings";
import {
BaseItemDto,
type BaseItemDto,
MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client";
import { useMemo } from "react";
@@ -9,7 +9,7 @@ import { useMemo } from "react";
// Used only for initial play settings.
const useDefaultPlaySettings = (
item: BaseItemDto,
settings: Settings | null
settings: Settings | null,
) => {
const playSettings = useMemo(() => {
// 1. Get first media source
@@ -21,11 +21,11 @@ const useDefaultPlaySettings = (
(x) =>
x.Type === "Audio" &&
x.Language ===
settings?.defaultAudioLanguage?.ThreeLetterISOLanguageName
settings?.defaultAudioLanguage?.ThreeLetterISOLanguageName,
)?.Index;
const firstAudioIndex = mediaSource?.MediaStreams?.find(
(x) => x.Type === "Audio"
(x) => x.Type === "Audio",
)?.Index;
// 4. Get default bitrate from settings or fallback to max

View File

@@ -1,6 +1,6 @@
import { usePlaySettings } from "@/providers/PlaySettingsProvider";
import { writeToLog } from "@/utils/log";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import * as FileSystem from "expo-file-system";
import { useRouter } from "expo-router";
import { useCallback } from "react";
@@ -41,7 +41,7 @@ export const useDownloadedFileOpener = () => {
console.error("Error opening file:", error);
}
},
[setOfflineSettings, setPlayUrl, router]
[setOfflineSettings, setPlayUrl, router],
);
return { openFile };

View File

@@ -1,9 +1,9 @@
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { useEffect, useState, useMemo } from "react";
import { useEffect, useMemo, useState } from "react";
export const useFavorite = (item: BaseItemDto) => {
const queryClient = useQueryClient();
@@ -26,7 +26,7 @@ export const useFavorite = (item: BaseItemDto) => {
...newData,
UserData: { ...old.UserData, ...newData.UserData },
};
}
},
);
};

View File

@@ -1,6 +1,6 @@
import { useSettings } from "@/utils/atoms/settings";
import { useCallback, useMemo } from "react";
import { Platform } from "react-native";
import { useSettings } from "@/utils/atoms/settings";
const Haptics = !Platform.isTV ? require("expo-haptics") : null;
export type HapticFeedbackType =
@@ -25,7 +25,7 @@ export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => {
? () => {}
: () => Haptics.impactAsync(type);
},
[]
[],
);
const createNotificationFeedback = useCallback(
(type: typeof Haptics.NotificationFeedbackType) => {
@@ -33,7 +33,7 @@ export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => {
? () => {}
: () => Haptics.notificationAsync(type);
},
[]
[],
);
const hapticHandlers = useMemo(
@@ -46,14 +46,14 @@ export const useHaptic = (feedbackType: HapticFeedbackType = "selection") => {
? () => {}
: Haptics.selectionAsync,
success: createNotificationFeedback(
Haptics.NotificationFeedbackType.Success
Haptics.NotificationFeedbackType.Success,
),
warning: createNotificationFeedback(
Haptics.NotificationFeedbackType.Warning
Haptics.NotificationFeedbackType.Warning,
),
error: createNotificationFeedback(Haptics.NotificationFeedbackType.Error),
}),
[createHapticHandler, createNotificationFeedback]
[createHapticHandler, createNotificationFeedback],
);
if (settings?.disableHapticFeedback) {

View File

@@ -7,7 +7,7 @@ import {
} from "@/utils/atoms/primaryColor";
import { getItemImage } from "@/utils/getItemImage";
import { storage } from "@/utils/mmkv";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useAtom, useAtomValue } from "jotai";
import { useEffect, useMemo } from "react";
import { Platform } from "react-native";
@@ -70,40 +70,48 @@ export const useImageColors = ({
fallback: "#fff",
cache: false,
})
.then((colors: { platform: string; dominant: string; vibrant: string; detail: string; primary: string; }) => {
let primary: string = "#fff";
let text: string = "#000";
let backup: string = "#fff";
.then(
(colors: {
platform: string;
dominant: string;
vibrant: string;
detail: string;
primary: string;
}) => {
let primary = "#fff";
let text = "#000";
let backup = "#fff";
// Select the appropriate color based on the platform
if (colors.platform === "android") {
primary = colors.dominant;
backup = colors.vibrant;
} else if (colors.platform === "ios") {
primary = colors.detail;
backup = colors.primary;
}
// Select the appropriate color based on the platform
if (colors.platform === "android") {
primary = colors.dominant;
backup = colors.vibrant;
} else if (colors.platform === "ios") {
primary = colors.detail;
backup = colors.primary;
}
// Adjust the primary color if it's too close to black
if (primary && isCloseToBlack(primary)) {
if (backup && !isCloseToBlack(backup)) primary = backup;
primary = adjustToNearBlack(primary);
}
// Adjust the primary color if it's too close to black
if (primary && isCloseToBlack(primary)) {
if (backup && !isCloseToBlack(backup)) primary = backup;
primary = adjustToNearBlack(primary);
}
// Calculate the text color based on the primary color
if (primary) text = calculateTextColor(primary);
// Calculate the text color based on the primary color
if (primary) text = calculateTextColor(primary);
setPrimaryColor({
primary,
text,
});
setPrimaryColor({
primary,
text,
});
// Cache the colors in storage
if (source.uri && primary) {
storage.set(`${source.uri}-primary`, primary);
storage.set(`${source.uri}-text`, text);
}
})
// Cache the colors in storage
if (source.uri && primary) {
storage.set(`${source.uri}-primary`, primary);
storage.set(`${source.uri}-text`, text);
}
},
)
.catch((error: any) => {
console.error("Error getting colors", error);
});

View File

@@ -62,7 +62,7 @@ const useImageStorage = () => {
console.warn("Error saving image:", error);
}
},
[]
[],
);
const loadImage = useCallback(async (key: string) => {

View File

@@ -1,10 +1,10 @@
import { useCallback, useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
import { writeToLog } from "@/utils/log";
import { msToSeconds, secondsToMs } from "@/utils/time";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { useCallback, useEffect, useState } from "react";
import { useHaptic } from "./useHaptic";
interface IntroTimestamps {
@@ -26,7 +26,7 @@ export const useIntroSkipper = (
currentTime: number,
seek: (ticks: number) => void,
play: () => void,
isVlc: boolean = false
isVlc = false,
) => {
const [api] = useAtom(apiAtom);
const [showSkipButton, setShowSkipButton] = useState(false);
@@ -54,7 +54,7 @@ export const useIntroSkipper = (
`${api.basePath}/Episode/${itemId}/IntroTimestamps`,
{
headers: getAuthHeaders(api),
}
},
);
if (res?.status !== 200) {
@@ -71,7 +71,7 @@ export const useIntroSkipper = (
if (introTimestamps) {
setShowSkipButton(
currentTime > introTimestamps.ShowSkipPromptAt &&
currentTime < introTimestamps.HideSkipPromptAt
currentTime < introTimestamps.HideSkipPromptAt,
);
}
}, [introTimestamps, currentTime]);

View File

@@ -1,4 +1,4 @@
import { useState, useCallback } from "react";
import { useCallback, useState } from "react";
import dgram from "react-native-udp";
const JELLYFIN_DISCOVERY_PORT = 7359;
@@ -53,7 +53,7 @@ export const useJellyfinDiscovery = () => {
return;
}
console.log("Discovery message sent successfully");
}
},
);
discoveryTimeout = setTimeout(() => {

View File

@@ -1,45 +1,52 @@
import axios, { AxiosError, AxiosInstance } from "axios";
import {MovieResult, Results, TvResult} from "@/utils/jellyseerr/server/models/Search";
import type { User as JellyseerrUser } from "@/utils/jellyseerr/server/entity/User";
import type {
MovieResult,
Results,
TvResult,
} from "@/utils/jellyseerr/server/models/Search";
import { storage } from "@/utils/mmkv";
import { inRange } from "lodash";
import { User as JellyseerrUser } from "@/utils/jellyseerr/server/entity/User";
import axios, { type AxiosError, type AxiosInstance } from "axios";
import { atom } from "jotai";
import { useAtom } from "jotai/index";
import { inRange } from "lodash";
import "@/augmentations";
import { useCallback, useMemo } from "react";
import { useSettings } from "@/utils/atoms/settings";
import { toast } from "sonner-native";
import type { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes";
import {
IssueStatus,
type IssueType,
} from "@/utils/jellyseerr/server/constants/issue";
import {
MediaRequestStatus,
MediaType,
} from "@/utils/jellyseerr/server/constants/media";
import MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
import {MediaRequestBody, RequestResultsResponse} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
import { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import {
SeasonWithEpisodes,
TvDetails,
} from "@/utils/jellyseerr/server/models/Tv";
import {
IssueStatus,
IssueType,
} from "@/utils/jellyseerr/server/constants/issue";
import Issue from "@/utils/jellyseerr/server/entity/Issue";
import { RTRating } from "@/utils/jellyseerr/server/api/rating/rottentomatoes";
import { writeErrorLog } from "@/utils/log";
import DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
import { t } from "i18next";
import {
import type DiscoverSlider from "@/utils/jellyseerr/server/entity/DiscoverSlider";
import type Issue from "@/utils/jellyseerr/server/entity/Issue";
import type MediaRequest from "@/utils/jellyseerr/server/entity/MediaRequest";
import type { GenreSliderItem } from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces";
import type {
MediaRequestBody,
RequestResultsResponse,
} from "@/utils/jellyseerr/server/interfaces/api/requestInterfaces";
import type {
ServiceCommonServer,
ServiceCommonServerWithDetails,
} from "@/utils/jellyseerr/server/interfaces/api/serviceInterfaces";
import type { UserResultsResponse } from "@/utils/jellyseerr/server/interfaces/api/userInterfaces";
import type { MovieDetails } from "@/utils/jellyseerr/server/models/Movie";
import type {
CombinedCredit,
PersonDetails,
} from "@/utils/jellyseerr/server/models/Person";
import type {
SeasonWithEpisodes,
TvDetails,
} from "@/utils/jellyseerr/server/models/Tv";
import { writeErrorLog } from "@/utils/log";
import { useQueryClient } from "@tanstack/react-query";
import {GenreSliderItem} from "@/utils/jellyseerr/server/interfaces/api/discoverInterfaces";
import {UserResultsResponse} from "@/utils/jellyseerr/server/interfaces/api/userInterfaces";
import {
ServiceCommonServer,
ServiceCommonServerWithDetails
} from "@/utils/jellyseerr/server/interfaces/api/serviceInterfaces";
import { t } from "i18next";
import { useCallback, useMemo } from "react";
import { toast } from "sonner-native";
interface SearchParams {
query: string;
@@ -134,15 +141,16 @@ export class JellyseerrApi {
const { status, headers, data } = response;
if (inRange(status, 200, 299)) {
if (data.version < "2.0.0") {
const error =
t("jellyseerr.toasts.jellyseer_does_not_meet_requirements");
const error = t(
"jellyseerr.toasts.jellyseer_does_not_meet_requirements",
);
toast.error(error);
throw Error(error);
}
storage.setAny(
JELLYSEERR_COOKIES,
headers["set-cookie"]?.flatMap((c) => c.split("; ")) ?? []
headers["set-cookie"]?.flatMap((c) => c.split("; ")) ?? [],
);
return {
isValid: true,
@@ -154,7 +162,7 @@ export class JellyseerrApi {
`Jellyseerr returned a ${status} for url:\n` +
response.config.url +
"\n" +
JSON.stringify(response.data)
JSON.stringify(response.data),
);
return {
isValid: false,
@@ -190,14 +198,14 @@ export class JellyseerrApi {
async discoverSettings(): Promise<DiscoverSlider[]> {
return this.axios
?.get<DiscoverSlider[]>(
Endpoints.API_V1 + Endpoints.SETTINGS + Endpoints.DISCOVER
Endpoints.API_V1 + Endpoints.SETTINGS + Endpoints.DISCOVER,
)
.then(({ data }) => data);
}
async discover(
endpoint: DiscoverEndpoint | string,
params: any
params: any,
): Promise<SearchResults> {
return this.axios
?.get<SearchResults>(Endpoints.API_V1 + endpoint, { params })
@@ -206,18 +214,23 @@ export class JellyseerrApi {
async getGenreSliders(
endpoint: Endpoints.TV | Endpoints.MOVIE,
params: any = undefined
params: any = undefined,
): Promise<GenreSliderItem[]> {
return this.axios
?.get<GenreSliderItem[]>(Endpoints.API_V1 + Endpoints.DISCOVER + Endpoints.GENRE_SLIDER + endpoint, { params })
?.get<GenreSliderItem[]>(
Endpoints.API_V1 +
Endpoints.DISCOVER +
Endpoints.GENRE_SLIDER +
endpoint,
{ params },
)
.then(({ data }) => data);
}
async search(params: SearchParams): Promise<SearchResults> {
return this.axios?.get<SearchResults>(
Endpoints.API_V1 + Endpoints.SEARCH,
{ params }
).then(({ data }) => data)
return this.axios
?.get<SearchResults>(Endpoints.API_V1 + Endpoints.SEARCH, { params })
.then(({ data }) => data);
}
async request(request: MediaRequestBody): Promise<MediaRequest> {
@@ -232,15 +245,19 @@ export class JellyseerrApi {
.then(({ data }) => data);
}
async requests(params = {
filter: "all",
take: 10,
sort: "modified",
skip: 0
}): Promise<RequestResultsResponse> {
async requests(
params = {
filter: "all",
take: 10,
sort: "modified",
skip: 0,
},
): Promise<RequestResultsResponse> {
return this.axios
?.get<RequestResultsResponse>(Endpoints.API_V1 + Endpoints.REQUEST, {params})
.then(({data}) => data);
?.get<RequestResultsResponse>(Endpoints.API_V1 + Endpoints.REQUEST, {
params,
})
.then(({ data }) => data);
}
async movieDetails(id: number) {
@@ -265,7 +282,7 @@ export class JellyseerrApi {
Endpoints.API_V1 +
Endpoints.PERSON +
`/${id}` +
Endpoints.COMBINED_CREDITS
Endpoints.COMBINED_CREDITS,
)
.then((response) => {
return response?.data;
@@ -275,7 +292,7 @@ export class JellyseerrApi {
async movieRatings(id: number) {
return this.axios
?.get<RTRating>(
`${Endpoints.API_V1}${Endpoints.MOVIE}/${id}${Endpoints.RATINGS}`
`${Endpoints.API_V1}${Endpoints.MOVIE}/${id}${Endpoints.RATINGS}`,
)
.then(({ data }) => data);
}
@@ -291,7 +308,7 @@ export class JellyseerrApi {
async tvRatings(id: number) {
return this.axios
?.get<RTRating>(
`${Endpoints.API_V1}${Endpoints.TV}/${id}${Endpoints.RATINGS}`
`${Endpoints.API_V1}${Endpoints.TV}/${id}${Endpoints.RATINGS}`,
)
.then(({ data }) => data);
}
@@ -299,7 +316,7 @@ export class JellyseerrApi {
async tvSeason(id: number, seasonId: number) {
return this.axios
?.get<SeasonWithEpisodes>(
`${Endpoints.API_V1}${Endpoints.TV}/${id}/season/${seasonId}`
`${Endpoints.API_V1}${Endpoints.TV}/${id}/season/${seasonId}`,
)
.then((response) => {
return response?.data;
@@ -308,21 +325,18 @@ export class JellyseerrApi {
async user(params: any) {
return this.axios
?.get<UserResultsResponse>(`${Endpoints.API_V1}${Endpoints.USER}`, { params })
.then(({data}) => data.results)
?.get<UserResultsResponse>(`${Endpoints.API_V1}${Endpoints.USER}`, {
params,
})
.then(({ data }) => data.results);
}
imageProxy(
path?: string,
filter: string = "original",
width: number = 1920,
quality: number = 75
) {
imageProxy(path?: string, filter = "original", width = 1920, quality = 75) {
return path
? this.axios.defaults.baseURL +
`/_next/image?` +
new URLSearchParams(
`url=https://image.tmdb.org/t/p/${filter}/${path}&w=${width}&q=${quality}`
`url=https://image.tmdb.org/t/p/${filter}/${path}&w=${width}&q=${quality}`,
).toString()
: this.axios?.defaults.baseURL +
`/images/overseerr_poster_not_found_logo_top.png`;
@@ -345,16 +359,20 @@ export class JellyseerrApi {
});
}
async service(type: 'radarr' | 'sonarr') {
async service(type: "radarr" | "sonarr") {
return this.axios
?.get<ServiceCommonServer[]>(Endpoints.API_V1 + Endpoints.SERVICE + `/${type}`)
.then(({data}) => data);
?.get<ServiceCommonServer[]>(
Endpoints.API_V1 + Endpoints.SERVICE + `/${type}`,
)
.then(({ data }) => data);
}
async serviceDetails(type: 'radarr' | 'sonarr', id: number) {
async serviceDetails(type: "radarr" | "sonarr", id: number) {
return this.axios
?.get<ServiceCommonServerWithDetails>(Endpoints.API_V1 + Endpoints.SERVICE + `/${type}` + `/${id}`)
.then(({data}) => data);
?.get<ServiceCommonServerWithDetails>(
Endpoints.API_V1 + Endpoints.SERVICE + `/${type}` + `/${id}`,
)
.then(({ data }) => data);
}
private setInterceptors() {
@@ -364,7 +382,7 @@ export class JellyseerrApi {
if (cookies) {
storage.setAny(
JELLYSEERR_COOKIES,
response.headers["set-cookie"]?.flatMap((c) => c.split("; "))
response.headers["set-cookie"]?.flatMap((c) => c.split("; ")),
);
}
return response;
@@ -378,20 +396,20 @@ export class JellyseerrApi {
`error: ${error.toString()}\n` +
`url: ${error?.config?.url}\n` +
`data:\n` +
JSON.stringify(error.response?.data)
JSON.stringify(error.response?.data),
);
if (error.status === 403) {
clearJellyseerrStorageData();
}
return Promise.reject(error);
}
},
);
this.axios.interceptors.request.use(
async (config) => {
const cookies = storage.get<string[]>(JELLYSEERR_COOKIES);
if (cookies) {
const headerName = this.axios.defaults.xsrfHeaderName!!;
const headerName = this.axios.defaults.xsrfHeaderName!;
const xsrfToken = cookies
.find((c) => c.includes(headerName))
?.split(headerName + "=")?.[1];
@@ -403,7 +421,7 @@ export class JellyseerrApi {
},
(error) => {
console.error("Jellyseerr request error", error);
}
},
);
}
}
@@ -439,55 +457,74 @@ export const useJellyseerr = () => {
switch (mediaRequest.status) {
case MediaRequestStatus.PENDING:
case MediaRequestStatus.APPROVED:
toast.success(t("jellyseerr.toasts.requested_item", {item: title}));
onSuccess?.()
toast.success(
t("jellyseerr.toasts.requested_item", { item: title }),
);
onSuccess?.();
break;
case MediaRequestStatus.DECLINED:
toast.error(t("jellyseerr.toasts.you_dont_have_permission_to_request"));
toast.error(
t("jellyseerr.toasts.you_dont_have_permission_to_request"),
);
break;
case MediaRequestStatus.FAILED:
toast.error(t("jellyseerr.toasts.something_went_wrong_requesting_media"));
toast.error(
t("jellyseerr.toasts.something_went_wrong_requesting_media"),
);
break;
}
});
},
[jellyseerrApi]
[jellyseerrApi],
);
const isJellyseerrResult = (
items: any | null | undefined
items: any | null | undefined,
): items is Results => {
return (
items &&
Object.hasOwn(items, "mediaType") &&
Object.values(MediaType).includes(items["mediaType"])
)
Object.hasOwn(items, "mediaType") &&
Object.values(MediaType).includes(items["mediaType"])
);
};
const getTitle = (item?: TvResult | TvDetails | MovieResult | MovieDetails) => {
const getTitle = (
item?: TvResult | TvDetails | MovieResult | MovieDetails,
) => {
return isJellyseerrResult(item)
? (item.mediaType == MediaType.MOVIE ? item?.title : item?.name)
: (item?.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.title : (item as TvDetails)?.name)
? item.mediaType == MediaType.MOVIE
? item?.title
: item?.name
: item?.mediaInfo.mediaType == MediaType.MOVIE
? (item as MovieDetails)?.title
: (item as TvDetails)?.name;
};
const getYear = (item?: TvResult | TvDetails | MovieResult | MovieDetails) => {
return new Date((
isJellyseerrResult(item)
? (item.mediaType == MediaType.MOVIE ? item?.releaseDate : item?.firstAirDate)
: (item?.mediaInfo.mediaType == MediaType.MOVIE ? (item as MovieDetails)?.releaseDate : (item as TvDetails)?.firstAirDate))
|| ""
)?.getFullYear?.()
const getYear = (
item?: TvResult | TvDetails | MovieResult | MovieDetails,
) => {
return new Date(
(isJellyseerrResult(item)
? item.mediaType == MediaType.MOVIE
? item?.releaseDate
: item?.firstAirDate
: item?.mediaInfo.mediaType == MediaType.MOVIE
? (item as MovieDetails)?.releaseDate
: (item as TvDetails)?.firstAirDate) || "",
)?.getFullYear?.();
};
const getMediaType = (item?: TvResult | TvDetails | MovieResult | MovieDetails): MediaType => {
const getMediaType = (
item?: TvResult | TvDetails | MovieResult | MovieDetails,
): MediaType => {
return isJellyseerrResult(item)
? item.mediaType
: item?.mediaInfo?.mediaType
: item?.mediaInfo?.mediaType;
};
const jellyseerrRegion = useMemo(
() => jellyseerrUser?.settings?.region || "US",
[jellyseerrUser]
[jellyseerrUser],
);
const jellyseerrLocale = useMemo(() => {

View File

@@ -1,10 +1,10 @@
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { markAsNotPlayed } from "@/utils/jellyfin/playstate/markAsNotPlayed";
import { markAsPlayed } from "@/utils/jellyfin/playstate/markAsPlayed";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useQueryClient } from "@tanstack/react-query";
import { useHaptic } from "./useHaptic";
import { useAtom } from "jotai";
import { useHaptic } from "./useHaptic";
export const useMarkAsPlayed = (items: BaseItemDto[]) => {
const [api] = useAtom(apiAtom);
@@ -24,10 +24,10 @@ export const useMarkAsPlayed = (items: BaseItemDto[]) => {
];
items.forEach((item) => {
if(!item.Id) return;
if (!item.Id) return;
queriesToInvalidate.push(["item", item.Id]);
});
queriesToInvalidate.forEach((queryKey) => {
queryClient.invalidateQueries({ queryKey });
});
@@ -37,7 +37,7 @@ export const useMarkAsPlayed = (items: BaseItemDto[]) => {
lightHapticFeedback();
items.forEach((item) => {
// Optimistic update
// Optimistic update
queryClient.setQueryData(
["item", item.Id],
(oldData: BaseItemDto | undefined) => {
@@ -51,17 +51,19 @@ export const useMarkAsPlayed = (items: BaseItemDto[]) => {
};
}
return oldData;
}
},
);
})
});
try {
// Process all items
await Promise.all(items.map(item =>
played
? markAsPlayed({ api, item, userId: user?.Id })
: markAsNotPlayed({ api, itemId: item?.Id, userId: user?.Id })
));
await Promise.all(
items.map((item) =>
played
? markAsPlayed({ api, item, userId: user?.Id })
: markAsNotPlayed({ api, itemId: item?.Id, userId: user?.Id }),
),
);
// Bulk invalidate
queryClient.invalidateQueries({
@@ -73,19 +75,21 @@ export const useMarkAsPlayed = (items: BaseItemDto[]) => {
"episodes",
"seasons",
"home",
...items.map(item => ["item", item.Id])
].flat()
...items.map((item) => ["item", item.Id]),
].flat(),
});
} catch (error) {
// Revert all optimistic updates on any failure
items.forEach(item => {
items.forEach((item) => {
queryClient.setQueryData(
["item", item.Id],
(oldData: BaseItemDto | undefined) =>
oldData ? {
...oldData,
UserData: { ...oldData.UserData, Played: played }
} : oldData
oldData
? {
...oldData,
UserData: { ...oldData.UserData, Played: played },
}
: oldData,
);
});
console.error("Error updating played status:", error);

View File

@@ -1,5 +1,5 @@
import orientationToOrientationLock from "@/utils/OrientationLockConverter";
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import orientationToOrientationLock from "@/utils/OrientationLockConverter";
import { useEffect, useState } from "react";
import { Platform } from "react-native";
@@ -7,7 +7,7 @@ export const useOrientation = () => {
const [orientation, setOrientation] = useState(
Platform.isTV
? ScreenOrientation.OrientationLock.LANDSCAPE
: ScreenOrientation.OrientationLock.UNKNOWN
: ScreenOrientation.OrientationLock.UNKNOWN,
);
if (Platform.isTV) return { orientation, setOrientation };
@@ -16,7 +16,7 @@ export const useOrientation = () => {
const orientationSubscription =
ScreenOrientation.addOrientationChangeListener((event) => {
setOrientation(
orientationToOrientationLock(event.orientationInfo.orientation)
orientationToOrientationLock(event.orientationInfo.orientation),
);
});

View File

@@ -2,7 +2,7 @@ import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getItemImage } from "@/utils/getItemImage";
import { writeErrorLog, writeInfoLog, writeToLog } from "@/utils/log";
import {
import type {
BaseItemDto,
MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client/models";
@@ -14,16 +14,16 @@ import { useRouter } from "expo-router";
const FFMPEGKitReactNative = !Platform.isTV
? require("ffmpeg-kit-react-native")
: null;
import { useSettings } from "@/utils/atoms/settings";
import useDownloadHelper from "@/utils/download";
import type { JobStatus } from "@/utils/optimize-server";
import type { Api } from "@jellyfin/sdk";
import { useAtomValue } from "jotai";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Platform } from "react-native";
import { toast } from "sonner-native";
import useImageStorage from "./useImageStorage";
import useDownloadHelper from "@/utils/download";
import { Api } from "@jellyfin/sdk";
import { useSettings } from "@/utils/atoms/settings";
import { JobStatus } from "@/utils/optimize-server";
import { Platform } from "react-native";
import { useTranslation } from "react-i18next";
type FFmpegSession = typeof FFMPEGKitReactNative.FFmpegSession;
type Statistics = typeof FFMPEGKitReactNative.Statistics;
@@ -107,7 +107,7 @@ export const useRemuxHlsToMp4 = () => {
setProcesses((prev: any[]) => {
return prev.filter(
(process: { itemId: string | undefined }) =>
process.itemId !== item.Id
process.itemId !== item.Id,
);
});
} catch (e) {
@@ -116,7 +116,7 @@ export const useRemuxHlsToMp4 = () => {
console.log("completeCallback ~ end");
},
[processes, setProcesses]
[processes, setProcesses],
);
const statisticsCallback = useCallback(
@@ -146,13 +146,13 @@ export const useRemuxHlsToMp4 = () => {
});
});
},
[setProcesses, completeCallback]
[setProcesses, completeCallback],
);
const startRemuxing = useCallback(
async (item: BaseItemDto, url: string, mediaSource: MediaSourceInfo) => {
const cacheDir = await FileSystem.getInfoAsync(
APP_CACHE_DOWNLOAD_DIRECTORY
APP_CACHE_DOWNLOAD_DIRECTORY,
);
if (!cacheDir.exists) {
await FileSystem.makeDirectoryAsync(APP_CACHE_DOWNLOAD_DIRECTORY, {
@@ -178,7 +178,7 @@ export const useRemuxHlsToMp4 = () => {
toast.dismiss();
},
},
}
},
);
try {
@@ -201,25 +201,25 @@ export const useRemuxHlsToMp4 = () => {
createFFmpegCommand(url, output).join(" "),
(session: any) => completeCallback(session, item),
undefined,
(s: any) => statisticsCallback(s, item)
(s: any) => statisticsCallback(s, item),
);
} catch (e) {
const error = e as Error;
console.error("Failed to remux:", error);
writeErrorLog(
`useRemuxHlsToMp4 ~ remuxing failed for item: ${item.Name},
Error: ${error.message}, Stack: ${error.stack}`
Error: ${error.message}, Stack: ${error.stack}`,
);
setProcesses((prev: any[]) => {
return prev.filter(
(process: { itemId: string | undefined }) =>
process.itemId !== item.Id
process.itemId !== item.Id,
);
});
throw error; // Re-throw the error to propagate it to the caller
}
},
[settings, processes, setProcesses, completeCallback, statisticsCallback]
[settings, processes, setProcesses, completeCallback, statisticsCallback],
);
const cancelRemuxing = useCallback(() => {

View File

@@ -1,8 +1,8 @@
import { useQuery } from "@tanstack/react-query";
import { apiAtom } from "@/providers/JellyfinProvider";
import { useAtom } from "jotai";
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
import { userAtom } from "@/providers/JellyfinProvider";
import { getSessionApi } from "@jellyfin/sdk/lib/utils/api/session-api";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { Platform } from "react-native";
const Notifications = !Platform.isTV ? require("expo-notifications") : null;
@@ -11,7 +11,10 @@ export interface useSessionsProps {
activeWithinSeconds: number;
}
export const useSessions = ({ refetchInterval = 5 * 1000, activeWithinSeconds = 360 }: useSessionsProps) => {
export const useSessions = ({
refetchInterval = 5 * 1000,
activeWithinSeconds = 360,
}: useSessionsProps) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
@@ -24,13 +27,17 @@ export const useSessions = ({ refetchInterval = 5 * 1000, activeWithinSeconds =
const response = await getSessionApi(api).getSessions({
activeWithinSeconds: activeWithinSeconds,
});
const result = response.data
.filter((s) => s.NowPlayingItem)
.sort((a, b) => (b.NowPlayingItem?.Name ?? "").localeCompare(a.NowPlayingItem?.Name ?? ""));
.sort((a, b) =>
(b.NowPlayingItem?.Name ?? "").localeCompare(
a.NowPlayingItem?.Name ?? "",
),
);
// Notifications.setBadgeCountAsync(result.length);
return result
return result;
},
refetchInterval: refetchInterval,
});

View File

@@ -1,6 +1,6 @@
import { apiAtom } from "@/providers/JellyfinProvider";
import { ticksToMs } from "@/utils/time";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { Image } from "expo-image";
import { useAtom } from "jotai";
import { useCallback, useMemo, useRef, useState } from "react";
@@ -107,7 +107,7 @@ export const useTrickplay = (item: BaseItemDto, enabled = true) => {
setTrickPlayUrl(newTrickPlayUrl);
return newTrickPlayUrl;
},
[trickplayInfo, item, api, enabled]
[trickplayInfo, item, api, enabled],
);
const prefetchAllTrickplayImages = useCallback(() => {

View File

@@ -1,8 +1,8 @@
import { useEffect } from "react";
import { Alert } from "react-native";
import { useRouter } from "expo-router";
import { useWebSocketContext } from "@/providers/WebSocketProvider";
import { useRouter } from "expo-router";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Alert } from "react-native";
interface UseWebSocketProps {
isPlaying: boolean;
@@ -42,7 +42,7 @@ export const useWebSocket = ({
console.log("Command ~ DisplayMessage");
const title = json?.Data?.Arguments?.Header;
const body = json?.Data?.Arguments?.Text;
Alert.alert(t("player.message_from_server", {message: title}), body);
Alert.alert(t("player.message_from_server", { message: title }), body);
}
};