Refactor IPTV endpoint logic for improved readability and maintainability
Consolidated channel and programme XML writing into dedicated methods, WriteChannel and WriteProgramme, enhancing code clarity and reducing duplication. Introduced a WriteIcon method to streamline icon handling for both channels and programmes. Updated GetChannelScheduleQueryHandler to utilize a new FranchisePosition record for better representation of franchise details in schedule entries, improving the overall structure and readability of the code.
This commit is contained in:
@@ -151,46 +151,14 @@ public static class IptvEndpoints
|
||||
writer.WriteStartElement("tv");
|
||||
writer.WriteAttributeString("generator-info-name", "TeleWave");
|
||||
|
||||
// Сначала все каналы, потом все передачи: XMLTV требует именно такого порядка секций.
|
||||
foreach (var channel in channels)
|
||||
{
|
||||
writer.WriteStartElement("channel");
|
||||
writer.WriteAttributeString("id", channel.Slug);
|
||||
writer.WriteElementString("display-name", channel.Name);
|
||||
if (channel.Number is { } number)
|
||||
writer.WriteElementString(
|
||||
"display-name",
|
||||
number.ToString(CultureInfo.InvariantCulture)
|
||||
);
|
||||
if (channel.LogoImageId is { } logoId)
|
||||
{
|
||||
writer.WriteStartElement("icon");
|
||||
writer.WriteAttributeString("src", $"{origin}/api/images/{logoId}");
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
WriteChannel(writer, channel, origin);
|
||||
|
||||
foreach (var channel in channels)
|
||||
{
|
||||
foreach (var programme in channel.Programmes)
|
||||
{
|
||||
writer.WriteStartElement("programme");
|
||||
writer.WriteAttributeString("start", XmltvTime(programme.StartsAtUtc));
|
||||
writer.WriteAttributeString("stop", XmltvTime(programme.EndsAtUtc));
|
||||
writer.WriteAttributeString("channel", channel.Slug);
|
||||
|
||||
writer.WriteElementString("title", programme.Title);
|
||||
if (!string.IsNullOrWhiteSpace(programme.SubTitle))
|
||||
writer.WriteElementString("sub-title", programme.SubTitle);
|
||||
if (!string.IsNullOrWhiteSpace(programme.Description))
|
||||
writer.WriteElementString("desc", programme.Description);
|
||||
if (programme.ImageId is { } imageId)
|
||||
{
|
||||
writer.WriteStartElement("icon");
|
||||
writer.WriteAttributeString("src", $"{origin}/api/images/{imageId}");
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
writer.WriteEndElement();
|
||||
WriteProgramme(writer, channel.Slug, programme, origin);
|
||||
}
|
||||
|
||||
writer.WriteEndElement();
|
||||
@@ -200,6 +168,53 @@ public static class IptvEndpoints
|
||||
return output.ToString();
|
||||
}
|
||||
|
||||
private static void WriteChannel(XmlWriter writer, IptvChannelDto channel, string origin)
|
||||
{
|
||||
writer.WriteStartElement("channel");
|
||||
writer.WriteAttributeString("id", channel.Slug);
|
||||
writer.WriteElementString("display-name", channel.Name);
|
||||
// Номер вторым display-name: так плееры с нумерацией подхватывают его без своих настроек.
|
||||
if (channel.Number is { } number)
|
||||
writer.WriteElementString(
|
||||
"display-name",
|
||||
number.ToString(CultureInfo.InvariantCulture)
|
||||
);
|
||||
WriteIcon(writer, channel.LogoImageId, origin);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
private static void WriteProgramme(
|
||||
XmlWriter writer,
|
||||
string slug,
|
||||
IptvProgrammeDto programme,
|
||||
string origin
|
||||
)
|
||||
{
|
||||
writer.WriteStartElement("programme");
|
||||
writer.WriteAttributeString("start", XmltvTime(programme.StartsAtUtc));
|
||||
writer.WriteAttributeString("stop", XmltvTime(programme.EndsAtUtc));
|
||||
writer.WriteAttributeString("channel", slug);
|
||||
|
||||
writer.WriteElementString("title", programme.Title);
|
||||
if (!string.IsNullOrWhiteSpace(programme.SubTitle))
|
||||
writer.WriteElementString("sub-title", programme.SubTitle);
|
||||
if (!string.IsNullOrWhiteSpace(programme.Description))
|
||||
writer.WriteElementString("desc", programme.Description);
|
||||
WriteIcon(writer, programme.ImageId, origin);
|
||||
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
private static void WriteIcon(XmlWriter writer, Guid? imageId, string origin)
|
||||
{
|
||||
if (imageId is not { } id)
|
||||
return;
|
||||
|
||||
writer.WriteStartElement("icon");
|
||||
writer.WriteAttributeString("src", $"{origin}/api/images/{id}");
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
/// <summary>Время XMLTV: <c>YYYYMMDDHHMMSS +0000</c>. Отдаём в UTC — плеер сдвинет сам.</summary>
|
||||
private static string XmltvTime(DateTimeOffset moment) =>
|
||||
moment.ToUniversalTime().ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture)
|
||||
|
||||
+34
-26
@@ -97,13 +97,12 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
|
||||
join collection in dbContext.Collections.AsNoTracking()
|
||||
on item.CollectionId equals collection.Id
|
||||
where collectionIds.Contains(item.CollectionId)
|
||||
select new
|
||||
{
|
||||
select new CollectionPart(
|
||||
item.CollectionId,
|
||||
item.ShowId,
|
||||
item.Position,
|
||||
collection.Name,
|
||||
}
|
||||
collection.Name
|
||||
)
|
||||
).ToListAsync(cancellationToken);
|
||||
var partsByCollection = collectionParts
|
||||
.GroupBy(p => p.CollectionId)
|
||||
@@ -120,25 +119,7 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
|
||||
bumperText = BumperText(bumper.RenderedLinesJson);
|
||||
}
|
||||
|
||||
// Номер части ищем по позиции шоу в коллекции, а не по индексу записи: франшизу могли
|
||||
// переупорядочить, и «часть 2» обязана означать вторую в текущем составе.
|
||||
int? part = null;
|
||||
int? parts = null;
|
||||
string? collectionName = null;
|
||||
if (
|
||||
e.CollectionId is { } collectionId
|
||||
&& e.ShowId is { } showId
|
||||
&& partsByCollection.TryGetValue(collectionId, out var ordered)
|
||||
)
|
||||
{
|
||||
var at = ordered.FindIndex(p => p.ShowId == showId);
|
||||
if (at >= 0)
|
||||
{
|
||||
part = at + 1;
|
||||
parts = ordered.Count;
|
||||
collectionName = ordered[0].Name;
|
||||
}
|
||||
}
|
||||
var franchise = FranchisePart(partsByCollection, e.CollectionId, e.ShowId);
|
||||
|
||||
dtos.Add(
|
||||
new ScheduleEntryDto(
|
||||
@@ -155,9 +136,9 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
|
||||
: null,
|
||||
bumperName,
|
||||
bumperText,
|
||||
part,
|
||||
parts,
|
||||
collectionName
|
||||
franchise?.Part,
|
||||
franchise?.Total,
|
||||
franchise?.Name
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -165,6 +146,33 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
|
||||
return Result.Success<IReadOnlyList<ScheduleEntryDto>>(dtos);
|
||||
}
|
||||
|
||||
/// <summary>Место записи во франшизе: «часть N из M» и её название.</summary>
|
||||
private sealed record FranchisePosition(int Part, int Total, string Name);
|
||||
|
||||
/// <summary>
|
||||
/// Какой частью коллекции шла запись. Номер ищется по позиции шоу в текущем составе, а не по
|
||||
/// индексу записи: франшизу могли переупорядочить, и «часть 2» обязана означать вторую сейчас.
|
||||
/// </summary>
|
||||
private static FranchisePosition? FranchisePart(
|
||||
IReadOnlyDictionary<Guid, List<CollectionPart>> partsByCollection,
|
||||
Guid? collectionId,
|
||||
Guid? showId
|
||||
)
|
||||
{
|
||||
if (
|
||||
collectionId is not { } id
|
||||
|| showId is not { } show
|
||||
|| !partsByCollection.TryGetValue(id, out var ordered)
|
||||
)
|
||||
return null;
|
||||
|
||||
var at = ordered.FindIndex(p => p.ShowId == show);
|
||||
return at < 0 ? null : new FranchisePosition(at + 1, ordered.Count, ordered[0].Name);
|
||||
}
|
||||
|
||||
/// <summary>Позиция шоу в коллекции вместе с её названием — то, чем считается номер части.</summary>
|
||||
private sealed record CollectionPart(Guid CollectionId, Guid ShowId, int Position, string Name);
|
||||
|
||||
/// <summary>Строки сыгравшей заставки одной меткой для расписания; null — показывать нечего.</summary>
|
||||
private static string? BumperText(string? renderedLinesJson)
|
||||
{
|
||||
|
||||
@@ -102,11 +102,10 @@ public sealed class GetIptvGuideQueryHandler(IAppDbContext dbContext)
|
||||
// Серии одного шоу встык — это один блок программы. Склеиваем по id, а не по
|
||||
// названию: у разных шоу оно может совпасть. Разрыв во времени блок рвёт — между
|
||||
// выходами шла реклама, и объявлять это одной передачей нельзя.
|
||||
if (
|
||||
lastShowId == entry.ShowId
|
||||
&& programmes.Count > 0
|
||||
&& programmes[^1].EndsAtUtc == entry.StartsAtUtc
|
||||
)
|
||||
//
|
||||
// Непустоту списка не проверяем: lastShowId выставляется только вместе с добавленной
|
||||
// передачей, и совпасть с ним на пустом списке нечему.
|
||||
if (lastShowId == entry.ShowId && programmes[^1].EndsAtUtc == entry.StartsAtUtc)
|
||||
{
|
||||
programmes[^1] = programmes[^1] with { EndsAtUtc = entry.EndsAtUtc };
|
||||
continue;
|
||||
|
||||
Reference in New Issue
Block a user