mirror of
https://github.com/Viren070/AIOStreams.git
synced 2025-12-01 23:14:04 +01:00
feat: add ability to customise group behaviour
This commit is contained in:
@@ -360,14 +360,28 @@ export const UserDataSchema = z.object({
|
||||
requiredStreamExpressions: z.array(z.string().min(1).max(3000)).optional(),
|
||||
preferredStreamExpressions: z.array(z.string().min(1).max(3000)).optional(),
|
||||
includedStreamExpressions: z.array(z.string().min(1).max(3000)).optional(),
|
||||
disableGroups: z.boolean().optional(),
|
||||
// disableGroups: z.boolean().optional(),
|
||||
// groups: z
|
||||
// .array(
|
||||
// z.object({
|
||||
// addons: z.array(z.string().min(1)),
|
||||
// condition: z.string().min(1).max(200),
|
||||
// })
|
||||
// )
|
||||
// .optional(),
|
||||
groups: z
|
||||
.array(
|
||||
z.object({
|
||||
addons: z.array(z.string().min(1)),
|
||||
condition: z.string().min(1).max(200),
|
||||
})
|
||||
)
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
groupings: z
|
||||
.array(
|
||||
z.object({
|
||||
addons: z.array(z.string().min(1)),
|
||||
condition: z.string().min(1).max(200),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
behaviour: z.enum(['sequential', 'parallel']).optional(),
|
||||
})
|
||||
.optional(),
|
||||
sortCriteria: z.object({
|
||||
// global must be defined.
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
verifyHash,
|
||||
validateConfig,
|
||||
formatZodError,
|
||||
applyMigrations,
|
||||
} from '../utils';
|
||||
|
||||
const APIError = constants.APIError;
|
||||
@@ -193,7 +194,7 @@ export class UserRepository {
|
||||
Env.TRUSTED_UUIDS?.split(',').some((u) => new RegExp(u).test(uuid)) ??
|
||||
false;
|
||||
logger.info(`Retrieved configuration for user ${uuid}`);
|
||||
return decryptedConfig;
|
||||
return applyMigrations(decryptedConfig);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error retrieving user ${uuid}: ${error instanceof Error ? error.message : String(error)}`
|
||||
|
||||
@@ -168,52 +168,54 @@ class StreamFetcher {
|
||||
|
||||
// If groups are configured, handle group-based fetching
|
||||
if (
|
||||
this.userData.groups &&
|
||||
this.userData.groups.length > 0 &&
|
||||
this.userData.disableGroups !== true
|
||||
this.userData.groups?.groupings &&
|
||||
this.userData.groups.groupings.length > 0 &&
|
||||
this.userData.groups.enabled !== false
|
||||
) {
|
||||
// add addons that are not assigned to any group to the first group
|
||||
const unassignedAddons = addons.filter(
|
||||
(addon) =>
|
||||
!this.userData.groups!.some((group) =>
|
||||
!this.userData.groups?.groupings?.some((group) =>
|
||||
group.addons.includes(addon.preset.id)
|
||||
)
|
||||
);
|
||||
if (unassignedAddons.length > 0) {
|
||||
this.userData.groups[0].addons.push(
|
||||
if (unassignedAddons.length > 0 && this.userData.groups.groupings[0]) {
|
||||
this.userData.groups.groupings[0].addons.push(
|
||||
...unassignedAddons.map((addon) => addon.preset.id)
|
||||
);
|
||||
}
|
||||
const groupPromises = this.userData.groups.map((group) => {
|
||||
const groupAddons = addons.filter(
|
||||
(addon) => addon.preset.id && group.addons.includes(addon.preset.id)
|
||||
);
|
||||
logger.info(
|
||||
`Queueing fetch for group with ${groupAddons.length} addons.`
|
||||
);
|
||||
return fetchFromGroup(groupAddons);
|
||||
});
|
||||
|
||||
const behaviour = this.userData.groups.behaviour || 'parallel';
|
||||
let totalTimeTaken = 0;
|
||||
let previousGroupStreams: ParsedStream[] = [];
|
||||
let previousGroupTimeTaken = 0;
|
||||
|
||||
for (let i = 0; i < groupPromises.length; i++) {
|
||||
const groupResult = await groupPromises[i];
|
||||
const group = this.userData.groups[i];
|
||||
if (behaviour === 'parallel') {
|
||||
// Fetch all groups in parallel but still evaluate conditions
|
||||
const groupPromises = this.userData.groups.groupings.map((group) => {
|
||||
const groupAddons = addons.filter(
|
||||
(addon) => addon.preset.id && group.addons.includes(addon.preset.id)
|
||||
);
|
||||
logger.info(
|
||||
`Queueing parallel fetch for group with ${groupAddons.length} addons.`
|
||||
);
|
||||
return fetchFromGroup(groupAddons);
|
||||
});
|
||||
|
||||
if (i === 0) {
|
||||
allStreams.push(...groupResult.streams);
|
||||
allErrors.push(...groupResult.errors);
|
||||
allStatisticStreams.push(...groupResult.statistics);
|
||||
totalTimeTaken = groupResult.totalTime;
|
||||
previousGroupStreams = groupResult.streams;
|
||||
previousGroupTimeTaken = groupResult.totalTime;
|
||||
for (let i = 0; i < this.userData.groups.groupings.length; i++) {
|
||||
const groupResult = await groupPromises[i];
|
||||
const group = this.userData.groups.groupings[i];
|
||||
|
||||
// After the first group, check the condition for the second group
|
||||
if (groupPromises.length > 1) {
|
||||
const nextGroup = this.userData.groups[1];
|
||||
if (!nextGroup.condition || !nextGroup.addons.length) continue;
|
||||
if (i === 0) {
|
||||
allStreams.push(...groupResult.streams);
|
||||
allErrors.push(...groupResult.errors);
|
||||
allStatisticStreams.push(...groupResult.statistics);
|
||||
totalTimeTaken = groupResult.totalTime;
|
||||
previousGroupStreams = groupResult.streams;
|
||||
previousGroupTimeTaken = groupResult.totalTime;
|
||||
} else {
|
||||
// For groups other than the first, check their condition
|
||||
if (!group.condition || !group.addons.length) continue;
|
||||
|
||||
const evaluator = new GroupConditionEvaluator(
|
||||
previousGroupStreams,
|
||||
@@ -222,46 +224,64 @@ class StreamFetcher {
|
||||
totalTimeTaken,
|
||||
queryType
|
||||
);
|
||||
const shouldFetchNext = await evaluator.evaluate(
|
||||
nextGroup.condition
|
||||
);
|
||||
const shouldInclude = await evaluator.evaluate(group.condition);
|
||||
|
||||
if (!shouldFetchNext) {
|
||||
if (shouldInclude) {
|
||||
logger.info(
|
||||
`Condition not met for group 2 based on group 1 results. Halting further processing.`
|
||||
`Condition met for parallel group ${i + 1}, including streams.`
|
||||
);
|
||||
allStreams.push(...groupResult.streams);
|
||||
allErrors.push(...groupResult.errors);
|
||||
allStatisticStreams.push(...groupResult.statistics);
|
||||
totalTimeTaken = Math.max(totalTimeTaken, groupResult.totalTime);
|
||||
previousGroupStreams = groupResult.streams;
|
||||
previousGroupTimeTaken = groupResult.totalTime;
|
||||
} else {
|
||||
logger.info(
|
||||
`Condition not met for parallel group ${i + 1}, skipping streams.`
|
||||
);
|
||||
break; // Exit the loop, returning only group 1 streams
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For groups other than the first, check their condition before processing
|
||||
if (!group.condition || !group.addons.length) continue;
|
||||
}
|
||||
} else {
|
||||
// Sequential behavior - fetch and evaluate one group at a time
|
||||
for (let i = 0; i < this.userData.groups.groupings.length; i++) {
|
||||
const group = this.userData.groups.groupings[i];
|
||||
|
||||
const evaluator = new GroupConditionEvaluator(
|
||||
previousGroupStreams,
|
||||
allStreams,
|
||||
previousGroupTimeTaken,
|
||||
totalTimeTaken,
|
||||
queryType
|
||||
);
|
||||
const shouldFetch = await evaluator.evaluate(group.condition);
|
||||
// For groups after the first, check condition before fetching
|
||||
if (i > 0 && group.condition) {
|
||||
const evaluator = new GroupConditionEvaluator(
|
||||
previousGroupStreams,
|
||||
allStreams,
|
||||
previousGroupTimeTaken,
|
||||
totalTimeTaken,
|
||||
queryType
|
||||
);
|
||||
const shouldFetch = await evaluator.evaluate(group.condition);
|
||||
|
||||
if (shouldFetch) {
|
||||
logger.info(
|
||||
`Condition met for group ${i + 1}, processing streams.`
|
||||
);
|
||||
allStreams.push(...groupResult.streams);
|
||||
allErrors.push(...groupResult.errors);
|
||||
allStatisticStreams.push(...groupResult.statistics);
|
||||
totalTimeTaken += groupResult.totalTime;
|
||||
previousGroupStreams = groupResult.streams;
|
||||
previousGroupTimeTaken = groupResult.totalTime;
|
||||
} else {
|
||||
logger.info(
|
||||
`Condition not met for group ${i + 1}, skipping remaining groups.`
|
||||
);
|
||||
break; // Stop processing any more groups
|
||||
if (!shouldFetch) {
|
||||
logger.info(
|
||||
`Condition not met for sequential group ${i + 1}, stopping.`
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const groupAddons = addons.filter(
|
||||
(addon) => addon.preset.id && group.addons.includes(addon.preset.id)
|
||||
);
|
||||
logger.info(
|
||||
`Fetching from sequential group ${i + 1} with ${groupAddons.length} addons.`
|
||||
);
|
||||
|
||||
const groupResult = await fetchFromGroup(groupAddons);
|
||||
|
||||
allStreams.push(...groupResult.streams);
|
||||
allErrors.push(...groupResult.errors);
|
||||
allStatisticStreams.push(...groupResult.statistics);
|
||||
totalTimeTaken += groupResult.totalTime;
|
||||
previousGroupStreams = groupResult.streams;
|
||||
previousGroupTimeTaken = groupResult.totalTime;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -352,8 +352,8 @@ export async function validateConfig(
|
||||
}
|
||||
}
|
||||
|
||||
if (config.groups) {
|
||||
for (const group of config.groups) {
|
||||
if (config.groups?.groupings) {
|
||||
for (const group of config.groups.groupings) {
|
||||
await validateGroup(group);
|
||||
}
|
||||
}
|
||||
@@ -456,8 +456,8 @@ function removeInvalidPresetReferences(config: UserData) {
|
||||
existingPresetIds?.includes(addon)
|
||||
);
|
||||
}
|
||||
if (config.groups) {
|
||||
config.groups = config.groups.map((group) => ({
|
||||
if (config.groups?.groupings) {
|
||||
config.groups.groupings = config.groups.groupings.map((group) => ({
|
||||
...group,
|
||||
addons: group.addons?.filter((addon) =>
|
||||
existingPresetIds?.includes(addon)
|
||||
@@ -467,7 +467,7 @@ function removeInvalidPresetReferences(config: UserData) {
|
||||
return config;
|
||||
}
|
||||
|
||||
export function applyMigrations(config: UserData): UserData {
|
||||
export function applyMigrations(config: any): UserData {
|
||||
if (
|
||||
config.deduplicator &&
|
||||
typeof config.deduplicator.multiGroupBehaviour === 'string'
|
||||
@@ -495,6 +495,14 @@ export function applyMigrations(config: UserData): UserData {
|
||||
};
|
||||
delete config.titleMatching.matchYear;
|
||||
}
|
||||
|
||||
if (Array.isArray(config.groups)) {
|
||||
config.groups = {
|
||||
enabled: config.disableGroups ? false : true,
|
||||
groupings: config.groups,
|
||||
behaviour: 'parallel',
|
||||
};
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ import { IoExtensionPuzzle } from 'react-icons/io5';
|
||||
import { NumberInput } from '../ui/number-input';
|
||||
import { useDisclosure } from '@/hooks/disclosure';
|
||||
import { useMode } from '@/context/mode';
|
||||
import { Select } from '../ui/select';
|
||||
|
||||
interface CatalogModification {
|
||||
id: string;
|
||||
@@ -1191,7 +1192,7 @@ function AddonGroupCard() {
|
||||
// Helper function to get presets that are not in any group except the current one
|
||||
const getAvailablePresets = (currentGroupIndex: number) => {
|
||||
const presetsInOtherGroups = new Set(
|
||||
userData.groups?.flatMap((group, idx) =>
|
||||
userData.groups?.groupings?.flatMap((group, idx) =>
|
||||
idx !== currentGroupIndex ? group.addons : []
|
||||
) || []
|
||||
);
|
||||
@@ -1213,7 +1214,7 @@ function AddonGroupCard() {
|
||||
) => {
|
||||
setUserData((prev) => {
|
||||
// Initialize groups array if it doesn't exist
|
||||
const currentGroups = prev.groups || [];
|
||||
const currentGroups = prev.groups?.groupings || [];
|
||||
|
||||
// Create a new array with all existing groups
|
||||
const newGroups = [...currentGroups];
|
||||
@@ -1231,7 +1232,10 @@ function AddonGroupCard() {
|
||||
|
||||
return {
|
||||
...prev,
|
||||
groups: newGroups,
|
||||
groups: {
|
||||
...prev.groups,
|
||||
groupings: newGroups,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -1259,21 +1263,46 @@ function AddonGroupCard() {
|
||||
for a detailed guide to using groups.
|
||||
</div>
|
||||
<Switch
|
||||
label="Disable Groups"
|
||||
value={userData.disableGroups ?? false}
|
||||
label="Enable"
|
||||
value={userData.groups?.enabled ?? false}
|
||||
onValueChange={(value) => {
|
||||
setUserData((prev) => ({ ...prev, disableGroups: value }));
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
groups: { ...prev.groups, enabled: value },
|
||||
}));
|
||||
}}
|
||||
side="right"
|
||||
help="If enabled, groups will be ignored and all addons will be used."
|
||||
/>
|
||||
{(userData.groups || []).map((group, index) => (
|
||||
<Select
|
||||
label="Behaviour"
|
||||
value={userData.groups?.behaviour ?? 'parallel'}
|
||||
onValueChange={(value) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
groups: {
|
||||
...prev.groups,
|
||||
behaviour: value as 'sequential' | 'parallel',
|
||||
},
|
||||
}));
|
||||
}}
|
||||
options={[
|
||||
{ label: 'Parallel', value: 'parallel' },
|
||||
{ label: 'Sequential', value: 'sequential' },
|
||||
]}
|
||||
disabled={userData.groups?.enabled === false}
|
||||
help={
|
||||
userData.groups?.behaviour === 'sequential'
|
||||
? 'Streams are fetched from the first group only to begin with. If the condition for the next group is met, streams are fetched from the next group, and so on.'
|
||||
: 'Streams are fetched from all groups at the same time. When a condition is not met, results from its group onwards are simply not shown.'
|
||||
}
|
||||
/>
|
||||
{(userData.groups?.groupings || []).map((group, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<div className="flex-1 flex gap-2">
|
||||
<div className="flex-1">
|
||||
<Combobox
|
||||
multiple
|
||||
disabled={userData.disableGroups}
|
||||
disabled={userData.groups?.enabled === false}
|
||||
value={group.addons}
|
||||
options={getAvailablePresets(index)}
|
||||
emptyMessage="You haven't installed any addons yet or they are already in a group"
|
||||
@@ -1287,7 +1316,7 @@ function AddonGroupCard() {
|
||||
<div className="flex-1">
|
||||
<TextInput
|
||||
value={index === 0 ? 'true' : group.condition}
|
||||
disabled={index === 0 || userData.disableGroups}
|
||||
disabled={index === 0 || userData.groups?.enabled === false}
|
||||
label="Condition"
|
||||
placeholder="Enter condition"
|
||||
onValueChange={(value) => {
|
||||
@@ -1299,16 +1328,16 @@ function AddonGroupCard() {
|
||||
<IconButton
|
||||
size="sm"
|
||||
rounded
|
||||
disabled={userData.disableGroups}
|
||||
disabled={userData.groups?.enabled === false}
|
||||
icon={<FaRegTrashAlt />}
|
||||
intent="alert-subtle"
|
||||
onClick={() => {
|
||||
setUserData((prev) => {
|
||||
const newGroups = [...(prev.groups || [])];
|
||||
const newGroups = [...(prev.groups?.groupings || [])];
|
||||
newGroups.splice(index, 1);
|
||||
return {
|
||||
...prev,
|
||||
groups: newGroups,
|
||||
groups: { ...prev.groups, groupings: newGroups },
|
||||
};
|
||||
});
|
||||
}}
|
||||
@@ -1321,13 +1350,16 @@ function AddonGroupCard() {
|
||||
size="sm"
|
||||
intent="primary-subtle"
|
||||
icon={<FaPlus />}
|
||||
disabled={userData.disableGroups}
|
||||
disabled={userData.groups?.enabled === false}
|
||||
onClick={() => {
|
||||
setUserData((prev) => {
|
||||
const currentGroups = prev.groups || [];
|
||||
const currentGroups = prev.groups?.groupings || [];
|
||||
return {
|
||||
...prev,
|
||||
groups: [...currentGroups, { addons: [], condition: '' }],
|
||||
groups: {
|
||||
...prev.groups,
|
||||
groupings: [...currentGroups, { addons: [], condition: '' }],
|
||||
},
|
||||
};
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
ConfirmationDialog,
|
||||
useConfirmationDialog,
|
||||
} from '../shared/confirmation-dialog';
|
||||
import { UserData } from '@aiostreams/core';
|
||||
import { applyMigrations, UserData } from '@aiostreams/core';
|
||||
|
||||
export function SaveInstallMenu() {
|
||||
return (
|
||||
@@ -164,7 +164,9 @@ function Content() {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
try {
|
||||
const json = JSON.parse(event.target?.result as string);
|
||||
const json = applyMigrations(
|
||||
JSON.parse(event.target?.result as string)
|
||||
);
|
||||
// const validate = UserDataSchema.safeParse(json);
|
||||
// if (!validate.success) {
|
||||
// toast.error('Failed to import configuration: Invalid JSON file');
|
||||
|
||||
Reference in New Issue
Block a user