aiostreams / packages /frontend /src /context /userData.tsx
f4b404's picture
Upload folder using huggingface_hub
f55c62a verified
Raw
History Blame Contribute Delete
16.7 kB
import React from 'react';
import { UserData } from '@aiostreams/core';
import {
QUALITIES,
RESOLUTIONS,
SERVICE_DETAILS,
DEFAULT_PRECACHE_SELECTOR,
DEFAULT_SMART_DETECT_ATTRIBUTES,
} from '../../../core/src/utils/constants';
import { useStatus } from './status';
const USER_DATA_KEY = 'aiostreams-user-data';
export function applyMigrations(config: any): UserData {
if (
config &&
config.addonPassword !== undefined &&
config.accessToken === undefined
) {
config.accessToken = config.addonPassword;
}
if (config && config.addonPassword !== undefined) {
delete config.addonPassword;
}
if (
config &&
config.accessToken !== undefined &&
config.accessKey === undefined
) {
config.accessKey = config.accessToken;
}
if (config && config.accessToken !== undefined) {
delete config.accessToken;
}
if (
config.deduplicator &&
typeof config.deduplicator.multiGroupBehaviour === 'string'
) {
switch (config.deduplicator.multiGroupBehaviour as string) {
case 'remove_uncached':
config.deduplicator.multiGroupBehaviour = 'aggressive';
break;
case 'remove_uncached_same_service':
config.deduplicator.multiGroupBehaviour = 'conservative';
break;
case 'remove_nothing':
config.deduplicator.multiGroupBehaviour = 'keep_all';
break;
}
}
if (typeof config.digitalReleaseFilter === 'boolean') {
const oldValue = config.digitalReleaseFilter;
config.digitalReleaseFilter = {
enabled: oldValue,
tolerance: 0,
requestTypes: ['movie', 'series', 'anime'],
addons: [],
};
}
if (config.titleMatching?.matchYear) {
config.yearMatching = {
enabled: true,
tolerance: config.titleMatching.yearTolerance
? config.titleMatching.yearTolerance
: 1,
requestTypes: config.titleMatching.requestTypes ?? [],
addons: config.titleMatching.addons ?? [],
};
delete config.titleMatching.matchYear;
}
if (Array.isArray(config.groups)) {
config.groups = {
enabled: config.disableGroups ? false : true,
groupings: config.groups,
behaviour: 'parallel',
};
}
if (config.showStatistics || config.statisticsPosition) {
config.statistics = {
enabled: config.showStatistics ?? false,
position: config.statisticsPosition ?? 'bottom',
statsToShow: ['addon', 'filter', 'timing'],
...(config.statistics ?? {}),
};
delete config.showStatistics;
delete config.statisticsPosition;
}
const migrateHOSBS = (
type: 'preferred' | 'required' | 'excluded' | 'included'
) => {
if (Array.isArray(config[type + 'Encodes'])) {
config[type + 'Encodes'] = config[type + 'Encodes'].filter(
(encode: string) => {
if (encode === 'H-OU' || encode === 'H-SBS') {
config[type + 'VisualTags'] = [
...(config[type + 'VisualTags'] ?? []),
encode,
];
return false;
}
return true;
}
);
}
};
migrateHOSBS('preferred');
migrateHOSBS('required');
migrateHOSBS('excluded');
migrateHOSBS('included');
// migrate comparisons of queryType to 'anime' to 'anime.series' or 'anime.movie'
const migrateAnimeQueryTypeInExpression = (expr?: string) => {
if (typeof expr !== 'string') return expr as any;
let updated = expr.replace(
/queryType\s*==\s*(["'])anime\1/g,
"(queryType == 'anime.series' or queryType == 'anime.movie')"
);
updated = updated.replace(
/(["'])anime\1\s*==\s*queryType/g,
"(queryType == 'anime.series' or queryType == 'anime.movie')"
);
updated = updated.replace(
/queryType\s*!=\s*(["'])anime\1/g,
"(queryType != 'anime.series' and queryType != 'anime.movie')"
);
updated = updated.replace(
/(["'])anime\1\s*!=\s*queryType/g,
"(queryType != 'anime.series' and queryType != 'anime.movie')"
);
return updated;
};
const expressionLists = [
'excludedStreamExpressions',
'requiredStreamExpressions',
'includedStreamExpressions',
'preferredStreamExpressions',
] as const;
for (const key of expressionLists) {
if (Array.isArray((config as any)[key])) {
(config as any)[key] = (config as any)[key].map((expr: unknown) => {
if (typeof expr === 'string') {
return migrateAnimeQueryTypeInExpression(expr);
}
if (typeof expr === 'object' && expr !== null && 'expression' in expr) {
return {
...(expr as any),
expression: migrateAnimeQueryTypeInExpression(
(expr as any).expression
),
};
}
return expr;
});
}
}
if (config.dynamicAddonFetching?.condition) {
config.dynamicAddonFetching.condition = migrateAnimeQueryTypeInExpression(
config.dynamicAddonFetching.condition
);
}
if (config.groups?.groupings) {
config.groups.groupings = config.groups.groupings.map((group: any) => ({
...group,
condition: migrateAnimeQueryTypeInExpression(group.condition),
}));
}
if (
config.rpdbUseRedirectApi !== undefined &&
config.usePosterRedirectApi === undefined
) {
config.usePosterRedirectApi = config.rpdbUseRedirectApi;
delete config.rpdbUseRedirectApi;
}
// migrate 'rpdb' to 'usePosterService' in all catalog modifications
if (Array.isArray(config.catalogModifications)) {
for (const mod of config.catalogModifications) {
if (mod.usePosterService === undefined && mod.rpdb === true) {
mod.usePosterService = true;
}
delete mod.rpdb;
}
}
// migrate alwaysPrecache to precacheCondition, then precacheCondition to precacheSelector
if (config.precacheSelector === undefined && config.precacheNextEpisode) {
// First handle the old precacheCondition field
if (config.precacheCondition !== undefined) {
// Convert condition to selector format
config.precacheSelector = `${config.precacheCondition} ? uncached(streams) : []`;
} else {
// Handle even older alwaysPrecache field
config.precacheSelector =
config.alwaysPrecache === true
? 'true ? uncached(streams) : []'
: DEFAULT_PRECACHE_SELECTOR;
}
}
delete config.alwaysPrecache;
delete config.precacheCondition;
// migrate p2pWrap to serviceWrap
if (config.p2pWrap !== undefined && config.serviceWrap === undefined) {
config.serviceWrap = config.p2pWrap;
delete config.p2pWrap;
}
// migrate stream expressions from string[] to {expression, enabled}[]
const streamExpressionKeys = [
'excludedStreamExpressions',
'requiredStreamExpressions',
'preferredStreamExpressions',
'includedStreamExpressions',
] as const;
for (const key of streamExpressionKeys) {
if (
Array.isArray(config[key]) &&
config[key].some((expr: unknown) => typeof expr === 'string')
) {
config[key] = config[key].map((expr: unknown) =>
typeof expr === 'string' ? { expression: expr, enabled: true } : expr
);
}
}
// migrate forceToTop at addon level to pinPosition set to 'top'
if (config.presets && Array.isArray(config.presets)) {
config.presets = config.presets.map((preset: any) => {
if (
preset.options?.forceToTop === true &&
preset.options.pinPosition === undefined
) {
delete preset.options.forceToTop;
return {
...preset,
options: {
...preset.options,
pinPosition: 'top',
},
};
}
return preset;
});
}
if (config.formatter && config.formatter.definition) {
config.formatter.definitions = {
...(config.formatter.definitions ?? {}),
custom: config.formatter.definition,
};
delete config.formatter.definition;
}
return config;
}
export function removeInvalidPresetReferences(config: UserData) {
// remove references to non-existent presets in options:
const existingPresetIds = config.presets?.map((preset) => preset.instanceId);
if (config.proxy) {
config.proxy.proxiedAddons = config.proxy.proxiedAddons?.filter((addon) =>
existingPresetIds?.includes(addon)
);
}
if (config.yearMatching) {
config.yearMatching.addons = config.yearMatching.addons?.filter((addon) =>
existingPresetIds?.includes(addon)
);
}
if (config.titleMatching) {
config.titleMatching.addons = config.titleMatching.addons?.filter((addon) =>
existingPresetIds?.includes(addon)
);
}
if (config.seasonEpisodeMatching) {
config.seasonEpisodeMatching.addons =
config.seasonEpisodeMatching.addons?.filter((addon) =>
existingPresetIds?.includes(addon)
);
}
if (config.groups?.groupings) {
config.groups.groupings = config.groups.groupings.map((group) => ({
...group,
addons: group.addons?.filter((addon) =>
existingPresetIds?.includes(addon)
),
}));
}
if (config.serviceWrap?.presets) {
config.serviceWrap.presets = config.serviceWrap.presets.filter((preset) =>
existingPresetIds?.includes(preset)
);
}
return config;
}
export const DefaultUserData: UserData = {
services: Object.values(SERVICE_DETAILS).map((service) => ({
id: service.id,
enabled: false,
credentials: {},
})),
presets: [],
formatter: {
id: 'gdrive',
},
preferredQualities: Object.values(QUALITIES),
preferredResolutions: Object.values(RESOLUTIONS),
excludedQualities: ['CAM', 'SCR', 'TS', 'TC'],
excludedVisualTags: ['3D'],
sortCriteria: {
global: [
{
key: 'cached',
direction: 'desc',
},
{
key: 'library',
direction: 'desc',
},
{
key: 'resolution',
direction: 'desc',
},
{
key: 'quality',
direction: 'desc',
},
{
key: 'streamExpressionScore',
direction: 'desc',
},
{
key: 'regexPatterns',
direction: 'desc',
},
{
key: 'streamType',
direction: 'desc',
},
{
key: 'visualTag',
direction: 'desc',
},
{
key: 'audioTag',
direction: 'desc',
},
{
key: 'audioChannel',
direction: 'desc',
},
{
key: 'encode',
direction: 'desc',
},
{
key: 'language',
direction: 'desc',
},
{
key: 'subtitle',
direction: 'desc',
},
{
key: 'size',
direction: 'desc',
},
],
},
posterService: 'rpdb',
deduplicator: {
enabled: true,
keys: ['filename', 'infoHash'],
multiGroupBehaviour: 'aggressive',
cached: 'single_result',
uncached: 'per_service',
p2p: 'single_result',
http: 'disabled',
live: 'disabled',
youtube: 'disabled',
external: 'disabled',
smartDetectAttributes: DEFAULT_SMART_DETECT_ATTRIBUTES,
smartDetectRounding: 10,
libraryBehaviour: 'ignore',
},
autoPlay: {
enabled: true,
method: 'matchingFile',
attributes: ['resolution', 'quality', 'releaseGroup'],
},
cacheAndPlay: {
enabled: false,
streamTypes: ['usenet'],
},
statistics: {
enabled: false,
position: 'bottom',
statsToShow: ['addon', 'filter', 'timing'],
showFilterStatsOnNoStreams: true,
},
digitalReleaseFilter: {
enabled: false,
tolerance: 0,
requestTypes: [],
addons: [],
showInfoOnFilter: true,
},
ageRangeTypes: ['usenet'],
seasonEpisodeMatching: {
addons: [],
requestTypes: [],
},
yearMatching: {
addons: [],
requestTypes: [],
},
titleMatching: {
addons: [],
requestTypes: [],
},
precacheNextEpisode: false,
precacheSingleStream: true,
precacheSelector: DEFAULT_PRECACHE_SELECTOR,
enableSeadex: true,
regexOverrides: [],
checkOwned: true,
};
interface UserDataContextType {
userData: UserData;
setUserData: (data: ((prev: UserData) => UserData | null) | null) => void;
uuid: string | null;
setUuid: (uuid: string | null) => void;
password: string | null;
setPassword: (password: string | null) => void;
encryptedPassword: string | null;
setEncryptedPassword: (encryptedPassword: string | null) => void;
}
const UserDataContext = React.createContext<UserDataContextType | undefined>(
undefined
);
export function UserDataProvider({ children }: { children: React.ReactNode }) {
const { status } = useStatus();
// Initialize userData from local storage or apply default
const [userData, setUserData] = React.useState<UserData>(() => {
try {
const stored = localStorage.getItem(USER_DATA_KEY);
const data = stored ? JSON.parse(stored) : DefaultUserData;
return applyMigrations(data);
} catch {
return DefaultUserData;
}
});
const [uuid, setUuid] = React.useState<string | null>(null);
const [password, setPassword] = React.useState<string | null>(null);
const [encryptedPassword, setEncryptedPassword] = React.useState<
string | null
>(null);
// Effect to persist userData to local storage
React.useEffect(() => {
localStorage.setItem(USER_DATA_KEY, JSON.stringify(userData));
}, [userData]);
const statusApplied = React.useRef(false);
React.useEffect(() => {
if (!status || statusApplied.current) return;
statusApplied.current = true;
const forced = status.settings.forced;
const defaults = status.settings.defaults;
const services = status.settings.services;
setUserData((prev) => {
const newData = { ...prev };
newData.proxy = {
...newData.proxy,
enabled: forced.proxy.enabled ?? defaults.proxy?.enabled ?? undefined,
id: (forced.proxy.id ?? defaults.proxy?.id ?? 'builtin') as
| 'builtin'
| 'mediaflow'
| 'stremthru'
| undefined,
url: forced.proxy.url ?? defaults.proxy?.url ?? undefined,
publicUrl:
forced.proxy.publicUrl ?? defaults.proxy?.publicUrl ?? undefined,
publicIp:
forced.proxy.publicIp ?? defaults.proxy?.publicIp ?? undefined,
credentials:
forced.proxy.credentials ?? defaults.proxy?.credentials ?? undefined,
proxiedServices:
forced.proxy.proxiedServices ?? defaults.proxy?.proxiedServices ?? [],
};
newData.services = (newData.services ?? []).map((service) => {
const serviceMeta = services[service.id];
if (!serviceMeta) return service;
serviceMeta.credentials.forEach((credential) => {
if (credential.forced) {
service.credentials[credential.id] = credential.forced;
} else if (credential.default) {
service.credentials[credential.id] = credential.default;
}
});
// enable if every credential is set
service.enabled = serviceMeta.credentials.every(
(credential) =>
credential.forced ||
credential.default ||
service.credentials[credential.id] !== undefined
);
return service;
});
return newData;
});
}, [status]);
const safeSetUserData = (
data: ((prev: UserData) => UserData | null) | null
) => {
if (data === null) {
setUserData(DefaultUserData);
} else {
setUserData((prev) => {
const result = data(prev);
return result === null ? DefaultUserData : result;
});
}
};
return (
<UserDataContext.Provider
value={{
userData,
setUserData: safeSetUserData,
uuid,
setUuid,
password,
setPassword,
encryptedPassword,
setEncryptedPassword,
}}
>
{children}
</UserDataContext.Provider>
);
}
export function useUserData() {
const context = React.useContext(UserDataContext);
if (context === undefined) {
throw new Error('useUserData must be used within a UserDataProvider');
}
return context;
}
export function useParentInheritance() {
const { userData } = useUserData();
const parentConfig = userData?.parentConfig;
const strategies = parentConfig?.mergeStrategies;
function isInherited(
section:
| 'presets'
| 'services'
| 'filters'
| 'sorting'
| 'formatter'
| 'proxy'
| 'metadata'
| 'misc'
): boolean {
if (!parentConfig) return false;
const strategy = strategies?.[section] ?? 'inherit';
return strategy === 'inherit';
}
return {
hasParent: !!parentConfig,
parentUuid: parentConfig?.uuid,
isInherited,
};
}