< Summary - Jellyfin

Information
Class: MediaBrowser.LocalMetadata.Savers.BaseXmlSaver
Assembly: MediaBrowser.LocalMetadata
File(s): /srv/git/jellyfin/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
Line coverage
2%
Covered lines: 5
Uncovered lines: 204
Coverable lines: 209
Total lines: 533
Line coverage: 2.3%
Branch coverage
0%
Covered branches: 0
Total branches: 134
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: 16.6% (5/30) Branch coverage: 0% (0/12) Total lines: 5124/19/2026 - 12:14:27 AM Line coverage: 2.5% (5/196) Branch coverage: 0% (0/124) Total lines: 5125/4/2026 - 12:15:16 AM Line coverage: 2.5% (5/197) Branch coverage: 0% (0/124) Total lines: 5115/5/2026 - 12:15:44 AM Line coverage: 2.3% (5/209) Branch coverage: 0% (0/134) Total lines: 533 1/23/2026 - 12:11:06 AM Line coverage: 16.6% (5/30) Branch coverage: 0% (0/12) Total lines: 5124/19/2026 - 12:14:27 AM Line coverage: 2.5% (5/196) Branch coverage: 0% (0/124) Total lines: 5125/4/2026 - 12:15:16 AM Line coverage: 2.5% (5/197) Branch coverage: 0% (0/124) Total lines: 5115/5/2026 - 12:15:44 AM Line coverage: 2.3% (5/209) Branch coverage: 0% (0/134) Total lines: 533

Coverage delta

Coverage delta 15 -15

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Name()100%210%
GetSavePath(...)100%210%
GetRootElementName(...)100%210%
SaveAsync()0%4260%
SetHidden(...)100%210%
AddCommonNodesAsync()0%8556920%
AddSharesAsync()0%620%
AddMediaInfo(...)0%156120%
AddLinkedChildren()0%506220%

File(s)

