Implement episode metadata management: add functionality to refresh episode metadata from external sources, including new API endpoints and UI integration. Enhance Show and Episode models to support additional metadata fields, and update database schema accordingly. Update ShowDetail and ShowMetadataCard components to display refreshed episode information and provide user feedback on metadata updates.

This commit is contained in:
Leonid Pershin
2026-07-25 09:46:19 +03:00
parent 0d2dee815e
commit 7fb46b5e0d
30 changed files with 1563 additions and 81 deletions
@@ -22,14 +22,17 @@ public sealed class AddEpisodeCommandHandler(IAppDbContext dbContext)
if (!show.CanAddEpisode)
return Result.Failure<Guid>(ShowErrors.SingleAlreadyHasEpisode);
var assetExists = await dbContext.MediaAssets.AnyAsync(
a => a.Id == command.MediaAssetId,
cancellationToken
);
if (!assetExists)
var fileName = await dbContext.MediaAssets
.Where(a => a.Id == command.MediaAssetId)
.Select(a => a.OriginalFileName)
.FirstOrDefaultAsync(cancellationToken);
if (fileName is null)
return Result.Failure<Guid>(ShowErrors.AssetNotFound);
var episode = show.AddEpisode(command.MediaAssetId);
if (EpisodeName.Parse(fileName) is { } parsed)
episode.SetNumbers(parsed.Season, parsed.Episode);
return Result.Success(episode.Id);
}
}
@@ -17,21 +17,35 @@ public static class EpisodeName
RegexOptions.Compiled | RegexOptions.IgnoreCase
);
// Ведущий номер серии: «01. Название», «02 - Название», «03_Название» — сезон считаем первым.
private static readonly Regex LeadingNumber = new(
@"^\s*(\d{1,3})[\s._)\]-]",
RegexOptions.Compiled
);
public static (int Season, int Episode)? Parse(string? name)
{
if (string.IsNullOrEmpty(name))
return null;
var match = SxxEyy.Match(name);
if (!match.Success)
match = NxNN.Match(name);
if (match.Success)
return (Int(match, 1), Int(match, 2));
return match.Success
? (int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture),
int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture))
: null;
match = NxNN.Match(name);
if (match.Success)
return (Int(match, 1), Int(match, 2));
match = LeadingNumber.Match(name);
if (match.Success)
return (1, Int(match, 1));
return null;
}
private static int Int(Match match, int group) =>
int.Parse(match.Groups[group].Value, CultureInfo.InvariantCulture);
public static int? ParseSeason(string? name) => Parse(name)?.Season;
/// <summary>Метка вида «S16E03», либо null если распознать не удалось.</summary>
@@ -39,7 +39,13 @@ public sealed class GetShowQueryHandler(IAppDbContext dbContext)
e.Position,
asset?.OriginalFileName,
asset?.Status,
asset?.Duration?.TotalSeconds
asset?.Duration?.TotalSeconds,
e.Season,
e.Episode,
e.Title,
e.Overview,
e.StillPath is not null,
e.AirDate
);
})
.ToList();
@@ -20,7 +20,13 @@ public sealed record EpisodeDto(
int Position,
string? AssetName,
MediaAssetStatus? AssetStatus,
double? DurationSeconds
double? DurationSeconds,
int? Season,
int? Episode,
string? Title,
string? Overview,
bool HasStill,
DateOnly? AirDate
);
public sealed record ShowDto(