Spaces:
Paused
Paused
| 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, | |
| }; | |
| } | |