mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-07-09 03:04:24 -04:00
commit
935de313d5
@ -6,7 +6,7 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace MediaBrowser.Server.Implementations.Drawing
|
namespace Emby.Drawing.Common
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Taken from http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349
|
/// 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 ImageMagickSharp;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace MediaBrowser.Server.Implementations.Drawing
|
namespace Emby.Drawing.ImageMagick
|
||||||
{
|
{
|
||||||
public class PercentPlayedDrawer
|
public class PercentPlayedDrawer
|
||||||
{
|
{
|
@ -4,7 +4,7 @@ using MediaBrowser.Model.Drawing;
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
namespace MediaBrowser.Server.Implementations.Drawing
|
namespace Emby.Drawing.ImageMagick
|
||||||
{
|
{
|
||||||
public class PlayedIndicatorDrawer
|
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 MediaBrowser.Model.Drawing;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace MediaBrowser.Server.Implementations.Drawing
|
namespace Emby.Drawing.ImageMagick
|
||||||
{
|
{
|
||||||
public class UnplayedCountIndicator
|
public class UnplayedCountIndicator
|
||||||
{
|
{
|
@ -1,4 +1,4 @@
|
|||||||
using ImageMagickSharp;
|
using Emby.Drawing.Common;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.IO;
|
using MediaBrowser.Common.IO;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
@ -18,7 +18,7 @@ using System.Linq;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace MediaBrowser.Server.Implementations.Drawing
|
namespace Emby.Drawing
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class ImageProcessor
|
/// Class ImageProcessor
|
||||||
@ -50,12 +50,14 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
private readonly IJsonSerializer _jsonSerializer;
|
||||||
private readonly IServerApplicationPaths _appPaths;
|
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;
|
_logger = logger;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
_jsonSerializer = jsonSerializer;
|
_jsonSerializer = jsonSerializer;
|
||||||
|
_imageEncoder = imageEncoder;
|
||||||
_appPaths = appPaths;
|
_appPaths = appPaths;
|
||||||
|
|
||||||
_saveImageSizeTimer = new Timer(SaveImageSizeCallback, null, Timeout.Infinite, Timeout.Infinite);
|
_saveImageSizeTimer = new Timer(SaveImageSizeCallback, null, Timeout.Infinite, Timeout.Infinite);
|
||||||
@ -85,8 +87,14 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||||||
}
|
}
|
||||||
|
|
||||||
_cachedImagedSizes = new ConcurrentDictionary<Guid, ImageSize>(sizeDictionary);
|
_cachedImagedSizes = new ConcurrentDictionary<Guid, ImageSize>(sizeDictionary);
|
||||||
|
}
|
||||||
|
|
||||||
LogImageMagickVersionVersion();
|
public string[] SupportedInputFormats
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _imageEncoder.SupportedInputFormats;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ResizedImageCachePath
|
private string ResizedImageCachePath
|
||||||
@ -130,44 +138,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||||||
|
|
||||||
public ImageFormat[] GetSupportedImageOutputFormats()
|
public ImageFormat[] GetSupportedImageOutputFormats()
|
||||||
{
|
{
|
||||||
if (_webpAvailable)
|
return _imageEncoder.SupportedOutputFormats;
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> ProcessImage(ImageProcessingOptions options)
|
public async Task<string> ProcessImage(ImageProcessingOptions options)
|
||||||
@ -244,36 +215,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
|
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(options.BackgroundColor))
|
_imageEncoder.EncodeImage(originalImagePath, cacheFilePath, newWidth, newHeight, quality, options);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -286,7 +228,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||||||
|
|
||||||
private ImageFormat GetOutputFormat(ImageFormat requestedFormat)
|
private ImageFormat GetOutputFormat(ImageFormat requestedFormat)
|
||||||
{
|
{
|
||||||
if (requestedFormat == ImageFormat.Webp && !_webpAvailable)
|
if (requestedFormat == ImageFormat.Webp && !_imageEncoder.SupportedOutputFormats.Contains(ImageFormat.Webp))
|
||||||
{
|
{
|
||||||
return ImageFormat.Png;
|
return ImageFormat.Png;
|
||||||
}
|
}
|
||||||
@ -294,46 +236,6 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||||||
return requestedFormat;
|
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>
|
/// <summary>
|
||||||
/// Crops whitespace from an image, caches the result, and returns the cached path
|
/// Crops whitespace from an image, caches the result, and returns the cached path
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -360,11 +262,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||||||
{
|
{
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(croppedImagePath));
|
Directory.CreateDirectory(Path.GetDirectoryName(croppedImagePath));
|
||||||
|
|
||||||
using (var wand = new MagickWand(originalImagePath))
|
_imageEncoder.CropWhiteSpace(originalImagePath, croppedImagePath);
|
||||||
{
|
|
||||||
wand.CurrentImage.TrimImage(10);
|
|
||||||
wand.SaveImage(croppedImagePath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -500,17 +398,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||||||
|
|
||||||
CheckDisposed();
|
CheckDisposed();
|
||||||
|
|
||||||
using (var wand = new MagickWand())
|
size = _imageEncoder.GetImageSize(path);
|
||||||
{
|
|
||||||
wand.PingImage(path);
|
|
||||||
var img = wand.CurrentImage;
|
|
||||||
|
|
||||||
size = new ImageSize
|
|
||||||
{
|
|
||||||
Width = img.Width,
|
|
||||||
Height = img.Height
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StartSaveImageSizeTimer();
|
StartSaveImageSizeTimer();
|
||||||
@ -838,6 +726,11 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||||||
return Path.Combine(path, filename);
|
return Path.Combine(path, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void CreateImageCollage(ImageCollageOptions options)
|
||||||
|
{
|
||||||
|
_imageEncoder.CreateImageCollage(options);
|
||||||
|
}
|
||||||
|
|
||||||
public IEnumerable<IImageEnhancer> GetSupportedEnhancers(IHasImages item, ImageType imageType)
|
public IEnumerable<IImageEnhancer> GetSupportedEnhancers(IHasImages item, ImageType imageType)
|
||||||
{
|
{
|
||||||
return ImageEnhancers.Where(i =>
|
return ImageEnhancers.Where(i =>
|
||||||
@ -860,7 +753,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
Wand.CloseEnvironment();
|
_imageEncoder.Dispose();
|
||||||
_saveImageSizeTimer.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)
|
lock (_activeTranscodingJobs)
|
||||||
{
|
{
|
||||||
var job = new TranscodingJob
|
var job = new TranscodingJob(Logger)
|
||||||
{
|
{
|
||||||
Type = type,
|
Type = type,
|
||||||
Path = path,
|
Path = path,
|
||||||
@ -284,28 +284,72 @@ namespace MediaBrowser.Api
|
|||||||
{
|
{
|
||||||
job.ActiveRequestCount++;
|
job.ActiveRequestCount++;
|
||||||
|
|
||||||
job.DisposeKillTimer();
|
if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
|
||||||
|
{
|
||||||
|
job.StopKillTimer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnTranscodeEndRequest(TranscodingJob job)
|
public void OnTranscodeEndRequest(TranscodingJob job)
|
||||||
{
|
{
|
||||||
job.ActiveRequestCount--;
|
job.ActiveRequestCount--;
|
||||||
|
Logger.Debug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount);
|
||||||
if (job.ActiveRequestCount == 0)
|
if (job.ActiveRequestCount <= 0)
|
||||||
{
|
{
|
||||||
// TODO: Lower this hls timeout
|
PingTimer(job, false);
|
||||||
var timerDuration = job.Type == TranscodingJobType.Progressive ?
|
}
|
||||||
1000 :
|
}
|
||||||
7200000;
|
internal void PingTranscodingJob(string playSessionId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(playSessionId))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("playSessionId");
|
||||||
|
}
|
||||||
|
|
||||||
if (job.KillTimer == null)
|
Logger.Debug("PingTranscodingJob PlaySessionId={0}", playSessionId);
|
||||||
{
|
|
||||||
job.KillTimer = new Timer(OnTranscodeKillTimerStopped, job, timerDuration, Timeout.Infinite);
|
var jobs = new List<TranscodingJob>();
|
||||||
}
|
|
||||||
else
|
lock (_activeTranscodingJobs)
|
||||||
{
|
{
|
||||||
job.KillTimer.Change(timerDuration, Timeout.Infinite);
|
// 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;
|
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);
|
KillTranscodingJob(job, path => true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,19 +375,14 @@ namespace MediaBrowser.Api
|
|||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
internal void KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles)
|
internal void KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(deviceId))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException("deviceId");
|
|
||||||
}
|
|
||||||
|
|
||||||
KillTranscodingJobs(j =>
|
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);
|
}, deleteFiles);
|
||||||
}
|
}
|
||||||
@ -381,6 +422,10 @@ namespace MediaBrowser.Api
|
|||||||
/// <param name="delete">The delete.</param>
|
/// <param name="delete">The delete.</param>
|
||||||
private void KillTranscodingJob(TranscodingJob job, Func<string, bool> delete)
|
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)
|
lock (_activeTranscodingJobs)
|
||||||
{
|
{
|
||||||
_activeTranscodingJobs.Remove(job);
|
_activeTranscodingJobs.Remove(job);
|
||||||
@ -389,34 +434,23 @@ namespace MediaBrowser.Api
|
|||||||
{
|
{
|
||||||
job.CancellationTokenSource.Cancel();
|
job.CancellationTokenSource.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
job.DisposeKillTimer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (job.ProcessLock)
|
lock (job.ProcessLock)
|
||||||
{
|
{
|
||||||
|
if (job.TranscodingThrottler != null)
|
||||||
|
{
|
||||||
|
job.TranscodingThrottler.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
var process = job.Process;
|
var process = job.Process;
|
||||||
|
|
||||||
var hasExited = true;
|
var hasExited = job.HasExited;
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
hasExited = process.HasExited;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.ErrorException("Error determining if ffmpeg process has exited for {0}", ex, job.Path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasExited)
|
if (!hasExited)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (job.TranscodingThrottler != null)
|
|
||||||
{
|
|
||||||
job.TranscodingThrottler.Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Info("Killing ffmpeg process for {0}", job.Path);
|
Logger.Info("Killing ffmpeg process for {0}", job.Path);
|
||||||
|
|
||||||
//process.Kill();
|
//process.Kill();
|
||||||
@ -558,6 +592,7 @@ namespace MediaBrowser.Api
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The process.</value>
|
/// <value>The process.</value>
|
||||||
public Process Process { get; set; }
|
public Process Process { get; set; }
|
||||||
|
public ILogger Logger { get; private set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the active request count.
|
/// Gets or sets the active request count.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -567,7 +602,7 @@ namespace MediaBrowser.Api
|
|||||||
/// Gets or sets the kill timer.
|
/// Gets or sets the kill timer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The kill timer.</value>
|
/// <value>The kill timer.</value>
|
||||||
public Timer KillTimer { get; set; }
|
private Timer KillTimer { get; set; }
|
||||||
|
|
||||||
public string DeviceId { get; set; }
|
public string DeviceId { get; set; }
|
||||||
|
|
||||||
@ -590,12 +625,74 @@ namespace MediaBrowser.Api
|
|||||||
|
|
||||||
public TranscodingThrottler TranscodingThrottler { get; set; }
|
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()
|
public void DisposeKillTimer()
|
||||||
{
|
{
|
||||||
if (KillTimer != null)
|
lock (_timerLock)
|
||||||
{
|
{
|
||||||
KillTimer.Dispose();
|
if (KillTimer != null)
|
||||||
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)
|
.GetRecursiveChildren(i => i is IHasArtist)
|
||||||
.Cast<IHasArtist>()
|
.Cast<IHasArtist>()
|
||||||
.SelectMany(i => i.AllArtists)
|
.SelectMany(i => i.AllArtists)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.FirstOrDefault(i =>
|
.FirstOrDefault(i =>
|
||||||
{
|
{
|
||||||
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
||||||
@ -281,7 +281,7 @@ namespace MediaBrowser.Api
|
|||||||
|
|
||||||
return libraryManager.RootFolder.GetRecursiveChildren()
|
return libraryManager.RootFolder.GetRecursiveChildren()
|
||||||
.SelectMany(i => i.Genres)
|
.SelectMany(i => i.Genres)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.FirstOrDefault(i =>
|
.FirstOrDefault(i =>
|
||||||
{
|
{
|
||||||
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
||||||
@ -301,7 +301,7 @@ namespace MediaBrowser.Api
|
|||||||
return libraryManager.RootFolder
|
return libraryManager.RootFolder
|
||||||
.GetRecursiveChildren(i => i is Game)
|
.GetRecursiveChildren(i => i is Game)
|
||||||
.SelectMany(i => i.Genres)
|
.SelectMany(i => i.Genres)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.FirstOrDefault(i =>
|
.FirstOrDefault(i =>
|
||||||
{
|
{
|
||||||
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
||||||
@ -324,7 +324,7 @@ namespace MediaBrowser.Api
|
|||||||
return libraryManager.RootFolder
|
return libraryManager.RootFolder
|
||||||
.GetRecursiveChildren()
|
.GetRecursiveChildren()
|
||||||
.SelectMany(i => i.Studios)
|
.SelectMany(i => i.Studios)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.FirstOrDefault(i =>
|
.FirstOrDefault(i =>
|
||||||
{
|
{
|
||||||
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
||||||
@ -348,7 +348,7 @@ namespace MediaBrowser.Api
|
|||||||
.GetRecursiveChildren()
|
.GetRecursiveChildren()
|
||||||
.SelectMany(i => i.People)
|
.SelectMany(i => i.People)
|
||||||
.Select(i => i.Name)
|
.Select(i => i.Name)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.FirstOrDefault(i =>
|
.FirstOrDefault(i =>
|
||||||
{
|
{
|
||||||
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
||||||
|
@ -123,7 +123,7 @@ namespace MediaBrowser.Api
|
|||||||
|
|
||||||
public void Post(AutoSetMetadataOptions request)
|
public void Post(AutoSetMetadataOptions request)
|
||||||
{
|
{
|
||||||
_configurationManager.DisableMetadataService("Media Browser Xml");
|
_configurationManager.DisableMetadataService("Emby Xml");
|
||||||
_configurationManager.SaveConfiguration();
|
_configurationManager.SaveConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ namespace MediaBrowser.Api
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
result.Genres = items.SelectMany(i => i.Genres)
|
result.Genres = items.SelectMany(i => i.Genres)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.OrderBy(i => i)
|
.OrderBy(i => i)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Dto;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
using MediaBrowser.Controller.LiveTv;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Model.Dto;
|
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")]
|
[ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
|
||||||
public bool? IsMovie { get; set; }
|
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")]
|
[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; }
|
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")]
|
[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; }
|
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")]
|
[ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
|
||||||
public bool? IsMovie { get; set; }
|
public bool? IsMovie { get; set; }
|
||||||
}
|
}
|
||||||
@ -422,11 +429,12 @@ namespace MediaBrowser.Api.LiveTv
|
|||||||
query.SortBy = (request.SortBy ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
query.SortBy = (request.SortBy ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
query.SortOrder = request.SortOrder;
|
query.SortOrder = request.SortOrder;
|
||||||
query.IsMovie = request.IsMovie;
|
query.IsMovie = request.IsMovie;
|
||||||
|
query.IsSports = request.IsSports;
|
||||||
query.Genres = (request.Genres ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
query.Genres = (request.Genres ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
var result = await _liveTvManager.GetPrograms(query, CancellationToken.None).ConfigureAwait(false);
|
var result = await _liveTvManager.GetPrograms(query, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
return ToOptimizedSerializedResultUsingCache(result);
|
return ToOptimizedResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<object> Get(GetRecommendedPrograms request)
|
public async Task<object> Get(GetRecommendedPrograms request)
|
||||||
@ -437,12 +445,13 @@ namespace MediaBrowser.Api.LiveTv
|
|||||||
IsAiring = request.IsAiring,
|
IsAiring = request.IsAiring,
|
||||||
Limit = request.Limit,
|
Limit = request.Limit,
|
||||||
HasAired = request.HasAired,
|
HasAired = request.HasAired,
|
||||||
IsMovie = request.IsMovie
|
IsMovie = request.IsMovie,
|
||||||
|
IsSports = request.IsSports
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = await _liveTvManager.GetRecommendedPrograms(query, CancellationToken.None).ConfigureAwait(false);
|
var result = await _liveTvManager.GetRecommendedPrograms(query, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
return ToOptimizedSerializedResultUsingCache(result);
|
return ToOptimizedResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public object Post(GetPrograms request)
|
public object Post(GetPrograms request)
|
||||||
@ -452,6 +461,9 @@ namespace MediaBrowser.Api.LiveTv
|
|||||||
|
|
||||||
public async Task<object> Get(GetRecordings request)
|
public async Task<object> Get(GetRecordings request)
|
||||||
{
|
{
|
||||||
|
var options = new DtoOptions();
|
||||||
|
options.DeviceId = AuthorizationContext.GetAuthorizationInfo(Request).DeviceId;
|
||||||
|
|
||||||
var result = await _liveTvManager.GetRecordings(new RecordingQuery
|
var result = await _liveTvManager.GetRecordings(new RecordingQuery
|
||||||
{
|
{
|
||||||
ChannelId = request.ChannelId,
|
ChannelId = request.ChannelId,
|
||||||
@ -463,16 +475,19 @@ namespace MediaBrowser.Api.LiveTv
|
|||||||
SeriesTimerId = request.SeriesTimerId,
|
SeriesTimerId = request.SeriesTimerId,
|
||||||
IsInProgress = request.IsInProgress
|
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)
|
public async Task<object> Get(GetRecording request)
|
||||||
{
|
{
|
||||||
var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(request.UserId);
|
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);
|
return ToOptimizedSerializedResultUsingCache(result);
|
||||||
}
|
}
|
||||||
|
@ -410,7 +410,7 @@ namespace MediaBrowser.Api.Movies
|
|||||||
return items
|
return items
|
||||||
.SelectMany(i => i.People.Where(p => !string.Equals(PersonType.Director, p.Type, StringComparison.OrdinalIgnoreCase)).Take(2))
|
.SelectMany(i => i.People.Where(p => !string.Equals(PersonType.Director, p.Type, StringComparison.OrdinalIgnoreCase)).Take(2))
|
||||||
.Select(i => i.Name)
|
.Select(i => i.Name)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase);
|
.DistinctNames();
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items)
|
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)))
|
.Select(i => i.People.FirstOrDefault(p => string.Equals(PersonType.Director, p.Type, StringComparison.OrdinalIgnoreCase)))
|
||||||
.Where(i => i != null)
|
.Where(i => i != null)
|
||||||
.Select(i => i.Name)
|
.Select(i => i.Name)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase);
|
.DistinctNames();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,12 +79,12 @@ namespace MediaBrowser.Api.Music
|
|||||||
|
|
||||||
var artists1 = album1
|
var artists1 = album1
|
||||||
.AllArtists
|
.AllArtists
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var artists2 = album2
|
var artists2 = album2
|
||||||
.AllArtists
|
.AllArtists
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
|
.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
return points + artists1.Where(artists2.ContainsKey).Sum(i => 5);
|
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.
|
// 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);
|
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);
|
await state.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
|
process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
|
||||||
@ -1514,6 +1514,10 @@ namespace MediaBrowser.Api.Playback
|
|||||||
request.PlaySessionId = val;
|
request.PlaySessionId = val;
|
||||||
}
|
}
|
||||||
else if (i == 22)
|
else if (i == 22)
|
||||||
|
{
|
||||||
|
// api_key
|
||||||
|
}
|
||||||
|
else if (i == 23)
|
||||||
{
|
{
|
||||||
request.LiveStreamId = val;
|
request.LiveStreamId = val;
|
||||||
}
|
}
|
||||||
@ -1624,14 +1628,19 @@ namespace MediaBrowser.Api.Playback
|
|||||||
var archivable = item as IArchivable;
|
var archivable = item as IArchivable;
|
||||||
state.IsInputArchive = archivable != null && archivable.IsArchive;
|
state.IsInputArchive = archivable != null && archivable.IsArchive;
|
||||||
|
|
||||||
MediaSourceInfo mediaSource = null;
|
MediaSourceInfo mediaSource;
|
||||||
if (string.IsNullOrWhiteSpace(request.LiveStreamId))
|
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)
|
mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
|
||||||
? mediaSources.First()
|
? 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
|
else
|
||||||
{
|
{
|
||||||
@ -1700,6 +1709,102 @@ namespace MediaBrowser.Api.Playback
|
|||||||
{
|
{
|
||||||
state.OutputAudioCodec = "copy";
|
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,
|
private void AttachMediaSourceInfo(StreamState state,
|
||||||
|
@ -5,7 +5,6 @@ using MediaBrowser.Controller.Configuration;
|
|||||||
using MediaBrowser.Controller.Devices;
|
using MediaBrowser.Controller.Devices;
|
||||||
using MediaBrowser.Controller.Dlna;
|
using MediaBrowser.Controller.Dlna;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
@ -518,25 +517,14 @@ namespace MediaBrowser.Api.Playback.Dash
|
|||||||
|
|
||||||
private async Task WaitForSegment(string playlist, string segment, CancellationToken cancellationToken)
|
private async Task WaitForSegment(string playlist, string segment, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var tmpPath = playlist + ".tmp";
|
|
||||||
|
|
||||||
var segmentFilename = Path.GetFileName(segment);
|
var segmentFilename = Path.GetFileName(segment);
|
||||||
|
|
||||||
Logger.Debug("Waiting for {0} in {1}", segmentFilename, playlist);
|
Logger.Debug("Waiting for {0} in {1}", segmentFilename, playlist);
|
||||||
|
|
||||||
while (true)
|
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
|
// 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))
|
using (var reader = new StreamReader(fileStream))
|
||||||
{
|
{
|
||||||
|
@ -3,7 +3,6 @@ using MediaBrowser.Controller.Configuration;
|
|||||||
using MediaBrowser.Controller.Devices;
|
using MediaBrowser.Controller.Devices;
|
||||||
using MediaBrowser.Controller.Dlna;
|
using MediaBrowser.Controller.Dlna;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Model.Extensions;
|
using MediaBrowser.Model.Extensions;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
@ -86,6 +85,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||||||
state.Request.StartTimeTicks = null;
|
state.Request.StartTimeTicks = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TranscodingJob job = null;
|
||||||
var playlist = state.OutputFilePath;
|
var playlist = state.OutputFilePath;
|
||||||
|
|
||||||
if (!File.Exists(playlist))
|
if (!File.Exists(playlist))
|
||||||
@ -98,7 +98,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||||||
// If the playlist doesn't already exist, startup ffmpeg
|
// If the playlist doesn't already exist, startup ffmpeg
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false);
|
job = await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@ -117,6 +117,12 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||||||
|
|
||||||
if (isLive)
|
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>());
|
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);
|
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>());
|
return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,7 +199,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
// Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
|
// 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))
|
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)
|
protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
|
||||||
{
|
{
|
||||||
var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream;
|
var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream;
|
||||||
|
@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration;
|
|||||||
using MediaBrowser.Controller.Devices;
|
using MediaBrowser.Controller.Devices;
|
||||||
using MediaBrowser.Controller.Dlna;
|
using MediaBrowser.Controller.Dlna;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
@ -128,9 +127,27 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
var startTranscoding = false;
|
||||||
|
|
||||||
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
|
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
|
||||||
var segmentGapRequiringTranscodingChange = 24/state.SegmentLength;
|
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
|
||||||
if (currentTranscodingIndex == null || requestedIndex < currentTranscodingIndex.Value || (requestedIndex - currentTranscodingIndex.Value) > segmentGapRequiringTranscodingChange)
|
|
||||||
|
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
|
// If the playlist doesn't already exist, startup ffmpeg
|
||||||
try
|
try
|
||||||
@ -145,7 +162,6 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||||||
request.StartTimeTicks = GetSeekPositionTicks(state, requestedIndex);
|
request.StartTimeTicks = GetSeekPositionTicks(state, requestedIndex);
|
||||||
|
|
||||||
job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
|
job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
|
||||||
ApiEntryPoint.Instance.OnTranscodeBeginRequest(job);
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@ -153,7 +169,15 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||||||
throw;
|
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);
|
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))
|
using (var reader = new StreamReader(fileStream))
|
||||||
{
|
{
|
||||||
@ -712,7 +736,7 @@ namespace MediaBrowser.Api.Playback.Hls
|
|||||||
).Trim();
|
).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,
|
inputModifier,
|
||||||
GetInputArgument(state),
|
GetInputArgument(state),
|
||||||
threads,
|
threads,
|
||||||
|
@ -3,12 +3,10 @@ using MediaBrowser.Controller.Configuration;
|
|||||||
using MediaBrowser.Controller.Devices;
|
using MediaBrowser.Controller.Devices;
|
||||||
using MediaBrowser.Controller.Dlna;
|
using MediaBrowser.Controller.Dlna;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using ServiceStack;
|
using ServiceStack;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Api.Playback.Hls
|
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.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
@ -59,23 +61,27 @@ namespace MediaBrowser.Api.Playback
|
|||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
private readonly IMediaSourceManager _mediaSourceManager;
|
||||||
private readonly IDeviceManager _deviceManager;
|
private readonly IDeviceManager _deviceManager;
|
||||||
private readonly ILibraryManager _libraryManager;
|
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;
|
_mediaSourceManager = mediaSourceManager;
|
||||||
_deviceManager = deviceManager;
|
_deviceManager = deviceManager;
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
|
_config = config;
|
||||||
|
_networkManager = networkManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<object> Get(GetPlaybackInfo request)
|
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);
|
return ToOptimizedResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<object> Get(GetLiveMediaInfo request)
|
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);
|
return ToOptimizedResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,29 +128,32 @@ namespace MediaBrowser.Api.Playback
|
|||||||
|
|
||||||
public async Task<object> Post(GetPostedPlaybackInfo request)
|
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 authInfo = AuthorizationContext.GetAuthorizationInfo(Request);
|
||||||
|
|
||||||
var profile = request.DeviceProfile;
|
var profile = request.DeviceProfile;
|
||||||
if (profile == null)
|
|
||||||
|
var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
|
||||||
|
if (caps != null)
|
||||||
{
|
{
|
||||||
var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
|
if (profile == null)
|
||||||
if (caps != null)
|
|
||||||
{
|
{
|
||||||
profile = caps.DeviceProfile;
|
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)
|
if (profile != null)
|
||||||
{
|
{
|
||||||
var mediaSourceId = request.MediaSourceId;
|
var mediaSourceId = request.MediaSourceId;
|
||||||
|
|
||||||
SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex);
|
SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ToOptimizedResult(info);
|
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();
|
var result = new PlaybackInfoResponse();
|
||||||
|
|
||||||
@ -153,7 +162,7 @@ namespace MediaBrowser.Api.Playback
|
|||||||
IEnumerable<MediaSourceInfo> mediaSources;
|
IEnumerable<MediaSourceInfo> mediaSources;
|
||||||
try
|
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)
|
catch (PlaybackException ex)
|
||||||
{
|
{
|
||||||
@ -223,7 +232,7 @@ namespace MediaBrowser.Api.Playback
|
|||||||
int? subtitleStreamIndex,
|
int? subtitleStreamIndex,
|
||||||
string playSessionId)
|
string playSessionId)
|
||||||
{
|
{
|
||||||
var streamBuilder = new StreamBuilder();
|
var streamBuilder = new StreamBuilder(Logger);
|
||||||
|
|
||||||
var options = new VideoOptions
|
var options = new VideoOptions
|
||||||
{
|
{
|
||||||
@ -231,8 +240,7 @@ namespace MediaBrowser.Api.Playback
|
|||||||
Context = EncodingContext.Streaming,
|
Context = EncodingContext.Streaming,
|
||||||
DeviceId = auth.DeviceId,
|
DeviceId = auth.DeviceId,
|
||||||
ItemId = item.Id.ToString("N"),
|
ItemId = item.Id.ToString("N"),
|
||||||
Profile = profile,
|
Profile = profile
|
||||||
MaxBitrate = maxBitrate
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
|
||||||
@ -248,6 +256,7 @@ namespace MediaBrowser.Api.Playback
|
|||||||
|
|
||||||
// Dummy this up to fool StreamBuilder
|
// Dummy this up to fool StreamBuilder
|
||||||
mediaSource.SupportsDirectStream = true;
|
mediaSource.SupportsDirectStream = true;
|
||||||
|
options.MaxBitrate = maxBitrate;
|
||||||
|
|
||||||
// The MediaSource supports direct stream, now test to see if the client supports it
|
// The MediaSource supports direct stream, now test to see if the client supports it
|
||||||
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
|
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
|
||||||
@ -270,6 +279,8 @@ namespace MediaBrowser.Api.Playback
|
|||||||
|
|
||||||
if (mediaSource.SupportsDirectStream)
|
if (mediaSource.SupportsDirectStream)
|
||||||
{
|
{
|
||||||
|
options.MaxBitrate = GetMaxBitrate(maxBitrate);
|
||||||
|
|
||||||
// The MediaSource supports direct stream, now test to see if the client supports it
|
// The MediaSource supports direct stream, now test to see if the client supports it
|
||||||
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
|
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
|
||||||
streamBuilder.BuildAudioItem(options) :
|
streamBuilder.BuildAudioItem(options) :
|
||||||
@ -288,6 +299,8 @@ namespace MediaBrowser.Api.Playback
|
|||||||
|
|
||||||
if (mediaSource.SupportsTranscoding)
|
if (mediaSource.SupportsTranscoding)
|
||||||
{
|
{
|
||||||
|
options.MaxBitrate = GetMaxBitrate(maxBitrate);
|
||||||
|
|
||||||
// The MediaSource supports direct stream, now test to see if the client supports it
|
// The MediaSource supports direct stream, now test to see if the client supports it
|
||||||
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
|
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
|
||||||
streamBuilder.BuildAudioItem(options) :
|
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)
|
private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
|
||||||
{
|
{
|
||||||
var profiles = info.GetSubtitleProfiles(false, "-", accessToken);
|
var profiles = info.GetSubtitleProfiles(false, "-", accessToken);
|
||||||
|
@ -63,6 +63,13 @@ namespace MediaBrowser.Api.Playback.Progressive
|
|||||||
new ProgressiveFileCopier(_fileSystem, _job)
|
new ProgressiveFileCopier(_fileSystem, _job)
|
||||||
.StreamFile(Path, responseStream);
|
.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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.ErrorException("Error streaming media. The client has most likely disconnected or transcoding has failed.", 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.Dlna;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.LiveTv;
|
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using ServiceStack;
|
using ServiceStack;
|
||||||
|
@ -70,7 +70,7 @@ namespace MediaBrowser.Api.Playback
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UnpauseTranscoding()
|
public void UnpauseTranscoding()
|
||||||
{
|
{
|
||||||
if (_isPaused)
|
if (_isPaused)
|
||||||
{
|
{
|
||||||
|
@ -383,12 +383,12 @@ namespace MediaBrowser.Api.Session
|
|||||||
|
|
||||||
if (!user.Policy.EnableRemoteControlOfOtherUsers)
|
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)
|
if (!user.Policy.EnableSharedDeviceControl)
|
||||||
{
|
{
|
||||||
result = result.Where(i => !i.UserId.HasValue);
|
result = result.Where(i => i.UserId.HasValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
result = result.Where(i =>
|
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);
|
points += item1.Studios.Where(i => item2.Studios.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 3);
|
||||||
|
|
||||||
var item2PeopleNames = item2.People.Select(i => i.Name)
|
var item2PeopleNames = item2.People.Select(i => i.Name)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
|
.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
points += item1.People.Where(i => item2PeopleNames.ContainsKey(i.Name)).Sum(i =>
|
points += item1.People.Where(i => item2PeopleNames.ContainsKey(i.Name)).Sum(i =>
|
||||||
|
@ -136,11 +136,11 @@ namespace MediaBrowser.Api.Subtitles
|
|||||||
_providerManager = providerManager;
|
_providerManager = providerManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public object Get(GetSubtitlePlaylist request)
|
public async Task<object> Get(GetSubtitlePlaylist request)
|
||||||
{
|
{
|
||||||
var item = (Video)_libraryManager.GetItemById(new Guid(request.Id));
|
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();
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
|
@ -248,6 +248,9 @@ namespace MediaBrowser.Api.Sync
|
|||||||
result.Targets = _syncManager.GetSyncTargets(request.UserId)
|
result.Targets = _syncManager.GetSyncTargets(request.UserId)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
var auth = AuthorizationContext.GetAuthorizationInfo(Request);
|
||||||
|
var authenticatedUser = _userManager.GetUserById(auth.UserId);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.TargetId))
|
if (!string.IsNullOrWhiteSpace(request.TargetId))
|
||||||
{
|
{
|
||||||
result.Targets = result.Targets
|
result.Targets = result.Targets
|
||||||
@ -255,11 +258,11 @@ namespace MediaBrowser.Api.Sync
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
result.QualityOptions = _syncManager
|
result.QualityOptions = _syncManager
|
||||||
.GetQualityOptions(request.TargetId)
|
.GetQualityOptions(request.TargetId, authenticatedUser)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
result.ProfileOptions = _syncManager
|
result.ProfileOptions = _syncManager
|
||||||
.GetProfileOptions(request.TargetId)
|
.GetProfileOptions(request.TargetId, authenticatedUser)
|
||||||
.ToList();
|
.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)
|
var items = request.ItemIds.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
.Select(_libraryManager.GetItemById)
|
.Select(_libraryManager.GetItemById)
|
||||||
.Where(i => i != null);
|
.Where(i => i != null);
|
||||||
|
@ -132,7 +132,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||||||
.Where(i => !i.IsFolder)
|
.Where(i => !i.IsFolder)
|
||||||
.OfType<IHasAlbumArtist>()
|
.OfType<IHasAlbumArtist>()
|
||||||
.SelectMany(i => i.AlbumArtists)
|
.SelectMany(i => i.AlbumArtists)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.Select(name =>
|
.Select(name =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -152,7 +152,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||||||
.Where(i => !i.IsFolder)
|
.Where(i => !i.IsFolder)
|
||||||
.OfType<IHasArtist>()
|
.OfType<IHasArtist>()
|
||||||
.SelectMany(i => i.AllArtists)
|
.SelectMany(i => i.AllArtists)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.Select(name =>
|
.Select(name =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -142,7 +142,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||||||
}
|
}
|
||||||
|
|
||||||
IEnumerable<Tuple<TItemType, List<BaseItem>>> tuples;
|
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()));
|
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 true;
|
|
||||||
return options.Fields.Contains(ItemFields.ItemCounts);
|
return options.Fields.Contains(ItemFields.ItemCounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||||||
|
|
||||||
return itemsList
|
return itemsList
|
||||||
.SelectMany(i => i.Genres)
|
.SelectMany(i => i.Genres)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.Select(name => LibraryManager.GetGameGenre(name));
|
.Select(name => LibraryManager.GetGameGenre(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||||||
{
|
{
|
||||||
return items
|
return items
|
||||||
.SelectMany(i => i.Genres)
|
.SelectMany(i => i.Genres)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.Select(name =>
|
.Select(name =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -105,7 +105,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||||||
|
|
||||||
return itemsList
|
return itemsList
|
||||||
.SelectMany(i => i.Genres)
|
.SelectMany(i => i.Genres)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.Select(name => LibraryManager.GetMusicGenre(name));
|
.Select(name => LibraryManager.GetMusicGenre(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,7 +127,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||||||
|
|
||||||
return allPeople
|
return allPeople
|
||||||
.Select(i => i.Name)
|
.Select(i => i.Name)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
|
|
||||||
.Select(name =>
|
.Select(name =>
|
||||||
{
|
{
|
||||||
|
@ -114,6 +114,15 @@ namespace MediaBrowser.Api.UserLibrary
|
|||||||
|
|
||||||
[ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
[ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||||
public int? SubtitleStreamIndex { get; set; }
|
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>
|
/// <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")]
|
[ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
|
||||||
public int? VolumeLevel { get; set; }
|
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>
|
/// <summary>
|
||||||
@ -191,6 +209,12 @@ namespace MediaBrowser.Api.UserLibrary
|
|||||||
/// <value>The position ticks.</value>
|
/// <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")]
|
[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; }
|
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]
|
[Authenticated]
|
||||||
@ -260,7 +284,10 @@ namespace MediaBrowser.Api.UserLibrary
|
|||||||
QueueableMediaTypes = queueableMediaTypes.Split(',').ToList(),
|
QueueableMediaTypes = queueableMediaTypes.Split(',').ToList(),
|
||||||
MediaSourceId = request.MediaSourceId,
|
MediaSourceId = request.MediaSourceId,
|
||||||
AudioStreamIndex = request.AudioStreamIndex,
|
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,
|
MediaSourceId = request.MediaSourceId,
|
||||||
AudioStreamIndex = request.AudioStreamIndex,
|
AudioStreamIndex = request.AudioStreamIndex,
|
||||||
SubtitleStreamIndex = request.SubtitleStreamIndex,
|
SubtitleStreamIndex = request.SubtitleStreamIndex,
|
||||||
VolumeLevel = request.VolumeLevel
|
VolumeLevel = request.VolumeLevel,
|
||||||
|
PlayMethod = request.PlayMethod,
|
||||||
|
PlaySessionId = request.PlaySessionId,
|
||||||
|
LiveStreamId = request.LiveStreamId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Post(ReportPlaybackProgress request)
|
public void Post(ReportPlaybackProgress request)
|
||||||
{
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.PlaySessionId))
|
||||||
|
{
|
||||||
|
ApiEntryPoint.Instance.PingTranscodingJob(request.PlaySessionId);
|
||||||
|
}
|
||||||
|
|
||||||
request.SessionId = GetSession().Result.Id;
|
request.SessionId = GetSession().Result.Id;
|
||||||
|
|
||||||
var task = _sessionManager.OnPlaybackProgress(request);
|
var task = _sessionManager.OnPlaybackProgress(request);
|
||||||
@ -311,12 +346,19 @@ namespace MediaBrowser.Api.UserLibrary
|
|||||||
{
|
{
|
||||||
ItemId = request.Id,
|
ItemId = request.Id,
|
||||||
PositionTicks = request.PositionTicks,
|
PositionTicks = request.PositionTicks,
|
||||||
MediaSourceId = request.MediaSourceId
|
MediaSourceId = request.MediaSourceId,
|
||||||
|
PlaySessionId = request.PlaySessionId,
|
||||||
|
LiveStreamId = request.LiveStreamId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Post(ReportPlaybackStopped request)
|
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;
|
request.SessionId = GetSession().Result.Id;
|
||||||
|
|
||||||
var task = _sessionManager.OnPlaybackStopped(request);
|
var task = _sessionManager.OnPlaybackStopped(request);
|
||||||
|
@ -109,7 +109,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||||||
|
|
||||||
return itemsList
|
return itemsList
|
||||||
.SelectMany(i => i.Studios)
|
.SelectMany(i => i.Studios)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.Select(name => LibraryManager.GetStudio(name));
|
.Select(name => LibraryManager.GetStudio(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,12 +101,6 @@ namespace MediaBrowser.Common.Implementations
|
|||||||
/// <value>The failed assemblies.</value>
|
/// <value>The failed assemblies.</value>
|
||||||
public List<string> FailedAssemblies { get; protected set; }
|
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>
|
/// <summary>
|
||||||
/// Gets all concrete types.
|
/// Gets all concrete types.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -438,9 +432,10 @@ namespace MediaBrowser.Common.Implementations
|
|||||||
Logger.Info("Loading {0}", assembly.FullName);
|
Logger.Info("Loading {0}", assembly.FullName);
|
||||||
}
|
}
|
||||||
|
|
||||||
AllTypes = assemblies.SelectMany(GetTypes).ToArray();
|
AllConcreteTypes = assemblies
|
||||||
|
.SelectMany(GetTypes)
|
||||||
AllConcreteTypes = AllTypes.Where(t => t.IsClass && !t.IsAbstract && !t.IsInterface && !t.IsGenericType).ToArray();
|
.Where(t => t.IsClass && !t.IsAbstract && !t.IsInterface && !t.IsGenericType)
|
||||||
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -172,11 +172,11 @@ namespace MediaBrowser.Common.Implementations.Networking
|
|||||||
Uri uri;
|
Uri uri;
|
||||||
if (Uri.TryCreate(endpoint, UriKind.RelativeOrAbsolute, out uri))
|
if (Uri.TryCreate(endpoint, UriKind.RelativeOrAbsolute, out uri))
|
||||||
{
|
{
|
||||||
var host = uri.DnsSafeHost;
|
|
||||||
Logger.Debug("Resolving host {0}", host);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var host = uri.DnsSafeHost;
|
||||||
|
Logger.Debug("Resolving host {0}", host);
|
||||||
|
|
||||||
address = GetIpAddresses(host).FirstOrDefault();
|
address = GetIpAddresses(host).FirstOrDefault();
|
||||||
|
|
||||||
if (address != null)
|
if (address != null)
|
||||||
@ -186,9 +186,13 @@ namespace MediaBrowser.Common.Implementations.Networking
|
|||||||
return IsInLocalNetworkInternal(address.ToString(), false);
|
return IsInLocalNetworkInternal(address.ToString(), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
// Can happen with reverse proxy or IIS url rewriting
|
||||||
|
}
|
||||||
catch (Exception ex)
|
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)
|
if (_lastExecutionResult == null)
|
||||||
{
|
{
|
||||||
|
var path = GetHistoryFilePath();
|
||||||
|
|
||||||
lock (_lastExecutionResultSyncLock)
|
lock (_lastExecutionResultSyncLock)
|
||||||
{
|
{
|
||||||
if (_lastExecutionResult == null)
|
if (_lastExecutionResult == null)
|
||||||
{
|
{
|
||||||
var path = GetHistoryFilePath();
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return JsonSerializer.DeserializeFromFile<TaskResult>(path);
|
return JsonSerializer.DeserializeFromFile<TaskResult>(path);
|
||||||
@ -152,6 +152,14 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
|
|||||||
private set
|
private set
|
||||||
{
|
{
|
||||||
_lastExecutionResult = value;
|
_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;
|
result.LongErrorMessage = ex.StackTrace;
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = GetHistoryFilePath();
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
|
||||||
|
|
||||||
JsonSerializer.SerializeToFile(result, path);
|
|
||||||
|
|
||||||
LastExecutionResult = result;
|
LastExecutionResult = result;
|
||||||
|
|
||||||
((TaskManager)TaskManager).OnTaskCompleted(this, result);
|
((TaskManager)TaskManager).OnTaskCompleted(this, result);
|
||||||
|
@ -5,7 +5,6 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Model.Users;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Channels
|
namespace MediaBrowser.Controller.Channels
|
||||||
{
|
{
|
||||||
@ -15,19 +14,9 @@ namespace MediaBrowser.Controller.Channels
|
|||||||
|
|
||||||
public override bool IsVisible(User user)
|
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;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.IsVisible(user);
|
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.Channels;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
@ -100,5 +101,10 @@ namespace MediaBrowser.Controller.Channels
|
|||||||
{
|
{
|
||||||
return false;
|
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;
|
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;
|
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>
|
/// </summary>
|
||||||
public interface IImageProcessor
|
public interface IImageProcessor
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the supported input formats.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The supported input formats.</value>
|
||||||
|
string[] SupportedInputFormats { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the image enhancers.
|
/// Gets the image enhancers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -93,5 +99,11 @@ namespace MediaBrowser.Controller.Drawing
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>ImageOutputFormat[].</returns>
|
/// <returns>ImageOutputFormat[].</returns>
|
||||||
ImageFormat[] GetSupportedImageOutputFormats();
|
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>
|
/// <returns>Task{BaseItemDto}.</returns>
|
||||||
BaseItemDto GetBaseItemDto(BaseItem item, List<ItemFields> fields, User user = null, BaseItem owner = null);
|
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>
|
/// <summary>
|
||||||
/// Gets the base item dto.
|
/// Gets the base item dto.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using System;
|
using MediaBrowser.Controller.Library;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Entities.Audio
|
namespace MediaBrowser.Controller.Entities.Audio
|
||||||
{
|
{
|
||||||
@ -20,11 +19,11 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||||||
{
|
{
|
||||||
public static bool HasArtist(this IHasArtist hasArtist, string artist)
|
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)
|
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.Common.IO;
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
using MediaBrowser.Controller.Collections;
|
using MediaBrowser.Controller.Collections;
|
||||||
@ -44,7 +45,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The supported image extensions
|
/// The supported image extensions
|
||||||
/// </summary>
|
/// </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();
|
public static readonly List<string> SupportedImageExtensionsList = SupportedImageExtensions.ToList();
|
||||||
|
|
||||||
@ -1143,6 +1144,11 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
}
|
}
|
||||||
|
|
||||||
public virtual bool IsVisibleStandalone(User user)
|
public virtual bool IsVisibleStandalone(User user)
|
||||||
|
{
|
||||||
|
return IsVisibleStandaloneInternal(user, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
|
||||||
{
|
{
|
||||||
if (!IsVisible(user))
|
if (!IsVisible(user))
|
||||||
{
|
{
|
||||||
@ -1154,7 +1160,23 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
@ -1219,18 +1241,6 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
private BaseItem FindLinkedChild(LinkedChild info)
|
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))
|
if (!string.IsNullOrEmpty(info.Path))
|
||||||
{
|
{
|
||||||
var itemByPath = LibraryManager.RootFolder.FindByPath(info.Path);
|
var itemByPath = LibraryManager.RootFolder.FindByPath(info.Path);
|
||||||
@ -1243,23 +1253,6 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
return itemByPath;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1540,7 +1533,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove it from the item
|
// Remove it from the item
|
||||||
ImageInfos.Remove(info);
|
RemoveImage(info);
|
||||||
|
|
||||||
// Delete the source file
|
// Delete the source file
|
||||||
var currentFile = new FileInfo(info.Path);
|
var currentFile = new FileInfo(info.Path);
|
||||||
@ -1559,6 +1552,11 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
return UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
|
return UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RemoveImage(ItemImageInfo image)
|
||||||
|
{
|
||||||
|
ImageInfos.Remove(image);
|
||||||
|
}
|
||||||
|
|
||||||
public virtual Task UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken)
|
public virtual Task UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return LibraryManager.UpdateItem(this, updateReason, cancellationToken);
|
return LibraryManager.UpdateItem(this, updateReason, cancellationToken);
|
||||||
@ -1651,7 +1649,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
public bool AddImages(ImageType imageType, IEnumerable<FileInfo> images)
|
public bool AddImages(ImageType imageType, IEnumerable<FileInfo> images)
|
||||||
{
|
{
|
||||||
return AddImages(imageType, images.Cast<FileSystemInfo>());
|
return AddImages(imageType, images.Cast<FileSystemInfo>().ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -1661,7 +1659,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
/// <param name="images">The images.</param>
|
/// <param name="images">The images.</param>
|
||||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||||
/// <exception cref="System.ArgumentException">Cannot call AddImages with chapter images</exception>
|
/// <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)
|
if (imageType == ImageType.Chapter)
|
||||||
{
|
{
|
||||||
@ -1672,6 +1670,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var newImageList = new List<FileSystemInfo>();
|
var newImageList = new List<FileSystemInfo>();
|
||||||
|
var imageAdded = false;
|
||||||
|
|
||||||
foreach (var newImage in images)
|
foreach (var newImage in images)
|
||||||
{
|
{
|
||||||
@ -1686,14 +1685,26 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
if (existing == null)
|
if (existing == null)
|
||||||
{
|
{
|
||||||
newImageList.Add(newImage);
|
newImageList.Add(newImage);
|
||||||
|
imageAdded = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
existing.DateModified = FileSystem.GetLastWriteTimeUtc(newImage);
|
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)));
|
ImageInfos.AddRange(newImageList.Select(i => GetImageInfo(i, imageType)));
|
||||||
|
|
||||||
return newImageList.Count > 0;
|
return newImageList.Count > 0;
|
||||||
@ -1882,5 +1893,18 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
return video.RefreshMetadata(newOptions, cancellationToken);
|
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 (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) ||
|
return false;
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1004,8 +991,9 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
}
|
}
|
||||||
|
|
||||||
var locations = user.RootFolder
|
var locations = user.RootFolder
|
||||||
.GetChildren(user, true)
|
.Children
|
||||||
.OfType<CollectionFolder>()
|
.OfType<CollectionFolder>()
|
||||||
|
.Where(i => i.IsVisible(user))
|
||||||
.SelectMany(i => i.PhysicalLocations)
|
.SelectMany(i => i.PhysicalLocations)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
@ -141,7 +141,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
/// <param name="imageType">Type of the image.</param>
|
/// <param name="imageType">Type of the image.</param>
|
||||||
/// <param name="images">The images.</param>
|
/// <param name="images">The images.</param>
|
||||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
/// <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>
|
/// <summary>
|
||||||
/// Determines whether [is save local metadata enabled].
|
/// Determines whether [is save local metadata enabled].
|
||||||
@ -190,6 +190,12 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns><c>true</c> if [is internet metadata enabled]; otherwise, <c>false</c>.</returns>
|
/// <returns><c>true</c> if [is internet metadata enabled]; otherwise, <c>false</c>.</returns>
|
||||||
bool IsInternetMetadataEnabled();
|
bool IsInternetMetadataEnabled();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the image.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="image">The image.</param>
|
||||||
|
void RemoveImage(ItemImageInfo image);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class HasImagesExtensions
|
public static class HasImagesExtensions
|
||||||
|
@ -9,9 +9,6 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
public string Path { get; set; }
|
public string Path { get; set; }
|
||||||
public LinkedChildType Type { get; set; }
|
public LinkedChildType Type { get; set; }
|
||||||
|
|
||||||
public string ItemName { get; set; }
|
|
||||||
public string ItemType { get; set; }
|
|
||||||
|
|
||||||
[IgnoreDataMember]
|
[IgnoreDataMember]
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
@ -175,17 +175,17 @@ namespace MediaBrowser.Controller.Entities.Movies
|
|||||||
|
|
||||||
public override bool IsVisible(User user)
|
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))
|
if (base.IsVisible(user))
|
||||||
{
|
{
|
||||||
var userId = user.Id.ToString("N");
|
return GetChildren(user, true).Any();
|
||||||
|
|
||||||
// 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 false;
|
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.Linq;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using MediaBrowser.Model.Users;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Entities
|
namespace MediaBrowser.Controller.Entities
|
||||||
{
|
{
|
||||||
public class PhotoAlbum : Folder
|
public class PhotoAlbum : Folder, IMetadataContainer
|
||||||
{
|
{
|
||||||
public override bool SupportsLocalMetadata
|
public override bool SupportsLocalMetadata
|
||||||
{
|
{
|
||||||
@ -28,5 +32,31 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
{
|
{
|
||||||
return config.BlockUnratedItems.Contains(UnratedItem.Other);
|
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;
|
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)
|
switch (viewType)
|
||||||
{
|
{
|
||||||
case CollectionType.Channels:
|
case CollectionType.Channels:
|
||||||
@ -107,9 +117,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
case CollectionType.LiveTv:
|
case CollectionType.LiveTv:
|
||||||
{
|
{
|
||||||
var result = await GetLiveTvFolders(user).ConfigureAwait(false);
|
return await GetLiveTvView(queryParent, user, query).ConfigureAwait(false);
|
||||||
|
|
||||||
return GetResult(result, queryParent, query);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case CollectionType.Books:
|
case CollectionType.Books:
|
||||||
@ -205,6 +213,9 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
case SpecialFolder.MusicLatest:
|
case SpecialFolder.MusicLatest:
|
||||||
return GetMusicLatest(queryParent, user, query);
|
return GetMusicLatest(queryParent, user, query);
|
||||||
|
|
||||||
|
case SpecialFolder.MusicPlaylists:
|
||||||
|
return await GetMusicPlaylists(queryParent, user, query).ConfigureAwait(false);
|
||||||
|
|
||||||
case SpecialFolder.MusicAlbums:
|
case SpecialFolder.MusicAlbums:
|
||||||
return GetMusicAlbums(queryParent, user, query);
|
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()
|
private int GetSpecialItemsLimit()
|
||||||
{
|
{
|
||||||
return 50;
|
return 50;
|
||||||
@ -257,12 +278,13 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
var list = new List<BaseItem>();
|
var list = new List<BaseItem>();
|
||||||
|
|
||||||
list.Add(await GetUserView(SpecialFolder.MusicLatest, user, "0", parent).ConfigureAwait(false));
|
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.MusicPlaylists, user, "1", parent).ConfigureAwait(false));
|
||||||
list.Add(await GetUserView(SpecialFolder.MusicAlbumArtists, user, "2", parent).ConfigureAwait(false));
|
list.Add(await GetUserView(SpecialFolder.MusicAlbums, user, "2", parent).ConfigureAwait(false));
|
||||||
list.Add(await GetUserView(SpecialFolder.MusicArtists, user, "3", parent).ConfigureAwait(false));
|
list.Add(await GetUserView(SpecialFolder.MusicAlbumArtists, user, "3", parent).ConfigureAwait(false));
|
||||||
list.Add(await GetUserView(SpecialFolder.MusicSongs, user, "4", parent).ConfigureAwait(false));
|
//list.Add(await GetUserView(SpecialFolder.MusicArtists, user, "4", parent).ConfigureAwait(false));
|
||||||
list.Add(await GetUserView(SpecialFolder.MusicGenres, user, "5", parent).ConfigureAwait(false));
|
list.Add(await GetUserView(SpecialFolder.MusicSongs, user, "5", parent).ConfigureAwait(false));
|
||||||
list.Add(await GetUserView(SpecialFolder.MusicFavorites, user, "6", 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);
|
return GetResult(list, parent, query);
|
||||||
}
|
}
|
||||||
@ -283,7 +305,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Music, CollectionType.MusicVideos })
|
var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Music, CollectionType.MusicVideos })
|
||||||
.Where(i => !i.IsFolder)
|
.Where(i => !i.IsFolder)
|
||||||
.SelectMany(i => i.Genres)
|
.SelectMany(i => i.Genres)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -313,7 +335,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
.Where(i => i.Genres.Contains(displayParent.Name, StringComparer.OrdinalIgnoreCase))
|
.Where(i => i.Genres.Contains(displayParent.Name, StringComparer.OrdinalIgnoreCase))
|
||||||
.OfType<IHasAlbumArtist>()
|
.OfType<IHasAlbumArtist>()
|
||||||
.SelectMany(i => i.AlbumArtists)
|
.SelectMany(i => i.AlbumArtists)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -337,7 +359,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
.Where(i => !i.IsFolder)
|
.Where(i => !i.IsFolder)
|
||||||
.OfType<IHasAlbumArtist>()
|
.OfType<IHasAlbumArtist>()
|
||||||
.SelectMany(i => i.AlbumArtists)
|
.SelectMany(i => i.AlbumArtists)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -361,7 +383,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
.Where(i => !i.IsFolder)
|
.Where(i => !i.IsFolder)
|
||||||
.OfType<IHasArtist>()
|
.OfType<IHasArtist>()
|
||||||
.SelectMany(i => i.Artists)
|
.SelectMany(i => i.Artists)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -385,7 +407,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
.Where(i => !i.IsFolder)
|
.Where(i => !i.IsFolder)
|
||||||
.OfType<IHasAlbumArtist>()
|
.OfType<IHasAlbumArtist>()
|
||||||
.SelectMany(i => i.AlbumArtists)
|
.SelectMany(i => i.AlbumArtists)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -403,6 +425,14 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
return GetResult(artists, parent, query);
|
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)
|
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));
|
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 })
|
var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Movies, CollectionType.BoxSets, string.Empty })
|
||||||
.Where(i => i is Movie)
|
.Where(i => i is Movie)
|
||||||
.SelectMany(i => i.Genres)
|
.SelectMany(i => i.Genres)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -724,7 +754,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.TvShows, string.Empty })
|
var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.TvShows, string.Empty })
|
||||||
.OfType<Series>()
|
.OfType<Series>()
|
||||||
.SelectMany(i => i.Genres)
|
.SelectMany(i => i.Genres)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -776,7 +806,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Games })
|
var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Games })
|
||||||
.OfType<Game>()
|
.OfType<Game>()
|
||||||
.SelectMany(i => i.Genres)
|
.SelectMany(i => i.Genres)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.DistinctNames()
|
||||||
.Select(i =>
|
.Select(i =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -1749,17 +1779,26 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
return parent.GetRecursiveChildren(user, filter);
|
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 list = new List<BaseItem>();
|
||||||
|
|
||||||
var parent = user.RootFolder;
|
|
||||||
|
|
||||||
//list.Add(await GetUserSubView(SpecialFolder.LiveTvNowPlaying, user, "0", parent).ConfigureAwait(false));
|
//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.LiveTvChannels, user, string.Empty, user.RootFolder).ConfigureAwait(false));
|
||||||
list.Add(await GetUserView(SpecialFolder.LiveTvRecordingGroups, user, string.Empty, parent).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)
|
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;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace MediaBrowser.Server.Implementations.HttpServer
|
namespace MediaBrowser.Controller.IO
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class for streaming data with throttling support.
|
/// Class for streaming data with throttling support.
|
||||||
@ -15,8 +15,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const long Infinite = 0;
|
public const long Infinite = 0;
|
||||||
|
|
||||||
public Func<long, long, long> ThrottleCallback { get; set; }
|
|
||||||
|
|
||||||
#region Private members
|
#region Private members
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The base stream.
|
/// The base stream.
|
||||||
@ -293,16 +291,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ThrottleCallback != null)
|
|
||||||
{
|
|
||||||
var val = ThrottleCallback(_maximumBytesPerSecond, _bytesWritten);
|
|
||||||
|
|
||||||
if (val == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -43,18 +43,10 @@ namespace MediaBrowser.Controller.Library
|
|||||||
/// <param name="id">The identifier.</param>
|
/// <param name="id">The identifier.</param>
|
||||||
/// <param name="userId">The user identifier.</param>
|
/// <param name="userId">The user identifier.</param>
|
||||||
/// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</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>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>IEnumerable<MediaSourceInfo>.</returns>
|
/// <returns>IEnumerable<MediaSourceInfo>.</returns>
|
||||||
Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, CancellationToken cancellationToken);
|
Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, string[] supportedLiveMediaTypes, 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);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the static media sources.
|
/// 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="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
|
||||||
/// <param name="user">The user.</param>
|
/// <param name="user">The user.</param>
|
||||||
/// <returns>IEnumerable<MediaSourceInfo>.</returns>
|
/// <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>
|
/// <summary>
|
||||||
/// Gets the static media source.
|
/// Gets the static media source.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -80,7 +64,7 @@ namespace MediaBrowser.Controller.Library
|
|||||||
/// <param name="mediaSourceId">The media source identifier.</param>
|
/// <param name="mediaSourceId">The media source identifier.</param>
|
||||||
/// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
|
/// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
|
||||||
/// <returns>MediaSourceInfo.</returns>
|
/// <returns>MediaSourceInfo.</returns>
|
||||||
MediaSourceInfo GetStaticMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution);
|
Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Opens the media source.
|
/// 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.Channels;
|
||||||
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.LiveTv;
|
using MediaBrowser.Model.LiveTv;
|
||||||
@ -74,10 +75,11 @@ namespace MediaBrowser.Controller.LiveTv
|
|||||||
/// Gets the recording.
|
/// Gets the recording.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The identifier.</param>
|
/// <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="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <param name="user">The user.</param>
|
||||||
/// <returns>Task{RecordingInfoDto}.</returns>
|
/// <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>
|
/// <summary>
|
||||||
/// Gets the channel.
|
/// Gets the channel.
|
||||||
@ -103,14 +105,15 @@ namespace MediaBrowser.Controller.LiveTv
|
|||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>Task{TimerInfoDto}.</returns>
|
/// <returns>Task{TimerInfoDto}.</returns>
|
||||||
Task<SeriesTimerInfoDto> GetSeriesTimer(string id, CancellationToken cancellationToken);
|
Task<SeriesTimerInfoDto> GetSeriesTimer(string id, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the recordings.
|
/// Gets the recordings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">The query.</param>
|
/// <param name="query">The query.</param>
|
||||||
|
/// <param name="options">The options.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>QueryResult{RecordingInfoDto}.</returns>
|
/// <returns>QueryResult{RecordingInfoDto}.</returns>
|
||||||
Task<QueryResult<RecordingInfoDto>> GetRecordings(RecordingQuery query, CancellationToken cancellationToken);
|
Task<QueryResult<RecordingInfoDto>> GetRecordings(RecordingQuery query, DtoOptions options, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the timers.
|
/// 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.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Controller.Channels;
|
|
||||||
using MediaBrowser.Controller.Drawing;
|
|
||||||
using MediaBrowser.Model.Dto;
|
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.LiveTv
|
namespace MediaBrowser.Controller.LiveTv
|
||||||
{
|
{
|
||||||
|
@ -52,6 +52,10 @@
|
|||||||
<SpecificVersion>False</SpecificVersion>
|
<SpecificVersion>False</SpecificVersion>
|
||||||
<HintPath>..\packages\morelinq.1.1.0\lib\net35\MoreLinq.dll</HintPath>
|
<HintPath>..\packages\morelinq.1.1.0\lib\net35\MoreLinq.dll</HintPath>
|
||||||
</Reference>
|
</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" />
|
||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
<Reference Include="System.Data" />
|
<Reference Include="System.Data" />
|
||||||
@ -115,6 +119,7 @@
|
|||||||
<Compile Include="Dlna\IMediaReceiverRegistrar.cs" />
|
<Compile Include="Dlna\IMediaReceiverRegistrar.cs" />
|
||||||
<Compile Include="Dlna\IUpnpService.cs" />
|
<Compile Include="Dlna\IUpnpService.cs" />
|
||||||
<Compile Include="Drawing\IImageProcessor.cs" />
|
<Compile Include="Drawing\IImageProcessor.cs" />
|
||||||
|
<Compile Include="Drawing\ImageCollageOptions.cs" />
|
||||||
<Compile Include="Drawing\ImageProcessingOptions.cs" />
|
<Compile Include="Drawing\ImageProcessingOptions.cs" />
|
||||||
<Compile Include="Drawing\ImageProcessorExtensions.cs" />
|
<Compile Include="Drawing\ImageProcessorExtensions.cs" />
|
||||||
<Compile Include="Drawing\ImageStream.cs" />
|
<Compile Include="Drawing\ImageStream.cs" />
|
||||||
@ -171,6 +176,7 @@
|
|||||||
<Compile Include="Entities\UserView.cs" />
|
<Compile Include="Entities\UserView.cs" />
|
||||||
<Compile Include="Entities\UserViewBuilder.cs" />
|
<Compile Include="Entities\UserViewBuilder.cs" />
|
||||||
<Compile Include="FileOrganization\IFileOrganizationService.cs" />
|
<Compile Include="FileOrganization\IFileOrganizationService.cs" />
|
||||||
|
<Compile Include="IO\ThrottledStream.cs" />
|
||||||
<Compile Include="Library\DeleteOptions.cs" />
|
<Compile Include="Library\DeleteOptions.cs" />
|
||||||
<Compile Include="Library\ILibraryPostScanTask.cs" />
|
<Compile Include="Library\ILibraryPostScanTask.cs" />
|
||||||
<Compile Include="Library\IMediaSourceManager.cs" />
|
<Compile Include="Library\IMediaSourceManager.cs" />
|
||||||
@ -184,6 +190,7 @@
|
|||||||
<Compile Include="Library\IUserViewManager.cs" />
|
<Compile Include="Library\IUserViewManager.cs" />
|
||||||
<Compile Include="Library\LibraryManagerExtensions.cs" />
|
<Compile Include="Library\LibraryManagerExtensions.cs" />
|
||||||
<Compile Include="Library\MetadataConfigurationStore.cs" />
|
<Compile Include="Library\MetadataConfigurationStore.cs" />
|
||||||
|
<Compile Include="Library\NameExtensions.cs" />
|
||||||
<Compile Include="Library\PlaybackStopEventArgs.cs" />
|
<Compile Include="Library\PlaybackStopEventArgs.cs" />
|
||||||
<Compile Include="Library\UserDataSaveEventArgs.cs" />
|
<Compile Include="Library\UserDataSaveEventArgs.cs" />
|
||||||
<Compile Include="LiveTv\ILiveTvItem.cs" />
|
<Compile Include="LiveTv\ILiveTvItem.cs" />
|
||||||
@ -211,8 +218,8 @@
|
|||||||
<Compile Include="MediaEncoding\IEncodingManager.cs" />
|
<Compile Include="MediaEncoding\IEncodingManager.cs" />
|
||||||
<Compile Include="MediaEncoding\ImageEncodingOptions.cs" />
|
<Compile Include="MediaEncoding\ImageEncodingOptions.cs" />
|
||||||
<Compile Include="MediaEncoding\IMediaEncoder.cs" />
|
<Compile Include="MediaEncoding\IMediaEncoder.cs" />
|
||||||
<Compile Include="MediaEncoding\InternalMediaInfoResult.cs" />
|
|
||||||
<Compile Include="MediaEncoding\ISubtitleEncoder.cs" />
|
<Compile Include="MediaEncoding\ISubtitleEncoder.cs" />
|
||||||
|
<Compile Include="MediaEncoding\MediaInfoRequest.cs" />
|
||||||
<Compile Include="MediaEncoding\MediaStreamSelector.cs" />
|
<Compile Include="MediaEncoding\MediaStreamSelector.cs" />
|
||||||
<Compile Include="Net\AuthenticatedAttribute.cs" />
|
<Compile Include="Net\AuthenticatedAttribute.cs" />
|
||||||
<Compile Include="Net\AuthorizationInfo.cs" />
|
<Compile Include="Net\AuthorizationInfo.cs" />
|
||||||
@ -394,6 +401,7 @@
|
|||||||
<Compile Include="Subtitles\SubtitleResponse.cs" />
|
<Compile Include="Subtitles\SubtitleResponse.cs" />
|
||||||
<Compile Include="Subtitles\SubtitleSearchRequest.cs" />
|
<Compile Include="Subtitles\SubtitleSearchRequest.cs" />
|
||||||
<Compile Include="Sync\IHasDynamicAccess.cs" />
|
<Compile Include="Sync\IHasDynamicAccess.cs" />
|
||||||
|
<Compile Include="Sync\IRemoteSyncProvider.cs" />
|
||||||
<Compile Include="Sync\IServerSyncProvider.cs" />
|
<Compile Include="Sync\IServerSyncProvider.cs" />
|
||||||
<Compile Include="Sync\ISyncDataProvider.cs" />
|
<Compile Include="Sync\ISyncDataProvider.cs" />
|
||||||
<Compile Include="Sync\ISyncManager.cs" />
|
<Compile Include="Sync\ISyncManager.cs" />
|
||||||
|
@ -41,6 +41,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
public int? SubtitleStreamIndex { get; set; }
|
public int? SubtitleStreamIndex { get; set; }
|
||||||
public int? MaxRefFrames { get; set; }
|
public int? MaxRefFrames { get; set; }
|
||||||
public int? MaxVideoBitDepth { get; set; }
|
public int? MaxVideoBitDepth { get; set; }
|
||||||
|
public int? CpuCoreLimit { get; set; }
|
||||||
|
public bool ReadInputAtNativeFramerate { get; set; }
|
||||||
public SubtitleDeliveryMethod SubtitleMethod { get; set; }
|
public SubtitleDeliveryMethod SubtitleMethod { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -63,16 +63,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
string filenamePrefix,
|
string filenamePrefix,
|
||||||
int? maxWidth,
|
int? maxWidth,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the media info.
|
/// Gets the media info.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="inputFiles">The input files.</param>
|
/// <param name="request">The request.</param>
|
||||||
/// <param name="protocol">The protocol.</param>
|
|
||||||
/// <param name="isAudio">if set to <c>true</c> [is audio].</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
Task<InternalMediaInfoResult> GetMediaInfo(string[] inputFiles, MediaProtocol protocol, bool isAudio, CancellationToken cancellationToken);
|
Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the probe size argument.
|
/// 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 MediaBrowser.Model.MediaInfo;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
@ -46,291 +44,5 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
.Where(f => !string.IsNullOrEmpty(f))
|
.Where(f => !string.IsNullOrEmpty(f))
|
||||||
.ToList();
|
.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)
|
switch (reader.Name)
|
||||||
{
|
{
|
||||||
case "Name":
|
|
||||||
{
|
|
||||||
linkedItem.ItemName = reader.ReadElementContentAsString();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "Path":
|
case "Path":
|
||||||
{
|
{
|
||||||
linkedItem.Path = reader.ReadElementContentAsString();
|
linkedItem.Path = reader.ReadElementContentAsString();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case "Type":
|
|
||||||
{
|
|
||||||
linkedItem.ItemType = reader.ReadElementContentAsString();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
reader.Skip();
|
reader.Skip();
|
||||||
break;
|
break;
|
||||||
@ -1435,7 +1423,7 @@ namespace MediaBrowser.Controller.Providers
|
|||||||
return linkedItem;
|
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.Drawing;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -78,6 +78,19 @@ namespace MediaBrowser.Controller.Providers
|
|||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
Task SaveImage(IHasImages item, Stream source, string mimeType, ImageType type, int? imageIndex, string internalCacheKey, CancellationToken cancellationToken);
|
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>
|
/// <summary>
|
||||||
/// Adds the metadata providers.
|
/// Adds the metadata providers.
|
||||||
|
@ -9,10 +9,10 @@ namespace MediaBrowser.Controller.Sync
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the synced file information.
|
/// Gets the synced file information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="remotePath">The remote path.</param>
|
/// <param name="id">The identifier.</param>
|
||||||
/// <param name="target">The target.</param>
|
/// <param name="target">The target.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>Task<SyncedFileInfo>.</returns>
|
/// <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;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -13,46 +14,39 @@ namespace MediaBrowser.Controller.Sync
|
|||||||
/// Transfers the file.
|
/// Transfers the file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="stream">The stream.</param>
|
/// <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="target">The target.</param>
|
||||||
/// <param name="progress">The progress.</param>
|
/// <param name="progress">The progress.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <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>
|
/// <summary>
|
||||||
/// Deletes the file.
|
/// Deletes the file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="id">The identifier.</param>
|
||||||
/// <param name="target">The target.</param>
|
/// <param name="target">The target.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
Task DeleteFile(string path, SyncTarget target, CancellationToken cancellationToken);
|
Task DeleteFile(string id, SyncTarget target, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the file.
|
/// Gets the file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="id">The identifier.</param>
|
||||||
/// <param name="target">The target.</param>
|
/// <param name="target">The target.</param>
|
||||||
/// <param name="progress">The progress.</param>
|
/// <param name="progress">The progress.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>Task<Stream>.</returns>
|
/// <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>
|
/// <summary>
|
||||||
/// Gets the full path.
|
/// Gets the files.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="query">The query.</param>
|
||||||
/// <param name="target">The target.</param>
|
/// <param name="target">The target.</param>
|
||||||
/// <returns>System.String.</returns>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
string GetFullPath(IEnumerable<string> path, SyncTarget target);
|
/// <returns>Task<QueryResult<FileMetadata>>.</returns>
|
||||||
|
Task<QueryResult<FileMetadata>> GetFiles(FileQuery query, SyncTarget target, CancellationToken cancellationToken);
|
||||||
/// <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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,20 +7,12 @@ namespace MediaBrowser.Controller.Sync
|
|||||||
public interface ISyncDataProvider
|
public interface ISyncDataProvider
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the server item ids.
|
/// Gets the local items.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="target">The target.</param>
|
/// <param name="target">The target.</param>
|
||||||
/// <param name="serverId">The server identifier.</param>
|
/// <param name="serverId">The server identifier.</param>
|
||||||
/// <returns>Task<List<System.String>>.</returns>
|
/// <returns>Task<List<LocalItem>>.</returns>
|
||||||
Task<List<string>> GetServerItemIds(SyncTarget target, string serverId);
|
Task<List<LocalItem>> GetLocalItems(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);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds the or update.
|
/// Adds the or update.
|
||||||
|
@ -174,6 +174,13 @@ namespace MediaBrowser.Controller.Sync
|
|||||||
/// <param name="targetId">The target identifier.</param>
|
/// <param name="targetId">The target identifier.</param>
|
||||||
/// <returns>IEnumerable<SyncQualityOption>.</returns>
|
/// <returns>IEnumerable<SyncQualityOption>.</returns>
|
||||||
IEnumerable<SyncQualityOption> GetQualityOptions(string targetId);
|
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>
|
/// <summary>
|
||||||
/// Gets the profile options.
|
/// Gets the profile options.
|
||||||
@ -181,5 +188,12 @@ namespace MediaBrowser.Controller.Sync
|
|||||||
/// <param name="targetId">The target identifier.</param>
|
/// <param name="targetId">The target identifier.</param>
|
||||||
/// <returns>IEnumerable<SyncQualityOption>.</returns>
|
/// <returns>IEnumerable<SyncQualityOption>.</returns>
|
||||||
IEnumerable<SyncProfileOption> GetProfileOptions(string targetId);
|
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>
|
/// </summary>
|
||||||
/// <value>The required HTTP headers.</value>
|
/// <value>The required HTTP headers.</value>
|
||||||
public Dictionary<string, string> RequiredHttpHeaders { get; set; }
|
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()
|
public SyncedFileInfo()
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<packages>
|
<packages>
|
||||||
<package id="morelinq" version="1.1.0" targetFramework="net45" />
|
<package id="morelinq" version="1.1.0" targetFramework="net45" />
|
||||||
|
<package id="Patterns.IO" version="1.0.0.3" targetFramework="net45" />
|
||||||
</packages>
|
</packages>
|
@ -223,7 +223,7 @@ namespace MediaBrowser.Dlna.ContentDirectory
|
|||||||
if (string.Equals(flag, "BrowseMetadata"))
|
if (string.Equals(flag, "BrowseMetadata"))
|
||||||
{
|
{
|
||||||
totalCount = 1;
|
totalCount = 1;
|
||||||
|
|
||||||
if (item.IsFolder || serverItem.StubType.HasValue)
|
if (item.IsFolder || serverItem.StubType.HasValue)
|
||||||
{
|
{
|
||||||
var childrenResult = (await GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount).ConfigureAwait(false));
|
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;
|
var folder = (Folder)item;
|
||||||
|
|
||||||
@ -389,7 +389,7 @@ namespace MediaBrowser.Dlna.ContentDirectory
|
|||||||
isFolder = true;
|
isFolder = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await folder.GetItems(new InternalItemsQuery
|
return folder.GetItems(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
Limit = limit,
|
Limit = limit,
|
||||||
StartIndex = startIndex,
|
StartIndex = startIndex,
|
||||||
@ -401,7 +401,7 @@ namespace MediaBrowser.Dlna.ContentDirectory
|
|||||||
IsFolder = isFolder,
|
IsFolder = isFolder,
|
||||||
MediaTypes = mediaTypes.ToArray()
|
MediaTypes = mediaTypes.ToArray()
|
||||||
|
|
||||||
}).ConfigureAwait(false);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<QueryResult<ServerItem>> GetUserItems(BaseItem item, StubType? stubType, User user, SortCriteria sort, int? startIndex, int? limit)
|
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.Dlna;
|
||||||
using MediaBrowser.Model.Drawing;
|
using MediaBrowser.Model.Drawing;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
|
using MediaBrowser.Model.Logging;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@ -124,9 +125,9 @@ namespace MediaBrowser.Dlna.Didl
|
|||||||
{
|
{
|
||||||
if (streamInfo == null)
|
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),
|
ItemId = GetClientId(video),
|
||||||
MediaSources = sources,
|
MediaSources = sources,
|
||||||
@ -351,9 +352,9 @@ namespace MediaBrowser.Dlna.Didl
|
|||||||
|
|
||||||
if (streamInfo == null)
|
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),
|
ItemId = GetClientId(audio),
|
||||||
MediaSources = sources,
|
MediaSources = sources,
|
||||||
|
@ -470,7 +470,7 @@ namespace MediaBrowser.Dlna.PlayTo
|
|||||||
|
|
||||||
var hasMediaSources = item as IHasMediaSources;
|
var hasMediaSources = item as IHasMediaSources;
|
||||||
var mediaSources = hasMediaSources != null
|
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>();
|
: new List<MediaSourceInfo>();
|
||||||
|
|
||||||
var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
|
var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
|
||||||
@ -542,7 +542,7 @@ namespace MediaBrowser.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
return new PlaylistItem
|
return new PlaylistItem
|
||||||
{
|
{
|
||||||
StreamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions
|
StreamInfo = new StreamBuilder(_logger).BuildVideoItem(new VideoOptions
|
||||||
{
|
{
|
||||||
ItemId = item.Id.ToString("N"),
|
ItemId = item.Id.ToString("N"),
|
||||||
MediaSources = mediaSources,
|
MediaSources = mediaSources,
|
||||||
@ -562,7 +562,7 @@ namespace MediaBrowser.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
return new PlaylistItem
|
return new PlaylistItem
|
||||||
{
|
{
|
||||||
StreamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions
|
StreamInfo = new StreamBuilder(_logger).BuildAudioItem(new AudioOptions
|
||||||
{
|
{
|
||||||
ItemId = item.Id.ToString("N"),
|
ItemId = item.Id.ToString("N"),
|
||||||
MediaSources = mediaSources,
|
MediaSources = mediaSources,
|
||||||
@ -892,7 +892,7 @@ namespace MediaBrowser.Dlna.PlayTo
|
|||||||
|
|
||||||
request.MediaSource = hasMediaSources == null ?
|
request.MediaSource = hasMediaSources == null ?
|
||||||
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))
|
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)
|
if (_config.GetDlnaConfiguration().EnableDebugLogging)
|
||||||
{
|
{
|
||||||
_logger.Debug("Delaying search response by {0} seconds", delay.TotalSeconds);
|
_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);
|
EventHelper.FireEventIfNotNull(MessageReceived, this, args, _logger);
|
||||||
|
@ -92,7 +92,7 @@ namespace MediaBrowser.LocalMetadata
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return "Media Browser Xml";
|
return "Emby Xml";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ using System.Linq;
|
|||||||
|
|
||||||
namespace MediaBrowser.LocalMetadata.Images
|
namespace MediaBrowser.LocalMetadata.Images
|
||||||
{
|
{
|
||||||
public class EpisodeLocalLocalImageProvider : ILocalImageFileProvider
|
public class EpisodeLocalLocalImageProvider : ILocalImageFileProvider, IHasOrder
|
||||||
{
|
{
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
|
|
||||||
@ -24,6 +24,11 @@ namespace MediaBrowser.LocalMetadata.Images
|
|||||||
get { return "Local Images"; }
|
get { return "Local Images"; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int Order
|
||||||
|
{
|
||||||
|
get { return 0; }
|
||||||
|
}
|
||||||
|
|
||||||
public bool Supports(IHasImages item)
|
public bool Supports(IHasImages item)
|
||||||
{
|
{
|
||||||
return item is Episode && item.SupportsLocalMetadata;
|
return item is Episode && item.SupportsLocalMetadata;
|
||||||
|
@ -26,6 +26,11 @@ namespace MediaBrowser.LocalMetadata.Images
|
|||||||
|
|
||||||
public bool Supports(IHasImages item)
|
public bool Supports(IHasImages item)
|
||||||
{
|
{
|
||||||
|
if (item is Photo)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!item.IsSaveLocalMetadataEnabled())
|
if (!item.IsSaveLocalMetadataEnabled())
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
@ -12,7 +12,7 @@ using System.Linq;
|
|||||||
|
|
||||||
namespace MediaBrowser.LocalMetadata.Images
|
namespace MediaBrowser.LocalMetadata.Images
|
||||||
{
|
{
|
||||||
public class LocalImageProvider : ILocalImageFileProvider
|
public class LocalImageProvider : ILocalImageFileProvider, IHasOrder
|
||||||
{
|
{
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
|
|
||||||
|
@ -756,11 +756,6 @@ namespace MediaBrowser.LocalMetadata.Savers
|
|||||||
{
|
{
|
||||||
builder.Append("<" + singularNodeName + ">");
|
builder.Append("<" + singularNodeName + ">");
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(link.ItemType))
|
|
||||||
{
|
|
||||||
builder.Append("<Type>" + SecurityElement.Escape(link.ItemType) + "</Type>");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(link.Path))
|
if (!string.IsNullOrWhiteSpace(link.Path))
|
||||||
{
|
{
|
||||||
builder.Append("<Path>" + SecurityElement.Escape((link.Path)) + "</Path>");
|
builder.Append("<Path>" + SecurityElement.Escape((link.Path)) + "</Path>");
|
||||||
|
@ -70,10 +70,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
encodingJob.OutputFilePath = GetOutputFilePath(encodingJob);
|
encodingJob.OutputFilePath = GetOutputFilePath(encodingJob);
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(encodingJob.OutputFilePath));
|
Directory.CreateDirectory(Path.GetDirectoryName(encodingJob.OutputFilePath));
|
||||||
|
|
||||||
if (options.Context == EncodingContext.Static && encodingJob.IsInputVideo)
|
encodingJob.ReadInputAtNativeFramerate = options.ReadInputAtNativeFramerate;
|
||||||
{
|
|
||||||
encodingJob.ReadInputAtNativeFramerate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
await AcquireResources(encodingJob, cancellationToken).ConfigureAwait(false);
|
await AcquireResources(encodingJob, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
@ -305,19 +302,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
/// <returns>System.Int32.</returns>
|
/// <returns>System.Int32.</returns>
|
||||||
protected int GetNumberOfThreads(EncodingJob job, bool isWebm)
|
protected int GetNumberOfThreads(EncodingJob job, bool isWebm)
|
||||||
{
|
{
|
||||||
// Only need one thread for sync
|
return job.Options.CpuCoreLimit ?? 0;
|
||||||
if (job.Options.Context == EncodingContext.Static)
|
|
||||||
{
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isWebm)
|
|
||||||
{
|
|
||||||
// Recommended per docs
|
|
||||||
return Math.Max(Environment.ProcessorCount - 1, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected EncodingQuality GetQualitySetting()
|
protected EncodingQuality GetQualitySetting()
|
||||||
|
@ -59,7 +59,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
|
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)
|
var mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
|
||||||
? mediaSources.First()
|
? mediaSources.First()
|
||||||
@ -124,10 +124,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
state.InputContainer = mediaSource.Container;
|
state.InputContainer = mediaSource.Container;
|
||||||
state.InputFileSize = mediaSource.Size;
|
state.InputFileSize = mediaSource.Size;
|
||||||
state.InputBitrate = mediaSource.Bitrate;
|
state.InputBitrate = mediaSource.Bitrate;
|
||||||
state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
|
|
||||||
state.RunTimeTicks = mediaSource.RunTimeTicks;
|
state.RunTimeTicks = mediaSource.RunTimeTicks;
|
||||||
state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
|
state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
|
||||||
|
|
||||||
|
if (mediaSource.ReadAtNativeFramerate)
|
||||||
|
{
|
||||||
|
state.ReadInputAtNativeFramerate = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (mediaSource.VideoType.HasValue)
|
if (mediaSource.VideoType.HasValue)
|
||||||
{
|
{
|
||||||
state.VideoType = mediaSource.VideoType.Value;
|
state.VideoType = mediaSource.VideoType.Value;
|
||||||
@ -148,7 +152,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
|
state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
|
||||||
state.InputBitrate = mediaSource.Bitrate;
|
state.InputBitrate = mediaSource.Bitrate;
|
||||||
state.InputFileSize = mediaSource.Size;
|
state.InputFileSize = mediaSource.Size;
|
||||||
state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
|
|
||||||
|
|
||||||
if (state.ReadInputAtNativeFramerate ||
|
if (state.ReadInputAtNativeFramerate ||
|
||||||
mediaSource.Protocol == MediaProtocol.File && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase))
|
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.LiveTv;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
|
using MediaBrowser.MediaEncoding.Probing;
|
||||||
|
using MediaBrowser.Model.Dlna;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Logging;
|
using MediaBrowser.Model.Logging;
|
||||||
using MediaBrowser.Model.MediaInfo;
|
using MediaBrowser.Model.MediaInfo;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -72,6 +75,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
protected readonly Func<ISubtitleEncoder> SubtitleEncoder;
|
protected readonly Func<ISubtitleEncoder> SubtitleEncoder;
|
||||||
protected readonly Func<IMediaSourceManager> MediaSourceManager;
|
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)
|
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;
|
_logger = logger;
|
||||||
@ -102,16 +107,19 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the media info.
|
/// Gets the media info.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="inputFiles">The input files.</param>
|
/// <param name="request">The request.</param>
|
||||||
/// <param name="protocol">The protocol.</param>
|
|
||||||
/// <param name="isAudio">if set to <c>true</c> [is audio].</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>Task.</returns>
|
/// <returns>Task.</returns>
|
||||||
public Task<InternalMediaInfoResult> GetMediaInfo(string[] inputFiles, MediaProtocol protocol, bool isAudio,
|
public Task<Model.MediaInfo.MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
return GetMediaInfoInternal(GetInputArgument(inputFiles, protocol), !isAudio,
|
var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
|
||||||
GetProbeSizeArgument(inputFiles, protocol), cancellationToken);
|
|
||||||
|
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>
|
/// <summary>
|
||||||
@ -141,13 +149,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
/// Gets the media info internal.
|
/// Gets the media info internal.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="inputPath">The input path.</param>
|
/// <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="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="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>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>Task{MediaInfoResult}.</returns>
|
/// <returns>Task{MediaInfoResult}.</returns>
|
||||||
/// <exception cref="System.ApplicationException"></exception>
|
/// <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,
|
string probeSizeArgument,
|
||||||
|
bool isAudio,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var args = extractChapters
|
var args = extractChapters
|
||||||
@ -164,6 +181,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
// Must consume both or ffmpeg may hang due to deadlocks. See comments below.
|
// Must consume both or ffmpeg may hang due to deadlocks. See comments below.
|
||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
|
RedirectStandardInput = true,
|
||||||
FileName = FFProbePath,
|
FileName = FFProbePath,
|
||||||
Arguments = string.Format(args,
|
Arguments = string.Format(args,
|
||||||
probeSizeArgument, inputPath).Trim(),
|
probeSizeArgument, inputPath).Trim(),
|
||||||
@ -177,15 +195,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
_logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
_logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||||
|
|
||||||
process.Exited += ProcessExited;
|
|
||||||
|
|
||||||
await _ffProbeResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
await _ffProbeResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
InternalMediaInfoResult result;
|
var processWrapper = new ProcessWrapper(process, this);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
process.Start();
|
StartProcess(processWrapper);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -200,19 +216,57 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
{
|
{
|
||||||
process.BeginErrorReadLine();
|
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
|
catch
|
||||||
{
|
{
|
||||||
// Hate having to do this
|
StopProcess(processWrapper, 100, true);
|
||||||
try
|
|
||||||
{
|
|
||||||
process.Kill();
|
|
||||||
}
|
|
||||||
catch (Exception ex1)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error killing ffprobe", ex1);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
@ -221,30 +275,102 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
_ffProbeResourcePool.Release();
|
_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
|
using (var reader = new StreamReader(source))
|
||||||
foreach (var stream in result.streams)
|
|
||||||
{
|
{
|
||||||
if (string.Equals(stream.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
|
while (!reader.EndOfStream)
|
||||||
{
|
{
|
||||||
stream.display_aspect_ratio = string.Empty;
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
}
|
|
||||||
if (string.Equals(stream.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
|
var line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||||
{
|
|
||||||
stream.sample_aspect_ratio = string.Empty;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
return result;
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.ErrorException("Error reading ffprobe output", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -252,16 +378,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
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)
|
public Task<Stream> ExtractAudioImage(string path, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return ExtractImage(new[] { path }, MediaProtocol.File, true, null, null, 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);
|
return await ExtractImageInternal(inputArgument, protocol, threedFormat, offset, true, resourcePool, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
_logger.Error("I-frame image extraction failed, will attempt standard way. Input: {0}", inputArgument);
|
_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);
|
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
process.Start();
|
var processWrapper = new ProcessWrapper(process, this);
|
||||||
|
|
||||||
|
StartProcess(processWrapper);
|
||||||
|
|
||||||
var memoryStream = new MemoryStream();
|
var memoryStream = new MemoryStream();
|
||||||
|
|
||||||
@ -384,23 +506,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
if (!ranToCompletion)
|
if (!ranToCompletion)
|
||||||
{
|
{
|
||||||
try
|
StopProcess(processWrapper, 1000, false);
|
||||||
{
|
|
||||||
_logger.Info("Killing ffmpeg process");
|
|
||||||
|
|
||||||
process.StandardInput.WriteLine("q");
|
|
||||||
|
|
||||||
process.WaitForExit(1000);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error killing process", ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resourcePool.Release();
|
resourcePool.Release();
|
||||||
|
|
||||||
var exitCode = ranToCompletion ? process.ExitCode : -1;
|
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
|
||||||
|
|
||||||
process.Dispose();
|
process.Dispose();
|
||||||
|
|
||||||
@ -419,31 +530,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
return memoryStream;
|
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)
|
public string GetTimeParameter(long ticks)
|
||||||
{
|
{
|
||||||
var time = TimeSpan.FromTicks(ticks);
|
var time = TimeSpan.FromTicks(ticks);
|
||||||
@ -510,9 +596,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
bool ranToCompletion;
|
bool ranToCompletion;
|
||||||
|
|
||||||
|
var processWrapper = new ProcessWrapper(process, this);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
process.Start();
|
StartProcess(processWrapper);
|
||||||
|
|
||||||
// Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
|
// 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.
|
// but we still need to detect if the process hangs.
|
||||||
@ -536,18 +624,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
if (!ranToCompletion)
|
if (!ranToCompletion)
|
||||||
{
|
{
|
||||||
try
|
StopProcess(processWrapper, 1000, false);
|
||||||
{
|
|
||||||
_logger.Info("Killing ffmpeg process");
|
|
||||||
|
|
||||||
process.StandardInput.WriteLine("q");
|
|
||||||
|
|
||||||
process.WaitForExit(1000);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.ErrorException("Error killing process", ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -555,7 +632,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
resourcePool.Release();
|
resourcePool.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
var exitCode = ranToCompletion ? process.ExitCode : -1;
|
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
|
||||||
|
|
||||||
process.Dispose();
|
process.Dispose();
|
||||||
|
|
||||||
@ -608,5 +685,122 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
return job.OutputFilePath;
|
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\JobLogger.cs" />
|
||||||
<Compile Include="Encoder\MediaEncoder.cs" />
|
<Compile Include="Encoder\MediaEncoder.cs" />
|
||||||
<Compile Include="Encoder\VideoEncoder.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="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Subtitles\ISubtitleParser.cs" />
|
<Compile Include="Subtitles\ISubtitleParser.cs" />
|
||||||
<Compile Include="Subtitles\ISubtitleWriter.cs" />
|
<Compile Include="Subtitles\ISubtitleWriter.cs" />
|
||||||
@ -91,6 +94,10 @@
|
|||||||
<Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
|
<Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
|
||||||
<Name>MediaBrowser.Controller</Name>
|
<Name>MediaBrowser.Controller</Name>
|
||||||
</ProjectReference>
|
</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">
|
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
|
||||||
<Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
|
<Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
|
||||||
<Name>MediaBrowser.Model</Name>
|
<Name>MediaBrowser.Model</Name>
|
||||||
@ -99,7 +106,9 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="packages.config" />
|
<None Include="packages.config" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup />
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Probing\whitelist.txt" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
<!-- 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.
|
Other similar extension points exist, see Microsoft.Common.targets.
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace MediaBrowser.Providers.MediaInfo
|
namespace MediaBrowser.MediaEncoding.Probing
|
||||||
{
|
{
|
||||||
public static class FFProbeHelpers
|
public static class FFProbeHelpers
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.MediaEncoding
|
namespace MediaBrowser.MediaEncoding.Probing
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class MediaInfoResult
|
/// Class MediaInfoResult
|
||||||
@ -89,7 +89,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The channel_layout.</value>
|
/// <value>The channel_layout.</value>
|
||||||
public string channel_layout { get; set; }
|
public string channel_layout { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the avg_frame_rate.
|
/// Gets or sets the avg_frame_rate.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -317,7 +317,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The probe_score.</value>
|
/// <value>The probe_score.</value>
|
||||||
public int probe_score { get; set; }
|
public int probe_score { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the tags.
|
/// Gets or sets the tags.
|
||||||
/// </summary>
|
/// </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…
x
Reference in New Issue
Block a user