< 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: 495
Coverable lines: 496
Total lines: 1055
Line coverage: 0.2%
Branch coverage
0%
Covered branches: 0
Total branches: 238
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/13/2026 - 12:11:21 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: 1055 2/13/2026 - 12:11:21 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: 1055

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%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).ConfigureAwait(false);
 0202            }
 0203        }
 204
 205        private async Task SaveToFileAsync(Stream stream, string path)
 206        {
 0207            var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not v
 0208            Directory.CreateDirectory(directory);
 209
 210            // On Windows, saving the file will fail if the file is hidden or readonly
 0211            FileSystem.SetAttributes(path, false, false);
 212
 0213            var fileStreamOptions = new FileStreamOptions()
 0214            {
 0215                Mode = FileMode.Create,
 0216                Access = FileAccess.Write,
 0217                Share = FileShare.None,
 0218                PreallocationSize = stream.Length,
 0219                Options = FileOptions.Asynchronous
 0220            };
 221
 0222            var filestream = new FileStream(path, fileStreamOptions);
 0223            await using (filestream.ConfigureAwait(false))
 224            {
 0225                await stream.CopyToAsync(filestream).ConfigureAwait(false);
 226            }
 227
 0228            if (ConfigurationManager.Configuration.SaveMetadataHidden)
 229            {
 0230                SetHidden(path, true);
 231            }
 0232        }
 233
 234        private void SetHidden(string path, bool hidden)
 235        {
 236            try
 237            {
 0238                FileSystem.SetHidden(path, hidden);
 0239            }
 0240            catch (IOException ex)
 241            {
 0242                Logger.LogError(ex, "Error setting hidden attribute on {Path}", path);
 0243            }
 0244        }
 245
 246        private void Save(BaseItem item, Stream stream, string xmlPath)
 247        {
 0248            var settings = new XmlWriterSettings
 0249            {
 0250                Indent = true,
 0251                Encoding = Encoding.UTF8,
 0252                CloseOutput = false
 0253            };
 254
 0255            using (var writer = XmlWriter.Create(stream, settings))
 256            {
 0257                var root = GetRootElementName(item);
 258
 0259                writer.WriteStartDocument(true);
 260
 0261                writer.WriteStartElement(root);
 262
 0263                var baseItem = item;
 264
 0265                if (baseItem is not null)
 266                {
 0267                    AddCommonNodes(baseItem, writer, LibraryManager, UserManager, UserDataManager, ConfigurationManager)
 268                }
 269
 0270                WriteCustomElements(item, writer);
 271
 0272                if (baseItem is IHasMediaSources hasMediaSources)
 273                {
 0274                    AddMediaInfo(hasMediaSources, writer);
 275                }
 276
 0277                var tagsUsed = GetTagsUsed(item).ToList();
 278
 279                try
 280                {
 0281                    AddCustomTags(xmlPath, tagsUsed, writer, Logger);
 0282                }
 0283                catch (FileNotFoundException)
 284                {
 0285                }
 0286                catch (IOException)
 287                {
 0288                }
 0289                catch (XmlException ex)
 290                {
 0291                    Logger.LogError(ex, "Error reading existing nfo");
 0292                }
 293
 0294                writer.WriteEndElement();
 295
 0296                writer.WriteEndDocument();
 0297            }
 0298        }
 299
 300        protected abstract void WriteCustomElements(BaseItem item, XmlWriter writer);
 301
 302        public static void AddMediaInfo<T>(T item, XmlWriter writer)
 303            where T : IHasMediaSources
 304        {
 0305            writer.WriteStartElement("fileinfo");
 0306            writer.WriteStartElement("streamdetails");
 307
 0308            var mediaStreams = item.GetMediaStreams();
 309
 0310            foreach (var stream in mediaStreams)
 311            {
 0312                writer.WriteStartElement(stream.Type.ToString().ToLowerInvariant());
 313
 0314                if (!string.IsNullOrEmpty(stream.Codec))
 315                {
 0316                    var codec = stream.Codec;
 317
 0318                    if ((stream.CodecTag ?? string.Empty).Contains("xvid", StringComparison.OrdinalIgnoreCase))
 319                    {
 0320                        codec = "xvid";
 321                    }
 0322                    else if ((stream.CodecTag ?? string.Empty).Contains("divx", StringComparison.OrdinalIgnoreCase))
 323                    {
 0324                        codec = "divx";
 325                    }
 326
 0327                    writer.WriteElementString("codec", codec);
 0328                    writer.WriteElementString("micodec", codec);
 329                }
 330
 0331                if (stream.BitRate.HasValue)
 332                {
 0333                    writer.WriteElementString("bitrate", stream.BitRate.Value.ToString(CultureInfo.InvariantCulture));
 334                }
 335
 0336                if (stream.Width.HasValue)
 337                {
 0338                    writer.WriteElementString("width", stream.Width.Value.ToString(CultureInfo.InvariantCulture));
 339                }
 340
 0341                if (stream.Height.HasValue)
 342                {
 0343                    writer.WriteElementString("height", stream.Height.Value.ToString(CultureInfo.InvariantCulture));
 344                }
 345
 0346                if (!string.IsNullOrEmpty(stream.AspectRatio))
 347                {
 0348                    writer.WriteElementString("aspect", stream.AspectRatio);
 0349                    writer.WriteElementString("aspectratio", stream.AspectRatio);
 350                }
 351
 0352                var framerate = stream.ReferenceFrameRate;
 353
 0354                if (framerate.HasValue)
 355                {
 0356                    writer.WriteElementString("framerate", framerate.Value.ToString(CultureInfo.InvariantCulture));
 357                }
 358
 0359                if (!string.IsNullOrEmpty(stream.Language))
 360                {
 0361                    writer.WriteElementString("language", InvalidXMLCharsRegexRegex().Replace(stream.Language, string.Em
 362                }
 363
 0364                var scanType = stream.IsInterlaced ? "interlaced" : "progressive";
 0365                writer.WriteElementString("scantype", scanType);
 366
 0367                if (stream.Channels.HasValue)
 368                {
 0369                    writer.WriteElementString("channels", stream.Channels.Value.ToString(CultureInfo.InvariantCulture));
 370                }
 371
 0372                if (stream.SampleRate.HasValue)
 373                {
 0374                    writer.WriteElementString("samplingrate", stream.SampleRate.Value.ToString(CultureInfo.InvariantCult
 375                }
 376
 0377                writer.WriteElementString("default", stream.IsDefault.ToString(CultureInfo.InvariantCulture));
 0378                writer.WriteElementString("forced", stream.IsForced.ToString(CultureInfo.InvariantCulture));
 379
 0380                if (stream.IsOriginal)
 381                {
 0382                    writer.WriteElementString("original", stream.IsOriginal.ToString(CultureInfo.InvariantCulture));
 383                }
 384
 0385                if (stream.Type == MediaStreamType.Video)
 386                {
 0387                    var runtimeTicks = item.RunTimeTicks;
 0388                    if (runtimeTicks.HasValue)
 389                    {
 0390                        var timespan = TimeSpan.FromTicks(runtimeTicks.Value);
 391
 0392                        writer.WriteElementString(
 0393                            "duration",
 0394                            Math.Floor(timespan.TotalMinutes).ToString(CultureInfo.InvariantCulture));
 0395                        writer.WriteElementString(
 0396                            "durationinseconds",
 0397                            Math.Floor(timespan.TotalSeconds).ToString(CultureInfo.InvariantCulture));
 398                    }
 399
 0400                    if (item is Video video)
 401                    {
 402                        // AddChapters(video, builder, itemRepository);
 403
 0404                        if (video.Video3DFormat.HasValue)
 405                        {
 0406                            switch (video.Video3DFormat.Value)
 407                            {
 408                                case Video3DFormat.FullSideBySide:
 0409                                    writer.WriteElementString("format3d", "FSBS");
 0410                                    break;
 411                                case Video3DFormat.FullTopAndBottom:
 0412                                    writer.WriteElementString("format3d", "FTAB");
 0413                                    break;
 414                                case Video3DFormat.HalfSideBySide:
 0415                                    writer.WriteElementString("format3d", "HSBS");
 0416                                    break;
 417                                case Video3DFormat.HalfTopAndBottom:
 0418                                    writer.WriteElementString("format3d", "HTAB");
 0419                                    break;
 420                                case Video3DFormat.MVC:
 0421                                    writer.WriteElementString("format3d", "MVC");
 422                                    break;
 423                            }
 424                        }
 425                    }
 426                }
 427
 0428                writer.WriteEndElement();
 429            }
 430
 0431            writer.WriteEndElement();
 0432            writer.WriteEndElement();
 0433        }
 434
 435        /// <summary>
 436        /// Adds the common nodes.
 437        /// </summary>
 438        private void AddCommonNodes(
 439            BaseItem item,
 440            XmlWriter writer,
 441            ILibraryManager libraryManager,
 442            IUserManager userManager,
 443            IUserDataManager userDataRepo,
 444            IServerConfigurationManager config)
 445        {
 0446            var writtenProviderIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 447
 0448            var overview = (item.Overview ?? string.Empty)
 0449                .StripHtml()
 0450                .Replace("&quot;", "'", StringComparison.Ordinal);
 451
 0452            var options = config.GetNfoConfiguration();
 453
 0454            if (item is MusicArtist)
 455            {
 0456                writer.WriteElementString("biography", overview);
 457            }
 0458            else if (item is MusicAlbum)
 459            {
 0460                writer.WriteElementString("review", overview);
 461            }
 462            else
 463            {
 0464                writer.WriteElementString("plot", overview);
 465            }
 466
 0467            if (item is not Video)
 468            {
 0469                writer.WriteElementString("outline", overview);
 470            }
 471
 0472            if (!string.IsNullOrWhiteSpace(item.CustomRating))
 473            {
 0474                writer.WriteElementString("customrating", item.CustomRating);
 475            }
 476
 0477            writer.WriteElementString("lockdata", item.IsLocked.ToString(CultureInfo.InvariantCulture).ToLowerInvariant(
 478
 0479            if (item.LockedFields.Length > 0)
 480            {
 0481                writer.WriteElementString("lockedfields", string.Join('|', item.LockedFields));
 482            }
 483
 0484            writer.WriteElementString("dateadded", item.DateCreated.ToString(DateAddedFormat, CultureInfo.InvariantCultu
 485
 0486            writer.WriteElementString("title", item.Name ?? string.Empty);
 487
 0488            if (!string.IsNullOrWhiteSpace(item.OriginalTitle))
 489            {
 0490                writer.WriteElementString("originaltitle", item.OriginalTitle);
 491            }
 492
 0493            if (!string.IsNullOrWhiteSpace(item.OriginalLanguage))
 494            {
 0495                writer.WriteElementString("originallanguage", item.OriginalLanguage);
 496            }
 497
 0498            var people = libraryManager.GetPeople(item);
 499
 0500            var directors = people
 0501                .Where(i => i.IsType(PersonKind.Director))
 0502                .Select(i => i.Name?.Trim())
 0503                .Distinct(StringComparer.OrdinalIgnoreCase)
 0504                .OrderBy(i => i)
 0505                .ToList();
 506
 0507            foreach (var person in directors)
 508            {
 0509                writer.WriteElementString("director", person);
 510            }
 511
 0512            var writers = people
 0513                .Where(i => i.IsType(PersonKind.Writer))
 0514                .Select(i => i.Name?.Trim())
 0515                .Distinct(StringComparer.OrdinalIgnoreCase)
 0516                .OrderBy(i => i)
 0517                .ToList();
 518
 0519            foreach (var person in writers)
 520            {
 0521                writer.WriteElementString("writer", person);
 522            }
 523
 0524            foreach (var person in writers)
 525            {
 0526                writer.WriteElementString("credits", person);
 527            }
 528
 0529            foreach (var trailer in item.RemoteTrailers.OrderBy(t => t.Url?.Trim()))
 530            {
 0531                writer.WriteElementString("trailer", GetOutputTrailerUrl(trailer.Url));
 532            }
 533
 0534            if (item.CommunityRating.HasValue)
 535            {
 0536                writer.WriteElementString("rating", item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture));
 537            }
 538
 0539            if (item.ProductionYear.HasValue)
 540            {
 0541                writer.WriteElementString("year", item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture));
 542            }
 543
 0544            var forcedSortName = item.ForcedSortName;
 0545            if (!string.IsNullOrEmpty(forcedSortName))
 546            {
 0547                writer.WriteElementString("sorttitle", forcedSortName);
 548            }
 549
 0550            if (!string.IsNullOrEmpty(item.OfficialRating))
 551            {
 0552                writer.WriteElementString("mpaa", item.OfficialRating);
 553            }
 554
 0555            if (item is IHasAspectRatio hasAspectRatio
 0556                && !string.IsNullOrEmpty(hasAspectRatio.AspectRatio))
 557            {
 0558                writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio);
 559            }
 560
 0561            if (item.TryGetProviderId(MetadataProvider.TmdbCollection, out var tmdbCollection))
 562            {
 0563                writer.WriteElementString("collectionnumber", tmdbCollection);
 0564                writtenProviderIds.Add(MetadataProvider.TmdbCollection.ToString());
 565            }
 566
 0567            if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb))
 568            {
 0569                if (item is Series)
 570                {
 0571                    writer.WriteElementString("imdb_id", imdb);
 572                }
 573                else
 574                {
 0575                    writer.WriteElementString("imdbid", imdb);
 576                }
 577
 0578                writtenProviderIds.Add(MetadataProvider.Imdb.ToString());
 579            }
 580
 581            // Series xml saver already saves this
 0582            if (item is not Series)
 583            {
 0584                if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb))
 585                {
 0586                    writer.WriteElementString("tvdbid", tvdb);
 0587                    writtenProviderIds.Add(MetadataProvider.Tvdb.ToString());
 588                }
 589            }
 590
 0591            if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdb))
 592            {
 0593                writer.WriteElementString("tmdbid", tmdb);
 0594                writtenProviderIds.Add(MetadataProvider.Tmdb.ToString());
 595            }
 596
 0597            if (!string.IsNullOrEmpty(item.PreferredMetadataLanguage))
 598            {
 0599                writer.WriteElementString("language", item.PreferredMetadataLanguage);
 600            }
 601
 0602            if (!string.IsNullOrEmpty(item.PreferredMetadataCountryCode))
 603            {
 0604                writer.WriteElementString("countrycode", item.PreferredMetadataCountryCode);
 605            }
 606
 0607            if (item.PremiereDate.HasValue && item is not Episode)
 608            {
 0609                var formatString = options.ReleaseDateFormat;
 610
 0611                if (item is MusicArtist)
 612                {
 0613                    writer.WriteElementString(
 0614                        "formed",
 0615                        item.PremiereDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
 616                }
 617                else
 618                {
 0619                    writer.WriteElementString(
 0620                        "premiered",
 0621                        item.PremiereDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
 0622                    writer.WriteElementString(
 0623                        "releasedate",
 0624                        item.PremiereDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
 625                }
 626            }
 627
 0628            if (item.EndDate.HasValue)
 629            {
 0630                if (item is not Episode)
 631                {
 0632                    var formatString = options.ReleaseDateFormat;
 633
 0634                    writer.WriteElementString(
 0635                        "enddate",
 0636                        item.EndDate.Value.ToString(formatString, CultureInfo.InvariantCulture));
 637                }
 638            }
 639
 0640            if (item.CriticRating.HasValue)
 641            {
 0642                writer.WriteElementString(
 0643                    "criticrating",
 0644                    item.CriticRating.Value.ToString(CultureInfo.InvariantCulture));
 645            }
 646
 0647            if (item is IHasDisplayOrder hasDisplayOrder)
 648            {
 0649                if (!string.IsNullOrEmpty(hasDisplayOrder.DisplayOrder))
 650                {
 0651                    writer.WriteElementString("displayorder", hasDisplayOrder.DisplayOrder);
 652                }
 653            }
 654
 655            // Use original runtime here, actual file runtime later in MediaInfo
 0656            var runTimeTicks = item.RunTimeTicks;
 657
 0658            if (runTimeTicks.HasValue)
 659            {
 0660                var timespan = TimeSpan.FromTicks(runTimeTicks.Value);
 661
 0662                writer.WriteElementString(
 0663                    "runtime",
 0664                    Convert.ToInt64(timespan.TotalMinutes).ToString(CultureInfo.InvariantCulture));
 665            }
 666
 0667            if (!string.IsNullOrWhiteSpace(item.Tagline))
 668            {
 0669                writer.WriteElementString("tagline", item.Tagline);
 670            }
 671
 0672            foreach (var country in item.ProductionLocations.Trimmed().OrderBy(country => country))
 673            {
 0674                writer.WriteElementString("country", country);
 675            }
 676
 0677            foreach (var genre in item.Genres.Trimmed().OrderBy(genre => genre))
 678            {
 0679                writer.WriteElementString("genre", genre);
 680            }
 681
 0682            foreach (var studio in item.Studios.Trimmed().OrderBy(studio => studio))
 683            {
 0684                writer.WriteElementString("studio", studio);
 685            }
 686
 0687            foreach (var tag in item.Tags.Trimmed().OrderBy(tag => tag))
 688            {
 0689                if (item is MusicAlbum || item is MusicArtist)
 690                {
 0691                    writer.WriteElementString("style", tag);
 692                }
 693                else
 694                {
 0695                    writer.WriteElementString("tag", tag);
 696                }
 697            }
 698
 0699            if (item.TryGetProviderId(MetadataProvider.AudioDbArtist, out var externalId))
 700            {
 0701                writer.WriteElementString("audiodbartistid", externalId);
 0702                writtenProviderIds.Add(MetadataProvider.AudioDbArtist.ToString());
 703            }
 704
 0705            if (item.TryGetProviderId(MetadataProvider.AudioDbAlbum, out externalId))
 706            {
 0707                writer.WriteElementString("audiodbalbumid", externalId);
 0708                writtenProviderIds.Add(MetadataProvider.AudioDbAlbum.ToString());
 709            }
 710
 0711            if (item.TryGetProviderId(MetadataProvider.Zap2It, out externalId))
 712            {
 0713                writer.WriteElementString("zap2itid", externalId);
 0714                writtenProviderIds.Add(MetadataProvider.Zap2It.ToString());
 715            }
 716
 0717            if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out externalId))
 718            {
 0719                writer.WriteElementString("musicbrainzalbumid", externalId);
 0720                writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbum.ToString());
 721            }
 722
 0723            if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out externalId))
 724            {
 0725                writer.WriteElementString("musicbrainzalbumartistid", externalId);
 0726                writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbumArtist.ToString());
 727            }
 728
 0729            if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out externalId))
 730            {
 0731                writer.WriteElementString("musicbrainzartistid", externalId);
 0732                writtenProviderIds.Add(MetadataProvider.MusicBrainzArtist.ToString());
 733            }
 734
 0735            if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out externalId))
 736            {
 0737                writer.WriteElementString("musicbrainzreleasegroupid", externalId);
 0738                writtenProviderIds.Add(MetadataProvider.MusicBrainzReleaseGroup.ToString());
 739            }
 740
 0741            if (item.TryGetProviderId(MetadataProvider.TvRage, out externalId))
 742            {
 0743                writer.WriteElementString("tvrageid", externalId);
 0744                writtenProviderIds.Add(MetadataProvider.TvRage.ToString());
 745            }
 746
 0747            if (item.ProviderIds is not null)
 748            {
 0749                foreach (var providerKey in item.ProviderIds.Keys.OrderBy(providerKey => providerKey))
 750                {
 0751                    var providerId = item.ProviderIds[providerKey];
 0752                    if (!string.IsNullOrEmpty(providerId) && !writtenProviderIds.Contains(providerKey))
 753                    {
 754                        try
 755                        {
 0756                            var tagName = GetTagForProviderKey(providerKey);
 0757                            Logger.LogDebug("Verifying custom provider tagname {0}", tagName);
 0758                            XmlConvert.VerifyName(tagName);
 0759                            Logger.LogDebug("Saving custom provider tagname {0}", tagName);
 760
 0761                            writer.WriteElementString(tagName, providerId);
 0762                        }
 0763                        catch (ArgumentException)
 764                        {
 765                            // catch invalid names without failing the entire operation
 0766                        }
 0767                        catch (XmlException)
 768                        {
 769                            // catch invalid names without failing the entire operation
 0770                        }
 771                    }
 772                }
 773            }
 774
 0775            if (options.SaveImagePathsInNfo)
 776            {
 0777                AddImages(item, writer, libraryManager);
 778            }
 779
 0780            AddUserData(item, writer, userManager, userDataRepo, options);
 781
 0782            if (item is not MusicAlbum && item is not MusicArtist)
 783            {
 0784                AddActors(people, writer, libraryManager, options.SaveImagePathsInNfo);
 785            }
 786
 0787            if (item is BoxSet folder)
 788            {
 0789                AddCollectionItems(folder, writer);
 790            }
 0791        }
 792
 793        private void AddCollectionItems(Folder item, XmlWriter writer)
 794        {
 0795            var linkedChildren = item.LinkedChildren
 0796                .Where(i => i.Type == LinkedChildType.Manual)
 0797                .ToList();
 798
 799            // Resolve ItemIds to paths and sort
 0800            var itemsWithPaths = linkedChildren
 0801                .Select(link =>
 0802                {
 0803                    if (link.ItemId.HasValue && !link.ItemId.Value.Equals(Guid.Empty))
 0804                    {
 0805                        var linkedItem = LibraryManager.GetItemById(link.ItemId.Value);
 0806                        return linkedItem?.Path;
 0807                    }
 0808
 0809                    return null;
 0810                })
 0811                .Where(path => !string.IsNullOrWhiteSpace(path))
 0812                .OrderBy(path => path?.Trim())
 0813                .ToList();
 814
 0815            foreach (var path in itemsWithPaths)
 816            {
 0817                writer.WriteStartElement("collectionitem");
 0818                writer.WriteElementString("path", path);
 0819                writer.WriteEndElement();
 820            }
 0821        }
 822
 823        /// <summary>
 824        /// Gets the output trailer URL.
 825        /// </summary>
 826        /// <param name="url">The URL.</param>
 827        /// <returns>System.String.</returns>
 828        private string GetOutputTrailerUrl(string url)
 829        {
 830            // This is what xbmc expects
 0831            return url.Replace(YouTubeWatchUrl, "plugin://plugin.video.youtube/play/?video_id=", StringComparison.Ordina
 832        }
 833
 834        private void AddImages(BaseItem item, XmlWriter writer, ILibraryManager libraryManager)
 835        {
 0836            writer.WriteStartElement("art");
 837
 0838            var image = item.GetImageInfo(ImageType.Primary, 0);
 839
 0840            if (image is not null)
 841            {
 0842                writer.WriteElementString("poster", GetImagePathToSave(image, libraryManager));
 843            }
 844
 0845            foreach (var backdrop in item.GetImages(ImageType.Backdrop).OrderBy(b => b.Path?.Trim()))
 846            {
 0847                writer.WriteElementString("fanart", GetImagePathToSave(backdrop, libraryManager));
 848            }
 849
 0850            writer.WriteEndElement();
 0851        }
 852
 853        private void AddUserData(BaseItem item, XmlWriter writer, IUserManager userManager, IUserDataManager userDataRep
 854        {
 0855            var userId = options.UserId;
 0856            if (string.IsNullOrWhiteSpace(userId))
 857            {
 0858                return;
 859            }
 860
 0861            var user = userManager.GetUserById(Guid.Parse(userId));
 862
 0863            if (user is null)
 864            {
 0865                return;
 866            }
 867
 0868            if (item.IsFolder)
 869            {
 0870                return;
 871            }
 872
 0873            var userdata = userDataRepo.GetUserData(user, item);
 874
 0875            if (userdata is not null)
 876            {
 0877                writer.WriteElementString(
 0878                "isuserfavorite",
 0879                userdata.IsFavorite.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
 880
 0881                if (userdata.Rating.HasValue)
 882                {
 0883                    writer.WriteElementString(
 0884                        "userrating",
 0885                        userdata.Rating.Value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
 886                }
 887
 0888                if (!item.IsFolder)
 889                {
 0890                    writer.WriteElementString(
 0891                        "playcount",
 0892                        userdata.PlayCount.ToString(CultureInfo.InvariantCulture));
 0893                    writer.WriteElementString(
 0894                        "watched",
 0895                        userdata.Played.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
 896
 0897                    if (userdata.LastPlayedDate.HasValue)
 898                    {
 0899                        writer.WriteElementString(
 0900                            "lastplayed",
 0901                            userdata.LastPlayedDate.Value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture).
 902                    }
 903
 0904                    writer.WriteStartElement("resume");
 905
 0906                    var runTimeTicks = item.RunTimeTicks ?? 0;
 907
 0908                    writer.WriteElementString(
 0909                        "position",
 0910                        TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds.ToString(CultureInfo.InvariantCu
 0911                    writer.WriteElementString(
 0912                        "total",
 0913                        TimeSpan.FromTicks(runTimeTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
 914                }
 915            }
 916
 0917            writer.WriteEndElement();
 0918        }
 919
 920        private void AddActors(IReadOnlyList<PersonInfo> people, XmlWriter writer, ILibraryManager libraryManager, bool 
 921        {
 0922            foreach (var person in people
 0923                .OrderBy(person => person.SortOrder ?? 0)
 0924                .ThenBy(person => person.Name?.Trim()))
 925            {
 0926                if (person.IsType(PersonKind.Director) || person.IsType(PersonKind.Writer))
 927                {
 928                    continue;
 929                }
 930
 0931                writer.WriteStartElement("actor");
 932
 0933                if (!string.IsNullOrWhiteSpace(person.Name))
 934                {
 0935                    writer.WriteElementString("name", person.Name);
 936                }
 937
 0938                if (!string.IsNullOrWhiteSpace(person.Role))
 939                {
 0940                    writer.WriteElementString("role", person.Role);
 941                }
 942
 0943                if (person.Type != PersonKind.Unknown)
 944                {
 0945                    writer.WriteElementString("type", person.Type.ToString());
 946                }
 947
 0948                if (person.SortOrder.HasValue)
 949                {
 0950                    writer.WriteElementString(
 0951                        "sortorder",
 0952                        person.SortOrder.Value.ToString(CultureInfo.InvariantCulture));
 953                }
 954
 0955                if (saveImagePath)
 956                {
 0957                    var personEntity = libraryManager.GetPerson(person.Name);
 0958                    var image = personEntity?.GetImageInfo(ImageType.Primary, 0);
 959
 0960                    if (image is not null)
 961                    {
 0962                        writer.WriteElementString(
 0963                            "thumb",
 0964                            GetImagePathToSave(image, libraryManager));
 965                    }
 966                }
 967
 0968                writer.WriteEndElement();
 969            }
 0970        }
 971
 972        private string GetImagePathToSave(ItemImageInfo image, ILibraryManager libraryManager)
 973        {
 0974            if (!image.IsLocalFile)
 975            {
 0976                return image.Path;
 977            }
 978
 0979            return libraryManager.GetPathAfterNetworkSubstitution(image.Path);
 980        }
 981
 982        private void AddCustomTags(string path, IReadOnlyCollection<string> xmlTagsUsed, XmlWriter writer, ILogger<BaseN
 983        {
 0984            var settings = new XmlReaderSettings()
 0985            {
 0986                ValidationType = ValidationType.None,
 0987                CheckCharacters = false,
 0988                IgnoreProcessingInstructions = true,
 0989                IgnoreComments = true
 0990            };
 991
 0992            using (var fileStream = File.OpenRead(path))
 0993            using (var streamReader = new StreamReader(fileStream, Encoding.UTF8))
 0994            using (var reader = XmlReader.Create(streamReader, settings))
 995            {
 996                try
 997                {
 0998                    reader.MoveToContent();
 0999                }
 01000                catch (Exception ex)
 1001                {
 01002                    logger.LogError(ex, "Error reading existing xml tags from {Path}.", path);
 01003                    return;
 1004                }
 1005
 01006                reader.Read();
 1007
 1008                // Loop through each element
 01009                while (!reader.EOF && reader.ReadState == ReadState.Interactive)
 1010                {
 01011                    if (reader.NodeType == XmlNodeType.Element)
 1012                    {
 01013                        var name = reader.Name;
 1014
 01015                        if (!_commonTags.Contains(name)
 01016                            && !xmlTagsUsed.Contains(name, StringComparison.OrdinalIgnoreCase))
 1017                        {
 01018                            writer.WriteNode(reader, false);
 1019                        }
 1020                        else
 1021                        {
 01022                            reader.Skip();
 1023                        }
 1024                    }
 1025                    else
 1026                    {
 01027                        reader.Read();
 1028                    }
 1029                }
 01030            }
 01031        }
 1032
 1033        private string GetTagForProviderKey(string providerKey)
 01034            => providerKey.ToLowerInvariant() + "id";
 1035
 1036        protected static string SortNameOrName(BaseItem item)
 1037        {
 01038            if (item is null)
 1039            {
 01040                return string.Empty;
 1041            }
 1042
 01043            if (item.SortName is not null)
 1044            {
 01045                string trimmed = item.SortName.Trim();
 01046                if (trimmed.Length > 0)
 1047                {
 01048                    return trimmed;
 1049                }
 1050            }
 1051
 01052            return (item.Name ?? string.Empty).Trim();
 1053        }
 1054    }
 1055}

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)