repo creation with initial code after cloning public repo

This commit is contained in:
2026-02-19 07:36:25 -05:00
commit 460884fea3
2860 changed files with 650825 additions and 0 deletions
@@ -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";
}
}
@@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v10.0", FrameworkDisplayName = ".NET 10.0")]
@@ -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 =
@@ -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