/srv/git/jellyfin/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.IO;
 5using System.Linq;
 6using System.Text;
 7using System.Threading;
 8using System.Threading.Tasks;
 9using System.Xml;
 10using MediaBrowser.Controller.Configuration;
 11using MediaBrowser.Controller.Entities;
 12using MediaBrowser.Controller.Entities.Movies;
 13using MediaBrowser.Controller.Entities.TV;
 14using MediaBrowser.Controller.Library;
 15using MediaBrowser.Controller.Playlists;
 16using MediaBrowser.Model.Entities;
 17using MediaBrowser.Model.IO;
 18using Microsoft.Extensions.Logging;
 19
 20namespace MediaBrowser.LocalMetadata.Savers
 21{
 22    /// <inheritdoc />
 23    public abstract class BaseXmlSaver : IMetadataFileSaver
 24    {
 25        /// <summary>
 26        /// Initializes a new instance of the <see cref="BaseXmlSaver"/> class.
 27        /// </summary>
 28        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
 29        /// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</par
 30        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 31        /// <param name="logger">Instance of the <see cref="ILogger{BaseXmlSaver}"/> interface.</param>
 32        protected BaseXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager
 33        {
 4234            FileSystem = fileSystem;
 4235            ConfigurationManager = configurationManager;
 4236            LibraryManager = libraryManager;
 4237            Logger = logger;
 4238        }
 39
 40        /// <summary>
 41        /// Gets the file system.
 42        /// </summary>
 43        protected IFileSystem FileSystem { get; private set; }
 44
 45        /// <summary>
 46        /// Gets the configuration manager.
 47        /// </summary>
 48        protected IServerConfigurationManager ConfigurationManager { get; private set; }
 49
 50        /// <summary>
 51        /// Gets the library manager.
 52        /// </summary>
 53        protected ILibraryManager LibraryManager { get; private set; }
 54
 55        /// <summary>
 56        /// Gets the logger.
 57        /// </summary>
 58        protected ILogger<BaseXmlSaver> Logger { get; private set; }
 59
 60        /// <inheritdoc />
 061        public string Name => XmlProviderUtils.Name;
 62
 63        /// <inheritdoc />
 64        public string GetSavePath(BaseItem item)
 65        {
 066            return GetLocalSavePath(item);
 67        }
 68
 69        /// <summary>
 70        /// Gets the save path.
 71        /// </summary>
 72        /// <param name="item">The item.</param>
 73        /// <returns>System.String.</returns>
 74        protected abstract string GetLocalSavePath(BaseItem item);
 75
 76        /// <summary>
 77        /// Gets the name of the root element.
 78        /// </summary>
 79        /// <param name="item">The item.</param>
 80        /// <returns>System.String.</returns>
 81        protected virtual string GetRootElementName(BaseItem item)
 082            => "Item";
 83
 84        /// <summary>
 85        /// Determines whether [is enabled for] [the specified item].
 86        /// </summary>
 87        /// <param name="item">The item.</param>
 88        /// <param name="updateType">Type of the update.</param>
 89        /// <returns><c>true</c> if [is enabled for] [the specified item]; otherwise, <c>false</c>.</returns>
 90        public abstract bool IsEnabledFor(BaseItem item, ItemUpdateType updateType);
 91
 92        /// <inheritdoc />
 93        public async Task SaveAsync(BaseItem item, CancellationToken cancellationToken)
 94        {
 095            var path = GetSavePath(item);
 096            var directory = Path.GetDirectoryName(path) ?? throw new InvalidDataException($"Provided path ({path}) is no
 097            Directory.CreateDirectory(directory);
 98
 99            // On Windows, saving the file will fail if the file is hidden or readonly
 0100            FileSystem.SetAttributes(path, false, false);
 101
 0102            var fileStreamOptions = new FileStreamOptions()
 0103            {
 0104                Mode = FileMode.Create,
 0105                Access = FileAccess.Write,
 0106                Share = FileShare.None
 0107            };
 108
 0109            var filestream = new FileStream(path, fileStreamOptions);
 0110            await using (filestream.ConfigureAwait(false))
 111            {
 0112                var settings = new XmlWriterSettings
 0113                {
 0114                    Indent = true,
 0115                    Encoding = Encoding.UTF8,
 0116                    Async = true
 0117                };
 118
 0119                var writer = XmlWriter.Create(filestream, settings);
 0120                await using (writer.ConfigureAwait(false))
 121                {
 0122                    var root = GetRootElementName(item);
 123
 0124                    await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
 125
 0126                    await writer.WriteStartElementAsync(null, root, null).ConfigureAwait(false);
 127
 0128                    var baseItem = item;
 129
 0130                    if (baseItem is not null)
 131                    {
 0132                        await AddCommonNodesAsync(baseItem, writer).ConfigureAwait(false);
 133                    }
 134
 0135                    await WriteCustomElementsAsync(item, writer).ConfigureAwait(false);
 136
 0137                    await writer.WriteEndElementAsync().ConfigureAwait(false);
 138
 0139                    await writer.WriteEndDocumentAsync().ConfigureAwait(false);
 0140                }
 0141            }
 142
 0143            if (ConfigurationManager.Configuration.SaveMetadataHidden)
 144            {
 0145                SetHidden(path, true);
 146            }
 0147        }
 148
 149        private void SetHidden(string path, bool hidden)
 150        {
 151            try
 152            {
 0153                FileSystem.SetHidden(path, hidden);
 0154            }
 0155            catch (Exception ex)
 156            {
 0157                Logger.LogError(ex, "Error setting hidden attribute on {Path}", path);
 0158            }
 0159        }
 160
 161        /// <summary>
 162        /// Write custom elements.
 163        /// </summary>
 164        /// <param name="item">The item.</param>
 165        /// <param name="writer">The xml writer.</param>
 166        /// <returns>The task object representing the asynchronous operation.</returns>
 167        protected abstract Task WriteCustomElementsAsync(BaseItem item, XmlWriter writer);
 168
 169        /// <summary>
 170        /// Adds the common nodes.
 171        /// </summary>
 172        /// <param name="item">The item.</param>
 173        /// <param name="writer">The xml writer.</param>
 174        /// <returns>The task object representing the asynchronous operation.</returns>
 175        private async Task AddCommonNodesAsync(BaseItem item, XmlWriter writer)
 176        {
 0177            if (!string.IsNullOrEmpty(item.OfficialRating))
 178            {
 0179                await writer.WriteElementStringAsync(null, "ContentRating", null, item.OfficialRating).ConfigureAwait(fa
 180            }
 181
 0182            await writer.WriteElementStringAsync(null, "Added", null, item.DateCreated.ToLocalTime().ToString("G", Cultu
 183
 0184            await writer.WriteElementStringAsync(null, "LockData", null, item.IsLocked.ToString(CultureInfo.InvariantCul
 185
 0186            if (item.LockedFields.Length > 0)
 187            {
 0188                await writer.WriteElementStringAsync(null, "LockedFields", null, string.Join('|', item.LockedFields)).Co
 189            }
 190
 0191            if (item.CriticRating.HasValue)
 192            {
 0193                await writer.WriteElementStringAsync(null, "CriticRating", null, item.CriticRating.Value.ToString(Cultur
 194            }
 195
 0196            if (!string.IsNullOrEmpty(item.Overview))
 197            {
 0198                await writer.WriteElementStringAsync(null, "Overview", null, item.Overview).ConfigureAwait(false);
 199            }
 200
 0201            if (!string.IsNullOrEmpty(item.OriginalTitle))
 202            {
 0203                await writer.WriteElementStringAsync(null, "OriginalTitle", null, item.OriginalTitle).ConfigureAwait(fal
 204            }
 205
 0206            if (!string.IsNullOrEmpty(item.CustomRating))
 207            {
 0208                await writer.WriteElementStringAsync(null, "CustomRating", null, item.CustomRating).ConfigureAwait(false
 209            }
 210
 0211            if (!string.IsNullOrEmpty(item.Name) && item is not Episode)
 212            {
 0213                await writer.WriteElementStringAsync(null, "LocalTitle", null, item.Name).ConfigureAwait(false);
 214            }
 215
 0216            var forcedSortName = item.ForcedSortName;
 0217            if (!string.IsNullOrEmpty(forcedSortName))
 218            {
 0219                await writer.WriteElementStringAsync(null, "SortTitle", null, forcedSortName).ConfigureAwait(false);
 220            }
 221
 0222            if (item.PremiereDate.HasValue)
 223            {
 0224                if (item is Person)
 225                {
 0226                    await writer.WriteElementStringAsync(null, "BirthDate", null, item.PremiereDate.Value.ToLocalTime().
 227                }
 0228                else if (item is not Episode)
 229                {
 0230                    await writer.WriteElementStringAsync(null, "PremiereDate", null, item.PremiereDate.Value.ToLocalTime
 231                }
 232            }
 233
 0234            if (item.EndDate.HasValue)
 235            {
 0236                if (item is Person)
 237                {
 0238                    await writer.WriteElementStringAsync(null, "DeathDate", null, item.EndDate.Value.ToString("yyyy-MM-d
 239                }
 0240                else if (item is not Episode)
 241                {
 0242                    await writer.WriteElementStringAsync(null, "EndDate", null, item.EndDate.Value.ToString("yyyy-MM-dd"
 243                }
 244            }
 245
 0246            if (item.RemoteTrailers.Count > 0)
 247            {
 0248                await writer.WriteStartElementAsync(null, "Trailers", null).ConfigureAwait(false);
 249
 0250                foreach (var trailer in item.RemoteTrailers)
 251                {
 0252                    await writer.WriteElementStringAsync(null, "Trailer", null, trailer.Url).ConfigureAwait(false);
 253                }
 254
 0255                await writer.WriteEndElementAsync().ConfigureAwait(false);
 256            }
 257
 0258            if (item.ProductionLocations.Length > 0)
 259            {
 0260                await writer.WriteStartElementAsync(null, "Countries", null).ConfigureAwait(false);
 261
 0262                foreach (var name in item.ProductionLocations)
 263                {
 0264                    await writer.WriteElementStringAsync(null, "Country", null, name).ConfigureAwait(false);
 265                }
 266
 0267                await writer.WriteEndElementAsync().ConfigureAwait(false);
 268            }
 269
 0270            if (item is IHasDisplayOrder hasDisplayOrder && !string.IsNullOrEmpty(hasDisplayOrder.DisplayOrder))
 271            {
 0272                await writer.WriteElementStringAsync(null, "DisplayOrder", null, hasDisplayOrder.DisplayOrder).Configure
 273            }
 274
 0275            if (item.CommunityRating.HasValue)
 276            {
 0277                await writer.WriteElementStringAsync(null, "Rating", null, item.CommunityRating.Value.ToString(CultureIn
 278            }
 279
 0280            if (item.ProductionYear.HasValue && item is not Person)
 281            {
 0282                await writer.WriteElementStringAsync(null, "ProductionYear", null, item.ProductionYear.Value.ToString(Cu
 283            }
 284
 0285            if (item is IHasAspectRatio hasAspectRatio)
 286            {
 0287                if (!string.IsNullOrEmpty(hasAspectRatio.AspectRatio))
 288                {
 0289                    await writer.WriteElementStringAsync(null, "AspectRatio", null, hasAspectRatio.AspectRatio).Configur
 290                }
 291            }
 292
 0293            if (!string.IsNullOrEmpty(item.PreferredMetadataLanguage))
 294            {
 0295                await writer.WriteElementStringAsync(null, "Language", null, item.PreferredMetadataLanguage).ConfigureAw
 296            }
 297
 0298            if (!string.IsNullOrEmpty(item.PreferredMetadataCountryCode))
 299            {
 0300                await writer.WriteElementStringAsync(null, "CountryCode", null, item.PreferredMetadataCountryCode).Confi
 301            }
 302
 303            // Use original runtime here, actual file runtime later in MediaInfo
 0304            var runTimeTicks = item.RunTimeTicks;
 305
 0306            if (runTimeTicks.HasValue)
 307            {
 0308                var timespan = TimeSpan.FromTicks(runTimeTicks.Value);
 309
 0310                await writer.WriteElementStringAsync(null, "RunningTime", null, Math.Floor(timespan.TotalMinutes).ToStri
 311            }
 312
 0313            if (item.ProviderIds is not null)
 314            {
 0315                foreach (var providerKey in item.ProviderIds.Keys)
 316                {
 0317                    var providerId = item.ProviderIds[providerKey];
 0318                    if (!string.IsNullOrEmpty(providerId))
 319                    {
 0320                        await writer.WriteElementStringAsync(null, providerKey + "Id", null, providerId).ConfigureAwait(
 321                    }
 322                }
 323            }
 324
 0325            if (!string.IsNullOrWhiteSpace(item.Tagline))
 326            {
 0327                await writer.WriteStartElementAsync(null, "Taglines", null).ConfigureAwait(false);
 0328                await writer.WriteElementStringAsync(null, "Tagline", null, item.Tagline).ConfigureAwait(false);
 0329                await writer.WriteEndElementAsync().ConfigureAwait(false);
 330            }
 331
 0332            if (item.Genres.Length > 0)
 333            {
 0334                await writer.WriteStartElementAsync(null, "Genres", null).ConfigureAwait(false);
 335
 0336                foreach (var genre in item.Genres)
 337                {
 0338                    await writer.WriteElementStringAsync(null, "Genre", null, genre).ConfigureAwait(false);
 339                }
 340
 0341                await writer.WriteEndElementAsync().ConfigureAwait(false);
 342            }
 343
 0344            if (item.Studios.Length > 0)
 345            {
 0346                await writer.WriteStartElementAsync(null, "Studios", null).ConfigureAwait(false);
 347
 0348                foreach (var studio in item.Studios)
 349                {
 0350                    await writer.WriteElementStringAsync(null, "Studio", null, studio).ConfigureAwait(false);
 351                }
 352
 0353                await writer.WriteEndElementAsync().ConfigureAwait(false);
 354            }
 355
 0356            if (item.Tags.Length > 0)
 357            {
 0358                await writer.WriteStartElementAsync(null, "Tags", null).ConfigureAwait(false);
 359
 0360                foreach (var tag in item.Tags)
 361                {
 0362                    await writer.WriteElementStringAsync(null, "Tag", null, tag).ConfigureAwait(false);
 363                }
 364
 0365                await writer.WriteEndElementAsync().ConfigureAwait(false);
 366            }
 367
 0368            var people = LibraryManager.GetPeople(item);
 369
 0370            if (people.Count > 0)
 371            {
 0372                await writer.WriteStartElementAsync(null, "Persons", null).ConfigureAwait(false);
 373
 0374                foreach (var person in people)
 375                {
 0376                    await writer.WriteStartElementAsync(null, "Person", null).ConfigureAwait(false);
 0377                    await writer.WriteElementStringAsync(null, "Name", null, person.Name).ConfigureAwait(false);
 0378                    await writer.WriteElementStringAsync(null, "Type", null, person.Type.ToString()).ConfigureAwait(fals
 0379                    await writer.WriteElementStringAsync(null, "Role", null, person.Role).ConfigureAwait(false);
 380
 0381                    if (person.SortOrder.HasValue)
 382                    {
 0383                        await writer.WriteElementStringAsync(null, "SortOrder", null, person.SortOrder.Value.ToString(Cu
 384                    }
 385
 0386                    await writer.WriteEndElementAsync().ConfigureAwait(false);
 0387                }
 388
 0389                await writer.WriteEndElementAsync().ConfigureAwait(false);
 390            }
 391
 0392            if (item is BoxSet boxset)
 393            {
 0394                await AddLinkedChildren(boxset, writer, "CollectionItems", "CollectionItem").ConfigureAwait(false);
 395            }
 396
 0397            if (item is Playlist playlist && !Playlist.IsPlaylistFile(playlist.Path))
 398            {
 0399                await writer.WriteElementStringAsync(null, "OwnerUserId", null, playlist.OwnerUserId.ToString("N")).Conf
 0400                await AddLinkedChildren(playlist, writer, "PlaylistItems", "PlaylistItem").ConfigureAwait(false);
 401            }
 402
 0403            if (item is IHasShares hasShares)
 404            {
 0405                await AddSharesAsync(hasShares, writer).ConfigureAwait(false);
 406            }
 407
 0408            await AddMediaInfo(item, writer).ConfigureAwait(false);
 0409        }
 410
 411        /// <summary>
 412        /// Add shares.
 413        /// </summary>
 414        /// <param name="item">The item.</param>
 415        /// <param name="writer">The xml writer.</param>
 416        /// <returns>The task object representing the asynchronous operation.</returns>
 417        private static async Task AddSharesAsync(IHasShares item, XmlWriter writer)
 418        {
 0419            await writer.WriteStartElementAsync(null, "Shares", null).ConfigureAwait(false);
 420
 0421            foreach (var share in item.Shares)
 422            {
 0423                await writer.WriteStartElementAsync(null, "Share", null).ConfigureAwait(false);
 424
 0425                await writer.WriteElementStringAsync(null, "UserId", null, share.UserId.ToString()).ConfigureAwait(false
 0426                await writer.WriteElementStringAsync(
 0427                    null,
 0428                    "CanEdit",
 0429                    null,
 0430                    share.CanEdit.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()).ConfigureAwait(false);
 431
 0432                await writer.WriteEndElementAsync().ConfigureAwait(false);
 0433            }
 434
 0435            await writer.WriteEndElementAsync().ConfigureAwait(false);
 0436        }
 437
 438        /// <summary>
 439        /// Appends the media info.
 440        /// </summary>
 441        /// <param name="item">The item.</param>
 442        /// <param name="writer">The xml writer.</param>
 443        /// <typeparam name="T">Type of item.</typeparam>
 444        /// <returns>The task object representing the asynchronous operation.</returns>
 445        private static Task AddMediaInfo<T>(T item, XmlWriter writer)
 446            where T : BaseItem
 447        {
 0448            if (item is Video video && video.Video3DFormat.HasValue)
 449            {
 0450                return video.Video3DFormat switch
 0451                {
 0452                    Video3DFormat.FullSideBySide =>
 0453                        writer.WriteElementStringAsync(null, "Format3D", null, "FSBS"),
 0454                    Video3DFormat.FullTopAndBottom =>
 0455                        writer.WriteElementStringAsync(null, "Format3D", null, "FTAB"),
 0456                    Video3DFormat.HalfSideBySide =>
 0457                        writer.WriteElementStringAsync(null, "Format3D", null, "HSBS"),
 0458                    Video3DFormat.HalfTopAndBottom =>
 0459                        writer.WriteElementStringAsync(null, "Format3D", null, "HTAB"),
 0460                    Video3DFormat.MVC =>
 0461                        writer.WriteElementStringAsync(null, "Format3D", null, "MVC"),
 0462                    _ => Task.CompletedTask
 0463                };
 464            }
 465
 0466            return Task.CompletedTask;
 467        }
 468
 469        /// <summary>
 470        /// Add linked children.
 471        /// </summary>
 472        /// <param name="item">The item.</param>
 473        /// <param name="writer">The xml writer.</param>
 474        /// <param name="pluralNodeName">The plural node name.</param>
 475        /// <param name="singularNodeName">The singular node name.</param>
 476        /// <returns>The task object representing the asynchronous operation.</returns>
 477        private async Task AddLinkedChildren(Folder item, XmlWriter writer, string pluralNodeName, string singularNodeNa
 478        {
 0479            var linkedChildren = item.LinkedChildren
 0480                .Where(i => i.Type == LinkedChildType.Manual)
 0481                .ToList();
 482
 0483            if (linkedChildren.Count == 0)
 484            {
 0485                return;
 486            }
 487
 488            // Batch-resolve all ItemIds to paths in a single query to avoid an N+1 round-trip per linked child
 0489            var idsToResolve = new HashSet<Guid>();
 0490            foreach (var link in linkedChildren)
 491            {
 0492                if (link.ItemId.HasValue && !link.ItemId.Value.Equals(Guid.Empty))
 493                {
 0494                    idsToResolve.Add(link.ItemId.Value);
 495                }
 496            }
 497
 0498            Dictionary<Guid, string?>? pathById = null;
 0499            if (idsToResolve.Count > 0)
 500            {
 0501                var batched = LibraryManager.GetItemList(new InternalItemsQuery
 0502                {
 0503                    ItemIds = [.. idsToResolve]
 0504                });
 0505                pathById = new Dictionary<Guid, string?>(batched.Count);
 0506                foreach (var batchedItem in batched)
 507                {
 0508                    pathById[batchedItem.Id] = batchedItem.Path;
 509                }
 510            }
 511
 0512            await writer.WriteStartElementAsync(null, pluralNodeName, null).ConfigureAwait(false);
 513
 0514            foreach (var link in linkedChildren)
 515            {
 0516                string? path = null;
 0517                if (pathById is not null && link.ItemId.HasValue && pathById.TryGetValue(link.ItemId.Value, out var reso
 518                {
 0519                    path = resolvedPath;
 520                }
 521
 0522                if (!string.IsNullOrWhiteSpace(path))
 523                {
 0524                    await writer.WriteStartElementAsync(null, singularNodeName, null).ConfigureAwait(false);
 0525                    await writer.WriteElementStringAsync(null, "Path", null, path).ConfigureAwait(false);
 0526                    await writer.WriteEndElementAsync().ConfigureAwait(false);
 527                }
 0528            }
 529
 0530            await writer.WriteEndElementAsync().ConfigureAwait(false);
 0531        }
 532    }
 533}