mirror of
https://github.com/Viren070/AIOStreams.git
synced 2025-12-01 23:14:04 +01:00
some fixes
This commit is contained in:
+49
-37
@@ -808,7 +808,7 @@ export class AIOStreams {
|
||||
);
|
||||
const shouldFetch = await parser.parse(group.condition);
|
||||
if (shouldFetch) {
|
||||
logger.info(`Condition met for group ${i}, fetching streams`);
|
||||
logger.info(`Condition met for group ${i + 1}, fetching streams`);
|
||||
|
||||
const groupAddons = supportedAddons.filter(
|
||||
(addon) => addon.id && group.addons.includes(addon.id)
|
||||
@@ -868,12 +868,15 @@ export class AIOStreams {
|
||||
requiredUncachedFromServices: { total: 0, details: {} },
|
||||
excludedUncachedMode: { total: 0, details: {} },
|
||||
requiredUncachedMode: { total: 0, details: {} },
|
||||
excludedEncode: { total: 0, details: {} },
|
||||
requiredEncode: { total: 0, details: {} },
|
||||
excludedCachedFromAddons: { total: 0, details: {} },
|
||||
requiredCachedFromAddons: { total: 0, details: {} },
|
||||
excludedCachedFromServices: { total: 0, details: {} },
|
||||
requiredCachedFromServices: { total: 0, details: {} },
|
||||
excludedCachedMode: { total: 0, details: {} },
|
||||
requiredCachedMode: { total: 0, details: {} },
|
||||
requiredLanguage: { total: 0, details: {} },
|
||||
};
|
||||
|
||||
const start = Date.now();
|
||||
@@ -945,40 +948,25 @@ export class AIOStreams {
|
||||
serviceIds: string[] | undefined,
|
||||
cached: boolean
|
||||
) => {
|
||||
if (this.userData.excludeCached && cached && stream.service?.cached) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.userData.excludeUncached &&
|
||||
cached === false &&
|
||||
stream.service?.cached === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const addonCriteriaMet =
|
||||
!addonIds ||
|
||||
addonIds.length === 0 ||
|
||||
(addonIds.some((addonId) => stream.addon.id === addonId) &&
|
||||
stream.service?.cached === cached);
|
||||
const serviceCriteriaMet =
|
||||
!serviceIds ||
|
||||
serviceIds.length === 0 ||
|
||||
(serviceIds.some((serviceId) => stream.service?.id === serviceId) &&
|
||||
stream.service?.cached === cached);
|
||||
|
||||
if (mode === 'and') {
|
||||
return (
|
||||
(!addonIds ||
|
||||
addonIds.length === 0 ||
|
||||
addonIds.some((addonId) => stream.addon.id === addonId)) &&
|
||||
(!serviceIds ||
|
||||
serviceIds.length === 0 ||
|
||||
serviceIds.some((serviceId) => stream.service?.id === serviceId)) &&
|
||||
stream.service?.cached === cached
|
||||
);
|
||||
return addonCriteriaMet && serviceCriteriaMet;
|
||||
} else {
|
||||
return (
|
||||
(!addonIds ||
|
||||
addonIds.length === 0 ||
|
||||
addonIds.some((addonId) => stream.addon.id === addonId)) &&
|
||||
(!serviceIds ||
|
||||
serviceIds.length === 0 ||
|
||||
serviceIds.some((serviceId) => stream.service?.id === serviceId)) &&
|
||||
stream.service?.cached === cached
|
||||
);
|
||||
return addonCriteriaMet || serviceCriteriaMet;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredStreams = streams.filter(async (stream) => {
|
||||
const shouldKeepStream = async (stream: ParsedStream): Promise<boolean> => {
|
||||
const file = stream.parsedFile;
|
||||
|
||||
// carry out include checks first
|
||||
@@ -1278,7 +1266,7 @@ export class AIOStreams {
|
||||
this.userData.excludeCachedFromAddons,
|
||||
this.userData.excludeCachedFromServices,
|
||||
true
|
||||
)
|
||||
) === false
|
||||
) {
|
||||
skipReasons.excludedCached.total++;
|
||||
return false;
|
||||
@@ -1291,7 +1279,7 @@ export class AIOStreams {
|
||||
this.userData.excludeUncachedFromAddons,
|
||||
this.userData.excludeUncachedFromServices,
|
||||
false
|
||||
)
|
||||
) === false
|
||||
) {
|
||||
skipReasons.excludedUncached.total++;
|
||||
return false;
|
||||
@@ -1327,22 +1315,45 @@ export class AIOStreams {
|
||||
// TODO: size filters
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const filterResults = await Promise.all(streams.map(shouldKeepStream));
|
||||
const filteredStreams = streams.filter((_, index) => filterResults[index]);
|
||||
|
||||
// Log filter summary
|
||||
const totalFiltered = streams.length - filteredStreams.length;
|
||||
if (totalFiltered > 0) {
|
||||
const summary = [`Filtered out ${totalFiltered} streams:`];
|
||||
const summary = [
|
||||
'\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
|
||||
` 🔍 Filter Summary`,
|
||||
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━',
|
||||
` 📊 Total Streams : ${streams.length}`,
|
||||
` ✔️ Kept : ${filteredStreams.length}`,
|
||||
` ❌ Filtered : ${totalFiltered}`,
|
||||
];
|
||||
|
||||
// Add filter details if any streams were filtered
|
||||
const filterDetails: string[] = [];
|
||||
for (const [reason, stats] of Object.entries(skipReasons)) {
|
||||
if (stats.total > 0) {
|
||||
summary.push(` - ${stats.total} due to ${reason}:`);
|
||||
// Convert camelCase to Title Case with spaces
|
||||
const formattedReason = reason
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, (str) => str.toUpperCase());
|
||||
|
||||
filterDetails.push(`\n 📌 ${formattedReason} (${stats.total})`);
|
||||
for (const [detail, count] of Object.entries(stats.details)) {
|
||||
summary.push(` • ${count} ${detail}`);
|
||||
filterDetails.push(` • ${count}× ${detail}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filterDetails.length > 0) {
|
||||
summary.push('\n 🔎 Filter Details:');
|
||||
summary.push(...filterDetails);
|
||||
}
|
||||
|
||||
summary.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
logger.info(summary.join('\n'));
|
||||
}
|
||||
|
||||
@@ -1684,7 +1695,7 @@ export class AIOStreams {
|
||||
(stream) => stream.service?.cached || stream.service === undefined // streams without a service can be considered as 'cached'
|
||||
);
|
||||
const uncachedStreams = streams.filter(
|
||||
(stream) => !stream.service?.cached
|
||||
(stream) => stream.service?.cached === false
|
||||
);
|
||||
|
||||
// sort the 2 lists separately, and put them after the other, depending on the direction of cached
|
||||
@@ -1714,6 +1725,7 @@ export class AIOStreams {
|
||||
sortedStreams = [...uncachedSorted, ...cachedSorted];
|
||||
}
|
||||
} else {
|
||||
logger.debug(`using sort criteria: ${JSON.stringify(sortCriteria)}`);
|
||||
sortedStreams = streams.slice().sort((a, b) => {
|
||||
const aKey = this.dynamicSortKey(a, sortCriteria, type);
|
||||
const bKey = this.dynamicSortKey(b, sortCriteria, type);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Stream, ParsedStream, Addon } from '../db';
|
||||
import { constants } from '../utils';
|
||||
import { constants, createLogger } from '../utils';
|
||||
import FileParser from './file';
|
||||
|
||||
const logger = createLogger('parser');
|
||||
class StreamParser {
|
||||
get errorRegexes(): { pattern: RegExp; message: string }[] | undefined {
|
||||
return [
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Addon, Option, UserData, Resource } from '../db';
|
||||
import { Addon, Option, UserData, Resource, Stream } from '../db';
|
||||
import { baseOptions, Preset } from './preset';
|
||||
import { Env } from '../utils';
|
||||
import { constants, ServiceId } from '../utils';
|
||||
import { StreamParser } from '../parser';
|
||||
|
||||
export class StremthruStorePreset extends Preset {
|
||||
static override get METADATA() {
|
||||
@@ -103,6 +104,7 @@ export class StremthruStorePreset extends Preset {
|
||||
: options.name || this.METADATA.NAME,
|
||||
manifestUrl: this.generateManifestUrl(userData, options, serviceId),
|
||||
enabled: true,
|
||||
library: true,
|
||||
resources: options.resources || this.METADATA.SUPPORTED_RESOURCES,
|
||||
timeout: options.timeout || this.METADATA.TIMEOUT,
|
||||
fromPresetId: this.METADATA.ID,
|
||||
|
||||
@@ -21,11 +21,7 @@ class TorboxStreamParser extends StreamParser {
|
||||
return (stream as any).hash;
|
||||
}
|
||||
override getInLibrary(stream: Stream): boolean {
|
||||
return (
|
||||
((stream as any).is_your_media ||
|
||||
stream.description?.includes('Your Media')) ??
|
||||
false
|
||||
);
|
||||
return (stream as any).is_your_media || stream.name?.includes('Your Media');
|
||||
}
|
||||
protected override getService(
|
||||
stream: Stream
|
||||
|
||||
@@ -258,7 +258,7 @@ export async function validateConfig(
|
||||
|
||||
if (config.groups) {
|
||||
for (const group of config.groups) {
|
||||
validateGroup(group);
|
||||
await validateGroup(group);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +383,7 @@ function validatePreset(preset: PresetObject) {
|
||||
}
|
||||
}
|
||||
|
||||
function validateGroup(group: Group) {
|
||||
async function validateGroup(group: Group) {
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
@@ -394,13 +394,18 @@ function validateGroup(group: Group) {
|
||||
}
|
||||
|
||||
// we must be able to parse the condition
|
||||
let result;
|
||||
try {
|
||||
const result = ConditionParser.testParse(group.condition);
|
||||
if (typeof result !== 'boolean') {
|
||||
throw new Error('Group condition must evaluate to a boolean');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Group condition is invalid: ${error}`);
|
||||
result = await ConditionParser.testParse(group.condition);
|
||||
} catch (error: any) {
|
||||
throw new Error(
|
||||
`Your group condition - '${group.condition}' - is invalid: ${error.message}`
|
||||
);
|
||||
}
|
||||
if (typeof result !== 'boolean') {
|
||||
throw new Error(
|
||||
`Your group condition - '${group.condition}' - is invalid. Expected evaluation to a boolean, instead got '${typeof result}'`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,12 @@ import { Option, Resource } from '@aiostreams/core';
|
||||
import { toast } from 'sonner';
|
||||
import { Tooltip } from '../ui/tooltip';
|
||||
import { StaticTabs } from '../ui/tabs';
|
||||
import { LuDownload, LuGlobe } from 'react-icons/lu';
|
||||
import {
|
||||
LuDownload,
|
||||
LuGlobe,
|
||||
LuChevronsUp,
|
||||
LuChevronsDown,
|
||||
} from 'react-icons/lu';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { PageControls } from '../shared/page-controls';
|
||||
import Image from 'next/image';
|
||||
@@ -44,6 +49,7 @@ import {
|
||||
ConfirmationDialog,
|
||||
useConfirmationDialog,
|
||||
} from '../shared/confirmation-dialog';
|
||||
import { MdRefresh } from 'react-icons/md';
|
||||
|
||||
interface CatalogModification {
|
||||
id: string;
|
||||
@@ -514,14 +520,14 @@ function SortableAddonItem({
|
||||
};
|
||||
return (
|
||||
<li ref={setNodeRef} style={style}>
|
||||
<div className="px-2.5 py-2 bg-[var(--background)] rounded-[--radius-md] border flex gap-3 relative">
|
||||
<div className="px-2.5 py-2 bg-[var(--background)] rounded-[--radius-md] border flex gap-2 sm:gap-3 relative">
|
||||
<div
|
||||
className="rounded-full w-6 h-auto bg-[--muted] md:bg-[--subtle] md:hover:bg-[--subtle-highlight] cursor-move"
|
||||
className="rounded-full w-6 h-auto bg-[--muted] md:bg-[--subtle] md:hover:bg-[--subtle-highlight] cursor-move flex-shrink-0"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
/>
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<div className="relative flex-shrink-0 h-8 w-8">
|
||||
<div className="flex items-center gap-2 sm:gap-3 flex-1 min-w-0">
|
||||
<div className="relative flex-shrink-0 h-8 w-8 hidden sm:block">
|
||||
{preset.ID === 'custom' ? (
|
||||
<PlusIcon className="w-full h-full object-contain" />
|
||||
) : (
|
||||
@@ -534,19 +540,26 @@ function SortableAddonItem({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-base line-clamp-1">{addon.options.name}</p>
|
||||
<p className="text-base line-clamp-1 truncate block">
|
||||
{addon.options.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch value={addon.enabled} onValueChange={onToggleEnabled} />
|
||||
<div className="flex items-center gap-1 sm:gap-2">
|
||||
<Switch
|
||||
value={addon.enabled}
|
||||
onValueChange={onToggleEnabled}
|
||||
size="sm"
|
||||
className="sm:scale-100 scale-90"
|
||||
/>
|
||||
<IconButton
|
||||
className="rounded-full"
|
||||
className="rounded-full sm:scale-100 scale-85"
|
||||
icon={<BiEdit />}
|
||||
intent="primary-subtle"
|
||||
onClick={onEdit}
|
||||
/>
|
||||
<IconButton
|
||||
className="rounded-full"
|
||||
className="rounded-full sm:scale-100 scale-85"
|
||||
icon={<BiTrash />}
|
||||
intent="alert-subtle"
|
||||
onClick={onRemove}
|
||||
@@ -1020,7 +1033,7 @@ function CatalogSettingsCard() {
|
||||
type: catalog.type,
|
||||
enabled: true,
|
||||
shuffle: false,
|
||||
rpdb: false,
|
||||
rpdb: userData.rpdbApiKey ? true : false,
|
||||
}));
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
@@ -1152,9 +1165,11 @@ function CatalogSettingsCard() {
|
||||
<h3 className="text-lg font-semibold">Catalogs</h3>
|
||||
<p className="text-[--muted] text-sm">Manage your catalogs</p>
|
||||
</div>
|
||||
<Button
|
||||
<IconButton
|
||||
size="sm"
|
||||
intent="primary-outline"
|
||||
intent="white-subtle"
|
||||
icon={<MdRefresh />}
|
||||
rounded
|
||||
onClick={() => {
|
||||
if (userData.catalogModifications?.length) {
|
||||
confirmRefreshCatalogs.open();
|
||||
@@ -1163,11 +1178,7 @@ function CatalogSettingsCard() {
|
||||
}
|
||||
}}
|
||||
loading={loading}
|
||||
>
|
||||
{userData.catalogModifications?.length
|
||||
? 'Refresh Catalogs'
|
||||
: 'Fetch Catalogs'}
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!userData.catalogModifications?.length && (
|
||||
@@ -1230,9 +1241,12 @@ function CatalogSettingsCard() {
|
||||
<Modal
|
||||
open={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
title={`Edit Catalog: ${editingCatalog?.name || editingCatalog?.id} - ${capitalise(
|
||||
editingCatalog?.type
|
||||
)}`}
|
||||
title={
|
||||
<div className="max-w-[calc(100vw-4rem)] sm:max-w-[400px] truncate">
|
||||
Edit Catalog: {editingCatalog?.name || editingCatalog?.id} -{' '}
|
||||
{capitalise(editingCatalog?.type)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<form
|
||||
className="space-y-4"
|
||||
@@ -1321,37 +1335,81 @@ function SortableCatalogItem({
|
||||
id: `${catalog.id}-${catalog.type}`,
|
||||
});
|
||||
|
||||
const { setUserData } = useUserData();
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
const moveToTop = () => {
|
||||
setUserData((prev) => {
|
||||
if (!prev.catalogModifications) return prev;
|
||||
const index = prev.catalogModifications.findIndex(
|
||||
(c) => c.id === catalog.id && c.type === catalog.type
|
||||
);
|
||||
if (index <= 0) return prev;
|
||||
const newMods = [...prev.catalogModifications];
|
||||
const [item] = newMods.splice(index, 1);
|
||||
newMods.unshift(item);
|
||||
return { ...prev, catalogModifications: newMods };
|
||||
});
|
||||
};
|
||||
|
||||
const moveToBottom = () => {
|
||||
setUserData((prev) => {
|
||||
if (!prev.catalogModifications) return prev;
|
||||
const index = prev.catalogModifications.findIndex(
|
||||
(c) => c.id === catalog.id && c.type === catalog.type
|
||||
);
|
||||
if (index === prev.catalogModifications.length - 1) return prev;
|
||||
const newMods = [...prev.catalogModifications];
|
||||
const [item] = newMods.splice(index, 1);
|
||||
newMods.push(item);
|
||||
return { ...prev, catalogModifications: newMods };
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style}>
|
||||
<div className="px-2.5 py-2 bg-[var(--background)] rounded-[--radius-md] border flex gap-3 relative">
|
||||
<div className="px-2.5 py-2 bg-[var(--background)] rounded-[--radius-md] border flex gap-2 sm:gap-3 relative">
|
||||
<div
|
||||
className="rounded-full w-6 h-auto bg-[--muted] md:bg-[--subtle] md:hover:bg-[--subtle-highlight] cursor-move"
|
||||
className="rounded-full w-6 h-auto bg-[--muted] md:bg-[--subtle] md:hover:bg-[--subtle-highlight] cursor-move flex-shrink-0"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
/>
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<p className="text-base line-clamp-1">
|
||||
{catalog.name || catalog.id} - {capitalise(catalog.type)}
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<p className="text-base line-clamp-1 truncate block">
|
||||
{catalog.name ?? catalog.id} - {capitalise(catalog.type)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 sm:gap-2">
|
||||
<Switch
|
||||
value={catalog.enabled ?? true}
|
||||
onValueChange={onToggleEnabled}
|
||||
size="sm"
|
||||
className="sm:scale-100 scale-90"
|
||||
/>
|
||||
<IconButton
|
||||
className="rounded-full"
|
||||
className="rounded-full sm:scale-100 scale-85"
|
||||
icon={<BiEdit />}
|
||||
intent="primary-subtle"
|
||||
onClick={onEdit}
|
||||
/>
|
||||
<IconButton
|
||||
className="rounded-full sm:scale-100 scale-85"
|
||||
icon={<LuChevronsUp />}
|
||||
intent="primary-subtle"
|
||||
onClick={moveToTop}
|
||||
/>
|
||||
<IconButton
|
||||
className="rounded-full sm:scale-100 scale-85"
|
||||
icon={<LuChevronsDown />}
|
||||
intent="primary-subtle"
|
||||
onClick={moveToBottom}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -740,6 +740,16 @@ function Content() {
|
||||
requiredKeywords: values,
|
||||
}));
|
||||
}}
|
||||
onValueChange={(value, index) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
requiredKeywords: [
|
||||
...(prev.requiredKeywords || []).slice(0, index),
|
||||
value,
|
||||
...(prev.requiredKeywords || []).slice(index + 1),
|
||||
],
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<TextInputs
|
||||
label="Excluded Keywords"
|
||||
@@ -752,6 +762,16 @@ function Content() {
|
||||
excludedKeywords: values,
|
||||
}));
|
||||
}}
|
||||
onValueChange={(value, index) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
excludedKeywords: [
|
||||
...(prev.excludedKeywords || []).slice(0, index),
|
||||
value,
|
||||
...(prev.excludedKeywords || []).slice(index + 1),
|
||||
],
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<TextInputs
|
||||
label="Included Keywords"
|
||||
@@ -764,6 +784,16 @@ function Content() {
|
||||
includedKeywords: values,
|
||||
}));
|
||||
}}
|
||||
onValueChange={(value, index) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
includedKeywords: [
|
||||
...(prev.includedKeywords || []).slice(0, index),
|
||||
value,
|
||||
...(prev.includedKeywords || []).slice(index + 1),
|
||||
],
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<TextInputs
|
||||
label="Preferred Keywords"
|
||||
@@ -776,6 +806,16 @@ function Content() {
|
||||
preferredKeywords: values,
|
||||
}));
|
||||
}}
|
||||
onValueChange={(value, index) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
preferredKeywords: [
|
||||
...(prev.preferredKeywords || []).slice(0, index),
|
||||
value,
|
||||
...(prev.preferredKeywords || []).slice(index + 1),
|
||||
],
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
@@ -818,6 +858,16 @@ function Content() {
|
||||
requiredRegexPatterns: values,
|
||||
}));
|
||||
}}
|
||||
onValueChange={(value, index) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
requiredRegexPatterns: [
|
||||
...(prev.requiredRegexPatterns || []).slice(0, index),
|
||||
value,
|
||||
...(prev.requiredRegexPatterns || []).slice(index + 1),
|
||||
],
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<TextInputs
|
||||
label="Excluded Regex"
|
||||
@@ -830,6 +880,16 @@ function Content() {
|
||||
excludedRegexPatterns: values,
|
||||
}));
|
||||
}}
|
||||
onValueChange={(value, index) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
excludedRegexPatterns: [
|
||||
...(prev.excludedRegexPatterns || []).slice(0, index),
|
||||
value,
|
||||
...(prev.excludedRegexPatterns || []).slice(index + 1),
|
||||
],
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<TextInputs
|
||||
label="Included Regex"
|
||||
@@ -842,16 +902,26 @@ function Content() {
|
||||
includedRegexPatterns: values,
|
||||
}));
|
||||
}}
|
||||
onValueChange={(value, index) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
includedRegexPatterns: [
|
||||
...(prev.includedRegexPatterns || []).slice(0, index),
|
||||
value,
|
||||
...(prev.includedRegexPatterns || []).slice(index + 1),
|
||||
],
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<TwoTextInputs
|
||||
title="Preferred Regex"
|
||||
description="Streams that contain any of these regular expressions will be preferred. Give each regex a name which will appear in your stream if the format you use supports it."
|
||||
title="Preferred Regex Patterns"
|
||||
description="Define regex patterns with names for easy reference"
|
||||
keyName="Name"
|
||||
keyId="name"
|
||||
keyPlaceholder="Enter the name of this regex here"
|
||||
valueName="Regex"
|
||||
keyPlaceholder="Enter pattern name"
|
||||
valueId="pattern"
|
||||
valuePlaceholder="Enter the actual regex pattern here"
|
||||
valueName="Pattern"
|
||||
valuePlaceholder="Enter regex pattern"
|
||||
values={(userData.preferredRegexPatterns || []).map(
|
||||
(pattern) => ({
|
||||
name: pattern.name,
|
||||
@@ -861,12 +931,38 @@ function Content() {
|
||||
onValuesChange={(values) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
preferredRegexPatterns: values.map((value) => ({
|
||||
name: value.name,
|
||||
pattern: value.value,
|
||||
preferredRegexPatterns: values.map((v) => ({
|
||||
name: v.name,
|
||||
pattern: v.value,
|
||||
})),
|
||||
}));
|
||||
}}
|
||||
onValueChange={(value, index) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
preferredRegexPatterns: [
|
||||
...(prev.preferredRegexPatterns || []).slice(0, index),
|
||||
{
|
||||
...(prev.preferredRegexPatterns || [])[index],
|
||||
pattern: value,
|
||||
},
|
||||
...(prev.preferredRegexPatterns || []).slice(index + 1),
|
||||
],
|
||||
}));
|
||||
}}
|
||||
onKeyChange={(key, index) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
preferredRegexPatterns: [
|
||||
...(prev.preferredRegexPatterns || []).slice(0, index),
|
||||
{
|
||||
...(prev.preferredRegexPatterns || [])[index],
|
||||
name: key,
|
||||
},
|
||||
...(prev.preferredRegexPatterns || []).slice(index + 1),
|
||||
],
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
@@ -885,12 +981,15 @@ function Content() {
|
||||
help="Global limit for all results"
|
||||
label="Global Limit"
|
||||
value={userData.resultLimits?.global || undefined}
|
||||
min={1}
|
||||
min={0}
|
||||
defaultValue={undefined}
|
||||
onValueChange={(value) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
resultLimits: { ...prev.resultLimits, global: value },
|
||||
resultLimits: {
|
||||
...prev.resultLimits,
|
||||
global: value || undefined,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
@@ -898,12 +997,15 @@ function Content() {
|
||||
help="Limit for results by service"
|
||||
label="Service Limit"
|
||||
value={userData.resultLimits?.service || undefined}
|
||||
min={1}
|
||||
min={0}
|
||||
defaultValue={undefined}
|
||||
onValueChange={(value) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
resultLimits: { ...prev.resultLimits, service: value },
|
||||
resultLimits: {
|
||||
...prev.resultLimits,
|
||||
service: value || undefined,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
@@ -911,12 +1013,15 @@ function Content() {
|
||||
help="Limit for results by addon"
|
||||
label="Addon Limit"
|
||||
value={userData.resultLimits?.addon || undefined}
|
||||
min={1}
|
||||
min={0}
|
||||
defaultValue={undefined}
|
||||
onValueChange={(value) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
resultLimits: { ...prev.resultLimits, addon: value },
|
||||
resultLimits: {
|
||||
...prev.resultLimits,
|
||||
addon: value || undefined,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
@@ -924,14 +1029,14 @@ function Content() {
|
||||
help="Limit for results by resolution"
|
||||
label="Resolution Limit"
|
||||
value={userData.resultLimits?.resolution || undefined}
|
||||
min={1}
|
||||
min={0}
|
||||
defaultValue={undefined}
|
||||
onValueChange={(value) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
resultLimits: {
|
||||
...prev.resultLimits,
|
||||
resolution: value,
|
||||
resolution: value || undefined,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
@@ -940,12 +1045,15 @@ function Content() {
|
||||
help="Limit for results by quality"
|
||||
label="Quality Limit"
|
||||
value={userData.resultLimits?.quality || undefined}
|
||||
min={1}
|
||||
min={0}
|
||||
defaultValue={undefined}
|
||||
onValueChange={(value) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
resultLimits: { ...prev.resultLimits, quality: value },
|
||||
resultLimits: {
|
||||
...prev.resultLimits,
|
||||
quality: value || undefined,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
@@ -953,12 +1061,15 @@ function Content() {
|
||||
help="Limit for results by indexer"
|
||||
label="Indexer Limit"
|
||||
value={userData.resultLimits?.indexer || undefined}
|
||||
min={1}
|
||||
min={0}
|
||||
defaultValue={undefined}
|
||||
onValueChange={(value) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
resultLimits: { ...prev.resultLimits, indexer: value },
|
||||
resultLimits: {
|
||||
...prev.resultLimits,
|
||||
indexer: value || undefined,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
@@ -966,14 +1077,14 @@ function Content() {
|
||||
help="Limit for results by release group"
|
||||
label="Release Group Limit"
|
||||
value={userData.resultLimits?.releaseGroup || undefined}
|
||||
min={1}
|
||||
min={0}
|
||||
defaultValue={undefined}
|
||||
onValueChange={(value) => {
|
||||
setUserData((prev) => ({
|
||||
...prev,
|
||||
resultLimits: {
|
||||
...prev.resultLimits,
|
||||
releaseGroup: value,
|
||||
releaseGroup: value || undefined,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
@@ -1410,6 +1521,7 @@ type TextInputProps = {
|
||||
help: string; // help text that shows below the label
|
||||
values: string[];
|
||||
onValuesChange: (values: string[]) => void;
|
||||
onValueChange: (value: string, index: number) => void;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
@@ -1421,6 +1533,7 @@ function TextInputs({
|
||||
help,
|
||||
values,
|
||||
onValuesChange,
|
||||
onValueChange,
|
||||
placeholder,
|
||||
}: TextInputProps) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -1471,13 +1584,14 @@ function TextInputs({
|
||||
value={value}
|
||||
label={itemName}
|
||||
placeholder={placeholder}
|
||||
onValueChange={(value) =>
|
||||
onValuesChange([
|
||||
...values.slice(0, index),
|
||||
value,
|
||||
...values.slice(index + 1),
|
||||
])
|
||||
}
|
||||
onValueChange={(value) => onValueChange(value, index)}
|
||||
// onValueChange={(value) =>
|
||||
// onValuesChange([
|
||||
// ...values.slice(0, index),
|
||||
// value,
|
||||
// ...values.slice(index + 1),
|
||||
// ])
|
||||
// }
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
@@ -1556,6 +1670,8 @@ type KeyValueInputProps = {
|
||||
valuePlaceholder: string;
|
||||
values: { name: string; value: string }[];
|
||||
onValuesChange: (values: { name: string; value: string }[]) => void;
|
||||
onValueChange: (value: string, index: number) => void;
|
||||
onKeyChange: (key: string, index: number) => void;
|
||||
};
|
||||
|
||||
function TwoTextInputs({
|
||||
@@ -1569,6 +1685,8 @@ function TwoTextInputs({
|
||||
valuePlaceholder,
|
||||
values,
|
||||
onValuesChange,
|
||||
onValueChange,
|
||||
onKeyChange,
|
||||
}: KeyValueInputProps) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -1602,11 +1720,17 @@ function TwoTextInputs({
|
||||
if (
|
||||
Array.isArray(data) &&
|
||||
data.every(
|
||||
(value: { name: string; value: string }) =>
|
||||
typeof value.name === 'string' && typeof value.value === 'string'
|
||||
(value: { [key: string]: string }) =>
|
||||
typeof value[keyId] === 'string' &&
|
||||
typeof value[valueId] === 'string'
|
||||
)
|
||||
) {
|
||||
onValuesChange(data);
|
||||
onValuesChange(
|
||||
data.map((v: { [key: string]: string }) => ({
|
||||
name: v[keyId],
|
||||
value: v[valueId],
|
||||
}))
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error importing file:', error);
|
||||
@@ -1622,31 +1746,21 @@ function TwoTextInputs({
|
||||
<SettingsCard title={title} description={description}>
|
||||
{values.map((value, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<div className="flex-1 flex gap-2">
|
||||
<div className="flex-1">
|
||||
<TextInput
|
||||
value={value.name}
|
||||
label={keyName}
|
||||
placeholder={keyPlaceholder}
|
||||
onValueChange={(value) => {
|
||||
const newValues = [...values];
|
||||
newValues[index].name = value;
|
||||
onValuesChange(newValues);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<TextInput
|
||||
value={value.value}
|
||||
label={valueName}
|
||||
placeholder={valuePlaceholder}
|
||||
onValueChange={(value) => {
|
||||
const newValues = [...values];
|
||||
newValues[index].value = value;
|
||||
onValuesChange(newValues);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<TextInput
|
||||
value={value.name}
|
||||
label={keyName}
|
||||
placeholder={keyPlaceholder}
|
||||
onValueChange={(newValue) => onKeyChange(newValue, index)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<TextInput
|
||||
value={value.value}
|
||||
label={valueName}
|
||||
placeholder={valuePlaceholder}
|
||||
onValueChange={(newValue) => onValueChange(newValue, index)}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
size="sm"
|
||||
|
||||
Reference in New Issue
Block a user