瀏覽代碼

Add support for external audio files

Jonas Resch 3 年之前
父節點
當前提交
9978164438

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

@@ -97,6 +97,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>

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

@@ -678,6 +678,12 @@ namespace MediaBrowser.Controller.MediaEncoding
             arg.Append("-i ")
             arg.Append("-i ")
                 .Append(GetInputPathArgument(state));
                 .Append(GetInputPathArgument(state));
 
 
+            if (state.AudioStream.IsExternal)
+            {
+                arg.Append(" -i ")
+                    .Append(string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", state.AudioStream.Path));
+            }
+
             if (state.SubtitleStream != null
             if (state.SubtitleStream != null
                 && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode
                 && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode
                 && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
                 && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)
@@ -1999,10 +2005,17 @@ 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)
+                {
+                    args += " -map 1:a";
+                }
+                else
+                {
+                    args += string.Format(
+                        CultureInfo.InvariantCulture,
+                        " -map 0:{0}",
+                        state.AudioStream.Index);
+                }
             }
             }
             else
             else
             {
             {

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

@@ -0,0 +1,275 @@
+#nullable disable
+
+#pragma warning disable CA1002, CS1591
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+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
+{
+    public class AudioResolver
+    {
+        private readonly ILocalizationManager _localization;
+
+        private readonly IMediaEncoder _mediaEncoder;
+
+        private readonly CancellationToken _cancellationToken;
+
+        public AudioResolver(ILocalizationManager localization, IMediaEncoder mediaEncoder, CancellationToken cancellationToken = default)
+        {
+            _localization = localization;
+            _mediaEncoder = mediaEncoder;
+            _cancellationToken = cancellationToken;
+        }
+
+        public List<MediaStream> GetExternalAudioStreams(
+            Video video,
+            int startIndex,
+            IDirectoryService directoryService,
+            bool clearCache)
+        {
+            var streams = new List<MediaStream>();
+
+            if (!video.IsFileProtocol)
+            {
+                return streams;
+            }
+
+            AddExternalAudioStreams(streams, video.ContainingFolderPath, video.Path, startIndex, directoryService, clearCache);
+
+            startIndex += streams.Count;
+
+            string folder = video.GetInternalMetadataPath();
+
+            if (!Directory.Exists(folder))
+            {
+                return streams;
+            }
+
+            try
+            {
+                AddExternalAudioStreams(streams, folder, video.Path, startIndex, directoryService, clearCache);
+            }
+            catch (IOException)
+            {
+            }
+
+            return streams;
+        }
+
+        public IEnumerable<string> GetExternalAudioFiles(
+            Video video,
+            IDirectoryService directoryService,
+            bool clearCache)
+        {
+            if (!video.IsFileProtocol)
+            {
+                yield break;
+            }
+
+            var streams = GetExternalAudioStreams(video, 0, directoryService, clearCache);
+
+            foreach (var stream in streams)
+            {
+                yield return stream.Path;
+            }
+        }
+
+        public void AddExternalAudioStreams(
+            List<MediaStream> streams,
+            string videoPath,
+            int startIndex,
+            IReadOnlyList<string> files)
+        {
+            var videoFileNameWithoutExtension = NormalizeFilenameForAudioComparison(videoPath);
+
+            for (var i = 0; i < files.Count; i++)
+            {
+
+                var fullName = files[i];
+                var extension = Path.GetExtension(fullName.AsSpan());
+                if (!IsAudioExtension(extension))
+                {
+                    continue;
+                }
+
+                Model.MediaInfo.MediaInfo mediaInfo = GetMediaInfo(fullName).Result;
+                MediaStream mediaStream = mediaInfo.MediaStreams.First();
+                mediaStream.Index = startIndex++;
+                mediaStream.Type = MediaStreamType.Audio;
+                mediaStream.IsExternal = true;
+                mediaStream.Path = fullName;
+                mediaStream.IsDefault = false;
+                mediaStream.Title = null;
+
+                var fileNameWithoutExtension = NormalizeFilenameForAudioComparison(fullName);
+
+                // 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))
+                {
+                    mediaStream.Path = fullName;
+                }
+                else if (fileNameWithoutExtension.Length > videoFileNameWithoutExtension.Length
+                         && fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.'
+                         && fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
+                {
+
+                    // Support xbmc naming conventions - 300.spanish.m4a
+                    var languageSpan = fileNameWithoutExtension;
+                    while (languageSpan.Length > 0)
+                    {
+                        var lastDot = languageSpan.LastIndexOf('.');
+                        var currentSlice = languageSpan[lastDot..];
+                        languageSpan = languageSpan[(lastDot + 1)..];
+                        break;
+                    }
+
+                    // Try to translate to three character code
+                    // Be flexible and check against both the full and three character versions
+                    var language = languageSpan.ToString();
+                    var culture = _localization.FindLanguageInfo(language);
+
+                    language = culture == null ? language : culture.ThreeLetterISOLanguageName;
+                    mediaStream.Language = language;
+                }
+                else
+                {
+                    continue;
+                }
+
+                mediaStream.Codec = extension.TrimStart('.').ToString().ToLowerInvariant();
+
+                streams.Add(mediaStream);
+            }
+        }
+
+        private static bool IsAudioExtension(ReadOnlySpan<char> extension)
+        {
+            String[] audioExtensions = new[]
+            {
+                ".nsv",
+                ".m4a",
+                ".flac",
+                ".aac",
+                ".strm",
+                ".pls",
+                ".rm",
+                ".mpa",
+                ".wav",
+                ".wma",
+                ".ogg",
+                ".opus",
+                ".mp3",
+                ".mp2",
+                ".mod",
+                ".amf",
+                ".669",
+                ".dmf",
+                ".dsm",
+                ".far",
+                ".gdm",
+                ".imf",
+                ".it",
+                ".m15",
+                ".med",
+                ".okt",
+                ".s3m",
+                ".stm",
+                ".sfx",
+                ".ult",
+                ".uni",
+                ".xm",
+                ".sid",
+                ".ac3",
+                ".dts",
+                ".cue",
+                ".aif",
+                ".aiff",
+                ".ape",
+                ".mac",
+                ".mpc",
+                ".mp+",
+                ".mpp",
+                ".shn",
+                ".wv",
+                ".nsf",
+                ".spc",
+                ".gym",
+                ".adplug",
+                ".adx",
+                ".dsp",
+                ".adp",
+                ".ymf",
+                ".ast",
+                ".afc",
+                ".hps",
+                ".xsp",
+                ".acc",
+                ".m4b",
+                ".oga",
+                ".dsf",
+                ".mka"
+            };
+
+            foreach (String audioExtension in audioExtensions)
+            {
+                if (extension.Equals(audioExtension, StringComparison.OrdinalIgnoreCase))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(string path)
+        {
+            _cancellationToken.ThrowIfCancellationRequested();
+
+            return _mediaEncoder.GetMediaInfo(
+                new MediaInfoRequest
+                {
+                    MediaType = DlnaProfileType.Audio,
+                    MediaSource = new MediaSourceInfo
+                    {
+                        Path = path,
+                        Protocol = MediaProtocol.File
+                    }
+                },
+                _cancellationToken);
+        }
+
+        private static ReadOnlySpan<char> NormalizeFilenameForAudioComparison(string filename)
+        {
+            // Try to account for sloppy file naming
+            filename = filename.Replace("_", string.Empty, StringComparison.Ordinal);
+            filename = filename.Replace(" ", string.Empty, StringComparison.Ordinal);
+            return Path.GetFileNameWithoutExtension(filename.AsSpan());
+        }
+
+        private void AddExternalAudioStreams(
+            List<MediaStream> streams,
+            string folder,
+            string videoPath,
+            int startIndex,
+            IDirectoryService directoryService,
+            bool clearCache)
+        {
+            var files = directoryService.GetFilePaths(folder, clearCache, true);
+
+            AddExternalAudioStreams(streams, videoPath, startIndex, files);
+        }
+    }
+}

+ 11 - 0
MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs

@@ -50,6 +50,8 @@ namespace MediaBrowser.Providers.MediaInfo
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly SubtitleResolver _subtitleResolver;
         private readonly SubtitleResolver _subtitleResolver;
 
 
+        private readonly AudioResolver _audioResolver;
+
         private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
         private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
 
 
         public FFProbeProvider(
         public FFProbeProvider(
@@ -78,6 +80,7 @@ namespace MediaBrowser.Providers.MediaInfo
             _mediaSourceManager = mediaSourceManager;
             _mediaSourceManager = mediaSourceManager;
 
 
             _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager);
             _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager);
+            _audioResolver = new AudioResolver(BaseItem.LocalizationManager, mediaEncoder);
         }
         }
 
 
         public string Name => "ffprobe";
         public string Name => "ffprobe";
@@ -111,6 +114,14 @@ namespace MediaBrowser.Providers.MediaInfo
                 return true;
                 return true;
             }
             }
 
 
+            if (item.SupportsLocalMetadata && video != null && !video.IsPlaceHolder
+                && !video.AudioFiles.SequenceEqual(
+                        _audioResolver.GetExternalAudioFiles(video, directoryService, false), StringComparer.Ordinal))
+            {
+                _logger.LogDebug("Refreshing {0} due to external audio change.", item.Path);
+                return true;
+            }
+
             return false;
             return false;
         }
         }
 
 

