浏览代码

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

Add support for external audio files
Cody Robibero 3 年之前
父节点
当前提交
8b4a36d6f7

+ 1 - 0
CONTRIBUTORS.md

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

+ 7 - 0
MediaBrowser.Controller/Entities/Video.cs

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

+ 23 - 4
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -696,6 +696,11 @@ namespace MediaBrowser.Controller.MediaEncoding
                 arg.Append(" -i \"").Append(subtitlePath).Append('\"');
                 arg.Append(" -i \"").Append(subtitlePath).Append('\"');
             }
             }
 
 
+            if (state.AudioStream != null && state.AudioStream.IsExternal)
+            {
+                arg.Append(" -i \"").Append(state.AudioStream.Path).Append('"');
+            }
+
             return arg.ToString();
             return arg.ToString();
         }
         }
 
 
@@ -1999,10 +2004,24 @@ namespace MediaBrowser.Controller.MediaEncoding
 
 
             if (state.AudioStream != null)
             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
             else
             {
             {

+ 176 - 0
MediaBrowser.Providers/MediaInfo/AudioResolver.cs

@@ -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);
+        }
+    }
+}

+ 17 - 5
MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs

@@ -7,6 +7,7 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Emby.Naming.Common;
 using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -39,6 +40,7 @@ namespace MediaBrowser.Providers.MediaInfo
     {
     {
         private readonly ILogger<FFProbeProvider> _logger;
         private readonly ILogger<FFProbeProvider> _logger;
         private readonly SubtitleResolver _subtitleResolver;
         private readonly SubtitleResolver _subtitleResolver;
+        private readonly AudioResolver _audioResolver;
         private readonly FFProbeVideoInfo _videoProber;
         private readonly FFProbeVideoInfo _videoProber;
         private readonly FFProbeAudioInfo _audioProber;
         private readonly FFProbeAudioInfo _audioProber;
 
 
@@ -55,10 +57,11 @@ namespace MediaBrowser.Providers.MediaInfo
             IServerConfigurationManager config,
             IServerConfigurationManager config,
             ISubtitleManager subtitleManager,
             ISubtitleManager subtitleManager,
             IChapterManager chapterManager,
             IChapterManager chapterManager,
-            ILibraryManager libraryManager)
+            ILibraryManager libraryManager,
+            NamingOptions namingOptions)
         {
         {
             _logger = logger;
             _logger = logger;
-
+            _audioResolver = new AudioResolver(localization, mediaEncoder, namingOptions);
             _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager);
             _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager);
             _videoProber = new FFProbeVideoInfo(
             _videoProber = new FFProbeVideoInfo(
                 _logger,
                 _logger,
@@ -71,7 +74,8 @@ namespace MediaBrowser.Providers.MediaInfo
                 config,
                 config,
                 subtitleManager,
                 subtitleManager,
                 chapterManager,
                 chapterManager,
-                libraryManager);
+                libraryManager,
+                _audioResolver);
             _audioProber = new FFProbeAudioInfo(mediaSourceManager, mediaEncoder, itemRepo, libraryManager);
             _audioProber = new FFProbeAudioInfo(mediaSourceManager, mediaEncoder, itemRepo, libraryManager);
         }
         }
 
 
@@ -92,7 +96,7 @@ namespace MediaBrowser.Providers.MediaInfo
                     var file = directoryService.GetFile(path);
                     var file = directoryService.GetFile(path);
                     if (file != null && file.LastWriteTimeUtc != item.DateModified)
                     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;
                         return true;
                     }
                     }
                 }
                 }
@@ -102,7 +106,15 @@ namespace MediaBrowser.Providers.MediaInfo
                 && !video.SubtitleFiles.SequenceEqual(
                 && !video.SubtitleFiles.SequenceEqual(
                         _subtitleResolver.GetExternalSubtitleFiles(video, directoryService, false), StringComparer.Ordinal))
                         _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;
                 return true;
             }
             }
 
 

+ 31 - 1
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -44,6 +44,7 @@ namespace MediaBrowser.Providers.MediaInfo
         private readonly ISubtitleManager _subtitleManager;
         private readonly ISubtitleManager _subtitleManager;
         private readonly IChapterManager _chapterManager;
         private readonly IChapterManager _chapterManager;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
+        private readonly AudioResolver _audioResolver;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaSourceManager _mediaSourceManager;
 
 
         private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks;
         private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks;
@@ -59,7 +60,8 @@ namespace MediaBrowser.Providers.MediaInfo
             IServerConfigurationManager config,
             IServerConfigurationManager config,
             ISubtitleManager subtitleManager,
             ISubtitleManager subtitleManager,
             IChapterManager chapterManager,
             IChapterManager chapterManager,
-            ILibraryManager libraryManager)
+            ILibraryManager libraryManager,
+            AudioResolver audioResolver)
         {
         {
             _logger = logger;
             _logger = logger;
             _mediaEncoder = mediaEncoder;
             _mediaEncoder = mediaEncoder;
@@ -71,6 +73,7 @@ namespace MediaBrowser.Providers.MediaInfo
             _subtitleManager = subtitleManager;
             _subtitleManager = subtitleManager;
             _chapterManager = chapterManager;
             _chapterManager = chapterManager;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
+            _audioResolver = audioResolver;
             _mediaSourceManager = mediaSourceManager;
             _mediaSourceManager = mediaSourceManager;
         }
         }
 
 
@@ -214,6 +217,8 @@ namespace MediaBrowser.Providers.MediaInfo
 
 
             await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
             await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
 
 
+            await AddExternalAudioAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
+
             var libraryOptions = _libraryManager.GetLibraryOptions(video);
             var libraryOptions = _libraryManager.GetLibraryOptions(video);
 
 
             if (mediaInfo != null)
             if (mediaInfo != null)
@@ -574,6 +579,31 @@ namespace MediaBrowser.Providers.MediaInfo
             currentStreams.AddRange(externalSubtitleStreams);
             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>
         /// <summary>
         /// Creates dummy chapters.
         /// Creates dummy chapters.
         /// </summary>
         /// </summary>