< 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: 490
Coverable lines: 491
Total lines: 1044
Line coverage: 0.2%
Branch coverage
0%
Covered branches: 0
Total branches: 234
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 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: 1044 1/23/2026 - 12:11:06 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: 1044

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%2040%
SetHidden(...)100%210%
Save(...)0%2040%
AddMediaInfo(...)0%1980440%
AddCommonNodes(...)0%135721160%
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            "watched",
 071            "playcount",
 072            "lastplayed",
 073            "art",
 074            "resume",
 075            "biography",
 076            "formed",
 077            "review",
 078            "style",
 079            "imdbid",
 080            "imdb_id",
 081            "country",
 082            "audiodbalbumid",
 083            "audiodbartistid",
 084            "enddate",
 085            "lockedfields",
 086            "zap2itid",
 087            "tvrageid",
 088
 089            "musicbrainzartistid",
 090            "musicbrainzalbumartistid",
 091            "musicbrainzalbumid",
 092            "musicbrainzreleasegroupid",
 093            "tvdbid",
 094            "collectionitem",
 095
 096            "isuserfavorite",
 097            "userrating",
 098
 099            "countrycode"
 0100        };
 101
 102        protected BaseNfoSaver(
 103            IFileSystem fileSystem,
 104            IServerConfigurationManager configurationManager,
 105            ILibraryManager libraryManager,
 106            IUserManager userManager,
 107            IUserDataManager userDataManager,
 108            ILogger<BaseNfoSaver> logger)
 109        {
 110            Logger = logger;
 111            UserDataManager = userDataManager;
 112            UserManager = userManager;
 113            LibraryManager = libraryManager;
 114            ConfigurationManager = configurationManager;
 115            FileSystem = fileSystem;
 126116        }
 117
 118        protected IFileSystem FileSystem { get; }
 119
 120        protected IServerConfigurationManager ConfigurationManager { get; }
 121
 122        protected ILibraryManager LibraryManager { get; }
 123
 124        protected IUserManager UserManager { get; }
 125
 126        protected IUserDataManager UserDataManager { get; }
 127
 128        protected ILogger<BaseNfoSaver> Logger { get; }
 129
 130        protected ItemUpdateType MinimumUpdateType
 131        {
 132            get
 133            {
 0134                if (ConfigurationManager.GetNfoConfiguration().SaveImagePathsInNfo)
 135                {
 0136                    return ItemUpdateType.ImageUpdate;
 137                }
 138
 0139                return ItemUpdateType.MetadataDownload;
 140            }
 141        }
 142
 143        /// <inheritdoc />
 0144        public string Name => SaverName;
 145
 0146        public static string SaverName => "Nfo";
 147
 148        // filters control characters but allows only properly-formed surrogate sequences
 149        // http://web.archive.org/web/20181230211547/https://emby.media/community/index.php?/topic/49071-nfo-not-generat
 150        // Web Archive version of link since it's not really explained in the thread.
 151        [GeneratedRegex(@"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-
 152        private static partial Regex InvalidXMLCharsRegexRegex();
 153
 154        /// <inheritdoc />
 155        public string GetSavePath(BaseItem item)
 0156            => GetLocalSavePath(item);
 157
 158        /// <summary>
 159        /// Gets the save path.
 160        /// </summary>
 161        /// <param name="item">The item.</param>
 162        /// <returns><see cref="string" />.</returns>
 163        protected abstract string GetLocalSavePath(BaseItem item);
 164
 165        /// <summary>
 166        /// Gets the name of the root element.
 167        /// </summary>
 168        /// <param name="item">The item.</param>
 169        /// <returns><see cref="string" />.</returns>
 170        protected abstract string GetRootElementName(BaseItem item);
 171
 172        /// <inheritdoc />
 173        public abstract bool IsEnabledFor(BaseItem item, ItemUpdateType updateType);
 174
 175        protected virtual IEnumerable<string> GetTagsUsed(BaseItem item)
 176        {
 0177            foreach (var providerKey in item.ProviderIds.Keys)
 178            {
 0179                var providerIdTagName = GetTagForProviderKey(providerKey);
 0180                if (!_commonTags.Contains(providerIdTagName))
 181                {
 0182                    yield return providerIdTagName;
 183                }
 184            }
 0185        }
 186
 187        /// <inheritdoc />
 188        public async Task SaveAsync(BaseItem item, CancellationToken cancellationToken)
 189        {
 0190            var path = GetSavePath(item);
 191
 0192            using (var memoryStream = new MemoryStream())
 193            {
 0194                Save(item, memoryStream, path);
 195
 0196                memoryStream.Position = 0;
 197
 0198                cancellationToken.ThrowIfCancellationRequested();
 199
 0200                await SaveToFileAsync(memoryStream, path).ConfigureAwait(false);
 0201            }
 0202        }
 203
 204        private async Task SaveToFileAsync(Stream stream, string path)
 205        {
 0206            var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not v
 0207            Directory.CreateDirectory(directory);
 208
 209            // On Windows, saving the file will fail if the file is hidden or readonly
 0210            FileSystem.SetAttributes(path, false, false);
 211
 0212            var fileStreamOptions = new FileStreamOptions()
 0213            {
 0214                Mode = FileMode.Create,
 0215                Access = FileAccess.Write,
 0216                Share = FileShare.None,
 0217                PreallocationSize = stream.Length,
 0218                Options = FileOptions.Asynchronous
 0219            };
 220
 0221            var filestream = new FileStream(path, fileStreamOptions);
 0222            await using (filestream.ConfigureAwait(false))
 223            {
 0224                await stream.CopyToAsync(filestream).ConfigureAwait(false);
 225            }
 226
 0227            if (ConfigurationManager.Configuration.SaveMetadataHidden)
 228            {
 0229                SetHidden(path, true);
 230            }
 0231        }
 232
 233        private void SetHidden(string path, bool hidden)
 234        {
 235            try
 236            {
 0237                FileSystem.SetHidden(path, hidden);
 0238            }
 0239            catch (IOException ex)
 240            {
 0241                Logger.LogError(ex, "Error setting hidden attribute on {Path}", path);
 0242            }
 0243        }
 244
 245        private void Save(BaseItem item, Stream stream, string xmlPath)
 246        {
 0247            var settings = new XmlWriterSettings
 0248            {
 0249                Indent = true,
 0250                Encoding = Encoding.UTF8,
 0251                CloseOutput = false
 0252            };
 253
 0254            using (var writer = XmlWriter.Create(stream, settings))
 255            {
 0256                var root = GetRootElementName(item);
 257
 0258                writer.WriteStartDocument(true);
 259
 0260                writer.WriteStartElement(root);
 261
 0262                var baseItem = item;
 263
 0264                if (baseItem is not null)
 265                {
 0266                    AddCommonNodes(baseItem, writer, LibraryManager, UserManager, UserDataManager, ConfigurationManager)
 267                }
 268
 0269                WriteCustomElements(item, writer);
 270
 0271                if (baseItem is IHasMediaSources hasMediaSources)
 272                {
 0273                    AddMediaInfo(hasMediaSources, writer);
 274                }
 275
 0276                var tagsUsed = GetTagsUsed(item).ToList();
 277
 278                try
 279                {
 0280                    AddCustomTags(xmlPath, tagsUsed, writer, Logger);
 0281                }
 0282                catch (FileNotFoundException)
 283                {
 0284                }
 0285                catch (IOException)
 286                {
 0287                }
 0288                catch (XmlException ex)
 289                {
 0290                    Logger.LogError(ex, "Error reading existing nfo");
 0291                }
 292
 0293                writer.WriteEndElement();
 294
 0295                writer.WriteEndDocument();
 0296            }
 0297        }
 298
 299        protected abstract void WriteCustomElements(BaseItem item, XmlWriter writer);
 300
 301        public static void AddMediaInfo<T>(T item, XmlWriter writer)
 302            where T : IHasMediaSources
 303        {
 0304            writer.WriteStartElement("fileinfo");
 0305            writer.WriteStartElement("streamdetails");
 306
 0307            var mediaStreams = item.GetMediaStreams();
 308
 0309            foreach (var stream in mediaStreams)
 310            {
 0311                writer.WriteStartElement(stream.Type.ToString().ToLowerInvariant());
 312
 0313                if (!string.IsNullOrEmpty(stream.Codec))
 314                {
 0315                    var codec = stream.Codec;
 316
 0317                    if ((stream.CodecTag ?? string.Empty).Contains("xvid", StringComparison.OrdinalIgnoreCase))
 318                    {
 0319                        codec = "xvid";
 320                    }
 0321                    else if ((stream.CodecTag ?? string.Empty).Contains("divx", StringComparison.OrdinalIgnoreCase))
 322                    {
 0323                        codec = "divx";
 324                    }
 325
 0326                    writer.WriteElementString("codec", codec);
 0327                    writer.WriteElementString("micodec", codec);
 328                }
 329
 0330                if (stream.BitRate.HasValue)
 331                {
 0332                    writer.WriteElementString("bitrate", stream.BitRate.Value.ToString(CultureInfo.InvariantCulture));
 333                }
 334
 0335                if (stream.Width.HasValue)
 336                {
 0337                    writer.WriteElementString("width", stream.Width.Value.ToString(CultureInfo.InvariantCulture));
 338                }
 339
 0340                if (stream.Height.HasValue)
 341                {
 0342                    writer.WriteElementString("height", stream.Height.Value.ToString(CultureInfo.InvariantCulture));
 343                }
 344
 0345                if (!string.IsNullOrEmpty(stream.AspectRatio))
 346                {
 0347                    writer.WriteElementString("aspect", stream.AspectRatio);
 0348                    writer.WriteElementString("aspectratio", stream.AspectRatio);
 349                }
 350
 0351                var framerate = stream.ReferenceFrameRate;
 352
 0353                if (framerate.HasValue)
 354                {
 0355                    writer.WriteElementString("framerate", framerate.Value.ToString(CultureInfo.InvariantCulture));
 356                }
 357
 0358                if (!string.IsNullOrEmpty(stream.Language))
 359                {
 0360                    writer.WriteElementString("language", InvalidXMLCharsRegexRegex().Replace(stream.Language, string.Em
 361                }
 362
 0363                var scanType = stream.IsInterlaced ? "interlaced" : "progressive";
 0364                writer.WriteElementString("scantype", scanType);
 365
 0366                if (stream.Channels.HasValue)
 367                {
 0368                    writer.WriteElementString("channels", stream.Channels.Value.ToString(CultureInfo.InvariantCulture));
 369                }
 370
 0371                if (stream.SampleRate.HasValue)
 372                {
 0373                    writer.WriteElementString("samplingrate", stream.SampleRate.Value.ToString(CultureInfo.InvariantCult
 374                }
 375
 0376                writer.WriteElementString("default", stream.IsDefault.ToString(CultureInfo.InvariantCulture));
 0377                writer.WriteElementString("forced", stream.IsForced.ToString(CultureInfo.InvariantCulture));
 378
 0379                if (stream.Type == MediaStreamType.Video)
 380                {
 0381                    var runtimeTicks = item.RunTimeTicks;
 0382                    if (runtimeTicks.HasValue)
 383                    {
 0384                        var timespan = TimeSpan.FromTicks(runtimeTicks.Value);
 385
 0386                        writer.WriteElementString(
 0387                            "duration",
 0388                            Math.Floor(timespan.TotalMinutes).ToString(CultureInfo.InvariantCulture));
 0389                        writer.WriteElementString(
 0390                            "durationinseconds",
 0391                            Math.Floor(timespan.TotalSeconds).ToString(CultureInfo.InvariantCulture));
 392                    }
 393
 0394                    if (item is Video video)
 395                    {
 396                        // AddChapters(video, builder, itemRepository);
 397
 0398                        if (video.Video3DFormat.HasValue)
 399                        {
 0400                            switch (video.Video3DFormat.Value)
 401                            {
 402                                case Video3DFormat.FullSideBySide:
 0403                                    writer.WriteElementString("format3d", "FSBS");
 0404                                    break;
 405                                case Video3DFormat.FullTopAndBottom:
 0406                                    writer.WriteElementString("format3d", "FTAB");
 0407                                    break;
 408                                case Video3DFormat.HalfSideBySide:
 0409                                    writer.WriteElementString("format3d", "HSBS");
 0410                                    break;
 411                                case Video3DFormat.HalfTopAndBottom:
 0412                                    writer.WriteElementString("format3d", "HTAB");
 0413                                    break;
 414                                case Video3DFormat.MVC:
 0415                                    writer.WriteElementString("format3d", "MVC");
 416                                    break;
 417                            }
 418                        }
 419                    }
 420                }
 421
 0422                writer.WriteEndElement();
 423            }
 424
 0425            writer.WriteEndElement();
 0426            writer.WriteEndElement();
 0427        }
 428
 429        /// <summary>
 430        /// Adds the common nodes.
 431        /// </summary>
 432        private void AddCommonNodes(
 433            BaseItem item,
 434            XmlWriter writer,
 435            ILibraryManager libraryManager,
 436            IUserManager userManager,
 437            IUserDataManager userDataRepo,
 438            IServerConfigurationManager config)
 439        {
 0440            var writtenProviderIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 441
 0442            var overview = (item.Overview ?? string.Empty)
 0443                .StripHtml()
 0444                .Replace("&quot;", "'", StringComparison.Ordinal);
 445
 0446            var options = config.GetNfoConfiguration();
 447
 0448            if (item is MusicArtist)
 449            {
 0450                writer.WriteElementString("biography", overview);
 451            }
 0452            else if (item is MusicAlbum)
 453            {
 0454                writer.WriteElementString("review", overview);
 455            }
 456            else
 457            {
 0458                writer.WriteElementString("plot", overview);
 459            }
 460
 0461            if (item is not Video)
 462            {
 0463                writer.WriteElementString("outline", overview);
 464            }
 465
 0466            if (!string.IsNullOrWhiteSpace(item.CustomRating))
 467            {
 0468                writer.WriteElementString("customrating", item.CustomRating);
 469            }
 470
 0471            writer.WriteElementString("lockdata", item.IsLocked.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(
 472
 0473            if (item.LockedFields.Length > 0)
 474            {
 0475                writer.WriteElementString("lockedfields", string.Join('|', item.LockedFields));
 476            }
 477
 0478            writer.WriteElementString("dateadded", item.DateCreated.ToString(DateAddedFormat, CultureInfo.InvariantCultu
 479
 0480            writer.WriteElementString("title", item.Name ?? string.Empty);
 481
 0482            if (!string.IsNullOrWhiteSpace(item.OriginalTitle))
 483            {
 0484                writer.WriteElementString("originaltitle", item.OriginalTitle);
 485            }
 486
 0487            var people = libraryManager.GetPeople(item);
 488
 0489            var directors = people
 0490                .Where(i => i.IsType(PersonKind.Director))
 0491                .Select(i => i.Name?.Trim())
 0492                .Distinct(StringComparer.OrdinalIgnoreCase)
 0493                .OrderBy(i => i)
 0494                .ToList();
 495
 0496            foreach (var person in directors)
 497            {
 0498                writer.WriteElementString("director", person);
 499            }
 500
 0501            var writers = people
 0502                .Where(i => i.IsType(PersonKind.Writer))
 0503                .Select(i => i.Name?.Trim())
 0504                .Distinct(StringComparer.OrdinalIgnoreCase)
 0505                .OrderBy(i => i)
 0506                .ToList();
 507
 0508            foreach (var person in writers)
 509            {
 0510                writer.WriteElementString("writer", person);
 511            }
 512
 0513            foreach (var person in writers)
 514            {
 0515                writer.WriteElementString("credits", person);
 516            }
 517
 0518            foreach (var trailer in item.RemoteTrailers.OrderBy(t => t.Url?.Trim()))
 519            {
 0520                writer.WriteElementString("trailer", GetOutputTrailerUrl(trailer.Url));
 521            }
 522
 0523            if (item.CommunityRating.HasValue)
 524            {
 0525                writer.WriteElementString("rating", item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture));
 526            }
 527
 0528            if (item.ProductionYear.HasValue)
 529            {
 0530                writer.WriteElementString("year", item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture));
 531            }
 532
 0533            var forcedSortName = item.ForcedSortName;
 0534            if (!string.IsNullOrEmpty(forcedSortName))
 535            {
 0536                writer.WriteElementString("sorttitle", forcedSortName);
 537            }
 538
 0539            if (!string.IsNullOrEmpty(item.OfficialRating))
 540            {
 0541                writer.WriteElementString("mpaa", item.OfficialRating);
 542            }
 543
 0544            if (item is IHasAspectRatio hasAspectRatio
 0545                && !string.IsNullOrEmpty(hasAspectRatio.AspectRatio))
 546            {
 0547                writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio);
 548            }
 549
 0550            if (item.TryGetProviderId(MetadataProvider.TmdbCollection, out var tmdbCollection))
 551            {
 0552                writer.WriteElementString("collectionnumber", tmdbCollection);
 0553                writtenProviderIds.Add(MetadataProvider.TmdbCollection.ToString());
 554            }
 555
 0556            if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb))
 557            {
 0558                if (item is Series)
 559                {
 0560                    writer.WriteElementString("imdb_id", imdb);
 561                }
 562                else
 563                {
 0564                    writer.WriteElementString("imdbid", imdb);
 565                }
 566
 0567                writtenProviderIds.Add(MetadataProvider.Imdb.ToString());
 568            }
 569
 570            // Series xml saver already saves this
 0571            if (item is not Series)
 572            {
 0573                if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb))
 574                {
 0575                    writer.WriteElementString("tvdbid", tvdb);
 0576                    writtenProviderIds.Add(MetadataProvider.Tvdb.ToString());
 577                }
 578            }
 579
 0580            if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdb))
 581            {
 0582                writer.WriteElementString("tmdbid", tmdb);
 0583                writtenProviderIds.Add(MetadataProvider.Tmdb.ToString());
 584            }
 585
 0586            if (!string.IsNullOrEmpty(item.PreferredMetadataLanguage))
 587            {
 0588                writer.WriteElementString("language", item.PreferredMetadataLanguage);
 589            }
 590
 0591            if (!string.IsNullOrEmpty(item.PreferredMetadataCountryCode))
 592            {
 0593                writer.WriteElementString("countrycode", item.PreferredMetadataCountryCode);
 594            }
 595
 0596            if (item.PremiereDate.HasValue && item is not Episode)
 597            {
 0598                var formatString = options.ReleaseDateFormat;
 599
 0600                if (item is MusicArtist)
 601                {
 0602                    writer.WriteElementString(
 0603                        "formed",
 0604                        item.PremiereDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
 605                }
 606                else
 607                {
 0608                    writer.WriteElementString(
 0609                        "premiered",
 0610                        item.PremiereDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
 0611                    writer.WriteElementString(
 0612                        "releasedate",
 0613                        item.PremiereDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
 614                }
 615            }
 616
 0617            if (item.EndDate.HasValue)
 618            {
 0619                if (item is not Episode)
 620                {
 0621                    var formatString = options.ReleaseDateFormat;
 622
 0623                    writer.WriteElementString(
 0624                        "enddate",
 0625                        item.EndDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
 626                }
 627            }
 628
 0629            if (item.CriticRating.HasValue)
 630            {
 0631                writer.WriteElementString(
 0632                    "criticrating",
 0633                    item.CriticRating.Value.ToString(CultureInfo.InvariantCulture));
 634            }
 635
 0636            if (item is IHasDisplayOrder hasDisplayOrder)
 637            {
 0638                if (!string.IsNullOrEmpty(hasDisplayOrder.DisplayOrder))
 639                {
 0640                    writer.WriteElementString("displayorder", hasDisplayOrder.DisplayOrder);
 641                }
 642            }
 643
 644            // Use original runtime here, actual file runtime later in MediaInfo
 0645            var runTimeTicks = item.RunTimeTicks;
 646
 0647            if (runTimeTicks.HasValue)
 648            {
 0649                var timespan = TimeSpan.FromTicks(runTimeTicks.Value);
 650
 0651                writer.WriteElementString(
 0652                    "runtime",
 0653                    Convert.ToInt64(timespan.TotalMinutes).ToString(CultureInfo.InvariantCulture));
 654            }
 655
 0656            if (!string.IsNullOrWhiteSpace(item.Tagline))
 657            {
 0658                writer.WriteElementString("tagline", item.Tagline);
 659            }
 660
 0661            foreach (var country in item.ProductionLocations.Trimmed().OrderBy(country => country))
 662            {
 0663                writer.WriteElementString("country", country);
 664            }
 665
 0666            foreach (var genre in item.Genres.Trimmed().OrderBy(genre => genre))
 667            {
 0668                writer.WriteElementString("genre", genre);
 669            }
 670
 0671            foreach (var studio in item.Studios.Trimmed().OrderBy(studio => studio))
 672            {
 0673                writer.WriteElementString("studio", studio);
 674            }
 675
 0676            foreach (var tag in item.Tags.Trimmed().OrderBy(tag => tag))
 677            {
 0678                if (item is MusicAlbum || item is MusicArtist)
 679                {
 0680                    writer.WriteElementString("style", tag);
 681                }
 682                else
 683                {
 0684                    writer.WriteElementString("tag", tag);
 685                }
 686            }
 687
 0688            if (item.TryGetProviderId(MetadataProvider.AudioDbArtist, out var externalId))
 689            {
 0690                writer.WriteElementString("audiodbartistid", externalId);
 0691                writtenProviderIds.Add(MetadataProvider.AudioDbArtist.ToString());
 692            }
 693
 0694            if (item.TryGetProviderId(MetadataProvider.AudioDbAlbum, out externalId))
 695            {
 0696                writer.WriteElementString("audiodbalbumid", externalId);
 0697                writtenProviderIds.Add(MetadataProvider.AudioDbAlbum.ToString());
 698            }
 699
 0700            if (item.TryGetProviderId(MetadataProvider.Zap2It, out externalId))
 701            {
 0702                writer.WriteElementString("zap2itid", externalId);
 0703                writtenProviderIds.Add(MetadataProvider.Zap2It.ToString());
 704            }
 705
 0706            if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out externalId))
 707            {
 0708                writer.WriteElementString("musicbrainzalbumid", externalId);
 0709                writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbum.ToString());
 710            }
 711
 0712            if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out externalId))
 713            {
 0714                writer.WriteElementString("musicbrainzalbumartistid", externalId);
 0715                writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbumArtist.ToString());
 716            }
 717
 0718            if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out externalId))
 719            {
 0720                writer.WriteElementString("musicbrainzartistid", externalId);
 0721                writtenProviderIds.Add(MetadataProvider.MusicBrainzArtist.ToString());
 722            }
 723
 0724            if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out externalId))
 725            {
 0726                writer.WriteElementString("musicbrainzreleasegroupid", externalId);
 0727                writtenProviderIds.Add(MetadataProvider.MusicBrainzReleaseGroup.ToString());
 728            }
 729
 0730            if (item.TryGetProviderId(MetadataProvider.TvRage, out externalId))
 731            {
 0732                writer.WriteElementString("tvrageid", externalId);
 0733                writtenProviderIds.Add(MetadataProvider.TvRage.ToString());
 734            }
 735
 0736            if (item.ProviderIds is not null)
 737            {
 0738                foreach (var providerKey in item.ProviderIds.Keys.OrderBy(providerKey => providerKey))
 739                {
 0740                    var providerId = item.ProviderIds[providerKey];
 0741                    if (!string.IsNullOrEmpty(providerId) && !writtenProviderIds.Contains(providerKey))
 742                    {
 743                        try
 744                        {
 0745                            var tagName = GetTagForProviderKey(providerKey);
 0746                            Logger.LogDebug("Verifying custom provider tagname {0}", tagName);
 0747                            XmlConvert.VerifyName(tagName);
 0748                            Logger.LogDebug("Saving custom provider tagname {0}", tagName);
 749
 0750                            writer.WriteElementString(tagName, providerId);
 0751                        }
 0752                        catch (ArgumentException)
 753                        {
 754                            // catch invalid names without failing the entire operation
 0755                        }
 0756                        catch (XmlException)
 757                        {
 758                            // catch invalid names without failing the entire operation
 0759                        }
 760                    }
 761                }
 762            }
 763
 0764            if (options.SaveImagePathsInNfo)
 765            {
 0766                AddImages(item, writer, libraryManager);
 767            }
 768
 0769            AddUserData(item, writer, userManager, userDataRepo, options);
 770
 0771            if (item is not MusicAlbum && item is not MusicArtist)
 772            {
 0773                AddActors(people, writer, libraryManager, options.SaveImagePathsInNfo);
 774            }
 775
 0776            if (item is BoxSet folder)
 777            {
 0778                AddCollectionItems(folder, writer);
 779            }
 0780        }
 781
 782        private void AddCollectionItems(Folder item, XmlWriter writer)
 783        {
 0784            var linkedChildren = item.LinkedChildren
 0785                .Where(i => i.Type == LinkedChildType.Manual)
 0786                .ToList();
 787
 788            // Resolve ItemIds to paths and sort
 0789            var itemsWithPaths = linkedChildren
 0790                .Select(link =>
 0791                {
 0792                    if (link.ItemId.HasValue && !link.ItemId.Value.Equals(Guid.Empty))
 0793                    {
 0794                        var linkedItem = LibraryManager.GetItemById(link.ItemId.Value);
 0795                        return linkedItem?.Path;
 0796                    }
 0797
 0798                    return null;
 0799                })
 0800                .Where(path => !string.IsNullOrWhiteSpace(path))
 0801                .OrderBy(path => path?.Trim())
 0802                .ToList();
 803
 0804            foreach (var path in itemsWithPaths)
 805            {
 0806                writer.WriteStartElement("collectionitem");
 0807                writer.WriteElementString("path", path);
 0808                writer.WriteEndElement();
 809            }
 0810        }
 811
 812        /// <summary>
 813        /// Gets the output trailer URL.
 814        /// </summary>
 815        /// <param name="url">The URL.</param>
 816        /// <returns>System.String.</returns>
 817        private string GetOutputTrailerUrl(string url)
 818        {
 819            // This is what xbmc expects
 0820            return url.Replace(YouTubeWatchUrl, "plugin://plugin.video.youtube/play/?video_id=", StringComparison.Ordina
 821        }
 822
 823        private void AddImages(BaseItem item, XmlWriter writer, ILibraryManager libraryManager)
 824        {
 0825            writer.WriteStartElement("art");
 826
 0827            var image = item.GetImageInfo(ImageType.Primary, 0);
 828
 0829            if (image is not null)
 830            {
 0831                writer.WriteElementString("poster", GetImagePathToSave(image, libraryManager));
 832            }
 833
 0834            foreach (var backdrop in item.GetImages(ImageType.Backdrop).OrderBy(b => b.Path?.Trim()))
 835            {
 0836                writer.WriteElementString("fanart", GetImagePathToSave(backdrop, libraryManager));
 837            }
 838
 0839            writer.WriteEndElement();
 0840        }
 841
 842        private void AddUserData(BaseItem item, XmlWriter writer, IUserManager userManager, IUserDataManager userDataRep
 843        {
 0844            var userId = options.UserId;
 0845            if (string.IsNullOrWhiteSpace(userId))
 846            {
 0847                return;
 848            }
 849
 0850            var user = userManager.GetUserById(Guid.Parse(userId));
 851
 0852            if (user is null)
 853            {
 0854                return;
 855            }
 856
 0857            if (item.IsFolder)
 858            {
 0859                return;
 860            }
 861
 0862            var userdata = userDataRepo.GetUserData(user, item);
 863
 0864            if (userdata is not null)
 865            {
 0866                writer.WriteElementString(
 0867                "isuserfavorite",
 0868                userdata.IsFavorite.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
 869
 0870                if (userdata.Rating.HasValue)
 871                {
 0872                    writer.WriteElementString(
 0873                        "userrating",
 0874                        userdata.Rating.Value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
 875                }
 876
 0877                if (!item.IsFolder)
 878                {
 0879                    writer.WriteElementString(
 0880                        "playcount",
 0881                        userdata.PlayCount.ToString(CultureInfo.InvariantCulture));
 0882                    writer.WriteElementString(
 0883                        "watched",
 0884                        userdata.Played.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
 885
 0886                    if (userdata.LastPlayedDate.HasValue)
 887                    {
 0888                        writer.WriteElementString(
 0889                            "lastplayed",
 0890                            userdata.LastPlayedDate.Value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture).
 891                    }
 892
 0893                    writer.WriteStartElement("resume");
 894
 0895                    var runTimeTicks = item.RunTimeTicks ?? 0;
 896
 0897                    writer.WriteElementString(
 0898                        "position",
 0899                        TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds.ToString(CultureInfo.InvariantCu
 0900                    writer.WriteElementString(
 0901                        "total",
 0902                        TimeSpan.FromTicks(runTimeTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
 903                }
 904            }
 905
 0906            writer.WriteEndElement();
 0907        }
 908
 909        private void AddActors(IReadOnlyList<PersonInfo> people, XmlWriter writer, ILibraryManager libraryManager, bool 
 910        {
 0911            foreach (var person in people
 0912                .OrderBy(person => person.SortOrder ?? 0)
 0913                .ThenBy(person => person.Name?.Trim()))
 914            {
 0915                if (person.IsType(PersonKind.Director) || person.IsType(PersonKind.Writer))
 916                {
 917                    continue;
 918                }
 919
 0920                writer.WriteStartElement("actor");
 921
 0922                if (!string.IsNullOrWhiteSpace(person.Name))
 923                {
 0924                    writer.WriteElementString("name", person.Name);
 925                }
 926
 0927                if (!string.IsNullOrWhiteSpace(person.Role))
 928                {
 0929                    writer.WriteElementString("role", person.Role);
 930                }
 931
 0932                if (person.Type != PersonKind.Unknown)
 933                {
 0934                    writer.WriteElementString("type", person.Type.ToString());
 935                }
 936
 0937                if (person.SortOrder.HasValue)
 938                {
 0939                    writer.WriteElementString(
 0940                        "sortorder",
 0941                        person.SortOrder.Value.ToString(CultureInfo.InvariantCulture));
 942                }
 943
 0944                if (saveImagePath)
 945                {
 0946                    var personEntity = libraryManager.GetPerson(person.Name);
 0947                    var image = personEntity?.GetImageInfo(ImageType.Primary, 0);
 948
 0949                    if (image is not null)
 950                    {
 0951                        writer.WriteElementString(
 0952                            "thumb",
 0953                            GetImagePathToSave(image, libraryManager));
 954                    }
 955                }
 956
 0957                writer.WriteEndElement();
 958            }
 0959        }
 960
 961        private string GetImagePathToSave(ItemImageInfo image, ILibraryManager libraryManager)
 962        {
 0963            if (!image.IsLocalFile)
 964            {
 0965                return image.Path;
 966            }
 967
 0968            return libraryManager.GetPathAfterNetworkSubstitution(image.Path);
 969        }
 970
 971        private void AddCustomTags(string path, IReadOnlyCollection<string> xmlTagsUsed, XmlWriter writer, ILogger<BaseN
 972        {
 0973            var settings = new XmlReaderSettings()
 0974            {
 0975                ValidationType = ValidationType.None,
 0976                CheckCharacters = false,
 0977                IgnoreProcessingInstructions = true,
 0978                IgnoreComments = true
 0979            };
 980
 0981            using (var fileStream = File.OpenRead(path))
 0982            using (var streamReader = new StreamReader(fileStream, Encoding.UTF8))
 0983            using (var reader = XmlReader.Create(streamReader, settings))
 984            {
 985                try
 986                {
 0987                    reader.MoveToContent();
 0988                }
 0989                catch (Exception ex)
 990                {
 0991                    logger.LogError(ex, "Error reading existing xml tags from {Path}.", path);
 0992                    return;
 993                }
 994
 0995                reader.Read();
 996
 997                // Loop through each element
 0998                while (!reader.EOF && reader.ReadState == ReadState.Interactive)
 999                {
 01000                    if (reader.NodeType == XmlNodeType.Element)
 1001                    {
 01002                        var name = reader.Name;
 1003
 01004                        if (!_commonTags.Contains(name)
 01005                            && !xmlTagsUsed.Contains(name, StringComparison.OrdinalIgnoreCase))
 1006                        {
 01007                            writer.WriteNode(reader, false);
 1008                        }
 1009                        else
 1010                        {
 01011                            reader.Skip();
 1012                        }
 1013                    }
 1014                    else
 1015                    {
 01016                        reader.Read();
 1017                    }
 1018                }
 01019            }
 01020        }
 1021
 1022        private string GetTagForProviderKey(string providerKey)
 01023            => providerKey.ToLowerInvariant() + "id";
 1024
 1025        protected static string SortNameOrName(BaseItem item)
 1026        {
 01027            if (item is null)
 1028            {
 01029                return string.Empty;
 1030            }
 1031
 01032            if (item.SortName is not null)
 1033            {
 01034                string trimmed = item.SortName.Trim();
 01035                if (trimmed.Length > 0)
 1036                {
 01037                    return trimmed;
 1038                }
 1039            }
 1040
 01041            return (item.Name ?? string.Empty).Trim();
 1042        }
 1043    }
 1044}

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)