mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-09 23:15:46 +01:00
History done
This commit is contained in:
@@ -42,8 +42,7 @@ export default function TorrentsComponent() {
|
|||||||
(service) =>
|
(service) =>
|
||||||
service.type === 'qBittorrent' ||
|
service.type === 'qBittorrent' ||
|
||||||
service.type === 'Transmission' ||
|
service.type === 'Transmission' ||
|
||||||
service.type === 'Deluge' ||
|
service.type === 'Deluge'
|
||||||
service.type === 'Sabnzbd'
|
|
||||||
) ?? [];
|
) ?? [];
|
||||||
|
|
||||||
const hideComplete: boolean =
|
const hideComplete: boolean =
|
||||||
|
|||||||
@@ -1,20 +1,39 @@
|
|||||||
import { Center, Table, Text, Title, Tooltip, useMantineTheme } from '@mantine/core';
|
import { Center, Pagination, Skeleton, Table, Text, Title, Tooltip } from '@mantine/core';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import duration from 'dayjs/plugin/duration';
|
import duration from 'dayjs/plugin/duration';
|
||||||
import { FunctionComponent } from 'react';
|
import { FunctionComponent, useState } from 'react';
|
||||||
|
import { useGetUsenetHistory } from '../../tools/hooks/api';
|
||||||
import { humanFileSize } from '../../tools/humanFileSize';
|
import { humanFileSize } from '../../tools/humanFileSize';
|
||||||
import { UsenetHistoryItem } from './types';
|
|
||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
interface UsenetHistoryListProps {
|
interface UsenetHistoryListProps {
|
||||||
items: UsenetHistoryItem[];
|
serviceId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ items }) => {
|
const PAGE_SIZE = 10;
|
||||||
const theme = useMantineTheme();
|
|
||||||
|
|
||||||
if (items.length <= 0) {
|
export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ serviceId }) => {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
|
const { data, isLoading } = useGetUsenetHistory({
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
offset: (page - 1) * PAGE_SIZE,
|
||||||
|
serviceId,
|
||||||
|
});
|
||||||
|
const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Skeleton height={40} mt={10} />
|
||||||
|
<Skeleton height={40} mt={10} />
|
||||||
|
<Skeleton height={40} mt={10} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || data.items.length <= 0) {
|
||||||
return (
|
return (
|
||||||
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<Title order={3}>Queue is empty</Title>
|
<Title order={3}>Queue is empty</Title>
|
||||||
@@ -23,6 +42,7 @@ export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ i
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
<Table highlightOnHover style={{ tableLayout: 'fixed' }}>
|
<Table highlightOnHover style={{ tableLayout: 'fixed' }}>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col span={1} />
|
<col span={1} />
|
||||||
@@ -37,7 +57,7 @@ export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ i
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{items.map((history) => (
|
{data.items.map((history) => (
|
||||||
<tr key={history.id}>
|
<tr key={history.id}>
|
||||||
<td>
|
<td>
|
||||||
<Tooltip position="top" label={history.name}>
|
<Tooltip position="top" label={history.name}>
|
||||||
@@ -67,5 +87,14 @@ export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ i
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
<Pagination
|
||||||
|
size="sm"
|
||||||
|
position="center"
|
||||||
|
mt="md"
|
||||||
|
total={totalPages}
|
||||||
|
page={page}
|
||||||
|
onChange={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,38 +1,42 @@
|
|||||||
import { Skeleton, Tabs, useMantineTheme } from '@mantine/core';
|
import { Group, Select, Tabs } from '@mantine/core';
|
||||||
import { IconDownload } from '@tabler/icons';
|
import { IconDownload } from '@tabler/icons';
|
||||||
import { FunctionComponent } from 'react';
|
import { FunctionComponent, useState } from 'react';
|
||||||
|
|
||||||
import { IModule } from '../ModuleTypes';
|
import { IModule } from '../ModuleTypes';
|
||||||
import { useGetUsenetDownloads, useGetUsenetHistory } from '../../tools/hooks/api';
|
|
||||||
import { UsenetQueueList } from './UsenetQueueList';
|
import { UsenetQueueList } from './UsenetQueueList';
|
||||||
import { UsenetHistoryList } from './UsenetHistoryList';
|
import { UsenetHistoryList } from './UsenetHistoryList';
|
||||||
|
import { useGetServiceByType } from '../../tools/hooks/useGetServiceByType';
|
||||||
|
|
||||||
export const UsenetComponent: FunctionComponent = () => {
|
export const UsenetComponent: FunctionComponent = () => {
|
||||||
const theme = useMantineTheme();
|
const downloadServices = useGetServiceByType('Sabnzbd');
|
||||||
const { isLoading, data: nzbs = [] } = useGetUsenetDownloads();
|
|
||||||
const { data: history = [] } = useGetUsenetHistory();
|
|
||||||
|
|
||||||
if (isLoading) {
|
const [selectedServiceId, setSelectedService] = useState<string | null>(downloadServices[0]?.id);
|
||||||
return (
|
|
||||||
<>
|
if (!selectedServiceId) {
|
||||||
<Skeleton height={40} mt={10} />
|
return null;
|
||||||
<Skeleton height={40} mt={10} />
|
|
||||||
<Skeleton height={40} mt={10} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue="queue">
|
<Tabs keepMounted={false} defaultValue="queue">
|
||||||
<Tabs.List>
|
<Group mb="md">
|
||||||
|
<Tabs.List style={{ flex: 1 }}>
|
||||||
<Tabs.Tab value="queue">Queue</Tabs.Tab>
|
<Tabs.Tab value="queue">Queue</Tabs.Tab>
|
||||||
<Tabs.Tab value="history">History</Tabs.Tab>
|
<Tabs.Tab value="history">History</Tabs.Tab>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
|
{downloadServices.length > 1 && (
|
||||||
|
<Select
|
||||||
|
value={selectedServiceId}
|
||||||
|
onChange={setSelectedService}
|
||||||
|
ml="xs"
|
||||||
|
data={downloadServices.map((service) => ({ value: service.id, label: service.name }))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
<Tabs.Panel value="queue">
|
<Tabs.Panel value="queue">
|
||||||
<UsenetQueueList items={nzbs} />
|
<UsenetQueueList serviceId={selectedServiceId} />
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
<Tabs.Panel value="history">
|
<Tabs.Panel value="history">
|
||||||
<UsenetHistoryList items={history} />
|
<UsenetHistoryList serviceId={selectedServiceId} />
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,20 +1,48 @@
|
|||||||
import { Center, Progress, Table, Text, Title, Tooltip, useMantineTheme } from '@mantine/core';
|
import {
|
||||||
|
Center,
|
||||||
|
Progress,
|
||||||
|
Skeleton,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Tooltip,
|
||||||
|
useMantineTheme,
|
||||||
|
} from '@mantine/core';
|
||||||
import { IconPlayerPause, IconPlayerPlay } from '@tabler/icons';
|
import { IconPlayerPause, IconPlayerPlay } from '@tabler/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import duration from 'dayjs/plugin/duration';
|
import duration from 'dayjs/plugin/duration';
|
||||||
import { FunctionComponent } from 'react';
|
import { FunctionComponent, useState } from 'react';
|
||||||
|
import { useGetUsenetDownloads } from '../../tools/hooks/api';
|
||||||
import { humanFileSize } from '../../tools/humanFileSize';
|
import { humanFileSize } from '../../tools/humanFileSize';
|
||||||
import { UsenetQueueItem } from './types';
|
|
||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
interface UsenetQueueListProps {
|
interface UsenetQueueListProps {
|
||||||
items: UsenetQueueItem[];
|
serviceId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ items }) => {
|
const PAGE_SIZE = 10;
|
||||||
const theme = useMantineTheme();
|
|
||||||
|
|
||||||
if (items.length <= 0) {
|
export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ serviceId }) => {
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const { data, isLoading } = useGetUsenetDownloads({
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
offset: (page - 1) * PAGE_SIZE,
|
||||||
|
serviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Skeleton height={40} mt={10} />
|
||||||
|
<Skeleton height={40} mt={10} />
|
||||||
|
<Skeleton height={40} mt={10} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || data.items.length <= 0) {
|
||||||
return (
|
return (
|
||||||
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<Title order={3}>Queue is empty</Title>
|
<Title order={3}>Queue is empty</Title>
|
||||||
@@ -34,7 +62,7 @@ export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ items
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{items.map((nzb) => (
|
{data.items.map((nzb) => (
|
||||||
<tr key={nzb.id}>
|
<tr key={nzb.id}>
|
||||||
<td>
|
<td>
|
||||||
{nzb.state === 'paused' ? (
|
{nzb.state === 'paused' ? (
|
||||||
|
|||||||
@@ -5,37 +5,51 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { Client } from 'sabnzbd-api';
|
import { Client } from 'sabnzbd-api';
|
||||||
import { UsenetHistoryItem } from '../../../../modules';
|
import { UsenetHistoryItem } from '../../../../modules';
|
||||||
import { getConfig } from '../../../../tools/getConfig';
|
import { getConfig } from '../../../../tools/getConfig';
|
||||||
|
import { getServiceById } from '../../../../tools/hooks/useGetServiceByType';
|
||||||
import { Config } from '../../../../tools/types';
|
import { Config } from '../../../../tools/types';
|
||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
|
export interface UsenetHistoryRequestParams {
|
||||||
|
serviceId: string;
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsenetHistoryResponse {
|
||||||
|
items: UsenetHistoryItem[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
const configName = getCookie('config-name', { req });
|
const configName = getCookie('config-name', { req });
|
||||||
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
|
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
|
||||||
const nzbServices = config.services.filter((service) => service.type === 'Sabnzbd');
|
const { limit, offset, serviceId } = req.query as any as UsenetHistoryRequestParams;
|
||||||
|
|
||||||
const history: UsenetHistoryItem[] = [];
|
const service = getServiceById(config, serviceId);
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`);
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
nzbServices.map(async (service) => {
|
|
||||||
if (!service.apiKey) {
|
if (!service.apiKey) {
|
||||||
throw new Error(`API Key for service "${service.name}" is missing`);
|
throw new Error(`API Key for service "${service.name}" is missing`);
|
||||||
}
|
}
|
||||||
const queue = await new Client(service.url, service.apiKey).history();
|
const history = await new Client(service.url, service.apiKey).history(offset, limit);
|
||||||
|
|
||||||
queue.slots.forEach((slot) => {
|
const items: UsenetHistoryItem[] = history.slots.map((slot) => ({
|
||||||
history.push({
|
|
||||||
id: slot.nzo_id,
|
id: slot.nzo_id,
|
||||||
name: slot.name,
|
name: slot.name,
|
||||||
size: slot.bytes,
|
size: slot.bytes,
|
||||||
time: slot.download_time,
|
time: slot.download_time,
|
||||||
});
|
}));
|
||||||
});
|
const response: UsenetHistoryResponse = {
|
||||||
})
|
items,
|
||||||
);
|
total: history.noofslots,
|
||||||
|
};
|
||||||
|
|
||||||
return res.status(200).json(history);
|
return res.status(200).json(response);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return res.status(401).json(err);
|
return res.status(401).json(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,45 +5,63 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { Client } from 'sabnzbd-api';
|
import { Client } from 'sabnzbd-api';
|
||||||
import { UsenetQueueItem } from '../../../../modules';
|
import { UsenetQueueItem } from '../../../../modules';
|
||||||
import { getConfig } from '../../../../tools/getConfig';
|
import { getConfig } from '../../../../tools/getConfig';
|
||||||
|
import { getServiceById } from '../../../../tools/hooks/useGetServiceByType';
|
||||||
import { Config } from '../../../../tools/types';
|
import { Config } from '../../../../tools/types';
|
||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
|
export interface UsenetQueueRequestParams {
|
||||||
|
serviceId: string;
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsenetQueueResponse {
|
||||||
|
items: UsenetQueueItem[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
const configName = getCookie('config-name', { req });
|
const configName = getCookie('config-name', { req });
|
||||||
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
|
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
|
||||||
const nzbServices = config.services.filter((service) => service.type === 'Sabnzbd');
|
const { limit, offset, serviceId } = req.query as any as UsenetQueueRequestParams;
|
||||||
|
|
||||||
const downloads: UsenetQueueItem[] = [];
|
const service = getServiceById(config, serviceId);
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`);
|
||||||
|
}
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
nzbServices.map(async (service) => {
|
|
||||||
if (!service.apiKey) {
|
if (!service.apiKey) {
|
||||||
throw new Error(`API Key for service "${service.name}" is missing`);
|
throw new Error(`API Key for service "${service.name}" is missing`);
|
||||||
}
|
}
|
||||||
const queue = await new Client(service.url, service.apiKey).queue();
|
const queue = await new Client(service.url, service.apiKey).queue(offset, limit);
|
||||||
|
|
||||||
queue.slots.forEach((slot) => {
|
const items: UsenetQueueItem[] = queue.slots.map((slot) => {
|
||||||
const [hours, minutes, seconds] = slot.timeleft.split(':');
|
const [hours, minutes, seconds] = slot.timeleft.split(':');
|
||||||
const eta = dayjs.duration({
|
const eta = dayjs.duration({
|
||||||
hour: parseInt(hours, 10),
|
hour: parseInt(hours, 10),
|
||||||
minutes: parseInt(minutes, 10),
|
minutes: parseInt(minutes, 10),
|
||||||
seconds: parseInt(seconds, 10),
|
seconds: parseInt(seconds, 10),
|
||||||
} as any);
|
} as any);
|
||||||
downloads.push({
|
|
||||||
|
return {
|
||||||
id: slot.nzo_id,
|
id: slot.nzo_id,
|
||||||
eta: eta.asSeconds(),
|
eta: eta.asSeconds(),
|
||||||
name: slot.filename,
|
name: slot.filename,
|
||||||
progress: parseFloat(slot.percentage),
|
progress: parseFloat(slot.percentage),
|
||||||
size: parseFloat(slot.mb) * 1000 * 1000,
|
size: parseFloat(slot.mb) * 1000 * 1000,
|
||||||
state: slot.status.toLowerCase() as any,
|
state: slot.status.toLowerCase() as any,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).json(downloads);
|
const response: UsenetQueueResponse = {
|
||||||
|
items,
|
||||||
|
total: queue.noofslots_total,
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.status(200).json(response);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return res.status(401).json(err);
|
return res.status(401).json(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,37 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { UsenetHistoryItem, UsenetQueueItem } from '../../modules';
|
import { UsenetQueueRequestParams, UsenetQueueResponse } from '../../pages/api/modules/usenet';
|
||||||
|
import {
|
||||||
|
UsenetHistoryRequestParams,
|
||||||
|
UsenetHistoryResponse,
|
||||||
|
} from '../../pages/api/modules/usenet/history';
|
||||||
|
|
||||||
export const useGetUsenetDownloads = () =>
|
export const useGetUsenetDownloads = (params: UsenetQueueRequestParams) =>
|
||||||
useQuery(
|
useQuery(
|
||||||
['usenetDownloads'],
|
['usenetDownloads', ...Object.values(params)],
|
||||||
async () => (await axios.get<UsenetQueueItem[]>('/api/modules/usenet')).data,
|
async () =>
|
||||||
|
(
|
||||||
|
await axios.get<UsenetQueueResponse>('/api/modules/usenet', {
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
).data,
|
||||||
{
|
{
|
||||||
refetchInterval: 1000,
|
refetchInterval: 1000,
|
||||||
|
keepPreviousData: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const useGetUsenetHistory = () =>
|
export const useGetUsenetHistory = (params: UsenetHistoryRequestParams) =>
|
||||||
useQuery(
|
useQuery(
|
||||||
['usenetHistory'],
|
['usenetHistory', ...Object.values(params)],
|
||||||
async () => (await axios.get<UsenetHistoryItem[]>('/api/modules/usenet/history')).data,
|
async () =>
|
||||||
|
(
|
||||||
|
await axios.get<UsenetHistoryResponse>('/api/modules/usenet/history', {
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
).data,
|
||||||
{
|
{
|
||||||
refetchInterval: 1000,
|
refetchInterval: 1000,
|
||||||
|
keepPreviousData: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
14
src/tools/hooks/useGetServiceByType.ts
Normal file
14
src/tools/hooks/useGetServiceByType.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { useConfig } from '../state';
|
||||||
|
import { Config, ServiceType } from '../types';
|
||||||
|
|
||||||
|
export const useGetServiceByType = (...serviceTypes: ServiceType[]) => {
|
||||||
|
const { config } = useConfig();
|
||||||
|
|
||||||
|
return getServiceByType(config, ...serviceTypes);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getServiceByType = (config: Config, ...serviceTypes: ServiceType[]) =>
|
||||||
|
config.services.filter((s) => serviceTypes.includes(s.type));
|
||||||
|
|
||||||
|
export const getServiceById = (config: Config, id: string) =>
|
||||||
|
config.services.find((s) => s.id === id);
|
||||||
Reference in New Issue
Block a user