mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-07-09 03:04:24 -04:00
Merge pull request #6898 from jonas-resch/support-external-audio-files
Add support for external audio files
This commit is contained in:
commit
8b4a36d6f7
@ -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
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1998,12 +2003,26 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state.AudioStream != null)
|
if (state.AudioStream != null)
|
||||||
|
{
|
||||||
|
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(
|
args += string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
" -map 0:{0}",
|
" -map 0:{0}",
|
||||||
state.AudioStream.Index);
|
state.AudioStream.Index);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
args += " -map -0:a";
|
args += " -map -0:a";
|
||||||
|
176
MediaBrowser.Providers/MediaInfo/AudioResolver.cs
Normal file
176
MediaBrowser.Providers/MediaInfo/AudioResolver.cs
Normal file
@ -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.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user