This commit is contained in:
tikuf 2014-04-16 10:14:20 +10:00
commit 13196544c2
105 changed files with 2068 additions and 4746 deletions

View file

@ -0,0 +1,89 @@
using MediaBrowser.Controller.Dlna;
using ServiceStack;
using ServiceStack.Web;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace MediaBrowser.Api.Dlna
{
[Route("/Dlna/{UuId}/description.xml", "GET", Summary = "Gets dlna server info")]
[Route("/Dlna/{UuId}/description", "GET", Summary = "Gets dlna server info")]
public class GetDescriptionXml
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UuId { get; set; }
}
[Route("/Dlna/{UuId}/contentdirectory.xml", "GET", Summary = "Gets dlna content directory xml")]
[Route("/Dlna/{UuId}/contentdirectory", "GET", Summary = "Gets the content directory xml")]
public class GetContentDirectory
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UuId { get; set; }
}
[Route("/Dlna/{UuId}/control", "POST", Summary = "Processes a control request")]
public class ProcessControlRequest : IRequiresRequestStream
{
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
public string UuId { get; set; }
public Stream RequestStream { get; set; }
}
public class DlnaServerService : BaseApiService
{
private readonly IDlnaManager _dlnaManager;
public DlnaServerService(IDlnaManager dlnaManager)
{
_dlnaManager = dlnaManager;
}
public object Get(GetDescriptionXml request)
{
var xml = _dlnaManager.GetServerDescriptionXml(GetRequestHeaders(), request.UuId);
return ResultFactory.GetResult(xml, "text/xml");
}
public object Get(GetContentDirectory request)
{
var xml = _dlnaManager.GetContentDirectoryXml(GetRequestHeaders());
return ResultFactory.GetResult(xml, "text/xml");
}
public object Post(ProcessControlRequest request)
{
var response = PostAsync(request).Result;
return ResultFactory.GetResult(response.Xml, "text/xml");
}
private async Task<ControlResponse> PostAsync(ProcessControlRequest request)
{
using (var reader = new StreamReader(request.RequestStream))
{
return _dlnaManager.ProcessControlRequest(new ControlRequest
{
Headers = GetRequestHeaders(),
InputXml = await reader.ReadToEndAsync().ConfigureAwait(false)
});
}
}
private IDictionary<string, string> GetRequestHeaders()
{
var headers = new Dictionary<string, string>();
foreach (var key in Request.Headers.AllKeys)
{
headers[key] = Request.Headers[key];
}
return headers;
}
}
}

View file

@ -4,7 +4,7 @@ using ServiceStack;
using System.Collections.Generic;
using System.Linq;
namespace MediaBrowser.Api
namespace MediaBrowser.Api.Dlna
{
[Route("/Dlna/ProfileInfos", "GET", Summary = "Gets a list of profiles")]
public class GetProfileInfos : IReturn<List<DeviceProfileInfo>>

View file

@ -36,7 +36,7 @@ namespace MediaBrowser.Api.Library
public string Id { get; set; }
}
[Route("/Videos/{Id}/Subtitle/{Index}", "GET")]
[Route("/Videos/{Id}/Subtitles/{Index}", "GET")]
[Api(Description = "Gets an external subtitle file")]
public class GetSubtitle
{

View file

@ -66,7 +66,8 @@
<Link>Properties\SharedVersion.cs</Link>
</Compile>
<Compile Include="ChannelService.cs" />
<Compile Include="DlnaService.cs" />
<Compile Include="Dlna\DlnaServerService.cs" />
<Compile Include="Dlna\DlnaService.cs" />
<Compile Include="Movies\CollectionService.cs" />
<Compile Include="Music\AlbumsService.cs" />
<Compile Include="AppThemeService.cs" />

View file

@ -185,9 +185,9 @@ namespace MediaBrowser.Api.Playback
{
var args = string.Empty;
if (state.IsRemote || !state.HasMediaStreams)
if (!state.HasMediaStreams)
{
return string.Empty;
return state.IsInputVideo ? "-sn" : string.Empty;
}
if (state.VideoStream != null)
@ -1341,6 +1341,12 @@ namespace MediaBrowser.Api.Playback
RequestedUrl = url
};
if (!string.IsNullOrWhiteSpace(request.AudioCodec))
{
state.SupportedAudioCodecs = request.AudioCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
state.Request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault();
}
var item = string.IsNullOrEmpty(request.MediaSourceId) ?
DtoService.GetItemByDtoId(request.Id) :
DtoService.GetItemByDtoId(request.MediaSourceId);
@ -1487,33 +1493,27 @@ namespace MediaBrowser.Api.Playback
if (videoRequest != null)
{
if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream, state.VideoType))
if (state.VideoStream != null && CanStreamCopyVideo(videoRequest, state.VideoStream))
{
videoRequest.VideoCodec = "copy";
}
//if (state.AudioStream != null && CanStreamCopyAudio(request, state.AudioStream))
//{
// request.AudioCodec = "copy";
//}
if (state.AudioStream != null && CanStreamCopyAudio(request, state.AudioStream, state.SupportedAudioCodecs))
{
request.AudioCodec = "copy";
}
}
return state;
}
private bool CanStreamCopyVideo(VideoStreamRequest request, MediaStream videoStream, VideoType videoType)
private bool CanStreamCopyVideo(VideoStreamRequest request, MediaStream videoStream)
{
if (videoStream.IsInterlaced)
{
return false;
}
// Not going to attempt this with folder rips
if (videoType != VideoType.VideoFile)
{
return false;
}
// Source and target codecs must match
if (!string.Equals(request.VideoCodec, videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
@ -1584,13 +1584,13 @@ namespace MediaBrowser.Api.Playback
}
}
return SupportsAutomaticVideoStreamCopy;
return request.EnableAutoStreamCopy;
}
private bool CanStreamCopyAudio(StreamRequest request, MediaStream audioStream)
private bool CanStreamCopyAudio(StreamRequest request, MediaStream audioStream, List<string> supportedAudioCodecs)
{
// Source and target codecs must match
if (string.IsNullOrEmpty(request.AudioCodec) || !string.Equals(request.AudioCodec, audioStream.Codec, StringComparison.OrdinalIgnoreCase))
if (string.IsNullOrEmpty(audioStream.Codec) || !supportedAudioCodecs.Contains(audioStream.Codec, StringComparer.OrdinalIgnoreCase))
{
return false;
}
@ -1623,15 +1623,7 @@ namespace MediaBrowser.Api.Playback
}
}
return SupportsAutomaticVideoStreamCopy;
}
protected virtual bool SupportsAutomaticVideoStreamCopy
{
get
{
return false;
}
return true;
}
private void ApplyDeviceProfileSettings(StreamState state)

View file

@ -24,7 +24,8 @@ namespace MediaBrowser.Api.Playback.Hls
/// </summary>
public abstract class BaseHlsService : BaseStreamingService
{
protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager, IEncodingManager encodingManager, IDlnaManager dlnaManager) : base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository, liveTvManager, encodingManager, dlnaManager)
protected BaseHlsService(IServerConfigurationManager serverConfig, IUserManager userManager, ILibraryManager libraryManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IDtoService dtoService, IFileSystem fileSystem, IItemRepository itemRepository, ILiveTvManager liveTvManager, IEncodingManager encodingManager, IDlnaManager dlnaManager)
: base(serverConfig, userManager, libraryManager, isoManager, mediaEncoder, dtoService, fileSystem, itemRepository, liveTvManager, encodingManager, dlnaManager)
{
}
@ -77,6 +78,7 @@ namespace MediaBrowser.Api.Playback.Hls
return ProcessRequestAsync(request).Result;
}
private static readonly SemaphoreSlim FfmpegStartLock = new SemaphoreSlim(1, 1);
/// <summary>
/// Processes the request async.
/// </summary>
@ -103,31 +105,40 @@ namespace MediaBrowser.Api.Playback.Hls
}
var playlist = GetOutputFilePath(state);
var isPlaylistNewlyCreated = false;
// If the playlist doesn't already exist, startup ffmpeg
if (!File.Exists(playlist))
{
isPlaylistNewlyCreated = true;
try
{
await StartFfMpeg(state, playlist).ConfigureAwait(false);
}
catch
{
state.Dispose();
throw;
}
}
else
if (File.Exists(playlist))
{
ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls);
}
if (isPlaylistNewlyCreated)
else
{
await WaitForMinimumSegmentCount(playlist, GetSegmentWait()).ConfigureAwait(false);
await FfmpegStartLock.WaitAsync().ConfigureAwait(false);
try
{
if (File.Exists(playlist))
{
ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType.Hls);
}
else
{
// If the playlist doesn't already exist, startup ffmpeg
try
{
await StartFfMpeg(state, playlist).ConfigureAwait(false);
}
catch
{
state.Dispose();
throw;
}
}
await WaitForMinimumSegmentCount(playlist, GetSegmentWait()).ConfigureAwait(false);
}
finally
{
FfmpegStartLock.Release();
}
}
int audioBitrate;
@ -295,7 +306,7 @@ namespace MediaBrowser.Api.Playback.Hls
// If performSubtitleConversions is true we're actually starting ffmpeg
var startNumberParam = performSubtitleConversions ? GetStartNumber(state).ToString(UsCulture) : "0";
var args = string.Format("{0} {1} -i {2}{3} -map_metadata -1 -threads {4} {5} {6} -sc_threshold 0 {7} -hls_time {8} -start_number {9} -hls_list_size {10} \"{11}\"",
itsOffset,
inputModifier,

View file

@ -98,14 +98,6 @@ namespace MediaBrowser.Api.Playback.Hls
ApiEntryPoint.Instance.OnTranscodeEndRequest(playlist, TranscodingJobType.Hls);
}
protected override bool SupportsAutomaticVideoStreamCopy
{
get
{
return true;
}
}
/// <summary>
/// Gets the specified request.
/// </summary>

View file

@ -171,5 +171,13 @@ namespace MediaBrowser.Api.Playback
return Width.HasValue || Height.HasValue;
}
}
[ApiMember(Name = "EnableAutoStreamCopy", Description = "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
public bool EnableAutoStreamCopy { get; set; }
public VideoStreamRequest()
{
EnableAutoStreamCopy = true;
}
}
}

View file

@ -67,10 +67,13 @@ namespace MediaBrowser.Api.Playback
public string AudioSync = "1";
public string VideoSync = "vfr";
public List<string> SupportedAudioCodecs { get; set; }
public StreamState(ILiveTvManager liveTvManager, ILogger logger)
{
_liveTvManager = liveTvManager;
_logger = logger;
SupportedAudioCodecs = new List<string>();
}
public string InputAudioSync { get; set; }

View file

