Merge pull request #1079 from MediaBrowser/dev

3.0.5582.0
This commit is contained in:
Luke 2015-04-14 00:43:41 -04:00
commit 935de313d5
269 changed files with 7207 additions and 3159 deletions

View File

@ -6,7 +6,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace MediaBrowser.Server.Implementations.Drawing
namespace Emby.Drawing.Common
{
/// <summary>
/// Taken from http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349

View 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>

View 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;
}
}
}

View 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"; }
}
}
}

View 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;
}
}
}

View 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);
}
}
}
}
}

View 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);
}
}
}
}
}
}

View 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);
}
}
}
}
}
}

View 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; }
}
}

View 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);
}
}
}
}

View File

@ -1,7 +1,7 @@
using ImageMagickSharp;
using System;
namespace MediaBrowser.Server.Implementations.Drawing
namespace Emby.Drawing.ImageMagick
{
public class PercentPlayedDrawer
{

View File

@ -4,7 +4,7 @@ using MediaBrowser.Model.Drawing;
using System;
using System.IO;
namespace MediaBrowser.Server.Implementations.Drawing
namespace Emby.Drawing.ImageMagick
{
public class PlayedIndicatorDrawer
{

View 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); }
}
}
}

View File

@ -3,7 +3,7 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Drawing;
using System.Globalization;
namespace MediaBrowser.Server.Implementations.Drawing
namespace Emby.Drawing.ImageMagick
{
public class UnplayedCountIndicator
{

View File

@ -1,4 +1,4 @@
using ImageMagickSharp;
using Emby.Drawing.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.IO;
using MediaBrowser.Controller;
@ -18,7 +18,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Server.Implementations.Drawing
namespace Emby.Drawing
{
/// <summary>
/// Class ImageProcessor
@ -50,12 +50,14 @@ namespace MediaBrowser.Server.Implementations.Drawing
private readonly IFileSystem _fileSystem;
private readonly IJsonSerializer _jsonSerializer;
private readonly IServerApplicationPaths _appPaths;
private readonly IImageEncoder _imageEncoder;
public ImageProcessor(ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem, IJsonSerializer jsonSerializer)
public ImageProcessor(ILogger logger, IServerApplicationPaths appPaths, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IImageEncoder imageEncoder)
{
_logger = logger;
_fileSystem = fileSystem;
_jsonSerializer = jsonSerializer;
_imageEncoder = imageEncoder;
_appPaths = appPaths;
_saveImageSizeTimer = new Timer(SaveImageSizeCallback, null, Timeout.Infinite, Timeout.Infinite);
@ -85,8 +87,14 @@ namespace MediaBrowser.Server.Implementations.Drawing
}
_cachedImagedSizes = new ConcurrentDictionary<Guid, ImageSize>(sizeDictionary);
}
LogImageMagickVersionVersion();
public string[] SupportedInputFormats
{
get
{
return _imageEncoder.SupportedInputFormats;
}
}
private string ResizedImageCachePath
@ -130,44 +138,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
public ImageFormat[] GetSupportedImageOutputFormats()
{
if (_webpAvailable)
{
return new[] { ImageFormat.Webp, ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png };
}
return new[] { ImageFormat.Gif, ImageFormat.Jpg, ImageFormat.Png };
}
private bool _webpAvailable = true;
private void TestWebp()
{
try
{
var tmpPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".webp");
Directory.CreateDirectory(Path.GetDirectoryName(tmpPath));
using (var wand = new MagickWand(1, 1, new PixelWand("none", 1)))
{
wand.SaveImage(tmpPath);
}
}
catch (Exception ex)
{
_logger.ErrorException("Error loading webp: ", ex);
_webpAvailable = false;
}
}
private void LogImageMagickVersionVersion()
{
try
{
_logger.Info("ImageMagick version: " + Wand.VersionString);
}
catch (Exception ex)
{
_logger.ErrorException("Error loading ImageMagick: ", ex);
}
TestWebp();
return _imageEncoder.SupportedOutputFormats;
}
public async Task<string> ProcessImage(ImageProcessingOptions options)
@ -244,36 +215,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
if (string.IsNullOrWhiteSpace(options.BackgroundColor))
{
using (var originalImage = new MagickWand(originalImagePath))
{
originalImage.CurrentImage.ResizeImage(newWidth, newHeight);
DrawIndicator(originalImage, newWidth, newHeight, options);
originalImage.CurrentImage.CompressionQuality = quality;
originalImage.SaveImage(cacheFilePath);
}
}
else
{
using (var wand = new MagickWand(newWidth, newHeight, options.BackgroundColor))
{
using (var originalImage = new MagickWand(originalImagePath))
{
originalImage.CurrentImage.ResizeImage(newWidth, newHeight);
wand.CurrentImage.CompositeImage(originalImage, CompositeOperator.OverCompositeOp, 0, 0);
DrawIndicator(wand, newWidth, newHeight, options);
wand.CurrentImage.CompressionQuality = quality;
wand.SaveImage(cacheFilePath);
}
}
}
_imageEncoder.EncodeImage(originalImagePath, cacheFilePath, newWidth, newHeight, quality, options);
}
}
finally
@ -286,7 +228,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
private ImageFormat GetOutputFormat(ImageFormat requestedFormat)
{
if (requestedFormat == ImageFormat.Webp && !_webpAvailable)
if (requestedFormat == ImageFormat.Webp && !_imageEncoder.SupportedOutputFormats.Contains(ImageFormat.Webp))
{
return ImageFormat.Png;
}
@ -294,46 +236,6 @@ namespace MediaBrowser.Server.Implementations.Drawing
return requestedFormat;
}
/// <summary>
/// Draws the indicator.
/// </summary>
/// <param name="wand">The wand.</param>
/// <param name="imageWidth">Width of the image.</param>
/// <param name="imageHeight">Height of the image.</param>
/// <param name="options">The options.</param>
private void DrawIndicator(MagickWand wand, int imageWidth, int imageHeight, ImageProcessingOptions options)
{
if (!options.AddPlayedIndicator && !options.UnplayedCount.HasValue && options.PercentPlayed.Equals(0))
{
return;
}
try
{
if (options.AddPlayedIndicator)
{
var currentImageSize = new ImageSize(imageWidth, imageHeight);
new PlayedIndicatorDrawer(_appPaths).DrawPlayedIndicator(wand, currentImageSize);
}
else if (options.UnplayedCount.HasValue)
{
var currentImageSize = new ImageSize(imageWidth, imageHeight);
new UnplayedCountIndicator(_appPaths).DrawUnplayedCountIndicator(wand, currentImageSize, options.UnplayedCount.Value);
}
if (options.PercentPlayed > 0)
{
new PercentPlayedDrawer().Process(wand, options.PercentPlayed);
}
}
catch (Exception ex)
{
_logger.ErrorException("Error drawing indicator overlay", ex);
}
}
/// <summary>
/// Crops whitespace from an image, caches the result, and returns the cached path
/// </summary>
@ -360,11 +262,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
{
Directory.CreateDirectory(Path.GetDirectoryName(croppedImagePath));
using (var wand = new MagickWand(originalImagePath))
{
wand.CurrentImage.TrimImage(10);
wand.SaveImage(croppedImagePath);
}
_imageEncoder.CropWhiteSpace(originalImagePath, croppedImagePath);
}
catch (Exception ex)
{
@ -500,17 +398,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
CheckDisposed();
using (var wand = new MagickWand())
{
wand.PingImage(path);
var img = wand.CurrentImage;
size = new ImageSize
{
Width = img.Width,
Height = img.Height
};
}
size = _imageEncoder.GetImageSize(path);
}
StartSaveImageSizeTimer();
@ -838,6 +726,11 @@ namespace MediaBrowser.Server.Implementations.Drawing
return Path.Combine(path, filename);
}
public void CreateImageCollage(ImageCollageOptions options)
{
_imageEncoder.CreateImageCollage(options);
}
public IEnumerable<IImageEnhancer> GetSupportedEnhancers(IHasImages item, ImageType imageType)
{
return ImageEnhancers.Where(i =>
@ -860,7 +753,7 @@ namespace MediaBrowser.Server.Implementations.Drawing
public void Dispose()
{
_disposed = true;
Wand.CloseEnvironment();
_imageEncoder.Dispose();
_saveImageSizeTimer.Dispose();
}

View 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
//

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="ImageMagickSharp" version="1.0.0.14" targetFramework="net45" />
</packages>

View File

@ -151,7 +151,7 @@ namespace MediaBrowser.Api
{
lock (_activeTranscodingJobs)
{
var job = new TranscodingJob
var job = new TranscodingJob(Logger)
{
Type = type,
Path = path,
@ -284,28 +284,72 @@ namespace MediaBrowser.Api
{
job.ActiveRequestCount++;
job.DisposeKillTimer();
if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive)
{
job.StopKillTimer();
}
}
public void OnTranscodeEndRequest(TranscodingJob job)
{
job.ActiveRequestCount--;
if (job.ActiveRequestCount == 0)
Logger.Debug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount);
if (job.ActiveRequestCount <= 0)
{
// TODO: Lower this hls timeout
var timerDuration = job.Type == TranscodingJobType.Progressive ?
1000 :
7200000;
PingTimer(job, false);
}
}
internal void PingTranscodingJob(string playSessionId)
{
if (string.IsNullOrEmpty(playSessionId))
{
throw new ArgumentNullException("playSessionId");
}
if (job.KillTimer == null)
{
job.KillTimer = new Timer(OnTranscodeKillTimerStopped, job, timerDuration, Timeout.Infinite);
}
else
{
job.KillTimer.Change(timerDuration, Timeout.Infinite);
}
Logger.Debug("PingTranscodingJob PlaySessionId={0}", playSessionId);
var jobs = new List<TranscodingJob>();
lock (_activeTranscodingJobs)
{
// This is really only needed for HLS.
// Progressive streams can stop on their own reliably
jobs = jobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
}
foreach (var job in jobs)
{
PingTimer(job, true);
}
}
private void PingTimer(TranscodingJob job, bool isProgressCheckIn)
{
if (job.HasExited)
{
job.StopKillTimer();
return;
}
// TODO: Lower this hls timeout
var timerDuration = job.Type == TranscodingJobType.Progressive ?
1000 :
1800000;
// We can really reduce the timeout for apps that are using the newer api
if (!string.IsNullOrWhiteSpace(job.PlaySessionId) && job.Type != TranscodingJobType.Progressive)
{
timerDuration = 20000;
}
// Don't start the timer for playback checkins with progressive streaming
if (job.Type != TranscodingJobType.Progressive || !isProgressCheckIn)
{
job.StartKillTimer(timerDuration, OnTranscodeKillTimerStopped);
}
else
{
job.ChangeKillTimerIfStarted(timerDuration);
}
}
@ -317,6 +361,8 @@ namespace MediaBrowser.Api
{
var job = (TranscodingJob)state;
Logger.Debug("Transcoding kill timer stopped for JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
KillTranscodingJob(job, path => true);
}
@ -329,19 +375,14 @@ namespace MediaBrowser.Api
/// <returns>Task.</returns>
internal void KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles)
{
if (string.IsNullOrEmpty(deviceId))
{
throw new ArgumentNullException("deviceId");
}
KillTranscodingJobs(j =>
{
if (string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase))
if (!string.IsNullOrWhiteSpace(playSessionId))
{
return string.IsNullOrWhiteSpace(playSessionId) || string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase);
return string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase);
}
return false;
return string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase);
}, deleteFiles);
}
@ -381,6 +422,10 @@ namespace MediaBrowser.Api
/// <param name="delete">The delete.</param>
private void KillTranscodingJob(TranscodingJob job, Func<string, bool> delete)
{
job.DisposeKillTimer();
Logger.Debug("KillTranscodingJob - JobId {0} PlaySessionId {1}. Killing transcoding", job.Id, job.PlaySessionId);
lock (_activeTranscodingJobs)
{
_activeTranscodingJobs.Remove(job);
@ -389,34 +434,23 @@ namespace MediaBrowser.Api
{
job.CancellationTokenSource.Cancel();
}
job.DisposeKillTimer();
}
lock (job.ProcessLock)
{
if (job.TranscodingThrottler != null)
{
job.TranscodingThrottler.Stop();
}
var process = job.Process;
var hasExited = true;
try
{
hasExited = process.HasExited;
}
catch (Exception ex)
{
Logger.ErrorException("Error determining if ffmpeg process has exited for {0}", ex, job.Path);
}
var hasExited = job.HasExited;
if (!hasExited)
{
try
{
if (job.TranscodingThrottler != null)
{
job.TranscodingThrottler.Stop();
}
Logger.Info("Killing ffmpeg process for {0}", job.Path);
//process.Kill();
@ -558,6 +592,7 @@ namespace MediaBrowser.Api
/// </summary>
/// <value>The process.</value>
public Process Process { get; set; }
public ILogger Logger { get; private set; }
/// <summary>
/// Gets or sets the active request count.
/// </summary>
@ -567,7 +602,7 @@ namespace MediaBrowser.Api
/// Gets or sets the kill timer.
/// </summary>
/// <value>The kill timer.</value>
public Timer KillTimer { get; set; }
private Timer KillTimer { get; set; }
public string DeviceId { get; set; }
@ -590,12 +625,74 @@ namespace MediaBrowser.Api
public TranscodingThrottler TranscodingThrottler { get; set; }
private readonly object _timerLock = new object();
public TranscodingJob(ILogger logger)
{
Logger = logger;
}
public void StopKillTimer()
{
lock (_timerLock)
{
if (KillTimer != null)
{
KillTimer.Change(Timeout.Infinite, Timeout.Infinite);
}
}
}
public void DisposeKillTimer()
{
if (KillTimer != null)
lock (_timerLock)
{
KillTimer.Dispose();
KillTimer = null;
if (KillTimer != null)
{
KillTimer.Dispose();
KillTimer = null;
}
}
}
public void StartKillTimer(int intervalMs, TimerCallback callback)
{
CheckHasExited();
lock (_timerLock)
{
if (KillTimer == null)
{
Logger.Debug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
KillTimer = new Timer(callback, this, intervalMs, Timeout.Infinite);
}
else
{
Logger.Debug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
KillTimer.Change(intervalMs, Timeout.Infinite);
}
}
}
public void ChangeKillTimerIfStarted(int intervalMs)
{
CheckHasExited();
lock (_timerLock)
{
if (KillTimer != null)
{
Logger.Debug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
KillTimer.Change(intervalMs, Timeout.Infinite);
}
}
}
private void CheckHasExited()
{
if (HasExited)
{
throw new ObjectDisposedException("Job");
}
}
}

View File

@ -259,7 +259,7 @@ namespace MediaBrowser.Api
.GetRecursiveChildren(i => i is IHasArtist)
.Cast<IHasArtist>()
.SelectMany(i => i.AllArtists)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.FirstOrDefault(i =>
{
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
@ -281,7 +281,7 @@ namespace MediaBrowser.Api
return libraryManager.RootFolder.GetRecursiveChildren()
.SelectMany(i => i.Genres)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.FirstOrDefault(i =>
{
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
@ -301,7 +301,7 @@ namespace MediaBrowser.Api
return libraryManager.RootFolder
.GetRecursiveChildren(i => i is Game)
.SelectMany(i => i.Genres)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.FirstOrDefault(i =>
{
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
@ -324,7 +324,7 @@ namespace MediaBrowser.Api
return libraryManager.RootFolder
.GetRecursiveChildren()
.SelectMany(i => i.Studios)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.FirstOrDefault(i =>
{
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
@ -348,7 +348,7 @@ namespace MediaBrowser.Api
.GetRecursiveChildren()
.SelectMany(i => i.People)
.Select(i => i.Name)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.FirstOrDefault(i =>
{
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));

View File

@ -123,7 +123,7 @@ namespace MediaBrowser.Api
public void Post(AutoSetMetadataOptions request)
{
_configurationManager.DisableMetadataService("Media Browser Xml");
_configurationManager.DisableMetadataService("Emby Xml");
_configurationManager.SaveConfiguration();
}

View File

@ -76,7 +76,7 @@ namespace MediaBrowser.Api
.ToArray();
result.Genres = items.SelectMany(i => i.Genres)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.OrderBy(i => i)
.ToArray();

View File

@ -1,4 +1,5 @@
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dto;
@ -186,6 +187,9 @@ namespace MediaBrowser.Api.LiveTv
[ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
public bool? IsMovie { get; set; }
[ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
public bool? IsSports { get; set; }
[ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? StartIndex { get; set; }
@ -218,6 +222,9 @@ namespace MediaBrowser.Api.LiveTv
[ApiMember(Name = "HasAired", Description = "Optional. Filter by programs that have completed airing, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? HasAired { get; set; }
[ApiMember(Name = "IsSports", Description = "Optional filter for sports.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET,POST")]
public bool? IsSports { get; set; }
[ApiMember(Name = "IsMovie", Description = "Optional filter for movies.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? IsMovie { get; set; }
}
@ -422,11 +429,12 @@ namespace MediaBrowser.Api.LiveTv
query.SortBy = (request.SortBy ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
query.SortOrder = request.SortOrder;
query.IsMovie = request.IsMovie;
query.IsSports = request.IsSports;
query.Genres = (request.Genres ?? String.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
var result = await _liveTvManager.GetPrograms(query, CancellationToken.None).ConfigureAwait(false);
return ToOptimizedSerializedResultUsingCache(result);
return ToOptimizedResult(result);
}
public async Task<object> Get(GetRecommendedPrograms request)
@ -437,12 +445,13 @@ namespace MediaBrowser.Api.LiveTv
IsAiring = request.IsAiring,
Limit = request.Limit,
HasAired = request.HasAired,
IsMovie = request.IsMovie
IsMovie = request.IsMovie,
IsSports = request.IsSports
};
var result = await _liveTvManager.GetRecommendedPrograms(query, CancellationToken.None).ConfigureAwait(false);
return ToOptimizedSerializedResultUsingCache(result);
return ToOptimizedResult(result);
}
public object Post(GetPrograms request)
@ -452,6 +461,9 @@ namespace MediaBrowser.Api.LiveTv
public async Task<object> Get(GetRecordings request)
{
var options = new DtoOptions();
options.DeviceId = AuthorizationContext.GetAuthorizationInfo(Request).DeviceId;
var result = await _liveTvManager.GetRecordings(new RecordingQuery
{
ChannelId = request.ChannelId,
@ -463,16 +475,19 @@ namespace MediaBrowser.Api.LiveTv
SeriesTimerId = request.SeriesTimerId,
IsInProgress = request.IsInProgress
}, CancellationToken.None).ConfigureAwait(false);
}, options, CancellationToken.None).ConfigureAwait(false);
return ToOptimizedSerializedResultUsingCache(result);
return ToOptimizedResult(result);
}
public async Task<object> Get(GetRecording request)
{
var user = string.IsNullOrEmpty(request.UserId) ? null : _userManager.GetUserById(request.UserId);
var result = await _liveTvManager.GetRecording(request.Id, CancellationToken.None, user).ConfigureAwait(false);
var options = new DtoOptions();
options.DeviceId = AuthorizationContext.GetAuthorizationInfo(Request).DeviceId;
var result = await _liveTvManager.GetRecording(request.Id, options, CancellationToken.None, user).ConfigureAwait(false);
return ToOptimizedSerializedResultUsingCache(result);
}

View File

@ -410,7 +410,7 @@ namespace MediaBrowser.Api.Movies
return items
.SelectMany(i => i.People.Where(p => !string.Equals(PersonType.Director, p.Type, StringComparison.OrdinalIgnoreCase)).Take(2))
.Select(i => i.Name)
.Distinct(StringComparer.OrdinalIgnoreCase);
.DistinctNames();
}
private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items)
@ -419,7 +419,7 @@ namespace MediaBrowser.Api.Movies
.Select(i => i.People.FirstOrDefault(p => string.Equals(PersonType.Director, p.Type, StringComparison.OrdinalIgnoreCase)))
.Where(i => i != null)
.Select(i => i.Name)
.Distinct(StringComparer.OrdinalIgnoreCase);
.DistinctNames();
}
}
}

View File

@ -79,12 +79,12 @@ namespace MediaBrowser.Api.Music
var artists1 = album1
.AllArtists
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.ToList();
var artists2 = album2
.AllArtists
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
return points + artists1.Where(artists2.ContainsKey).Sum(i => 5);

View File

@ -1026,7 +1026,7 @@ namespace MediaBrowser.Api.Playback
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
state.LogFileStream = FileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true);
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(commandLineLogMessage + Environment.NewLine + Environment.NewLine);
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(Request.AbsoluteUri + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
await state.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
@ -1514,6 +1514,10 @@ namespace MediaBrowser.Api.Playback
request.PlaySessionId = val;
}
else if (i == 22)
{
// api_key
}
else if (i == 23)
{
request.LiveStreamId = val;
}
@ -1624,14 +1628,19 @@ namespace MediaBrowser.Api.Playback
var archivable = item as IArchivable;
state.IsInputArchive = archivable != null && archivable.IsArchive;
MediaSourceInfo mediaSource = null;
MediaSourceInfo mediaSource;
if (string.IsNullOrWhiteSpace(request.LiveStreamId))
{
var mediaSources = await MediaSourceManager.GetPlayackMediaSources(request.Id, false, cancellationToken).ConfigureAwait(false);
var mediaSources = (await MediaSourceManager.GetPlayackMediaSources(request.Id, null, false, new[] { MediaType.Audio, MediaType.Video }, cancellationToken).ConfigureAwait(false)).ToList();
mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
? mediaSources.First()
: mediaSources.First(i => string.Equals(i.Id, request.MediaSourceId));
: mediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId));
if (mediaSource == null && string.Equals(request.Id, request.MediaSourceId, StringComparison.OrdinalIgnoreCase))
{
mediaSource = mediaSources.First();
}
}
else
{
@ -1700,6 +1709,102 @@ namespace MediaBrowser.Api.Playback
{
state.OutputAudioCodec = "copy";
}
if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase) && TranscodingJobType == TranscodingJobType.Hls)
{
var segmentLength = GetSegmentLength(state);
if (segmentLength.HasValue)
{
state.SegmentLength = segmentLength.Value;
}
}
}
private int? GetSegmentLength(StreamState state)
{
var stream = state.VideoStream;
if (stream == null)
{
return null;
}
var frames = stream.KeyFrames;
if (frames == null || frames.Count < 2)
{
return null;
}
Logger.Debug("Found keyframes at {0}", string.Join(",", frames.ToArray()));
var intervals = new List<int>();
for (var i = 1; i < frames.Count; i++)
{
var start = frames[i - 1];
var end = frames[i];
intervals.Add(end - start);
}
Logger.Debug("Found keyframes intervals {0}", string.Join(",", intervals.ToArray()));
var results = new List<Tuple<int, int>>();
for (var i = 1; i <= 10; i++)
{
var idealMs = i*1000;
if (intervals.Max() < idealMs - 1000)
{
break;
}
var segments = PredictStreamCopySegments(intervals, idealMs);
var variance = segments.Select(s => Math.Abs(idealMs - s)).Sum();
results.Add(new Tuple<int, int>(i, variance));
}
if (results.Count == 0)
{
return null;
}
return results.OrderBy(i => i.Item2).ThenBy(i => i.Item1).Select(i => i.Item1).First();
}
private List<int> PredictStreamCopySegments(List<int> intervals, int idealMs)
{
var segments = new List<int>();
var currentLength = 0;
foreach (var interval in intervals)
{
if (currentLength == 0 || (currentLength + interval) <= idealMs)
{
currentLength += interval;
}
else
{
// The segment will either be above or below the ideal.
// Need to figure out which is preferable
var offset1 = Math.Abs(idealMs - currentLength);
var offset2 = Math.Abs(idealMs - (currentLength + interval));
if (offset1 <= offset2)
{
segments.Add(currentLength);
currentLength = interval;
}
else
{
currentLength += interval;
}
}
}
Logger.Debug("Predicted actual segment lengths for length {0}: {1}", idealMs, string.Join(",", segments.ToArray()));
return segments;
}
private void AttachMediaSourceInfo(StreamState state,

View File

@ -5,7 +5,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.IO;
@ -518,25 +517,14 @@ namespace MediaBrowser.Api.Playback.Dash
private async Task WaitForSegment(string playlist, string segment, CancellationToken cancellationToken)
{
var tmpPath = playlist + ".tmp";
var segmentFilename = Path.GetFileName(segment);
Logger.Debug("Waiting for {0} in {1}", segmentFilename, playlist);
while (true)
{
FileStream fileStream;
try
{
fileStream = FileSystem.GetFileStream(tmpPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
}
catch (IOException)
{
fileStream = FileSystem.GetFileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
}
// Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
using (fileStream)
using (var fileStream = GetPlaylistFileStream(playlist))
{
using (var reader = new StreamReader(fileStream))
{

View File

@ -3,7 +3,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO;
@ -86,6 +85,7 @@ namespace MediaBrowser.Api.Playback.Hls
state.Request.StartTimeTicks = null;
}
TranscodingJob job = null;
var playlist = state.OutputFilePath;
if (!File.Exists(playlist))
@ -98,7 +98,7 @@ namespace MediaBrowser.Api.Playback.Hls
// If the playlist doesn't already exist, startup ffmpeg
try
{
await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false);
job = await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false);
}
catch
{
@ -117,6 +117,12 @@ namespace MediaBrowser.Api.Playback.Hls
if (isLive)
{
job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
if (job != null)
{
ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
}
return ResultFactory.GetResult(GetLivePlaylistText(playlist, state.SegmentLength), MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
}
@ -135,6 +141,13 @@ namespace MediaBrowser.Api.Playback.Hls
var playlistText = GetMasterPlaylistFileText(playlist, videoBitrate + audioBitrate, appendBaselineStream, baselineStreamBitrate);
job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType);
if (job != null)
{
ApiEntryPoint.Instance.OnTranscodeEndRequest(job);
}
return ResultFactory.GetResult(playlistText, MimeTypes.GetMimeType("playlist.m3u8"), new Dictionary<string, string>());
}
@ -186,7 +199,7 @@ namespace MediaBrowser.Api.Playback.Hls
while (true)
{
// Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written
using (var fileStream = FileSystem.GetFileStream(playlist, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
using (var fileStream = GetPlaylistFileStream(playlist))
{
using (var reader = new StreamReader(fileStream))
{
@ -212,6 +225,20 @@ namespace MediaBrowser.Api.Playback.Hls
}
}
protected Stream GetPlaylistFileStream(string path)
{
var tmpPath = path + ".tmp";
try
{
return FileSystem.GetFileStream(tmpPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
}
catch (IOException)
{
return FileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true);
}
}
protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
{
var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream;

View File

@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
@ -128,9 +127,27 @@ namespace MediaBrowser.Api.Playback.Hls
}
else
{
var startTranscoding = false;
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
var segmentGapRequiringTranscodingChange = 24/state.SegmentLength;
if (currentTranscodingIndex == null || requestedIndex < currentTranscodingIndex.Value || (requestedIndex - currentTranscodingIndex.Value) > segmentGapRequiringTranscodingChange)
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
if (currentTranscodingIndex == null)
{
Logger.Debug("Starting transcoding because currentTranscodingIndex=null");
startTranscoding = true;
}
else if (requestedIndex < currentTranscodingIndex.Value)
{
Logger.Debug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", requestedIndex, currentTranscodingIndex);
startTranscoding = true;
}
else if ((requestedIndex - currentTranscodingIndex.Value) > segmentGapRequiringTranscodingChange)
{
Logger.Debug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", (requestedIndex - currentTranscodingIndex.Value), segmentGapRequiringTranscodingChange, requestedIndex);
startTranscoding = true;
}
if (startTranscoding)
{
// If the playlist doesn't already exist, startup ffmpeg
try
@ -145,7 +162,6 @@ namespace MediaBrowser.Api.Playback.Hls
request.StartTimeTicks = GetSeekPositionTicks(state, requestedIndex);
job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
ApiEntryPoint.Instance.OnTranscodeBeginRequest(job);
}
catch
{
@ -153,7 +169,15 @@ namespace MediaBrowser.Api.Playback.Hls
throw;
}
await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
//await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
}
else
{
job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
if (job.TranscodingThrottler != null)
{
job.TranscodingThrottler.UnpauseTranscoding();
}
}
}
}
@ -300,7 +324,7 @@ namespace MediaBrowser.Api.Playback.Hls
var segmentFilename = Path.GetFileName(segmentPath);
using (var fileStream = FileSystem.GetFileStream(playlistPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
using (var fileStream = GetPlaylistFileStream(playlistPath))
{
using (var reader = new StreamReader(fileStream))
{
@ -712,7 +736,7 @@ namespace MediaBrowser.Api.Playback.Hls
).Trim();
}
return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -flags -global_header -sc_threshold 0 {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"",
return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -flags -global_header -copyts -sc_threshold 0 {5} -hls_time {6} -start_number {7} -hls_list_size {8} -y \"{9}\"",
inputModifier,
GetInputArgument(state),
threads,

View File

@ -3,12 +3,10 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO;
using ServiceStack;
using System;
using System.IO;
namespace MediaBrowser.Api.Playback.Hls
{

View File

@ -1,4 +1,6 @@
using MediaBrowser.Controller.Devices;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
@ -59,23 +61,27 @@ namespace MediaBrowser.Api.Playback
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IDeviceManager _deviceManager;
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _config;
private readonly INetworkManager _networkManager;
public MediaInfoService(IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager)
public MediaInfoService(IMediaSourceManager mediaSourceManager, IDeviceManager deviceManager, ILibraryManager libraryManager, IServerConfigurationManager config, INetworkManager networkManager)
{
_mediaSourceManager = mediaSourceManager;
_deviceManager = deviceManager;
_libraryManager = libraryManager;
_config = config;
_networkManager = networkManager;
}
public async Task<object> Get(GetPlaybackInfo request)
{
var result = await GetPlaybackInfo(request.Id, request.UserId).ConfigureAwait(false);
var result = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }).ConfigureAwait(false);
return ToOptimizedResult(result);
}
public async Task<object> Get(GetLiveMediaInfo request)
{
var result = await GetPlaybackInfo(request.Id, request.UserId).ConfigureAwait(false);
var result = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }).ConfigureAwait(false);
return ToOptimizedResult(result);
}
@ -122,29 +128,32 @@ namespace MediaBrowser.Api.Playback
public async Task<object> Post(GetPostedPlaybackInfo request)
{
var info = await GetPlaybackInfo(request.Id, request.UserId, request.MediaSourceId, request.LiveStreamId).ConfigureAwait(false);
var authInfo = AuthorizationContext.GetAuthorizationInfo(Request);
var profile = request.DeviceProfile;
if (profile == null)
var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
if (caps != null)
{
var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
if (caps != null)
if (profile == null)
{
profile = caps.DeviceProfile;
}
}
var info = await GetPlaybackInfo(request.Id, request.UserId, new[] { MediaType.Audio, MediaType.Video }, request.MediaSourceId, request.LiveStreamId).ConfigureAwait(false);
if (profile != null)
{
var mediaSourceId = request.MediaSourceId;
SetDeviceSpecificData(request.Id, info, profile, authInfo, request.MaxStreamingBitrate, request.StartTimeTicks ?? 0, mediaSourceId, request.AudioStreamIndex, request.SubtitleStreamIndex);
}
return ToOptimizedResult(info);
}
private async Task<PlaybackInfoResponse> GetPlaybackInfo(string id, string userId, string mediaSourceId = null, string liveStreamId = null)
private async Task<PlaybackInfoResponse> GetPlaybackInfo(string id, string userId, string[] supportedLiveMediaTypes, string mediaSourceId = null, string liveStreamId = null)
{
var result = new PlaybackInfoResponse();
@ -153,7 +162,7 @@ namespace MediaBrowser.Api.Playback
IEnumerable<MediaSourceInfo> mediaSources;
try
{
mediaSources = await _mediaSourceManager.GetPlayackMediaSources(id, userId, true, CancellationToken.None).ConfigureAwait(false);
mediaSources = await _mediaSourceManager.GetPlayackMediaSources(id, userId, true, supportedLiveMediaTypes, CancellationToken.None).ConfigureAwait(false);
}
catch (PlaybackException ex)
{
@ -223,7 +232,7 @@ namespace MediaBrowser.Api.Playback
int? subtitleStreamIndex,
string playSessionId)
{
var streamBuilder = new StreamBuilder();
var streamBuilder = new StreamBuilder(Logger);
var options = new VideoOptions
{
@ -231,8 +240,7 @@ namespace MediaBrowser.Api.Playback
Context = EncodingContext.Streaming,
DeviceId = auth.DeviceId,
ItemId = item.Id.ToString("N"),
Profile = profile,
MaxBitrate = maxBitrate
Profile = profile
};
if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
@ -248,6 +256,7 @@ namespace MediaBrowser.Api.Playback
// Dummy this up to fool StreamBuilder
mediaSource.SupportsDirectStream = true;
options.MaxBitrate = maxBitrate;
// The MediaSource supports direct stream, now test to see if the client supports it
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
@ -270,6 +279,8 @@ namespace MediaBrowser.Api.Playback
if (mediaSource.SupportsDirectStream)
{
options.MaxBitrate = GetMaxBitrate(maxBitrate);
// The MediaSource supports direct stream, now test to see if the client supports it
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
streamBuilder.BuildAudioItem(options) :
@ -288,6 +299,8 @@ namespace MediaBrowser.Api.Playback
if (mediaSource.SupportsTranscoding)
{
options.MaxBitrate = GetMaxBitrate(maxBitrate);
// The MediaSource supports direct stream, now test to see if the client supports it
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) ?
streamBuilder.BuildAudioItem(options) :
@ -309,6 +322,18 @@ namespace MediaBrowser.Api.Playback
}
}
private int? GetMaxBitrate(int? clientMaxBitrate)
{
var maxBitrate = clientMaxBitrate;
if (_config.Configuration.RemoteClientBitrateLimit > 0 && !_networkManager.IsInLocalNetwork(Request.RemoteIp))
{
maxBitrate = Math.Min(maxBitrate ?? _config.Configuration.RemoteClientBitrateLimit, _config.Configuration.RemoteClientBitrateLimit);
}
return maxBitrate;
}
private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
{
var profiles = info.GetSubtitleProfiles(false, "-", accessToken);

View File

@ -63,6 +63,13 @@ namespace MediaBrowser.Api.Playback.Progressive
new ProgressiveFileCopier(_fileSystem, _job)
.StreamFile(Path, responseStream);
}
catch (IOException)
{
// These error are always the same so don't dump the whole stack trace
Logger.Error("Error streaming media. The client has most likely disconnected or transcoding has failed.");
throw;
}
catch (Exception ex)
{
Logger.ErrorException("Error streaming media. The client has most likely disconnected or transcoding has failed.", ex);

View File

@ -5,7 +5,6 @@ using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO;
using ServiceStack;

View File

@ -70,7 +70,7 @@ namespace MediaBrowser.Api.Playback
}
}
private void UnpauseTranscoding()
public void UnpauseTranscoding()
{
if (_isPaused)
{

View File

@ -383,12 +383,12 @@ namespace MediaBrowser.Api.Session
if (!user.Policy.EnableRemoteControlOfOtherUsers)
{
result = result.Where(i => i.ContainsUser(request.ControllableByUserId.Value));
result = result.Where(i => !i.UserId.HasValue || i.ContainsUser(request.ControllableByUserId.Value));
}
if (!user.Policy.EnableSharedDeviceControl)
{
result = result.Where(i => !i.UserId.HasValue);
result = result.Where(i => i.UserId.HasValue);
}
result = result.Where(i =>

View File

@ -170,7 +170,7 @@ namespace MediaBrowser.Api
points += item1.Studios.Where(i => item2.Studios.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 3);
var item2PeopleNames = item2.People.Select(i => i.Name)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);
points += item1.People.Where(i => item2PeopleNames.ContainsKey(i.Name)).Sum(i =>

View File

@ -136,11 +136,11 @@ namespace MediaBrowser.Api.Subtitles
_providerManager = providerManager;
}
public object Get(GetSubtitlePlaylist request)
public async Task<object> Get(GetSubtitlePlaylist request)
{
var item = (Video)_libraryManager.GetItemById(new Guid(request.Id));
var mediaSource = _mediaSourceManager.GetStaticMediaSource(item, request.MediaSourceId, false);
var mediaSource = await _mediaSourceManager.GetMediaSource(item, request.MediaSourceId, false).ConfigureAwait(false);
var builder = new StringBuilder();

View File

@ -248,6 +248,9 @@ namespace MediaBrowser.Api.Sync
result.Targets = _syncManager.GetSyncTargets(request.UserId)
.ToList();
var auth = AuthorizationContext.GetAuthorizationInfo(Request);
var authenticatedUser = _userManager.GetUserById(auth.UserId);
if (!string.IsNullOrWhiteSpace(request.TargetId))
{
result.Targets = result.Targets
@ -255,11 +258,11 @@ namespace MediaBrowser.Api.Sync
.ToList();
result.QualityOptions = _syncManager
.GetQualityOptions(request.TargetId)
.GetQualityOptions(request.TargetId, authenticatedUser)
.ToList();
result.ProfileOptions = _syncManager
.GetProfileOptions(request.TargetId)
.GetProfileOptions(request.TargetId, authenticatedUser)
.ToList();
}
@ -277,10 +280,6 @@ namespace MediaBrowser.Api.Sync
}
};
var auth = AuthorizationContext.GetAuthorizationInfo(Request);
var authenticatedUser = _userManager.GetUserById(auth.UserId);
var items = request.ItemIds.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(_libraryManager.GetItemById)
.Where(i => i != null);

View File

@ -132,7 +132,7 @@ namespace MediaBrowser.Api.UserLibrary
.Where(i => !i.IsFolder)
.OfType<IHasAlbumArtist>()
.SelectMany(i => i.AlbumArtists)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.Select(name =>
{
try
@ -152,7 +152,7 @@ namespace MediaBrowser.Api.UserLibrary
.Where(i => !i.IsFolder)
.OfType<IHasArtist>()
.SelectMany(i => i.AllArtists)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.Select(name =>
{
try

View File

@ -142,7 +142,7 @@ namespace MediaBrowser.Api.UserLibrary
}
IEnumerable<Tuple<TItemType, List<BaseItem>>> tuples;
if (dtoOptions.Fields.Contains(ItemFields.ItemCounts) || true)
if (dtoOptions.Fields.Contains(ItemFields.ItemCounts))
{
tuples = ibnItems.Select(i => new Tuple<TItemType, List<BaseItem>>(i, i.GetTaggedItems(libraryItems).ToList()));
}
@ -177,7 +177,6 @@ namespace MediaBrowser.Api.UserLibrary
return true;
}
return true;
return options.Fields.Contains(ItemFields.ItemCounts);
}

View File

@ -105,7 +105,7 @@ namespace MediaBrowser.Api.UserLibrary
return itemsList
.SelectMany(i => i.Genres)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.Select(name => LibraryManager.GetGameGenre(name));
}
}

View File

@ -108,7 +108,7 @@ namespace MediaBrowser.Api.UserLibrary
{
return items
.SelectMany(i => i.Genres)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.Select(name =>
{
try

View File

@ -105,7 +105,7 @@ namespace MediaBrowser.Api.UserLibrary
return itemsList
.SelectMany(i => i.Genres)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.Select(name => LibraryManager.GetMusicGenre(name));
}
}

View File

@ -127,7 +127,7 @@ namespace MediaBrowser.Api.UserLibrary
return allPeople
.Select(i => i.Name)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.Select(name =>
{

View File

@ -114,6 +114,15 @@ namespace MediaBrowser.Api.UserLibrary
[ApiMember(Name = "SubtitleStreamIndex", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
public int? SubtitleStreamIndex { get; set; }
[ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public PlayMethod PlayMethod { get; set; }
[ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public string LiveStreamId { get; set; }
[ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public string PlaySessionId { get; set; }
}
/// <summary>
@ -160,6 +169,15 @@ namespace MediaBrowser.Api.UserLibrary
[ApiMember(Name = "VolumeLevel", Description = "Scale of 0-100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "POST")]
public int? VolumeLevel { get; set; }
[ApiMember(Name = "PlayMethod", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public PlayMethod PlayMethod { get; set; }
[ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public string LiveStreamId { get; set; }
[ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public string PlaySessionId { get; set; }
}
/// <summary>
@ -191,6 +209,12 @@ namespace MediaBrowser.Api.UserLibrary
/// <value>The position ticks.</value>
[ApiMember(Name = "PositionTicks", Description = "Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "DELETE")]
public long? PositionTicks { get; set; }
[ApiMember(Name = "LiveStreamId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public string LiveStreamId { get; set; }
[ApiMember(Name = "PlaySessionId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public string PlaySessionId { get; set; }
}
[Authenticated]
@ -260,7 +284,10 @@ namespace MediaBrowser.Api.UserLibrary
QueueableMediaTypes = queueableMediaTypes.Split(',').ToList(),
MediaSourceId = request.MediaSourceId,
AudioStreamIndex = request.AudioStreamIndex,
SubtitleStreamIndex = request.SubtitleStreamIndex
SubtitleStreamIndex = request.SubtitleStreamIndex,
PlayMethod = request.PlayMethod,
PlaySessionId = request.PlaySessionId,
LiveStreamId = request.LiveStreamId
});
}
@ -288,12 +315,20 @@ namespace MediaBrowser.Api.UserLibrary
MediaSourceId = request.MediaSourceId,
AudioStreamIndex = request.AudioStreamIndex,
SubtitleStreamIndex = request.SubtitleStreamIndex,
VolumeLevel = request.VolumeLevel
VolumeLevel = request.VolumeLevel,
PlayMethod = request.PlayMethod,
PlaySessionId = request.PlaySessionId,
LiveStreamId = request.LiveStreamId
});
}
public void Post(ReportPlaybackProgress request)
{
if (!string.IsNullOrWhiteSpace(request.PlaySessionId))
{
ApiEntryPoint.Instance.PingTranscodingJob(request.PlaySessionId);
}
request.SessionId = GetSession().Result.Id;
var task = _sessionManager.OnPlaybackProgress(request);
@ -311,12 +346,19 @@ namespace MediaBrowser.Api.UserLibrary
{
ItemId = request.Id,
PositionTicks = request.PositionTicks,
MediaSourceId = request.MediaSourceId
MediaSourceId = request.MediaSourceId,
PlaySessionId = request.PlaySessionId,
LiveStreamId = request.LiveStreamId
});
}
public void Post(ReportPlaybackStopped request)
{
if (!string.IsNullOrWhiteSpace(request.PlaySessionId))
{
ApiEntryPoint.Instance.KillTranscodingJobs(AuthorizationContext.GetAuthorizationInfo(Request).DeviceId, request.PlaySessionId, s => true);
}
request.SessionId = GetSession().Result.Id;
var task = _sessionManager.OnPlaybackStopped(request);

View File

@ -109,7 +109,7 @@ namespace MediaBrowser.Api.UserLibrary
return itemsList
.SelectMany(i => i.Studios)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.Select(name => LibraryManager.GetStudio(name));
}
}

View File

@ -101,12 +101,6 @@ namespace MediaBrowser.Common.Implementations
/// <value>The failed assemblies.</value>
public List<string> FailedAssemblies { get; protected set; }
/// <summary>
/// Gets all types within all running assemblies
/// </summary>
/// <value>All types.</value>
public Type[] AllTypes { get; protected set; }
/// <summary>
/// Gets all concrete types.
/// </summary>
@ -438,9 +432,10 @@ namespace MediaBrowser.Common.Implementations
Logger.Info("Loading {0}", assembly.FullName);
}
AllTypes = assemblies.SelectMany(GetTypes).ToArray();
AllConcreteTypes = AllTypes.Where(t => t.IsClass && !t.IsAbstract && !t.IsInterface && !t.IsGenericType).ToArray();
AllConcreteTypes = assemblies
.SelectMany(GetTypes)
.Where(t => t.IsClass && !t.IsAbstract && !t.IsInterface && !t.IsGenericType)
.ToArray();
}
/// <summary>

View File

@ -172,11 +172,11 @@ namespace MediaBrowser.Common.Implementations.Networking
Uri uri;
if (Uri.TryCreate(endpoint, UriKind.RelativeOrAbsolute, out uri))
{
var host = uri.DnsSafeHost;
Logger.Debug("Resolving host {0}", host);
try
{
var host = uri.DnsSafeHost;
Logger.Debug("Resolving host {0}", host);
address = GetIpAddresses(host).FirstOrDefault();
if (address != null)
@ -186,9 +186,13 @@ namespace MediaBrowser.Common.Implementations.Networking
return IsInLocalNetworkInternal(address.ToString(), false);
}
}
catch (InvalidOperationException)
{
// Can happen with reverse proxy or IIS url rewriting
}
catch (Exception ex)
{
Logger.ErrorException("Error resovling hostname {0}", ex, host);
Logger.ErrorException("Error resovling hostname", ex);
}
}
}

View File

@ -121,12 +121,12 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
{
if (_lastExecutionResult == null)
{
var path = GetHistoryFilePath();
lock (_lastExecutionResultSyncLock)
{
if (_lastExecutionResult == null)
{
var path = GetHistoryFilePath();
try
{
return JsonSerializer.DeserializeFromFile<TaskResult>(path);
@ -152,6 +152,14 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
private set
{
_lastExecutionResult = value;
var path = GetHistoryFilePath();
Directory.CreateDirectory(Path.GetDirectoryName(path));
lock (_lastExecutionResultSyncLock)
{
JsonSerializer.SerializeToFile(value, path);
}
}
}
@ -582,11 +590,6 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks
result.LongErrorMessage = ex.StackTrace;
}
var path = GetHistoryFilePath();
Directory.CreateDirectory(Path.GetDirectoryName(path));
JsonSerializer.SerializeToFile(result, path);
LastExecutionResult = result;
((TaskManager)TaskManager).OnTaskCompleted(this, result);

View File

@ -5,7 +5,6 @@ using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Users;
namespace MediaBrowser.Controller.Channels
{
@ -15,19 +14,9 @@ namespace MediaBrowser.Controller.Channels
public override bool IsVisible(User user)
{
if (user.Policy.BlockedChannels != null)
if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
{
if (user.Policy.BlockedChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
{
return false;
}
}
else
{
if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
{
return false;
}
return false;
}
return base.IsVisible(user);

View File

@ -1,4 +1,5 @@
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Model.Channels;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
@ -100,5 +101,10 @@ namespace MediaBrowser.Controller.Channels
{
return false;
}
public override bool IsVisibleStandalone(User user)
{
return IsVisibleStandaloneInternal(user, false) && ChannelVideoItem.IsChannelVisible(this, user);
}
}
}

View File

@ -80,5 +80,10 @@ namespace MediaBrowser.Controller.Channels
{
return false;
}
public override bool IsVisibleStandalone(User user)
{
return IsVisibleStandaloneInternal(user, false) && ChannelVideoItem.IsChannelVisible(this, user);
}
}
}

View File

@ -130,5 +130,17 @@ namespace MediaBrowser.Controller.Channels
{
return false;
}
public override bool IsVisibleStandalone(User user)
{
return IsVisibleStandaloneInternal(user, false) && IsChannelVisible(this, user);
}
internal static bool IsChannelVisible(IChannelItem item, User user)
{
var channel = ChannelManager.GetChannel(item.ChannelId);
return channel.IsVisible(user);
}
}
}

View File

@ -13,6 +13,12 @@ namespace MediaBrowser.Controller.Drawing
/// </summary>
public interface IImageProcessor
{
/// <summary>
/// Gets the supported input formats.
/// </summary>
/// <value>The supported input formats.</value>
string[] SupportedInputFormats { get; }
/// <summary>
/// Gets the image enhancers.
/// </summary>
@ -93,5 +99,11 @@ namespace MediaBrowser.Controller.Drawing
/// </summary>
/// <returns>ImageOutputFormat[].</returns>
ImageFormat[] GetSupportedImageOutputFormats();
/// <summary>
/// Creates the image collage.
/// </summary>
/// <param name="options">The options.</param>
void CreateImageCollage(ImageCollageOptions options);
}
}

View 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; }
}
}

View File

@ -35,6 +35,14 @@ namespace MediaBrowser.Controller.Dto
/// <returns>Task{BaseItemDto}.</returns>
BaseItemDto GetBaseItemDto(BaseItem item, List<ItemFields> fields, User user = null, BaseItem owner = null);
/// <summary>
/// Fills the synchronize information.
/// </summary>
/// <param name="dtos">The dtos.</param>
/// <param name="options">The options.</param>
/// <param name="user">The user.</param>
void FillSyncInfo(IEnumerable<IHasSyncInfo> dtos, DtoOptions options, User user);
/// <summary>
/// Gets the base item dto.
/// </summary>

View File

@ -1,6 +1,5 @@
using System;
using MediaBrowser.Controller.Library;
using System.Collections.Generic;
using System.Linq;
namespace MediaBrowser.Controller.Entities.Audio
{
@ -20,11 +19,11 @@ namespace MediaBrowser.Controller.Entities.Audio
{
public static bool HasArtist(this IHasArtist hasArtist, string artist)
{
return hasArtist.Artists.Contains(artist, StringComparer.OrdinalIgnoreCase);
return NameExtensions.EqualsAny(hasArtist.Artists, artist);
}
public static bool HasAnyArtist(this IHasArtist hasArtist, string artist)
{
return hasArtist.AllArtists.Contains(artist, StringComparer.OrdinalIgnoreCase);
return NameExtensions.EqualsAny(hasArtist.AllArtists, artist);
}
}
}

View File

@ -1,4 +1,5 @@
using MediaBrowser.Common.Extensions;
using System.Globalization;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Collections;
@ -44,7 +45,7 @@ namespace MediaBrowser.Controller.Entities
/// <summary>
/// The supported image extensions
/// </summary>
public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg", ".tbn" };
public static readonly string[] SupportedImageExtensions = { ".png", ".jpg", ".jpeg" };
public static readonly List<string> SupportedImageExtensionsList = SupportedImageExtensions.ToList();
@ -1143,6 +1144,11 @@ namespace MediaBrowser.Controller.Entities
}
public virtual bool IsVisibleStandalone(User user)
{
return IsVisibleStandaloneInternal(user, true);
}
protected bool IsVisibleStandaloneInternal(User user, bool checkFolders)
{
if (!IsVisible(user))
{
@ -1154,7 +1160,23 @@ namespace MediaBrowser.Controller.Entities
return false;
}
// TODO: Need some work here, e.g. is in user library, for channels, can user access channel, etc.
if (checkFolders)
{
var topParent = Parents.LastOrDefault() ?? this;
if (string.IsNullOrWhiteSpace(topParent.Path))
{
return true;
}
var userCollectionFolders = user.RootFolder.GetChildren(user, true).Select(i => i.Id).ToList();
var itemCollectionFolders = LibraryManager.GetCollectionFolders(this).Select(i => i.Id);
if (!itemCollectionFolders.Any(userCollectionFolders.Contains))
{
return false;
}
}
return true;
}
@ -1219,18 +1241,6 @@ namespace MediaBrowser.Controller.Entities
private BaseItem FindLinkedChild(LinkedChild info)
{
if (!string.IsNullOrWhiteSpace(info.ItemName))
{
if (string.Equals(info.ItemType, "musicgenre", StringComparison.OrdinalIgnoreCase))
{
return LibraryManager.GetMusicGenre(info.ItemName);
}
if (string.Equals(info.ItemType, "musicartist", StringComparison.OrdinalIgnoreCase))
{
return LibraryManager.GetArtist(info.ItemName);
}
}
if (!string.IsNullOrEmpty(info.Path))
{
var itemByPath = LibraryManager.RootFolder.FindByPath(info.Path);
@ -1243,23 +1253,6 @@ namespace MediaBrowser.Controller.Entities
return itemByPath;
}
if (!string.IsNullOrWhiteSpace(info.ItemName) && !string.IsNullOrWhiteSpace(info.ItemType))
{
return LibraryManager.RootFolder.GetRecursiveChildren(i =>
{
if (string.Equals(i.Name, info.ItemName, StringComparison.OrdinalIgnoreCase))
{
if (string.Equals(i.GetType().Name, info.ItemType, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}).FirstOrDefault();
}
return null;
}
@ -1540,7 +1533,7 @@ namespace MediaBrowser.Controller.Entities
}
// Remove it from the item
ImageInfos.Remove(info);
RemoveImage(info);
// Delete the source file
var currentFile = new FileInfo(info.Path);
@ -1559,6 +1552,11 @@ namespace MediaBrowser.Controller.Entities
return UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
}
public void RemoveImage(ItemImageInfo image)
{
ImageInfos.Remove(image);
}
public virtual Task UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken)
{
return LibraryManager.UpdateItem(this, updateReason, cancellationToken);
@ -1651,7 +1649,7 @@ namespace MediaBrowser.Controller.Entities
public bool AddImages(ImageType imageType, IEnumerable<FileInfo> images)
{
return AddImages(imageType, images.Cast<FileSystemInfo>());
return AddImages(imageType, images.Cast<FileSystemInfo>().ToList());
}
/// <summary>
@ -1661,7 +1659,7 @@ namespace MediaBrowser.Controller.Entities
/// <param name="images">The images.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
/// <exception cref="System.ArgumentException">Cannot call AddImages with chapter images</exception>
public bool AddImages(ImageType imageType, IEnumerable<FileSystemInfo> images)
public bool AddImages(ImageType imageType, List<FileSystemInfo> images)
{
if (imageType == ImageType.Chapter)
{
@ -1672,6 +1670,7 @@ namespace MediaBrowser.Controller.Entities
.ToList();
var newImageList = new List<FileSystemInfo>();
var imageAdded = false;
foreach (var newImage in images)
{
@ -1686,14 +1685,26 @@ namespace MediaBrowser.Controller.Entities
if (existing == null)
{
newImageList.Add(newImage);
imageAdded = true;
}
else
{
existing.DateModified = FileSystem.GetLastWriteTimeUtc(newImage);
existing.Length = ((FileInfo) newImage).Length;
existing.Length = ((FileInfo)newImage).Length;
}
}
if (imageAdded || images.Count != existingImages.Count)
{
var newImagePaths = images.Select(i => i.FullName).ToList();
var deleted = existingImages
.Where(i => !newImagePaths.Contains(i.Path, StringComparer.OrdinalIgnoreCase) && !File.Exists(i.Path))
.ToList();
ImageInfos = ImageInfos.Except(deleted).ToList();
}
ImageInfos.AddRange(newImageList.Select(i => GetImageInfo(i, imageType)));
return newImageList.Count > 0;
@ -1882,5 +1893,18 @@ namespace MediaBrowser.Controller.Entities
return video.RefreshMetadata(newOptions, cancellationToken);
}
public string GetEtag()
{
return string.Join("|", GetEtagValues().ToArray()).GetMD5().ToString("N");
}
protected virtual List<string> GetEtagValues()
{
return new List<string>
{
DateLastSaved.Ticks.ToString(CultureInfo.InvariantCulture)
};
}
}
}

View File

@ -334,22 +334,9 @@ namespace MediaBrowser.Controller.Entities
{
if (this is ICollectionFolder && !(this is BasePluginFolder))
{
if (user.Policy.BlockedMediaFolders != null)
if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
{
if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase) ||
// Backwards compatibility
user.Policy.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase))
{
return false;
}
}
else
{
if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase))
{
return false;
}
return false;
}
}
@ -1004,8 +991,9 @@ namespace MediaBrowser.Controller.Entities
}
var locations = user.RootFolder
.GetChildren(user, true)
.Children
.OfType<CollectionFolder>()
.Where(i => i.IsVisible(user))
.SelectMany(i => i.PhysicalLocations)
.ToList();

View File

@ -141,7 +141,7 @@ namespace MediaBrowser.Controller.Entities
/// <param name="imageType">Type of the image.</param>
/// <param name="images">The images.</param>
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
bool AddImages(ImageType imageType, IEnumerable<FileSystemInfo> images);
bool AddImages(ImageType imageType, List<FileSystemInfo> images);
/// <summary>
/// Determines whether [is save local metadata enabled].
@ -190,6 +190,12 @@ namespace MediaBrowser.Controller.Entities
/// </summary>
/// <returns><c>true</c> if [is internet metadata enabled]; otherwise, <c>false</c>.</returns>
bool IsInternetMetadataEnabled();
/// <summary>
/// Removes the image.
/// </summary>
/// <param name="image">The image.</param>
void RemoveImage(ItemImageInfo image);
}
public static class HasImagesExtensions

View File

@ -9,9 +9,6 @@ namespace MediaBrowser.Controller.Entities
public string Path { get; set; }
public LinkedChildType Type { get; set; }
public string ItemName { get; set; }
public string ItemType { get; set; }
[IgnoreDataMember]
public string Id { get; set; }

View File

@ -175,17 +175,17 @@ namespace MediaBrowser.Controller.Entities.Movies
public override bool IsVisible(User user)
{
var userId = user.Id.ToString("N");
// Need to check Count > 0 for boxsets created prior to the introduction of Shares
if (Shares.Count > 0 && Shares.Any(i => string.Equals(userId, i.UserId, StringComparison.OrdinalIgnoreCase)))
{
return true;
}
if (base.IsVisible(user))
{
var userId = user.Id.ToString("N");
// Need to check Count > 0 for boxsets created prior to the introduction of Shares
if (Shares.Count > 0 && !Shares.Any(i => string.Equals(userId, i.UserId, StringComparison.OrdinalIgnoreCase)))
{
//return false;
}
return true;
return GetChildren(user, true).Any();
}
return false;

View File

@ -1,11 +1,15 @@
using MediaBrowser.Model.Configuration;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Users;
using System;
using System.Linq;
using System.Runtime.Serialization;
using MediaBrowser.Model.Users;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Controller.Entities
{
public class PhotoAlbum : Folder
public class PhotoAlbum : Folder, IMetadataContainer
{
public override bool SupportsLocalMetadata
{
@ -28,5 +32,31 @@ namespace MediaBrowser.Controller.Entities
{
return config.BlockUnratedItems.Contains(UnratedItem.Other);
}
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
{
var items = GetRecursiveChildren().ToList();
var totalItems = items.Count;
var numComplete = 0;
// Refresh songs
foreach (var item in items)
{
cancellationToken.ThrowIfCancellationRequested();
await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
numComplete++;
double percent = numComplete;
percent /= totalItems;
progress.Report(percent * 100);
}
// Refresh current item
await RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
progress.Report(100);
}
}
}

View File

@ -50,6 +50,16 @@ namespace MediaBrowser.Controller.Entities
{
var user = query.User;
if (query.IncludeItemTypes != null &&
query.IncludeItemTypes.Length == 1 &&
string.Equals(query.IncludeItemTypes[0], "Playlist", StringComparison.OrdinalIgnoreCase))
{
if (!string.Equals(viewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
{
return await FindPlaylists(queryParent, user, query).ConfigureAwait(false);
}
}
switch (viewType)
{
case CollectionType.Channels:
@ -107,9 +117,7 @@ namespace MediaBrowser.Controller.Entities
case CollectionType.LiveTv:
{
var result = await GetLiveTvFolders(user).ConfigureAwait(false);
return GetResult(result, queryParent, query);
return await GetLiveTvView(queryParent, user, query).ConfigureAwait(false);
}
case CollectionType.Books:
@ -205,6 +213,9 @@ namespace MediaBrowser.Controller.Entities
case SpecialFolder.MusicLatest:
return GetMusicLatest(queryParent, user, query);
case SpecialFolder.MusicPlaylists:
return await GetMusicPlaylists(queryParent, user, query).ConfigureAwait(false);
case SpecialFolder.MusicAlbums:
return GetMusicAlbums(queryParent, user, query);
@ -240,6 +251,16 @@ namespace MediaBrowser.Controller.Entities
}
}
private async Task<QueryResult<BaseItem>> FindPlaylists(Folder parent, User user, InternalItemsQuery query)
{
var collectionFolders = user.RootFolder.GetChildren(user, true).Select(i => i.Id).ToList();
var list = _playlistManager.GetPlaylists(user.Id.ToString("N"))
.Where(i => i.GetChildren(user, true).Any(media => _libraryManager.GetCollectionFolders(media).Select(c => c.Id).Any(collectionFolders.Contains)));
return GetResult(list, parent, query);
}
private int GetSpecialItemsLimit()
{
return 50;
@ -257,12 +278,13 @@ namespace MediaBrowser.Controller.Entities
var list = new List<BaseItem>();
list.Add(await GetUserView(SpecialFolder.MusicLatest, user, "0", parent).ConfigureAwait(false));
list.Add(await GetUserView(SpecialFolder.MusicAlbums, user, "1", parent).ConfigureAwait(false));
list.Add(await GetUserView(SpecialFolder.MusicAlbumArtists, user, "2", parent).ConfigureAwait(false));
list.Add(await GetUserView(SpecialFolder.MusicArtists, user, "3", parent).ConfigureAwait(false));
list.Add(await GetUserView(SpecialFolder.MusicSongs, user, "4", parent).ConfigureAwait(false));
list.Add(await GetUserView(SpecialFolder.MusicGenres, user, "5", parent).ConfigureAwait(false));
list.Add(await GetUserView(SpecialFolder.MusicFavorites, user, "6", parent).ConfigureAwait(false));
list.Add(await GetUserView(SpecialFolder.MusicPlaylists, user, "1", parent).ConfigureAwait(false));
list.Add(await GetUserView(SpecialFolder.MusicAlbums, user, "2", parent).ConfigureAwait(false));
list.Add(await GetUserView(SpecialFolder.MusicAlbumArtists, user, "3", parent).ConfigureAwait(false));
//list.Add(await GetUserView(SpecialFolder.MusicArtists, user, "4", parent).ConfigureAwait(false));
list.Add(await GetUserView(SpecialFolder.MusicSongs, user, "5", parent).ConfigureAwait(false));
list.Add(await GetUserView(SpecialFolder.MusicGenres, user, "6", parent).ConfigureAwait(false));
list.Add(await GetUserView(SpecialFolder.MusicFavorites, user, "7", parent).ConfigureAwait(false));
return GetResult(list, parent, query);
}
@ -283,7 +305,7 @@ namespace MediaBrowser.Controller.Entities
var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Music, CollectionType.MusicVideos })
.Where(i => !i.IsFolder)
.SelectMany(i => i.Genres)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.Select(i =>
{
try
@ -313,7 +335,7 @@ namespace MediaBrowser.Controller.Entities
.Where(i => i.Genres.Contains(displayParent.Name, StringComparer.OrdinalIgnoreCase))
.OfType<IHasAlbumArtist>()
.SelectMany(i => i.AlbumArtists)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.Select(i =>
{
try
@ -337,7 +359,7 @@ namespace MediaBrowser.Controller.Entities
.Where(i => !i.IsFolder)
.OfType<IHasAlbumArtist>()
.SelectMany(i => i.AlbumArtists)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.Select(i =>
{
try
@ -361,7 +383,7 @@ namespace MediaBrowser.Controller.Entities
.Where(i => !i.IsFolder)
.OfType<IHasArtist>()
.SelectMany(i => i.Artists)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.Select(i =>
{
try
@ -385,7 +407,7 @@ namespace MediaBrowser.Controller.Entities
.Where(i => !i.IsFolder)
.OfType<IHasAlbumArtist>()
.SelectMany(i => i.AlbumArtists)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.Select(i =>
{
try
@ -403,6 +425,14 @@ namespace MediaBrowser.Controller.Entities
return GetResult(artists, parent, query);
}
private Task<QueryResult<BaseItem>> GetMusicPlaylists(Folder parent, User user, InternalItemsQuery query)
{
query.IncludeItemTypes = new[] { "Playlist" };
query.Recursive = true;
return parent.GetItems(query);
}
private QueryResult<BaseItem> GetMusicAlbums(Folder parent, User user, InternalItemsQuery query)
{
var items = GetRecursiveChildren(parent, user, new[] { CollectionType.Music, CollectionType.MusicVideos }, i => (i is MusicAlbum) && FilterItem(i, query));
@ -552,7 +582,7 @@ namespace MediaBrowser.Controller.Entities
var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Movies, CollectionType.BoxSets, string.Empty })
.Where(i => i is Movie)
.SelectMany(i => i.Genres)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.Select(i =>
{
try
@ -724,7 +754,7 @@ namespace MediaBrowser.Controller.Entities
var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.TvShows, string.Empty })
.OfType<Series>()
.SelectMany(i => i.Genres)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.Select(i =>
{
try
@ -776,7 +806,7 @@ namespace MediaBrowser.Controller.Entities
var tasks = GetRecursiveChildren(parent, user, new[] { CollectionType.Games })
.OfType<Game>()
.SelectMany(i => i.Genres)
.Distinct(StringComparer.OrdinalIgnoreCase)
.DistinctNames()
.Select(i =>
{
try
@ -1749,17 +1779,26 @@ namespace MediaBrowser.Controller.Entities
return parent.GetRecursiveChildren(user, filter);
}
private async Task<IEnumerable<BaseItem>> GetLiveTvFolders(User user)
private async Task<QueryResult<BaseItem>> GetLiveTvView(Folder queryParent, User user, InternalItemsQuery query)
{
if (query.Recursive)
{
return await _liveTvManager.GetInternalRecordings(new RecordingQuery
{
IsInProgress = false,
Status = RecordingStatus.Completed,
UserId = user.Id.ToString("N")
}, CancellationToken.None).ConfigureAwait(false);
}
var list = new List<BaseItem>();
var parent = user.RootFolder;
//list.Add(await GetUserSubView(SpecialFolder.LiveTvNowPlaying, user, "0", parent).ConfigureAwait(false));
list.Add(await GetUserView(SpecialFolder.LiveTvChannels, user, string.Empty, parent).ConfigureAwait(false));
list.Add(await GetUserView(SpecialFolder.LiveTvRecordingGroups, user, string.Empty, parent).ConfigureAwait(false));
list.Add(await GetUserView(SpecialFolder.LiveTvChannels, user, string.Empty, user.RootFolder).ConfigureAwait(false));
list.Add(await GetUserView(SpecialFolder.LiveTvRecordingGroups, user, string.Empty, user.RootFolder).ConfigureAwait(false));
return list;
return GetResult(list, queryParent, query);
}
private async Task<UserView> GetUserView(string name, string type, User user, string sortName, BaseItem parent)

View File

@ -3,7 +3,7 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Server.Implementations.HttpServer
namespace MediaBrowser.Controller.IO
{
/// <summary>
/// Class for streaming data with throttling support.
@ -15,8 +15,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
/// </summary>
public const long Infinite = 0;
public Func<long, long, long> ThrottleCallback { get; set; }
#region Private members
/// <summary>
/// The base stream.
@ -293,16 +291,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
return false;
}
if (ThrottleCallback != null)
{
var val = ThrottleCallback(_maximumBytesPerSecond, _bytesWritten);
if (val == 0)
{
return false;
}
}
return true;
}

View File

@ -43,18 +43,10 @@ namespace MediaBrowser.Controller.Library
/// <param name="id">The identifier.</param>
/// <param name="userId">The user identifier.</param>
/// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
/// <param name="supportedLiveMediaTypes">The supported live media types.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>IEnumerable&lt;MediaSourceInfo&gt;.</returns>
Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, CancellationToken cancellationToken);
/// <summary>
/// Gets the playack media sources.
/// </summary>
/// <param name="id">The identifier.</param>
/// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task&lt;IEnumerable&lt;MediaSourceInfo&gt;&gt;.</returns>
Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, bool enablePathSubstitution, CancellationToken cancellationToken);
Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, string[] supportedLiveMediaTypes, CancellationToken cancellationToken);
/// <summary>
/// Gets the static media sources.
@ -63,16 +55,8 @@ namespace MediaBrowser.Controller.Library
/// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
/// <param name="user">The user.</param>
/// <returns>IEnumerable&lt;MediaSourceInfo&gt;.</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&lt;MediaSourceInfo&gt;.</returns>
IEnumerable<MediaSourceInfo> GetStaticMediaSources(IHasMediaSources item, bool enablePathSubstitution);
/// <summary>
/// Gets the static media source.
/// </summary>
@ -80,7 +64,7 @@ namespace MediaBrowser.Controller.Library
/// <param name="mediaSourceId">The media source identifier.</param>
/// <param name="enablePathSubstitution">if set to <c>true</c> [enable path substitution].</param>
/// <returns>MediaSourceInfo.</returns>
MediaSourceInfo GetStaticMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution);
Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution);
/// <summary>
/// Opens the media source.

View 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);
}
}
}

View File

@ -1,4 +1,5 @@
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.LiveTv;
@ -74,10 +75,11 @@ namespace MediaBrowser.Controller.LiveTv
/// Gets the recording.
/// </summary>
/// <param name="id">The identifier.</param>
/// <param name="user">The user.</param>
/// <param name="options">The options.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="user">The user.</param>
/// <returns>Task{RecordingInfoDto}.</returns>
Task<RecordingInfoDto> GetRecording(string id, CancellationToken cancellationToken, User user = null);
Task<RecordingInfoDto> GetRecording(string id, DtoOptions options, CancellationToken cancellationToken, User user = null);
/// <summary>
/// Gets the channel.
@ -103,14 +105,15 @@ namespace MediaBrowser.Controller.LiveTv
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{TimerInfoDto}.</returns>
Task<SeriesTimerInfoDto> GetSeriesTimer(string id, CancellationToken cancellationToken);
/// <summary>
/// Gets the recordings.
/// </summary>
/// <param name="query">The query.</param>
/// <param name="options">The options.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>QueryResult{RecordingInfoDto}.</returns>
Task<QueryResult<RecordingInfoDto>> GetRecordings(RecordingQuery query, CancellationToken cancellationToken);
Task<QueryResult<RecordingInfoDto>> GetRecordings(RecordingQuery query, DtoOptions options, CancellationToken cancellationToken);
/// <summary>
/// Gets the timers.

View File

@ -1,10 +1,9 @@
using System;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Dto;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.Dto;
namespace MediaBrowser.Controller.LiveTv
{

View File

@ -52,6 +52,10 @@
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\morelinq.1.1.0\lib\net35\MoreLinq.dll</HintPath>
</Reference>
<Reference Include="Patterns.IO, Version=1.0.5580.36861, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\Patterns.IO.1.0.0.3\lib\portable-net45+sl4+wp71+win8+wpa81\Patterns.IO.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
@ -115,6 +119,7 @@
<Compile Include="Dlna\IMediaReceiverRegistrar.cs" />
<Compile Include="Dlna\IUpnpService.cs" />
<Compile Include="Drawing\IImageProcessor.cs" />
<Compile Include="Drawing\ImageCollageOptions.cs" />
<Compile Include="Drawing\ImageProcessingOptions.cs" />
<Compile Include="Drawing\ImageProcessorExtensions.cs" />
<Compile Include="Drawing\ImageStream.cs" />
@ -171,6 +176,7 @@
<Compile Include="Entities\UserView.cs" />
<Compile Include="Entities\UserViewBuilder.cs" />
<Compile Include="FileOrganization\IFileOrganizationService.cs" />
<Compile Include="IO\ThrottledStream.cs" />
<Compile Include="Library\DeleteOptions.cs" />
<Compile Include="Library\ILibraryPostScanTask.cs" />
<Compile Include="Library\IMediaSourceManager.cs" />
@ -184,6 +190,7 @@
<Compile Include="Library\IUserViewManager.cs" />
<Compile Include="Library\LibraryManagerExtensions.cs" />
<Compile Include="Library\MetadataConfigurationStore.cs" />
<Compile Include="Library\NameExtensions.cs" />
<Compile Include="Library\PlaybackStopEventArgs.cs" />
<Compile Include="Library\UserDataSaveEventArgs.cs" />
<Compile Include="LiveTv\ILiveTvItem.cs" />
@ -211,8 +218,8 @@
<Compile Include="MediaEncoding\IEncodingManager.cs" />
<Compile Include="MediaEncoding\ImageEncodingOptions.cs" />
<Compile Include="MediaEncoding\IMediaEncoder.cs" />
<Compile Include="MediaEncoding\InternalMediaInfoResult.cs" />
<Compile Include="MediaEncoding\ISubtitleEncoder.cs" />
<Compile Include="MediaEncoding\MediaInfoRequest.cs" />
<Compile Include="MediaEncoding\MediaStreamSelector.cs" />
<Compile Include="Net\AuthenticatedAttribute.cs" />
<Compile Include="Net\AuthorizationInfo.cs" />
@ -394,6 +401,7 @@
<Compile Include="Subtitles\SubtitleResponse.cs" />
<Compile Include="Subtitles\SubtitleSearchRequest.cs" />
<Compile Include="Sync\IHasDynamicAccess.cs" />
<Compile Include="Sync\IRemoteSyncProvider.cs" />
<Compile Include="Sync\IServerSyncProvider.cs" />
<Compile Include="Sync\ISyncDataProvider.cs" />
<Compile Include="Sync\ISyncManager.cs" />

View File

@ -41,6 +41,8 @@ namespace MediaBrowser.Controller.MediaEncoding
public int? SubtitleStreamIndex { get; set; }
public int? MaxRefFrames { get; set; }
public int? MaxVideoBitDepth { get; set; }
public int? CpuCoreLimit { get; set; }
public bool ReadInputAtNativeFramerate { get; set; }
public SubtitleDeliveryMethod SubtitleMethod { get; set; }
/// <summary>

View File

@ -63,16 +63,14 @@ namespace MediaBrowser.Controller.MediaEncoding
string filenamePrefix,
int? maxWidth,
CancellationToken cancellationToken);
/// <summary>
/// Gets the media info.
/// </summary>
/// <param name="inputFiles">The input files.</param>
/// <param name="protocol">The protocol.</param>
/// <param name="isAudio">if set to <c>true</c> [is audio].</param>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task<InternalMediaInfoResult> GetMediaInfo(string[] inputFiles, MediaProtocol protocol, bool isAudio, CancellationToken cancellationToken);
Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken);
/// <summary>
/// Gets the probe size argument.

View File

@ -1,9 +1,7 @@
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
@ -46,291 +44,5 @@ namespace MediaBrowser.Controller.MediaEncoding
.Where(f => !string.IsNullOrEmpty(f))
.ToList();
}
public static MediaInfo GetMediaInfo(InternalMediaInfoResult data)
{
var internalStreams = data.streams ?? new MediaStreamInfo[] { };
var info = new MediaInfo
{
MediaStreams = internalStreams.Select(s => GetMediaStream(s, data.format))
.Where(i => i != null)
.ToList()
};
if (data.format != null)
{
info.Format = data.format.format_name;
if (!string.IsNullOrEmpty(data.format.bit_rate))
{
info.TotalBitrate = int.Parse(data.format.bit_rate, UsCulture);
}
}
return info;
}
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
/// <summary>
/// Converts ffprobe stream info to our MediaStream class
/// </summary>
/// <param name="streamInfo">The stream info.</param>
/// <param name="formatInfo">The format info.</param>
/// <returns>MediaStream.</returns>
private static MediaStream GetMediaStream(MediaStreamInfo streamInfo, MediaFormatInfo formatInfo)
{
var stream = new MediaStream
{
Codec = streamInfo.codec_name,
Profile = streamInfo.profile,
Level = streamInfo.level,
Index = streamInfo.index,
PixelFormat = streamInfo.pix_fmt
};
if (streamInfo.tags != null)
{
stream.Language = GetDictionaryValue(streamInfo.tags, "language");
}
if (string.Equals(streamInfo.codec_type, "audio", StringComparison.OrdinalIgnoreCase))
{
stream.Type = MediaStreamType.Audio;
stream.Channels = streamInfo.channels;
if (!string.IsNullOrEmpty(streamInfo.sample_rate))
{
stream.SampleRate = int.Parse(streamInfo.sample_rate, UsCulture);
}
stream.ChannelLayout = ParseChannelLayout(streamInfo.channel_layout);
}
else if (string.Equals(streamInfo.codec_type, "subtitle", StringComparison.OrdinalIgnoreCase))
{
stream.Type = MediaStreamType.Subtitle;
}
else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase))
{
stream.Type = (streamInfo.codec_name ?? string.Empty).IndexOf("mjpeg", StringComparison.OrdinalIgnoreCase) != -1
? MediaStreamType.EmbeddedImage
: MediaStreamType.Video;
stream.Width = streamInfo.width;
stream.Height = streamInfo.height;
stream.AspectRatio = GetAspectRatio(streamInfo);
stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate);
stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate);
stream.BitDepth = GetBitDepth(stream.PixelFormat);
//stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) ||
// string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) ||
// string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase);
stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase);
}
else
{
return null;
}
// Get stream bitrate
var bitrate = 0;
if (!string.IsNullOrEmpty(streamInfo.bit_rate))
{
bitrate = int.Parse(streamInfo.bit_rate, UsCulture);
}
else if (formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate) && stream.Type == MediaStreamType.Video)
{
// If the stream info doesn't have a bitrate get the value from the media format info
bitrate = int.Parse(formatInfo.bit_rate, UsCulture);
}
if (bitrate > 0)
{
stream.BitRate = bitrate;
}
if (streamInfo.disposition != null)
{
var isDefault = GetDictionaryValue(streamInfo.disposition, "default");
var isForced = GetDictionaryValue(streamInfo.disposition, "forced");
stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase);
stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase);
}
return stream;
}
private static int? GetBitDepth(string pixelFormat)
{
var eightBit = new List<string>
{
"yuv420p",
"yuv411p",
"yuvj420p",
"uyyvyy411",
"nv12",
"nv21",
"rgb444le",
"rgb444be",
"bgr444le",
"bgr444be",
"yuvj411p"
};
if (!string.IsNullOrEmpty(pixelFormat))
{
if (eightBit.Contains(pixelFormat, StringComparer.OrdinalIgnoreCase))
{
return 8;
}
}
return null;
}
/// <summary>
/// Gets a string from an FFProbeResult tags dictionary
/// </summary>
/// <param name="tags">The tags.</param>
/// <param name="key">The key.</param>
/// <returns>System.String.</returns>
private static string GetDictionaryValue(Dictionary<string, string> tags, string key)
{
if (tags == null)
{
return null;
}
string val;
tags.TryGetValue(key, out val);
return val;
}
private static string ParseChannelLayout(string input)
{
if (string.IsNullOrEmpty(input))
{
return input;
}
return input.Split('(').FirstOrDefault();
}
private static string GetAspectRatio(MediaStreamInfo info)
{
var original = info.display_aspect_ratio;
int height;
int width;
var parts = (original ?? string.Empty).Split(':');
if (!(parts.Length == 2 &&
int.TryParse(parts[0], NumberStyles.Any, UsCulture, out width) &&
int.TryParse(parts[1], NumberStyles.Any, UsCulture, out height) &&
width > 0 &&
height > 0))
{
width = info.width;
height = info.height;
}
if (width > 0 && height > 0)
{
double ratio = width;
ratio /= height;
if (IsClose(ratio, 1.777777778, .03))
{
return "16:9";
}
if (IsClose(ratio, 1.3333333333, .05))
{
return "4:3";
}
if (IsClose(ratio, 1.41))
{
return "1.41:1";
}
if (IsClose(ratio, 1.5))
{
return "1.5:1";
}
if (IsClose(ratio, 1.6))
{
return "1.6:1";
}
if (IsClose(ratio, 1.66666666667))
{
return "5:3";
}
if (IsClose(ratio, 1.85, .02))
{
return "1.85:1";
}
if (IsClose(ratio, 2.35, .025))
{
return "2.35:1";
}
if (IsClose(ratio, 2.4, .025))
{
return "2.40:1";
}
}
return original;
}
private static bool IsClose(double d1, double d2, double variance = .005)
{
return Math.Abs(d1 - d2) <= variance;
}
/// <summary>
/// Gets a frame rate from a string value in ffprobe output
/// This could be a number or in the format of 2997/125.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>System.Nullable{System.Single}.</returns>
private static float? GetFrameRate(string value)
{
if (!string.IsNullOrEmpty(value))
{
var parts = value.Split('/');
float result;
if (parts.Length == 2)
{
result = float.Parse(parts[0], UsCulture) / float.Parse(parts[1], UsCulture);
}
else
{
result = float.Parse(parts[0], UsCulture);
}
return float.IsNaN(result) ? (float?)null : result;
}
return null;
}
}
}

