< Summary - Jellyfin

Information
Class: MediaBrowser.LocalMetadata.Savers.BaseXmlSaver
Assembly: MediaBrowser.LocalMetadata
File(s): /srv/git/jellyfin/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
Line coverage
16%
Covered lines: 5
Uncovered lines: 25
Coverable lines: 30
Total lines: 512
Line coverage: 16.6%
Branch coverage
0%
Covered branches: 0
Total branches: 12
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
.ctor(...)100%11100%
get_Name()100%210%
GetSavePath(...)100%210%
GetRootElementName(...)100%210%
SetHidden(...)100%210%
AddMediaInfo(...)0%156120%

File(s)

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

#LineLine coverage
 1using System;
 2using System.Globalization;
 3using System.IO;
 4using System.Linq;
 5using System.Text;
 6using System.Threading;
 7using System.Threading.Tasks;
 8using System.Xml;
 9using Jellyfin.Data.Enums;
 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        {
 4434            FileSystem = fileSystem;
 4435            ConfigurationManager = configurationManager;
 4436            LibraryManager = libraryManager;
 4437            Logger = logger;
 4438        }
 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        {
 95            var path = GetSavePath(item);
 96            var directory = Path.GetDirectoryName(path) ?? throw new InvalidDataException($"Provided path ({path}) is no
 97            Directory.CreateDirectory(directory);
 98
 99            // On Windows, savint the file will fail if the file is hidden or readonly
 100            FileSystem.SetAttributes(path, false, false);
 101
 102            var fileStreamOptions = new FileStreamOptions()
 103            {
 104                Mode = FileMode.Create,
 105                Access = FileAccess.Write,
 106                Share = FileShare.None
 107            };
 108
 109            var filestream = new FileStream(path, fileStreamOptions);
 110            await using (filestream.ConfigureAwait(false))
 111            {
 112                var settings = new XmlWriterSettings
 113                {
 114                    Indent = true,
 115                    Encoding = Encoding.UTF8,
 116                    Async = true
 117                };
 118
 119                var writer = XmlWriter.Create(filestream, settings);
 120                await using (writer.ConfigureAwait(false))
 121                {
 122                    var root = GetRootElementName(item);
 123
 124                    await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
 125
 126                    await writer.WriteStartElementAsync(null, root, null).ConfigureAwait(false);
 127
 128                    var baseItem = item;
 129
 130                    if (baseItem is not null)
 131                    {
 132                        await AddCommonNodesAsync(baseItem, writer).ConfigureAwait(false);
 133                    }
 134
 135                    await WriteCustomElementsAsync(item, writer).ConfigureAwait(false);
 136
 137                    await writer.WriteEndElementAsync().ConfigureAwait(false);
 138
 139                    await writer.WriteEndDocumentAsync().ConfigureAwait(false);
 140                }
 141            }
 142
 143            if (ConfigurationManager.Configuration.SaveMetadataHidden)
 144            {
 145                SetHidden(path, true);
 146            }
 147        }
 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        {
 177            if (!string.IsNullOrEmpty(item.OfficialRating))
 178            {
 179                await writer.WriteElementStringAsync(null, "ContentRating", null, item.OfficialRating).ConfigureAwait(fa
 180            }
 181
 182            await writer.WriteElementStringAsync(null, "Added", null, item.DateCreated.ToLocalTime().ToString("G", Cultu
 183
 184            await writer.WriteElementStringAsync(null, "LockData", null, item.IsLocked.ToString(CultureInfo.InvariantCul
 185
 186            if (item.LockedFields.Length > 0)
 187            {
 188                await writer.WriteElementStringAsync(null, "LockedFields", null, string.Join('|', item.LockedFields)).Co
 189            }
 190
 191            if (item.CriticRating.HasValue)
 192            {
 193                await writer.WriteElementStringAsync(null, "CriticRating", null, item.CriticRating.Value.ToString(Cultur
 194            }
 195
 196            if (!string.IsNullOrEmpty(item.Overview))
 197            {
 198                await writer.WriteElementStringAsync(null, "Overview", null, item.Overview).ConfigureAwait(false);
 199            }
 200
 201            if (!string.IsNullOrEmpty(item.OriginalTitle))
 202            {
 203                await writer.WriteElementStringAsync(null, "OriginalTitle", null, item.OriginalTitle).ConfigureAwait(fal
 204            }
 205
 206            if (!string.IsNullOrEmpty(item.CustomRating))
 207            {
 208                await writer.WriteElementStringAsync(null, "CustomRating", null, item.CustomRating).ConfigureAwait(false
 209            }
 210
 211            if (!string.IsNullOrEmpty(item.Name) && item is not Episode)
 212            {
 213                await writer.WriteElementStringAsync(null, "LocalTitle", null, item.Name).ConfigureAwait(false);
 214            }
 215
 216            var forcedSortName = item.ForcedSortName;
 217            if (!string.IsNullOrEmpty(forcedSortName))
 218            {
 219                await writer.WriteElementStringAsync(null, "SortTitle", null, forcedSortName).ConfigureAwait(false);
 220            }
 221
 222            if (item.PremiereDate.HasValue)
 223            {
 224                if (item is Person)
 225                {
 226                    await writer.WriteElementStringAsync(null, "BirthDate", null, item.PremiereDate.Value.ToLocalTime().
 227                }
 228                else if (item is not Episode)
 229                {
 230                    await writer.WriteElementStringAsync(null, "PremiereDate", null, item.PremiereDate.Value.ToLocalTime
 231                }
 232            }
 233
 234            if (item.EndDate.HasValue)
 235            {
 236                if (item is Person)
 237                {
 238                    await writer.WriteElementStringAsync(null, "DeathDate", null, item.EndDate.Value.ToLocalTime().ToStr
 239                }
 240                else if (item is not Episode)
 241                {
 242                    await writer.WriteElementStringAsync(null, "EndDate", null, item.EndDate.Value.ToLocalTime().ToStrin
 243                }
 244            }
 245
 246            if (item.RemoteTrailers.Count > 0)
 247            {
 248                await writer.WriteStartElementAsync(null, "Trailers", null).ConfigureAwait(false);
 249
 250                foreach (var trailer in item.RemoteTrailers)
 251                {
 252                    await writer.WriteElementStringAsync(null, "Trailer", null, trailer.Url).ConfigureAwait(false);
 253                }
 254
 255                await writer.WriteEndElementAsync().ConfigureAwait(false);
 256            }
 257
 258            if (item.ProductionLocations.Length > 0)
 259            {
 260                await writer.WriteStartElementAsync(null, "Countries", null).ConfigureAwait(false);
 261
 262                foreach (var name in item.ProductionLocations)
 263                {
 264                    await writer.WriteElementStringAsync(null, "Country", null, name).ConfigureAwait(false);
 265                }
 266
 267                await writer.WriteEndElementAsync().ConfigureAwait(false);
 268            }
 269
 270            if (item is IHasDisplayOrder hasDisplayOrder && !string.IsNullOrEmpty(hasDisplayOrder.DisplayOrder))
 271            {
 272                await writer.WriteElementStringAsync(null, "DisplayOrder", null, hasDisplayOrder.DisplayOrder).Configure
 273            }
 274
 275            if (item.CommunityRating.HasValue)
 276            {
 277                await writer.WriteElementStringAsync(null, "Rating", null, item.CommunityRating.Value.ToString(CultureIn
 278            }
 279
 280            if (item.ProductionYear.HasValue && item is not Person)
 281            {
 282                await writer.WriteElementStringAsync(null, "ProductionYear", null, item.ProductionYear.Value.ToString(Cu
 283            }
 284
 285            if (item is IHasAspectRatio hasAspectRatio)
 286            {
 287                if (!string.IsNullOrEmpty(hasAspectRatio.AspectRatio))
 288                {
 289                    await writer.WriteElementStringAsync(null, "AspectRatio", null, hasAspectRatio.AspectRatio).Configur
 290                }
 291            }
 292
 293            if (!string.IsNullOrEmpty(item.PreferredMetadataLanguage))
 294            {
 295                await writer.WriteElementStringAsync(null, "Language", null, item.PreferredMetadataLanguage).ConfigureAw
 296            }
 297
 298            if (!string.IsNullOrEmpty(item.PreferredMetadataCountryCode))
 299            {
 300                await writer.WriteElementStringAsync(null, "CountryCode", null, item.PreferredMetadataCountryCode).Confi
 301            }
 302
 303            // Use original runtime here, actual file runtime later in MediaInfo
 304            var runTimeTicks = item.RunTimeTicks;
 305
 306            if (runTimeTicks.HasValue)
 307            {
 308                var timespan = TimeSpan.FromTicks(runTimeTicks.Value);
 309
 310                await writer.WriteElementStringAsync(null, "RunningTime", null, Math.Floor(timespan.TotalMinutes).ToStri
 311            }
 312
 313            if (item.ProviderIds is not null)
 314            {
 315                foreach (var providerKey in item.ProviderIds.Keys)
 316                {
 317                    var providerId = item.ProviderIds[providerKey];
 318                    if (!string.IsNullOrEmpty(providerId))
 319                    {
 320                        await writer.WriteElementStringAsync(null, providerKey + "Id", null, providerId).ConfigureAwait(
 321                    }
 322                }
 323            }
 324
 325            if (!string.IsNullOrWhiteSpace(item.Tagline))
 326            {
 327                await writer.WriteStartElementAsync(null, "Taglines", null).ConfigureAwait(false);
 328                await writer.WriteElementStringAsync(null, "Tagline", null, item.Tagline).ConfigureAwait(false);
 329                await writer.WriteEndElementAsync().ConfigureAwait(false);
 330            }
 331
 332            if (item.Genres.Length > 0)
 333            {
 334                await writer.WriteStartElementAsync(null, "Genres", null).ConfigureAwait(false);
 335
 336                foreach (var genre in item.Genres)
 337                {
 338                    await writer.WriteElementStringAsync(null, "Genre", null, genre).ConfigureAwait(false);
 339                }
 340
 341                await writer.WriteEndElementAsync().ConfigureAwait(false);
 342            }
 343
 344            if (item.Studios.Length > 0)
 345            {
 346                await writer.WriteStartElementAsync(null, "Studios", null).ConfigureAwait(false);
 347
 348                foreach (var studio in item.Studios)
 349                {
 350                    await writer.WriteElementStringAsync(null, "Studio", null, studio).ConfigureAwait(false);
 351                }
 352
 353                await writer.WriteEndElementAsync().ConfigureAwait(false);
 354            }
 355
 356            if (item.Tags.Length > 0)
 357            {
 358                await writer.WriteStartElementAsync(null, "Tags", null).ConfigureAwait(false);
 359
 360                foreach (var tag in item.Tags)
 361                {
 362                    await writer.WriteElementStringAsync(null, "Tag", null, tag).ConfigureAwait(false);
 363                }
 364
 365                await writer.WriteEndElementAsync().ConfigureAwait(false);
 366            }
 367
 368            var people = LibraryManager.GetPeople(item);
 369
 370            if (people.Count > 0)
 371            {
 372                await writer.WriteStartElementAsync(null, "Persons", null).ConfigureAwait(false);
 373
 374                foreach (var person in people)
 375                {
 376                    await writer.WriteStartElementAsync(null, "Person", null).ConfigureAwait(false);
 377                    await writer.WriteElementStringAsync(null, "Name", null, person.Name).ConfigureAwait(false);
 378                    await writer.WriteElementStringAsync(null, "Type", null, person.Type.ToString()).ConfigureAwait(fals
 379                    await writer.WriteElementStringAsync(null, "Role", null, person.Role).ConfigureAwait(false);
 380
 381                    if (person.SortOrder.HasValue)
 382                    {
 383                        await writer.WriteElementStringAsync(null, "SortOrder", null, person.SortOrder.Value.ToString(Cu
 384                    }
 385
 386                    await writer.WriteEndElementAsync().ConfigureAwait(false);
 387                }
 388
 389                await writer.WriteEndElementAsync().ConfigureAwait(false);
 390            }
 391
 392            if (item is BoxSet boxset)
 393            {
 394                await AddLinkedChildren(boxset, writer, "CollectionItems", "CollectionItem").ConfigureAwait(false);
 395            }
 396
 397            if (item is Playlist playlist && !Playlist.IsPlaylistFile(playlist.Path))
 398            {
 399                await writer.WriteElementStringAsync(null, "OwnerUserId", null, playlist.OwnerUserId.ToString("N")).Conf
 400                await AddLinkedChildren(playlist, writer, "PlaylistItems", "PlaylistItem").ConfigureAwait(false);
 401            }
 402
 403            if (item is IHasShares hasShares)
 404            {
 405                await AddSharesAsync(hasShares, writer).ConfigureAwait(false);
 406            }
 407
 408            await AddMediaInfo(item, writer).ConfigureAwait(false);
 409        }
 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        {
 419            await writer.WriteStartElementAsync(null, "Shares", null).ConfigureAwait(false);
 420
 421            foreach (var share in item.Shares)
 422            {
 423                await writer.WriteStartElementAsync(null, "Share", null).ConfigureAwait(false);
 424
 425                await writer.WriteElementStringAsync(null, "UserId", null, share.UserId.ToString()).ConfigureAwait(false
 426                await writer.WriteElementStringAsync(
 427                    null,
 428                    "CanEdit",
 429                    null,
 430                    share.CanEdit.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()).ConfigureAwait(false);
 431
 432                await writer.WriteEndElementAsync().ConfigureAwait(false);
 433            }
 434
 435            await writer.WriteEndElementAsync().ConfigureAwait(false);
 436        }
 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 static async Task AddLinkedChildren(Folder item, XmlWriter writer, string pluralNodeName, string singula
 478        {
 479            var items = item.LinkedChildren
 480                .Where(i => i.Type == LinkedChildType.Manual)
 481                .ToList();
 482
 483            if (items.Count == 0)
 484            {
 485                return;
 486            }
 487
 488            await writer.WriteStartElementAsync(null, pluralNodeName, null).ConfigureAwait(false);
 489
 490            foreach (var link in items)
 491            {
 492                if (!string.IsNullOrWhiteSpace(link.Path) || !string.IsNullOrWhiteSpace(link.LibraryItemId))
 493                {
 494                    await writer.WriteStartElementAsync(null, singularNodeName, null).ConfigureAwait(false);
 495                    if (!string.IsNullOrWhiteSpace(link.Path))
 496                    {
 497                        await writer.WriteElementStringAsync(null, "Path", null, link.Path).ConfigureAwait(false);
 498                    }
 499
 500                    if (!string.IsNullOrWhiteSpace(link.LibraryItemId))
 501                    {
 502                        await writer.WriteElementStringAsync(null, "ItemId", null, link.LibraryItemId).ConfigureAwait(fa
 503                    }
 504
 505                    await writer.WriteEndElementAsync().ConfigureAwait(false);
 506                }
 507            }
 508
 509            await writer.WriteEndElementAsync().ConfigureAwait(false);
 510        }
 511    }
 512}