Refactor .gitignore to streamline ignored files and enhance clarity. Update CLAUDE.md to improve unit test instructions and add coverage reporting details. Revise README.md for better project overview and deployment instructions. Refactor ChannelEndpoints and StreamingEndpoints to utilize SegmentFiles for file resolution, improving code maintainability. Remove unused JunctionHandlers and update DependencyInjection for cleaner service registration. Enhance media processing services for better job handling and error management. Update frontend API types for consistency and clarity.
build / backend (push) Successful in 1m28s
build / frontend (push) Failing after 31s
tests / backend-tests (push) Canceled after 0s
sonar / analyze (push) Successful in 4m39s

This commit is contained in:
Leonid Pershin
2026-07-26 20:43:38 +03:00
parent f36dbfa9cb
commit 205672b77d
77 changed files with 3292 additions and 3102 deletions
@@ -3,8 +3,9 @@ import { ChevronDown, ChevronRight, Folder } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { listShows } from '@/features/admin/shows/api'
import { HttpError } from '@/shared/api/client'
import { qk } from '@/shared/api/query-keys'
import type { ManualInboxFileDto } from '@/shared/api/types'
import { useApiError } from '@/shared/lib/use-api-error'
import { Badge } from '@/shared/ui/badge'
import { Button } from '@/shared/ui/button'
import {
@@ -57,10 +58,10 @@ export function ManualInboxDialog({ onClose }: { onClose: () => void }) {
const [collapsed, setCollapsed] = useState<string[]>([])
const { data, isLoading } = useQuery({
queryKey: ['admin', 'media', 'manual'],
queryKey: qk.media.manual,
queryFn: listManualInbox,
})
const { data: shows } = useQuery({ queryKey: ['admin', 'shows'], queryFn: () => listShows() })
const { data: shows } = useQuery({ queryKey: qk.shows.all, queryFn: () => listShows() })
const regexOk = isValidRegex(regexStr)
const seasonOverride = seasonStr.trim() ? Number(seasonStr) : null
@@ -146,6 +147,8 @@ export function ManualInboxDialog({ onClose }: { onClose: () => void }) {
(f) => parsedByPath.get(f.relativePath)?.episode != null,
).length
const onError = useApiError()
const importMutation = useMutation({
mutationFn: () =>
importManualInbox(
@@ -167,12 +170,11 @@ export function ManualInboxDialog({ onClose }: { onClose: () => void }) {
toast.error(`${failure.relativePath}: ${failure.reason}`)
setSelected([])
void queryClient.invalidateQueries({ queryKey: ['admin', 'media'] })
void queryClient.invalidateQueries({ queryKey: ['admin', 'shows'] })
void queryClient.invalidateQueries({ queryKey: qk.media.all })
void queryClient.invalidateQueries({ queryKey: qk.shows.all })
if (result.failed.length === 0) onClose()
},
onError: (error: unknown) =>
toast.error(error instanceof HttpError ? error.detail : t('common.error')),
onError,
})
const toggle = (path: string) =>
@@ -2,14 +2,14 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FolderInput, ListPlus, Upload } from 'lucide-react'
import { HttpError } from '@/shared/api/client'
import { qk } from '@/shared/api/query-keys'
import type { MediaAssetDto, MediaAssetStatus } from '@/shared/api/types'
import { useApiError } from '@/shared/lib/use-api-error'
import { Badge, type BadgeProps } from '@/shared/ui/badge'
import { Button } from '@/shared/ui/button'
import { Pager } from '@/shared/ui/pager'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/ui/select'
import { SortHeader, useTableSort } from '@/shared/ui/sortable'
import { toast } from '@/shared/ui/toast-store'
import { deleteMedia, getMediaStats, listMedia } from './api'
import { ManualInboxDialog } from './ManualInboxDialog'
import { UploadToShowDialog } from './UploadToShowDialog'
@@ -63,7 +63,7 @@ export function MediaPanel() {
}
const { data, isLoading, refetch } = useQuery({
queryKey: ['admin', 'media', filter, page, sort.key, sort.desc],
queryKey: qk.media.list(filter, page, sort.key, sort.desc),
queryFn: () =>
listMedia({
page,
@@ -80,7 +80,7 @@ export function MediaPanel() {
})
const { data: stats } = useQuery({
queryKey: ['admin', 'media', 'stats'],
queryKey: qk.media.stats,
queryFn: getMediaStats,
// Пока есть незавершённая работа — освежаем чипы очереди/обработки.
refetchInterval: (query) =>
@@ -101,9 +101,8 @@ export function MediaPanel() {
void refetch()
}, [activity, refetch])
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['admin', 'media'] })
const onError = (error: unknown) =>
toast.error(error instanceof HttpError ? error.detail : t('common.error'))
const invalidate = () => queryClient.invalidateQueries({ queryKey: qk.media.all })
const onError = useApiError()
const deleteMutation = useMutation({ mutationFn: deleteMedia, onSuccess: invalidate, onError })
@@ -1,6 +1,7 @@
import { useQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { qk } from '@/shared/api/query-keys'
import { Badge } from '@/shared/ui/badge'
import { Button } from '@/shared/ui/button'
import {
@@ -30,7 +31,7 @@ export function UploadToShowDialog({ files, onClose }: { files: File[]; onClose:
// Ручные правки привязки: имя файла → id шоу ('' — явно в библиотеку). Иначе берётся автоопределение.
const [overrides, setOverrides] = useState<Record<string, string>>({})
const { data: shows } = useQuery({ queryKey: ['admin', 'shows'], queryFn: () => listShows() })
const { data: shows } = useQuery({ queryKey: qk.shows.all, queryFn: () => listShows() })
const regexOk = isValidRegex(regexStr)
const seasonOverride = seasonStr.trim() ? Number(seasonStr) : null
+1 -1
View File
@@ -10,7 +10,7 @@ import type {
PagedList,
} from '@/shared/api/types'
export type ListMediaParams = {
type ListMediaParams = {
page: number
pageSize: number
statuses?: MediaAssetStatus[]
@@ -1,4 +1,4 @@
export type ParseOptions = {
type ParseOptions = {
/** Ручной сезон — перебивает распознанный/дефолтный. */
seasonOverride?: number | null
/** Пользовательский regex для номера серии: 1 группа = серия, 2 группы = (сезон, серия). */
@@ -5,7 +5,7 @@
* («Star Trek Discovery» важнее «Star Trek»).
*/
export type ShowNameRef = { id: string; name: string; originalName?: string | null }
type ShowNameRef = { id: string; name: string; originalName?: string | null }
/** Приводит строку к «словам через пробел»: буквы/цифры сохраняем, всё прочее — разделитель. */
function normalize(value: string): string {
@@ -1,4 +1,5 @@
import { create } from 'zustand'
import { qk } from '@/shared/api/query-keys'
import { HttpError, refreshAccessToken } from '@/shared/api/client'
import { queryClient } from '@/shared/api/query-client'
import { importInterstitials } from '@/features/admin/interstitials/api'
@@ -31,7 +32,7 @@ type UploadStore = {
* <c>showId</c> — общий для всех файлов; <c>resolveShowId</c> — привязка на каждый файл (напр.
* автоопределение шоу по имени релиза). Приоритет у <c>resolveShowId</c>, затем общий <c>showId</c>.
*/
export type EnqueueOptions = {
type EnqueueOptions = {
showId?: string
resolveShowId?: (file: File) => string | undefined
/** Загрузка с экрана «Ролики»: каждый файл после аплоада становится роликом (Show + серия). */
@@ -105,13 +106,13 @@ async function pump() {
if (created) {
patch(job.id, { status: 'done', percent: 100 })
void queryClient.invalidateQueries({ queryKey: ['admin', 'media'] })
void queryClient.invalidateQueries({ queryKey: qk.media.all })
// Привязка к шоу: добавляем свежий ассет серией (порядок — как в очереди).
if (job.showId) {
try {
await addEpisode(job.showId, created.id)
void queryClient.invalidateQueries({ queryKey: ['admin', 'shows'] })
void queryClient.invalidateQueries({ queryKey: qk.shows.all })
} catch {
toast.error(`${job.file.name}: не удалось добавить в шоу`)
}
@@ -119,7 +120,7 @@ async function pump() {
// Ролик заводится сразу после аплоада: длительность подтянется, когда ассет обработается.
try {
await importInterstitials([created.id])
void queryClient.invalidateQueries({ queryKey: ['admin', 'interstitials'] })
void queryClient.invalidateQueries({ queryKey: qk.interstitials.all })
} catch {
toast.error(`${job.file.name}: не удалось завести ролик`)
}