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.WriteStartElement("tv");
|
||||||
writer.WriteAttributeString("generator-info-name", "TeleWave");
|
writer.WriteAttributeString("generator-info-name", "TeleWave");
|
||||||
|
|
||||||
|
// Сначала все каналы, потом все передачи: XMLTV требует именно такого порядка секций.
|
||||||
|
foreach (var channel in channels)
|
||||||
|
WriteChannel(writer, channel, origin);
|
||||||
|
|
||||||
foreach (var channel in channels)
|
foreach (var channel in channels)
|
||||||
{
|
{
|
||||||
writer.WriteStartElement("channel");
|
foreach (var programme in channel.Programmes)
|
||||||
writer.WriteAttributeString("id", channel.Slug);
|
WriteProgramme(writer, channel.Slug, programme, origin);
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.WriteEndElement();
|
writer.WriteEndElement();
|
||||||
@@ -200,6 +168,53 @@ public static class IptvEndpoints
|
|||||||
return output.ToString();
|
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>
|
/// <summary>Время XMLTV: <c>YYYYMMDDHHMMSS +0000</c>. Отдаём в UTC — плеер сдвинет сам.</summary>
|
||||||
private static string XmltvTime(DateTimeOffset moment) =>
|
private static string XmltvTime(DateTimeOffset moment) =>
|
||||||
moment.ToUniversalTime().ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture)
|
moment.ToUniversalTime().ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture)
|
||||||
|
|||||||
+34
-26
@@ -97,13 +97,12 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
|
|||||||
join collection in dbContext.Collections.AsNoTracking()
|
join collection in dbContext.Collections.AsNoTracking()
|
||||||
on item.CollectionId equals collection.Id
|
on item.CollectionId equals collection.Id
|
||||||
where collectionIds.Contains(item.CollectionId)
|
where collectionIds.Contains(item.CollectionId)
|
||||||
select new
|
select new CollectionPart(
|
||||||
{
|
|
||||||
item.CollectionId,
|
item.CollectionId,
|
||||||
item.ShowId,
|
item.ShowId,
|
||||||
item.Position,
|
item.Position,
|
||||||
collection.Name,
|
collection.Name
|
||||||
}
|
)
|
||||||
).ToListAsync(cancellationToken);
|
).ToListAsync(cancellationToken);
|
||||||
var partsByCollection = collectionParts
|
var partsByCollection = collectionParts
|
||||||
.GroupBy(p => p.CollectionId)
|
.GroupBy(p => p.CollectionId)
|
||||||
@@ -120,25 +119,7 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
|
|||||||
bumperText = BumperText(bumper.RenderedLinesJson);
|
bumperText = BumperText(bumper.RenderedLinesJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Номер части ищем по позиции шоу в коллекции, а не по индексу записи: франшизу могли
|
var franchise = FranchisePart(partsByCollection, e.CollectionId, e.ShowId);
|
||||||
// переупорядочить, и «часть 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dtos.Add(
|
dtos.Add(
|
||||||
new ScheduleEntryDto(
|
new ScheduleEntryDto(
|
||||||
@@ -155,9 +136,9 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
|
|||||||
: null,
|
: null,
|
||||||
bumperName,
|
bumperName,
|
||||||
bumperText,
|
bumperText,
|
||||||
part,
|
franchise?.Part,
|
||||||
parts,
|
franchise?.Total,
|
||||||
collectionName
|
franchise?.Name
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -165,6 +146,33 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext)
|
|||||||
return Result.Success<IReadOnlyList<ScheduleEntryDto>>(dtos);
|
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>
|
/// <summary>Строки сыгравшей заставки одной меткой для расписания; null — показывать нечего.</summary>
|
||||||
private static string? BumperText(string? renderedLinesJson)
|
private static string? BumperText(string? renderedLinesJson)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -102,11 +102,10 @@ public sealed class GetIptvGuideQueryHandler(IAppDbContext dbContext)
|
|||||||
// Серии одного шоу встык — это один блок программы. Склеиваем по id, а не по
|
// Серии одного шоу встык — это один блок программы. Склеиваем по id, а не по
|
||||||
// названию: у разных шоу оно может совпасть. Разрыв во времени блок рвёт — между
|
// названию: у разных шоу оно может совпасть. Разрыв во времени блок рвёт — между
|
||||||
// выходами шла реклама, и объявлять это одной передачей нельзя.
|
// выходами шла реклама, и объявлять это одной передачей нельзя.
|
||||||
if (
|
//
|
||||||
lastShowId == entry.ShowId
|
// Непустоту списка не проверяем: lastShowId выставляется только вместе с добавленной
|
||||||
&& programmes.Count > 0
|
// передачей, и совпасть с ним на пустом списке нечему.
|
||||||
&& programmes[^1].EndsAtUtc == entry.StartsAtUtc
|
if (lastShowId == entry.ShowId && programmes[^1].EndsAtUtc == entry.StartsAtUtc)
|
||||||
)
|
|
||||||
{
|
{
|
||||||
programmes[^1] = programmes[^1] with { EndsAtUtc = entry.EndsAtUtc };
|
programmes[^1] = programmes[^1] with { EndsAtUtc = entry.EndsAtUtc };
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
Reference in New Issue
Block a user