jellyfin-server/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegDownloader.cs

430 lines
16 KiB
C#
Raw Normal View History

2014-10-06 23:58:46 +00:00
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.IO;
using MediaBrowser.Common.Net;
2013-09-25 00:54:51 +00:00
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Net;
2014-10-06 23:58:46 +00:00
using Mono.Unix.Native;
using System;
2014-10-06 23:58:46 +00:00
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Server.Startup.Common.FFMpeg
{
public class FFMpegDownloader
{
private readonly IHttpClient _httpClient;
private readonly IApplicationPaths _appPaths;
private readonly ILogger _logger;
2013-09-25 00:54:51 +00:00
private readonly IZipClient _zipClient;
private readonly IFileSystem _fileSystem;
2014-11-23 23:10:41 +00:00
private readonly NativeEnvironment _environment;
2014-07-22 01:29:06 +00:00
private readonly string[] _fontUrls =
{
2014-10-08 23:31:44 +00:00
"https://github.com/MediaBrowser/MediaBrowser.Resources/raw/master/ffmpeg/ARIALUNI.7z"
2014-07-22 01:29:06 +00:00
};
2014-11-23 23:10:41 +00:00
public FFMpegDownloader(ILogger logger, IApplicationPaths appPaths, IHttpClient httpClient, IZipClient zipClient, IFileSystem fileSystem, NativeEnvironment environment)
{
_logger = logger;
_appPaths = appPaths;
_httpClient = httpClient;
2013-09-25 00:54:51 +00:00
_zipClient = zipClient;
_fileSystem = fileSystem;
2014-11-23 23:10:41 +00:00
_environment = environment;
}
public async Task<FFMpegInfo> GetFFMpegInfo(NativeEnvironment environment, StartupOptions options, IProgress<double> progress)
{
2014-09-14 15:26:33 +00:00
var customffMpegPath = options.GetOption("-ffmpeg");
var customffProbePath = options.GetOption("-ffprobe");
if (!string.IsNullOrWhiteSpace(customffMpegPath) && !string.IsNullOrWhiteSpace(customffProbePath))
{
return new FFMpegInfo
{
ProbePath = customffProbePath,
EncoderPath = customffMpegPath,
Version = "custom"
};
}
var downloadInfo = FFMpegDownloadInfo.GetInfo(environment);
var version = downloadInfo.Version;
if (string.Equals(version, "path", StringComparison.OrdinalIgnoreCase))
{
return new FFMpegInfo
{
ProbePath = downloadInfo.FFProbeFilename,
EncoderPath = downloadInfo.FFMpegFilename,
Version = version
};
}
2014-05-07 02:28:19 +00:00
var rootEncoderPath = Path.Combine(_appPaths.ProgramDataPath, "ffmpeg");
var versionedDirectoryPath = Path.Combine(rootEncoderPath, version);
var info = new FFMpegInfo
{
ProbePath = Path.Combine(versionedDirectoryPath, downloadInfo.FFProbeFilename),
EncoderPath = Path.Combine(versionedDirectoryPath, downloadInfo.FFMpegFilename),
Version = version
};
Directory.CreateDirectory(versionedDirectoryPath);
2014-09-14 15:10:51 +00:00
var excludeFromDeletions = new List<string> { versionedDirectoryPath };
2014-05-07 02:28:19 +00:00
if (!File.Exists(info.ProbePath) || !File.Exists(info.EncoderPath))
{
2014-05-07 02:28:19 +00:00
// ffmpeg not present. See if there's an older version we can start with
var existingVersion = GetExistingVersion(info, rootEncoderPath);
2014-05-07 02:28:19 +00:00
// No older version. Need to download and block until complete
if (existingVersion == null)
{
await DownloadFFMpeg(downloadInfo, versionedDirectoryPath, progress).ConfigureAwait(false);
2014-05-07 02:28:19 +00:00
}
else
{
// Older version found.
// Start with that. Download new version in the background.
var newPath = versionedDirectoryPath;
Task.Run(() => DownloadFFMpegInBackground(downloadInfo, newPath));
2014-05-07 02:28:19 +00:00
info = existingVersion;
versionedDirectoryPath = Path.GetDirectoryName(info.EncoderPath);
2014-09-14 15:10:51 +00:00
excludeFromDeletions.Add(versionedDirectoryPath);
2014-05-07 02:28:19 +00:00
}
}
2014-05-07 02:28:19 +00:00
await DownloadFonts(versionedDirectoryPath).ConfigureAwait(false);
2014-09-14 15:10:51 +00:00
DeleteOlderFolders(Path.GetDirectoryName(versionedDirectoryPath), excludeFromDeletions);
2014-05-07 02:28:19 +00:00
return info;
}
2015-02-11 20:23:07 +00:00
private void DeleteOlderFolders(string path, IEnumerable<string> excludeFolders)
2014-09-14 15:10:51 +00:00
{
var folders = Directory.GetDirectories(path)
.Where(i => !excludeFolders.Contains(i, StringComparer.OrdinalIgnoreCase))
.ToList();
foreach (var folder in folders)
{
DeleteFolder(folder);
}
}
private void DeleteFolder(string path)
{
try
{
_fileSystem.DeleteDirectory(path, true);
2014-09-14 15:10:51 +00:00
}
catch (Exception ex)
{
_logger.ErrorException("Error deleting {0}", ex, path);
}
}
2014-05-07 02:28:19 +00:00
private FFMpegInfo GetExistingVersion(FFMpegInfo info, string rootEncoderPath)
{
var encoderFilename = Path.GetFileName(info.EncoderPath);
var probeFilename = Path.GetFileName(info.ProbePath);
foreach (var directory in Directory.EnumerateDirectories(rootEncoderPath, "*", SearchOption.TopDirectoryOnly)
.ToList())
{
2014-05-07 02:28:19 +00:00
var allFiles = Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories).ToList();
2014-05-07 02:28:19 +00:00
var encoder = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), encoderFilename, StringComparison.OrdinalIgnoreCase));
var probe = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), probeFilename, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(encoder) &&
!string.IsNullOrWhiteSpace(probe))
{
2014-05-07 02:28:19 +00:00
return new FFMpegInfo
{
2015-02-11 20:23:07 +00:00
EncoderPath = encoder,
ProbePath = probe,
Version = Path.GetFileName(Path.GetDirectoryName(probe))
2014-05-07 02:28:19 +00:00
};
}
2014-05-07 02:28:19 +00:00
}
2013-09-23 17:27:38 +00:00
2014-05-07 02:28:19 +00:00
return null;
}
private async void DownloadFFMpegInBackground(FFMpegDownloadInfo downloadinfo, string directory)
2014-05-07 02:28:19 +00:00
{
try
{
await DownloadFFMpeg(downloadinfo, directory, new Progress<double>()).ConfigureAwait(false);
2014-05-07 02:28:19 +00:00
}
catch (Exception ex)
{
_logger.ErrorException("Error downloading ffmpeg", ex);
}
}
private async Task DownloadFFMpeg(FFMpegDownloadInfo downloadinfo, string directory, IProgress<double> progress)
{
foreach (var url in downloadinfo.DownloadUrls)
{
progress.Report(0);
try
{
var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions
{
Url = url,
CancellationToken = CancellationToken.None,
Progress = progress
}).ConfigureAwait(false);
ExtractFFMpeg(downloadinfo, tempFile, directory);
return;
}
2014-01-21 06:10:58 +00:00
catch (Exception ex)
{
2014-07-30 03:31:35 +00:00
_logger.ErrorException("Error downloading {0}", ex, url);
}
}
2013-09-23 17:27:38 +00:00
2015-04-02 21:01:42 +00:00
if (downloadinfo.DownloadUrls.Length == 0)
{
throw new ApplicationException("ffmpeg unvailable. Please install it and start the server with two command line arguments: -ffmpeg \"{PATH}\" and -ffprobe \"{PATH}\"");
}
else
{
throw new ApplicationException("Unable to download required components. Please try again later.");
}
}
private void ExtractFFMpeg(FFMpegDownloadInfo downloadinfo, string tempFile, string targetFolder)
{
2014-05-07 02:28:19 +00:00
_logger.Info("Extracting ffmpeg from {0}", tempFile);
var tempFolder = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString());
Directory.CreateDirectory(tempFolder);
try
{
ExtractArchive(downloadinfo, tempFile, tempFolder);
2013-10-13 03:39:22 +00:00
2015-02-11 20:23:07 +00:00
var files = Directory.EnumerateFiles(tempFolder, "*", SearchOption.AllDirectories)
.ToList();
2013-10-13 03:39:22 +00:00
foreach (var file in files.Where(i =>
{
var filename = Path.GetFileName(i);
2013-10-13 03:39:22 +00:00
return
string.Equals(filename, downloadinfo.FFProbeFilename, StringComparison.OrdinalIgnoreCase) ||
string.Equals(filename, downloadinfo.FFMpegFilename, StringComparison.OrdinalIgnoreCase);
2013-10-13 03:39:22 +00:00
}))
{
2015-01-25 21:04:29 +00:00
var targetFile = Path.Combine(targetFolder, Path.GetFileName(file));
File.Copy(file, targetFile, true);
SetFilePermissions(targetFile);
}
}
finally
{
DeleteFile(tempFile);
}
}
2015-01-25 21:04:29 +00:00
private void SetFilePermissions(string path)
2014-10-06 23:58:46 +00:00
{
// Linux: File permission to 666, and user's execute bit
2014-11-23 23:10:41 +00:00
if (_environment.OperatingSystem == OperatingSystem.Bsd || _environment.OperatingSystem == OperatingSystem.Linux || _environment.OperatingSystem == OperatingSystem.Osx)
2014-10-06 23:58:46 +00:00
{
2015-02-11 20:23:07 +00:00
_logger.Info("Syscall.chmod {0} FilePermissions.DEFFILEMODE | FilePermissions.S_IRWXU | FilePermissions.S_IXGRP | FilePermissions.S_IXOTH", path);
2015-01-28 04:17:35 +00:00
Syscall.chmod(path, FilePermissions.DEFFILEMODE | FilePermissions.S_IRWXU | FilePermissions.S_IXGRP | FilePermissions.S_IXOTH);
2014-10-06 23:58:46 +00:00
}
}
private void ExtractArchive(FFMpegDownloadInfo downloadinfo, string archivePath, string targetPath)
{
2014-05-07 02:28:19 +00:00
_logger.Info("Extracting {0} to {1}", archivePath, targetPath);
if (string.Equals(downloadinfo.ArchiveType, "7z", StringComparison.OrdinalIgnoreCase))
2013-10-13 03:39:22 +00:00
{
_zipClient.ExtractAllFrom7z(archivePath, targetPath, true);
}
else if (string.Equals(downloadinfo.ArchiveType, "gz", StringComparison.OrdinalIgnoreCase))
2013-10-13 03:39:22 +00:00
{
_zipClient.ExtractAllFromTar(archivePath, targetPath, true);
}
}
2013-10-13 19:56:54 +00:00
private void Extract7zArchive(string archivePath, string targetPath)
{
2014-05-07 02:28:19 +00:00
_logger.Info("Extracting {0} to {1}", archivePath, targetPath);
2013-10-13 19:56:54 +00:00
_zipClient.ExtractAllFrom7z(archivePath, targetPath, true);
}
private void DeleteFile(string path)
{
try
{
_fileSystem.DeleteFile(path);
}
catch (IOException ex)
{
_logger.ErrorException("Error deleting temp file {0}", ex, path);
}
}
/// <summary>
/// Extracts the fonts.
/// </summary>
/// <param name="targetPath">The target path.</param>
2014-05-07 02:28:19 +00:00
/// <returns>Task.</returns>
private async Task DownloadFonts(string targetPath)
{
2013-09-23 17:27:38 +00:00
try
{
2013-09-23 17:27:38 +00:00
var fontsDirectory = Path.Combine(targetPath, "fonts");
Directory.CreateDirectory(fontsDirectory);
2013-09-23 17:27:38 +00:00
const string fontFilename = "ARIALUNI.TTF";
2013-09-23 17:27:38 +00:00
var fontFile = Path.Combine(fontsDirectory, fontFilename);
2014-05-07 02:28:19 +00:00
if (File.Exists(fontFile))
2013-09-23 17:27:38 +00:00
{
2014-05-07 02:28:19 +00:00
await WriteFontConfigFile(fontsDirectory).ConfigureAwait(false);
}
else
{
// Kick this off, but no need to wait on it
Task.Run(async () =>
{
await DownloadFontFile(fontsDirectory, fontFilename, new Progress<double>()).ConfigureAwait(false);
2015-02-11 20:23:07 +00:00
2014-05-07 02:28:19 +00:00
await WriteFontConfigFile(fontsDirectory).ConfigureAwait(false);
});
2013-09-23 17:27:38 +00:00
}
}
catch (HttpException ex)
{
2013-09-23 17:27:38 +00:00
// Don't let the server crash because of this
_logger.ErrorException("Error downloading ffmpeg font files", ex);
}
catch (Exception ex)
{
// Don't let the server crash because of this
_logger.ErrorException("Error writing ffmpeg font files", ex);
}
}
/// <summary>
/// Downloads the font file.
/// </summary>
/// <param name="fontsDirectory">The fonts directory.</param>
/// <param name="fontFilename">The font filename.</param>
/// <returns>Task.</returns>
private async Task DownloadFontFile(string fontsDirectory, string fontFilename, IProgress<double> progress)
{
var existingFile = Directory
.EnumerateFiles(_appPaths.ProgramDataPath, fontFilename, SearchOption.AllDirectories)
.FirstOrDefault();
if (existingFile != null)
{
try
{
File.Copy(existingFile, Path.Combine(fontsDirectory, fontFilename), true);
return;
}
catch (IOException ex)
{
// Log this, but don't let it fail the operation
_logger.ErrorException("Error copying file", ex);
}
}
2013-09-23 18:14:07 +00:00
string tempFile = null;
foreach (var url in _fontUrls)
{
progress.Report(0);
2013-09-23 18:14:07 +00:00
try
{
tempFile = await _httpClient.GetTempFile(new HttpRequestOptions
{
Url = url,
Progress = progress
2013-09-23 18:14:07 +00:00
}).ConfigureAwait(false);
break;
}
catch (Exception ex)
{
// The core can function without the font file, so handle this
_logger.ErrorException("Failed to download ffmpeg font file from {0}", ex, url);
}
}
if (string.IsNullOrEmpty(tempFile))
{
return;
}
2013-10-13 19:56:54 +00:00
Extract7zArchive(tempFile, fontsDirectory);
2015-02-11 20:23:07 +00:00
try
{
_fileSystem.DeleteFile(tempFile);
}
catch (IOException ex)
{
// Log this, but don't let it fail the operation
_logger.ErrorException("Error deleting temp file {0}", ex, tempFile);
}
}
/// <summary>
/// Writes the font config file.
/// </summary>
/// <param name="fontsDirectory">The fonts directory.</param>
/// <returns>Task.</returns>
private async Task WriteFontConfigFile(string fontsDirectory)
{
const string fontConfigFilename = "fonts.conf";
var fontConfigFile = Path.Combine(fontsDirectory, fontConfigFilename);
if (!File.Exists(fontConfigFile))
{
var contents = string.Format("<?xml version=\"1.0\"?><fontconfig><dir>{0}</dir><alias><family>Arial</family><prefer>Arial Unicode MS</prefer></alias></fontconfig>", fontsDirectory);
var bytes = Encoding.UTF8.GetBytes(contents);
using (var fileStream = _fileSystem.GetFileStream(fontConfigFile, FileMode.Create, FileAccess.Write,
FileShare.Read, true))
{
await fileStream.WriteAsync(bytes, 0, bytes.Length);
}
}
}
}
}