< 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: 451
Coverable lines: 452
Total lines: 1040
Line coverage: 0.2%
Branch coverage
0%
Covered branches: 0
Total branches: 230
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
.ctor(...)100%11100%
get_MinimumUpdateType()0%620%
get_Name()100%210%
get_SaverName()100%210%
GetSavePath(...)100%210%
SetHidden(...)100%210%
Save(...)0%2040%
AddMediaInfo(...)0%1980440%
AddCommonNodes(...)0%135721160%
AddCollectionItems(...)0%4260%
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        {
 177            foreach (var providerKey in item.ProviderIds.Keys)
 178            {
 179                var providerIdTagName = GetTagForProviderKey(providerKey);
 180                if (!_commonTags.Contains(providerIdTagName))
 181                {
 182                    yield return providerIdTagName;
 183                }
 184            }
 185        }
 186
 187        /// <inheritdoc />
 188        public async Task SaveAsync(BaseItem item, CancellationToken cancellationToken)
 189        {
 190            var path = GetSavePath(item);
 191
 192            using (var memoryStream = new MemoryStream())
 193            {
 194                Save(item, memoryStream, path);
 195
 196                memoryStream.Position = 0;
 197
 198                cancellationToken.ThrowIfCancellationRequested();
 199
 200                await SaveToFileAsync(memoryStream, path).ConfigureAwait(false);
 201            }
 202        }
 203
 204        private async Task SaveToFileAsync(Stream stream, string path)
 205        {
 206            var directory = Path.GetDirectoryName(path) ?? throw new ArgumentException($"Provided path ({path}) is not v
 207            Directory.CreateDirectory(directory);
 208
 209            // On Windows, saving the file will fail if the file is hidden or readonly
 210            FileSystem.SetAttributes(path, false, false);
 211
 212            var fileStreamOptions = new FileStreamOptions()
 213            {
 214                Mode = FileMode.Create,
 215                Access = FileAccess.Write,
 216                Share = FileShare.None,
 217                PreallocationSize = stream.Length,
 218                Options = FileOptions.Asynchronous
 219            };
 220
 221            var filestream = new FileStream(path, fileStreamOptions);
 222            await using (filestream.ConfigureAwait(false))
 223            {
 224                await stream.CopyToAsync(filestream).ConfigureAwait(false);
 225            }
 226
 227            if (ConfigurationManager.Configuration.SaveMetadataHidden)
 228            {
 229                SetHidden(path, true);
 230            }
 231        }
 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.Tmdb, 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 items = item.LinkedChildren
 0785                .Where(i => i.Type == LinkedChildType.Manual)
 0786                .OrderBy(i => i.Path?.Trim())
 0787                .ThenBy(i => i.LibraryItemId?.Trim())
 0788                .ToList();
 789
 0790            foreach (var link in items)
 791            {
 0792                writer.WriteStartElement("collectionitem");
 793
 0794                if (!string.IsNullOrWhiteSpace(link.Path))
 795                {
 0796                    writer.WriteElementString("path", link.Path);
 797                }
 798
 0799                if (!string.IsNullOrWhiteSpace(link.LibraryItemId))
 800                {
 0801                    writer.WriteElementString("ItemId", link.LibraryItemId);
 802                }
 803
 0804                writer.WriteEndElement();
 805            }
 0806        }
 807
 808        /// <summary>
 809        /// Gets the output trailer URL.
 810        /// </summary>
 811        /// <param name="url">The URL.</param>
 812        /// <returns>System.String.</returns>
 813        private string GetOutputTrailerUrl(string url)
 814        {
 815            // This is what xbmc expects
 0816            return url.Replace(YouTubeWatchUrl, "plugin://plugin.video.youtube/play/?video_id=", StringComparison.Ordina
 817        }
 818
 819        private void AddImages(BaseItem item, XmlWriter writer, ILibraryManager libraryManager)
 820        {
 0821            writer.WriteStartElement("art");
 822
 0823            var image = item.GetImageInfo(ImageType.Primary, 0);
 824
 0825            if (image is not null)
 826            {
 0827                writer.WriteElementString("poster", GetImagePathToSave(image, libraryManager));
 828            }
 829
 0830            foreach (var backdrop in item.GetImages(ImageType.Backdrop).OrderBy(b => b.Path?.Trim()))
 831            {
 0832                writer.WriteElementString("fanart", GetImagePathToSave(backdrop, libraryManager));
 833            }
 834
 0835            writer.WriteEndElement();
 0836        }
 837
 838        private void AddUserData(BaseItem item, XmlWriter writer, IUserManager userManager, IUserDataManager userDataRep
 839        {
 0840            var userId = options.UserId;
 0841            if (string.IsNullOrWhiteSpace(userId))
 842            {
 0843                return;
 844            }
 845
 0846            var user = userManager.GetUserById(Guid.Parse(userId));
 847
 0848            if (user is null)
 849            {
 0850                return;
 851            }
 852
 0853            if (item.IsFolder)
 854            {
 0855                return;
 856            }
 857
 0858            var userdata = userDataRepo.GetUserData(user, item);
 859
 0860            if (userdata is not null)
 861            {
 0862                writer.WriteElementString(
 0863                "isuserfavorite",
 0864                userdata.IsFavorite.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
 865
 0866                if (userdata.Rating.HasValue)
 867                {
 0868                    writer.WriteElementString(
 0869                        "userrating",
 0870                        userdata.Rating.Value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
 871                }
 872
 0873                if (!item.IsFolder)
 874                {
 0875                    writer.WriteElementString(
 0876                        "playcount",
 0877                        userdata.PlayCount.ToString(CultureInfo.InvariantCulture));
 0878                    writer.WriteElementString(
 0879                        "watched",
 0880                        userdata.Played.ToString(CultureInfo.InvariantCulture).ToLowerInvariant());
 881
 0882                    if (userdata.LastPlayedDate.HasValue)
 883                    {
 0884                        writer.WriteElementString(
 0885                            "lastplayed",
 0886                            userdata.LastPlayedDate.Value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture).
 887                    }
 888
 0889                    writer.WriteStartElement("resume");
 890
 0891                    var runTimeTicks = item.RunTimeTicks ?? 0;
 892
 0893                    writer.WriteElementString(
 0894                        "position",
 0895                        TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds.ToString(CultureInfo.InvariantCu
 0896                    writer.WriteElementString(
 0897                        "total",
 0898                        TimeSpan.FromTicks(runTimeTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture));
 899                }
 900            }
 901
 0902            writer.WriteEndElement();
 0903        }
 904
 905        private void AddActors(IReadOnlyList<PersonInfo> people, XmlWriter writer, ILibraryManager libraryManager, bool 
 906        {
 0907            foreach (var person in people
 0908                .OrderBy(person => person.SortOrder ?? 0)
 0909                .ThenBy(person => person.Name?.Trim()))
 910            {
 0911                if (person.IsType(PersonKind.Director) || person.IsType(PersonKind.Writer))
 912                {
 913                    continue;
 914                }
 915
 0916                writer.WriteStartElement("actor");
 917
 0918                if (!string.IsNullOrWhiteSpace(person.Name))
 919                {
 0920                    writer.WriteElementString("name", person.Name);
 921                }
 922
 0923                if (!string.IsNullOrWhiteSpace(person.Role))
 924                {
 0925                    writer.WriteElementString("role", person.Role);
 926                }
 927
 0928                if (person.Type != PersonKind.Unknown)
 929                {
 0930                    writer.WriteElementString("type", person.Type.ToString());
 931                }
 932
 0933                if (person.SortOrder.HasValue)
 934                {
 0935                    writer.WriteElementString(
 0936                        "sortorder",
 0937                        person.SortOrder.Value.ToString(CultureInfo.InvariantCulture));
 938                }
 939
 0940                if (saveImagePath)
 941                {
 0942                    var personEntity = libraryManager.GetPerson(person.Name);
 0943                    var image = personEntity?.GetImageInfo(ImageType.Primary, 0);
 944
 0945                    if (image is not null)
 946                    {
 0947                        writer.WriteElementString(
 0948                            "thumb",
 0949                            GetImagePathToSave(image, libraryManager));
 950                    }
 951                }
 952
 0953                writer.WriteEndElement();
 954            }
 0955        }
 956
 957        private string GetImagePathToSave(ItemImageInfo image, ILibraryManager libraryManager)
 958        {
 0959            if (!image.IsLocalFile)
 960            {
 0961                return image.Path;
 962            }
 963
 0964            return libraryManager.GetPathAfterNetworkSubstitution(image.Path);
 965        }
 966
 967        private void AddCustomTags(string path, IReadOnlyCollection<string> xmlTagsUsed, XmlWriter writer, ILogger<BaseN
 968        {
 0969            var settings = new XmlReaderSettings()
 0970            {
 0971                ValidationType = ValidationType.None,
 0972                CheckCharacters = false,
 0973                IgnoreProcessingInstructions = true,
 0974                IgnoreComments = true
 0975            };
 976
 0977            using (var fileStream = File.OpenRead(path))
 0978            using (var streamReader = new StreamReader(fileStream, Encoding.UTF8))
 0979            using (var reader = XmlReader.Create(streamReader, settings))
 980            {
 981                try
 982                {
 0983                    reader.MoveToContent();
 0984                }
 0985                catch (Exception ex)
 986                {
 0987                    logger.LogError(ex, "Error reading existing xml tags from {Path}.", path);
 0988                    return;
 989                }
 990
 0991                reader.Read();
 992
 993                // Loop through each element
 0994                while (!reader.EOF && reader.ReadState == ReadState.Interactive)
 995                {
 0996                    if (reader.NodeType == XmlNodeType.Element)
 997                    {
 0998                        var name = reader.Name;
 999
 01000                        if (!_commonTags.Contains(name)
 01001                            && !xmlTagsUsed.Contains(name, StringComparison.OrdinalIgnoreCase))
 1002                        {
 01003                            writer.WriteNode(reader, false);
 1004                        }
 1005                        else
 1006                        {
 01007                            reader.Skip();
 1008                        }
 1009                    }
 1010                    else
 1011                    {
 01012                        reader.Read();
 1013                    }
 1014                }
 01015            }
 01016        }
 1017
 1018        private string GetTagForProviderKey(string providerKey)
 01019            => providerKey.ToLowerInvariant() + "id";
 1020
 1021        protected static string SortNameOrName(BaseItem item)
 1022        {
 01023            if (item is null)
 1024            {
 01025                return string.Empty;
 1026            }
 1027
 01028            if (item.SortName is not null)
 1029            {
 01030                string trimmed = item.SortName.Trim();
 01031                if (trimmed.Length > 0)
 1032                {
 01033                    return trimmed;
 1034                }
 1035            }
 1036
 01037            return (item.Name ?? string.Empty).Trim();
 1038        }
 1039    }
 1040}

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)
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)