View 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>();
}
}
}

View File

@ -1404,24 +1404,12 @@ namespace MediaBrowser.Controller.Providers
{
switch (reader.Name)
{
case "Name":
{
linkedItem.ItemName = reader.ReadElementContentAsString();
break;
}
case "Path":
{
linkedItem.Path = reader.ReadElementContentAsString();
break;
}
case "Type":
{
linkedItem.ItemType = reader.ReadElementContentAsString();
break;
}
default:
reader.Skip();
break;
@ -1435,7 +1423,7 @@ namespace MediaBrowser.Controller.Providers
return linkedItem;
}
return string.IsNullOrWhiteSpace(linkedItem.ItemName) || string.IsNullOrWhiteSpace(linkedItem.ItemType) ? null : linkedItem;
return null;
}

View File

@ -1,5 +1,4 @@
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
using System.Threading.Tasks;

View File

@ -78,6 +78,19 @@ namespace MediaBrowser.Controller.Providers
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task SaveImage(IHasImages item, Stream source, string mimeType, ImageType type, int? imageIndex, string internalCacheKey, CancellationToken cancellationToken);
/// <summary>
/// Saves the image.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="source">The source.</param>
/// <param name="mimeType">Type of the MIME.</param>
/// <param name="type">The type.</param>
/// <param name="imageIndex">Index of the image.</param>
/// <param name="internalCacheKey">The internal cache key.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task SaveImage(IHasImages item, string source, string mimeType, ImageType type, int? imageIndex, string internalCacheKey, CancellationToken cancellationToken);
/// <summary>
/// Adds the metadata providers.

