|
@@ -10,16 +10,15 @@ using System.Globalization;
|
|
|
using System.IO;
|
|
|
using System.Linq;
|
|
|
using System.Net.Http;
|
|
|
-using System.Text;
|
|
|
using System.Threading;
|
|
|
using System.Threading.Tasks;
|
|
|
-using System.Xml;
|
|
|
using AsyncKeyedLock;
|
|
|
using Jellyfin.Data.Enums;
|
|
|
using Jellyfin.Data.Events;
|
|
|
using Jellyfin.Extensions;
|
|
|
using Jellyfin.LiveTv.Configuration;
|
|
|
using Jellyfin.LiveTv.IO;
|
|
|
+using Jellyfin.LiveTv.Recordings;
|
|
|
using Jellyfin.LiveTv.Timers;
|
|
|
using MediaBrowser.Common.Configuration;
|
|
|
using MediaBrowser.Common.Extensions;
|
|
@@ -44,8 +43,6 @@ namespace Jellyfin.LiveTv.EmbyTV
|
|
|
{
|
|
|
public sealed class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable
|
|
|
{
|
|
|
- public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
|
|
|
-
|
|
|
private readonly ILogger<EmbyTV> _logger;
|
|
|
private readonly IHttpClientFactory _httpClientFactory;
|
|
|
private readonly IServerConfigurationManager _config;
|
|
@@ -61,6 +58,7 @@ namespace Jellyfin.LiveTv.EmbyTV
|
|
|
private readonly LiveTvDtoService _tvDtoService;
|
|
|
private readonly TimerManager _timerManager;
|
|
|
private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerManager;
|
|
|
+ private readonly RecordingsMetadataManager _recordingsMetadataManager;
|
|
|
|
|
|
private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
|
|
|
new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
|
|
@@ -84,7 +82,8 @@ namespace Jellyfin.LiveTv.EmbyTV
|
|
|
IListingsManager listingsManager,
|
|
|
LiveTvDtoService tvDtoService,
|
|
|
TimerManager timerManager,
|
|
|
- SeriesTimerManager seriesTimerManager)
|
|
|
+ SeriesTimerManager seriesTimerManager,
|
|
|
+ RecordingsMetadataManager recordingsMetadataManager)
|
|
|
{
|
|
|
Current = this;
|
|
|
|
|
@@ -103,6 +102,7 @@ namespace Jellyfin.LiveTv.EmbyTV
|
|
|
_tvDtoService = tvDtoService;
|
|
|
_timerManager = timerManager;
|
|
|
_seriesTimerManager = seriesTimerManager;
|
|
|
+ _recordingsMetadataManager = recordingsMetadataManager;
|
|
|
|
|
|
_timerManager.TimerFired += OnTimerManagerTimerFired;
|
|
|
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
|
|
@@ -998,7 +998,7 @@ namespace Jellyfin.LiveTv.EmbyTV
|
|
|
timer.Status = RecordingStatus.InProgress;
|
|
|
_timerManager.AddOrUpdate(timer, false);
|
|
|
|
|
|
- await SaveRecordingMetadata(timer, recordPath, seriesPath).ConfigureAwait(false);
|
|
|
+ await _recordingsMetadataManager.SaveRecordingMetadata(timer, recordPath, seriesPath).ConfigureAwait(false);
|
|
|
|
|
|
await CreateRecordingFolders().ConfigureAwait(false);
|
|
|
|
|
@@ -1377,452 +1377,6 @@ namespace Jellyfin.LiveTv.EmbyTV
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private async Task SaveRecordingImage(string recordingPath, LiveTvProgram program, ItemImageInfo image)
|
|
|
- {
|
|
|
- if (!image.IsLocalFile)
|
|
|
- {
|
|
|
- image = await _libraryManager.ConvertImageToLocal(program, image, 0).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- string imageSaveFilenameWithoutExtension = image.Type switch
|
|
|
- {
|
|
|
- ImageType.Primary => program.IsSeries ? Path.GetFileNameWithoutExtension(recordingPath) + "-thumb" : "poster",
|
|
|
- ImageType.Logo => "logo",
|
|
|
- ImageType.Thumb => program.IsSeries ? Path.GetFileNameWithoutExtension(recordingPath) + "-thumb" : "landscape",
|
|
|
- ImageType.Backdrop => "fanart",
|
|
|
- _ => null
|
|
|
- };
|
|
|
-
|
|
|
- if (imageSaveFilenameWithoutExtension is null)
|
|
|
- {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- var imageSavePath = Path.Combine(Path.GetDirectoryName(recordingPath), imageSaveFilenameWithoutExtension);
|
|
|
-
|
|
|
- // preserve original image extension
|
|
|
- imageSavePath = Path.ChangeExtension(imageSavePath, Path.GetExtension(image.Path));
|
|
|
-
|
|
|
- File.Copy(image.Path, imageSavePath, true);
|
|
|
- }
|
|
|
-
|
|
|
- private async Task SaveRecordingImages(string recordingPath, LiveTvProgram program)
|
|
|
- {
|
|
|
- var image = program.IsSeries ?
|
|
|
- (program.GetImageInfo(ImageType.Thumb, 0) ?? program.GetImageInfo(ImageType.Primary, 0)) :
|
|
|
- (program.GetImageInfo(ImageType.Primary, 0) ?? program.GetImageInfo(ImageType.Thumb, 0));
|
|
|
-
|
|
|
- if (image is not null)
|
|
|
- {
|
|
|
- try
|
|
|
- {
|
|
|
- await SaveRecordingImage(recordingPath, program, image).ConfigureAwait(false);
|
|
|
- }
|
|
|
- catch (Exception ex)
|
|
|
- {
|
|
|
- _logger.LogError(ex, "Error saving recording image");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (!program.IsSeries)
|
|
|
- {
|
|
|
- image = program.GetImageInfo(ImageType.Backdrop, 0);
|
|
|
- if (image is not null)
|
|
|
- {
|
|
|
- try
|
|
|
- {
|
|
|
- await SaveRecordingImage(recordingPath, program, image).ConfigureAwait(false);
|
|
|
- }
|
|
|
- catch (Exception ex)
|
|
|
- {
|
|
|
- _logger.LogError(ex, "Error saving recording image");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- image = program.GetImageInfo(ImageType.Thumb, 0);
|
|
|
- if (image is not null)
|
|
|
- {
|
|
|
- try
|
|
|
- {
|
|
|
- await SaveRecordingImage(recordingPath, program, image).ConfigureAwait(false);
|
|
|
- }
|
|
|
- catch (Exception ex)
|
|
|
- {
|
|
|
- _logger.LogError(ex, "Error saving recording image");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- image = program.GetImageInfo(ImageType.Logo, 0);
|
|
|
- if (image is not null)
|
|
|
- {
|
|
|
- try
|
|
|
- {
|
|
|
- await SaveRecordingImage(recordingPath, program, image).ConfigureAwait(false);
|
|
|
- }
|
|
|
- catch (Exception ex)
|
|
|
- {
|
|
|
- _logger.LogError(ex, "Error saving recording image");
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private async Task SaveRecordingMetadata(TimerInfo timer, string recordingPath, string seriesPath)
|
|
|
- {
|
|
|
- try
|
|
|
- {
|
|
|
- var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery
|
|
|
- {
|
|
|
- IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
|
|
|
- Limit = 1,
|
|
|
- ExternalId = timer.ProgramId,
|
|
|
- DtoOptions = new DtoOptions(true)
|
|
|
- }).FirstOrDefault() as LiveTvProgram;
|
|
|
-
|
|
|
- // dummy this up
|
|
|
- if (program is null)
|
|
|
- {
|
|
|
- program = new LiveTvProgram
|
|
|
- {
|
|
|
- Name = timer.Name,
|
|
|
- Overview = timer.Overview,
|
|
|
- Genres = timer.Genres,
|
|
|
- CommunityRating = timer.CommunityRating,
|
|
|
- OfficialRating = timer.OfficialRating,
|
|
|
- ProductionYear = timer.ProductionYear,
|
|
|
- PremiereDate = timer.OriginalAirDate,
|
|
|
- IndexNumber = timer.EpisodeNumber,
|
|
|
- ParentIndexNumber = timer.SeasonNumber
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- if (timer.IsSports)
|
|
|
- {
|
|
|
- program.AddGenre("Sports");
|
|
|
- }
|
|
|
-
|
|
|
- if (timer.IsKids)
|
|
|
- {
|
|
|
- program.AddGenre("Kids");
|
|
|
- program.AddGenre("Children");
|
|
|
- }
|
|
|
-
|
|
|
- if (timer.IsNews)
|
|
|
- {
|
|
|
- program.AddGenre("News");
|
|
|
- }
|
|
|
-
|
|
|
- var config = _config.GetLiveTvConfiguration();
|
|
|
-
|
|
|
- if (config.SaveRecordingNFO)
|
|
|
- {
|
|
|
- if (timer.IsProgramSeries)
|
|
|
- {
|
|
|
- await SaveSeriesNfoAsync(timer, seriesPath).ConfigureAwait(false);
|
|
|
- await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false);
|
|
|
- }
|
|
|
- else if (!timer.IsMovie || timer.IsSports || timer.IsNews)
|
|
|
- {
|
|
|
- await SaveVideoNfoAsync(timer, recordingPath, program, true).ConfigureAwait(false);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (config.SaveRecordingImages)
|
|
|
- {
|
|
|
- await SaveRecordingImages(recordingPath, program).ConfigureAwait(false);
|
|
|
- }
|
|
|
- }
|
|
|
- catch (Exception ex)
|
|
|
- {
|
|
|
- _logger.LogError(ex, "Error saving nfo");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private async Task SaveSeriesNfoAsync(TimerInfo timer, string seriesPath)
|
|
|
- {
|
|
|
- var nfoPath = Path.Combine(seriesPath, "tvshow.nfo");
|
|
|
-
|
|
|
- if (File.Exists(nfoPath))
|
|
|
- {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
|
|
|
- await using (stream.ConfigureAwait(false))
|
|
|
- {
|
|
|
- var settings = new XmlWriterSettings
|
|
|
- {
|
|
|
- Indent = true,
|
|
|
- Encoding = Encoding.UTF8,
|
|
|
- Async = true
|
|
|
- };
|
|
|
-
|
|
|
- var writer = XmlWriter.Create(stream, settings);
|
|
|
- await using (writer.ConfigureAwait(false))
|
|
|
- {
|
|
|
- await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
|
|
|
- await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false);
|
|
|
- if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out var id))
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "id", null, id).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out id))
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "imdb_id", null, id).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out id))
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "tmdbid", null, id).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Zap2It.ToString(), out id))
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "zap2itid", null, id).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- if (!string.IsNullOrWhiteSpace(timer.Name))
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "title", null, timer.Name).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- if (!string.IsNullOrWhiteSpace(timer.OfficialRating))
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "mpaa", null, timer.OfficialRating).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- foreach (var genre in timer.Genres)
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "genre", null, genre).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- await writer.WriteEndElementAsync().ConfigureAwait(false);
|
|
|
- await writer.WriteEndDocumentAsync().ConfigureAwait(false);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private async Task SaveVideoNfoAsync(TimerInfo timer, string recordingPath, BaseItem item, bool lockData)
|
|
|
- {
|
|
|
- var nfoPath = Path.ChangeExtension(recordingPath, ".nfo");
|
|
|
-
|
|
|
- if (File.Exists(nfoPath))
|
|
|
- {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
|
|
|
- await using (stream.ConfigureAwait(false))
|
|
|
- {
|
|
|
- var settings = new XmlWriterSettings
|
|
|
- {
|
|
|
- Indent = true,
|
|
|
- Encoding = Encoding.UTF8,
|
|
|
- Async = true
|
|
|
- };
|
|
|
-
|
|
|
- var options = _config.GetNfoConfiguration();
|
|
|
-
|
|
|
- var isSeriesEpisode = timer.IsProgramSeries;
|
|
|
-
|
|
|
- var writer = XmlWriter.Create(stream, settings);
|
|
|
- await using (writer.ConfigureAwait(false))
|
|
|
- {
|
|
|
- await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
|
|
|
-
|
|
|
- if (isSeriesEpisode)
|
|
|
- {
|
|
|
- await writer.WriteStartElementAsync(null, "episodedetails", null).ConfigureAwait(false);
|
|
|
-
|
|
|
- if (!string.IsNullOrWhiteSpace(timer.EpisodeTitle))
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "title", null, timer.EpisodeTitle).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- var premiereDate = item.PremiereDate ?? (!timer.IsRepeat ? DateTime.UtcNow : null);
|
|
|
-
|
|
|
- if (premiereDate.HasValue)
|
|
|
- {
|
|
|
- var formatString = options.ReleaseDateFormat;
|
|
|
-
|
|
|
- await writer.WriteElementStringAsync(
|
|
|
- null,
|
|
|
- "aired",
|
|
|
- null,
|
|
|
- premiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- if (item.IndexNumber.HasValue)
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "episode", null, item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- if (item.ParentIndexNumber.HasValue)
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "season", null, item.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
|
|
- }
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- await writer.WriteStartElementAsync(null, "movie", null).ConfigureAwait(false);
|
|
|
-
|
|
|
- if (!string.IsNullOrWhiteSpace(item.Name))
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "title", null, item.Name).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- if (!string.IsNullOrWhiteSpace(item.OriginalTitle))
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "originaltitle", null, item.OriginalTitle).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- if (item.PremiereDate.HasValue)
|
|
|
- {
|
|
|
- var formatString = options.ReleaseDateFormat;
|
|
|
-
|
|
|
- await writer.WriteElementStringAsync(
|
|
|
- null,
|
|
|
- "premiered",
|
|
|
- null,
|
|
|
- item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
|
|
- await writer.WriteElementStringAsync(
|
|
|
- null,
|
|
|
- "releasedate",
|
|
|
- null,
|
|
|
- item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- await writer.WriteElementStringAsync(
|
|
|
- null,
|
|
|
- "dateadded",
|
|
|
- null,
|
|
|
- DateTime.Now.ToString(DateAddedFormat, CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
|
|
-
|
|
|
- if (item.ProductionYear.HasValue)
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "year", null, item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- if (!string.IsNullOrEmpty(item.OfficialRating))
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "mpaa", null, item.OfficialRating).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- var overview = (item.Overview ?? string.Empty)
|
|
|
- .StripHtml()
|
|
|
- .Replace(""", "'", StringComparison.Ordinal);
|
|
|
-
|
|
|
- await writer.WriteElementStringAsync(null, "plot", null, overview).ConfigureAwait(false);
|
|
|
-
|
|
|
- if (item.CommunityRating.HasValue)
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "rating", null, item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- foreach (var genre in item.Genres)
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "genre", null, genre).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- var people = item.Id.IsEmpty() ? new List<PersonInfo>() : _libraryManager.GetPeople(item);
|
|
|
-
|
|
|
- var directors = people
|
|
|
- .Where(i => i.IsType(PersonKind.Director))
|
|
|
- .Select(i => i.Name)
|
|
|
- .ToList();
|
|
|
-
|
|
|
- foreach (var person in directors)
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "director", null, person).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- var writers = people
|
|
|
- .Where(i => i.IsType(PersonKind.Writer))
|
|
|
- .Select(i => i.Name)
|
|
|
- .Distinct(StringComparer.OrdinalIgnoreCase)
|
|
|
- .ToList();
|
|
|
-
|
|
|
- foreach (var person in writers)
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "writer", null, person).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- foreach (var person in writers)
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "credits", null, person).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- var tmdbCollection = item.GetProviderId(MetadataProvider.TmdbCollection);
|
|
|
-
|
|
|
- if (!string.IsNullOrEmpty(tmdbCollection))
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "collectionnumber", null, tmdbCollection).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- var imdb = item.GetProviderId(MetadataProvider.Imdb);
|
|
|
- if (!string.IsNullOrEmpty(imdb))
|
|
|
- {
|
|
|
- if (!isSeriesEpisode)
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "id", null, imdb).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- await writer.WriteElementStringAsync(null, "imdbid", null, imdb).ConfigureAwait(false);
|
|
|
-
|
|
|
- // No need to lock if we have identified the content already
|
|
|
- lockData = false;
|
|
|
- }
|
|
|
-
|
|
|
- var tvdb = item.GetProviderId(MetadataProvider.Tvdb);
|
|
|
- if (!string.IsNullOrEmpty(tvdb))
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "tvdbid", null, tvdb).ConfigureAwait(false);
|
|
|
-
|
|
|
- // No need to lock if we have identified the content already
|
|
|
- lockData = false;
|
|
|
- }
|
|
|
-
|
|
|
- var tmdb = item.GetProviderId(MetadataProvider.Tmdb);
|
|
|
- if (!string.IsNullOrEmpty(tmdb))
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "tmdbid", null, tmdb).ConfigureAwait(false);
|
|
|
-
|
|
|
- // No need to lock if we have identified the content already
|
|
|
- lockData = false;
|
|
|
- }
|
|
|
-
|
|
|
- if (lockData)
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "lockdata", null, "true").ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- if (item.CriticRating.HasValue)
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "criticrating", null, item.CriticRating.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- if (!string.IsNullOrWhiteSpace(item.Tagline))
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "tagline", null, item.Tagline).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- foreach (var studio in item.Studios)
|
|
|
- {
|
|
|
- await writer.WriteElementStringAsync(null, "studio", null, studio).ConfigureAwait(false);
|
|
|
- }
|
|
|
-
|
|
|
- await writer.WriteEndElementAsync().ConfigureAwait(false);
|
|
|
- await writer.WriteEndDocumentAsync().ConfigureAwait(false);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
private LiveTvProgram GetProgramInfoFromCache(string programId)
|
|
|
{
|
|
|
var query = new InternalItemsQuery
|