repo creation with initial code after cloning public repo
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Audio;
|
||||
using Emby.Naming.AudioBook;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Class AudioResolver.
|
||||
/// </summary>
|
||||
public class AudioResolver : ItemResolver<MediaBrowser.Controller.Entities.Audio.Audio>, IMultiItemResolver
|
||||
{
|
||||
private readonly NamingOptions _namingOptions;
|
||||
|
||||
public AudioResolver(NamingOptions namingOptions)
|
||||
{
|
||||
_namingOptions = namingOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public override ResolverPriority Priority => ResolverPriority.Fifth;
|
||||
|
||||
public MultiItemResolverResult ResolveMultiple(
|
||||
Folder parent,
|
||||
List<FileSystemMetadata> files,
|
||||
CollectionType? collectionType,
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
var result = ResolveMultipleInternal(parent, files, collectionType);
|
||||
|
||||
if (result is not null)
|
||||
{
|
||||
foreach (var item in result.Items)
|
||||
{
|
||||
SetInitialItemValues((MediaBrowser.Controller.Entities.Audio.Audio)item, null);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private MultiItemResolverResult ResolveMultipleInternal(
|
||||
Folder parent,
|
||||
List<FileSystemMetadata> files,
|
||||
CollectionType? collectionType)
|
||||
{
|
||||
if (collectionType == CollectionType.books)
|
||||
{
|
||||
return ResolveMultipleAudio(parent, files, true);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified args.
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>Entities.Audio.Audio.</returns>
|
||||
protected override MediaBrowser.Controller.Entities.Audio.Audio Resolve(ItemResolveArgs args)
|
||||
{
|
||||
// Return audio if the path is a file and has a matching extension
|
||||
|
||||
var collectionType = args.GetCollectionType();
|
||||
|
||||
var isBooksCollectionType = collectionType == CollectionType.books;
|
||||
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
if (!isBooksCollectionType)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return FindAudioBook(args, false);
|
||||
}
|
||||
|
||||
if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
|
||||
{
|
||||
var extension = Path.GetExtension(args.Path.AsSpan());
|
||||
|
||||
if (extension.Equals(".cue", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// if audio file exists of same name, return null
|
||||
return null;
|
||||
}
|
||||
|
||||
var isMixedCollectionType = collectionType is null;
|
||||
|
||||
// For conflicting extensions, give priority to videos
|
||||
if (isMixedCollectionType && VideoResolver.IsVideoFile(args.Path, _namingOptions))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
MediaBrowser.Controller.Entities.Audio.Audio item = null;
|
||||
|
||||
var isMusicCollectionType = collectionType == CollectionType.music;
|
||||
|
||||
// Use regular audio type for mixed libraries, owned items and music
|
||||
if (isMixedCollectionType ||
|
||||
args.Parent is null ||
|
||||
isMusicCollectionType)
|
||||
{
|
||||
item = new MediaBrowser.Controller.Entities.Audio.Audio();
|
||||
}
|
||||
else if (isBooksCollectionType)
|
||||
{
|
||||
item = new AudioBook();
|
||||
}
|
||||
|
||||
if (item is not null)
|
||||
{
|
||||
item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
item.IsInMixedFolder = true;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private AudioBook FindAudioBook(ItemResolveArgs args, bool parseName)
|
||||
{
|
||||
// TODO: Allow GetMultiDiscMovie in here
|
||||
var result = ResolveMultipleAudio(args.Parent, args.GetActualFileSystemChildren(), parseName);
|
||||
|
||||
if (result is null || result.Items.Count != 1 || result.Items[0] is not AudioBook item)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we were supporting this we'd be checking filesFromOtherItems
|
||||
item.IsInMixedFolder = false;
|
||||
item.Name = Path.GetFileName(item.ContainingFolderPath);
|
||||
return item;
|
||||
}
|
||||
|
||||
private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName)
|
||||
{
|
||||
var files = new List<FileSystemMetadata>();
|
||||
var leftOver = new List<FileSystemMetadata>();
|
||||
|
||||
// Loop through each child file/folder and see if we find a video
|
||||
foreach (var child in fileSystemEntries)
|
||||
{
|
||||
if (child.IsDirectory)
|
||||
{
|
||||
leftOver.Add(child);
|
||||
}
|
||||
else
|
||||
{
|
||||
files.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
var resolver = new AudioBookListResolver(_namingOptions);
|
||||
var resolverResult = resolver.Resolve(files).ToList();
|
||||
|
||||
var result = new MultiItemResolverResult
|
||||
{
|
||||
ExtraFiles = leftOver,
|
||||
Items = new List<BaseItem>()
|
||||
};
|
||||
|
||||
var isInMixedFolder = resolverResult.Count > 1 || (parent is not null && parent.IsTopParent);
|
||||
|
||||
foreach (var resolvedItem in resolverResult)
|
||||
{
|
||||
if (resolvedItem.Files.Count > 1)
|
||||
{
|
||||
// For now, until we sort out naming for multi-part books
|
||||
continue;
|
||||
}
|
||||
|
||||
// Until multi-part books are handled letting files stack hides them from browsing in the client
|
||||
if (resolvedItem.Files.Count == 0 || resolvedItem.Extras.Count > 0 || resolvedItem.AlternateVersions.Count > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var firstMedia = resolvedItem.Files[0];
|
||||
|
||||
var libraryItem = new AudioBook
|
||||
{
|
||||
Path = firstMedia.Path,
|
||||
IsInMixedFolder = isInMixedFolder,
|
||||
ProductionYear = resolvedItem.Year,
|
||||
Name = parseName ?
|
||||
resolvedItem.Name :
|
||||
Path.GetFileNameWithoutExtension(firstMedia.Path),
|
||||
// AdditionalParts = resolvedItem.Files.Skip(1).Select(i => i.Path).ToArray(),
|
||||
// LocalAlternateVersions = resolvedItem.AlternateVersions.Select(i => i.Path).ToArray()
|
||||
};
|
||||
|
||||
result.Items.Add(libraryItem);
|
||||
}
|
||||
|
||||
result.ExtraFiles.AddRange(files.Where(i => !ContainsFile(resolverResult, i)));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool ContainsFile(IEnumerable<AudioBookInfo> result, FileSystemMetadata file)
|
||||
{
|
||||
return result.Any(i => ContainsFile(i, file));
|
||||
}
|
||||
|
||||
private static bool ContainsFile(AudioBookInfo result, FileSystemMetadata file)
|
||||
{
|
||||
return result.Files.Any(i => ContainsFile(i, file)) ||
|
||||
result.AlternateVersions.Any(i => ContainsFile(i, file)) ||
|
||||
result.Extras.Any(i => ContainsFile(i, file));
|
||||
}
|
||||
|
||||
private static bool ContainsFile(AudioBookFileInfo result, FileSystemMetadata file)
|
||||
{
|
||||
return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Naming.Audio;
|
||||
using Emby.Naming.Common;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// The music album resolver.
|
||||
/// </summary>
|
||||
public class MusicAlbumResolver : ItemResolver<MusicAlbum>
|
||||
{
|
||||
private readonly ILogger<MusicAlbumResolver> _logger;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
{
|
||||
_logger = logger;
|
||||
_namingOptions = namingOptions;
|
||||
_directoryService = directoryService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public override ResolverPriority Priority => ResolverPriority.Third;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified args.
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>MusicAlbum.</returns>
|
||||
protected override MusicAlbum Resolve(ItemResolveArgs args)
|
||||
{
|
||||
var collectionType = args.GetCollectionType();
|
||||
var isMusicMediaFolder = collectionType == CollectionType.music;
|
||||
|
||||
// If there's a collection type and it's not music, don't allow it.
|
||||
if (!isMusicMediaFolder)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!args.IsDirectory)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Avoid mis-identifying top folders
|
||||
if (args.HasParent<MusicAlbum>())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (args.Parent.IsRoot)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return IsMusicAlbum(args) ? new MusicAlbum() : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if the supplied file data points to a music album.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to check.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
/// <returns><c>true</c> if the provided path points to a music album; otherwise, <c>false</c>.</returns>
|
||||
public bool IsMusicAlbum(string path, IDirectoryService directoryService)
|
||||
{
|
||||
return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if the supplied resolve args should be considered a music album.
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns><c>true</c> if [is music album] [the specified args]; otherwise, <c>false</c>.</returns>
|
||||
private bool IsMusicAlbum(ItemResolveArgs args)
|
||||
{
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
// If args is a artist subfolder it's not a music album
|
||||
foreach (var subfolder in _namingOptions.ArtistSubfolders)
|
||||
{
|
||||
if (Path.GetDirectoryName(args.Path.AsSpan()).Equals(subfolder, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("Found release folder: {Path}", args.Path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If args contains music it's a music album
|
||||
if (ContainsMusic(args.FileSystemChildren, true, _directoryService))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if the supplied list contains what we should consider music.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the provided path list contains music; otherwise, <c>false</c>.</returns>
|
||||
private bool ContainsMusic(
|
||||
ICollection<FileSystemMetadata> list,
|
||||
bool allowSubfolders,
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
// Check for audio files before digging down into directories
|
||||
var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && AudioFileParser.IsAudioFile(fileSystemInfo.FullName, _namingOptions));
|
||||
if (foundAudioFile)
|
||||
{
|
||||
// At least one audio file exists
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!allowSubfolders)
|
||||
{
|
||||
// Not music since no audio file exists and we're not looking into subfolders
|
||||
return false;
|
||||
}
|
||||
|
||||
var discSubfolderCount = 0;
|
||||
|
||||
var parser = new AlbumParser(_namingOptions);
|
||||
|
||||
var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory);
|
||||
|
||||
var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
|
||||
{
|
||||
var path = fileSystemInfo.FullName;
|
||||
var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService);
|
||||
|
||||
if (hasMusic)
|
||||
{
|
||||
if (parser.IsMultiPart(path))
|
||||
{
|
||||
_logger.LogDebug("Found multi-disc folder: {Path}", path);
|
||||
Interlocked.Increment(ref discSubfolderCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
|
||||
state.Stop();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!result.IsCompleted)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return discSubfolderCount > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Naming.Audio;
|
||||
using Emby.Naming.Common;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// The music artist resolver.
|
||||
/// </summary>
|
||||
public class MusicArtistResolver : ItemResolver<MusicArtist>
|
||||
{
|
||||
private readonly ILogger<MusicAlbumResolver> _logger;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="MusicAlbumResolver"/> interface.</param>
|
||||
/// <param name="namingOptions">The <see cref="NamingOptions"/>.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public MusicArtistResolver(
|
||||
ILogger<MusicAlbumResolver> logger,
|
||||
NamingOptions namingOptions,
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
_logger = logger;
|
||||
_namingOptions = namingOptions;
|
||||
_directoryService = directoryService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public override ResolverPriority Priority => ResolverPriority.Second;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified resolver arguments.
|
||||
/// </summary>
|
||||
/// <param name="args">The resolver arguments.</param>
|
||||
/// <returns>A <see cref="MusicArtist"/>.</returns>
|
||||
protected override MusicArtist Resolve(ItemResolveArgs args)
|
||||
{
|
||||
if (!args.IsDirectory)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Don't allow nested artists
|
||||
if (args.HasParent<MusicArtist>() || args.HasParent<MusicAlbum>())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var collectionType = args.GetCollectionType();
|
||||
|
||||
var isMusicMediaFolder = collectionType == CollectionType.music;
|
||||
|
||||
// If there's a collection type and it's not music, it can't be a music artist
|
||||
if (!isMusicMediaFolder)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (args.ContainsFileSystemEntryByName("artist.nfo"))
|
||||
{
|
||||
return new MusicArtist();
|
||||
}
|
||||
|
||||
// Avoid mis-identifying top folders
|
||||
if (args.Parent.IsRoot)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService);
|
||||
var albumParser = new AlbumParser(_namingOptions);
|
||||
|
||||
var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
|
||||
|
||||
var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
|
||||
{
|
||||
// If we contain a artist subfolder assume we are an artist folder
|
||||
foreach (var subfolder in _namingOptions.ArtistSubfolders)
|
||||
{
|
||||
if (fileSystemInfo.Name.Equals(subfolder, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Stop once we see an artist subfolder
|
||||
state.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
// If the folder is a multi-disc folder, then it is not an artist folder
|
||||
if (albumParser.IsMultiPart(fileSystemInfo.FullName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If we contain a music album assume we are an artist folder
|
||||
if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, _directoryService))
|
||||
{
|
||||
// Stop once we see a music album
|
||||
state.Stop();
|
||||
}
|
||||
});
|
||||
|
||||
return !result.IsCompleted ? new MusicArtist() : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using DiscUtils.Udf;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves a Path into a Video or Video subclass.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of item to resolve.</typeparam>
|
||||
public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T>
|
||||
where T : Video, new()
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
{
|
||||
_logger = logger;
|
||||
NamingOptions = namingOptions;
|
||||
DirectoryService = directoryService;
|
||||
}
|
||||
|
||||
protected NamingOptions NamingOptions { get; }
|
||||
|
||||
protected IDirectoryService DirectoryService { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified args.
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>`0.</returns>
|
||||
protected override T Resolve(ItemResolveArgs args)
|
||||
{
|
||||
return ResolveVideo<T>(args, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the video.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVideoType">The type of the T video type.</typeparam>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <param name="parseName">if set to <c>true</c> [parse name].</param>
|
||||
/// <returns>``0.</returns>
|
||||
protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
|
||||
where TVideoType : Video, new()
|
||||
{
|
||||
VideoFileInfo videoInfo = null;
|
||||
VideoType? videoType = null;
|
||||
|
||||
// If the path is a file check for a matching extensions
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
// Loop through each child file/folder and see if we find a video
|
||||
foreach (var child in args.FileSystemChildren)
|
||||
{
|
||||
var filename = child.Name;
|
||||
if (child.IsDirectory)
|
||||
{
|
||||
if (IsDvdDirectory(child.FullName, filename, DirectoryService))
|
||||
{
|
||||
var videoTmp = new TVideoType
|
||||
{
|
||||
Path = args.Path,
|
||||
VideoType = VideoType.Dvd
|
||||
};
|
||||
Set3DFormat(videoTmp);
|
||||
return videoTmp;
|
||||
}
|
||||
|
||||
if (IsBluRayDirectory(filename))
|
||||
{
|
||||
var videoTmp = new TVideoType
|
||||
{
|
||||
Path = args.Path,
|
||||
VideoType = VideoType.BluRay
|
||||
};
|
||||
Set3DFormat(videoTmp);
|
||||
return videoTmp;
|
||||
}
|
||||
}
|
||||
else if (IsDvdFile(filename))
|
||||
{
|
||||
videoType = VideoType.Dvd;
|
||||
}
|
||||
|
||||
if (videoType is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
videoInfo = VideoResolver.ResolveDirectory(args.Path, NamingOptions, parseName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
videoInfo = VideoResolver.Resolve(args.Path, false, NamingOptions, parseName);
|
||||
}
|
||||
|
||||
if (videoInfo is null || (!videoInfo.IsStub && !VideoResolver.IsVideoFile(args.Path, NamingOptions)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var video = new TVideoType
|
||||
{
|
||||
Name = videoInfo.Name,
|
||||
Path = args.Path,
|
||||
ProductionYear = videoInfo.Year,
|
||||
ExtraType = videoInfo.ExtraType
|
||||
};
|
||||
|
||||
if (videoType.HasValue)
|
||||
{
|
||||
video.VideoType = videoType.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetVideoType(video, videoInfo);
|
||||
}
|
||||
|
||||
Set3DFormat(video, videoInfo);
|
||||
|
||||
return video;
|
||||
}
|
||||
|
||||
protected void SetVideoType(Video video, VideoFileInfo videoInfo)
|
||||
{
|
||||
var extension = Path.GetExtension(video.Path.AsSpan());
|
||||
video.VideoType = extension.Equals(".iso", StringComparison.OrdinalIgnoreCase)
|
||||
|| extension.Equals(".img", StringComparison.OrdinalIgnoreCase)
|
||||
? VideoType.Iso
|
||||
: VideoType.VideoFile;
|
||||
|
||||
video.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
|
||||
video.IsPlaceHolder = videoInfo.IsStub;
|
||||
|
||||
if (videoInfo.IsStub)
|
||||
{
|
||||
if (string.Equals(videoInfo.StubType, "dvd", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.VideoType = VideoType.Dvd;
|
||||
}
|
||||
else if (string.Equals(videoInfo.StubType, "bluray", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.VideoType = VideoType.BluRay;
|
||||
}
|
||||
}
|
||||
|
||||
SetIsoType(video);
|
||||
}
|
||||
|
||||
protected void SetIsoType(Video video)
|
||||
{
|
||||
if (video.VideoType == VideoType.Iso)
|
||||
{
|
||||
if (video.Path.Contains("dvd", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.IsoType = IsoType.Dvd;
|
||||
}
|
||||
else if (video.Path.Contains("bluray", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.IsoType = IsoType.BluRay;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
// use disc-utils, both DVDs and BDs use UDF filesystem
|
||||
using var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
using UdfReader udfReader = new UdfReader(videoFileStream);
|
||||
if (udfReader.DirectoryExists("VIDEO_TS"))
|
||||
{
|
||||
video.IsoType = IsoType.Dvd;
|
||||
}
|
||||
else if (udfReader.DirectoryExists("BDMV"))
|
||||
{
|
||||
video.IsoType = IsoType.BluRay;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error opening UDF/ISO image: {Value}", video.Path ?? video.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void Set3DFormat(Video video, bool is3D, string format3D)
|
||||
{
|
||||
if (is3D)
|
||||
{
|
||||
if (string.Equals(format3D, "fsbs", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.Video3DFormat = Video3DFormat.FullSideBySide;
|
||||
}
|
||||
else if (string.Equals(format3D, "ftab", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.Video3DFormat = Video3DFormat.FullTopAndBottom;
|
||||
}
|
||||
else if (string.Equals(format3D, "hsbs", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.Video3DFormat = Video3DFormat.HalfSideBySide;
|
||||
}
|
||||
else if (string.Equals(format3D, "htab", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.Video3DFormat = Video3DFormat.HalfTopAndBottom;
|
||||
}
|
||||
else if (string.Equals(format3D, "sbs", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.Video3DFormat = Video3DFormat.HalfSideBySide;
|
||||
}
|
||||
else if (string.Equals(format3D, "sbs3d", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.Video3DFormat = Video3DFormat.HalfSideBySide;
|
||||
}
|
||||
else if (string.Equals(format3D, "tab", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.Video3DFormat = Video3DFormat.HalfTopAndBottom;
|
||||
}
|
||||
else if (string.Equals(format3D, "mvc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.Video3DFormat = Video3DFormat.MVC;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void Set3DFormat(Video video, VideoFileInfo videoInfo)
|
||||
{
|
||||
Set3DFormat(video, videoInfo.Is3D, videoInfo.Format3D);
|
||||
}
|
||||
|
||||
protected void Set3DFormat(Video video)
|
||||
{
|
||||
var result = Format3DParser.Parse(video.Path, NamingOptions);
|
||||
|
||||
Set3DFormat(video, result.Is3D, result.Format3D);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether [is DVD directory] [the specified directory name].
|
||||
/// </summary>
|
||||
/// <param name="fullPath">The full path of the directory.</param>
|
||||
/// <param name="directoryName">The name of the directory.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
/// <returns><c>true</c> if the provided directory is a DVD directory, <c>false</c> otherwise.</returns>
|
||||
protected bool IsDvdDirectory(string fullPath, string directoryName, IDirectoryService directoryService)
|
||||
{
|
||||
if (!string.Equals(directoryName, "video_ts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return directoryService.GetFilePaths(fullPath).Any(i => Path.GetExtension(i.AsSpan()).Equals(".vob", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether [is DVD file] [the specified name].
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <returns><c>true</c> if [is DVD file] [the specified name]; otherwise, <c>false</c>.</returns>
|
||||
protected bool IsDvdFile(string name)
|
||||
{
|
||||
return string.Equals(name, "video_ts.ifo", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether [is bluray directory] [the specified directory name].
|
||||
/// </summary>
|
||||
/// <param name="directoryName">The directory name.</param>
|
||||
/// <returns>Whether the directory is a bluray directory.</returns>
|
||||
protected bool IsBluRayDirectory(string directoryName)
|
||||
{
|
||||
return string.Equals(directoryName, "bdmv", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Book;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||
{
|
||||
public class BookResolver : ItemResolver<Book>
|
||||
{
|
||||
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
|
||||
|
||||
protected override Book Resolve(ItemResolveArgs args)
|
||||
{
|
||||
var collectionType = args.GetCollectionType();
|
||||
|
||||
// Only process items that are in a collection folder containing books
|
||||
if (collectionType != CollectionType.books)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
return GetBook(args);
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(args.Path.AsSpan());
|
||||
|
||||
if (!_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = BookFileNameParser.Parse(Path.GetFileNameWithoutExtension(args.Path));
|
||||
|
||||
return new Book
|
||||
{
|
||||
Path = args.Path,
|
||||
Name = result.Name ?? string.Empty,
|
||||
IndexNumber = result.Index,
|
||||
ProductionYear = result.Year,
|
||||
SeriesName = result.SeriesName ?? Path.GetFileName(Path.GetDirectoryName(args.Path)),
|
||||
IsInMixedFolder = true,
|
||||
};
|
||||
}
|
||||
|
||||
private Book GetBook(ItemResolveArgs args)
|
||||
{
|
||||
var bookFiles = args.FileSystemChildren.Where(f =>
|
||||
{
|
||||
var fileExtension = Path.GetExtension(f.FullName.AsSpan());
|
||||
|
||||
return _validExtensions.Contains(
|
||||
fileExtension,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}).ToList();
|
||||
|
||||
// directory is only considered a book when it contains exactly one supported file
|
||||
// other library structures with multiple books to a directory will get picked up as individual files
|
||||
if (bookFiles.Count != 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = BookFileNameParser.Parse(Path.GetFileName(args.Path));
|
||||
|
||||
return new Book
|
||||
{
|
||||
Path = bookFiles[0].FullName,
|
||||
Name = result.Name ?? string.Empty,
|
||||
IndexNumber = result.Index,
|
||||
ProductionYear = result.Year,
|
||||
SeriesName = result.SeriesName ?? string.Empty,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Emby.Naming.Video.ExtraRuleResolver;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves a Path into a Video or Video subclass.
|
||||
/// </summary>
|
||||
internal class ExtraResolver : BaseVideoResolver<Video>
|
||||
{
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IItemResolver[] _trailerResolvers;
|
||||
private readonly IItemResolver[] _videoResolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExtraResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
: base(logger, namingOptions, directoryService)
|
||||
{
|
||||
_namingOptions = namingOptions;
|
||||
_trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService) };
|
||||
_videoResolvers = new IItemResolver[] { this };
|
||||
}
|
||||
|
||||
protected override Video Resolve(ItemResolveArgs args)
|
||||
{
|
||||
return ResolveVideo<Video>(args, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the resolvers for the extra type.
|
||||
/// </summary>
|
||||
/// <param name="extraType">The extra type.</param>
|
||||
/// <returns>The resolvers for the extra type.</returns>
|
||||
public IItemResolver[]? GetResolversForExtraType(ExtraType extraType) => extraType switch
|
||||
{
|
||||
ExtraType.Trailer => _trailerResolvers,
|
||||
// For audio we'll have to rely on the AudioResolver, which is a "built-in"
|
||||
ExtraType.ThemeSong => null,
|
||||
_ => _videoResolvers
|
||||
};
|
||||
|
||||
public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, string? libraryRoot = "")
|
||||
{
|
||||
var extraResult = GetExtraInfo(path, _namingOptions, libraryRoot);
|
||||
if (extraResult.ExtraType is null)
|
||||
{
|
||||
extraType = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var cleanDateTimeResult = CleanDateTimeParser.Clean(Path.GetFileNameWithoutExtension(path), _namingOptions.CleanDateTimeRegexes);
|
||||
var name = cleanDateTimeResult.Name;
|
||||
var year = cleanDateTimeResult.Year;
|
||||
|
||||
var parentDir = ownerVideoFileInfo.IsDirectory ? ownerVideoFileInfo.Path : Path.GetDirectoryName(ownerVideoFileInfo.Path.AsSpan());
|
||||
|
||||
var trimmedFileNameWithoutExtension = TrimFilenameDelimiters(ownerVideoFileInfo.FileNameWithoutExtension, _namingOptions.VideoFlagDelimiters);
|
||||
var trimmedVideoInfoName = TrimFilenameDelimiters(ownerVideoFileInfo.Name, _namingOptions.VideoFlagDelimiters);
|
||||
var trimmedExtraFileName = TrimFilenameDelimiters(name, _namingOptions.VideoFlagDelimiters);
|
||||
|
||||
// first check filenames
|
||||
bool isValid = StartsWith(trimmedExtraFileName, trimmedFileNameWithoutExtension)
|
||||
|| (StartsWith(trimmedExtraFileName, trimmedVideoInfoName) && year == ownerVideoFileInfo.Year);
|
||||
|
||||
if (!isValid)
|
||||
{
|
||||
// When the extra rule type is DirectoryName we must go one level higher to get the "real" dir name
|
||||
var currentParentDir = extraResult.Rule?.RuleType == ExtraRuleType.DirectoryName
|
||||
? Path.GetDirectoryName(Path.GetDirectoryName(path.AsSpan()))
|
||||
: Path.GetDirectoryName(path.AsSpan());
|
||||
|
||||
isValid = !currentParentDir.IsEmpty && !parentDir.IsEmpty && currentParentDir.Equals(parentDir, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
extraType = extraResult.ExtraType;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
|
||||
{
|
||||
return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
|
||||
}
|
||||
|
||||
private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName)
|
||||
{
|
||||
return !baseName.IsEmpty && fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#nullable disable
|
||||
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
/// <summary>
|
||||
/// Class FolderResolver.
|
||||
/// </summary>
|
||||
public class FolderResolver : GenericFolderResolver<Folder>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public override ResolverPriority Priority => ResolverPriority.Last;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified args.
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>Folder.</returns>
|
||||
protected override Folder Resolve(ItemResolveArgs args)
|
||||
{
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
return new Folder();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#nullable disable
|
||||
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
/// <summary>
|
||||
/// Class FolderResolver.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItemType">The type of the T item type.</typeparam>
|
||||
public abstract class GenericFolderResolver<TItemType> : ItemResolver<TItemType>
|
||||
where TItemType : Folder, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets the initial item values.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="args">The args.</param>
|
||||
protected override void SetInitialItemValues(TItemType item, ItemResolveArgs args)
|
||||
{
|
||||
base.SetInitialItemValues(item, args);
|
||||
|
||||
item.IsRoot = args.Parent is null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#nullable disable
|
||||
|
||||
using Emby.Naming.Common;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves a Path into an instance of the <see cref="Video"/> class.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of item to resolve.</typeparam>
|
||||
public class GenericVideoResolver<T> : BaseVideoResolver<T>
|
||||
where T : Video, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GenericVideoResolver{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
: base(logger, namingOptions, directoryService)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
{
|
||||
/// <summary>
|
||||
/// Class BoxSetResolver.
|
||||
/// </summary>
|
||||
public class BoxSetResolver : GenericFolderResolver<BoxSet>
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolves the specified args.
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>BoxSet.</returns>
|
||||
protected override BoxSet Resolve(ItemResolveArgs args)
|
||||
{
|
||||
// It's a boxset if all of the following conditions are met:
|
||||
// Is a Directory
|
||||
// Contains [boxset] in the path
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
var filename = Path.GetFileName(args.Path);
|
||||
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (filename.Contains("[boxset]", StringComparison.OrdinalIgnoreCase) || args.ContainsFileSystemEntryByName("collection.xml"))
|
||||
{
|
||||
return new BoxSet
|
||||
{
|
||||
Path = args.Path,
|
||||
Name = Path.GetFileName(args.Path).Replace("[boxset]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the initial item values.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="args">The args.</param>
|
||||
protected override void SetInitialItemValues(BoxSet item, ItemResolveArgs args)
|
||||
{
|
||||
base.SetInitialItemValues(item, args);
|
||||
|
||||
SetProviderIdFromPath(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the provider id from path.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
private static void SetProviderIdFromPath(BaseItem item)
|
||||
{
|
||||
// we need to only look at the name of this actual item (not parents)
|
||||
var justName = Path.GetFileName(item.Path.AsSpan());
|
||||
|
||||
var id = justName.GetAttributeValue("tmdbid");
|
||||
item.TrySetProviderId(MetadataProvider.Tmdb, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,586 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MovieResolver.
|
||||
/// </summary>
|
||||
public partial class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
|
||||
{
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
|
||||
private static readonly CollectionType[] _validCollectionTypes = new[]
|
||||
{
|
||||
CollectionType.movies,
|
||||
CollectionType.homevideos,
|
||||
CollectionType.musicvideos,
|
||||
CollectionType.tvshows,
|
||||
CollectionType.photos
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MovieResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
: base(logger, namingOptions, directoryService)
|
||||
{
|
||||
_imageProcessor = imageProcessor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public override ResolverPriority Priority => ResolverPriority.Fourth;
|
||||
|
||||
[GeneratedRegex(@"\bsample\b", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex IsIgnoredRegex();
|
||||
|
||||
/// <inheritdoc />
|
||||
public MultiItemResolverResult ResolveMultiple(
|
||||
Folder parent,
|
||||
List<FileSystemMetadata> files,
|
||||
CollectionType? collectionType,
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
var result = ResolveMultipleInternal(parent, files, collectionType);
|
||||
|
||||
if (result is not null)
|
||||
{
|
||||
foreach (var item in result.Items)
|
||||
{
|
||||
SetInitialItemValues((Video)item, null);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified args.
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>Video.</returns>
|
||||
protected override Video Resolve(ItemResolveArgs args)
|
||||
{
|
||||
var collectionType = args.GetCollectionType();
|
||||
|
||||
// Find movies with their own folders
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
if (IsInvalid(args.Parent, collectionType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Video movie = null;
|
||||
var files = args.GetActualFileSystemChildren().ToList();
|
||||
|
||||
if (collectionType == CollectionType.musicvideos)
|
||||
{
|
||||
movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
|
||||
}
|
||||
|
||||
if (collectionType == CollectionType.homevideos)
|
||||
{
|
||||
movie = FindMovie<Video>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
|
||||
}
|
||||
|
||||
if (collectionType is null)
|
||||
{
|
||||
// Owned items will be caught by the video extra resolver
|
||||
if (args.Parent is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (args.HasParent<Series>())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
|
||||
}
|
||||
|
||||
if (collectionType == CollectionType.movies)
|
||||
{
|
||||
movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
|
||||
}
|
||||
|
||||
// ignore extras
|
||||
return movie?.ExtraType is null ? movie : null;
|
||||
}
|
||||
|
||||
if (args.Parent is null)
|
||||
{
|
||||
return base.Resolve(args);
|
||||
}
|
||||
|
||||
if (IsInvalid(args.Parent, collectionType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Video item = null;
|
||||
|
||||
if (collectionType == CollectionType.musicvideos)
|
||||
{
|
||||
item = ResolveVideo<MusicVideo>(args, false);
|
||||
}
|
||||
|
||||
// To find a movie file, the collection type must be movies or boxsets
|
||||
else if (collectionType == CollectionType.movies)
|
||||
{
|
||||
item = ResolveVideo<Movie>(args, true);
|
||||
}
|
||||
else if (collectionType == CollectionType.homevideos || collectionType == CollectionType.photos)
|
||||
{
|
||||
item = ResolveVideo<Video>(args, false);
|
||||
}
|
||||
else if (collectionType is null)
|
||||
{
|
||||
if (args.HasParent<Series>())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
item = ResolveVideo<Video>(args, false);
|
||||
}
|
||||
|
||||
// Ignore extras
|
||||
if (item?.ExtraType is not null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (item is not null)
|
||||
{
|
||||
item.IsInMixedFolder = true;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
private MultiItemResolverResult ResolveMultipleInternal(
|
||||
Folder parent,
|
||||
List<FileSystemMetadata> files,
|
||||
CollectionType? collectionType)
|
||||
{
|
||||
if (IsInvalid(parent, collectionType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (collectionType is CollectionType.musicvideos)
|
||||
{
|
||||
return ResolveVideos<MusicVideo>(parent, files, true, collectionType, false);
|
||||
}
|
||||
|
||||
if (collectionType == CollectionType.homevideos || collectionType == CollectionType.photos)
|
||||
{
|
||||
return ResolveVideos<Video>(parent, files, false, collectionType, false);
|
||||
}
|
||||
|
||||
if (collectionType is null)
|
||||
{
|
||||
// Owned items should just use the plain video type
|
||||
if (parent is null)
|
||||
{
|
||||
return ResolveVideos<Video>(parent, files, false, collectionType, false);
|
||||
}
|
||||
|
||||
if (parent is Series || parent.GetParents().OfType<Series>().Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ResolveVideos<Movie>(parent, files, false, collectionType, true);
|
||||
}
|
||||
|
||||
if (collectionType == CollectionType.movies)
|
||||
{
|
||||
return ResolveVideos<Movie>(parent, files, true, collectionType, true);
|
||||
}
|
||||
|
||||
if (collectionType == CollectionType.tvshows)
|
||||
{
|
||||
return ResolveVideos<Episode>(parent, files, false, collectionType, true);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private MultiItemResolverResult ResolveVideos<T>(
|
||||
Folder parent,
|
||||
IEnumerable<FileSystemMetadata> fileSystemEntries,
|
||||
bool supportMultiEditions,
|
||||
CollectionType? collectionType,
|
||||
bool parseName)
|
||||
where T : Video, new()
|
||||
{
|
||||
var files = new List<FileSystemMetadata>();
|
||||
var leftOver = new List<FileSystemMetadata>();
|
||||
var hasCollectionType = collectionType is not null;
|
||||
|
||||
// Loop through each child file/folder and see if we find a video
|
||||
foreach (var child in fileSystemEntries)
|
||||
{
|
||||
// This is a hack but currently no better way to resolve a sometimes ambiguous situation
|
||||
if (!hasCollectionType)
|
||||
{
|
||||
if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(child.Name, "season.nfo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (child.IsDirectory)
|
||||
{
|
||||
leftOver.Add(child);
|
||||
}
|
||||
else if (!IsIgnoredRegex().IsMatch(child.Name))
|
||||
{
|
||||
files.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
var videoInfos = files
|
||||
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName, parent.ContainingFolderPath))
|
||||
.Where(f => f is not null)
|
||||
.ToList();
|
||||
|
||||
var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath);
|
||||
|
||||
var result = new MultiItemResolverResult
|
||||
{
|
||||
ExtraFiles = leftOver
|
||||
};
|
||||
|
||||
var isInMixedFolder = resolverResult.Count > 1 || parent?.IsTopParent == true;
|
||||
|
||||
foreach (var video in resolverResult)
|
||||
{
|
||||
var firstVideo = video.Files[0];
|
||||
var path = firstVideo.Path;
|
||||
if (video.ExtraType is not null)
|
||||
{
|
||||
result.ExtraFiles.Add(files.Find(f => string.Equals(f.FullName, path, StringComparison.OrdinalIgnoreCase)));
|
||||
continue;
|
||||
}
|
||||
|
||||
var additionalParts = video.Files.Count > 1 ? video.Files.Skip(1).Select(i => i.Path).ToArray() : Array.Empty<string>();
|
||||
|
||||
var videoItem = new T
|
||||
{
|
||||
Path = path,
|
||||
IsInMixedFolder = isInMixedFolder,
|
||||
ProductionYear = video.Year,
|
||||
Name = parseName ? video.Name : firstVideo.Name,
|
||||
AdditionalParts = additionalParts,
|
||||
LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToArray()
|
||||
};
|
||||
|
||||
SetVideoType(videoItem, firstVideo);
|
||||
Set3DFormat(videoItem, firstVideo);
|
||||
|
||||
result.Items.Add(videoItem);
|
||||
}
|
||||
|
||||
result.ExtraFiles.AddRange(files.Where(i => !ContainsFile(resolverResult, i)));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
|
||||
{
|
||||
for (var i = 0; i < result.Count; i++)
|
||||
{
|
||||
var current = result[i];
|
||||
for (var j = 0; j < current.Files.Count; j++)
|
||||
{
|
||||
if (ContainsFile(current.Files[j], file))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (var j = 0; j < current.AlternateVersions.Count; j++)
|
||||
{
|
||||
if (ContainsFile(current.AlternateVersions[j], file))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ContainsFile(VideoFileInfo result, FileSystemMetadata file)
|
||||
{
|
||||
return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the initial item values.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="args">The args.</param>
|
||||
protected override void SetInitialItemValues(Video item, ItemResolveArgs args)
|
||||
{
|
||||
base.SetInitialItemValues(item, args);
|
||||
|
||||
SetProviderIdsFromPath(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the provider id from path.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
private static void SetProviderIdsFromPath(Video item)
|
||||
{
|
||||
if (item is Movie || item is MusicVideo)
|
||||
{
|
||||
// We need to only look at the name of this actual item (not parents)
|
||||
var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan());
|
||||
|
||||
var tmdbid = justName.GetAttributeValue("tmdbid");
|
||||
|
||||
// If not in a mixed folder and ID not found in folder path, check filename
|
||||
if (string.IsNullOrEmpty(tmdbid) && !item.IsInMixedFolder)
|
||||
{
|
||||
tmdbid = Path.GetFileName(item.Path.AsSpan()).GetAttributeValue("tmdbid");
|
||||
}
|
||||
|
||||
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid);
|
||||
|
||||
if (!string.IsNullOrEmpty(item.Path))
|
||||
{
|
||||
// Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name)
|
||||
var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid");
|
||||
item.TrySetProviderId(MetadataProvider.Imdb, imdbid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a movie based on a child file system entries.
|
||||
/// </summary>
|
||||
/// <returns>Movie.</returns>
|
||||
private T FindMovie<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, CollectionType? collectionType, bool parseName)
|
||||
where T : Video, new()
|
||||
{
|
||||
var multiDiscFolders = new List<FileSystemMetadata>();
|
||||
|
||||
var libraryOptions = args.LibraryOptions;
|
||||
var supportPhotos = collectionType == CollectionType.homevideos && libraryOptions.EnablePhotos;
|
||||
var photos = new List<FileSystemMetadata>();
|
||||
|
||||
// Search for a folder rip
|
||||
foreach (var child in fileSystemEntries)
|
||||
{
|
||||
var filename = child.Name;
|
||||
|
||||
if (child.IsDirectory)
|
||||
{
|
||||
if (NamingOptions.AllExtrasTypesFolderNames.ContainsKey(filename))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsDvdDirectory(child.FullName, filename, directoryService))
|
||||
{
|
||||
var movie = new T
|
||||
{
|
||||
Path = path,
|
||||
VideoType = VideoType.Dvd
|
||||
};
|
||||
Set3DFormat(movie);
|
||||
return movie;
|
||||
}
|
||||
|
||||
if (IsBluRayDirectory(filename))
|
||||
{
|
||||
var movie = new T
|
||||
{
|
||||
Path = path,
|
||||
VideoType = VideoType.BluRay
|
||||
};
|
||||
Set3DFormat(movie);
|
||||
return movie;
|
||||
}
|
||||
|
||||
multiDiscFolders.Add(child);
|
||||
}
|
||||
else if (IsDvdFile(filename))
|
||||
{
|
||||
var movie = new T
|
||||
{
|
||||
Path = path,
|
||||
VideoType = VideoType.Dvd
|
||||
};
|
||||
Set3DFormat(movie);
|
||||
return movie;
|
||||
}
|
||||
else if (supportPhotos && PhotoResolver.IsImageFile(child.FullName, _imageProcessor))
|
||||
{
|
||||
photos.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Allow GetMultiDiscMovie in here
|
||||
const bool SupportsMultiVersion = true;
|
||||
|
||||
var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ??
|
||||
new MultiItemResolverResult();
|
||||
|
||||
var isPhotosCollection = collectionType == CollectionType.homevideos || collectionType == CollectionType.photos;
|
||||
if (!isPhotosCollection && result.Items.Count == 1)
|
||||
{
|
||||
var videoPath = result.Items[0].Path;
|
||||
var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));
|
||||
var hasOtherSubfolders = multiDiscFolders.Count > 0;
|
||||
|
||||
if (!hasPhotos && !hasOtherSubfolders)
|
||||
{
|
||||
var movie = (T)result.Items[0];
|
||||
movie.IsInMixedFolder = false;
|
||||
if (collectionType == CollectionType.movies || collectionType is null)
|
||||
{
|
||||
movie.Name = Path.GetFileName(movie.ContainingFolderPath);
|
||||
}
|
||||
|
||||
return movie;
|
||||
}
|
||||
}
|
||||
else if (result.Items.Count == 0 && multiDiscFolders.Count > 0)
|
||||
{
|
||||
return GetMultiDiscMovie<T>(multiDiscFolders, directoryService);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the multi disc movie.
|
||||
/// </summary>
|
||||
/// <param name="multiDiscFolders">The folders.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
/// <returns>``0.</returns>
|
||||
private T GetMultiDiscMovie<T>(List<FileSystemMetadata> multiDiscFolders, IDirectoryService directoryService)
|
||||
where T : Video, new()
|
||||
{
|
||||
var videoTypes = new List<VideoType>();
|
||||
|
||||
var folderPaths = multiDiscFolders.Select(i => i.FullName).Where(i =>
|
||||
{
|
||||
var subFileEntries = directoryService.GetFileSystemEntries(i);
|
||||
|
||||
var subfolders = subFileEntries
|
||||
.Where(e => e.IsDirectory)
|
||||
.ToList();
|
||||
|
||||
if (subfolders.Any(s => IsDvdDirectory(s.FullName, s.Name, directoryService)))
|
||||
{
|
||||
videoTypes.Add(VideoType.Dvd);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (subfolders.Any(s => IsBluRayDirectory(s.Name)))
|
||||
{
|
||||
videoTypes.Add(VideoType.BluRay);
|
||||
return true;
|
||||
}
|
||||
|
||||
var subFiles = subFileEntries
|
||||
.Where(e => !e.IsDirectory)
|
||||
.Select(d => d.Name);
|
||||
|
||||
if (subFiles.Any(IsDvdFile))
|
||||
{
|
||||
videoTypes.Add(VideoType.Dvd);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}).Order().ToList();
|
||||
|
||||
// If different video types were found, don't allow this
|
||||
if (videoTypes.Distinct().Count() > 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (folderPaths.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = StackResolver.ResolveDirectories(folderPaths, NamingOptions).ToList();
|
||||
|
||||
if (result.Count != 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int additionalPartsLen = folderPaths.Count - 1;
|
||||
var additionalParts = new string[additionalPartsLen];
|
||||
folderPaths.CopyTo(1, additionalParts, 0, additionalPartsLen);
|
||||
|
||||
var returnVideo = new T
|
||||
{
|
||||
Path = folderPaths[0],
|
||||
AdditionalParts = additionalParts,
|
||||
VideoType = videoTypes[0],
|
||||
Name = result[0].Name
|
||||
};
|
||||
|
||||
SetIsoType(returnVideo);
|
||||
|
||||
return returnVideo;
|
||||
}
|
||||
|
||||
private bool IsInvalid(Folder parent, CollectionType? collectionType)
|
||||
{
|
||||
if (parent is not null)
|
||||
{
|
||||
if (parent.IsRoot)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (collectionType is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !_validCollectionTypes.Contains(collectionType.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using Emby.Naming.Common;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
/// <summary>
|
||||
/// Class PhotoAlbumResolver.
|
||||
/// </summary>
|
||||
public class PhotoAlbumResolver : GenericFolderResolver<PhotoAlbum>
|
||||
{
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PhotoAlbumResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public PhotoAlbumResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
|
||||
{
|
||||
_imageProcessor = imageProcessor;
|
||||
_namingOptions = namingOptions;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override ResolverPriority Priority => ResolverPriority.Second;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified args.
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>Trailer.</returns>
|
||||
protected override PhotoAlbum Resolve(ItemResolveArgs args)
|
||||
{
|
||||
// Must be an image file within a photo collection
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
// Must be an image file within a photo collection
|
||||
var collectionType = args.GetCollectionType();
|
||||
|
||||
if (collectionType == CollectionType.photos
|
||||
|| (collectionType == CollectionType.homevideos && args.LibraryOptions.EnablePhotos))
|
||||
{
|
||||
if (HasPhotos(args))
|
||||
{
|
||||
return new PhotoAlbum
|
||||
{
|
||||
Path = args.Path
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool HasPhotos(ItemResolveArgs args)
|
||||
{
|
||||
var files = args.FileSystemChildren;
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (!file.IsDirectory && PhotoResolver.IsImageFile(file.FullName, _imageProcessor))
|
||||
{
|
||||
var filename = file.Name;
|
||||
var ownedByMedia = false;
|
||||
|
||||
foreach (var siblingFile in files)
|
||||
{
|
||||
if (PhotoResolver.IsOwnedByMedia(_namingOptions, siblingFile.FullName, filename))
|
||||
{
|
||||
ownedByMedia = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ownedByMedia)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Video;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
/// <summary>
|
||||
/// Class PhotoResolver.
|
||||
/// </summary>
|
||||
public class PhotoResolver : ItemResolver<Photo>
|
||||
{
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
private static readonly string[] _ignoreFiles = new[]
|
||||
{
|
||||
"folder",
|
||||
"thumb",
|
||||
"landscape",
|
||||
"fanart",
|
||||
"backdrop",
|
||||
"poster",
|
||||
"cover",
|
||||
"logo",
|
||||
"default"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PhotoResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
{
|
||||
_imageProcessor = imageProcessor;
|
||||
_namingOptions = namingOptions;
|
||||
_directoryService = directoryService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified args.
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>Trailer.</returns>
|
||||
protected override Photo? Resolve(ItemResolveArgs args)
|
||||
{
|
||||
if (!args.IsDirectory)
|
||||
{
|
||||
// Must be an image file within a photo collection
|
||||
var collectionType = args.CollectionType;
|
||||
|
||||
if (collectionType == CollectionType.photos
|
||||
|| (collectionType == CollectionType.homevideos && args.LibraryOptions.EnablePhotos))
|
||||
{
|
||||
if (IsImageFile(args.Path, _imageProcessor))
|
||||
{
|
||||
var filename = Path.GetFileNameWithoutExtension(args.Path.AsSpan());
|
||||
|
||||
// Make sure the image doesn't belong to a video file
|
||||
var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)
|
||||
?? throw new InvalidOperationException("Path can't be a root directory."));
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (IsOwnedByMedia(_namingOptions, file.FullName, filename))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return new Photo
|
||||
{
|
||||
Path = args.Path
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, ReadOnlySpan<char> imageFilename)
|
||||
{
|
||||
return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
|
||||
}
|
||||
|
||||
internal static bool IsOwnedByResolvedMedia(ReadOnlySpan<char> file, ReadOnlySpan<char> imageFilename)
|
||||
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(path);
|
||||
|
||||
var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
|
||||
if (!imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var filename = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
if (_ignoreFiles.Any(i => filename.StartsWith(i, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.LocalMetadata.Savers;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IItemResolver"/> for <see cref="Playlist"/> library items.
|
||||
/// </summary>
|
||||
public class PlaylistResolver : GenericFolderResolver<Playlist>
|
||||
{
|
||||
private readonly CollectionType?[] _musicPlaylistCollectionTypes =
|
||||
[
|
||||
null,
|
||||
CollectionType.music
|
||||
];
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Playlist Resolve(ItemResolveArgs args)
|
||||
{
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
// It's a playlist if the path is a directory with [playlist] in its name
|
||||
var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (filename.Contains("[playlist]", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Playlist
|
||||
{
|
||||
Path = args.Path,
|
||||
Name = filename.Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim(),
|
||||
OpenAccess = true
|
||||
};
|
||||
}
|
||||
|
||||
// It's a directory-based playlist if the directory contains a playlist file
|
||||
var filePaths = Directory.EnumerateFiles(args.Path, "*", new EnumerationOptions { IgnoreInaccessible = true });
|
||||
if (filePaths.Any(f => f.EndsWith(PlaylistXmlSaver.DefaultPlaylistFilename, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return new Playlist
|
||||
{
|
||||
Path = args.Path,
|
||||
Name = filename,
|
||||
OpenAccess = true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is a music playlist file
|
||||
// It should have the correct collection type and a supported file extension
|
||||
else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType))
|
||||
{
|
||||
var extension = Path.GetExtension(args.Path.AsSpan());
|
||||
if (Playlist.SupportedExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Playlist
|
||||
{
|
||||
Path = args.Path,
|
||||
Name = Path.GetFileNameWithoutExtension(args.Path),
|
||||
IsInMixedFolder = true,
|
||||
PlaylistMediaType = MediaType.Audio,
|
||||
OpenAccess = true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
public class SpecialFolderResolver : GenericFolderResolver<Folder>
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IServerApplicationPaths _appPaths;
|
||||
|
||||
public SpecialFolderResolver(IFileSystem fileSystem, IServerApplicationPaths appPaths)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_appPaths = appPaths;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public override ResolverPriority Priority => ResolverPriority.First;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified args.
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>Folder.</returns>
|
||||
protected override Folder Resolve(ItemResolveArgs args)
|
||||
{
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
if (args.IsPhysicalRoot)
|
||||
{
|
||||
return new AggregateFolder();
|
||||
}
|
||||
|
||||
if (string.Equals(args.Path, _appPaths.DefaultUserViewsPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new UserRootFolder(); // if we got here and still a root - must be user root
|
||||
}
|
||||
|
||||
if (args.IsVf)
|
||||
{
|
||||
return new CollectionFolder
|
||||
{
|
||||
CollectionType = GetCollectionType(args),
|
||||
PhysicalLocationsList = args.PhysicalLocations
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private CollectionType? GetCollectionType(ItemResolveArgs args)
|
||||
{
|
||||
return args.FileSystemChildren
|
||||
.Where(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return !i.IsDirectory &&
|
||||
string.Equals(".collection", i.Extension, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.Select(i => _fileSystem.GetFileNameWithoutExtension(i))
|
||||
.Select(i => Enum.TryParse<CollectionType>(i, out var collectionType) ? collectionType : (CollectionType?)null)
|
||||
.FirstOrDefault(i => i is not null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Class EpisodeResolver.
|
||||
/// </summary>
|
||||
public class EpisodeResolver : BaseVideoResolver<Episode>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
|
||||
: base(logger, namingOptions, directoryService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified args.
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>Episode.</returns>
|
||||
protected override Episode Resolve(ItemResolveArgs args)
|
||||
{
|
||||
var parent = args.Parent;
|
||||
|
||||
if (parent is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Just in case the user decided to nest episodes.
|
||||
// Not officially supported but in some cases we can handle it.
|
||||
|
||||
var season = parent as Season ?? parent.GetParents().OfType<Season>().FirstOrDefault();
|
||||
|
||||
// If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something
|
||||
// Also handle flat tv folders
|
||||
if (season is not null
|
||||
|| args.GetCollectionType() == CollectionType.tvshows
|
||||
|| args.HasParent<Series>())
|
||||
{
|
||||
var episode = ResolveVideo<Episode>(args, false);
|
||||
|
||||
// Ignore extras
|
||||
if (episode is null || episode.ExtraType is not null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault();
|
||||
|
||||
if (series is not null)
|
||||
{
|
||||
episode.SeriesId = series.Id;
|
||||
episode.SeriesName = series.Name;
|
||||
}
|
||||
|
||||
if (season is not null)
|
||||
{
|
||||
episode.SeasonId = season.Id;
|
||||
episode.SeasonName = season.Name;
|
||||
}
|
||||
|
||||
// Assume season 1 if there's no season folder and a season number could not be determined
|
||||
if (season is null && !episode.ParentIndexNumber.HasValue && (episode.IndexNumber.HasValue || episode.PremiereDate.HasValue))
|
||||
{
|
||||
episode.ParentIndexNumber = 1;
|
||||
}
|
||||
|
||||
return episode;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Globalization;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.TV;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Class SeasonResolver.
|
||||
/// </summary>
|
||||
public class SeasonResolver : GenericFolderResolver<Season>
|
||||
{
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly ILogger<SeasonResolver> _logger;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SeasonResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="localization">The localization.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public SeasonResolver(
|
||||
NamingOptions namingOptions,
|
||||
ILocalizationManager localization,
|
||||
ILogger<SeasonResolver> logger)
|
||||
{
|
||||
_namingOptions = namingOptions;
|
||||
_localization = localization;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified args.
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>Season.</returns>
|
||||
protected override Season Resolve(ItemResolveArgs args)
|
||||
{
|
||||
if (args.Parent is Series series && args.IsDirectory)
|
||||
{
|
||||
var namingOptions = _namingOptions;
|
||||
|
||||
var path = args.Path;
|
||||
|
||||
var seasonParserResult = SeasonPathParser.Parse(path, series.ContainingFolderPath, true, true);
|
||||
|
||||
var season = new Season
|
||||
{
|
||||
IndexNumber = seasonParserResult.SeasonNumber,
|
||||
SeriesId = series.Id,
|
||||
SeriesName = series.Name,
|
||||
Path = seasonParserResult.IsSeasonFolder ? path : null
|
||||
};
|
||||
|
||||
if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder)
|
||||
{
|
||||
var resolver = new Naming.TV.EpisodeResolver(namingOptions);
|
||||
|
||||
var folderName = System.IO.Path.GetFileName(path);
|
||||
var testPath = @"\\test\" + folderName;
|
||||
|
||||
var episodeInfo = resolver.Resolve(testPath, true);
|
||||
|
||||
if (episodeInfo?.EpisodeNumber is not null && episodeInfo.SeasonNumber.HasValue)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Found folder underneath series with episode number: {0}. Season {1}. Episode {2}",
|
||||
path,
|
||||
episodeInfo.SeasonNumber.Value,
|
||||
episodeInfo.EpisodeNumber.Value);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (season.IndexNumber.HasValue && string.IsNullOrEmpty(season.Name))
|
||||
{
|
||||
var seasonNumber = season.IndexNumber.Value;
|
||||
season.Name = seasonNumber == 0 ?
|
||||
args.LibraryOptions.SeasonZeroDisplayName :
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("NameSeasonNumber"),
|
||||
seasonNumber,
|
||||
args.LibraryOptions.PreferredMetadataLanguage);
|
||||
}
|
||||
|
||||
return season;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.TV;
|
||||
using Emby.Naming.Video;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Class SeriesResolver.
|
||||
/// </summary>
|
||||
public class SeriesResolver : GenericFolderResolver<Series>
|
||||
{
|
||||
private readonly ILogger<SeriesResolver> _logger;
|
||||
private readonly NamingOptions _namingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SeriesResolver"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
public SeriesResolver(ILogger<SeriesResolver> logger, NamingOptions namingOptions)
|
||||
{
|
||||
_logger = logger;
|
||||
_namingOptions = namingOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
/// <value>The priority.</value>
|
||||
public override ResolverPriority Priority => ResolverPriority.Second;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified args.
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>Series.</returns>
|
||||
protected override Series Resolve(ItemResolveArgs args)
|
||||
{
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
if (args.HasParent<Series>() || args.HasParent<Season>())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path);
|
||||
|
||||
var collectionType = args.GetCollectionType();
|
||||
if (collectionType == CollectionType.tvshows)
|
||||
{
|
||||
// TODO refactor into separate class or something, this is copied from LibraryManager.GetConfiguredContentType
|
||||
var configuredContentType = args.GetConfiguredContentType();
|
||||
if (configuredContentType != CollectionType.tvshows)
|
||||
{
|
||||
return new Series
|
||||
{
|
||||
Path = args.Path,
|
||||
Name = seriesInfo.Name
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (collectionType is null)
|
||||
{
|
||||
if (args.ContainsFileSystemEntryByName("tvshow.nfo"))
|
||||
{
|
||||
if (args.Parent is not null && args.Parent.IsRoot)
|
||||
{
|
||||
// For now, return null, but if we want to allow this in the future then add some additional checks to guard against a misplaced tvshow.nfo
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Series
|
||||
{
|
||||
Path = args.Path,
|
||||
Name = seriesInfo.Name
|
||||
};
|
||||
}
|
||||
|
||||
if (args.Parent is not null && args.Parent.IsRoot)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (IsSeriesFolder(args.Path, args.FileSystemChildren, false))
|
||||
{
|
||||
return new Series
|
||||
{
|
||||
Path = args.Path,
|
||||
Name = seriesInfo.Name
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsSeriesFolder(
|
||||
string path,
|
||||
IEnumerable<FileSystemMetadata> fileSystemChildren,
|
||||
bool isTvContentType)
|
||||
{
|
||||
foreach (var child in fileSystemChildren)
|
||||
{
|
||||
if (child.IsDirectory)
|
||||
{
|
||||
if (IsSeasonFolder(child.FullName, path, isTvContentType))
|
||||
{
|
||||
_logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string fullName = child.FullName;
|
||||
if (VideoResolver.IsVideoFile(path, _namingOptions))
|
||||
{
|
||||
if (isTvContentType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var namingOptions = _namingOptions;
|
||||
|
||||
var episodeResolver = new Naming.TV.EpisodeResolver(namingOptions);
|
||||
|
||||
var episodeInfo = episodeResolver.Resolve(fullName, false, true, false, fillExtendedInfo: false);
|
||||
if (episodeInfo is not null && episodeInfo.EpisodeNumber.HasValue)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug("{Path} is not a series folder.", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether [is season folder] [the specified path].
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="parentPath">The parentpath.</param>
|
||||
/// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
|
||||
/// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
|
||||
private static bool IsSeasonFolder(string path, string parentPath, bool isTvContentType)
|
||||
{
|
||||
var seasonNumber = SeasonPathParser.Parse(path, parentPath, isTvContentType, isTvContentType).SeasonNumber;
|
||||
|
||||
return seasonNumber.HasValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the initial item values.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="args">The args.</param>
|
||||
protected override void SetInitialItemValues(Series item, ItemResolveArgs args)
|
||||
{
|
||||
base.SetInitialItemValues(item, args);
|
||||
|
||||
SetProviderIdFromPath(item, args.Path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the provider id from path.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="path">The path.</param>
|
||||
private static void SetProviderIdFromPath(Series item, string path)
|
||||
{
|
||||
var justName = Path.GetFileName(path.AsSpan());
|
||||
|
||||
var imdbId = justName.GetAttributeValue("imdbid");
|
||||
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
|
||||
|
||||
var tvdbId = justName.GetAttributeValue("tvdbid");
|
||||
item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId);
|
||||
|
||||
var tvmazeId = justName.GetAttributeValue("tvmazeid");
|
||||
item.TrySetProviderId(MetadataProvider.TvMaze, tvmazeId);
|
||||
|
||||
var tmdbId = justName.GetAttributeValue("tmdbid");
|
||||
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
|
||||
|
||||
var anidbId = justName.GetAttributeValue("anidbid");
|
||||
item.TrySetProviderId("AniDB", anidbId);
|
||||
|
||||
var aniListId = justName.GetAttributeValue("anilistid");
|
||||
item.TrySetProviderId("AniList", aniListId);
|
||||
|
||||
var aniSearchId = justName.GetAttributeValue("anisearchid");
|
||||
item.TrySetProviderId("AniSearch", aniSearchId);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user