mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-15 18:08:53 -07:00
merge branch master into plugin
This commit is contained in:
commit
cb2523e2ef
@ -128,6 +128,7 @@
|
||||
- [xosdy](https://github.com/xosdy)
|
||||
- [XVicarious](https://github.com/XVicarious)
|
||||
- [YouKnowBlom](https://github.com/YouKnowBlom)
|
||||
- [KristupasSavickas](https://github.com/KristupasSavickas)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
@ -74,4 +74,4 @@ VOLUME /cache /config /media
|
||||
ENTRYPOINT ["./jellyfin/jellyfin", \
|
||||
"--datadir", "/config", \
|
||||
"--cachedir", "/cache", \
|
||||
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg"]
|
||||
"--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
|
||||
|
@ -30,7 +30,6 @@ using Emby.Server.Implementations.Configuration;
|
||||
using Emby.Server.Implementations.Cryptography;
|
||||
using Emby.Server.Implementations.Data;
|
||||
using Emby.Server.Implementations.Devices;
|
||||
using Emby.Server.Implementations.Diagnostics;
|
||||
using Emby.Server.Implementations.Dto;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using Emby.Server.Implementations.HttpServer.Security;
|
||||
@ -86,7 +85,6 @@ using MediaBrowser.MediaEncoding.BdInfo;
|
||||
using MediaBrowser.Model.Activity;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.Diagnostics;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
@ -324,8 +322,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
internal IImageEncoder ImageEncoder { get; private set; }
|
||||
|
||||
protected IProcessFactory ProcessFactory { get; private set; }
|
||||
|
||||
protected readonly IXmlSerializer XmlSerializer;
|
||||
|
||||
protected ISocketFactory SocketFactory { get; private set; }
|
||||
@ -667,9 +663,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
serviceCollection.AddSingleton(XmlSerializer);
|
||||
|
||||
ProcessFactory = new ProcessFactory();
|
||||
serviceCollection.AddSingleton(ProcessFactory);
|
||||
|
||||
serviceCollection.AddSingleton(typeof(IStreamHelper), typeof(StreamHelper));
|
||||
|
||||
var cryptoProvider = new CryptographyProvider();
|
||||
@ -730,7 +723,6 @@ namespace Emby.Server.Implementations
|
||||
LoggerFactory.CreateLogger<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(),
|
||||
ServerConfigurationManager,
|
||||
FileSystemManager,
|
||||
ProcessFactory,
|
||||
LocalizationManager,
|
||||
() => SubtitleEncoder,
|
||||
startupConfig,
|
||||
@ -844,8 +836,7 @@ namespace Emby.Server.Implementations
|
||||
FileSystemManager,
|
||||
MediaEncoder,
|
||||
HttpClient,
|
||||
MediaSourceManager,
|
||||
ProcessFactory);
|
||||
MediaSourceManager);
|
||||
serviceCollection.AddSingleton(SubtitleEncoder);
|
||||
|
||||
serviceCollection.AddSingleton(typeof(IResourceFileManager), typeof(ResourceFileManager));
|
||||
@ -1664,15 +1655,17 @@ namespace Emby.Server.Implementations
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
var process = ProcessFactory.Create(new ProcessOptions
|
||||
var process = new Process
|
||||
{
|
||||
FileName = url,
|
||||
EnableRaisingEvents = true,
|
||||
UseShellExecute = true,
|
||||
ErrorDialog = false
|
||||
});
|
||||
|
||||
process.Exited += ProcessExited;
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = url,
|
||||
UseShellExecute = true,
|
||||
ErrorDialog = false
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
process.Exited += (sender, args) => ((Process)sender).Dispose();
|
||||
|
||||
try
|
||||
{
|
||||
@ -1685,11 +1678,6 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessExited(object sender, EventArgs e)
|
||||
{
|
||||
((IProcess)sender).Dispose();
|
||||
}
|
||||
|
||||
public virtual void EnableLoopback(string appName)
|
||||
{
|
||||
}
|
||||
|
@ -1,152 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Diagnostics;
|
||||
|
||||
namespace Emby.Server.Implementations.Diagnostics
|
||||
{
|
||||
public class CommonProcess : IProcess
|
||||
{
|
||||
private readonly Process _process;
|
||||
|
||||
private bool _disposed = false;
|
||||
private bool _hasExited;
|
||||
|
||||
public CommonProcess(ProcessOptions options)
|
||||
{
|
||||
StartInfo = options;
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
Arguments = options.Arguments,
|
||||
FileName = options.FileName,
|
||||
WorkingDirectory = options.WorkingDirectory,
|
||||
UseShellExecute = options.UseShellExecute,
|
||||
CreateNoWindow = options.CreateNoWindow,
|
||||
RedirectStandardError = options.RedirectStandardError,
|
||||
RedirectStandardInput = options.RedirectStandardInput,
|
||||
RedirectStandardOutput = options.RedirectStandardOutput,
|
||||
ErrorDialog = options.ErrorDialog
|
||||
};
|
||||
|
||||
|
||||
if (options.IsHidden)
|
||||
{
|
||||
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
|
||||
}
|
||||
|
||||
_process = new Process
|
||||
{
|
||||
StartInfo = startInfo
|
||||
};
|
||||
|
||||
if (options.EnableRaisingEvents)
|
||||
{
|
||||
_process.EnableRaisingEvents = true;
|
||||
_process.Exited += OnProcessExited;
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler Exited;
|
||||
|
||||
public ProcessOptions StartInfo { get; }
|
||||
|
||||
public StreamWriter StandardInput => _process.StandardInput;
|
||||
|
||||
public StreamReader StandardError => _process.StandardError;
|
||||
|
||||
public StreamReader StandardOutput => _process.StandardOutput;
|
||||
|
||||
public int ExitCode => _process.ExitCode;
|
||||
|
||||
private bool HasExited
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_hasExited)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_hasExited = _process.HasExited;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
_hasExited = true;
|
||||
}
|
||||
|
||||
return _hasExited;
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_process.Start();
|
||||
}
|
||||
|
||||
public void Kill()
|
||||
{
|
||||
_process.Kill();
|
||||
}
|
||||
|
||||
public bool WaitForExit(int timeMs)
|
||||
{
|
||||
return _process.WaitForExit(timeMs);
|
||||
}
|
||||
|
||||
public Task<bool> WaitForExitAsync(int timeMs)
|
||||
{
|
||||
// Note: For this function to work correctly, the option EnableRisingEvents needs to be set to true.
|
||||
|
||||
if (HasExited)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
timeMs = Math.Max(0, timeMs);
|
||||
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
var cancellationToken = new CancellationTokenSource(timeMs).Token;
|
||||
|
||||
_process.Exited += (sender, args) => tcs.TrySetResult(true);
|
||||
|
||||
cancellationToken.Register(() => tcs.TrySetResult(HasExited));
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_process?.Dispose();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private void OnProcessExited(object sender, EventArgs e)
|
||||
{
|
||||
_hasExited = true;
|
||||
Exited?.Invoke(this, e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using MediaBrowser.Model.Diagnostics;
|
||||
|
||||
namespace Emby.Server.Implementations.Diagnostics
|
||||
{
|
||||
public class ProcessFactory : IProcessFactory
|
||||
{
|
||||
public IProcess Create(ProcessOptions options)
|
||||
{
|
||||
return new CommonProcess(options);
|
||||
}
|
||||
}
|
||||
}
|
@ -239,7 +239,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ErrorHandler(Exception ex, IRequest httpReq, bool logExceptionStackTrace)
|
||||
private async Task ErrorHandler(Exception ex, IRequest httpReq, bool logExceptionStackTrace, string urlToLog)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -247,11 +247,11 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (logExceptionStackTrace)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing request");
|
||||
_logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Error processing request: {Message}", ex.Message);
|
||||
_logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog);
|
||||
}
|
||||
|
||||
var httpRes = httpReq.Response;
|
||||
@ -271,7 +271,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
catch (Exception errorEx)
|
||||
{
|
||||
_logger.LogError(errorEx, "Error this.ProcessRequest(context)(Exception while writing error to the response)");
|
||||
_logger.LogError(errorEx, "Error this.ProcessRequest(context)(Exception while writing error to the response). URL: {Url}", urlToLog);
|
||||
}
|
||||
}
|
||||
|
||||
@ -456,7 +456,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
var stopWatch = new Stopwatch();
|
||||
stopWatch.Start();
|
||||
var httpRes = httpReq.Response;
|
||||
string urlToLog = null;
|
||||
string urlToLog = GetUrlToLog(urlString);
|
||||
string remoteIp = httpReq.RemoteIp;
|
||||
|
||||
try
|
||||
@ -502,8 +502,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return;
|
||||
}
|
||||
|
||||
urlToLog = GetUrlToLog(urlString);
|
||||
|
||||
if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
|
||||
@ -553,7 +551,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|| ex is OperationCanceledException
|
||||
|| ex is SecurityException
|
||||
|| ex is FileNotFoundException;
|
||||
await ErrorHandler(ex, httpReq, ignoreStackTrace).ConfigureAwait(false);
|
||||
await ErrorHandler(ex, httpReq, !ignoreStackTrace, urlToLog).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -25,7 +26,6 @@ using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Diagnostics;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Events;
|
||||
@ -61,7 +61,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IProcessFactory _processFactory;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IStreamHelper _streamHelper;
|
||||
|
||||
@ -88,8 +87,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
ILibraryManager libraryManager,
|
||||
ILibraryMonitor libraryMonitor,
|
||||
IProviderManager providerManager,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IProcessFactory processFactory)
|
||||
IMediaEncoder mediaEncoder)
|
||||
{
|
||||
Current = this;
|
||||
|
||||
@ -102,7 +100,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
_libraryMonitor = libraryMonitor;
|
||||
_providerManager = providerManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_processFactory = processFactory;
|
||||
_liveTvManager = (LiveTvManager)liveTvManager;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
@ -1662,7 +1659,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http))
|
||||
{
|
||||
return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _processFactory, _config);
|
||||
return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config);
|
||||
}
|
||||
|
||||
return new DirectRecorder(_logger, _httpClient, _streamHelper);
|
||||
@ -1683,16 +1680,19 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
try
|
||||
{
|
||||
var process = _processFactory.Create(new ProcessOptions
|
||||
var process = new Process
|
||||
{
|
||||
Arguments = GetPostProcessArguments(path, options.RecordingPostProcessorArguments),
|
||||
CreateNoWindow = true,
|
||||
EnableRaisingEvents = true,
|
||||
ErrorDialog = false,
|
||||
FileName = options.RecordingPostProcessor,
|
||||
IsHidden = true,
|
||||
UseShellExecute = false
|
||||
});
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
Arguments = GetPostProcessArguments(path, options.RecordingPostProcessorArguments),
|
||||
CreateNoWindow = true,
|
||||
ErrorDialog = false,
|
||||
FileName = options.RecordingPostProcessor,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
UseShellExecute = false
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
|
||||
_logger.LogInformation("Running recording post processor {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
@ -1712,11 +1712,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
private void Process_Exited(object sender, EventArgs e)
|
||||
{
|
||||
using (var process = (IProcess)sender)
|
||||
using (var process = (Process)sender)
|
||||
{
|
||||
_logger.LogInformation("Recording post-processing script completed with exit code {ExitCode}", process.ExitCode);
|
||||
|
||||
process.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
@ -13,7 +14,6 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Diagnostics;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
@ -29,8 +29,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
private bool _hasExited;
|
||||
private Stream _logFileStream;
|
||||
private string _targetPath;
|
||||
private IProcess _process;
|
||||
private readonly IProcessFactory _processFactory;
|
||||
private Process _process;
|
||||
private readonly IJsonSerializer _json;
|
||||
private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
|
||||
private readonly IServerConfigurationManager _config;
|
||||
@ -40,14 +39,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
IMediaEncoder mediaEncoder,
|
||||
IServerApplicationPaths appPaths,
|
||||
IJsonSerializer json,
|
||||
IProcessFactory processFactory,
|
||||
IServerConfigurationManager config)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_appPaths = appPaths;
|
||||
_json = json;
|
||||
_processFactory = processFactory;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
@ -79,7 +76,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
_targetPath = targetFile;
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
|
||||
|
||||
var process = _processFactory.Create(new ProcessOptions
|
||||
var processStartInfo = new ProcessStartInfo
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
@ -90,14 +87,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
Arguments = GetCommandLineArgs(mediaSource, inputFile, targetFile, duration),
|
||||
|
||||
IsHidden = true,
|
||||
ErrorDialog = false,
|
||||
EnableRaisingEvents = true
|
||||
});
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
};
|
||||
|
||||
_process = process;
|
||||
|
||||
var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
|
||||
var commandLineLogMessage = processStartInfo.FileName + " " + processStartInfo.Arguments;
|
||||
_logger.LogInformation(commandLineLogMessage);
|
||||
|
||||
var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt");
|
||||
@ -109,16 +103,21 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
|
||||
_logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length);
|
||||
|
||||
process.Exited += (sender, args) => OnFfMpegProcessExited(process, inputFile);
|
||||
_process = new Process
|
||||
{
|
||||
StartInfo = processStartInfo,
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
_process.Exited += (sender, args) => OnFfMpegProcessExited(_process, inputFile);
|
||||
|
||||
process.Start();
|
||||
_process.Start();
|
||||
|
||||
cancellationToken.Register(Stop);
|
||||
|
||||
onStarted();
|
||||
|
||||
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
|
||||
StartStreamingLog(process.StandardError.BaseStream, _logFileStream);
|
||||
StartStreamingLog(_process.StandardError.BaseStream, _logFileStream);
|
||||
|
||||
_logger.LogInformation("ffmpeg recording process started for {0}", _targetPath);
|
||||
|
||||
@ -292,30 +291,33 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
/// <summary>
|
||||
/// Processes the exited.
|
||||
/// </summary>
|
||||
private void OnFfMpegProcessExited(IProcess process, string inputFile)
|
||||
private void OnFfMpegProcessExited(Process process, string inputFile)
|
||||
{
|
||||
_hasExited = true;
|
||||
|
||||
_logFileStream?.Dispose();
|
||||
_logFileStream = null;
|
||||
|
||||
var exitCode = process.ExitCode;
|
||||
|
||||
_logger.LogInformation("FFMpeg recording exited with code {ExitCode} for {Path}", exitCode, _targetPath);
|
||||
|
||||
if (exitCode == 0)
|
||||
using (process)
|
||||
{
|
||||
_taskCompletionSource.TrySetResult(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_taskCompletionSource.TrySetException(
|
||||
new Exception(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Recording for {0} failed. Exit code {1}",
|
||||
_targetPath,
|
||||
exitCode)));
|
||||
_hasExited = true;
|
||||
|
||||
_logFileStream?.Dispose();
|
||||
_logFileStream = null;
|
||||
|
||||
var exitCode = process.ExitCode;
|
||||
|
||||
_logger.LogInformation("FFMpeg recording exited with code {ExitCode} for {Path}", exitCode, _targetPath);
|
||||
|
||||
if (exitCode == 0)
|
||||
{
|
||||
_taskCompletionSource.TrySetResult(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_taskCompletionSource.TrySetException(
|
||||
new Exception(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Recording for {0} failed. Exit code {1}",
|
||||
_targetPath,
|
||||
exitCode)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"Albums": "Album",
|
||||
"Albums": "Albums",
|
||||
"AppDeviceValues": "App: {0}, Enhed: {1}",
|
||||
"Application": "Applikation",
|
||||
"Artists": "Kunstnere",
|
||||
@ -106,5 +106,7 @@
|
||||
"TasksChannelsCategory": "Internet Kanaler",
|
||||
"TasksApplicationCategory": "Applikation",
|
||||
"TasksLibraryCategory": "Bibliotek",
|
||||
"TasksMaintenanceCategory": "Vedligeholdelse"
|
||||
"TasksMaintenanceCategory": "Vedligeholdelse",
|
||||
"TaskRefreshChapterImages": "Udtræk Kapitel billeder",
|
||||
"TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler."
|
||||
}
|
||||
|
@ -92,5 +92,27 @@
|
||||
"UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
|
||||
"ValueSpecialEpisodeName": "Special - {0}",
|
||||
"VersionNumber": "Version {0}"
|
||||
"VersionNumber": "Version {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration.",
|
||||
"TaskDownloadMissingSubtitles": "Download missing subtitles",
|
||||
"TaskRefreshChannelsDescription": "Refreshes internet channel information.",
|
||||
"TaskRefreshChannels": "Refresh Channels",
|
||||
"TaskCleanTranscodeDescription": "Deletes transcode files more than one day old.",
|
||||
"TaskCleanTranscode": "Clean Transcode Directory",
|
||||
"TaskUpdatePluginsDescription": "Downloads and installs updates for plugins that are configured to update automatically.",
|
||||
"TaskUpdatePlugins": "Update Plugins",
|
||||
"TaskRefreshPeopleDescription": "Updates metadata for actors and directors in your media library.",
|
||||
"TaskRefreshPeople": "Refresh People",
|
||||
"TaskCleanLogsDescription": "Deletes log files that are more than {0} days old.",
|
||||
"TaskCleanLogs": "Clean Log Directory",
|
||||
"TaskRefreshLibraryDescription": "Scans your media library for new files and refreshes metadata.",
|
||||
"TaskRefreshLibrary": "Scan Media Library",
|
||||
"TaskRefreshChapterImagesDescription": "Creates thumbnails for videos that have chapters.",
|
||||
"TaskRefreshChapterImages": "Extract Chapter Images",
|
||||
"TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.",
|
||||
"TaskCleanCache": "Clean Cache Directory",
|
||||
"TasksChannelsCategory": "Internet Channels",
|
||||
"TasksApplicationCategory": "Application",
|
||||
"TasksLibraryCategory": "Library",
|
||||
"TasksMaintenanceCategory": "Maintenance"
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
"Collections": "Colecciones",
|
||||
"Artists": "Artistas",
|
||||
"DeviceOnlineWithName": "{0} está conectado",
|
||||
"DeviceOfflineWithName": "{0} ha desconectado",
|
||||
"DeviceOfflineWithName": "{0} se ha desconectado",
|
||||
"ChapterNameValue": "Capítulo {0}",
|
||||
"CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
|
||||
"AuthenticationSucceededWithUserName": "{0} autenticado con éxito",
|
||||
|
@ -23,7 +23,7 @@
|
||||
"HeaderFavoriteEpisodes": "قسمتهای مورد علاقه",
|
||||
"HeaderFavoriteShows": "سریالهای مورد علاقه",
|
||||
"HeaderFavoriteSongs": "آهنگهای مورد علاقه",
|
||||
"HeaderLiveTV": "پخش زنده تلویزیون",
|
||||
"HeaderLiveTV": "تلویزیون زنده",
|
||||
"HeaderNextUp": "قسمت بعدی",
|
||||
"HeaderRecordingGroups": "گروههای ضبط",
|
||||
"HomeVideos": "ویدیوهای خانگی",
|
||||
@ -92,5 +92,27 @@
|
||||
"UserStoppedPlayingItemWithValues": "{0} پخش {1} را بر روی {2} به پایان رساند",
|
||||
"ValueHasBeenAddedToLibrary": "{0} به کتابخانهی رسانهی شما افزوده شد",
|
||||
"ValueSpecialEpisodeName": "ویژه - {0}",
|
||||
"VersionNumber": "نسخه {0}"
|
||||
"VersionNumber": "نسخه {0}",
|
||||
"TaskCleanTranscodeDescription": "فایلهای کدگذاری که قدیمیتر از یک روز هستند را حذف میکند.",
|
||||
"TaskCleanTranscode": "پاکسازی مسیر کد گذاری",
|
||||
"TaskUpdatePluginsDescription": "دانلود و نصب به روز رسانی افزونههایی که برای به روز رسانی خودکار پیکربندی شدهاند.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "جستجوی زیرنویسهای ناموجود در اینترنت بر اساس پیکربندی ابردادهها.",
|
||||
"TaskDownloadMissingSubtitles": "دانلود زیرنویسهای ناموجود",
|
||||
"TaskRefreshChannelsDescription": "اطلاعات کانال اینترنتی را تازه سازی میکند.",
|
||||
"TaskRefreshChannels": "تازه سازی کانالها",
|
||||
"TaskUpdatePlugins": "به روز رسانی افزونهها",
|
||||
"TaskRefreshPeopleDescription": "ابردادهها برای بازیگران و کارگردانان در کتابخانه رسانه شما به روزرسانی می شوند.",
|
||||
"TaskRefreshPeople": "تازه سازی افراد",
|
||||
"TaskCleanLogsDescription": "واقعه نگارهایی را که قدیمی تر {0} روز هستند را حذف می کند.",
|
||||
"TaskCleanLogs": "پاکسازی مسیر واقعه نگار",
|
||||
"TaskRefreshLibraryDescription": "کتابخانه رسانه شما را اسکن میکند و ابردادهها را تازه سازی میکند.",
|
||||
"TaskRefreshLibrary": "اسکن کتابخانه رسانه",
|
||||
"TaskRefreshChapterImagesDescription": "عکسهای کوچک برای ویدیوهایی که سکانس دارند ایجاد میکند.",
|
||||
"TaskRefreshChapterImages": "استخراج عکسهای سکانس",
|
||||
"TaskCleanCacheDescription": "فایلهای حافظه موقت که توسط سیستم دیگر مورد نیاز نیستند حذف میشوند.",
|
||||
"TaskCleanCache": "پاکسازی مسیر حافظه موقت",
|
||||
"TasksChannelsCategory": "کانالهای داخلی",
|
||||
"TasksApplicationCategory": "برنامه",
|
||||
"TasksLibraryCategory": "کتابخانه",
|
||||
"TasksMaintenanceCategory": "تعمیر"
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"HeaderLiveTV": "TV-lähetykset",
|
||||
"HeaderLiveTV": "Suorat lähetykset",
|
||||
"NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.",
|
||||
"NameSeasonUnknown": "Tuntematon Kausi",
|
||||
"NameSeasonNumber": "Kausi {0}",
|
||||
@ -19,12 +19,12 @@
|
||||
"ItemAddedWithName": "{0} lisättiin kirjastoon",
|
||||
"Inherit": "Periytyä",
|
||||
"HomeVideos": "Kotivideot",
|
||||
"HeaderRecordingGroups": "Nauhoitusryhmät",
|
||||
"HeaderRecordingGroups": "Nauhoiteryhmät",
|
||||
"HeaderNextUp": "Seuraavaksi",
|
||||
"HeaderFavoriteSongs": "Lempikappaleet",
|
||||
"HeaderFavoriteShows": "Lempisarjat",
|
||||
"HeaderFavoriteEpisodes": "Lempijaksot",
|
||||
"HeaderCameraUploads": "Kameralataukset",
|
||||
"HeaderCameraUploads": "Kamerasta Lähetetyt",
|
||||
"HeaderFavoriteArtists": "Lempiartistit",
|
||||
"HeaderFavoriteAlbums": "Lempialbumit",
|
||||
"HeaderContinueWatching": "Jatka katsomista",
|
||||
@ -63,10 +63,10 @@
|
||||
"UserPasswordChangedWithName": "Salasana vaihdettu käyttäjälle {0}",
|
||||
"UserOnlineFromDevice": "{0} on paikalla osoitteesta {1}",
|
||||
"UserOfflineFromDevice": "{0} yhteys katkaistu {1}",
|
||||
"UserLockedOutWithName": "Käyttäjä {0} kirjautui ulos",
|
||||
"UserDownloadingItemWithValues": "{0} latautumassa {1}",
|
||||
"UserDeletedWithName": "Poistettiin käyttäjä {0}",
|
||||
"UserCreatedWithName": "Luotiin käyttäjä {0}",
|
||||
"UserLockedOutWithName": "Käyttäjä {0} lukittu",
|
||||
"UserDownloadingItemWithValues": "{0} lataa {1}",
|
||||
"UserDeletedWithName": "Käyttäjä {0} poistettu",
|
||||
"UserCreatedWithName": "Käyttäjä {0} luotu",
|
||||
"TvShows": "TV-Ohjelmat",
|
||||
"Sync": "Synkronoi",
|
||||
"SubtitleDownloadFailureFromForItem": "Tekstityksen lataaminen epäonnistui {0} - {1}",
|
||||
@ -74,22 +74,44 @@
|
||||
"Songs": "Kappaleet",
|
||||
"Shows": "Ohjelmat",
|
||||
"ServerNameNeedsToBeRestarted": "{0} vaatii uudelleenkäynnistyksen",
|
||||
"ProviderValue": "Palveluntarjoaja: {0}",
|
||||
"ProviderValue": "Tarjoaja: {0}",
|
||||
"Plugin": "Liitännäinen",
|
||||
"NotificationOptionVideoPlaybackStopped": "Videon toistaminen pysäytetty",
|
||||
"NotificationOptionVideoPlayback": "Videon toistaminen aloitettu",
|
||||
"NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos",
|
||||
"NotificationOptionTaskFailed": "Ajastetun tehtävän ongelma",
|
||||
"NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty",
|
||||
"NotificationOptionVideoPlayback": "Videon toisto aloitettu",
|
||||
"NotificationOptionUserLockedOut": "Käyttäjä lukittu",
|
||||
"NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui",
|
||||
"NotificationOptionServerRestartRequired": "Palvelimen uudelleenkäynnistys vaaditaan",
|
||||
"NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty",
|
||||
"NotificationOptionPluginUpdateInstalled": "Lisäosan päivitys asennettu",
|
||||
"NotificationOptionPluginUninstalled": "Liitännäinen poistettu",
|
||||
"NotificationOptionPluginInstalled": "Liitännäinen asennettu",
|
||||
"NotificationOptionPluginError": "Ongelma liitännäisessä",
|
||||
"NotificationOptionNewLibraryContent": "Uutta sisältöä lisätty",
|
||||
"NotificationOptionInstallationFailed": "Asennus epäonnistui",
|
||||
"NotificationOptionCameraImageUploaded": "Kuva ladattu kamerasta",
|
||||
"NotificationOptionAudioPlaybackStopped": "Audion toisto pysäytetty",
|
||||
"NotificationOptionAudioPlayback": "Audion toisto aloitettu",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Ohjelmistopäivitys asennettu",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Ohjelmistopäivitys saatavilla"
|
||||
"NotificationOptionCameraImageUploaded": "Kameran kuva ladattu",
|
||||
"NotificationOptionAudioPlaybackStopped": "Äänen toisto lopetettu",
|
||||
"NotificationOptionAudioPlayback": "Toistetaan ääntä",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Uusi sovellusversio asennettu",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Sovelluksesta on uusi versio saatavilla",
|
||||
"TasksMaintenanceCategory": "Ylläpito",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Etsii puuttuvia tekstityksiä videon metadatatietojen pohjalta.",
|
||||
"TaskDownloadMissingSubtitles": "Lataa puuttuvat tekstitykset",
|
||||
"TaskRefreshChannelsDescription": "Päivittää internet-kanavien tiedot.",
|
||||
"TaskRefreshChannels": "Päivitä kanavat",
|
||||
"TaskCleanTranscodeDescription": "Poistaa transkoodatut tiedostot jotka ovat yli päivän vanhoja.",
|
||||
"TaskCleanTranscode": "Puhdista transkoodaushakemisto",
|
||||
"TaskUpdatePluginsDescription": "Lataa ja asentaa päivitykset liitännäisille jotka on asetettu päivittymään automaattisesti.",
|
||||
"TaskUpdatePlugins": "Päivitä liitännäiset",
|
||||
"TaskRefreshPeopleDescription": "Päivittää näyttelijöiden ja ohjaajien mediatiedot kirjastossasi.",
|
||||
"TaskRefreshPeople": "Päivitä henkilöt",
|
||||
"TaskCleanLogsDescription": "Poistaa lokitiedostot jotka ovat yli {0} päivää vanhoja.",
|
||||
"TaskCleanLogs": "Puhdista lokihakemisto",
|
||||
"TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uusien tiedostojen varalle, sekä virkistää metatiedot.",
|
||||
"TaskRefreshLibrary": "Skannaa mediakirjasto",
|
||||
"TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on lukuja.",
|
||||
"TaskRefreshChapterImages": "Eristä lukujen kuvat",
|
||||
"TaskCleanCacheDescription": "Poistaa järjestelmälle tarpeettomat väliaikaistiedostot.",
|
||||
"TaskCleanCache": "Tyhjennä välimuisti-hakemisto",
|
||||
"TasksChannelsCategory": "Internet kanavat",
|
||||
"TasksApplicationCategory": "Sovellus",
|
||||
"TasksLibraryCategory": "Kirjasto"
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
"Artists": "Artistes",
|
||||
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
|
||||
"Books": "Livres",
|
||||
"CameraImageUploadedFrom": "Une nouvelle photo a été chargée depuis {0}",
|
||||
"CameraImageUploadedFrom": "Une nouvelle photographie a été chargée depuis {0}",
|
||||
"Channels": "Chaînes",
|
||||
"ChapterNameValue": "Chapitre {0}",
|
||||
"Collections": "Collections",
|
||||
|
@ -71,7 +71,7 @@
|
||||
"ScheduledTaskFailedWithName": "{0} sikertelen",
|
||||
"ScheduledTaskStartedWithName": "{0} elkezdve",
|
||||
"ServerNameNeedsToBeRestarted": "{0}-t újra kell indítani",
|
||||
"Shows": "Műsorok",
|
||||
"Shows": "Sorozatok",
|
||||
"Songs": "Dalok",
|
||||
"StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek, próbáld újra hamarosan.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
|
@ -91,5 +91,9 @@
|
||||
"CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}",
|
||||
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
|
||||
"Application": "Aplicação",
|
||||
"AppDeviceValues": "Aplicação {0}, Dispositivo: {1}"
|
||||
"AppDeviceValues": "Aplicação {0}, Dispositivo: {1}",
|
||||
"TaskCleanCache": "Limpar Diretório de Cache",
|
||||
"TasksApplicationCategory": "Aplicação",
|
||||
"TasksLibraryCategory": "Biblioteca",
|
||||
"TasksMaintenanceCategory": "Manutenção"
|
||||
}
|
||||
|
@ -1 +1,117 @@
|
||||
{}
|
||||
{
|
||||
"HeaderFavoriteAlbums": "پسندیدہ البمز",
|
||||
"HeaderNextUp": "اگلا",
|
||||
"HeaderFavoriteArtists": "پسندیدہ فنکار",
|
||||
"HeaderAlbumArtists": "البم کے فنکار",
|
||||
"Movies": "فلمیں",
|
||||
"HeaderFavoriteEpisodes": "پسندیدہ اقساط",
|
||||
"Collections": "مجموعہ",
|
||||
"Folders": "فولڈرز",
|
||||
"HeaderLiveTV": "براہ راست ٹی وی",
|
||||
"Channels": "چینل",
|
||||
"HeaderContinueWatching": "دیکھنا جاری رکھیں",
|
||||
"Playlists": "پلے لسٹس",
|
||||
"ValueSpecialEpisodeName": "خاص - {0}",
|
||||
"Shows": "شوز",
|
||||
"Genres": "انواع",
|
||||
"Artists": "فنکار",
|
||||
"Sync": "مطابقت",
|
||||
"Photos": "تصوریں",
|
||||
"Albums": "البم",
|
||||
"Favorites": "پسندیدہ",
|
||||
"Songs": "گانے",
|
||||
"Books": "کتابیں",
|
||||
"HeaderFavoriteSongs": "پسندیدہ گانے",
|
||||
"HeaderFavoriteShows": "پسندیدہ شوز",
|
||||
"TaskDownloadMissingSubtitlesDescription": "میٹا ڈیٹا کی تشکیل پر مبنی ذیلی عنوانات کے غائب عنوانات انٹرنیٹ پے تلاش کرتا ہے۔",
|
||||
"TaskDownloadMissingSubtitles": "غائب سب ٹائٹلز ڈاؤن لوڈ کریں",
|
||||
"TaskRefreshChannelsDescription": "انٹرنیٹ چینل کی معلومات کو تازہ دم کرتا ہے۔",
|
||||
"TaskRefreshChannels": "چینلز ریفریش کریں",
|
||||
"TaskCleanTranscodeDescription": "ایک دن سے زیادہ پرانی ٹرانسکوڈ فائلوں کو حذف کرتا ہے۔",
|
||||
"TaskCleanTranscode": "ٹرانس کوڈ ڈائرکٹری صاف کریں",
|
||||
"TaskUpdatePluginsDescription": "پلگ انز کے لئے اپ ڈیٹس ڈاؤن لوڈ اور انسٹال کرتے ہیں جو خود بخود اپ ڈیٹ کرنے کیلئے تشکیل شدہ ہیں۔",
|
||||
"TaskUpdatePlugins": "پلگ انز کو اپ ڈیٹ کریں",
|
||||
"TaskRefreshPeopleDescription": "آپ کی میڈیا لائبریری میں اداکاروں اور ہدایت کاروں کے لئے میٹا ڈیٹا کی تازہ کاری۔",
|
||||
"TaskRefreshPeople": "لوگوں کو تروتازہ کریں",
|
||||
"TaskCleanLogsDescription": "لاگ فائلوں کو حذف کریں جو {0} دن سے زیادہ پرانی ہیں۔",
|
||||
"TaskCleanLogs": "لاگ ڈائرکٹری کو صاف کریں",
|
||||
"TaskRefreshLibraryDescription": "میڈیا لائبریری کو اسکین کرتا ھے ہر میٹا دیٹا کہ تازہ دم کرتا ھے.",
|
||||
"TaskRefreshLibrary": "اسکین میڈیا لائبریری",
|
||||
"TaskRefreshChapterImagesDescription": "بابوں والی ویڈیوز کے لئے تمبنیل بنایں۔",
|
||||
"TaskRefreshChapterImages": "باب کی تصاویر نکالیں",
|
||||
"TaskCleanCacheDescription": "فائلوں کو حذف کریں جنکی ضرورت نھیں ھے۔",
|
||||
"TaskCleanCache": "کیش ڈائرکٹری کلیر کریں",
|
||||
"TasksChannelsCategory": "انٹرنیٹ چینلز",
|
||||
"TasksApplicationCategory": "پروگرام",
|
||||
"TasksLibraryCategory": "لآیبریری",
|
||||
"TasksMaintenanceCategory": "مرمت",
|
||||
"VersionNumber": "ورژن {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} آپ کی میڈیا لائبریری میں شامل کر دیا گیا ہے",
|
||||
"UserStoppedPlayingItemWithValues": "{0} نے {1} چلانا ختم کر دیا ھے {2} پے",
|
||||
"UserStartedPlayingItemWithValues": "{0} چلا رہا ہے {1} {2} پے",
|
||||
"UserPolicyUpdatedWithName": "صارف {0} کی پالیسی کیلئے تازہ کاری کی گئی ہے",
|
||||
"UserPasswordChangedWithName": "صارف {0} کے لئے پاس ورڈ تبدیل کر دیا گیا ہے",
|
||||
"UserOnlineFromDevice": "{0} آن لائن ہے {1} سے",
|
||||
"UserOfflineFromDevice": "{0} سے منقطع ہوگیا ہے {1}",
|
||||
"UserLockedOutWithName": "صارف {0} کو لاک آؤٹ کردیا گیا ہے",
|
||||
"UserDownloadingItemWithValues": "{0} ڈاؤن لوڈ کر رھا ھے {1}",
|
||||
"UserDeletedWithName": "صارف {0} کو ہٹا دیا گیا ہے",
|
||||
"UserCreatedWithName": "صارف {0} تشکیل دیا گیا ہے",
|
||||
"User": "صارف",
|
||||
"TvShows": "ٹی وی کے پروگرام",
|
||||
"System": "نظام",
|
||||
"SubtitleDownloadFailureFromForItem": "ذیلی عنوانات {0} سے ڈاؤن لوڈ کرنے میں ناکام {1} کے لیے",
|
||||
"StartupEmbyServerIsLoading": "جیلیفن سرور لوڈ ہورہا ہے۔ براہ کرم جلد ہی دوبارہ کوشش کریں۔",
|
||||
"ServerNameNeedsToBeRestarted": "{0} دوبارہ چلانے کرنے کی ضرورت ہے",
|
||||
"ScheduledTaskStartedWithName": "{0} شروع",
|
||||
"ScheduledTaskFailedWithName": "{0} ناکام",
|
||||
"ProviderValue": "فراہم کرنے والا: {0}",
|
||||
"PluginUpdatedWithName": "{0} تازہ کاری کی گئی تھی",
|
||||
"PluginUninstalledWithName": "[0} ہٹا دیا گیا تھا",
|
||||
"PluginInstalledWithName": "{0} انسٹال کیا گیا تھا",
|
||||
"Plugin": "پلگن",
|
||||
"NotificationOptionVideoPlaybackStopped": "ویڈیو پلے بیک رک گیا",
|
||||
"NotificationOptionVideoPlayback": "ویڈیو پلے بیک شروع ہوا",
|
||||
"NotificationOptionUserLockedOut": "صارف کو لاک آؤٹ کیا گیا",
|
||||
"NotificationOptionTaskFailed": "طے شدہ کام کی ناکامی",
|
||||
"NotificationOptionServerRestartRequired": "سرور دوبارہ چلانے کرنے کی ضرورت ہے",
|
||||
"NotificationOptionPluginUpdateInstalled": "پلگ ان اپ ڈیٹ انسٹال",
|
||||
"NotificationOptionPluginUninstalled": "پلگ ان ہٹا دیا گیا",
|
||||
"NotificationOptionPluginInstalled": "پلگ ان انسٹال ہوا",
|
||||
"NotificationOptionPluginError": "پلگ ان کی ناکامی",
|
||||
"NotificationOptionNewLibraryContent": "نیا مواد شامل کیا گیا",
|
||||
"NotificationOptionInstallationFailed": "تنصیب کی ناکامی",
|
||||
"NotificationOptionCameraImageUploaded": "کیمرے کی تصویر اپ لوڈ ہوگئی",
|
||||
"NotificationOptionAudioPlaybackStopped": "آڈیو پلے بیک رک گیا",
|
||||
"NotificationOptionAudioPlayback": "آڈیو پلے بیک شروع ہوا",
|
||||
"NotificationOptionApplicationUpdateInstalled": "پروگرام اپ ڈیٹ انسٹال ہوچکا ھے",
|
||||
"NotificationOptionApplicationUpdateAvailable": "پروگرام کی تازہ کاری دستیاب ہے",
|
||||
"NewVersionIsAvailable": "جیلیفن سرور کا ایک نیا ورژن ڈاؤن لوڈ کے لئے دستیاب ہے۔",
|
||||
"NameSeasonUnknown": "نامعلوم باب",
|
||||
"NameSeasonNumber": "باب {0}",
|
||||
"NameInstallFailed": "{0} تنصیب ناکام ہوگئی",
|
||||
"MusicVideos": "موسیقی ویڈیو",
|
||||
"Music": "موسیقی",
|
||||
"MixedContent": "مخلوط مواد",
|
||||
"MessageServerConfigurationUpdated": "سرور کو اپ ڈیٹ کر دیا گیا ہے",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "سرور ضمن {0} کو ترتیب دے دیا گیا ھے",
|
||||
"MessageApplicationUpdatedTo": "جیلیفن سرور کو اپ ڈیٹ کیا ہے {0}",
|
||||
"MessageApplicationUpdated": "جیلیفن سرور کو اپ ڈیٹ کر دیا گیا ہے",
|
||||
"Latest": "تازہ ترین",
|
||||
"LabelRunningTimeValue": "چلانے کی مدت",
|
||||
"LabelIpAddressValue": "ای پی پتے {0}",
|
||||
"ItemRemovedWithName": "لائبریری سے ہٹا دیا گیا ھے",
|
||||
"ItemAddedWithName": "[0} لائبریری میں شامل کیا گیا ھے",
|
||||
"Inherit": "وراثت میں",
|
||||
"HomeVideos": "ہوم ویڈیو",
|
||||
"HeaderRecordingGroups": "ریکارڈنگ گروپس",
|
||||
"HeaderCameraUploads": "کیمرہ اپلوڈز",
|
||||
"FailedLoginAttemptWithUserName": "لاگن کئ کوشش ناکام {0}",
|
||||
"DeviceOnlineWithName": "{0} متصل ھو چکا ھے",
|
||||
"DeviceOfflineWithName": "{0} منقطع ھو چکا ھے",
|
||||
"ChapterNameValue": "باب",
|
||||
"AuthenticationSucceededWithUserName": "{0} کامیابی کے ساتھ تصدیق ھوچکی ھے",
|
||||
"CameraImageUploadedFrom": "ایک نئی کیمرہ تصویر اپ لوڈ کی گئی ہے {0}",
|
||||
"Application": "پروگرام",
|
||||
"AppDeviceValues": "پروگرام:{0}, آلہ:{1}"
|
||||
}
|
||||
|
@ -86,12 +86,9 @@ namespace MediaBrowser.Api
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
if (removeEmpty)
|
||||
{
|
||||
return value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
return value.Split(separator);
|
||||
return removeEmpty
|
||||
? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
|
||||
: value.Split(separator);
|
||||
}
|
||||
|
||||
public SemaphoreSlim GetTranscodingLock(string outputPath)
|
||||
@ -258,7 +255,7 @@ namespace MediaBrowser.Api
|
||||
|
||||
public void ReportTranscodingProgress(TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate)
|
||||
{
|
||||
var ticks = transcodingPosition.HasValue ? transcodingPosition.Value.Ticks : (long?)null;
|
||||
var ticks = transcodingPosition?.Ticks;
|
||||
|
||||
if (job != null)
|
||||
{
|
||||
@ -487,16 +484,9 @@ namespace MediaBrowser.Api
|
||||
/// <returns>Task.</returns>
|
||||
internal Task KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles)
|
||||
{
|
||||
return KillTranscodingJobs(j =>
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(playSessionId))
|
||||
{
|
||||
return string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
}, deleteFiles);
|
||||
return KillTranscodingJobs(j => string.IsNullOrWhiteSpace(playSessionId)
|
||||
? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase)
|
||||
: string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase), deleteFiles);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -561,10 +551,7 @@ namespace MediaBrowser.Api
|
||||
|
||||
lock (job.ProcessLock)
|
||||
{
|
||||
if (job.TranscodingThrottler != null)
|
||||
{
|
||||
job.TranscodingThrottler.Stop().GetAwaiter().GetResult();
|
||||
}
|
||||
job.TranscodingThrottler?.Stop().GetAwaiter().GetResult();
|
||||
|
||||
var process = job.Process;
|
||||
|
||||
|
@ -58,12 +58,9 @@ namespace MediaBrowser.Api
|
||||
|
||||
public static string[] SplitValue(string value, char delim)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
return value.Split(new[] { delim }, StringSplitOptions.RemoveEmptyEntries);
|
||||
return value == null
|
||||
? Array.Empty<string>()
|
||||
: value.Split(new[] { delim }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
public static Guid[] GetGuids(string value)
|
||||
@ -97,19 +94,10 @@ namespace MediaBrowser.Api
|
||||
var authenticatedUser = auth.User;
|
||||
|
||||
// If they're going to update the record of another user, they must be an administrator
|
||||
if (!userId.Equals(auth.UserId))
|
||||
if ((!userId.Equals(auth.UserId) && !authenticatedUser.Policy.IsAdministrator)
|
||||
|| (restrictUserPreferences && !authenticatedUser.Policy.EnableUserPreferenceAccess))
|
||||
{
|
||||
if (!authenticatedUser.Policy.IsAdministrator)
|
||||
{
|
||||
throw new SecurityException("Unauthorized access.");
|
||||
}
|
||||
}
|
||||
else if (restrictUserPreferences)
|
||||
{
|
||||
if (!authenticatedUser.Policy.EnableUserPreferenceAccess)
|
||||
{
|
||||
throw new SecurityException("Unauthorized access.");
|
||||
}
|
||||
throw new SecurityException("Unauthorized access.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -138,8 +126,8 @@ namespace MediaBrowser.Api
|
||||
options.Fields = hasFields.GetItemFields();
|
||||
}
|
||||
|
||||
if (!options.ContainsField(Model.Querying.ItemFields.RecursiveItemCount)
|
||||
|| !options.ContainsField(Model.Querying.ItemFields.ChildCount))
|
||||
if (!options.ContainsField(ItemFields.RecursiveItemCount)
|
||||
|| !options.ContainsField(ItemFields.ChildCount))
|
||||
{
|
||||
var client = authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
|
||||
if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 ||
|
||||
@ -150,7 +138,7 @@ namespace MediaBrowser.Api
|
||||
int oldLen = options.Fields.Length;
|
||||
var arr = new ItemFields[oldLen + 1];
|
||||
options.Fields.CopyTo(arr, 0);
|
||||
arr[oldLen] = Model.Querying.ItemFields.RecursiveItemCount;
|
||||
arr[oldLen] = ItemFields.RecursiveItemCount;
|
||||
options.Fields = arr;
|
||||
}
|
||||
|
||||
@ -166,7 +154,7 @@ namespace MediaBrowser.Api
|
||||
int oldLen = options.Fields.Length;
|
||||
var arr = new ItemFields[oldLen + 1];
|
||||
options.Fields.CopyTo(arr, 0);
|
||||
arr[oldLen] = Model.Querying.ItemFields.ChildCount;
|
||||
arr[oldLen] = ItemFields.ChildCount;
|
||||
options.Fields = arr;
|
||||
}
|
||||
}
|
||||
@ -282,27 +270,21 @@ namespace MediaBrowser.Api
|
||||
|
||||
}).OfType<T>().FirstOrDefault();
|
||||
|
||||
if (result == null)
|
||||
result ??= libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
result = libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
Name = name.Replace(BaseItem.SlugChar, '/'),
|
||||
IncludeItemTypes = new[] { typeof(T).Name },
|
||||
DtoOptions = dtoOptions
|
||||
Name = name.Replace(BaseItem.SlugChar, '/'),
|
||||
IncludeItemTypes = new[] { typeof(T).Name },
|
||||
DtoOptions = dtoOptions
|
||||
|
||||
}).OfType<T>().FirstOrDefault();
|
||||
}
|
||||
}).OfType<T>().FirstOrDefault();
|
||||
|
||||
if (result == null)
|
||||
result ??= libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
result = libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
Name = name.Replace(BaseItem.SlugChar, '?'),
|
||||
IncludeItemTypes = new[] { typeof(T).Name },
|
||||
DtoOptions = dtoOptions
|
||||
Name = name.Replace(BaseItem.SlugChar, '?'),
|
||||
IncludeItemTypes = new[] { typeof(T).Name },
|
||||
DtoOptions = dtoOptions
|
||||
|
||||
}).OfType<T>().FirstOrDefault();
|
||||
}
|
||||
}).OfType<T>().FirstOrDefault();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -116,12 +116,9 @@ namespace MediaBrowser.Api
|
||||
{
|
||||
var val = Filters;
|
||||
|
||||
if (string.IsNullOrEmpty(val))
|
||||
{
|
||||
return new ItemFilter[] { };
|
||||
}
|
||||
|
||||
return val.Split(',').Select(v => (ItemFilter)Enum.Parse(typeof(ItemFilter), v, true));
|
||||
return string.IsNullOrEmpty(val)
|
||||
? Array.Empty<ItemFilter>()
|
||||
: val.Split(',').Select(v => Enum.Parse<ItemFilter>(v, true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -173,14 +170,9 @@ namespace MediaBrowser.Api
|
||||
/// <returns>IEnumerable{ItemFilter}.</returns>
|
||||
public IEnumerable<ItemFilter> GetFilters()
|
||||
{
|
||||
var val = Filters;
|
||||
|
||||
if (string.IsNullOrEmpty(val))
|
||||
{
|
||||
return new ItemFilter[] { };
|
||||
}
|
||||
|
||||
return val.Split(',').Select(v => (ItemFilter)Enum.Parse(typeof(ItemFilter), v, true));
|
||||
return string.IsNullOrEmpty(Filters)
|
||||
? Array.Empty<ItemFilter>()
|
||||
: Filters.Split(',').Select(v => Enum.Parse<ItemFilter>(v, true));
|
||||
}
|
||||
}
|
||||
|
||||
@ -241,7 +233,7 @@ namespace MediaBrowser.Api
|
||||
{
|
||||
Limit = request.Limit,
|
||||
StartIndex = request.StartIndex,
|
||||
ChannelIds = new Guid[] { new Guid(request.Id) },
|
||||
ChannelIds = new[] { new Guid(request.Id) },
|
||||
ParentId = string.IsNullOrWhiteSpace(request.FolderId) ? Guid.Empty : new Guid(request.FolderId),
|
||||
OrderBy = request.GetOrderBy(),
|
||||
DtoOptions = new Controller.Dto.DtoOptions
|
||||
|
@ -155,16 +155,14 @@ namespace MediaBrowser.Api.Devices
|
||||
Id = id
|
||||
});
|
||||
}
|
||||
else
|
||||
|
||||
return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo
|
||||
{
|
||||
return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo
|
||||
{
|
||||
MimeType = Request.ContentType,
|
||||
Album = album,
|
||||
Name = name,
|
||||
Id = id
|
||||
});
|
||||
}
|
||||
MimeType = Request.ContentType,
|
||||
Album = album,
|
||||
Name = name,
|
||||
Id = id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -258,12 +258,7 @@ namespace MediaBrowser.Api
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!request.IncludeDirectories && isDirectory)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return request.IncludeDirectories || !isDirectory;
|
||||
});
|
||||
|
||||
return entries.Select(f => new FileSystemEntryInfo
|
||||
|
@ -133,7 +133,7 @@ namespace MediaBrowser.Api
|
||||
// Non recursive not yet supported for library folders
|
||||
if ((request.Recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder)
|
||||
{
|
||||
genreQuery.AncestorIds = parentItem == null ? Array.Empty<Guid>() : new Guid[] { parentItem.Id };
|
||||
genreQuery.AncestorIds = parentItem == null ? Array.Empty<Guid>() : new[] { parentItem.Id };
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -231,7 +231,7 @@ namespace MediaBrowser.Api
|
||||
EnableTotalRecordCount = false,
|
||||
DtoOptions = new Controller.Dto.DtoOptions
|
||||
{
|
||||
Fields = new ItemFields[] { ItemFields.Genres, ItemFields.Tags },
|
||||
Fields = new[] { ItemFields.Genres, ItemFields.Tags },
|
||||
EnableImages = false,
|
||||
EnableUserData = false
|
||||
}
|
||||
|
@ -650,7 +650,7 @@ namespace MediaBrowser.Api.Images
|
||||
if (!string.IsNullOrWhiteSpace(request.Format)
|
||||
&& Enum.TryParse(request.Format, true, out ImageFormat format))
|
||||
{
|
||||
return new ImageFormat[] { format };
|
||||
return new[] { format };
|
||||
}
|
||||
|
||||
return GetClientSupportedFormats();
|
||||
@ -743,24 +743,22 @@ namespace MediaBrowser.Api.Images
|
||||
/// <returns>Task.</returns>
|
||||
public async Task PostImage(BaseItem entity, Stream inputStream, ImageType imageType, string mimeType)
|
||||
{
|
||||
using (var reader = new StreamReader(inputStream))
|
||||
using var reader = new StreamReader(inputStream);
|
||||
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
var bytes = Convert.FromBase64String(text);
|
||||
|
||||
var memoryStream = new MemoryStream(bytes)
|
||||
{
|
||||
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
Position = 0
|
||||
};
|
||||
|
||||
var bytes = Convert.FromBase64String(text);
|
||||
// Handle image/png; charset=utf-8
|
||||
mimeType = mimeType.Split(';').FirstOrDefault();
|
||||
|
||||
var memoryStream = new MemoryStream(bytes)
|
||||
{
|
||||
Position = 0
|
||||
};
|
||||
await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
// Handle image/png; charset=utf-8
|
||||
mimeType = mimeType.Split(';').FirstOrDefault();
|
||||
|
||||
await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
entity.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
|
||||
}
|
||||
entity.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -261,27 +261,25 @@ namespace MediaBrowser.Api.Images
|
||||
/// <returns>Task.</returns>
|
||||
private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath)
|
||||
{
|
||||
using (var result = await _httpClient.GetResponse(new HttpRequestOptions
|
||||
using var result = await _httpClient.GetResponse(new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
BufferContent = false
|
||||
|
||||
}).ConfigureAwait(false))
|
||||
}).ConfigureAwait(false);
|
||||
var ext = result.ContentType.Split('/').Last();
|
||||
|
||||
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
|
||||
using (var stream = result.Content)
|
||||
{
|
||||
var ext = result.ContentType.Split('/').Last();
|
||||
|
||||
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
|
||||
using (var stream = result.Content)
|
||||
using (var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true))
|
||||
{
|
||||
await stream.CopyToAsync(filestream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
|
||||
File.WriteAllText(pointerCachePath, fullCachePath);
|
||||
using var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
|
||||
await stream.CopyToAsync(filestream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
|
||||
File.WriteAllText(pointerCachePath, fullCachePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -305,9 +305,16 @@ namespace MediaBrowser.Api
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
|
||||
using (var stream = result.Content)
|
||||
using (var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true))
|
||||
{
|
||||
await stream.CopyToAsync(filestream).ConfigureAwait(false);
|
||||
using var fileStream = new FileStream(
|
||||
fullCachePath,
|
||||
FileMode.Create,
|
||||
FileAccess.Write,
|
||||
FileShare.Read,
|
||||
IODefaults.FileStreamBufferSize,
|
||||
true);
|
||||
|
||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
|
||||
|
@ -263,8 +263,7 @@ namespace MediaBrowser.Api
|
||||
item.Overview = request.Overview;
|
||||
item.Genres = request.Genres;
|
||||
|
||||
var episode = item as Episode;
|
||||
if (episode != null)
|
||||
if (item is Episode episode)
|
||||
{
|
||||
episode.AirsAfterSeasonNumber = request.AirsAfterSeasonNumber;
|
||||
episode.AirsBeforeEpisodeNumber = request.AirsBeforeEpisodeNumber;
|
||||
@ -302,14 +301,12 @@ namespace MediaBrowser.Api
|
||||
item.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode;
|
||||
item.PreferredMetadataLanguage = request.PreferredMetadataLanguage;
|
||||
|
||||
var hasDisplayOrder = item as IHasDisplayOrder;
|
||||
if (hasDisplayOrder != null)
|
||||
if (item is IHasDisplayOrder hasDisplayOrder)
|
||||
{
|
||||
hasDisplayOrder.DisplayOrder = request.DisplayOrder;
|
||||
}
|
||||
|
||||
var hasAspectRatio = item as IHasAspectRatio;
|
||||
if (hasAspectRatio != null)
|
||||
if (item is IHasAspectRatio hasAspectRatio)
|
||||
{
|
||||
hasAspectRatio.AspectRatio = request.AspectRatio;
|
||||
}
|
||||
@ -337,16 +334,14 @@ namespace MediaBrowser.Api
|
||||
|
||||
item.ProviderIds = request.ProviderIds;
|
||||
|
||||
var video = item as Video;
|
||||
if (video != null)
|
||||
if (item is Video video)
|
||||
{
|
||||
video.Video3DFormat = request.Video3DFormat;
|
||||
}
|
||||
|
||||
if (request.AlbumArtists != null)
|
||||
{
|
||||
var hasAlbumArtists = item as IHasAlbumArtist;
|
||||
if (hasAlbumArtists != null)
|
||||
if (item is IHasAlbumArtist hasAlbumArtists)
|
||||
{
|
||||
hasAlbumArtists.AlbumArtists = request
|
||||
.AlbumArtists
|
||||
@ -357,8 +352,7 @@ namespace MediaBrowser.Api
|
||||
|
||||
if (request.ArtistItems != null)
|
||||
{
|
||||
var hasArtists = item as IHasArtist;
|
||||
if (hasArtists != null)
|
||||
if (item is IHasArtist hasArtists)
|
||||
{
|
||||
hasArtists.Artists = request
|
||||
.ArtistItems
|
||||
@ -367,20 +361,17 @@ namespace MediaBrowser.Api
|
||||
}
|
||||
}
|
||||
|
||||
var song = item as Audio;
|
||||
if (song != null)
|
||||
if (item is Audio song)
|
||||
{
|
||||
song.Album = request.Album;
|
||||
}
|
||||
|
||||
var musicVideo = item as MusicVideo;
|
||||
if (musicVideo != null)
|
||||
if (item is MusicVideo musicVideo)
|
||||
{
|
||||
musicVideo.Album = request.Album;
|
||||
}
|
||||
|
||||
var series = item as Series;
|
||||
if (series != null)
|
||||
if (item is Series series)
|
||||
{
|
||||
series.Status = GetSeriesStatus(request);
|
||||
|
||||
@ -400,7 +391,6 @@ namespace MediaBrowser.Api
|
||||
}
|
||||
|
||||
return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -348,28 +348,19 @@ namespace MediaBrowser.Api.Library
|
||||
|
||||
private string[] GetRepresentativeItemTypes(string contentType)
|
||||
{
|
||||
switch (contentType)
|
||||
return contentType switch
|
||||
{
|
||||
case CollectionType.BoxSets:
|
||||
return new string[] { "BoxSet" };
|
||||
case CollectionType.Playlists:
|
||||
return new string[] { "Playlist" };
|
||||
case CollectionType.Movies:
|
||||
return new string[] { "Movie" };
|
||||
case CollectionType.TvShows:
|
||||
return new string[] { "Series", "Season", "Episode" };
|
||||
case CollectionType.Books:
|
||||
return new string[] { "Book" };
|
||||
case CollectionType.Music:
|
||||
return new string[] { "MusicAlbum", "MusicArtist", "Audio", "MusicVideo" };
|
||||
case CollectionType.HomeVideos:
|
||||
case CollectionType.Photos:
|
||||
return new string[] { "Video", "Photo" };
|
||||
case CollectionType.MusicVideos:
|
||||
return new string[] { "MusicVideo" };
|
||||
default:
|
||||
return new string[] { "Series", "Season", "Episode", "Movie" };
|
||||
}
|
||||
CollectionType.BoxSets => new[] {"BoxSet"},
|
||||
CollectionType.Playlists => new[] {"Playlist"},
|
||||
CollectionType.Movies => new[] {"Movie"},
|
||||
CollectionType.TvShows => new[] {"Series", "Season", "Episode"},
|
||||
CollectionType.Books => new[] {"Book"},
|
||||
CollectionType.Music => new[] {"MusicAlbum", "MusicArtist", "Audio", "MusicVideo"},
|
||||
CollectionType.HomeVideos => new[] {"Video", "Photo"},
|
||||
CollectionType.Photos => new[] {"Video", "Photo"},
|
||||
CollectionType.MusicVideos => new[] {"MusicVideo"},
|
||||
_ => new[] {"Series", "Season", "Episode", "Movie"}
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary)
|
||||
@ -397,54 +388,22 @@ namespace MediaBrowser.Api.Library
|
||||
{
|
||||
if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if (string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (string.Equals(name, "The Open Movie Database", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
return false;
|
||||
return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions
|
||||
.Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
|
||||
if (metadataOptions.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase));
|
||||
return metadataOptions.Length == 0
|
||||
|| metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
|
||||
@ -453,50 +412,17 @@ namespace MediaBrowser.Api.Library
|
||||
{
|
||||
if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if (string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (string.Equals(name, "The Open Movie Database", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (string.Equals(name, "Emby Designs", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return false;
|
||||
return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(name, "Emby Designs", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions
|
||||
@ -561,8 +487,7 @@ namespace MediaBrowser.Api.Library
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
ImageOption[] defaultImageOptions = null;
|
||||
TypeOptions.DefaultImageOptions.TryGetValue(type, out defaultImageOptions);
|
||||
TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions);
|
||||
|
||||
typeOptions.Add(new LibraryTypeOptions
|
||||
{
|
||||
@ -609,8 +534,6 @@ namespace MediaBrowser.Api.Library
|
||||
|
||||
public object Get(GetSimilarItems request)
|
||||
{
|
||||
var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null;
|
||||
|
||||
var item = string.IsNullOrEmpty(request.Id) ?
|
||||
(!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() :
|
||||
_libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id);
|
||||
@ -668,7 +591,7 @@ namespace MediaBrowser.Api.Library
|
||||
// ExcludeArtistIds
|
||||
if (!string.IsNullOrEmpty(request.ExcludeArtistIds))
|
||||
{
|
||||
query.ExcludeArtistIds = BaseApiService.GetGuids(request.ExcludeArtistIds);
|
||||
query.ExcludeArtistIds = GetGuids(request.ExcludeArtistIds);
|
||||
}
|
||||
|
||||
List<BaseItem> itemsResult;
|
||||
@ -689,7 +612,6 @@ namespace MediaBrowser.Api.Library
|
||||
var result = new QueryResult<BaseItemDto>
|
||||
{
|
||||
Items = returnList,
|
||||
|
||||
TotalRecordCount = itemsResult.Count
|
||||
};
|
||||
|
||||
@ -919,12 +841,10 @@ namespace MediaBrowser.Api.Library
|
||||
|
||||
private BaseItem TranslateParentItem(BaseItem item, User user)
|
||||
{
|
||||
if (item.GetParent() is AggregateFolder)
|
||||
{
|
||||
return _libraryManager.GetUserRootFolder().GetChildren(user, true).FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path));
|
||||
}
|
||||
|
||||
return item;
|
||||
return item.GetParent() is AggregateFolder
|
||||
? _libraryManager.GetUserRootFolder().GetChildren(user, true)
|
||||
.FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path))
|
||||
: item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -1086,7 +1006,7 @@ namespace MediaBrowser.Api.Library
|
||||
var item = string.IsNullOrEmpty(request.Id)
|
||||
? (!request.UserId.Equals(Guid.Empty)
|
||||
? _libraryManager.GetUserRootFolder()
|
||||
: (Folder)_libraryManager.RootFolder)
|
||||
: _libraryManager.RootFolder)
|
||||
: _libraryManager.GetItemById(request.Id);
|
||||
|
||||
if (item == null)
|
||||
@ -1094,18 +1014,13 @@ namespace MediaBrowser.Api.Library
|
||||
throw new ResourceNotFoundException("Item not found.");
|
||||
}
|
||||
|
||||
BaseItem[] themeItems = Array.Empty<BaseItem>();
|
||||
IEnumerable<BaseItem> themeItems;
|
||||
|
||||
while (true)
|
||||
{
|
||||
themeItems = item.GetThemeSongs().ToArray();
|
||||
themeItems = item.GetThemeSongs();
|
||||
|
||||
if (themeItems.Length > 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!request.InheritFromParent)
|
||||
if (themeItems.Any() || !request.InheritFromParent)
|
||||
{
|
||||
break;
|
||||
}
|
||||
@ -1119,11 +1034,9 @@ namespace MediaBrowser.Api.Library
|
||||
}
|
||||
|
||||
var dtoOptions = GetDtoOptions(_authContext, request);
|
||||
|
||||
var dtos = themeItems
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item));
|
||||
|
||||
var items = dtos.ToArray();
|
||||
var items = themeItems
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
|
||||
.ToArray();
|
||||
|
||||
return new ThemeMediaResult
|
||||
{
|
||||
@ -1140,9 +1053,7 @@ namespace MediaBrowser.Api.Library
|
||||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetThemeVideos request)
|
||||
{
|
||||
var result = GetThemeVideos(request);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
return ToOptimizedResult(GetThemeVideos(request));
|
||||
}
|
||||
|
||||
public ThemeMediaResult GetThemeVideos(GetThemeVideos request)
|
||||
@ -1152,7 +1063,7 @@ namespace MediaBrowser.Api.Library
|
||||
var item = string.IsNullOrEmpty(request.Id)
|
||||
? (!request.UserId.Equals(Guid.Empty)
|
||||
? _libraryManager.GetUserRootFolder()
|
||||
: (Folder)_libraryManager.RootFolder)
|
||||
: _libraryManager.RootFolder)
|
||||
: _libraryManager.GetItemById(request.Id);
|
||||
|
||||
if (item == null)
|
||||
@ -1160,18 +1071,13 @@ namespace MediaBrowser.Api.Library
|
||||
throw new ResourceNotFoundException("Item not found.");
|
||||
}
|
||||
|
||||
BaseItem[] themeItems = Array.Empty<BaseItem>();
|
||||
IEnumerable<BaseItem> themeItems;
|
||||
|
||||
while (true)
|
||||
{
|
||||
themeItems = item.GetThemeVideos().ToArray();
|
||||
themeItems = item.GetThemeVideos();
|
||||
|
||||
if (themeItems.Length > 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!request.InheritFromParent)
|
||||
if (themeItems.Any() || !request.InheritFromParent)
|
||||
{
|
||||
break;
|
||||
}
|
||||
@ -1186,10 +1092,9 @@ namespace MediaBrowser.Api.Library
|
||||
|
||||
var dtoOptions = GetDtoOptions(_authContext, request);
|
||||
|
||||
var dtos = themeItems
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item));
|
||||
|
||||
var items = dtos.ToArray();
|
||||
var items = themeItems
|
||||
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
|
||||
.ToArray();
|
||||
|
||||
return new ThemeMediaResult
|
||||
{
|
||||
|
@ -327,15 +327,11 @@ namespace MediaBrowser.Api.Library
|
||||
|
||||
try
|
||||
{
|
||||
var mediaPath = request.PathInfo;
|
||||
|
||||
if (mediaPath == null)
|
||||
var mediaPath = request.PathInfo ?? new MediaPathInfo
|
||||
{
|
||||
mediaPath = new MediaPathInfo
|
||||
{
|
||||
Path = request.Path
|
||||
};
|
||||
}
|
||||
Path = request.Path
|
||||
};
|
||||
|
||||
_libraryManager.AddMediaPath(request.Name, mediaPath);
|
||||
}
|
||||
finally
|
||||
|
@ -885,11 +885,10 @@ namespace MediaBrowser.Api.LiveTv
|
||||
{
|
||||
// SchedulesDirect requires a SHA1 hash of the user's password
|
||||
// https://github.com/SchedulesDirect/JSON-Service/wiki/API-20141201#obtain-a-token
|
||||
using (SHA1 sha = SHA1.Create())
|
||||
{
|
||||
return Hex.Encode(
|
||||
sha.ComputeHash(Encoding.UTF8.GetBytes(str)));
|
||||
}
|
||||
using SHA1 sha = SHA1.Create();
|
||||
|
||||
return Hex.Encode(
|
||||
sha.ComputeHash(Encoding.UTF8.GetBytes(str)));
|
||||
}
|
||||
|
||||
public void Delete(DeleteListingProvider request)
|
||||
@ -1050,8 +1049,7 @@ namespace MediaBrowser.Api.LiveTv
|
||||
{
|
||||
query.IsSeries = true;
|
||||
|
||||
var series = _libraryManager.GetItemById(request.LibrarySeriesId) as Series;
|
||||
if (series != null)
|
||||
if (_libraryManager.GetItemById(request.LibrarySeriesId) is Series series)
|
||||
{
|
||||
query.Name = series.Name;
|
||||
}
|
||||
|
@ -394,7 +394,7 @@ namespace MediaBrowser.Api.Movies
|
||||
{
|
||||
var people = _libraryManager.GetPeople(new InternalPeopleQuery
|
||||
{
|
||||
PersonTypes = new string[]
|
||||
PersonTypes = new[]
|
||||
{
|
||||
PersonType.Director
|
||||
}
|
||||
|
@ -137,12 +137,9 @@ namespace MediaBrowser.Api.Playback
|
||||
var ext = outputFileExtension.ToLowerInvariant();
|
||||
var folder = ServerConfigurationManager.GetTranscodePath();
|
||||
|
||||
if (EnableOutputInSubFolder)
|
||||
{
|
||||
return Path.Combine(folder, filename, filename + ext);
|
||||
}
|
||||
|
||||
return Path.Combine(folder, filename + ext);
|
||||
return EnableOutputInSubFolder
|
||||
? Path.Combine(folder, filename, filename + ext)
|
||||
: Path.Combine(folder, filename + ext);
|
||||
}
|
||||
|
||||
protected virtual string GetDefaultEncoderPreset()
|
||||
@ -248,14 +245,8 @@ namespace MediaBrowser.Api.Playback
|
||||
if (state.VideoRequest != null
|
||||
&& string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logFilePrefix = "ffmpeg-remux";
|
||||
}
|
||||
else
|
||||
{
|
||||
logFilePrefix = "ffmpeg-directstream";
|
||||
}
|
||||
logFilePrefix = string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)
|
||||
? "ffmpeg-remux" : "ffmpeg-directstream";
|
||||
}
|
||||
|
||||
var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt");
|
||||
@ -389,195 +380,181 @@ namespace MediaBrowser.Api.Playback
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i == 0)
|
||||
switch (i)
|
||||
{
|
||||
request.DeviceProfileId = val;
|
||||
}
|
||||
else if (i == 1)
|
||||
{
|
||||
request.DeviceId = val;
|
||||
}
|
||||
else if (i == 2)
|
||||
{
|
||||
request.MediaSourceId = val;
|
||||
}
|
||||
else if (i == 3)
|
||||
{
|
||||
request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else if (i == 4)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.VideoCodec = val;
|
||||
}
|
||||
}
|
||||
else if (i == 5)
|
||||
{
|
||||
request.AudioCodec = val;
|
||||
}
|
||||
else if (i == 6)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
else if (i == 7)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
else if (i == 8)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
else if (i == 9)
|
||||
{
|
||||
request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
else if (i == 10)
|
||||
{
|
||||
request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
else if (i == 11)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
else if (i == 12)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
else if (i == 13)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
else if (i == 14)
|
||||
{
|
||||
request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
else if (i == 15)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.Level = val;
|
||||
}
|
||||
}
|
||||
else if (i == 16)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
else if (i == 17)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
else if (i == 18)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.Profile = val;
|
||||
}
|
||||
}
|
||||
else if (i == 19)
|
||||
{
|
||||
// cabac no longer used
|
||||
}
|
||||
else if (i == 20)
|
||||
{
|
||||
request.PlaySessionId = val;
|
||||
}
|
||||
else if (i == 21)
|
||||
{
|
||||
// api_key
|
||||
}
|
||||
else if (i == 22)
|
||||
{
|
||||
request.LiveStreamId = val;
|
||||
}
|
||||
else if (i == 23)
|
||||
{
|
||||
// Duplicating ItemId because of MediaMonkey
|
||||
}
|
||||
else if (i == 24)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
else if (i == 25)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(val) && videoRequest != null)
|
||||
{
|
||||
if (Enum.TryParse(val, out SubtitleDeliveryMethod method))
|
||||
case 0:
|
||||
request.DeviceProfileId = val;
|
||||
break;
|
||||
case 1:
|
||||
request.DeviceId = val;
|
||||
break;
|
||||
case 2:
|
||||
request.MediaSourceId = val;
|
||||
break;
|
||||
case 3:
|
||||
request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
break;
|
||||
case 4:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.SubtitleMethod = method;
|
||||
videoRequest.VideoCodec = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (i == 26)
|
||||
{
|
||||
request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
else if (i == 27)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
else if (i == 28)
|
||||
{
|
||||
request.Tag = val;
|
||||
}
|
||||
else if (i == 29)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
else if (i == 30)
|
||||
{
|
||||
request.SubtitleCodec = val;
|
||||
}
|
||||
else if (i == 31)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
else if (i == 32)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
else if (i == 33)
|
||||
{
|
||||
request.TranscodeReasons = val;
|
||||
|
||||
break;
|
||||
case 5:
|
||||
request.AudioCodec = val;
|
||||
break;
|
||||
case 6:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
break;
|
||||
case 7:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
break;
|
||||
case 8:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
break;
|
||||
case 9:
|
||||
request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case 10:
|
||||
request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case 11:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
break;
|
||||
case 12:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
break;
|
||||
case 13:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
break;
|
||||
case 14:
|
||||
request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case 15:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.Level = val;
|
||||
}
|
||||
|
||||
break;
|
||||
case 16:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
break;
|
||||
case 17:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
break;
|
||||
case 18:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.Profile = val;
|
||||
}
|
||||
|
||||
break;
|
||||
case 19:
|
||||
// cabac no longer used
|
||||
break;
|
||||
case 20:
|
||||
request.PlaySessionId = val;
|
||||
break;
|
||||
case 21:
|
||||
// api_key
|
||||
break;
|
||||
case 22:
|
||||
request.LiveStreamId = val;
|
||||
break;
|
||||
case 23:
|
||||
// Duplicating ItemId because of MediaMonkey
|
||||
break;
|
||||
case 24:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
break;
|
||||
case 25:
|
||||
if (!string.IsNullOrWhiteSpace(val) && videoRequest != null)
|
||||
{
|
||||
if (Enum.TryParse(val, out SubtitleDeliveryMethod method))
|
||||
{
|
||||
videoRequest.SubtitleMethod = method;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case 26:
|
||||
request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case 27:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
break;
|
||||
case 28:
|
||||
request.Tag = val;
|
||||
break;
|
||||
case 29:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
break;
|
||||
case 30:
|
||||
request.SubtitleCodec = val;
|
||||
break;
|
||||
case 31:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
break;
|
||||
case 32:
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
break;
|
||||
case 33:
|
||||
request.TranscodeReasons = val;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -630,14 +607,9 @@ namespace MediaBrowser.Api.Playback
|
||||
throw new ArgumentException("Invalid timeseek header");
|
||||
}
|
||||
int index = value.IndexOf('-');
|
||||
if (index == -1)
|
||||
{
|
||||
value = value.Substring(Npt.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = value.Substring(Npt.Length, index - Npt.Length);
|
||||
}
|
||||
value = index == -1
|
||||
? value.Substring(Npt.Length)
|
||||
: value.Substring(Npt.Length, index - Npt.Length);
|
||||
|
||||
if (value.IndexOf(':') == -1)
|
||||
{
|
||||
@ -856,21 +828,11 @@ namespace MediaBrowser.Api.Playback
|
||||
{
|
||||
state.DeviceProfile = DlnaManager.GetProfile(state.Request.DeviceProfileId);
|
||||
}
|
||||
else
|
||||
else if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
|
||||
{
|
||||
var caps = DeviceManager.GetCapabilities(state.Request.DeviceId);
|
||||
var caps = DeviceManager.GetCapabilities(state.Request.DeviceId);
|
||||
|
||||
if (caps != null)
|
||||
{
|
||||
state.DeviceProfile = caps.DeviceProfile;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.DeviceProfile = DlnaManager.GetProfile(headers);
|
||||
}
|
||||
}
|
||||
state.DeviceProfile = caps == null ? DlnaManager.GetProfile(headers) : caps.DeviceProfile;
|
||||
}
|
||||
|
||||
var profile = state.DeviceProfile;
|
||||
|
@ -140,7 +140,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
|
||||
if (isLive)
|
||||
{
|
||||
job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
|
||||
job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
|
||||
|
||||
if (job != null)
|
||||
{
|
||||
@ -156,7 +156,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
|
||||
var playlistText = GetMasterPlaylistFileText(playlist, videoBitrate + audioBitrate, baselineStreamBitrate);
|
||||
|
||||
job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
|
||||
job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
|
||||
|
||||
if (job != null)
|
||||
{
|
||||
@ -168,22 +168,19 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
|
||||
private string GetLivePlaylistText(string path, int segmentLength)
|
||||
{
|
||||
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
{
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var text = reader.ReadToEnd();
|
||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT");
|
||||
var text = reader.ReadToEnd();
|
||||
|
||||
var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture);
|
||||
text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT");
|
||||
|
||||
text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
|
||||
//text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
|
||||
var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
return text;
|
||||
}
|
||||
}
|
||||
text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
|
||||
//text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
private string GetMasterPlaylistFileText(string firstPlaylist, int bitrate, int baselineStreamBitrate)
|
||||
@ -212,29 +209,25 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
try
|
||||
{
|
||||
// Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
|
||||
using (var fileStream = GetPlaylistFileStream(playlist))
|
||||
using var fileStream = GetPlaylistFileStream(playlist);
|
||||
using var reader = new StreamReader(fileStream);
|
||||
var count = 0;
|
||||
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
using (var reader = new StreamReader(fileStream))
|
||||
var line = reader.ReadLine();
|
||||
|
||||
if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
var count = 0;
|
||||
|
||||
while (!reader.EndOfStream)
|
||||
count++;
|
||||
if (count >= segmentCount)
|
||||
{
|
||||
var line = reader.ReadLine();
|
||||
|
||||
if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
count++;
|
||||
if (count >= segmentCount)
|
||||
{
|
||||
Logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist);
|
||||
return;
|
||||
}
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
@ -247,17 +240,13 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
|
||||
protected Stream GetPlaylistFileStream(string path)
|
||||
{
|
||||
var tmpPath = path + ".tmp";
|
||||
tmpPath = path;
|
||||
|
||||
try
|
||||
{
|
||||
return new FileStream(tmpPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.SequentialScan);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.SequentialScan);
|
||||
}
|
||||
return new FileStream(
|
||||
path,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite,
|
||||
IODefaults.FileStreamBufferSize,
|
||||
FileOptions.SequentialScan);
|
||||
}
|
||||
|
||||
protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding)
|
||||
|
@ -284,7 +284,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
//}
|
||||
|
||||
Logger.LogDebug("returning {0} [general case]", segmentPath);
|
||||
job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
|
||||
job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
|
||||
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@ -438,8 +438,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
{
|
||||
var segmentId = "0";
|
||||
|
||||
var segmentRequest = request as GetHlsVideoSegment;
|
||||
if (segmentRequest != null)
|
||||
if (request is GetHlsVideoSegment segmentRequest)
|
||||
{
|
||||
segmentId = segmentRequest.SegmentId;
|
||||
}
|
||||
@ -690,8 +689,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
return false;
|
||||
}
|
||||
|
||||
var request = state.Request as IMasterHlsRequest;
|
||||
if (request != null && !request.EnableAdaptiveBitrateStreaming)
|
||||
if (state.Request is IMasterHlsRequest request && !request.EnableAdaptiveBitrateStreaming)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -936,7 +934,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
||||
|
||||
var framerate = state.VideoStream?.RealFrameRate;
|
||||
|
||||
if (framerate != null && framerate.HasValue)
|
||||
if (framerate.HasValue)
|
||||
{
|
||||
// This is to make sure keyframe interval is limited to our segment,
|
||||
// as forcing keyframes is not enough.
|
||||
|
@ -234,7 +234,7 @@ namespace MediaBrowser.Api.Playback
|
||||
OpenToken = mediaSource.OpenToken
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
info.MediaSources = new MediaSourceInfo[] { openStreamResult.MediaSource };
|
||||
info.MediaSources = new[] { openStreamResult.MediaSource };
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,7 +289,7 @@ namespace MediaBrowser.Api.Playback
|
||||
{
|
||||
var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
mediaSources = new MediaSourceInfo[] { mediaSource };
|
||||
mediaSources = new[] { mediaSource };
|
||||
}
|
||||
|
||||
if (mediaSources.Length == 0)
|
||||
@ -366,7 +366,7 @@ namespace MediaBrowser.Api.Playback
|
||||
|
||||
var options = new VideoOptions
|
||||
{
|
||||
MediaSources = new MediaSourceInfo[] { mediaSource },
|
||||
MediaSources = new[] { mediaSource },
|
||||
Context = EncodingContext.Streaming,
|
||||
DeviceId = auth.DeviceId,
|
||||
ItemId = item.Id,
|
||||
@ -572,8 +572,7 @@ namespace MediaBrowser.Api.Playback
|
||||
{
|
||||
attachment.DeliveryUrl = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}/Videos/{1}/{2}/Attachments/{3}",
|
||||
ServerConfigurationManager.Configuration.BaseUrl,
|
||||
"/Videos/{0}/{1}/Attachments/{2}",
|
||||
item.Id,
|
||||
mediaSource.Id,
|
||||
attachment.Index);
|
||||
@ -583,7 +582,7 @@ namespace MediaBrowser.Api.Playback
|
||||
private long? GetMaxBitrate(long? clientMaxBitrate, User user)
|
||||
{
|
||||
var maxBitrate = clientMaxBitrate;
|
||||
var remoteClientMaxBitrate = user == null ? 0 : user.Policy.RemoteClientBitrateLimit;
|
||||
var remoteClientMaxBitrate = user?.Policy.RemoteClientBitrateLimit ?? 0;
|
||||
|
||||
if (remoteClientMaxBitrate <= 0)
|
||||
{
|
||||
@ -662,17 +661,9 @@ namespace MediaBrowser.Api.Playback
|
||||
};
|
||||
}).ThenBy(i =>
|
||||
{
|
||||
if (maxBitrate.HasValue)
|
||||
if (maxBitrate.HasValue && i.Bitrate.HasValue)
|
||||
{
|
||||
if (i.Bitrate.HasValue)
|
||||
{
|
||||
if (i.Bitrate.Value <= maxBitrate.Value)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 2;
|
||||
}
|
||||
return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
|
||||
}
|
||||
|
||||
return 1;
|
||||
|
@ -167,7 +167,7 @@ namespace MediaBrowser.Api.Playback
|
||||
AudioCodec = request.AudioCodec,
|
||||
Protocol = request.TranscodingProtocol,
|
||||
BreakOnNonKeyFrames = request.BreakOnNonKeyFrames,
|
||||
MaxAudioChannels = request.TranscodingAudioChannels.HasValue ? request.TranscodingAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : null
|
||||
MaxAudioChannels = request.TranscodingAudioChannels?.ToString(CultureInfo.InvariantCulture)
|
||||
}
|
||||
};
|
||||
|
||||
@ -300,7 +300,7 @@ namespace MediaBrowser.Api.Playback
|
||||
|
||||
// hls segment container can only be mpegts or fmp4 per ffmpeg documentation
|
||||
// TODO: remove this when we switch back to the segment muxer
|
||||
var supportedHLSContainers = new string[] { "mpegts", "fmp4" };
|
||||
var supportedHLSContainers = new[] { "mpegts", "fmp4" };
|
||||
|
||||
var newRequest = new GetMasterHlsAudioPlaylist
|
||||
{
|
||||
|
@ -243,9 +243,7 @@ namespace MediaBrowser.Api
|
||||
// https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs
|
||||
var id = Guid.Parse(GetPathValue(1));
|
||||
|
||||
var plugin = _appHost.Plugins.First(p => p.Id == id) as IHasPluginConfiguration;
|
||||
|
||||
if (plugin == null)
|
||||
if (!(_appHost.Plugins.First(p => p.Id == id) is IHasPluginConfiguration plugin))
|
||||
{
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
@ -123,9 +123,7 @@ namespace MediaBrowser.Api.ScheduledTasks
|
||||
{
|
||||
var isHidden = false;
|
||||
|
||||
var configurableTask = i.ScheduledTask as IConfigurableScheduledTask;
|
||||
|
||||
if (configurableTask != null)
|
||||
if (i.ScheduledTask is IConfigurableScheduledTask configurableTask)
|
||||
{
|
||||
isHidden = configurableTask.IsHidden;
|
||||
}
|
||||
@ -142,9 +140,7 @@ namespace MediaBrowser.Api.ScheduledTasks
|
||||
{
|
||||
var isEnabled = true;
|
||||
|
||||
var configurableTask = i.ScheduledTask as IConfigurableScheduledTask;
|
||||
|
||||
if (configurableTask != null)
|
||||
if (i.ScheduledTask is IConfigurableScheduledTask configurableTask)
|
||||
{
|
||||
isEnabled = configurableTask.IsEnabled;
|
||||
}
|
||||
|
@ -234,59 +234,48 @@ namespace MediaBrowser.Api
|
||||
SetThumbImageInfo(result, item);
|
||||
SetBackdropImageInfo(result, item);
|
||||
|
||||
var program = item as LiveTvProgram;
|
||||
if (program != null)
|
||||
switch (item)
|
||||
{
|
||||
result.StartDate = program.StartDate;
|
||||
}
|
||||
case IHasSeries hasSeries:
|
||||
result.Series = hasSeries.SeriesName;
|
||||
break;
|
||||
case LiveTvProgram program:
|
||||
result.StartDate = program.StartDate;
|
||||
break;
|
||||
case Series series:
|
||||
if (series.Status.HasValue)
|
||||
{
|
||||
result.Status = series.Status.Value.ToString();
|
||||
}
|
||||
|
||||
var hasSeries = item as IHasSeries;
|
||||
if (hasSeries != null)
|
||||
{
|
||||
result.Series = hasSeries.SeriesName;
|
||||
}
|
||||
break;
|
||||
case MusicAlbum album:
|
||||
result.Artists = album.Artists;
|
||||
result.AlbumArtist = album.AlbumArtist;
|
||||
break;
|
||||
case Audio song:
|
||||
result.AlbumArtist = song.AlbumArtists.FirstOrDefault();
|
||||
result.Artists = song.Artists;
|
||||
|
||||
var series = item as Series;
|
||||
if (series != null)
|
||||
{
|
||||
if (series.Status.HasValue)
|
||||
{
|
||||
result.Status = series.Status.Value.ToString();
|
||||
}
|
||||
}
|
||||
MusicAlbum musicAlbum = song.AlbumEntity;
|
||||
|
||||
var album = item as MusicAlbum;
|
||||
if (musicAlbum != null)
|
||||
{
|
||||
result.Album = musicAlbum.Name;
|
||||
result.AlbumId = musicAlbum.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Album = song.Album;
|
||||
}
|
||||
|
||||
if (album != null)
|
||||
{
|
||||
result.Artists = album.Artists;
|
||||
result.AlbumArtist = album.AlbumArtist;
|
||||
}
|
||||
|
||||
var song = item as Audio;
|
||||
|
||||
if (song != null)
|
||||
{
|
||||
result.AlbumArtist = song.AlbumArtists.FirstOrDefault();
|
||||
result.Artists = song.Artists;
|
||||
|
||||
album = song.AlbumEntity;
|
||||
|
||||
if (album != null)
|
||||
{
|
||||
result.Album = album.Name;
|
||||
result.AlbumId = album.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Album = song.Album;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!item.ChannelId.Equals(Guid.Empty))
|
||||
{
|
||||
var channel = _libraryManager.GetItemById(item.ChannelId);
|
||||
result.ChannelName = channel == null ? null : channel.Name;
|
||||
result.ChannelName = channel?.Name;
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -296,12 +285,9 @@ namespace MediaBrowser.Api
|
||||
{
|
||||
var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null;
|
||||
|
||||
if (itemWithImage == null)
|
||||
if (itemWithImage == null && item is Episode)
|
||||
{
|
||||
if (item is Episode)
|
||||
{
|
||||
itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb);
|
||||
}
|
||||
itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb);
|
||||
}
|
||||
|
||||
if (itemWithImage == null)
|
||||
@ -323,12 +309,8 @@ namespace MediaBrowser.Api
|
||||
|
||||
private void SetBackdropImageInfo(SearchHint hint, BaseItem item)
|
||||
{
|
||||
var itemWithImage = item.HasImage(ImageType.Backdrop) ? item : null;
|
||||
|
||||
if (itemWithImage == null)
|
||||
{
|
||||
itemWithImage = GetParentWithImage<BaseItem>(item, ImageType.Backdrop);
|
||||
}
|
||||
var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null)
|
||||
?? GetParentWithImage<BaseItem>(item, ImageType.Backdrop);
|
||||
|
||||
if (itemWithImage != null)
|
||||
{
|
||||
|
@ -230,17 +230,14 @@ namespace MediaBrowser.Api.Subtitles
|
||||
|
||||
if (string.Equals(request.Format, "vtt", StringComparison.OrdinalIgnoreCase) && request.AddVttTimeMap)
|
||||
{
|
||||
using (var stream = await GetSubtitles(request).ConfigureAwait(false))
|
||||
{
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var text = reader.ReadToEnd();
|
||||
using var stream = await GetSubtitles(request).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000");
|
||||
var text = reader.ReadToEnd();
|
||||
|
||||
return ResultFactory.GetResult(Request, text, MimeTypes.GetMimeType("file." + request.Format));
|
||||
}
|
||||
}
|
||||
text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000");
|
||||
|
||||
return ResultFactory.GetResult(Request, text, MimeTypes.GetMimeType("file." + request.Format));
|
||||
}
|
||||
|
||||
return ResultFactory.GetResult(Request, await GetSubtitles(request).ConfigureAwait(false), MimeTypes.GetMimeType("file." + request.Format));
|
||||
|
@ -168,12 +168,9 @@ namespace MediaBrowser.Api.System
|
||||
.First(i => string.Equals(i.Name, request.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// For older files, assume fully static
|
||||
if (file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1))
|
||||
{
|
||||
return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShare.Read);
|
||||
}
|
||||
var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
|
||||
|
||||
return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShare.ReadWrite);
|
||||
return ResultFactory.GetStaticFileResult(Request, file.FullName, fileShare);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -92,10 +92,7 @@ namespace MediaBrowser.Api
|
||||
{
|
||||
lock (_timerLock)
|
||||
{
|
||||
if (KillTimer != null)
|
||||
{
|
||||
KillTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
}
|
||||
KillTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -424,9 +424,7 @@ namespace MediaBrowser.Api
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.SeasonId))
|
||||
{
|
||||
var season = _libraryManager.GetItemById(new Guid(request.SeasonId)) as Season;
|
||||
|
||||
if (season == null)
|
||||
if (!(_libraryManager.GetItemById(new Guid(request.SeasonId)) is Season season))
|
||||
{
|
||||
throw new ResourceNotFoundException("No season exists with Id " + request.SeasonId);
|
||||
}
|
||||
@ -444,14 +442,7 @@ namespace MediaBrowser.Api
|
||||
|
||||
var season = series.GetSeasons(user, dtoOptions).FirstOrDefault(i => i.IndexNumber == request.Season.Value);
|
||||
|
||||
if (season == null)
|
||||
{
|
||||
episodes = new List<BaseItem>();
|
||||
}
|
||||
else
|
||||
{
|
||||
episodes = ((Season)season).GetEpisodes(user, dtoOptions);
|
||||
}
|
||||
episodes = season == null ? new List<BaseItem>() : ((Season)season).GetEpisodes(user, dtoOptions);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -126,12 +126,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
|
||||
protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query)
|
||||
{
|
||||
if (request is GetAlbumArtists)
|
||||
{
|
||||
return LibraryManager.GetAlbumArtists(query);
|
||||
}
|
||||
|
||||
return LibraryManager.GetArtists(query);
|
||||
return request is GetAlbumArtists ? LibraryManager.GetAlbumArtists(query) : LibraryManager.GetArtists(query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -82,8 +82,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
{
|
||||
var parent = GetParentItem(request);
|
||||
|
||||
var collectionFolder = parent as IHasCollectionType;
|
||||
if (collectionFolder != null)
|
||||
if (parent is IHasCollectionType collectionFolder)
|
||||
{
|
||||
return collectionFolder.CollectionType;
|
||||
}
|
||||
@ -274,7 +273,7 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
DtoOptions = dtoOptions
|
||||
};
|
||||
|
||||
Func<BaseItem, bool> filter = i => FilterItem(request, i, excludeItemTypes, includeItemTypes, mediaTypes);
|
||||
bool Filter(BaseItem i) => FilterItem(request, i, excludeItemTypes, includeItemTypes, mediaTypes);
|
||||
|
||||
if (parentItem.IsFolder)
|
||||
{
|
||||
@ -284,18 +283,18 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
{
|
||||
items = request.Recursive ?
|
||||
folder.GetRecursiveChildren(user, query).ToList() :
|
||||
folder.GetChildren(user, true).Where(filter).ToList();
|
||||
folder.GetChildren(user, true).Where(Filter).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
items = request.Recursive ?
|
||||
folder.GetRecursiveChildren(filter) :
|
||||
folder.Children.Where(filter).ToList();
|
||||
folder.GetRecursiveChildren(Filter) :
|
||||
folder.Children.Where(Filter).ToList();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
items = new[] { parentItem }.Where(filter).ToList();
|
||||
items = new[] { parentItem }.Where(Filter).ToList();
|
||||
}
|
||||
|
||||
var extractedItems = GetAllItems(request, items);
|
||||
@ -346,30 +345,21 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
private bool FilterItem(GetItemsByName request, BaseItem f, string[] excludeItemTypes, string[] includeItemTypes, string[] mediaTypes)
|
||||
{
|
||||
// Exclude item types
|
||||
if (excludeItemTypes.Length > 0)
|
||||
if (excludeItemTypes.Length > 0 && excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Include item types
|
||||
if (includeItemTypes.Length > 0)
|
||||
if (includeItemTypes.Length > 0 && !includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Include MediaTypes
|
||||
if (mediaTypes.Length > 0)
|
||||
if (mediaTypes.Length > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -396,12 +396,10 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
|
||||
public VideoType[] GetVideoTypes()
|
||||
{
|
||||
if (string.IsNullOrEmpty(VideoTypes))
|
||||
{
|
||||
return Array.Empty<VideoType>();
|
||||
}
|
||||
|
||||
return VideoTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => (VideoType)Enum.Parse(typeof(VideoType), v, true)).ToArray();
|
||||
return string.IsNullOrEmpty(VideoTypes)
|
||||
? Array.Empty<VideoType>()
|
||||
: VideoTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(v => Enum.Parse<VideoType>(v, true)).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -412,12 +410,10 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
{
|
||||
var val = Filters;
|
||||
|
||||
if (string.IsNullOrEmpty(val))
|
||||
{
|
||||
return new ItemFilter[] { };
|
||||
}
|
||||
|
||||
return val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => (ItemFilter)Enum.Parse(typeof(ItemFilter), v, true)).ToArray();
|
||||
return string.IsNullOrEmpty(val)
|
||||
? Array.Empty<ItemFilter>()
|
||||
: val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(v => Enum.Parse<ItemFilter>(v, true)).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -428,12 +424,9 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
{
|
||||
var val = ImageTypes;
|
||||
|
||||
if (string.IsNullOrEmpty(val))
|
||||
{
|
||||
return new ImageType[] { };
|
||||
}
|
||||
|
||||
return val.Split(',').Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true)).ToArray();
|
||||
return string.IsNullOrEmpty(val)
|
||||
? Array.Empty<ImageType>()
|
||||
: val.Split(',').Select(v => Enum.Parse<ImageType>(v, true)).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -469,7 +462,9 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
var sortOrderIndex = sortOrders.Length > i ? i : 0;
|
||||
|
||||
var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
|
||||
var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase) ? MediaBrowser.Model.Entities.SortOrder.Descending : MediaBrowser.Model.Entities.SortOrder.Ascending;
|
||||
var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase)
|
||||
? MediaBrowser.Model.Entities.SortOrder.Descending
|
||||
: MediaBrowser.Model.Entities.SortOrder.Ascending;
|
||||
|
||||
result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder);
|
||||
}
|
||||
|
@ -199,14 +199,12 @@ namespace MediaBrowser.Api.UserLibrary
|
||||
item = _libraryManager.GetUserRootFolder();
|
||||
}
|
||||
|
||||
Folder folder = item as Folder;
|
||||
if (folder == null)
|
||||
if (!(item is Folder folder))
|
||||
{
|
||||
folder = _libraryManager.GetUserRootFolder();
|
||||
}
|
||||
|
||||
var hasCollectionType = folder as IHasCollectionType;
|
||||
if (hasCollectionType != null
|
||||
if (folder is IHasCollectionType hasCollectionType
|
||||
&& string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
request.Recursive = true;
|
||||
|
@ -139,17 +139,11 @@ namespace MediaBrowser.Api
|
||||
.ToList();
|
||||
|
||||
var primaryVersion = videosWithVersions.FirstOrDefault();
|
||||
|
||||
if (primaryVersion == null)
|
||||
{
|
||||
primaryVersion = items.OrderBy(i =>
|
||||
{
|
||||
if (i.Video3DFormat.HasValue)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (i.VideoType != Model.Entities.VideoType.VideoFile)
|
||||
if (i.Video3DFormat.HasValue || i.VideoType != Model.Entities.VideoType.VideoFile)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
@ -158,10 +152,7 @@ namespace MediaBrowser.Api
|
||||
})
|
||||
.ThenByDescending(i =>
|
||||
{
|
||||
var stream = i.GetDefaultVideoStream();
|
||||
|
||||
return stream == null || stream.Width == null ? 0 : stream.Width.Value;
|
||||
|
||||
return i.GetDefaultVideoStream()?.Width ?? 0;
|
||||
}).First();
|
||||
}
|
||||
|
||||
|
80
MediaBrowser.Common/Extensions/ProcessExtensions.cs
Normal file
80
MediaBrowser.Common/Extensions/ProcessExtensions.cs
Normal file
@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Common.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="Process"/>.
|
||||
/// </summary>
|
||||
public static class ProcessExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronously wait for the process to exit.
|
||||
/// </summary>
|
||||
/// <param name="process">The process to wait for.</param>
|
||||
/// <param name="timeout">The duration to wait before cancelling waiting for the task.</param>
|
||||
/// <returns>True if the task exited normally, false if the timeout elapsed before the process exited.</returns>
|
||||
/// <exception cref="InvalidOperationException">If <see cref="Process.EnableRaisingEvents"/> is not set to true for the process.</exception>
|
||||
public static async Task<bool> WaitForExitAsync(this Process process, TimeSpan timeout)
|
||||
{
|
||||
using (var cancelTokenSource = new CancellationTokenSource(timeout))
|
||||
{
|
||||
return await WaitForExitAsync(process, cancelTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously wait for the process to exit.
|
||||
/// </summary>
|
||||
/// <param name="process">The process to wait for.</param>
|
||||
/// <param name="cancelToken">A <see cref="CancellationToken"/> to observe while waiting for the process to exit.</param>
|
||||
/// <returns>True if the task exited normally, false if cancelled before the process exited.</returns>
|
||||
public static async Task<bool> WaitForExitAsync(this Process process, CancellationToken cancelToken)
|
||||
{
|
||||
if (!process.EnableRaisingEvents)
|
||||
{
|
||||
throw new InvalidOperationException("EnableRisingEvents must be enabled to async wait for a task to exit.");
|
||||
}
|
||||
|
||||
// Add an event handler for the process exit event
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
process.Exited += (sender, args) => tcs.TrySetResult(true);
|
||||
|
||||
// Return immediately if the process has already exited
|
||||
if (process.HasExitedSafe())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Register with the cancellation token then await
|
||||
using (var cancelRegistration = cancelToken.Register(() => tcs.TrySetResult(process.HasExitedSafe())))
|
||||
{
|
||||
return await tcs.Task.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the associated process has been terminated using
|
||||
/// <see cref="Process.HasExited"/>. This is safe to call even if there is no operating system process
|
||||
/// associated with the <see cref="Process"/>.
|
||||
/// </summary>
|
||||
/// <param name="process">The process to check the exit status for.</param>
|
||||
/// <returns>
|
||||
/// True if the operating system process referenced by the <see cref="Process"/> component has
|
||||
/// terminated, or if there is no associated operating system process; otherwise, false.
|
||||
/// </returns>
|
||||
private static bool HasExitedSafe(this Process process)
|
||||
{
|
||||
try
|
||||
{
|
||||
return process.HasExited;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -78,8 +78,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
if (!string.IsNullOrEmpty(hwType)
|
||||
&& encodingOptions.EnableHardwareEncoding
|
||||
&& codecMap.ContainsKey(hwType)
|
||||
&& CheckVaapi(state, hwType, encodingOptions))
|
||||
&& codecMap.ContainsKey(hwType))
|
||||
{
|
||||
var preferredEncoder = codecMap[hwType];
|
||||
|
||||
@ -93,23 +92,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return defaultEncoder;
|
||||
}
|
||||
|
||||
private bool CheckVaapi(EncodingJobInfo state, string hwType, EncodingOptions encodingOptions)
|
||||
{
|
||||
if (!string.Equals(hwType, "vaapi", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// No vaapi requested, return OK.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(encodingOptions.VaapiDevice))
|
||||
{
|
||||
// No device specified, return OK.
|
||||
return true;
|
||||
}
|
||||
|
||||
return IsVaapiSupported(state);
|
||||
}
|
||||
|
||||
private bool IsVaapiSupported(EncodingJobInfo state)
|
||||
{
|
||||
var videoStream = state.VideoStream;
|
||||
@ -424,7 +406,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "aac -strict experimental";
|
||||
// Use libfdk_aac for better audio quality if using custom build of FFmpeg which has fdk_aac support
|
||||
if (_mediaEncoder.SupportsEncoder("libfdk_aac"))
|
||||
{
|
||||
return "libfdk_aac";
|
||||
}
|
||||
|
||||
return "aac";
|
||||
}
|
||||
|
||||
if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
|
||||
@ -1605,7 +1593,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
// For VAAPI and CUVID decoder
|
||||
// these encoders cannot automatically adjust the size of graphical subtitles to fit the output video,
|
||||
// thus needs to be manually adjusted.
|
||||
if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)
|
||||
if ((IsVaapiSupported(state) && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
|
||||
|| (videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
var videoStream = state.VideoStream;
|
||||
@ -1648,7 +1636,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
|
||||
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
|
||||
else if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)
|
||||
else if (IsVaapiSupported(state) && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
/*
|
||||
@ -2014,19 +2002,29 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
|
||||
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
|
||||
else if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)
|
||||
else if (IsVaapiSupported(state) && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var codec = videoStream.Codec.ToLowerInvariant();
|
||||
var pixelFormat = videoStream.PixelFormat.ToLowerInvariant();
|
||||
var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile) && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase)
|
||||
|| videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// Assert hardware VAAPI decodable (Except h264 10-bit and higher color depth)
|
||||
// TODO: a propery way to detect hardware capabilities and falling back when transcoding is failed
|
||||
if ((pixelFormat ?? string.Empty).IndexOf("p10", StringComparison.OrdinalIgnoreCase) == -1
|
||||
|| ((pixelFormat ?? string.Empty).IndexOf("p10", StringComparison.OrdinalIgnoreCase) != -1
|
||||
&& (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))))
|
||||
// Assert 10-bit hardware VAAPI decodable
|
||||
if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
/*
|
||||
Download data from GPU to CPU as p010le format.
|
||||
Colorspace conversion is unnecessary here as libx264 will handle it.
|
||||
If this step is missing, it will fail on AMD but not on intel.
|
||||
*/
|
||||
filters.Add("hwdownload");
|
||||
filters.Add("format=p010le");
|
||||
}
|
||||
|
||||
// Assert 8-bit hardware VAAPI decodable
|
||||
else if (!isColorDepth10)
|
||||
{
|
||||
filters.Add("hwdownload");
|
||||
filters.Add("format=nv12");
|
||||
|
@ -155,48 +155,45 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
inputPath,
|
||||
attachmentStreamIndex,
|
||||
outputPath);
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
Arguments = processArgs,
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
};
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = startInfo
|
||||
};
|
||||
|
||||
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
int exitCode;
|
||||
|
||||
process.Start();
|
||||
|
||||
var processTcs = new TaskCompletionSource<bool>();
|
||||
process.EnableRaisingEvents = true;
|
||||
process.Exited += (sender, args) => processTcs.TrySetResult(true);
|
||||
var unregister = cancellationToken.Register(() => processTcs.TrySetResult(process.HasExited));
|
||||
var ranToCompletion = await processTcs.Task.ConfigureAwait(false);
|
||||
unregister.Dispose();
|
||||
|
||||
if (!ranToCompletion)
|
||||
{
|
||||
try
|
||||
using (var process = new Process
|
||||
{
|
||||
_logger.LogWarning("Killing ffmpeg attachment extraction process");
|
||||
process.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
Arguments = processArgs,
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
})
|
||||
{
|
||||
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
process.Start();
|
||||
|
||||
var ranToCompletion = await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing attachment extraction process");
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Killing ffmpeg attachment extraction process");
|
||||
process.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing attachment extraction process");
|
||||
}
|
||||
}
|
||||
|
||||
exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
}
|
||||
|
||||
var exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
|
||||
process.Dispose();
|
||||
|
||||
var failed = false;
|
||||
|
||||
if (exitCode != 0)
|
||||
|
@ -42,6 +42,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
"libvpx",
|
||||
"libvpx-vp9",
|
||||
"aac",
|
||||
"libfdk_aac",
|
||||
"libmp3lame",
|
||||
"libopus",
|
||||
"libvorbis",
|
||||
|
@ -13,7 +13,6 @@ using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.MediaEncoding.Probing;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Diagnostics;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
@ -22,6 +21,7 @@ using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.System;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
@ -38,7 +38,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
private readonly ILogger _logger;
|
||||
private readonly IServerConfigurationManager _configurationManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IProcessFactory _processFactory;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly Func<ISubtitleEncoder> _subtitleEncoder;
|
||||
private readonly IConfiguration _configuration;
|
||||
@ -58,7 +57,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
ILogger<MediaEncoder> logger,
|
||||
IServerConfigurationManager configurationManager,
|
||||
IFileSystem fileSystem,
|
||||
IProcessFactory processFactory,
|
||||
ILocalizationManager localization,
|
||||
Func<ISubtitleEncoder> subtitleEncoder,
|
||||
IConfiguration configuration,
|
||||
@ -67,7 +65,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
_logger = logger;
|
||||
_configurationManager = configurationManager;
|
||||
_fileSystem = fileSystem;
|
||||
_processFactory = processFactory;
|
||||
_localization = localization;
|
||||
_startupOptionFFmpegPath = startupOptionsFFmpegPath;
|
||||
_subtitleEncoder = subtitleEncoder;
|
||||
@ -362,30 +359,33 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
: "{0} -i {1} -threads 0 -v warning -print_format json -show_streams -show_format";
|
||||
args = string.Format(args, probeSizeArgument, inputPath).Trim();
|
||||
|
||||
var process = _processFactory.Create(new ProcessOptions
|
||||
var process = new Process
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
|
||||
// Must consume both or ffmpeg may hang due to deadlocks. See comments below.
|
||||
RedirectStandardOutput = true,
|
||||
// Must consume both or ffmpeg may hang due to deadlocks. See comments below.
|
||||
RedirectStandardOutput = true,
|
||||
|
||||
FileName = _ffprobePath,
|
||||
Arguments = args,
|
||||
FileName = _ffprobePath,
|
||||
Arguments = args,
|
||||
|
||||
|
||||
IsHidden = true,
|
||||
ErrorDialog = false,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false,
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
});
|
||||
};
|
||||
|
||||
if (forceEnableLogging)
|
||||
{
|
||||
_logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
_logger.LogInformation("{ProcessFileName} {ProcessArgs}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
_logger.LogDebug("{ProcessFileName} {ProcessArgs}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
}
|
||||
|
||||
using (var processWrapper = new ProcessWrapper(process, this))
|
||||
@ -571,18 +571,21 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
}
|
||||
}
|
||||
|
||||
var process = _processFactory.Create(new ProcessOptions
|
||||
var process = new Process
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = _ffmpegPath,
|
||||
Arguments = args,
|
||||
IsHidden = true,
|
||||
ErrorDialog = false,
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = _ffmpegPath,
|
||||
Arguments = args,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false,
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
});
|
||||
};
|
||||
|
||||
_logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
_logger.LogDebug("{ProcessFileName} {ProcessArguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
using (var processWrapper = new ProcessWrapper(process, this))
|
||||
{
|
||||
@ -599,7 +602,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
timeoutMs = DefaultImageExtractionTimeout;
|
||||
}
|
||||
|
||||
ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false);
|
||||
ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
{
|
||||
@ -700,23 +703,27 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
}
|
||||
}
|
||||
|
||||
var process = _processFactory.Create(new ProcessOptions
|
||||
var processStartInfo = new ProcessStartInfo
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = _ffmpegPath,
|
||||
Arguments = args,
|
||||
IsHidden = true,
|
||||
ErrorDialog = false,
|
||||
EnableRaisingEvents = true
|
||||
});
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
};
|
||||
|
||||
_logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments);
|
||||
_logger.LogInformation(processStartInfo.FileName + " " + processStartInfo.Arguments);
|
||||
|
||||
await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
bool ranToCompletion = false;
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = processStartInfo,
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
using (var processWrapper = new ProcessWrapper(process, this))
|
||||
{
|
||||
try
|
||||
@ -732,7 +739,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
while (isResponsive)
|
||||
{
|
||||
if (await process.WaitForExitAsync(30000).ConfigureAwait(false))
|
||||
if (await process.WaitForExitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false))
|
||||
{
|
||||
ranToCompletion = true;
|
||||
break;
|
||||
@ -949,14 +956,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
public ProcessWrapper(IProcess process, MediaEncoder mediaEncoder)
|
||||
public ProcessWrapper(Process process, MediaEncoder mediaEncoder)
|
||||
{
|
||||
Process = process;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
Process.Exited += OnProcessExited;
|
||||
}
|
||||
|
||||
public IProcess Process { get; }
|
||||
public Process Process { get; }
|
||||
|
||||
public bool HasExited { get; private set; }
|
||||
|
||||
@ -964,7 +971,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
void OnProcessExited(object sender, EventArgs e)
|
||||
{
|
||||
var process = (IProcess)sender;
|
||||
var process = (Process)sender;
|
||||
|
||||
HasExited = true;
|
||||
|
||||
@ -979,7 +986,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
DisposeProcess(process);
|
||||
}
|
||||
|
||||
private void DisposeProcess(IProcess process)
|
||||
private void DisposeProcess(Process process)
|
||||
{
|
||||
lock (_mediaEncoder._runningProcessesLock)
|
||||
{
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -12,7 +13,6 @@ using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Diagnostics;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
@ -31,7 +31,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IProcessFactory _processFactory;
|
||||
|
||||
public SubtitleEncoder(
|
||||
ILibraryManager libraryManager,
|
||||
@ -40,8 +39,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
IFileSystem fileSystem,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IHttpClient httpClient,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IProcessFactory processFactory)
|
||||
IMediaSourceManager mediaSourceManager)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
@ -50,7 +48,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_httpClient = httpClient;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_processFactory = processFactory;
|
||||
}
|
||||
|
||||
private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
|
||||
@ -429,50 +426,54 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
encodingParam = " -sub_charenc " + encodingParam;
|
||||
}
|
||||
|
||||
var process = _processFactory.Create(new ProcessOptions
|
||||
int exitCode;
|
||||
|
||||
using (var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
Arguments = string.Format("{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath),
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
})
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
Arguments = string.Format("{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath),
|
||||
EnableRaisingEvents = true,
|
||||
IsHidden = true,
|
||||
ErrorDialog = false
|
||||
});
|
||||
_logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
_logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error starting ffmpeg");
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
var ranToCompletion = await process.WaitForExitAsync(300000).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Killing ffmpeg subtitle conversion process");
|
||||
|
||||
process.Kill();
|
||||
process.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing subtitle conversion process");
|
||||
_logger.LogError(ex, "Error starting ffmpeg");
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(5)).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Killing ffmpeg subtitle conversion process");
|
||||
|
||||
process.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing subtitle conversion process");
|
||||
}
|
||||
}
|
||||
|
||||
exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
}
|
||||
|
||||
var exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
|
||||
process.Dispose();
|
||||
|
||||
var failed = false;
|
||||
|
||||
if (exitCode == -1)
|
||||
@ -578,50 +579,54 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
outputCodec,
|
||||
outputPath);
|
||||
|
||||
var process = _processFactory.Create(new ProcessOptions
|
||||
int exitCode;
|
||||
|
||||
using (var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
Arguments = processArgs,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
})
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
EnableRaisingEvents = true,
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
Arguments = processArgs,
|
||||
IsHidden = true,
|
||||
ErrorDialog = false
|
||||
});
|
||||
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error starting ffmpeg");
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
var ranToCompletion = await process.WaitForExitAsync(300000).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Killing ffmpeg subtitle extraction process");
|
||||
|
||||
process.Kill();
|
||||
process.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing subtitle extraction process");
|
||||
_logger.LogError(ex, "Error starting ffmpeg");
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(5)).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Killing ffmpeg subtitle extraction process");
|
||||
|
||||
process.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing subtitle extraction process");
|
||||
}
|
||||
}
|
||||
|
||||
exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
}
|
||||
|
||||
var exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
|
||||
process.Dispose();
|
||||
|
||||
var failed = false;
|
||||
|
||||
if (exitCode == -1)
|
||||
|
@ -1,23 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Model.Diagnostics
|
||||
{
|
||||
public interface IProcess : IDisposable
|
||||
{
|
||||
event EventHandler Exited;
|
||||
|
||||
void Kill();
|
||||
bool WaitForExit(int timeMs);
|
||||
Task<bool> WaitForExitAsync(int timeMs);
|
||||
int ExitCode { get; }
|
||||
void Start();
|
||||
StreamWriter StandardInput { get; }
|
||||
StreamReader StandardError { get; }
|
||||
StreamReader StandardOutput { get; }
|
||||
ProcessOptions StartInfo { get; }
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Model.Diagnostics
|
||||
{
|
||||
public interface IProcessFactory
|
||||
{
|
||||
IProcess Create(ProcessOptions options);
|
||||
}
|
||||
|
||||
public class ProcessOptions
|
||||
{
|
||||
public string FileName { get; set; }
|
||||
public string Arguments { get; set; }
|
||||
public string WorkingDirectory { get; set; }
|
||||
public bool CreateNoWindow { get; set; }
|
||||
public bool UseShellExecute { get; set; }
|
||||
public bool EnableRaisingEvents { get; set; }
|
||||
public bool ErrorDialog { get; set; }
|
||||
public bool RedirectStandardError { get; set; }
|
||||
public bool RedirectStandardInput { get; set; }
|
||||
public bool RedirectStandardOutput { get; set; }
|
||||
public bool IsHidden { get; set; }
|
||||
}
|
||||
}
|
6
nuget.config
Normal file
6
nuget.config
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="NuGet official package source" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
@ -7,6 +7,8 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
{
|
||||
public class ExtraTests : BaseVideoTest
|
||||
{
|
||||
private readonly NamingOptions _videoOptions = new NamingOptions();
|
||||
|
||||
// Requirements
|
||||
// movie-deleted = ExtraType deletedscene
|
||||
|
||||
@ -15,42 +17,64 @@ namespace Jellyfin.Naming.Tests.Video
|
||||
[Fact]
|
||||
public void TestKodiExtras()
|
||||
{
|
||||
var videoOptions = new NamingOptions();
|
||||
Test("trailer.mp4", ExtraType.Trailer, _videoOptions);
|
||||
Test("300-trailer.mp4", ExtraType.Trailer, _videoOptions);
|
||||
|
||||
Test("trailer.mp4", ExtraType.Trailer, videoOptions);
|
||||
Test("300-trailer.mp4", ExtraType.Trailer, videoOptions);
|
||||
|
||||
Test("theme.mp3", ExtraType.ThemeSong, videoOptions);
|
||||
Test("theme.mp3", ExtraType.ThemeSong, _videoOptions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestExpandedExtras()
|
||||
{
|
||||
var videoOptions = new NamingOptions();
|
||||
Test("trailer.mp4", ExtraType.Trailer, _videoOptions);
|
||||
Test("trailer.mp3", null, _videoOptions);
|
||||
Test("300-trailer.mp4", ExtraType.Trailer, _videoOptions);
|
||||
|
||||
Test("trailer.mp4", ExtraType.Trailer, videoOptions);
|
||||
Test("trailer.mp3", null, videoOptions);
|
||||
Test("300-trailer.mp4", ExtraType.Trailer, videoOptions);
|
||||
Test("theme.mp3", ExtraType.ThemeSong, _videoOptions);
|
||||
Test("theme.mkv", null, _videoOptions);
|
||||
|
||||
Test("theme.mp3", ExtraType.ThemeSong, videoOptions);
|
||||
Test("theme.mkv", null, videoOptions);
|
||||
Test("300-scene.mp4", ExtraType.Scene, _videoOptions);
|
||||
Test("300-scene2.mp4", ExtraType.Scene, _videoOptions);
|
||||
Test("300-clip.mp4", ExtraType.Clip, _videoOptions);
|
||||
|
||||
Test("300-scene.mp4", ExtraType.Scene, videoOptions);
|
||||
Test("300-scene2.mp4", ExtraType.Scene, videoOptions);
|
||||
Test("300-clip.mp4", ExtraType.Clip, videoOptions);
|
||||
Test("300-deleted.mp4", ExtraType.DeletedScene, _videoOptions);
|
||||
Test("300-deletedscene.mp4", ExtraType.DeletedScene, _videoOptions);
|
||||
Test("300-interview.mp4", ExtraType.Interview, _videoOptions);
|
||||
Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes, _videoOptions);
|
||||
}
|
||||
|
||||
Test("300-deleted.mp4", ExtraType.DeletedScene, videoOptions);
|
||||
Test("300-deletedscene.mp4", ExtraType.DeletedScene, videoOptions);
|
||||
Test("300-interview.mp4", ExtraType.Interview, videoOptions);
|
||||
Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes, videoOptions);
|
||||
[Theory]
|
||||
[InlineData(ExtraType.BehindTheScenes, "behind the scenes" )]
|
||||
[InlineData(ExtraType.DeletedScene, "deleted scenes" )]
|
||||
[InlineData(ExtraType.Interview, "interviews" )]
|
||||
[InlineData(ExtraType.Scene, "scenes" )]
|
||||
[InlineData(ExtraType.Sample, "samples" )]
|
||||
[InlineData(ExtraType.Clip, "shorts" )]
|
||||
[InlineData(ExtraType.Clip, "featurettes" )]
|
||||
[InlineData(ExtraType.Unknown, "extras" )]
|
||||
public void TestDirectories(ExtraType type, string dirName)
|
||||
{
|
||||
Test(dirName + "/300.mp4", type, _videoOptions);
|
||||
Test("300/" + dirName + "/something.mkv", type, _videoOptions);
|
||||
Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", type, _videoOptions);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("gibberish")]
|
||||
[InlineData("not a scene")]
|
||||
[InlineData("The Big Short")]
|
||||
public void TestNonExtraDirectories(string dirName)
|
||||
{
|
||||
Test(dirName + "/300.mp4", null, _videoOptions);
|
||||
Test("300/" + dirName + "/something.mkv", null, _videoOptions);
|
||||
Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", null, _videoOptions);
|
||||
Test("/data/something/Movies/" + dirName + "/" + dirName + ".mp4", null, _videoOptions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestSample()
|
||||
{
|
||||
var videoOptions = new NamingOptions();
|
||||
|
||||
Test("300-sample.mp4", ExtraType.Sample, videoOptions);
|
||||
Test("300-sample.mp4", ExtraType.Sample, _videoOptions);
|
||||
}
|
||||
|
||||
private void Test(string input, ExtraType? expectedType, NamingOptions videoOptions)
|
||||
|
Loading…
Reference in New Issue
Block a user