+ 25 - 0
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -214,6 +214,8 @@ namespace MediaBrowser.Providers.MediaInfo
 
 
             await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
             await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
 
 
+            AddExternalAudio(video, mediaStreams, options, cancellationToken);
+
             var libraryOptions = _libraryManager.GetLibraryOptions(video);
             var libraryOptions = _libraryManager.GetLibraryOptions(video);
 
 
             if (mediaInfo != null)
             if (mediaInfo != null)
@@ -574,6 +576,29 @@ 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 void AddExternalAudio(
+            Video video,
+            List<MediaStream> currentStreams,
+            MetadataRefreshOptions options,
+            CancellationToken cancellationToken)
+        {
+            var audioResolver = new AudioResolver(_localization, _mediaEncoder, cancellationToken);
+
+            var startIndex = currentStreams.Count == 0 ? 0 : (currentStreams.Select(i => i.Index).Max() + 1);
+            var externalAudioStreams = audioResolver.GetExternalAudioStreams(video, startIndex, options.DirectoryService, false);
+
+            video.AudioFiles = externalAudioStreams.Select(i => i.Path).ToArray();
+
+            currentStreams.AddRange(externalAudioStreams);
+        }
+
         /// <summary>
         /// <summary>
         /// Creates dummy chapters.
         /// Creates dummy chapters.
         /// </summary>
         /// </summary>