< 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: 205
Coverable lines: 210
Total lines: 535
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 5/16/2026 - 12:15:55 AM Line coverage: 2.3% (5/209) Branch coverage: 0% (0/134) Total lines: 53310/29/2026 - 12:17:11 AM Line coverage: 2.3% (5/210) Branch coverage: 0% (0/134) Total lines: 535 5/16/2026 - 12:15:55 AM Line coverage: 2.3% (5/209) Branch coverage: 0% (0/134) Total lines: 53310/29/2026 - 12:17:11 AM Line coverage: 2.3% (5/210) Branch coverage: 0% (0/134) Total lines: 535

Coverage delta

Coverage delta 1 -1

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.Dto;
 12using MediaBrowser.Controller.Entities;
 13using MediaBrowser.Controller.Entities.Movies;
 14using MediaBrowser.Controller.Entities.TV;
 15using MediaBrowser.Controller.Library;
 16using MediaBrowser.Controller.Playlists;
 17using MediaBrowser.Model.Entities;
 18using MediaBrowser.Model.IO;
 19using Microsoft.Extensions.Logging;
 20
 21namespace MediaBrowser.LocalMetadata.Savers
 22{
 23    /// <inheritdoc />
 24    public abstract class BaseXmlSaver : IMetadataFileSaver
 25    {
 26        /// <summary>
 27        /// Initializes a new instance of the <see cref="BaseXmlSaver"/> class.
 28        /// </summary>
 29        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
 30        /// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</par
 31        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 32        /// <param name="logger">Instance of the <see cref="ILogger{BaseXmlSaver}"/> interface.</param>
 33        protected BaseXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager
 34        {
 5435            FileSystem = fileSystem;
 5436            ConfigurationManager = configurationManager;
 5437            LibraryManager = libraryManager;
 5438            Logger = logger;
 5439        }
 40
 41        /// <summary>
 42        /// Gets the file system.
 43        /// </summary>
 44        protected IFileSystem FileSystem { get; private set; }
 45
 46        /// <summary>
 47        /// Gets the configuration manager.
 48        /// </summary>
 49        protected IServerConfigurationManager ConfigurationManager { get; private set; }
 50
 51        /// <summary>
 52        /// Gets the library manager.
 53        /// </summary>
 54        protected ILibraryManager LibraryManager { get; private set; }
 55
 56        /// <summary>
 57        /// Gets the logger.
 58        /// </summary>
 59        protected ILogger<BaseXmlSaver> Logger { get; private set; }
 60
 61        /// <inheritdoc />
 062        public string Name => XmlProviderUtils.Name;
 63
 64        /// <inheritdoc />
 65        public string GetSavePath(BaseItem item)
 66        {
 067            return GetLocalSavePath(item);
 68        }
 69
 70        /// <summary>
 71        /// Gets the save path.
 72        /// </summary>
 73        /// <param name="item">The item.</param>
 74        /// <returns>System.String.</returns>
 75        protected abstract string GetLocalSavePath(BaseItem item);
 76
 77        /// <summary>
 78        /// Gets the name of the root element.
 79        /// </summary>
 80        /// <param name="item">The item.</param>
 81        /// <returns>System.String.</returns>
 82        protected virtual string GetRootElementName(BaseItem item)
 083            => "Item";
 84
 85        /// <summary>
 86        /// Determines whether [is enabled for] [the specified item].
 87        /// </summary>
 88        /// <param name="item">The item.</param>
 89        /// <param name="updateType">Type of the update.</param>
 90        /// <returns><c>true</c> if [is enabled for] [the specified item]; otherwise, <c>false</c>.</returns>
 91        public abstract bool IsEnabledFor(BaseItem item, ItemUpdateType updateType);
 92
 93        /// <inheritdoc />
 94        public async Task SaveAsync(BaseItem item, CancellationToken cancellationToken)
 95        {
 096            var path = GetSavePath(item);
 097            var directory = Path.GetDirectoryName(path) ?? throw new InvalidDataException($"Provided path ({path}) is no
 098            Directory.CreateDirectory(directory);
 99
 100            // On Windows, saving the file will fail if the file is hidden or readonly
 0101            FileSystem.SetAttributes(path, false, false);
 102
 0103            var fileStreamOptions = new FileStreamOptions()
 0104            {
 0105                Mode = FileMode.Create,
 0106                Access = FileAccess.Write,
 0107                Share = FileShare.None
 0108            };
 109
 0110            var filestream = new FileStream(path, fileStreamOptions);
 0111            await using (filestream.ConfigureAwait(false))
 112            {
 0113                var settings = new XmlWriterSettings
 0114                {
 0115                    Indent = true,
 0116                    Encoding = Encoding.UTF8,
 0117                    Async = true
 0118                };
 119
 0120                var writer = XmlWriter.Create(filestream, settings);
 0121                await using (writer.ConfigureAwait(false))
 122                {
 0123                    var root = GetRootElementName(item);
 124
 0125                    await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
 126
 0127                    await writer.WriteStartElementAsync(null, root, null).ConfigureAwait(false);
 128
 0129                    var baseItem = item;
 130
 0131                    if (baseItem is not null)
 132                    {
 0133                        await AddCommonNodesAsync(baseItem, writer).ConfigureAwait(false);
 134                    }
 135
 0136                    await WriteCustomElementsAsync(item, writer).ConfigureAwait(false);
 137
 0138                    await writer.WriteEndElementAsync().ConfigureAwait(false);
 139
 0140                    await writer.WriteEndDocumentAsync().ConfigureAwait(false);
 0141                }
 0142            }
 143
 0144            if (ConfigurationManager.Configuration.SaveMetadataHidden)
 145            {
 0146                SetHidden(path, true);
 147            }
 0148        }
 149
 150        private void SetHidden(string path, bool hidden)
 151        {
 152            try
 153            {
 0154                FileSystem.SetHidden(path, hidden);
 0155            }
 0156            catch (Exception ex)
 157            {
 0158                Logger.LogError(ex, "Error setting hidden attribute on {Path}", path);
 0159            }
 0160        }
 161
 162        /// <summary>
 163        /// Write custom elements.
 164        /// </summary>
 165        /// <param name="item">The item.</param>
 166        /// <param name="writer">The xml writer.</param>
 167        /// <returns>The task object representing the asynchronous operation.</returns>
 168        protected abstract Task WriteCustomElementsAsync(BaseItem item, XmlWriter writer);
 169
 170        /// <summary>
 171        /// Adds the common nodes.
 172        /// </summary>
 173        /// <param name="item">The item.</param>
 174        /// <param name="writer">The xml writer.</param>
 175        /// <returns>The task object representing the asynchronous operation.</returns>
 176        private async Task AddCommonNodesAsync(BaseItem item, XmlWriter writer)
 177        {
 0178            if (!string.IsNullOrEmpty(item.OfficialRating))
 179            {
 0180                await writer.WriteElementStringAsync(null, "ContentRating", null, item.OfficialRating).ConfigureAwait(fa
 181            }
 182
 0183            await writer.WriteElementStringAsync(null, "Added", null, item.DateCreated.ToLocalTime().ToString("G", Cultu
 184
 0185            await writer.WriteElementStringAsync(null, "LockData", null, item.IsLocked.ToString(CultureInfo.InvariantCul
 186
 0187            if (item.LockedFields.Length > 0)
 188            {
 0189                await writer.WriteElementStringAsync(null, "LockedFields", null, string.Join('|', item.LockedFields)).Co
 190            }
 191
 0192            if (item.CriticRating.HasValue)
 193            {
 0194                await writer.WriteElementStringAsync(null, "CriticRating", null, item.CriticRating.Value.ToString(Cultur
 195            }
 196
 0197            if (!string.IsNullOrEmpty(item.Overview))
 198            {
 0199                await writer.WriteElementStringAsync(null, "Overview", null, item.Overview).ConfigureAwait(false);
 200            }
 201
 0202            if (!string.IsNullOrEmpty(item.OriginalTitle))
 203            {
 0204                await writer.WriteElementStringAsync(null, "OriginalTitle", null, item.OriginalTitle).ConfigureAwait(fal
 205            }
 206
 0207            if (!string.IsNullOrEmpty(item.CustomRating))
 208            {
 0209                await writer.WriteElementStringAsync(null, "CustomRating", null, item.CustomRating).ConfigureAwait(false
 210            }
 211
 0212            if (!string.IsNullOrEmpty(item.Name) && item is not Episode)
 213            {
 0214                await writer.WriteElementStringAsync(null, "LocalTitle", null, item.Name).ConfigureAwait(false);
 215            }
 216
 0217            var forcedSortName = item.ForcedSortName;
 0218            if (!string.IsNullOrEmpty(forcedSortName))
 219            {
 0220                await writer.WriteElementStringAsync(null, "SortTitle", null, forcedSortName).ConfigureAwait(false);
 221            }
 222
 0223            if (item.PremiereDate.HasValue)
 224            {
 0225                if (item is Person)
 226                {
 0227                    await writer.WriteElementStringAsync(null, "BirthDate", null, item.PremiereDate.Value.ToLocalTime().
 228                }
 0229                else if (item is not Episode)
 230                {
 0231                    await writer.WriteElementStringAsync(null, "PremiereDate", null, item.PremiereDate.Value.ToLocalTime
 232                }
 233            }
 234
 0235            if (item.EndDate.HasValue)
 236            {
 0237                if (item is Person)
 238                {
 0239                    await writer.WriteElementStringAsync(null, "DeathDate", null, item.EndDate.Value.ToString("yyyy-MM-d
 240                }
 0241                else if (item is not Episode)
 242                {
 0243                    await writer.WriteElementStringAsync(null, "EndDate", null, item.EndDate.Value.ToString("yyyy-MM-dd"
 244                }
 245            }
 246
 0247            if (item.RemoteTrailers.Count > 0)
 248            {
 0249                await writer.WriteStartElementAsync(null, "Trailers", null).ConfigureAwait(false);
 250
 0251                foreach (var trailer in item.RemoteTrailers)
 252                {
 0253                    await writer.WriteElementStringAsync(null, "Trailer", null, trailer.Url).ConfigureAwait(false);
 254                }
 255
 0256                await writer.WriteEndElementAsync().ConfigureAwait(false);
 257            }
 258
 0259            if (item.ProductionLocations.Length > 0)
 260            {
 0261                await writer.WriteStartElementAsync(null, "Countries", null).ConfigureAwait(false);
 262
 0263                foreach (var name in item.ProductionLocations)
 264                {
 0265                    await writer.WriteElementStringAsync(null, "Country", null, name).ConfigureAwait(false);
 266                }
 267
 0268                await writer.WriteEndElementAsync().ConfigureAwait(false);
 269            }
 270
 0271            if (item is IHasDisplayOrder hasDisplayOrder && !string.IsNullOrEmpty(hasDisplayOrder.DisplayOrder))
 272            {
 0273                await writer.WriteElementStringAsync(null, "DisplayOrder", null, hasDisplayOrder.DisplayOrder).Configure
 274            }
 275
 0276            if (item.CommunityRating.HasValue)
 277            {
 0278                await writer.WriteElementStringAsync(null, "Rating", null, item.CommunityRating.Value.ToString(CultureIn
 279            }
 280
 0281            if (item.ProductionYear is not null && item is not Person)
 282            {
 0283                await writer.WriteElementStringAsync(null, "ProductionYear", null, item.ProductionYear.Value.ToString(Cu
 284            }
 285
 0286            if (item is IHasAspectRatio hasAspectRatio)
 287            {
 0288                if (!string.IsNullOrEmpty(hasAspectRatio.AspectRatio))
 289                {
 0290                    await writer.WriteElementStringAsync(null, "AspectRatio", null, hasAspectRatio.AspectRatio).Configur
 291                }
 292            }
 293
 0294            if (!string.IsNullOrEmpty(item.PreferredMetadataLanguage))
 295            {
 0296                await writer.WriteElementStringAsync(null, "Language", null, item.PreferredMetadataLanguage).ConfigureAw
 297            }
 298
 0299            if (!string.IsNullOrEmpty(item.PreferredMetadataCountryCode))
 300            {
 0301                await writer.WriteElementStringAsync(null, "CountryCode", null, item.PreferredMetadataCountryCode).Confi
 302            }
 303
 304            // Use original runtime here, actual file runtime later in MediaInfo
 0305            var runTimeTicks = item.RunTimeTicks;
 306
 0307            if (runTimeTicks.HasValue)
 308            {
 0309                var timespan = TimeSpan.FromTicks(runTimeTicks.Value);
 310
 0311                await writer.WriteElementStringAsync(null, "RunningTime", null, Math.Floor(timespan.TotalMinutes).ToStri
 312            }
 313
 0314            if (item.ProviderIds is not null)
 315            {
 0316                foreach (var providerKey in item.ProviderIds.Keys)
 317                {
 0318                    var providerId = item.ProviderIds[providerKey];
 0319                    if (!string.IsNullOrEmpty(providerId))
 320                    {
 0321                        await writer.WriteElementStringAsync(null, providerKey + "Id", null, providerId).ConfigureAwait(
 322                    }
 323                }
 324            }
 325
 0326            if (!string.IsNullOrWhiteSpace(item.Tagline))
 327            {
 0328                await writer.WriteStartElementAsync(null, "Taglines", null).ConfigureAwait(false);
 0329                await writer.WriteElementStringAsync(null, "Tagline", null, item.Tagline).ConfigureAwait(false);
 0330                await writer.WriteEndElementAsync().ConfigureAwait(false);
 331            }
 332
 0333            if (item.Genres.Length > 0)
 334            {
 0335                await writer.WriteStartElementAsync(null, "Genres", null).ConfigureAwait(false);
 336
 0337                foreach (var genre in item.Genres)
 338                {
 0339                    await writer.WriteElementStringAsync(null, "Genre", null, genre).ConfigureAwait(false);
 340                }
 341
 0342                await writer.WriteEndElementAsync().ConfigureAwait(false);
 343            }
 344
 0345            if (item.Studios.Length > 0)
 346            {
 0347                await writer.WriteStartElementAsync(null, "Studios", null).ConfigureAwait(false);
 348
 0349                foreach (var studio in item.Studios)
 350                {
 0351                    await writer.WriteElementStringAsync(null, "Studio", null, studio).ConfigureAwait(false);
 352                }
 353
 0354                await writer.WriteEndElementAsync().ConfigureAwait(false);
 355            }
 356
 0357            if (item.Tags.Length > 0)
 358            {
 0359                await writer.WriteStartElementAsync(null, "Tags", null).ConfigureAwait(false);
 360
 0361                foreach (var tag in item.Tags)
 362                {
 0363                    await writer.WriteElementStringAsync(null, "Tag", null, tag).ConfigureAwait(false);
 364                }
 365
 0366                await writer.WriteEndElementAsync().ConfigureAwait(false);
 367            }
 368
 0369            var people = LibraryManager.GetPeople(item);
 370
 0371            if (people.Count > 0)
 372            {
 0373                await writer.WriteStartElementAsync(null, "Persons", null).ConfigureAwait(false);
 374
 0375                foreach (var person in people)
 376                {
 0377                    await writer.WriteStartElementAsync(null, "Person", null).ConfigureAwait(false);
 0378                    await writer.WriteElementStringAsync(null, "Name", null, person.Name).ConfigureAwait(false);
 0379                    await writer.WriteElementStringAsync(null, "Type", null, person.Type.ToString()).ConfigureAwait(fals
 0380                    await writer.WriteElementStringAsync(null, "Role", null, person.Role).ConfigureAwait(false);
 381
 0382                    if (person.SortOrder.HasValue)
 383                    {
 0384                        await writer.WriteElementStringAsync(null, "SortOrder", null, person.SortOrder.Value.ToString(Cu
 385                    }
 386
 0387                    await writer.WriteEndElementAsync().ConfigureAwait(false);
 0388                }
 389
 0390                await writer.WriteEndElementAsync().ConfigureAwait(false);
 391            }
 392
 0393            if (item is BoxSet boxset)
 394            {
 0395                await AddLinkedChildren(boxset, writer, "CollectionItems", "CollectionItem").ConfigureAwait(false);
 396            }
 397
 0398            if (item is Playlist playlist && !Playlist.IsPlaylistFile(playlist.Path))
 399            {
 0400                await writer.WriteElementStringAsync(null, "OwnerUserId", null, playlist.OwnerUserId.ToString("N")).Conf
 0401                await AddLinkedChildren(playlist, writer, "PlaylistItems", "PlaylistItem").ConfigureAwait(false);
 402            }
 403
 0404            if (item is IHasShares hasShares)
 405            {
 0406                await AddSharesAsync(hasShares, writer).ConfigureAwait(false);
 407            }
 408
 0409            await AddMediaInfo(item, writer).ConfigureAwait(false);
 0410        }
 411
 412        /// <summary>
 413        /// Add shares.
 414        /// </summary>
 415        /// <param name="item">The item.</param>
 416        /// <param name="writer">The xml writer.</param>
 417        /// <returns>The task object representing the asynchronous operation.</returns>
 418        private static async Task AddSharesAsync(IHasShares item, XmlWriter writer)
 419        {
 0420            await writer.WriteStartElementAsync(null, "Shares", null).ConfigureAwait(false);
 421
 0422            foreach (var share in item.Shares)
 423            {
 0424                await writer.WriteStartElementAsync(null, "Share", null).ConfigureAwait(false);
 425
 0426                await writer.WriteElementStringAsync(null, "UserId", null, share.UserId.ToString()).ConfigureAwait(false
 0427                await writer.WriteElementStringAsync(
 0428                    null,
 0429                    "CanEdit",
 0430                    null,
 0431                    share.CanEdit.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()).ConfigureAwait(false);
 432
 0433                await writer.WriteEndElementAsync().ConfigureAwait(false);
 0434            }
 435
 0436            await writer.WriteEndElementAsync().ConfigureAwait(false);
 0437        }
 438
 439        /// <summary>
 440        /// Appends the media info.
 441        /// </summary>
 442        /// <param name="item">The item.</param>
 443        /// <param name="writer">The xml writer.</param>
 444        /// <typeparam name="T">Type of item.</typeparam>
 445        /// <returns>The task object representing the asynchronous operation.</returns>
 446        private static Task AddMediaInfo<T>(T item, XmlWriter writer)
 447            where T : BaseItem
 448        {
 0449            if (item is Video video && video.Video3DFormat.HasValue)
 450            {
 0451                return video.Video3DFormat switch
 0452                {
 0453                    Video3DFormat.FullSideBySide =>
 0454                        writer.WriteElementStringAsync(null, "Format3D", null, "FSBS"),
 0455                    Video3DFormat.FullTopAndBottom =>
 0456                        writer.WriteElementStringAsync(null, "Format3D", null, "FTAB"),
 0457                    Video3DFormat.HalfSideBySide =>
 0458                        writer.WriteElementStringAsync(null, "Format3D", null, "HSBS"),
 0459                    Video3DFormat.HalfTopAndBottom =>
 0460                        writer.WriteElementStringAsync(null, "Format3D", null, "HTAB"),
 0461                    Video3DFormat.MVC =>
 0462                        writer.WriteElementStringAsync(null, "Format3D", null, "MVC"),
 0463                    _ => Task.CompletedTask
 0464                };
 465            }
 466
 0467            return Task.CompletedTask;
 468        }
 469
 470        /// <summary>
 471        /// Add linked children.
 472        /// </summary>
 473        /// <param name="item">The item.</param>
 474        /// <param name="writer">The xml writer.</param>
 475        /// <param name="pluralNodeName">The plural node name.</param>
 476        /// <param name="singularNodeName">The singular node name.</param>
 477        /// <returns>The task object representing the asynchronous operation.</returns>
 478        private async Task AddLinkedChildren(Folder item, XmlWriter writer, string pluralNodeName, string singularNodeNa
 479        {
 0480            var linkedChildren = item.LinkedChildren
 0481                .Where(i => i.Type == LinkedChildType.Manual)
 0482                .ToList();
 483
 0484            if (linkedChildren.Count == 0)
 485            {
 0486                return;
 487            }
 488
 489            // Batch-resolve all ItemIds to paths in a single query to avoid an N+1 round-trip per linked child
 0490            var idsToResolve = new HashSet<Guid>();
 0491            foreach (var link in linkedChildren)
 492            {
 0493                if (link.ItemId.HasValue && !link.ItemId.Value.Equals(Guid.Empty))
 494                {
 0495                    idsToResolve.Add(link.ItemId.Value);
 496                }
 497            }
 498
 0499            Dictionary<Guid, string?>? pathById = null;
 0500            if (idsToResolve.Count > 0)
 501            {
 0502                var batched = LibraryManager.GetItemList(new InternalItemsQuery
 0503                {
 0504                    ItemIds = [.. idsToResolve],
 0505                    DtoOptions = DtoOptions.StoredColumnsOnly
 0506                });
 0507                pathById = new Dictionary<Guid, string?>(batched.Count);
 0508                foreach (var batchedItem in batched)
 509                {
 0510                    pathById[batchedItem.Id] = batchedItem.Path;
 511                }
 512            }
 513
 0514            await writer.WriteStartElementAsync(null, pluralNodeName, null).ConfigureAwait(false);
 515
 0516            foreach (var link in linkedChildren)
 517            {
 0518                string? path = null;
 0519                if (pathById is not null && link.ItemId.HasValue && pathById.TryGetValue(link.ItemId.Value, out var reso
 520                {
 0521                    path = resolvedPath;
 522                }
 523
 0524                if (!string.IsNullOrWhiteSpace(path))
 525                {
 0526                    await writer.WriteStartElementAsync(null, singularNodeName, null).ConfigureAwait(false);
 0527                    await writer.WriteElementStringAsync(null, "Path", null, path).ConfigureAwait(false);
 0528                    await writer.WriteEndElementAsync().ConfigureAwait(false);
 529                }
 0530            }
 531
 0532            await writer.WriteEndElementAsync().ConfigureAwait(false);
 0533        }
 534    }
 535}