< Summary - Jellyfin

Information
Class: MediaBrowser.XbmcMetadata.Savers.BaseNfoSaver
Assembly: MediaBrowser.XbmcMetadata
File(s): /srv/git/jellyfin/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
Line coverage
0%
Covered lines: 1
Uncovered lines: 498
Coverable lines: 499
Total lines: 1063
Line coverage: 0.2%
Branch coverage
0%
Covered branches: 0
Total branches: 242
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 2/22/2026 - 12:14:10 AM Line coverage: 0.2% (1/452) Branch coverage: 0% (0/230) Total lines: 10404/19/2026 - 12:14:27 AM Line coverage: 0.2% (1/482) Branch coverage: 0% (0/238) Total lines: 10405/4/2026 - 12:15:16 AM Line coverage: 0.2% (1/491) Branch coverage: 0% (0/234) Total lines: 10445/8/2026 - 12:15:13 AM Line coverage: 0.2% (1/496) Branch coverage: 0% (0/238) Total lines: 10556/1/2026 - 12:16:05 AM Line coverage: 0.2% (1/499) Branch coverage: 0% (0/242) Total lines: 1063 2/22/2026 - 12:14:10 AM Line coverage: 0.2% (1/452) Branch coverage: 0% (0/230) Total lines: 10404/19/2026 - 12:14:27 AM Line coverage: 0.2% (1/482) Branch coverage: 0% (0/238) Total lines: 10405/4/2026 - 12:15:16 AM Line coverage: 0.2% (1/491) Branch coverage: 0% (0/234) Total lines: 10445/8/2026 - 12:15:13 AM Line coverage: 0.2% (1/496) Branch coverage: 0% (0/238) Total lines: 10556/1/2026 - 12:16:05 AM Line coverage: 0.2% (1/499) Branch coverage: 0% (0/242) Total lines: 1063

Coverage delta

Coverage delta 1 -1

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
.ctor(...)100%11100%
get_MinimumUpdateType()0%620%
get_Name()100%210%
get_SaverName()100%210%
GetSavePath(...)100%210%
GetTagsUsed()0%2040%
SaveAsync()100%210%
SaveToFileAsync()0%7280%
SetHidden(...)100%210%
Save(...)0%2040%
AddMediaInfo(...)0%2162460%
AddCommonNodes(...)0%140421180%
AddCollectionItems(...)0%620%
GetOutputTrailerUrl(...)100%210%
AddImages(...)0%2040%
AddUserData(...)0%210140%
AddActors(...)0%420200%
GetImagePathToSave(...)0%620%
AddCustomTags(...)0%110100%
GetTagForProviderKey(...)100%210%
SortNameOrName(...)0%7280%

File(s)

/srv/git/jellyfin/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs

