Merge pull request #1109 from MediaBrowser/dev

3.0.5621.3
pull/702/head
Luke 10 years ago
commit 8bd7055d17

@ -115,9 +115,17 @@ namespace Emby.Drawing.ImageMagick
}
}
private bool HasTransparency(string path)
{
var ext = Path.GetExtension(path);
return string.Equals(ext, ".png", StringComparison.OrdinalIgnoreCase) ||
string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase);
}
public void EncodeImage(string inputPath, string outputPath, int width, int height, int quality, ImageProcessingOptions options)
{
if (string.IsNullOrWhiteSpace(options.BackgroundColor))
if (string.IsNullOrWhiteSpace(options.BackgroundColor) || !HasTransparency(inputPath))
{
using (var originalImage = new MagickWand(inputPath))
{

@ -250,19 +250,19 @@ namespace MediaBrowser.Api
return GetTranscodingJob(path, type) != null;
}
public TranscodingJob GetTranscodingJob(string path, TranscodingJobType type)
public TranscodingJob GetTranscodingJobByPlaySessionId(string playSessionId)
{
lock (_activeTranscodingJobs)
{
return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
return _activeTranscodingJobs.FirstOrDefault(j => j.PlaySessionId.Equals(playSessionId, StringComparison.OrdinalIgnoreCase));
}
}
public TranscodingJob GetTranscodingJob(string id)
public TranscodingJob GetTranscodingJob(string path, TranscodingJobType type)
{
lock (_activeTranscodingJobs)
{
return _activeTranscodingJobs.FirstOrDefault(j => j.Id.Equals(id, StringComparison.OrdinalIgnoreCase));
return _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && j.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
}
}
@ -339,14 +339,17 @@ namespace MediaBrowser.Api
return;
}
var timerDuration = job.Type == TranscodingJobType.Progressive ?
1000 :
1800000;
var timerDuration = 1000;
// We can really reduce the timeout for apps that are using the newer api
if (!string.IsNullOrWhiteSpace(job.PlaySessionId) && job.Type != TranscodingJobType.Progressive)
if (job.Type != TranscodingJobType.Progressive)
{
timerDuration = 50000;
timerDuration = 1800000;
// We can really reduce the timeout for apps that are using the newer api
if (!string.IsNullOrWhiteSpace(job.PlaySessionId))
{
timerDuration = 60000;
}
}
job.PingTimeout = timerDuration;
@ -628,6 +631,9 @@ namespace MediaBrowser.Api
/// </summary>
/// <value>The live stream identifier.</value>
public string LiveStreamId { get; set; }
public bool IsLiveOutput { get; set; }
/// <summary>
/// Gets or sets the path.
/// </summary>

@ -109,7 +109,7 @@ namespace MediaBrowser.Api.Dlna
private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar;
// TODO: Add utf-8
private const string XMLContentType = "text/xml";
private const string XMLContentType = "text/xml; charset=UTF-8";
public DlnaServerService(IDlnaManager dlnaManager, IContentDirectory contentDirectory, IConnectionManager connectionManager, IMediaReceiverRegistrar mediaReceiverRegistrar)
{

@ -625,6 +625,8 @@ namespace MediaBrowser.Api.Images
var file = await _imageProcessor.ProcessImage(options).ConfigureAwait(false);
headers["Vary"] = "Accept";
return ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
{
CacheDuration = cacheDuration,
@ -659,8 +661,10 @@ namespace MediaBrowser.Api.Images
return ImageFormat.Png;
}
if (string.Equals(Path.GetExtension(image.Path), ".jpg", StringComparison.OrdinalIgnoreCase) ||
string.Equals(Path.GetExtension(image.Path), ".jpeg", StringComparison.OrdinalIgnoreCase))
var extension = Path.GetExtension(image.Path);
if (string.Equals(extension, ".jpg", StringComparison.OrdinalIgnoreCase) ||
string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase))
{
return ImageFormat.Jpg;
}

@ -389,22 +389,28 @@ namespace MediaBrowser.Api
game.PlayersSupported = request.Players;
}
var hasAlbumArtists = item as IHasAlbumArtist;
if (hasAlbumArtists != null)
if (request.AlbumArtists != null)
{
hasAlbumArtists.AlbumArtists = request
.AlbumArtists
.Select(i => i.Name)
.ToList();
var hasAlbumArtists = item as IHasAlbumArtist;
if (hasAlbumArtists != null)
{
hasAlbumArtists.AlbumArtists = request
.AlbumArtists
.Select(i => i.Name)
.ToList();
}
}
var hasArtists = item as IHasArtist;
if (hasArtists != null)
if (request.ArtistItems != null)
{
hasArtists.Artists = request
.ArtistItems
.Select(i => i.Name)
.ToList();
var hasArtists = item as IHasArtist;
if (hasArtists != null)
{
hasArtists.Artists = request
.ArtistItems
.Select(i => i.Name)
.ToList();
}
}
var song = item as Audio;

@ -84,10 +84,28 @@
<Compile Include="Playback\MediaInfoService.cs" />
<Compile Include="Playback\TranscodingThrottler.cs" />
<Compile Include="PlaylistService.cs" />
<Compile Include="Reports\ReportFieldType.cs" />
<Compile Include="Reports\ReportResult.cs" />
<Compile Include="Reports\ReportsService.cs" />
<Compile Include="Reports\Common\HeaderMetadata.cs" />
<Compile Include="Reports\Common\ItemViewType.cs" />
<Compile Include="Reports\Common\ReportBuilderBase.cs" />
<Compile Include="Reports\Common\ReportExportType.cs" />
<Compile Include="Reports\Common\ReportFieldType.cs" />
<Compile Include="Reports\Common\ReportHeaderIdType.cs" />
<Compile Include="Reports\Common\ReportHelper.cs" />
<Compile Include="Reports\Common\ReportViewType.cs" />
<Compile Include="Reports\Data\ReportBuilder.cs" />
<Compile Include="Reports\Data\ReportExport.cs" />
<Compile Include="Reports\Data\ReportGroup.cs" />
<Compile Include="Reports\Data\ReportHeader.cs" />
<Compile Include="Reports\Data\ReportItem.cs" />
<Compile Include="Reports\Data\ReportOptions.cs" />
<Compile Include="Reports\Data\ReportResult.cs" />
<Compile Include="Reports\Data\ReportRow.cs" />
<Compile Include="Reports\ReportRequests.cs" />
<Compile Include="Reports\ReportsService.cs" />
<Compile Include="Reports\Stat\ReportStatBuilder.cs" />
<Compile Include="Reports\Stat\ReportStatGroup.cs" />
<Compile Include="Reports\Stat\ReportStatItem.cs" />
<Compile Include="Reports\Stat\ReportStatResult.cs" />
<Compile Include="StartupWizardService.cs" />
<Compile Include="Subtitles\SubtitleService.cs" />
<Compile Include="Movies\CollectionService.cs" />

@ -50,7 +50,7 @@ namespace MediaBrowser.Api.Music
[Route("/MusicGenres/InstantMix", "GET", Summary = "Creates an instant playlist based on a music genre")]
public class GetInstantMixFromMusicGenreId : BaseGetSimilarItems
{
[ApiMember(Name = "Id", Description = "The genre Id", IsRequired = true, DataType = "string", ParameterType = "querypath", Verb = "GET")]
[ApiMember(Name = "Id", Description = "The genre Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")]
public string Id { get; set; }
}

@ -56,6 +56,8 @@ namespace MediaBrowser.Api
[ApiMember(Name = "IsAdult", Description = "Optional. Filter by package that contain adult content.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")]
public bool? IsAdult { get; set; }
public bool? IsAppStoreEnabled { get; set; }
}
/// <summary>
@ -207,6 +209,11 @@ namespace MediaBrowser.Api
packages = packages.Where(p => p.adult == request.IsAdult.Value);
}
if (request.IsAppStoreEnabled.HasValue)
{
packages = packages.Where(p => p.enableInAppStore == request.IsAppStoreEnabled.Value);
}
return ToOptimizedResult(packages.ToList());
}

