Normalize line endings to LF via .gitattributes
Репозиторий хранил фронтенд в CRLF, а часть бэкенда — вперемешку, хотя CI и Docker-сборка работают под Linux. Прибиваем LF атрибутом `* text=auto eol=lf` и разово нормализуем дерево, чтобы форматтеры не переписывали файлы целиком на каждом прогоне. Коммит чисто механический: изменений содержимого нет, только концы строк. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9d1c6d2fc3
commit
0442056367
@@ -1,176 +1,176 @@
|
||||
namespace TeleWave.Domain.Library;
|
||||
|
||||
/// <summary>
|
||||
/// Переиспользуемое шоу в общей библиотеке: сериал (упорядоченные серии) либо полнометражка.
|
||||
/// Серии идут строго в порядке <see cref="ShowEpisode.Position"/>; где остановился показ — знает
|
||||
/// состояние слота планировщика (<c>Programming/SlotState</c>), а не само шоу: одно шоу играет
|
||||
/// на нескольких каналах и в нескольких слотах, и курсор у каждого свой.
|
||||
/// </summary>
|
||||
public class Show
|
||||
{
|
||||
private readonly List<ShowEpisode> _episodes = [];
|
||||
private readonly List<ShowGenre> _genres = [];
|
||||
|
||||
public Guid Id { get; private set; }
|
||||
public string Name { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>Оригинальное название (обычно на английском) — по нему ищутся метаданные; на экранах
|
||||
/// продолжаем показывать <see cref="Name"/>. Null/пусто — ищем по <see cref="Name"/>.</summary>
|
||||
public string? OriginalName { get; private set; }
|
||||
|
||||
public string? Description { get; private set; }
|
||||
public ShowKind Kind { get; private set; }
|
||||
|
||||
/// <summary>Возрастной рейтинг (MPAA) или null, если не проставлен ни источником, ни вручную.
|
||||
/// Шоу без рейтинга планировщик не отсекает: неизвестное не значит «взрослое».</summary>
|
||||
public ShowAudience? Audience { get; private set; }
|
||||
|
||||
public DateTimeOffset CreatedAt { get; private set; }
|
||||
|
||||
// ── Метаданные (TMDb/OMDb/вручную) ──
|
||||
/// <summary>Источник метаданных: «tmdb»/«omdb»/«manual» или null, если не заданы.</summary>
|
||||
public string? MetadataProvider { get; private set; }
|
||||
|
||||
/// <summary>Идентификатор шоу во внешнем источнике (для довыгрузки серий).</summary>
|
||||
public string? MetadataExternalId { get; private set; }
|
||||
|
||||
public int? Year { get; private set; }
|
||||
|
||||
/// <summary>Постер шоу — ссылка на запись общего реестра изображений (<c>Domain/Images</c>) или null.</summary>
|
||||
public Guid? PosterImageId { get; private set; }
|
||||
|
||||
/// <summary>Серии шоу (backing-field для EF). Порядок показа — по <see cref="ShowEpisode.Position"/>;
|
||||
/// потребители сортируют явно (см. загрузчик планировщика).</summary>
|
||||
public IReadOnlyList<ShowEpisode> Episodes => _episodes;
|
||||
|
||||
/// <summary>Жанры шоу (backing-field для EF). Ровно один помечен основным, если список не пуст.</summary>
|
||||
public IReadOnlyList<ShowGenre> Genres => _genres;
|
||||
|
||||
private Show() { }
|
||||
|
||||
public static Show Create(
|
||||
string name,
|
||||
ShowKind kind,
|
||||
string? description = null,
|
||||
string? originalName = null,
|
||||
ShowAudience? audience = null
|
||||
) =>
|
||||
new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = name,
|
||||
OriginalName = Normalize(originalName),
|
||||
Kind = kind,
|
||||
Description = description,
|
||||
Audience = audience,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
/// <summary>Задать возрастной рейтинг; null — снять (вернуть в «не проставлен»).</summary>
|
||||
public void SetAudience(ShowAudience? audience) => Audience = audience;
|
||||
|
||||
/// <summary>Основной жанр или null, если жанры не проставлены.</summary>
|
||||
public Guid? PrimaryGenreId => _genres.FirstOrDefault(g => g.IsPrimary)?.GenreId;
|
||||
|
||||
/// <summary>
|
||||
/// Полностью заменяет набор жанров; дубликаты и пустые идентификаторы отбрасываются. Основным
|
||||
/// становится <paramref name="primaryGenreId"/>, если он попал в набор, иначе первый в списке —
|
||||
/// так шоу с жанрами никогда не остаётся без основного.
|
||||
/// </summary>
|
||||
public void SetGenres(IEnumerable<Guid> genreIds, Guid? primaryGenreId = null)
|
||||
{
|
||||
var ids = genreIds.Where(id => id != Guid.Empty).Distinct().ToList();
|
||||
_genres.Clear();
|
||||
if (ids.Count == 0)
|
||||
return;
|
||||
|
||||
var primary =
|
||||
primaryGenreId is { } candidate && ids.Contains(candidate) ? candidate : ids[0];
|
||||
foreach (var id in ids)
|
||||
_genres.Add(ShowGenre.Create(Id, id, id == primary));
|
||||
}
|
||||
|
||||
public void Rename(string name, string? description)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
}
|
||||
|
||||
/// <summary>Изменить отображаемое название (на экранах). Метаданные ищутся по <see cref="OriginalName"/>.</summary>
|
||||
public void SetName(string name) => Name = name;
|
||||
|
||||
/// <summary>Задать/снять оригинальное название (пустая строка трактуется как отсутствие).</summary>
|
||||
public void SetOriginalName(string? originalName) => OriginalName = Normalize(originalName);
|
||||
|
||||
private static string? Normalize(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
|
||||
/// <summary>Добавляет серию в конец. Для <see cref="ShowKind.Single"/> допустима ровно одна серия
|
||||
/// (инвариант защищён самим агрегатом; вызывающий обычно проверяет <see cref="CanAddEpisode"/> заранее
|
||||
/// и возвращает управляемую ошибку — исключение здесь лишь страховка от обхода).</summary>
|
||||
public ShowEpisode AddEpisode(Guid mediaAssetId)
|
||||
{
|
||||
if (!CanAddEpisode)
|
||||
throw new InvalidOperationException(
|
||||
"Только сериал может содержать больше одной серии."
|
||||
);
|
||||
|
||||
var nextPosition = _episodes.Count == 0 ? 0 : _episodes.Max(e => e.Position) + 1;
|
||||
var episode = ShowEpisode.Create(Id, mediaAssetId, nextPosition);
|
||||
_episodes.Add(episode);
|
||||
return episode;
|
||||
}
|
||||
|
||||
public bool RemoveEpisode(Guid episodeId)
|
||||
{
|
||||
var episode = _episodes.FirstOrDefault(e => e.Id == episodeId);
|
||||
if (episode is null)
|
||||
return false;
|
||||
_episodes.Remove(episode);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Несколько серий бывает только у сериала. У полнометражки и у ролика-врезки серия ровно
|
||||
/// одна: ролик с тремя сериями вёл бы себя в планировщике как мини-сериал, а задуман как единица.</summary>
|
||||
public bool CanAddEpisode => Kind == ShowKind.Series || _episodes.Count == 0;
|
||||
|
||||
/// <summary>Применить метаданные из внешнего источника. Постер (уже зарегистрирован в реестре) может быть null.</summary>
|
||||
public void ApplyMetadata(
|
||||
string provider,
|
||||
string externalId,
|
||||
string? description,
|
||||
int? year,
|
||||
Guid? posterImageId
|
||||
)
|
||||
{
|
||||
MetadataProvider = provider;
|
||||
MetadataExternalId = externalId;
|
||||
if (!string.IsNullOrWhiteSpace(description))
|
||||
Description = description;
|
||||
Year = year;
|
||||
if (posterImageId is not null)
|
||||
PosterImageId = posterImageId;
|
||||
}
|
||||
|
||||
/// <summary>Ручная правка метаданных (без внешнего источника).</summary>
|
||||
public void UpdateMetadataManual(string? description, int? year)
|
||||
{
|
||||
MetadataProvider = "manual";
|
||||
MetadataExternalId = null;
|
||||
Description = description;
|
||||
Year = year;
|
||||
}
|
||||
|
||||
/// <summary>Привязать/снять постер шоу (ссылка на запись реестра изображений).</summary>
|
||||
public void SetPosterImage(Guid? imageId) => PosterImageId = imageId;
|
||||
|
||||
/// <summary>Сбросить все метаданные и отвязать постер (сама картинка остаётся в галерее).</summary>
|
||||
public void ClearMetadata()
|
||||
{
|
||||
MetadataProvider = null;
|
||||
MetadataExternalId = null;
|
||||
Year = null;
|
||||
PosterImageId = null;
|
||||
Description = null;
|
||||
}
|
||||
}
|
||||
namespace TeleWave.Domain.Library;
|
||||
|
||||
/// <summary>
|
||||
/// Переиспользуемое шоу в общей библиотеке: сериал (упорядоченные серии) либо полнометражка.
|
||||
/// Серии идут строго в порядке <see cref="ShowEpisode.Position"/>; где остановился показ — знает
|
||||
/// состояние слота планировщика (<c>Programming/SlotState</c>), а не само шоу: одно шоу играет
|
||||
/// на нескольких каналах и в нескольких слотах, и курсор у каждого свой.
|
||||
/// </summary>
|
||||
public class Show
|
||||
{
|
||||
private readonly List<ShowEpisode> _episodes = [];
|
||||
private readonly List<ShowGenre> _genres = [];
|
||||
|
||||
public Guid Id { get; private set; }
|
||||
public string Name { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>Оригинальное название (обычно на английском) — по нему ищутся метаданные; на экранах
|
||||
/// продолжаем показывать <see cref="Name"/>. Null/пусто — ищем по <see cref="Name"/>.</summary>
|
||||
public string? OriginalName { get; private set; }
|
||||
|
||||
public string? Description { get; private set; }
|
||||
public ShowKind Kind { get; private set; }
|
||||
|
||||
/// <summary>Возрастной рейтинг (MPAA) или null, если не проставлен ни источником, ни вручную.
|
||||
/// Шоу без рейтинга планировщик не отсекает: неизвестное не значит «взрослое».</summary>
|
||||
public ShowAudience? Audience { get; private set; }
|
||||
|
||||
public DateTimeOffset CreatedAt { get; private set; }
|
||||
|
||||
// ── Метаданные (TMDb/OMDb/вручную) ──
|
||||
/// <summary>Источник метаданных: «tmdb»/«omdb»/«manual» или null, если не заданы.</summary>
|
||||
public string? MetadataProvider { get; private set; }
|
||||
|
||||
/// <summary>Идентификатор шоу во внешнем источнике (для довыгрузки серий).</summary>
|
||||
public string? MetadataExternalId { get; private set; }
|
||||
|
||||
public int? Year { get; private set; }
|
||||
|
||||
/// <summary>Постер шоу — ссылка на запись общего реестра изображений (<c>Domain/Images</c>) или null.</summary>
|
||||
public Guid? PosterImageId { get; private set; }
|
||||
|
||||
/// <summary>Серии шоу (backing-field для EF). Порядок показа — по <see cref="ShowEpisode.Position"/>;
|
||||
/// потребители сортируют явно (см. загрузчик планировщика).</summary>
|
||||
public IReadOnlyList<ShowEpisode> Episodes => _episodes;
|
||||
|
||||
/// <summary>Жанры шоу (backing-field для EF). Ровно один помечен основным, если список не пуст.</summary>
|
||||
public IReadOnlyList<ShowGenre> Genres => _genres;
|
||||
|
||||
private Show() { }
|
||||
|
||||
public static Show Create(
|
||||
string name,
|
||||
ShowKind kind,
|
||||
string? description = null,
|
||||
string? originalName = null,
|
||||
ShowAudience? audience = null
|
||||
) =>
|
||||
new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = name,
|
||||
OriginalName = Normalize(originalName),
|
||||
Kind = kind,
|
||||
Description = description,
|
||||
Audience = audience,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
/// <summary>Задать возрастной рейтинг; null — снять (вернуть в «не проставлен»).</summary>
|
||||
public void SetAudience(ShowAudience? audience) => Audience = audience;
|
||||
|
||||
/// <summary>Основной жанр или null, если жанры не проставлены.</summary>
|
||||
public Guid? PrimaryGenreId => _genres.FirstOrDefault(g => g.IsPrimary)?.GenreId;
|
||||
|
||||
/// <summary>
|
||||
/// Полностью заменяет набор жанров; дубликаты и пустые идентификаторы отбрасываются. Основным
|
||||
/// становится <paramref name="primaryGenreId"/>, если он попал в набор, иначе первый в списке —
|
||||
/// так шоу с жанрами никогда не остаётся без основного.
|
||||
/// </summary>
|
||||
public void SetGenres(IEnumerable<Guid> genreIds, Guid? primaryGenreId = null)
|
||||
{
|
||||
var ids = genreIds.Where(id => id != Guid.Empty).Distinct().ToList();
|
||||
_genres.Clear();
|
||||
if (ids.Count == 0)
|
||||
return;
|
||||
|
||||
var primary =
|
||||
primaryGenreId is { } candidate && ids.Contains(candidate) ? candidate : ids[0];
|
||||
foreach (var id in ids)
|
||||
_genres.Add(ShowGenre.Create(Id, id, id == primary));
|
||||
}
|
||||
|
||||
public void Rename(string name, string? description)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
}
|
||||
|
||||
/// <summary>Изменить отображаемое название (на экранах). Метаданные ищутся по <see cref="OriginalName"/>.</summary>
|
||||
public void SetName(string name) => Name = name;
|
||||
|
||||
/// <summary>Задать/снять оригинальное название (пустая строка трактуется как отсутствие).</summary>
|
||||
public void SetOriginalName(string? originalName) => OriginalName = Normalize(originalName);
|
||||
|
||||
private static string? Normalize(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
|
||||
/// <summary>Добавляет серию в конец. Для <see cref="ShowKind.Single"/> допустима ровно одна серия
|
||||
/// (инвариант защищён самим агрегатом; вызывающий обычно проверяет <see cref="CanAddEpisode"/> заранее
|
||||
/// и возвращает управляемую ошибку — исключение здесь лишь страховка от обхода).</summary>
|
||||
public ShowEpisode AddEpisode(Guid mediaAssetId)
|
||||
{
|
||||
if (!CanAddEpisode)
|
||||
throw new InvalidOperationException(
|
||||
"Только сериал может содержать больше одной серии."
|
||||
);
|
||||
|
||||
var nextPosition = _episodes.Count == 0 ? 0 : _episodes.Max(e => e.Position) + 1;
|
||||
var episode = ShowEpisode.Create(Id, mediaAssetId, nextPosition);
|
||||
_episodes.Add(episode);
|
||||
return episode;
|
||||
}
|
||||
|
||||
public bool RemoveEpisode(Guid episodeId)
|
||||
{
|
||||
var episode = _episodes.FirstOrDefault(e => e.Id == episodeId);
|
||||
if (episode is null)
|
||||
return false;
|
||||
_episodes.Remove(episode);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Несколько серий бывает только у сериала. У полнометражки и у ролика-врезки серия ровно
|
||||
/// одна: ролик с тремя сериями вёл бы себя в планировщике как мини-сериал, а задуман как единица.</summary>
|
||||
public bool CanAddEpisode => Kind == ShowKind.Series || _episodes.Count == 0;
|
||||
|
||||
/// <summary>Применить метаданные из внешнего источника. Постер (уже зарегистрирован в реестре) может быть null.</summary>
|
||||
public void ApplyMetadata(
|
||||
string provider,
|
||||
string externalId,
|
||||
string? description,
|
||||
int? year,
|
||||
Guid? posterImageId
|
||||
)
|
||||
{
|
||||
MetadataProvider = provider;
|
||||
MetadataExternalId = externalId;
|
||||
if (!string.IsNullOrWhiteSpace(description))
|
||||
Description = description;
|
||||
Year = year;
|
||||
if (posterImageId is not null)
|
||||
PosterImageId = posterImageId;
|
||||
}
|
||||
|
||||
/// <summary>Ручная правка метаданных (без внешнего источника).</summary>
|
||||
public void UpdateMetadataManual(string? description, int? year)
|
||||
{
|
||||
MetadataProvider = "manual";
|
||||
MetadataExternalId = null;
|
||||
Description = description;
|
||||
Year = year;
|
||||
}
|
||||
|
||||
/// <summary>Привязать/снять постер шоу (ссылка на запись реестра изображений).</summary>
|
||||
public void SetPosterImage(Guid? imageId) => PosterImageId = imageId;
|
||||
|
||||
/// <summary>Сбросить все метаданные и отвязать постер (сама картинка остаётся в галерее).</summary>
|
||||
public void ClearMetadata()
|
||||
{
|
||||
MetadataProvider = null;
|
||||
MetadataExternalId = null;
|
||||
Year = null;
|
||||
PosterImageId = null;
|
||||
Description = null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user