diff --git a/components/settings/AudioToggles.tsx b/components/settings/AudioToggles.tsx new file mode 100644 index 00000000..4b2bc043 --- /dev/null +++ b/components/settings/AudioToggles.tsx @@ -0,0 +1,78 @@ +import { TouchableOpacity, View, ViewProps } from "react-native"; +import * as DropdownMenu from "zeego/dropdown-menu"; +import { Text } from "../common/Text"; +import { useMedia } from "./MediaContext"; + +interface Props extends ViewProps {} + +export const AudioToggles: React.FC = ({ ...props }) => { + const media = useMedia(); + const { settings, updateSettings } = media; + const cultures = media.cultures; + + if (!settings) return null; + + return ( + + Audio + + + + Audio language + + Choose a default audio language. + + + + + + + {settings?.defaultAudioLanguage?.DisplayName || "None"} + + + + + Languages + { + updateSettings({ + defaultAudioLanguage: null, + }); + }} + > + None + + {cultures?.map((l) => ( + { + updateSettings({ + defaultAudioLanguage: l, + }); + }} + > + + {l.DisplayName} + + + ))} + + + + + + ); +}; diff --git a/components/settings/MediaContext.tsx b/components/settings/MediaContext.tsx new file mode 100644 index 00000000..70aefbab --- /dev/null +++ b/components/settings/MediaContext.tsx @@ -0,0 +1,136 @@ +import { Settings, useSettings } from "@/utils/atoms/settings"; +import { useAtomValue } from "jotai"; +import React, { createContext, useContext, ReactNode, useEffect } from "react"; +import { apiAtom } from "@/providers/JellyfinProvider"; +import { getLocalizationApi, getUserApi } from "@jellyfin/sdk/lib/utils/api"; +import { + CultureDto, + UserDto, + UserConfiguration, +} from "@jellyfin/sdk/lib/generated-client/models"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; + +interface MediaContextType { + settings: any; + updateSettings: any; + user: UserDto | undefined; + cultures: CultureDto[]; +} + +const MediaContext = createContext(undefined); + +export const useMedia = () => { + const context = useContext(MediaContext); + if (!context) { + throw new Error("useMedia must be used within a MediaProvider"); + } + return context; +}; + +export const MediaProvider = ({ children }: { children: ReactNode }) => { + const [settings, updateSettings] = useSettings(); + const api = useAtomValue(apiAtom); + const queryClient = useQueryClient(); + + const updateSetingsWrapper = (update: Partial) => { + const updateUserConfiguration = async ( + update: Partial + ) => { + if (api && user) { + try { + await getUserApi(api).updateUserConfiguration({ + userConfiguration: { + ...user.Configuration, + ...update, + }, + }); + queryClient.invalidateQueries({ queryKey: ["authUser"] }); + } catch (error) {} + } + }; + + updateSettings(update); + + console.log("update", update); + + let updatePayload = {} as Partial; + + updatePayload.AudioLanguagePreference = + update?.defaultAudioLanguage === null + ? "" + : update?.defaultAudioLanguage?.ThreeLetterISOLanguageName || + settings?.defaultAudioLanguage?.ThreeLetterISOLanguageName || + ""; + + updatePayload.SubtitleLanguagePreference = + update?.defaultSubtitleLanguage === null + ? "" + : update?.defaultSubtitleLanguage?.ThreeLetterISOLanguageName || + settings?.defaultSubtitleLanguage?.ThreeLetterISOLanguageName || + ""; + + console.log("updatePayload", updatePayload); + + updateUserConfiguration(updatePayload); + }; + + const { data: user } = useQuery({ + queryKey: ["authUser"], + queryFn: async () => { + if (!api) return; + + const userApi = await getUserApi(api).getCurrentUser(); + return userApi.data; + }, + enabled: !!api, + staleTime: 0, + refetchOnMount: true, + }); + + const { data: cultures = [] } = useQuery({ + queryKey: ["cultures"], + queryFn: async () => { + if (!api) return []; + const localizationApi = await getLocalizationApi(api).getCultures(); + const cultures = localizationApi.data; + return cultures; + }, + enabled: !!api, + staleTime: 0, + refetchOnMount: true, + }); + + // Set default settings from user configuration.s + useEffect(() => { + const userSubtitlePreference = + user?.Configuration?.SubtitleLanguagePreference; + const userAudioPreference = user?.Configuration?.AudioLanguagePreference; + + const subtitlePreference = cultures.find( + (x) => x.ThreeLetterISOLanguageName === userSubtitlePreference + ); + const audioPreference = cultures.find( + (x) => x.ThreeLetterISOLanguageName === userAudioPreference + ); + + updateSettings({ + defaultSubtitleLanguage: subtitlePreference, + defaultAudioLanguage: audioPreference, + }); + }, [user, cultures]); + + if (!api) return null; + + return ( + + {children} + + ); +}; diff --git a/components/settings/MediaToggles.tsx b/components/settings/MediaToggles.tsx index 0d8f0f9e..c92902e2 100644 --- a/components/settings/MediaToggles.tsx +++ b/components/settings/MediaToggles.tsx @@ -1,9 +1,6 @@ import { useSettings } from "@/utils/atoms/settings"; import { TouchableOpacity, View, ViewProps } from "react-native"; -import * as DropdownMenu from "zeego/dropdown-menu"; import { Text } from "../common/Text"; -import { LANGUAGES } from "@/constants/Languages"; -import { TextInput } from "react-native-gesture-handler"; interface Props extends ViewProps {} @@ -16,152 +13,6 @@ export const MediaToggles: React.FC = ({ ...props }) => { Media - - - Audio language - - Choose a default audio language. - - - - - - {settings?.defaultAudioLanguage?.label || "None"} - - - - Languages - { - updateSettings({ - defaultAudioLanguage: null, - }); - }} - > - None - - {LANGUAGES.map((l) => ( - { - updateSettings({ - defaultAudioLanguage: l, - }); - }} - > - {l.label} - - ))} - - - - - - Subtitle language - - Choose a default subtitle language. - - - - - - - {settings?.defaultSubtitleLanguage?.label || "None"} - - - - - Languages - { - updateSettings({ - defaultSubtitleLanguage: null, - }); - }} - > - None - - {LANGUAGES.map((l) => ( - { - updateSettings({ - defaultSubtitleLanguage: l, - }); - }} - > - {l.label} - - ))} - - - - - - - Subtitle Size - - Choose a default subtitle size for direct play (only works for - some subtitle formats). - - - - - updateSettings({ - subtitleSize: Math.max(0, settings.subtitleSize - 5), - }) - } - className="w-8 h-8 bg-neutral-800 rounded-l-lg flex items-center justify-center" - > - - - - - {settings.subtitleSize} - - - updateSettings({ - subtitleSize: Math.min(120, settings.subtitleSize + 5), - }) - } - > - + - - - - = ({ ...props }) => { */} - + + + + + Other @@ -409,19 +420,24 @@ export const SettingToggles: React.FC = ({ ...props }) => { Show Custom Menu Links - Show custom menu links defined inside your Jellyfin web config.json file + Show custom menu links defined inside your Jellyfin web + config.json file - Linking.openURL("https://jellyfin.org/docs/general/clients/web-config/#custom-menu-links") + onPress={() => + Linking.openURL( + "https://jellyfin.org/docs/general/clients/web-config/#custom-menu-links" + ) } > More info updateSettings({ showCustomMenuLinks: value })} + value={settings.showCustomMenuLinks} + onValueChange={(value) => + updateSettings({ showCustomMenuLinks: value }) + } /> @@ -491,15 +507,16 @@ export const SettingToggles: React.FC = ({ ...props }) => { className={` flex flex-row space-x-2 items-center justify-between bg-neutral-900 p-4 ${ - settings.downloadMethod === "remux" - ? "opacity-100" - : "opacity-50" - }`} + settings.downloadMethod === "remux" + ? "opacity-100" + : "opacity-50" + }`} > Remux max download - This is the total media you want to be able to download at the same time. + This is the total media you want to be able to download at the + same time. = ({ ...props }) => { step={1} min={1} max={4} - onUpdate={(value) => updateSettings({remuxConcurrentLimit: value as Settings["remuxConcurrentLimit"]})} + onUpdate={(value) => + updateSettings({ + remuxConcurrentLimit: + value as Settings["remuxConcurrentLimit"], + }) + } /> = ({ ...props }) => { className={` flex flex-row space-x-2 items-center justify-between bg-neutral-900 p-4 ${ - settings.downloadMethod === "optimized" - ? "opacity-100" - : "opacity-50" - }`} + settings.downloadMethod === "optimized" + ? "opacity-100" + : "opacity-50" + }`} > Auto download diff --git a/components/settings/SubtitleToggles.tsx b/components/settings/SubtitleToggles.tsx new file mode 100644 index 00000000..cf3bb6dc --- /dev/null +++ b/components/settings/SubtitleToggles.tsx @@ -0,0 +1,134 @@ +import { useSettings } from "@/utils/atoms/settings"; +import { TouchableOpacity, View, ViewProps } from "react-native"; +import * as DropdownMenu from "zeego/dropdown-menu"; +import { Text } from "../common/Text"; +import { useMedia } from "./MediaContext"; +import { Switch } from "react-native-gesture-handler"; + +interface Props extends ViewProps {} + +export const SubtitleToggles: React.FC = ({ ...props }) => { + const media = useMedia(); + const { settings, updateSettings } = media; + const cultures = media.cultures; + + if (!settings) return null; + + return ( + + Subtitle + + + + Subtitle language + + Choose a default subtitle language. + + + + + + + {settings?.defaultSubtitleLanguage?.DisplayName || "None"} + + + + + Languages + { + updateSettings({ + defaultSubtitleLanguage: null, + }); + }} + > + None + + {cultures?.map((l) => ( + { + updateSettings({ + defaultSubtitleLanguage: l, + }); + }} + > + + {l.DisplayName} + + + ))} + + + + + + + Subtitle Size + + Choose a default subtitle size for direct play (only works for + some subtitle formats). + + + + + updateSettings({ + subtitleSize: Math.max(0, settings.subtitleSize - 5), + }) + } + className="w-8 h-8 bg-neutral-800 rounded-l-lg flex items-center justify-center" + > + - + + + {settings.subtitleSize} + + + updateSettings({ + subtitleSize: Math.min(120, settings.subtitleSize + 5), + }) + } + > + + + + + + + + + + + Made by: lostb1t + + + updateSettings({ usePopularPlugin: value }) + } + /> + + + + + ); +}; diff --git a/hooks/useDefaultPlaySettings.ts b/hooks/useDefaultPlaySettings.ts index b13e1c43..45062e59 100644 --- a/hooks/useDefaultPlaySettings.ts +++ b/hooks/useDefaultPlaySettings.ts @@ -17,34 +17,23 @@ const useDefaultPlaySettings = ( // 2. Get default or preferred audio const defaultAudioIndex = mediaSource?.DefaultAudioStreamIndex; const preferedAudioIndex = mediaSource?.MediaStreams?.find( - (x) => x.Type === "Audio" && x.Language === settings?.defaultAudioLanguage + (x) => + x.Type === "Audio" && + x.Language === + settings?.defaultAudioLanguage?.ThreeLetterISOLanguageName )?.Index; const firstAudioIndex = mediaSource?.MediaStreams?.find( (x) => x.Type === "Audio" )?.Index; - // 3. Get default or preferred subtitle - const preferedSubtitleIndex = mediaSource?.MediaStreams?.find( - (x) => - x.Type === "Subtitle" && - x.Language === settings?.defaultSubtitleLanguage?.value - )?.Index; - - const defaultSubtitleIndex = mediaSource?.MediaStreams?.find( - (stream) => stream.Type === "Subtitle" && stream.IsDefault - )?.Index; - // 4. Get default bitrate const bitrate = BITRATES[0]; return { defaultAudioIndex: preferedAudioIndex || defaultAudioIndex || firstAudioIndex || undefined, - defaultSubtitleIndex: - preferedSubtitleIndex !== undefined - ? preferedSubtitleIndex - : defaultSubtitleIndex || undefined, + defaultSubtitleIndex: mediaSource?.DefaultSubtitleStreamIndex || -1, defaultMediaSource: mediaSource || undefined, defaultBitrate: bitrate || undefined, }; diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index ca825e87..6e745be7 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -3,6 +3,7 @@ import { useEffect } from "react"; import * as ScreenOrientation from "expo-screen-orientation"; import { storage } from "../mmkv"; import { Platform } from "react-native"; +import { CultureDto } from "@jellyfin/sdk/lib/generated-client"; export type DownloadQuality = "original" | "high" | "low"; @@ -66,8 +67,8 @@ export type Settings = { openInVLC?: boolean; downloadQuality?: DownloadOption; libraryOptions: LibraryOptions; - defaultSubtitleLanguage: DefaultLanguageOption | null; - defaultAudioLanguage: DefaultLanguageOption | null; + defaultSubtitleLanguage: CultureDto | null; + defaultAudioLanguage: CultureDto | null; showHomeTitles: boolean; defaultVideoOrientation: ScreenOrientation.OrientationLock; forwardSkipTime: number; @@ -144,6 +145,7 @@ export const useSettings = () => { const updateSettings = (update: Partial) => { if (settings) { const newSettings = { ...settings, ...update }; + setSettings(newSettings); saveSettings(newSettings); } diff --git a/utils/jellyfin/getDefaultPlaySettings.ts b/utils/jellyfin/getDefaultPlaySettings.ts index d82b387f..97c8e1e8 100644 --- a/utils/jellyfin/getDefaultPlaySettings.ts +++ b/utils/jellyfin/getDefaultPlaySettings.ts @@ -35,19 +35,14 @@ export function getDefaultPlaySettings( // 2. Get default or preferred audio const defaultAudioIndex = mediaSource?.DefaultAudioStreamIndex; const preferedAudioIndex = mediaSource?.MediaStreams?.find( - (x) => x.Language === settings?.defaultAudioLanguage + (x) => x.Type === "Audio" && x.Language === settings?.defaultAudioLanguage )?.Index; const firstAudioIndex = mediaSource?.MediaStreams?.find( (x) => x.Type === "Audio" )?.Index; - // 3. Get default or preferred subtitle - const preferedSubtitleIndex = mediaSource?.MediaStreams?.find( - (x) => x.Language === settings?.defaultSubtitleLanguage?.value - )?.Index; - const defaultSubtitleIndex = mediaSource?.MediaStreams?.find( - (stream) => stream.Type === "Subtitle" && stream.IsDefault - )?.Index; + // TODO: Need to most common next subtitle index as an option. + const finalSubtitleIndex = mediaSource?.DefaultAudioStreamIndex; // 4. Get default bitrate const bitrate = BITRATES.sort( @@ -59,6 +54,6 @@ export function getDefaultPlaySettings( bitrate, mediaSource, audioIndex: preferedAudioIndex ?? defaultAudioIndex ?? firstAudioIndex, - subtitleIndex: preferedSubtitleIndex ?? defaultSubtitleIndex ?? -1, + subtitleIndex: finalSubtitleIndex || -1, }; } diff --git a/utils/jellyfin/media/getStreamUrl.ts b/utils/jellyfin/media/getStreamUrl.ts index 0b3b853d..0250ebeb 100644 --- a/utils/jellyfin/media/getStreamUrl.ts +++ b/utils/jellyfin/media/getStreamUrl.ts @@ -109,7 +109,6 @@ export const getStreamUrl = async ({ if (item.MediaType === "Video") { if (mediaSource?.TranscodingUrl) { - const urlObj = new URL(api.basePath + mediaSource?.TranscodingUrl); // Create a URL object // If there is no subtitle stream index, add it to the URL. @@ -124,10 +123,7 @@ export const getStreamUrl = async ({ // Get the updated URL const transcodeUrl = urlObj.toString(); - console.log( - "Video has transcoding URL:", - `${transcodeUrl}` - ); + console.log("Video has transcoding URL:", `${transcodeUrl}`); return { url: transcodeUrl, sessionId: sessionId, diff --git a/utils/rankStreamType.ts b/utils/rankStreamType.ts new file mode 100644 index 00000000..49469acb --- /dev/null +++ b/utils/rankStreamType.ts @@ -0,0 +1,96 @@ +export function rankStreamType( + prevIndex, + prevSource, + mediaStreams, + trackOptions, + streamType, + isSecondarySubtitle +) { + if (prevIndex == -1) { + console.debug(`AutoSet ${streamType} - No Stream Set`); + if (streamType == "Subtitle") { + if (isSecondarySubtitle) { + trackOptions.DefaultSecondarySubtitleStreamIndex = -1; + } else { + trackOptions.DefaultSubtitleStreamIndex = -1; + } + } + return; + } + + if (!prevSource.MediaStreams || !mediaStreams) { + console.debug(`AutoSet ${streamType} - No MediaStreams`); + return; + } + + let bestStreamIndex = null; + let bestStreamScore = 0; + const prevStream = prevSource.MediaStreams[prevIndex]; + + if (!prevStream) { + console.debug(`AutoSet ${streamType} - No prevStream`); + return; + } + + console.debug( + `AutoSet ${streamType} - Previous was ${prevStream.Index} - ${prevStream.DisplayTitle}` + ); + + let prevRelIndex = 0; + for (const stream of prevSource.MediaStreams) { + if (stream.Type != streamType) continue; + + if (stream.Index == prevIndex) break; + + prevRelIndex += 1; + } + + let newRelIndex = 0; + for (const stream of mediaStreams) { + if (stream.Type != streamType) continue; + + let score = 0; + + if (prevStream.Codec == stream.Codec) score += 1; + if (prevRelIndex == newRelIndex) score += 1; + if ( + prevStream.DisplayTitle && + prevStream.DisplayTitle == stream.DisplayTitle + ) + score += 2; + if ( + prevStream.Language && + prevStream.Language != "und" && + prevStream.Language == stream.Language + ) + score += 2; + + console.debug( + `AutoSet ${streamType} - Score ${score} for ${stream.Index} - ${stream.DisplayTitle}` + ); + if (score > bestStreamScore && score >= 3) { + bestStreamScore = score; + bestStreamIndex = stream.Index; + } + + newRelIndex += 1; + } + + if (bestStreamIndex != null) { + console.debug( + `AutoSet ${streamType} - Using ${bestStreamIndex} score ${bestStreamScore}.` + ); + if (streamType == "Subtitle") { + if (isSecondarySubtitle) { + trackOptions.DefaultSecondarySubtitleStreamIndex = bestStreamIndex; + } else { + trackOptions.DefaultSubtitleStreamIndex = bestStreamIndex; + } + } + if (streamType == "Audio") { + trackOptions.DefaultAudioStreamIndex = bestStreamIndex; + } + } else { + console.debug(`AutoSet ${streamType} - Threshold not met. Using default.`); + } +}