View File

@ -9,10 +9,10 @@ namespace MediaBrowser.Controller.Sync
/// <summary>
/// Gets the synced file information.
/// </summary>
/// <param name="remotePath">The remote path.</param>
/// <param name="id">The identifier.</param>
/// <param name="target">The target.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task&lt;SyncedFileInfo&gt;.</returns>
Task<SyncedFileInfo> GetSyncedFileInfo(string remotePath, SyncTarget target, CancellationToken cancellationToken);
Task<SyncedFileInfo> GetSyncedFileInfo(string id, SyncTarget target, CancellationToken cancellationToken);
}
}

View File

@ -0,0 +1,10 @@

namespace MediaBrowser.Controller.Sync
{
/// <summary>
/// A marker interface
/// </summary>
public interface IRemoteSyncProvider
{
}
}

View File

@ -1,6 +1,7 @@
using MediaBrowser.Model.Sync;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Sync;
using Patterns.IO;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@ -13,46 +14,39 @@ namespace MediaBrowser.Controller.Sync
/// Transfers the file.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="remotePath">The remote path.</param>
/// <param name="pathParts">The path parts.</param>
/// <param name="target">The target.</param>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task<SyncedFileInfo> SendFile(Stream stream, string remotePath, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
Task<SyncedFileInfo> SendFile(Stream stream, string[] pathParts, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
/// <summary>
/// Deletes the file.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="id">The identifier.</param>
/// <param name="target">The target.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task DeleteFile(string path, SyncTarget target, CancellationToken cancellationToken);
Task DeleteFile(string id, SyncTarget target, CancellationToken cancellationToken);
/// <summary>
/// Gets the file.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="id">The identifier.</param>
/// <param name="target">The target.</param>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task&lt;Stream&gt;.</returns>
Task<Stream> GetFile(string path, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
Task<Stream> GetFile(string id, SyncTarget target, IProgress<double> progress, CancellationToken cancellationToken);
/// <summary>
/// Gets the full path.
/// Gets the files.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="query">The query.</param>
/// <param name="target">The target.</param>
/// <returns>System.String.</returns>
string GetFullPath(IEnumerable<string> path, SyncTarget target);
/// <summary>
/// Gets the parent directory path.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="target">The target.</param>
/// <returns>System.String.</returns>
string GetParentDirectoryPath(string path, SyncTarget target);
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task&lt;QueryResult&lt;FileMetadata&gt;&gt;.</returns>
Task<QueryResult<FileMetadata>> GetFiles(FileQuery query, SyncTarget target, CancellationToken cancellationToken);
}
}

