mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-01 19:48:28 +01:00
Merge branch 'develop' into feat/i18n
This commit is contained in:
21
README.md
21
README.md
@@ -32,22 +32,17 @@ Downloading works by using ffmpeg to convert an HLS stream into a video file on
|
|||||||
|
|
||||||
Chromecast support is still in development, and we're working on improving it. Currently, it supports casting videos and audio, but we're working on adding support for subtitles and other features.
|
Chromecast support is still in development, and we're working on improving it. Currently, it supports casting videos and audio, but we're working on adding support for subtitles and other features.
|
||||||
|
|
||||||
## Plugins
|
### Streamyfin Plugin
|
||||||
|
|
||||||
In Streamyfin we have built-in support for a few plugins. These plugins are not required to use Streamyfin, but they add some extra functionality.
|
The Jellyfin Plugin for Streamyfin is a plugin you install into Jellyfin that hold all settings for the client Streamyfin. This allows you to syncronize settings accross all your users, like:
|
||||||
|
|
||||||
### Collection rows
|
- Auto log in to Jellyseerr without the user having to do anythin
|
||||||
|
- Choose the default languages
|
||||||
|
- Set download method and search provider
|
||||||
|
- Customize homescreen
|
||||||
|
- And more...
|
||||||
|
|
||||||
Jellyfin collections can be shown as rows or carousel on the home screen.
|
[Streamyfin Plugin](https://github.com/streamyfin/jellyfin-plugin-streamyfin)
|
||||||
The following tags can be added to a collection to provide this functionality.
|
|
||||||
|
|
||||||
Available tags:
|
|
||||||
|
|
||||||
- sf_promoted: will make the collection a row at home
|
|
||||||
- sf_carousel: will make the collection a carousel on home.
|
|
||||||
|
|
||||||
A plugin exists to create collections based on external sources like mdblist. This make the automatic process of managing collections such as trending, most watched, etc.
|
|
||||||
See [Collection Import Plugin](https://github.com/lostb1t/jellyfin-plugin-collection-import) for more info.
|
|
||||||
|
|
||||||
### Jellysearch
|
### Jellysearch
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,13 @@ export default function index() {
|
|||||||
const user = useAtomValue(userAtom);
|
const user = useAtomValue(userAtom);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [settings, _] = useSettings();
|
const [
|
||||||
|
settings,
|
||||||
|
updateSettings,
|
||||||
|
pluginSettings,
|
||||||
|
setPluginSettings,
|
||||||
|
refreshStreamyfinPluginSettings,
|
||||||
|
] = useSettings();
|
||||||
|
|
||||||
const [isConnected, setIsConnected] = useState<boolean | null>(null);
|
const [isConnected, setIsConnected] = useState<boolean | null>(null);
|
||||||
const [loadingRetry, setLoadingRetry] = useState(false);
|
const [loadingRetry, setLoadingRetry] = useState(false);
|
||||||
@@ -113,6 +119,7 @@ export default function index() {
|
|||||||
cleanCacheDirectory().catch((e) =>
|
cleanCacheDirectory().catch((e) =>
|
||||||
console.error("Something went wrong cleaning cache directory")
|
console.error("Something went wrong cleaning cache directory")
|
||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
@@ -157,6 +164,7 @@ export default function index() {
|
|||||||
|
|
||||||
const refetch = useCallback(async () => {
|
const refetch = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
await refreshStreamyfinPluginSettings();
|
||||||
await invalidateCache();
|
await invalidateCache();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -192,7 +200,7 @@ export default function index() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let sections: Section[] = [];
|
let sections: Section[] = [];
|
||||||
if (settings?.home === null || settings?.home?.sections === null) {
|
if (!settings?.home || !settings?.home?.sections) {
|
||||||
sections = useMemo(() => {
|
sections = useMemo(() => {
|
||||||
if (!api || !user?.Id) return [];
|
if (!api || !user?.Id) return [];
|
||||||
|
|
||||||
@@ -303,20 +311,33 @@ export default function index() {
|
|||||||
const section = settings.home?.sections[key];
|
const section = settings.home?.sections[key];
|
||||||
ss.push({
|
ss.push({
|
||||||
title: key,
|
title: key,
|
||||||
queryKey: ["home", key, user?.Id],
|
queryKey: ["home", key],
|
||||||
queryFn: async () =>
|
queryFn: async () => {
|
||||||
(
|
if (section.items) {
|
||||||
await getItemsApi(api).getItems({
|
const response = await getItemsApi(api).getItems({
|
||||||
userId: user?.Id,
|
userId: user?.Id,
|
||||||
limit: section.items?.limit || 20,
|
limit: section.items?.limit || 25,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
includeItemTypes: section.items?.includeItemTypes,
|
includeItemTypes: section.items?.includeItemTypes,
|
||||||
sortBy: section.items?.sortBy,
|
sortBy: section.items?.sortBy,
|
||||||
sortOrder: section.items?.sortOrder,
|
sortOrder: section.items?.sortOrder,
|
||||||
filters: section.items?.filters,
|
filters: section.items?.filters,
|
||||||
parentId: section.items?.parentId,
|
parentId: section.items?.parentId,
|
||||||
})
|
});
|
||||||
).data.Items || [],
|
return response.data.Items || [];
|
||||||
|
} else if (section.nextUp) {
|
||||||
|
const response = await getTvShowsApi(api).getNextUp({
|
||||||
|
userId: user?.Id,
|
||||||
|
fields: ["MediaSourceCount"],
|
||||||
|
limit: section.items?.limit || 25,
|
||||||
|
enableImageTypes: ["Primary", "Backdrop", "Thumb"],
|
||||||
|
enableResumable: section.items?.enableResumable || false,
|
||||||
|
enableRewatching: section.items?.enableRewatching || false,
|
||||||
|
});
|
||||||
|
return response.data.Items || [];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
type: "ScrollingCollectionList",
|
type: "ScrollingCollectionList",
|
||||||
orientation: section?.orientation || "vertical",
|
orientation: section?.orientation || "vertical",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
|
|||||||
else
|
else
|
||||||
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
|
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item.ImageTags?.["Thumb"])
|
||||||
|
return `${api?.basePath}/Items/${item.Id}/Images/Thumb?fillHeight=389&quality=80&tag=${item.ImageTags?.["Thumb"]}`;
|
||||||
|
else
|
||||||
|
return `${api?.basePath}/Items/${item.Id}/Images/Primary?fillHeight=389&quality=80`;
|
||||||
}, [item]);
|
}, [item]);
|
||||||
|
|
||||||
const progress = useMemo(() => {
|
const progress = useMemo(() => {
|
||||||
|
|||||||
@@ -107,7 +107,12 @@ export const ScrollingCollectionList: React.FC<Props> = ({
|
|||||||
{item.Type === "Movie" && orientation === "vertical" && (
|
{item.Type === "Movie" && orientation === "vertical" && (
|
||||||
<MoviePoster item={item} />
|
<MoviePoster item={item} />
|
||||||
)}
|
)}
|
||||||
{item.Type === "Series" && <SeriesPoster item={item} />}
|
{item.Type === "Series" && orientation === "vertical" && (
|
||||||
|
<SeriesPoster item={item} />
|
||||||
|
)}
|
||||||
|
{item.Type === "Series" && orientation === "horizontal" && (
|
||||||
|
<ContinueWatchingPoster item={item} />
|
||||||
|
)}
|
||||||
{item.Type === "Program" && (
|
{item.Type === "Program" && (
|
||||||
<ContinueWatchingPoster item={item} />
|
<ContinueWatchingPoster item={item} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import * as BackgroundFetch from "expo-background-fetch";
|
|||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import * as ScreenOrientation from "expo-screen-orientation";
|
import * as ScreenOrientation from "expo-screen-orientation";
|
||||||
import * as TaskManager from "expo-task-manager";
|
import * as TaskManager from "expo-task-manager";
|
||||||
import React, {useEffect, useMemo} from "react";
|
import React, { useEffect, useMemo } from "react";
|
||||||
import { Linking, Switch, TouchableOpacity } from "react-native";
|
import { Linking, Switch, TouchableOpacity } from "react-native";
|
||||||
import { toast } from "sonner-native";
|
import { toast } from "sonner-native";
|
||||||
import { Text } from "../common/Text";
|
import { Text } from "../common/Text";
|
||||||
@@ -55,28 +55,28 @@ export const OtherSettings: React.FC = () => {
|
|||||||
/**********************
|
/**********************
|
||||||
*********************/
|
*********************/
|
||||||
|
|
||||||
const disabled = useMemo(() => (
|
const disabled = useMemo(
|
||||||
pluginSettings?.autoRotate?.locked === true &&
|
() =>
|
||||||
pluginSettings?.defaultVideoOrientation?.locked === true &&
|
pluginSettings?.autoRotate?.locked === true &&
|
||||||
pluginSettings?.safeAreaInControlsEnabled?.locked === true &&
|
pluginSettings?.defaultVideoOrientation?.locked === true &&
|
||||||
pluginSettings?.showCustomMenuLinks?.locked === true &&
|
pluginSettings?.safeAreaInControlsEnabled?.locked === true &&
|
||||||
pluginSettings?.hiddenLibraries?.locked === true &&
|
pluginSettings?.showCustomMenuLinks?.locked === true &&
|
||||||
pluginSettings?.disableHapticFeedback?.locked === true
|
pluginSettings?.hiddenLibraries?.locked === true &&
|
||||||
), [pluginSettings]);
|
pluginSettings?.disableHapticFeedback?.locked === true,
|
||||||
|
[pluginSettings]
|
||||||
|
);
|
||||||
|
|
||||||
const orientations = [
|
const orientations = [
|
||||||
ScreenOrientation.OrientationLock.DEFAULT,
|
ScreenOrientation.OrientationLock.DEFAULT,
|
||||||
ScreenOrientation.OrientationLock.PORTRAIT_UP,
|
ScreenOrientation.OrientationLock.PORTRAIT_UP,
|
||||||
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT,
|
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT,
|
||||||
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT
|
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT,
|
||||||
]
|
];
|
||||||
|
|
||||||
if (!settings) return null;
|
if (!settings) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DisabledSetting
|
<DisabledSetting disabled={disabled}>
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<ListGroup title={t("home.settings.other.other_title")} className="">
|
<ListGroup title={t("home.settings.other.other_title")} className="">
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.other.auto_rotate")}
|
title={t("home.settings.other.auto_rotate")}
|
||||||
@@ -85,32 +85,40 @@ export const OtherSettings: React.FC = () => {
|
|||||||
<Switch
|
<Switch
|
||||||
value={settings.autoRotate}
|
value={settings.autoRotate}
|
||||||
disabled={pluginSettings?.autoRotate?.locked}
|
disabled={pluginSettings?.autoRotate?.locked}
|
||||||
onValueChange={(value) => updateSettings({autoRotate: value})}
|
onValueChange={(value) => updateSettings({ autoRotate: value })}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title={t("home.settings.other.video_orientation")}
|
title={t("home.settings.other.video_orientation")}
|
||||||
disabled={pluginSettings?.defaultVideoOrientation?.locked || settings.autoRotate}
|
disabled={
|
||||||
|
pluginSettings?.defaultVideoOrientation?.locked ||
|
||||||
|
settings.autoRotate
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
data={orientations}
|
data={orientations}
|
||||||
disabled={pluginSettings?.defaultVideoOrientation?.locked || settings.autoRotate}
|
disabled={
|
||||||
keyExtractor={String}
|
pluginSettings?.defaultVideoOrientation?.locked ||
|
||||||
titleExtractor={(item) =>
|
settings.autoRotate
|
||||||
t(ScreenOrientationEnum[item])
|
|
||||||
}
|
}
|
||||||
|
keyExtractor={String}
|
||||||
|
titleExtractor={(item) => ScreenOrientationEnum[item]}
|
||||||
title={
|
title={
|
||||||
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
<TouchableOpacity className="flex flex-row items-center justify-between py-3 pl-3">
|
||||||
<Text className="mr-1 text-[#8E8D91]">
|
<Text className="mr-1 text-[#8E8D91]">
|
||||||
{t(ScreenOrientationEnum[settings.defaultVideoOrientation])}
|
{t(ScreenOrientationEnum[settings.defaultVideoOrientation])}
|
||||||
</Text>
|
</Text>
|
||||||
<Ionicons name="chevron-expand-sharp" size={18} color="#5A5960"/>
|
<Ionicons
|
||||||
|
name="chevron-expand-sharp"
|
||||||
|
size={18}
|
||||||
|
color="#5A5960"
|
||||||
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
}
|
}
|
||||||
label={t("home.settings.other.orientation")}
|
label={t("home.settings.other.orientation")}
|
||||||
onSelected={(defaultVideoOrientation) =>
|
onSelected={(defaultVideoOrientation) =>
|
||||||
updateSettings({defaultVideoOrientation})
|
updateSettings({ defaultVideoOrientation })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@@ -123,7 +131,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
value={settings.safeAreaInControlsEnabled}
|
value={settings.safeAreaInControlsEnabled}
|
||||||
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
disabled={pluginSettings?.safeAreaInControlsEnabled?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateSettings({safeAreaInControlsEnabled: value})
|
updateSettings({ safeAreaInControlsEnabled: value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@@ -141,7 +149,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
value={settings.showCustomMenuLinks}
|
value={settings.showCustomMenuLinks}
|
||||||
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
disabled={pluginSettings?.showCustomMenuLinks?.locked}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
updateSettings({showCustomMenuLinks: value})
|
updateSettings({ showCustomMenuLinks: value })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@@ -158,7 +166,7 @@ export const OtherSettings: React.FC = () => {
|
|||||||
value={settings.disableHapticFeedback}
|
value={settings.disableHapticFeedback}
|
||||||
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
disabled={pluginSettings?.disableHapticFeedback?.locked}
|
||||||
onValueChange={(disableHapticFeedback) =>
|
onValueChange={(disableHapticFeedback) =>
|
||||||
updateSettings({disableHapticFeedback})
|
updateSettings({ disableHapticFeedback })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|||||||
@@ -177,6 +177,14 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
|
|
||||||
useInterval(pollQuickConnect, isPolling ? 1000 : null);
|
useInterval(pollQuickConnect, isPolling ? 1000 : null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
await refreshStreamyfinPluginSettings();
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useInterval(refreshStreamyfinPluginSettings, 60 * 5 * 1000); // 5 min
|
||||||
|
|
||||||
const discoverServers = async (url: string): Promise<Server[]> => {
|
const discoverServers = async (url: string): Promise<Server[]> => {
|
||||||
const servers = await jellyfin?.discovery.getRecommendedServerCandidates(
|
const servers = await jellyfin?.discovery.getRecommendedServerCandidates(
|
||||||
url
|
url
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export type Home = {
|
|||||||
export type HomeSection = {
|
export type HomeSection = {
|
||||||
orientation?: "horizontal" | "vertical";
|
orientation?: "horizontal" | "vertical";
|
||||||
items?: HomeSectionItemResolver;
|
items?: HomeSectionItemResolver;
|
||||||
|
nextUp?: HomeSectionNextUpResolver;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HomeSectionItemResolver = {
|
export type HomeSectionItemResolver = {
|
||||||
@@ -92,6 +93,13 @@ export type HomeSectionItemResolver = {
|
|||||||
filters?: Array<ItemFilter>;
|
filters?: Array<ItemFilter>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type HomeSectionNextUpResolver = {
|
||||||
|
parentId?: string;
|
||||||
|
limit?: number;
|
||||||
|
enableResumable?: boolean;
|
||||||
|
enableRewatching?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type Settings = {
|
export type Settings = {
|
||||||
home?: Home | null;
|
home?: Home | null;
|
||||||
autoRotate?: boolean;
|
autoRotate?: boolean;
|
||||||
@@ -191,7 +199,14 @@ const loadSettings = (): Settings => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const EXCLUDE_FROM_SAVE = ["home"];
|
||||||
|
|
||||||
const saveSettings = (settings: Settings) => {
|
const saveSettings = (settings: Settings) => {
|
||||||
|
Object.keys(settings).forEach((key) => {
|
||||||
|
if (EXCLUDE_FROM_SAVE.includes(key)) {
|
||||||
|
delete settings[key as keyof Settings];
|
||||||
|
}
|
||||||
|
});
|
||||||
const jsonValue = JSON.stringify(settings);
|
const jsonValue = JSON.stringify(settings);
|
||||||
storage.set("settings", jsonValue);
|
storage.set("settings", jsonValue);
|
||||||
};
|
};
|
||||||
@@ -223,11 +238,13 @@ export const useSettings = () => {
|
|||||||
|
|
||||||
const refreshStreamyfinPluginSettings = useCallback(async () => {
|
const refreshStreamyfinPluginSettings = useCallback(async () => {
|
||||||
if (!api) return;
|
if (!api) return;
|
||||||
const settings = await api
|
const settings = await api.getStreamyfinPluginConfig().then(
|
||||||
.getStreamyfinPluginConfig()
|
({ data }) => {
|
||||||
.then(({ data }) => data?.settings);
|
writeInfoLog(`Got remote settings`);
|
||||||
|
return data?.settings;
|
||||||
writeInfoLog(`Got remote settings: ${JSON.stringify(settings)}`);
|
},
|
||||||
|
(err) => undefined
|
||||||
|
);
|
||||||
|
|
||||||
setPluginSettings(settings);
|
setPluginSettings(settings);
|
||||||
return settings;
|
return settings;
|
||||||
@@ -277,6 +294,7 @@ export const useSettings = () => {
|
|||||||
if (Object.keys(unlockedPluginDefaults).length > 0) {
|
if (Object.keys(unlockedPluginDefaults).length > 0) {
|
||||||
updateSettings(unlockedPluginDefaults);
|
updateSettings(unlockedPluginDefaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
..._settings,
|
..._settings,
|
||||||
...overrideSettings,
|
...overrideSettings,
|
||||||
|
|||||||
Reference in New Issue
Block a user