Implement collection suggestions and bulk show addition features
ci / build-backend (push) Successful in 2m41s
ci / build-frontend (push) Successful in 44s
ci / tests (push) Successful in 2m33s
ci / sonar (push) Successful in 5m12s

Added new API endpoints for suggesting collections based on existing shows and for bulk adding shows to collections. Enhanced the backend with necessary logic and DTOs to support these features. Updated the frontend to include new components for displaying collection suggestions and managing bulk additions, improving the user experience for collection management. Localization updates were made to support these new features in both English and Russian.
This commit is contained in:
Leonid Pershin
2026-07-27 05:39:23 +03:00
parent 1a7f73a5bd
commit 5ad28b2966
39 changed files with 2710 additions and 10 deletions
+3
View File
@@ -34,6 +34,8 @@ export const qk = {
all: ['admin', 'shows'] as const,
byGenre: (genreId: string | null) => ['admin', 'shows', { genreId }] as const,
detail: (id: string) => ['admin', 'shows', id] as const,
/** Где шоу задействовано: группы, коллекции, каналы. */
usage: (id: string) => ['admin', 'shows', id, 'usage'] as const,
},
groups: {
@@ -47,6 +49,7 @@ export const qk = {
collections: {
all: ['admin', 'collections'] as const,
detail: (id: string) => ['admin', 'collections', id] as const,
suggestions: ['admin', 'collections', 'suggestions'] as const,
},
genres: {
+26
View File
@@ -160,6 +160,20 @@ export type ShowSummaryDto = {
otherGenres: string[]
}
/** Где шоу задействовано — показывается на его карточке. */
export type ShowUsageDto = {
groups: ShowUsageRefDto[]
collections: ShowUsageRefDto[]
channels: ShowUsageRefDto[]
}
type ShowUsageRefDto = {
id: string
name: string
/** Шоу попало в группу правилом, а не явной позицией. */
viaRule: boolean
}
type ShowGenreDto = {
id: string
name: string
@@ -218,6 +232,18 @@ export type CollectionDto = {
items: CollectionItemDto[]
}
/** Откуда взялось предложение франшизы: данные источника либо догадка по названиям. */
export type CollectionSuggestionSource = 'Metadata' | 'NameGuess'
/** Предложение собрать франшизу: имя и части в предлагаемом порядке. */
export type CollectionSuggestionDto = {
key: string
source: CollectionSuggestionSource
name: string
parts: { showId: string; name: string; year: number | null }[]
alreadyExists: boolean
}
/** Коллекция, в которую входит шоу — для блока на экране шоу. */
type ShowCollectionRefDto = {
id: string
+18
View File
@@ -116,6 +116,7 @@ export const en = {
restore: 'Restore',
dynamicOrderHint:
'Order: pinned items first (drag to arrange), then rule matches alphabetically.',
previewComposition: 'With this rule the composition would be: {{count}}',
pendingFound: 'New matches for this rule: {{count}} —',
pendingAdd: 'Add all ({{count}})',
elementKinds: { Show: 'Show', Collection: 'Collection' },
@@ -165,6 +166,14 @@ export const en = {
pickShow: 'Pick a show',
empty: 'Collection is empty',
orderHint: 'Drag to set the order of the parts — that is the order they air in.',
suggestions: {
title: 'Franchise suggestions',
hint: 'Built from movies not in any collection yet. Check the parts — the name-based guess sometimes misses.',
parts: '{{count}} parts',
exists: 'already exists',
created: 'Collection created',
sources: { Metadata: 'from metadata', NameGuess: 'by names' },
},
},
interstitials: {
title: 'Clips',
@@ -281,6 +290,7 @@ export const en = {
uploadingCount: 'Uploading {{done}}/{{total}}',
cancelAll: 'Cancel all uploads',
skippedDuplicates: 'Skipped duplicates: {{count}}',
queueDrained: 'Processing finished — the queue is empty',
filterActive: 'Active',
filterAll: 'All',
name: 'File',
@@ -363,12 +373,20 @@ export const en = {
enriching: 'Searching…',
enriched: 'Metadata applied: {{count}}',
moreSkipped: 'Skipped as well: {{count}}',
toCollection: 'To collection…',
toCollectionHint:
'The selected shows go into a franchise. Parts are ordered by release year; drag to rearrange them in the collection card.',
newCollection: 'New collection',
addToCollection: 'Add ({{count}})',
addedToCollection: '“{{name}}” updated: {{count}}',
},
genresEmpty: 'No genres set',
genresEdit: 'Genres',
genresHint:
'Pick the genres of the show. The primary one is listed; all of them are used for selection.',
genrePrimary: 'primary',
inGroups: 'In groups',
inChannels: 'Scheduled on',
inCollections: 'Part of collections',
episodes: 'Episodes',
episode: 'Episode',
+18
View File
@@ -116,6 +116,7 @@ export const ru = {
restore: 'Вернуть',
dynamicOrderHint:
'Порядок: сначала закреплённое (перетаскиванием), затем найденное правилом по алфавиту.',
previewComposition: 'При этом правиле в составе будет: {{count}}',
pendingFound: 'Под правило группы подходит нового: {{count}} —',
pendingAdd: 'Добавить все ({{count}})',
elementKinds: { Show: 'Шоу', Collection: 'Коллекция' },
@@ -165,6 +166,14 @@ export const ru = {
pickShow: 'Выберите шоу',
empty: 'Коллекция пуста',
orderHint: 'Порядок частей задаётся перетаскиванием — в нём они и пойдут в эфир.',
suggestions: {
title: 'Предложения франшиз',
hint: 'Собрано из полнометражек, не входящих ни в одну коллекцию. Проверьте состав — по названиям догадка иногда промахивается.',
parts: '{{count}} частей',
exists: 'уже есть',
created: 'Коллекция создана',
sources: { Metadata: 'из метаданных', NameGuess: 'по названиям' },
},
},
interstitials: {
title: 'Ролики',
@@ -282,6 +291,7 @@ export const ru = {
uploadingCount: 'Загрузка {{done}}/{{total}}',
cancelAll: 'Отменить все загрузки',
skippedDuplicates: 'Пропущено дубликатов: {{count}}',
queueDrained: 'Обработка завершена — очередь пуста',
filterActive: 'Активные',
filterAll: 'Все',
name: 'Файл',
@@ -364,12 +374,20 @@ export const ru = {
enriching: 'Ищем…',
enriched: 'Метаданные применены: {{count}}',
moreSkipped: 'Ещё пропущено: {{count}}',
toCollection: 'В коллекцию…',
toCollectionHint:
'Отмеченные шоу уйдут во франшизу. Порядок частей — по году выпуска; поправить его можно перетаскиванием в карточке коллекции.',
newCollection: 'Новая коллекция',
addToCollection: 'Добавить ({{count}})',
addedToCollection: '«{{name}}» пополнена: {{count}}',
},
genresEmpty: 'Жанры не проставлены',
genresEdit: 'Жанры',
genresHint:
'Отметьте жанры шоу. Основной показывается в списке; в отборе контента участвуют все.',
genrePrimary: 'основной',
inGroups: 'В группах',
inChannels: 'В эфире каналов',
inCollections: 'Входит в коллекции',
episodes: 'Серии',
episode: 'Серия',