View File

@ -7,20 +7,12 @@ namespace MediaBrowser.Controller.Sync
public interface ISyncDataProvider
{
/// <summary>
/// Gets the server item ids.
/// Gets the local items.
/// </summary>
/// <param name="target">The target.</param>
/// <param name="serverId">The server identifier.</param>
/// <returns>Task&lt;List&lt;System.String&gt;&gt;.</returns>
Task<List<string>> GetServerItemIds(SyncTarget target, string serverId);
/// <summary>
/// Gets the synchronize job item ids.
/// </summary>
/// <param name="target">The target.</param>
/// <param name="serverId">The server identifier.</param>
/// <returns>Task&lt;List&lt;System.String&gt;&gt;.</returns>
Task<List<string>> GetSyncJobItemIds(SyncTarget target, string serverId);
/// <returns>Task&lt;List&lt;LocalItem&gt;&gt;.</returns>
Task<List<LocalItem>> GetLocalItems(SyncTarget target, string serverId);
/// <summary>
/// Adds the or update.

View File

@ -174,6 +174,13 @@ namespace MediaBrowser.Controller.Sync
/// <param name="targetId">The target identifier.</param>
/// <returns>IEnumerable&lt;SyncQualityOption&gt;.</returns>
IEnumerable<SyncQualityOption> GetQualityOptions(string targetId);
/// <summary>
/// Gets the quality options.
/// </summary>
/// <param name="targetId">The target identifier.</param>
/// <param name="user">The user.</param>
/// <returns>IEnumerable&lt;SyncQualityOption&gt;.</returns>
IEnumerable<SyncQualityOption> GetQualityOptions(string targetId, User user);
/// <summary>
/// Gets the profile options.
@ -181,5 +188,12 @@ namespace MediaBrowser.Controller.Sync
/// <param name="targetId">The target identifier.</param>
/// <returns>IEnumerable&lt;SyncQualityOption&gt;.</returns>
IEnumerable<SyncProfileOption> GetProfileOptions(string targetId);
/// <summary>
/// Gets the profile options.
/// </summary>
/// <param name="targetId">The target identifier.</param>
/// <param name="user">The user.</param>
/// <returns>IEnumerable&lt;SyncProfileOption&gt;.</returns>
IEnumerable<SyncProfileOption> GetProfileOptions(string targetId, User user);
}
}

