#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Emby.Naming.Common;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
using PDFtoImage;
using SharpCompress.Archives;
namespace MediaBrowser.Providers.MediaInfo
{
///
/// The probe provider.
///
public class ProbeProvider : ICustomMetadataProvider,
ICustomMetadataProvider,
ICustomMetadataProvider,
ICustomMetadataProvider,
ICustomMetadataProvider,
ICustomMetadataProvider,
ICustomMetadataProvider,
ICustomMetadataProvider,
IHasOrder,
IForcedProvider,
IPreRefreshProvider,
IHasItemChangeMonitor
{
private readonly ILogger _logger;
private readonly AudioResolver _audioResolver;
private readonly SubtitleResolver _subtitleResolver;
private readonly LyricResolver _lyricResolver;
private readonly FFProbeVideoInfo _videoProber;
private readonly AudioFileProber _audioProber;
private readonly Task _cachedTask = Task.FromResult(ItemUpdateType.None);
///
/// Initializes a new instance of the class.
///
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the .
/// Instance of the interface.
/// The .
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
public ProbeProvider(
IMediaSourceManager mediaSourceManager,
IMediaEncoder mediaEncoder,
IBlurayExaminer blurayExaminer,
ILocalizationManager localization,
IChapterManager chapterManager,
IServerConfigurationManager config,
ISubtitleManager subtitleManager,
ILibraryManager libraryManager,
IFileSystem fileSystem,
ILoggerFactory loggerFactory,
NamingOptions namingOptions,
ILyricManager lyricManager,
IMediaAttachmentRepository mediaAttachmentRepository,
IMediaStreamRepository mediaStreamRepository)
{
_logger = loggerFactory.CreateLogger();
_audioResolver = new AudioResolver(loggerFactory.CreateLogger(), localization, mediaEncoder, fileSystem, namingOptions);
_subtitleResolver = new SubtitleResolver(loggerFactory.CreateLogger(), localization, mediaEncoder, fileSystem, namingOptions);
_lyricResolver = new LyricResolver(loggerFactory.CreateLogger(), localization, mediaEncoder, fileSystem, namingOptions);
_videoProber = new FFProbeVideoInfo(
loggerFactory.CreateLogger(),
mediaSourceManager,
mediaEncoder,
blurayExaminer,
localization,
chapterManager,
config,
subtitleManager,
libraryManager,
_audioResolver,
_subtitleResolver,
mediaAttachmentRepository,
mediaStreamRepository);
_audioProber = new AudioFileProber(
loggerFactory.CreateLogger(),
mediaSourceManager,
mediaEncoder,
libraryManager,
_lyricResolver,
lyricManager,
mediaStreamRepository,
chapterManager);
}
///
public string Name => "Probe Provider";
///
public int Order => 100;
///
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
{
var video = item as Video;
if (video is null || video.VideoType == VideoType.VideoFile || video.VideoType == VideoType.Iso)
{
var path = item.Path;
if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol)
{
var file = directoryService.GetFile(path);
if (file is not null && item.HasChanged(file.LastWriteTimeUtc))
{
_logger.LogDebug("Refreshing {ItemPath} due to file system modification.", path);
return true;
}
}
}
if (video is not null
&& item.SupportsLocalMetadata
&& !video.IsPlaceHolder)
{
var externalFiles = new HashSet(_subtitleResolver.GetExternalFiles(video, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase);
if (!new HashSet(video.SubtitleFiles, StringComparer.Ordinal).SetEquals(externalFiles))
{
_logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path);
return true;
}
externalFiles = new HashSet(_audioResolver.GetExternalFiles(video, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase);
if (!new HashSet(video.AudioFiles, StringComparer.Ordinal).SetEquals(externalFiles))
{
_logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path);
return true;
}
}
if (item is Audio audio
&& item.SupportsLocalMetadata)
{
var externalFiles = new HashSet(_lyricResolver.GetExternalFiles(audio, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase);
if (!new HashSet(audio.LyricFiles, StringComparer.Ordinal).SetEquals(externalFiles))
{
_logger.LogDebug("Refreshing {ItemPath} due to external lyrics change.", item.Path);
return true;
}
}
return false;
}
///
public Task FetchAsync(Episode item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
return FetchVideoInfo(item, options, cancellationToken);
}
///
public Task FetchAsync(MusicVideo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
return FetchVideoInfo(item, options, cancellationToken);
}
///
public Task FetchAsync(Movie item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
return FetchVideoInfo(item, options, cancellationToken);
}
///
public Task FetchAsync(Trailer item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
return FetchVideoInfo(item, options, cancellationToken);
}
///
public Task FetchAsync(Video item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
return FetchVideoInfo(item, options, cancellationToken);
}
///
public Task FetchAsync(Audio item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
return FetchAudioInfo(item, options, cancellationToken);
}
///
public Task FetchAsync(AudioBook item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
return FetchAudioInfo(item, options, cancellationToken);
}
///
public Task FetchAsync(Book item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
if (item.IsVirtualItem || !item.IsFileProtocol)
{
return _cachedTask;
}
long pageCount;
switch (Path.GetExtension(item.Path).ToLowerInvariant())
{
case ".cb7":
case ".cbr":
case ".cbt":
case ".cbz":
using (var stream = File.OpenRead(item.Path))
using (var archive = ArchiveFactory.OpenArchive(stream))
{
pageCount = archive.Entries.Count(e => !e.IsDirectory);
}
break;
#pragma warning disable CA1416
case ".pdf":
using (var stream = File.OpenRead(item.Path))
{
pageCount = Conversion.GetPageCount(stream);
}
break;
#pragma warning restore CA1416
case ".epub":
// TODO process CFI and store as a string when multiple progress types are supported
// current progress value is percentage stored as a proportion of one second worth of ticks
item.RunTimeTicks = TimeSpan.TicksPerSecond;
return Task.FromResult(ItemUpdateType.MetadataImport);
default:
return _cachedTask;
}
// TODO use page count without modification when multiple progress types are supported
// book players report page count and the web client multiplies that value by 10000 to convert the expected milliseconds into ticks
item.RunTimeTicks = pageCount * 10000;
return Task.FromResult(ItemUpdateType.MetadataImport);
}
///
/// Fetches video information for an item.
///
/// The item.
/// The .
/// The .
/// The type of item to resolve.
/// A fetching the for an item.
public Task FetchVideoInfo(T item, MetadataRefreshOptions options, CancellationToken cancellationToken)
where T : Video
{
if (item.IsPlaceHolder)
{
return _cachedTask;
}
if (!item.IsCompleteMedia)
{
return _cachedTask;
}
if (item.IsVirtualItem)
{
return _cachedTask;
}
if (!options.EnableRemoteContentProbe && !item.IsFileProtocol)
{
return _cachedTask;
}
if (item.IsShortcut)
{
FetchShortcutInfo(item);
}
return _videoProber.ProbeVideo(item, options, cancellationToken);
}
private string NormalizeStrmLine(string line)
{
return line.Replace("\t", string.Empty, StringComparison.Ordinal)
.Replace("\r", string.Empty, StringComparison.Ordinal)
.Replace("\n", string.Empty, StringComparison.Ordinal)
.Trim();
}
private void FetchShortcutInfo(BaseItem item)
{
var shortcutPath = File.ReadAllLines(item.Path)
.Select(NormalizeStrmLine)
.FirstOrDefault(i => !string.IsNullOrWhiteSpace(i) && !i.StartsWith('#'));
if (string.IsNullOrWhiteSpace(shortcutPath))
{
return;
}
// Only allow remote URLs in .strm files to prevent local file access
if (Uri.TryCreate(shortcutPath, UriKind.Absolute, out var uri)
&& (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase)
|| string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase)
|| string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase)
|| string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase)))
{
item.ShortcutPath = shortcutPath;
}
else
{
_logger.LogWarning("Ignoring invalid or non-remote .strm path in {File}: {Path}", item.Path, shortcutPath);
}
}
///
/// Fetches audio information for an item.
///
/// The item.
/// The .
/// The .
/// The type of item to resolve.
/// A fetching the for an item.
public Task FetchAudioInfo(T item, MetadataRefreshOptions options, CancellationToken cancellationToken)
where T : Audio
{
if (item.IsVirtualItem)
{
return _cachedTask;
}
if (!options.EnableRemoteContentProbe && !item.IsFileProtocol)
{
return _cachedTask;
}
if (item.IsShortcut)
{
FetchShortcutInfo(item);
}
return _audioProber.Probe(item, options, cancellationToken);
}
}
}