@ -157,11 +157,11 @@ namespace MediaBrowser.Api.Playback
/// <value>The fast seek command line parameter.</value>
protected string GetFastSeekCommandLineParameter(StreamRequest request)
{
var time = request.StartTimeTicks;
var time = request.StartTimeTicks ?? 0;
if (time.HasValue && time.Value > 0)
if (time > 0)
{
return string.Format("-ss {0}", MediaEncoder.GetTimeParameter(time.Value));
return string.Format("-ss {0}", MediaEncoder.GetTimeParameter(time));
}
return string.Empty;
@ -690,7 +690,7 @@ namespace MediaBrowser.Api.Playback
// TODO: Perhaps also use original_size=1920x800 ??
return string.Format("subtitles=filename='{0}'{1},setpts=PTS -{2}/TB",
subtitlePath.Replace('\\', '/').Replace(":/", "\\:/"),
subtitlePath.Replace("'", "\\'").Replace('\\', '/').Replace(":/", "\\:/"),
charsetParam,
seconds.ToString(UsCulture));
}
@ -698,7 +698,7 @@ namespace MediaBrowser.Api.Playback
var mediaPath = state.MediaPath ?? string.Empty;
return string.Format("subtitles='{0}:si={1}',setpts=PTS -{2}/TB",
mediaPath.Replace('\\', '/').Replace(":/", "\\:/"),
mediaPath.Replace("'", "\\'").Replace('\\', '/').Replace(":/", "\\:/"),
state.InternalSubtitleStreamOffset.ToString(UsCulture),
seconds.ToString(UsCulture));
}
@ -769,26 +769,31 @@ namespace MediaBrowser.Api.Playback
/// <returns>System.Nullable{System.Int32}.</returns>
private int? GetNumAudioChannelsParam(StreamRequest request, MediaStream audioStream, string outputAudioCodec)
{
if (audioStream != null)
{
var codec = outputAudioCodec ?? string.Empty;
var inputChannels = audioStream == null
? null
: audioStream.Channels;
if (audioStream.Channels > 2 && codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1)
{
// wmav2 currently only supports two channel output
return 2;
}
var codec = outputAudioCodec ?? string.Empty;
if (codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1)
{
// wmav2 currently only supports two channel output
return Math.Min(2, inputChannels ?? 2);
}
if (request.MaxAudioChannels.HasValue)
{
if (audioStream != null && audioStream.Channels.HasValue)
if (inputChannels.HasValue)
{
return Math.Min(request.MaxAudioChannels.Value, audioStream.Channels.Value);
return Math.Min(request.MaxAudioChannels.Value, inputChannels.Value);
}
var channelLimit = codec.IndexOf("mp3", StringComparison.OrdinalIgnoreCase) != -1
? 2
: 5;
// If we don't have any media info then limit it to 5 to prevent encoding errors due to asking for too many channels
return Math.Min(request.MaxAudioChannels.Value, 5);
return Math.Min(request.MaxAudioChannels.Value, channelLimit);
}
return request.AudioChannels;
@ -1055,7 +1060,7 @@ namespace MediaBrowser.Api.Playback
private void StartThrottler(StreamState state, TranscodingJob transcodingJob)
{
if (state.InputProtocol == MediaProtocol.File &&
if (EnableThrottling(state) && state.InputProtocol == MediaProtocol.File &&
state.RunTimeTicks.HasValue &&
state.VideoType == VideoType.VideoFile &&
!string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
@ -1068,6 +1073,11 @@ namespace MediaBrowser.Api.Playback
}
}
protected virtual bool EnableThrottling(StreamState state)
{
return true;
}
private async void StartStreamingLog(TranscodingJob transcodingJob, StreamState state, Stream source, Stream target)
{
try
@ -1690,6 +1700,11 @@ namespace MediaBrowser.Api.Playback
private void TryStreamCopy(StreamState state, VideoStreamRequest videoRequest)
{
if (!EnableStreamCopy)
{
return;
}
if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream))
{
state.OutputVideoCodec = "copy";
@ -1701,6 +1716,14 @@ namespace MediaBrowser.Api.Playback
}
}
protected virtual bool EnableStreamCopy
{
get
{
return true;
}
}
private void AttachMediaSourceInfo(StreamState state,
MediaSourceInfo mediaSource,
VideoStreamRequest videoRequest,

@ -7,13 +7,13 @@ using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Api.Playback.Hls
{
@ -100,6 +100,7 @@ namespace MediaBrowser.Api.Playback.Hls
try
{
job = await StartFfMpeg(state, playlist, cancellationTokenSource).ConfigureAwait(false);
job.IsLiveOutput = isLive;
}
catch
{
@ -133,7 +134,7 @@ namespace MediaBrowser.Api.Playback.Hls
var appendBaselineStream = false;
var baselineStreamBitrate = 64000;
var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream;
var hlsVideoRequest = state.VideoRequest as GetHlsVideoStreamLegacy;
if (hlsVideoRequest != null)
{
appendBaselineStream = hlsVideoRequest.AppendBaselineStream;
@ -244,7 +245,7 @@ namespace MediaBrowser.Api.Playback.Hls
protected override string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding)
{
var hlsVideoRequest = state.VideoRequest as GetHlsVideoStream;
var hlsVideoRequest = state.VideoRequest as GetHlsVideoStreamLegacy;
var itsOffsetMs = hlsVideoRequest == null
? 0

@ -13,6 +13,7 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using ServiceStack;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@ -29,27 +30,60 @@ namespace MediaBrowser.Api.Playback.Hls
/// </summary>
[Route("/Videos/{Id}/master.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
[Route("/Videos/{Id}/master.m3u8", "HEAD", Summary = "Gets a video stream using HTTP live streaming.")]
public class GetMasterHlsVideoStream : VideoStreamRequest
public class GetMasterHlsVideoPlaylist : VideoStreamRequest, IMasterHlsRequest
{
public bool EnableAdaptiveBitrateStreaming { get; set; }
public GetMasterHlsVideoStream()
public GetMasterHlsVideoPlaylist()
{
EnableAdaptiveBitrateStreaming = true;
}
}
[Route("/Audio/{Id}/master.m3u8", "GET", Summary = "Gets an audio stream using HTTP live streaming.")]
[Route("/Audio/{Id}/master.m3u8", "HEAD", Summary = "Gets an audio stream using HTTP live streaming.")]
public class GetMasterHlsAudioPlaylist : StreamRequest, IMasterHlsRequest
{
public bool EnableAdaptiveBitrateStreaming { get; set; }
public GetMasterHlsAudioPlaylist()
{
EnableAdaptiveBitrateStreaming = true;
}
}
public interface IMasterHlsRequest
{
bool EnableAdaptiveBitrateStreaming { get; set; }
}
[Route("/Videos/{Id}/main.m3u8", "GET", Summary = "Gets a video stream using HTTP live streaming.")]
public class GetMainHlsVideoStream : VideoStreamRequest
public class GetVariantHlsVideoPlaylist : VideoStreamRequest
{
}
[Route("/Audio/{Id}/main.m3u8", "GET", Summary = "Gets an audio stream using HTTP live streaming.")]
public class GetVariantHlsAudioPlaylist : StreamRequest
{
}
/// <summary>
/// Class GetHlsVideoSegment
/// </summary>
[Route("/Videos/{Id}/hlsdynamic/{PlaylistId}/{SegmentId}.ts", "GET")]
[Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
public class GetDynamicHlsVideoSegment : VideoStreamRequest
public class GetHlsVideoSegment : VideoStreamRequest
{
public string PlaylistId { get; set; }
/// <summary>
/// Gets or sets the segment id.
/// </summary>
/// <value>The segment id.</value>
public string SegmentId { get; set; }
}
[Route("/Audio/{Id}/hlsdynamic/{PlaylistId}/{SegmentId}.aac", "GET")]
[Route("/Audio/{Id}/hlsdynamic/{PlaylistId}/{SegmentId}.ts", "GET")]
[Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
public class GetHlsAudioSegment : StreamRequest
{
public string PlaylistId { get; set; }
@ -62,34 +96,55 @@ namespace MediaBrowser.Api.Playback.Hls
public class DynamicHlsService : BaseHlsService
{
public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, INetworkManager networkManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer)
public DynamicHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IDlnaManager dlnaManager, ISubtitleEncoder subtitleEncoder, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager, IZipClient zipClient, IJsonSerializer jsonSerializer, INetworkManager networkManager)
: base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, fileSystem, dlnaManager, subtitleEncoder, deviceManager, mediaSourceManager, zipClient, jsonSerializer)
{
NetworkManager = networkManager;
}
protected INetworkManager NetworkManager { get; private set; }
public Task<object> Get(GetMasterHlsVideoStream request)
public Task<object> Get(GetMasterHlsVideoPlaylist request)
{
return GetMasterPlaylistInternal(request, "GET");
}
public Task<object> Head(GetMasterHlsVideoPlaylist request)
{
return GetAsync(request, "GET");
return GetMasterPlaylistInternal(request, "HEAD");
}
public Task<object> Head(GetMasterHlsVideoStream request)
public Task<object> Get(GetMasterHlsAudioPlaylist request)
{
return GetAsync(request, "HEAD");
return GetMasterPlaylistInternal(request, "GET");
}
public Task<object> Get(GetMainHlsVideoStream request)
public Task<object> Head(GetMasterHlsAudioPlaylist request)
{
return GetPlaylistAsync(request, "main");
return GetMasterPlaylistInternal(request, "HEAD");
}
public Task<object> Get(GetVariantHlsVideoPlaylist request)
{
return GetVariantPlaylistInternal(request, true, "main");
}
public Task<object> Get(GetVariantHlsAudioPlaylist request)
{
return GetVariantPlaylistInternal(request, false, "main");
}
public Task<object> Get(GetHlsVideoSegment request)
{
return GetDynamicSegment(request, request.SegmentId);
}
public Task<object> Get(GetDynamicHlsVideoSegment request)
public Task<object> Get(GetHlsAudioSegment request)
{
return GetDynamicSegment(request, request.SegmentId);
}
private async Task<object> GetDynamicSegment(VideoStreamRequest request, string segmentId)
private async Task<object> GetDynamicSegment(StreamRequest request, string segmentId)
{
if ((request.StartTimeTicks ?? 0) > 0)
{
@ -105,7 +160,7 @@ namespace MediaBrowser.Api.Playback.Hls
var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
var segmentPath = GetSegmentPath(playlistPath, requestedIndex);
var segmentPath = GetSegmentPath(state, playlistPath, requestedIndex);
var segmentLength = state.SegmentLength;
var segmentExtension = GetSegmentFileExtension(state);
@ -130,7 +185,7 @@ namespace MediaBrowser.Api.Playback.Hls
{
var startTranscoding = false;
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, request.PlaySessionId, segmentExtension);
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
if (currentTranscodingIndex == null)
@ -155,12 +210,14 @@ namespace MediaBrowser.Api.Playback.Hls
{
ApiEntryPoint.Instance.KillTranscodingJobs(request.DeviceId, request.PlaySessionId, p => false);
await ReadSegmentLengths(playlistPath).ConfigureAwait(false);
if (currentTranscodingIndex.HasValue)
{
DeleteLastFile(playlistPath, segmentExtension, 0);
}
request.StartTimeTicks = GetSeekPositionTicks(state, requestedIndex);
request.StartTimeTicks = GetSeekPositionTicks(state, playlistPath, requestedIndex);
job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false);
}
@ -187,28 +244,92 @@ namespace MediaBrowser.Api.Playback.Hls
ApiEntryPoint.Instance.TranscodingStartLock.Release();
}
Logger.Info("waiting for {0}", segmentPath);
while (!File.Exists(segmentPath))
{
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
}
//Logger.Info("waiting for {0}", segmentPath);
//while (!File.Exists(segmentPath))
//{
// await Task.Delay(50, cancellationToken).ConfigureAwait(false);
//}
Logger.Info("returning {0}", segmentPath);
job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
return await GetSegmentResult(playlistPath, segmentPath, requestedIndex, segmentLength, job, cancellationToken).ConfigureAwait(false);
}
private long GetSeekPositionTicks(StreamState state, int requestedIndex)
private static readonly ConcurrentDictionary<string, double> SegmentLengths = new ConcurrentDictionary<string, double>(StringComparer.OrdinalIgnoreCase);
private async Task ReadSegmentLengths(string playlist)
{
var startSeconds = requestedIndex * state.SegmentLength;
var position = TimeSpan.FromSeconds(startSeconds).Ticks;
try
{
using (var fileStream = GetPlaylistFileStream(playlist))
{
using (var reader = new StreamReader(fileStream))
{
double duration = -1;
while (!reader.EndOfStream)
{
var text = await reader.ReadLineAsync().ConfigureAwait(false);
if (text.StartsWith("#EXTINF", StringComparison.OrdinalIgnoreCase))
{
var parts = text.Split(new[] { ':' }, 2);
if (parts.Length == 2)
{
var time = parts[1].Trim(new[] { ',' }).Trim();
double timeValue;
if (double.TryParse(time, NumberStyles.Any, CultureInfo.InvariantCulture, out timeValue))
{
duration = timeValue;
continue;
}
}
}
else if (duration != -1)
{
SegmentLengths.AddOrUpdate(text, duration, (k, v) => duration);
Logger.Debug("Added segment length of {0} for {1}", duration, text);
}
duration = -1;
}
}
}
}
catch (FileNotFoundException)
{
}
}
private long GetSeekPositionTicks(StreamState state, string playlist, int requestedIndex)
{
double startSeconds = 0;
for (var i = 0; i < requestedIndex; i++)
{
var segmentPath = GetSegmentPath(state, playlist, i);
double length;
if (SegmentLengths.TryGetValue(Path.GetFileName(segmentPath), out length))
{
Logger.Debug("Found segment length of {0} for index {1}", length, i);
startSeconds += length;
}
else
{
startSeconds += state.SegmentLength;
}
}
var position = TimeSpan.FromSeconds(startSeconds).Ticks;
return position;
}
public int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
public int? GetCurrentTranscodingIndex(string playlist, string playSessionId, string segmentExtension)
{
var job = ApiEntryPoint.Instance.GetTranscodingJob(playlist, TranscodingJobType);
var job = string.IsNullOrWhiteSpace(playSessionId) ?
ApiEntryPoint.Instance.GetTranscodingJob(playlist, TranscodingJobType) :
ApiEntryPoint.Instance.GetTranscodingJobByPlaySessionId(playSessionId);
if (job == null || job.HasExited)
{
@ -292,7 +413,7 @@ namespace MediaBrowser.Api.Playback.Hls
{
var segmentId = "0";
var segmentRequest = request as GetDynamicHlsVideoSegment;
var segmentRequest = request as GetHlsVideoSegment;
if (segmentRequest != null)
{
segmentId = segmentRequest.SegmentId;
@ -301,13 +422,13 @@ namespace MediaBrowser.Api.Playback.Hls
return int.Parse(segmentId, NumberStyles.Integer, UsCulture);
}
private string GetSegmentPath(string playlist, int index)
private string GetSegmentPath(StreamState state, string playlist, int index)
{
var folder = Path.GetDirectoryName(playlist);
var filename = Path.GetFileNameWithoutExtension(playlist);
return Path.Combine(folder, filename + index.ToString(UsCulture) + ".ts");
return Path.Combine(folder, filename + index.ToString(UsCulture) + GetSegmentFileExtension(state));
}
private async Task<object> GetSegmentResult(string playlistPath,
@ -325,21 +446,26 @@ namespace MediaBrowser.Api.Playback.Hls
var segmentFilename = Path.GetFileName(segmentPath);
using (var fileStream = GetPlaylistFileStream(playlistPath))
while (!cancellationToken.IsCancellationRequested)
{
using (var reader = new StreamReader(fileStream))
using (var fileStream = GetPlaylistFileStream(playlistPath))
{
while (!reader.EndOfStream)
using (var reader = new StreamReader(fileStream))
{
var text = await reader.ReadLineAsync().ConfigureAwait(false);
// If it appears in the playlist, it's done
if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1)
while (!reader.EndOfStream)
{
return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob);
var text = await reader.ReadLineAsync().ConfigureAwait(false);
// If it appears in the playlist, it's done
if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1)
{
return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob);
}
}
}
}
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
// if a different file is encoding, it's done
@ -349,34 +475,35 @@ namespace MediaBrowser.Api.Playback.Hls
//return GetSegmentResult(segmentPath, segmentIndex);
//}
// Wait for the file to stop being written to, then stream it
var length = new FileInfo(segmentPath).Length;
var eofCount = 0;
while (eofCount < 10)
{
var info = new FileInfo(segmentPath);
if (!info.Exists)
{
break;
}
var newLength = info.Length;
//// Wait for the file to stop being written to, then stream it
//var length = new FileInfo(segmentPath).Length;
//var eofCount = 0;
if (newLength == length)
{
eofCount++;
}
else
{
eofCount = 0;
}
length = newLength;
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
//while (eofCount < 10)
//{
// var info = new FileInfo(segmentPath);
// if (!info.Exists)
// {
// break;
// }
// var newLength = info.Length;
// if (newLength == length)
// {
// eofCount++;
// }
// else
// {
// eofCount = 0;
// }
// length = newLength;
// await Task.Delay(100, cancellationToken).ConfigureAwait(false);
//}
cancellationToken.ThrowIfCancellationRequested();
return GetSegmentResult(segmentPath, segmentIndex, segmentLength, transcodingJob);
}
@ -400,7 +527,7 @@ namespace MediaBrowser.Api.Playback.Hls
});
}
private async Task<object> GetAsync(GetMasterHlsVideoStream request, string method)
private async Task<object> GetMasterPlaylistInternal(StreamRequest request, string method)
{
var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
@ -437,14 +564,16 @@ namespace MediaBrowser.Api.Playback.Hls
var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
playlistUrl += queryString;
var request = (GetMasterHlsVideoStream)state.Request;
var request = state.Request;
var subtitleStreams = state.MediaSource
.MediaStreams
.Where(i => i.IsTextSubtitleStream)
.ToList();
var subtitleGroup = subtitleStreams.Count > 0 && request.SubtitleMethod == SubtitleDeliveryMethod.Hls ?
var subtitleGroup = subtitleStreams.Count > 0 &&
(request is GetMasterHlsVideoPlaylist) &&
((GetMasterHlsVideoPlaylist)request).SubtitleMethod == SubtitleDeliveryMethod.Hls ?
"subs" :
null;
@ -452,7 +581,7 @@ namespace MediaBrowser.Api.Playback.Hls
if (EnableAdaptiveBitrateStreaming(state, isLiveStream))
{
var requestedVideoBitrate = state.VideoRequest.VideoBitRate.Value;
var requestedVideoBitrate = state.VideoRequest == null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
// By default, vary by just 200k
var variation = GetBitrateVariation(totalBitrate);
@ -522,7 +651,7 @@ namespace MediaBrowser.Api.Playback.Hls
return false;
}
var request = state.Request as GetMasterHlsVideoStream;
var request = state.Request as IMasterHlsRequest;
if (request != null && !request.EnableAdaptiveBitrateStreaming)
{
return false;
@ -544,6 +673,11 @@ namespace MediaBrowser.Api.Playback.Hls
return false;
}
if (!state.IsOutputVideo)
{
return false;
}
// Having problems in android
return false;
//return state.VideoRequest.VideoBitRate.HasValue;
@ -599,7 +733,7 @@ namespace MediaBrowser.Api.Playback.Hls
return variation;
}
private async Task<object> GetPlaylistAsync(VideoStreamRequest request, string name)
private async Task<object> GetVariantPlaylistInternal(StreamRequest request, bool isOutputVideo, string name)
{
var state = await GetState(request, CancellationToken.None).ConfigureAwait(false);
@ -607,7 +741,7 @@ namespace MediaBrowser.Api.Playback.Hls
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-VERSION:3");
builder.AppendLine("#EXT-X-TARGETDURATION:" + state.SegmentLength.ToString(UsCulture));
builder.AppendLine("#EXT-X-TARGETDURATION:" + (state.SegmentLength).ToString(UsCulture));
builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
var queryStringIndex = Request.RawUrl.IndexOf('?');
@ -623,10 +757,11 @@ namespace MediaBrowser.Api.Playback.Hls
builder.AppendLine("#EXTINF:" + length.ToString(UsCulture) + ",");
builder.AppendLine(string.Format("hlsdynamic/{0}/{1}.ts{2}",
builder.AppendLine(string.Format("hlsdynamic/{0}/{1}{2}{3}",
name,
index.ToString(UsCulture),
GetSegmentFileExtension(isOutputVideo),
queryString));
seconds -= state.SegmentLength;
@ -642,6 +777,28 @@ namespace MediaBrowser.Api.Playback.Hls
protected override string GetAudioArguments(StreamState state)
{
if (!state.IsOutputVideo)
{
var audioTranscodeParams = new List<string>();
if (state.OutputAudioBitrate.HasValue)
{
audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(UsCulture));
}
if (state.OutputAudioChannels.HasValue)
{
audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(UsCulture));
}
if (state.OutputAudioSampleRate.HasValue)
{
audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(UsCulture));
}
audioTranscodeParams.Add("-vn");
return string.Join(" ", audioTranscodeParams.ToArray());
}
var codec = state.OutputAudioCodec;
if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase))
@ -672,6 +829,11 @@ namespace MediaBrowser.Api.Playback.Hls
protected override string GetVideoArguments(StreamState state)
{
if (!state.IsOutputVideo)
{
return string.Empty;
}
var codec = state.OutputVideoCodec;
var args = "-codec:v:0 " + codec;
@ -684,30 +846,36 @@ namespace MediaBrowser.Api.Playback.Hls
// See if we can save come cpu cycles by avoiding encoding
if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
{
return state.VideoStream != null && IsH264(state.VideoStream) ?
args + " -bsf:v h264_mp4toannexb" :
args;
args += state.VideoStream != null && IsH264(state.VideoStream)
? args + " -bsf:v h264_mp4toannexb"
: args;
}
else
{
var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
state.SegmentLength.ToString(UsCulture));
var keyFrameArg = string.Format(" -force_key_frames expr:gte(t,n_forced*{0})",
state.SegmentLength.ToString(UsCulture));
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream;
args += " " + GetVideoQualityParam(state, H264Encoder, true) + keyFrameArg;
args += " " + GetVideoQualityParam(state, H264Encoder, true) + keyFrameArg;
//args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
// Add resolution params, if specified
if (!hasGraphicalSubs)
{
args += GetOutputSizeParam(state, codec, false);
}
// Add resolution params, if specified
if (!hasGraphicalSubs)
{
args += GetOutputSizeParam(state, codec, false);
}
// This is for internal graphical subs
if (hasGraphicalSubs)
{
args += GetGraphicalSubtitleParam(state, codec);
// This is for internal graphical subs
if (hasGraphicalSubs)
{
args += GetGraphicalSubtitleParam(state, codec);
}
}
args += " -flags +loop-global_header -sc_threshold 0";
return args;
}
@ -715,43 +883,96 @@ namespace MediaBrowser.Api.Playback.Hls
{
var threads = GetNumberOfThreads(state, false);
var inputModifier = GetInputModifier(state);
var inputModifier = GetInputModifier(state, false);
// If isEncoding is true we're actually starting ffmpeg
var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0";
if (state.EnableGenericHlsSegmenter)
var toTimeParam = string.Empty;
if (EnableSplitTranscoding(state))
{
var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d.ts";
var startTime = state.Request.StartTimeTicks ?? 0;
var durationSeconds = ApiEntryPoint.Instance.GetEncodingOptions().ThrottleThresholdInSeconds;
var endTime = startTime + TimeSpan.FromSeconds(durationSeconds).Ticks;
endTime = Math.Min(endTime, state.RunTimeTicks.Value);
if (endTime < state.RunTimeTicks.Value)
{
//toTimeParam = " -to " + MediaEncoder.GetTimeParameter(endTime);
toTimeParam = " -t " + MediaEncoder.GetTimeParameter(TimeSpan.FromSeconds(durationSeconds).Ticks);
}
}
return string.Format("{0} {1} -map_metadata -1 -threads {2} {3} {4} -flags -global_header -sc_threshold 0 {5} -f segment -segment_time {6} -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
inputModifier,
GetInputArgument(state),
threads,
GetMapArgs(state),
GetVideoArguments(state),
GetAudioArguments(state),
state.SegmentLength.ToString(UsCulture),
startNumberParam,
outputPath,
outputTsArg
).Trim();
var timestampOffsetParam = string.Empty;
if (state.IsOutputVideo)
{
timestampOffsetParam = " -output_ts_offset " + MediaEncoder.GetTimeParameter(state.Request.StartTimeTicks ?? 0).ToString(CultureInfo.InvariantCulture);
}
var mapArgs = state.IsOutputVideo ? GetMapArgs(state) : string.Empty;
//var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state);
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}\"",
//return string.Format("{0} {11} {1}{10} -map_metadata -1 -threads {2} {3} {4} {5} -f segment -segment_time {6} -segment_format mpegts -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"",
// inputModifier,
// GetInputArgument(state),
// threads,
// mapArgs,
// GetVideoArguments(state),
// GetAudioArguments(state),
// state.SegmentLength.ToString(UsCulture),
// startNumberParam,
// outputPath,
// outputTsArg,
// slowSeekParam,
// toTimeParam
// ).Trim();
return string.Format("{0}{11} {1} -map_metadata -1 -threads {2} {3} {4}{5} {6} -hls_time {7} -start_number {8} -hls_list_size {9} -y \"{10}\"",
inputModifier,
GetInputArgument(state),
threads,
GetMapArgs(state),
mapArgs,
GetVideoArguments(state),
timestampOffsetParam,
GetAudioArguments(state),
state.SegmentLength.ToString(UsCulture),
startNumberParam,
state.HlsListSize.ToString(UsCulture),
outputPath
outputPath,
toTimeParam
).Trim();
}
protected override bool EnableThrottling(StreamState state)
{
return !EnableSplitTranscoding(state);
}
private bool EnableSplitTranscoding(StreamState state)
{
if (string.Equals(Request.QueryString["EnableSplitTranscoding"], "false", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return state.RunTimeTicks.HasValue && state.IsOutputVideo;
}
protected override bool EnableStreamCopy
{
get
{
return false;
}
}
/// <summary>
/// Gets the segment file extension.
/// </summary>
@ -759,7 +980,12 @@ namespace MediaBrowser.Api.Playback.Hls
/// <returns>System.String.</returns>
protected override string GetSegmentFileExtension(StreamState state)
{
return ".ts";
return GetSegmentFileExtension(state.IsOutputVideo);
}
protected string GetSegmentFileExtension(bool isOutputVideo)
{
return isOutputVideo ? ".ts" : ".ts";
}
}
}

@ -14,8 +14,10 @@ namespace MediaBrowser.Api.Playback.Hls
[Route("/Audio/{Id}/hls/{SegmentId}/stream.mp3", "GET")]
[Route("/Audio/{Id}/hls/{SegmentId}/stream.aac", "GET")]
[Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
public class GetHlsAudioSegment
public class GetHlsAudioSegmentLegacy
{
// TODO: Deprecate with new iOS app
/// <summary>
/// Gets or sets the id.
/// </summary>
@ -29,12 +31,31 @@ namespace MediaBrowser.Api.Playback.Hls
public string SegmentId { get; set; }
}
/// <summary>
/// Class GetHlsVideoStream
/// </summary>
[Route("/Videos/{Id}/stream.m3u8", "GET")]
[Api(Description = "Gets a video stream using HTTP live streaming.")]
public class GetHlsVideoStreamLegacy : VideoStreamRequest
{
// TODO: Deprecate with new iOS app
[ApiMember(Name = "BaselineStreamAudioBitRate", Description = "Optional. Specify the audio bitrate for the baseline stream.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? BaselineStreamAudioBitRate { get; set; }
[ApiMember(Name = "AppendBaselineStream", Description = "Optional. Whether or not to include a baseline audio-only stream in the master playlist.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool AppendBaselineStream { get; set; }
[ApiMember(Name = "TimeStampOffsetMs", Description = "Optional. Alter the timestamps in the playlist by a given amount, in ms. Default is 1000.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int TimeStampOffsetMs { get; set; }
}
/// <summary>
/// Class GetHlsVideoSegment
/// </summary>
[Route("/Videos/{Id}/hls/{PlaylistId}/stream.m3u8", "GET")]
[Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
public class GetHlsPlaylist
public class GetHlsPlaylistLegacy
{
// TODO: Deprecate with new iOS app
@ -63,8 +84,10 @@ namespace MediaBrowser.Api.Playback.Hls
/// </summary>
[Route("/Videos/{Id}/hls/{PlaylistId}/{SegmentId}.ts", "GET")]
[Api(Description = "Gets an Http live streaming segment file. Internal use only.")]
public class GetHlsVideoSegment : VideoStreamRequest
public class GetHlsVideoSegmentLegacy : VideoStreamRequest
{
// TODO: Deprecate with new iOS app
public string PlaylistId { get; set; }
/// <summary>
@ -85,7 +108,7 @@ namespace MediaBrowser.Api.Playback.Hls
_config = config;
}
public object Get(GetHlsPlaylist request)
public object Get(GetHlsPlaylistLegacy request)
{
var file = request.PlaylistId + Path.GetExtension(Request.PathInfo);
file = Path.Combine(_appPaths.TranscodingTempPath, file);
@ -103,7 +126,7 @@ namespace MediaBrowser.Api.Playback.Hls
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
public object Get(GetHlsVideoSegment request)
public object Get(GetHlsVideoSegmentLegacy request)
{
var file = request.SegmentId + Path.GetExtension(Request.PathInfo);
file = Path.Combine(_config.ApplicationPaths.TranscodingTempPath, file);
@ -121,7 +144,7 @@ namespace MediaBrowser.Api.Playback.Hls
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
public object Get(GetHlsAudioSegment request)
public object Get(GetHlsAudioSegmentLegacy request)
{
// TODO: Deprecate with new iOS app
var file = request.SegmentId + Path.GetExtension(Request.PathInfo);

@ -11,25 +11,6 @@ using System;
namespace MediaBrowser.Api.Playback.Hls
{
/// <summary>
/// Class GetHlsVideoStream
/// </summary>
[Route("/Videos/{Id}/stream.m3u8", "GET")]
[Api(Description = "Gets a video stream using HTTP live streaming.")]
public class GetHlsVideoStream : VideoStreamRequest
{
// TODO: Deprecate with new iOS app
[ApiMember(Name = "BaselineStreamAudioBitRate", Description = "Optional. Specify the audio bitrate for the baseline stream.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? BaselineStreamAudioBitRate { get; set; }
[ApiMember(Name = "AppendBaselineStream", Description = "Optional. Whether or not to include a baseline audio-only stream in the master playlist.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool AppendBaselineStream { get; set; }
[ApiMember(Name = "TimeStampOffsetMs", Description = "Optional. Alter the timestamps in the playlist by a given amount, in ms. Default is 1000.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int TimeStampOffsetMs { get; set; }
}
[Route("/Videos/{Id}/live.m3u8", "GET")]
[Api(Description = "Gets a video stream using HTTP live streaming.")]
public class GetLiveHlsStream : VideoStreamRequest
@ -50,7 +31,7 @@ namespace MediaBrowser.Api.Playback.Hls
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
public object Get(GetHlsVideoStream request)
public object Get(GetHlsVideoStreamLegacy request)
{
return ProcessRequest(request, false);
}

@ -15,7 +15,7 @@ using System.IO;
namespace MediaBrowser.Api.Playback.Progressive
{
/// <summary>
/// Class GetAudioStream
/// Class GetVideoStream
/// </summary>
[Route("/Videos/{Id}/stream.ts", "GET")]
[Route("/Videos/{Id}/stream.webm", "GET")]

@ -41,7 +41,7 @@ namespace MediaBrowser.Api.Playback
public string InputContainer { get; set; }
public MediaSourceInfo MediaSource { get; set; }
public MediaStream AudioStream { get; set; }
public MediaStream VideoStream { get; set; }
public MediaStream SubtitleStream { get; set; }
@ -57,6 +57,10 @@ namespace MediaBrowser.Api.Playback
public MediaProtocol InputProtocol { get; set; }
public bool IsOutputVideo
{
get { return Request is VideoStreamRequest; }
}
public bool IsInputVideo { get; set; }
public bool IsInputArchive { get; set; }
@ -66,7 +70,6 @@ namespace MediaBrowser.Api.Playback
public List<string> PlayableStreamFileNames { get; set; }
public int SegmentLength = 3;
public bool EnableGenericHlsSegmenter = false;
public int HlsListSize
{
get

@ -42,7 +42,7 @@ namespace MediaBrowser.Api.Playback
var options = GetOptions();
if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleThresholdSeconds))
if (options.EnableThrottling && IsThrottleAllowed(_job, options.ThrottleThresholdInSeconds))
{
PauseTranscoding();
}

@ -1,7 +1,9 @@
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Security;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Plugins;
@ -25,6 +27,7 @@ namespace MediaBrowser.Api
[Authenticated]
public class GetPlugins : IReturn<List<PluginInfo>>
{
public bool? IsAppStoreEnabled { get; set; }
}
/// <summary>
@ -133,8 +136,10 @@ namespace MediaBrowser.Api
private readonly ISecurityManager _securityManager;
private readonly IInstallationManager _installationManager;
private readonly INetworkManager _network;
private readonly IDeviceManager _deviceManager;
public PluginService(IJsonSerializer jsonSerializer, IApplicationHost appHost, ISecurityManager securityManager, IInstallationManager installationManager)
public PluginService(IJsonSerializer jsonSerializer, IApplicationHost appHost, ISecurityManager securityManager, IInstallationManager installationManager, INetworkManager network, IDeviceManager deviceManager)
: base()
{
if (jsonSerializer == null)
@ -145,6 +150,8 @@ namespace MediaBrowser.Api
_appHost = appHost;
_securityManager = securityManager;
_installationManager = installationManager;
_network = network;
_deviceManager = deviceManager;
_jsonSerializer = jsonSerializer;
}
@ -164,13 +171,15 @@ namespace MediaBrowser.Api
{
var result = await _securityManager.GetRegistrationStatus(request.Name).ConfigureAwait(false);
return ToOptimizedResult(new RegistrationInfo
var info = new RegistrationInfo
{
ExpirationDate = result.ExpirationDate,
IsRegistered = result.IsRegistered,
IsTrial = result.TrialVersion,
Name = request.Name
});
};
return ToOptimizedResult(info);
}
/// <summary>
@ -181,6 +190,7 @@ namespace MediaBrowser.Api
public async Task<object> Get(GetPlugins request)
{
var result = _appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()).ToList();
var requireAppStoreEnabled = request.IsAppStoreEnabled.HasValue && request.IsAppStoreEnabled.Value;
// Don't fail just on account of image url's
try
@ -197,10 +207,26 @@ namespace MediaBrowser.Api
plugin.ImageUrl = pkg.thumbImage;
}
}
if (requireAppStoreEnabled)
{
result = result
.Where(plugin =>
{
var pkg = packages.FirstOrDefault(i => !string.IsNullOrWhiteSpace(i.guid) && new Guid(plugin.Id).Equals(new Guid(i.guid)));
return pkg != null && pkg.enableInAppStore;
})
.ToList();
}
}
catch
{
// Play it safe here
if (requireAppStoreEnabled)
{
result = new List<PluginInfo>();
}
}
return ToOptimizedSerializedResultUsingCache(result);

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MediaBrowser.Api.Reports
{
public enum HeaderMetadata
{
None,
Name,
PremiereDate,
DateAdded,
ReleaseDate,
Runtime,
PlayCount,
Season,
SeasonNumber,
Series,
Network,
Year,
ParentalRating,
CommunityRating,
Trailers,
Specials,
GameSystem,
Players,
AlbumArtist,
Album,
Disc,
Track,
Audio,
EmbeddedImage,
Video,
Resolution,
Subtitles,
Genres,
Countries,
StatusImage,
Tracks,
EpisodeSeries,
EpisodeSeason,
AudioAlbumArtist,
MusicArtist,
AudioAlbum,
Status
}
}

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MediaBrowser.Api.Reports
{
public enum ItemViewType
{
None,
Detail,
Edit,
List,
ItemByNameDetails,
StatusImage,
EmbeddedImage,
SubtitleImage,
TrailersImage,
SpecialsImage
}
}

@ -0,0 +1,229 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Channels;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Api.Reports
{
/// <summary> A report builder base. </summary>
public class ReportBuilderBase
{
/// <summary>
/// Initializes a new instance of the MediaBrowser.Api.Reports.ReportBuilderBase class. </summary>
/// <param name="libraryManager"> Manager for library. </param>
public ReportBuilderBase(ILibraryManager libraryManager)
{
_libraryManager = libraryManager;
}
/// <summary> Manager for library. </summary>
protected readonly ILibraryManager _libraryManager;
/// <summary> Gets audio stream. </summary>
/// <param name="item"> The item. </param>
/// <returns> The audio stream. </returns>
protected string GetAudioStream(BaseItem item)
{
var stream = GetStream(item, MediaStreamType.Audio);
if (stream != null)
return stream.Codec.ToUpper() == "DCA" ? stream.Profile : stream.Codec.
ToUpper();
return string.Empty;
}
/// <summary> Gets an episode. </summary>
/// <param name="item"> The item. </param>
/// <returns> The episode. </returns>
protected string GetEpisode(BaseItem item)
{
if (item.GetClientTypeName() == ChannelMediaContentType.Episode.ToString() && item.ParentIndexNumber != null)
return "Season " + item.ParentIndexNumber;
else
return item.Name;
}
/// <summary> Gets a genre. </summary>
/// <param name="name"> The name. </param>
/// <returns> The genre. </returns>
protected Genre GetGenre(string name)
{
if (string.IsNullOrEmpty(name))
return null;
return _libraryManager.GetGenre(name);
}
/// <summary> Gets genre identifier. </summary>
/// <param name="name"> The name. </param>
/// <returns> The genre identifier. </returns>
protected string GetGenreID(string name)
{
if (string.IsNullOrEmpty(name))
return string.Empty;
return string.Format("{0:N}",
GetGenre(name).Id);
}
/// <summary> Gets list as string. </summary>
/// <param name="items"> The items. </param>
/// <returns> The list as string. </returns>
protected string GetListAsString(List<string> items)
{
return String.Join("; ", items);
}
/// <summary> Gets media source information. </summary>
/// <param name="item"> The item. </param>
/// <returns> The media source information. </returns>
protected MediaSourceInfo GetMediaSourceInfo(BaseItem item)
{
var mediaSource = item as IHasMediaSources;
if (mediaSource != null)
return mediaSource.GetMediaSources(false).FirstOrDefault(n => n.Type == MediaSourceType.Default);
return null;
}
/// <summary> Gets an object. </summary>
/// <typeparam name="T"> Generic type parameter. </typeparam>
/// <typeparam name="R"> Type of the r. </typeparam>
/// <param name="item"> The item. </param>
/// <param name="function"> The function. </param>
/// <param name="defaultValue"> The default value. </param>
/// <returns> The object. </returns>
protected R GetObject<T, R>(BaseItem item, Func<T, R> function, R defaultValue = default(R)) where T : class
{
var value = item as T;
if (value != null && function != null)
return function(value);
else
return defaultValue;
}
/// <summary> Gets a person. </summary>
/// <param name="name"> The name. </param>
/// <returns> The person. </returns>
protected Person GetPerson(string name)
{
if (string.IsNullOrEmpty(name))
return null;
return _libraryManager.GetPerson(name);
}
/// <summary> Gets person identifier. </summary>
/// <param name="name"> The name. </param>
/// <returns> The person identifier. </returns>
protected string GetPersonID(string name)
{
if (string.IsNullOrEmpty(name))
return string.Empty;
return string.Format("{0:N}",
GetPerson(name).Id);
}
/// <summary> Gets runtime date time. </summary>
/// <param name="runtime"> The runtime. </param>
/// <returns> The runtime date time. </returns>
protected double? GetRuntimeDateTime(long? runtime)
{
if (runtime.HasValue)
return Math.Ceiling(new TimeSpan(runtime.Value).TotalMinutes);
return null;
}
/// <summary> Gets series production year. </summary>
/// <param name="item"> The item. </param>
/// <returns> The series production year. </returns>
protected string GetSeriesProductionYear(BaseItem item)
{
string productionYear = item.ProductionYear.ToString();
var series = item as Series;
if (series == null)
{
if (item.ProductionYear == null || item.ProductionYear == 0)
return string.Empty;
return productionYear;
}
if (series.Status == SeriesStatus.Continuing)
return productionYear += "-Present";
if (series.EndDate != null && series.EndDate.Value.Year != series.ProductionYear)
return productionYear += "-" + series.EndDate.Value.Year;
return productionYear;
}
/// <summary> Gets a stream. </summary>
/// <param name="item"> The item. </param>
/// <param name="streamType"> Type of the stream. </param>
/// <returns> The stream. </returns>
protected MediaStream GetStream(BaseItem item, MediaStreamType streamType)
{
var itemInfo = GetMediaSourceInfo(item);
if (itemInfo != null)
return itemInfo.MediaStreams.FirstOrDefault(n => n.Type == streamType);
return null;
}
/// <summary> Gets a studio. </summary>
/// <param name="name"> The name. </param>
/// <returns> The studio. </returns>
protected Studio GetStudio(string name)
{
if (string.IsNullOrEmpty(name))
return null;
return _libraryManager.GetStudio(name);
}
/// <summary> Gets studio identifier. </summary>
/// <param name="name"> The name. </param>
/// <returns> The studio identifier. </returns>
protected string GetStudioID(string name)
{
if (string.IsNullOrEmpty(name))
return string.Empty;
return string.Format("{0:N}",
GetStudio(name).Id);
}
/// <summary> Gets video resolution. </summary>
/// <param name="item"> The item. </param>
/// <returns> The video resolution. </returns>
protected string GetVideoResolution(BaseItem item)
{
var stream = GetStream(item,
MediaStreamType.Video);
if (stream != null && stream.Width != null)
return string.Format("{0} * {1}",
stream.Width,
(stream.Height != null ? stream.Height.ToString() : "-"));
return string.Empty;
}
/// <summary> Gets video stream. </summary>
/// <param name="item"> The item. </param>
/// <returns> The video stream. </returns>
protected string GetVideoStream(BaseItem item)
{
var stream = GetStream(item, MediaStreamType.Video);
if (stream != null)
return stream.Codec.ToUpper();
return string.Empty;
}
}
}

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MediaBrowser.Api.Reports
{
public enum ReportExportType
{
CSV,
Excel
}
}

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MediaBrowser.Api.Reports
{
public enum ReportFieldType
{
String,
Boolean,
Date,
Time,
DateTime,
Int,
Image,
Object,
Minutes
}
}

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MediaBrowser.Api.Reports
{
public enum ReportHeaderIdType
{
Row,
Item
}
}

@ -0,0 +1,101 @@
using MediaBrowser.Controller.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Api.Reports
{
public class ReportHelper
{
/// <summary> Gets java script localized string. </summary>
/// <param name="phrase"> The phrase. </param>
/// <returns> The java script localized string. </returns>
public static string GetJavaScriptLocalizedString(string phrase)
{
var dictionary = BaseItem.LocalizationManager.GetJavaScriptLocalizationDictionary(BaseItem.ConfigurationManager.Configuration.UICulture);
string value;
if (dictionary.TryGetValue(phrase, out value))
{
return value;
}
return phrase;
}
/// <summary> Gets server localized string. </summary>
/// <param name="phrase"> The phrase. </param>
/// <returns> The server localized string. </returns>
public static string GetServerLocalizedString(string phrase)
{
return BaseItem.LocalizationManager.GetLocalizedString(phrase, BaseItem.ConfigurationManager.Configuration.UICulture);
}
/// <summary> Gets row type. </summary>
/// <param name="rowType"> The type. </param>
/// <returns> The row type. </returns>
public static ReportViewType GetRowType(string rowType)
{
if (string.IsNullOrEmpty(rowType))
return ReportViewType.BaseItem;
ReportViewType rType;
if (!Enum.TryParse<ReportViewType>(rowType, out rType))
return ReportViewType.BaseItem;
return rType;
}
/// <summary> Gets header metadata type. </summary>
/// <param name="header"> The header. </param>
/// <returns> The header metadata type. </returns>
public static HeaderMetadata GetHeaderMetadataType(string header)
{
if (string.IsNullOrEmpty(header))
return HeaderMetadata.None;
HeaderMetadata rType;
if (!Enum.TryParse<HeaderMetadata>(header, out rType))
return HeaderMetadata.None;
return rType;
}
/// <summary> Convert field to string. </summary>
/// <typeparam name="T"> Generic type parameter. </typeparam>
/// <param name="value"> The value. </param>
/// <param name="fieldType"> Type of the field. </param>
/// <returns> The field converted to string. </returns>
public static string ConvertToString<T>(T value, ReportFieldType fieldType)
{
if (value == null)
return "";
switch (fieldType)
{
case ReportFieldType.String:
return value.ToString();
case ReportFieldType.Boolean:
return value.ToString();
case ReportFieldType.Date:
return string.Format("{0:d}", value);
case ReportFieldType.Time:
return string.Format("{0:t}", value);
case ReportFieldType.DateTime:
return string.Format("{0:d}", value);
case ReportFieldType.Minutes:
return string.Format("{0}mn", value);
case ReportFieldType.Int:
return string.Format("", value);
default:
if (value is Guid)
return string.Format("{0:N}", value);
return value.ToString();
}
}
}
}

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MediaBrowser.Api.Reports
{
public enum ReportViewType
{
MusicArtist,
MusicAlbum,
Book,
BoxSet,
Episode,
Game,
Video,
Movie,
MusicVideo,
Trailer,
Season,
Series,
Audio,
BaseItem,
Artist
}
}

@ -0,0 +1,589 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Localization;
using MediaBrowser.Model.Channels;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Api.Reports
{
/// <summary> A report builder. </summary>
/// <seealso cref="T:MediaBrowser.Api.Reports.ReportBuilderBase"/>
public class ReportBuilder : ReportBuilderBase
{
/// <summary>
/// Initializes a new instance of the MediaBrowser.Api.Reports.ReportBuilder class. </summary>
/// <param name="libraryManager"> Manager for library. </param>
public ReportBuilder(ILibraryManager libraryManager)
: base(libraryManager)
{
}
private Func<bool, string> GetBoolString = s => s == true ? "x" : "";
public ReportResult GetReportResult(BaseItem[] items, ReportViewType reportRowType, BaseReportRequest request)
{
List<HeaderMetadata> headersMetadata = this.GetFilteredReportHeaderMetadata(reportRowType, request);
var headers = GetReportHeaders(reportRowType, headersMetadata);
var rows = GetReportRows(items, headersMetadata);
ReportResult result = new ReportResult { Headers = headers };
HeaderMetadata groupBy = ReportHelper.GetHeaderMetadataType(request.GroupBy);
int i = headers.FindIndex(x => x.FieldName == groupBy);
if (groupBy != HeaderMetadata.None && i > 0)
{
var rowsGroup = rows.SelectMany(x => x.Columns[i].Name.Split(';'), (x, g) => new { Genre = g.Trim(), Rows = x })
.GroupBy(x => x.Genre)
.OrderBy(x => x.Key)
.Select(x => new ReportGroup { Name = x.Key, Rows = x.Select(r => r.Rows).ToList() });
result.Groups = rowsGroup.ToList();
result.IsGrouped = true;
}
else
{
result.Rows = rows;
result.IsGrouped = false;
}
return result;
}
public List<ReportHeader> GetReportHeaders(ReportViewType reportRowType, BaseReportRequest request)
{
List<ReportHeader> headersMetadata = this.GetReportHeaders(reportRowType);
if (request != null && !string.IsNullOrEmpty(request.ReportColumns))
{
List<HeaderMetadata> headersMetadataFiltered = this.GetFilteredReportHeaderMetadata(reportRowType, request);
foreach (ReportHeader reportHeader in headersMetadata)
{
if (!headersMetadataFiltered.Contains(reportHeader.FieldName))
{
reportHeader.Visible = false;
}
}
}
return headersMetadata;
}
public List<ReportHeader> GetReportHeaders(ReportViewType reportRowType, List<HeaderMetadata> headersMetadata = null)
{
if (headersMetadata == null)
headersMetadata = this.GetDefaultReportHeaderMetadata(reportRowType);
List<ReportOptions<BaseItem>> options = new List<ReportOptions<BaseItem>>();
foreach (HeaderMetadata header in headersMetadata)
{
options.Add(GetReportOption(header));
}
List<ReportHeader> headers = new List<ReportHeader>();
foreach (ReportOptions<BaseItem> option in options)
{
headers.Add(option.Header);
}
return headers;
}
private List<ReportRow> GetReportRows(IEnumerable<BaseItem> items, List<HeaderMetadata> headersMetadata)
{
List<ReportOptions<BaseItem>> options = new List<ReportOptions<BaseItem>>();
foreach (HeaderMetadata header in headersMetadata)
{
options.Add(GetReportOption(header));
}
var rows = new List<ReportRow>();
foreach (BaseItem item in items)
{
ReportRow rRow = GetRow(item);
foreach (ReportOptions<BaseItem> option in options)
{
object itemColumn = option.Column != null ? option.Column(item, rRow) : "";
object itemId = option.ItemID != null ? option.ItemID(item) : "";
ReportItem rItem = new ReportItem
{
Name = ReportHelper.ConvertToString(itemColumn, option.Header.HeaderFieldType),
Id = ReportHelper.ConvertToString(itemId, ReportFieldType.Object)
};
rRow.Columns.Add(rItem);
}
rows.Add(rRow);
}
return rows;
}
/// <summary> Gets a row. </summary>
/// <param name="item"> The item. </param>
/// <returns> The row. </returns>
private ReportRow GetRow(BaseItem item)
{
var hasTrailers = item as IHasTrailers;
var hasSpecialFeatures = item as IHasSpecialFeatures;
var video = item as Video;
ReportRow rRow = new ReportRow
{
Id = item.Id.ToString("N"),
HasLockData = item.IsLocked,
IsUnidentified = item.IsUnidentified,
HasLocalTrailer = hasTrailers != null ? hasTrailers.GetTrailerIds().Count() > 0 : false,
HasImageTagsPrimary = (item.ImageInfos != null && item.ImageInfos.Count(n => n.Type == ImageType.Primary) > 0),
HasImageTagsBackdrop = (item.ImageInfos != null && item.ImageInfos.Count(n => n.Type == ImageType.Backdrop) > 0),
HasImageTagsLogo = (item.ImageInfos != null && item.ImageInfos.Count(n => n.Type == ImageType.Logo) > 0),
HasSpecials = hasSpecialFeatures != null ? hasSpecialFeatures.SpecialFeatureIds.Count > 0 : false,
HasSubtitles = video != null ? video.HasSubtitles : false,
RowType = ReportHelper.GetRowType(item.GetClientTypeName())
};
return rRow;
}
public List<HeaderMetadata> GetFilteredReportHeaderMetadata(ReportViewType reportRowType, BaseReportRequest request)
{
if (request != null && !string.IsNullOrEmpty(request.ReportColumns))
{
var s = request.ReportColumns.Split('|').Select(x => ReportHelper.GetHeaderMetadataType(x)).Where(x => x != HeaderMetadata.None);
return s.ToList();
}
else
return this.GetDefaultReportHeaderMetadata(reportRowType);
}
public List<HeaderMetadata> GetDefaultReportHeaderMetadata(ReportViewType reportRowType)
{
switch (reportRowType)
{
case ReportViewType.Season:
return new List<HeaderMetadata>
{
HeaderMetadata.StatusImage,
HeaderMetadata.Series,
HeaderMetadata.Season,
HeaderMetadata.SeasonNumber,
HeaderMetadata.DateAdded,
HeaderMetadata.Year,
HeaderMetadata.Genres
};
case ReportViewType.Series:
return new List<HeaderMetadata>
{
HeaderMetadata.StatusImage,
HeaderMetadata.Name,
HeaderMetadata.Network,
HeaderMetadata.DateAdded,
HeaderMetadata.Year,
HeaderMetadata.Genres,
HeaderMetadata.ParentalRating,
HeaderMetadata.CommunityRating,
HeaderMetadata.Runtime,
HeaderMetadata.Trailers,
HeaderMetadata.Specials
};
case ReportViewType.MusicAlbum:
return new List<HeaderMetadata>
{
HeaderMetadata.StatusImage,
HeaderMetadata.Name,
HeaderMetadata.AlbumArtist,
HeaderMetadata.DateAdded,
HeaderMetadata.ReleaseDate,
HeaderMetadata.Tracks,
HeaderMetadata.Year,
HeaderMetadata.Genres
};
case ReportViewType.MusicArtist:
return new List<HeaderMetadata>
{
HeaderMetadata.StatusImage,
HeaderMetadata.MusicArtist,
HeaderMetadata.Countries,
HeaderMetadata.DateAdded,
HeaderMetadata.Year,
HeaderMetadata.Genres
};
case ReportViewType.Game:
return new List<HeaderMetadata>
{
HeaderMetadata.StatusImage,
HeaderMetadata.Name,
HeaderMetadata.GameSystem,
HeaderMetadata.DateAdded,
HeaderMetadata.ReleaseDate,
HeaderMetadata.ParentalRating,
HeaderMetadata.CommunityRating,
HeaderMetadata.Players,
HeaderMetadata.Year,
HeaderMetadata.Genres,
HeaderMetadata.Trailers
};
case ReportViewType.Movie:
return new List<HeaderMetadata>
{
HeaderMetadata.StatusImage,
HeaderMetadata.Name,
HeaderMetadata.DateAdded,
HeaderMetadata.ReleaseDate,
HeaderMetadata.Year,
HeaderMetadata.Genres,
HeaderMetadata.ParentalRating,
HeaderMetadata.CommunityRating,
HeaderMetadata.Runtime,
HeaderMetadata.Video,
HeaderMetadata.Resolution,
HeaderMetadata.Audio,
HeaderMetadata.Subtitles,
HeaderMetadata.Trailers,
HeaderMetadata.Specials
};
case ReportViewType.Book:
return new List<HeaderMetadata>
{
HeaderMetadata.StatusImage,
HeaderMetadata.Name,
HeaderMetadata.DateAdded,
HeaderMetadata.ReleaseDate,
HeaderMetadata.Year,
HeaderMetadata.Genres,
HeaderMetadata.ParentalRating,
HeaderMetadata.CommunityRating
};
case ReportViewType.BoxSet:
return new List<HeaderMetadata>
{
HeaderMetadata.StatusImage,
HeaderMetadata.Name,
HeaderMetadata.DateAdded,
HeaderMetadata.ReleaseDate,
HeaderMetadata.Year,
HeaderMetadata.Genres,
HeaderMetadata.ParentalRating,
HeaderMetadata.CommunityRating,
HeaderMetadata.Trailers
};
case ReportViewType.Audio:
return new List<HeaderMetadata>
{
HeaderMetadata.StatusImage,
HeaderMetadata.Name,
HeaderMetadata.AudioAlbumArtist,
HeaderMetadata.AudioAlbum,
HeaderMetadata.Disc,
HeaderMetadata.Track,
HeaderMetadata.DateAdded,
HeaderMetadata.ReleaseDate,
HeaderMetadata.Year,
HeaderMetadata.Genres,
HeaderMetadata.ParentalRating,
HeaderMetadata.CommunityRating,
HeaderMetadata.Runtime,
HeaderMetadata.Audio
};
case ReportViewType.Episode:
return new List<HeaderMetadata>
{
HeaderMetadata.StatusImage,
HeaderMetadata.Name,
HeaderMetadata.EpisodeSeries,
HeaderMetadata.Season,
HeaderMetadata.DateAdded,
HeaderMetadata.ReleaseDate,
HeaderMetadata.Year,
HeaderMetadata.Genres,
HeaderMetadata.ParentalRating,
HeaderMetadata.CommunityRating,
HeaderMetadata.Runtime,
HeaderMetadata.Video,
HeaderMetadata.Resolution,
HeaderMetadata.Audio,
HeaderMetadata.Subtitles,
HeaderMetadata.Trailers,
HeaderMetadata.Specials
};
case ReportViewType.Video:
case ReportViewType.MusicVideo:
case ReportViewType.Trailer:
case ReportViewType.BaseItem:
default:
return new List<HeaderMetadata>
{
HeaderMetadata.StatusImage,
HeaderMetadata.Name,
HeaderMetadata.DateAdded,
HeaderMetadata.ReleaseDate,
HeaderMetadata.Year,
HeaderMetadata.Genres,
HeaderMetadata.ParentalRating,
HeaderMetadata.CommunityRating,
HeaderMetadata.Runtime,
HeaderMetadata.Video,
HeaderMetadata.Resolution,
HeaderMetadata.Audio,
HeaderMetadata.Subtitles,
HeaderMetadata.Trailers,
HeaderMetadata.Specials
};
}
}
/// <summary> Gets report option. </summary>
/// <param name="header"> The header. </param>
/// <param name="sortField"> The sort field. </param>
/// <returns> The report option. </returns>
private ReportOptions<BaseItem> GetReportOption(HeaderMetadata header, string sortField = "")
{
ReportHeader reportHeader = new ReportHeader
{
HeaderFieldType = ReportFieldType.String,
SortField = sortField,
Type = "",
ItemViewType = ItemViewType.None
};
Func<BaseItem, ReportRow, object> column = null;
Func<BaseItem, object> itemId = null;
HeaderMetadata internalHeader = header;
switch (header)
{
case HeaderMetadata.StatusImage:
reportHeader.ItemViewType = ItemViewType.StatusImage;
internalHeader = HeaderMetadata.Status;
reportHeader.CanGroup = false;
break;
case HeaderMetadata.Name:
column = (i, r) => i.Name;
reportHeader.ItemViewType = ItemViewType.Detail;
reportHeader.SortField = "SortName";
break;
case HeaderMetadata.DateAdded:
column = (i, r) => i.DateCreated;
reportHeader.SortField = "DateCreated,SortName";
reportHeader.HeaderFieldType = ReportFieldType.DateTime;
reportHeader.Type = "";
break;
case HeaderMetadata.PremiereDate:
case HeaderMetadata.ReleaseDate:
column = (i, r) => i.PremiereDate;
reportHeader.HeaderFieldType = ReportFieldType.DateTime;
reportHeader.SortField = "ProductionYear,PremiereDate,SortName";
break;
case HeaderMetadata.Runtime:
column = (i, r) => this.GetRuntimeDateTime(i.RunTimeTicks);
reportHeader.HeaderFieldType = ReportFieldType.Minutes;
reportHeader.SortField = "Runtime,SortName";
break;
case HeaderMetadata.PlayCount:
reportHeader.HeaderFieldType = ReportFieldType.Int;
break;
case HeaderMetadata.Season:
column = (i, r) => this.GetEpisode(i);
reportHeader.ItemViewType = ItemViewType.Detail;
reportHeader.SortField = "SortName";
break;
case HeaderMetadata.SeasonNumber:
column = (i, r) => this.GetObject<Season, string>(i, (x) => x.IndexNumber == null ? "" : x.IndexNumber.ToString());
reportHeader.SortField = "IndexNumber";
reportHeader.HeaderFieldType = ReportFieldType.Int;
break;
case HeaderMetadata.Series:
column = (i, r) => this.GetObject<IHasSeries, string>(i, (x) => x.SeriesName);
reportHeader.ItemViewType = ItemViewType.Detail;
reportHeader.SortField = "SeriesSortName,SortName";
break;
case HeaderMetadata.EpisodeSeries:
column = (i, r) => this.GetObject<IHasSeries, string>(i, (x) => x.SeriesName);
reportHeader.ItemViewType = ItemViewType.Detail;
itemId = (i) =>
{
Series series = this.GetObject<Episode, Series>(i, (x) => x.Series);
if (series == null)
return string.Empty;
return series.Id;
};
reportHeader.SortField = "SeriesSortName,SortName";
internalHeader = HeaderMetadata.Series;
break;
case HeaderMetadata.EpisodeSeason:
column = (i, r) => this.GetObject<IHasSeries, string>(i, (x) => x.SeriesName);
reportHeader.ItemViewType = ItemViewType.Detail;
itemId = (i) =>
{
Season season = this.GetObject<Episode, Season>(i, (x) => x.Season);
if (season == null)
return string.Empty;
return season.Id;
};
reportHeader.SortField = "SortName";
internalHeader = HeaderMetadata.Season;
break;
case HeaderMetadata.Network:
column = (i, r) => this.GetListAsString(i.Studios);
itemId = (i) => this.GetStudioID(i.Studios.FirstOrDefault());
reportHeader.ItemViewType = ItemViewType.ItemByNameDetails;
reportHeader.SortField = "Studio,SortName";
break;
case HeaderMetadata.Year:
column = (i, r) => this.GetSeriesProductionYear(i);
reportHeader.SortField = "ProductionYear,PremiereDate,SortName";
break;
case HeaderMetadata.ParentalRating:
column = (i, r) => i.OfficialRating;
reportHeader.SortField = "OfficialRating,SortName";
break;
case HeaderMetadata.CommunityRating:
column = (i, r) => i.CommunityRating;
reportHeader.SortField = "CommunityRating,SortName";
break;
case HeaderMetadata.Trailers:
column = (i, r) => this.GetBoolString(r.HasLocalTrailer);
reportHeader.ItemViewType = ItemViewType.TrailersImage;
break;
case HeaderMetadata.Specials:
column = (i, r) => this.GetBoolString(r.HasSpecials);
reportHeader.ItemViewType = ItemViewType.SpecialsImage;
break;
case HeaderMetadata.GameSystem:
column = (i, r) => this.GetObject<Game, string>(i, (x) => x.GameSystem);
reportHeader.SortField = "GameSystem,SortName";
break;
case HeaderMetadata.Players:
column = (i, r) => this.GetObject<Game, int?>(i, (x) => x.PlayersSupported);
reportHeader.SortField = "Players,GameSystem,SortName";
break;
case HeaderMetadata.AlbumArtist:
column = (i, r) => this.GetObject<MusicAlbum, string>(i, (x) => x.AlbumArtist);
itemId = (i) => this.GetPersonID(this.GetObject<MusicAlbum, string>(i, (x) => x.AlbumArtist));
reportHeader.ItemViewType = ItemViewType.Detail;
reportHeader.SortField = "AlbumArtist,Album,SortName";
break;
case HeaderMetadata.MusicArtist:
column = (i, r) => this.GetObject<MusicArtist, string>(i, (x) => x.GetLookupInfo().Name);
reportHeader.ItemViewType = ItemViewType.Detail;
reportHeader.SortField = "AlbumArtist,Album,SortName";
internalHeader = HeaderMetadata.AlbumArtist;
break;
case HeaderMetadata.AudioAlbumArtist:
column = (i, r) => this.GetListAsString(this.GetObject<Audio, List<string>>(i, (x) => x.AlbumArtists));
reportHeader.SortField = "AlbumArtist,Album,SortName";
internalHeader = HeaderMetadata.AlbumArtist;
break;
case HeaderMetadata.AudioAlbum:
column = (i, r) => this.GetObject<Audio, string>(i, (x) => x.Album);
reportHeader.SortField = "Album,SortName";
internalHeader = HeaderMetadata.Album;
break;
case HeaderMetadata.Countries:
column = (i, r) => this.GetListAsString(this.GetObject<IHasProductionLocations, List<string>>(i, (x) => x.ProductionLocations));
break;
case HeaderMetadata.Disc:
column = (i, r) => i.ParentIndexNumber;
break;
case HeaderMetadata.Track:
column = (i, r) => i.IndexNumber;
break;
case HeaderMetadata.Tracks:
column = (i, r) => this.GetObject<MusicAlbum, List<Audio>>(i, (x) => x.Tracks.ToList(), new List<Audio>()).Count();
break;
case HeaderMetadata.Audio:
column = (i, r) => this.GetAudioStream(i);
break;
case HeaderMetadata.EmbeddedImage:
break;
case HeaderMetadata.Video:
column = (i, r) => this.GetVideoStream(i);
break;
case HeaderMetadata.Resolution:
column = (i, r) => this.GetVideoResolution(i);
break;
case HeaderMetadata.Subtitles:
column = (i, r) => this.GetBoolString(r.HasSubtitles);
reportHeader.ItemViewType = ItemViewType.SubtitleImage;
break;
case HeaderMetadata.Genres:
column = (i, r) => this.GetListAsString(i.Genres);
break;
}
string headerName = "";
if (internalHeader != HeaderMetadata.None)
{
string localHeader = "Header" + internalHeader.ToString();
headerName = internalHeader != HeaderMetadata.None ? ReportHelper.GetJavaScriptLocalizedString(localHeader) : "";
if (string.Compare(localHeader, headerName, StringComparison.CurrentCultureIgnoreCase) == 0)
headerName = ReportHelper.GetServerLocalizedString(localHeader);
}
reportHeader.Name = headerName;
reportHeader.FieldName = header;
ReportOptions<BaseItem> option = new ReportOptions<BaseItem>()
{
Header = reportHeader,
Column = column,
ItemID = itemId
};
return option;
}
}
}

@ -0,0 +1,212 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Api.Reports
{
/// <summary> A report export. </summary>
public class ReportExport
{
/// <summary> Export to CSV. </summary>
/// <param name="reportResult"> The report result. </param>
/// <returns> A string. </returns>
public string ExportToCsv(ReportResult reportResult)
{
StringBuilder returnValue = new StringBuilder();
returnValue.AppendLine(string.Join(";", reportResult.Headers.Select(s => s.Name.Replace(',', ' ')).ToArray()));
if (reportResult.IsGrouped)
foreach (ReportGroup group in reportResult.Groups)
{
foreach (ReportRow row in reportResult.Rows)
{
returnValue.AppendLine(string.Join(";", row.Columns.Select(s => s.Name.Replace(',', ' ')).ToArray()));
}
}
else
foreach (ReportRow row in reportResult.Rows)
{
returnValue.AppendLine(string.Join(";", row.Columns.Select(s => s.Name.Replace(',', ' ')).ToArray()));
}
return returnValue.ToString();
}
/// <summary> Export to excel. </summary>
/// <param name="reportResult"> The report result. </param>
/// <returns> A string. </returns>
public string ExportToExcel(ReportResult reportResult)
{
string style = @"<style type='text/css'>
BODY {
font-family: Arial;
font-size: 12px;
}
TABLE {
font-family: Arial;
font-size: 12px;
}
A {
font-family: Arial;
color: #144A86;
font-size: 12px;
cursor: pointer;
text-decoration: none;
font-weight: bold;
}
DIV {
font-family: Arial;
font-size: 12px;
margin-bottom: 0px;
}
P, LI, DIV {
font-size: 12px;
margin-bottom: 0px;
}
P, UL {
font-size: 12px;
margin-bottom: 6px;
margin-top: 0px;
}
H1 {
font-size: 18pt;
}
H2 {
font-weight: bold;
font-size: 14pt;
COLOR: #C0C0C0;
}
H3 {
font-weight: normal;
font-size: 14pt;
text-indent: +1em;
}
H4 {
font-size: 10pt;
font-weight: normal;
}
H5 {
font-size: 10pt;
font-weight: normal;
background: #A9A9A9;
COLOR: white;
display: inline;
}
H6 {
padding: 2 1 2 5;
font-size: 11px;
font-weight: bold;
text-decoration: none;
margin-bottom: 1px;
}
UL {
line-height: 1.5em;
list-style-type: disc;
}
OL {
line-height: 1.5em;
}
LI {
line-height: 1.5em;
}
A IMG {
border: 0;
}
table.gridtable {
color: #333333;
border-width: 0.1pt;
border-color: #666666;
border-collapse: collapse;
}
table.gridtable th {
border-width: 0.1pt;
padding: 8px;
border-style: solid;
border-color: #666666;
background-color: #dedede;
}
table.gridtable tr {
background-color: #ffffff;
}
table.gridtable td {
border-width: 0.1pt;
padding: 8px;
border-style: solid;
border-color: #666666;
background-color: #ffffff;
}
</style>";
string Html = @"<!DOCTYPE html>
<html xmlns='http://www.w3.org/1999/xhtml'>
<head>
<meta http-equiv='X-UA-Compatible' content='IE=8, IE=9, IE=10' />
<meta charset='utf-8'>
<title>Emby Reports Export</title>";
Html += "\n" + style + "\n";
Html += "</head>\n";
Html += "<body>\n";
StringBuilder returnValue = new StringBuilder();
returnValue.AppendLine("<table class='gridtable'>");
returnValue.AppendLine("<tr>");
returnValue.AppendLine(string.Join("", reportResult.Headers.Select(s => string.Format("<th>{0}</th>", s.Name)).ToArray()));
returnValue.AppendLine("</tr>");
if (reportResult.IsGrouped)
foreach (ReportGroup group in reportResult.Groups)
{
returnValue.AppendLine("<tr>");
returnValue.AppendLine("<th scope='rowgroup' colspan='" + reportResult.Headers.Count + "'>" + (string.IsNullOrEmpty(group.Name) ? "&nbsp;" : group.Name) + "</th>");
returnValue.AppendLine("</tr>");
foreach (ReportRow row in group.Rows)
{
ExportToExcelRow(reportResult, returnValue, row);
}
returnValue.AppendLine("<tr>");
returnValue.AppendLine("<th style='background-color: #ffffff;' scope='rowgroup' colspan='" + reportResult.Headers.Count + "'>" + "&nbsp;" + "</th>");
returnValue.AppendLine("</tr>");
}
else
foreach (ReportRow row in reportResult.Rows)
{
ExportToExcelRow(reportResult, returnValue, row);
}
returnValue.AppendLine("</table>");
Html += returnValue.ToString();
Html += "</body>";
Html += "</html>";
return Html;
}
private static void ExportToExcelRow(ReportResult reportResult,
StringBuilder returnValue,
ReportRow row)
{
returnValue.AppendLine("<tr>");
returnValue.AppendLine(string.Join("", row.Columns.Select(s => string.Format("<td>{0}</td>", s.Name)).ToArray()));
returnValue.AppendLine("</tr>");
}
}
}

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Api.Reports
{
/// <summary> A report group. </summary>
public class ReportGroup
{
/// <summary>
/// Initializes a new instance of the MediaBrowser.Api.Reports.ReportGroup class. </summary>
public ReportGroup()
{
Rows = new List<ReportRow>();
}
/// <summary>
/// Initializes a new instance of the MediaBrowser.Api.Reports.ReportGroup class. </summary>
/// <param name="rows"> The rows. </param>
public ReportGroup(List<ReportRow> rows)
{
Rows = rows;
}
/// <summary> Gets or sets the name. </summary>
/// <value> The name. </value>
public string Name { get; set; }
/// <summary> Gets or sets the rows. </summary>
/// <value> The rows. </value>
public List<ReportRow> Rows { get; set; }
/// <summary> Returns a string that represents the current object. </summary>
/// <returns> A string that represents the current object. </returns>
/// <seealso cref="M:System.Object.ToString()"/>
public override string ToString()
{
return Name;
}
}
}

@ -0,0 +1,54 @@
using MediaBrowser.Controller.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Api.Reports
{
/// <summary> A report header. </summary>
public class ReportHeader
{
/// <summary> Initializes a new instance of the ReportHeader class. </summary>
public ReportHeader()
{
ItemViewType = ItemViewType.None;
Visible = true;
CanGroup = true;
}
/// <summary> Gets or sets the type of the header field. </summary>
/// <value> The type of the header field. </value>
public ReportFieldType HeaderFieldType { get; set; }
/// <summary> Gets or sets the name of the header. </summary>
/// <value> The name of the header. </value>
public string Name { get; set; }
/// <summary> Gets or sets the name of the field. </summary>
/// <value> The name of the field. </value>
public HeaderMetadata FieldName { get; set; }
/// <summary> Gets or sets the sort field. </summary>
/// <value> The sort field. </value>
public string SortField { get; set; }
/// <summary> Gets or sets the type. </summary>
/// <value> The type. </value>
public string Type { get; set; }
/// <summary> Gets or sets the type of the item view. </summary>
/// <value> The type of the item view. </value>
public ItemViewType ItemViewType { get; set; }
/// <summary> Gets or sets a value indicating whether this object is visible. </summary>
/// <value> true if visible, false if not. </value>
public bool Visible { get; set; }
/// <summary> Gets or sets a value indicating whether we can group. </summary>
/// <value> true if we can group, false if not. </value>
public bool CanGroup { get; set; }
}
}

@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Api.Reports
{
/// <summary> A report item. </summary>
public class ReportItem
{
/// <summary> Gets or sets the identifier. </summary>
/// <value> The identifier. </value>
public string Id { get; set; }
/// <summary> Gets or sets the name. </summary>
/// <value> The name. </value>
public string Name { get; set; }
public string Image { get; set; }
/// <summary> Gets or sets the custom tag. </summary>
/// <value> The custom tag. </value>
public string CustomTag { get; set; }
/// <summary> Returns a string that represents the current object. </summary>
/// <returns> A string that represents the current object. </returns>
/// <seealso cref="M:System.Object.ToString()"/>
public override string ToString()
{
return Name;
}
}
}

@ -0,0 +1,52 @@
using MediaBrowser.Controller.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Api.Reports
{
/// <summary> A report options. </summary>
internal class ReportOptions<I>
{
/// <summary> Initializes a new instance of the ReportOptions class. </summary>
public ReportOptions()
{
}
/// <summary> Initializes a new instance of the ReportOptions class. </summary>
/// <param name="header"> . </param>
/// <param name="row"> . </param>
public ReportOptions(ReportHeader header, Func<I, ReportRow, object> column)
{
Header = header;
Column = column;
}
/// <summary>
/// Initializes a new instance of the ReportOptions class.
/// </summary>
/// <param name="header"></param>
/// <param name="column"></param>
/// <param name="itemID"></param>
public ReportOptions(ReportHeader header, Func<I, ReportRow, object> column, Func<I, object> itemID)
{
Header = header;
Column = column;
ItemID = itemID;
}
/// <summary> Gets or sets the header. </summary>
/// <value> The header. </value>
public ReportHeader Header { get; set; }
/// <summary> Gets or sets the column. </summary>
/// <value> The column. </value>
public Func<I, ReportRow, object> Column { get; set; }
/// <summary> Gets or sets the identifier of the item. </summary>
/// <value> The identifier of the item. </value>
public Func<I, object> ItemID { get; set; }
}
}

@ -0,0 +1,53 @@
using System.Collections.Generic;
namespace MediaBrowser.Api.Reports
{
/// <summary> Encapsulates the result of a report. </summary>
public class ReportResult
{
/// <summary>
/// Initializes a new instance of the MediaBrowser.Api.Reports.ReportResult class. </summary>
public ReportResult()
{
Rows = new List<ReportRow>();
Headers = new List<ReportHeader>();
Groups = new List<ReportGroup>();
TotalRecordCount = 0;
IsGrouped = false;
}
/// <summary>
/// Initializes a new instance of the MediaBrowser.Api.Reports.ReportResult class. </summary>
/// <param name="headers"> The headers. </param>
/// <param name="rows"> The rows. </param>
public ReportResult(List<ReportHeader> headers, List<ReportRow> rows)
{
Rows = rows;
Headers = headers;
TotalRecordCount = 0;
}
/// <summary> Gets or sets the rows. </summary>
/// <value> The rows. </value>
public List<ReportRow> Rows { get; set; }
/// <summary> Gets or sets the headers. </summary>
/// <value> The headers. </value>
public List<ReportHeader> Headers { get; set; }
/// <summary> Gets or sets the groups. </summary>
/// <value> The groups. </value>
public List<ReportGroup> Groups { get; set; }
/// <summary> Gets or sets the number of total records. </summary>
/// <value> The total number of record count. </value>
public int TotalRecordCount { get; set; }
/// <summary> Gets or sets the is grouped. </summary>
/// <value> The is grouped. </value>
public bool IsGrouped { get; set; }
}
}

@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Api.Reports
{
public class ReportRow
{
/// <summary>
/// Initializes a new instance of the ReportRow class.
/// </summary>
public ReportRow()
{
Columns = new List<ReportItem>();
}
/// <summary> Gets or sets the identifier. </summary>
/// <value> The identifier. </value>
public string Id { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this object has backdrop image. </summary>
/// <value> true if this object has backdrop image, false if not. </value>
public bool HasImageTagsBackdrop { get; set; }
/// <summary> Gets or sets a value indicating whether this object has image tags. </summary>
/// <value> true if this object has image tags, false if not. </value>
public bool HasImageTagsPrimary { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this object has image tags logo. </summary>
/// <value> true if this object has image tags logo, false if not. </value>
public bool HasImageTagsLogo { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this object has local trailer. </summary>
/// <value> true if this object has local trailer, false if not. </value>
public bool HasLocalTrailer { get; set; }
/// <summary> Gets or sets a value indicating whether this object has lock data. </summary>
/// <value> true if this object has lock data, false if not. </value>
public bool HasLockData { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this object has embedded image. </summary>
/// <value> true if this object has embedded image, false if not. </value>
public bool HasEmbeddedImage { get; set; }
/// <summary> Gets or sets a value indicating whether this object has subtitles. </summary>
/// <value> true if this object has subtitles, false if not. </value>
public bool HasSubtitles { get; set; }
/// <summary> Gets or sets a value indicating whether this object has specials. </summary>
/// <value> true if this object has specials, false if not. </value>
public bool HasSpecials { get; set; }
/// <summary> Gets or sets a value indicating whether this object is unidentified. </summary>
/// <value> true if this object is unidentified, false if not. </value>
public bool IsUnidentified { get; set; }
/// <summary> Gets or sets the columns. </summary>
/// <value> The columns. </value>
public List<ReportItem> Columns { get; set; }
/// <summary> Gets or sets the type. </summary>
/// <value> The type. </value>
public ReportViewType RowType { get; set; }
}
}

@ -1,9 +0,0 @@

namespace MediaBrowser.Api.Reports
{
public enum ReportFieldType
{
String,
Boolean
}
}

@ -1,33 +1,287 @@
using ServiceStack;
using System;
using System.Linq;
using MediaBrowser.Api.UserLibrary;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Entities;
using ServiceStack;
using System.Collections.Generic;
namespace MediaBrowser.Api.Reports
{
public class BaseReportRequest : IReturn<ReportResult>
{
public class BaseReportRequest : BaseItemsRequest
{
/// <summary>
/// Specify this to localize the search to a specific item or folder. Omit to use the root.
/// Gets or sets the user id.
/// </summary>
/// <value>The parent id.</value>
[ApiMember(Name = "ParentId", Description = "Specify this to localize the search to a specific item or folder. Omit to use the root", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string ParentId { get; set; }
/// <value>The user id.</value>
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public Guid? UserId { get; set; }
/// <summary>
/// Skips over a given number of items within the results. Use for paging.
/// Limit results to items containing a specific person
/// </summary>
/// <value>The start index.</value>
[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; }
/// <value>The person.</value>
[ApiMember(Name = "Person", Description = "Optional. If specified, results will be filtered to include only those containing the specified person.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string Person { get; set; }
[ApiMember(Name = "PersonIds", Description = "Optional. If specified, results will be filtered to include only those containing the specified person.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string PersonIds { get; set; }
/// <summary>
/// The maximum number of items to return
/// If the Person filter is used, this can also be used to restrict to a specific person type
/// </summary>
/// <value>The limit.</value>
[ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? Limit { get; set; }
}
/// <value>The type of the person.</value>
[ApiMember(Name = "PersonTypes", Description = "Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string PersonTypes { get; set; }
/// <summary>
/// Limit results to items containing specific studios
/// </summary>
/// <value>The studios.</value>
[ApiMember(Name = "Studios", Description = "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
public string Studios { get; set; }
[ApiMember(Name = "StudioIds", Description = "Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
public string StudioIds { get; set; }
/// <summary>
/// Gets or sets the studios.
/// </summary>
/// <value>The studios.</value>
[ApiMember(Name = "Artists", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
public string Artists { get; set; }
[ApiMember(Name = "ArtistIds", Description = "Optional. If specified, results will be filtered based on artist. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
public string ArtistIds { get; set; }
[ApiMember(Name = "Albums", Description = "Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
public string Albums { get; set; }
/// <summary>
/// Gets or sets the item ids.
/// </summary>
/// <value>The item ids.</value>
[ApiMember(Name = "Ids", Description = "Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
public string Ids { get; set; }
public bool HasQueryLimit { get; set; }
public string GroupBy { get; set; }
public string ReportColumns { get; set; }
/// <summary>
/// Gets or sets the video types.
/// </summary>
/// <value>The video types.</value>
[ApiMember(Name = "VideoTypes", Description = "Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
public string VideoTypes { get; set; }
/// <summary>
/// Gets or sets the video formats.
/// </summary>
/// <value>The video formats.</value>
[ApiMember(Name = "Is3D", Description = "Optional filter by items that are 3D, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? Is3D { get; set; }
/// <summary>
/// Gets or sets the series status.
/// </summary>
/// <value>The series status.</value>
[ApiMember(Name = "SeriesStatus", Description = "Optional filter by Series Status. Allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
public string SeriesStatus { get; set; }
[ApiMember(Name = "NameStartsWithOrGreater", Description = "Optional filter by items whose name is sorted equally or greater than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string NameStartsWithOrGreater { get; set; }
[ApiMember(Name = "NameStartsWith", Description = "Optional filter by items whose name is sorted equally than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string NameStartsWith { get; set; }
[ApiMember(Name = "NameLessThan", Description = "Optional filter by items whose name is equally or lesser than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string NameLessThan { get; set; }
[ApiMember(Name = "AlbumArtistStartsWithOrGreater", Description = "Optional filter by items whose album artist is sorted equally or greater than a given input string.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string AlbumArtistStartsWithOrGreater { get; set; }
/// <summary>
/// Gets or sets the air days.
/// </summary>
/// <value>The air days.</value>
[ApiMember(Name = "AirDays", Description = "Optional filter by Series Air Days. Allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
public string AirDays { get; set; }
/// <summary>
/// Gets or sets the min offical rating.
/// </summary>
/// <value>The min offical rating.</value>
[ApiMember(Name = "MinOfficialRating", Description = "Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string MinOfficialRating { get; set; }
/// <summary>
/// Gets or sets the max offical rating.
/// </summary>
/// <value>The max offical rating.</value>
[ApiMember(Name = "MaxOfficialRating", Description = "Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string MaxOfficialRating { get; set; }
[ApiMember(Name = "HasThemeSong", Description = "Optional filter by items with theme songs.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public bool? HasThemeSong { get; set; }
[ApiMember(Name = "HasThemeVideo", Description = "Optional filter by items with theme videos.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public bool? HasThemeVideo { get; set; }
[ApiMember(Name = "HasSubtitles", Description = "Optional filter by items with subtitles.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public bool? HasSubtitles { get; set; }
[ApiMember(Name = "HasSpecialFeature", Description = "Optional filter by items with special features.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public bool? HasSpecialFeature { get; set; }
[ApiMember(Name = "HasTrailer", Description = "Optional filter by items with trailers.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public bool? HasTrailer { get; set; }
[Route("/Reports/Items", "GET", Summary = "Gets reports based on library items")]
public class GetItemReport : BaseReportRequest
{
[ApiMember(Name = "AdjacentTo", Description = "Optional. Return items that are siblings of a supplied item.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public string AdjacentTo { get; set; }
[ApiMember(Name = "MinIndexNumber", Description = "Optional filter by minimum index number.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? MinIndexNumber { get; set; }
[ApiMember(Name = "MinPlayers", Description = "Optional filter by minimum number of game players.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? MinPlayers { get; set; }
[ApiMember(Name = "MaxPlayers", Description = "Optional filter by maximum number of game players.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? MaxPlayers { get; set; }
[ApiMember(Name = "ParentIndexNumber", Description = "Optional filter by parent index number.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? ParentIndexNumber { get; set; }
[ApiMember(Name = "HasParentalRating", Description = "Optional filter by items that have or do not have a parental rating", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? HasParentalRating { get; set; }
[ApiMember(Name = "IsHD", Description = "Optional filter by items that are HD or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? IsHD { get; set; }
[ApiMember(Name = "LocationTypes", Description = "Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
public string LocationTypes { get; set; }
[ApiMember(Name = "ExcludeLocationTypes", Description = "Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)]
public string ExcludeLocationTypes { get; set; }
[ApiMember(Name = "IsMissing", Description = "Optional filter by items that are missing episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? IsMissing { get; set; }
[ApiMember(Name = "IsUnaired", Description = "Optional filter by items that are unaired episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? IsUnaired { get; set; }
[ApiMember(Name = "IsVirtualUnaired", Description = "Optional filter by items that are virtual unaired episodes or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? IsVirtualUnaired { get; set; }
[ApiMember(Name = "MinCommunityRating", Description = "Optional filter by minimum community rating.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public double? MinCommunityRating { get; set; }
[ApiMember(Name = "MinCriticRating", Description = "Optional filter by minimum critic rating.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public double? MinCriticRating { get; set; }
[ApiMember(Name = "AiredDuringSeason", Description = "Gets all episodes that aired during a season, including specials.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
public int? AiredDuringSeason { get; set; }
[ApiMember(Name = "MinPremiereDate", Description = "Optional. The minimum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public string MinPremiereDate { get; set; }
[ApiMember(Name = "MaxPremiereDate", Description = "Optional. The maximum premiere date. Format = ISO", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public string MaxPremiereDate { get; set; }
[ApiMember(Name = "HasOverview", Description = "Optional filter by items that have an overview or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? HasOverview { get; set; }
[ApiMember(Name = "HasImdbId", Description = "Optional filter by items that have an imdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? HasImdbId { get; set; }
[ApiMember(Name = "HasTmdbId", Description = "Optional filter by items that have a tmdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? HasTmdbId { get; set; }
[ApiMember(Name = "HasTvdbId", Description = "Optional filter by items that have a tvdb id or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? HasTvdbId { get; set; }
[ApiMember(Name = "IsYearMismatched", Description = "Optional filter by items that are potentially misidentified.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? IsYearMismatched { get; set; }
[ApiMember(Name = "IsInBoxSet", Description = "Optional filter by items that are in boxsets, or not.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? IsInBoxSet { get; set; }
[ApiMember(Name = "IsLocked", Description = "Optional filter by items that are locked.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public bool? IsLocked { get; set; }
[ApiMember(Name = "IsUnidentified", Description = "Optional filter by items that are unidentified by internet metadata providers.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public bool? IsUnidentified { get; set; }
[ApiMember(Name = "IsPlaceHolder", Description = "Optional filter by items that are placeholders", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public bool? IsPlaceHolder { get; set; }
[ApiMember(Name = "HasOfficialRating", Description = "Optional filter by items that have official ratings", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
public bool? HasOfficialRating { get; set; }
[ApiMember(Name = "CollapseBoxSetItems", Description = "Whether or not to hide items behind their boxsets.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool? CollapseBoxSetItems { get; set; }
public string[] GetStudios()
{
return (Studios ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
}
public string[] GetStudioIds()
{
return (StudioIds ?? string.Empty).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
}
public string[] GetPersonTypes()
{
return (PersonTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}
public string[] GetPersonIds()
{
return (PersonIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
}
public VideoType[] GetVideoTypes()
{
var val = VideoTypes;
if (string.IsNullOrEmpty(val))
{
return new VideoType[] { };
}
return val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => (VideoType)Enum.Parse(typeof(VideoType), v, true)).ToArray();
}
}
[Route("/Reports/Items", "GET", Summary = "Gets reports based on library items")]
public class GetItemReport : BaseReportRequest, IReturn<ReportResult>
{
}
[Route("/Reports/Headers", "GET", Summary = "Gets reports headers based on library items")]
public class GetReportHeaders : BaseReportRequest, IReturn<List<ReportHeader>>
{
}
[Route("/Reports/Statistics", "GET", Summary = "Gets reports statistics based on library items")]
public class GetReportStatistics : BaseReportRequest, IReturn<ReportStatResult>
{
public int? TopItems { get; set; }
}
[Route("/Reports/Items/Download", "GET", Summary = "Downloads report")]
public class GetReportDownload : BaseReportRequest
{
public GetReportDownload()
{
ExportType = ReportExportType.CSV;
}
public ReportExportType ExportType { get; set; }
}
}

@ -1,16 +0,0 @@
using System.Collections.Generic;
namespace MediaBrowser.Api.Reports
{
public class ReportResult
{
public List<List<string>> Rows { get; set; }
public List<ReportFieldType> Columns { get; set; }
public ReportResult()
{
Rows = new List<List<string>>();
Columns = new List<ReportFieldType>();
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,214 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Api.Reports
{
/// <summary> A report stat builder. </summary>
/// <seealso cref="T:MediaBrowser.Api.Reports.ReportBuilderBase"/>
public class ReportStatBuilder : ReportBuilderBase
{
/// <summary>
/// Initializes a new instance of the MediaBrowser.Api.Reports.ReportStatBuilder class. </summary>
/// <param name="libraryManager"> Manager for library. </param>
public ReportStatBuilder(ILibraryManager libraryManager)
: base(libraryManager)
{
}
/// <summary> Gets report stat result. </summary>
/// <param name="items"> The items. </param>
/// <param name="reportRowType"> Type of the report row. </param>
/// <param name="topItem"> The top item. </param>
/// <returns> The report stat result. </returns>
public ReportStatResult GetReportStatResult(BaseItem[] items, ReportViewType reportRowType, int topItem = 5)
{
ReportStatResult result = new ReportStatResult();
result = this.GetResultGenres(result, items, topItem);
result = this.GetResultStudios(result, items, topItem);
result = this.GetResultPersons(result, items, topItem);
result = this.GetResultProductionYears(result, items, topItem);
result = this.GetResulProductionLocations(result, items, topItem);
result = this.GetResultCommunityRatings(result, items, topItem);
result = this.GetResultParentalRatings(result, items, topItem);
switch (reportRowType)
{
case ReportViewType.Season:
case ReportViewType.Series:
case ReportViewType.MusicAlbum:
case ReportViewType.MusicArtist:
case ReportViewType.Game:
break;
case ReportViewType.Movie:
case ReportViewType.BoxSet:
break;
case ReportViewType.Book:
case ReportViewType.Episode:
case ReportViewType.Video:
case ReportViewType.MusicVideo:
case ReportViewType.Trailer:
case ReportViewType.Audio:
case ReportViewType.BaseItem:
default:
break;
}
result.Groups = result.Groups.OrderByDescending(n => n.Items.Count()).ToList();
return result;
}
private ReportStatResult GetResultGenres(ReportStatResult result, BaseItem[] items, int topItem = 5)
{
this.GetGroups(result, ReportHelper.GetServerLocalizedString("HeaderGenres"), topItem,
items.SelectMany(x => x.Genres)
.GroupBy(x => x)
.OrderByDescending(x => x.Count())
.Take(topItem)
.Select(x => new ReportStatItem
{
Name = x.Key,
Value = x.Count().ToString(),
Id = GetGenreID(x.Key)
}));
return result;
}
private ReportStatResult GetResultStudios(ReportStatResult result, BaseItem[] items, int topItem = 5)
{
this.GetGroups(result, ReportHelper.GetServerLocalizedString("HeaderStudios"), topItem,
items.SelectMany(x => x.Studios)
.GroupBy(x => x)
.OrderByDescending(x => x.Count())
.Take(topItem)
.Select(x => new ReportStatItem
{
Name = x.Key,
Value = x.Count().ToString(),
Id = GetStudioID(x.Key)
})
);
return result;
}
private ReportStatResult GetResultPersons(ReportStatResult result, BaseItem[] items, int topItem = 5)
{
List<string> t = new List<string> { PersonType.Actor, PersonType.Composer, PersonType.Director, PersonType.GuestStar, PersonType.Producer, PersonType.Writer, "Artist", "AlbumArtist" };
foreach (var item in t)
{
this.GetGroups(result, ReportHelper.GetServerLocalizedString("Option" + item), topItem,
items.SelectMany(x => x.People)
.Where(n => n.Type == item)
.GroupBy(x => x.Name)
.OrderByDescending(x => x.Count())
.Take(topItem)
.Select(x => new ReportStatItem
{
Name = x.Key,
Value = x.Count().ToString(),
Id = GetPersonID(x.Key)
})
);
}
return result;
}
private ReportStatResult GetResultCommunityRatings(ReportStatResult result, BaseItem[] items, int topItem = 5)
{
this.GetGroups(result, ReportHelper.GetServerLocalizedString("LabelCommunityRating"), topItem,
items.Where(x => x.CommunityRating != null && x.CommunityRating > 0)
.GroupBy(x => x.CommunityRating)
.OrderByDescending(x => x.Count())
.Take(topItem)
.Select(x => new ReportStatItem
{
Name = x.Key.ToString(),
Value = x.Count().ToString()
})
);
return result;
}
private ReportStatResult GetResultParentalRatings(ReportStatResult result, BaseItem[] items, int topItem = 5)
{
this.GetGroups(result, ReportHelper.GetServerLocalizedString("HeaderParentalRatings"), topItem,
items.Where(x => x.OfficialRating != null)
.GroupBy(x => x.OfficialRating)
.OrderByDescending(x => x.Count())
.Take(topItem)
.Select(x => new ReportStatItem
{
Name = x.Key.ToString(),
Value = x.Count().ToString()
})
);
return result;
}
private ReportStatResult GetResultProductionYears(ReportStatResult result, BaseItem[] items, int topItem = 5)
{
this.GetGroups(result, ReportHelper.GetServerLocalizedString("HeaderYears"), topItem,
items.Where(x => x.ProductionYear != null && x.ProductionYear > 0)
.GroupBy(x => x.ProductionYear)
.OrderByDescending(x => x.Count())
.Take(topItem)
.Select(x => new ReportStatItem
{
Name = x.Key.ToString(),
Value = x.Count().ToString()
})
);
return result;
}
private ReportStatResult GetResulProductionLocations(ReportStatResult result, BaseItem[] items, int topItem = 5)
{
this.GetGroups(result, ReportHelper.GetServerLocalizedString("HeaderCountries"), topItem,
items.OfType<IHasProductionLocations>()
.Where(x => x.ProductionLocations != null)
.SelectMany(x => x.ProductionLocations)
.GroupBy(x => x)
.OrderByDescending(x => x.Count())
.Take(topItem)
.Select(x => new ReportStatItem
{
Name = x.Key.ToString(),
Value = x.Count().ToString()
})
);
return result;
}
/// <summary> Gets the groups. </summary>
/// <param name="result"> The result. </param>
/// <param name="header"> The header. </param>
/// <param name="topItem"> The top item. </param>
/// <param name="top"> The top. </param>
private void GetGroups(ReportStatResult result, string header, int topItem, IEnumerable<ReportStatItem> top)
{
if (top.Count() > 0)
{
var group = new ReportStatGroup { Header = ReportStatGroup.FormatedHeader(header, topItem) };
group.Items.AddRange(top);
result.Groups.Add(group);
}
}
}
}

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Api.Reports
{
/// <summary> A report stat group. </summary>
public class ReportStatGroup
{
/// <summary>
/// Initializes a new instance of the MediaBrowser.Api.Reports.ReportStatGroup class. </summary>
public ReportStatGroup()
{
Items = new List<ReportStatItem>();
TotalRecordCount = 0;
}
/// <summary> Gets or sets the header. </summary>
/// <value> The header. </value>
public string Header { get; set; }
/// <summary> Gets or sets the items. </summary>
/// <value> The items. </value>
public List<ReportStatItem> Items { get; set; }
/// <summary> Gets or sets the number of total records. </summary>
/// <value> The total number of record count. </value>
public int TotalRecordCount { get; set; }
internal static string FormatedHeader(string header, int topItem)
{
return string.Format("Top {0} {1}", topItem, header);
}
}
}

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Api.Reports
{
/// <summary> A report stat item. </summary>
public class ReportStatItem
{
/// <summary> Gets or sets the name. </summary>
/// <value> The name. </value>
public string Name { get; set; }
/// <summary> Gets or sets the image. </summary>
/// <value> The image. </value>
public string Image { get; set; }
/// <summary> Gets or sets the value. </summary>
/// <value> The value. </value>
public string Value { get; set; }
/// <summary> Gets or sets the identifier. </summary>
/// <value> The identifier. </value>
public string Id { get; set; }
}
}

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Api.Reports
{
/// <summary> Encapsulates the result of a report stat. </summary>
public class ReportStatResult
{
/// <summary>
/// Initializes a new instance of the MediaBrowser.Api.Reports.ReportStatResult class. </summary>
public ReportStatResult()
{
Groups = new List<ReportStatGroup>();
TotalRecordCount = 0;
}
/// <summary> Gets or sets the groups. </summary>
/// <value> The groups. </value>
public List<ReportStatGroup> Groups { get; set; }
/// <summary> Gets or sets the number of total records. </summary>
/// <value> The total number of record count. </value>
public int TotalRecordCount { get; set; }
}
}

@ -158,9 +158,10 @@ namespace MediaBrowser.Controller.LiveTv
/// Gets the channel stream.
/// </summary>
/// <param name="id">The identifier.</param>
/// <param name="mediaSourceId">The media source identifier.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{StreamResponseInfo}.</returns>
Task<MediaSourceInfo> GetChannelStream(string id, CancellationToken cancellationToken);
Task<MediaSourceInfo> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken);
/// <summary>
/// Gets the program.

@ -1,9 +1,30 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Providers
{
public class ItemInfo
{
public string Path { get; set; }
public ItemInfo()
{
}
public ItemInfo(IHasMetadata item)
{
Path = item.Path;
ContainingFolderPath = item.ContainingFolderPath;
IsInMixedFolder = item.IsInMixedFolder;
var video = item as Video;
if (video != null)
{
VideoType = video.VideoType;
}
}
public string Path { get; set; }
public string ContainingFolderPath { get; set; }
public VideoType VideoType { get; set; }
public bool IsInMixedFolder { get; set; }
}
}

@ -635,15 +635,25 @@ namespace MediaBrowser.Dlna.PlayTo
}
XElement uPnpResponse;
// Handle different variations sent back by devices
try
{
uPnpResponse = XElement.Parse(trackString);
}
catch (Exception ex)
catch (Exception)
{
_logger.ErrorException("Unable to parse xml {0}", ex, trackString);
return new Tuple<bool, uBaseObject>(true, null);
// first try to add a root node with a dlna namesapce
try
{
uPnpResponse = XElement.Parse("<data xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + trackString + "</data>");
uPnpResponse = uPnpResponse.Descendants().First();
}
catch (Exception ex)
{
_logger.ErrorException("Unable to parse xml {0}", ex, trackString);
return new Tuple<bool, uBaseObject>(true, null);
}
}
var e = uPnpResponse.Element(uPnpNamespaces.items);

@ -265,7 +265,7 @@ namespace MediaBrowser.Dlna.PlayTo
{
var ticks = _device.Position.Ticks;
if (!info.IsDirectStream)
if (!EnableClientSideSeek(info))
{
ticks += info.StartPositionTicks;
}
@ -376,23 +376,28 @@ namespace MediaBrowser.Dlna.PlayTo
{
var info = StreamParams.ParseFromUrl(media.Url, _libraryManager, _mediaSourceManager);
if (info.Item != null && !info.IsDirectStream)
if (info.Item != null && !EnableClientSideSeek(info))
{
var user = _session.UserId.HasValue ? _userManager.GetUserById(_session.UserId.Value) : null;
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl).ConfigureAwait(false);
if (newItem.StreamInfo.IsDirectStream)
{
await _device.Seek(TimeSpan.FromTicks(newPosition)).ConfigureAwait(false);
}
return;
}
await _device.Seek(TimeSpan.FromTicks(newPosition)).ConfigureAwait(false);
await SeekAfterTransportChange(newPosition).ConfigureAwait(false);
}
}
private bool EnableClientSideSeek(StreamParams info)
{
return info.IsDirectStream;
}
private bool EnableClientSideSeek(StreamInfo info)
{
return info.IsDirectStream;
}
public Task SendUserDataChangeInfo(UserDataChangeInfo info, CancellationToken cancellationToken)
{
return Task.FromResult(true);
@ -607,8 +612,10 @@ namespace MediaBrowser.Dlna.PlayTo
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl);
var streamInfo = currentitem.StreamInfo;
if (streamInfo.StartPositionTicks > 0 && streamInfo.IsDirectStream)
await _device.Seek(TimeSpan.FromTicks(streamInfo.StartPositionTicks));
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
{
await SeekAfterTransportChange(streamInfo.StartPositionTicks).ConfigureAwait(false);
}
}
#endregion
@ -742,9 +749,9 @@ namespace MediaBrowser.Dlna.PlayTo
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl).ConfigureAwait(false);
if (newItem.StreamInfo.IsDirectStream)
if (EnableClientSideSeek(newItem.StreamInfo))
{
await _device.Seek(TimeSpan.FromTicks(newPosition)).ConfigureAwait(false);
await SeekAfterTransportChange(newPosition).ConfigureAwait(false);
}
}
}
@ -768,17 +775,28 @@ namespace MediaBrowser.Dlna.PlayTo
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl).ConfigureAwait(false);
if (newItem.StreamInfo.IsDirectStream && newPosition > 0)
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
{
// This is rather arbitrary, but give the player time to start playing
await Task.Delay(2000).ConfigureAwait(false);
await _device.Seek(TimeSpan.FromTicks(newPosition)).ConfigureAwait(false);
await SeekAfterTransportChange(newPosition).ConfigureAwait(false);
}
}
}
}
private async Task SeekAfterTransportChange(long positionTicks)
{
const int maxWait = 15000000;
const int interval = 500;
var currentWait = 0;
while (_device.TransportState != TRANSPORTSTATE.PLAYING && currentWait < maxWait)
{
await Task.Delay(interval).ConfigureAwait(false);
currentWait += interval;
}
await _device.Seek(TimeSpan.FromTicks(positionTicks)).ConfigureAwait(false);
}
private class StreamParams
{
public string ItemId { get; set; }

@ -31,8 +31,8 @@ namespace MediaBrowser.Dlna.Profiles
MaxIconWidth = 48;
MaxIconHeight = 48;
MaxStreamingBitrate = 8000000;
MaxStaticBitrate = 8000000;
MaxStreamingBitrate = 10000000;
MaxStaticBitrate = 10000000;
MusicStreamingTranscodingBitrate = 128000;
MusicSyncBitrate = 128000;

@ -15,7 +15,7 @@ namespace MediaBrowser.Dlna.Profiles
Identification = new DeviceIdentification
{
ModelName = "WD TV HD Live",
ModelName = "WD TV",
Headers = new []
{

@ -22,8 +22,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -34,7 +34,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="avi,mpeg,mkv,ts,mp4,mov,m4v,asf,webm,ogg,ogv,iso" type="Video" />

@ -16,8 +16,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -28,7 +28,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="mp3,wma" type="Audio" />

@ -21,8 +21,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -33,7 +33,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="mp3,flac,m4a,wma" type="Audio" />

@ -22,8 +22,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -34,7 +34,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg2video" type="Video" />

@ -23,8 +23,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -35,7 +35,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="mp4,mkv,mpeg,ts" audioCodec="mp3,ac3,aac,he-aac,pcm" videoCodec="h264,mpeg2video" type="Video" />

File diff suppressed because one or more lines are too long

@ -22,8 +22,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -34,7 +34,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="ts" audioCodec="aac,ac3,mp3" videoCodec="h264" type="Video" />

@ -20,8 +20,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -32,7 +32,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="mp3,flac,m4a,wma" type="Audio" />

@ -22,8 +22,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -34,7 +34,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="mp3" audioCodec="mp2,mp3" type="Audio" />

@ -23,8 +23,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -35,7 +35,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes>
<XmlAttribute name="xmlns:pv" value="http://www.pv.com/pvns/" />
</XmlRootAttributes>

@ -16,8 +16,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -28,7 +28,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="mp4,mov" audioCodec="aac" videoCodec="h264,mpeg4" type="Video" />

@ -22,8 +22,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -34,7 +34,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes>
<XmlAttribute name="xmlns:sec" value="http://www.sec.co.kr/" />
</XmlRootAttributes>

@ -22,8 +22,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -34,7 +34,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes>
<XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
</XmlRootAttributes>

@ -24,8 +24,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -36,7 +36,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes>
<XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
</XmlRootAttributes>

@ -23,8 +23,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -36,7 +36,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes>
<XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
</XmlRootAttributes>

@ -23,8 +23,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -36,7 +36,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes>
<XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
</XmlRootAttributes>

@ -23,8 +23,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -36,7 +36,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes>
<XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
</XmlRootAttributes>

@ -23,8 +23,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -36,7 +36,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes>
<XmlAttribute name="xmlns:av" value="urn:schemas-sony-com:av" />
</XmlRootAttributes>

@ -23,8 +23,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -36,7 +36,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />

@ -22,8 +22,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -34,7 +34,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="avi,mpeg,mkv,ts,mp4,mov,m4v,asf,webm,ogg,ogv,iso" type="Video" />

@ -2,7 +2,7 @@
<Profile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Name>WDTV Live</Name>
<Identification>
<ModelName>WD TV HD Live</ModelName>
<ModelName>WD TV</ModelName>
<Headers>
<HttpHeaderInfo name="User-Agent" value="alphanetworks" match="Substring" />
<HttpHeaderInfo name="User-Agent" value="ALPHA Networks" match="Substring" />
@ -23,8 +23,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -35,7 +35,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>true</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="avi" audioCodec="ac3,dca,mp2,mp3,pcm" videoCodec="mpeg1video,mpeg2video,mpeg4,h264,vc1" type="Video" />

@ -23,8 +23,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -35,7 +35,6 @@
<EnableMSMediaReceiverRegistrar>true</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>false</EnableDlnaProtocol>
<EnableUrlBase>true</EnableUrlBase>
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="avi" audioCodec="ac3,mp3" videoCodec="mpeg4" type="Video" />

@ -23,8 +23,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -35,7 +35,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="ts" audioCodec="ac3" videoCodec="h264" type="Video" />

@ -22,8 +22,8 @@
<MaxAlbumArtHeight>480</MaxAlbumArtHeight>
<MaxIconWidth>48</MaxIconWidth>
<MaxIconHeight>48</MaxIconHeight>
<MaxStreamingBitrate>8000000</MaxStreamingBitrate>
<MaxStaticBitrate>8000000</MaxStaticBitrate>
<MaxStreamingBitrate>10000000</MaxStreamingBitrate>
<MaxStaticBitrate>10000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>128000</MusicStreamingTranscodingBitrate>
<MusicSyncBitrate>128000</MusicSyncBitrate>
<XDlnaDoc>DMS-1.50</XDlnaDoc>
@ -34,7 +34,6 @@
<EnableMSMediaReceiverRegistrar>false</EnableMSMediaReceiverRegistrar>
<IgnoreTranscodeByteRangeRequests>false</IgnoreTranscodeByteRangeRequests>
<EnableDlnaProtocol>true</EnableDlnaProtocol>
<EnableUrlBase>false</EnableUrlBase>
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="mp3" audioCodec="mp2,mp3" type="Audio" />

@ -58,7 +58,7 @@ namespace MediaBrowser.LocalMetadata
public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date)
{
var file = GetXmlFile(new ItemInfo { IsInMixedFolder = item.IsInMixedFolder, Path = item.Path }, directoryService);
var file = GetXmlFile(new ItemInfo(item), directoryService);
if (file == null)
{

@ -2,13 +2,10 @@
using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Session;
using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@ -39,7 +36,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
public BaseEncoder(MediaEncoder mediaEncoder,
protected BaseEncoder(MediaEncoder mediaEncoder,
ILogger logger,
IServerConfigurationManager configurationManager,
IFileSystem fileSystem,
@ -64,7 +61,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
IProgress<double> progress,
CancellationToken cancellationToken)
{
var encodingJob = await new EncodingJobFactory(Logger, LibraryManager, MediaSourceManager)
var encodingJob = await new EncodingJobFactory(Logger, LibraryManager, MediaSourceManager, ConfigurationManager)
.CreateJob(options, IsVideoEncoder, progress, cancellationToken).ConfigureAwait(false);
encodingJob.OutputFilePath = GetOutputFilePath(encodingJob);
@ -305,25 +302,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
return job.Options.CpuCoreLimit ?? 0;
}
protected EncodingQuality GetQualitySetting()
{
var quality = GetEncodingOptions().EncodingQuality;
if (quality == EncodingQuality.Auto)
{
var cpuCount = Environment.ProcessorCount;
if (cpuCount >= 4)
{
//return EncodingQuality.HighQuality;
}
return EncodingQuality.HighSpeed;
}
return quality;
}
protected string GetInputModifier(EncodingJob job, bool genPts = true)
{
var inputModifier = string.Empty;
@ -543,7 +521,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
var isVc1 = state.VideoStream != null &&
string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
var qualitySetting = GetQualitySetting();
var qualitySetting = state.Quality;
if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
{

@ -1,5 +1,6 @@
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
@ -24,7 +25,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
public Stream LogFileStream { get; set; }
public IProgress<double> Progress { get; set; }
public TaskCompletionSource<bool> TaskCompletionSource;
public EncodingQuality Quality { get; set; }
public EncodingJobOptions Options { get; set; }
public string InputContainer { get; set; }
public MediaSourceInfo MediaSource { get; set; }

@ -1,8 +1,7 @@
using System.IO;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@ -22,14 +21,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IConfigurationManager _config;
protected static readonly CultureInfo UsCulture = new CultureInfo("en-US");
public EncodingJobFactory(ILogger logger, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager)
public EncodingJobFactory(ILogger logger, ILibraryManager libraryManager, IMediaSourceManager mediaSourceManager, IConfigurationManager config)
{
_logger = logger;
_libraryManager = libraryManager;
_mediaSourceManager = mediaSourceManager;
_config = config;
}
public async Task<EncodingJob> CreateJob(EncodingJobOptions options, bool isVideoRequest, IProgress<double> progress, CancellationToken cancellationToken)
@ -95,6 +96,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
TryStreamCopy(state, request);
state.Quality = options.Context == EncodingContext.Static ?
EncodingQuality.MaxQuality :
GetQualitySetting();
return state;
}
@ -194,6 +199,30 @@ namespace MediaBrowser.MediaEncoding.Encoder
state.MediaSource = mediaSource;
}
protected EncodingQuality GetQualitySetting()
{
var quality = GetEncodingOptions().EncodingQuality;
if (quality == EncodingQuality.Auto)
{
var cpuCount = Environment.ProcessorCount;
if (cpuCount >= 4)
{
//return EncodingQuality.HighQuality;
}
return EncodingQuality.HighSpeed;
}
return quality;
}
protected EncodingOptions GetEncodingOptions()
{
return _config.GetConfiguration<EncodingOptions>("encoding");
}
/// <summary>
/// Infers the video codec.
/// </summary>

@ -199,7 +199,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
await _ffProbeResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
using (var processWrapper = new ProcessWrapper(process, this))
using (var processWrapper = new ProcessWrapper(process, this, _logger))
{
try
{
@ -308,7 +308,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_logger.Debug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
using (var processWrapper = new ProcessWrapper(process, this))
using (var processWrapper = new ProcessWrapper(process, this, _logger))
{
StartProcess(processWrapper);
@ -492,7 +492,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
using (var processWrapper = new ProcessWrapper(process, this))
using (var processWrapper = new ProcessWrapper(process, this, _logger))
{
bool ranToCompletion;
@ -607,7 +607,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
bool ranToCompletion = false;
using (var processWrapper = new ProcessWrapper(process, this))
using (var processWrapper = new ProcessWrapper(process, this, _logger))
{
try
{
@ -792,11 +792,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
public bool HasExited;
public int? ExitCode;
private readonly MediaEncoder _mediaEncoder;
private readonly ILogger _logger;
public ProcessWrapper(Process process, MediaEncoder mediaEncoder)
public ProcessWrapper(Process process, MediaEncoder mediaEncoder, ILogger logger)
{
Process = process;
this._mediaEncoder = mediaEncoder;
_logger = logger;
Process.Exited += Process_Exited;
}
@ -806,7 +808,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
HasExited = true;
ExitCode = process.ExitCode;
try
{
ExitCode = process.ExitCode;
}
catch (Exception ex)
{
_logger.ErrorException("Error determing process exit code", ex);
}
lock (_mediaEncoder._runningProcesses)
{

@ -129,7 +129,7 @@ namespace MediaBrowser.MediaEncoding.Probing
}
else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase))
{
stream.Type = isAudio
stream.Type = isAudio || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
? MediaStreamType.EmbeddedImage
: MediaStreamType.Video;
@ -146,44 +146,8 @@ namespace MediaBrowser.MediaEncoding.Probing
// string.Equals(stream.AspectRatio, "2.35:1", StringComparison.OrdinalIgnoreCase) ||
// string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase);
if (string.Equals(streamInfo.sample_aspect_ratio, "1:1", StringComparison.OrdinalIgnoreCase))
{
stream.IsAnamorphic = false;
}
else if (!((string.IsNullOrWhiteSpace(streamInfo.sample_aspect_ratio) || string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))))
{
stream.IsAnamorphic = true;
}
else if (string.IsNullOrWhiteSpace(streamInfo.display_aspect_ratio) || string.Equals(streamInfo.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
{
stream.IsAnamorphic = false;
}
else
{
var ratioParts = streamInfo.display_aspect_ratio.Split(':');
if (ratioParts.Length != 2)
{
stream.IsAnamorphic = false;
}
else
{
int ratio0;
int ratio1;
if (!Int32.TryParse(ratioParts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out ratio0))
{
stream.IsAnamorphic = false;
}
else if (!Int32.TryParse(ratioParts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out ratio1))
{
stream.IsAnamorphic = false;
}
else
{
stream.IsAnamorphic = ((streamInfo.width * ratio1) != (stream.Height * ratio0));
}
}
}
// http://stackoverflow.com/questions/17353387/how-to-detect-anamorphic-video-with-ffprobe
stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase);
}
else
{
@ -519,6 +483,7 @@ namespace MediaBrowser.MediaEncoding.Probing
FetchStudios(audio, tags, "organization");
FetchStudios(audio, tags, "ensemble");
FetchStudios(audio, tags, "publisher");
FetchStudios(audio, tags, "label");
// These support mulitple values, but for now we only store the first.
audio.SetProviderId(MetadataProviders.MusicBrainzAlbumArtist, GetMultipleMusicBrainzId(FFProbeHelpers.GetDictionaryValue(tags, "MusicBrainz Album Artist Id")));

@ -9,7 +9,7 @@ namespace MediaBrowser.Model.Configuration
public string H264Encoder { get; set; }
public bool EnableDebugLogging { get; set; }
public bool EnableThrottling { get; set; }
public int ThrottleThresholdSeconds { get; set; }
public int ThrottleThresholdInSeconds { get; set; }
public EncodingOptions()
{
@ -17,7 +17,7 @@ namespace MediaBrowser.Model.Configuration
DownMixAudioBoost = 2;
EncodingQuality = EncodingQuality.Auto;
EnableThrottling = true;
ThrottleThresholdSeconds = 110;
ThrottleThresholdInSeconds = 120;
}
}
}

@ -277,7 +277,7 @@ namespace MediaBrowser.Model.Configuration
{
new ImageOption
{
Limit = 3,
Limit = 2,
MinWidth = 1280,
Type = ImageType.Backdrop
},
@ -304,7 +304,7 @@ namespace MediaBrowser.Model.Configuration
new ImageOption
{
Limit = 1,
Limit = 0,
Type = ImageType.Banner
},
@ -374,7 +374,7 @@ namespace MediaBrowser.Model.Configuration
{
new ImageOption
{
Limit = 1,
Limit = 0,
MinWidth = 1280,
Type = ImageType.Backdrop
},
@ -414,6 +414,14 @@ namespace MediaBrowser.Model.Configuration
{
Limit = 0,
Type = ImageType.Art
},
// Don't download this by default
// Generally not used
new ImageOption
{
Limit = 0,
Type = ImageType.Logo
}
}
},

@ -485,7 +485,7 @@ namespace MediaBrowser.Model.Dlna
if (targetAudioChannels.HasValue)
{
if (targetAudioChannels.Value >= 5 && (maxTotalBitrate ?? 0) >= 1500000)
if (targetAudioChannels.Value >= 5 && (maxTotalBitrate ?? 0) >= 2000000)
{
defaultBitrate = 320000;
}

@ -158,6 +158,11 @@ namespace MediaBrowser.Model.Dlna
if (MediaType == DlnaProfileType.Audio)
{
if (StringHelper.EqualsIgnoreCase(SubProtocol, "hls"))
{
return string.Format("{0}/audio/{1}/master.m3u8?{2}", baseUrl, ItemId, queryString);
}
return string.Format("{0}/audio/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
}

@ -153,6 +153,12 @@ namespace MediaBrowser.Model.Updates
/// <value>The versions.</value>
public List<PackageVersionInfo> versions { get; set; }
/// <summary>
/// Gets or sets a value indicating whether [enable in application store].
/// </summary>
/// <value><c>true</c> if [enable in application store]; otherwise, <c>false</c>.</value>
public bool enableInAppStore { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="PackageInfo"/> class.
/// </summary>

@ -378,7 +378,7 @@ namespace MediaBrowser.Providers.Manager
var providerName = provider.GetType().Name;
Logger.Debug("Running {0} for {1}", providerName, logName);
var itemInfo = new ItemInfo { Path = item.Path, IsInMixedFolder = item.IsInMixedFolder };
var itemInfo = new ItemInfo(item);
try
{

@ -130,7 +130,7 @@ namespace MediaBrowser.Providers.MediaInfo
return ItemUpdateType.MetadataImport;
}
private const string SchemaVersion = "4";
private const string SchemaVersion = "5";
private async Task<Model.MediaInfo.MediaInfo> GetMediaInfo(Video item,
IIsoMount isoMount,

@ -7,6 +7,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using MediaBrowser.Providers.TV;
using System;
using System.Collections.Generic;
using System.Globalization;
@ -16,7 +17,6 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using MediaBrowser.Providers.TV;
namespace MediaBrowser.Providers.Music
{

@ -1,6 +1,7 @@
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.IO;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Devices;
@ -26,6 +27,7 @@ namespace MediaBrowser.Server.Implementations.Devices
private readonly ILibraryMonitor _libraryMonitor;
private readonly IConfigurationManager _config;
private readonly ILogger _logger;
private readonly INetworkManager _network;
public event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded;
@ -34,7 +36,7 @@ namespace MediaBrowser.Server.Implementations.Devices
/// </summary>
public event EventHandler<GenericEventArgs<DeviceInfo>> DeviceOptionsUpdated;
public DeviceManager(IDeviceRepository repo, IUserManager userManager, IFileSystem fileSystem, ILibraryMonitor libraryMonitor, IConfigurationManager config, ILogger logger)
public DeviceManager(IDeviceRepository repo, IUserManager userManager, IFileSystem fileSystem, ILibraryMonitor libraryMonitor, IConfigurationManager config, ILogger logger, INetworkManager network)
{
_repo = repo;
_userManager = userManager;
@ -42,6 +44,7 @@ namespace MediaBrowser.Server.Implementations.Devices
_libraryMonitor = libraryMonitor;
_config = config;
_logger = logger;
_network = network;
}
public async Task<DeviceInfo> RegisterDevice(string reportedId, string name, string appName, string appVersion, string usedByUserId)
@ -233,6 +236,12 @@ namespace MediaBrowser.Server.Implementations.Devices
}
var user = _userManager.GetUserById(userId);
if (user == null)
{
throw new ArgumentException("user not found");
}
if (!CanAccessDevice(user.Policy, deviceId))
{
var capabilities = GetCapabilities(deviceId);

@ -45,19 +45,15 @@ namespace MediaBrowser.Server.Implementations.HttpServer
}
}
if (dto is CompressedResult)
{
// Per Google PageSpeed
// This instructs the proxies to cache two versions of the resource: one compressed, and one uncompressed.
// The correct version of the resource is delivered based on the client request header.
// This is a good choice for applications that are singly homed and depend on public proxies for user locality.
res.AddHeader("Vary", "Accept-Encoding");
}
var vary = "Accept-Encoding";
var hasOptions = dto as IHasOptions;
var sharpResponse = res as WebSocketSharpResponse;
if (hasOptions != null)
{
//hasOptions.Options["Server"] = "Mono-HTTPAPI/1.1";
// Content length has to be explicitly set on on HttpListenerResponse or it won't be happy
string contentLength;
@ -79,14 +75,29 @@ namespace MediaBrowser.Server.Implementations.HttpServer
return;
}
var sharpResponse = res as WebSocketSharpResponse;
if (sharpResponse != null)
{
sharpResponse.SendChunked = false;
}
}
}
string hasOptionsVary;
if (hasOptions.Options.TryGetValue("Vary", out hasOptionsVary))
{
vary = hasOptionsVary;
}
hasOptions.Options["Vary"] = vary;
}
//res.KeepAlive = false;
// Per Google PageSpeed
// This instructs the proxies to cache two versions of the resource: one compressed, and one uncompressed.
// The correct version of the resource is delivered based on the client request header.
// This is a good choice for applications that are singly homed and depend on public proxies for user locality.
res.AddHeader("Vary", vary);
}
/// <summary>

@ -34,8 +34,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security
// }
//}
var session = _sessionManager.GetSession(authorization.DeviceId, authorization.Client, authorization.Version);
return Task.FromResult(session);
var user = string.IsNullOrWhiteSpace(authorization.UserId) ? null : _userManager.GetUserById(authorization.UserId);
return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user);
}
private AuthenticationInfo GetTokenInfo(IServiceRequest request)

@ -82,9 +82,9 @@ namespace MediaBrowser.Server.Implementations.IO
}
// This is an arbitraty amount of time, but delay it because file system writes often trigger events after RemoveTempIgnore has been called.
// Seeing long delays in some situations, especially over the network.
// Seeing delays up to 40 seconds, but not going to ignore changes for that long.
await Task.Delay(5000).ConfigureAwait(false);
// Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds
// But if we make this delay too high, we risk missing legitimate changes
await Task.Delay(10000).ConfigureAwait(false);
string val;
_tempIgnoredPaths.TryRemove(path, out val);

@ -32,6 +32,7 @@ namespace MediaBrowser.Server.Implementations.Library
".wd_tv",
// Synology
"@eaDir",
"eaDir"
};

@ -17,6 +17,7 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using System;
@ -330,12 +331,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
public async Task<MediaSourceInfo> GetRecordingStream(string id, CancellationToken cancellationToken)
{
return await GetLiveStream(id, false, cancellationToken).ConfigureAwait(false);
return await GetLiveStream(id, null, false, cancellationToken).ConfigureAwait(false);
}
public async Task<MediaSourceInfo> GetChannelStream(string id, CancellationToken cancellationToken)
public async Task<MediaSourceInfo> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken)
{
return await GetLiveStream(id, true, cancellationToken).ConfigureAwait(false);
return await GetLiveStream(id, mediaSourceId, true, cancellationToken).ConfigureAwait(false);
}
public async Task<IEnumerable<MediaSourceInfo>> GetRecordingMediaSources(string id, CancellationToken cancellationToken)
@ -351,7 +352,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var item = GetInternalChannel(id);
var service = GetService(item);
return await service.GetChannelStreamMediaSources(id, cancellationToken).ConfigureAwait(false);
var sources = await service.GetChannelStreamMediaSources(item.ExternalId, cancellationToken).ConfigureAwait(false);
var list = sources.ToList();
foreach (var source in list)
{
Normalize(source, item.ChannelType == ChannelType.TV);
}
return list;
}
private ILiveTvService GetService(ILiveTvItem item)
@ -364,7 +373,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
return _services.FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
}
private async Task<MediaSourceInfo> GetLiveStream(string id, bool isChannel, CancellationToken cancellationToken)
private async Task<MediaSourceInfo> GetLiveStream(string id, string mediaSourceId, bool isChannel, CancellationToken cancellationToken)
{
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
@ -379,7 +388,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
isVideo = channel.ChannelType == ChannelType.TV;
var service = GetService(channel);
_logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId);
info = await service.GetChannelStream(channel.ExternalId, null, cancellationToken).ConfigureAwait(false);
info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
info.RequiresClosing = true;
if (info.RequiresClosing)
@ -519,6 +528,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv
stream.Index = -1;
}
}
// Set the total bitrate if not already supplied
if (!mediaSource.Bitrate.HasValue)
{
var total = mediaSource.MediaStreams.Select(i => i.BitRate ?? 0).Sum();
if (total > 0)
{
mediaSource.Bitrate = total;
}
}
}
private async Task<LiveTvChannel> GetChannel(ChannelInfo channelInfo, string serviceName, CancellationToken cancellationToken)

@ -1,10 +1,12 @@
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using System;
using System.Collections.Generic;
@ -21,13 +23,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv
private readonly ILogger _logger;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IServerApplicationHost _appHost;
public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IJsonSerializer jsonSerializer, ILogManager logManager, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
public LiveTvMediaSourceProvider(ILiveTvManager liveTvManager, IJsonSerializer jsonSerializer, ILogManager logManager, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IServerApplicationHost appHost)
{
_liveTvManager = liveTvManager;
_jsonSerializer = jsonSerializer;
_mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
_appHost = appHost;
_logger = logManager.GetLogger(GetType().Name);
}
@ -74,6 +78,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
}
var list = sources.ToList();
var serverUrl = _appHost.LocalApiUrl;
foreach (var source in list)
{
@ -84,7 +89,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
var openKeys = new List<string>();
openKeys.Add(item.GetType().Name);
openKeys.Add(item.Id.ToString("N"));
openKeys.Add(source.Id ?? string.Empty);
source.OpenToken = string.Join("|", openKeys.ToArray());
// Dummy this up so that direct play checks can still run
if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http)
{
source.Path = serverUrl;
}
}
_logger.Debug("MediaSources: {0}", _jsonSerializer.SerializeToString(list));
@ -95,13 +107,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
public async Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
{
MediaSourceInfo stream;
var isAudio = false;
const bool isAudio = false;
var keys = openToken.Split(new[] { '|' }, 2);
var keys = openToken.Split(new[] { '|' }, 3);
var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase))
{
stream = await _liveTvManager.GetChannelStream(keys[1], cancellationToken).ConfigureAwait(false);
stream = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false);
}
else
{
@ -162,30 +175,40 @@ namespace MediaBrowser.Server.Implementations.LiveTv
mediaSource.DefaultAudioStreamIndex = audioStream.Index;
}
// Try to estimate this
if (!mediaSource.Bitrate.HasValue)
var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Video);
if (videoStream != null)
{
var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Video);
if (videoStream != null)
if (!videoStream.BitRate.HasValue)
{
var width = videoStream.Width ?? 1920;
if (width >= 1900)
{
mediaSource.Bitrate = 10000000;
videoStream.BitRate = 8000000;
}
else if (width >= 1260)
{
mediaSource.Bitrate = 6000000;
videoStream.BitRate = 3000000;
}
else if (width >= 700)
{
mediaSource.Bitrate = 4000000;
videoStream.BitRate = 1000000;
}
}
}
// Try to estimate this
if (!mediaSource.Bitrate.HasValue)
{
var total = mediaSource.MediaStreams.Select(i => i.BitRate ?? 0).Sum();
if (total > 0)
{
mediaSource.Bitrate = total;
}
}
}
public Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken)

@ -40,6 +40,7 @@
"TitleLiveTV": "Live TV",
"TitleSync": "Sync",
"ButtonDonate": "Donate",
"LabelRecurringDonationCanBeCancelledHelp": "Recurring donations can be cancelled at any time from within your PayPal account.",
"HeaderMyMedia": "My Media",
"TitleNotifications": "Notifications",
"ErrorLaunchingChromecast": "There was an error launching chromecast. Please ensure your device is connected to your wireless network.",
@ -97,7 +98,7 @@
"HeaderSupporterBenefit": "A supporter membership provides additional benefits such as access to sync, premium plugins, internet channel content, and more. {0}Learn more{1}.",
"LabelSyncNoTargetsHelp": "It looks like you don't currently have any apps that support sync.",
"HeaderWelcomeToProjectServerDashboard": "Welcome to the Emby Server Dashboard",
"HeaderWelcomeToProjectWebClient": "Welcome to the Emby Web Client",
"HeaderWelcomeToProjectWebClient": "Welcome to Emby",
"ButtonTakeTheTour": "Take the tour",
"HeaderWelcomeBack": "Welcome back!",
"TitlePlugins": "Plugins",
@ -762,5 +763,8 @@
"ButtonSignInWithConnect": "Sign in with Emby Connect",
"HeaderNewServer": "New Server",
"MyDevice": "My Device",
"ButtonRemote": "Remote"
"ButtonRemote": "Remote",
"TabInfo": "Info",
"TabCast": "Cast",
"TabScenes": "Scenes"
}

@ -40,6 +40,7 @@
"TitleLiveTV": "Live TV",
"TitleSync": "Sync",
"ButtonDonate": "Donate",
"LabelRecurringDonationCanBeCancelledHelp": "Recurring donations can be cancelled at any time from within your PayPal account.",
"HeaderMyMedia": "My Media",
"TitleNotifications": "Notifications",
"ErrorLaunchingChromecast": "There was an error launching chromecast. Please ensure your device is connected to your wireless network.",
@ -97,7 +98,7 @@
"HeaderSupporterBenefit": "A supporter membership provides additional benefits such as access to sync, premium plugins, internet channel content, and more. {0}Learn more{1}.",
"LabelSyncNoTargetsHelp": "It looks like you don't currently have any apps that support sync.",
"HeaderWelcomeToProjectServerDashboard": "Welcome to the Emby Server Dashboard",
"HeaderWelcomeToProjectWebClient": "Welcome to the Emby Web Client",
"HeaderWelcomeToProjectWebClient": "Welcome to Emby",
"ButtonTakeTheTour": "Take the tour",
"HeaderWelcomeBack": "Welcome back!",
"TitlePlugins": "Plugins",
@ -762,5 +763,8 @@
"ButtonSignInWithConnect": "Sign in with Emby Connect",
"HeaderNewServer": "New Server",
"MyDevice": "My Device",
"ButtonRemote": "Remote"
"ButtonRemote": "Remote",
"TabInfo": "Info",
"TabCast": "Cast",
"TabScenes": "Scenes"
}

@ -40,6 +40,7 @@
"TitleLiveTV": "\u0422V \u043d\u0430 \u0436\u0438\u0432\u043e",
"TitleSync": "Sync",
"ButtonDonate": "Donate",
"LabelRecurringDonationCanBeCancelledHelp": "Recurring donations can be cancelled at any time from within your PayPal account.",
"HeaderMyMedia": "My Media",
"TitleNotifications": "\u0418\u0437\u0432\u0435\u0441\u0442\u0438\u044f",
"ErrorLaunchingChromecast": "There was an error launching chromecast. Please ensure your device is connected to your wireless network.",
@ -97,7 +98,7 @@
"HeaderSupporterBenefit": "A supporter membership provides additional benefits such as access to sync, premium plugins, internet channel content, and more. {0}Learn more{1}.",
"LabelSyncNoTargetsHelp": "It looks like you don't currently have any apps that support sync.",
"HeaderWelcomeToProjectServerDashboard": "Welcome to the Emby Server Dashboard",
"HeaderWelcomeToProjectWebClient": "Welcome to the Emby Web Client",
"HeaderWelcomeToProjectWebClient": "Welcome to Emby",
"ButtonTakeTheTour": "\u0420\u0430\u0437\u0433\u043b\u0435\u0434\u0430\u0439 \u043d\u0430\u043e\u043a\u043e\u043b\u043e",
"HeaderWelcomeBack": "Welcome back!",
"TitlePlugins": "Plugins",
@ -762,5 +763,8 @@
"ButtonSignInWithConnect": "Sign in with Emby Connect",
"HeaderNewServer": "New Server",
"MyDevice": "My Device",
"ButtonRemote": "Remote"
"ButtonRemote": "Remote",
"TabInfo": "Info",
"TabCast": "Cast",
"TabScenes": "Scenes"
}

@ -40,6 +40,7 @@
"TitleLiveTV": "Live TV",
"TitleSync": "Sync",
"ButtonDonate": "Donate",
"LabelRecurringDonationCanBeCancelledHelp": "Recurring donations can be cancelled at any time from within your PayPal account.",
"HeaderMyMedia": "My Media",
"TitleNotifications": "Notifications",
"ErrorLaunchingChromecast": "There was an error launching chromecast. Please ensure your device is connected to your wireless network.",
@ -97,7 +98,7 @@
"HeaderSupporterBenefit": "A supporter membership provides additional benefits such as access to sync, premium plugins, internet channel content, and more. {0}Learn more{1}.",
"LabelSyncNoTargetsHelp": "It looks like you don't currently have any apps that support sync.",
"HeaderWelcomeToProjectServerDashboard": "Welcome to the Emby Server Dashboard",
"HeaderWelcomeToProjectWebClient": "Welcome to the Emby Web Client",
"HeaderWelcomeToProjectWebClient": "Welcome to Emby",
"ButtonTakeTheTour": "Take the tour",
"HeaderWelcomeBack": "Welcome back!",
"TitlePlugins": "Plugins",
@ -762,5 +763,8 @@
"ButtonSignInWithConnect": "Sign in with Emby Connect",
"HeaderNewServer": "New Server",
"MyDevice": "My Device",
"ButtonRemote": "Remote"
"ButtonRemote": "Remote",
"TabInfo": "Info",
"TabCast": "Cast",
"TabScenes": "Scenes"
}

@ -40,6 +40,7 @@
"TitleLiveTV": "\u017div\u00e1 TV",
"TitleSync": "Sync",
"ButtonDonate": "Donate",
"LabelRecurringDonationCanBeCancelledHelp": "Recurring donations can be cancelled at any time from within your PayPal account.",
"HeaderMyMedia": "My Media",
"TitleNotifications": "Notifications",
"ErrorLaunchingChromecast": "There was an error launching chromecast. Please ensure your device is connected to your wireless network.",
@ -97,7 +98,7 @@
"HeaderSupporterBenefit": "A supporter membership provides additional benefits such as access to sync, premium plugins, internet channel content, and more. {0}Learn more{1}.",
"LabelSyncNoTargetsHelp": "It looks like you don't currently have any apps that support sync.",
"HeaderWelcomeToProjectServerDashboard": "Welcome to the Emby Server Dashboard",
"HeaderWelcomeToProjectWebClient": "Welcome to the Emby Web Client",
"HeaderWelcomeToProjectWebClient": "Welcome to Emby",
"ButtonTakeTheTour": "Take the tour",
"HeaderWelcomeBack": "Welcome back!",
"TitlePlugins": "Plugins",
@ -762,5 +763,8 @@
"ButtonSignInWithConnect": "Sign in with Emby Connect",
"HeaderNewServer": "New Server",
"MyDevice": "My Device",
"ButtonRemote": "Remote"
"ButtonRemote": "Remote",
"TabInfo": "Info",
"TabCast": "Cast",
"TabScenes": "Scenes"
}

@ -40,13 +40,14 @@
"TitleLiveTV": "Live-TV",
"TitleSync": "Synchronisation",
"ButtonDonate": "Spenden",
"LabelRecurringDonationCanBeCancelledHelp": "Fortlaufende Spenden k\u00f6nnen jederzeit \u00fcber deinen PayPal Account gek\u00fcndigt werden.",
"HeaderMyMedia": "Meine Medien",
"TitleNotifications": "Benachrichtigungen",
"ErrorLaunchingChromecast": "W\u00e4hrend des startens von Chromecast ist ein Fehler aufgetreten. Bitte stelle sicher, dass dein Ger\u00e4te mit dem WLAN verbunden ist.",
"MessageErrorLoadingSupporterInfo": "Es trat ein Fehler beim laden der Unterst\u00fctzer-Informationen auf. Bitte versuchen Sie es sp\u00e4ter erneut.",
"MessageLinkYourSupporterKey": "Verbinden Sie Ihren Unterst\u00fctzer-Schl\u00fcssel mit bis zu {0} Emby Connect Benutzern um kostenfreien Zugriff auf die folgenden Apps zu erhalten:",
"HeaderConfirmRemoveUser": "Entferne Benutzer",
"MessageSwipeDownOnRemoteControl": "Welcome to remote control. Select the device to control by clicking the cast icon in the upper right corner. Swipe down anywhere on this screen to go back to where you came from.",
"MessageSwipeDownOnRemoteControl": "Willkommen zur Fernbedienung. W\u00e4hlen Sie ein Ger\u00e4t durch Klick auf das Cast-Icon in der rechten oberen Ecke, um es fernzusteuern. Streichen Sie irgendwo auf dem Bildschirm nach unten um zur\u00fcck zu gehen.",
"MessageConfirmRemoveConnectSupporter": "M\u00f6chten Sie wirklich zus\u00e4tzliche Unterst\u00fctzer-Features von diesem Anwender entfernen?",
"ValueTimeLimitSingleHour": "Zeitlimit: 1 Stunde",
"ValueTimeLimitMultiHour": "Zeitlimit: {0} Stunden",
@ -97,7 +98,7 @@
"HeaderSupporterBenefit": "Eine Unterst\u00fctzer-Mitgliedschaft bietet weitere Funktionen wie z.B. Zugriff auf die Synchronisation, Premium-Plugins, Internet Kan\u00e4le und mehr. {0}Erfahren Sie mehr{1}.",
"LabelSyncNoTargetsHelp": "Es sieht so aus als w\u00fcrden Sie aktuell keine Apps verwenden, die Synchronisation unterst\u00fctzen.",
"HeaderWelcomeToProjectServerDashboard": "Willkommen zur Emby Server \u00dcbersicht",
"HeaderWelcomeToProjectWebClient": "Willkommen im Emby Web-Client",
"HeaderWelcomeToProjectWebClient": "Willkommen zu Emby",
"ButtonTakeTheTour": "Mache die Tour",
"HeaderWelcomeBack": "Willkommen zur\u00fcck!",
"TitlePlugins": "Plugins",
@ -123,7 +124,7 @@
"LabelFree": "Frei",
"HeaderPlaybackError": "Wiedergabefehler",
"MessagePlaybackErrorNotAllowed": "Sie sind nicht befugt diese Inhalte wiederzugeben. Bitte kontaktieren Sie Ihren Systemadministrator f\u00fcr weitere Details.",
"MessagePlaybackErrorNoCompatibleStream": "No compatible streams are currently available. Please try again later or contact your system administrator for details.",
"MessagePlaybackErrorNoCompatibleStream": "Es sind keine kompatiblen Streams verf\u00fcgbar. Bitte versuchen Sie es sp\u00e4ter erneut oder kontaktieren Sie Ihren Systemadministrator f\u00fcr weitere Details.",
"MessagePlaybackErrorRateLimitExceeded": "Ihr Wiedergabelimit wurde \u00fcberschritten. Bitte kontaktieren Sie Ihren Systemadministrator f\u00fcr weitere Details.",
"MessagePlaybackErrorPlaceHolder": "Der gew\u00e4hlte Inhalt kann auf diesem Ger\u00e4t nicht abgespielt werden.",
"HeaderSelectAudio": "W\u00e4hle Audio",
@ -398,7 +399,7 @@
"TabMetadata": "Metadata",
"TabDLNA": "DLNA",
"TabLiveTV": "Live-TV",
"TabAutoOrganize": "Automatische Organisation",
"TabAutoOrganize": "Autom.Organisation",
"TabPlugins": "Plugins",
"TabAdvanced": "Erweitert",
"TabHelp": "Hilfe",
@ -586,7 +587,7 @@
"TooltipLike": "Like",
"TooltipDislike": "Dislike",
"TooltipPlayed": "Gespielt",
"ValueSeriesYearToPresent": "{0}-vorhanden",
"ValueSeriesYearToPresent": "{0}-heute",
"ValueAwards": "Auszeichnungen: {0}",
"ValueBudget": "Budget: {0}",
"ValueRevenue": "Einnahmen: {0}",
@ -688,7 +689,7 @@
"DashboardTourHelp": "Die In-App-Hilfe Schaltfl\u00e4che bietet eine schnelle M\u00f6glichkeit um eine Wiki-Seite zum aktuellen Inhalt zu \u00f6ffnen.",
"DashboardTourUsers": "Erstelle einfach Benutzeraccounts f\u00fcr Freunde und Familie. Jeder mit seinen individuellen Einstellungen bei Berechtigungen, Blibliothekenzugriff, Kindersicherung und mehr.",
"DashboardTourCinemaMode": "Der Kino-Modus bringt das Kinoerlebnis direkt in dein Wohnzimmer, mit der F\u00e4higkeit Trailer und benutzerdefinierte Intros vor dem Hauptfilm zu spielen.",
"DashboardTourChapters": "Aktiviere die Bildgenerierung f\u00fcr die Kapitel deiner Videos f\u00fcr eine bessere Darstellung w\u00e4hrend des Ansehens.",
"DashboardTourChapters": "Aktiviere Kapitel-Bilder Generierung f\u00fcr Videos f\u00fcr eine bessere Darstellung.",
"DashboardTourSubtitles": "Lade automatisch Untertitel f\u00fcr jede Sprache f\u00fcr deine Videos herunter.",
"DashboardTourPlugins": "Installiere Plugins wie Internet Videoportale, Live-TV, Metadatenscanner und mehr.",
"DashboardTourNotifications": "Sende automatisch Benachrichtigungen von Serverereignissen auf dein mobiles Endger\u00e4t, per E-Mail und mehr.",
@ -757,10 +758,13 @@
"SyncJobItemStatusCancelled": "Abgebrochen",
"LabelProfile": "Profil:",
"LabelBitrateMbps": "Datenrate (Mbps):",
"EmbyIntroDownloadMessage": "To download and install Emby Server visit {0}.",
"ButtonNewServer": "New Server",
"ButtonSignInWithConnect": "Sign in with Emby Connect",
"HeaderNewServer": "New Server",
"MyDevice": "My Device",
"ButtonRemote": "Remote"
"EmbyIntroDownloadMessage": "Um Emby herunterzuladen und zu installieren, besuchen Sie: {0}.",
"ButtonNewServer": "Neuer Server",
"ButtonSignInWithConnect": "Anmelden mit Emby Connect",
"HeaderNewServer": "Neuer Server",
"MyDevice": "Mein Ger\u00e4t",
"ButtonRemote": "Fernbedienung",
"TabInfo": "Info",
"TabCast": "Darsteller",
"TabScenes": "Szenen"
}

@ -40,6 +40,7 @@
"TitleLiveTV": "Live TV",
"TitleSync": "Sync",
"ButtonDonate": "Donate",
"LabelRecurringDonationCanBeCancelledHelp": "Recurring donations can be cancelled at any time from within your PayPal account.",
"HeaderMyMedia": "My Media",
"TitleNotifications": "\u0395\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2",
"ErrorLaunchingChromecast": "There was an error launching chromecast. Please ensure your device is connected to your wireless network.",
@ -97,7 +98,7 @@
"HeaderSupporterBenefit": "A supporter membership provides additional benefits such as access to sync, premium plugins, internet channel content, and more. {0}Learn more{1}.",
"LabelSyncNoTargetsHelp": "It looks like you don't currently have any apps that support sync.",
"HeaderWelcomeToProjectServerDashboard": "Welcome to the Emby Server Dashboard",
"HeaderWelcomeToProjectWebClient": "Welcome to the Emby Web Client",
"HeaderWelcomeToProjectWebClient": "Welcome to Emby",
"ButtonTakeTheTour": "\u039a\u03ac\u03bd\u03c4\u03b5 \u03c4\u03b7\u03bd \u039e\u03b5\u03bd\u03ac\u03b3\u03b7\u03c3\u03b7",
"HeaderWelcomeBack": "Welcome back!",
"TitlePlugins": "\u03a0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03b1",
@ -762,5 +763,8 @@
"ButtonSignInWithConnect": "Sign in with Emby Connect",
"HeaderNewServer": "New Server",
"MyDevice": "My Device",
"ButtonRemote": "Remote"
"ButtonRemote": "Remote",
"TabInfo": "Info",
"TabCast": "Cast",
"TabScenes": "Scenes"
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save