Remove downloads management features: delete DownloadsEndpoints and related API logic, remove associated frontend components and routes, and update documentation to reflect the removal of the downloads directory from the application structure.

This commit is contained in:
Leonid Pershin
2026-07-24 18:18:22 +03:00
parent 44d10f667c
commit 7309a25764
18 changed files with 6 additions and 382 deletions
@@ -1,112 +0,0 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { ExternalLink, RotateCw } from 'lucide-react'
import { HttpError } from '@/shared/api/client'
import { Button } from '@/shared/ui/button'
import { toast } from '@/shared/ui/toast-store'
import { importDownload, listDownloads } from './api'
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`
const units = ['KB', 'MB', 'GB', 'TB']
let value = bytes / 1024
let i = 0
while (value >= 1024 && i < units.length - 1) {
value /= 1024
i++
}
return `${value.toFixed(1)} ${units[i]}`
}
export function DownloadsPanel() {
const { t } = useTranslation()
const queryClient = useQueryClient()
const { data, isLoading, refetch, isFetching } = useQuery({
queryKey: ['admin', 'downloads'],
queryFn: listDownloads,
refetchInterval: 15_000,
})
const onError = (error: unknown) =>
toast.error(error instanceof HttpError ? error.detail : t('common.error'))
const importMutation = useMutation({
mutationFn: importDownload,
onSuccess: () => {
toast.success(t('admin.downloads.imported'))
void queryClient.invalidateQueries({ queryKey: ['admin', 'media'] })
},
onError,
})
const clientUrl =
typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.hostname}:8090` : '#'
return (
<div className="flex flex-col gap-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<h2 className="crt-glow text-xl font-semibold">{t('admin.downloads.title')}</h2>
<div className="flex items-center gap-2">
<Button size="sm" variant="outline" asChild>
<a href={clientUrl} target="_blank" rel="noreferrer">
<ExternalLink className="h-4 w-4" />
{t('admin.downloads.openClient')}
</a>
</Button>
<Button size="sm" variant="outline" disabled={isFetching} onClick={() => void refetch()}>
<RotateCw className="h-4 w-4" />
{t('admin.downloads.refresh')}
</Button>
</div>
</div>
<p className="text-sm text-muted-foreground">{t('admin.downloads.hint')}</p>
<div className="crt-panel overflow-x-auto rounded-md">
<table className="w-full text-sm">
<thead className="border-b border-border text-left text-muted-foreground">
<tr>
<th className="px-4 py-2 font-medium">{t('admin.downloads.path')}</th>
<th className="px-4 py-2 font-medium">{t('admin.downloads.size')}</th>
<th className="px-4 py-2 font-medium">{t('common.actions')}</th>
</tr>
</thead>
<tbody>
{isLoading && (
<tr>
<td className="px-4 py-3 text-muted-foreground" colSpan={3}>
{t('common.loading')}
</td>
</tr>
)}
{data?.map((file) => (
<tr key={file.relativePath} className="border-b border-border last:border-0">
<td className="px-4 py-2" title={file.relativePath}>
{file.relativePath}
</td>
<td className="px-4 py-2 text-muted-foreground">{formatBytes(file.sizeBytes)}</td>
<td className="px-4 py-2">
<Button
size="sm"
disabled={importMutation.isPending}
onClick={() => importMutation.mutate(file.relativePath)}
>
{t('admin.downloads.import')}
</Button>
</td>
</tr>
))}
{data && data.length === 0 && !isLoading && (
<tr>
<td className="px-4 py-3 text-muted-foreground" colSpan={3}>
{t('admin.downloads.empty')}
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)
}
@@ -1,13 +0,0 @@
import { apiRequest } from '@/shared/api/client'
import type { CreatedIdResponse, DownloadFileDto } from '@/shared/api/types'
export function listDownloads() {
return apiRequest<DownloadFileDto[]>('/admin/downloads')
}
export function importDownload(relativePath: string) {
return apiRequest<CreatedIdResponse>('/admin/downloads/import', {
method: 'POST',
body: { relativePath },
})
}
-21
View File
@@ -17,7 +17,6 @@ import { Route as RegisterRouteImport } from './routes/register'
import { Route as SettingsRouteImport } from './routes/settings'
import { Route as AdminIndexRouteImport } from './routes/admin/index'
import { Route as AdminChannelsRouteImport } from './routes/admin/channels'
import { Route as AdminDownloadsRouteImport } from './routes/admin/downloads'
import { Route as AdminMediaRouteImport } from './routes/admin/media'
import { Route as AdminRolesRouteImport } from './routes/admin/roles'
import { Route as AdminShowsRouteImport } from './routes/admin/shows'
@@ -65,11 +64,6 @@ const AdminChannelsRoute = AdminChannelsRouteImport.update({
path: '/channels',
getParentRoute: () => AdminRoute,
} as any)
const AdminDownloadsRoute = AdminDownloadsRouteImport.update({
id: '/downloads',
path: '/downloads',
getParentRoute: () => AdminRoute,
} as any)
const AdminMediaRoute = AdminMediaRouteImport.update({
id: '/media',
path: '/media',
@@ -109,7 +103,6 @@ export interface FileRoutesByFullPath {
'/register': typeof RegisterRoute
'/settings': typeof SettingsRoute
'/admin/channels': typeof AdminChannelsRouteWithChildren
'/admin/downloads': typeof AdminDownloadsRoute
'/admin/media': typeof AdminMediaRoute
'/admin/roles': typeof AdminRolesRoute
'/admin/shows': typeof AdminShowsRouteWithChildren
@@ -125,7 +118,6 @@ export interface FileRoutesByTo {
'/register': typeof RegisterRoute
'/settings': typeof SettingsRoute
'/admin/channels': typeof AdminChannelsRouteWithChildren
'/admin/downloads': typeof AdminDownloadsRoute
'/admin/media': typeof AdminMediaRoute
'/admin/roles': typeof AdminRolesRoute
'/admin/shows': typeof AdminShowsRouteWithChildren
@@ -143,7 +135,6 @@ export interface FileRoutesById {
'/register': typeof RegisterRoute
'/settings': typeof SettingsRoute
'/admin/channels': typeof AdminChannelsRouteWithChildren
'/admin/downloads': typeof AdminDownloadsRoute
'/admin/media': typeof AdminMediaRoute
'/admin/roles': typeof AdminRolesRoute
'/admin/shows': typeof AdminShowsRouteWithChildren
@@ -162,7 +153,6 @@ export interface FileRouteTypes {
| '/register'
| '/settings'
| '/admin/channels'
| '/admin/downloads'
| '/admin/media'
| '/admin/roles'
| '/admin/shows'
@@ -178,7 +168,6 @@ export interface FileRouteTypes {
| '/register'
| '/settings'
| '/admin/channels'
| '/admin/downloads'
| '/admin/media'
| '/admin/roles'
| '/admin/shows'
@@ -195,7 +184,6 @@ export interface FileRouteTypes {
| '/register'
| '/settings'
| '/admin/channels'
| '/admin/downloads'
| '/admin/media'
| '/admin/roles'
| '/admin/shows'
@@ -272,13 +260,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AdminChannelsRouteImport
parentRoute: typeof AdminRoute
}
'/admin/downloads': {
id: '/admin/downloads'
path: '/downloads'
fullPath: '/admin/downloads'
preLoaderRoute: typeof AdminDownloadsRouteImport
parentRoute: typeof AdminRoute
}
'/admin/media': {
id: '/admin/media'
path: '/media'
@@ -350,7 +331,6 @@ const AdminShowsRouteWithChildren = AdminShowsRoute._addFileChildren(
interface AdminRouteChildren {
AdminChannelsRoute: typeof AdminChannelsRouteWithChildren
AdminDownloadsRoute: typeof AdminDownloadsRoute
AdminMediaRoute: typeof AdminMediaRoute
AdminRolesRoute: typeof AdminRolesRoute
AdminShowsRoute: typeof AdminShowsRouteWithChildren
@@ -360,7 +340,6 @@ interface AdminRouteChildren {
const AdminRouteChildren: AdminRouteChildren = {
AdminChannelsRoute: AdminChannelsRouteWithChildren,
AdminDownloadsRoute: AdminDownloadsRoute,
AdminMediaRoute: AdminMediaRoute,
AdminRolesRoute: AdminRolesRoute,
AdminShowsRoute: AdminShowsRouteWithChildren,
-7
View File
@@ -22,13 +22,6 @@ function AdminLayout() {
>
{t('admin.media.title')}
</Link>
<Link
to="/admin/downloads"
className={cn('pb-2 uppercase tracking-wide text-muted-foreground hover:text-foreground')}
activeProps={{ className: 'border-b-2 border-primary text-primary' }}
>
{t('admin.downloads.title')}
</Link>
<Link
to="/admin/shows"
className={cn('pb-2 uppercase tracking-wide text-muted-foreground hover:text-foreground')}
-4
View File
@@ -1,4 +0,0 @@
import { createFileRoute } from '@tanstack/react-router'
import { DownloadsPanel } from '@/features/admin/downloads/DownloadsPanel'
export const Route = createFileRoute('/admin/downloads')({ component: DownloadsPanel })
-3
View File
@@ -151,8 +151,5 @@ export type ScheduleEntryDto = {
episodeIndex: number | null
}
// ── Загрузки (торрент) ─────────────────────────────────────────────────────
export type DownloadFileDto = { relativePath: string; name: string; sizeBytes: number }
// ── Публичный эфир ─────────────────────────────────────────────────────────
export type PublicChannelDto = { id: string; slug: string; name: string }
-22
View File
@@ -117,17 +117,6 @@ const resources = {
Failed: 'Ошибка',
},
},
downloads: {
title: 'Загрузки',
empty: 'Папка загрузок пуста. Добавьте торрент в клиенте.',
import: 'Импортировать',
imported: 'Файл импортирован, идёт обработка',
size: 'Размер',
path: 'Файл',
refresh: 'Обновить',
openClient: 'Открыть qBittorrent',
hint: 'Торрент-клиент качает в downloads/. Выберите готовый файл и импортируйте — он скопируется в библиотеку и уйдёт на обработку, торрент продолжит сидироваться.',
},
shows: {
title: 'Шоу',
name: 'Название',
@@ -296,17 +285,6 @@ const resources = {
Failed: 'Failed',
},
},
downloads: {
title: 'Downloads',
empty: 'Downloads folder is empty. Add a torrent in the client.',
import: 'Import',
imported: 'File imported, processing started',
size: 'Size',
path: 'File',
refresh: 'Refresh',
openClient: 'Open qBittorrent',
hint: 'The torrent client downloads into downloads/. Pick a finished file and import it — it is copied into the library and queued for processing, the torrent keeps seeding.',
},
shows: {
title: 'Shows',
name: 'Name',