View File

@ -20,6 +20,11 @@ namespace MediaBrowser.Controller.Sync
/// </summary>
/// <value>The required HTTP headers.</value>
public Dictionary<string, string> RequiredHttpHeaders { get; set; }
/// <summary>
/// Gets or sets the identifier.
/// </summary>
/// <value>The identifier.</value>
public string Id { get; set; }
public SyncedFileInfo()
{

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="morelinq" version="1.1.0" targetFramework="net45" />
<package id="Patterns.IO" version="1.0.0.3" targetFramework="net45" />
</packages>

View File

@ -223,7 +223,7 @@ namespace MediaBrowser.Dlna.ContentDirectory
if (string.Equals(flag, "BrowseMetadata"))
{
totalCount = 1;
if (item.IsFolder || serverItem.StubType.HasValue)
{
var childrenResult = (await GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount).ConfigureAwait(false));
@ -350,7 +350,7 @@ namespace MediaBrowser.Dlna.ContentDirectory
};
}
private async Task<QueryResult<BaseItem>> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit)
private Task<QueryResult<BaseItem>> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit)
{
var folder = (Folder)item;
@ -389,7 +389,7 @@ namespace MediaBrowser.Dlna.ContentDirectory
isFolder = true;
}
return await folder.GetItems(new InternalItemsQuery
return folder.GetItems(new InternalItemsQuery
{
Limit = limit,
StartIndex = startIndex,
@ -401,7 +401,7 @@ namespace MediaBrowser.Dlna.ContentDirectory
IsFolder = isFolder,
MediaTypes = mediaTypes.ToArray()
}).ConfigureAwait(false);
});
}
private async Task<QueryResult<ServerItem>> GetUserItems(BaseItem item, StubType? stubType, User user, SortCriteria sort, int? startIndex, int? limit)

