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.IO;
using System.Linq; using System.Linq;
namespace MediaBrowser.Server.Implementations.Drawing namespace Emby.Drawing.Common
{ {
/// <summary> /// <summary>
/// Taken from http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349 /// Taken from http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file/111349

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 ImageMagickSharp;
using System; using System;
namespace MediaBrowser.Server.Implementations.Drawing namespace Emby.Drawing.ImageMagick
{ {
public class PercentPlayedDrawer public class PercentPlayedDrawer
{ {

View File

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

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 MediaBrowser.Model.Drawing;
using System.Globalization; using System.Globalization;
namespace MediaBrowser.Server.Implementations.Drawing namespace Emby.Drawing.ImageMagick
{ {
public class UnplayedCountIndicator public class UnplayedCountIndicator
{ {

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.Channels;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
@ -100,5 +101,10 @@ namespace MediaBrowser.Controller.Channels
{ {
return false; return false;
} }
public override bool IsVisibleStandalone(User user)
{
return IsVisibleStandaloneInternal(user, false) && ChannelVideoItem.IsChannelVisible(this, user);
}
} }
} }

View File

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

View File

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

View File

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

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> /// <returns>Task{BaseItemDto}.</returns>
BaseItemDto GetBaseItemDto(BaseItem item, List<ItemFields> fields, User user = null, BaseItem owner = null); BaseItemDto GetBaseItemDto(BaseItem item, List<ItemFields> fields, User user = null, BaseItem owner = null);
/// <summary>
/// Fills the synchronize information.
/// </summary>
/// <param name="dtos">The dtos.</param>
/// <param name="options">The options.</param>
/// <param name="user">The user.</param>
void FillSyncInfo(IEnumerable<IHasSyncInfo> dtos, DtoOptions options, User user);
/// <summary> /// <summary>
/// Gets the base item dto. /// Gets the base item dto.
/// </summary> /// </summary>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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.Drawing;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using System.Threading.Tasks; using System.Threading.Tasks;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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