import { StrictMode, createContext, memo, useContext, useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom/client'; // Define API URL and base path const apiUrl = (window.origin === 'null' || window.origin === 'http://localhost:3031') ? 'http://localhost:3030' : ''; interface ErrorType { id?: number, method?: string, path?: string, status?: number, statusText?: string, text: string, }; interface ContextType { setCloseableError: (error: ErrorType) => void, setOtherError: (error: ErrorType) => void, makeRequest: (method: string, path: string, data: any, showError: boolean) => Promise, requests: { getTorrentDetails: any, getTorrentStats: any, }, refreshTorrents: () => void, } const AppContext = createContext(null); // Interface for the Torrent API response interface TorrentId { id: number; info_hash: string; } // Interface for the Torrent Details API response interface TorrentDetails { files: { name: string; length: number; included: boolean; }[]; } // Interface for the Torrent Stats API response interface TorrentStats { snapshot: { have_bytes: number; downloaded_and_checked_bytes: number; downloaded_and_checked_pieces: number; fetched_bytes: number; uploaded_bytes: number; initially_needed_bytes: number; remaining_bytes: number; total_bytes: number; total_piece_download_ms: number; peer_stats: { queued: number; connecting: number; live: number; seen: number; dead: number; not_needed: number; }; }; average_piece_download_time: { secs: number; nanos: number; }; download_speed: { mbps: number; human_readable: string; }; all_time_download_speed: { mbps: number; human_readable: string; }; time_remaining: { human_readable: string; } | null; } function TorrentRow({ detailsResponse, statsResponse }) { const totalBytes = statsResponse.snapshot.total_bytes; const downloadedBytes = statsResponse.snapshot.have_bytes; const downloadPercentage = (downloadedBytes / totalBytes) * 100; return (
); } const Column = ({ label, value }) => (

{label}

{value}

); const ColumnWithProgressBar = ({ label, percentage }) => (

{label}

{percentage.toFixed(2)}%

); const Torrent = ({ torrent }) => { const defaultDetails: TorrentDetails = { files: [] }; const defaultStats: TorrentStats = { snapshot: { have_bytes: 0, downloaded_and_checked_bytes: 0, downloaded_and_checked_pieces: 0, fetched_bytes: 0, uploaded_bytes: 0, initially_needed_bytes: 0, remaining_bytes: 0, total_bytes: 0, total_piece_download_ms: 0, peer_stats: { queued: 0, connecting: 0, live: 0, seen: 0, dead: 0, not_needed: 0 } }, average_piece_download_time: { secs: 0, nanos: 0 }, download_speed: { mbps: 0, human_readable: '' }, all_time_download_speed: { mbps: 0, human_readable: '' }, time_remaining: { human_readable: '' } }; const [detailsResponse, updateDetailsResponse] = useState(defaultDetails); const [statsResponse, updateStatsResponse] = useState(defaultStats); let ctx = useContext(AppContext); const update = async () => { return await Promise.all([ ctx.requests.getTorrentDetails(torrent.id).then((details) => { updateDetailsResponse(details); return details; }), ctx.requests.getTorrentStats(torrent.id).then((stats) => { updateStatsResponse(stats); return stats; }) ]).then(([_, stats]) => { return torrentIsDone(stats) ? 10000 : 500; }, (e) => { return 5000; }) }; useEffect(() => { let clear = customSetInterval(update, 0); return clear; }, []); return } const Spinner = () => (
Loading...
) const TorrentsList = (props: { torrents: Array, loading: boolean }) => { if (props.torrents === null && props.loading) { return } // The app either just started, or there was an error loading torrents. if (props.torrents === null) { return <> } if (props.torrents.length === 0) { return (

No existing torrents found. Add them through buttons below.

) } return (
{props.torrents.map((t: TorrentId) => )}
) }; const Root = () => { const [closeableError, setCloseableError] = useState(null); const [otherError, setOtherError] = useState(null); const [torrents, setTorrents] = useState>(null); const [torrentsLoading, setTorrentsLoading] = useState(false); const makeRequest = async (method: string, path: string, data: any, showError: boolean): Promise => { console.log(method, path); const url = apiUrl + path; const options: RequestInit = { method, headers: { 'Accept': 'application/json', }, body: data, }; const maybeShowError = (e: ErrorType) => { if (showError) { setCloseableError(e); } } let error: ErrorType = { method: method, path: path, text: '' }; let response: Response; try { response = await fetch(url, options); } catch (e) { error.text = 'unknown error: ' + e.toString(); maybeShowError(error); return Promise.reject(error); } error.status = response.status; error.statusText = response.statusText; if (!response.ok) { const errorBody = await response.text(); try { const json = JSON.parse(errorBody); error.text = json.human_readable !== undefined ? json.human_readable : JSON.stringify(json, null, 2); } catch (e) { error.text = errorBody; } maybeShowError(error); return Promise.reject(error); } const result = await response.json(); return result; } const requests = { getTorrentDetails: (index: number): Promise => { return makeRequest('GET', `/torrents/${index}`, null, false); }, getTorrentStats: (index: number): Promise => { return makeRequest('GET', `/torrents/${index}/stats`, null, false); } }; const refreshTorrents = async () => { setTorrentsLoading(true); let torrents: { torrents: Array } = await makeRequest('GET', '/torrents', null, false).finally(() => setTorrentsLoading(false)); setTorrents(torrents.torrents); return torrents; }; useEffect(() => { let interval = 500; let clear = customSetInterval(async () => { try { await refreshTorrents(); setOtherError(null); return interval; } catch (e) { setOtherError(e); console.error(e); return 5000; } }, interval); return clear; }, []); const context: ContextType = { setCloseableError, setOtherError, makeRequest, requests, refreshTorrents, } return } const Error = (props: { error: ErrorType, remove?: () => void }) => { let { error, remove } = props; if (error == null) { return null; } return (
{error.method && ( Error calling {error.method} {error.path}: )} {error.status && ( {error.status} {error.statusText}: )} {error.text} { remove && ( ) }
); }; const MagnetInput = () => { let ctx = useContext(AppContext); // Function to add a torrent from a magnet link async function addTorrentFromMagnet(): Promise { const magnetLink = prompt('Enter magnet link:'); if (magnetLink) { await ctx.makeRequest('POST', '/torrents?overwrite=true', magnetLink, true); ctx.refreshTorrents(); } } return }; const FileInput = () => { const inputRef = useRef(); let ctx = useContext(AppContext); const inputOnChange = async (e) => { let file = e.target.files[0]; await ctx.makeRequest('POST', '/torrents?overwrite=true', file, true); ctx.refreshTorrents(); } const onClick = () => { inputRef.current.click(); } return (<> ); }; const Buttons = () => { return (
); }; const LastErrors = (props: { lastErrors: Array }) => { return
{props.lastErrors.map((e: ErrorType) => (
))}
} const RootContent = (props: { closeableError: ErrorType, otherError: ErrorType, torrents: Array, torrentsLoading: boolean }) => { let ctx = useContext(AppContext); return <> ctx.setCloseableError(null)} /> }; function torrentIsDone(stats: TorrentStats): boolean { return stats.snapshot.have_bytes == stats.snapshot.total_bytes; } // Render function to display all torrents async function displayTorrents() { // Get the torrents container const torrentsContainer = document.getElementById('output'); const RootMemo = memo(Root, (prev, next) => true); ReactDOM.createRoot(torrentsContainer).render(); } // Function to format bytes to GB function formatBytesToGB(bytes: number): string { const GB = bytes / (1024 * 1024 * 1024); return GB.toFixed(2); } // Function to get the name of the largest file in a torrent function getLargestFileName(torrentDetails: TorrentDetails): string { if (torrentDetails.files.length == 0) { return 'Loading...'; } const largestFile = torrentDetails.files.reduce((prev: any, current: any) => (prev.length > current.length) ? prev : current); return largestFile.name; } // Function to get the completion ETA of a torrent function getCompletionETA(stats: TorrentStats): string { if (stats.time_remaining) { return stats.time_remaining.human_readable; } else { return 'N/A'; } } function customSetInterval(asyncCallback: any, interval: number) { let timeoutId: number; let currentInterval: number = interval; const executeCallback = async () => { currentInterval = await asyncCallback(); if (currentInterval === null || currentInterval === undefined) { throw 'asyncCallback returned null or undefined'; } scheduleNext(); } let scheduleNext = () => { timeoutId = setTimeout(executeCallback, currentInterval); } scheduleNext(); let clearCustomInterval = () => { clearTimeout(timeoutId); } return clearCustomInterval; } // List all torrents on page load and set up auto-refresh async function init(): Promise { await displayTorrents(); } // Call init function on page load document.addEventListener('DOMContentLoaded', init);