'use client'; import { PageWrapper } from '../shared/page-wrapper'; import * as constants from '../../../../core/src/utils/constants'; import { ParsedStream } from '../../../../core/src/db/schemas'; import React, { useState, useEffect, useCallback } from 'react'; import { useUserData } from '@/context/userData'; import { SettingsCard } from '../shared/settings-card'; import { Textarea } from '../ui/textarea'; import { Select } from '../ui/select'; import { Switch } from '../ui/switch'; import { TextInput } from '../ui/text-input'; import FileParser from '@aiostreams/core/src/parser/file'; import { UserConfigAPI } from '@/services/api'; import { SNIPPETS } from '../../../../core/src/utils/constants'; import { Modal } from '@/components/ui/modal'; import { useDisclosure } from '@/hooks/disclosure'; import { Button } from '../ui/button'; import { CopyIcon } from 'lucide-react'; import { toast } from 'sonner'; import { NumberInput } from '../ui/number-input'; import { PageControls } from '../shared/page-controls'; const formatterChoices = Object.values(constants.FORMATTER_DETAILS); // Remove the throttle utility and replace with FormatQueue class FormatQueue { private queue: (() => Promise)[] = []; private processing = false; private readonly delay: number; constructor(delay: number) { this.delay = delay; } enqueue(formatFn: () => Promise) { // Replace any existing queued format request with the new one this.queue = [formatFn]; this.process(); } private async process() { if (this.processing) return; this.processing = true; while (this.queue.length > 0) { const formatFn = this.queue.shift(); if (formatFn) { try { await formatFn(); } catch (error) { console.error('Error in format queue:', error); } // Wait for the specified delay before processing the next request await new Promise((resolve) => setTimeout(resolve, this.delay)); } } this.processing = false; } } export function FormatterMenu() { return ( <> ); } function FormatterPreviewBox({ name, description, }: { name?: string; description?: string; }) { return (
{name}
{description}
); } function Content() { const { userData, setUserData } = useUserData(); const [selectedFormatter, setSelectedFormatter] = useState( (userData.formatter?.id as constants.FormatterType) || formatterChoices[0].id ); const [formattedStream, setFormattedStream] = useState<{ name: string; description: string; } | null>(null); const [isFormatting, setIsFormatting] = useState(false); // Create format queue ref to persist between renders const formatQueueRef = React.useRef(new FormatQueue(200)); // Stream preview state const [filename, setFilename] = useState( 'Movie.Title.2023.2160p.BluRay.HEVC.DV.TrueHD.Atmos.7.1.iTA.ENG-GROUP.mkv' ); const [folder, setFolder] = useState( 'Movie.Title.2023.2160p.BluRay.HEVC.DV.TrueHD.Atmos.7.1.iTA.ENG-GROUP' ); const [indexer, setIndexer] = useState('RARBG'); const [seeders, setSeeders] = useState(125); const [age, setAge] = useState('10d'); const [addonName, setAddonName] = useState('Torrentio'); const [providerId, setProviderId] = useState( 'none' ); const [isCached, setIsCached] = useState(true); const [type, setType] = useState<(typeof constants.STREAM_TYPES)[number]>('debrid'); const [library, setLibrary] = useState(false); const [duration, setDuration] = useState(9120000); // 2h 32m in milliseconds const [fileSize, setFileSize] = useState(62500000000); // 58.2 GB in bytes const [folderSize, setFolderSize] = useState( 125000000000 ); // 116.4 GB in bytes const [proxied, setProxied] = useState(false); const [regexMatched, setRegexMatched] = useState( undefined ); const [message, setMessage] = useState('This is a message'); // Custom formatter state (to avoid losing one field when editing the other) const [customName, setCustomName] = useState( userData.formatter?.definition?.name || '' ); const [customDescription, setCustomDescription] = useState( userData.formatter?.definition?.description || '' ); // Keep userData in sync with custom formatter fields useEffect(() => { if (selectedFormatter === constants.CUSTOM_FORMATTER) { setUserData((prev) => ({ ...prev, formatter: { id: constants.CUSTOM_FORMATTER, definition: { name: customName, description: customDescription }, }, })); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [customName, customDescription, selectedFormatter]); const handleFormatterChange = (value: string) => { setSelectedFormatter(value as constants.FormatterType); if (value === constants.CUSTOM_FORMATTER) { setCustomName(userData.formatter?.definition?.name || ''); setCustomDescription(userData.formatter?.definition?.description || ''); } setUserData((prev) => ({ ...prev, formatter: { id: value as constants.FormatterType, definition: value === constants.CUSTOM_FORMATTER ? { name: userData.formatter?.definition?.name || '', description: userData.formatter?.definition?.description || '', } : // keep definitions even when switching to a non-custom formatter prev.formatter?.definition, }, })); }; const formatStream = useCallback(async () => { if (isFormatting) return; try { setIsFormatting(true); const parsedFile = FileParser.parse(filename); const stream: ParsedStream = { id: 'preview', type, addon: { name: addonName, presetType: 'custom', presetInstanceId: 'custom', enabled: true, manifestUrl: 'http://localhost:2000/manifest.json', timeout: 10000, }, library, parsedFile, filename, folderName: folder, folderSize, indexer, regexMatched: { name: regexMatched, index: 0, }, torrent: { infoHash: type === 'p2p' ? '1234567890' : undefined, seeders, }, service: providerId === 'none' ? undefined : { id: providerId, cached: isCached, }, age, duration, size: fileSize, proxied, message, }; let data; if (selectedFormatter === constants.CUSTOM_FORMATTER) { const res = await UserConfigAPI.formatStream( stream, selectedFormatter, { name: customName, description: customDescription, }, userData.addonName ); if (!res.success) { toast.error(res.error?.message || 'Failed to format stream'); return; } data = res.data; } else { const res = await UserConfigAPI.formatStream( stream, selectedFormatter, undefined, userData.addonName ); if (!res.success) { toast.error(res.error?.message || 'Failed to format stream'); return; } data = res.data; } setFormattedStream(data ?? null); } catch (error) { console.error('Error formatting stream:', error); toast.error('Failed to format stream'); } finally { setIsFormatting(false); } }, [ filename || undefined, folder || undefined, indexer, seeders, age, addonName, providerId, isCached, type, library, duration, fileSize, folderSize, proxied, selectedFormatter, isFormatting, customName, customDescription, regexMatched, message, ]); useEffect(() => { formatQueueRef.current.enqueue(formatStream); }, [ filename, folder, indexer, seeders, age, addonName, providerId, isCached, type, library, duration, fileSize, folderSize, proxied, selectedFormatter, customName, regexMatched, customDescription, message, ]); return ( <>

Formatter

Format your streams to your liking.

{/* Formatter Selection in its own SettingsCard */}