< Summary - Jellyfin

Information
Class: Jellyfin.LiveTv.Recordings.RecordingsMetadataManager
Assembly: Jellyfin.LiveTv
File(s): /srv/git/jellyfin/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs
Line coverage
100%
Covered lines: 4
Uncovered lines: 0
Coverable lines: 4
Total lines: 501
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%

File(s)

/srv/git/jellyfin/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.IO;
 5using System.Linq;
 6using System.Text;
 7using System.Threading.Tasks;
 8using System.Xml;
 9using Jellyfin.Data.Enums;
 10using Jellyfin.Extensions;
 11using Jellyfin.LiveTv.Configuration;
 12using MediaBrowser.Common.Configuration;
 13using MediaBrowser.Common.Extensions;
 14using MediaBrowser.Controller.Dto;
 15using MediaBrowser.Controller.Entities;
 16using MediaBrowser.Controller.Library;
 17using MediaBrowser.Controller.LiveTv;
 18using MediaBrowser.Model.Entities;
 19using Microsoft.Extensions.Logging;
 20
 21namespace Jellyfin.LiveTv.Recordings;
 22
 23/// <summary>
 24/// A service responsible for saving recording metadata.
 25/// </summary>
 26public class RecordingsMetadataManager
 27{
 28    private const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
 29
 30    private readonly ILogger<RecordingsMetadataManager> _logger;
 31    private readonly IConfigurationManager _config;
 32    private readonly ILibraryManager _libraryManager;
 33
 34    /// <summary>
 35    /// Initializes a new instance of the <see cref="RecordingsMetadataManager"/> class.
 36    /// </summary>
 37    /// <param name="logger">The <see cref="ILogger"/>.</param>
 38    /// <param name="config">The <see cref="IConfigurationManager"/>.</param>
 39    /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
 40    public RecordingsMetadataManager(
 41        ILogger<RecordingsMetadataManager> logger,
 42        IConfigurationManager config,
 43        ILibraryManager libraryManager)
 44    {
 2245        _logger = logger;
 2246        _config = config;
 2247        _libraryManager = libraryManager;
 2248    }
 49
 50    /// <summary>
 51    /// Saves the metadata for a provided recording.
 52    /// </summary>
 53    /// <param name="timer">The recording timer.</param>
 54    /// <param name="recordingPath">The recording path.</param>
 55    /// <param name="seriesPath">The series path.</param>
 56    /// <returns>A task representing the metadata saving.</returns>
 57    public async Task SaveRecordingMetadata(TimerInfo timer, string recordingPath, string? seriesPath)
 58    {
 59        try
 60        {
 61            var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalIt
 62            {
 63                IncludeItemTypes = [BaseItemKind.LiveTvProgram],
 64                Limit = 1,
 65                ExternalId = timer.ProgramId,
 66                DtoOptions = new DtoOptions(true)
 67            }).FirstOrDefault() as LiveTvProgram;
 68
 69            // dummy this up
 70            program ??= new LiveTvProgram
 71            {
 72                Name = timer.Name,
 73                Overview = timer.Overview,
 74                Genres = timer.Genres,
 75                CommunityRating = timer.CommunityRating,
 76                OfficialRating = timer.OfficialRating,
 77                ProductionYear = timer.ProductionYear,
 78                PremiereDate = timer.OriginalAirDate,
 79                IndexNumber = timer.EpisodeNumber,
 80                ParentIndexNumber = timer.SeasonNumber
 81            };
 82
 83            if (timer.IsSports)
 84            {
 85                program.AddGenre("Sports");
 86            }
 87
 88            if (timer.IsKids)
 89            {
 90                program.AddGenre("Kids");
 91                program.AddGenre("Children");
 92            }
 93
 94            if (timer.IsNews)
 95            {
 96                program.AddGenre("News");
 97            }
 98
 99            var config = _config.GetLiveTvConfiguration();
 100
 101            if (config.SaveRecordingNFO)
 102            {
 103                if (timer.IsProgramSeries)
 104                {
 105                    ArgumentNullException.ThrowIfNull(seriesPath);
 106
 107                    await SaveSeriesNfoAsync(timer, seriesPath).ConfigureAwait(false);
 108                    await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false);
 109                }
 110                else if (!timer.IsMovie || timer.IsSports || timer.IsNews)
 111                {
 112                    await SaveVideoNfoAsync(timer, recordingPath, program, true).ConfigureAwait(false);
 113                }
 114                else
 115                {
 116                    await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false);
 117                }
 118            }
 119
 120            if (config.SaveRecordingImages)
 121            {
 122                await SaveRecordingImages(recordingPath, program).ConfigureAwait(false);
 123            }
 124        }
 125        catch (Exception ex)
 126        {
 127            _logger.LogError(ex, "Error saving nfo");
 128        }
 129    }
 130
 131    private static async Task SaveSeriesNfoAsync(TimerInfo timer, string seriesPath)
 132    {
 133        var nfoPath = Path.Combine(seriesPath, "tvshow.nfo");
 134
 135        if (File.Exists(nfoPath))
 136        {
 137            return;
 138        }
 139
 140        var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
 141        await using (stream.ConfigureAwait(false))
 142        {
 143            var settings = new XmlWriterSettings
 144            {
 145                Indent = true,
 146                Encoding = Encoding.UTF8,
 147                Async = true
 148            };
 149
 150            var writer = XmlWriter.Create(stream, settings);
 151            await using (writer.ConfigureAwait(false))
 152            {
 153                await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
 154                await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false);
 155                if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out var id))
 156                {
 157                    await writer.WriteElementStringAsync(null, "id", null, id).ConfigureAwait(false);
 158                }
 159
 160                if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out id))
 161                {
 162                    await writer.WriteElementStringAsync(null, "imdb_id", null, id).ConfigureAwait(false);
 163                }
 164
 165                if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out id))
 166                {
 167                    await writer.WriteElementStringAsync(null, "tmdbid", null, id).ConfigureAwait(false);
 168                }
 169
 170                if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Zap2It.ToString(), out id))
 171                {
 172                    await writer.WriteElementStringAsync(null, "zap2itid", null, id).ConfigureAwait(false);
 173                }
 174
 175                if (!string.IsNullOrWhiteSpace(timer.Name))
 176                {
 177                    await writer.WriteElementStringAsync(null, "title", null, timer.Name).ConfigureAwait(false);
 178                }
 179
 180                if (!string.IsNullOrWhiteSpace(timer.OfficialRating))
 181                {
 182                    await writer.WriteElementStringAsync(null, "mpaa", null, timer.OfficialRating).ConfigureAwait(false)
 183                }
 184
 185                foreach (var genre in timer.Genres)
 186                {
 187                    await writer.WriteElementStringAsync(null, "genre", null, genre).ConfigureAwait(false);
 188                }
 189
 190                await writer.WriteEndElementAsync().ConfigureAwait(false);
 191                await writer.WriteEndDocumentAsync().ConfigureAwait(false);
 192            }
 193        }
 194    }
 195
 196    private async Task SaveVideoNfoAsync(TimerInfo timer, string recordingPath, BaseItem item, bool lockData)
 197    {
 198        var nfoPath = Path.ChangeExtension(recordingPath, ".nfo");
 199
 200        if (File.Exists(nfoPath))
 201        {
 202            return;
 203        }
 204
 205        var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
 206        await using (stream.ConfigureAwait(false))
 207        {
 208            var settings = new XmlWriterSettings
 209            {
 210                Indent = true,
 211                Encoding = Encoding.UTF8,
 212                Async = true
 213            };
 214
 215            var options = _config.GetNfoConfiguration();
 216
 217            var isSeriesEpisode = timer.IsProgramSeries;
 218
 219            var writer = XmlWriter.Create(stream, settings);
 220            await using (writer.ConfigureAwait(false))
 221            {
 222                await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
 223
 224                if (isSeriesEpisode)
 225                {
 226                    await writer.WriteStartElementAsync(null, "episodedetails", null).ConfigureAwait(false);
 227
 228                    if (!string.IsNullOrWhiteSpace(timer.EpisodeTitle))
 229                    {
 230                        await writer.WriteElementStringAsync(null, "title", null, timer.EpisodeTitle).ConfigureAwait(fal
 231                    }
 232
 233                    var premiereDate = item.PremiereDate ?? (!timer.IsRepeat ? DateTime.UtcNow : null);
 234
 235                    if (premiereDate.HasValue)
 236                    {
 237                        var formatString = options.ReleaseDateFormat;
 238
 239                        await writer.WriteElementStringAsync(
 240                            null,
 241                            "aired",
 242                            null,
 243                            premiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)).Confi
 244                    }
 245
 246                    if (item.IndexNumber.HasValue)
 247                    {
 248                        await writer.WriteElementStringAsync(null, "episode", null, item.IndexNumber.Value.ToString(Cult
 249                    }
 250
 251                    if (item.ParentIndexNumber.HasValue)
 252                    {
 253                        await writer.WriteElementStringAsync(null, "season", null, item.ParentIndexNumber.Value.ToString
 254                    }
 255                }
 256                else
 257                {
 258                    await writer.WriteStartElementAsync(null, "movie", null).ConfigureAwait(false);
 259
 260                    if (!string.IsNullOrWhiteSpace(item.Name))
 261                    {
 262                        await writer.WriteElementStringAsync(null, "title", null, item.Name).ConfigureAwait(false);
 263                    }
 264
 265                    if (!string.IsNullOrWhiteSpace(item.OriginalTitle))
 266                    {
 267                        await writer.WriteElementStringAsync(null, "originaltitle", null, item.OriginalTitle).ConfigureA
 268                    }
 269
 270                    if (item.PremiereDate.HasValue)
 271                    {
 272                        var formatString = options.ReleaseDateFormat;
 273
 274                        await writer.WriteElementStringAsync(
 275                            null,
 276                            "premiered",
 277                            null,
 278                            item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)).
 279                        await writer.WriteElementStringAsync(
 280                            null,
 281                            "releasedate",
 282                            null,
 283                            item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)).
 284                    }
 285                }
 286
 287                await writer.WriteElementStringAsync(
 288                    null,
 289                    "dateadded",
 290                    null,
 291                    DateTime.Now.ToString(DateAddedFormat, CultureInfo.InvariantCulture)).ConfigureAwait(false);
 292
 293                if (item.ProductionYear.HasValue)
 294                {
 295                    await writer.WriteElementStringAsync(null, "year", null, item.ProductionYear.Value.ToString(CultureI
 296                }
 297
 298                if (!string.IsNullOrEmpty(item.OfficialRating))
 299                {
 300                    await writer.WriteElementStringAsync(null, "mpaa", null, item.OfficialRating).ConfigureAwait(false);
 301                }
 302
 303                var overview = (item.Overview ?? string.Empty)
 304                    .StripHtml()
 305                    .Replace("&quot;", "'", StringComparison.Ordinal);
 306
 307                await writer.WriteElementStringAsync(null, "plot", null, overview).ConfigureAwait(false);
 308
 309                if (item.CommunityRating.HasValue)
 310                {
 311                    await writer.WriteElementStringAsync(null, "rating", null, item.CommunityRating.Value.ToString(Cultu
 312                }
 313
 314                foreach (var genre in item.Genres)
 315                {
 316                    await writer.WriteElementStringAsync(null, "genre", null, genre).ConfigureAwait(false);
 317                }
 318
 319                var people = item.Id.IsEmpty() ? new List<PersonInfo>() : _libraryManager.GetPeople(item);
 320
 321                var directors = people
 322                    .Where(i => i.IsType(PersonKind.Director))
 323                    .Select(i => i.Name)
 324                    .ToList();
 325
 326                foreach (var person in directors)
 327                {
 328                    await writer.WriteElementStringAsync(null, "director", null, person).ConfigureAwait(false);
 329                }
 330
 331                var writers = people
 332                    .Where(i => i.IsType(PersonKind.Writer))
 333                    .Select(i => i.Name)
 334                    .Distinct(StringComparer.OrdinalIgnoreCase)
 335                    .ToList();
 336
 337                foreach (var person in writers)
 338                {
 339                    await writer.WriteElementStringAsync(null, "writer", null, person).ConfigureAwait(false);
 340                }
 341
 342                foreach (var person in writers)
 343                {
 344                    await writer.WriteElementStringAsync(null, "credits", null, person).ConfigureAwait(false);
 345                }
 346
 347                var tmdbCollection = item.GetProviderId(MetadataProvider.TmdbCollection);
 348
 349                if (!string.IsNullOrEmpty(tmdbCollection))
 350                {
 351                    await writer.WriteElementStringAsync(null, "collectionnumber", null, tmdbCollection).ConfigureAwait(
 352                }
 353
 354                var imdb = item.GetProviderId(MetadataProvider.Imdb);
 355                if (!string.IsNullOrEmpty(imdb))
 356                {
 357                    if (!isSeriesEpisode)
 358                    {
 359                        await writer.WriteElementStringAsync(null, "id", null, imdb).ConfigureAwait(false);
 360                    }
 361
 362                    await writer.WriteElementStringAsync(null, "imdbid", null, imdb).ConfigureAwait(false);
 363
 364                    // No need to lock if we have identified the content already
 365                    lockData = false;
 366                }
 367
 368                var tvdb = item.GetProviderId(MetadataProvider.Tvdb);
 369                if (!string.IsNullOrEmpty(tvdb))
 370                {
 371                    await writer.WriteElementStringAsync(null, "tvdbid", null, tvdb).ConfigureAwait(false);
 372
 373                    // No need to lock if we have identified the content already
 374                    lockData = false;
 375                }
 376
 377                var tmdb = item.GetProviderId(MetadataProvider.Tmdb);
 378                if (!string.IsNullOrEmpty(tmdb))
 379                {
 380                    await writer.WriteElementStringAsync(null, "tmdbid", null, tmdb).ConfigureAwait(false);
 381
 382                    // No need to lock if we have identified the content already
 383                    lockData = false;
 384                }
 385
 386                if (lockData)
 387                {
 388                    await writer.WriteElementStringAsync(null, "lockdata", null, "true").ConfigureAwait(false);
 389                }
 390
 391                if (item.CriticRating.HasValue)
 392                {
 393                    await writer.WriteElementStringAsync(null, "criticrating", null, item.CriticRating.Value.ToString(Cu
 394                }
 395
 396                if (!string.IsNullOrWhiteSpace(item.Tagline))
 397                {
 398                    await writer.WriteElementStringAsync(null, "tagline", null, item.Tagline).ConfigureAwait(false);
 399                }
 400
 401                foreach (var studio in item.Studios)
 402                {
 403                    await writer.WriteElementStringAsync(null, "studio", null, studio).ConfigureAwait(false);
 404                }
 405
 406                await writer.WriteEndElementAsync().ConfigureAwait(false);
 407                await writer.WriteEndDocumentAsync().ConfigureAwait(false);
 408            }
 409        }
 410    }
 411
 412    private async Task SaveRecordingImages(string recordingPath, LiveTvProgram program)
 413    {
 414        var image = program.IsSeries ?
 415            (program.GetImageInfo(ImageType.Thumb, 0) ?? program.GetImageInfo(ImageType.Primary, 0)) :
 416            (program.GetImageInfo(ImageType.Primary, 0) ?? program.GetImageInfo(ImageType.Thumb, 0));
 417
 418        if (image is not null)
 419        {
 420            try
 421            {
 422                await SaveRecordingImage(recordingPath, program, image).ConfigureAwait(false);
 423            }
 424            catch (Exception ex)
 425            {
 426                _logger.LogError(ex, "Error saving recording image");
 427            }
 428        }
 429
 430        if (!program.IsSeries)
 431        {
 432            image = program.GetImageInfo(ImageType.Backdrop, 0);
 433            if (image is not null)
 434            {
 435                try
 436                {
 437                    await SaveRecordingImage(recordingPath, program, image).ConfigureAwait(false);
 438                }
 439                catch (Exception ex)
 440                {
 441                    _logger.LogError(ex, "Error saving recording image");
 442                }
 443            }
 444
 445            image = program.GetImageInfo(ImageType.Thumb, 0);
 446            if (image is not null)
 447            {
 448                try
 449                {
 450                    await SaveRecordingImage(recordingPath, program, image).ConfigureAwait(false);
 451                }
 452                catch (Exception ex)
 453                {
 454                    _logger.LogError(ex, "Error saving recording image");
 455                }
 456            }
 457
 458            image = program.GetImageInfo(ImageType.Logo, 0);
 459            if (image is not null)
 460            {
 461                try
 462                {
 463                    await SaveRecordingImage(recordingPath, program, image).ConfigureAwait(false);
 464                }
 465                catch (Exception ex)
 466                {
 467                    _logger.LogError(ex, "Error saving recording image");
 468                }
 469            }
 470        }
 471    }
 472
 473    private async Task SaveRecordingImage(string recordingPath, LiveTvProgram program, ItemImageInfo image)
 474    {
 475        if (!image.IsLocalFile)
 476        {
 477            image = await _libraryManager.ConvertImageToLocal(program, image, 0).ConfigureAwait(false);
 478        }
 479
 480        var imageSaveFilenameWithoutExtension = image.Type switch
 481        {
 482            ImageType.Primary => program.IsSeries ? Path.GetFileNameWithoutExtension(recordingPath) + "-thumb" : "poster
 483            ImageType.Logo => "logo",
 484            ImageType.Thumb => program.IsSeries ? Path.GetFileNameWithoutExtension(recordingPath) + "-thumb" : "landscap
 485            ImageType.Backdrop => "fanart",
 486            _ => null
 487        };
 488
 489        if (imageSaveFilenameWithoutExtension is null)
 490        {
 491            return;
 492        }
 493
 494        var imageSavePath = Path.Combine(Path.GetDirectoryName(recordingPath)!, imageSaveFilenameWithoutExtension);
 495
 496        // preserve original image extension
 497        imageSavePath = Path.ChangeExtension(imageSavePath, Path.GetExtension(image.Path));
 498
 499        File.Copy(image.Path, imageSavePath, true);
 500    }
 501}