@ -1,5 +1,4 @@
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Session;
using ServiceStack;
@ -32,10 +31,10 @@ namespace MediaBrowser.Api
}
/// <summary>
/// Class BrowseTo
/// Class DisplayContent
/// </summary>
[Route("/Sessions/{Id}/Viewing", "POST", Summary = "Instructs a session to browse to an item or view")]
public class BrowseTo : IReturnVoid
public class DisplayContent : IReturnVoid
{
/// <summary>
/// Gets or sets the id.
@ -218,6 +217,7 @@ namespace MediaBrowser.Api
public Guid UserId { get; set; }
}
[Route("/Sessions/Capabilities", "POST", Summary = "Updates capabilities for a device")]
[Route("/Sessions/{Id}/Capabilities", "POST", Summary = "Updates capabilities for a device")]
public class PostCapabilities : IReturnVoid
{
@ -226,7 +226,7 @@ namespace MediaBrowser.Api
/// </summary>
/// <value>The id.</value>
[ApiMember(Name = "Id", Description = "Session Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
public Guid Id { get; set; }
public string Id { get; set; }
[ApiMember(Name = "PlayableMediaTypes", Description = "A list of playable media types, comma delimited. Audio, Video, Book, Game, Photo.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
public string PlayableMediaTypes { get; set; }
@ -307,7 +307,7 @@ namespace MediaBrowser.Api
/// Posts the specified request.
/// </summary>
/// <param name="request">The request.</param>
public void Post(BrowseTo request)
public void Post(DisplayContent request)
{
var command = new BrowseRequest
{
@ -421,7 +421,11 @@ namespace MediaBrowser.Api
public void Post(PostCapabilities request)
{
_sessionManager.ReportCapabilities(request.Id, new SessionCapabilities
if (string.IsNullOrWhiteSpace(request.Id))
{
request.Id = GetSession().Id.ToString("N");
}
_sessionManager.ReportCapabilities(new Guid(request.Id), new SessionCapabilities
{
PlayableMediaTypes = request.PlayableMediaTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(),

View file

@ -15,6 +15,44 @@ namespace MediaBrowser.Common.Implementations.Networking
/// </summary>
/// <returns>IPAddress.</returns>
public IEnumerable<string> GetLocalIpAddresses()
{
var list = GetIPsDefault().Where(i => !IPAddress.IsLoopback(i)).Select(i => i.ToString()).ToList();
if (list.Count > 0)
{
return list;
}
return GetLocalIpAddressesFallback();
}
private IEnumerable<IPAddress> GetIPsDefault()
{
foreach (var adapter in NetworkInterface.GetAllNetworkInterfaces())
{
var props = adapter.GetIPProperties();
var gateways = from ga in props.GatewayAddresses
where !ga.Address.Equals(IPAddress.Any)
select true;
if (!gateways.Any())
{
continue;
}
foreach (var uni in props.UnicastAddresses)
{
var address = uni.Address;
if (address.AddressFamily != AddressFamily.InterNetwork)
{
continue;
}
yield return address;
}
}
}
private IEnumerable<string> GetLocalIpAddressesFallback()
{
var host = Dns.GetHostEntry(Dns.GetHostName());
@ -25,7 +63,7 @@ namespace MediaBrowser.Common.Implementations.Networking
.Select(i => i.ToString())
.Reverse();
}
/// <summary>
/// Gets a random port number that is currently available
/// </summary>
@ -50,6 +88,7 @@ namespace MediaBrowser.Common.Implementations.Networking
.Select(i => BitConverter.ToString(i.GetPhysicalAddress().GetAddressBytes()))
.FirstOrDefault();
}
/// <summary>
/// Parses the specified endpointstring.
/// </summary>

View file

@ -0,0 +1,28 @@
using System.Collections.Generic;
namespace MediaBrowser.Controller.Dlna
{
public class ControlRequest
{
public IDictionary<string, string> Headers { get; set; }
public string InputXml { get; set; }
public ControlRequest()
{
Headers = new Dictionary<string, string>();
}
}
public class ControlResponse
{
public IDictionary<string, string> Headers { get; set; }
public string Xml { get; set; }
public ControlResponse()
{
Headers = new Dictionary<string, string>();
}
}
}

View file

@ -55,5 +55,27 @@ namespace MediaBrowser.Controller.Dlna
/// <param name="deviceInfo">The device information.</param>
/// <returns>DeviceProfile.</returns>
DeviceProfile GetProfile(DeviceIdentification deviceInfo);
/// <summary>
/// Gets the server description XML.
/// </summary>
/// <param name="headers">The headers.</param>
/// <param name="serverUuId">The server uu identifier.</param>
/// <returns>System.String.</returns>
string GetServerDescriptionXml(IDictionary<string, string> headers, string serverUuId);
/// <summary>
/// Gets the content directory XML.
/// </summary>
/// <param name="headers">The headers.</param>
/// <returns>System.String.</returns>
string GetContentDirectoryXml(IDictionary<string, string> headers);
/// <summary>
/// Processes the control request.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>ControlResponse.</returns>
ControlResponse ProcessControlRequest(ControlRequest request);
}
}

View file

@ -10,7 +10,7 @@ namespace MediaBrowser.Controller.Entities.Audio
/// <summary>
/// Class Audio
/// </summary>
public class Audio : BaseItem, IHasMediaStreams, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<SongInfo>, IHasSeries
public class Audio : BaseItem, IHasMediaStreams, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<SongInfo>
{
public Audio()
{
@ -51,15 +51,6 @@ namespace MediaBrowser.Controller.Entities.Audio
}
}
[IgnoreDataMember]
public string SeriesName
{
get
{
return Album;
}
}
/// <summary>
/// Gets or sets the artist.
/// </summary>

View file

@ -11,7 +11,7 @@ namespace MediaBrowser.Controller.Entities.Audio
/// <summary>
/// Class MusicAlbum
/// </summary>
public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasTags, IHasLookupInfo<AlbumInfo>, IHasSeries
public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasTags, IHasLookupInfo<AlbumInfo>
{
public List<Guid> SoundtrackIds { get; set; }
@ -67,15 +67,6 @@ namespace MediaBrowser.Controller.Entities.Audio
}
}
[IgnoreDataMember]
public string SeriesName
{
get
{
return AlbumArtist;
}
}
/// <summary>
/// Override this to true if class should be grouped under a container in indicies
/// The container class should be defined via IndexContainer

View file

@ -1,4 +1,6 @@

using MediaBrowser.Model.Entities;
using System.Collections.Generic;
namespace MediaBrowser.Controller.LiveTv
{
public class LiveStreamInfo
@ -20,5 +22,22 @@ namespace MediaBrowser.Controller.LiveTv
/// </summary>
/// <value>The identifier.</value>
public string Id { get; set; }
/// <summary>
/// Gets or sets the media container.
/// </summary>
/// <value>The media container.</value>
public string MediaContainer { get; set; }
/// <summary>
/// Gets or sets the media streams.
/// </summary>
/// <value>The media streams.</value>
public List<MediaStream> MediaStreams { get; set; }
public LiveStreamInfo()
{
MediaStreams = new List<MediaStream>();
}
}
}

View file

@ -78,6 +78,7 @@
<Compile Include="Channels\Channel.cs" />
<Compile Include="Collections\CollectionCreationOptions.cs" />
<Compile Include="Collections\ICollectionManager.cs" />
<Compile Include="Dlna\ControlRequest.cs" />
<Compile Include="Dlna\IDlnaManager.cs" />
<Compile Include="Drawing\IImageProcessor.cs" />
<Compile Include="Drawing\ImageFormat.cs" />

View file

@ -103,8 +103,9 @@ namespace MediaBrowser.Controller.Resolvers
".wav",
".ape",
".ogg",
".oga"
".oga",
".asf",
".mp4"
};
private static readonly Dictionary<string, string> AudioFileExtensionsDictionary = AudioFileExtensions.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase);

View file

@ -35,14 +35,6 @@ namespace MediaBrowser.Controller.Session
/// <returns>Task.</returns>
Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken);
/// <summary>
/// Sends the browse command.
/// </summary>
/// <param name="command">The command.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task SendBrowseCommand(BrowseRequest command, CancellationToken cancellationToken);
/// <summary>
/// Sends the playstate command.
/// </summary>
@ -96,6 +88,22 @@ namespace MediaBrowser.Controller.Session
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task SendSessionEndedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken);
/// <summary>
/// Sends the playback start notification.
/// </summary>
/// <param name="sessionInfo">The session information.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken);
/// <summary>
/// Sends the playback start notification.
/// </summary>
/// <param name="sessionInfo">The session information.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken);
/// <summary>
/// Sends the server restart notification.

View file

@ -0,0 +1,12 @@

namespace MediaBrowser.Dlna.Common
{
public class Argument
{
public string Name { get; set; }
public string Direction { get; set; }
public string RelatedStateVariable { get; set; }
}
}

View file

@ -1,5 +1,5 @@

namespace MediaBrowser.Dlna.PlayTo
namespace MediaBrowser.Dlna.Common
{
public class DeviceIcon
{

View file

@ -1,5 +1,5 @@

namespace MediaBrowser.Dlna.PlayTo
namespace MediaBrowser.Dlna.Common
{
public class DeviceService
{
@ -13,15 +13,6 @@ namespace MediaBrowser.Dlna.PlayTo
public string EventSubUrl { get; set; }
public DeviceService(string serviceType, string serviceId, string scpdUrl, string controlUrl, string eventSubUrl)
{
ServiceType = serviceType;
ServiceId = serviceId;
ScpdUrl = scpdUrl;
ControlUrl = controlUrl;
EventSubUrl = eventSubUrl;
}
public override string ToString()
{
return string.Format("{0}", ServiceId);

View file

@ -0,0 +1,21 @@
using System.Collections.Generic;
namespace MediaBrowser.Dlna.Common
{
public class ServiceAction
{
public string Name { get; set; }
public List<Argument> ArgumentList { get; set; }
public override string ToString()
{
return Name;
}
public ServiceAction()
{
ArgumentList = new List<Argument>();
}
}
}

View file

@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace MediaBrowser.Dlna.Common
{
public class StateVariable
{
public string Name { get; set; }
public string DataType { get; set; }
public bool SendsEvents { get; set; }
public List<string> AllowedValues { get; set; }
public override string ToString()
{
return Name;
}
public StateVariable()
{
AllowedValues = new List<string>();
}
}
}

View file

@ -1,9 +1,9 @@
using System.Text;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Dlna.Profiles;
using MediaBrowser.Dlna.Server;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Serialization;
@ -11,6 +11,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace MediaBrowser.Dlna
@ -476,5 +477,26 @@ namespace MediaBrowser.Dlna
internal DeviceProfileInfo Info { get; set; }
internal string Path { get; set; }
}
public string GetServerDescriptionXml(IDictionary<string, string> headers, string serverUuId)
{
var profile = GetProfile(headers) ??
GetDefaultProfile();
return new DescriptionXmlBuilder(profile, serverUuId).GetXml();
}
public string GetContentDirectoryXml(IDictionary<string, string> headers)
{
var profile = GetProfile(headers) ??
GetDefaultProfile();
return new ContentDirectoryXmlBuilder(profile).GetXml();
}
public ControlResponse ProcessControlRequest(ControlRequest request)
{
return new ControlHandler(_logger).ProcessControlRequest(request);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -52,13 +52,13 @@
<Link>Properties\SharedVersion.cs</Link>
</Compile>
<Compile Include="DlnaManager.cs" />
<Compile Include="PlayTo\Argument.cs" />
<Compile Include="Common\Argument.cs" />
<Compile Include="PlayTo\CurrentIdEventArgs.cs" />
<Compile Include="PlayTo\Device.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="PlayTo\DeviceInfo.cs" />
<Compile Include="PlayTo\DeviceService.cs" />
<Compile Include="Common\DeviceService.cs" />
<Compile Include="PlayTo\DidlBuilder.cs" />
<Compile Include="PlayTo\DlnaController.cs" />
<Compile Include="PlayTo\Extensions.cs" />
@ -68,20 +68,25 @@
<Compile Include="PlayTo\PlaylistItemFactory.cs" />
<Compile Include="PlayTo\PlayToManager.cs" />
<Compile Include="PlayTo\PlayToServerEntryPoint.cs" />
<Compile Include="PlayTo\ServiceAction.cs" />
<Compile Include="Common\ServiceAction.cs" />
<Compile Include="Profiles\Foobar2000Profile.cs" />
<Compile Include="Profiles\Windows81Profile.cs" />
<Compile Include="Profiles\WindowsMediaCenterProfile.cs" />
<Compile Include="Profiles\WindowsPhoneProfile.cs" />
<Compile Include="Server\ControlHandler.cs" />
<Compile Include="Server\ServiceActionListBuilder.cs" />
<Compile Include="Server\ContentDirectoryXmlBuilder.cs" />
<Compile Include="Server\Datagram.cs" />
<Compile Include="Server\DescriptionXmlBuilder.cs" />
<Compile Include="Ssdp\SsdpHelper.cs" />
<Compile Include="PlayTo\SsdpHttpClient.cs" />
<Compile Include="PlayTo\StateVariable.cs" />
<Compile Include="Common\StateVariable.cs" />
<Compile Include="PlayTo\StreamHelper.cs" />
<Compile Include="PlayTo\TransportCommands.cs" />
<Compile Include="PlayTo\TransportStateEventArgs.cs" />
<Compile Include="PlayTo\uBaseObject.cs" />
<Compile Include="PlayTo\uContainer.cs" />
<Compile Include="PlayTo\DeviceIcon.cs" />
<Compile Include="Common\DeviceIcon.cs" />
<Compile Include="PlayTo\uParser.cs" />
<Compile Include="PlayTo\uPnpNamespaces.cs" />
<Compile Include="Profiles\DefaultProfile.cs" />
@ -141,6 +146,12 @@
<ItemGroup>
<EmbeddedResource Include="Profiles\Xml\Default.xml" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Images\logo-120.jpg" />
<EmbeddedResource Include="Images\logo-120.png" />
<EmbeddedResource Include="Images\logo-48.jpg" />
<EmbeddedResource Include="Images\logo-48.png" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

View file

@ -1,29 +0,0 @@
using System;
using System.Xml.Linq;
namespace MediaBrowser.Dlna.PlayTo
{
public class Argument
{
public string Name { get; set; }
public string Direction { get; set; }
public string RelatedStateVariable { get; set; }
public static Argument FromXml(XElement container)
{
if (container == null)
{
throw new ArgumentNullException("container");
}
return new Argument
{
Name = container.GetValue(uPnpNamespaces.svc + "name"),
Direction = container.GetValue(uPnpNamespaces.svc + "direction"),
RelatedStateVariable = container.GetValue(uPnpNamespaces.svc + "relatedStateVariable")
};
}
}
}

View file

@ -1,9 +1,10 @@
using System.Globalization;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Dlna.Common;
using MediaBrowser.Model.Logging;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -209,6 +210,9 @@ namespace MediaBrowser.Dlna.PlayTo
return SetVolume(tmp);
}
/// <summary>
/// Sets volume on a scale of 0-100
/// </summary>
public async Task<bool> SetVolume(int value)
{
var command = RendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
@ -337,7 +341,7 @@ namespace MediaBrowser.Dlna.PlayTo
throw new InvalidOperationException("Unable to find service");
}
var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType, 1))
var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, 1))
.ConfigureAwait(false);
_lapsCount = GetLapsCount();
@ -352,7 +356,7 @@ namespace MediaBrowser.Dlna.PlayTo
var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType);
var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType, 1))
var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, 1))
.ConfigureAwait(false);
await Task.Delay(50).ConfigureAwait(false);
return true;
@ -366,7 +370,7 @@ namespace MediaBrowser.Dlna.PlayTo
var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType);
var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, RendererCommands.BuildPost(command, service.ServiceType, 1))
var result = await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, 1))
.ConfigureAwait(false);
await Task.Delay(50).ConfigureAwait(false);
@ -575,7 +579,7 @@ namespace MediaBrowser.Dlna.PlayTo
return false;
}
var trackString = (string) track;
var trackString = (string)track;
if (string.IsNullOrWhiteSpace(trackString) || string.Equals(trackString, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
{
@ -583,7 +587,7 @@ namespace MediaBrowser.Dlna.PlayTo
}
XElement uPnpResponse;
try
{
uPnpResponse = XElement.Parse(trackString);
@ -838,7 +842,14 @@ namespace MediaBrowser.Dlna.PlayTo
var controlURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("controlURL"));
var eventSubURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("eventSubURL"));
return new DeviceService(type, id, scpdUrl, controlURL, eventSubURL);
return new DeviceService
{
ControlUrl = controlURL,
EventSubUrl = eventSubURL,
ScpdUrl = scpdUrl,
ServiceId = id,
ServiceType = type
};
}
#region Events

View file

@ -1,6 +1,6 @@
using MediaBrowser.Controller.Dlna;
using System.Collections.Generic;
using MediaBrowser.Dlna.Common;
using MediaBrowser.Model.Dlna;
using System.Collections.Generic;
namespace MediaBrowser.Dlna.PlayTo
{

View file

@ -1,4 +1,5 @@
using MediaBrowser.Common.Net;
using System.Globalization;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Entities;
@ -206,7 +207,8 @@ namespace MediaBrowser.Dlna.PlayTo
IsPaused = _device.IsPaused,
MediaSourceId = playlistItem.MediaSourceId,
AudioStreamIndex = playlistItem.AudioStreamIndex,
SubtitleStreamIndex = playlistItem.SubtitleStreamIndex
SubtitleStreamIndex = playlistItem.SubtitleStreamIndex,
VolumeLevel = _device.Volume
}).ConfigureAwait(false);
}
@ -331,12 +333,17 @@ namespace MediaBrowser.Dlna.PlayTo
return Task.FromResult(true);
}
public Task SendServerShutdownNotification(CancellationToken cancellationToken)
public Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
{
return Task.FromResult(true);
}
public Task SendBrowseCommand(BrowseRequest command, CancellationToken cancellationToken)
public Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
{
return Task.FromResult(true);
}
public Task SendServerShutdownNotification(CancellationToken cancellationToken)
{
return Task.FromResult(true);
}
@ -609,11 +616,13 @@ namespace MediaBrowser.Dlna.PlayTo
}
}
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
public Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
{
GeneralCommandType commandType;
if (!Enum.TryParse(command.Name, true, out commandType))
if (Enum.TryParse(command.Name, true, out commandType))
{
switch (commandType)
{
@ -627,6 +636,24 @@ namespace MediaBrowser.Dlna.PlayTo
return _device.VolumeUp(true);
case GeneralCommandType.ToggleMute:
return _device.ToggleMute();
case GeneralCommandType.SetVolume:
{
string volumeArg;
if (command.Arguments.TryGetValue("Volume", out volumeArg))
{
int volume;
if (int.TryParse(volumeArg, NumberStyles.Any, _usCulture, out volume))
{
return _device.SetVolume(volume);
}
throw new ArgumentException("Unsupported volume value supplied.");
}
throw new ArgumentException("Volume argument cannot be null");
}
default:
return Task.FromResult(true);
}

View file

@ -265,7 +265,8 @@ namespace MediaBrowser.Dlna.PlayTo
GeneralCommandType.VolumeUp.ToString(),
GeneralCommandType.Mute.ToString(),
GeneralCommandType.Unmute.ToString(),
GeneralCommandType.ToggleMute.ToString()
GeneralCommandType.ToggleMute.ToString(),
GeneralCommandType.SetVolume.ToString()
}
});

