commit
935de313d5
|
@ -6,7 +6,7 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Drawing
|
||||
namespace Emby.Drawing.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Taken from http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349
|
98
Emby.Drawing/Emby.Drawing.csproj
Normal file
98
Emby.Drawing/Emby.Drawing.csproj
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{08FFF49B-F175-4807-A2B5-73B0EBD9F716}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Emby.Drawing</RootNamespace>
|
||||
<AssemblyName>Emby.Drawing</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
|
||||
<RestorePackages>true</RestorePackages>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="ImageMagickSharp, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\ImageMagickSharp.1.0.0.14\lib\net45\ImageMagickSharp.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SharedVersion.cs">
|
||||
<Link>Properties\SharedVersion.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="GDI\DynamicImageHelpers.cs" />
|
||||
<Compile Include="GDI\GDIImageEncoder.cs" />
|
||||
<Compile Include="GDI\ImageExtensions.cs" />
|
||||
<Compile Include="GDI\PercentPlayedDrawer.cs" />
|
||||
<Compile Include="GDI\PlayedIndicatorDrawer.cs" />
|
||||
<Compile Include="GDI\UnplayedCountIndicator.cs" />
|
||||
<Compile Include="IImageEncoder.cs" />
|
||||
<Compile Include="Common\ImageHeader.cs" />
|
||||
<Compile Include="ImageMagick\ImageMagickEncoder.cs" />
|
||||
<Compile Include="ImageMagick\StripCollageBuilder.cs" />
|
||||
<Compile Include="ImageProcessor.cs" />
|
||||
<Compile Include="ImageMagick\PercentPlayedDrawer.cs" />
|
||||
<Compile Include="ImageMagick\PlayedIndicatorDrawer.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="ImageMagick\UnplayedCountIndicator.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="ImageMagick\fonts\MontserratLight.otf" />
|
||||
<EmbeddedResource Include="ImageMagick\fonts\robotoregular.ttf" />
|
||||
<EmbeddedResource Include="ImageMagick\fonts\webdings.ttf" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj">
|
||||
<Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project>
|
||||
<Name>MediaBrowser.Common</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj">
|
||||
<Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
|
||||
<Name>MediaBrowser.Controller</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
|
||||
<Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
|
||||
<Name>MediaBrowser.Model</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
138
Emby.Drawing/GDI/DynamicImageHelpers.cs
Normal file
138
Emby.Drawing/GDI/DynamicImageHelpers.cs
Normal file
|
@ -0,0 +1,138 @@
|
|||
using Emby.Drawing.ImageMagick;
|
||||
using MediaBrowser.Common.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Emby.Drawing.GDI
|
||||
{
|
||||
public static class DynamicImageHelpers
|
||||
{
|
||||
public static void CreateThumbCollage(List<string> files,
|
||||
IFileSystem fileSystem,
|
||||
string file,
|
||||
int width,
|
||||
int height)
|
||||
{
|
||||
const int numStrips = 4;
|
||||
files = StripCollageBuilder.ProjectPaths(files, numStrips).ToList();
|
||||
|
||||
const int rows = 1;
|
||||
int cols = numStrips;
|
||||
|
||||
int cellWidth = 2 * (width / 3);
|
||||
int cellHeight = height;
|
||||
var index = 0;
|
||||
|
||||
using (var img = new Bitmap(width, height, PixelFormat.Format32bppPArgb))
|
||||
{
|
||||
using (var graphics = Graphics.FromImage(img))
|
||||
{
|
||||
graphics.CompositingQuality = CompositingQuality.HighQuality;
|
||||
graphics.SmoothingMode = SmoothingMode.HighQuality;
|
||||
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||
graphics.CompositingMode = CompositingMode.SourceCopy;
|
||||
|
||||
for (var row = 0; row < rows; row++)
|
||||
{
|
||||
for (var col = 0; col < cols; col++)
|
||||
{
|
||||
var x = col * (cellWidth / 2);
|
||||
var y = row * cellHeight;
|
||||
|
||||
if (files.Count > index)
|
||||
{
|
||||
using (var fileStream = fileSystem.GetFileStream(files[index], FileMode.Open, FileAccess.Read, FileShare.Read, true))
|
||||
{
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
fileStream.CopyTo(memoryStream);
|
||||
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using (var imgtemp = Image.FromStream(memoryStream, true, false))
|
||||
{
|
||||
graphics.DrawImage(imgtemp, x, y, cellWidth, cellHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
img.Save(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void CreateSquareCollage(List<string> files,
|
||||
IFileSystem fileSystem,
|
||||
string file,
|
||||
int width,
|
||||
int height)
|
||||
{
|
||||
files = StripCollageBuilder.ProjectPaths(files, 4).ToList();
|
||||
|
||||
const int rows = 2;
|
||||
const int cols = 2;
|
||||
|
||||
int singleSize = width / 2;
|
||||
var index = 0;
|
||||
|
||||
using (var img = new Bitmap(width, height, PixelFormat.Format32bppPArgb))
|
||||
{
|
||||
using (var graphics = Graphics.FromImage(img))
|
||||
{
|
||||
graphics.CompositingQuality = CompositingQuality.HighQuality;
|
||||
graphics.SmoothingMode = SmoothingMode.HighQuality;
|
||||
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||
graphics.CompositingMode = CompositingMode.SourceCopy;
|
||||
|
||||
for (var row = 0; row < rows; row++)
|
||||
{
|
||||
for (var col = 0; col < cols; col++)
|
||||
{
|
||||
var x = col * singleSize;
|
||||
var y = row * singleSize;
|
||||
|
||||
using (var fileStream = fileSystem.GetFileStream(files[index], FileMode.Open, FileAccess.Read, FileShare.Read, true))
|
||||
{
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
fileStream.CopyTo(memoryStream);
|
||||
|
||||
memoryStream.Position = 0;
|
||||
|
||||
using (var imgtemp = Image.FromStream(memoryStream, true, false))
|
||||
{
|
||||
graphics.DrawImage(imgtemp, x, y, singleSize, singleSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
img.Save(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream GetStream(Image image)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
|
||||
image.Save(ms, ImageFormat.Png);
|
||||
|
||||
ms.Position = 0;
|
||||
|
||||
return ms;
|
||||
}
|
||||
}
|
||||
}
|
254
Emby.Drawing/GDI/GDIImageEncoder.cs
Normal file
254
Emby.Drawing/GDI/GDIImageEncoder.cs
Normal file
|
@ -0,0 +1,254 @@
|
|||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using ImageFormat = MediaBrowser.Model.Drawing.ImageFormat;
|
||||
|
||||
namespace Emby.Drawing.GDI
|
||||
{
|
||||
public class GDIImageEncoder : IImageEncoder
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public GDIImageEncoder(IFileSystem fileSystem, ILogger logger)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string[] SupportedInputFormats
|
||||
{
|
||||
get
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
"png",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"gif",
|
||||
"bmp"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public ImageFormat[] SupportedOutputFormats
|
||||
{
|
||||
get
|
||||
{
|
||||
return new[] { ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png };
|
||||
}
|
||||
}
|
||||
|
||||
public ImageSize GetImageSize(string path)
|
||||
{
|
||||
using (var image = Image.FromFile(path))
|
||||
{
|
||||
return new ImageSize
|
||||
{
|
||||
Width = image.Width,
|
||||
Height = image.Height
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public void CropWhiteSpace(string inputPath, string outputPath)
|
||||
{
|
||||
using (var image = (Bitmap)Image.FromFile(inputPath))
|
||||
{
|
||||
using (var croppedImage = image.CropWhitespace())
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
||||
|
||||
using (var outputStream = _fileSystem.GetFileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.Read, false))
|
||||
{
|
||||
croppedImage.Save(System.Drawing.Imaging.ImageFormat.Png, outputStream, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void EncodeImage(string inputPath, string cacheFilePath, int width, int height, int quality, ImageProcessingOptions options)
|
||||
{
|
||||
var hasPostProcessing = !string.IsNullOrEmpty(options.BackgroundColor) || options.UnplayedCount.HasValue || options.AddPlayedIndicator || options.PercentPlayed > 0;
|
||||
|
||||
using (var originalImage = Image.FromFile(inputPath))
|
||||
{
|
||||
var newWidth = Convert.ToInt32(width);
|
||||
var newHeight = Convert.ToInt32(height);
|
||||
|
||||
var selectedOutputFormat = options.OutputFormat;
|
||||
|
||||
// Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
|
||||
// Also, Webp only supports Format32bppArgb and Format32bppRgb
|
||||
var pixelFormat = selectedOutputFormat == ImageFormat.Webp
|
||||
? PixelFormat.Format32bppArgb
|
||||
: PixelFormat.Format32bppPArgb;
|
||||
|
||||
using (var thumbnail = new Bitmap(newWidth, newHeight, pixelFormat))
|
||||
{
|
||||
// Mono throw an exeception if assign 0 to SetResolution
|
||||
if (originalImage.HorizontalResolution > 0 && originalImage.VerticalResolution > 0)
|
||||
{
|
||||
// Preserve the original resolution
|
||||
thumbnail.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
|
||||
}
|
||||
|
||||
using (var thumbnailGraph = Graphics.FromImage(thumbnail))
|
||||
{
|
||||
thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
|
||||
thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
|
||||
thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||
thumbnailGraph.CompositingMode = !hasPostProcessing ?
|
||||
CompositingMode.SourceCopy :
|
||||
CompositingMode.SourceOver;
|
||||
|
||||
SetBackgroundColor(thumbnailGraph, options);
|
||||
|
||||
thumbnailGraph.DrawImage(originalImage, 0, 0, newWidth, newHeight);
|
||||
|
||||
DrawIndicator(thumbnailGraph, newWidth, newHeight, options);
|
||||
|
||||
var outputFormat = GetOutputFormat(originalImage, selectedOutputFormat);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
|
||||
|
||||
// Save to the cache location
|
||||
using (var cacheFileStream = _fileSystem.GetFileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, false))
|
||||
{
|
||||
// Save to the memory stream
|
||||
thumbnail.Save(outputFormat, cacheFileStream, quality);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the color of the background.
|
||||
/// </summary>
|
||||
/// <param name="graphics">The graphics.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
private void SetBackgroundColor(Graphics graphics, ImageProcessingOptions options)
|
||||
{
|
||||
var color = options.BackgroundColor;
|
||||
|
||||
if (!string.IsNullOrEmpty(color))
|
||||
{
|
||||
Color drawingColor;
|
||||
|
||||
try
|
||||
{
|
||||
drawingColor = ColorTranslator.FromHtml(color);
|
||||
}
|
||||
catch
|
||||
{
|
||||
drawingColor = ColorTranslator.FromHtml("#" + color);
|
||||
}
|
||||
|
||||
graphics.Clear(drawingColor);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the indicator.
|
||||
/// </summary>
|
||||
/// <param name="graphics">The graphics.</param>
|
||||
/// <param name="imageWidth">Width of the image.</param>
|
||||
/// <param name="imageHeight">Height of the image.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
private void DrawIndicator(Graphics graphics, int imageWidth, int imageHeight, ImageProcessingOptions options)
|
||||
{
|
||||
if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (options.AddPlayedIndicator)
|
||||
{
|
||||
var currentImageSize = new Size(imageWidth, imageHeight);
|
||||
|
||||
new PlayedIndicatorDrawer().DrawPlayedIndicator(graphics, currentImageSize);
|
||||
}
|
||||
else if (options.UnplayedCount.HasValue)
|
||||
{
|
||||
var currentImageSize = new Size(imageWidth, imageHeight);
|
||||
|
||||
new UnplayedCountIndicator().DrawUnplayedCountIndicator(graphics, currentImageSize, options.UnplayedCount.Value);
|
||||
}
|
||||
|
||||
if (options.PercentPlayed > 0)
|
||||
{
|
||||
var currentImageSize = new Size(imageWidth, imageHeight);
|
||||
|
||||
new PercentPlayedDrawer().Process(graphics, currentImageSize, options.PercentPlayed);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error drawing indicator overlay", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the output format.
|
||||
/// </summary>
|
||||
/// <param name="image">The image.</param>
|
||||
/// <param name="outputFormat">The output format.</param>
|
||||
/// <returns>ImageFormat.</returns>
|
||||
private System.Drawing.Imaging.ImageFormat GetOutputFormat(Image image, ImageFormat outputFormat)
|
||||
{
|
||||
switch (outputFormat)
|
||||
{
|
||||
case ImageFormat.Bmp:
|
||||
return System.Drawing.Imaging.ImageFormat.Bmp;
|
||||
case ImageFormat.Gif:
|
||||
return System.Drawing.Imaging.ImageFormat.Gif;
|
||||
case ImageFormat.Jpg:
|
||||
return System.Drawing.Imaging.ImageFormat.Jpeg;
|
||||
case ImageFormat.Png:
|
||||
return System.Drawing.Imaging.ImageFormat.Png;
|
||||
default:
|
||||
return image.RawFormat;
|
||||
}
|
||||
}
|
||||
|
||||
public void CreateImageCollage(ImageCollageOptions options)
|
||||
{
|
||||
double ratio = options.Width;
|
||||
ratio /= options.Height;
|
||||
|
||||
if (ratio >= 1.4)
|
||||
{
|
||||
DynamicImageHelpers.CreateThumbCollage(options.InputPaths.ToList(), _fileSystem, options.OutputPath, options.Width, options.Height);
|
||||
}
|
||||
else if (ratio >= .9)
|
||||
{
|
||||
DynamicImageHelpers.CreateSquareCollage(options.InputPaths.ToList(), _fileSystem, options.OutputPath, options.Width, options.Height);
|
||||
}
|
||||
else
|
||||
{
|
||||
DynamicImageHelpers.CreateSquareCollage(options.InputPaths.ToList(), _fileSystem, options.OutputPath, options.Width, options.Width);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return "GDI"; }
|
||||
}
|
||||
}
|
||||
}
|
217
Emby.Drawing/GDI/ImageExtensions.cs
Normal file
217
Emby.Drawing/GDI/ImageExtensions.cs
Normal file
|
@ -0,0 +1,217 @@
|
|||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
|
||||
namespace Emby.Drawing.GDI
|
||||
{
|
||||
public static class ImageExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves the image.
|
||||
/// </summary>
|
||||
/// <param name="outputFormat">The output format.</param>
|
||||
/// <param name="image">The image.</param>
|
||||
/// <param name="toStream">To stream.</param>
|
||||
/// <param name="quality">The quality.</param>
|
||||
public static void Save(this Image image, ImageFormat outputFormat, Stream toStream, int quality)
|
||||
{
|
||||
// Use special save methods for jpeg and png that will result in a much higher quality image
|
||||
// All other formats use the generic Image.Save
|
||||
if (ImageFormat.Jpeg.Equals(outputFormat))
|
||||
{
|
||||
SaveAsJpeg(image, toStream, quality);
|
||||
}
|
||||
else if (ImageFormat.Png.Equals(outputFormat))
|
||||
{
|
||||
image.Save(toStream, ImageFormat.Png);
|
||||
}
|
||||
else
|
||||
{
|
||||
image.Save(toStream, outputFormat);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the JPEG.
|
||||
/// </summary>
|
||||
/// <param name="image">The image.</param>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <param name="quality">The quality.</param>
|
||||
public static void SaveAsJpeg(this Image image, Stream target, int quality)
|
||||
{
|
||||
using (var encoderParameters = new EncoderParameters(1))
|
||||
{
|
||||
encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, quality);
|
||||
image.Save(target, GetImageCodecInfo("image/jpeg"), encoderParameters);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly ImageCodecInfo[] Encoders = ImageCodecInfo.GetImageEncoders();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image codec info.
|
||||
/// </summary>
|
||||
/// <param name="mimeType">Type of the MIME.</param>
|
||||
/// <returns>ImageCodecInfo.</returns>
|
||||
private static ImageCodecInfo GetImageCodecInfo(string mimeType)
|
||||
{
|
||||
foreach (var encoder in Encoders)
|
||||
{
|
||||
if (string.Equals(encoder.MimeType, mimeType, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return encoder;
|
||||
}
|
||||
}
|
||||
|
||||
return Encoders.Length == 0 ? null : Encoders[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Crops an image by removing whitespace and transparency from the edges
|
||||
/// </summary>
|
||||
/// <param name="bmp">The BMP.</param>
|
||||
/// <returns>Bitmap.</returns>
|
||||
/// <exception cref="System.Exception"></exception>
|
||||
public static Bitmap CropWhitespace(this Bitmap bmp)
|
||||
{
|
||||
var width = bmp.Width;
|
||||
var height = bmp.Height;
|
||||
|
||||
var topmost = 0;
|
||||
for (int row = 0; row < height; ++row)
|
||||
{
|
||||
if (IsAllWhiteRow(bmp, row, width))
|
||||
topmost = row;
|
||||
else break;
|
||||
}
|
||||
|
||||
int bottommost = 0;
|
||||
for (int row = height - 1; row >= 0; --row)
|
||||
{
|
||||
if (IsAllWhiteRow(bmp, row, width))
|
||||
bottommost = row;
|
||||
else break;
|
||||
}
|
||||
|
||||
int leftmost = 0, rightmost = 0;
|
||||
for (int col = 0; col < width; ++col)
|
||||
{
|
||||
if (IsAllWhiteColumn(bmp, col, height))
|
||||
leftmost = col;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
for (int col = width - 1; col >= 0; --col)
|
||||
{
|
||||
if (IsAllWhiteColumn(bmp, col, height))
|
||||
rightmost = col;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
if (rightmost == 0) rightmost = width; // As reached left
|
||||
if (bottommost == 0) bottommost = height; // As reached top.
|
||||
|
||||
var croppedWidth = rightmost - leftmost;
|
||||
var croppedHeight = bottommost - topmost;
|
||||
|
||||
if (croppedWidth == 0) // No border on left or right
|
||||
{
|
||||
leftmost = 0;
|
||||
croppedWidth = width;
|
||||
}
|
||||
|
||||
if (croppedHeight == 0) // No border on top or bottom
|
||||
{
|
||||
topmost = 0;
|
||||
croppedHeight = height;
|
||||
}
|
||||
|
||||
// Graphics.FromImage will throw an exception if the PixelFormat is Indexed, so we need to handle that here
|
||||
var thumbnail = new Bitmap(croppedWidth, croppedHeight, PixelFormat.Format32bppPArgb);
|
||||
|
||||
// Preserve the original resolution
|
||||
TrySetResolution(thumbnail, bmp.HorizontalResolution, bmp.VerticalResolution);
|
||||
|
||||
using (var thumbnailGraph = Graphics.FromImage(thumbnail))
|
||||
{
|
||||
thumbnailGraph.CompositingQuality = CompositingQuality.HighQuality;
|
||||
thumbnailGraph.SmoothingMode = SmoothingMode.HighQuality;
|
||||
thumbnailGraph.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
thumbnailGraph.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||
thumbnailGraph.CompositingMode = CompositingMode.SourceCopy;
|
||||
|
||||
thumbnailGraph.DrawImage(bmp,
|
||||
new RectangleF(0, 0, croppedWidth, croppedHeight),
|
||||
new RectangleF(leftmost, topmost, croppedWidth, croppedHeight),
|
||||
GraphicsUnit.Pixel);
|
||||
}
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries the set resolution.
|
||||
/// </summary>
|
||||
/// <param name="bmp">The BMP.</param>
|
||||
/// <param name="x">The x.</param>
|
||||
/// <param name="y">The y.</param>
|
||||
private static void TrySetResolution(Bitmap bmp, float x, float y)
|
||||
{
|
||||
if (x > 0 && y > 0)
|
||||
{
|
||||
bmp.SetResolution(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether or not a row of pixels is all whitespace
|
||||
/// </summary>
|
||||
/// <param name="bmp">The BMP.</param>
|
||||
/// <param name="row">The row.</param>
|
||||
/// <param name="width">The width.</param>
|
||||
/// <returns><c>true</c> if [is all white row] [the specified BMP]; otherwise, <c>false</c>.</returns>
|
||||
private static bool IsAllWhiteRow(Bitmap bmp, int row, int width)
|
||||
{
|
||||
for (var i = 0; i < width; ++i)
|
||||
{
|
||||
if (!IsWhiteSpace(bmp.GetPixel(i, row)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether or not a column of pixels is all whitespace
|
||||
/// </summary>
|
||||
/// <param name="bmp">The BMP.</param>
|
||||
/// <param name="col">The col.</param>
|
||||
/// <param name="height">The height.</param>
|
||||
/// <returns><c>true</c> if [is all white column] [the specified BMP]; otherwise, <c>false</c>.</returns>
|
||||
private static bool IsAllWhiteColumn(Bitmap bmp, int col, int height)
|
||||
{
|
||||
for (var i = 0; i < height; ++i)
|
||||
{
|
||||
if (!IsWhiteSpace(bmp.GetPixel(col, i)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a color is whitespace
|
||||
/// </summary>
|
||||
/// <param name="color">The color.</param>
|
||||
/// <returns><c>true</c> if [is white space] [the specified color]; otherwise, <c>false</c>.</returns>
|
||||
private static bool IsWhiteSpace(Color color)
|
||||
{
|
||||
return (color.R == 255 && color.G == 255 && color.B == 255) || color.A == 0;
|
||||
}
|
||||
}
|
||||
}
|
34
Emby.Drawing/GDI/PercentPlayedDrawer.cs
Normal file
34
Emby.Drawing/GDI/PercentPlayedDrawer.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
namespace Emby.Drawing.GDI
|
||||
{
|
||||
public class PercentPlayedDrawer
|
||||
{
|
||||
private const int IndicatorHeight = 8;
|
||||
|
||||
public void Process(Graphics graphics, Size imageSize, double percent)
|
||||
{
|
||||
var y = imageSize.Height - IndicatorHeight;
|
||||
|
||||
using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 0, 0, 0)))
|
||||
{
|
||||
const int innerX = 0;
|
||||
var innerY = y;
|
||||
var innerWidth = imageSize.Width;
|
||||
var innerHeight = imageSize.Height;
|
||||
|
||||
graphics.FillRectangle(backdroundBrush, innerX, innerY, innerWidth, innerHeight);
|
||||
|
||||
using (var foregroundBrush = new SolidBrush(Color.FromArgb(82, 181, 75)))
|
||||
{
|
||||
double foregroundWidth = innerWidth;
|
||||
foregroundWidth *= percent;
|
||||
foregroundWidth /= 100;
|
||||
|
||||
graphics.FillRectangle(foregroundBrush, innerX, innerY, Convert.ToInt32(Math.Round(foregroundWidth)), innerHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
32
Emby.Drawing/GDI/PlayedIndicatorDrawer.cs
Normal file
32
Emby.Drawing/GDI/PlayedIndicatorDrawer.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using System.Drawing;
|
||||
|
||||
namespace Emby.Drawing.GDI
|
||||
{
|
||||
public class PlayedIndicatorDrawer
|
||||
{
|
||||
private const int IndicatorHeight = 40;
|
||||
public const int IndicatorWidth = 40;
|
||||
private const int FontSize = 40;
|
||||
private const int OffsetFromTopRightCorner = 10;
|
||||
|
||||
public void DrawPlayedIndicator(Graphics graphics, Size imageSize)
|
||||
{
|
||||
var x = imageSize.Width - IndicatorWidth - OffsetFromTopRightCorner;
|
||||
|
||||
using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 82, 181, 75)))
|
||||
{
|
||||
graphics.FillEllipse(backdroundBrush, x, OffsetFromTopRightCorner, IndicatorWidth, IndicatorHeight);
|
||||
|
||||
x = imageSize.Width - 45 - OffsetFromTopRightCorner;
|
||||
|
||||
using (var font = new Font("Webdings", FontSize, FontStyle.Regular, GraphicsUnit.Pixel))
|
||||
{
|
||||
using (var fontBrush = new SolidBrush(Color.White))
|
||||
{
|
||||
graphics.DrawString("a", font, fontBrush, x, OffsetFromTopRightCorner - 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
50
Emby.Drawing/GDI/UnplayedCountIndicator.cs
Normal file
50
Emby.Drawing/GDI/UnplayedCountIndicator.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
using System.Drawing;
|
||||
|
||||
namespace Emby.Drawing.GDI
|
||||
{
|
||||
public class UnplayedCountIndicator
|
||||
{
|
||||
private const int IndicatorHeight = 41;
|
||||
public const int IndicatorWidth = 41;
|
||||
private const int OffsetFromTopRightCorner = 10;
|
||||
|
||||
public void DrawUnplayedCountIndicator(Graphics graphics, Size imageSize, int count)
|
||||
{
|
||||
var x = imageSize.Width - IndicatorWidth - OffsetFromTopRightCorner;
|
||||
|
||||
using (var backdroundBrush = new SolidBrush(Color.FromArgb(225, 82, 181, 75)))
|
||||
{
|
||||
graphics.FillEllipse(backdroundBrush, x, OffsetFromTopRightCorner, IndicatorWidth, IndicatorHeight);
|
||||
|
||||
var text = count.ToString();
|
||||
|
||||
x = imageSize.Width - IndicatorWidth - OffsetFromTopRightCorner;
|
||||
var y = OffsetFromTopRightCorner + 6;
|
||||
var fontSize = 24;
|
||||
|
||||
if (text.Length == 1)
|
||||
{
|
||||
x += 10;
|
||||
}
|
||||
else if (text.Length == 2)
|
||||
{
|
||||
x += 3;
|
||||
}
|
||||
else if (text.Length == 3)
|
||||
{
|
||||
x += 1;
|
||||
y += 1;
|
||||
fontSize = 20;
|
||||
}
|
||||
|
||||
using (var font = new Font("Sans-Serif", fontSize, FontStyle.Regular, GraphicsUnit.Pixel))
|
||||
{
|
||||
using (var fontBrush = new SolidBrush(Color.White))
|
||||
{
|
||||
graphics.DrawString(text, font, fontBrush, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
53
Emby.Drawing/IImageEncoder.cs
Normal file
53
Emby.Drawing/IImageEncoder.cs
Normal file
|
@ -0,0 +1,53 @@
|
|||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using System;
|
||||
|
||||
namespace Emby.Drawing
|
||||
{
|
||||
public interface IImageEncoder : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the supported input formats.
|
||||
/// </summary>
|
||||
/// <value>The supported input formats.</value>
|
||||
string[] SupportedInputFormats { get; }
|
||||
/// <summary>
|
||||
/// Gets the supported output formats.
|
||||
/// </summary>
|
||||
/// <value>The supported output formats.</value>
|
||||
ImageFormat[] SupportedOutputFormats { get; }
|
||||
/// <summary>
|
||||
/// Gets the size of the image.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>ImageSize.</returns>
|
||||
ImageSize GetImageSize(string path);
|
||||
/// <summary>
|
||||
/// Crops the white space.
|
||||
/// </summary>
|
||||
/// <param name="inputPath">The input path.</param>
|
||||
/// <param name="outputPath">The output path.</param>
|
||||
void CropWhiteSpace(string inputPath, string outputPath);
|
||||
/// <summary>
|
||||
/// Encodes the image.
|
||||
/// </summary>
|
||||
/// <param name="inputPath">The input path.</param>
|
||||
/// <param name="outputPath">The output path.</param>
|
||||
/// <param name="width">The width.</param>
|
||||
/// <param name="height">The height.</param>
|
||||
/// <param name="quality">The quality.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
void EncodeImage(string inputPath, string outputPath, int width, int height, int quality, ImageProcessingOptions options);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the image collage.
|
||||
/// </summary>
|
||||
/// <param name="options">The options.</param>
|
||||
void CreateImageCollage(ImageCollageOptions options);
|
||||
/// <summary>
|
||||
/// Gets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
string Name { get; }
|
||||
}
|
||||
}
|
229
Emby.Drawing/ImageMagick/ImageMagickEncoder.cs
Normal file
229
Emby.Drawing/ImageMagick/ImageMagickEncoder.cs
Normal file
|
@ -0,0 +1,229 @@
|
|||
using ImageMagickSharp;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Emby.Drawing.ImageMagick
|
||||
{
|
||||
public class ImageMagickEncoder : IImageEncoder
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
|
||||
public ImageMagickEncoder(ILogger logger, IApplicationPaths appPaths)
|
||||
{
|
||||
_logger = logger;
|
||||
_appPaths = appPaths;
|
||||
|
||||
LogImageMagickVersion();
|
||||
}
|
||||
|
||||
public string[] SupportedInputFormats
|
||||
{
|
||||
get
|
||||
{
|
||||
// Some common file name extensions for RAW picture files include: .cr2, .crw, .dng, .nef, .orf, .rw2, .pef, .arw, .sr2, .srf, and .tif.
|
||||
return new[]
|
||||
{
|
||||
"tiff",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"png",
|
||||
"aiff",
|
||||
"cr2",
|
||||
"crw",
|
||||
"dng",
|
||||
"nef",
|
||||
"orf",
|
||||
"pef",
|
||||
"arw",
|
||||
"webp",
|
||||
"gif",
|
||||
"bmp"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public ImageFormat[] SupportedOutputFormats
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_webpAvailable)
|
||||
{
|
||||
return new[] { ImageFormat.Webp, ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png };
|
||||
}
|
||||
return new[] { ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png };
|
||||
}
|
||||
}
|
||||
|
||||
private void LogImageMagickVersion()
|
||||
{
|
||||
_logger.Info("ImageMagick version: " + Wand.VersionString);
|
||||
TestWebp();
|
||||
}
|
||||
|
||||
private bool _webpAvailable = true;
|
||||
private void TestWebp()
|
||||
{
|
||||
try
|
||||
{
|
||||
var tmpPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".webp");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(tmpPath));
|
||||
|
||||
using (var wand = new MagickWand(1, 1, new PixelWand("none", 1)))
|
||||
{
|
||||
wand.SaveImage(tmpPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error loading webp: ", ex);
|
||||
_webpAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void CropWhiteSpace(string inputPath, string outputPath)
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
using (var wand = new MagickWand(inputPath))
|
||||
{
|
||||
wand.CurrentImage.TrimImage(10);
|
||||
wand.SaveImage(outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
public ImageSize GetImageSize(string path)
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
using (var wand = new MagickWand())
|
||||
{
|
||||
wand.PingImage(path);
|
||||
var img = wand.CurrentImage;
|
||||
|
||||
return new ImageSize
|
||||
{
|
||||
Width = img.Width,
|
||||
Height = img.Height
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public void EncodeImage(string inputPath, string outputPath, int width, int height, int quality, ImageProcessingOptions options)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.BackgroundColor))
|
||||
{
|
||||
using (var originalImage = new MagickWand(inputPath))
|
||||
{
|
||||
originalImage.CurrentImage.ResizeImage(width, height);
|
||||
|
||||
DrawIndicator(originalImage, width, height, options);
|
||||
|
||||
originalImage.CurrentImage.CompressionQuality = quality;
|
||||
|
||||
originalImage.SaveImage(outputPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var wand = new MagickWand(width, height, options.BackgroundColor))
|
||||
{
|
||||
using (var originalImage = new MagickWand(inputPath))
|
||||
{
|
||||
originalImage.CurrentImage.ResizeImage(width, height);
|
||||
|
||||
wand.CurrentImage.CompositeImage(originalImage, CompositeOperator.OverCompositeOp, 0, 0);
|
||||
DrawIndicator(wand, width, height, options);
|
||||
|
||||
wand.CurrentImage.CompressionQuality = quality;
|
||||
|
||||
wand.SaveImage(outputPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the indicator.
|
||||
/// </summary>
|
||||
/// <param name="wand">The wand.</param>
|
||||
/// <param name="imageWidth">Width of the image.</param>
|
||||
/// <param name="imageHeight">Height of the image.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
private void DrawIndicator(MagickWand wand, int imageWidth, int imageHeight, ImageProcessingOptions options)
|
||||
{
|
||||
if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (options.AddPlayedIndicator)
|
||||
{
|
||||
var currentImageSize = new ImageSize(imageWidth, imageHeight);
|
||||
|
||||
new PlayedIndicatorDrawer(_appPaths).DrawPlayedIndicator(wand, currentImageSize);
|
||||
}
|
||||
else if (options.UnplayedCount.HasValue)
|
||||
{
|
||||
var currentImageSize = new ImageSize(imageWidth, imageHeight);
|
||||
|
||||
new UnplayedCountIndicator(_appPaths).DrawUnplayedCountIndicator(wand, currentImageSize, options.UnplayedCount.Value);
|
||||
}
|
||||
|
||||
if (options.PercentPlayed > 0)
|
||||
{
|
||||
new PercentPlayedDrawer().Process(wand, options.PercentPlayed);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error drawing indicator overlay", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void CreateImageCollage(ImageCollageOptions options)
|
||||
{
|
||||
double ratio = options.Width;
|
||||
ratio /= options.Height;
|
||||
|
||||
if (ratio >= 1.4)
|
||||
{
|
||||
new StripCollageBuilder(_appPaths).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, options.Text);
|
||||
}
|
||||
else if (ratio >= .9)
|
||||
{
|
||||
new StripCollageBuilder(_appPaths).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, options.Text);
|
||||
}
|
||||
else
|
||||
{
|
||||
new StripCollageBuilder(_appPaths).BuildPosterCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, options.Text);
|
||||
}
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return "ImageMagick"; }
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
Wand.CloseEnvironment();
|
||||
}
|
||||
|
||||
private void CheckDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(GetType().Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
using ImageMagickSharp;
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Drawing
|
||||
namespace Emby.Drawing.ImageMagick
|
||||
{
|
||||
public class PercentPlayedDrawer
|
||||
{
|
|
@ -4,7 +4,7 @@ using MediaBrowser.Model.Drawing;
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Drawing
|
||||
namespace Emby.Drawing.ImageMagick
|
||||
{
|
||||
public class PlayedIndicatorDrawer
|
||||
{
|
518
Emby.Drawing/ImageMagick/StripCollageBuilder.cs
Normal file
518
Emby.Drawing/ImageMagick/StripCollageBuilder.cs
Normal file
|
@ -0,0 +1,518 @@
|
|||
using ImageMagickSharp;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Emby.Drawing.ImageMagick
|
||||
{
|
||||
public class StripCollageBuilder
|
||||
{
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
|
||||
public StripCollageBuilder(IApplicationPaths appPaths)
|
||||
{
|
||||
_appPaths = appPaths;
|
||||
}
|
||||
|
||||
public void BuildPosterCollage(IEnumerable<string> paths, string outputPath, int width, int height, string text)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
using (var wand = BuildPosterCollageWandWithText(paths, text, width, height))
|
||||
{
|
||||
wand.SaveImage(outputPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var wand = BuildPosterCollageWand(paths, width, height))
|
||||
{
|
||||
wand.SaveImage(outputPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void BuildSquareCollage(IEnumerable<string> paths, string outputPath, int width, int height, string text)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
using (var wand = BuildSquareCollageWandWithText(paths, text, width, height))
|
||||
{
|
||||
wand.SaveImage(outputPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var wand = BuildSquareCollageWand(paths, width, height))
|
||||
{
|
||||
wand.SaveImage(outputPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void BuildThumbCollage(IEnumerable<string> paths, string outputPath, int width, int height, string text)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
using (var wand = BuildThumbCollageWandWithText(paths, text, width, height))
|
||||
{
|
||||
wand.SaveImage(outputPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var wand = BuildThumbCollageWand(paths, width, height))
|
||||
{
|
||||
wand.SaveImage(outputPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static string[] ProjectPaths(IEnumerable<string> paths, int count)
|
||||
{
|
||||
var clone = paths.ToList();
|
||||
var list = new List<string>();
|
||||
|
||||
while (list.Count < count)
|
||||
{
|
||||
foreach (var path in clone)
|
||||
{
|
||||
list.Add(path);
|
||||
|
||||
if (list.Count >= count)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list.Take(count).ToArray();
|
||||
}
|
||||
|
||||
private MagickWand BuildThumbCollageWandWithText(IEnumerable<string> paths, string text, int width, int height)
|
||||
{
|
||||
var inputPaths = ProjectPaths(paths, 8);
|
||||
using (var wandImages = new MagickWand(inputPaths))
|
||||
{
|
||||
var wand = new MagickWand(width, height);
|
||||
wand.OpenImage("gradient:#111111-#111111");
|
||||
using (var draw = new DrawingWand())
|
||||
{
|
||||
using (var fcolor = new PixelWand(ColorName.White))
|
||||
{
|
||||
draw.FillColor = fcolor;
|
||||
draw.Font = MontserratLightFont;
|
||||
draw.FontSize = 60;
|
||||
draw.FontWeight = FontWeightType.LightStyle;
|
||||
draw.TextAntialias = true;
|
||||
}
|
||||
|
||||
var fontMetrics = wand.QueryFontMetrics(draw, text);
|
||||
var textContainerY = Convert.ToInt32(height * .165);
|
||||
wand.CurrentImage.AnnotateImage(draw, (width - fontMetrics.TextWidth) / 2, textContainerY, 0.0, text);
|
||||
|
||||
var iSlice = Convert.ToInt32(width * .1166666667);
|
||||
int iTrans = Convert.ToInt32(height * 0.2);
|
||||
int iHeight = Convert.ToInt32(height * 0.46296296296296296296296296296296);
|
||||
var horizontalImagePadding = Convert.ToInt32(width * 0.0125);
|
||||
|
||||
foreach (var element in wandImages.ImageList)
|
||||
{
|
||||
int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height);
|
||||
element.Gravity = GravityType.CenterGravity;
|
||||
element.BackgroundColor = new PixelWand("none", 1);
|
||||
element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter);
|
||||
int ix = (int)Math.Abs((iWidth - iSlice) / 2);
|
||||
element.CropImage(iSlice, iHeight, ix, 0);
|
||||
|
||||
element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0);
|
||||
}
|
||||
|
||||
wandImages.SetFirstIterator();
|
||||
using (var wandList = wandImages.AppendImages())
|
||||
{
|
||||
wandList.CurrentImage.TrimImage(1);
|
||||
using (var mwr = wandList.CloneMagickWand())
|
||||
{
|
||||
using (var blackPixelWand = new PixelWand(ColorName.Black))
|
||||
{
|
||||
using (var greyPixelWand = new PixelWand(ColorName.Grey70))
|
||||
{
|
||||
mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1);
|
||||
mwr.CurrentImage.FlipImage();
|
||||
|
||||
mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel;
|
||||
mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand);
|
||||
|
||||
using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans))
|
||||
{
|
||||
mwg.OpenImage("gradient:black-none");
|
||||
var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
|
||||
mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.DstInCompositeOp, 0, verticalSpacing);
|
||||
|
||||
wandList.AddImage(mwr);
|
||||
int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2;
|
||||
wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * 0.26851851851851851851851851851852));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return wand;
|
||||
}
|
||||
}
|
||||
|
||||
private MagickWand BuildPosterCollageWand(IEnumerable<string> paths, int width, int height)
|
||||
{
|
||||
var inputPaths = ProjectPaths(paths, 4);
|
||||
using (var wandImages = new MagickWand(inputPaths))
|
||||
{
|
||||
var wand = new MagickWand(width, height);
|
||||
wand.OpenImage("gradient:#111111-#111111");
|
||||
using (var draw = new DrawingWand())
|
||||
{
|
||||
var iSlice = Convert.ToInt32(width * 0.225);
|
||||
int iTrans = Convert.ToInt32(height * .25);
|
||||
int iHeight = Convert.ToInt32(height * .65);
|
||||
var horizontalImagePadding = Convert.ToInt32(width * 0.0275);
|
||||
|
||||
foreach (var element in wandImages.ImageList)
|
||||
{
|
||||
using (var blackPixelWand = new PixelWand(ColorName.Black))
|
||||
{
|
||||
int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height);
|
||||
element.Gravity = GravityType.CenterGravity;
|
||||
element.BackgroundColor = blackPixelWand;
|
||||
element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter);
|
||||
int ix = (int)Math.Abs((iWidth - iSlice) / 2);
|
||||
element.CropImage(iSlice, iHeight, ix, 0);
|
||||
|
||||
element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0);
|
||||
}
|
||||
}
|
||||
|
||||
wandImages.SetFirstIterator();
|
||||
using (var wandList = wandImages.AppendImages())
|
||||
{
|
||||
wandList.CurrentImage.TrimImage(1);
|
||||
using (var mwr = wandList.CloneMagickWand())
|
||||
{
|
||||
using (var blackPixelWand = new PixelWand(ColorName.Black))
|
||||
{
|
||||
using (var greyPixelWand = new PixelWand(ColorName.Grey70))
|
||||
{
|
||||
mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1);
|
||||
mwr.CurrentImage.FlipImage();
|
||||
|
||||
mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel;
|
||||
mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand);
|
||||
|
||||
using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans))
|
||||
{
|
||||
mwg.OpenImage("gradient:black-none");
|
||||
var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
|
||||
mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.CopyOpacityCompositeOp, 0, verticalSpacing);
|
||||
|
||||
wandList.AddImage(mwr);
|
||||
int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2;
|
||||
wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * .05));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return wand;
|
||||
}
|
||||
}
|
||||
|
||||
private MagickWand BuildPosterCollageWandWithText(IEnumerable<string> paths, string label, int width, int height)
|
||||
{
|
||||
var inputPaths = ProjectPaths(paths, 4);
|
||||
using (var wandImages = new MagickWand(inputPaths))
|
||||
{
|
||||
var wand = new MagickWand(width, height);
|
||||
wand.OpenImage("gradient:#111111-#111111");
|
||||
using (var draw = new DrawingWand())
|
||||
{
|
||||
using (var fcolor = new PixelWand(ColorName.White))
|
||||
{
|
||||
draw.FillColor = fcolor;
|
||||
draw.Font = MontserratLightFont;
|
||||
draw.FontSize = 60;
|
||||
draw.FontWeight = FontWeightType.LightStyle;
|
||||
draw.TextAntialias = true;
|
||||
}
|
||||
|
||||
var fontMetrics = wand.QueryFontMetrics(draw, label);
|
||||
var textContainerY = Convert.ToInt32(height * .165);
|
||||
wand.CurrentImage.AnnotateImage(draw, (width - fontMetrics.TextWidth) / 2, textContainerY, 0.0, label);
|
||||
|
||||
var iSlice = Convert.ToInt32(width * 0.225);
|
||||
int iTrans = Convert.ToInt32(height * 0.2);
|
||||
int iHeight = Convert.ToInt32(height * 0.46296296296296296296296296296296);
|
||||
var horizontalImagePadding = Convert.ToInt32(width * 0.0275);
|
||||
|
||||
foreach (var element in wandImages.ImageList)
|
||||
{
|
||||
int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height);
|
||||
element.Gravity = GravityType.CenterGravity;
|
||||
element.BackgroundColor = new PixelWand("none", 1);
|
||||
element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter);
|
||||
int ix = (int)Math.Abs((iWidth - iSlice) / 2);
|
||||
element.CropImage(iSlice, iHeight, ix, 0);
|
||||
|
||||
element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0);
|
||||
}
|
||||
|
||||
wandImages.SetFirstIterator();
|
||||
using (var wandList = wandImages.AppendImages())
|
||||
{
|
||||
wandList.CurrentImage.TrimImage(1);
|
||||
using (var mwr = wandList.CloneMagickWand())
|
||||
{
|
||||
using (var blackPixelWand = new PixelWand(ColorName.Black))
|
||||
{
|
||||
using (var greyPixelWand = new PixelWand(ColorName.Grey70))
|
||||
{
|
||||
mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1);
|
||||
mwr.CurrentImage.FlipImage();
|
||||
|
||||
mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel;
|
||||
mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand);
|
||||
|
||||
using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans))
|
||||
{
|
||||
mwg.OpenImage("gradient:black-none");
|
||||
var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
|
||||
mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.DstInCompositeOp, 0, verticalSpacing);
|
||||
|
||||
wandList.AddImage(mwr);
|
||||
int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2;
|
||||
wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * 0.26851851851851851851851851851852));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return wand;
|
||||
}
|
||||
}
|
||||
|
||||
private MagickWand BuildThumbCollageWand(IEnumerable<string> paths, int width, int height)
|
||||
{
|
||||
var inputPaths = ProjectPaths(paths, 8);
|
||||
using (var wandImages = new MagickWand(inputPaths))
|
||||
{
|
||||
var wand = new MagickWand(width, height);
|
||||
wand.OpenImage("gradient:#111111-#111111");
|
||||
using (var draw = new DrawingWand())
|
||||
{
|
||||
var iSlice = Convert.ToInt32(width * .1166666667);
|
||||
int iTrans = Convert.ToInt32(height * .25);
|
||||
int iHeight = Convert.ToInt32(height * .62);
|
||||
var horizontalImagePadding = Convert.ToInt32(width * 0.0125);
|
||||
|
||||
foreach (var element in wandImages.ImageList)
|
||||
{
|
||||
using (var blackPixelWand = new PixelWand(ColorName.Black))
|
||||
{
|
||||
int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height);
|
||||
element.Gravity = GravityType.CenterGravity;
|
||||
element.BackgroundColor = blackPixelWand;
|
||||
element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter);
|
||||
int ix = (int)Math.Abs((iWidth - iSlice) / 2);
|
||||
element.CropImage(iSlice, iHeight, ix, 0);
|
||||
|
||||
element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0);
|
||||
}
|
||||
}
|
||||
|
||||
wandImages.SetFirstIterator();
|
||||
using (var wandList = wandImages.AppendImages())
|
||||
{
|
||||
wandList.CurrentImage.TrimImage(1);
|
||||
using (var mwr = wandList.CloneMagickWand())
|
||||
{
|
||||
using (var blackPixelWand = new PixelWand(ColorName.Black))
|
||||
{
|
||||
using (var greyPixelWand = new PixelWand(ColorName.Grey70))
|
||||
{
|
||||
mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1);
|
||||
mwr.CurrentImage.FlipImage();
|
||||
|
||||
mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel;
|
||||
mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand);
|
||||
|
||||
using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans))
|
||||
{
|
||||
mwg.OpenImage("gradient:black-none");
|
||||
var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
|
||||
mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.CopyOpacityCompositeOp, 0, verticalSpacing);
|
||||
|
||||
wandList.AddImage(mwr);
|
||||
int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2;
|
||||
wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * .085));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return wand;
|
||||
}
|
||||
}
|
||||
|
||||
private MagickWand BuildSquareCollageWand(IEnumerable<string> paths, int width, int height)
|
||||
{
|
||||
var inputPaths = ProjectPaths(paths, 4);
|
||||
using (var wandImages = new MagickWand(inputPaths))
|
||||
{
|
||||
var wand = new MagickWand(width, height);
|
||||
wand.OpenImage("gradient:#111111-#111111");
|
||||
using (var draw = new DrawingWand())
|
||||
{
|
||||
var iSlice = Convert.ToInt32(width * .225);
|
||||
int iTrans = Convert.ToInt32(height * .25);
|
||||
int iHeight = Convert.ToInt32(height * .63);
|
||||
var horizontalImagePadding = Convert.ToInt32(width * 0.02);
|
||||
|
||||
foreach (var element in wandImages.ImageList)
|
||||
{
|
||||
using (var blackPixelWand = new PixelWand(ColorName.Black))
|
||||
{
|
||||
int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height);
|
||||
element.Gravity = GravityType.CenterGravity;
|
||||
element.BackgroundColor = blackPixelWand;
|
||||
element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter);
|
||||
int ix = (int)Math.Abs((iWidth - iSlice) / 2);
|
||||
element.CropImage(iSlice, iHeight, ix, 0);
|
||||
|
||||
element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0);
|
||||
}
|
||||
}
|
||||
|
||||
wandImages.SetFirstIterator();
|
||||
using (var wandList = wandImages.AppendImages())
|
||||
{
|
||||
wandList.CurrentImage.TrimImage(1);
|
||||
using (var mwr = wandList.CloneMagickWand())
|
||||
{
|
||||
using (var blackPixelWand = new PixelWand(ColorName.Black))
|
||||
{
|
||||
using (var greyPixelWand = new PixelWand(ColorName.Grey70))
|
||||
{
|
||||
mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1);
|
||||
mwr.CurrentImage.FlipImage();
|
||||
|
||||
mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel;
|
||||
mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand);
|
||||
|
||||
using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans))
|
||||
{
|
||||
mwg.OpenImage("gradient:black-none");
|
||||
var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
|
||||
mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.CopyOpacityCompositeOp, 0, verticalSpacing);
|
||||
|
||||
wandList.AddImage(mwr);
|
||||
int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2;
|
||||
wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * .07));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return wand;
|
||||
}
|
||||
}
|
||||
|
||||
private MagickWand BuildSquareCollageWandWithText(IEnumerable<string> paths, string label, int width, int height)
|
||||
{
|
||||
var inputPaths = ProjectPaths(paths, 4);
|
||||
using (var wandImages = new MagickWand(inputPaths))
|
||||
{
|
||||
var wand = new MagickWand(width, height);
|
||||
wand.OpenImage("gradient:#111111-#111111");
|
||||
using (var draw = new DrawingWand())
|
||||
{
|
||||
using (var fcolor = new PixelWand(ColorName.White))
|
||||
{
|
||||
draw.FillColor = fcolor;
|
||||
draw.Font = MontserratLightFont;
|
||||
draw.FontSize = 60;
|
||||
draw.FontWeight = FontWeightType.LightStyle;
|
||||
draw.TextAntialias = true;
|
||||
}
|
||||
|
||||
var fontMetrics = wand.QueryFontMetrics(draw, label);
|
||||
var textContainerY = Convert.ToInt32(height * .165);
|
||||
wand.CurrentImage.AnnotateImage(draw, (width - fontMetrics.TextWidth) / 2, textContainerY, 0.0, label);
|
||||
|
||||
var iSlice = Convert.ToInt32(width * .225);
|
||||
int iTrans = Convert.ToInt32(height * 0.2);
|
||||
int iHeight = Convert.ToInt32(height * 0.46296296296296296296296296296296);
|
||||
var horizontalImagePadding = Convert.ToInt32(width * 0.02);
|
||||
|
||||
foreach (var element in wandImages.ImageList)
|
||||
{
|
||||
int iWidth = (int)Math.Abs(iHeight * element.Width / element.Height);
|
||||
element.Gravity = GravityType.CenterGravity;
|
||||
element.BackgroundColor = new PixelWand("none", 1);
|
||||
element.ResizeImage(iWidth, iHeight, FilterTypes.LanczosFilter);
|
||||
int ix = (int)Math.Abs((iWidth - iSlice) / 2);
|
||||
element.CropImage(iSlice, iHeight, ix, 0);
|
||||
|
||||
element.ExtentImage(iSlice, iHeight, 0 - horizontalImagePadding, 0);
|
||||
}
|
||||
|
||||
wandImages.SetFirstIterator();
|
||||
using (var wandList = wandImages.AppendImages())
|
||||
{
|
||||
wandList.CurrentImage.TrimImage(1);
|
||||
using (var mwr = wandList.CloneMagickWand())
|
||||
{
|
||||
using (var blackPixelWand = new PixelWand(ColorName.Black))
|
||||
{
|
||||
using (var greyPixelWand = new PixelWand(ColorName.Grey70))
|
||||
{
|
||||
mwr.CurrentImage.ResizeImage(wandList.CurrentImage.Width, (wandList.CurrentImage.Height / 2), FilterTypes.LanczosFilter, 1);
|
||||
mwr.CurrentImage.FlipImage();
|
||||
|
||||
mwr.CurrentImage.AlphaChannel = AlphaChannelType.DeactivateAlphaChannel;
|
||||
mwr.CurrentImage.ColorizeImage(blackPixelWand, greyPixelWand);
|
||||
|
||||
using (var mwg = new MagickWand(wandList.CurrentImage.Width, iTrans))
|
||||
{
|
||||
mwg.OpenImage("gradient:black-none");
|
||||
var verticalSpacing = Convert.ToInt32(height * 0.01111111111111111111111111111111);
|
||||
mwr.CurrentImage.CompositeImage(mwg, CompositeOperator.DstInCompositeOp, 0, verticalSpacing);
|
||||
|
||||
wandList.AddImage(mwr);
|
||||
int ex = (int)(wand.CurrentImage.Width - mwg.CurrentImage.Width) / 2;
|
||||
wand.CurrentImage.CompositeImage(wandList.AppendImages(true), CompositeOperator.AtopCompositeOp, ex, Convert.ToInt32(height * 0.26851851851851851851851851851852));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return wand;
|
||||
}
|
||||
}
|
||||
|
||||
private string MontserratLightFont
|
||||
{
|
||||
get { return PlayedIndicatorDrawer.ExtractFont("MontserratLight.otf", _appPaths); }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ using MediaBrowser.Common.Configuration;
|
|||
using MediaBrowser.Model.Drawing;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Drawing
|
||||
namespace Emby.Drawing.ImageMagick
|
||||
{
|
||||
public class UnplayedCountIndicator
|
||||
{
|
|
@ -1,4 +1,4 @@
|
|||
using ImageMagickSharp;
|
||||
using Emby.Drawing.Common;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller;
|
||||
|
@ -18,7 +18,7 @@ using System.Linq;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Drawing
|
||||
namespace Emby.Drawing
|
||||
{
|
||||
/// <summary>
|
||||
/// Class ImageProcessor
|
||||
|
@ -50,12 +50,14 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IServerApplicationPaths _appPaths;
|
||||
private readonly IImageEncoder _imageEncoder;
|
||||
|
||||
public ImageProcessor(ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem, IJsonSerializer jsonSerializer)
|
||||
public ImageProcessor(ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IImageEncoder imageEncoder)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_imageEncoder = imageEncoder;
|
||||
_appPaths = appPaths;
|
||||
|
||||
_saveImageSizeTimer = new Timer(SaveImageSizeCallback, null, Timeout.Infinite, Timeout.Infinite);
|
||||
|
@ -85,8 +87,14 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||
}
|
||||
|
||||
_cachedImagedSizes = new ConcurrentDictionary<Guid, ImageSize>(sizeDictionary);
|
||||
}
|
||||
|
||||
LogImageMagickVersionVersion();
|
||||
public string[] SupportedInputFormats
|
||||
{
|
||||
get
|
||||
{
|
||||
return _imageEncoder.SupportedInputFormats;
|
||||
}
|
||||
}
|
||||
|
||||
private string ResizedImageCachePath
|
||||
|
@ -130,44 +138,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||
|
||||
public ImageFormat[] GetSupportedImageOutputFormats()
|
||||
{
|
||||
if (_webpAvailable)
|
||||
{
|
||||
return new[] { ImageFormat.Webp, ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png };
|
||||
}
|
||||
return new[] { ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png };
|
||||
}
|
||||
|
||||
private bool _webpAvailable = true;
|
||||
private void TestWebp()
|
||||
{
|
||||
try
|
||||
{
|
||||
var tmpPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".webp");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(tmpPath));
|
||||
|
||||
using (var wand = new MagickWand(1, 1, new PixelWand("none", 1)))
|
||||
{
|
||||
wand.SaveImage(tmpPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error loading webp: ", ex);
|
||||
_webpAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void LogImageMagickVersionVersion()
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Info("ImageMagick version: " + Wand.VersionString);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error loading ImageMagick: ", ex);
|
||||
}
|
||||
TestWebp();
|
||||
return _imageEncoder.SupportedOutputFormats;
|
||||
}
|
||||
|
||||
public async Task<string> ProcessImage(ImageProcessingOptions options)
|
||||
|
@ -244,36 +215,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.BackgroundColor))
|
||||
{
|
||||
using (var originalImage = new MagickWand(originalImagePath))
|
||||
{
|
||||
originalImage.CurrentImage.ResizeImage(newWidth, newHeight);
|
||||
|
||||
DrawIndicator(originalImage, newWidth, newHeight, options);
|
||||
|
||||
originalImage.CurrentImage.CompressionQuality = quality;
|
||||
|
||||
originalImage.SaveImage(cacheFilePath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var wand = new MagickWand(newWidth, newHeight, options.BackgroundColor))
|
||||
{
|
||||
using (var originalImage = new MagickWand(originalImagePath))
|
||||
{
|
||||
originalImage.CurrentImage.ResizeImage(newWidth, newHeight);
|
||||
|
||||
wand.CurrentImage.CompositeImage(originalImage, CompositeOperator.OverCompositeOp, 0, 0);
|
||||
DrawIndicator(wand, newWidth, newHeight, options);
|
||||
|
||||
wand.CurrentImage.CompressionQuality = quality;
|
||||
|
||||
wand.SaveImage(cacheFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
_imageEncoder.EncodeImage(originalImagePath, cacheFilePath, newWidth, newHeight, quality, options);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
@ -286,7 +228,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||
|
||||
private ImageFormat GetOutputFormat(ImageFormat requestedFormat)
|
||||
{
|
||||
if (requestedFormat == ImageFormat.Webp && !_webpAvailable)
|
||||
if (requestedFormat == ImageFormat.Webp && !_imageEncoder.SupportedOutputFormats.Contains(ImageFormat.Webp))
|
||||
{
|
||||
return ImageFormat.Png;
|
||||
}
|
||||
|
@ -294,46 +236,6 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||
return requestedFormat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the indicator.
|
||||
/// </summary>
|
||||
/// <param name="wand">The wand.</param>
|
||||
/// <param name="imageWidth">Width of the image.</param>
|
||||
/// <param name="imageHeight">Height of the image.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
private void DrawIndicator(MagickWand wand, int imageWidth, int imageHeight, ImageProcessingOptions options)
|
||||
{
|
||||
if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (options.AddPlayedIndicator)
|
||||
{
|
||||
var currentImageSize = new ImageSize(imageWidth, imageHeight);
|
||||
|
||||
new PlayedIndicatorDrawer(_appPaths).DrawPlayedIndicator(wand, currentImageSize);
|
||||
}
|
||||
else if (options.UnplayedCount.HasValue)
|
||||
{
|
||||
var currentImageSize = new ImageSize(imageWidth, imageHeight);
|
||||
|
||||
new UnplayedCountIndicator(_appPaths).DrawUnplayedCountIndicator(wand, currentImageSize, options.UnplayedCount.Value);
|
||||
}
|
||||
|
||||
if (options.PercentPlayed > 0)
|
||||
{
|
||||
new PercentPlayedDrawer().Process(wand, options.PercentPlayed);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error drawing indicator overlay", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Crops whitespace from an image, caches the result, and returns the cached path
|
||||
/// </summary>
|
||||
|
@ -360,11 +262,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(croppedImagePath));
|
||||
|
||||
using (var wand = new MagickWand(originalImagePath))
|
||||
{
|
||||
wand.CurrentImage.TrimImage(10);
|
||||
wand.SaveImage(croppedImagePath);
|
||||
}
|
||||
_imageEncoder.CropWhiteSpace(originalImagePath, croppedImagePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -500,17 +398,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||
|
||||
CheckDisposed();
|
||||
|
||||
using (var wand = new MagickWand())
|
||||
{
|
||||
wand.PingImage(path);
|
||||
var img = wand.CurrentImage;
|
||||
|
||||
size = new ImageSize
|
||||
{
|
||||
Width = img.Width,
|
||||
Height = img.Height
|
||||
};
|
||||
}
|
||||
size = _imageEncoder.GetImageSize(path);
|
||||
}
|
||||
|
||||
StartSaveImageSizeTimer();
|
||||
|
@ -838,6 +726,11 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||
return Path.Combine(path, filename);
|
||||
}
|
||||
|
||||
public void CreateImageCollage(ImageCollageOptions options)
|
||||
{
|
||||
_imageEncoder.CreateImageCollage(options);
|
||||
}
|
||||
|
||||
public IEnumerable<IImageEnhancer> GetSupportedEnhancers(IHasImages item, ImageType imageType)
|
||||
{
|
||||
return ImageEnhancers.Where(i =>
|
||||
|
@ -860,7 +753,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
Wand.CloseEnvironment();
|
||||
_imageEncoder.Dispose();
|
||||
_saveImageSizeTimer.Dispose();
|
||||
}
|
||||
|
31
Emby.Drawing/Properties/AssemblyInfo.cs
Normal file
31
Emby.Drawing/Properties/AssemblyInfo.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Emby.Drawing")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("Emby.Drawing")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2015")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("87b6f14e-16d8-4a58-a553-fd9945e47458")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
4
Emby.Drawing/packages.config
Normal file
4
Emby.Drawing/packages.config
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="ImageMagickSharp" version="1.0.0.14" targetFramework="net45" />
|
||||
</packages>
|
|
@ -151,7 +151,7 @@ namespace MediaBrowser.Api
|
|||
{
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
var job = new TranscodingJob
|
||||
var job = new TranscodingJob(Logger)
|
||||
{
|
||||
Type = type,
|
||||
Path = path,
|
||||
|
@ -284,28 +284,72 @@ namespace MediaBrowser.Api
|
|||
{
|
||||
job.ActiveRequestCount++;
|
||||
|
||||
job.DisposeKillTimer();
|
||||
if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
|
||||
{
|
||||
job.StopKillTimer();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void OnTranscodeEndRequest(TranscodingJob job)
|
||||
{
|
||||
job.ActiveRequestCount--;
|
||||
|
||||
if (job.ActiveRequestCount == 0)
|
||||
Logger.Debug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount);
|
||||
if (job.ActiveRequestCount <= 0)
|
||||
{
|
||||
// TODO: Lower this hls timeout
|
||||
var timerDuration = job.Type == TranscodingJobType.Progressive ?
|
||||
1000 :
|
||||
7200000;
|
||||
PingTimer(job, false);
|
||||
}
|
||||
}
|
||||
internal void PingTranscodingJob(string playSessionId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(playSessionId))
|
||||
{
|
||||
throw new ArgumentNullException("playSessionId");
|
||||
}
|
||||
|
||||
if (job.KillTimer == null)
|
||||
{
|
||||
job.KillTimer = new Timer(OnTranscodeKillTimerStopped, job, timerDuration, Timeout.Infinite);
|
||||
}
|
||||
else
|
||||
{
|
||||
job.KillTimer.Change(timerDuration, Timeout.Infinite);
|
||||
}
|
||||
Logger.Debug("PingTranscodingJob PlaySessionId={0}", playSessionId);
|
||||
|
||||
var jobs = new List<TranscodingJob>();
|
||||
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
// This is really only needed for HLS.
|
||||
// Progressive streams can stop on their own reliably
|
||||
jobs = jobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
|
||||
foreach (var job in jobs)
|
||||
{
|
||||
PingTimer(job, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
|
||||
{
|
||||
if (job.HasExited)
|
||||
{
|
||||
job.StopKillTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Lower this hls timeout
|
||||
var timerDuration = job.Type == TranscodingJobType.Progressive ?
|
||||
1000 :
|
||||
1800000;
|
||||
|
||||
// We can really reduce the timeout for apps that are using the newer api
|
||||
if (!string.IsNullOrWhiteSpace(job.PlaySessionId) && job.Type != TranscodingJobType.Progressive)
|
||||
{
|
||||
timerDuration = 20000;
|
||||
}
|
||||
|
||||
// Don't start the timer for playback checkins with progressive streaming
|
||||
if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
|
||||
{
|
||||
job.StartKillTimer(timerDuration, OnTranscodeKillTimerStopped);
|
||||
}
|
||||
else
|
||||
{
|
||||
job.ChangeKillTimerIfStarted(timerDuration);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -317,6 +361,8 @@ namespace MediaBrowser.Api
|
|||
{
|
||||
var job = (TranscodingJob)state;
|
||||
|
||||
Logger.Debug("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
|
||||
|
||||
KillTranscodingJob(job, path => true);
|
||||
}
|
||||
|
||||
|
@ -329,19 +375,14 @@ namespace MediaBrowser.Api
|
|||
/// <returns>Task.</returns>
|
||||
internal void KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles)
|
||||
{
|
||||
if (string.IsNullOrEmpty(deviceId))
|
||||
{
|
||||
throw new ArgumentNullException("deviceId");
|
||||
}
|
||||
|
||||
KillTranscodingJobs(j =>
|
||||
{
|
||||
if (string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase))
|
||||
if (!string.IsNullOrWhiteSpace(playSessionId))
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(playSessionId) || string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase);
|
||||
return string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return false;
|
||||
return string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
}, deleteFiles);
|
||||
}
|
||||
|
@ -381,6 +422,10 @@ namespace MediaBrowser.Api
|
|||
/// <param name="delete">The delete.</param>
|
||||
private void KillTranscodingJob(TranscodingJob job, Func<string, bool> delete)
|
||||
{
|
||||
job.DisposeKillTimer();
|
||||
|
||||
Logger.Debug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
|
||||
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
_activeTranscodingJobs.Remove(job);
|
||||
|
@ -389,34 +434,23 @@ namespace MediaBrowser.Api
|
|||
{
|
||||
job.CancellationTokenSource.Cancel();
|
||||
}
|
||||
|
||||
job.DisposeKillTimer();
|
||||
}
|
||||
|
||||
lock (job.ProcessLock)
|
||||
{
|
||||
if (job.TranscodingThrottler != null)
|
||||
{
|
||||
job.TranscodingThrottler.Stop();
|
||||
}
|
||||
|
||||
var process = job.Process;
|
||||
|
||||
var hasExited = true;
|
||||
|
||||
try
|
||||
{
|
||||
hasExited = process.HasExited;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorException("Error determining if ffmpeg process has exited for {0}", ex, job.Path);
|
||||
}
|
||||
var hasExited = job.HasExited;
|
||||
|
||||
if (!hasExited)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (job.TranscodingThrottler != null)
|
||||
{
|
||||
job.TranscodingThrottler.Stop();
|
||||
}
|
||||
|
||||
Logger.Info("Killing ffmpeg process for {0}", job.Path);
|
||||
|
||||
//process.Kill();
|
||||
|
@ -558,6 +592,7 @@ namespace MediaBrowser.Api
|
|||
/// </summary>
|
||||
/// <value>The process.</value>
|
||||
public Process Process { get; set; }
|
||||
public ILogger Logger { get; private set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the active request count.
|
||||
/// </summary>
|
||||
|
@ -567,7 +602,7 @@ namespace MediaBrowser.Api
|
|||
/// Gets or sets the kill timer.
|
||||
/// </summary>
|
||||
/// <value>The kill timer.</value>
|
||||
public Timer KillTimer { get; set; }
|
||||
private Timer KillTimer { get; set; }
|
||||
|
||||
public string DeviceId { get; set; }
|
||||
|
||||
|
@ -590,12 +625,74 @@ namespace MediaBrowser.Api
|
|||
|
||||
public TranscodingThrottler TranscodingThrottler { get; set; }
|
||||
|
||||
private readonly object _timerLock = new object();
|
||||
|
||||
public TranscodingJob(ILogger logger)
|
||||
{
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public void StopKillTimer()
|
||||
{
|
||||
lock (_timerLock)
|
||||
{
|
||||
if (KillTimer != null)
|
||||
{
|
||||
KillTimer.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void DisposeKillTimer()
|
||||
{
|
||||
if (KillTimer != null)
|
||||
lock (_timerLock)
|
||||
{
|
||||
KillTimer.Dispose();
|
||||
KillTimer = null;
|
||||
if (KillTimer != null)
|
||||
{
|
||||
KillTimer.Dispose();
|
||||
KillTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void StartKillTimer(int intervalMs, TimerCallback callback)
|
||||
{
|
||||
CheckHasExited();
|
||||
|
||||
lock (_timerLock)
|
||||
{
|
||||
if (KillTimer == null)
|
||||
{
|
||||
Logger.Debug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
|
||||
KillTimer = new Timer(callback, this, intervalMs, Timeout.Infinite);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Debug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
|
||||
KillTimer.Change(intervalMs, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ChangeKillTimerIfStarted(int intervalMs)
|
||||
{
|
||||
CheckHasExited();
|
||||
|
||||
lock (_timerLock)
|
||||
{
|
||||
if (KillTimer != null)
|
||||
{
|
||||
Logger.Debug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
|
||||
KillTimer.Change(intervalMs, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckHasExited()
|
||||
{
|
||||
if (HasExited)
|
||||
{
|
||||
throw new ObjectDisposedException("Job");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -259,7 +259,7 @@ namespace MediaBrowser.Api
|
|||
.GetRecursiveChildren(i => i is IHasArtist)
|
||||
.Cast<IHasArtist>()
|
||||
.SelectMany(i => i.AllArtists)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.FirstOrDefault(i =>
|
||||
{
|
||||
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
||||
|
@ -281,7 +281,7 @@ namespace MediaBrowser.Api
|
|||
|
||||
return libraryManager.RootFolder.GetRecursiveChildren()
|
||||
.SelectMany(i => i.Genres)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.FirstOrDefault(i =>
|
||||
{
|
||||
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
||||
|
@ -301,7 +301,7 @@ namespace MediaBrowser.Api
|
|||
return libraryManager.RootFolder
|
||||
.GetRecursiveChildren(i => i is Game)
|
||||
.SelectMany(i => i.Genres)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.FirstOrDefault(i =>
|
||||
{
|
||||
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
||||
|
@ -324,7 +324,7 @@ namespace MediaBrowser.Api
|
|||
return libraryManager.RootFolder
|
||||
.GetRecursiveChildren()
|
||||
.SelectMany(i => i.Studios)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.FirstOrDefault(i =>
|
||||
{
|
||||
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
||||
|
@ -348,7 +348,7 @@ namespace MediaBrowser.Api
|
|||
.GetRecursiveChildren()
|
||||
.SelectMany(i => i.People)
|
||||
.Select(i => i.Name)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.FirstOrDefault(i =>
|
||||
{
|
||||
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
||||
|
|
|
@ -123,7 +123,7 @@ namespace MediaBrowser.Api
|
|||
|
||||
public void Post(AutoSetMetadataOptions request)
|
||||
{
|
||||
_configurationManager.DisableMetadataService("Media Browser Xml");
|
||||
_configurationManager.DisableMetadataService("Emby Xml");
|
||||
_configurationManager.SaveConfiguration();
|
||||
}
|
||||
|
||||
|
|
|
@ -76,7 +76,7 @@ namespace MediaBrowser.Api
|
|||
.ToArray();
|
||||
|
||||
result.Genres = items.SelectMany(i => i.Genres)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.OrderBy(i => i)
|
||||
.ToArray();
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
@ -186,6 +187,9 @@ namespace MediaBrowser.Api.LiveTv
|
|||
[ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
|
||||
public bool? IsMovie { get; set; }
|
||||
|
||||
[ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
|
||||
public bool? IsSports { get; set; }
|
||||
|
||||
[ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? StartIndex { get; set; }
|
||||
|
||||
|
@ -218,6 +222,9 @@ namespace MediaBrowser.Api.LiveTv
|
|||
[ApiMember(Name = "HasAired", Description = "Optional. Filter by programs that have completed airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
|
||||
public bool? HasAired { get; set; }
|
||||
|
||||
[ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
|
||||
public bool? IsSports { get; set; }
|
||||
|
||||
[ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
|
||||
public bool? IsMovie { get; set; }
|
||||
}
|
||||
|
@ -422,11 +429,12 @@ namespace MediaBrowser.Api.LiveTv
|
|||
query.SortBy = (request.SortBy ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
query.SortOrder = request.SortOrder;
|
||||
query.IsMovie = request.IsMovie;
|
||||
query.IsSports = request.IsSports;
|
||||
query.Genres = (request.Genres ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var result = await _liveTvManager.GetPrograms(query, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public async Task<object> Get(GetRecommendedPrograms request)
|
||||
|
@ -437,12 +445,13 @@ namespace MediaBrowser.Api.LiveTv
|
|||
IsAiring = request.IsAiring,
|
||||
Limit = request.Limit,
|
||||
HasAired = request.HasAired,
|
||||
IsMovie = request.IsMovie
|
||||
IsMovie = request.IsMovie,
|
||||
IsSports = request.IsSports
|
||||
};
|
||||
|
||||
var result = await _liveTvManager.GetRecommendedPrograms(query, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public object Post(GetPrograms request)
|
||||
|
@ -452,6 +461,9 @@ namespace MediaBrowser.Api.LiveTv
|
|||
|
||||
public async Task<object> Get(GetRecordings request)
|
||||
{
|
||||
var options = new DtoOptions();
|
||||
options.DeviceId = AuthorizationContext.GetAuthorizationInfo(Request).DeviceId;
|
||||
|
||||
var result = await _liveTvManager.GetRecordings(new RecordingQuery
|
||||
{
|
||||
ChannelId = request.ChannelId,
|
||||
|
@ -463,16 +475,19 @@ namespace MediaBrowser.Api.LiveTv
|
|||
SeriesTimerId = request.SeriesTimerId,
|
||||
IsInProgress = request.IsInProgress
|
||||
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
}, options, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public async Task<object> Get(GetRecording request)
|
||||
{
|
||||
var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(request.UserId);
|
||||
|
||||
var result = await _liveTvManager.GetRecording(request.Id, CancellationToken.None, user).ConfigureAwait(false);
|
||||
var options = new DtoOptions();
|
||||
options.DeviceId = AuthorizationContext.GetAuthorizationInfo(Request).DeviceId;
|
||||
|
||||
var result = await _liveTvManager.GetRecording(request.Id, options, CancellationToken.None, user).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedSerializedResultUsingCache(result);
|
||||
}
|
||||
|
|
|
@ -410,7 +410,7 @@ namespace MediaBrowser.Api.Movies
|
|||
return items
|
||||
.SelectMany(i => i.People.Where(p => !string.Equals(PersonType.Director, p.Type, StringComparison.OrdinalIgnoreCase)).Take(2))
|
||||
.Select(i => i.Name)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
.DistinctNames();
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items)
|
||||
|
@ -419,7 +419,7 @@ namespace MediaBrowser.Api.Movies
|
|||
.Select(i => i.People.FirstOrDefault(p => string.Equals(PersonType.Director, p.Type, StringComparison.OrdinalIgnoreCase)))
|
||||
.Where(i => i != null)
|
||||
.Select(i => i.Name)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
.DistinctNames();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,12 +79,12 @@ namespace MediaBrowser.Api.Music
|
|||
|
||||
var artists1 = album1
|
||||
.AllArtists
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.ToList();
|
||||
|
||||
var artists2 = album2
|
||||
.AllArtists
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return points + artists1.Where(artists2.ContainsKey).Sum(i => 5);
|
||||
|
|
|
@ -1026,7 +1026,7 @@ namespace MediaBrowser.Api.Playback
|
|||
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
|
||||
state.LogFileStream = FileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true);
|
||||
|
||||
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(commandLineLogMessage + Environment.NewLine + Environment.NewLine);
|
||||
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(Request.AbsoluteUri + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
|
||||
await state.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
|
||||
process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
|
||||
|
@ -1514,6 +1514,10 @@ namespace MediaBrowser.Api.Playback
|
|||
request.PlaySessionId = val;
|
||||
}
|
||||
else if (i == 22)
|
||||
{
|
||||
// api_key
|
||||
}
|
||||
else if (i == 23)
|
||||
{
|
||||
request.LiveStreamId = val;
|
||||
}
|
||||
|
@ -1624,14 +1628,19 @@ namespace MediaBrowser.Api.Playback
|
|||
var archivable = item as IArchivable;
|
||||
state.IsInputArchive = archivable != null && archivable.IsArchive;
|
||||
|
||||
MediaSourceInfo mediaSource = null;
|
||||
MediaSourceInfo mediaSource;
|
||||
if (string.IsNullOrWhiteSpace(request.LiveStreamId))
|
||||
{
|
||||
var mediaSources = await MediaSourceManager.GetPlayackMediaSources(request.Id, false, cancellationToken).ConfigureAwait(false);
|
||||
var mediaSources = (await MediaSourceManager.GetPlayackMediaSources(request.Id, null, false, new[] { MediaType.Audio, MediaType.Video }, cancellationToken).ConfigureAwait(false)).ToList();
|
||||
|
||||
mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
|
||||
? mediaSources.First()
|
||||
: mediaSources.First(i => string.Equals(i.Id, request.MediaSourceId));
|
||||
: mediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId));
|
||||
|
||||
if (mediaSource == null && string.Equals(request.Id, request.MediaSourceId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mediaSource = mediaSources.First();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1700,6 +1709,102 @@ namespace MediaBrowser.Api.Playback
|
|||
{
|
||||
state.OutputAudioCodec = "copy";
|
||||
}
|
||||
|
||||
if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) && TranscodingJobType == TranscodingJobType.Hls)
|
||||
{
|
||||
var segmentLength = GetSegmentLength(state);
|
||||
if (segmentLength.HasValue)
|
||||
{
|
||||
state.SegmentLength = segmentLength.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int? GetSegmentLength(StreamState state)
|
||||
{
|
||||
var stream = state.VideoStream;
|
||||
|
||||
if (stream == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var frames = stream.KeyFrames;
|
||||
|
||||
if (frames == null || frames.Count < 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Logger.Debug("Found keyframes at {0}", string.Join(",", frames.ToArray()));
|
||||
|
||||
var intervals = new List<int>();
|
||||
for (var i = 1; i < frames.Count; i++)
|
||||
{
|
||||
var start = frames[i - 1];
|
||||
var end = frames[i];
|
||||
intervals.Add(end - start);
|
||||
}
|
||||
|
||||
Logger.Debug("Found keyframes intervals {0}", string.Join(",", intervals.ToArray()));
|
||||
|
||||
var results = new List<Tuple<int, int>>();
|
||||
|
||||
for (var i = 1; i <= 10; i++)
|
||||
{
|
||||
var idealMs = i*1000;
|
||||
|
||||
if (intervals.Max() < idealMs - 1000)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var segments = PredictStreamCopySegments(intervals, idealMs);
|
||||
var variance = segments.Select(s => Math.Abs(idealMs - s)).Sum();
|
||||
|
||||
results.Add(new Tuple<int, int>(i, variance));
|
||||
}
|
||||
|
||||
if (results.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return results.OrderBy(i => i.Item2).ThenBy(i => i.Item1).Select(i => i.Item1).First();
|
||||
}
|
||||
|
||||
private List<int> PredictStreamCopySegments(List<int> intervals, int idealMs)
|
||||
{
|
||||
var segments = new List<int>();
|
||||
var currentLength = 0;
|
||||
|
||||
foreach (var interval in intervals)
|
||||
{
|
||||
if (currentLength == 0 || (currentLength + interval) <= idealMs)
|
||||
{
|
||||
currentLength += interval;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
// The segment will either be above or below the ideal.
|
||||
// Need to figure out which is preferable
|
||||
var offset1 = Math.Abs(idealMs - currentLength);
|
||||
var offset2 = Math.Abs(idealMs - (currentLength + interval));
|
||||
|
||||
if (offset1 <= offset2)
|
||||
{
|
||||
segments.Add(currentLength);
|
||||
currentLength = interval;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentLength += interval;
|
||||
}
|
||||
}
|
||||
}
|
||||
Logger.Debug("Predicted actual segment lengths for length {0}: {1}", idealMs, string.Join(",", segments.ToArray()));
|
||||
return segments;
|
||||
}
|
||||
|
||||
private void AttachMediaSourceInfo(StreamState state,
|
||||
|
|
|
@ -5,7 +5,6 @@ using MediaBrowser.Controller.Configuration;
|
|||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
@ -518,25 +517,14 @@ namespace MediaBrowser.Api.Playback.Dash
|
|||
|
||||
private async Task WaitForSegment(string playlist, string segment, CancellationToken cancellationToken)
|
||||
{
|
||||
var tmpPath = playlist + ".tmp";
|
||||
|
||||
var segmentFilename = Path.GetFileName(segment);
|
||||
|
||||
Logger.Debug("Waiting for {0} in {1}", segmentFilename, playlist);
|
||||
|
||||
while (true)
|
||||
{
|
||||
FileStream fileStream;
|
||||
try
|
||||
{
|
||||
fileStream = FileSystem.GetFileStream(tmpPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
fileStream = FileSystem.GetFileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
|
||||
}
|
||||
// Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
|
||||
using (fileStream)
|
||||
using (var fileStream = GetPlaylistFileStream(playlist))
|
||||
{
|
||||
using (var reader = new StreamReader(fileStream))
|
||||
{
|
||||
|
|
|
@ -3,7 +3,6 @@ using MediaBrowser.Controller.Configuration;
|
|||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
@ -86,6 +85,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
state.Request.StartTimeTicks = null;
|
||||
}
|
||||
|
||||
TranscodingJob job = null;
|
||||
var playlist = state.OutputFilePath;
|
||||
|
||||
if (!File.Exists(playlist))
|
||||
|
@ -98,7 +98,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
// If the playlist doesn't already exist, startup ffmpeg
|
||||
try
|
||||
{
|
||||
await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false);
|
||||
job = await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -117,6 +117,12 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
|
||||
if (isLive)
|
||||
{
|
||||
job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
|
||||
|
||||
if (job != null)
|
||||
{
|
||||
ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
|
||||
}
|
||||
return ResultFactory.GetResult(GetLivePlaylistText(playlist, state.SegmentLength), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
|
||||
}
|
||||
|
||||
|
@ -135,6 +141,13 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
|
||||
var playlistText = GetMasterPlaylistFileText(playlist, videoBitrate + audioBitrate, appendBaselineStream, baselineStreamBitrate);
|
||||
|
||||
job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
|
||||
|
||||
if (job != null)
|
||||
{
|
||||
ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
|
||||
}
|
||||
|
||||
return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
|
||||
}
|
||||
|
||||
|
@ -186,7 +199,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
while (true)
|
||||
{
|
||||
// Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
|
||||
using (var fileStream = FileSystem.GetFileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
|
||||
using (var fileStream = GetPlaylistFileStream(playlist))
|
||||
{
|
||||
using (var reader = new StreamReader(fileStream))
|
||||
{
|
||||
|
@ -212,6 +225,20 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
}
|
||||
}
|
||||
|
||||
protected Stream GetPlaylistFileStream(string path)
|
||||
{
|
||||
var tmpPath = path + ".tmp";
|
||||
|
||||
try
|
||||
{
|
||||
return FileSystem.GetFileStream(tmpPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return FileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
|
||||
{
|
||||
var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream;
|
||||
|
|
|
@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration;
|
|||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
@ -128,9 +127,27 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
}
|
||||
else
|
||||
{
|
||||
var startTranscoding = false;
|
||||
|
||||
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
|
||||
var segmentGapRequiringTranscodingChange = 24/state.SegmentLength;
|
||||
if (currentTranscodingIndex == null || requestedIndex < currentTranscodingIndex.Value || (requestedIndex - currentTranscodingIndex.Value) > segmentGapRequiringTranscodingChange)
|
||||
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
|
||||
|
||||
if (currentTranscodingIndex == null)
|
||||
{
|
||||
Logger.Debug("Starting transcoding because currentTranscodingIndex=null");
|
||||
startTranscoding = true;
|
||||
}
|
||||
else if (requestedIndex < currentTranscodingIndex.Value)
|
||||
{
|
||||
Logger.Debug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", requestedIndex, currentTranscodingIndex);
|
||||
startTranscoding = true;
|
||||
}
|
||||
else if ((requestedIndex - currentTranscodingIndex.Value) > segmentGapRequiringTranscodingChange)
|
||||
{
|
||||
Logger.Debug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", (requestedIndex - currentTranscodingIndex.Value), segmentGapRequiringTranscodingChange, requestedIndex);
|
||||
startTranscoding = true;
|
||||
}
|
||||
if (startTranscoding)
|
||||
{
|
||||
// If the playlist doesn't already exist, startup ffmpeg
|
||||
try
|
||||
|
@ -145,7 +162,6 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
request.StartTimeTicks = GetSeekPositionTicks(state, requestedIndex);
|
||||
|
||||
job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
|
||||
ApiEntryPoint.Instance.OnTranscodeBeginRequest(job);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -153,7 +169,15 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
throw;
|
||||
}
|
||||
|
||||
await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
//await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
|
||||
if (job.TranscodingThrottler != null)
|
||||
{
|
||||
job.TranscodingThrottler.UnpauseTranscoding();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -300,7 +324,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
|
||||
var segmentFilename = Path.GetFileName(segmentPath);
|
||||
|
||||
using (var fileStream = FileSystem.GetFileStream(playlistPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
|
||||
using (var fileStream = GetPlaylistFileStream(playlistPath))
|
||||
{
|
||||
using (var reader = new StreamReader(fileStream))
|
||||
{
|
||||
|
@ -712,7 +736,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||
).Trim();
|
||||
}
|
||||
|
||||
return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -flags -global_header -sc_threshold 0 {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"",
|
||||
return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -flags -global_header -copyts -sc_threshold 0 {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"",
|
||||
inputModifier,
|
||||
GetInputArgument(state),
|
||||
threads,
|
||||
|
|
|
@ -3,12 +3,10 @@ using MediaBrowser.Controller.Configuration;
|
|||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.IO;
|
||||
using ServiceStack;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace MediaBrowser.Api.Playback.Hls
|
||||
{
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
|
@ -59,23 +61,27 @@ namespace MediaBrowser.Api.Playback
|
|||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly INetworkManager _networkManager;
|
||||
|
||||
public MediaInfoService(IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager)
|
||||
public MediaInfoService(IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager, IServerConfigurationManager config, INetworkManager networkManager)
|
||||
{
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_deviceManager = deviceManager;
|
||||
_libraryManager = libraryManager;
|
||||
_config = config;
|
||||
_networkManager = networkManager;
|
||||
}
|
||||
|
||||
public async Task<object> Get(GetPlaybackInfo request)
|
||||
{
|
||||
var result = await GetPlaybackInfo(request.Id, request.UserId).ConfigureAwait(false);
|
||||
var result = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }).ConfigureAwait(false);
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public async Task<object> Get(GetLiveMediaInfo request)
|
||||
{
|
||||
var result = await GetPlaybackInfo(request.Id, request.UserId).ConfigureAwait(false);
|
||||
var result = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }).ConfigureAwait(false);
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
|
@ -122,29 +128,32 @@ namespace MediaBrowser.Api.Playback
|
|||
|
||||
public async Task<object> Post(GetPostedPlaybackInfo request)
|
||||
{
|
||||
var info = await GetPlaybackInfo(request.Id, request.UserId, request.MediaSourceId, request.LiveStreamId).ConfigureAwait(false);
|
||||
var authInfo = AuthorizationContext.GetAuthorizationInfo(Request);
|
||||
|
||||
var profile = request.DeviceProfile;
|
||||
if (profile == null)
|
||||
|
||||
var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
|
||||
if (caps != null)
|
||||
{
|
||||
var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
|
||||
if (caps != null)
|
||||
if (profile == null)
|
||||
{
|
||||
profile = caps.DeviceProfile;
|
||||
}
|
||||
}
|
||||
|
||||
var info = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }, request.MediaSourceId, request.LiveStreamId).ConfigureAwait(false);
|
||||
|
||||
if (profile != null)
|
||||
{
|
||||
var mediaSourceId = request.MediaSourceId;
|
||||
|
||||
SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex);
|
||||
}
|
||||
|
||||
return ToOptimizedResult(info);
|
||||
}
|
||||
|
||||
private async Task<PlaybackInfoResponse> GetPlaybackInfo(string id, string userId, string mediaSourceId = null, string liveStreamId = null)
|
||||
private async Task<PlaybackInfoResponse> GetPlaybackInfo(string id, string userId, string[] supportedLiveMediaTypes, string mediaSourceId = null, string liveStreamId = null)
|
||||
{
|
||||
var result = new PlaybackInfoResponse();
|
||||
|
||||
|
@ -153,7 +162,7 @@ namespace MediaBrowser.Api.Playback
|
|||
IEnumerable<MediaSourceInfo> mediaSources;
|
||||
try
|
||||
{
|
||||
mediaSources = await _mediaSourceManager.GetPlayackMediaSources(id, userId, true, CancellationToken.None).ConfigureAwait(false);
|
||||
mediaSources = await _mediaSourceManager.GetPlayackMediaSources(id, userId, true, supportedLiveMediaTypes, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (PlaybackException ex)
|
||||
{
|
||||
|
@ -223,7 +232,7 @@ namespace MediaBrowser.Api.Playback
|
|||
int? subtitleStreamIndex,
|
||||
string playSessionId)
|
||||
{
|
||||
var streamBuilder = new StreamBuilder();
|
||||
var streamBuilder = new StreamBuilder(Logger);
|
||||
|
||||
var options = new VideoOptions
|
||||
{
|
||||
|
@ -231,8 +240,7 @@ namespace MediaBrowser.Api.Playback
|
|||
Context = EncodingContext.Streaming,
|
||||
DeviceId = auth.DeviceId,
|
||||
ItemId = item.Id.ToString("N"),
|
||||
Profile = profile,
|
||||
MaxBitrate = maxBitrate
|
||||
Profile = profile
|
||||
};
|
||||
|
||||
if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
|
||||
|
@ -248,6 +256,7 @@ namespace MediaBrowser.Api.Playback
|
|||
|
||||
// Dummy this up to fool StreamBuilder
|
||||
mediaSource.SupportsDirectStream = true;
|
||||
options.MaxBitrate = maxBitrate;
|
||||
|
||||
// The MediaSource supports direct stream, now test to see if the client supports it
|
||||
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
|
||||
|
@ -270,6 +279,8 @@ namespace MediaBrowser.Api.Playback
|
|||
|
||||
if (mediaSource.SupportsDirectStream)
|
||||
{
|
||||
options.MaxBitrate = GetMaxBitrate(maxBitrate);
|
||||
|
||||
// The MediaSource supports direct stream, now test to see if the client supports it
|
||||
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
|
||||
streamBuilder.BuildAudioItem(options) :
|
||||
|
@ -288,6 +299,8 @@ namespace MediaBrowser.Api.Playback
|
|||
|
||||
if (mediaSource.SupportsTranscoding)
|
||||
{
|
||||
options.MaxBitrate = GetMaxBitrate(maxBitrate);
|
||||
|
||||
// The MediaSource supports direct stream, now test to see if the client supports it
|
||||
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
|
||||
streamBuilder.BuildAudioItem(options) :
|
||||
|
@ -309,6 +322,18 @@ namespace MediaBrowser.Api.Playback
|
|||
}
|
||||
}
|
||||
|
||||
private int? GetMaxBitrate(int? clientMaxBitrate)
|
||||
{
|
||||
var maxBitrate = clientMaxBitrate;
|
||||
|
||||
if (_config.Configuration.RemoteClientBitrateLimit > 0 && !_networkManager.IsInLocalNetwork(Request.RemoteIp))
|
||||
{
|
||||
maxBitrate = Math.Min(maxBitrate ?? _config.Configuration.RemoteClientBitrateLimit, _config.Configuration.RemoteClientBitrateLimit);
|
||||
}
|
||||
|
||||
return maxBitrate;
|
||||
}
|
||||
|
||||
private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
|
||||
{
|
||||
var profiles = info.GetSubtitleProfiles(false, "-", accessToken);
|
||||
|
|
|
@ -63,6 +63,13 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||
new ProgressiveFileCopier(_fileSystem, _job)
|
||||
.StreamFile(Path, responseStream);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// These error are always the same so don't dump the whole stack trace
|
||||
Logger.Error("Error streaming media. The client has most likely disconnected or transcoding has failed.");
|
||||
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorException("Error streaming media. The client has most likely disconnected or transcoding has failed.", ex);
|
||||
|
|
|
@ -5,7 +5,6 @@ using MediaBrowser.Controller.Devices;
|
|||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.IO;
|
||||
using ServiceStack;
|
||||
|
|
|
@ -70,7 +70,7 @@ namespace MediaBrowser.Api.Playback
|
|||
}
|
||||
}
|
||||
|
||||
private void UnpauseTranscoding()
|
||||
public void UnpauseTranscoding()
|
||||
{
|
||||
if (_isPaused)
|
||||
{
|
||||
|
|
|
@ -383,12 +383,12 @@ namespace MediaBrowser.Api.Session
|
|||
|
||||
if (!user.Policy.EnableRemoteControlOfOtherUsers)
|
||||
{
|
||||
result = result.Where(i => i.ContainsUser(request.ControllableByUserId.Value));
|
||||
result = result.Where(i => !i.UserId.HasValue || i.ContainsUser(request.ControllableByUserId.Value));
|
||||
}
|
||||
|
||||
if (!user.Policy.EnableSharedDeviceControl)
|
||||
{
|
||||
result = result.Where(i => !i.UserId.HasValue);
|
||||
result = result.Where(i => i.UserId.HasValue);
|
||||
}
|
||||
|
||||
result = result.Where(i =>
|
||||
|
|
|
@ -170,7 +170,7 @@ namespace MediaBrowser.Api
|
|||
points += item1.Studios.Where(i => item2.Studios.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 3);
|
||||
|
||||
var item2PeopleNames = item2.People.Select(i => i.Name)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
points += item1.People.Where(i => item2PeopleNames.ContainsKey(i.Name)).Sum(i =>
|
||||
|
|
|
@ -136,11 +136,11 @@ namespace MediaBrowser.Api.Subtitles
|
|||
_providerManager = providerManager;
|
||||
}
|
||||
|
||||
public object Get(GetSubtitlePlaylist request)
|
||||
public async Task<object> Get(GetSubtitlePlaylist request)
|
||||
{
|
||||
var item = (Video)_libraryManager.GetItemById(new Guid(request.Id));
|
||||
|
||||
var mediaSource = _mediaSourceManager.GetStaticMediaSource(item, request.MediaSourceId, false);
|
||||
var mediaSource = await _mediaSourceManager.GetMediaSource(item, request.MediaSourceId, false).ConfigureAwait(false);
|
||||
|
||||
var builder = new StringBuilder();
|
||||
|
||||
|
|
|
@ -248,6 +248,9 @@ namespace MediaBrowser.Api.Sync
|
|||
result.Targets = _syncManager.GetSyncTargets(request.UserId)
|
||||
.ToList();
|
||||
|
||||
var auth = AuthorizationContext.GetAuthorizationInfo(Request);
|
||||
var authenticatedUser = _userManager.GetUserById(auth.UserId);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.TargetId))
|
||||
{
|
||||
result.Targets = result.Targets
|
||||
|
@ -255,11 +258,11 @@ namespace MediaBrowser.Api.Sync
|
|||
.ToList();
|
||||
|
||||
result.QualityOptions = _syncManager
|
||||
.GetQualityOptions(request.TargetId)
|
||||
.GetQualityOptions(request.TargetId, authenticatedUser)
|
||||
.ToList();
|
||||
|
||||
result.ProfileOptions = _syncManager
|
||||
.GetProfileOptions(request.TargetId)
|
||||
.GetProfileOptions(request.TargetId, authenticatedUser)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
@ -277,10 +280,6 @@ namespace MediaBrowser.Api.Sync
|
|||
}
|
||||
};
|
||||
|
||||
var auth = AuthorizationContext.GetAuthorizationInfo(Request);
|
||||
|
||||
var authenticatedUser = _userManager.GetUserById(auth.UserId);
|
||||
|
||||
var items = request.ItemIds.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(_libraryManager.GetItemById)
|
||||
.Where(i => i != null);
|
||||
|
|
|
@ -132,7 +132,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
.Where(i => !i.IsFolder)
|
||||
.OfType<IHasAlbumArtist>()
|
||||
.SelectMany(i => i.AlbumArtists)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.Select(name =>
|
||||
{
|
||||
try
|
||||
|
@ -152,7 +152,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
.Where(i => !i.IsFolder)
|
||||
.OfType<IHasArtist>()
|
||||
.SelectMany(i => i.AllArtists)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.Select(name =>
|
||||
{
|
||||
try
|
||||
|
|
|
@ -142,7 +142,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
}
|
||||
|
||||
IEnumerable<Tuple<TItemType, List<BaseItem>>> tuples;
|
||||
if (dtoOptions.Fields.Contains(ItemFields.ItemCounts) || true)
|
||||
if (dtoOptions.Fields.Contains(ItemFields.ItemCounts))
|
||||
{
|
||||
tuples = ibnItems.Select(i => new Tuple<TItemType, List<BaseItem>>(i, i.GetTaggedItems(libraryItems).ToList()));
|
||||
}
|
||||
|
@ -177,7 +177,6 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
return options.Fields.Contains(ItemFields.ItemCounts);
|
||||
}
|
||||
|
||||
|
|
|
@ -105,7 +105,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
|
||||
return itemsList
|
||||
.SelectMany(i => i.Genres)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.Select(name => LibraryManager.GetGameGenre(name));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -108,7 +108,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
{
|
||||
return items
|
||||
.SelectMany(i => i.Genres)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.Select(name =>
|
||||
{
|
||||
try
|
||||
|
|
|
@ -105,7 +105,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
|
||||
return itemsList
|
||||
.SelectMany(i => i.Genres)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.Select(name => LibraryManager.GetMusicGenre(name));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,7 +127,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
|
||||
return allPeople
|
||||
.Select(i => i.Name)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
|
||||
.Select(name =>
|
||||
{
|
||||
|
|
|
@ -114,6 +114,15 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
|
||||
[ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||
public int? SubtitleStreamIndex { get; set; }
|
||||
|
||||
[ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public PlayMethod PlayMethod { get; set; }
|
||||
|
||||
[ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string LiveStreamId { get; set; }
|
||||
|
||||
[ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string PlaySessionId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -160,6 +169,15 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
|
||||
[ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||
public int? VolumeLevel { get; set; }
|
||||
|
||||
[ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public PlayMethod PlayMethod { get; set; }
|
||||
|
||||
[ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string LiveStreamId { get; set; }
|
||||
|
||||
[ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string PlaySessionId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -191,6 +209,12 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
/// <value>The position ticks.</value>
|
||||
[ApiMember(Name = "PositionTicks", Description = "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "DELETE")]
|
||||
public long? PositionTicks { get; set; }
|
||||
|
||||
[ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string LiveStreamId { get; set; }
|
||||
|
||||
[ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string PlaySessionId { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
|
@ -260,7 +284,10 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
QueueableMediaTypes = queueableMediaTypes.Split(',').ToList(),
|
||||
MediaSourceId = request.MediaSourceId,
|
||||
AudioStreamIndex = request.AudioStreamIndex,
|
||||
SubtitleStreamIndex = request.SubtitleStreamIndex
|
||||
SubtitleStreamIndex = request.SubtitleStreamIndex,
|
||||
PlayMethod = request.PlayMethod,
|
||||
PlaySessionId = request.PlaySessionId,
|
||||
LiveStreamId = request.LiveStreamId
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -288,12 +315,20 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
MediaSourceId = request.MediaSourceId,
|
||||
AudioStreamIndex = request.AudioStreamIndex,
|
||||
SubtitleStreamIndex = request.SubtitleStreamIndex,
|
||||
VolumeLevel = request.VolumeLevel
|
||||
VolumeLevel = request.VolumeLevel,
|
||||
PlayMethod = request.PlayMethod,
|
||||
PlaySessionId = request.PlaySessionId,
|
||||
LiveStreamId = request.LiveStreamId
|
||||
});
|
||||
}
|
||||
|
||||
public void Post(ReportPlaybackProgress request)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(request.PlaySessionId))
|
||||
{
|
||||
ApiEntryPoint.Instance.PingTranscodingJob(request.PlaySessionId);
|
||||
}
|
||||
|
||||
request.SessionId = GetSession().Result.Id;
|
||||
|
||||
var task = _sessionManager.OnPlaybackProgress(request);
|
||||
|
@ -311,12 +346,19 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
{
|
||||
ItemId = request.Id,
|
||||
PositionTicks = request.PositionTicks,
|
||||
MediaSourceId = request.MediaSourceId
|
||||
MediaSourceId = request.MediaSourceId,
|
||||
PlaySessionId = request.PlaySessionId,
|
||||
LiveStreamId = request.LiveStreamId
|
||||
});
|
||||
}
|
||||
|
||||
public void Post(ReportPlaybackStopped request)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(request.PlaySessionId))
|
||||
{
|
||||
ApiEntryPoint.Instance.KillTranscodingJobs(AuthorizationContext.GetAuthorizationInfo(Request).DeviceId, request.PlaySessionId, s => true);
|
||||
}
|
||||
|
||||
request.SessionId = GetSession().Result.Id;
|
||||
|
||||
var task = _sessionManager.OnPlaybackStopped(request);
|
||||
|
|
|
@ -109,7 +109,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
|
||||
return itemsList
|
||||
.SelectMany(i => i.Studios)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.Select(name => LibraryManager.GetStudio(name));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,12 +101,6 @@ namespace MediaBrowser.Common.Implementations
|
|||
/// <value>The failed assemblies.</value>
|
||||
public List<string> FailedAssemblies { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets all types within all running assemblies
|
||||
/// </summary>
|
||||
/// <value>All types.</value>
|
||||
public Type[] AllTypes { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets all concrete types.
|
||||
/// </summary>
|
||||
|
@ -438,9 +432,10 @@ namespace MediaBrowser.Common.Implementations
|
|||
Logger.Info("Loading {0}", assembly.FullName);
|
||||
}
|
||||
|
||||
AllTypes = assemblies.SelectMany(GetTypes).ToArray();
|
||||
|
||||
AllConcreteTypes = AllTypes.Where(t => t.IsClass && !t.IsAbstract && !t.IsInterface && !t.IsGenericType).ToArray();
|
||||
AllConcreteTypes = assemblies
|
||||
.SelectMany(GetTypes)
|
||||
.Where(t => t.IsClass && !t.IsAbstract && !t.IsInterface && !t.IsGenericType)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -172,11 +172,11 @@ namespace MediaBrowser.Common.Implementations.Networking
|
|||
Uri uri;
|
||||
if (Uri.TryCreate(endpoint, UriKind.RelativeOrAbsolute, out uri))
|
||||
{
|
||||
var host = uri.DnsSafeHost;
|
||||
Logger.Debug("Resolving host {0}", host);
|
||||
|
||||
try
|
||||
{
|
||||
var host = uri.DnsSafeHost;
|
||||
Logger.Debug("Resolving host {0}", host);
|
||||
|
||||
address = GetIpAddresses(host).FirstOrDefault();
|
||||
|
||||
if (address != null)
|
||||
|
@ -186,9 +186,13 @@ namespace MediaBrowser.Common.Implementations.Networking
|
|||
return IsInLocalNetworkInternal(address.ToString(), false);
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Can happen with reverse proxy or IIS url rewriting
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.ErrorException("Error resovling hostname {0}", ex, host);
|
||||
Logger.ErrorException("Error resovling hostname", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,12 +121,12 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
|||
{
|
||||
if (_lastExecutionResult == null)
|
||||
{
|
||||
var path = GetHistoryFilePath();
|
||||
|
||||
lock (_lastExecutionResultSyncLock)
|
||||
{
|
||||
if (_lastExecutionResult == null)
|
||||
{
|
||||
var path = GetHistoryFilePath();
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.DeserializeFromFile<TaskResult>(path);
|
||||
|
@ -152,6 +152,14 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
|||
private set
|
||||
{
|
||||
_lastExecutionResult = value;
|
||||
|
||||
var path = GetHistoryFilePath();
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
|
||||
lock (_lastExecutionResultSyncLock)
|
||||
{
|
||||
JsonSerializer.SerializeToFile(value, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -582,11 +590,6 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
|||
result.LongErrorMessage = ex.StackTrace;
|
||||
}
|
||||
|
||||
var path = GetHistoryFilePath();
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
|
||||
JsonSerializer.SerializeToFile(result, path);
|
||||
|
||||
LastExecutionResult = result;
|
||||
|
||||
((TaskManager)TaskManager).OnTaskCompleted(this, result);
|
||||
|
|
|
@ -5,7 +5,6 @@ using System;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Users;
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
|
@ -15,19 +14,9 @@ namespace MediaBrowser.Controller.Channels
|
|||
|
||||
public override bool IsVisible(User user)
|
||||
{
|
||||
if (user.Policy.BlockedChannels != null)
|
||||
if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (user.Policy.BlockedChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.IsVisible(user);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Model.Channels;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
@ -100,5 +101,10 @@ namespace MediaBrowser.Controller.Channels
|
|||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool IsVisibleStandalone(User user)
|
||||
{
|
||||
return IsVisibleStandaloneInternal(user, false) && ChannelVideoItem.IsChannelVisible(this, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,5 +80,10 @@ namespace MediaBrowser.Controller.Channels
|
|||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool IsVisibleStandalone(User user)
|
||||
{
|
||||
return IsVisibleStandaloneInternal(user, false) && ChannelVideoItem.IsChannelVisible(this, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,5 +130,17 @@ namespace MediaBrowser.Controller.Channels
|
|||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool IsVisibleStandalone(User user)
|
||||
{
|
||||
return IsVisibleStandaloneInternal(user, false) && IsChannelVisible(this, user);
|
||||
}
|
||||
|
||||
internal static bool IsChannelVisible(IChannelItem item, User user)
|
||||
{
|
||||
var channel = ChannelManager.GetChannel(item.ChannelId);
|
||||
|
||||
return channel.IsVisible(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,12 @@ namespace MediaBrowser.Controller.Drawing
|
|||
/// </summary>
|
||||
public interface IImageProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the supported input formats.
|
||||
/// </summary>
|
||||
/// <value>The supported input formats.</value>
|
||||
string[] SupportedInputFormats { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image enhancers.
|
||||
/// </summary>
|
||||
|
@ -93,5 +99,11 @@ namespace MediaBrowser.Controller.Drawing
|
|||
/// </summary>
|
||||
/// <returns>ImageOutputFormat[].</returns>
|
||||
ImageFormat[] GetSupportedImageOutputFormats();
|
||||
|
||||
/// <summary>
|
||||
/// Creates the image collage.
|
||||
/// </summary>
|
||||
/// <param name="options">The options.</param>
|
||||
void CreateImageCollage(ImageCollageOptions options);
|
||||
}
|
||||
}
|
||||
|
|
32
MediaBrowser.Controller/Drawing/ImageCollageOptions.cs
Normal file
32
MediaBrowser.Controller/Drawing/ImageCollageOptions.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
|
||||
namespace MediaBrowser.Controller.Drawing
|
||||
{
|
||||
public class ImageCollageOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the input paths.
|
||||
/// </summary>
|
||||
/// <value>The input paths.</value>
|
||||
public string[] InputPaths { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the output path.
|
||||
/// </summary>
|
||||
/// <value>The output path.</value>
|
||||
public string OutputPath { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the width.
|
||||
/// </summary>
|
||||
/// <value>The width.</value>
|
||||
public int Width { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the height.
|
||||
/// </summary>
|
||||
/// <value>The height.</value>
|
||||
public int Height { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the text.
|
||||
/// </summary>
|
||||
/// <value>The text.</value>
|
||||
public string Text { get; set; }
|
||||
}
|
||||
}
|
|
@ -35,6 +35,14 @@ namespace MediaBrowser.Controller.Dto
|
|||
/// <returns>Task{BaseItemDto}.</returns>
|
||||
BaseItemDto GetBaseItemDto(BaseItem item, List<ItemFields> fields, User user = null, BaseItem owner = null);
|
||||
|
||||
/// <summary>
|
||||
/// Fills the synchronize information.
|
||||
/// </summary>
|
||||
/// <param name="dtos">The dtos.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
void FillSyncInfo(IEnumerable<IHasSyncInfo> dtos, DtoOptions options, User user);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base item dto.
|
||||
/// </summary>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Audio
|
||||
{
|
||||
|
@ -20,11 +19,11 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||
{
|
||||
public static bool HasArtist(this IHasArtist hasArtist, string artist)
|
||||
{
|
||||
return hasArtist.Artists.Contains(artist, StringComparer.OrdinalIgnoreCase);
|
||||
return NameExtensions.EqualsAny(hasArtist.Artists, artist);
|
||||
}
|
||||
public static bool HasAnyArtist(this IHasArtist hasArtist, string artist)
|
||||
{
|
||||
return hasArtist.AllArtists.Contains(artist, StringComparer.OrdinalIgnoreCase);
|
||||
return NameExtensions.EqualsAny(hasArtist.AllArtists, artist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using MediaBrowser.Common.Extensions;
|
||||
using System.Globalization;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Collections;
|
||||
|
@ -44,7 +45,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
/// <summary>
|
||||
/// The supported image extensions
|
||||
/// </summary>
|
||||
public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".tbn" };
|
||||
public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg" };
|
||||
|
||||
public static readonly List<string> SupportedImageExtensionsList = SupportedImageExtensions.ToList();
|
||||
|
||||
|
@ -1143,6 +1144,11 @@ namespace MediaBrowser.Controller.Entities
|
|||
}
|
||||
|
||||
public virtual bool IsVisibleStandalone(User user)
|
||||
{
|
||||
return IsVisibleStandaloneInternal(user, true);
|
||||
}
|
||||
|
||||
protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
|
||||
{
|
||||
if (!IsVisible(user))
|
||||
{
|
||||
|
@ -1154,7 +1160,23 @@ namespace MediaBrowser.Controller.Entities
|
|||
return false;
|
||||
}
|
||||
|
||||
// TODO: Need some work here, e.g. is in user library, for channels, can user access channel, etc.
|
||||
if (checkFolders)
|
||||
{
|
||||
var topParent = Parents.LastOrDefault() ?? this;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(topParent.Path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var userCollectionFolders = user.RootFolder.GetChildren(user, true).Select(i => i.Id).ToList();
|
||||
var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id);
|
||||
|
||||
if (!itemCollectionFolders.Any(userCollectionFolders.Contains))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -1219,18 +1241,6 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
private BaseItem FindLinkedChild(LinkedChild info)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(info.ItemName))
|
||||
{
|
||||
if (string.Equals(info.ItemType, "musicgenre", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LibraryManager.GetMusicGenre(info.ItemName);
|
||||
}
|
||||
if (string.Equals(info.ItemType, "musicartist", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return LibraryManager.GetArtist(info.ItemName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(info.Path))
|
||||
{
|
||||
var itemByPath = LibraryManager.RootFolder.FindByPath(info.Path);
|
||||
|
@ -1243,23 +1253,6 @@ namespace MediaBrowser.Controller.Entities
|
|||
return itemByPath;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(info.ItemName) && !string.IsNullOrWhiteSpace(info.ItemType))
|
||||
{
|
||||
return LibraryManager.RootFolder.GetRecursiveChildren(i =>
|
||||
{
|
||||
if (string.Equals(i.Name, info.ItemName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.Equals(i.GetType().Name, info.ItemType, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}).FirstOrDefault();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -1540,7 +1533,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
}
|
||||
|
||||
// Remove it from the item
|
||||
ImageInfos.Remove(info);
|
||||
RemoveImage(info);
|
||||
|
||||
// Delete the source file
|
||||
var currentFile = new FileInfo(info.Path);
|
||||
|
@ -1559,6 +1552,11 @@ namespace MediaBrowser.Controller.Entities
|
|||
return UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
|
||||
}
|
||||
|
||||
public void RemoveImage(ItemImageInfo image)
|
||||
{
|
||||
ImageInfos.Remove(image);
|
||||
}
|
||||
|
||||
public virtual Task UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||
{
|
||||
return LibraryManager.UpdateItem(this, updateReason, cancellationToken);
|
||||
|
@ -1651,7 +1649,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
public bool AddImages(ImageType imageType, IEnumerable<FileInfo> images)
|
||||
{
|
||||
return AddImages(imageType, images.Cast<FileSystemInfo>());
|
||||
return AddImages(imageType, images.Cast<FileSystemInfo>().ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1661,7 +1659,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
/// <param name="images">The images.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||
/// <exception cref="System.ArgumentException">Cannot call AddImages with chapter images</exception>
|
||||
public bool AddImages(ImageType imageType, IEnumerable<FileSystemInfo> images)
|
||||
public bool AddImages(ImageType imageType, List<FileSystemInfo> images)
|
||||
{
|
||||
if (imageType == ImageType.Chapter)
|
||||
{
|
||||
|
@ -1672,6 +1670,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
.ToList();
|
||||
|
||||
var newImageList = new List<FileSystemInfo>();
|
||||
var imageAdded = false;
|
||||
|
||||
foreach (var newImage in images)
|
||||
{
|
||||
|
@ -1686,14 +1685,26 @@ namespace MediaBrowser.Controller.Entities
|
|||
if (existing == null)
|
||||
{
|
||||
newImageList.Add(newImage);
|
||||
imageAdded = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.DateModified = FileSystem.GetLastWriteTimeUtc(newImage);
|
||||
existing.Length = ((FileInfo) newImage).Length;
|
||||
existing.Length = ((FileInfo)newImage).Length;
|
||||
}
|
||||
}
|
||||
|
||||
if (imageAdded || images.Count != existingImages.Count)
|
||||
{
|
||||
var newImagePaths = images.Select(i => i.FullName).ToList();
|
||||
|
||||
var deleted = existingImages
|
||||
.Where(i => !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !File.Exists(i.Path))
|
||||
.ToList();
|
||||
|
||||
ImageInfos = ImageInfos.Except(deleted).ToList();
|
||||
}
|
||||
|
||||
ImageInfos.AddRange(newImageList.Select(i => GetImageInfo(i, imageType)));
|
||||
|
||||
return newImageList.Count > 0;
|
||||
|
@ -1882,5 +1893,18 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
return video.RefreshMetadata(newOptions, cancellationToken);
|
||||
}
|
||||
|
||||
public string GetEtag()
|
||||
{
|
||||
return string.Join("|", GetEtagValues().ToArray()).GetMD5().ToString("N");
|
||||
}
|
||||
|
||||
protected virtual List<string> GetEtagValues()
|
||||
{
|
||||
return new List<string>
|
||||
{
|
||||
DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -334,22 +334,9 @@ namespace MediaBrowser.Controller.Entities
|
|||
{
|
||||
if (this is ICollectionFolder && !(this is BasePluginFolder))
|
||||
{
|
||||
if (user.Policy.BlockedMediaFolders != null)
|
||||
if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase) ||
|
||||
|
||||
// Backwards compatibility
|
||||
user.Policy.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1004,8 +991,9 @@ namespace MediaBrowser.Controller.Entities
|
|||
}
|
||||
|
||||
var locations = user.RootFolder
|
||||
.GetChildren(user, true)
|
||||
.Children
|
||||
.OfType<CollectionFolder>()
|
||||
.Where(i => i.IsVisible(user))
|
||||
.SelectMany(i => i.PhysicalLocations)
|
||||
.ToList();
|
||||
|
||||
|
|
|
@ -141,7 +141,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
/// <param name="imageType">Type of the image.</param>
|
||||
/// <param name="images">The images.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||
bool AddImages(ImageType imageType, IEnumerable<FileSystemInfo> images);
|
||||
bool AddImages(ImageType imageType, List<FileSystemInfo> images);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether [is save local metadata enabled].
|
||||
|
@ -190,6 +190,12 @@ namespace MediaBrowser.Controller.Entities
|
|||
/// </summary>
|
||||
/// <returns><c>true</c> if [is internet metadata enabled]; otherwise, <c>false</c>.</returns>
|
||||
bool IsInternetMetadataEnabled();
|
||||
|
||||
/// <summary>
|
||||
/// Removes the image.
|
||||
/// </summary>
|
||||
/// <param name="image">The image.</param>
|
||||
void RemoveImage(ItemImageInfo image);
|
||||
}
|
||||
|
||||
public static class HasImagesExtensions
|
||||
|
|
|
@ -9,9 +9,6 @@ namespace MediaBrowser.Controller.Entities
|
|||
public string Path { get; set; }
|
||||
public LinkedChildType Type { get; set; }
|
||||
|
||||
public string ItemName { get; set; }
|
||||
public string ItemType { get; set; }
|
||||
|
||||
[IgnoreDataMember]
|
||||
public string Id { get; set; }
|
||||
|
||||
|
|
|
@ -175,17 +175,17 @@ namespace MediaBrowser.Controller.Entities.Movies
|
|||
|
||||
public override bool IsVisible(User user)
|
||||
{
|
||||
var userId = user.Id.ToString("N");
|
||||
|
||||
// Need to check Count > 0 for boxsets created prior to the introduction of Shares
|
||||
if (Shares.Count > 0 && Shares.Any(i => string.Equals(userId, i.UserId, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (base.IsVisible(user))
|
||||
{
|
||||
var userId = user.Id.ToString("N");
|
||||
|
||||
// Need to check Count > 0 for boxsets created prior to the introduction of Shares
|
||||
if (Shares.Count > 0 && !Shares.Any(i => string.Equals(userId, i.UserId, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
//return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return GetChildren(user, true).Any();
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Users;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using MediaBrowser.Model.Users;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public class PhotoAlbum : Folder
|
||||
public class PhotoAlbum : Folder, IMetadataContainer
|
||||
{
|
||||
public override bool SupportsLocalMetadata
|
||||
{
|
||||
|
@ -28,5 +32,31 @@ namespace MediaBrowser.Controller.Entities
|
|||
{
|
||||
return config.BlockUnratedItems.Contains(UnratedItem.Other);
|
||||
}
|
||||
|
||||
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var items = GetRecursiveChildren().ToList();
|
||||
|
||||
var totalItems = items.Count;
|
||||
var numComplete = 0;
|
||||
|
||||
// Refresh songs
|
||||
foreach (var item in items)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= totalItems;
|
||||
progress.Report(percent * 100);
|
||||
}
|
||||
|
||||
// Refresh current item
|
||||
await RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,16 @@ namespace MediaBrowser.Controller.Entities
|
|||
{
|
||||
var user = query.User;
|
||||
|
||||
if (query.IncludeItemTypes != null &&
|
||||
query.IncludeItemTypes.Length == 1 &&
|
||||
string.Equals(query.IncludeItemTypes[0], "Playlist", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!string.Equals(viewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return await FindPlaylists(queryParent, user, query).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
switch (viewType)
|
||||
{
|
||||
case CollectionType.Channels:
|
||||
|
@ -107,9 +117,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
case CollectionType.LiveTv:
|
||||
{
|
||||
var result = await GetLiveTvFolders(user).ConfigureAwait(false);
|
||||
|
||||
return GetResult(result, queryParent, query);
|
||||
return await GetLiveTvView(queryParent, user, query).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
case CollectionType.Books:
|
||||
|
@ -205,6 +213,9 @@ namespace MediaBrowser.Controller.Entities
|
|||
case SpecialFolder.MusicLatest:
|
||||
return GetMusicLatest(queryParent, user, query);
|
||||
|
||||
case SpecialFolder.MusicPlaylists:
|
||||
return await GetMusicPlaylists(queryParent, user, query).ConfigureAwait(false);
|
||||
|
||||
case SpecialFolder.MusicAlbums:
|
||||
return GetMusicAlbums(queryParent, user, query);
|
||||
|
||||
|
@ -240,6 +251,16 @@ namespace MediaBrowser.Controller.Entities
|
|||
}
|
||||
}
|
||||
|
||||
private async Task<QueryResult<BaseItem>> FindPlaylists(Folder parent, User user, InternalItemsQuery query)
|
||||
{
|
||||
var collectionFolders = user.RootFolder.GetChildren(user, true).Select(i => i.Id).ToList();
|
||||
|
||||
var list = _playlistManager.GetPlaylists(user.Id.ToString("N"))
|
||||
.Where(i => i.GetChildren(user, true).Any(media => _libraryManager.GetCollectionFolders(media).Select(c => c.Id).Any(collectionFolders.Contains)));
|
||||
|
||||
return GetResult(list, parent, query);
|
||||
}
|
||||
|
||||
private int GetSpecialItemsLimit()
|
||||
{
|
||||
return 50;
|
||||
|
@ -257,12 +278,13 @@ namespace MediaBrowser.Controller.Entities
|
|||
var list = new List<BaseItem>();
|
||||
|
||||
list.Add(await GetUserView(SpecialFolder.MusicLatest, user, "0", parent).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.MusicAlbums, user, "1", parent).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.MusicAlbumArtists, user, "2", parent).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.MusicArtists, user, "3", parent).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.MusicSongs, user, "4", parent).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.MusicGenres, user, "5", parent).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.MusicFavorites, user, "6", parent).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.MusicPlaylists, user, "1", parent).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.MusicAlbums, user, "2", parent).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.MusicAlbumArtists, user, "3", parent).ConfigureAwait(false));
|
||||
//list.Add(await GetUserView(SpecialFolder.MusicArtists, user, "4", parent).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.MusicSongs, user, "5", parent).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.MusicGenres, user, "6", parent).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.MusicFavorites, user, "7", parent).ConfigureAwait(false));
|
||||
|
||||
return GetResult(list, parent, query);
|
||||
}
|
||||
|
@ -283,7 +305,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Music, CollectionType.MusicVideos })
|
||||
.Where(i => !i.IsFolder)
|
||||
.SelectMany(i => i.Genres)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
|
@ -313,7 +335,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
.Where(i => i.Genres.Contains(displayParent.Name, StringComparer.OrdinalIgnoreCase))
|
||||
.OfType<IHasAlbumArtist>()
|
||||
.SelectMany(i => i.AlbumArtists)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
|
@ -337,7 +359,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
.Where(i => !i.IsFolder)
|
||||
.OfType<IHasAlbumArtist>()
|
||||
.SelectMany(i => i.AlbumArtists)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
|
@ -361,7 +383,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
.Where(i => !i.IsFolder)
|
||||
.OfType<IHasArtist>()
|
||||
.SelectMany(i => i.Artists)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
|
@ -385,7 +407,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
.Where(i => !i.IsFolder)
|
||||
.OfType<IHasAlbumArtist>()
|
||||
.SelectMany(i => i.AlbumArtists)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
|
@ -403,6 +425,14 @@ namespace MediaBrowser.Controller.Entities
|
|||
return GetResult(artists, parent, query);
|
||||
}
|
||||
|
||||
private Task<QueryResult<BaseItem>> GetMusicPlaylists(Folder parent, User user, InternalItemsQuery query)
|
||||
{
|
||||
query.IncludeItemTypes = new[] { "Playlist" };
|
||||
query.Recursive = true;
|
||||
|
||||
return parent.GetItems(query);
|
||||
}
|
||||
|
||||
private QueryResult<BaseItem> GetMusicAlbums(Folder parent, User user, InternalItemsQuery query)
|
||||
{
|
||||
var items = GetRecursiveChildren(parent, user, new[] { CollectionType.Music, CollectionType.MusicVideos }, i => (i is MusicAlbum) && FilterItem(i, query));
|
||||
|
@ -552,7 +582,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Movies, CollectionType.BoxSets, string.Empty })
|
||||
.Where(i => i is Movie)
|
||||
.SelectMany(i => i.Genres)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
|
@ -724,7 +754,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.TvShows, string.Empty })
|
||||
.OfType<Series>()
|
||||
.SelectMany(i => i.Genres)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
|
@ -776,7 +806,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Games })
|
||||
.OfType<Game>()
|
||||
.SelectMany(i => i.Genres)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.DistinctNames()
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
|
@ -1749,17 +1779,26 @@ namespace MediaBrowser.Controller.Entities
|
|||
return parent.GetRecursiveChildren(user, filter);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<BaseItem>> GetLiveTvFolders(User user)
|
||||
private async Task<QueryResult<BaseItem>> GetLiveTvView(Folder queryParent, User user, InternalItemsQuery query)
|
||||
{
|
||||
if (query.Recursive)
|
||||
{
|
||||
return await _liveTvManager.GetInternalRecordings(new RecordingQuery
|
||||
{
|
||||
IsInProgress = false,
|
||||
Status = RecordingStatus.Completed,
|
||||
UserId = user.Id.ToString("N")
|
||||
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var list = new List<BaseItem>();
|
||||
|
||||
var parent = user.RootFolder;
|
||||
|
||||
//list.Add(await GetUserSubView(SpecialFolder.LiveTvNowPlaying, user, "0", parent).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.LiveTvChannels, user, string.Empty, parent).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.LiveTvRecordingGroups, user, string.Empty, parent).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.LiveTvChannels, user, string.Empty, user.RootFolder).ConfigureAwait(false));
|
||||
list.Add(await GetUserView(SpecialFolder.LiveTvRecordingGroups, user, string.Empty, user.RootFolder).ConfigureAwait(false));
|
||||
|
||||
return list;
|
||||
return GetResult(list, queryParent, query);
|
||||
}
|
||||
|
||||
private async Task<UserView> GetUserView(string name, string type, User user, string sortName, BaseItem parent)
|
||||
|
|
|
@ -3,7 +3,7 @@ using System.IO;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
namespace MediaBrowser.Controller.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// Class for streaming data with throttling support.
|
||||
|
@ -15,8 +15,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
|||
/// </summary>
|
||||
public const long Infinite = 0;
|
||||
|
||||
public Func<long, long, long> ThrottleCallback { get; set; }
|
||||
|
||||
#region Private members
|
||||
/// <summary>
|
||||
/// The base stream.
|
||||
|
@ -293,16 +291,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
|||
return false;
|
||||
}
|
||||
|
||||
if (ThrottleCallback != null)
|
||||
{
|
||||
var val = ThrottleCallback(_maximumBytesPerSecond, _bytesWritten);
|
||||
|
||||
if (val == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -43,18 +43,10 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <param name="id">The identifier.</param>
|
||||
/// <param name="userId">The user identifier.</param>
|
||||
/// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
|
||||
/// <param name="supportedLiveMediaTypes">The supported live media types.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>IEnumerable<MediaSourceInfo>.</returns>
|
||||
Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the playack media sources.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task<IEnumerable<MediaSourceInfo>>.</returns>
|
||||
Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, bool enablePathSubstitution, CancellationToken cancellationToken);
|
||||
Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, string[] supportedLiveMediaTypes, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the static media sources.
|
||||
|
@ -63,16 +55,8 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>IEnumerable<MediaSourceInfo>.</returns>
|
||||
IEnumerable<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user);
|
||||
IEnumerable<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution, User user = null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the static media sources.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
|
||||
/// <returns>IEnumerable<MediaSourceInfo>.</returns>
|
||||
IEnumerable<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the static media source.
|
||||
/// </summary>
|
||||
|
@ -80,7 +64,7 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <param name="mediaSourceId">The media source identifier.</param>
|
||||
/// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
|
||||
/// <returns>MediaSourceInfo.</returns>
|
||||
MediaSourceInfo GetStaticMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution);
|
||||
Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution);
|
||||
|
||||
/// <summary>
|
||||
/// Opens the media source.
|
||||
|
|
41
MediaBrowser.Controller/Library/NameExtensions.cs
Normal file
41
MediaBrowser.Controller/Library/NameExtensions.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
using MediaBrowser.Common.Extensions;
|
||||
using MoreLinq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
public static class NameExtensions
|
||||
{
|
||||
public static bool AreEqual(string name1, string name2)
|
||||
{
|
||||
name1 = NormalizeForComparison(name1);
|
||||
name2 = NormalizeForComparison(name2);
|
||||
|
||||
return string.Equals(name1, name2, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static bool EqualsAny(IEnumerable<string> names, string name)
|
||||
{
|
||||
name = NormalizeForComparison(name);
|
||||
|
||||
return names.Any(i => string.Equals(NormalizeForComparison(i), name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static string NormalizeForComparison(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return name.RemoveDiacritics();
|
||||
}
|
||||
|
||||
public static IEnumerable<string> DistinctNames(this IEnumerable<string> names)
|
||||
{
|
||||
return names.DistinctBy(NormalizeForComparison, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
|
@ -74,10 +75,11 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
/// Gets the recording.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>Task{RecordingInfoDto}.</returns>
|
||||
Task<RecordingInfoDto> GetRecording(string id, CancellationToken cancellationToken, User user = null);
|
||||
Task<RecordingInfoDto> GetRecording(string id, DtoOptions options, CancellationToken cancellationToken, User user = null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the channel.
|
||||
|
@ -103,14 +105,15 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{TimerInfoDto}.</returns>
|
||||
Task<SeriesTimerInfoDto> GetSeriesTimer(string id, CancellationToken cancellationToken);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the recordings.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>QueryResult{RecordingInfoDto}.</returns>
|
||||
Task<QueryResult<RecordingInfoDto>> GetRecordings(RecordingQuery query, CancellationToken cancellationToken);
|
||||
Task<QueryResult<RecordingInfoDto>> GetRecordings(RecordingQuery query, DtoOptions options, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timers.
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
using System;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
||||
namespace MediaBrowser.Controller.LiveTv
|
||||
{
|
||||
|
|
|
@ -52,6 +52,10 @@
|
|||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\morelinq.1.1.0\lib\net35\MoreLinq.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Patterns.IO, Version=1.0.5580.36861, Culture=neutral, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\Patterns.IO.1.0.0.3\lib\portable-net45+sl4+wp71+win8+wpa81\Patterns.IO.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Data" />
|
||||
|
@ -115,6 +119,7 @@
|
|||
<Compile Include="Dlna\IMediaReceiverRegistrar.cs" />
|
||||
<Compile Include="Dlna\IUpnpService.cs" />
|
||||
<Compile Include="Drawing\IImageProcessor.cs" />
|
||||
<Compile Include="Drawing\ImageCollageOptions.cs" />
|
||||
<Compile Include="Drawing\ImageProcessingOptions.cs" />
|
||||
<Compile Include="Drawing\ImageProcessorExtensions.cs" />
|
||||
<Compile Include="Drawing\ImageStream.cs" />
|
||||
|
@ -171,6 +176,7 @@
|
|||
<Compile Include="Entities\UserView.cs" />
|
||||
<Compile Include="Entities\UserViewBuilder.cs" />
|
||||
<Compile Include="FileOrganization\IFileOrganizationService.cs" />
|
||||
<Compile Include="IO\ThrottledStream.cs" />
|
||||
<Compile Include="Library\DeleteOptions.cs" />
|
||||
<Compile Include="Library\ILibraryPostScanTask.cs" />
|
||||
<Compile Include="Library\IMediaSourceManager.cs" />
|
||||
|
@ -184,6 +190,7 @@
|
|||
<Compile Include="Library\IUserViewManager.cs" />
|
||||
<Compile Include="Library\LibraryManagerExtensions.cs" />
|
||||
<Compile Include="Library\MetadataConfigurationStore.cs" />
|
||||
<Compile Include="Library\NameExtensions.cs" />
|
||||
<Compile Include="Library\PlaybackStopEventArgs.cs" />
|
||||
<Compile Include="Library\UserDataSaveEventArgs.cs" />
|
||||
<Compile Include="LiveTv\ILiveTvItem.cs" />
|
||||
|
@ -211,8 +218,8 @@
|
|||
<Compile Include="MediaEncoding\IEncodingManager.cs" />
|
||||
<Compile Include="MediaEncoding\ImageEncodingOptions.cs" />
|
||||
<Compile Include="MediaEncoding\IMediaEncoder.cs" />
|
||||
<Compile Include="MediaEncoding\InternalMediaInfoResult.cs" />
|
||||
<Compile Include="MediaEncoding\ISubtitleEncoder.cs" />
|
||||
<Compile Include="MediaEncoding\MediaInfoRequest.cs" />
|
||||
<Compile Include="MediaEncoding\MediaStreamSelector.cs" />
|
||||
<Compile Include="Net\AuthenticatedAttribute.cs" />
|
||||
<Compile Include="Net\AuthorizationInfo.cs" />
|
||||
|
@ -394,6 +401,7 @@
|
|||
<Compile Include="Subtitles\SubtitleResponse.cs" />
|
||||
<Compile Include="Subtitles\SubtitleSearchRequest.cs" />
|
||||
<Compile Include="Sync\IHasDynamicAccess.cs" />
|
||||
<Compile Include="Sync\IRemoteSyncProvider.cs" />
|
||||
<Compile Include="Sync\IServerSyncProvider.cs" />
|
||||
<Compile Include="Sync\ISyncDataProvider.cs" />
|
||||
<Compile Include="Sync\ISyncManager.cs" />
|
||||
|
|
|
@ -41,6 +41,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
public int? SubtitleStreamIndex { get; set; }
|
||||
public int? MaxRefFrames { get; set; }
|
||||
public int? MaxVideoBitDepth { get; set; }
|
||||
public int? CpuCoreLimit { get; set; }
|
||||
public bool ReadInputAtNativeFramerate { get; set; }
|
||||
public SubtitleDeliveryMethod SubtitleMethod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -63,16 +63,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
string filenamePrefix,
|
||||
int? maxWidth,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the media info.
|
||||
/// </summary>
|
||||
/// <param name="inputFiles">The input files.</param>
|
||||
/// <param name="protocol">The protocol.</param>
|
||||
/// <param name="isAudio">if set to <c>true</c> [is audio].</param>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task<InternalMediaInfoResult> GetMediaInfo(string[] inputFiles, MediaProtocol protocol, bool isAudio, CancellationToken cancellationToken);
|
||||
Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the probe size argument.
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
|
@ -46,291 +44,5 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
.Where(f => !string.IsNullOrEmpty(f))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static MediaInfo GetMediaInfo(InternalMediaInfoResult data)
|
||||
{
|
||||
var internalStreams = data.streams ?? new MediaStreamInfo[] { };
|
||||
|
||||
var info = new MediaInfo
|
||||
{
|
||||
MediaStreams = internalStreams.Select(s => GetMediaStream(s, data.format))
|
||||
.Where(i => i != null)
|
||||
.ToList()
|
||||
};
|
||||
|
||||
if (data.format != null)
|
||||
{
|
||||
info.Format = data.format.format_name;
|
||||
|
||||
if (!string.IsNullOrEmpty(data.format.bit_rate))
|
||||
{
|
||||
info.TotalBitrate = int.Parse(data.format.bit_rate, UsCulture);
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
/// <summary>
|
||||
/// Converts ffprobe stream info to our MediaStream class
|
||||
/// </summary>
|
||||
/// <param name="streamInfo">The stream info.</param>
|
||||
/// <param name="formatInfo">The format info.</param>
|
||||
/// <returns>MediaStream.</returns>
|
||||
private static MediaStream GetMediaStream(MediaStreamInfo streamInfo, MediaFormatInfo formatInfo)
|
||||
{
|
||||
var stream = new MediaStream
|
||||
{
|
||||
Codec = streamInfo.codec_name,
|
||||
Profile = streamInfo.profile,
|
||||
Level = streamInfo.level,
|
||||
Index = streamInfo.index,
|
||||
PixelFormat = streamInfo.pix_fmt
|
||||
};
|
||||
|
||||
if (streamInfo.tags != null)
|
||||
{
|
||||
stream.Language = GetDictionaryValue(streamInfo.tags, "language");
|
||||
}
|
||||
|
||||
if (string.Equals(streamInfo.codec_type, "audio", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = MediaStreamType.Audio;
|
||||
|
||||
stream.Channels = streamInfo.channels;
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.sample_rate))
|
||||
{
|
||||
stream.SampleRate = int.Parse(streamInfo.sample_rate, UsCulture);
|
||||
}
|
||||
|
||||
stream.ChannelLayout = ParseChannelLayout(streamInfo.channel_layout);
|
||||
}
|
||||
else if (string.Equals(streamInfo.codec_type, "subtitle", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = MediaStreamType.Subtitle;
|
||||
}
|
||||
else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = (streamInfo.codec_name ?? string.Empty).IndexOf("mjpeg", StringComparison.OrdinalIgnoreCase) != -1
|
||||
? MediaStreamType.EmbeddedImage
|
||||
: MediaStreamType.Video;
|
||||
|
||||
stream.Width = streamInfo.width;
|
||||
stream.Height = streamInfo.height;
|
||||
stream.AspectRatio = GetAspectRatio(streamInfo);
|
||||
|
||||
stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate);
|
||||
stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate);
|
||||
|
||||
stream.BitDepth = GetBitDepth(stream.PixelFormat);
|
||||
|
||||
//stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) ||
|
||||
// string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) ||
|
||||
// string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get stream bitrate
|
||||
var bitrate = 0;
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.bit_rate))
|
||||
{
|
||||
bitrate = int.Parse(streamInfo.bit_rate, UsCulture);
|
||||
}
|
||||
else if (formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate) && stream.Type == MediaStreamType.Video)
|
||||
{
|
||||
// If the stream info doesn't have a bitrate get the value from the media format info
|
||||
bitrate = int.Parse(formatInfo.bit_rate, UsCulture);
|
||||
}
|
||||
|
||||
if (bitrate > 0)
|
||||
{
|
||||
stream.BitRate = bitrate;
|
||||
}
|
||||
|
||||
if (streamInfo.disposition != null)
|
||||
{
|
||||
var isDefault = GetDictionaryValue(streamInfo.disposition, "default");
|
||||
var isForced = GetDictionaryValue(streamInfo.disposition, "forced");
|
||||
|
||||
stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
private static int? GetBitDepth(string pixelFormat)
|
||||
{
|
||||
var eightBit = new List<string>
|
||||
{
|
||||
"yuv420p",
|
||||
"yuv411p",
|
||||
"yuvj420p",
|
||||
"uyyvyy411",
|
||||
"nv12",
|
||||
"nv21",
|
||||
"rgb444le",
|
||||
"rgb444be",
|
||||
"bgr444le",
|
||||
"bgr444be",
|
||||
"yuvj411p"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(pixelFormat))
|
||||
{
|
||||
if (eightBit.Contains(pixelFormat, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string from an FFProbeResult tags dictionary
|
||||
/// </summary>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private static string GetDictionaryValue(Dictionary<string, string> tags, string key)
|
||||
{
|
||||
if (tags == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string val;
|
||||
|
||||
tags.TryGetValue(key, out val);
|
||||
return val;
|
||||
}
|
||||
|
||||
private static string ParseChannelLayout(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
return input.Split('(').FirstOrDefault();
|
||||
}
|
||||
|
||||
private static string GetAspectRatio(MediaStreamInfo info)
|
||||
{
|
||||
var original = info.display_aspect_ratio;
|
||||
|
||||
int height;
|
||||
int width;
|
||||
|
||||
var parts = (original ?? string.Empty).Split(':');
|
||||
if (!(parts.Length == 2 &&
|
||||
int.TryParse(parts[0], NumberStyles.Any, UsCulture, out width) &&
|
||||
int.TryParse(parts[1], NumberStyles.Any, UsCulture, out height) &&
|
||||
width > 0 &&
|
||||
height > 0))
|
||||
{
|
||||
width = info.width;
|
||||
height = info.height;
|
||||
}
|
||||
|
||||
if (width > 0 && height > 0)
|
||||
{
|
||||
double ratio = width;
|
||||
ratio /= height;
|
||||
|
||||
if (IsClose(ratio, 1.777777778, .03))
|
||||
{
|
||||
return "16:9";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.3333333333, .05))
|
||||
{
|
||||
return "4:3";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.41))
|
||||
{
|
||||
return "1.41:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.5))
|
||||
{
|
||||
return "1.5:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.6))
|
||||
{
|
||||
return "1.6:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.66666666667))
|
||||
{
|
||||
return "5:3";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.85, .02))
|
||||
{
|
||||
return "1.85:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 2.35, .025))
|
||||
{
|
||||
return "2.35:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 2.4, .025))
|
||||
{
|
||||
return "2.40:1";
|
||||
}
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
private static bool IsClose(double d1, double d2, double variance = .005)
|
||||
{
|
||||
return Math.Abs(d1 - d2) <= variance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a frame rate from a string value in ffprobe output
|
||||
/// This could be a number or in the format of 2997/125.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>System.Nullable{System.Single}.</returns>
|
||||
private static float? GetFrameRate(string value)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
var parts = value.Split('/');
|
||||
|
||||
float result;
|
||||
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
result = float.Parse(parts[0], UsCulture) / float.Parse(parts[1], UsCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = float.Parse(parts[0], UsCulture);
|
||||
}
|
||||
|
||||
return float.IsNaN(result) ? (float?)null : result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
25
MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs
Normal file
25
MediaBrowser.Controller/MediaEncoding/MediaInfoRequest.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
public class MediaInfoRequest
|
||||
{
|
||||
public string InputPath { get; set; }
|
||||
public MediaProtocol Protocol { get; set; }
|
||||
public bool ExtractChapters { get; set; }
|
||||
public DlnaProfileType MediaType { get; set; }
|
||||
public IIsoMount MountedIso { get; set; }
|
||||
public VideoType VideoType { get; set; }
|
||||
public List<string> PlayableStreamFileNames { get; set; }
|
||||
public bool ExtractKeyFrameInterval { get; set; }
|
||||
|
||||
public MediaInfoRequest()
|
||||
{
|
||||
PlayableStreamFileNames = new List<string>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1404,24 +1404,12 @@ namespace MediaBrowser.Controller.Providers
|
|||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Name":
|
||||
{
|
||||
linkedItem.ItemName = reader.ReadElementContentAsString();
|
||||
break;
|
||||
}
|
||||
|
||||
case "Path":
|
||||
{
|
||||
linkedItem.Path = reader.ReadElementContentAsString();
|
||||
break;
|
||||
}
|
||||
|
||||
case "Type":
|
||||
{
|
||||
linkedItem.ItemType = reader.ReadElementContentAsString();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
|
@ -1435,7 +1423,7 @@ namespace MediaBrowser.Controller.Providers
|
|||
return linkedItem;
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(linkedItem.ItemName) || string.IsNullOrWhiteSpace(linkedItem.ItemType) ? null : linkedItem;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System.Threading.Tasks;
|
||||
|
|
|
@ -78,6 +78,19 @@ namespace MediaBrowser.Controller.Providers
|
|||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task SaveImage(IHasImages item, Stream source, string mimeType, ImageType type, int? imageIndex, string internalCacheKey, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the image.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="mimeType">Type of the MIME.</param>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="imageIndex">Index of the image.</param>
|
||||
/// <param name="internalCacheKey">The internal cache key.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task SaveImage(IHasImages item, string source, string mimeType, ImageType type, int? imageIndex, string internalCacheKey, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the metadata providers.
|
||||
|
|
|
@ -9,10 +9,10 @@ namespace MediaBrowser.Controller.Sync
|
|||
/// <summary>
|
||||
/// Gets the synced file information.
|
||||
/// </summary>
|
||||
/// <param name="remotePath">The remote path.</param>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task<SyncedFileInfo>.</returns>
|
||||
Task<SyncedFileInfo> GetSyncedFileInfo(string remotePath, SyncTarget target, CancellationToken cancellationToken);
|
||||
Task<SyncedFileInfo> GetSyncedFileInfo(string id, SyncTarget target, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
10
MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs
Normal file
10
MediaBrowser.Controller/Sync/IRemoteSyncProvider.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
|
||||
namespace MediaBrowser.Controller.Sync
|
||||
{
|
||||
/// <summary>
|
||||
/// A marker interface
|
||||
/// </summary>
|
||||
public interface IRemoteSyncProvider
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
using MediaBrowser.Model.Sync;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Sync;
|
||||
using Patterns.IO;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -13,46 +14,39 @@ namespace MediaBrowser.Controller.Sync
|
|||
/// Transfers the file.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <param name="remotePath">The remote path.</param>
|
||||
/// <param name="pathParts">The path parts.</param>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task<SyncedFileInfo> SendFile(Stream stream, string remotePath, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
|
||||
Task<SyncedFileInfo> SendFile(Stream stream, string[] pathParts, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the file.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task DeleteFile(string path, SyncTarget target, CancellationToken cancellationToken);
|
||||
Task DeleteFile(string id, SyncTarget target, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task<Stream>.</returns>
|
||||
Task<Stream> GetFile(string path, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
|
||||
Task<Stream> GetFile(string id, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full path.
|
||||
/// Gets the files.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string GetFullPath(IEnumerable<string> path, SyncTarget target);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parent directory path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string GetParentDirectoryPath(string path, SyncTarget target);
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task<QueryResult<FileMetadata>>.</returns>
|
||||
Task<QueryResult<FileMetadata>> GetFiles(FileQuery query, SyncTarget target, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,20 +7,12 @@ namespace MediaBrowser.Controller.Sync
|
|||
public interface ISyncDataProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the server item ids.
|
||||
/// Gets the local items.
|
||||
/// </summary>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <param name="serverId">The server identifier.</param>
|
||||
/// <returns>Task<List<System.String>>.</returns>
|
||||
Task<List<string>> GetServerItemIds(SyncTarget target, string serverId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the synchronize job item ids.
|
||||
/// </summary>
|
||||
/// <param name="target">The target.</param>
|
||||
/// <param name="serverId">The server identifier.</param>
|
||||
/// <returns>Task<List<System.String>>.</returns>
|
||||
Task<List<string>> GetSyncJobItemIds(SyncTarget target, string serverId);
|
||||
/// <returns>Task<List<LocalItem>>.</returns>
|
||||
Task<List<LocalItem>> GetLocalItems(SyncTarget target, string serverId);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the or update.
|
||||
|
|
|
@ -174,6 +174,13 @@ namespace MediaBrowser.Controller.Sync
|
|||
/// <param name="targetId">The target identifier.</param>
|
||||
/// <returns>IEnumerable<SyncQualityOption>.</returns>
|
||||
IEnumerable<SyncQualityOption> GetQualityOptions(string targetId);
|
||||
/// <summary>
|
||||
/// Gets the quality options.
|
||||
/// </summary>
|
||||
/// <param name="targetId">The target identifier.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>IEnumerable<SyncQualityOption>.</returns>
|
||||
IEnumerable<SyncQualityOption> GetQualityOptions(string targetId, User user);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the profile options.
|
||||
|
@ -181,5 +188,12 @@ namespace MediaBrowser.Controller.Sync
|
|||
/// <param name="targetId">The target identifier.</param>
|
||||
/// <returns>IEnumerable<SyncQualityOption>.</returns>
|
||||
IEnumerable<SyncProfileOption> GetProfileOptions(string targetId);
|
||||
/// <summary>
|
||||
/// Gets the profile options.
|
||||
/// </summary>
|
||||
/// <param name="targetId">The target identifier.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>IEnumerable<SyncProfileOption>.</returns>
|
||||
IEnumerable<SyncProfileOption> GetProfileOptions(string targetId, User user);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,11 @@ namespace MediaBrowser.Controller.Sync
|
|||
/// </summary>
|
||||
/// <value>The required HTTP headers.</value>
|
||||
public Dictionary<string, string> RequiredHttpHeaders { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the identifier.
|
||||
/// </summary>
|
||||
/// <value>The identifier.</value>
|
||||
public string Id { get; set; }
|
||||
|
||||
public SyncedFileInfo()
|
||||
{
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="morelinq" version="1.1.0" targetFramework="net45" />
|
||||
<package id="Patterns.IO" version="1.0.0.3" targetFramework="net45" />
|
||||
</packages>
|
|
@ -223,7 +223,7 @@ namespace MediaBrowser.Dlna.ContentDirectory
|
|||
if (string.Equals(flag, "BrowseMetadata"))
|
||||
{
|
||||
totalCount = 1;
|
||||
|
||||
|
||||
if (item.IsFolder || serverItem.StubType.HasValue)
|
||||
{
|
||||
var childrenResult = (await GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount).ConfigureAwait(false));
|
||||
|
@ -350,7 +350,7 @@ namespace MediaBrowser.Dlna.ContentDirectory
|
|||
};
|
||||
}
|
||||
|
||||
private async Task<QueryResult<BaseItem>> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit)
|
||||
private Task<QueryResult<BaseItem>> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit)
|
||||
{
|
||||
var folder = (Folder)item;
|
||||
|
||||
|
@ -389,7 +389,7 @@ namespace MediaBrowser.Dlna.ContentDirectory
|
|||
isFolder = true;
|
||||
}
|
||||
|
||||
return await folder.GetItems(new InternalItemsQuery
|
||||
return folder.GetItems(new InternalItemsQuery
|
||||
{
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
|
@ -401,7 +401,7 @@ namespace MediaBrowser.Dlna.ContentDirectory
|
|||
IsFolder = isFolder,
|
||||
MediaTypes = mediaTypes.ToArray()
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<QueryResult<ServerItem>> GetUserItems(BaseItem item, StubType? stubType, User user, SortCriteria sort, int? startIndex, int? limit)
|
||||
|
|
|
@ -12,6 +12,7 @@ using MediaBrowser.Dlna.ContentDirectory;
|
|||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Net;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
@ -124,9 +125,9 @@ namespace MediaBrowser.Dlna.Didl
|
|||
{
|
||||
if (streamInfo == null)
|
||||
{
|
||||
var sources = _user == null ? _mediaSourceManager.GetStaticMediaSources(video, true).ToList() : _mediaSourceManager.GetStaticMediaSources(video, true, _user).ToList();
|
||||
var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user).ToList();
|
||||
|
||||
streamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions
|
||||
streamInfo = new StreamBuilder(new NullLogger()).BuildVideoItem(new VideoOptions
|
||||
{
|
||||
ItemId = GetClientId(video),
|
||||
MediaSources = sources,
|
||||
|
@ -351,9 +352,9 @@ namespace MediaBrowser.Dlna.Didl
|
|||
|
||||
if (streamInfo == null)
|
||||
{
|
||||
var sources = _user == null ? _mediaSourceManager.GetStaticMediaSources(audio, true).ToList() : _mediaSourceManager.GetStaticMediaSources(audio, true, _user).ToList();
|
||||
var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user).ToList();
|
||||
|
||||
streamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions
|
||||
streamInfo = new StreamBuilder(new NullLogger()).BuildAudioItem(new AudioOptions
|
||||
{
|
||||
ItemId = GetClientId(audio),
|
||||
MediaSources = sources,
|
||||
|
|
|
@ -470,7 +470,7 @@ namespace MediaBrowser.Dlna.PlayTo
|
|||
|
||||
var hasMediaSources = item as IHasMediaSources;
|
||||
var mediaSources = hasMediaSources != null
|
||||
? (user == null ? _mediaSourceManager.GetStaticMediaSources(hasMediaSources, true) : _mediaSourceManager.GetStaticMediaSources(hasMediaSources, true, user)).ToList()
|
||||
? (_mediaSourceManager.GetStaticMediaSources(hasMediaSources, true, user)).ToList()
|
||||
: new List<MediaSourceInfo>();
|
||||
|
||||
var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
|
||||
|
@ -542,7 +542,7 @@ namespace MediaBrowser.Dlna.PlayTo
|
|||
{
|
||||
return new PlaylistItem
|
||||
{
|
||||
StreamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions
|
||||
StreamInfo = new StreamBuilder(_logger).BuildVideoItem(new VideoOptions
|
||||
{
|
||||
ItemId = item.Id.ToString("N"),
|
||||
MediaSources = mediaSources,
|
||||
|
@ -562,7 +562,7 @@ namespace MediaBrowser.Dlna.PlayTo
|
|||
{
|
||||
return new PlaylistItem
|
||||
{
|
||||
StreamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions
|
||||
StreamInfo = new StreamBuilder(_logger).BuildAudioItem(new AudioOptions
|
||||
{
|
||||
ItemId = item.Id.ToString("N"),
|
||||
MediaSources = mediaSources,
|
||||
|
@ -892,7 +892,7 @@ namespace MediaBrowser.Dlna.PlayTo
|
|||
|
||||
request.MediaSource = hasMediaSources == null ?
|
||||
null :
|
||||
mediaSourceManager.GetStaticMediaSource(hasMediaSources, request.MediaSourceId, false);
|
||||
mediaSourceManager.GetMediaSource(hasMediaSources, request.MediaSourceId, false).Result;
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -62,16 +62,22 @@ namespace MediaBrowser.Dlna.Ssdp
|
|||
{
|
||||
if (string.Equals(args.Method, "M-SEARCH", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
TimeSpan delay = GetSearchDelay(args.Headers);
|
||||
var headers = args.Headers;
|
||||
|
||||
TimeSpan delay = GetSearchDelay(headers);
|
||||
|
||||
if (_config.GetDlnaConfiguration().EnableDebugLogging)
|
||||
{
|
||||
_logger.Debug("Delaying search response by {0} seconds", delay.TotalSeconds);
|
||||
}
|
||||
|
||||
await Task.Delay(delay).ConfigureAwait(false);
|
||||
await Task.Delay(delay).ConfigureAwait(false);
|
||||
|
||||
RespondToSearch(args.EndPoint, args.Headers["st"]);
|
||||
string st;
|
||||
if (headers.TryGetValue("st", out st))
|
||||
{
|
||||
RespondToSearch(args.EndPoint, st);
|
||||
}
|
||||
}
|
||||
|
||||
EventHelper.FireEventIfNotNull(MessageReceived, this, args, _logger);
|
||||
|
|
|
@ -92,7 +92,7 @@ namespace MediaBrowser.LocalMetadata
|
|||
{
|
||||
get
|
||||
{
|
||||
return "Media Browser Xml";
|
||||
return "Emby Xml";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ using System.Linq;
|
|||
|
||||
namespace MediaBrowser.LocalMetadata.Images
|
||||
{
|
||||
public class EpisodeLocalLocalImageProvider : ILocalImageFileProvider
|
||||
public class EpisodeLocalLocalImageProvider : ILocalImageFileProvider, IHasOrder
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
|
@ -24,6 +24,11 @@ namespace MediaBrowser.LocalMetadata.Images
|
|||
get { return "Local Images"; }
|
||||
}
|
||||
|
||||
public int Order
|
||||
{
|
||||
get { return 0; }
|
||||
}
|
||||
|
||||
public bool Supports(IHasImages item)
|
||||
{
|
||||
return item is Episode && item.SupportsLocalMetadata;
|
||||
|
|
|
@ -26,6 +26,11 @@ namespace MediaBrowser.LocalMetadata.Images
|
|||
|
||||
public bool Supports(IHasImages item)
|
||||
{
|
||||
if (item is Photo)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!item.IsSaveLocalMetadataEnabled())
|
||||
{
|
||||
return true;
|
||||
|
|
|
@ -12,7 +12,7 @@ using System.Linq;
|
|||
|
||||
namespace MediaBrowser.LocalMetadata.Images
|
||||
{
|
||||
public class LocalImageProvider : ILocalImageFileProvider
|
||||
public class LocalImageProvider : ILocalImageFileProvider, IHasOrder
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
|
|
|
@ -756,11 +756,6 @@ namespace MediaBrowser.LocalMetadata.Savers
|
|||
{
|
||||
builder.Append("<" + singularNodeName + ">");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(link.ItemType))
|
||||
{
|
||||
builder.Append("<Type>" + SecurityElement.Escape(link.ItemType) + "</Type>");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(link.Path))
|
||||
{
|
||||
builder.Append("<Path>" + SecurityElement.Escape((link.Path)) + "</Path>");
|
||||
|
|
|
@ -70,10 +70,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
encodingJob.OutputFilePath = GetOutputFilePath(encodingJob);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(encodingJob.OutputFilePath));
|
||||
|
||||
if (options.Context == EncodingContext.Static && encodingJob.IsInputVideo)
|
||||
{
|
||||
encodingJob.ReadInputAtNativeFramerate = true;
|
||||
}
|
||||
encodingJob.ReadInputAtNativeFramerate = options.ReadInputAtNativeFramerate;
|
||||
|
||||
await AcquireResources(encodingJob, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
@ -305,19 +302,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
/// <returns>System.Int32.</returns>
|
||||
protected int GetNumberOfThreads(EncodingJob job, bool isWebm)
|
||||
{
|
||||
// Only need one thread for sync
|
||||
if (job.Options.Context == EncodingContext.Static)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (isWebm)
|
||||
{
|
||||
// Recommended per docs
|
||||
return Math.Max(Environment.ProcessorCount - 1, 2);
|
||||
}
|
||||
|
||||
return 0;
|
||||
return job.Options.CpuCoreLimit ?? 0;
|
||||
}
|
||||
|
||||
protected EncodingQuality GetQualitySetting()
|
||||
|
|
|
@ -59,7 +59,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
|
||||
state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(request.ItemId, false, cancellationToken).ConfigureAwait(false);
|
||||
var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(request.ItemId, null, false, new[] { MediaType.Audio, MediaType.Video }, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
|
||||
? mediaSources.First()
|
||||
|
@ -124,10 +124,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
state.InputContainer = mediaSource.Container;
|
||||
state.InputFileSize = mediaSource.Size;
|
||||
state.InputBitrate = mediaSource.Bitrate;
|
||||
state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
|
||||
state.RunTimeTicks = mediaSource.RunTimeTicks;
|
||||
state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
|
||||
|
||||
if (mediaSource.ReadAtNativeFramerate)
|
||||
{
|
||||
state.ReadInputAtNativeFramerate = true;
|
||||
}
|
||||
|
||||
if (mediaSource.VideoType.HasValue)
|
||||
{
|
||||
state.VideoType = mediaSource.VideoType.Value;
|
||||
|
@ -148,7 +152,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
|
||||
state.InputBitrate = mediaSource.Bitrate;
|
||||
state.InputFileSize = mediaSource.Size;
|
||||
state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
|
||||
|
||||
if (state.ReadInputAtNativeFramerate ||
|
||||
mediaSource.Protocol == MediaProtocol.File && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase))
|
||||
|
|
|
@ -5,12 +5,15 @@ using MediaBrowser.Controller.Library;
|
|||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.MediaEncoding.Probing;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
@ -72,6 +75,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
protected readonly Func<ISubtitleEncoder> SubtitleEncoder;
|
||||
protected readonly Func<IMediaSourceManager> MediaSourceManager;
|
||||
|
||||
private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
|
||||
|
||||
public MediaEncoder(ILogger logger, IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, string version, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager, Func<ISubtitleEncoder> subtitleEncoder, Func<IMediaSourceManager> mediaSourceManager)
|
||||
{
|
||||
_logger = logger;
|
||||
|
@ -102,16 +107,19 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
/// <summary>
|
||||
/// Gets the media info.
|
||||
/// </summary>
|
||||
/// <param name="inputFiles">The input files.</param>
|
||||
/// <param name="protocol">The protocol.</param>
|
||||
/// <param name="isAudio">if set to <c>true</c> [is audio].</param>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task<InternalMediaInfoResult> GetMediaInfo(string[] inputFiles, MediaProtocol protocol, bool isAudio,
|
||||
CancellationToken cancellationToken)
|
||||
public Task<Model.MediaInfo.MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return GetMediaInfoInternal(GetInputArgument(inputFiles, protocol), !isAudio,
|
||||
GetProbeSizeArgument(inputFiles, protocol), cancellationToken);
|
||||
var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
|
||||
|
||||
var inputFiles = MediaEncoderHelpers.GetInputArgument(request.InputPath, request.Protocol, request.MountedIso, request.PlayableStreamFileNames);
|
||||
|
||||
var extractKeyFrameInterval = request.ExtractKeyFrameInterval && request.Protocol == MediaProtocol.File && request.VideoType == VideoType.VideoFile;
|
||||
|
||||
return GetMediaInfoInternal(GetInputArgument(inputFiles, request.Protocol), request.InputPath, request.Protocol, extractChapters, extractKeyFrameInterval,
|
||||
GetProbeSizeArgument(inputFiles, request.Protocol), request.MediaType == DlnaProfileType.Audio, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -141,13 +149,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
/// Gets the media info internal.
|
||||
/// </summary>
|
||||
/// <param name="inputPath">The input path.</param>
|
||||
/// <param name="primaryPath">The primary path.</param>
|
||||
/// <param name="protocol">The protocol.</param>
|
||||
/// <param name="extractChapters">if set to <c>true</c> [extract chapters].</param>
|
||||
/// <param name="extractKeyFrameInterval">if set to <c>true</c> [extract key frame interval].</param>
|
||||
/// <param name="probeSizeArgument">The probe size argument.</param>
|
||||
/// <param name="isAudio">if set to <c>true</c> [is audio].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{MediaInfoResult}.</returns>
|
||||
/// <exception cref="System.ApplicationException"></exception>
|
||||
private async Task<InternalMediaInfoResult> GetMediaInfoInternal(string inputPath, bool extractChapters,
|
||||
private async Task<Model.MediaInfo.MediaInfo> GetMediaInfoInternal(string inputPath,
|
||||
string primaryPath,
|
||||
MediaProtocol protocol,
|
||||
bool extractChapters,
|
||||
bool extractKeyFrameInterval,
|
||||
string probeSizeArgument,
|
||||
bool isAudio,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var args = extractChapters
|
||||
|
@ -164,6 +181,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
// Must consume both or ffmpeg may hang due to deadlocks. See comments below.
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
FileName = FFProbePath,
|
||||
Arguments = string.Format(args,
|
||||
probeSizeArgument, inputPath).Trim(),
|
||||
|
@ -177,15 +195,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
|
||||
_logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
process.Exited += ProcessExited;
|
||||
|
||||
await _ffProbeResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
InternalMediaInfoResult result;
|
||||
var processWrapper = new ProcessWrapper(process, this);
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
StartProcess(processWrapper);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -200,19 +216,57 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
{
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
result = _jsonSerializer.DeserializeFromStream<InternalMediaInfoResult>(process.StandardOutput.BaseStream);
|
||||
var result = _jsonSerializer.DeserializeFromStream<InternalMediaInfoResult>(process.StandardOutput.BaseStream);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
if (result.streams != null)
|
||||
{
|
||||
// Normalize aspect ratio if invalid
|
||||
foreach (var stream in result.streams)
|
||||
{
|
||||
if (string.Equals(stream.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.display_aspect_ratio = string.Empty;
|
||||
}
|
||||
if (string.Equals(stream.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.sample_aspect_ratio = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mediaInfo = new ProbeResultNormalizer(_logger, FileSystem).GetMediaInfo(result, isAudio, primaryPath, protocol);
|
||||
|
||||
if (extractKeyFrameInterval && mediaInfo.RunTimeTicks.HasValue)
|
||||
{
|
||||
foreach (var stream in mediaInfo.MediaStreams)
|
||||
{
|
||||
if (stream.Type == MediaStreamType.Video && string.Equals(stream.Codec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
//stream.KeyFrames = await GetKeyFrames(inputPath, stream.Index, cancellationToken)
|
||||
// .ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error getting key frame interval", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mediaInfo;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Hate having to do this
|
||||
try
|
||||
{
|
||||
process.Kill();
|
||||
}
|
||||
catch (Exception ex1)
|
||||
{
|
||||
_logger.ErrorException("Error killing ffprobe", ex1);
|
||||
}
|
||||
StopProcess(processWrapper, 100, true);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
@ -221,30 +275,102 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
_ffProbeResourcePool.Release();
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
throw new ApplicationException(string.Format("FFProbe failed for {0}", inputPath));
|
||||
}
|
||||
|
||||
private async Task<List<int>> GetKeyFrames(string inputPath, int videoStreamIndex, CancellationToken cancellationToken)
|
||||
{
|
||||
const string args = "-i {0} -select_streams v:{1} -show_frames -show_entries frame=pkt_dts,key_frame -print_format compact";
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
throw new ApplicationException(string.Format("FFProbe failed for {0}", inputPath));
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
|
||||
// Must consume both or ffmpeg may hang due to deadlocks. See comments below.
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
FileName = FFProbePath,
|
||||
Arguments = string.Format(args, inputPath, videoStreamIndex.ToString(CultureInfo.InvariantCulture)).Trim(),
|
||||
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
},
|
||||
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
|
||||
_logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
var processWrapper = new ProcessWrapper(process, this);
|
||||
|
||||
StartProcess(processWrapper);
|
||||
|
||||
var lines = new List<int>();
|
||||
|
||||
try
|
||||
{
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
await StartReadingOutput(process.StandardOutput.BaseStream, lines, 120000, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
StopProcess(processWrapper, 100, true);
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return lines;
|
||||
}
|
||||
|
||||
if (result.streams != null)
|
||||
private async Task StartReadingOutput(Stream source, List<int> lines, int timeoutMs, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Normalize aspect ratio if invalid
|
||||
foreach (var stream in result.streams)
|
||||
using (var reader = new StreamReader(source))
|
||||
{
|
||||
if (string.Equals(stream.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
stream.display_aspect_ratio = string.Empty;
|
||||
}
|
||||
if (string.Equals(stream.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.sample_aspect_ratio = string.Empty;
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||
|
||||
var values = (line ?? string.Empty).Split('|')
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => i.Split('='))
|
||||
.Where(i => i.Length == 2)
|
||||
.ToDictionary(i => i[0], i => i[1]);
|
||||
|
||||
string pktDts;
|
||||
int frameMs;
|
||||
if (values.TryGetValue("pkt_dts", out pktDts) && int.TryParse(pktDts, NumberStyles.Any, CultureInfo.InvariantCulture, out frameMs))
|
||||
{
|
||||
string keyFrame;
|
||||
if (values.TryGetValue("key_frame", out keyFrame) && string.Equals(keyFrame, "1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
lines.Add(frameMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error reading ffprobe output", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -252,16 +378,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
/// </summary>
|
||||
protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
/// <summary>
|
||||
/// Processes the exited.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender.</param>
|
||||
/// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
|
||||
private void ProcessExited(object sender, EventArgs e)
|
||||
{
|
||||
((Process)sender).Dispose();
|
||||
}
|
||||
|
||||
public Task<Stream> ExtractAudioImage(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
return ExtractImage(new[] { path }, MediaProtocol.File, true, null, null, cancellationToken);
|
||||
|
@ -286,6 +402,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
{
|
||||
return await ExtractImageInternal(inputArgument, protocol, threedFormat, offset, true, resourcePool, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.Error("I-frame image extraction failed, will attempt standard way. Input: {0}", inputArgument);
|
||||
|
@ -368,7 +488,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
|
||||
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
process.Start();
|
||||
var processWrapper = new ProcessWrapper(process, this);
|
||||
|
||||
StartProcess(processWrapper);
|
||||
|
||||
var memoryStream = new MemoryStream();
|
||||
|
||||
|
@ -384,23 +506,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
|
||||
if (!ranToCompletion)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Info("Killing ffmpeg process");
|
||||
|
||||
process.StandardInput.WriteLine("q");
|
||||
|
||||
process.WaitForExit(1000);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error killing process", ex);
|
||||
}
|
||||
StopProcess(processWrapper, 1000, false);
|
||||
}
|
||||
|
||||
resourcePool.Release();
|
||||
|
||||
var exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
|
||||
|
||||
process.Dispose();
|
||||
|
||||
|
@ -419,31 +530,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
return memoryStream;
|
||||
}
|
||||
|
||||
public Task<Stream> EncodeImage(ImageEncodingOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
_videoImageResourcePool.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public string GetTimeParameter(long ticks)
|
||||
{
|
||||
var time = TimeSpan.FromTicks(ticks);
|
||||
|
@ -510,9 +596,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
|
||||
bool ranToCompletion;
|
||||
|
||||
var processWrapper = new ProcessWrapper(process, this);
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
StartProcess(processWrapper);
|
||||
|
||||
// Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
|
||||
// but we still need to detect if the process hangs.
|
||||
|
@ -536,18 +624,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
|
||||
if (!ranToCompletion)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Info("Killing ffmpeg process");
|
||||
|
||||
process.StandardInput.WriteLine("q");
|
||||
|
||||
process.WaitForExit(1000);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error killing process", ex);
|
||||
}
|
||||
StopProcess(processWrapper, 1000, false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
@ -555,7 +632,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
resourcePool.Release();
|
||||
}
|
||||
|
||||
var exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
|
||||
|
||||
process.Dispose();
|
||||
|
||||
|
@ -608,5 +685,122 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
|
||||
return job.OutputFilePath;
|
||||
}
|
||||
|
||||
private void StartProcess(ProcessWrapper process)
|
||||
{
|
||||
process.Process.Start();
|
||||
|
||||
lock (_runningProcesses)
|
||||
{
|
||||
_runningProcesses.Add(process);
|
||||
}
|
||||
}
|
||||
private void StopProcess(ProcessWrapper process, int waitTimeMs, bool enableForceKill)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.Info("Killing ffmpeg process");
|
||||
|
||||
try
|
||||
{
|
||||
process.Process.StandardInput.WriteLine("q");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_logger.Error("Error sending q command to process");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (process.Process.WaitForExit(waitTimeMs))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error("Error in WaitForExit", ex);
|
||||
}
|
||||
|
||||
if (enableForceKill)
|
||||
{
|
||||
process.Process.Kill();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error killing process", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void StopProcesses()
|
||||
{
|
||||
List<ProcessWrapper> proceses;
|
||||
lock (_runningProcesses)
|
||||
{
|
||||
proceses = _runningProcesses.ToList();
|
||||
}
|
||||
_runningProcesses.Clear();
|
||||
|
||||
foreach (var process in proceses)
|
||||
{
|
||||
if (!process.HasExited)
|
||||
{
|
||||
StopProcess(process, 500, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
_videoImageResourcePool.Dispose();
|
||||
StopProcesses();
|
||||
}
|
||||
}
|
||||
|
||||
private class ProcessWrapper
|
||||
{
|
||||
public readonly Process Process;
|
||||
public bool HasExited;
|
||||
public int? ExitCode;
|
||||
private readonly MediaEncoder _mediaEncoder;
|
||||
|
||||
public ProcessWrapper(Process process, MediaEncoder mediaEncoder)
|
||||
{
|
||||
Process = process;
|
||||
this._mediaEncoder = mediaEncoder;
|
||||
Process.Exited += Process_Exited;
|
||||
}
|
||||
|
||||
void Process_Exited(object sender, EventArgs e)
|
||||
{
|
||||
var process = (Process)sender;
|
||||
|
||||
HasExited = true;
|
||||
|
||||
ExitCode = process.ExitCode;
|
||||
|
||||
lock (_mediaEncoder._runningProcesses)
|
||||
{
|
||||
_mediaEncoder._runningProcesses.Remove(this);
|
||||
}
|
||||
|
||||
process.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,6 +68,9 @@
|
|||
<Compile Include="Encoder\JobLogger.cs" />
|
||||
<Compile Include="Encoder\MediaEncoder.cs" />
|
||||
<Compile Include="Encoder\VideoEncoder.cs" />
|
||||
<Compile Include="Probing\FFProbeHelpers.cs" />
|
||||
<Compile Include="Probing\InternalMediaInfoResult.cs" />
|
||||
<Compile Include="Probing\ProbeResultNormalizer.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Subtitles\ISubtitleParser.cs" />
|
||||
<Compile Include="Subtitles\ISubtitleWriter.cs" />
|
||||
|
@ -91,6 +94,10 @@
|
|||
<Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
|
||||
<Name>MediaBrowser.Controller</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\MediaBrowser.MediaInfo\MediaBrowser.MediaInfo.csproj">
|
||||
<Project>{6e4145e4-c6d4-4e4d-94f2-87188db6e239}</Project>
|
||||
<Name>MediaBrowser.MediaInfo</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
|
||||
<Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
|
||||
<Name>MediaBrowser.Model</Name>
|
||||
|
@ -99,7 +106,9 @@
|
|||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Probing\whitelist.txt" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Providers.MediaInfo
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
public static class FFProbeHelpers
|
||||
{
|
|
@ -1,6 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MediaInfoResult
|
||||
|
@ -89,7 +89,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
/// </summary>
|
||||
/// <value>The channel_layout.</value>
|
||||
public string channel_layout { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the avg_frame_rate.
|
||||
/// </summary>
|
||||
|
@ -317,7 +317,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
/// </summary>
|
||||
/// <value>The probe_score.</value>
|
||||
public int probe_score { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tags.
|
||||
/// </summary>
|
887
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
Normal file
887
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
Normal file
|
@ -0,0 +1,887 @@
|
|||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.MediaInfo;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
public class ProbeResultNormalizer
|
||||
{
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
private readonly ILogger _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public ProbeResultNormalizer(ILogger logger, IFileSystem fileSystem)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
public Model.MediaInfo.MediaInfo GetMediaInfo(InternalMediaInfoResult data, bool isAudio, string path, MediaProtocol protocol)
|
||||
{
|
||||
var info = new Model.MediaInfo.MediaInfo
|
||||
{
|
||||
Path = path,
|
||||
Protocol = protocol
|
||||
};
|
||||
|
||||
FFProbeHelpers.NormalizeFFProbeResult(data);
|
||||
SetSize(data, info);
|
||||
|
||||
var internalStreams = data.streams ?? new MediaStreamInfo[] { };
|
||||
|
||||
info.MediaStreams = internalStreams.Select(s => GetMediaStream(s, data.format))
|
||||
.Where(i => i != null)
|
||||
.ToList();
|
||||
|
||||
if (data.format != null)
|
||||
{
|
||||
info.Container = data.format.format_name;
|
||||
|
||||
if (!string.IsNullOrEmpty(data.format.bit_rate))
|
||||
{
|
||||
info.Bitrate = int.Parse(data.format.bit_rate, _usCulture);
|
||||
}
|
||||
}
|
||||
|
||||
if (isAudio)
|
||||
{
|
||||
SetAudioRuntimeTicks(data, info);
|
||||
|
||||
if (data.format != null && data.format.tags != null)
|
||||
{
|
||||
SetAudioInfoFromTags(info, data.format.tags);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (data.format != null && !string.IsNullOrEmpty(data.format.duration))
|
||||
{
|
||||
info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration, _usCulture)).Ticks;
|
||||
}
|
||||
|
||||
FetchWtvInfo(info, data);
|
||||
|
||||
if (data.Chapters != null)
|
||||
{
|
||||
info.Chapters = data.Chapters.Select(GetChapterInfo).ToList();
|
||||
}
|
||||
|
||||
ExtractTimestamp(info);
|
||||
|
||||
var videoStream = info.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
|
||||
|
||||
if (videoStream != null)
|
||||
{
|
||||
UpdateFromMediaInfo(info, videoStream);
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts ffprobe stream info to our MediaStream class
|
||||
/// </summary>
|
||||
/// <param name="streamInfo">The stream info.</param>
|
||||
/// <param name="formatInfo">The format info.</param>
|
||||
/// <returns>MediaStream.</returns>
|
||||
private MediaStream GetMediaStream(MediaStreamInfo streamInfo, MediaFormatInfo formatInfo)
|
||||
{
|
||||
var stream = new MediaStream
|
||||
{
|
||||
Codec = streamInfo.codec_name,
|
||||
Profile = streamInfo.profile,
|
||||
Level = streamInfo.level,
|
||||
Index = streamInfo.index,
|
||||
PixelFormat = streamInfo.pix_fmt
|
||||
};
|
||||
|
||||
if (streamInfo.tags != null)
|
||||
{
|
||||
stream.Language = GetDictionaryValue(streamInfo.tags, "language");
|
||||
}
|
||||
|
||||
if (string.Equals(streamInfo.codec_type, "audio", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = MediaStreamType.Audio;
|
||||
|
||||
stream.Channels = streamInfo.channels;
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.sample_rate))
|
||||
{
|
||||
stream.SampleRate = int.Parse(streamInfo.sample_rate, _usCulture);
|
||||
}
|
||||
|
||||
stream.ChannelLayout = ParseChannelLayout(streamInfo.channel_layout);
|
||||
}
|
||||
else if (string.Equals(streamInfo.codec_type, "subtitle", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = MediaStreamType.Subtitle;
|
||||
}
|
||||
else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = (streamInfo.codec_name ?? string.Empty).IndexOf("mjpeg", StringComparison.OrdinalIgnoreCase) != -1
|
||||
? MediaStreamType.EmbeddedImage
|
||||
: MediaStreamType.Video;
|
||||
|
||||
stream.Width = streamInfo.width;
|
||||
stream.Height = streamInfo.height;
|
||||
stream.AspectRatio = GetAspectRatio(streamInfo);
|
||||
|
||||
stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate);
|
||||
stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate);
|
||||
|
||||
stream.BitDepth = GetBitDepth(stream.PixelFormat);
|
||||
|
||||
//stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) ||
|
||||
// string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) ||
|
||||
// string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get stream bitrate
|
||||
var bitrate = 0;
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.bit_rate))
|
||||
{
|
||||
bitrate = int.Parse(streamInfo.bit_rate, _usCulture);
|
||||
}
|
||||
else if (formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate) && stream.Type == MediaStreamType.Video)
|
||||
{
|
||||
// If the stream info doesn't have a bitrate get the value from the media format info
|
||||
bitrate = int.Parse(formatInfo.bit_rate, _usCulture);
|
||||
}
|
||||
|
||||
if (bitrate > 0)
|
||||
{
|
||||
stream.BitRate = bitrate;
|
||||
}
|
||||
|
||||
if (streamInfo.disposition != null)
|
||||
{
|
||||
var isDefault = GetDictionaryValue(streamInfo.disposition, "default");
|
||||
var isForced = GetDictionaryValue(streamInfo.disposition, "forced");
|
||||
|
||||
stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
private int? GetBitDepth(string pixelFormat)
|
||||
{
|
||||
var eightBit = new List<string>
|
||||
{
|
||||
"yuv420p",
|
||||
"yuv411p",
|
||||
"yuvj420p",
|
||||
"uyyvyy411",
|
||||
"nv12",
|
||||
"nv21",
|
||||
"rgb444le",
|
||||
"rgb444be",
|
||||
"bgr444le",
|
||||
"bgr444be",
|
||||
"yuvj411p"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(pixelFormat))
|
||||
{
|
||||
if (eightBit.Contains(pixelFormat, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string from an FFProbeResult tags dictionary
|
||||
/// </summary>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetDictionaryValue(Dictionary<string, string> tags, string key)
|
||||
{
|
||||
if (tags == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string val;
|
||||
|
||||
tags.TryGetValue(key, out val);
|
||||
return val;
|
||||
}
|
||||
|
||||
private string ParseChannelLayout(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
return input.Split('(').FirstOrDefault();
|
||||
}
|
||||
|
||||
private string GetAspectRatio(MediaStreamInfo info)
|
||||
{
|
||||
var original = info.display_aspect_ratio;
|
||||
|
||||
int height;
|
||||
int width;
|
||||
|
||||
var parts = (original ?? string.Empty).Split(':');
|
||||
if (!(parts.Length == 2 &&
|
||||
int.TryParse(parts[0], NumberStyles.Any, _usCulture, out width) &&
|
||||
int.TryParse(parts[1], NumberStyles.Any, _usCulture, out height) &&
|
||||
width > 0 &&
|
||||
height > 0))
|
||||
{
|
||||
width = info.width;
|
||||
height = info.height;
|
||||
}
|
||||
|
||||
if (width > 0 && height > 0)
|
||||
{
|
||||
double ratio = width;
|
||||
ratio /= height;
|
||||
|
||||
if (IsClose(ratio, 1.777777778, .03))
|
||||
{
|
||||
return "16:9";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.3333333333, .05))
|
||||
{
|
||||
return "4:3";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.41))
|
||||
{
|
||||
return "1.41:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.5))
|
||||
{
|
||||
return "1.5:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.6))
|
||||
{
|
||||
return "1.6:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.66666666667))
|
||||
{
|
||||
return "5:3";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 1.85, .02))
|
||||
{
|
||||
return "1.85:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 2.35, .025))
|
||||
{
|
||||
return "2.35:1";
|
||||
}
|
||||
|
||||
if (IsClose(ratio, 2.4, .025))
|
||||
{
|
||||
return "2.40:1";
|
||||
}
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
private bool IsClose(double d1, double d2, double variance = .005)
|
||||
{
|
||||
return Math.Abs(d1 - d2) <= variance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a frame rate from a string value in ffprobe output
|
||||
/// This could be a number or in the format of 2997/125.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>System.Nullable{System.Single}.</returns>
|
||||
private float? GetFrameRate(string value)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
var parts = value.Split('/');
|
||||
|
||||
float result;
|
||||
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
result = float.Parse(parts[0], _usCulture) / float.Parse(parts[1], _usCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = float.Parse(parts[0], _usCulture);
|
||||
}
|
||||
|
||||
return float.IsNaN(result) ? (float?)null : result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void SetAudioRuntimeTicks(InternalMediaInfoResult result, Model.MediaInfo.MediaInfo data)
|
||||
{
|
||||
if (result.streams != null)
|
||||
{
|
||||
// Get the first audio stream
|
||||
var stream = result.streams.FirstOrDefault(s => string.Equals(s.codec_type, "audio", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (stream != null)
|
||||
{
|
||||
// Get duration from stream properties
|
||||
var duration = stream.duration;
|
||||
|
||||
// If it's not there go into format properties
|
||||
if (string.IsNullOrEmpty(duration))
|
||||
{
|
||||
duration = result.format.duration;
|
||||
}
|
||||
|
||||
// If we got something, parse it
|
||||
if (!string.IsNullOrEmpty(duration))
|
||||
{
|
||||
data.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(duration, _usCulture)).Ticks;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetSize(InternalMediaInfoResult data, Model.MediaInfo.MediaInfo info)
|
||||
{
|
||||
if (data.format != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(data.format.size))
|
||||
{
|
||||
info.Size = long.Parse(data.format.size, _usCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
info.Size = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetAudioInfoFromTags(Model.MediaInfo.MediaInfo audio, Dictionary<string, string> tags)
|
||||
{
|
||||
var title = FFProbeHelpers.GetDictionaryValue(tags, "title");
|
||||
|
||||
// Only set Name if title was found in the dictionary
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
audio.Title = title;
|
||||
}
|
||||
|
||||
var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(composer))
|
||||
{
|
||||
foreach (var person in Split(composer, false))
|
||||
{
|
||||
audio.People.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer });
|
||||
}
|
||||
}
|
||||
|
||||
audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
|
||||
|
||||
var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(artists))
|
||||
{
|
||||
audio.Artists = artists.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist");
|
||||
if (string.IsNullOrWhiteSpace(artist))
|
||||
{
|
||||
audio.Artists.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
audio.Artists = SplitArtists(artist)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
var albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "albumartist");
|
||||
if (string.IsNullOrWhiteSpace(albumArtist))
|
||||
{
|
||||
albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album artist");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(albumArtist))
|
||||
{
|
||||
albumArtist = FFProbeHelpers.GetDictionaryValue(tags, "album_artist");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(albumArtist))
|
||||
{
|
||||
audio.AlbumArtists = new List<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
audio.AlbumArtists = SplitArtists(albumArtist)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
}
|
||||
|
||||
// Track number
|
||||
audio.IndexNumber = GetDictionaryDiscValue(tags, "track");
|
||||
|
||||
// Disc number
|
||||
audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc");
|
||||
|
||||
audio.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
|
||||
|
||||
// Several different forms of retaildate
|
||||
audio.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "date");
|
||||
|
||||
// If we don't have a ProductionYear try and get it from PremiereDate
|
||||
if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
|
||||
{
|
||||
audio.ProductionYear = audio.PremiereDate.Value.ToLocalTime().Year;
|
||||
}
|
||||
|
||||
FetchGenres(audio, tags);
|
||||
|
||||
// There's several values in tags may or may not be present
|
||||
FetchStudios(audio, tags, "organization");
|
||||
FetchStudios(audio, tags, "ensemble");
|
||||
FetchStudios(audio, tags, "publisher");
|
||||
|
||||
// These support mulitple values, but for now we only store the first.
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzAlbumArtist, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Artist Id")));
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzArtist, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Artist Id")));
|
||||
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzAlbum, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Id")));
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Group Id")));
|
||||
audio.SetProviderId(MetadataProviders.MusicBrainzTrack, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Release Track Id")));
|
||||
}
|
||||
|
||||
private string GetMultipleMusicBrainzId(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(i => i.Trim())
|
||||
.FirstOrDefault(i => !string.IsNullOrWhiteSpace(i));
|
||||
}
|
||||
|
||||
private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
|
||||
|
||||
/// <summary>
|
||||
/// Splits the specified val.
|
||||
/// </summary>
|
||||
/// <param name="val">The val.</param>
|
||||
/// <param name="allowCommaDelimiter">if set to <c>true</c> [allow comma delimiter].</param>
|
||||
/// <returns>System.String[][].</returns>
|
||||
private IEnumerable<string> Split(string val, bool allowCommaDelimiter)
|
||||
{
|
||||
// Only use the comma as a delimeter if there are no slashes or pipes.
|
||||
// We want to be careful not to split names that have commas in them
|
||||
var delimeter = !allowCommaDelimiter || _nameDelimiters.Any(i => val.IndexOf(i) != -1) ?
|
||||
_nameDelimiters :
|
||||
new[] { ',' };
|
||||
|
||||
return val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => i.Trim());
|
||||
}
|
||||
|
||||
private const string ArtistReplaceValue = " | ";
|
||||
|
||||
private IEnumerable<string> SplitArtists(string val)
|
||||
{
|
||||
val = val.Replace(" featuring ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(" feat. ", ArtistReplaceValue, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var artistsFound = new List<string>();
|
||||
|
||||
foreach (var whitelistArtist in GetSplitWhitelist())
|
||||
{
|
||||
var originalVal = val;
|
||||
val = val.Replace(whitelistArtist, "|", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!string.Equals(originalVal, val, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
artistsFound.Add(whitelistArtist);
|
||||
}
|
||||
}
|
||||
|
||||
// Only use the comma as a delimeter if there are no slashes or pipes.
|
||||
// We want to be careful not to split names that have commas in them
|
||||
var delimeter = _nameDelimiters;
|
||||
|
||||
var artists = val.Split(delimeter, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => i.Trim());
|
||||
|
||||
artistsFound.AddRange(artists);
|
||||
return artistsFound;
|
||||
}
|
||||
|
||||
|
||||
private List<string> _splitWhiteList = null;
|
||||
|
||||
private IEnumerable<string> GetSplitWhitelist()
|
||||
{
|
||||
if (_splitWhiteList == null)
|
||||
{
|
||||
var file = GetType().Namespace + ".whitelist.txt";
|
||||
|
||||
using (var stream = GetType().Assembly.GetManifestResourceStream(file))
|
||||
{
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var list = new List<string>();
|
||||
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var val = reader.ReadLine();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
list.Add(val);
|
||||
}
|
||||
}
|
||||
|
||||
_splitWhiteList = list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _splitWhiteList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the studios from the tags collection
|
||||
/// </summary>
|
||||
/// <param name="audio">The audio.</param>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="tagName">Name of the tag.</param>
|
||||
private void FetchStudios(Model.MediaInfo.MediaInfo audio, Dictionary<string, string> tags, string tagName)
|
||||
{
|
||||
var val = FFProbeHelpers.GetDictionaryValue(tags, tagName);
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
var studios = Split(val, true);
|
||||
|
||||
foreach (var studio in studios)
|
||||
{
|
||||
// Sometimes the artist name is listed here, account for that
|
||||
if (audio.Artists.Contains(studio, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (audio.AlbumArtists.Contains(studio, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
audio.Studios.Add(studio);
|
||||
}
|
||||
|
||||
audio.Studios = audio.Studios
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the genres from the tags collection
|
||||
/// </summary>
|
||||
/// <param name="info">The information.</param>
|
||||
/// <param name="tags">The tags.</param>
|
||||
private void FetchGenres(Model.MediaInfo.MediaInfo info, Dictionary<string, string> tags)
|
||||
{
|
||||
var val = FFProbeHelpers.GetDictionaryValue(tags, "genre");
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
foreach (var genre in Split(val, true))
|
||||
{
|
||||
info.Genres.Add(genre);
|
||||
}
|
||||
|
||||
info.Genres = info.Genres
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the disc number, which is sometimes can be in the form of '1', or '1/3'
|
||||
/// </summary>
|
||||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="tagName">Name of the tag.</param>
|
||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
private int? GetDictionaryDiscValue(Dictionary<string, string> tags, string tagName)
|
||||
{
|
||||
var disc = FFProbeHelpers.GetDictionaryValue(tags, tagName);
|
||||
|
||||
if (!string.IsNullOrEmpty(disc))
|
||||
{
|
||||
disc = disc.Split('/')[0];
|
||||
|
||||
int num;
|
||||
|
||||
if (int.TryParse(disc, out num))
|
||||
{
|
||||
return num;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private ChapterInfo GetChapterInfo(MediaChapter chapter)
|
||||
{
|
||||
var info = new ChapterInfo();
|
||||
|
||||
if (chapter.tags != null)
|
||||
{
|
||||
string name;
|
||||
if (chapter.tags.TryGetValue("title", out name))
|
||||
{
|
||||
info.Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
// Limit accuracy to milliseconds to match xml saving
|
||||
var secondsString = chapter.start_time;
|
||||
double seconds;
|
||||
|
||||
if (double.TryParse(secondsString, NumberStyles.Any, CultureInfo.InvariantCulture, out seconds))
|
||||
{
|
||||
var ms = Math.Round(TimeSpan.FromSeconds(seconds).TotalMilliseconds);
|
||||
info.StartPositionTicks = TimeSpan.FromMilliseconds(ms).Ticks;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private const int MaxSubtitleDescriptionExtractionLength = 100; // When extracting subtitles, the maximum length to consider (to avoid invalid filenames)
|
||||
|
||||
private void FetchWtvInfo(Model.MediaInfo.MediaInfo video, InternalMediaInfoResult data)
|
||||
{
|
||||
if (data.format == null || data.format.tags == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/Genre");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(genres))
|
||||
{
|
||||
//genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "genre");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(genres))
|
||||
{
|
||||
video.Genres = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => i.Trim())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var officialRating = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/ParentalRating");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(officialRating))
|
||||
{
|
||||
video.OfficialRating = officialRating;
|
||||
}
|
||||
|
||||
var people = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaCredits");
|
||||
|
||||
if (!string.IsNullOrEmpty(people))
|
||||
{
|
||||
video.People = people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => new BaseItemPerson { Name = i.Trim(), Type = PersonType.Actor })
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var year = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/OriginalReleaseTime");
|
||||
if (!string.IsNullOrWhiteSpace(year))
|
||||
{
|
||||
int val;
|
||||
|
||||
if (int.TryParse(year, NumberStyles.Integer, _usCulture, out val))
|
||||
{
|
||||
video.ProductionYear = val;
|
||||
}
|
||||
}
|
||||
|
||||
var premiereDateString = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaOriginalBroadcastDateTime");
|
||||
if (!string.IsNullOrWhiteSpace(premiereDateString))
|
||||
{
|
||||
DateTime val;
|
||||
|
||||
// Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
|
||||
// DateTime is reported along with timezone info (typically Z i.e. UTC hence assume None)
|
||||
if (DateTime.TryParse(year, null, DateTimeStyles.None, out val))
|
||||
{
|
||||
video.PremiereDate = val.ToUniversalTime();
|
||||
}
|
||||
}
|
||||
|
||||
var description = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitleDescription");
|
||||
|
||||
var subTitle = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitle");
|
||||
|
||||
// For below code, credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
|
||||
|
||||
// Sometimes for TV Shows the Subtitle field is empty and the subtitle description contains the subtitle, extract if possible. See ticket https://mcebuddy2x.codeplex.com/workitem/1910
|
||||
// The format is -> EPISODE/TOTAL_EPISODES_IN_SEASON. SUBTITLE: DESCRIPTION
|
||||
// OR -> COMMENT. SUBTITLE: DESCRIPTION
|
||||
// e.g. -> 4/13. The Doctor's Wife: Science fiction drama. When he follows a Time Lord distress signal, the Doctor puts Amy, Rory and his beloved TARDIS in grave danger. Also in HD. [AD,S]
|
||||
// e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S]
|
||||
if (String.IsNullOrWhiteSpace(subTitle) && !String.IsNullOrWhiteSpace(description) && description.Substring(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).Contains(":")) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
|
||||
{
|
||||
string[] parts = description.Split(':');
|
||||
if (parts.Length > 0)
|
||||
{
|
||||
string subtitle = parts[0];
|
||||
try
|
||||
{
|
||||
if (subtitle.Contains("/")) // It contains a episode number and season number
|
||||
{
|
||||
string[] numbers = subtitle.Split(' ');
|
||||
video.IndexNumber = int.Parse(numbers[0].Replace(".", "").Split('/')[0]);
|
||||
int totalEpisodesInSeason = int.Parse(numbers[0].Replace(".", "").Split('/')[1]);
|
||||
|
||||
description = String.Join(" ", numbers, 1, numbers.Length - 1).Trim(); // Skip the first, concatenate the rest, clean up spaces and save it
|
||||
}
|
||||
else
|
||||
throw new Exception(); // Switch to default parsing
|
||||
}
|
||||
catch // Default parsing
|
||||
{
|
||||
if (subtitle.Contains(".")) // skip the comment, keep the subtitle
|
||||
description = String.Join(".", subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
|
||||
else
|
||||
description = subtitle.Trim(); // Clean up whitespaces and save it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
video.Overview = description;
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractTimestamp(Model.MediaInfo.MediaInfo video)
|
||||
{
|
||||
if (video.VideoType == VideoType.VideoFile)
|
||||
{
|
||||
if (string.Equals(video.Container, "mpeg2ts", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(video.Container, "m2ts", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(video.Container, "ts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
video.Timestamp = GetMpegTimestamp(video.Path);
|
||||
|
||||
_logger.Debug("Video has {0} timestamp", video.Timestamp);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error extracting timestamp info from {0}", ex, video.Path);
|
||||
video.Timestamp = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TransportStreamTimestamp GetMpegTimestamp(string path)
|
||||
{
|
||||
var packetBuffer = new byte['Å'];
|
||||
|
||||
using (var fs = _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
fs.Read(packetBuffer, 0, packetBuffer.Length);
|
||||
}
|
||||
|
||||
if (packetBuffer[0] == 71)
|
||||
{
|
||||
return TransportStreamTimestamp.None;
|
||||
}
|
||||
|
||||
if ((packetBuffer[4] == 71) && (packetBuffer['Ä'] == 71))
|
||||
{
|
||||
if ((packetBuffer[0] == 0) && (packetBuffer[1] == 0) && (packetBuffer[2] == 0) && (packetBuffer[3] == 0))
|
||||
{
|
||||
return TransportStreamTimestamp.Zero;
|
||||
}
|
||||
|
||||
return TransportStreamTimestamp.Valid;
|
||||
}
|
||||
|
||||
return TransportStreamTimestamp.None;
|
||||
}
|
||||
|
||||
private void UpdateFromMediaInfo(MediaSourceInfo video, MediaStream videoStream)
|
||||
{
|
||||
if (video.VideoType == VideoType.VideoFile && video.Protocol == MediaProtocol.File)
|
||||
{
|
||||
if (videoStream != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = new MediaInfoLib().GetVideoInfo(video.Path);
|
||||
|
||||
videoStream.IsCabac = result.IsCabac ?? videoStream.IsCabac;
|
||||
videoStream.IsInterlaced = result.IsInterlaced ?? videoStream.IsInterlaced;
|
||||
videoStream.BitDepth = result.BitDepth ?? videoStream.BitDepth;
|
||||
videoStream.RefFrames = result.RefFrames;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error running MediaInfo on {0}", ex, video.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user