#LineLine coverage
 1#pragma warning disable CS1591
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Globalization;
 6using System.IO;
 7using System.Linq;
 8using System.Text;
 9using System.Text.RegularExpressions;
 10using System.Threading;
 11using System.Threading.Tasks;
 12using System.Xml;
 13using Jellyfin.Data.Enums;
 14using Jellyfin.Extensions;
 15using MediaBrowser.Common.Extensions;
 16using MediaBrowser.Controller.Configuration;
 17using MediaBrowser.Controller.Entities;
 18using MediaBrowser.Controller.Entities.Audio;
 19using MediaBrowser.Controller.Entities.Movies;
 20using MediaBrowser.Controller.Entities.TV;
 21using MediaBrowser.Controller.Library;
 22using MediaBrowser.Model.Configuration;
 23using MediaBrowser.Model.Entities;
 24using MediaBrowser.Model.IO;
 25using MediaBrowser.XbmcMetadata.Configuration;
 26using Microsoft.Extensions.Logging;
 27
 28namespace MediaBrowser.XbmcMetadata.Savers
 29{
 30    public abstract partial class BaseNfoSaver : IMetadataFileSaver
 31    {
 32        public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
 33
 34        public const string YouTubeWatchUrl = "https://www.youtube.com/watch?v=";
 35
 036        private static readonly HashSet<string> _commonTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
 037        {
 038            "plot",
 039            "customrating",
 040            "lockdata",
 041            "dateadded",
 042            "title",
 043            "rating",
 044            "year",
 045            "sorttitle",
 046            "mpaa",
 047            "aspectratio",
 048            "collectionnumber",
 049            "tmdbid",
 050            "rottentomatoesid",
 051            "language",
 052            "tvcomid",
 053            "tagline",
 054            "studio",
 055            "genre",
 056            "tag",
 057            "runtime",
 058            "actor",
 059            "criticrating",
 060            "fileinfo",
 061            "director",
 062            "writer",
 063            "trailer",
 064            "premiered",
 065            "releasedate",
 066            "outline",
 067            "id",
 068            "credits",
 069            "originaltitle",
 070            "originallanguage",
 071            "watched",
 072            "playcount",
 073            "lastplayed",
 074            "art",
 075            "resume",
 076            "biography",
 077            "formed",
 078            "review",
 079            "style",
 080            "imdbid",
 081            "imdb_id",
 082            "country",
 083            "audiodbalbumid",
 084            "audiodbartistid",
 085            "enddate",
 086            "lockedfields",
 087            "zap2itid",
 088            "tvrageid",
 089
 090            "musicbrainzartistid",
 091            "musicbrainzalbumartistid",
 092            "musicbrainzalbumid",
 093            "musicbrainzreleasegroupid",
 094            "tvdbid",
 095            "collectionitem",
 096
 097            "isuserfavorite",
 098            "userrating",
 099
 0100            "countrycode"
 0101        };
 102
 103        protected BaseNfoSaver(
 104            IFileSystem fileSystem,
 105            IServerConfigurationManager configurationManager,
 106            ILibraryManager libraryManager,
 107            IUserManager userManager,
 108            IUserDataManager userDataManager,
 109            ILogger<BaseNfoSaver> logger)
 110        {
 111            Logger = logger;
 112            UserDataManager = userDataManager;
 113            UserManager = userManager;
 114            LibraryManager = libraryManager;
 115            ConfigurationManager = configurationManager;
 116            FileSystem = fileSystem;
 126117        }
 118
 119        protected IFileSystem FileSystem { get; }
 120
 121        protected IServerConfigurationManager ConfigurationManager { get; }
 122
 123        protected ILibraryManager LibraryManager { get; }
 124
 125        protected IUserManager UserManager { get; }
 126
 127        protected IUserDataManager UserDataManager { get; }
 128
 129        protected ILogger<BaseNfoSaver> Logger { get; }
 130
 131        protected ItemUpdateType MinimumUpdateType
 132        {
 133            get
 134            {
 0135                if (ConfigurationManager.GetNfoConfiguration().SaveImagePathsInNfo)
 136                {
 0137                    return ItemUpdateType.ImageUpdate;
 138                }
 139
 0140                return ItemUpdateType.MetadataDownload;
 141            }
 142        }
 143
 144        /// <inheritdoc />
 0145        public string Name => SaverName;
 146
 0147        public static string SaverName => "Nfo";
 148
 149        // filters control characters but allows only properly-formed surrogate sequences
 150        // http://web.archive.org/web/20181230211547/https://emby.media/community/index.php?/topic/49071-nfo-not-generat
 151        // Web Archive version of link since it's not really explained in the thread.
 152        [GeneratedRegex(@"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-
 153        private static partial Regex InvalidXMLCharsRegexRegex();
 154
 155        /// <inheritdoc />
 156        public string GetSavePath(BaseItem item)
 0157            => GetLocalSavePath(item);
 158
 159        /// <summary>
 160        /// Gets the save path.
 161        /// </summary>
 162        /// <param name="item">The item.</param>
 163        /// <returns><see cref="string" />.</returns>
 164        protected abstract string GetLocalSavePath(BaseItem item);
 165
 166        /// <summary>
 167        /// Gets the name of the root element.
 168        /// </summary>
 169        /// <param name="item">The item.</param>
 170        /// <returns><see cref="string" />.</returns>
 171        protected abstract string GetRootElementName(BaseItem item);
 172
 173        /// <inheritdoc />
 174        public abstract bool IsEnabledFor(BaseItem item, ItemUpdateType updateType);
 175
 176        protected virtual IEnumerable<string> GetTagsUsed(BaseItem item)
 177        {
 0178            foreach (var providerKey in item.ProviderIds.Keys)
 179            {
 0180                var providerIdTagName = GetTagForProviderKey(providerKey);
 0181                if (!_commonTags.Contains(providerIdTagName))
 182                {
 0183                    yield return providerIdTagName;
 184                }
 185            }
 0186        }
 187
 188        /// <inheritdoc />
 189        public async Task SaveAsync(BaseItem item, CancellationToken cancellationToken)
 190        {
 0191            var path = GetSavePath(item);
 192
 0193            using (var memoryStream = new MemoryStream())
 194            {
 0195                Save(item, memoryStream, path);
 196
 0197                memoryStream.Position = 0;
 198
 0199                cancellationToken.ThrowIfCancellationRequested();
 200
 0201                await SaveToFileAsync(memoryStream, path, cancellationToken).ConfigureAwait(false);
 0202            }
 0203        }
 204
 205        private async Task SaveToFileAsync(Stream stream, string path, CancellationToken cancellationToken)
 206        {
 0207            var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not v
 0208            Directory.CreateDirectory(directory);
 209
 210            // Compare byte-for-byte before proceeding.
 0211            if (File.Exists(path) && await stream.IsFileIdenticalAsync(path, cancellationToken).ConfigureAwait(false))
 212            {
 0213                return; // Don't save since .nfo is unchanged.
 214            }
 215
 0216            stream.Position = 0;
 217
 218            // On Windows, saving the file will fail if the file is hidden or readonly
 0219            FileSystem.SetAttributes(path, false, false);
 220
 0221            var fileStreamOptions = new FileStreamOptions()
 0222            {
 0223                Mode = FileMode.Create,
 0224                Access = FileAccess.Write,
 0225                Share = FileShare.None,
 0226                PreallocationSize = stream.Length,
 0227                Options = FileOptions.Asynchronous
 0228            };
 229
 0230            var filestream = new FileStream(path, fileStreamOptions);
 0231            await using (filestream.ConfigureAwait(false))
 232            {
 0233                await stream.CopyToAsync(filestream, cancellationToken).ConfigureAwait(false);
 234            }
 235
 0236            if (ConfigurationManager.Configuration.SaveMetadataHidden)
 237            {
 0238                SetHidden(path, true);
 239            }
 0240        }
 241
 242        private void SetHidden(string path, bool hidden)
 243        {
 244            try
 245            {
 0246                FileSystem.SetHidden(path, hidden);
 0247            }
 0248            catch (IOException ex)
 249            {
 0250                Logger.LogError(ex, "Error setting hidden attribute on {Path}", path);
 0251            }
 0252        }
 253
 254        private void Save(BaseItem item, Stream stream, string xmlPath)
 255        {
 0256            var settings = new XmlWriterSettings
 0257            {
 0258                Indent = true,
 0259                Encoding = Encoding.UTF8,
 0260                CloseOutput = false
 0261            };
 262
 0263            using (var writer = XmlWriter.Create(stream, settings))
 264            {
 0265                var root = GetRootElementName(item);
 266
 0267                writer.WriteStartDocument(true);
 268
 0269                writer.WriteStartElement(root);
 270
 0271                var baseItem = item;
 272
 0273                if (baseItem is not null)
 274                {
 0275                    AddCommonNodes(baseItem, writer, LibraryManager, UserManager, UserDataManager, ConfigurationManager)
 276                }
 277
 0278                WriteCustomElements(item, writer);
 279
 0280                if (baseItem is IHasMediaSources hasMediaSources)
 281                {
 0282                    AddMediaInfo(hasMediaSources, writer);
 283                }
 284
 0285                var tagsUsed = GetTagsUsed(item).ToList();
 286
 287                try
 288                {
 0289                    AddCustomTags(xmlPath, tagsUsed, writer, Logger);
 0290                }
 0291                catch (FileNotFoundException)
 292                {
 0293                }
 0294                catch (IOException)
 295                {
 0296                }
 0297                catch (XmlException ex)
 298                {
 0299                    Logger.LogError(ex, "Error reading existing nfo");
 0300                }
 301
 0302                writer.WriteEndElement();
 303
 0304                writer.WriteEndDocument();
 0305            }
 0306        }
 307
 308        protected abstract void WriteCustomElements(BaseItem item, XmlWriter writer);
 309
 310        public static void AddMediaInfo<T>(T item, XmlWriter writer)
 311            where T : IHasMediaSources
 312        {
 0313            writer.WriteStartElement("fileinfo");
 0314            writer.WriteStartElement("streamdetails");
 315
 0316            var mediaStreams = item.GetMediaStreams();
 317
 0318            foreach (var stream in mediaStreams)
 319            {
 0320                writer.WriteStartElement(stream.Type.ToString().ToLowerInvariant());
 321
 0322                if (!string.IsNullOrEmpty(stream.Codec))
 323                {
 0324                    var codec = stream.Codec;
 325
 0326                    if ((stream.CodecTag ?? string.Empty).Contains("xvid", StringComparison.OrdinalIgnoreCase))
 327                    {
 0328                        codec = "xvid";
 329                    }
 0330                    else if ((stream.CodecTag ?? string.Empty).Contains("divx", StringComparison.OrdinalIgnoreCase))
 331                    {
 0332                        codec = "divx";
 333                    }
 334
 0335                    writer.WriteElementString("codec", codec);
 0336                    writer.WriteElementString("micodec", codec);
 337                }
 338
 0339                if (stream.BitRate.HasValue)
 340                {
 0341                    writer.WriteElementString("bitrate", stream.BitRate.Value.ToString(CultureInfo.InvariantCulture));
 342                }
 343
 0344                if (stream.Width.HasValue)
 345                {
 0346                    writer.WriteElementString("width", stream.Width.Value.ToString(CultureInfo.InvariantCulture));
 347                }
 348
 0349                if (stream.Height.HasValue)
 350                {
 0351                    writer.WriteElementString("height", stream.Height.Value.ToString(CultureInfo.InvariantCulture));
 352                }
 353
 0354                if (!string.IsNullOrEmpty(stream.AspectRatio))
 355                {
 0356                    writer.WriteElementString("aspect", stream.AspectRatio);
 0357                    writer.WriteElementString("aspectratio", stream.AspectRatio);
 358                }
 359
 0360                var framerate = stream.ReferenceFrameRate;
 361
 0362                if (framerate.HasValue)
 363                {
 0364                    writer.WriteElementString("framerate", framerate.Value.ToString(CultureInfo.InvariantCulture));
 365                }
 366
 0367                if (!string.IsNullOrEmpty(stream.Language))
 368                {
 0369                    writer.WriteElementString("language", InvalidXMLCharsRegexRegex().Replace(stream.Language, string.Em
 370                }
 371
 0372                var scanType = stream.IsInterlaced ? "interlaced" : "progressive";
 0373                writer.WriteElementString("scantype", scanType);
 374
 0375                if (stream.Channels.HasValue)
 376                {
 0377                    writer.WriteElementString("channels", stream.Channels.Value.ToString(CultureInfo.InvariantCulture));
 378                }
 379
 0380                if (stream.SampleRate.HasValue)
 381                {
 0382                    writer.WriteElementString("samplingrate", stream.SampleRate.Value.ToString(CultureInfo.InvariantCult
 383                }
 384
 0385                writer.WriteElementString("default", stream.IsDefault.ToString(CultureInfo.InvariantCulture));
 0386                writer.WriteElementString("forced", stream.IsForced.ToString(CultureInfo.InvariantCulture));
 387
 0388                if (stream.IsOriginal)
 389                {
 0390                    writer.WriteElementString("original", stream.IsOriginal.ToString(CultureInfo.InvariantCulture));
 391                }
 392
 0393                if (stream.Type == MediaStreamType.Video)
 394                {
 0395                    var runtimeTicks = item.RunTimeTicks;
 0396                    if (runtimeTicks.HasValue)
 397                    {
 0398                        var timespan = TimeSpan.FromTicks(runtimeTicks.Value);
 399
 0400                        writer.WriteElementString(
 0401                            "duration",
 0402                            Math.Floor(timespan.TotalMinutes).ToString(CultureInfo.InvariantCulture));
 0403                        writer.WriteElementString(
 0404                            "durationinseconds",
 0405                            Math.Floor(timespan.TotalSeconds).ToString(CultureInfo.InvariantCulture));
 406                    }
 407
 0408                    if (item is Video video)
 409                    {
 410                        // AddChapters(video, builder, itemRepository);
 411
 0412                        if (video.Video3DFormat.HasValue)
 413                        {
 0414                            switch (video.Video3DFormat.Value)
 415                            {
 416                                case Video3DFormat.FullSideBySide:
 0417                                    writer.WriteElementString("format3d", "FSBS");
 0418                                    break;
 419                                case Video3DFormat.FullTopAndBottom:
 0420                                    writer.WriteElementString("format3d", "FTAB");
 0421                                    break;
 422                                case Video3DFormat.HalfSideBySide:
 0423                                    writer.WriteElementString("format3d", "HSBS");
 0424                                    break;
 425                                case Video3DFormat.HalfTopAndBottom:
 0426                                    writer.WriteElementString("format3d", "HTAB");
 0427                                    break;
 428                                case Video3DFormat.MVC:
 0429                                    writer.WriteElementString("format3d", "MVC");
 430                                    break;
 431                            }
 432                        }
 433                    }
 434                }
 435
 0436                writer.WriteEndElement();
 437            }
 438
 0439            writer.WriteEndElement();
 0440            writer.WriteEndElement();
 0441        }
 442
 443        /// <summary>
 444        /// Adds the common nodes.
 445        /// </summary>
 446        private void AddCommonNodes(
 447            BaseItem item,
 448            XmlWriter writer,
 449            ILibraryManager libraryManager,
 450            IUserManager userManager,
 451            IUserDataManager userDataRepo,
 452            IServerConfigurationManager config)
 453        {
 0454            var writtenProviderIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 455
 0456            var overview = (item.Overview ?? string.Empty)
 0457                .StripHtml()
 0458                .Replace("&quot;", "'", StringComparison.Ordinal);
 459
 0460            var options = config.GetNfoConfiguration();
 461
 0462            if (item is MusicArtist)
 463            {
 0464                writer.WriteElementString("biography", overview);
 465            }
 0466            else if (item is MusicAlbum)
 467            {
 0468                writer.WriteElementString("review", overview);
 469            }
 470            else
 471            {
 0472                writer.WriteElementString("plot", overview);
 473            }
 474
 0475            if (item is not Video)
 476            {
 0477                writer.WriteElementString("outline", overview);
 478            }
 479
 0480            if (!string.IsNullOrWhiteSpace(item.CustomRating))
 481            {
 0482                writer.WriteElementString("customrating", item.CustomRating);
 483            }
 484
 0485            writer.WriteElementString("lockdata", item.IsLocked.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(
 486
 0487            if (item.LockedFields.Length > 0)
 488            {
 0489                writer.WriteElementString("lockedfields", string.Join('|', item.LockedFields));
 490            }
 491
 0492            writer.WriteElementString("dateadded", item.DateCreated.ToString(DateAddedFormat, CultureInfo.InvariantCultu
 493
 0494            writer.WriteElementString("title", item.Name ?? string.Empty);
 495
 0496            if (!string.IsNullOrWhiteSpace(item.OriginalTitle))
 497            {
 0498                writer.WriteElementString("originaltitle", item.OriginalTitle);
 499            }
 500
 0501            if (!string.IsNullOrWhiteSpace(item.OriginalLanguage))
 502            {
 0503                writer.WriteElementString("originallanguage", item.OriginalLanguage);
 504            }
 505
 0506            var people = libraryManager.GetPeople(item);
 507
 0508            var directors = people
 0509                .Where(i => i.IsType(PersonKind.Director))
 0510                .Select(i => i.Name?.Trim())
 0511                .Distinct(StringComparer.OrdinalIgnoreCase)
 0512                .OrderBy(i => i)
 0513                .ToList();
 514
 0515            foreach (var person in directors)
 516            {
 0517                writer.WriteElementString("director", person);
 518            }
 519
 0520            var writers = people
 0521                .Where(i => i.IsType(PersonKind.Writer))
 0522                .Select(i => i.Name?.Trim())
 0523                .Distinct(StringComparer.OrdinalIgnoreCase)
 0524                .OrderBy(i => i)
 0525                .ToList();
 526
 0527            foreach (var person in writers)
 528            {
 0529                writer.WriteElementString("writer", person);
 530            }
 531
 0532            foreach (var person in writers)
 533            {
 0534                writer.WriteElementString("credits", person);
 535            }
 536
 0537            foreach (var trailer in item.RemoteTrailers.OrderBy(t => t.Url?.Trim()))
 538            {
 0539                writer.WriteElementString("trailer", GetOutputTrailerUrl(trailer.Url));
 540            }
 541
 0542            if (item.CommunityRating.HasValue)
 543            {
 0544                writer.WriteElementString("rating", item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture));
 545            }
 546
 0547            if (item.ProductionYear.HasValue)
 548            {
 0549                writer.WriteElementString("year", item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture));
 550            }
 551
 0552            var forcedSortName = item.ForcedSortName;
 0553            if (!string.IsNullOrEmpty(forcedSortName))
 554            {
 0555                writer.WriteElementString("sorttitle", forcedSortName);
 556            }
 557
 0558            if (!string.IsNullOrEmpty(item.OfficialRating))
 559            {
 0560                writer.WriteElementString("mpaa", item.OfficialRating);
 561            }
 562
 0563            if (item is IHasAspectRatio hasAspectRatio
 0564                && !string.IsNullOrEmpty(hasAspectRatio.AspectRatio))
 565            {
 0566                writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio);
 567            }
 568
 0569            if (item.TryGetProviderId(MetadataProvider.TmdbCollection, out var tmdbCollection))
 570            {
 0571                writer.WriteElementString("collectionnumber", tmdbCollection);
 0572                writtenProviderIds.Add(MetadataProvider.TmdbCollection.ToString());
 573            }
 574
 0575            if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb))
 576            {
 0577                if (item is Series)
 578                {
 0579                    writer.WriteElementString("imdb_id", imdb);
 580                }
 581                else
 582                {
 0583                    writer.WriteElementString("imdbid", imdb);
 584                }
 585
 0586                writtenProviderIds.Add(MetadataProvider.Imdb.ToString());
 587            }
 588
 589            // Series xml saver already saves this
 0590            if (item is not Series)
 591            {
 0592                if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb))
 593                {
 0594                    writer.WriteElementString("tvdbid", tvdb);
 0595                    writtenProviderIds.Add(MetadataProvider.Tvdb.ToString());
 596                }
 597            }
 598
 0599            if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdb))
 600            {
 0601                writer.WriteElementString("tmdbid", tmdb);
 0602                writtenProviderIds.Add(MetadataProvider.Tmdb.ToString());
 603            }
 604
 0605            if (!string.IsNullOrEmpty(item.PreferredMetadataLanguage))
 606            {
 0607                writer.WriteElementString("language", item.PreferredMetadataLanguage);
 608            }
 609
 0610            if (!string.IsNullOrEmpty(item.PreferredMetadataCountryCode))
 611            {
 0612                writer.WriteElementString("countrycode", item.PreferredMetadataCountryCode);
 613            }
 614
 0615            if (item.PremiereDate.HasValue && item is not Episode)
 616            {
 0617                var formatString = options.ReleaseDateFormat;
 618
 0619                if (item is MusicArtist)
 620                {
 0621                    writer.WriteElementString(
 0622                        "formed",
 0623                        item.PremiereDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
 624                }
 625                else
 626                {
 0627                    writer.WriteElementString(
 0628                        "premiered",
 0629                        item.PremiereDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
 0630                    writer.WriteElementString(
 0631                        "releasedate",
 0632                        item.PremiereDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
 633                }
 634            }
 635
 0636            if (item.EndDate.HasValue)
 637            {
 0638                if (item is not Episode)
 639                {
 0640                    var formatString = options.ReleaseDateFormat;
 641
 0642                    writer.WriteElementString(
 0643                        "enddate",
 0644                        item.EndDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
 645                }
 646            }
 647
 0648            if (item.CriticRating.HasValue)
 649            {
 0650                writer.WriteElementString(
 0651                    "criticrating",
 0652                    item.CriticRating.Value.ToString(CultureInfo.InvariantCulture));
 653            }
 654
 0655            if (item is IHasDisplayOrder hasDisplayOrder)
 656            {
 0657                if (!string.IsNullOrEmpty(hasDisplayOrder.DisplayOrder))
 658                {
 0659                    writer.WriteElementString("displayorder", hasDisplayOrder.DisplayOrder);
 660                }
 661            }
 662
 663            // Use original runtime here, actual file runtime later in MediaInfo
 0664            var runTimeTicks = item.RunTimeTicks;
 665
 0666            if (runTimeTicks.HasValue)
 667            {
 0668                var timespan = TimeSpan.FromTicks(runTimeTicks.Value);
 669
 0670                writer.WriteElementString(
 0671                    "runtime",
 0672                    Convert.ToInt64(timespan.TotalMinutes).ToString(CultureInfo.InvariantCulture));
 673            }
 674
 0675            if (!string.IsNullOrWhiteSpace(item.Tagline))
 676            {
 0677                writer.WriteElementString("tagline", item.Tagline);
 678            }
 679
 0680            foreach (var country in item.ProductionLocations.Trimmed().OrderBy(country => country))
 681            {
 0682                writer.WriteElementString("country", country);
 683            }
 684
 0685            foreach (var genre in item.Genres.Trimmed().OrderBy(genre => genre))
 686            {
 0687                writer.WriteElementString("genre", genre);
 688            }
 689
 0690            foreach (var studio in item.Studios.Trimmed().OrderBy(studio => studio))
 691            {
 0692                writer.WriteElementString("studio", studio);
 693            }
 694
 0695            foreach (var tag in item.Tags.Trimmed().OrderBy(tag => tag))
 696            {
 0697                if (item is MusicAlbum || item is MusicArtist)
 698                {
 0699                    writer.WriteElementString("style", tag);
 700                }
 701                else
 702                {
 0703                    writer.WriteElementString("tag", tag);
 704                }
 705            }
 706
 0707            if (item.TryGetProviderId(MetadataProvider.AudioDbArtist, out var externalId))
 708            {
 0709                writer.WriteElementString("audiodbartistid", externalId);
 0710                writtenProviderIds.Add(MetadataProvider.AudioDbArtist.ToString());
 711            }
 712
 0713            if (item.TryGetProviderId(MetadataProvider.AudioDbAlbum, out externalId))
 714            {
 0715                writer.WriteElementString("audiodbalbumid", externalId);
 0716                writtenProviderIds.Add(MetadataProvider.AudioDbAlbum.ToString());
 717            }
 718
 0719            if (item.TryGetProviderId(MetadataProvider.Zap2It, out externalId))
 720            {
 0721                writer.WriteElementString("zap2itid", externalId);
 0722                writtenProviderIds.Add(MetadataProvider.Zap2It.ToString());
 723            }
 724
 0725            if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out externalId))
 726            {
 0727                writer.WriteElementString("musicbrainzalbumid", externalId);
 0728                writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbum.ToString());
 729            }
 730
 0731            if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out externalId))
 732            {
 0733                writer.WriteElementString("musicbrainzalbumartistid", externalId);
 0734                writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbumArtist.ToString());
 735            }
 736
 0737            if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out externalId))
 738            {
 0739                writer.WriteElementString("musicbrainzartistid", externalId);
 0740                writtenProviderIds.Add(MetadataProvider.MusicBrainzArtist.ToString());
 741            }
 742
 0743            if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out externalId))
 744            {
 0745                writer.WriteElementString("musicbrainzreleasegroupid", externalId);
 0746                writtenProviderIds.Add(MetadataProvider.MusicBrainzReleaseGroup.ToString());
 747            }
 748
 0749            if (item.TryGetProviderId(MetadataProvider.TvRage, out externalId))
 750            {
 0751                writer.WriteElementString("tvrageid", externalId);
 0752                writtenProviderIds.Add(MetadataProvider.TvRage.ToString());
 753            }
 754
 0755            if (item.ProviderIds is not null)
 756            {
 0757                foreach (var providerKey in item.ProviderIds.Keys.OrderBy(providerKey => providerKey))
 758                {
 0759                    var providerId = item.ProviderIds[providerKey];
 0760                    if (!string.IsNullOrEmpty(providerId) && !writtenProviderIds.Contains(providerKey))
 761                    {
 762                        try
 763                        {
 0764                            var tagName = GetTagForProviderKey(providerKey);
 0765                            Logger.LogDebug("Verifying custom provider tagname {0}", tagName);
 0766                            XmlConvert.VerifyName(tagName);
 0767                            Logger.LogDebug("Saving custom provider tagname {0}", tagName);
 768
 0769                            writer.WriteElementString(tagName, providerId);
 0770                        }
 0771                        catch (ArgumentException)
 772                        {
 773                            // catch invalid names without failing the entire operation
 0774                        }
 0775                        catch (XmlException)
 776                        {
 777                            // catch invalid names without failing the entire operation
 0778                        }
 779                    }
 780                }
 781            }
 782
 0783            if (options.SaveImagePathsInNfo)
 784            {
 0785                AddImages(item, writer, libraryManager);
 786            }
 787
 0788            AddUserData(item, writer, userManager, userDataRepo, options);
 789
 0790            if (item is not MusicAlbum && item is not MusicArtist)
 791            {
 0792                AddActors(people, writer, libraryManager, options.SaveImagePathsInNfo);
 793            }
 794
 0795            if (item is BoxSet folder)
 796            {
 0797                AddCollectionItems(folder, writer);
 798            }
 0799        }
 800
 801        private void AddCollectionItems(Folder item, XmlWriter writer)
 802        {
 0803            var linkedChildren = item.LinkedChildren
 0804                .Where(i => i.Type == LinkedChildType.Manual)
 0805                .ToList();
 806
 807            // Resolve ItemIds to paths and sort
 0808            var itemsWithPaths = linkedChildren
 0809                .Select(link =>
 0810                {
 0811                    if (link.ItemId.HasValue && !link.ItemId.Value.Equals(Guid.Empty))
 0812                    {
 0813                        var linkedItem = LibraryManager.GetItemById(link.ItemId.Value);
 0814                        return linkedItem?.Path;
 0815                    }
 0816
 0817                    return null;
 0818                })
 0819                .Where(path => !string.IsNullOrWhiteSpace(path))
 0820                .OrderBy(path => path?.Trim())
 0821                .ToList();
 822
 0823            foreach (var path in itemsWithPaths)
 824            {
 0825                writer.WriteStartElement("collectionitem");
 0826                writer.WriteElementString("path", path);
 0827                writer.WriteEndElement();
 828            }
 0829        }
 830
 831        /// <summary>
 832        /// Gets the output trailer URL.
 833        /// </summary>
 834        /// <param name="url">The URL.</param>
 835        /// <returns>System.String.</returns>
 836        private string GetOutputTrailerUrl(string url)
 837        {
 838            // This is what xbmc expects
 0839            return url.Replace(YouTubeWatchUrl, "plugin://plugin.video.youtube/play/?video_id=", StringComparison.Ordina
 840        }
 841
 842        private void AddImages(BaseItem item, XmlWriter writer, ILibraryManager libraryManager)
 843        {
 0844            writer.WriteStartElement("art");
 845
 0846            var image = item.GetImageInfo(ImageType.Primary, 0);
 847
 0848            if (image is not null)
 849            {
 0850                writer.WriteElementString("poster", GetImagePathToSave(image, libraryManager));
 851            }
 852
 0853            foreach (var backdrop in item.GetImages(ImageType.Backdrop).OrderBy(b => b.Path?.Trim()))
 854            {
 0855                writer.WriteElementString("fanart", GetImagePathToSave(backdrop, libraryManager));
 856            }
 857
 0858            writer.WriteEndElement();
 0859        }
 860
 861        private void AddUserData(BaseItem item, XmlWriter writer, IUserManager userManager, IUserDataManager userDataRep
 862        {
 0863            var userId = options.UserId;
 0864            if (string.IsNullOrWhiteSpace(userId))
 865            {
 0866                return;
 867            }
 868
 0869            var user = userManager.GetUserById(Guid.Parse(userId));
 870
 0871            if (user is null)
 872            {
 0873                return;
 874            }
 875
 0876            if (item.IsFolder)
 877            {
 0878                return;
 879            }
 880
 0881            var userdata = userDataRepo.GetUserData(user, item);
 882
 0883            if (userdata is not null)
 884            {
 0885                writer.WriteElementString(
 0886                "isuserfavorite",
 0887                userdata.IsFavorite.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
 888
 0889                if (userdata.Rating.HasValue)
 890                {
 0891                    writer.WriteElementString(
 0892                        "userrating",
 0893                        userdata.Rating.Value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
 894                }
 895
 0896                if (!item.IsFolder)
 897                {
 0898                    writer.WriteElementString(
 0899                        "playcount",
 0900                        userdata.PlayCount.ToString(CultureInfo.InvariantCulture));
 0901                    writer.WriteElementString(
 0902                        "watched",
 0903                        userdata.Played.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
 904
 0905                    if (userdata.LastPlayedDate.HasValue)
 906                    {
 0907                        writer.WriteElementString(
 0908                            "lastplayed",
 0909                            userdata.LastPlayedDate.Value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture).
 910                    }
 911
 0912                    writer.WriteStartElement("resume");
 913
 0914                    var runTimeTicks = item.RunTimeTicks ?? 0;
 915
 0916                    writer.WriteElementString(
 0917                        "position",
 0918                        TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds.ToString(CultureInfo.InvariantCu
 0919                    writer.WriteElementString(
 0920                        "total",
 0921                        TimeSpan.FromTicks(runTimeTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
 922                }
 923            }
 924
 0925            writer.WriteEndElement();
 0926        }
 927
 928        private void AddActors(IReadOnlyList<PersonInfo> people, XmlWriter writer, ILibraryManager libraryManager, bool 
 929        {
 0930            foreach (var person in people
 0931                .OrderBy(person => person.SortOrder ?? 0)
 0932                .ThenBy(person => person.Name?.Trim()))
 933            {
 0934                if (person.IsType(PersonKind.Director) || person.IsType(PersonKind.Writer))
 935                {
 936                    continue;
 937                }
 938
 0939                writer.WriteStartElement("actor");
 940
 0941                if (!string.IsNullOrWhiteSpace(person.Name))
 942                {
 0943                    writer.WriteElementString("name", person.Name);
 944                }
 945
 0946                if (!string.IsNullOrWhiteSpace(person.Role))
 947                {
 0948                    writer.WriteElementString("role", person.Role);
 949                }
 950
 0951                if (person.Type != PersonKind.Unknown)
 952                {
 0953                    writer.WriteElementString("type", person.Type.ToString());
 954                }
 955
 0956                if (person.SortOrder.HasValue)
 957                {
 0958                    writer.WriteElementString(
 0959                        "sortorder",
 0960                        person.SortOrder.Value.ToString(CultureInfo.InvariantCulture));
 961                }
 962
 0963                if (saveImagePath)
 964                {
 0965                    var personEntity = libraryManager.GetPerson(person.Name);
 0966                    var image = personEntity?.GetImageInfo(ImageType.Primary, 0);
 967
 0968                    if (image is not null)
 969                    {
 0970                        writer.WriteElementString(
 0971                            "thumb",
 0972                            GetImagePathToSave(image, libraryManager));
 973                    }
 974                }
 975
 0976                writer.WriteEndElement();
 977            }
 0978        }
 979
 980        private string GetImagePathToSave(ItemImageInfo image, ILibraryManager libraryManager)
 981        {
 0982            if (!image.IsLocalFile)
 983            {
 0984                return image.Path;
 985            }
 986
 0987            return libraryManager.GetPathAfterNetworkSubstitution(image.Path);
 988        }
 989
 990        private void AddCustomTags(string path, IReadOnlyCollection<string> xmlTagsUsed, XmlWriter writer, ILogger<BaseN
 991        {
 0992            var settings = new XmlReaderSettings()
 0993            {
 0994                ValidationType = ValidationType.None,
 0995                CheckCharacters = false,
 0996                IgnoreProcessingInstructions = true,
 0997                IgnoreComments = true
 0998            };
 999
 01000            using (var fileStream = File.OpenRead(path))
 01001            using (var streamReader = new StreamReader(fileStream, Encoding.UTF8))
 01002            using (var reader = XmlReader.Create(streamReader, settings))
 1003            {
 1004                try
 1005                {
 01006                    reader.MoveToContent();
 01007                }
 01008                catch (Exception ex)
 1009                {
 01010                    logger.LogError(ex, "Error reading existing xml tags from {Path}.", path);
 01011                    return;
 1012                }
 1013
 01014                reader.Read();
 1015
 1016                // Loop through each element
 01017                while (!reader.EOF && reader.ReadState == ReadState.Interactive)
 1018                {
 01019                    if (reader.NodeType == XmlNodeType.Element)
 1020                    {
 01021                        var name = reader.Name;
 1022
 01023                        if (!_commonTags.Contains(name)
 01024                            && !xmlTagsUsed.Contains(name, StringComparison.OrdinalIgnoreCase))
 1025                        {
 01026                            writer.WriteNode(reader, false);
 1027                        }
 1028                        else
 1029                        {
 01030                            reader.Skip();
 1031                        }
 1032                    }
 1033                    else
 1034                    {
 01035                        reader.Read();
 1036                    }
 1037                }
 01038            }
 01039        }
 1040
 1041        private string GetTagForProviderKey(string providerKey)
 01042            => providerKey.ToLowerInvariant() + "id";
 1043
 1044        protected static string SortNameOrName(BaseItem item)
 1045        {
 01046            if (item is null)
 1047            {
 01048                return string.Empty;
 1049            }
 1050
 01051            if (item.SortName is not null)
 1052            {
 01053                string trimmed = item.SortName.Trim();
 01054                if (trimmed.Length > 0)
 1055                {
 01056                    return trimmed;
 1057                }
 1058            }
 1059
 01060            return (item.Name ?? string.Empty).Trim();
 1061        }
 1062    }
 1063}

Methods/Properties

.cctor()
.ctor(MediaBrowser.Model.IO.IFileSystem,MediaBrowser.Controller.Configuration.IServerConfigurationManager,MediaBrowser.Controller.Library.ILibraryManager,MediaBrowser.Controller.Library.IUserManager,MediaBrowser.Controller.Library.IUserDataManager,Microsoft.Extensions.Logging.ILogger`1<MediaBrowser.XbmcMetadata.Savers.BaseNfoSaver>)
get_MinimumUpdateType()
get_Name()
get_SaverName()
GetSavePath(MediaBrowser.Controller.Entities.BaseItem)
GetTagsUsed()
SaveAsync()
SaveToFileAsync()
SetHidden(System.String,System.Boolean)
Save(MediaBrowser.Controller.Entities.BaseItem,System.IO.Stream,System.String)
AddMediaInfo(T,System.Xml.XmlWriter)
AddCommonNodes(MediaBrowser.Controller.Entities.BaseItem,System.Xml.XmlWriter,MediaBrowser.Controller.Library.ILibraryManager,MediaBrowser.Controller.Library.IUserManager,MediaBrowser.Controller.Library.IUserDataManager,MediaBrowser.Controller.Configuration.IServerConfigurationManager)
AddCollectionItems(MediaBrowser.Controller.Entities.Folder,System.Xml.XmlWriter)
GetOutputTrailerUrl(System.String)
AddImages(MediaBrowser.Controller.Entities.BaseItem,System.Xml.XmlWriter,MediaBrowser.Controller.Library.ILibraryManager)
AddUserData(MediaBrowser.Controller.Entities.BaseItem,System.Xml.XmlWriter,MediaBrowser.Controller.Library.IUserManager,MediaBrowser.Controller.Library.IUserDataManager,MediaBrowser.Model.Configuration.XbmcMetadataOptions)
AddActors(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.PersonInfo>,System.Xml.XmlWriter,MediaBrowser.Controller.Library.ILibraryManager,System.Boolean)
GetImagePathToSave(MediaBrowser.Controller.Entities.ItemImageInfo,MediaBrowser.Controller.Library.ILibraryManager)
AddCustomTags(System.String,System.Collections.Generic.IReadOnlyCollection`1<System.String>,System.Xml.XmlWriter,Microsoft.Extensions.Logging.ILogger`1<MediaBrowser.XbmcMetadata.Savers.BaseNfoSaver>)
GetTagForProviderKey(System.String)
SortNameOrName(MediaBrowser.Controller.Entities.BaseItem)