View file

@ -1,34 +0,0 @@
using System.Collections.Generic;
using System.Xml.Linq;
namespace MediaBrowser.Dlna.PlayTo
{
public class ServiceAction
{
public string Name { get; set; }
public List<Argument> ArgumentList { get; set; }
public override string ToString()
{
return Name;
}
public static ServiceAction FromXml(XElement container)
{
var argumentList = new List<Argument>();
foreach (var arg in container.Descendants(uPnpNamespaces.svc + "argument"))
{
argumentList.Add(Argument.FromXml(arg));
}
return new ServiceAction
{
Name = container.GetValue(uPnpNamespaces.svc + "name"),
ArgumentList = argumentList
};
}
}
}

View file

@ -1,5 +1,6 @@
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Dlna.Common;
using System;
using System.Globalization;
using System.IO;

View file

@ -1,52 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace MediaBrowser.Dlna.PlayTo
{
public class StateVariable
{
public string Name { get; set; }
public string DataType { get; set; }
private List<string> _allowedValues = new List<string>();
public List<string> AllowedValues
{
get
{
return _allowedValues;
}
set
{
_allowedValues = value;
}
}
public override string ToString()
{
return Name;
}
public static StateVariable FromXml(XElement container)
{
var allowedValues = new List<string>();
var element = container.Descendants(uPnpNamespaces.svc + "allowedValueList")
.FirstOrDefault();
if (element != null)
{
var values = element.Descendants(uPnpNamespaces.svc + "allowedValue");
allowedValues.AddRange(values.Select(child => child.Value));
}
return new StateVariable
{
Name = container.GetValue(uPnpNamespaces.svc + "name"),
DataType = container.GetValue(uPnpNamespaces.svc + "dataType"),
AllowedValues = allowedValues
};
}
}
}

View file

@ -1,4 +1,6 @@
using System.Collections.Generic;
using System;
using MediaBrowser.Dlna.Common;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@ -40,7 +42,7 @@ namespace MediaBrowser.Dlna.PlayTo
foreach (var container in actionList.Descendants(uPnpNamespaces.svc + "action"))
{
command.ServiceActions.Add(ServiceAction.FromXml(container));
command.ServiceActions.Add(ServiceActionFromXml(container));
}
var stateValues = document.Descendants(uPnpNamespaces.ServiceStateTable).FirstOrDefault();
@ -49,13 +51,66 @@ namespace MediaBrowser.Dlna.PlayTo
{
foreach (var container in stateValues.Elements(uPnpNamespaces.svc + "stateVariable"))
{
command.StateVariables.Add(StateVariable.FromXml(container));
command.StateVariables.Add(FromXml(container));
}
}
return command;
}
private static ServiceAction ServiceActionFromXml(XElement container)
{
var argumentList = new List<Argument>();
foreach (var arg in container.Descendants(uPnpNamespaces.svc + "argument"))
{
argumentList.Add(ArgumentFromXml(arg));
}
return new ServiceAction
{
Name = container.GetValue(uPnpNamespaces.svc + "name"),
ArgumentList = argumentList
};
}
private static Argument ArgumentFromXml(XElement container)
{
if (container == null)
{
throw new ArgumentNullException("container");
}
return new Argument
{
Name = container.GetValue(uPnpNamespaces.svc + "name"),
Direction = container.GetValue(uPnpNamespaces.svc + "direction"),
RelatedStateVariable = container.GetValue(uPnpNamespaces.svc + "relatedStateVariable")
};
}
public static StateVariable FromXml(XElement container)
{
var allowedValues = new List<string>();
var element = container.Descendants(uPnpNamespaces.svc + "allowedValueList")
.FirstOrDefault();
if (element != null)
{
var values = element.Descendants(uPnpNamespaces.svc + "allowedValue");
allowedValues.AddRange(values.Select(child => child.Value));
}
return new StateVariable
{
Name = container.GetValue(uPnpNamespaces.svc + "name"),
DataType = container.GetValue(uPnpNamespaces.svc + "dataType"),
AllowedValues = allowedValues
};
}
public string BuildPost(ServiceAction action, string xmlNamespace)
{
var stateString = string.Empty;

View file

@ -0,0 +1,235 @@
using MediaBrowser.Dlna.Common;
using MediaBrowser.Model.Dlna;
using System.Collections.Generic;
using System.Security;
using System.Text;
namespace MediaBrowser.Dlna.Server
{
public class ContentDirectoryXmlBuilder
{
private readonly DeviceProfile _profile;
public ContentDirectoryXmlBuilder(DeviceProfile profile)
{
_profile = profile;
}
public string GetXml()
{
var builder = new StringBuilder();
builder.Append("<?xml version=\"1.0\"?>");
builder.Append("scpd xmlns=\"urn:schemas-upnp-org:service-1-0\"");
builder.Append("<specVersion>");
builder.Append("<major>1</major>");
builder.Append("<minor>0</minor>");
builder.Append("</specVersion>");
AppendActionList(builder);
AppendServiceStateTable(builder);
builder.Append("</scpd>");
return builder.ToString();
}
private void AppendActionList(StringBuilder builder)
{
builder.Append("<actionList>");
foreach (var item in new ServiceActionListBuilder().GetActions())
{
builder.Append("<action>");
builder.Append("<name>" + SecurityElement.Escape(item.Name ?? string.Empty) + "</name>");
builder.Append("<argumentList>");
foreach (var argument in item.ArgumentList)
{
builder.Append("<argument>");
builder.Append("<name>" + SecurityElement.Escape(argument.Name ?? string.Empty) + "</name>");
builder.Append("<direction>" + SecurityElement.Escape(argument.Direction ?? string.Empty) + "</direction>");
builder.Append("<relatedStateVariable>" + SecurityElement.Escape(argument.RelatedStateVariable ?? string.Empty) + "</relatedStateVariable>");
builder.Append("</argument>");
}
builder.Append("</argumentList>");
builder.Append("</action>");
}
builder.Append("</actionList>");
}
private void AppendServiceStateTable(StringBuilder builder)
{
builder.Append("<serviceStateTable>");
foreach (var item in GetStateVariables())
{
var sendEvents = item.SendsEvents ? "yes" : "no";
builder.Append("<stateVariable sendEvents=\"" + sendEvents + "\">");
builder.Append("<name>" + SecurityElement.Escape(item.Name ?? string.Empty) + "</name>");
builder.Append("<dataType>" + SecurityElement.Escape(item.DataType ?? string.Empty) + "</dataType>");
if (item.AllowedValues.Count > 0)
{
builder.Append("<allowedValueList>");
foreach (var allowedValue in item.AllowedValues)
{
builder.Append("<allowedValue>" + SecurityElement.Escape(allowedValue) + "</allowedValue>");
}
builder.Append("</allowedValueList>");
}
builder.Append("</stateVariable>");
}
builder.Append("</serviceStateTable>");
}
private IEnumerable<StateVariable> GetStateVariables()
{
var list = new List<StateVariable>();
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_SortCriteria",
DataType = "string",
SendsEvents = false
});
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_UpdateID",
DataType = "ui4",
SendsEvents = false
});
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_SearchCriteria",
DataType = "string",
SendsEvents = false
});
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_Filter",
DataType = "string",
SendsEvents = false
});
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_Result",
DataType = "string",
SendsEvents = false
});
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_Index",
DataType = "ui4",
SendsEvents = false
});
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_ObjectID",
DataType = "string",
SendsEvents = false
});
list.Add(new StateVariable
{
Name = "SortCapabilities",
DataType = "string",
SendsEvents = false
});
list.Add(new StateVariable
{
Name = "SearchCapabilities",
DataType = "string",
SendsEvents = false
});
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_Count",
DataType = "ui4",
SendsEvents = false
});
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_BrowseFlag",
DataType = "string",
SendsEvents = false,
AllowedValues = new List<string>
{
"BrowseMetadata",
"BrowseDirectChildren"
}
});
list.Add(new StateVariable
{
Name = "SystemUpdateID",
DataType = "ui4",
SendsEvents = true
});
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_BrowseLetter",
DataType = "string",
SendsEvents = false
});
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_CategoryType",
DataType = "ui4",
SendsEvents = false
});
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_RID",
DataType = "ui4",
SendsEvents = false
});
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_PosSec",
DataType = "ui4",
SendsEvents = false
});
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_Featurelist",
DataType = "string",
SendsEvents = false
});
return list;
}
public override string ToString()
{
return GetXml();
}
}
}

View file

