Merge pull request #6898 from jonas-resch/support-external-audio-files

Add support for external audio files
pull/6974/head v10.8.0-alpha3
Cody Robibero 3 years ago committed by GitHub
commit 8b4a36d6f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -150,6 +150,7 @@
- [ianjazz246](https://github.com/ianjazz246)
- [peterspenler](https://github.com/peterspenler)
- [MBR-0001](https://github.com/MBR-0001)
- [jonas-resch](https://github.com/jonas-resch)
# Emby Contributors

@ -33,6 +33,7 @@ namespace MediaBrowser.Controller.Entities
AdditionalParts = Array.Empty<string>();
LocalAlternateVersions = Array.Empty<string>();
SubtitleFiles = Array.Empty<string>();
AudioFiles = Array.Empty<string>();
LinkedAlternateVersions = Array.Empty<LinkedChild>();
}
@ -97,6 +98,12 @@ namespace MediaBrowser.Controller.Entities
/// <value>The subtitle paths.</value>
public string[] SubtitleFiles { get; set; }
/// <summary>
/// Gets or sets the audio paths.
/// </summary>
/// <value>The audio paths.</value>
public string[] AudioFiles { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance has subtitles.
/// </summary>

@ -696,6 +696,11 @@ namespace MediaBrowser.Controller.MediaEncoding
arg.Append(" -i \"").Append(subtitlePath).Append('\"');
}
if (state.AudioStream != null && state.AudioStream.IsExternal)
{
arg.Append(" -i \"").Append(state.AudioStream.Path).Append('"');
}
return arg.ToString();
}
@ -1999,10 +2004,24 @@ namespace MediaBrowser.Controller.MediaEncoding
if (state.AudioStream != null)
{
args += string.Format(
CultureInfo.InvariantCulture,
" -map 0:{0}",
state.AudioStream.Index);
if (state.AudioStream.IsExternal)
{
int externalAudioMapIndex = state.SubtitleStream != null && state.SubtitleStream.IsExternal ? 2 : 1;
int externalAudioStream = state.MediaSource.MediaStreams.Where(i => i.Path == state.AudioStream.Path).ToList().IndexOf(state.AudioStream);
args += string.Format(
CultureInfo.InvariantCulture,
" -map {0}:{1}",
externalAudioMapIndex,
externalAudioStream);
}
else
{
args += string.Format(
CultureInfo.InvariantCulture,
" -map 0:{0}",
state.AudioStream.Index);
}
}
else
{

@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Emby.Naming.Audio;
using Emby.Naming.Common;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.MediaInfo;
namespace MediaBrowser.Providers.MediaInfo
{
/// <summary>
/// Resolves external audios for videos.
/// </summary>
public class AudioResolver
{
private readonly ILocalizationManager _localizationManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly NamingOptions _namingOptions;
/// <summary>
/// Initializes a new instance of the <see cref="AudioResolver"/> class.
/// </summary>
/// <param name="localizationManager">The localization manager.</param>
/// <param name="mediaEncoder">The media encoder.</param>
/// <param name="namingOptions">The naming options.</param>
public AudioResolver(
ILocalizationManager localizationManager,
IMediaEncoder mediaEncoder,
NamingOptions namingOptions)
{
_localizationManager = localizationManager;
_mediaEncoder = mediaEncoder;
_namingOptions = namingOptions;
}
/// <summary>
/// Returns the audio streams found in the external audio files for the given video.
/// </summary>
/// <param name="video">The video to get the external audio streams from.</param>
/// <param name="startIndex">The stream index to start adding audio streams at.</param>
/// <param name="directoryService">The directory service to search for files.</param>
/// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
/// <returns>A list of external audio streams.</returns>
public async IAsyncEnumerable<MediaStream> GetExternalAudioStreams(
Video video,
int startIndex,
IDirectoryService directoryService,
bool clearCache,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (!video.IsFileProtocol)
{
yield break;
}
IEnumerable<string> paths = GetExternalAudioFiles(video, directoryService, clearCache);
foreach (string path in paths)
{
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
Model.MediaInfo.MediaInfo mediaInfo = await GetMediaInfo(path, cancellationToken).ConfigureAwait(false);
foreach (MediaStream mediaStream in mediaInfo.MediaStreams)
{
mediaStream.Index = startIndex++;
mediaStream.Type = MediaStreamType.Audio;
mediaStream.IsExternal = true;
mediaStream.Path = path;
mediaStream.IsDefault = false;
mediaStream.Title = null;
if (string.IsNullOrEmpty(mediaStream.Language))
{
// Try to translate to three character code
// Be flexible and check against both the full and three character versions
var language = StringExtensions.RightPart(fileNameWithoutExtension, '.').ToString();
if (language != fileNameWithoutExtension)
{
var culture = _localizationManager.FindLanguageInfo(language);
language = culture == null ? language : culture.ThreeLetterISOLanguageName;
mediaStream.Language = language;
}
}
yield return mediaStream;
}
}
}
/// <summary>
/// Returns the external audio file paths for the given video.
/// </summary>
/// <param name="video">The video to get the external audio file paths from.</param>
/// <param name="directoryService">The directory service to search for files.</param>
/// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
/// <returns>A list of external audio file paths.</returns>
public IEnumerable<string> GetExternalAudioFiles(
Video video,
IDirectoryService directoryService,
bool clearCache)
{
if (!video.IsFileProtocol)
{
yield break;
}
// Check if video folder exists
string folder = video.ContainingFolderPath;
if (!Directory.Exists(folder))
{
yield break;
}
string videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(video.Path);
var files = directoryService.GetFilePaths(folder, clearCache, true);
for (int i = 0; i < files.Count; i++)
{
string file = files[i];
if (string.Equals(video.Path, file, StringComparison.OrdinalIgnoreCase)
|| !AudioFileParser.IsAudioFile(file, _namingOptions)
|| Path.GetExtension(file.AsSpan()).Equals(".strm", StringComparison.OrdinalIgnoreCase))
{
continue;
}
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file);
// The audio filename must either be equal to the video filename or start with the video filename followed by a dot
if (videoFileNameWithoutExtension.Equals(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)
|| (fileNameWithoutExtension.Length > videoFileNameWithoutExtension.Length
&& fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.'
&& fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)))
{
yield return file;
}
}
}
/// <summary>
/// Returns the media info of the given audio file.
/// </summary>
/// <param name="path">The path to the audio file.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
/// <returns>The media info for the given audio file.</returns>
private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(string path, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return _mediaEncoder.GetMediaInfo(
new MediaInfoRequest
{
MediaType = DlnaProfileType.Audio,
MediaSource = new MediaSourceInfo
{
Path = path,
Protocol = MediaProtocol.File
}
},
cancellationToken);
}
}
}

@ -7,6 +7,7 @@ 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;
@ -39,6 +40,7 @@ namespace MediaBrowser.Providers.MediaInfo
{
private readonly ILogger<FFProbeProvider> _logger;
private readonly SubtitleResolver _subtitleResolver;
private readonly AudioResolver _audioResolver;
private readonly FFProbeVideoInfo _videoProber;
private readonly FFProbeAudioInfo _audioProber;
@ -55,10 +57,11 @@ namespace MediaBrowser.Providers.MediaInfo
IServerConfigurationManager config,
ISubtitleManager subtitleManager,
IChapterManager chapterManager,
ILibraryManager libraryManager)
ILibraryManager libraryManager,
NamingOptions namingOptions)
{
_logger = logger;
_audioResolver = new AudioResolver(localization, mediaEncoder, namingOptions);
_subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager);
_videoProber = new FFProbeVideoInfo(
_logger,
@ -71,7 +74,8 @@ namespace MediaBrowser.Providers.MediaInfo
config,
subtitleManager,
chapterManager,
libraryManager);
libraryManager,
_audioResolver);
_audioProber = new FFProbeAudioInfo(mediaSourceManager, mediaEncoder, itemRepo, libraryManager);
}
@ -92,7 +96,7 @@ namespace MediaBrowser.Providers.MediaInfo
var file = directoryService.GetFile(path);
if (file != null && file.LastWriteTimeUtc != item.DateModified)
{
_logger.LogDebug("Refreshing {0} due to date modified timestamp change.", path);
_logger.LogDebug("Refreshing {ItemPath} due to date modified timestamp change.", path);
return true;
}
}
@ -102,7 +106,15 @@ namespace MediaBrowser.Providers.MediaInfo
&& !video.SubtitleFiles.SequenceEqual(
_subtitleResolver.GetExternalSubtitleFiles(video, directoryService, false), StringComparer.Ordinal))
{
_logger.LogDebug("Refreshing {0} due to external subtitles change.", item.Path);
_logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path);
return true;
}
if (item.SupportsLocalMetadata && video != null && !video.IsPlaceHolder
&& !video.AudioFiles.SequenceEqual(
_audioResolver.GetExternalAudioFiles(video, directoryService, false), StringComparer.Ordinal))
{
_logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path);
return true;
}