View File

@ -12,6 +12,7 @@ using MediaBrowser.Dlna.ContentDirectory;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Net;
using System;
using System.Globalization;
@ -124,9 +125,9 @@ namespace MediaBrowser.Dlna.Didl
{
if (streamInfo == null)
{
var sources = _user == null ? _mediaSourceManager.GetStaticMediaSources(video, true).ToList() : _mediaSourceManager.GetStaticMediaSources(video, true, _user).ToList();
var sources = _mediaSourceManager.GetStaticMediaSources(video, true, _user).ToList();
streamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions
streamInfo = new StreamBuilder(new NullLogger()).BuildVideoItem(new VideoOptions
{
ItemId = GetClientId(video),
MediaSources = sources,
@ -351,9 +352,9 @@ namespace MediaBrowser.Dlna.Didl
if (streamInfo == null)
{
var sources = _user == null ? _mediaSourceManager.GetStaticMediaSources(audio, true).ToList() : _mediaSourceManager.GetStaticMediaSources(audio, true, _user).ToList();
var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user).ToList();
streamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions
streamInfo = new StreamBuilder(new NullLogger()).BuildAudioItem(new AudioOptions
{
ItemId = GetClientId(audio),
MediaSources = sources,

View File

@ -470,7 +470,7 @@ namespace MediaBrowser.Dlna.PlayTo
var hasMediaSources = item as IHasMediaSources;
var mediaSources = hasMediaSources != null
? (user == null ? _mediaSourceManager.GetStaticMediaSources(hasMediaSources, true) : _mediaSourceManager.GetStaticMediaSources(hasMediaSources, true, user)).ToList()
? (_mediaSourceManager.GetStaticMediaSources(hasMediaSources, true, user)).ToList()
: new List<MediaSourceInfo>();
var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex);
@ -542,7 +542,7 @@ namespace MediaBrowser.Dlna.PlayTo
{
return new PlaylistItem
{
StreamInfo = new StreamBuilder().BuildVideoItem(new VideoOptions
StreamInfo = new StreamBuilder(_logger).BuildVideoItem(new VideoOptions
{
ItemId = item.Id.ToString("N"),
MediaSources = mediaSources,
@ -562,7 +562,7 @@ namespace MediaBrowser.Dlna.PlayTo
{
return new PlaylistItem
{
StreamInfo = new StreamBuilder().BuildAudioItem(new AudioOptions
StreamInfo = new StreamBuilder(_logger).BuildAudioItem(new AudioOptions
{
ItemId = item.Id.ToString("N"),
MediaSources = mediaSources,
@ -892,7 +892,7 @@ namespace MediaBrowser.Dlna.PlayTo
request.MediaSource = hasMediaSources == null ?
null :
mediaSourceManager.GetStaticMediaSource(hasMediaSources, request.MediaSourceId, false);
mediaSourceManager.GetMediaSource(hasMediaSources, request.MediaSourceId, false).Result;

View File

@ -62,16 +62,22 @@ namespace MediaBrowser.Dlna.Ssdp
{
if (string.Equals(args.Method, "M-SEARCH", StringComparison.OrdinalIgnoreCase))
{
TimeSpan delay = GetSearchDelay(args.Headers);
var headers = args.Headers;
TimeSpan delay = GetSearchDelay(headers);
if (_config.GetDlnaConfiguration().EnableDebugLogging)
{
_logger.Debug("Delaying search response by {0} seconds", delay.TotalSeconds);
}
await Task.Delay(delay).ConfigureAwait(false);
await Task.Delay(delay).ConfigureAwait(false);
RespondToSearch(args.EndPoint, args.Headers["st"]);
string st;
if (headers.TryGetValue("st", out st))
{
RespondToSearch(args.EndPoint, st);
}
}
EventHelper.FireEventIfNotNull(MessageReceived, this, args, _logger);

View File

@ -92,7 +92,7 @@ namespace MediaBrowser.LocalMetadata
{
get
{
return "Media Browser Xml";
return "Emby Xml";
}
}

View File

@ -10,7 +10,7 @@ using System.Linq;
namespace MediaBrowser.LocalMetadata.Images
{
public class EpisodeLocalLocalImageProvider : ILocalImageFileProvider
public class EpisodeLocalLocalImageProvider : ILocalImageFileProvider, IHasOrder
{
private readonly IFileSystem _fileSystem;
@ -24,6 +24,11 @@ namespace MediaBrowser.LocalMetadata.Images
get { return "Local Images"; }
}
public int Order
{
get { return 0; }
}
public bool Supports(IHasImages item)
{
return item is Episode && item.SupportsLocalMetadata;

View File

@ -26,6 +26,11 @@ namespace MediaBrowser.LocalMetadata.Images
public bool Supports(IHasImages item)
{
if (item is Photo)
{
return false;
}
if (!item.IsSaveLocalMetadataEnabled())
{
return true;

View File

@ -12,7 +12,7 @@ using System.Linq;
namespace MediaBrowser.LocalMetadata.Images
{
public class LocalImageProvider : ILocalImageFileProvider
public class LocalImageProvider : ILocalImageFileProvider, IHasOrder
{
private readonly IFileSystem _fileSystem;

View File

@ -756,11 +756,6 @@ namespace MediaBrowser.LocalMetadata.Savers
{
builder.Append("<" + singularNodeName + ">");
if (!string.IsNullOrWhiteSpace(link.ItemType))
{
builder.Append("<Type>" + SecurityElement.Escape(link.ItemType) + "</Type>");
}
if (!string.IsNullOrWhiteSpace(link.Path))
{
builder.Append("<Path>" + SecurityElement.Escape((link.Path)) + "</Path>");

View File

@ -70,10 +70,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
encodingJob.OutputFilePath = GetOutputFilePath(encodingJob);
Directory.CreateDirectory(Path.GetDirectoryName(encodingJob.OutputFilePath));
if (options.Context == EncodingContext.Static && encodingJob.IsInputVideo)
{
encodingJob.ReadInputAtNativeFramerate = true;
}
encodingJob.ReadInputAtNativeFramerate = options.ReadInputAtNativeFramerate;
await AcquireResources(encodingJob, cancellationToken).ConfigureAwait(false);
@ -305,19 +302,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <returns>System.Int32.</returns>
protected int GetNumberOfThreads(EncodingJob job, bool isWebm)
{
// Only need one thread for sync
if (job.Options.Context == EncodingContext.Static)
{
return 1;
}
if (isWebm)
{
// Recommended per docs
return Math.Max(Environment.ProcessorCount - 1, 2);
}
return 0;
return job.Options.CpuCoreLimit ?? 0;
}
protected EncodingQuality GetQualitySetting()

View File

@ -59,7 +59,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(request.ItemId, false, cancellationToken).ConfigureAwait(false);
var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(request.ItemId, null, false, new[] { MediaType.Audio, MediaType.Video }, cancellationToken).ConfigureAwait(false);
var mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
? mediaSources.First()
@ -124,10 +124,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
state.InputContainer = mediaSource.Container;
state.InputFileSize = mediaSource.Size;
state.InputBitrate = mediaSource.Bitrate;
state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
state.RunTimeTicks = mediaSource.RunTimeTicks;
state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
if (mediaSource.ReadAtNativeFramerate)
{
state.ReadInputAtNativeFramerate = true;
}
if (mediaSource.VideoType.HasValue)
{
state.VideoType = mediaSource.VideoType.Value;
@ -148,7 +152,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders;
state.InputBitrate = mediaSource.Bitrate;
state.InputFileSize = mediaSource.Size;
state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate;
if (state.ReadInputAtNativeFramerate ||
mediaSource.Protocol == MediaProtocol.File && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase))

View File

@ -5,12 +5,15 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Session;
using MediaBrowser.MediaEncoding.Probing;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
@ -72,6 +75,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
protected readonly Func<ISubtitleEncoder> SubtitleEncoder;
protected readonly Func<IMediaSourceManager> MediaSourceManager;
private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
public MediaEncoder(ILogger logger, IJsonSerializer jsonSerializer, string ffMpegPath, string ffProbePath, string version, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILiveTvManager liveTvManager, IIsoManager isoManager, ILibraryManager libraryManager, IChannelManager channelManager, ISessionManager sessionManager, Func<ISubtitleEncoder> subtitleEncoder, Func<IMediaSourceManager> mediaSourceManager)
{
_logger = logger;
@ -102,16 +107,19 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// <summary>
/// Gets the media info.
/// </summary>
/// <param name="inputFiles">The input files.</param>
/// <param name="protocol">The protocol.</param>
/// <param name="isAudio">if set to <c>true</c> [is audio].</param>
/// <param name="request">The request.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task<InternalMediaInfoResult> GetMediaInfo(string[] inputFiles, MediaProtocol protocol, bool isAudio,
CancellationToken cancellationToken)
public Task<Model.MediaInfo.MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
{
return GetMediaInfoInternal(GetInputArgument(inputFiles, protocol), !isAudio,
GetProbeSizeArgument(inputFiles, protocol), cancellationToken);
var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
var inputFiles = MediaEncoderHelpers.GetInputArgument(request.InputPath, request.Protocol, request.MountedIso, request.PlayableStreamFileNames);
var extractKeyFrameInterval = request.ExtractKeyFrameInterval && request.Protocol == MediaProtocol.File && request.VideoType == VideoType.VideoFile;
return GetMediaInfoInternal(GetInputArgument(inputFiles, request.Protocol), request.InputPath, request.Protocol, extractChapters, extractKeyFrameInterval,
GetProbeSizeArgument(inputFiles, request.Protocol), request.MediaType == DlnaProfileType.Audio, cancellationToken);
}
/// <summary>
@ -141,13 +149,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// Gets the media info internal.
/// </summary>
/// <param name="inputPath">The input path.</param>
/// <param name="primaryPath">The primary path.</param>
/// <param name="protocol">The protocol.</param>
/// <param name="extractChapters">if set to <c>true</c> [extract chapters].</param>
/// <param name="extractKeyFrameInterval">if set to <c>true</c> [extract key frame interval].</param>
/// <param name="probeSizeArgument">The probe size argument.</param>
/// <param name="isAudio">if set to <c>true</c> [is audio].</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{MediaInfoResult}.</returns>
/// <exception cref="System.ApplicationException"></exception>
private async Task<InternalMediaInfoResult> GetMediaInfoInternal(string inputPath, bool extractChapters,
private async Task<Model.MediaInfo.MediaInfo> GetMediaInfoInternal(string inputPath,
string primaryPath,
MediaProtocol protocol,
bool extractChapters,
bool extractKeyFrameInterval,
string probeSizeArgument,
bool isAudio,
CancellationToken cancellationToken)
{
var args = extractChapters
@ -164,6 +181,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Must consume both or ffmpeg may hang due to deadlocks. See comments below.
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
FileName = FFProbePath,
Arguments = string.Format(args,
probeSizeArgument, inputPath).Trim(),
@ -177,15 +195,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
_logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
process.Exited += ProcessExited;
await _ffProbeResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
InternalMediaInfoResult result;
var processWrapper = new ProcessWrapper(process, this);
try
{
process.Start();
StartProcess(processWrapper);
}
catch (Exception ex)
{
@ -200,19 +216,57 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
process.BeginErrorReadLine();
result = _jsonSerializer.DeserializeFromStream<InternalMediaInfoResult>(process.StandardOutput.BaseStream);
var result = _jsonSerializer.DeserializeFromStream<InternalMediaInfoResult>(process.StandardOutput.BaseStream);
if (result != null)
{
if (result.streams != null)
{
// Normalize aspect ratio if invalid
foreach (var stream in result.streams)
{
if (string.Equals(stream.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
{
stream.display_aspect_ratio = string.Empty;
}
if (string.Equals(stream.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
{
stream.sample_aspect_ratio = string.Empty;
}
}
}
var mediaInfo = new ProbeResultNormalizer(_logger, FileSystem).GetMediaInfo(result, isAudio, primaryPath, protocol);
if (extractKeyFrameInterval && mediaInfo.RunTimeTicks.HasValue)
{
foreach (var stream in mediaInfo.MediaStreams)
{
if (stream.Type == MediaStreamType.Video && string.Equals(stream.Codec, "h264", StringComparison.OrdinalIgnoreCase))
{
try
{
//stream.KeyFrames = await GetKeyFrames(inputPath, stream.Index, cancellationToken)
// .ConfigureAwait(false);
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
_logger.ErrorException("Error getting key frame interval", ex);
}
}
}
}
return mediaInfo;
}
}
catch
{
// Hate having to do this
try
{
process.Kill();
}
catch (Exception ex1)
{
_logger.ErrorException("Error killing ffprobe", ex1);
}
StopProcess(processWrapper, 100, true);
throw;
}
@ -221,30 +275,102 @@ namespace MediaBrowser.MediaEncoding.Encoder
_ffProbeResourcePool.Release();
}
if (result == null)
throw new ApplicationException(string.Format("FFProbe failed for {0}", inputPath));
}
private async Task<List<int>> GetKeyFrames(string inputPath, int videoStreamIndex, CancellationToken cancellationToken)
{
const string args = "-i {0} -select_streams v:{1} -show_frames -show_entries frame=pkt_dts,key_frame -print_format compact";
var process = new Process
{
throw new ApplicationException(string.Format("FFProbe failed for {0}", inputPath));
StartInfo = new ProcessStartInfo
{
CreateNoWindow = true,
UseShellExecute = false,
// Must consume both or ffmpeg may hang due to deadlocks. See comments below.
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
FileName = FFProbePath,
Arguments = string.Format(args, inputPath, videoStreamIndex.ToString(CultureInfo.InvariantCulture)).Trim(),
WindowStyle = ProcessWindowStyle.Hidden,
ErrorDialog = false
},
EnableRaisingEvents = true
};
_logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
var processWrapper = new ProcessWrapper(process, this);
StartProcess(processWrapper);
var lines = new List<int>();
try
{
process.BeginErrorReadLine();
await StartReadingOutput(process.StandardOutput.BaseStream, lines, 120000, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
if (cancellationToken.IsCancellationRequested)
{
throw;
}
}
finally
{
StopProcess(processWrapper, 100, true);
}
cancellationToken.ThrowIfCancellationRequested();
return lines;
}
if (result.streams != null)
private async Task StartReadingOutput(Stream source, List<int> lines, int timeoutMs, CancellationToken cancellationToken)
{
try
{
// Normalize aspect ratio if invalid
foreach (var stream in result.streams)
using (var reader = new StreamReader(source))
{
if (string.Equals(stream.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
while (!reader.EndOfStream)
{
stream.display_aspect_ratio = string.Empty;
}
if (string.Equals(stream.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
{
stream.sample_aspect_ratio = string.Empty;
cancellationToken.ThrowIfCancellationRequested();
var line = await reader.ReadLineAsync().ConfigureAwait(false);
var values = (line ?? string.Empty).Split('|')
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => i.Split('='))
.Where(i => i.Length == 2)
.ToDictionary(i => i[0], i => i[1]);
string pktDts;
int frameMs;
if (values.TryGetValue("pkt_dts", out pktDts) && int.TryParse(pktDts, NumberStyles.Any, CultureInfo.InvariantCulture, out frameMs))
{
string keyFrame;
if (values.TryGetValue("key_frame", out keyFrame) && string.Equals(keyFrame, "1", StringComparison.OrdinalIgnoreCase))
{
lines.Add(frameMs);
}
}
}
}
}
return result;
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
_logger.ErrorException("Error reading ffprobe output", ex);
}
}
/// <summary>
@ -252,16 +378,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
/// </summary>
protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
/// <summary>
/// Processes the exited.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
private void ProcessExited(object sender, EventArgs e)
{
((Process)sender).Dispose();
}
public Task<Stream> ExtractAudioImage(string path, CancellationToken cancellationToken)
{
return ExtractImage(new[] { path }, MediaProtocol.File, true, null, null, cancellationToken);
@ -286,6 +402,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
{
return await ExtractImageInternal(inputArgument, protocol, threedFormat, offset, true, resourcePool, cancellationToken).ConfigureAwait(false);
}
catch (ArgumentException)
{
throw;
}
catch
{
_logger.Error("I-frame image extraction failed, will attempt standard way. Input: {0}", inputArgument);
@ -368,7 +488,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
process.Start();
var processWrapper = new ProcessWrapper(process, this);
StartProcess(processWrapper);
var memoryStream = new MemoryStream();
@ -384,23 +506,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (!ranToCompletion)
{
try
{
_logger.Info("Killing ffmpeg process");
process.StandardInput.WriteLine("q");
process.WaitForExit(1000);
}
catch (Exception ex)
{
_logger.ErrorException("Error killing process", ex);
}
StopProcess(processWrapper, 1000, false);
}
resourcePool.Release();
var exitCode = ranToCompletion ? process.ExitCode : -1;
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
process.Dispose();
@ -419,31 +530,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
return memoryStream;
}
public Task<Stream> EncodeImage(ImageEncodingOptions options, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool dispose)
{
if (dispose)
{
_videoImageResourcePool.Dispose();
}
}
public string GetTimeParameter(long ticks)
{
var time = TimeSpan.FromTicks(ticks);
@ -510,9 +596,11 @@ namespace MediaBrowser.MediaEncoding.Encoder
bool ranToCompletion;
var processWrapper = new ProcessWrapper(process, this);
try
{
process.Start();
StartProcess(processWrapper);
// Need to give ffmpeg enough time to make all the thumbnails, which could be a while,
// but we still need to detect if the process hangs.
@ -536,18 +624,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
if (!ranToCompletion)
{
try
{
_logger.Info("Killing ffmpeg process");
process.StandardInput.WriteLine("q");
process.WaitForExit(1000);
}
catch (Exception ex)
{
_logger.ErrorException("Error killing process", ex);
}
StopProcess(processWrapper, 1000, false);
}
}
finally
@ -555,7 +632,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
resourcePool.Release();
}
var exitCode = ranToCompletion ? process.ExitCode : -1;
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
process.Dispose();
@ -608,5 +685,122 @@ namespace MediaBrowser.MediaEncoding.Encoder
return job.OutputFilePath;
}
private void StartProcess(ProcessWrapper process)
{
process.Process.Start();
lock (_runningProcesses)
{
_runningProcesses.Add(process);
}
}
private void StopProcess(ProcessWrapper process, int waitTimeMs, bool enableForceKill)
{
try
{
_logger.Info("Killing ffmpeg process");
try
{
process.Process.StandardInput.WriteLine("q");
}
catch (Exception)
{
_logger.Error("Error sending q command to process");
}
try
{
if (process.Process.WaitForExit(waitTimeMs))
{
return;
}
}
catch (Exception ex)
{
_logger.Error("Error in WaitForExit", ex);
}
if (enableForceKill)
{
process.Process.Kill();
}
}
catch (Exception ex)
{
_logger.ErrorException("Error killing process", ex);
}
}
private void StopProcesses()
{
List<ProcessWrapper> proceses;
lock (_runningProcesses)
{
proceses = _runningProcesses.ToList();
}
_runningProcesses.Clear();
foreach (var process in proceses)
{
if (!process.HasExited)
{
StopProcess(process, 500, true);
}
}
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool dispose)
{
if (dispose)
{
_videoImageResourcePool.Dispose();
StopProcesses();
}
}
private class ProcessWrapper
{
public readonly Process Process;
public bool HasExited;
public int? ExitCode;
private readonly MediaEncoder _mediaEncoder;
public ProcessWrapper(Process process, MediaEncoder mediaEncoder)
{
Process = process;
this._mediaEncoder = mediaEncoder;
Process.Exited += Process_Exited;
}
void Process_Exited(object sender, EventArgs e)
{
var process = (Process)sender;
HasExited = true;
ExitCode = process.ExitCode;
lock (_mediaEncoder._runningProcesses)
{
_mediaEncoder._runningProcesses.Remove(this);
}
process.Dispose();
}
}
}
}

View File

@ -68,6 +68,9 @@
<Compile Include="Encoder\JobLogger.cs" />
<Compile Include="Encoder\MediaEncoder.cs" />
<Compile Include="Encoder\VideoEncoder.cs" />
<Compile Include="Probing\FFProbeHelpers.cs" />
<Compile Include="Probing\InternalMediaInfoResult.cs" />
<Compile Include="Probing\ProbeResultNormalizer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Subtitles\ISubtitleParser.cs" />
<Compile Include="Subtitles\ISubtitleWriter.cs" />
@ -91,6 +94,10 @@
<Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project>
<Name>MediaBrowser.Controller</Name>
</ProjectReference>
<ProjectReference Include="..\MediaBrowser.MediaInfo\MediaBrowser.MediaInfo.csproj">
<Project>{6e4145e4-c6d4-4e4d-94f2-87188db6e239}</Project>
<Name>MediaBrowser.MediaInfo</Name>
</ProjectReference>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
<Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project>
<Name>MediaBrowser.Model</Name>
@ -99,7 +106,9 @@
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup />
<ItemGroup>
<EmbeddedResource Include="Probing\whitelist.txt" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

View File

@ -2,7 +2,7 @@
using System;
using System.Collections.Generic;
namespace MediaBrowser.Providers.MediaInfo
namespace MediaBrowser.MediaEncoding.Probing
{
public static class FFProbeHelpers
{

View File

@ -1,6 +1,6 @@
using System.Collections.Generic;
namespace MediaBrowser.Controller.MediaEncoding
namespace MediaBrowser.MediaEncoding.Probing
{
/// <summary>
/// Class MediaInfoResult
@ -89,7 +89,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary>
/// <value>The channel_layout.</value>
public string channel_layout { get; set; }
/// <summary>
/// Gets or sets the avg_frame_rate.
/// </summary>
@ -317,7 +317,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary>
/// <value>The probe_score.</value>
public int probe_score { get; set; }
/// <summary>
/// Gets or sets the tags.
/// </summary>

View 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