@ -0,0 +1,190 @@
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Model.Logging;
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
namespace MediaBrowser.Dlna.Server
{
public class ControlHandler
{
private readonly ILogger _logger;
private const string NS_DC = "http://purl.org/dc/elements/1.1/";
private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/";
private const string NS_SEC = "http://www.sec.co.kr/";
private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/";
private const int systemID = 0;
public ControlHandler(ILogger logger)
{
_logger = logger;
}
public ControlResponse ProcessControlRequest(ControlRequest request)
{
var soap = new XmlDocument();
soap.LoadXml(request.InputXml);
var sparams = new Headers();
var body = soap.GetElementsByTagName("Body", NS_SOAPENV).Item(0);
var method = body.FirstChild;
foreach (var p in method.ChildNodes)
{
var e = p as XmlElement;
if (e == null)
{
continue;
}
sparams.Add(e.LocalName, e.InnerText.Trim());
}
var env = new XmlDocument();
env.AppendChild(env.CreateXmlDeclaration("1.0", "utf-8", "yes"));
var envelope = env.CreateElement("SOAP-ENV", "Envelope", NS_SOAPENV);
env.AppendChild(envelope);
envelope.SetAttribute("encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/");
var rbody = env.CreateElement("SOAP-ENV:Body", NS_SOAPENV);
env.DocumentElement.AppendChild(rbody);
IEnumerable<KeyValuePair<string, string>> result;
switch (method.LocalName)
{
case "GetSearchCapabilities":
result = HandleGetSearchCapabilities();
break;
case "GetSortCapabilities":
result = HandleGetSortCapabilities();
break;
case "GetSystemUpdateID":
result = HandleGetSystemUpdateID();
break;
case "Browse":
result = HandleBrowse(sparams);
break;
case "X_GetFeatureList":
result = HandleXGetFeatureList();
break;
case "X_SetBookmark":
result = HandleXSetBookmark(sparams);
break;
default:
throw new ResourceNotFoundException();
}
var response = env.CreateElement(String.Format("u:{0}Response", method.LocalName), method.NamespaceURI);
rbody.AppendChild(response);
foreach (var i in result)
{
var ri = env.CreateElement(i.Key);
ri.InnerText = i.Value;
response.AppendChild(ri);
}
var controlResponse = new ControlResponse
{
Xml = env.OuterXml
};
controlResponse.Headers.Add("EXT", string.Empty);
return controlResponse;
}
private Headers HandleXSetBookmark(Headers sparams)
{
var id = sparams["ObjectID"];
//var item = GetItem(id) as IBookmarkable;
//if (item != null)
//{
// var newbookmark = long.Parse(sparams["PosSecond"]);
// if (newbookmark > 30)
// {
// newbookmark -= 5;
// }
// if (newbookmark > 30 || !item.Bookmark.HasValue || item.Bookmark.Value < 60)
// {
// item.Bookmark = newbookmark;
// }
//}
return new Headers();
}
private Headers HandleGetSearchCapabilities()
{
return new Headers { { "SearchCaps", string.Empty } };
}
private Headers HandleGetSortCapabilities()
{
return new Headers { { "SortCaps", string.Empty } };
}
private Headers HandleGetSystemUpdateID()
{
return new Headers { { "Id", systemID.ToString() } };
}
private Headers HandleXGetFeatureList()
{
return new Headers { { "FeatureList", GetFeatureListXml() } };
}
private string GetFeatureListXml()
{
var builder = new StringBuilder();
builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
builder.Append("<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">");
builder.Append("<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">");
builder.Append("<container id=\"I\" type=\"object.item.imageItem\"/>");
builder.Append("<container id=\"A\" type=\"object.item.audioItem\"/>");
builder.Append("<container id=\"V\" type=\"object.item.videoItem\"/>");
builder.Append("</Feature>");
builder.Append("</Features>");
return builder.ToString();
}
private IEnumerable<KeyValuePair<string, string>> HandleBrowse(Headers sparams)
{
var id = sparams["ObjectID"];
var flag = sparams["BrowseFlag"];
int requested;
var provided = 0;
int start;
if (sparams.ContainsKey("RequestedCount") && int.TryParse(sparams["RequestedCount"], out requested) && requested <= 0)
{
requested = 20;
}
if (sparams.ContainsKey("StartingIndex") && int.TryParse(sparams["StartingIndex"], out start) && start <= 0)
{
start = 0;
}
//var root = GetItem(id) as IMediaFolder;
var result = new XmlDocument();
var didl = result.CreateElement(string.Empty, "DIDL-Lite", NS_DIDL);
didl.SetAttribute("xmlns:dc", NS_DC);
didl.SetAttribute("xmlns:dlna", NS_DLNA);
didl.SetAttribute("xmlns:upnp", NS_UPNP);
didl.SetAttribute("xmlns:sec", NS_SEC);
result.AppendChild(didl);
return null;
}
}
}

View file

@ -0,0 +1,65 @@
using MediaBrowser.Model.Logging;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace MediaBrowser.Dlna.Server
{
public class Datagram
{
public IPEndPoint EndPoint { get; private set; }
public IPAddress LocalAddress { get; private set; }
public string Message { get; private set; }
public bool Sticky { get; private set; }
public int SendCount { get; private set; }
private readonly ILogger _logger;
public Datagram(IPEndPoint endPoint, IPAddress localAddress, ILogger logger, string message, bool sticky)
{
Message = message;
_logger = logger;
Sticky = sticky;
LocalAddress = localAddress;
EndPoint = endPoint;
}
public void Send()
{
var msg = Encoding.ASCII.GetBytes(Message);
try
{
var client = new UdpClient();
client.Client.Bind(new IPEndPoint(LocalAddress, 0));
client.BeginSend(msg, msg.Length, EndPoint, result =>
{
try
{
client.EndSend(result);
}
catch (Exception ex)
{
_logger.ErrorException("Error sending Datagram", ex);
}
finally
{
try
{
client.Close();
}
catch (Exception)
{
}
}
}, null);
}
catch (Exception ex)
{
_logger.ErrorException("Error sending Datagram", ex);
}
++SendCount;
}
}
}

View file

@ -0,0 +1,190 @@
using MediaBrowser.Dlna.Common;
using MediaBrowser.Model.Dlna;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Security;
using System.Text;
namespace MediaBrowser.Dlna.Server
{
public class DescriptionXmlBuilder
{
private readonly DeviceProfile _profile;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly string _serverUdn;
public DescriptionXmlBuilder(DeviceProfile profile, string serverUdn)
{
if (string.IsNullOrWhiteSpace(serverUdn))
{
throw new ArgumentNullException("serverUdn");
}
_profile = profile;
_serverUdn = serverUdn;
}
public string GetXml()
{
var builder = new StringBuilder();
builder.Append("<?xml version=\"1.0\"?>");
builder.Append("<root xmlns=\"urn:schemas-upnp-org:device-1-0\" xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\" xmlns:sec=\"http://www.sec.co.kr/dlna\">");
builder.Append("<specVersion>");
builder.Append("<major>1</major>");
builder.Append("<minor>0</minor>");
builder.Append("</specVersion>");
AppendDeviceInfo(builder);
builder.Append("</root>");
return builder.ToString();
}
private void AppendDeviceInfo(StringBuilder builder)
{
builder.Append("<device>");
AppendDeviceProperties(builder);
AppendIconList(builder);
AppendServiceList(builder);
builder.Append("</device>");
}
private void AppendDeviceProperties(StringBuilder builder)
{
builder.Append("<UDN>" + SecurityElement.Escape(_serverUdn) + "</UDN>");
builder.Append("<dlna:X_DLNACAP>" + SecurityElement.Escape(_profile.XDlnaCap ?? string.Empty) + "</dlna:X_DLNACAP>");
if (!string.IsNullOrWhiteSpace(_profile.XDlnaDoc))
{
builder.Append("<dlna:X_DLNADOC xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" +
SecurityElement.Escape(_profile.XDlnaDoc) + "</dlna:X_DLNADOC>");
}
else
{
builder.Append("<dlna:X_DLNADOC xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">DMS-1.50</dlna:X_DLNADOC>");
}
builder.Append("<friendlyName>" + SecurityElement.Escape(_profile.FriendlyName ?? string.Empty) + "</friendlyName>");
builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>");
builder.Append("<manufacturer>" + SecurityElement.Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>");
builder.Append("<manufacturerURL>" + SecurityElement.Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>");
builder.Append("<modelName>" + SecurityElement.Escape(_profile.ModelName ?? string.Empty) + "</modelName>");
builder.Append("<modelDescription>" + SecurityElement.Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>");
builder.Append("<modelNumber>" + SecurityElement.Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>");
builder.Append("<modelURL>" + SecurityElement.Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>");
builder.Append("<serialNumber>" + SecurityElement.Escape(_profile.SerialNumber ?? string.Empty) + "</serialNumber>");
builder.Append("<sec:ProductCap>DCM10,getMediaInfo.sec</sec:ProductCap>");
builder.Append("<sec:X_ProductCap>DCM10,getMediaInfo.sec</sec:X_ProductCap>");
}
private void AppendIconList(StringBuilder builder)
{
builder.Append("<iconList>");
foreach (var icon in GetIcons())
{
builder.Append("<icon>");
builder.Append("<mimetype>" + SecurityElement.Escape(icon.MimeType ?? string.Empty) + "</mimetype>");
builder.Append("<width>" + SecurityElement.Escape(icon.Width.ToString(_usCulture)) + "</width>");
builder.Append("<height>" + SecurityElement.Escape(icon.Height.ToString(_usCulture)) + "</height>");
builder.Append("<depth>" + SecurityElement.Escape(icon.Depth ?? string.Empty) + "</depth>");
builder.Append("<url>" + SecurityElement.Escape(icon.Url ?? string.Empty) + "</url>");
builder.Append("</icon>");
}
builder.Append("</iconList>");
}
private void AppendServiceList(StringBuilder builder)
{
builder.Append("<serviceList>");
foreach (var service in GetServices())
{
builder.Append("<service>");
builder.Append("<serviceType>" + SecurityElement.Escape(service.ServiceType ?? string.Empty) + "</serviceType>");
builder.Append("<serviceId>" + SecurityElement.Escape(service.ServiceId ?? string.Empty) + "</serviceId>");
builder.Append("<SCPDURL>" + SecurityElement.Escape(service.ScpdUrl ?? string.Empty) + "</SCPDURL>");
builder.Append("<controlURL>" + SecurityElement.Escape(service.ControlUrl ?? string.Empty) + "</controlURL>");
builder.Append("<eventSubURL>" + SecurityElement.Escape(service.EventSubUrl ?? string.Empty) + "</eventSubURL>");
builder.Append("</service>");
}
builder.Append("</serviceList>");
}
private IEnumerable<DeviceIcon> GetIcons()
{
var list = new List<DeviceIcon>();
list.Add(new DeviceIcon
{
MimeType = "image/jpeg",
Depth = "24",
Width = 48,
Height = 48,
Url = "/mediabrowser/dlna/icons/small.jpg"
});
list.Add(new DeviceIcon
{
MimeType = "image/jpeg",
Depth = "24",
Width = 120,
Height = 120,
Url = "/mediabrowser/dlna/icons/large.jpg"
});
list.Add(new DeviceIcon
{
MimeType = "image/png",
Depth = "24",
Width = 48,
Height = 48,
Url = "/mediabrowser/dlna/icons/small.png"
});
list.Add(new DeviceIcon
{
MimeType = "image/png",
Depth = "24",
Width = 120,
Height = 120,
Url = "/mediabrowser/dlna/icons/large.png"
});
return list;
}
private IEnumerable<DeviceService> GetServices()
{
var list = new List<DeviceService>();
list.Add(new DeviceService
{
ServiceType = "urn:schemas-upnp-org:service:ContentDirectory:1",
ServiceId = "urn:upnp-org:serviceId:ContentDirectory",
ScpdUrl = "/mediabrowser/dlna/contentdirectory.xml",
ControlUrl = "/servicecontrol"
});
return list;
}
public override string ToString()
{
return GetXml();
}
}
}

View file

@ -1,8 +1,11 @@
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Logging;
using System;
using System.Net;
namespace MediaBrowser.Dlna.Server
{
@ -13,11 +16,13 @@ namespace MediaBrowser.Dlna.Server
private SsdpHandler _ssdpHandler;
private readonly IApplicationHost _appHost;
private readonly INetworkManager _network;
public DlnaServerEntryPoint(IServerConfigurationManager config, ILogManager logManager, IApplicationHost appHost)
public DlnaServerEntryPoint(IServerConfigurationManager config, ILogManager logManager, IApplicationHost appHost, INetworkManager network)
{
_config = config;
_appHost = appHost;
_network = network;
_logger = logManager.GetLogger("DlnaServer");
}
@ -25,12 +30,12 @@ namespace MediaBrowser.Dlna.Server
{
_config.ConfigurationUpdated += ConfigurationUpdated;
//ReloadServer();
ReloadServer();
}
void ConfigurationUpdated(object sender, EventArgs e)
{
//ReloadServer();
ReloadServer();
}
private void ReloadServer()
@ -57,6 +62,8 @@ namespace MediaBrowser.Dlna.Server
try
{
_ssdpHandler = new SsdpHandler(_logger, _config, signature);
RegisterEndpoints();
}
catch (Exception ex)
{
@ -65,6 +72,20 @@ namespace MediaBrowser.Dlna.Server
}
}
private void RegisterEndpoints()
{
foreach (var address in _network.GetLocalIpAddresses())
{
var guid = address.GetMD5();
var descriptorURI = "/mediabrowser/dlna/" + guid.ToString("N") + "/description.xml";
var uri = new Uri(string.Format("http://{0}:{1}{2}", address, _config.Configuration.HttpServerPortNumber, descriptorURI));
_ssdpHandler.RegisterNotification(guid, uri, IPAddress.Parse(address));
}
}
private void DisposeServer()
{
lock (_syncLock)

View file

@ -0,0 +1,209 @@
using MediaBrowser.Dlna.Common;
using System.Collections.Generic;
namespace MediaBrowser.Dlna.Server
{
public class ServiceActionListBuilder
{
public IEnumerable<ServiceAction> GetActions()
{
var list = new List<ServiceAction>
{
GetGetSystemUpdateIDAction(),
GetSearchCapabilitiesAction(),
GetSortCapabilitiesAction(),
GetBrowseAction(),
GetX_GetFeatureListAction(),
GetXSetBookmarkAction()
};
return list;
}
private ServiceAction GetGetSystemUpdateIDAction()
{
var action = new ServiceAction
{
Name = "GetSystemUpdateID"
};
action.ArgumentList.Add(new Argument
{
Name = "Id",
Direction = "out",
RelatedStateVariable = "SystemUpdateID"
});
return action;
}
private ServiceAction GetSearchCapabilitiesAction()
{
var action = new ServiceAction
{
Name = "GetSearchCapabilities"
};
action.ArgumentList.Add(new Argument
{
Name = "SearchCaps",
Direction = "out",
RelatedStateVariable = "SearchCapabilities"
});
return action;
}
private ServiceAction GetSortCapabilitiesAction()
{
var action = new ServiceAction
{
Name = "GetSortCapabilities"
};
action.ArgumentList.Add(new Argument
{
Name = "SortCaps",
Direction = "out",
RelatedStateVariable = "SortCapabilities"
});
return action;
}
private ServiceAction GetX_GetFeatureListAction()
{
var action = new ServiceAction
{
Name = "X_GetFeatureList"
};
action.ArgumentList.Add(new Argument
{
Name = "FeatureList",
Direction = "out",
RelatedStateVariable = "A_ARG_TYPE_Featurelist"
});
return action;
}
private ServiceAction GetBrowseAction()
{
var action = new ServiceAction
{
Name = "Browse"
};
action.ArgumentList.Add(new Argument
{
Name = "ObjectID",
Direction = "in",
RelatedStateVariable = "A_ARG_TYPE_ObjectID"
});
action.ArgumentList.Add(new Argument
{
Name = "BrowseFlag",
Direction = "in",
RelatedStateVariable = "A_ARG_TYPE_BrowseFlag"
});
action.ArgumentList.Add(new Argument
{
Name = "Filter",
Direction = "in",
RelatedStateVariable = "A_ARG_TYPE_Filter"
});
action.ArgumentList.Add(new Argument
{
Name = "StartingIndex",
Direction = "in",
RelatedStateVariable = "A_ARG_TYPE_Index"
});
action.ArgumentList.Add(new Argument
{
Name = "RequestedCount",
Direction = "in",
RelatedStateVariable = "A_ARG_TYPE_Count"
});
action.ArgumentList.Add(new Argument
{
Name = "SortCriteria",
Direction = "in",
RelatedStateVariable = "A_ARG_TYPE_SortCriteria"
});
action.ArgumentList.Add(new Argument
{
Name = "Result",
Direction = "out",
RelatedStateVariable = "A_ARG_TYPE_Result"
});
action.ArgumentList.Add(new Argument
{
Name = "NumberReturned",
Direction = "out",
RelatedStateVariable = "A_ARG_TYPE_Count"
});
action.ArgumentList.Add(new Argument
{
Name = "TotalMatches",
Direction = "out",
RelatedStateVariable = "A_ARG_TYPE_Count"
});
action.ArgumentList.Add(new Argument
{
Name = "UpdateID",
Direction = "out",
RelatedStateVariable = "A_ARG_TYPE_UpdateID"
});
return action;
}
private ServiceAction GetXSetBookmarkAction()
{
var action = new ServiceAction
{
Name = "X_SetBookmark"
};
action.ArgumentList.Add(new Argument
{
Name = "CategoryType",
Direction = "in",
RelatedStateVariable = "A_ARG_TYPE_CategoryType"
});
action.ArgumentList.Add(new Argument
{
Name = "RID",
Direction = "in",
RelatedStateVariable = "A_ARG_TYPE_RID"
});
action.ArgumentList.Add(new Argument
{
Name = "ObjectID",
Direction = "in",
RelatedStateVariable = "A_ARG_TYPE_ObjectID"
});
action.ArgumentList.Add(new Argument
{
Name = "PosSecond",
Direction = "in",
RelatedStateVariable = "A_ARG_TYPE_PosSec"
});
return action;
}
}
}

View file

@ -1,21 +1,26 @@
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace MediaBrowser.Dlna.Server
{
public class SsdpHandler : IDisposable
{
private readonly AutoResetEvent _datagramPosted = new AutoResetEvent(false);
private readonly ConcurrentQueue<Datagram> _messageQueue = new ConcurrentQueue<Datagram>();
private readonly ILogger _logger;
private readonly IServerConfigurationManager _config;
private readonly string _serverSignature;
private bool _isDisposed = false;
private bool _isDisposed;
const string SSDPAddr = "239.255.255.250";
const int SSDPPort = 1900;
@ -27,6 +32,9 @@ namespace MediaBrowser.Dlna.Server
private readonly Dictionary<Guid, List<UpnpDevice>> _devices = new Dictionary<Guid, List<UpnpDevice>>();
private Timer _queueTimer;
private Timer _notificationTimer;
public SsdpHandler(ILogger logger, IServerConfigurationManager config, string serverSignature)
{
_logger = logger;
@ -59,6 +67,8 @@ namespace MediaBrowser.Dlna.Server
_udpClient.JoinMulticastGroup(_ssdpIp, 2);
_logger.Info("SSDP service started");
Receive();
StartNotificationTimer();
}
private void Receive()
@ -137,7 +147,7 @@ namespace MediaBrowser.Dlna.Server
foreach (var d in Devices)
{
if (!string.IsNullOrEmpty(req) && req != d.Type)
if (!string.IsNullOrEmpty(req) && !string.Equals(req, d.Type, StringComparison.OrdinalIgnoreCase))
{
continue;
}
@ -157,28 +167,67 @@ namespace MediaBrowser.Dlna.Server
headers.Add("ST", dev.Type);
headers.Add("USN", dev.USN);
SendDatagram(endpoint, String.Format("HTTP/1.1 200 OK\r\n{0}\r\n", headers.HeaderBlock), false);
var msg = String.Format("HTTP/1.1 200 OK\r\n{0}\r\n", headers.HeaderBlock);
SendDatagram(endpoint, dev.Address, msg, false);
_logger.Info("{1} - Responded to a {0} request", dev.Type, endpoint);
}
private void SendDatagram(IPEndPoint endpoint, string msg, bool sticky)
private void SendDatagram(IPEndPoint endpoint, IPAddress localAddress, string msg, bool sticky)
{
if (_isDisposed)
{
return;
}
//var dgram = new Datagram(endpoint, msg, sticky);
//if (messageQueue.Count == 0)
//{
// dgram.Send();
//}
//messageQueue.Enqueue(dgram);
//queueTimer.Enabled = true;
var dgram = new Datagram(endpoint, localAddress, _logger, msg, sticky);
if (_messageQueue.Count == 0)
{
dgram.Send();
}
_messageQueue.Enqueue(dgram);
StartQueueTimer();
}
private void QueueTimerCallback(object state)
{
while (_messageQueue.Count != 0)
{
Datagram msg;
if (!_messageQueue.TryPeek(out msg))
{
continue;
}
if (msg != null && (!_isDisposed || msg.Sticky))
{
msg.Send();
if (msg.SendCount > 2)
{
_messageQueue.TryDequeue(out msg);
}
break;
}
_messageQueue.TryDequeue(out msg);
}
_datagramPosted.Set();
if (_messageQueue.Count > 0)
{
StartQueueTimer();
}
else
{
DisposeQueueTimer();
}
}
private void NotifyAll()
{
_logger.Debug("NotifyAll");
_logger.Debug("Sending alive notifications");
foreach (var d in Devices)
{
NotifyDevice(d, "alive", false);
@ -197,64 +246,128 @@ namespace MediaBrowser.Dlna.Server
headers.Add("NT", dev.Type);
headers.Add("USN", dev.USN);
SendDatagram(_ssdpEndp, String.Format("NOTIFY * HTTP/1.1\r\n{0}\r\n", headers.HeaderBlock), sticky);
var msg = String.Format("NOTIFY * HTTP/1.1\r\n{0}\r\n", headers.HeaderBlock);
_logger.Debug("{0} said {1}", dev.USN, type);
SendDatagram(_ssdpEndp, dev.Address, msg, sticky);
}
private void RegisterNotification(Guid UUID, Uri Descriptor)
public void RegisterNotification(Guid uuid, Uri descriptor, IPAddress address)
{
List<UpnpDevice> list;
lock (_devices)
{
if (!_devices.TryGetValue(UUID, out list))
if (!_devices.TryGetValue(uuid, out list))
{
_devices.Add(UUID, list = new List<UpnpDevice>());
_devices.Add(uuid, list = new List<UpnpDevice>());
}
}
foreach (var t in new[] { "upnp:rootdevice", "urn:schemas-upnp-org:device:MediaServer:1", "urn:schemas-upnp-org:service:ContentDirectory:1", "uuid:" + UUID })
foreach (var t in new[]
{
list.Add(new UpnpDevice(UUID, t, Descriptor));
"upnp:rootdevice",
"urn:schemas-upnp-org:device:MediaServer:1",
"urn:schemas-upnp-org:service:ContentDirectory:1",
"uuid:" + uuid
})
{
list.Add(new UpnpDevice(uuid, t, descriptor, address));
}
NotifyAll();
_logger.Debug("Registered mount {0}", UUID);
_logger.Debug("Registered mount {0} at {1}", uuid, descriptor);
}
internal void UnregisterNotification(Guid UUID)
private void UnregisterNotification(Guid uuid)
{
List<UpnpDevice> dl;
lock (_devices)
{
if (!_devices.TryGetValue(UUID, out dl))
if (!_devices.TryGetValue(uuid, out dl))
{
return;
}
_devices.Remove(UUID);
_devices.Remove(uuid);
}
foreach (var d in dl)
{
NotifyDevice(d, "byebye", true);
}
_logger.Debug("Unregistered mount {0}", UUID);
_logger.Debug("Unregistered mount {0}", uuid);
}
public void Dispose()
{
_isDisposed = true;
//while (messageQueue.Count != 0)
//{
// datagramPosted.WaitOne();
//}
while (_messageQueue.Count != 0)
{
_datagramPosted.WaitOne();
}
_udpClient.DropMulticastGroup(_ssdpIp);
_udpClient.Close();
//notificationTimer.Enabled = false;
//queueTimer.Enabled = false;
//notificationTimer.Dispose();
//queueTimer.Dispose();
//datagramPosted.Dispose();
DisposeNotificationTimer();
DisposeQueueTimer();
_datagramPosted.Dispose();
}
private readonly object _queueTimerSyncLock = new object();
private void StartQueueTimer()
{
lock (_queueTimerSyncLock)
{
if (_queueTimer == null)
{
_queueTimer = new Timer(QueueTimerCallback, null, 1000, Timeout.Infinite);
}
else
{
_queueTimer.Change(1000, Timeout.Infinite);
}
}
}
private void DisposeQueueTimer()
{
lock (_queueTimerSyncLock)
{
if (_queueTimer != null)
{
_queueTimer.Dispose();
_queueTimer = null;
}
}
}
private readonly object _notificationTimerSyncLock = new object();
private void StartNotificationTimer()
{
const int intervalMs = 60000;
lock (_notificationTimerSyncLock)
{
if (_notificationTimer == null)
{
_notificationTimer = new Timer(state => NotifyAll(), null, intervalMs, intervalMs);
}
else
{
_notificationTimer.Change(intervalMs, intervalMs);
}
}
}
private void DisposeNotificationTimer()
{
lock (_notificationTimerSyncLock)
{
if (_notificationTimer != null)
{
_notificationTimer.Dispose();
_notificationTimer = null;
}
}
}
}
}

View file

@ -1,4 +1,5 @@
using System;
using System.Net;
namespace MediaBrowser.Dlna.Server
{
@ -8,13 +9,16 @@ namespace MediaBrowser.Dlna.Server
public readonly string Type;
public readonly string USN;
public readonly Guid Uuid;
public readonly IPAddress Address;
public UpnpDevice(Guid aUuid, string aType, Uri aDescriptor)
public UpnpDevice(Guid aUuid, string aType, Uri aDescriptor, IPAddress address)
{
Uuid = aUuid;
Type = aType;
Descriptor = aDescriptor;
Address = address;
if (Type.StartsWith("uuid:"))
{
USN = Type;

View file

@ -31,6 +31,11 @@
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release Mono|AnyCPU' ">
<Optimize>false</Optimize>
<OutputPath>bin\Release Mono</OutputPath>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="BDInfo">
<HintPath>..\packages\MediaBrowser.BdInfo.1.0.0.10\lib\net35\BDInfo.dll</HintPath>
@ -75,7 +80,6 @@
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">

View file

@ -62,11 +62,10 @@ namespace MediaBrowser.Model.ApiClient
/// <summary>
/// Reports the capabilities.
/// </summary>
/// <param name="sessionId">The session identifier.</param>
/// <param name="capabilities">The capabilities.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task ReportCapabilities(string sessionId, ClientCapabilities capabilities, CancellationToken cancellationToken);
Task ReportCapabilities(ClientCapabilities capabilities, CancellationToken cancellationToken);
/// <summary>
/// Gets the index of the game players.
@ -771,6 +770,13 @@ namespace MediaBrowser.Model.ApiClient
/// <returns>System.String.</returns>
string GetImageUrl(ProgramInfoDto item, ImageOptions options);
/// <summary>
/// Gets the subtitle URL.
/// </summary>
/// <param name="options">The options.</param>
/// <returns>System.String.</returns>
string GetSubtitleUrl(SubtitleOptions options);
/// <summary>
/// Gets an image url that can be used to download an image from the api
/// </summary>

View file

@ -34,6 +34,7 @@ namespace MediaBrowser.Model.Dlna
public string ModelDescription { get; set; }
public string ModelNumber { get; set; }
public string ModelUrl { get; set; }
public string SerialNumber { get; set; }
public bool IgnoreTranscodeByteRangeRequests { get; set; }
public bool EnableAlbumArtInDidl { get; set; }

View file

@ -158,4 +158,19 @@
/// <value>The device id.</value>
public string DeviceId { get; set; }
}
public class SubtitleOptions
{
/// <summary>
/// Gets or sets the item identifier.
/// </summary>
/// <value>The item identifier.</value>
public string ItemId { get; set; }
/// <summary>
/// Gets or sets the index of the stream.
/// </summary>
/// <value>The index of the stream.</value>
public int StreamIndex { get; set; }
}
}

View file

@ -1,7 +1,7 @@
using System.ComponentModel;
using System.Diagnostics;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Configuration;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.Serialization;
namespace MediaBrowser.Model.Dto

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.Serialization;
@ -46,6 +47,12 @@ namespace MediaBrowser.Model.Entities
/// <value>The primary image tag.</value>
public Guid? PrimaryImageTag { get; set; }
/// <summary>
/// Gets or sets the primary image item identifier.
/// </summary>
/// <value>The primary image item identifier.</value>
public string PrimaryImageItemId { get; set; }
/// <summary>
/// Gets or sets the thumb image tag.
/// </summary>
@ -75,6 +82,54 @@ namespace MediaBrowser.Model.Entities
/// </summary>
/// <value>The media version identifier.</value>
public string MediaSourceId { get; set; }
/// <summary>
/// Gets or sets the premiere date.
/// </summary>
/// <value>The premiere date.</value>
public DateTime? PremiereDate { get; set; }
/// <summary>
/// Gets or sets the production year.
/// </summary>
/// <value>The production year.</value>
public int? ProductionYear { get; set; }
/// <summary>
/// Gets or sets the index number.
/// </summary>
/// <value>The index number.</value>
public int? IndexNumber { get; set; }
/// <summary>
/// Gets or sets the index number end.
/// </summary>
/// <value>The index number end.</value>
public int? IndexNumberEnd { get; set; }
/// <summary>
/// Gets or sets the parent index number.
/// </summary>
/// <value>The parent index number.</value>
public int? ParentIndexNumber { get; set; }
/// <summary>
/// Gets or sets the name of the series.
/// </summary>
/// <value>The name of the series.</value>
public string SeriesName { get; set; }
/// <summary>
/// Gets or sets the album.
/// </summary>
/// <value>The album.</value>
public string Album { get; set; }
/// <summary>
/// Gets or sets the artists.
/// </summary>
/// <value>The artists.</value>
public List<string> Artists { get; set; }
/// <summary>
/// Gets a value indicating whether this instance has primary image.
@ -85,5 +140,10 @@ namespace MediaBrowser.Model.Entities
{
get { return PrimaryImageTag.HasValue; }
}
public BaseItemInfo()
{
Artists = new List<string>();
}
}
}

View file

@ -46,6 +46,8 @@ namespace MediaBrowser.Model.Session
ToggleMute = 21,
SetVolume = 22,
SetAudioStreamIndex = 23,
SetSubtitleStreamIndex = 24
SetSubtitleStreamIndex = 24,
ToggleFullscreen = 25,
DisplayContent = 26
}
}

View file

@ -31,10 +31,6 @@ namespace MediaBrowser.Model.Session
/// </summary>
Seek,
/// <summary>
/// The fullscreen
/// </summary>
Fullscreen,
/// <summary>
/// The rewind
/// </summary>
Rewind,

View file

@ -51,6 +51,12 @@ namespace MediaBrowser.Model.Session
/// <value>The user id.</value>
public string UserId { get; set; }
/// <summary>
/// Gets or sets the user primary image tag.
/// </summary>
/// <value>The user primary image tag.</value>
public Guid? UserPrimaryImageTag { get; set; }
/// <summary>
/// Gets or sets the name of the user.
/// </summary>
@ -164,12 +170,6 @@ namespace MediaBrowser.Model.Session
/// </summary>
/// <value><c>true</c> if [supports remote control]; otherwise, <c>false</c>.</value>
public bool SupportsRemoteControl { get; set; }
/// <summary>
/// Gets or sets a value indicating whether [supports navigation commands].
/// </summary>
/// <value><c>true</c> if [supports navigation commands]; otherwise, <c>false</c>.</value>
public bool SupportsNavigationControl { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
@ -203,10 +203,12 @@ namespace MediaBrowser.Model.Session
public class ClientCapabilities
{
public List<string> PlayableMediaTypes { get; set; }
public List<string> SupportedCommands { get; set; }
public ClientCapabilities()
{
PlayableMediaTypes = new List<string>();
SupportedCommands = new List<string>();
}
}
}

View file

@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Server.Mono",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.Dlna", "MediaBrowser.Dlna\MediaBrowser.Dlna.csproj", "{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MediaBrowser.MediaEncoding", "MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj", "{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x86 = Debug|x86
@ -29,6 +31,14 @@ Global
Release Mono|Any CPU = Release Mono|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Debug|x86.ActiveCfg = Debug|Any CPU
{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Debug|x86.Build.0 = Debug|Any CPU
{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Release Mono|Any CPU.ActiveCfg = Release|Any CPU
{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Release Mono|Any CPU.Build.0 = Release|Any CPU
{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Release|Any CPU.Build.0 = Release|Any CPU
{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Release|x86.ActiveCfg = Release|Any CPU
{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}.Release|x86.Build.0 = Release|Any CPU
{175A9388-F352-4586-A6B4-070DED62B644}.Debug|x86.ActiveCfg = Debug|x86
{175A9388-F352-4586-A6B4-070DED62B644}.Debug|x86.Build.0 = Debug|x86
{175A9388-F352-4586-A6B4-070DED62B644}.Release Mono|Any CPU.ActiveCfg = Release Mono|Any CPU
@ -77,6 +87,14 @@ Global
{5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|Any CPU.Build.0 = Release|Any CPU
{5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|x86.ActiveCfg = Release|Any CPU
{5624B7B5-B5A7-41D8-9F10-CC5611109619}.Release|x86.Build.0 = Release|Any CPU
{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Debug|x86.ActiveCfg = Debug|Any CPU
{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Debug|x86.Build.0 = Debug|Any CPU
{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release Mono|Any CPU.ActiveCfg = Release Mono|Any CPU
{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release Mono|Any CPU.Build.0 = Release Mono|Any CPU
{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release|Any CPU.Build.0 = Release|Any CPU
{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release|x86.ActiveCfg = Release|Any CPU
{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release|x86.Build.0 = Release|Any CPU
{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|x86.ActiveCfg = Debug|Any CPU
{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Debug|x86.Build.0 = Debug|Any CPU
{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}.Release Mono|Any CPU.ActiveCfg = Release Mono|Any CPU
@ -101,14 +119,6 @@ Global
{C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Release|Any CPU.Build.0 = Release|Any CPU
{C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Release|x86.ActiveCfg = Release|Any CPU
{C4D2573A-3FD3-441F-81AF-174AC4CD4E1D}.Release|x86.Build.0 = Release|Any CPU
{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Debug|x86.ActiveCfg = Debug|Any CPU
{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Debug|x86.Build.0 = Debug|Any CPU
{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release Mono|Any CPU.ActiveCfg = Release Mono|Any CPU
{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release Mono|Any CPU.Build.0 = Release Mono|Any CPU
{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release|Any CPU.Build.0 = Release|Any CPU
{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release|x86.ActiveCfg = Release|Any CPU
{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(MonoDevelopProperties) = preSolution
StartupItem = MediaBrowser.Server.Mono\MediaBrowser.Server.Mono.csproj

View file

@ -5,7 +5,7 @@
<File FileName="MediaBrowser.Server.Mono\app.config" Line="1" Column="1" />
<File FileName="MediaBrowser.ServerApplication\ApplicationHost.cs" Line="1" Column="1" />
<File FileName="MediaBrowser.Server.Mono\Native\NativeApp.cs" Line="1" Column="1" />
<File FileName="MediaBrowser.Server.Mono\Program.cs" Line="36" Column="34" />
<File FileName="MediaBrowser.Server.Mono\Program.cs" Line="36" Column="37" />
</Files>
</MonoDevelop.Ide.Workbench>
<MonoDevelop.Ide.DebuggingService.Breakpoints>

View file

@ -71,10 +71,18 @@ namespace MediaBrowser.Providers.Manager
var saveLocally = item.SupportsLocalMetadata && item.IsSaveLocalMetadataEnabled() && !item.IsOwnedItem && !(item is Audio);
if (item is IItemByName || item is User)
if (item is User)
{
saveLocally = true;
}
if (item is IItemByName)
{
var hasDualAccess = item as IHasDualAccess;
if (hasDualAccess == null || hasDualAccess.IsAccessedByName)
{
saveLocally = true;
}
}
if (type != ImageType.Primary && item is Episode)
{

View file

@ -139,8 +139,6 @@ namespace MediaBrowser.Providers.Movies
/// </summary>
private TmdbSettingsResult _tmdbSettings;
private readonly SemaphoreSlim _tmdbSettingsSemaphore = new SemaphoreSlim(1, 1);
/// <summary>
/// Gets the TMDB settings.
/// </summary>
@ -152,32 +150,17 @@ namespace MediaBrowser.Providers.Movies
return _tmdbSettings;
}
await _tmdbSettingsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
using (var json = await GetMovieDbResponse(new HttpRequestOptions
{
// Check again in case it got populated while we were waiting.
if (_tmdbSettings != null)
{
return _tmdbSettings;
}
Url = string.Format(TmdbConfigUrl, ApiKey),
CancellationToken = cancellationToken,
AcceptHeader = AcceptHeader
using (var json = await GetMovieDbResponse(new HttpRequestOptions
{
Url = string.Format(TmdbConfigUrl, ApiKey),
CancellationToken = cancellationToken,
AcceptHeader = AcceptHeader
}).ConfigureAwait(false))
{
_tmdbSettings = _jsonSerializer.DeserializeFromStream<TmdbSettingsResult>(json);
return _tmdbSettings;
}
}
finally
}).ConfigureAwait(false))
{
_tmdbSettingsSemaphore.Release();
_tmdbSettings = _jsonSerializer.DeserializeFromStream<TmdbSettingsResult>(json);
return _tmdbSettings;
}
}

View file

@ -9,11 +9,8 @@ using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Serialization;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -64,7 +61,7 @@ namespace MediaBrowser.Providers.Music
return result;
}
protected virtual async Task FetchLastfmData(MusicArtist item, string musicBrainzId, CancellationToken cancellationToken)
protected async Task FetchLastfmData(MusicArtist item, string musicBrainzId, CancellationToken cancellationToken)
{
// Get artist info with provided id
var url = RootUrl + String.Format("method=artist.getInfo&mbid={0}&api_key={1}&format=json", UrlEncode(musicBrainzId), ApiKey);

View file

@ -78,7 +78,7 @@ namespace MediaBrowser.Providers.TV
try
{
var seriesOffset = TvdbSeriesProvider.GetSeriesOffset(series.ProviderIds);
if (seriesOffset != null)
if (seriesOffset != null && seriesOffset.Value != 0)
return TvdbSeasonImageProvider.GetImages(path, language, seriesOffset.Value + 1, cancellationToken);
return GetImages(path, language, cancellationToken);

View file

@ -360,7 +360,7 @@ namespace MediaBrowser.Server.Implementations.Dto
{
return _imageProcessor.GetImageCacheTag(item, type);
}
catch (IOException ex)
catch (Exception ex)
{
_logger.ErrorException("Error getting {0} image info", ex, type);
return null;
@ -373,7 +373,7 @@ namespace MediaBrowser.Server.Implementations.Dto
{
return _imageProcessor.GetImageCacheTag(item, image);
}
catch (IOException ex)
catch (Exception ex)
{
_logger.ErrorException("Error getting {0} image info for {1}", ex, image.Type, image.Path);
return null;

View file

@ -34,8 +34,10 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio
{
var collectionType = args.GetCollectionType();
if (string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase) ||
string.IsNullOrWhiteSpace(collectionType))
var isStandalone = args.Parent == null;
if (isStandalone ||
string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase))
{
return new Controller.Entities.Audio.Audio();
}

View file

@ -31,9 +31,21 @@ namespace MediaBrowser.Server.Implementations.Library
public async Task<QueryResult<SearchHintInfo>> GetSearchHints(SearchQuery query)
{
var user = _userManager.GetUserById(new Guid(query.UserId));
IEnumerable<BaseItem> inputItems;
var inputItems = user.RootFolder.GetRecursiveChildren(user, null).Where(i => !(i is ICollectionFolder));
if (string.IsNullOrEmpty(query.UserId))
{
inputItems = _libraryManager.RootFolder.RecursiveChildren;
}
else
{
var user = _userManager.GetUserById(new Guid(query.UserId));
inputItems = user.RootFolder.GetRecursiveChildren(user, null);
}
inputItems = inputItems.Where(i => !(i is ICollectionFolder));
inputItems = _libraryManager.ReplaceVideosWithPrimaryVersions(inputItems);

View file

@ -0,0 +1 @@
{"SettingsSaved":"Configuraci\u00f3 guardada.","AddUser":"Afegir Usuari","Users":"Usuaris","Delete":"Esborrar","Administrator":"Administrador","Password":"Contrasenya","DeleteImage":"Esborrar Imatge","DeleteImageConfirmation":"Esteu segur que voleu suprimir aquesta imatge?","FileReadCancelled":"La lectura de l'arxiu ha estat cancel\u00b7lada.","FileNotFound":"Arxiu no trobat.","FileReadError":"S'ha produ\u00eft un error en llegir el fitxer.","DeleteUser":"Esborrar Usuari","DeleteUserConfirmation":"Esteu segur que voleu suprimir {0}?","PasswordResetHeader":"Restablir contrasenya","PasswordResetComplete":"La contrasenya s'ha restablert.","PasswordResetConfirmation":"Esteu segur que voleu restablir la contrasenya?","PasswordSaved":"S'ha guardat la contrasenya.","PasswordMatchError":"Confirmaci\u00f3 de la contrasenya i la contrasenya han de coincidir.","OptionOff":"Apagat","OptionOn":"Enc\u00e8s","OptionRelease":"Versi\u00f3 Oficial","OptionBeta":"Beta","OptionDev":"Dev (Inestable)","UninstallPluginHeader":"Desinstal\u00b7lar Plugin.","UninstallPluginConfirmation":"Esteu segur que voleu desinstal\u00b7lar {0}?","NoPluginConfigurationMessage":"Aquest plugin no necessita configuraci\u00f3.","NoPluginsInstalledMessage":"No t\u00e9 cap plugin instal\u00b7lat.","BrowsePluginCatalogMessage":"Consulti el nostre cat\u00e0leg per veure els plugins disponibles."}

View file

@ -1 +1 @@
{"SettingsSaved":"Einstellungen gespeichert","AddUser":"Benutzer hinzuf\u00fcgen","Users":"Benutzer","Delete":"L\u00f6schen","Administrator":"Administrator","Password":"Passwort","DeleteImage":"Bild l\u00f6schen","DeleteImageConfirmation":"M\u00f6chten Sie das Bild wirklich l\u00f6schen?","FileReadCancelled":"Das Einlesen der Datei wurde abgebrochen.","FileNotFound":"Datei nicht gefunden","FileReadError":"Beim Lesen der Datei ist ein Fehler aufgetreten.","DeleteUser":"Benutzer l\u00f6schen","DeleteUserConfirmation":"M\u00f6chten Sie {0} wirklich l\u00f6schen?","PasswordResetHeader":"Passwort zur\u00fccksetzen","PasswordResetComplete":"Das Passwort wurde zur\u00fcckgesetzt.","PasswordResetConfirmation":"M\u00f6chten Sie das Passwort wirklich zur\u00fccksetzen?","PasswordSaved":"Passwort gespeichert","PasswordMatchError":"Passwort und Passwortbest\u00e4tigung stimmen nicht \u00fcberein.","OptionOff":"Aus","OptionOn":"Ein","OptionRelease":"Release","OptionBeta":"Beta","OptionDev":"Dev","UninstallPluginHeader":"Deinstalliere Plugin","UninstallPluginConfirmation":"M\u00f6chten Sie {0} wirklich deinstallieren?","NoPluginConfigurationMessage":"Bei diesem Plugin kann nichts eingestellt werden.","NoPluginsInstalledMessage":"Sie haben keine Plugins installiert.","BrowsePluginCatalogMessage":"Durchsuchen Sie unsere Bibliothek um alle verf\u00fcgbaren Plugins anzuzeigen."}
{"SettingsSaved":"Einstellungen gespeichert","AddUser":"Benutzer hinzuf\u00fcgen","Users":"Benutzer","Delete":"L\u00f6schen","Administrator":"Administrator","Password":"Passwort","DeleteImage":"Bild l\u00f6schen","DeleteImageConfirmation":"M\u00f6chten Sie das Bild wirklich l\u00f6schen?","FileReadCancelled":"Das Einlesen der Datei wurde abgebrochen.","FileNotFound":"Datei nicht gefunden","FileReadError":"Beim Lesen der Datei ist ein Fehler aufgetreten.","DeleteUser":"Benutzer l\u00f6schen","DeleteUserConfirmation":"M\u00f6chten Sie {0} wirklich l\u00f6schen?","PasswordResetHeader":"Passwort zur\u00fccksetzen","PasswordResetComplete":"Das Passwort wurde zur\u00fcckgesetzt.","PasswordResetConfirmation":"M\u00f6chten Sie das Passwort wirklich zur\u00fccksetzen?","PasswordSaved":"Passwort gespeichert","PasswordMatchError":"Passwort und Passwortbest\u00e4tigung stimmen nicht \u00fcberein.","OptionOff":"Aus","OptionOn":"Ein","OptionRelease":"Offizielles Release","OptionBeta":"Beta","OptionDev":"Entwickler (instabil)","UninstallPluginHeader":"Deinstalliere Plugin","UninstallPluginConfirmation":"M\u00f6chten Sie {0} wirklich deinstallieren?","NoPluginConfigurationMessage":"Bei diesem Plugin kann nichts eingestellt werden.","NoPluginsInstalledMessage":"Sie haben keine Plugins installiert.","BrowsePluginCatalogMessage":"Durchsuchen Sie unsere Bibliothek um alle verf\u00fcgbaren Plugins anzuzeigen."}

View file

@ -0,0 +1 @@
{"SettingsSaved":"Seting Disimpan","AddUser":"Tambah Pengguna","Users":"Para Pengguna","Delete":"Padam","Administrator":"Administrator","Password":"Password","DeleteImage":"Delete Image","DeleteImageConfirmation":"Are you sure you wish to delete this image?","FileReadCancelled":"The file read has been canceled.","FileNotFound":"File not found.","FileReadError":"An error occurred while reading the file.","DeleteUser":"Delete User","DeleteUserConfirmation":"Are you sure you wish to delete {0}?","PasswordResetHeader":"Password Reset","PasswordResetComplete":"The password has been reset.","PasswordResetConfirmation":"Are you sure you wish to reset the password?","PasswordSaved":"Password saved.","PasswordMatchError":"Password and password confirmation must match.","OptionOff":"Off","OptionOn":"On","OptionRelease":"Official Release","OptionBeta":"Beta","OptionDev":"Dev (Unstable)","UninstallPluginHeader":"Uninstall Plugin","UninstallPluginConfirmation":"Are you sure you wish to uninstall {0}?","NoPluginConfigurationMessage":"This plugin has nothing to configure.","NoPluginsInstalledMessage":"You have no plugins installed.","BrowsePluginCatalogMessage":"Browse our plugin catalog to view available plugins."}

View file

@ -1 +1 @@
{"SettingsSaved":"Prefer\u00eancias salvas.","AddUser":"Adicionar Usu\u00e1rio","Users":"Usu\u00e1rios","Delete":"Apagar","Administrator":"Administrador","Password":"Senha","DeleteImage":"Apagar Imagem","DeleteImageConfirmation":"Tem certeza que deseja apagar esta imagem?","FileReadCancelled":"A leitura do arquivo foi cancelada.","FileNotFound":"Arquivo n\u00e3o encontrado.","FileReadError":"Ocorreu um erro ao ler o arquivo.","DeleteUser":"Apagar Usu\u00e1rio","DeleteUserConfirmation":"Tem certeza que deseja apagar {0}?","PasswordResetHeader":"Redefinir Senha","PasswordResetComplete":"A senha foi redefinida.","PasswordResetConfirmation":"Deseja realmente redefinir a senha?","PasswordSaved":"Senha salva.","PasswordMatchError":"A senha e confirma\u00e7\u00e3o da senha devem conferir.","OptionOff":"Off","OptionOn":"On","OptionRelease":"Lan\u00e7amento Oficial","OptionBeta":"Beta","OptionDev":"Dev (Inst\u00e1vel)","UninstallPluginHeader":"Desintalar Plugin","UninstallPluginConfirmation":"Deseja realmente desinstalar {0}?","NoPluginConfigurationMessage":"Este plugin n\u00e3o necessita configurar.","NoPluginsInstalledMessage":"N\u00e3o existem plugins instalados.","BrowsePluginCatalogMessage":"Navegue pelo cat\u00e1logo de plugins para ver os dispon\u00edveis."}
{"SettingsSaved":"Ajustes salvos.","AddUser":"Adicionar Usu\u00e1rio","Users":"Usu\u00e1rios","Delete":"Apagar","Administrator":"Administrador","Password":"Senha","DeleteImage":"Apagar Imagem","DeleteImageConfirmation":"Tem certeza que deseja apagar esta imagem?","FileReadCancelled":"A leitura do arquivo foi cancelada.","FileNotFound":"Arquivo n\u00e3o encontrado.","FileReadError":"Ocorreu um erro ao ler o arquivo.","DeleteUser":"Apagar Usu\u00e1rio","DeleteUserConfirmation":"Deseja realmente apagar {0}?","PasswordResetHeader":"Redefinir Senha","PasswordResetComplete":"A senha foi redefinida.","PasswordResetConfirmation":"Deseja realmente redefinir a senha?","PasswordSaved":"Senha salva.","PasswordMatchError":"A senha e a confirma\u00e7\u00e3o da senha devem ser iguais.","OptionOff":"Off","OptionOn":"On","OptionRelease":"Lan\u00e7amento Oficial","OptionBeta":"Beta","OptionDev":"Dev (Inst\u00e1vel)","UninstallPluginHeader":"Desinstalar Plugin","UninstallPluginConfirmation":"Deseja realmente desinstalar {0}?","NoPluginConfigurationMessage":"Este plugin n\u00e3o necessita configurar.","NoPluginsInstalledMessage":"N\u00e3o existem plugins instalados.","BrowsePluginCatalogMessage":"Navegue pelo cat\u00e1logo de plugins para ver os dispon\u00edveis."}

View file

@ -1 +1 @@
{"SettingsSaved":"\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u044b","AddUser":"\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f","Users":"\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438","Delete":"\u0423\u0434\u0430\u043b\u0438\u0442\u044c","Administrator":"\u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440","Password":"\u041f\u0430\u0440\u043e\u043b\u044c","DeleteImage":"\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435","DeleteImageConfirmation":"\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0436\u0435\u043b\u0430\u0435\u0442\u0435 \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0434\u0430\u043d\u043d\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435?","FileReadCancelled":"\u0427\u0442\u0435\u043d\u0438\u0435 \u0444\u0430\u0439\u043b\u0430 \u0431\u044b\u043b\u043e \u043e\u0442\u043c\u0435\u043d\u0435\u043d\u043e.","FileNotFound":"\u0424\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d","FileReadError":"\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0447\u0442\u0435\u043d\u0438\u044f \u0444\u0430\u0439\u043b\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430","DeleteUser":"\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f","DeleteUserConfirmation":"\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0436\u0435\u043b\u0430\u0435\u0442\u0435 \u0443\u0434\u0430\u043b\u0438\u0442\u044c {0}?","PasswordResetHeader":"\u0421\u0431\u0440\u043e\u0441 \u043f\u0430\u0440\u043e\u043b\u044f","PasswordResetComplete":"\u041f\u0430\u0440\u043e\u043b\u044c \u0431\u044b\u043b \u0441\u0431\u0440\u043e\u0448\u0435\u043d","PasswordResetConfirmation":"\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0436\u0435\u043b\u0430\u0435\u0442\u0435 \u0441\u0431\u0440\u043e\u0441\u0438\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c?","PasswordSaved":"\u041f\u0430\u0440\u043e\u043b\u044c \u0441\u043e\u0445\u0440\u0430\u043d\u0451\u043d","PasswordMatchError":"\u041f\u043e\u043b\u044f \u041f\u0430\u0440\u043e\u043b\u044c \u0438 \u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u043f\u0430\u0440\u043e\u043b\u044f \u0434\u043e\u043b\u0436\u043d\u044b \u0441\u043e\u0432\u043f\u0430\u0434\u0430\u0442\u044c","OptionOff":"\u0412\u044b\u043a\u043b","OptionOn":"\u0412\u043a\u043b","OptionRelease":"\u041e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 \u0432\u044b\u043f\u0443\u0441\u043a","OptionBeta":"\u0411\u0435\u0442\u0430","OptionDev":"\u0420\u0430\u0437\u0440\u0430\u0431 (\u043d\u0435\u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u043e)","UninstallPluginHeader":"\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u043f\u043b\u0430\u0433\u0438\u043d","UninstallPluginConfirmation":"\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0436\u0435\u043b\u0430\u0435\u0442\u0435 \u0443\u0434\u0430\u043b\u0438\u0442\u044c {0}?","NoPluginConfigurationMessage":"\u0414\u043b\u044f \u0434\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u043b\u0430\u0433\u0438\u043d\u0430 \u043d\u0435\u0447\u0435\u0433\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c.","NoPluginsInstalledMessage":"\u0423 \u0412\u0430\u0441 \u043d\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u043d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u043f\u043b\u0430\u0433\u0438\u043d\u0430.","BrowsePluginCatalogMessage":"\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u043d\u0430\u0448\u0438\u043c \u043a\u0430\u0442\u0430\u043b\u043e\u0433\u043e\u043c \u0434\u043b\u044f \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u043f\u043b\u0430\u0433\u0438\u043d\u043e\u0432."}
{"SettingsSaved":"\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u044b","AddUser":"\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f","Users":"\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438","Delete":"\u0423\u0434\u0430\u043b\u0438\u0442\u044c","Administrator":"\u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440","Password":"\u041f\u0430\u0440\u043e\u043b\u044c","DeleteImage":"\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0440\u0438\u0441\u0443\u043d\u043e\u043a","DeleteImageConfirmation":"\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0436\u0435\u043b\u0430\u0435\u0442\u0435 \u0443\u0434\u0430\u043b\u0438\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0439 \u0440\u0438\u0441\u0443\u043d\u043e\u043a?","FileReadCancelled":"\u0427\u0442\u0435\u043d\u0438\u0435 \u0444\u0430\u0439\u043b\u0430 \u0431\u044b\u043b\u043e \u043e\u0442\u043c\u0435\u043d\u0435\u043d\u043e.","FileNotFound":"\u0424\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d","FileReadError":"\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0447\u0442\u0435\u043d\u0438\u044f \u0444\u0430\u0439\u043b\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430","DeleteUser":"\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f","DeleteUserConfirmation":"\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0436\u0435\u043b\u0430\u0435\u0442\u0435 \u0443\u0434\u0430\u043b\u0438\u0442\u044c {0}?","PasswordResetHeader":"\u0421\u0431\u0440\u043e\u0441 \u043f\u0430\u0440\u043e\u043b\u044f","PasswordResetComplete":"\u041f\u0430\u0440\u043e\u043b\u044c \u0431\u044b\u043b \u0441\u0431\u0440\u043e\u0448\u0435\u043d","PasswordResetConfirmation":"\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0436\u0435\u043b\u0430\u0435\u0442\u0435 \u0441\u0431\u0440\u043e\u0441\u0438\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c?","PasswordSaved":"\u041f\u0430\u0440\u043e\u043b\u044c \u0441\u043e\u0445\u0440\u0430\u043d\u0451\u043d","PasswordMatchError":"\u041f\u043e\u043b\u044f \u041f\u0430\u0440\u043e\u043b\u044c \u0438 \u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u043f\u0430\u0440\u043e\u043b\u044f \u0434\u043e\u043b\u0436\u043d\u044b \u0441\u043e\u0432\u043f\u0430\u0434\u0430\u0442\u044c","OptionOff":"\u0412\u044b\u043a\u043b","OptionOn":"\u0412\u043a\u043b","OptionRelease":"\u041e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 \u0432\u044b\u043f\u0443\u0441\u043a","OptionBeta":"\u0411\u0435\u0442\u0430","OptionDev":"\u0420\u0430\u0437\u0440\u0430\u0431 (\u043d\u0435\u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u043e)","UninstallPluginHeader":"\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u043f\u043b\u0430\u0433\u0438\u043d","UninstallPluginConfirmation":"\u0412\u044b \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0436\u0435\u043b\u0430\u0435\u0442\u0435 \u0443\u0434\u0430\u043b\u0438\u0442\u044c {0}?","NoPluginConfigurationMessage":"\u0414\u043b\u044f \u0434\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u043b\u0430\u0433\u0438\u043d\u0430 \u043d\u0435\u0447\u0435\u0433\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c.","NoPluginsInstalledMessage":"\u0423 \u0412\u0430\u0441 \u043d\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u043d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u043f\u043b\u0430\u0433\u0438\u043d\u0430.","BrowsePluginCatalogMessage":"\u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u043d\u0430\u0448 \u043a\u0430\u0442\u0430\u043b\u043e\u0433, \u0447\u0442\u043e\u0431\u044b \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u0438\u0442\u044c\u0441\u044f \u0441 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u043f\u043b\u0430\u0433\u0438\u043d\u0430\u043c\u0438."}

View file

@ -125,7 +125,18 @@ namespace MediaBrowser.Server.Implementations.Localization
public IEnumerable<CountryInfo> GetCountries()
{
return CultureInfo.GetCultures(CultureTypes.SpecificCultures)
.Select(c => new RegionInfo(c.LCID))
.Select(c =>
{
try
{
return new RegionInfo(c.LCID);
}
catch (CultureNotFoundException)
{
return null;
}
})
.Where(i => i != null)
.OrderBy(c => c.DisplayName)
.DistinctBy(c => c.TwoLetterISORegionName)
.Select(c => new CountryInfo
@ -356,7 +367,7 @@ namespace MediaBrowser.Server.Implementations.Localization
}.OrderBy(i => i.Name);
}
public string LocalizeDocument(string document, string culture, Func<string,string> tokenBuilder)
public string LocalizeDocument(string document, string culture, Func<string, string> tokenBuilder)
{
foreach (var pair in GetLocalizationDictionary(culture).ToList())
{

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -450,5 +450,70 @@
"LabelMinResumeDuration": "Min resume duration (seconds):",
"LabelMinResumePercentageHelp": "Titles are assumed unplayed if stopped before this time",
"LabelMaxResumePercentageHelp": "Titles are assumed fully played if stopped after this time",
"LabelMinResumeDurationHelp": "Titles shorter than this will not be resumable"
"LabelMinResumeDurationHelp": "Titles shorter than this will not be resumable",
"TitleAutoOrganize": "Auto-Organize",
"TabActivityLog": "Activity Log",
"HeaderName": "Name",
"HeaderDate": "Date",
"HeaderSource": "Source",
"HeaderStatus": "Status",
"HeaderDestination": "Destination",
"HeaderProgram": "Program",
"HeaderClients": "Clients",
"LabelCompleted": "Completed",
"LabelFailed": "Failed",
"LabelSkipped": "Skipped",
"HeaderEpisodeOrganization": "Episode Organization",
"LabelSeries": "Series:",
"LabelSeasonNumber": "Season number:",
"LabelEpisodeNumber": "Episode number:",
"LabelEndingEpisodeNumber": "Ending episode number:",
"LabelEndingEpisodeNumberHelp": "Only required for multi-episode files",
"HeaderSupportTheTeam": "Support the Media Browser Team",
"LabelSupportAmount": "Amount (USD)",
"HeaderSupportTheTeamHelp": "Help ensure the continued development of this project by donating. A portion of all donations will be contributed to other free tools we depend on.",
"ButtonEnterSupporterKey": "Enter supporter key",
"DonationNextStep": "Once complete, please return and enter your supporter key, which you will receive by email.",
"AutoOrganizeHelp": "Auto-organize monitors your download folders for new files and moves them to your media directories.",
"AutoOrganizeTvHelp": "TV file organizing will only add episodes to existing series. It will not create new series folders.",
"OptionEnableEpisodeOrganization": "Enable new episode organization",
"LabelWatchFolder": "Watch folder:",
"LabelWatchFolderHelp": "The server will poll this folder during the 'Organize new media files' scheduled task.",
"ButtonViewScheduledTasks": "View scheduled tasks",
"LabelMinFileSizeForOrganize": "Minimum file size (MB):",
"LabelMinFileSizeForOrganizeHelp": "Files under this size will be ignored.",
"LabelSeasonFolderPattern": "Season folder pattern:",
"LabelSeasonZeroFolderName": "Season zero folder name:",
"HeaderEpisodeFilePattern": "Episode file pattern",
"LabelEpisodePattern": "Episode pattern:",
"LabelMultiEpisodePattern": "Multi-Episode pattern:",
"HeaderSupportedPatterns": "Supported Patterns",
"HeaderTerm": "Term",
"HeaderPattern": "Pattern",
"HeaderResult": "Result",
"LabelDeleteEmptyFolders": "Delete empty folders after organizing",
"LabelDeleteEmptyFoldersHelp": "Enable this to keep the download directory clean.",
"LabelDeleteLeftOverFiles": "Delete left over files with the following extensions:",
"LabelDeleteLeftOverFilesHelp": "Separate with ;. For example: .nfo;.txt",
"OptionOverwriteExistingEpisodes": "Overwrite existing episodes",
"LabelTransferMethod": "Transfer method",
"OptionCopy": "Copy",
"OptionMove": "Move",
"LabelTransferMethodHelp": "Copy or move files from the watch folder",
"HeaderLatestNews": "Latest News",
"HeaderHelpImproveMediaBrowser": "Help Improve Media Browser",
"HeaderRunningTasks": "Running Tasks",
"HeaderActiveDevices": "Active Devices",
"HeaderPendingInstallations": "Pending Installations",
"HeaerServerInformation": "Server Information",
"ButtonRestartNow": "Restart Now",
"ButtonRestart": "Restart",
"ButtonShutdown": "Shutdown",
"ButtonUpdateNow": "Update Now",
"PleaseUpdateManually": "Please shutdown the server and update manually.",
"NewServerVersionAvailable": "A new version of Media Browser Server is available!",
"ServerUpToDate": "Media Browser Server is up to date",
"ErrorConnectingToMediaBrowserRepository": "There was an error connecting to the remote Media Browser repository.",
"LabelComponentsUpdated": "The following components have been installed or updated:",
"MessagePleaseRestartServerToFinishUpdating": "Please restart the server to finish applying updates."
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -319,6 +319,10 @@
<EmbeddedResource Include="Localization\Server\sv.json" />
<EmbeddedResource Include="Localization\JavaScript\cs.json" />
<EmbeddedResource Include="Localization\Server\cs.json" />
<EmbeddedResource Include="Localization\JavaScript\ca.json" />
<EmbeddedResource Include="Localization\JavaScript\ms.json" />
<EmbeddedResource Include="Localization\Server\ca.json" />
<EmbeddedResource Include="Localization\Server\ms.json" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>

View file

@ -46,6 +46,16 @@ namespace MediaBrowser.Server.Implementations.Roku
return Task.FromResult(true);
}
public Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
{
return Task.FromResult(true);
}
public Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
{
return Task.FromResult(true);
}
public Task SendMessageCommand(MessageCommand command, CancellationToken cancellationToken)
{
return SendCommand(new WebSocketMessage<MessageCommand>
@ -66,16 +76,6 @@ namespace MediaBrowser.Server.Implementations.Roku
}, cancellationToken);
}
public Task SendBrowseCommand(BrowseRequest command, CancellationToken cancellationToken)
{
return SendCommand(new WebSocketMessage<BrowseRequest>
{
MessageType = "Browse",
Data = command
}, cancellationToken);
}
public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
{
return SendCommand(new WebSocketMessage<PlaystateRequest>

View file

@ -8,6 +8,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Entities;
@ -444,6 +445,8 @@ namespace MediaBrowser.Server.Implementations.Session
MediaSourceId = info.MediaSourceId
}, _logger);
await SendPlaybackStartNotification(session, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
@ -583,6 +586,8 @@ namespace MediaBrowser.Server.Implementations.Session
MediaSourceId = mediaSourceId
}, _logger);
await SendPlaybackStoppedNotification(session, CancellationToken.None).ConfigureAwait(false);
}
private string GetMediaSourceId(BaseItem item, string reportedMediaSourceId)
@ -858,12 +863,17 @@ namespace MediaBrowser.Server.Implementations.Session
public Task SendBrowseCommand(Guid controllingSessionId, Guid sessionId, BrowseRequest command, CancellationToken cancellationToken)
{
var session = GetSessionForRemoteControl(sessionId);
var generalCommand = new GeneralCommand
{
Name = GeneralCommandType.DisplayContent.ToString()
};
var controllingSession = GetSession(controllingSessionId);
AssertCanControl(session, controllingSession);
generalCommand.Arguments["Context"] = command.Context;
generalCommand.Arguments["ItemId"] = command.ItemId;
generalCommand.Arguments["ItemName"] = command.ItemName;
generalCommand.Arguments["ItemType"] = command.ItemType;
return session.SessionController.SendBrowseCommand(command, cancellationToken);
return SendGeneralCommand(controllingSessionId, sessionId, generalCommand, cancellationToken);
}
public Task SendPlaystateCommand(Guid controllingSessionId, Guid sessionId, PlaystateRequest command, CancellationToken cancellationToken)
@ -972,7 +982,6 @@ namespace MediaBrowser.Server.Implementations.Session
return Task.WhenAll(tasks);
}
public Task SendSessionEndedNotification(SessionInfo sessionInfo, CancellationToken cancellationToken)
{
var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList();
@ -994,6 +1003,48 @@ namespace MediaBrowser.Server.Implementations.Session
return Task.WhenAll(tasks);
}
public Task SendPlaybackStartNotification(SessionInfo sessionInfo, CancellationToken cancellationToken)
{
var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList();
var dto = GetSessionInfoDto(sessionInfo);
var tasks = sessions.Select(session => Task.Run(async () =>
{
try
{
await session.SessionController.SendPlaybackStartNotification(dto, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.ErrorException("Error in SendPlaybackStartNotification.", ex);
}
}, cancellationToken));
return Task.WhenAll(tasks);
}
public Task SendPlaybackStoppedNotification(SessionInfo sessionInfo, CancellationToken cancellationToken)
{
var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList();
var dto = GetSessionInfoDto(sessionInfo);
var tasks = sessions.Select(session => Task.Run(async () =>
{
try
{
await session.SessionController.SendPlaybackStoppedNotification(dto, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.ErrorException("Error in SendPlaybackStoppedNotification.", ex);
}
}, cancellationToken));
return Task.WhenAll(tasks);
}
/// <summary>
/// Adds the additional user.
/// </summary>
@ -1131,6 +1182,13 @@ namespace MediaBrowser.Server.Implementations.Session
if (session.UserId.HasValue)
{
dto.UserId = session.UserId.Value.ToString("N");
var user = _userManager.GetUserById(session.UserId.Value);
if (user != null)
{
dto.UserPrimaryImageTag = GetImageCacheTag(user, ImageType.Primary);
}
}
return dto;
@ -1158,19 +1216,84 @@ namespace MediaBrowser.Server.Implementations.Session
MediaType = item.MediaType,
Type = item.GetClientTypeName(),
RunTimeTicks = nowPlayingRuntimeTicks,
MediaSourceId = mediaSourceId
MediaSourceId = mediaSourceId,
IndexNumber = item.IndexNumber,
ParentIndexNumber = item.ParentIndexNumber,
PremiereDate = item.PremiereDate,
ProductionYear = item.ProductionYear
};
info.PrimaryImageTag = GetImageCacheTag(item, ImageType.Primary);
if (info.PrimaryImageTag.HasValue)
{
info.PrimaryImageItemId = GetDtoId(item);
}
var episode = item as Episode;
if (episode != null)
{
info.IndexNumberEnd = episode.IndexNumberEnd;
}
var hasSeries = item as IHasSeries;
if (hasSeries != null)
{
info.SeriesName = hasSeries.SeriesName;
}
var recording = item as ILiveTvRecording;
if (recording != null && recording.RecordingInfo != null)
{
if (recording.RecordingInfo.IsSeries)
{
info.Name = recording.RecordingInfo.EpisodeTitle;
info.SeriesName = recording.RecordingInfo.Name;
if (string.IsNullOrWhiteSpace(info.Name))
{
info.Name = recording.RecordingInfo.Name;
}
}
}
var audio = item as Audio;
if (audio != null)
{
info.Album = audio.Album;
info.Artists = audio.Artists;
if (!info.PrimaryImageTag.HasValue)
{
var album = audio.Parents.OfType<MusicAlbum>().FirstOrDefault();
if (album != null && album.HasImage(ImageType.Primary))
{
info.PrimaryImageTag = GetImageCacheTag(album, ImageType.Primary);
if (info.PrimaryImageTag.HasValue)
{
info.PrimaryImageItemId = GetDtoId(album);
}
}
}
}
var musicVideo = item as MusicVideo;
if (musicVideo != null)
{
info.Album = musicVideo.Album;
if (!string.IsNullOrWhiteSpace(musicVideo.Artist))
{
info.Artists.Add(musicVideo.Artist);
}
}
var backropItem = item.HasImage(ImageType.Backdrop) ? item : null;
var thumbItem = item.HasImage(ImageType.Thumb) ? item : null;
if (thumbItem == null)
{
var episode = item as Episode;
if (episode != null)
{
var series = episode.Series;
@ -1184,8 +1307,6 @@ namespace MediaBrowser.Server.Implementations.Session
if (backropItem == null)
{
var episode = item as Episode;
if (episode != null)
{
var series = episode.Series;
@ -1197,6 +1318,11 @@ namespace MediaBrowser.Server.Implementations.Session
}
}
if (backropItem == null)
{
backropItem = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Backdrop));
}
if (thumbItem == null)
{
thumbItem = item.Parents.FirstOrDefault(i => i.HasImage(ImageType.Thumb));
@ -1208,7 +1334,7 @@ namespace MediaBrowser.Server.Implementations.Session
info.ThumbItemId = GetDtoId(thumbItem);
}
if (thumbItem != null)
if (backropItem != null)
{
info.BackdropImageTag = GetImageCacheTag(backropItem, ImageType.Backdrop);
info.BackdropItemId = GetDtoId(backropItem);

View file

@ -81,18 +81,6 @@ namespace MediaBrowser.Server.Implementations.Session
}, cancellationToken);
}
public Task SendBrowseCommand(BrowseRequest command, CancellationToken cancellationToken)
{
var socket = GetActiveSocket();
return socket.SendAsync(new WebSocketMessage<BrowseRequest>
{
MessageType = "Browse",
Data = command
}, cancellationToken);
}
public Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken)
{
var socket = GetActiveSocket();
@ -210,5 +198,29 @@ namespace MediaBrowser.Server.Implementations.Session
}, cancellationToken);
}
public Task SendPlaybackStartNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
{
var socket = GetActiveSocket();
return socket.SendAsync(new WebSocketMessage<SessionInfoDto>
{
MessageType = "PlaybackStart",
Data = sessionInfo
}, cancellationToken);
}
public Task SendPlaybackStoppedNotification(SessionInfoDto sessionInfo, CancellationToken cancellationToken)
{
var socket = GetActiveSocket();
return socket.SendAsync(new WebSocketMessage<SessionInfoDto>
{
MessageType = "PlaybackStopped",
Data = sessionInfo
}, cancellationToken);
}
}
}

View file

@ -1,5 +1,4 @@
using System.Threading;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Logging;

View file

@ -101,7 +101,7 @@
<Name>MediaBrowser.Providers</Name>
</ProjectReference>
<ProjectReference Include="..\MediaBrowser.Dlna\MediaBrowser.Dlna.csproj">
<Project>{734098eb-6dc1-4dd0-a1ca-3140dcd2737c}</Project>
<Project>{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}</Project>
<Name>MediaBrowser.Dlna</Name>
</ProjectReference>
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj">
@ -124,6 +124,10 @@
<Project>{4FD51AC5-2C16-4308-A993-C3A84F3B4582}</Project>
<Name>MediaBrowser.Api</Name>
</ProjectReference>
<ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj">
<Project>{0BD82FA6-EB8A-4452-8AF5-74F9C3849451}</Project>
<Name>MediaBrowser.MediaEncoding</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Folder Include="EntryPoints\" />

View file

@ -16,6 +16,7 @@ using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using System.Reflection;
using System.Linq;
// MONOMKBUNDLE: For the embedded version, mkbundle tool
#if MONOMKBUNDLE
using Mono.Unix;
@ -39,8 +40,13 @@ namespace MediaBrowser.Server.Mono
#else
var applicationPath = Assembly.GetEntryAssembly ().Location;
#endif
var commandArgs = Environment.GetCommandLineArgs();
// Allow this to be specified on the command line.
var customProgramDataPath = commandArgs.ElementAtOrDefault(1);
var appPaths = CreateApplicationPaths(applicationPath);
var appPaths = CreateApplicationPaths(applicationPath, customProgramDataPath);
var logManager = new NlogManager(appPaths.LogDirectoryPath, "server");
logManager.ReloadLogger(LogSeverity.Info);
@ -70,9 +76,14 @@ namespace MediaBrowser.Server.Mono
}
}
private static ServerApplicationPaths CreateApplicationPaths(string applicationPath)
private static ServerApplicationPaths CreateApplicationPaths(string applicationPath, string programDataPath)
{
return new ServerApplicationPaths(applicationPath);
if (string.IsNullOrEmpty(programDataPath))
{
return new ServerApplicationPaths(applicationPath);
}
return new ServerApplicationPaths(programDataPath, applicationPath);
}
/// <summary>

View file

@ -372,8 +372,11 @@ namespace MediaBrowser.WebDashboard.Api
sb.Append("<meta http-equiv=\"X-UA-Compatibility\" content=\"IE=Edge\">");
sb.Append("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">");
sb.Append("<meta name=\"apple-mobile-web-app-capable\" content=\"yes\">");
sb.Append("<meta name=\"mobile-web-app-capable\" content=\"yes\">");
//sb.Append("<meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">");
sb.Append("<link rel=\"icon\" sizes=\"114x114\" href=\"css/images/touchicon114.png\" />");
// http://developer.apple.com/library/ios/#DOCUMENTATION/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html
sb.Append("<link rel=\"apple-touch-icon\" href=\"css/images/touchicon.png\" />");
sb.Append("<link rel=\"apple-touch-icon\" sizes=\"72x72\" href=\"css/images/touchicon72.png\" />");
@ -419,6 +422,7 @@ namespace MediaBrowser.WebDashboard.Api
{
"scripts/all.js" + versionString,
"thirdparty/jstree1.0/jquery.jstree.min.js",
"thirdparty/jquery.unveil-custom.js",
"https://www.gstatic.com/cv/js/sender/v1/cast_sender.js"
};
@ -453,17 +457,17 @@ namespace MediaBrowser.WebDashboard.Api
await memoryStream.WriteAsync(newLineBytes, 0, newLineBytes.Length).ConfigureAwait(false);
var builder = new StringBuilder();
var assembly = GetType().Assembly;
using (var stream = assembly.GetManifestResourceStream("MediaBrowser.WebDashboard.ApiClient.js"))
using (var fs = _fileSystem.GetFileStream(GetDashboardResourcePath("thirdparty/mediabrowser.apiclient.js"), FileMode.Open, FileAccess.Read, FileShare.ReadWrite, true))
{
using (var streamReader = new StreamReader(stream))
using (var streamReader = new StreamReader(fs))
{
var text = await streamReader.ReadToEndAsync().ConfigureAwait(false);
builder.Append(text);
builder.Append(Environment.NewLine);
}
}
foreach (var file in GetScriptFiles())
{
var path = GetDashboardResourcePath("scripts/" + file);
@ -515,6 +519,7 @@ namespace MediaBrowser.WebDashboard.Api
"mediaplayer.js",
"mediaplayer-video.js",
"nowplayingbar.js",
"ratingdialog.js",
"aboutpage.js",
@ -603,6 +608,7 @@ namespace MediaBrowser.WebDashboard.Api
"supporterkeypage.js",
"supporterpage.js",
"episodes.js",
"thememediaplayer.js",
"tvgenres.js",
"tvlatest.js",
"tvpeople.js",

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