aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs458
-rw-r--r--src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs2
-rw-r--r--src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs502
3 files changed, 510 insertions, 452 deletions
diff --git a/src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs b/src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs
index cfd142d43..d1688dfd9 100644
--- a/src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs
+++ b/src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs
@@ -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("&quot;", "'", 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
diff --git a/src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs b/src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs
index 4f05a85e4..d02be31cf 100644
--- a/src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs
+++ b/src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs
@@ -2,6 +2,7 @@
using Jellyfin.LiveTv.Guide;
using Jellyfin.LiveTv.IO;
using Jellyfin.LiveTv.Listings;
+using Jellyfin.LiveTv.Recordings;
using Jellyfin.LiveTv.Timers;
using Jellyfin.LiveTv.TunerHosts;
using Jellyfin.LiveTv.TunerHosts.HdHomerun;
@@ -26,6 +27,7 @@ public static class LiveTvServiceCollectionExtensions
services.AddSingleton<LiveTvDtoService>();
services.AddSingleton<TimerManager>();
services.AddSingleton<SeriesTimerManager>();
+ services.AddSingleton<RecordingsMetadataManager>();
services.AddSingleton<ILiveTvManager, LiveTvManager>();
services.AddSingleton<IChannelManager, ChannelManager>();
services.AddSingleton<IStreamHelper, StreamHelper>();
diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs
new file mode 100644
index 000000000..0a71a4d46
--- /dev/null
+++ b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs
@@ -0,0 +1,502 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Xml;
+using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
+using Jellyfin.LiveTv.Configuration;
+using Jellyfin.LiveTv.EmbyTV;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Dto;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.LiveTv.Recordings;
+
+/// <summary>
+/// A service responsible for saving recording metadata.
+/// </summary>
+public class RecordingsMetadataManager
+{
+ private const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
+
+ private readonly ILogger<RecordingsMetadataManager> _logger;
+ private readonly IConfigurationManager _config;
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RecordingsMetadataManager"/> class.
+ /// </summary>
+ /// <param name="logger">The <see cref="ILogger"/>.</param>
+ /// <param name="config">The <see cref="IConfigurationManager"/>.</param>
+ /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
+ public RecordingsMetadataManager(
+ ILogger<RecordingsMetadataManager> logger,
+ IConfigurationManager config,
+ ILibraryManager libraryManager)
+ {
+ _logger = logger;
+ _config = config;
+ _libraryManager = libraryManager;
+ }
+
+ /// <summary>
+ /// Saves the metadata for a provided recording.
+ /// </summary>
+ /// <param name="timer">The recording timer.</param>
+ /// <param name="recordingPath">The recording path.</param>
+ /// <param name="seriesPath">The series path.</param>
+ /// <returns>A task representing the metadata saving.</returns>
+ public async Task SaveRecordingMetadata(TimerInfo timer, string recordingPath, string? seriesPath)
+ {
+ try
+ {
+ var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery
+ {
+ IncludeItemTypes = [BaseItemKind.LiveTvProgram],
+ Limit = 1,
+ ExternalId = timer.ProgramId,
+ DtoOptions = new DtoOptions(true)
+ }).FirstOrDefault() as LiveTvProgram;
+
+ // dummy this up
+ 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)
+ {
+ ArgumentNullException.ThrowIfNull(seriesPath);
+
+ 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 static 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("&quot;", "'", 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 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 SaveRecordingImage(string recordingPath, LiveTvProgram program, ItemImageInfo image)
+ {
+ if (!image.IsLocalFile)
+ {
+ image = await _libraryManager.ConvertImageToLocal(program, image, 0).ConfigureAwait(false);
+ }
+
+ var 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);
+ }
+}