repo creation with initial code after cloning public repo
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace MediaBrowser.LocalMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// The BaseXmlProvider.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of provider.</typeparam>
|
||||
public abstract class BaseXmlProvider<T> : ILocalMetadataProvider<T>, IHasItemChangeMonitor, IHasOrder
|
||||
where T : BaseItem, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseXmlProvider{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
protected BaseXmlProvider(IFileSystem fileSystem)
|
||||
{
|
||||
this.FileSystem = fileSystem;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => XmlProviderUtils.Name;
|
||||
|
||||
/// After Nfo
|
||||
/// <inheritdoc />
|
||||
public virtual int Order => 1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the IFileSystem.
|
||||
/// </summary>
|
||||
protected IFileSystem FileSystem { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets metadata for item.
|
||||
/// </summary>
|
||||
/// <param name="info">The item info.</param>
|
||||
/// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The metadata for item.</returns>
|
||||
public Task<MetadataResult<T>> GetMetadata(
|
||||
ItemInfo info,
|
||||
IDirectoryService directoryService,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new MetadataResult<T>();
|
||||
|
||||
var file = GetXmlFile(info, directoryService);
|
||||
|
||||
if (file is null)
|
||||
{
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
var path = file.FullName;
|
||||
|
||||
try
|
||||
{
|
||||
result.Item = new T();
|
||||
|
||||
Fetch(result, path, cancellationToken);
|
||||
result.HasMetadata = true;
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
result.HasMetadata = false;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
result.HasMetadata = false;
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get metadata from path.
|
||||
/// </summary>
|
||||
/// <param name="result">Resulting metadata.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
protected abstract void Fetch(MetadataResult<T> result, string path, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Get metadata from xml file.
|
||||
/// </summary>
|
||||
/// <param name="info">Item inf.</param>
|
||||
/// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
|
||||
/// <returns>The file system metadata.</returns>
|
||||
protected abstract FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
||||
{
|
||||
var file = GetXmlFile(new ItemInfo(item), directoryService);
|
||||
|
||||
if (file is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return file.Exists && FileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace MediaBrowser.LocalMetadata.Images
|
||||
{
|
||||
/// <summary>
|
||||
/// Collection folder local image provider.
|
||||
/// </summary>
|
||||
public class CollectionFolderLocalImageProvider : ILocalImageProvider, IHasOrder
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CollectionFolderLocalImageProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
public CollectionFolderLocalImageProvider(IFileSystem fileSystem)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Collection Folder Images";
|
||||
|
||||
/// Run after LocalImageProvider
|
||||
/// <inheritdoc />
|
||||
public int Order => 1;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(BaseItem item)
|
||||
{
|
||||
return item is CollectionFolder && item.SupportsLocalMetadata;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService)
|
||||
{
|
||||
var collectionFolder = (CollectionFolder)item;
|
||||
|
||||
return new LocalImageProvider(_fileSystem).GetImages(item, collectionFolder.PhysicalLocations, directoryService);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace MediaBrowser.LocalMetadata.Images
|
||||
{
|
||||
/// <summary>
|
||||
/// Episode local image provider.
|
||||
/// </summary>
|
||||
public class EpisodeLocalImageProvider : ILocalImageProvider, IHasOrder
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Name => "Local Images";
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Order => 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(BaseItem item)
|
||||
{
|
||||
return item is Episode && item.SupportsLocalMetadata;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService)
|
||||
{
|
||||
var parentPath = Path.GetDirectoryName(item.Path);
|
||||
if (parentPath is null)
|
||||
{
|
||||
return Enumerable.Empty<LocalImageInfo>();
|
||||
}
|
||||
|
||||
var parentPathFiles = directoryService.GetFiles(parentPath);
|
||||
var nameWithoutExtension = Path.GetFileNameWithoutExtension(item.Path.AsSpan()).ToString();
|
||||
|
||||
var images = GetImageFilesFromFolder(nameWithoutExtension, parentPathFiles);
|
||||
|
||||
var metadataSubDir = directoryService.GetDirectories(parentPath).FirstOrDefault(d => d.Name.Equals("metadata", StringComparison.Ordinal));
|
||||
if (metadataSubDir is not null)
|
||||
{
|
||||
var files = directoryService.GetFiles(metadataSubDir.FullName);
|
||||
images.AddRange(GetImageFilesFromFolder(nameWithoutExtension, files));
|
||||
}
|
||||
|
||||
return images;
|
||||
}
|
||||
|
||||
private List<LocalImageInfo> GetImageFilesFromFolder(ReadOnlySpan<char> filenameWithoutExtension, List<FileSystemMetadata> filePaths)
|
||||
{
|
||||
var list = new List<LocalImageInfo>(1);
|
||||
var thumbName = string.Concat(filenameWithoutExtension, "-thumb");
|
||||
|
||||
foreach (var i in filePaths)
|
||||
{
|
||||
if (i.IsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (BaseItem.SupportedImageExtensions.Contains(i.Extension.AsSpan(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var currentNameWithoutExtension = Path.GetFileNameWithoutExtension(i.FullName.AsSpan());
|
||||
|
||||
if (filenameWithoutExtension.Equals(currentNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
list.Add(new LocalImageInfo { FileInfo = i, Type = ImageType.Primary });
|
||||
}
|
||||
else if (currentNameWithoutExtension.Equals(thumbName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
list.Add(new LocalImageInfo { FileInfo = i, Type = ImageType.Primary });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.LocalMetadata.Images
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal metadata folder image provider.
|
||||
/// </summary>
|
||||
public class InternalMetadataFolderImageProvider : ILocalImageProvider, IHasOrder
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILogger<InternalMetadataFolderImageProvider> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InternalMetadataFolderImageProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{InternalMetadataFolderImageProvider}"/> interface.</param>
|
||||
public InternalMetadataFolderImageProvider(
|
||||
IFileSystem fileSystem,
|
||||
ILogger<InternalMetadataFolderImageProvider> logger)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// Make sure this is last so that all other locations are scanned first
|
||||
/// <inheritdoc />
|
||||
public int Order => 1000;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Internal Images";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(BaseItem item)
|
||||
{
|
||||
if (item is Photo)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!item.IsSaveLocalMetadataEnabled())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Extracted images will be saved in here
|
||||
if (item is Audio)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.SupportsLocalMetadata && !item.AlwaysScanInternalMetadataPath)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService)
|
||||
{
|
||||
var path = item.GetInternalMetadataPath();
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
return Enumerable.Empty<LocalImageInfo>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return new LocalImageProvider(_fileSystem).GetImages(item, path, directoryService);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while getting images for {Library}", item.Name);
|
||||
return Enumerable.Empty<LocalImageInfo>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace MediaBrowser.LocalMetadata.Images
|
||||
{
|
||||
/// <summary>
|
||||
/// Local image provider.
|
||||
/// </summary>
|
||||
public class LocalImageProvider : ILocalImageProvider, IHasOrder
|
||||
{
|
||||
private static readonly string[] _commonImageFileNames =
|
||||
{
|
||||
"poster",
|
||||
"folder",
|
||||
"cover",
|
||||
"default"
|
||||
};
|
||||
|
||||
private static readonly string[] _musicImageFileNames =
|
||||
{
|
||||
"folder",
|
||||
"poster",
|
||||
"cover",
|
||||
"jacket",
|
||||
"default",
|
||||
"albumart"
|
||||
};
|
||||
|
||||
private static readonly string[] _personImageFileNames =
|
||||
{
|
||||
"folder",
|
||||
"poster"
|
||||
};
|
||||
|
||||
private static readonly string[] _seriesImageFileNames =
|
||||
{
|
||||
"poster",
|
||||
"folder",
|
||||
"cover",
|
||||
"default",
|
||||
"show"
|
||||
};
|
||||
|
||||
private static readonly string[] _videoImageFileNames =
|
||||
{
|
||||
"poster",
|
||||
"folder",
|
||||
"cover",
|
||||
"default",
|
||||
"movie"
|
||||
};
|
||||
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LocalImageProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
public LocalImageProvider(IFileSystem fileSystem)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => "Local Images";
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Order => 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Supports(BaseItem item)
|
||||
{
|
||||
if (item.SupportsLocalMetadata)
|
||||
{
|
||||
// Episode has its own provider
|
||||
if (item is Episode || item is Audio || item is Photo)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.LocationType == LocationType.Virtual)
|
||||
{
|
||||
var season = item as Season;
|
||||
var series = season?.Series;
|
||||
if (series is not null && series.IsFileProtocol)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IEnumerable<FileSystemMetadata> GetFiles(BaseItem item, bool includeDirectories, IDirectoryService directoryService)
|
||||
{
|
||||
if (!item.IsFileProtocol)
|
||||
{
|
||||
return Enumerable.Empty<FileSystemMetadata>();
|
||||
}
|
||||
|
||||
var path = item.ContainingFolderPath;
|
||||
|
||||
// Exit if the cache dir does not exist, alternative solution is to create it, but that's a lot of empty dirs...
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
return Enumerable.Empty<FileSystemMetadata>();
|
||||
}
|
||||
|
||||
return directoryService.GetFileSystemEntries(path)
|
||||
.Where(i =>
|
||||
(includeDirectories && i.IsDirectory)
|
||||
|| BaseItem.SupportedImageExtensions.Contains(i.Extension, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(i => Array.IndexOf(BaseItem.SupportedImageExtensions, i.Extension ?? string.Empty));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService)
|
||||
{
|
||||
var files = GetFiles(item, true, directoryService).ToList();
|
||||
|
||||
var list = new List<LocalImageInfo>();
|
||||
|
||||
PopulateImages(item, list, files, true, directoryService);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get images for item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="path">The images path.</param>
|
||||
/// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
|
||||
/// <returns>The local image info.</returns>
|
||||
public IEnumerable<LocalImageInfo> GetImages(BaseItem item, string path, IDirectoryService directoryService)
|
||||
{
|
||||
return GetImages(item, new[] { path }, directoryService);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get images for item from multiple paths.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="paths">The image paths.</param>
|
||||
/// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
|
||||
/// <returns>The local image info.</returns>
|
||||
public IEnumerable<LocalImageInfo> GetImages(BaseItem item, IEnumerable<string> paths, IDirectoryService directoryService)
|
||||
{
|
||||
IEnumerable<FileSystemMetadata> files = paths.SelectMany(i => _fileSystem.GetFiles(i, BaseItem.SupportedImageExtensions, true, false));
|
||||
|
||||
files = files
|
||||
.OrderBy(i => Array.IndexOf(BaseItem.SupportedImageExtensions, i.Extension ?? string.Empty));
|
||||
|
||||
var list = new List<LocalImageInfo>();
|
||||
|
||||
PopulateImages(item, list, files.ToList(), false, directoryService);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private void PopulateImages(BaseItem item, List<LocalImageInfo> images, List<FileSystemMetadata> files, bool supportParentSeriesFiles, IDirectoryService directoryService)
|
||||
{
|
||||
if (supportParentSeriesFiles)
|
||||
{
|
||||
if (item is Season season)
|
||||
{
|
||||
PopulateSeasonImagesFromSeriesFolder(season, images, directoryService);
|
||||
}
|
||||
}
|
||||
|
||||
var imagePrefix = item.FileNameWithoutExtension + "-";
|
||||
var isInMixedFolder = item.IsInMixedFolder;
|
||||
|
||||
PopulatePrimaryImages(item, images, files, imagePrefix, isInMixedFolder);
|
||||
|
||||
var added = false;
|
||||
var isEpisode = item is Episode;
|
||||
var isSong = item.GetType() == typeof(Audio);
|
||||
var isPerson = item is Person;
|
||||
|
||||
// Logo
|
||||
if (!isEpisode && !isSong && !isPerson)
|
||||
{
|
||||
added = AddImage(files, images, "logo", imagePrefix, isInMixedFolder, ImageType.Logo);
|
||||
if (!added)
|
||||
{
|
||||
AddImage(files, images, "clearlogo", imagePrefix, isInMixedFolder, ImageType.Logo);
|
||||
}
|
||||
}
|
||||
|
||||
// Art
|
||||
if (!isEpisode && !isSong && !isPerson)
|
||||
{
|
||||
AddImage(files, images, "clearart", imagePrefix, isInMixedFolder, ImageType.Art);
|
||||
}
|
||||
|
||||
// For music albums, prefer cdart before disc
|
||||
if (item is MusicAlbum)
|
||||
{
|
||||
added = AddImage(files, images, "cdart", imagePrefix, isInMixedFolder, ImageType.Disc);
|
||||
|
||||
if (!added)
|
||||
{
|
||||
AddImage(files, images, "disc", imagePrefix, isInMixedFolder, ImageType.Disc);
|
||||
}
|
||||
}
|
||||
else if (item is Video || item is BoxSet)
|
||||
{
|
||||
added = AddImage(files, images, "disc", imagePrefix, isInMixedFolder, ImageType.Disc);
|
||||
|
||||
if (!added)
|
||||
{
|
||||
added = AddImage(files, images, "cdart", imagePrefix, isInMixedFolder, ImageType.Disc);
|
||||
}
|
||||
|
||||
if (!added)
|
||||
{
|
||||
AddImage(files, images, "discart", imagePrefix, isInMixedFolder, ImageType.Disc);
|
||||
}
|
||||
}
|
||||
|
||||
// Banner
|
||||
if (!isEpisode && !isSong && !isPerson)
|
||||
{
|
||||
AddImage(files, images, "banner", imagePrefix, isInMixedFolder, ImageType.Banner);
|
||||
}
|
||||
|
||||
// Thumb
|
||||
if (!isEpisode && !isSong && !isPerson)
|
||||
{
|
||||
added = AddImage(files, images, "landscape", imagePrefix, isInMixedFolder, ImageType.Thumb);
|
||||
if (!added)
|
||||
{
|
||||
AddImage(files, images, "thumb", imagePrefix, isInMixedFolder, ImageType.Thumb);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isEpisode && !isSong && !isPerson)
|
||||
{
|
||||
PopulateBackdrops(item, images, files, imagePrefix, isInMixedFolder);
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulatePrimaryImages(BaseItem item, List<LocalImageInfo> images, List<FileSystemMetadata> files, string imagePrefix, bool isInMixedFolder)
|
||||
{
|
||||
string[] imageFileNames;
|
||||
|
||||
if (item is MusicAlbum || item is MusicArtist || item is PhotoAlbum)
|
||||
{
|
||||
// these prefer folder
|
||||
imageFileNames = _musicImageFileNames;
|
||||
}
|
||||
else if (item is Person)
|
||||
{
|
||||
// these prefer folder
|
||||
imageFileNames = _personImageFileNames;
|
||||
}
|
||||
else if (item is Series)
|
||||
{
|
||||
imageFileNames = _seriesImageFileNames;
|
||||
}
|
||||
else if (item is Video && item is not Episode)
|
||||
{
|
||||
imageFileNames = _videoImageFileNames;
|
||||
}
|
||||
else
|
||||
{
|
||||
imageFileNames = _commonImageFileNames;
|
||||
}
|
||||
|
||||
var fileNameWithoutExtension = item.FileNameWithoutExtension;
|
||||
if (!string.IsNullOrEmpty(fileNameWithoutExtension))
|
||||
{
|
||||
if (AddImage(files, images, fileNameWithoutExtension, ImageType.Primary))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var name in imageFileNames)
|
||||
{
|
||||
if (AddImage(files, images, name, ImageType.Primary, imagePrefix))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isInMixedFolder)
|
||||
{
|
||||
foreach (var name in imageFileNames)
|
||||
{
|
||||
if (AddImage(files, images, name, ImageType.Primary))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateBackdrops(BaseItem item, List<LocalImageInfo> images, List<FileSystemMetadata> files, string imagePrefix, bool isInMixedFolder)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.Path))
|
||||
{
|
||||
var name = item.FileNameWithoutExtension;
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
AddImage(files, images, name + "-fanart", ImageType.Backdrop, imagePrefix);
|
||||
|
||||
// Support without the prefix if it's in its own folder
|
||||
if (!isInMixedFolder)
|
||||
{
|
||||
AddImage(files, images, name + "-fanart", ImageType.Backdrop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PopulateBackdrops(images, files, imagePrefix, "fanart", "fanart-", isInMixedFolder, ImageType.Backdrop);
|
||||
PopulateBackdrops(images, files, imagePrefix, "background", "background-", isInMixedFolder, ImageType.Backdrop);
|
||||
PopulateBackdrops(images, files, imagePrefix, "art", "art-", isInMixedFolder, ImageType.Backdrop);
|
||||
|
||||
var extraFanartFolder = files
|
||||
.FirstOrDefault(i => string.Equals(i.Name, "extrafanart", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (extraFanartFolder is not null)
|
||||
{
|
||||
PopulateBackdropsFromExtraFanart(extraFanartFolder.FullName, images);
|
||||
}
|
||||
|
||||
PopulateBackdrops(images, files, imagePrefix, "backdrop", "backdrop", isInMixedFolder, ImageType.Backdrop);
|
||||
}
|
||||
|
||||
private void PopulateBackdropsFromExtraFanart(string path, List<LocalImageInfo> images)
|
||||
{
|
||||
var imageFiles = _fileSystem.GetFiles(path, BaseItem.SupportedImageExtensions, false, false);
|
||||
|
||||
images.AddRange(imageFiles.Where(i => i.Length > 0).Select(i => new LocalImageInfo
|
||||
{
|
||||
FileInfo = i,
|
||||
Type = ImageType.Backdrop
|
||||
}));
|
||||
}
|
||||
|
||||
private void PopulateBackdrops(List<LocalImageInfo> images, List<FileSystemMetadata> files, string imagePrefix, string firstFileName, string subsequentFileNamePrefix, bool isInMixedFolder, ImageType type)
|
||||
{
|
||||
AddImage(files, images, imagePrefix + firstFileName, type);
|
||||
|
||||
var unfound = 0;
|
||||
for (var i = 1; i <= 20; i++)
|
||||
{
|
||||
// Screenshot Image
|
||||
var found = AddImage(files, images, imagePrefix + subsequentFileNamePrefix + i, type);
|
||||
|
||||
if (!found)
|
||||
{
|
||||
unfound++;
|
||||
|
||||
if (unfound >= 3)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Support without the prefix
|
||||
if (!isInMixedFolder)
|
||||
{
|
||||
AddImage(files, images, firstFileName, type);
|
||||
|
||||
unfound = 0;
|
||||
for (var i = 1; i <= 20; i++)
|
||||
{
|
||||
// Screenshot Image
|
||||
var found = AddImage(files, images, subsequentFileNamePrefix + i, type);
|
||||
|
||||
if (!found)
|
||||
{
|
||||
unfound++;
|
||||
|
||||
if (unfound >= 3)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateSeasonImagesFromSeriesFolder(Season season, List<LocalImageInfo> images, IDirectoryService directoryService)
|
||||
{
|
||||
var seasonNumber = season.IndexNumber;
|
||||
|
||||
var series = season.Series;
|
||||
if (!seasonNumber.HasValue || !series.IsFileProtocol)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var seriesFiles = GetFiles(series, false, directoryService).ToList();
|
||||
|
||||
// Try using the season name
|
||||
var prefix = season.Name.Replace(" ", string.Empty, StringComparison.Ordinal).ToLowerInvariant();
|
||||
|
||||
var filenamePrefixes = new List<string> { prefix };
|
||||
|
||||
var seasonMarker = seasonNumber.Value == 0
|
||||
? "-specials"
|
||||
: seasonNumber.Value.ToString("00", CultureInfo.InvariantCulture);
|
||||
|
||||
// Get this one directly from the file system since we have to go up a level
|
||||
if (!string.Equals(prefix, seasonMarker, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
filenamePrefixes.Add("season" + seasonMarker);
|
||||
}
|
||||
|
||||
foreach (var filename in filenamePrefixes)
|
||||
{
|
||||
AddImage(seriesFiles, images, filename + "-poster", ImageType.Primary);
|
||||
AddImage(seriesFiles, images, filename + "-fanart", ImageType.Backdrop);
|
||||
AddImage(seriesFiles, images, filename + "-banner", ImageType.Banner);
|
||||
AddImage(seriesFiles, images, filename + "-landscape", ImageType.Thumb);
|
||||
}
|
||||
}
|
||||
|
||||
private bool AddImage(List<FileSystemMetadata> files, List<LocalImageInfo> images, string name, string imagePrefix, bool isInMixedFolder, ImageType type)
|
||||
{
|
||||
var added = AddImage(files, images, name, type, imagePrefix);
|
||||
|
||||
if (!isInMixedFolder)
|
||||
{
|
||||
if (AddImage(files, images, name, type))
|
||||
{
|
||||
added = true;
|
||||
}
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
private static bool AddImage(IReadOnlyList<FileSystemMetadata> files, List<LocalImageInfo> images, string name, ImageType type, string? prefix = null)
|
||||
{
|
||||
var image = GetImage(files, name, prefix);
|
||||
|
||||
if (image is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
images.Add(new LocalImageInfo
|
||||
{
|
||||
FileInfo = image,
|
||||
Type = type
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static FileSystemMetadata? GetImage(IReadOnlyList<FileSystemMetadata> files, string name, string? prefix = null)
|
||||
{
|
||||
var fileNameLength = name.Length + (prefix?.Length ?? 0);
|
||||
for (var i = 0; i < files.Count; i++)
|
||||
{
|
||||
var file = files[i];
|
||||
if (file.IsDirectory || file.Length <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fileName = Path.GetFileNameWithoutExtension(file.FullName.AsSpan());
|
||||
if (fileName.Length == fileNameLength
|
||||
&& fileName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
|
||||
&& fileName.EndsWith(name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{7EF9F3E0-697D-42F3-A08F-19DEB5F84392}</ProjectGuid>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SharedVersion.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Code Analyzers -->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="IDisposableAnalyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,872 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Xml;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.LocalMetadata.Parsers
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a base class for parsing metadata xml.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of item xml parser.</typeparam>
|
||||
public class BaseItemXmlParser<T>
|
||||
where T : BaseItem
|
||||
{
|
||||
private Dictionary<string, string>? _validProviderIds;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseItemXmlParser{T}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{BaseItemXmlParser}"/> interface.</param>
|
||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||
public BaseItemXmlParser(ILogger<BaseItemXmlParser<T>> logger, IProviderManager providerManager)
|
||||
{
|
||||
Logger = logger;
|
||||
ProviderManager = providerManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logger.
|
||||
/// </summary>
|
||||
protected ILogger<BaseItemXmlParser<T>> Logger { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the provider manager.
|
||||
/// </summary>
|
||||
protected IProviderManager ProviderManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Fetches metadata for an item from one xml file.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="metadataFile">The metadata file.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <exception cref="ArgumentNullException">Item is null.</exception>
|
||||
public void Fetch(MetadataResult<T> item, string metadataFile, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
ArgumentException.ThrowIfNullOrEmpty(metadataFile);
|
||||
|
||||
var settings = new XmlReaderSettings
|
||||
{
|
||||
ValidationType = ValidationType.None,
|
||||
CheckCharacters = false,
|
||||
IgnoreProcessingInstructions = true,
|
||||
IgnoreComments = true
|
||||
};
|
||||
|
||||
_validProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var idInfos = ProviderManager.GetExternalIdInfos(item.Item);
|
||||
|
||||
foreach (var info in idInfos)
|
||||
{
|
||||
var id = info.Key + "Id";
|
||||
_validProviderIds.TryAdd(id, info.Key);
|
||||
}
|
||||
|
||||
// Additional Mappings
|
||||
_validProviderIds.Add("IMDB", "Imdb");
|
||||
|
||||
// Fetch(item, metadataFile, settings, Encoding.GetEncoding("ISO-8859-1"), cancellationToken);
|
||||
Fetch(item, metadataFile, settings, Encoding.UTF8, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the specified item.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="metadataFile">The metadata file.</param>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <param name="encoding">The encoding.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
private void Fetch(MetadataResult<T> item, string metadataFile, XmlReaderSettings settings, Encoding encoding, CancellationToken cancellationToken)
|
||||
{
|
||||
item.ResetPeople();
|
||||
|
||||
using var fileStream = File.OpenRead(metadataFile);
|
||||
using var streamReader = new StreamReader(fileStream, encoding);
|
||||
using var reader = XmlReader.Create(streamReader, settings);
|
||||
reader.MoveToContent();
|
||||
reader.Read();
|
||||
|
||||
// Loop through each element
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
FetchDataFromXmlNode(reader, item);
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches metadata from one Xml Element.
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <param name="itemResult">The item result.</param>
|
||||
protected virtual void FetchDataFromXmlNode(XmlReader reader, MetadataResult<T> itemResult)
|
||||
{
|
||||
var item = itemResult.Item;
|
||||
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Added":
|
||||
if (reader.TryReadDateTime(out var dateCreated))
|
||||
{
|
||||
item.DateCreated = dateCreated;
|
||||
}
|
||||
|
||||
break;
|
||||
case "OriginalTitle":
|
||||
item.OriginalTitle = reader.ReadNormalizedString();
|
||||
break;
|
||||
case "LocalTitle":
|
||||
item.Name = reader.ReadNormalizedString();
|
||||
break;
|
||||
case "CriticRating":
|
||||
{
|
||||
var text = reader.ReadElementContentAsString();
|
||||
|
||||
if (float.TryParse(text, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
item.CriticRating = value;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "SortTitle":
|
||||
item.ForcedSortName = reader.ReadNormalizedString();
|
||||
break;
|
||||
case "Overview":
|
||||
case "Description":
|
||||
item.Overview = reader.ReadNormalizedString();
|
||||
break;
|
||||
case "Language":
|
||||
item.PreferredMetadataLanguage = reader.ReadNormalizedString();
|
||||
break;
|
||||
case "CountryCode":
|
||||
item.PreferredMetadataCountryCode = reader.ReadNormalizedString();
|
||||
break;
|
||||
case "PlaceOfBirth":
|
||||
var placeOfBirth = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(placeOfBirth) && item is Person person)
|
||||
{
|
||||
person.ProductionLocations = new[] { placeOfBirth };
|
||||
}
|
||||
|
||||
break;
|
||||
case "LockedFields":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
item.LockedFields = val.Split('|').Select(i =>
|
||||
{
|
||||
if (Enum.TryParse(i, true, out MetadataField field))
|
||||
{
|
||||
return (MetadataField?)field;
|
||||
}
|
||||
|
||||
return null;
|
||||
}).Where(i => i.HasValue).Select(i => i!.Value).ToArray();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "TagLines":
|
||||
{
|
||||
if (!reader.IsEmptyElement)
|
||||
{
|
||||
using (var subtree = reader.ReadSubtree())
|
||||
{
|
||||
FetchFromTaglinesNode(subtree, item);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "Countries":
|
||||
{
|
||||
if (!reader.IsEmptyElement)
|
||||
{
|
||||
reader.Skip();
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "ContentRating":
|
||||
case "MPAARating":
|
||||
item.OfficialRating = reader.ReadNormalizedString();
|
||||
break;
|
||||
case "CustomRating":
|
||||
item.CustomRating = reader.ReadNormalizedString();
|
||||
break;
|
||||
case "RunningTime":
|
||||
var runtimeText = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(runtimeText))
|
||||
{
|
||||
if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
|
||||
{
|
||||
item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case "AspectRatio":
|
||||
var aspectRatio = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(aspectRatio) && item is IHasAspectRatio hasAspectRatio)
|
||||
{
|
||||
hasAspectRatio.AspectRatio = aspectRatio;
|
||||
}
|
||||
|
||||
break;
|
||||
case "LockData":
|
||||
item.IsLocked = string.Equals(reader.ReadNormalizedString(), "true", StringComparison.OrdinalIgnoreCase);
|
||||
break;
|
||||
case "Network":
|
||||
foreach (var name in reader.GetStringArray())
|
||||
{
|
||||
item.AddStudio(name);
|
||||
}
|
||||
|
||||
break;
|
||||
case "Director":
|
||||
foreach (var director in reader.GetPersonArray(PersonKind.Director))
|
||||
{
|
||||
itemResult.AddPerson(director);
|
||||
}
|
||||
|
||||
break;
|
||||
case "Writer":
|
||||
foreach (var writer in reader.GetPersonArray(PersonKind.Writer))
|
||||
{
|
||||
itemResult.AddPerson(writer);
|
||||
}
|
||||
|
||||
break;
|
||||
case "Actors":
|
||||
foreach (var actor in reader.GetPersonArray(PersonKind.Actor))
|
||||
{
|
||||
itemResult.AddPerson(actor);
|
||||
}
|
||||
|
||||
break;
|
||||
case "GuestStars":
|
||||
foreach (var guestStar in reader.GetPersonArray(PersonKind.GuestStar))
|
||||
{
|
||||
itemResult.AddPerson(guestStar);
|
||||
}
|
||||
|
||||
break;
|
||||
case "Trailer":
|
||||
var trailer = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(trailer))
|
||||
{
|
||||
item.AddTrailerUrl(trailer);
|
||||
}
|
||||
|
||||
break;
|
||||
case "DisplayOrder":
|
||||
var displayOrder = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(displayOrder) && item is IHasDisplayOrder hasDisplayOrder)
|
||||
{
|
||||
hasDisplayOrder.DisplayOrder = displayOrder;
|
||||
}
|
||||
|
||||
break;
|
||||
case "Trailers":
|
||||
{
|
||||
if (!reader.IsEmptyElement)
|
||||
{
|
||||
using var subtree = reader.ReadSubtree();
|
||||
FetchDataFromTrailersNode(subtree, item);
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "ProductionYear":
|
||||
if (reader.TryReadInt(out var productionYear) && productionYear > 1850)
|
||||
{
|
||||
item.ProductionYear = productionYear;
|
||||
}
|
||||
|
||||
break;
|
||||
case "Rating":
|
||||
case "IMDBrating":
|
||||
{
|
||||
var rating = reader.ReadNormalizedString();
|
||||
|
||||
if (!string.IsNullOrEmpty(rating))
|
||||
{
|
||||
// All external meta is saving this as '.' for decimal I believe...but just to be sure
|
||||
if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
item.CommunityRating = val;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "BirthDate":
|
||||
case "PremiereDate":
|
||||
case "FirstAired":
|
||||
if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var firstAired))
|
||||
{
|
||||
item.PremiereDate = firstAired;
|
||||
item.ProductionYear = firstAired.Year;
|
||||
}
|
||||
|
||||
break;
|
||||
case "DeathDate":
|
||||
case "EndDate":
|
||||
if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var endDate))
|
||||
{
|
||||
item.EndDate = endDate;
|
||||
}
|
||||
|
||||
break;
|
||||
case "CollectionNumber":
|
||||
var tmdbCollection = reader.ReadNormalizedString();
|
||||
item.TrySetProviderId(MetadataProvider.TmdbCollection, tmdbCollection);
|
||||
|
||||
break;
|
||||
|
||||
case "Genres":
|
||||
{
|
||||
if (!reader.IsEmptyElement)
|
||||
{
|
||||
using var subtree = reader.ReadSubtree();
|
||||
FetchFromGenresNode(subtree, item);
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "Tags":
|
||||
{
|
||||
if (!reader.IsEmptyElement)
|
||||
{
|
||||
using var subtree = reader.ReadSubtree();
|
||||
FetchFromTagsNode(subtree, item);
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "Persons":
|
||||
{
|
||||
if (!reader.IsEmptyElement)
|
||||
{
|
||||
using var subtree = reader.ReadSubtree();
|
||||
FetchDataFromPersonsNode(subtree, itemResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "Studios":
|
||||
{
|
||||
if (!reader.IsEmptyElement)
|
||||
{
|
||||
using var subtree = reader.ReadSubtree();
|
||||
FetchFromStudiosNode(subtree, item);
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "Shares":
|
||||
{
|
||||
if (!reader.IsEmptyElement)
|
||||
{
|
||||
using var subtree = reader.ReadSubtree();
|
||||
if (item is IHasShares hasShares)
|
||||
{
|
||||
FetchFromSharesNode(subtree, hasShares);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "OwnerUserId":
|
||||
{
|
||||
var val = reader.ReadNormalizedString();
|
||||
|
||||
if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty))
|
||||
{
|
||||
if (item is Playlist playlist)
|
||||
{
|
||||
playlist.OwnerUserId = guid;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "Format3D":
|
||||
{
|
||||
var val = reader.ReadNormalizedString();
|
||||
|
||||
if (item is Video video)
|
||||
{
|
||||
if (string.Equals("HSBS", val, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.Video3DFormat = Video3DFormat.HalfSideBySide;
|
||||
}
|
||||
else if (string.Equals("HTAB", val, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.Video3DFormat = Video3DFormat.HalfTopAndBottom;
|
||||
}
|
||||
else if (string.Equals("FTAB", val, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.Video3DFormat = Video3DFormat.FullTopAndBottom;
|
||||
}
|
||||
else if (string.Equals("FSBS", val, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.Video3DFormat = Video3DFormat.FullSideBySide;
|
||||
}
|
||||
else if (string.Equals("MVC", val, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.Video3DFormat = Video3DFormat.MVC;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
string readerName = reader.Name;
|
||||
if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue))
|
||||
{
|
||||
var id = reader.ReadNormalizedString();
|
||||
item.TrySetProviderId(providerIdValue, id);
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Skip();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FetchFromSharesNode(XmlReader reader, IHasShares item)
|
||||
{
|
||||
var list = new List<PlaylistUserPermissions>();
|
||||
|
||||
reader.MoveToContent();
|
||||
reader.Read();
|
||||
|
||||
// Loop through each element
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Share":
|
||||
{
|
||||
if (reader.IsEmptyElement)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
using (var subReader = reader.ReadSubtree())
|
||||
{
|
||||
var child = GetShare(subReader);
|
||||
|
||||
if (child is not null)
|
||||
{
|
||||
list.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
}
|
||||
|
||||
item.Shares = [.. list];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches from taglines node.
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
private void FetchFromTaglinesNode(XmlReader reader, T item)
|
||||
{
|
||||
reader.MoveToContent();
|
||||
reader.Read();
|
||||
|
||||
// Loop through each element
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Tagline":
|
||||
var val = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
item.Tagline = val;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches from genres node.
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
private void FetchFromGenresNode(XmlReader reader, T item)
|
||||
{
|
||||
reader.MoveToContent();
|
||||
reader.Read();
|
||||
|
||||
// Loop through each element
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Genre":
|
||||
var genre = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(genre))
|
||||
{
|
||||
item.AddGenre(genre);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FetchFromTagsNode(XmlReader reader, BaseItem item)
|
||||
{
|
||||
reader.MoveToContent();
|
||||
reader.Read();
|
||||
|
||||
var tags = new List<string>();
|
||||
|
||||
// Loop through each element
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Tag":
|
||||
var tag = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(tag))
|
||||
{
|
||||
tags.Add(tag);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
}
|
||||
|
||||
item.Tags = tags.Distinct(StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the data from persons node.
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
private void FetchDataFromPersonsNode(XmlReader reader, MetadataResult<T> item)
|
||||
{
|
||||
reader.MoveToContent();
|
||||
reader.Read();
|
||||
|
||||
// Loop through each element
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Person":
|
||||
case "Actor":
|
||||
var person = reader.GetPersonFromXmlNode();
|
||||
if (person is not null)
|
||||
{
|
||||
item.AddPerson(person);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FetchDataFromTrailersNode(XmlReader reader, T item)
|
||||
{
|
||||
reader.MoveToContent();
|
||||
reader.Read();
|
||||
|
||||
// Loop through each element
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Trailer":
|
||||
var trailer = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(trailer))
|
||||
{
|
||||
item.AddTrailerUrl(trailer);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches from studios node.
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
private void FetchFromStudiosNode(XmlReader reader, T item)
|
||||
{
|
||||
reader.MoveToContent();
|
||||
reader.Read();
|
||||
|
||||
// Loop through each element
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Studio":
|
||||
var studio = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(studio))
|
||||
{
|
||||
item.AddStudio(studio);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get linked child.
|
||||
/// </summary>
|
||||
/// <param name="reader">The xml reader.</param>
|
||||
/// <returns>The linked child.</returns>
|
||||
protected LinkedChild? GetLinkedChild(XmlReader reader)
|
||||
{
|
||||
var linkedItem = new LinkedChild { Type = LinkedChildType.Manual };
|
||||
|
||||
reader.MoveToContent();
|
||||
reader.Read();
|
||||
|
||||
// Loop through each element
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Path":
|
||||
linkedItem.Path = reader.ReadNormalizedString();
|
||||
break;
|
||||
case "ItemId":
|
||||
linkedItem.LibraryItemId = reader.ReadNormalizedString();
|
||||
break;
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
}
|
||||
|
||||
// This is valid
|
||||
if (!string.IsNullOrWhiteSpace(linkedItem.Path) || !string.IsNullOrWhiteSpace(linkedItem.LibraryItemId))
|
||||
{
|
||||
return linkedItem;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get share.
|
||||
/// </summary>
|
||||
/// <param name="reader">The xml reader.</param>
|
||||
/// <returns>The share.</returns>
|
||||
protected PlaylistUserPermissions? GetShare(XmlReader reader)
|
||||
{
|
||||
reader.MoveToContent();
|
||||
reader.Read();
|
||||
string? userId = null;
|
||||
var canEdit = false;
|
||||
|
||||
// Loop through each element
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "UserId":
|
||||
userId = reader.ReadNormalizedString();
|
||||
break;
|
||||
case "CanEdit":
|
||||
canEdit = string.Equals(reader.ReadNormalizedString(), "true", StringComparison.OrdinalIgnoreCase);
|
||||
break;
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
}
|
||||
|
||||
// This is valid
|
||||
if (!string.IsNullOrEmpty(userId) && Guid.TryParse(userId, out var guid))
|
||||
{
|
||||
return new PlaylistUserPermissions(guid, canEdit);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Xml;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.LocalMetadata.Parsers
|
||||
{
|
||||
/// <summary>
|
||||
/// The box set xml parser.
|
||||
/// </summary>
|
||||
public class BoxSetXmlParser : BaseItemXmlParser<BoxSet>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BoxSetXmlParser"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{BoxSetXmlParser}"/> interface.</param>
|
||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||
public BoxSetXmlParser(ILogger<BoxSetXmlParser> logger, IProviderManager providerManager)
|
||||
: base(logger, providerManager)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<BoxSet> itemResult)
|
||||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "CollectionItems":
|
||||
|
||||
if (!reader.IsEmptyElement)
|
||||
{
|
||||
using (var subReader = reader.ReadSubtree())
|
||||
{
|
||||
FetchFromCollectionItemsNode(subReader, itemResult);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
base.FetchDataFromXmlNode(reader, itemResult);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void FetchFromCollectionItemsNode(XmlReader reader, MetadataResult<BoxSet> item)
|
||||
{
|
||||
var list = new List<LinkedChild>();
|
||||
|
||||
reader.MoveToContent();
|
||||
reader.Read();
|
||||
|
||||
// Loop through each element
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "CollectionItem":
|
||||
{
|
||||
if (!reader.IsEmptyElement)
|
||||
{
|
||||
using (var subReader = reader.ReadSubtree())
|
||||
{
|
||||
var child = GetLinkedChild(subReader);
|
||||
|
||||
if (child is not null)
|
||||
{
|
||||
list.Add(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
}
|
||||
|
||||
item.Item.LinkedChildren = list.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.LocalMetadata.Parsers
|
||||
{
|
||||
/// <summary>
|
||||
/// Playlist xml parser.
|
||||
/// </summary>
|
||||
public class PlaylistXmlParser : BaseItemXmlParser<Playlist>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PlaylistXmlParser"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{PlaylistXmlParser}"/> interface.</param>
|
||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||
public PlaylistXmlParser(ILogger<PlaylistXmlParser> logger, IProviderManager providerManager)
|
||||
: base(logger, providerManager)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Playlist> itemResult)
|
||||
{
|
||||
var item = itemResult.Item;
|
||||
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "PlaylistMediaType":
|
||||
if (Enum.TryParse<MediaType>(reader.ReadNormalizedString(), out var mediaType))
|
||||
{
|
||||
item.PlaylistMediaType = mediaType;
|
||||
}
|
||||
|
||||
break;
|
||||
case "PlaylistItems":
|
||||
|
||||
if (!reader.IsEmptyElement)
|
||||
{
|
||||
using (var subReader = reader.ReadSubtree())
|
||||
{
|
||||
FetchFromCollectionItemsNode(subReader, item);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
base.FetchDataFromXmlNode(reader, itemResult);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void FetchFromCollectionItemsNode(XmlReader reader, Playlist item)
|
||||
{
|
||||
var list = new List<LinkedChild>();
|
||||
|
||||
reader.MoveToContent();
|
||||
reader.Read();
|
||||
|
||||
// Loop through each element
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "PlaylistItem":
|
||||
{
|
||||
if (reader.IsEmptyElement)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
using (var subReader = reader.ReadSubtree())
|
||||
{
|
||||
var child = GetLinkedChild(subReader);
|
||||
|
||||
if (child is not null)
|
||||
{
|
||||
list.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
}
|
||||
|
||||
item.LinkedChildren = list.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("MediaBrowser.LocalMetadata")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("Jellyfin Project")]
|
||||
[assembly: AssemblyProduct("Jellyfin Server")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
[assembly: NeutralResourcesLanguage("en")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.LocalMetadata.Parsers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.LocalMetadata.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Class BoxSetXmlProvider.
|
||||
/// </summary>
|
||||
public class BoxSetXmlProvider : BaseXmlProvider<BoxSet>
|
||||
{
|
||||
private readonly ILogger<BoxSetXmlParser> _logger;
|
||||
private readonly IProviderManager _providerManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BoxSetXmlProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{BoxSetXmlParser}"/> interface.</param>
|
||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||
public BoxSetXmlProvider(IFileSystem fileSystem, ILogger<BoxSetXmlParser> logger, IProviderManager providerManager)
|
||||
: base(fileSystem)
|
||||
{
|
||||
_logger = logger;
|
||||
_providerManager = providerManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Fetch(MetadataResult<BoxSet> result, string path, CancellationToken cancellationToken)
|
||||
{
|
||||
new BoxSetXmlParser(_logger, _providerManager).Fetch(result, path, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService)
|
||||
{
|
||||
return directoryService.GetFile(Path.Combine(info.Path, "collection.xml"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System.Threading;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.LocalMetadata.Parsers;
|
||||
using MediaBrowser.LocalMetadata.Savers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.LocalMetadata.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Playlist xml provider.
|
||||
/// </summary>
|
||||
public class PlaylistXmlProvider : BaseXmlProvider<Playlist>
|
||||
{
|
||||
private readonly ILogger<PlaylistXmlParser> _logger;
|
||||
private readonly IProviderManager _providerManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PlaylistXmlProvider"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{PlaylistXmlParser}"/> interface.</param>
|
||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||
public PlaylistXmlProvider(
|
||||
IFileSystem fileSystem,
|
||||
ILogger<PlaylistXmlParser> logger,
|
||||
IProviderManager providerManager)
|
||||
: base(fileSystem)
|
||||
{
|
||||
_logger = logger;
|
||||
_providerManager = providerManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Fetch(MetadataResult<Playlist> result, string path, CancellationToken cancellationToken)
|
||||
{
|
||||
new PlaylistXmlParser(_logger, _providerManager).Fetch(result, path, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService)
|
||||
{
|
||||
return directoryService.GetFile(PlaylistXmlSaver.GetSavePath(info.Path));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.LocalMetadata.Savers
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public abstract class BaseXmlSaver : IMetadataFileSaver
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseXmlSaver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{BaseXmlSaver}"/> interface.</param>
|
||||
protected BaseXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger<BaseXmlSaver> logger)
|
||||
{
|
||||
FileSystem = fileSystem;
|
||||
ConfigurationManager = configurationManager;
|
||||
LibraryManager = libraryManager;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file system.
|
||||
/// </summary>
|
||||
protected IFileSystem FileSystem { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration manager.
|
||||
/// </summary>
|
||||
protected IServerConfigurationManager ConfigurationManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the library manager.
|
||||
/// </summary>
|
||||
protected ILibraryManager LibraryManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logger.
|
||||
/// </summary>
|
||||
protected ILogger<BaseXmlSaver> Logger { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name => XmlProviderUtils.Name;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetSavePath(BaseItem item)
|
||||
{
|
||||
return GetLocalSavePath(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the save path.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
protected abstract string GetLocalSavePath(BaseItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the root element.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
protected virtual string GetRootElementName(BaseItem item)
|
||||
=> "Item";
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether [is enabled for] [the specified item].
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="updateType">Type of the update.</param>
|
||||
/// <returns><c>true</c> if [is enabled for] [the specified item]; otherwise, <c>false</c>.</returns>
|
||||
public abstract bool IsEnabledFor(BaseItem item, ItemUpdateType updateType);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SaveAsync(BaseItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
var path = GetSavePath(item);
|
||||
var directory = Path.GetDirectoryName(path) ?? throw new InvalidDataException($"Provided path ({path}) is not valid.");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// On Windows, saving the file will fail if the file is hidden or readonly
|
||||
FileSystem.SetAttributes(path, false, false);
|
||||
|
||||
var fileStreamOptions = new FileStreamOptions()
|
||||
{
|
||||
Mode = FileMode.Create,
|
||||
Access = FileAccess.Write,
|
||||
Share = FileShare.None
|
||||
};
|
||||
|
||||
var filestream = new FileStream(path, fileStreamOptions);
|
||||
await using (filestream.ConfigureAwait(false))
|
||||
{
|
||||
var settings = new XmlWriterSettings
|
||||
{
|
||||
Indent = true,
|
||||
Encoding = Encoding.UTF8,
|
||||
Async = true
|
||||
};
|
||||
|
||||
var writer = XmlWriter.Create(filestream, settings);
|
||||
await using (writer.ConfigureAwait(false))
|
||||
{
|
||||
var root = GetRootElementName(item);
|
||||
|
||||
await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
|
||||
|
||||
await writer.WriteStartElementAsync(null, root, null).ConfigureAwait(false);
|
||||
|
||||
var baseItem = item;
|
||||
|
||||
if (baseItem is not null)
|
||||
{
|
||||
await AddCommonNodesAsync(baseItem, writer).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await WriteCustomElementsAsync(item, writer).ConfigureAwait(false);
|
||||
|
||||
await writer.WriteEndElementAsync().ConfigureAwait(false);
|
||||
|
||||
await writer.WriteEndDocumentAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (ConfigurationManager.Configuration.SaveMetadataHidden)
|
||||
{
|
||||
SetHidden(path, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetHidden(string path, bool hidden)
|
||||
{
|
||||
try
|
||||
{
|
||||
FileSystem.SetHidden(path, hidden);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error setting hidden attribute on {Path}", path);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write custom elements.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="writer">The xml writer.</param>
|
||||
/// <returns>The task object representing the asynchronous operation.</returns>
|
||||
protected abstract Task WriteCustomElementsAsync(BaseItem item, XmlWriter writer);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the common nodes.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="writer">The xml writer.</param>
|
||||
/// <returns>The task object representing the asynchronous operation.</returns>
|
||||
private async Task AddCommonNodesAsync(BaseItem item, XmlWriter writer)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(item.OfficialRating))
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "ContentRating", null, item.OfficialRating).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await writer.WriteElementStringAsync(null, "Added", null, item.DateCreated.ToLocalTime().ToString("G", CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
|
||||
await writer.WriteElementStringAsync(null, "LockData", null, item.IsLocked.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()).ConfigureAwait(false);
|
||||
|
||||
if (item.LockedFields.Length > 0)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "LockedFields", null, string.Join('|', item.LockedFields)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (item.CriticRating.HasValue)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "CriticRating", null, item.CriticRating.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(item.Overview))
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "Overview", null, item.Overview).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(item.OriginalTitle))
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "OriginalTitle", null, item.OriginalTitle).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(item.CustomRating))
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "CustomRating", null, item.CustomRating).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(item.Name) && item is not Episode)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "LocalTitle", null, item.Name).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var forcedSortName = item.ForcedSortName;
|
||||
if (!string.IsNullOrEmpty(forcedSortName))
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "SortTitle", null, forcedSortName).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (item.PremiereDate.HasValue)
|
||||
{
|
||||
if (item is Person)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "BirthDate", null, item.PremiereDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
}
|
||||
else if (item is not Episode)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "PremiereDate", null, item.PremiereDate.Value.ToLocalTime().ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.EndDate.HasValue)
|
||||
{
|
||||
if (item is Person)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "DeathDate", null, item.EndDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
}
|
||||
else if (item is not Episode)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "EndDate", null, item.EndDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.RemoteTrailers.Count > 0)
|
||||
{
|
||||
await writer.WriteStartElementAsync(null, "Trailers", null).ConfigureAwait(false);
|
||||
|
||||
foreach (var trailer in item.RemoteTrailers)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "Trailer", null, trailer.Url).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await writer.WriteEndElementAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (item.ProductionLocations.Length > 0)
|
||||
{
|
||||
await writer.WriteStartElementAsync(null, "Countries", null).ConfigureAwait(false);
|
||||
|
||||
foreach (var name in item.ProductionLocations)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "Country", null, name).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await writer.WriteEndElementAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (item is IHasDisplayOrder hasDisplayOrder && !string.IsNullOrEmpty(hasDisplayOrder.DisplayOrder))
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "DisplayOrder", null, hasDisplayOrder.DisplayOrder).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (item.CommunityRating.HasValue)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "Rating", null, item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (item.ProductionYear.HasValue && item is not Person)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "ProductionYear", null, item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (item is IHasAspectRatio hasAspectRatio)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(hasAspectRatio.AspectRatio))
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "AspectRatio", null, hasAspectRatio.AspectRatio).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(item.PreferredMetadataLanguage))
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "Language", null, item.PreferredMetadataLanguage).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(item.PreferredMetadataCountryCode))
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "CountryCode", null, item.PreferredMetadataCountryCode).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Use original runtime here, actual file runtime later in MediaInfo
|
||||
var runTimeTicks = item.RunTimeTicks;
|
||||
|
||||
if (runTimeTicks.HasValue)
|
||||
{
|
||||
var timespan = TimeSpan.FromTicks(runTimeTicks.Value);
|
||||
|
||||
await writer.WriteElementStringAsync(null, "RunningTime", null, Math.Floor(timespan.TotalMinutes).ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (item.ProviderIds is not null)
|
||||
{
|
||||
foreach (var providerKey in item.ProviderIds.Keys)
|
||||
{
|
||||
var providerId = item.ProviderIds[providerKey];
|
||||
if (!string.IsNullOrEmpty(providerId))
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, providerKey + "Id", null, providerId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(item.Tagline))
|
||||
{
|
||||
await writer.WriteStartElementAsync(null, "Taglines", null).ConfigureAwait(false);
|
||||
await writer.WriteElementStringAsync(null, "Tagline", null, item.Tagline).ConfigureAwait(false);
|
||||
await writer.WriteEndElementAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (item.Genres.Length > 0)
|
||||
{
|
||||
await writer.WriteStartElementAsync(null, "Genres", null).ConfigureAwait(false);
|
||||
|
||||
foreach (var genre in item.Genres)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "Genre", null, genre).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await writer.WriteEndElementAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (item.Studios.Length > 0)
|
||||
{
|
||||
await writer.WriteStartElementAsync(null, "Studios", null).ConfigureAwait(false);
|
||||
|
||||
foreach (var studio in item.Studios)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "Studio", null, studio).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await writer.WriteEndElementAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (item.Tags.Length > 0)
|
||||
{
|
||||
await writer.WriteStartElementAsync(null, "Tags", null).ConfigureAwait(false);
|
||||
|
||||
foreach (var tag in item.Tags)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "Tag", null, tag).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await writer.WriteEndElementAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var people = LibraryManager.GetPeople(item);
|
||||
|
||||
if (people.Count > 0)
|
||||
{
|
||||
await writer.WriteStartElementAsync(null, "Persons", null).ConfigureAwait(false);
|
||||
|
||||
foreach (var person in people)
|
||||
{
|
||||
await writer.WriteStartElementAsync(null, "Person", null).ConfigureAwait(false);
|
||||
await writer.WriteElementStringAsync(null, "Name", null, person.Name).ConfigureAwait(false);
|
||||
await writer.WriteElementStringAsync(null, "Type", null, person.Type.ToString()).ConfigureAwait(false);
|
||||
await writer.WriteElementStringAsync(null, "Role", null, person.Role).ConfigureAwait(false);
|
||||
|
||||
if (person.SortOrder.HasValue)
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "SortOrder", null, person.SortOrder.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await writer.WriteEndElementAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await writer.WriteEndElementAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (item is BoxSet boxset)
|
||||
{
|
||||
await AddLinkedChildren(boxset, writer, "CollectionItems", "CollectionItem").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (item is Playlist playlist && !Playlist.IsPlaylistFile(playlist.Path))
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "OwnerUserId", null, playlist.OwnerUserId.ToString("N")).ConfigureAwait(false);
|
||||
await AddLinkedChildren(playlist, writer, "PlaylistItems", "PlaylistItem").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (item is IHasShares hasShares)
|
||||
{
|
||||
await AddSharesAsync(hasShares, writer).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await AddMediaInfo(item, writer).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add shares.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="writer">The xml writer.</param>
|
||||
/// <returns>The task object representing the asynchronous operation.</returns>
|
||||
private static async Task AddSharesAsync(IHasShares item, XmlWriter writer)
|
||||
{
|
||||
await writer.WriteStartElementAsync(null, "Shares", null).ConfigureAwait(false);
|
||||
|
||||
foreach (var share in item.Shares)
|
||||
{
|
||||
await writer.WriteStartElementAsync(null, "Share", null).ConfigureAwait(false);
|
||||
|
||||
await writer.WriteElementStringAsync(null, "UserId", null, share.UserId.ToString()).ConfigureAwait(false);
|
||||
await writer.WriteElementStringAsync(
|
||||
null,
|
||||
"CanEdit",
|
||||
null,
|
||||
share.CanEdit.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()).ConfigureAwait(false);
|
||||
|
||||
await writer.WriteEndElementAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await writer.WriteEndElementAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the media info.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="writer">The xml writer.</param>
|
||||
/// <typeparam name="T">Type of item.</typeparam>
|
||||
/// <returns>The task object representing the asynchronous operation.</returns>
|
||||
private static Task AddMediaInfo<T>(T item, XmlWriter writer)
|
||||
where T : BaseItem
|
||||
{
|
||||
if (item is Video video && video.Video3DFormat.HasValue)
|
||||
{
|
||||
return video.Video3DFormat switch
|
||||
{
|
||||
Video3DFormat.FullSideBySide =>
|
||||
writer.WriteElementStringAsync(null, "Format3D", null, "FSBS"),
|
||||
Video3DFormat.FullTopAndBottom =>
|
||||
writer.WriteElementStringAsync(null, "Format3D", null, "FTAB"),
|
||||
Video3DFormat.HalfSideBySide =>
|
||||
writer.WriteElementStringAsync(null, "Format3D", null, "HSBS"),
|
||||
Video3DFormat.HalfTopAndBottom =>
|
||||
writer.WriteElementStringAsync(null, "Format3D", null, "HTAB"),
|
||||
Video3DFormat.MVC =>
|
||||
writer.WriteElementStringAsync(null, "Format3D", null, "MVC"),
|
||||
_ => Task.CompletedTask
|
||||
};
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ADd linked children.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="writer">The xml writer.</param>
|
||||
/// <param name="pluralNodeName">The plural node name.</param>
|
||||
/// <param name="singularNodeName">The singular node name.</param>
|
||||
/// <returns>The task object representing the asynchronous operation.</returns>
|
||||
private static async Task AddLinkedChildren(Folder item, XmlWriter writer, string pluralNodeName, string singularNodeName)
|
||||
{
|
||||
var items = item.LinkedChildren
|
||||
.Where(i => i.Type == LinkedChildType.Manual)
|
||||
.ToList();
|
||||
|
||||
if (items.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await writer.WriteStartElementAsync(null, pluralNodeName, null).ConfigureAwait(false);
|
||||
|
||||
foreach (var link in items)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(link.Path) || !string.IsNullOrWhiteSpace(link.LibraryItemId))
|
||||
{
|
||||
await writer.WriteStartElementAsync(null, singularNodeName, null).ConfigureAwait(false);
|
||||
if (!string.IsNullOrWhiteSpace(link.Path))
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "Path", null, link.Path).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(link.LibraryItemId))
|
||||
{
|
||||
await writer.WriteElementStringAsync(null, "ItemId", null, link.LibraryItemId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await writer.WriteEndElementAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
await writer.WriteEndElementAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.LocalMetadata.Savers
|
||||
{
|
||||
/// <summary>
|
||||
/// Box set xml saver.
|
||||
/// </summary>
|
||||
public class BoxSetXmlSaver : BaseXmlSaver
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BoxSetXmlSaver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{BoxSetXmlSaver}"/> interface.</param>
|
||||
public BoxSetXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger<BoxSetXmlSaver> logger)
|
||||
: base(fileSystem, configurationManager, libraryManager, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType)
|
||||
{
|
||||
if (!item.SupportsLocalMetadata)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return item is BoxSet && updateType >= ItemUpdateType.MetadataDownload;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task WriteCustomElementsAsync(BaseItem item, XmlWriter writer)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string GetLocalSavePath(BaseItem item)
|
||||
{
|
||||
return Path.Combine(item.Path, "collection.xml");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.LocalMetadata.Savers
|
||||
{
|
||||
/// <summary>
|
||||
/// Playlist xml saver.
|
||||
/// </summary>
|
||||
public class PlaylistXmlSaver : BaseXmlSaver
|
||||
{
|
||||
/// <summary>
|
||||
/// The default file name to use when creating a new playlist.
|
||||
/// </summary>
|
||||
public const string DefaultPlaylistFilename = "playlist.xml";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PlaylistXmlSaver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{PlaylistXmlSaver}"/> interface.</param>
|
||||
public PlaylistXmlSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger<PlaylistXmlSaver> logger)
|
||||
: base(fileSystem, configurationManager, libraryManager, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType)
|
||||
{
|
||||
if (!item.SupportsLocalMetadata)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return item is Playlist && updateType >= ItemUpdateType.MetadataImport;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task WriteCustomElementsAsync(BaseItem item, XmlWriter writer)
|
||||
{
|
||||
var game = (Playlist)item;
|
||||
|
||||
if (game.PlaylistMediaType == MediaType.Unknown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await writer.WriteElementStringAsync(null, "PlaylistMediaType", null, game.PlaylistMediaType.ToString()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string GetLocalSavePath(BaseItem item)
|
||||
{
|
||||
return GetSavePath(item.Path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the save path.
|
||||
/// </summary>
|
||||
/// <param name="itemPath">The item path.</param>
|
||||
/// <returns>The save path.</returns>
|
||||
public static string GetSavePath(string itemPath)
|
||||
{
|
||||
var path = itemPath;
|
||||
|
||||
if (Playlist.IsPlaylistFile(path))
|
||||
{
|
||||
return Path.ChangeExtension(itemPath, ".xml");
|
||||
}
|
||||
|
||||
return Path.Combine(path, DefaultPlaylistFilename);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace MediaBrowser.LocalMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// The xml provider utils.
|
||||
/// </summary>
|
||||
public static class XmlProviderUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name.
|
||||
/// </summary>
|
||||
public static string Name => "Emby Xml";
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v10.0", FrameworkDisplayName = ".NET 10.0")]
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
is_global = true
|
||||
build_property.TargetFramework = net10.0
|
||||
build_property.TargetFramework = net10.0
|
||||
build_property.TargetFrameworkIdentifier = .NETCoreApp
|
||||
build_property.TargetFrameworkIdentifier = .NETCoreApp
|
||||
build_property.TargetFrameworkVersion = v10.0
|
||||
build_property.TargetFrameworkVersion = v10.0
|
||||
build_property.TargetPlatformMinVersion =
|
||||
build_property.TargetPlatformMinVersion =
|
||||
build_property.UsingMicrosoftNETSdkWeb =
|
||||
build_property.UsingMicrosoftNETSdkWeb =
|
||||
build_property.ProjectTypeGuids =
|
||||
build_property.ProjectTypeGuids =
|
||||
build_property.InvariantGlobalization =
|
||||
build_property.InvariantGlobalization =
|
||||
build_property.PlatformNeutralAssembly =
|
||||
build_property.PlatformNeutralAssembly =
|
||||
build_property.EnforceExtendedAnalyzerRules =
|
||||
build_property.EnforceExtendedAnalyzerRules =
|
||||
build_property._SupportedPlatformList = Linux,macOS,Windows
|
||||
build_property._SupportedPlatformList = Linux,macOS,Windows
|
||||
build_property.RootNamespace = MediaBrowser.LocalMetadata
|
||||
build_property.ProjectDir = /srv/common_drive/Projects/pgsql-jellyfin/MediaBrowser.LocalMetadata/
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.EffectiveAnalysisLevelStyle = 10.0
|
||||
build_property.EnableCodeStyleSeverity =
|
||||
Binary file not shown.
BIN
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
||||
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/home/wjones/.nuget/packages/</NuGetPackageRoot>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/home/wjones/.nuget/packages/</NuGetPackageFolders>
|
||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<SourceRoot Include="/home/wjones/.nuget/packages/" />
|
||||
</ItemGroup>
|
||||
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<Import Project="$(NuGetPackageRoot)microsoft.entityframeworkcore/10.0.3/buildTransitive/net10.0/Microsoft.EntityFrameworkCore.props" Condition="Exists('$(NuGetPackageRoot)microsoft.entityframeworkcore/10.0.3/buildTransitive/net10.0/Microsoft.EntityFrameworkCore.props')" />
|
||||
<Import Project="$(NuGetPackageRoot)microsoft.codeanalysis.bannedapianalyzers/4.14.0/buildTransitive/Microsoft.CodeAnalysis.BannedApiAnalyzers.props" Condition="Exists('$(NuGetPackageRoot)microsoft.codeanalysis.bannedapianalyzers/4.14.0/buildTransitive/Microsoft.CodeAnalysis.BannedApiAnalyzers.props')" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<PkgStyleCop_Analyzers_Unstable Condition=" '$(PkgStyleCop_Analyzers_Unstable)' == '' ">/home/wjones/.nuget/packages/stylecop.analyzers.unstable/1.2.0.556</PkgStyleCop_Analyzers_Unstable>
|
||||
<PkgSmartAnalyzers_MultithreadingAnalyzer Condition=" '$(PkgSmartAnalyzers_MultithreadingAnalyzer)' == '' ">/home/wjones/.nuget/packages/smartanalyzers.multithreadinganalyzer/1.1.31</PkgSmartAnalyzers_MultithreadingAnalyzer>
|
||||
<PkgSerilogAnalyzer Condition=" '$(PkgSerilogAnalyzer)' == '' ">/home/wjones/.nuget/packages/seriloganalyzer/0.15.0</PkgSerilogAnalyzer>
|
||||
<PkgMicrosoft_CodeAnalysis_BannedApiAnalyzers Condition=" '$(PkgMicrosoft_CodeAnalysis_BannedApiAnalyzers)' == '' ">/home/wjones/.nuget/packages/microsoft.codeanalysis.bannedapianalyzers/4.14.0</PkgMicrosoft_CodeAnalysis_BannedApiAnalyzers>
|
||||
<PkgIDisposableAnalyzers Condition=" '$(PkgIDisposableAnalyzers)' == '' ">/home/wjones/.nuget/packages/idisposableanalyzers/4.0.8</PkgIDisposableAnalyzers>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<Import Project="$(NuGetPackageRoot)microsoft.extensions.options/10.0.3/buildTransitive/net8.0/Microsoft.Extensions.Options.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.options/10.0.3/buildTransitive/net8.0/Microsoft.Extensions.Options.targets')" />
|
||||
<Import Project="$(NuGetPackageRoot)microsoft.extensions.logging.abstractions/10.0.3/buildTransitive/net8.0/Microsoft.Extensions.Logging.Abstractions.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.logging.abstractions/10.0.3/buildTransitive/net8.0/Microsoft.Extensions.Logging.Abstractions.targets')" />
|
||||
<Import Project="$(NuGetPackageRoot)microsoft.extensions.configuration.binder/10.0.3/buildTransitive/netstandard2.0/Microsoft.Extensions.Configuration.Binder.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.configuration.binder/10.0.3/buildTransitive/netstandard2.0/Microsoft.Extensions.Configuration.Binder.targets')" />
|
||||
<Import Project="$(NuGetPackageRoot)microsoft.codeanalysis.bannedapianalyzers/4.14.0/buildTransitive/Microsoft.CodeAnalysis.BannedApiAnalyzers.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.codeanalysis.bannedapianalyzers/4.14.0/buildTransitive/Microsoft.CodeAnalysis.BannedApiAnalyzers.targets')" />
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "zRcj+30okik=",
|
||||
"success": true,
|
||||
"projectFilePath": "/srv/common_drive/Projects/pgsql-jellyfin/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj",
|
||||
"expectedPackageFiles": [
|
||||
"/home/wjones/.nuget/packages/bitfaster.caching/2.5.4/bitfaster.caching.2.5.4.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/diacritics/4.1.4/diacritics.4.1.4.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/icu4n/60.1.0-alpha.356/icu4n.60.1.0-alpha.356.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/icu4n.transliterator/60.1.0-alpha.356/icu4n.transliterator.60.1.0-alpha.356.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/idisposableanalyzers/4.0.8/idisposableanalyzers.4.0.8.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/j2n/2.0.0/j2n.2.0.0.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.codeanalysis.bannedapianalyzers/4.14.0/microsoft.codeanalysis.bannedapianalyzers.4.14.0.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore/10.0.3/microsoft.entityframeworkcore.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore.abstractions/10.0.3/microsoft.entityframeworkcore.abstractions.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore.analyzers/10.0.3/microsoft.entityframeworkcore.analyzers.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore.relational/10.0.3/microsoft.entityframeworkcore.relational.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.caching.abstractions/10.0.3/microsoft.extensions.caching.abstractions.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.caching.memory/10.0.3/microsoft.extensions.caching.memory.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.configuration/10.0.3/microsoft.extensions.configuration.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.configuration.abstractions/10.0.3/microsoft.extensions.configuration.abstractions.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.configuration.binder/10.0.3/microsoft.extensions.configuration.binder.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.dependencyinjection/10.0.3/microsoft.extensions.dependencyinjection.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.dependencyinjection.abstractions/10.0.3/microsoft.extensions.dependencyinjection.abstractions.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.logging/10.0.3/microsoft.extensions.logging.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.logging.abstractions/10.0.3/microsoft.extensions.logging.abstractions.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.options/10.0.3/microsoft.extensions.options.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/microsoft.extensions.primitives/10.0.3/microsoft.extensions.primitives.10.0.3.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/nebml/1.1.0.5/nebml.1.1.0.5.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/polly/8.6.5/polly.8.6.5.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/polly.core/8.6.5/polly.core.8.6.5.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/seriloganalyzer/0.15.0/seriloganalyzer.0.15.0.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/smartanalyzers.multithreadinganalyzer/1.1.31/smartanalyzers.multithreadinganalyzer.1.1.31.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/stylecop.analyzers/1.2.0-beta.556/stylecop.analyzers.1.2.0-beta.556.nupkg.sha512",
|
||||
"/home/wjones/.nuget/packages/stylecop.analyzers.unstable/1.2.0.556/stylecop.analyzers.unstable.1.2.0.556.nupkg.sha512"
|
||||
],
|
||||
"logs": []
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
17714532046400000
|
||||
@@ -0,0 +1 @@
|
||||
17715044199900000
|
||||
Reference in New Issue
Block a user