@ -44,6 +44,7 @@ namespace MediaBrowser.Providers.MediaInfo
private readonly ISubtitleManager _subtitleManager;
private readonly IChapterManager _chapterManager;
private readonly ILibraryManager _libraryManager;
private readonly AudioResolver _audioResolver;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks;
@ -59,7 +60,8 @@ namespace MediaBrowser.Providers.MediaInfo
IServerConfigurationManager config,
ISubtitleManager subtitleManager,
IChapterManager chapterManager,
ILibraryManager libraryManager)
ILibraryManager libraryManager,
AudioResolver audioResolver)
{
_logger = logger;
_mediaEncoder = mediaEncoder;
@ -71,6 +73,7 @@ namespace MediaBrowser.Providers.MediaInfo
_subtitleManager = subtitleManager;
_chapterManager = chapterManager;
_libraryManager = libraryManager;
_audioResolver = audioResolver;
_mediaSourceManager = mediaSourceManager;
}
@ -214,6 +217,8 @@ namespace MediaBrowser.Providers.MediaInfo
await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
await AddExternalAudioAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
var libraryOptions = _libraryManager.GetLibraryOptions(video);
if (mediaInfo != null)
@ -574,6 +579,31 @@ namespace MediaBrowser.Providers.MediaInfo
currentStreams.AddRange(externalSubtitleStreams);
}
/// <summary>
/// Adds the external audio.
/// </summary>
/// <param name="video">The video.</param>
/// <param name="currentStreams">The current streams.</param>
/// <param name="options">The refreshOptions.</param>
/// <param name="cancellationToken">The cancellation token.</param>
private async Task AddExternalAudioAsync(
Video video,
List<MediaStream> currentStreams,
MetadataRefreshOptions options,
CancellationToken cancellationToken)
{
var startIndex = currentStreams.Count == 0 ? 0 : currentStreams.Max(i => i.Index) + 1;
var externalAudioStreams = _audioResolver.GetExternalAudioStreams(video, startIndex, options.DirectoryService, false, cancellationToken);
await foreach (MediaStream externalAudioStream in externalAudioStreams)
{
currentStreams.Add(externalAudioStream);
}
// Select all external audio file paths
video.AudioFiles = currentStreams.Where(i => i.Type == MediaStreamType.Audio && i.IsExternal).Select(i => i.Path).Distinct().ToArray();
}
/// <summary>
/// Creates dummy chapters.
/// </summary>

Loading…
Cancel
Save