mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-15 18:08:53 -07:00
Merge branch 'dev' of https://github.com/MediaBrowser/Emby into dev
This commit is contained in:
commit
15a98c5eaa
@ -1,10 +1,8 @@
|
||||
using System;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Connect;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Connect;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using ServiceStack;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@ -75,28 +73,6 @@ namespace MediaBrowser.Api
|
||||
public string ConnectUserId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Connect/Supporters", "GET")]
|
||||
[Authenticated(Roles = "Admin")]
|
||||
public class GetConnectSupporterSummary : IReturn<ConnectSupporterSummary>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Connect/Supporters", "DELETE")]
|
||||
[Authenticated(Roles = "Admin")]
|
||||
public class RemoveConnectSupporter : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Connect/Supporters", "POST")]
|
||||
[Authenticated(Roles = "Admin")]
|
||||
public class AddConnectSupporter : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
public class ConnectService : BaseApiService
|
||||
{
|
||||
private readonly IConnectManager _connectManager;
|
||||
@ -108,35 +84,6 @@ namespace MediaBrowser.Api
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
public async Task<object> Get(GetConnectSupporterSummary request)
|
||||
{
|
||||
var result = await _connectManager.GetConnectSupporterSummary().ConfigureAwait(false);
|
||||
var existingConnectUserIds = result.Users.Select(i => i.Id).ToList();
|
||||
|
||||
result.EligibleUsers = _userManager.Users
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i.ConnectUserId))
|
||||
.Where(i => !existingConnectUserIds.Contains(i.ConnectUserId, StringComparer.OrdinalIgnoreCase))
|
||||
.OrderBy(i => i.Name)
|
||||
.Select(i => _userManager.GetUserDto(i))
|
||||
.ToList();
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public void Delete(RemoveConnectSupporter request)
|
||||
{
|
||||
var task = _connectManager.RemoveConnectSupporter(request.Id);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
public void Post(AddConnectSupporter request)
|
||||
{
|
||||
var task = _connectManager.AddConnectSupporter(request.Id);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
public object Post(CreateConnectLink request)
|
||||
{
|
||||
return _connectManager.LinkUser(request.Id, request.ConnectUsername);
|
||||
|
@ -1,9 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Connect;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Session;
|
||||
using ServiceStack;
|
||||
|
||||
namespace MediaBrowser.Api
|
||||
@ -13,6 +18,8 @@ namespace MediaBrowser.Api
|
||||
{
|
||||
[ApiMember(Name = "DeviceId", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string DeviceId { get; set; }
|
||||
[ApiMember(Name = "AppName", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string AppName { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Auth/Pin", "GET", Summary = "Gets pin status")]
|
||||
@ -35,7 +42,7 @@ namespace MediaBrowser.Api
|
||||
|
||||
[Route("/Auth/Pin/Validate", "POST", Summary = "Validates a pin")]
|
||||
[Authenticated]
|
||||
public class ValidatePinRequest : IReturnVoid
|
||||
public class ValidatePinRequest : IReturn<SessionInfoDto>
|
||||
{
|
||||
[ApiMember(Name = "Pin", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string Pin { get; set; }
|
||||
@ -43,10 +50,27 @@ namespace MediaBrowser.Api
|
||||
|
||||
public class PinLoginService : BaseApiService
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, MyPinStatus> _activeRequests = new ConcurrentDictionary<string, MyPinStatus>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly ConcurrentDictionary<string, MyPinStatus> _activeRequests = new ConcurrentDictionary<string, MyPinStatus>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
public PinLoginService(ISessionManager sessionManager, IUserManager userManager)
|
||||
{
|
||||
_sessionManager = sessionManager;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
public object Post(CreatePinRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.DeviceId))
|
||||
{
|
||||
throw new ArgumentNullException("DeviceId");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(request.AppName))
|
||||
{
|
||||
throw new ArgumentNullException("AppName");
|
||||
}
|
||||
|
||||
var pin = GetNewPin();
|
||||
|
||||
var value = new MyPinStatus
|
||||
@ -55,7 +79,8 @@ namespace MediaBrowser.Api
|
||||
IsConfirmed = false,
|
||||
IsExpired = false,
|
||||
Pin = pin,
|
||||
DeviceId = request.DeviceId
|
||||
DeviceId = request.DeviceId,
|
||||
AppName = request.AppName
|
||||
};
|
||||
|
||||
_activeRequests.AddOrUpdate(pin, value, (k, v) => value);
|
||||
@ -75,6 +100,7 @@ namespace MediaBrowser.Api
|
||||
|
||||
if (!_activeRequests.TryGetValue(request.Pin, out status))
|
||||
{
|
||||
Logger.Debug("Pin {0} not found.", request.Pin);
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
@ -88,12 +114,13 @@ namespace MediaBrowser.Api
|
||||
});
|
||||
}
|
||||
|
||||
public object Post(ExchangePinRequest request)
|
||||
public async Task<object> Post(ExchangePinRequest request)
|
||||
{
|
||||
MyPinStatus status;
|
||||
|
||||
if (!_activeRequests.TryGetValue(request.Pin, out status))
|
||||
{
|
||||
Logger.Debug("Pin {0} not found.", request.Pin);
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
@ -104,14 +131,24 @@ namespace MediaBrowser.Api
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
return ToOptimizedResult(new PinExchangeResult
|
||||
var auth = AuthorizationContext.GetAuthorizationInfo(Request);
|
||||
var user = _userManager.GetUserById(status.UserId);
|
||||
|
||||
var result = await _sessionManager.CreateNewSession(new AuthenticationRequest
|
||||
{
|
||||
// TODO: Add access token
|
||||
UserId = status.UserId
|
||||
});
|
||||
App = auth.Client,
|
||||
AppVersion = auth.Version,
|
||||
DeviceId = auth.DeviceId,
|
||||
DeviceName = auth.Device,
|
||||
RemoteEndPoint = Request.RemoteIp,
|
||||
Username = user.Name
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
return ToOptimizedResult(result);
|
||||
}
|
||||
|
||||
public void Post(ValidatePinRequest request)
|
||||
public object Post(ValidatePinRequest request)
|
||||
{
|
||||
MyPinStatus status;
|
||||
|
||||
@ -124,12 +161,18 @@ namespace MediaBrowser.Api
|
||||
|
||||
status.IsConfirmed = true;
|
||||
status.UserId = AuthorizationContext.GetAuthorizationInfo(Request).UserId;
|
||||
|
||||
return ToOptimizedResult(new ValidatePinResult
|
||||
{
|
||||
AppName = status.AppName
|
||||
});
|
||||
}
|
||||
|
||||
private void EnsureValid(string requestedDeviceId, MyPinStatus status)
|
||||
{
|
||||
if (!string.Equals(requestedDeviceId, status.DeviceId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Logger.Debug("Pin device Id's do not match. requestedDeviceId: {0}, status.DeviceId: {1}", requestedDeviceId, status.DeviceId);
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
@ -145,6 +188,7 @@ namespace MediaBrowser.Api
|
||||
|
||||
if (status.IsExpired)
|
||||
{
|
||||
Logger.Debug("Pin {0} is expired", status.Pin);
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
}
|
||||
@ -163,16 +207,7 @@ namespace MediaBrowser.Api
|
||||
|
||||
private string GetNewPinInternal()
|
||||
{
|
||||
var length = 5;
|
||||
var pin = string.Empty;
|
||||
|
||||
while (pin.Length < length)
|
||||
{
|
||||
var digit = new Random().Next(0, 9);
|
||||
pin += digit.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return pin;
|
||||
return new Random().Next(10000, 99999).ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private bool IsPinActive(string pin)
|
||||
@ -181,15 +216,15 @@ namespace MediaBrowser.Api
|
||||
|
||||
if (!_activeRequests.TryGetValue(pin, out status))
|
||||
{
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (status.IsExpired)
|
||||
{
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public class MyPinStatus : PinStatusResult
|
||||
@ -197,6 +232,12 @@ namespace MediaBrowser.Api
|
||||
public DateTime CreationTimeUtc { get; set; }
|
||||
public string DeviceId { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public string AppName { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public class ValidatePinResult
|
||||
{
|
||||
public string AppName { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1462,6 +1462,13 @@ namespace MediaBrowser.Api.Playback
|
||||
{
|
||||
// Duplicating ItemId because of MediaMonkey
|
||||
}
|
||||
else if (i == 24)
|
||||
{
|
||||
if (videoRequest != null)
|
||||
{
|
||||
videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2021,6 +2028,11 @@ namespace MediaBrowser.Api.Playback
|
||||
state.EstimateContentLength = transcodingProfile.EstimateContentLength;
|
||||
state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
|
||||
state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
|
||||
|
||||
if (state.VideoRequest != null)
|
||||
{
|
||||
state.VideoRequest.CopyTimestamps = transcodingProfile.CopyTimestamps;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2184,9 +2196,9 @@ namespace MediaBrowser.Api.Playback
|
||||
|
||||
if (state.VideoRequest != null)
|
||||
{
|
||||
if (string.Equals(state.OutputContainer, "mkv", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(state.OutputContainer, "mkv", StringComparison.OrdinalIgnoreCase) && state.VideoRequest.CopyTimestamps)
|
||||
{
|
||||
//inputModifier += " -noaccurate_seek";
|
||||
inputModifier += " -noaccurate_seek";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,9 +137,9 @@ namespace MediaBrowser.Api.Playback.Progressive
|
||||
|
||||
var isOutputMkv = string.Equals(state.OutputContainer, "mkv", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (state.RunTimeTicks.HasValue)
|
||||
if (state.RunTimeTicks.HasValue && state.VideoRequest.CopyTimestamps)
|
||||
{
|
||||
//args += " -copyts -avoid_negative_ts disabled -start_at_zero";
|
||||
args += " -copyts -avoid_negative_ts disabled -start_at_zero";
|
||||
}
|
||||
|
||||
if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase))
|
||||
|
@ -187,6 +187,9 @@ namespace MediaBrowser.Api.Playback
|
||||
[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; }
|
||||
|
||||
[ApiMember(Name = "CopyTimestamps", Description = "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
|
||||
public bool CopyTimestamps { get; set; }
|
||||
|
||||
[ApiMember(Name = "Cabac", Description = "Enable if cabac encoding is required", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
|
||||
public bool? Cabac { get; set; }
|
||||
|
||||
|
@ -415,23 +415,6 @@ namespace MediaBrowser.Api
|
||||
{
|
||||
var auth = AuthorizationContext.GetAuthorizationInfo(Request);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(auth.Client))
|
||||
{
|
||||
auth.Client = "Unknown app";
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(auth.Device))
|
||||
{
|
||||
auth.Device = "Unknown device";
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(auth.Version))
|
||||
{
|
||||
auth.Version = "Unknown version";
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(auth.DeviceId))
|
||||
{
|
||||
auth.DeviceId = "Unknown device id";
|
||||
}
|
||||
|
||||
var result = await _sessionMananger.AuthenticateNewSession(new AuthenticationRequest
|
||||
{
|
||||
App = auth.Client,
|
||||
|
@ -76,25 +76,5 @@ namespace MediaBrowser.Controller.Connect
|
||||
/// <param name="token">The token.</param>
|
||||
/// <returns><c>true</c> if [is authorization token valid] [the specified token]; otherwise, <c>false</c>.</returns>
|
||||
bool IsAuthorizationTokenValid(string token);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connect supporter summary.
|
||||
/// </summary>
|
||||
/// <returns>Task<ConnectSupporterSummary>.</returns>
|
||||
Task<ConnectSupporterSummary> GetConnectSupporterSummary();
|
||||
|
||||
/// <summary>
|
||||
/// Removes the connect supporter.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task RemoveConnectSupporter(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the connect supporter.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task AddConnectSupporter(string id);
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public interface IHasMediaSources : IHasId
|
||||
public interface IHasMediaSources : IHasUserData
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the media sources.
|
||||
|
@ -78,7 +78,17 @@ namespace MediaBrowser.Controller.Entities
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if played; otherwise, <c>false</c>.</value>
|
||||
public bool Played { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the audio stream.
|
||||
/// </summary>
|
||||
/// <value>The index of the audio stream.</value>
|
||||
public int? AudioStreamIndex { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the subtitle stream.
|
||||
/// </summary>
|
||||
/// <value>The index of the subtitle stream.</value>
|
||||
public int? SubtitleStreamIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This is an interpreted property to indicate likes or dislikes
|
||||
/// This should never be serialized.
|
||||
|
@ -46,6 +46,9 @@ namespace MediaBrowser.Controller.LiveTv
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task<List<MediaSourceInfo>>.</returns>
|
||||
Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken);
|
||||
}
|
||||
public interface IConfigurableTunerHost
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates the specified information.
|
||||
/// </summary>
|
||||
|
@ -59,6 +59,12 @@ namespace MediaBrowser.Controller.LiveTv
|
||||
/// <value>The clients.</value>
|
||||
public List<string> Clients { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance can reset.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance can reset; otherwise, <c>false</c>.</value>
|
||||
public bool CanReset { get; set; }
|
||||
|
||||
public LiveTvTunerInfo()
|
||||
{
|
||||
Clients = new List<string>();
|
||||
|
@ -44,6 +44,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
public int? CpuCoreLimit { get; set; }
|
||||
public bool ReadInputAtNativeFramerate { get; set; }
|
||||
public SubtitleDeliveryMethod SubtitleMethod { get; set; }
|
||||
public bool CopyTimestamps { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance has fixed resolution.
|
||||
|
@ -250,6 +250,13 @@ namespace MediaBrowser.Controller.Session
|
||||
/// <returns>Task{SessionInfo}.</returns>
|
||||
Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the new session.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>Task<AuthenticationResult>.</returns>
|
||||
Task<AuthenticationResult> CreateNewSession(AuthenticationRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Reports the capabilities.
|
||||
/// </summary>
|
||||
|
@ -132,6 +132,8 @@ namespace MediaBrowser.Dlna.Ssdp
|
||||
return;
|
||||
}
|
||||
|
||||
_ssdpHandler.LogMessageReceived(args, true);
|
||||
|
||||
TryCreateDevice(args);
|
||||
}
|
||||
}
|
||||
@ -219,14 +221,6 @@ namespace MediaBrowser.Dlna.Ssdp
|
||||
return;
|
||||
}
|
||||
|
||||
if (_config.GetDlnaConfiguration().EnableDebugLog)
|
||||
{
|
||||
var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value));
|
||||
var headerText = string.Join(",", headerTexts.ToArray());
|
||||
|
||||
_logger.Debug("{0} Device message received from {1}. Headers: {2}", args.Method, args.EndPoint, headerText);
|
||||
}
|
||||
|
||||
EventHelper.FireEventIfNotNull(DeviceDiscovered, this, args, _logger);
|
||||
}
|
||||
|
||||
|
@ -93,17 +93,7 @@ namespace MediaBrowser.Dlna.Ssdp
|
||||
return;
|
||||
}
|
||||
|
||||
var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog;
|
||||
|
||||
if (enableDebugLogging)
|
||||
{
|
||||
var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value));
|
||||
var headerText = string.Join(",", headerTexts.ToArray());
|
||||
|
||||
var protocol = isMulticast ? "Multicast" : "Unicast";
|
||||
var localEndPointString = args.LocalEndPoint == null ? "null" : args.LocalEndPoint.ToString();
|
||||
_logger.Debug("{0} message received from {1} on {3}. Protocol: {4} Headers: {2}", args.Method, args.EndPoint, headerText, localEndPointString, protocol);
|
||||
}
|
||||
LogMessageReceived(args, isMulticast);
|
||||
|
||||
var headers = args.Headers;
|
||||
string st;
|
||||
@ -125,6 +115,21 @@ namespace MediaBrowser.Dlna.Ssdp
|
||||
EventHelper.FireEventIfNotNull(MessageReceived, this, args, _logger);
|
||||
}
|
||||
|
||||
internal void LogMessageReceived(SsdpMessageEventArgs args, bool isMulticast)
|
||||
{
|
||||
var enableDebugLogging = _config.GetDlnaConfiguration().EnableDebugLog;
|
||||
|
||||
if (enableDebugLogging)
|
||||
{
|
||||
var headerTexts = args.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value));
|
||||
var headerText = string.Join(",", headerTexts.ToArray());
|
||||
|
||||
var protocol = isMulticast ? "Multicast" : "Unicast";
|
||||
var localEndPointString = args.LocalEndPoint == null ? "null" : args.LocalEndPoint.ToString();
|
||||
_logger.Debug("{0} message received from {1} on {3}. Protocol: {4} Headers: {2}", args.Method, args.EndPoint, headerText, localEndPointString, protocol);
|
||||
}
|
||||
}
|
||||
|
||||
internal bool IgnoreMessage(SsdpMessageEventArgs args, bool isMulticast)
|
||||
{
|
||||
string usn;
|
||||
@ -139,7 +144,7 @@ namespace MediaBrowser.Dlna.Ssdp
|
||||
//var protocol = isMulticast ? "Multicast" : "Unicast";
|
||||
//var localEndPointString = args.LocalEndPoint == null ? "null" : args.LocalEndPoint.ToString();
|
||||
//_logger.Debug("IGNORING {0} message received from {1} on {3}. Protocol: {4} Headers: {2}", args.Method, args.EndPoint, headerText, localEndPointString, protocol);
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -159,7 +164,7 @@ namespace MediaBrowser.Dlna.Ssdp
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -298,9 +303,17 @@ namespace MediaBrowser.Dlna.Ssdp
|
||||
|
||||
var msg = new SsdpMessageBuilder().BuildMessage(header, values);
|
||||
|
||||
SendDatagram(msg, endpoint, null, false, 1);
|
||||
SendDatagram(msg, endpoint, new IPEndPoint(d.Address, 0), false, 1);
|
||||
//SendDatagram(header, values, endpoint, null, true);
|
||||
var ipEndPoint = endpoint as IPEndPoint;
|
||||
if (ipEndPoint != null)
|
||||
{
|
||||
SendUnicastRequest(msg, ipEndPoint);
|
||||
}
|
||||
else
|
||||
{
|
||||
SendDatagram(msg, endpoint, null, false, 2);
|
||||
SendDatagram(msg, endpoint, new IPEndPoint(d.Address, 0), false, 2);
|
||||
//SendDatagram(header, values, endpoint, null, true);
|
||||
}
|
||||
|
||||
if (enableDebugLogging)
|
||||
{
|
||||
@ -473,6 +486,7 @@ namespace MediaBrowser.Dlna.Ssdp
|
||||
var msg = new SsdpMessageBuilder().BuildMessage(header, values);
|
||||
|
||||
SendDatagram(msg, _ssdpEndp, new IPEndPoint(dev.Address, 0), true);
|
||||
//SendUnicastRequest(msg, 1);
|
||||
}
|
||||
|
||||
public void RegisterNotification(string uuid, Uri descriptionUri, IPAddress address, IEnumerable<string> services)
|
||||
@ -577,12 +591,27 @@ namespace MediaBrowser.Dlna.Ssdp
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void SendUnicastRequest(string request)
|
||||
private void SendUnicastRequest(string request, int sendCount = 3)
|
||||
{
|
||||
if (_unicastClient == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.Debug("Sending unicast search request");
|
||||
|
||||
var ipSsdp = IPAddress.Parse(SSDPAddr);
|
||||
var ipTxEnd = new IPEndPoint(ipSsdp, SSDPPort);
|
||||
|
||||
SendUnicastRequest(request, ipTxEnd, sendCount);
|
||||
}
|
||||
|
||||
private async void SendUnicastRequest(string request, IPEndPoint toEndPoint, int sendCount = 3)
|
||||
{
|
||||
if (_unicastClient == null)
|
||||
{
|
||||
@ -592,18 +621,16 @@ namespace MediaBrowser.Dlna.Ssdp
|
||||
_logger.Debug("Sending unicast search request");
|
||||
|
||||
byte[] req = Encoding.ASCII.GetBytes(request);
|
||||
var ipSsdp = IPAddress.Parse(SSDPAddr);
|
||||
var ipTxEnd = new IPEndPoint(ipSsdp, SSDPPort);
|
||||
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < 3; i++)
|
||||
for (var i = 0; i < sendCount; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
await Task.Delay(50).ConfigureAwait(false);
|
||||
}
|
||||
_unicastClient.Send(req, req.Length, ipTxEnd);
|
||||
_unicastClient.Send(req, req.Length, toEndPoint);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -794,6 +794,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
state.EstimateContentLength = transcodingProfile.EstimateContentLength;
|
||||
state.EnableMpegtsM2TsMode = transcodingProfile.EnableMpegtsM2TsMode;
|
||||
state.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
|
||||
|
||||
state.Options.CopyTimestamps = transcodingProfile.CopyTimestamps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -129,7 +129,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task<Model.MediaInfo.MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
|
||||
public Task<MediaInfo> GetMediaInfo(MediaInfoRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
|
||||
|
||||
@ -175,7 +175,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{MediaInfoResult}.</returns>
|
||||
/// <exception cref="System.ApplicationException">ffprobe failed - streams and format are both null.</exception>
|
||||
private async Task<Model.MediaInfo.MediaInfo> GetMediaInfoInternal(string inputPath,
|
||||
private async Task<MediaInfo> GetMediaInfoInternal(string inputPath,
|
||||
string primaryPath,
|
||||
MediaProtocol protocol,
|
||||
bool extractChapters,
|
||||
@ -934,7 +934,13 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
_mediaEncoder._runningProcesses.Remove(this);
|
||||
}
|
||||
|
||||
process.Dispose();
|
||||
try
|
||||
{
|
||||
process.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
|
@ -27,7 +27,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
|
||||
public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType videoType, bool isAudio, string path, MediaProtocol protocol)
|
||||
{
|
||||
var info = new Model.MediaInfo.MediaInfo
|
||||
var info = new MediaInfo
|
||||
{
|
||||
Path = path,
|
||||
Protocol = protocol
|
||||
@ -56,40 +56,81 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
}
|
||||
}
|
||||
|
||||
if (isAudio)
|
||||
var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var tagStreamType = isAudio ? "audio" : "video";
|
||||
|
||||
if (data.streams != null)
|
||||
{
|
||||
SetAudioRuntimeTicks(data, info);
|
||||
var tagStream = data.streams.FirstOrDefault(i => string.Equals(i.codec_type, tagStreamType, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// tags are normally located under data.format, but we've seen some cases with ogg where they're part of the audio stream
|
||||
// so let's create a combined list of both
|
||||
|
||||
if (data.streams != null)
|
||||
if (tagStream != null && tagStream.tags != null)
|
||||
{
|
||||
var audioStream = data.streams.FirstOrDefault(i => string.Equals(i.codec_type, "audio", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (audioStream != null && audioStream.tags != null)
|
||||
{
|
||||
foreach (var pair in audioStream.tags)
|
||||
{
|
||||
tags[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.format != null && data.format.tags != null)
|
||||
{
|
||||
foreach (var pair in data.format.tags)
|
||||
foreach (var pair in tagStream.tags)
|
||||
{
|
||||
tags[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.format != null && data.format.tags != null)
|
||||
{
|
||||
foreach (var pair in data.format.tags)
|
||||
{
|
||||
tags[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
FetchGenres(info, tags);
|
||||
var overview = FFProbeHelpers.GetDictionaryValue(tags, "description");
|
||||
if (!string.IsNullOrWhiteSpace(overview))
|
||||
{
|
||||
info.Overview = overview;
|
||||
}
|
||||
|
||||
var title = FFProbeHelpers.GetDictionaryValue(tags, "title");
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
info.Name = title;
|
||||
}
|
||||
|
||||
info.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
|
||||
|
||||
// Several different forms of retaildate
|
||||
info.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "date");
|
||||
|
||||
if (isAudio)
|
||||
{
|
||||
SetAudioRuntimeTicks(data, info);
|
||||
|
||||
// tags are normally located under data.format, but we've seen some cases with ogg where they're part of the audio stream
|
||||
// so let's create a combined list of both
|
||||
|
||||
SetAudioInfoFromTags(info, tags);
|
||||
}
|
||||
else
|
||||
{
|
||||
var iTunEXTC = FFProbeHelpers.GetDictionaryValue(tags, "iTunEXTC");
|
||||
if (!string.IsNullOrWhiteSpace(iTunEXTC))
|
||||
{
|
||||
var parts = iTunEXTC.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
// Example
|
||||
// mpaa|G|100|For crude humor
|
||||
if (parts.Length == 4)
|
||||
{
|
||||
info.OfficialRating = parts[1];
|
||||
info.OfficialRatingDescription = parts[3];
|
||||
}
|
||||
}
|
||||
|
||||
var itunesXml = FFProbeHelpers.GetDictionaryValue(tags, "iTunMOVI");
|
||||
if (!string.IsNullOrWhiteSpace(itunesXml))
|
||||
{
|
||||
FetchFromItunesInfo(itunesXml, info);
|
||||
}
|
||||
|
||||
if (data.format != null && !string.IsNullOrEmpty(data.format.duration))
|
||||
{
|
||||
info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration, _usCulture)).Ticks;
|
||||
@ -108,6 +149,11 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
return info;
|
||||
}
|
||||
|
||||
private void FetchFromItunesInfo(string xml, MediaInfo info)
|
||||
{
|
||||
// <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>cast</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>name</key>\n\t\t\t<string>Blender Foundation</string>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>name</key>\n\t\t\t<string>Janus Bager Kristensen</string>\n\t\t</dict>\n\t</array>\n\t<key>directors</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>name</key>\n\t\t\t<string>Sacha Goedegebure</string>\n\t\t</dict>\n\t</array>\n\t<key>studio</key>\n\t<string>Blender Foundation</string>\n</dict>\n</plist>\n
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts ffprobe stream info to our MediaStream class
|
||||
/// </summary>
|
||||
@ -430,16 +476,8 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
}
|
||||
}
|
||||
|
||||
private void SetAudioInfoFromTags(Model.MediaInfo.MediaInfo audio, Dictionary<string, string> tags)
|
||||
private void SetAudioInfoFromTags(MediaInfo audio, Dictionary<string, string> tags)
|
||||
{
|
||||
var title = FFProbeHelpers.GetDictionaryValue(tags, "title");
|
||||
|
||||
// Only set Name if title was found in the dictionary
|
||||
if (!string.IsNullOrEmpty(title))
|
||||
{
|
||||
audio.Title = title;
|
||||
}
|
||||
|
||||
var composer = FFProbeHelpers.GetDictionaryValue(tags, "composer");
|
||||
if (!string.IsNullOrWhiteSpace(composer))
|
||||
{
|
||||
@ -458,6 +496,26 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
}
|
||||
}
|
||||
|
||||
var lyricist = FFProbeHelpers.GetDictionaryValue(tags, "lyricist");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(lyricist))
|
||||
{
|
||||
foreach (var person in Split(lyricist, false))
|
||||
{
|
||||
audio.People.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist });
|
||||
}
|
||||
}
|
||||
// Check for writer some music is tagged that way as alternative to composer/lyricist
|
||||
var writer = FFProbeHelpers.GetDictionaryValue(tags, "writer");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(writer))
|
||||
{
|
||||
foreach (var person in Split(writer, false))
|
||||
{
|
||||
audio.People.Add(new BaseItemPerson { Name = person, Type = PersonType.Writer });
|
||||
}
|
||||
}
|
||||
|
||||
audio.Album = FFProbeHelpers.GetDictionaryValue(tags, "album");
|
||||
|
||||
var artists = FFProbeHelpers.GetDictionaryValue(tags, "artists");
|
||||
@ -511,22 +569,12 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
// Disc number
|
||||
audio.ParentIndexNumber = GetDictionaryDiscValue(tags, "disc");
|
||||
|
||||
audio.ProductionYear = FFProbeHelpers.GetDictionaryNumericValue(tags, "date");
|
||||
|
||||
// Several different forms of retaildate
|
||||
audio.PremiereDate = FFProbeHelpers.GetDictionaryDateTime(tags, "retaildate") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "retail date") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "retail_date") ??
|
||||
FFProbeHelpers.GetDictionaryDateTime(tags, "date");
|
||||
|
||||
// If we don't have a ProductionYear try and get it from PremiereDate
|
||||
if (audio.PremiereDate.HasValue && !audio.ProductionYear.HasValue)
|
||||
{
|
||||
audio.ProductionYear = audio.PremiereDate.Value.ToLocalTime().Year;
|
||||
}
|
||||
|
||||
FetchGenres(audio, tags);
|
||||
|
||||
// There's several values in tags may or may not be present
|
||||
FetchStudios(audio, tags, "organization");
|
||||
FetchStudios(audio, tags, "ensemble");
|
||||
@ -693,7 +741,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
/// </summary>
|
||||
/// <param name="info">The information.</param>
|
||||
/// <param name="tags">The tags.</param>
|
||||
private void FetchGenres(Model.MediaInfo.MediaInfo info, Dictionary<string, string> tags)
|
||||
private void FetchGenres(MediaInfo info, Dictionary<string, string> tags)
|
||||
{
|
||||
var val = FFProbeHelpers.GetDictionaryValue(tags, "genre");
|
||||
|
||||
@ -764,7 +812,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
|
||||
private const int MaxSubtitleDescriptionExtractionLength = 100; // When extracting subtitles, the maximum length to consider (to avoid invalid filenames)
|
||||
|
||||
private void FetchWtvInfo(Model.MediaInfo.MediaInfo video, InternalMediaInfoResult data)
|
||||
private void FetchWtvInfo(MediaInfo video, InternalMediaInfoResult data)
|
||||
{
|
||||
if (data.format == null || data.format.tags == null)
|
||||
{
|
||||
@ -775,15 +823,16 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(genres))
|
||||
{
|
||||
//genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "genre");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(genres))
|
||||
{
|
||||
video.Genres = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
var genreList = genres.Split(new[] { ';', '/', ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => i.Trim())
|
||||
.ToList();
|
||||
|
||||
// If this is empty then don't overwrite genres that might have been fetched earlier
|
||||
if (genreList.Count > 0)
|
||||
{
|
||||
video.Genres = genreList;
|
||||
}
|
||||
}
|
||||
|
||||
var officialRating = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/ParentalRating");
|
||||
|
@ -182,8 +182,6 @@ namespace MediaBrowser.Model.Configuration
|
||||
public PeopleMetadataOptions PeopleMetadataOptions { get; set; }
|
||||
public bool FindInternetTrailers { get; set; }
|
||||
|
||||
public string[] InsecureApps9 { get; set; }
|
||||
|
||||
public bool SaveMetadataHidden { get; set; }
|
||||
|
||||
public NameValuePair[] ContentTypes { get; set; }
|
||||
@ -256,11 +254,6 @@ namespace MediaBrowser.Model.Configuration
|
||||
|
||||
PeopleMetadataOptions = new PeopleMetadataOptions();
|
||||
|
||||
InsecureApps9 = new[]
|
||||
{
|
||||
"Windows Phone"
|
||||
};
|
||||
|
||||
MetadataOptions = new[]
|
||||
{
|
||||
new MetadataOptions(1, 1280) {ItemType = "Book"},
|
||||
|
@ -48,11 +48,19 @@ namespace MediaBrowser.Model.Configuration
|
||||
public bool HidePlayedInLatest { get; set; }
|
||||
public bool DisplayChannelsInline { get; set; }
|
||||
|
||||
public bool RememberAudioSelections { get; set; }
|
||||
public bool RememberSubtitleSelections { get; set; }
|
||||
public bool EnableEpisodeAutoQueue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserConfiguration" /> class.
|
||||
/// </summary>
|
||||
public UserConfiguration()
|
||||
{
|
||||
EnableEpisodeAutoQueue = true;
|
||||
RememberAudioSelections = true;
|
||||
RememberSubtitleSelections = true;
|
||||
|
||||
HidePlayedInLatest = true;
|
||||
PlayDefaultAudioTrack = true;
|
||||
|
||||
|
@ -425,6 +425,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
playlistItem.TranscodeSeekInfo = transcodingProfile.TranscodeSeekInfo;
|
||||
playlistItem.AudioCodec = transcodingProfile.AudioCodec.Split(',')[0];
|
||||
playlistItem.VideoCodec = transcodingProfile.VideoCodec;
|
||||
playlistItem.CopyTimestamps = transcodingProfile.CopyTimestamps;
|
||||
playlistItem.SubProtocol = transcodingProfile.Protocol;
|
||||
playlistItem.AudioStreamIndex = audioStreamIndex;
|
||||
|
||||
|
@ -32,6 +32,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
public string VideoProfile { get; set; }
|
||||
|
||||
public bool? Cabac { get; set; }
|
||||
public bool CopyTimestamps { get; set; }
|
||||
public string AudioCodec { get; set; }
|
||||
|
||||
public int? AudioStreamIndex { get; set; }
|
||||
@ -231,6 +232,8 @@ namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
list.Add(new NameValuePair("ItemId", item.ItemId));
|
||||
}
|
||||
|
||||
list.Add(new NameValuePair("CopyTimestamps", (item.CopyTimestamps).ToString().ToLower()));
|
||||
|
||||
return list;
|
||||
}
|
||||
@ -269,7 +272,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
// HLS will preserve timestamps so we can just grab the full subtitle stream
|
||||
long startPositionTicks = StringHelper.EqualsIgnoreCase(SubProtocol, "hls")
|
||||
? 0
|
||||
: (this.PlayMethod == PlayMethod.Transcode ? StartPositionTicks : 0);
|
||||
: (PlayMethod == PlayMethod.Transcode && !CopyTimestamps ? StartPositionTicks : 0);
|
||||
|
||||
// First add the selected track
|
||||
if (SubtitleStreamIndex.HasValue)
|
||||
|
@ -29,6 +29,9 @@ namespace MediaBrowser.Model.Dlna
|
||||
[XmlAttribute("transcodeSeekInfo")]
|
||||
public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
|
||||
|
||||
[XmlAttribute("copyTimestamps")]
|
||||
public bool CopyTimestamps { get; set; }
|
||||
|
||||
[XmlAttribute("context")]
|
||||
public EncodingContext Context { get; set; }
|
||||
|
||||
|
@ -34,5 +34,9 @@ namespace MediaBrowser.Model.Entities
|
||||
/// The conductor
|
||||
/// </summary>
|
||||
public const string Conductor = "Conductor";
|
||||
/// <summary>
|
||||
/// The lyricist
|
||||
/// </summary>
|
||||
public const string Lyricist = "Lyricist";
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,12 @@ namespace MediaBrowser.Model.LiveTv
|
||||
/// <value>The clients.</value>
|
||||
public List<string> Clients { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance can reset.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance can reset; otherwise, <c>false</c>.</value>
|
||||
public bool CanReset { get; set; }
|
||||
|
||||
public LiveTvTunerInfoDto()
|
||||
{
|
||||
Clients = new List<string>();
|
||||
|
@ -9,11 +9,6 @@ namespace MediaBrowser.Model.MediaInfo
|
||||
{
|
||||
public List<ChapterInfo> Chapters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
/// <value>The title.</value>
|
||||
public string Title { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the album.
|
||||
/// </summary>
|
||||
@ -47,6 +42,11 @@ namespace MediaBrowser.Model.MediaInfo
|
||||
/// <value>The official rating.</value>
|
||||
public string OfficialRating { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the official rating description.
|
||||
/// </summary>
|
||||
/// <value>The official rating description.</value>
|
||||
public string OfficialRatingDescription { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the overview.
|
||||
/// </summary>
|
||||
/// <value>The overview.</value>
|
||||
|
@ -125,9 +125,9 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
private async Task FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo data)
|
||||
{
|
||||
// Only set Name if title was found in the dictionary
|
||||
if (!string.IsNullOrEmpty(data.Title))
|
||||
if (!string.IsNullOrEmpty(data.Name))
|
||||
{
|
||||
audio.Name = data.Title;
|
||||
audio.Name = data.Name;
|
||||
}
|
||||
|
||||
if (!audio.LockedFields.Contains(MetadataFields.Cast))
|
||||
|
@ -383,6 +383,11 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(data.OfficialRatingDescription) || isFullRefresh)
|
||||
{
|
||||
video.OfficialRatingDescription = data.OfficialRatingDescription;
|
||||
}
|
||||
|
||||
if (!video.LockedFields.Contains(MetadataFields.Genres))
|
||||
{
|
||||
if (video.Genres.Count == 0 || isFullRefresh)
|
||||
@ -437,6 +442,13 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
video.ParentIndexNumber = data.ParentIndexNumber;
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(data.Name))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(video.Name) || string.Equals(video.Name, Path.GetFileNameWithoutExtension(video.Path), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
video.Name = data.Name;
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have a ProductionYear try and get it from PremiereDate
|
||||
if (video.PremiereDate.HasValue && !video.ProductionYear.HasValue)
|
||||
|
@ -10,7 +10,6 @@ using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Security;
|
||||
using MediaBrowser.Model.Connect;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
@ -24,7 +23,6 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommonIO;
|
||||
using MediaBrowser.Common.IO;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Connect
|
||||
{
|
||||
@ -121,7 +119,6 @@ namespace MediaBrowser.Server.Implementations.Connect
|
||||
_securityManager = securityManager;
|
||||
_fileSystem = fileSystem;
|
||||
|
||||
_userManager.UserConfigurationUpdated += _userManager_UserConfigurationUpdated;
|
||||
_config.ConfigurationUpdated += _config_ConfigurationUpdated;
|
||||
|
||||
LoadCachedData();
|
||||
@ -1071,90 +1068,6 @@ namespace MediaBrowser.Server.Implementations.Connect
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ConnectSupporterSummary> GetConnectSupporterSummary()
|
||||
{
|
||||
var url = GetConnectUrl("keyAssociation");
|
||||
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = CancellationToken.None
|
||||
};
|
||||
|
||||
var postData = new Dictionary<string, string>
|
||||
{
|
||||
{"serverId", ConnectServerId},
|
||||
{"supporterKey", _securityManager.SupporterKey}
|
||||
};
|
||||
|
||||
options.SetPostData(postData);
|
||||
|
||||
SetServerAccessToken(options);
|
||||
SetApplicationHeader(options);
|
||||
|
||||
// No need to examine the response
|
||||
using (var stream = (await _httpClient.SendAsync(options, "POST").ConfigureAwait(false)).Content)
|
||||
{
|
||||
return _json.DeserializeFromStream<ConnectSupporterSummary>(stream);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddConnectSupporter(string id)
|
||||
{
|
||||
var url = GetConnectUrl("keyAssociation");
|
||||
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = CancellationToken.None
|
||||
};
|
||||
|
||||
var postData = new Dictionary<string, string>
|
||||
{
|
||||
{"serverId", ConnectServerId},
|
||||
{"supporterKey", _securityManager.SupporterKey},
|
||||
{"userId", id}
|
||||
};
|
||||
|
||||
options.SetPostData(postData);
|
||||
|
||||
SetServerAccessToken(options);
|
||||
SetApplicationHeader(options);
|
||||
|
||||
// No need to examine the response
|
||||
using (var stream = (await _httpClient.SendAsync(options, "POST").ConfigureAwait(false)).Content)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveConnectSupporter(string id)
|
||||
{
|
||||
var url = GetConnectUrl("keyAssociation");
|
||||
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = CancellationToken.None
|
||||
};
|
||||
|
||||
var postData = new Dictionary<string, string>
|
||||
{
|
||||
{"serverId", ConnectServerId},
|
||||
{"supporterKey", _securityManager.SupporterKey},
|
||||
{"userId", id}
|
||||
};
|
||||
|
||||
options.SetPostData(postData);
|
||||
|
||||
SetServerAccessToken(options);
|
||||
SetApplicationHeader(options);
|
||||
|
||||
// No need to examine the response
|
||||
using (var stream = (await _httpClient.SendAsync(options, "DELETE").ConfigureAwait(false)).Content)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Authenticate(string username, string passwordMd5)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
@ -1186,64 +1099,6 @@ namespace MediaBrowser.Server.Implementations.Connect
|
||||
}
|
||||
}
|
||||
|
||||
async void _userManager_UserConfigurationUpdated(object sender, GenericEventArgs<User> e)
|
||||
{
|
||||
var user = e.Argument;
|
||||
|
||||
await TryUploadUserPreferences(user, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task TryUploadUserPreferences(User user, CancellationToken cancellationToken)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException("user");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(user.ConnectUserId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(ConnectAccessKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var url = GetConnectUrl("user/preferences");
|
||||
url += "?userId=" + user.ConnectUserId;
|
||||
url += "&key=userpreferences";
|
||||
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = cancellationToken
|
||||
};
|
||||
|
||||
var postData = new Dictionary<string, string>();
|
||||
postData["data"] = _json.SerializeToString(ConnectUserPreferences.FromUserConfiguration(user.Configuration));
|
||||
options.SetPostData(postData);
|
||||
|
||||
SetServerAccessToken(options);
|
||||
SetApplicationHeader(options);
|
||||
|
||||
try
|
||||
{
|
||||
// No need to examine the response
|
||||
using (var stream = (await _httpClient.SendAsync(options, "POST").ConfigureAwait(false)).Content)
|
||||
{
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error uploading user preferences", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadUserPreferences(User user, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public async Task<User> GetLocalUser(string connectUserId)
|
||||
{
|
||||
var user = _userManager.Users
|
||||
|
@ -639,6 +639,8 @@ namespace MediaBrowser.Server.Implementations.Dto
|
||||
private IEnumerable<string> GetCacheTags(BaseItem item, ImageType type, int limit)
|
||||
{
|
||||
return item.GetImages(type)
|
||||
// Convert to a list now in case GetImageCacheTag is slow
|
||||
.ToList()
|
||||
.Select(p => GetImageCacheTag(item, p))
|
||||
.Where(i => i != null)
|
||||
.Take(limit)
|
||||
|
@ -502,7 +502,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization
|
||||
}
|
||||
}
|
||||
|
||||
return series ?? new Series();
|
||||
return series;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -348,6 +348,12 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
if (string.Equals(localPath, "/emby/pin", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
httpRes.RedirectToUrl("web/pin.html");
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(GlobalResponse))
|
||||
{
|
||||
httpRes.StatusCode = 503;
|
||||
|
@ -134,20 +134,17 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security
|
||||
|
||||
private bool IsExemptFromAuthenticationToken(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues)
|
||||
{
|
||||
if (!_config.Configuration.IsStartupWizardCompleted &&
|
||||
authAttribtues.AllowBeforeStartupWizard)
|
||||
if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _config.Configuration.InsecureApps9.Contains(auth.Client ?? string.Empty,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsExemptFromRoles(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, AuthenticationInfo tokenInfo)
|
||||
{
|
||||
if (!_config.Configuration.IsStartupWizardCompleted &&
|
||||
authAttribtues.AllowBeforeStartupWizard)
|
||||
if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
@ -45,7 +45,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
var auth = GetAuthorizationDictionary(httpReq);
|
||||
|
||||
string userId = null;
|
||||
string deviceId = null;
|
||||
string device = null;
|
||||
string client = null;
|
||||
@ -53,9 +52,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security
|
||||
|
||||
if (auth != null)
|
||||
{
|
||||
// TODO: Remove this
|
||||
auth.TryGetValue("UserId", out userId);
|
||||
|
||||
auth.TryGetValue("DeviceId", out deviceId);
|
||||
auth.TryGetValue("Device", out device);
|
||||
auth.TryGetValue("Client", out client);
|
||||
@ -78,7 +74,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security
|
||||
Client = client,
|
||||
Device = device,
|
||||
DeviceId = deviceId,
|
||||
UserId = userId,
|
||||
Version = version,
|
||||
Token = token
|
||||
};
|
||||
|
@ -30,8 +30,9 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
|
||||
private IMediaSourceProvider[] _providers;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
|
||||
public MediaSourceManager(IItemRepository itemRepo, IUserManager userManager, ILibraryManager libraryManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem)
|
||||
public MediaSourceManager(IItemRepository itemRepo, IUserManager userManager, ILibraryManager libraryManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IUserDataManager userDataManager)
|
||||
{
|
||||
_itemRepo = itemRepo;
|
||||
_userManager = userManager;
|
||||
@ -39,6 +40,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
_logger = logger;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_fileSystem = fileSystem;
|
||||
_userDataManager = userDataManager;
|
||||
}
|
||||
|
||||
public void AddParts(IEnumerable<IMediaSourceProvider> providers)
|
||||
@ -140,7 +142,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
{
|
||||
if (user != null)
|
||||
{
|
||||
SetUserProperties(source, user);
|
||||
SetUserProperties(hasMediaSources, source, user);
|
||||
}
|
||||
if (source.Protocol == MediaProtocol.File)
|
||||
{
|
||||
@ -257,25 +259,38 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
{
|
||||
foreach (var source in sources)
|
||||
{
|
||||
SetUserProperties(source, user);
|
||||
SetUserProperties(item, source, user);
|
||||
}
|
||||
}
|
||||
|
||||
return sources;
|
||||
}
|
||||
|
||||
private void SetUserProperties(MediaSourceInfo source, User user)
|
||||
private void SetUserProperties(IHasUserData item, MediaSourceInfo source, User user)
|
||||
{
|
||||
var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference)
|
||||
? new string[] { }
|
||||
: new[] { user.Configuration.AudioLanguagePreference };
|
||||
var userData = item == null ? new UserItemData() : _userDataManager.GetUserData(user.Id, item.GetUserDataKey());
|
||||
|
||||
SetDefaultAudioStreamIndex(source, userData, user);
|
||||
SetDefaultSubtitleStreamIndex(source, userData, user);
|
||||
}
|
||||
|
||||
private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user)
|
||||
{
|
||||
if (userData.SubtitleStreamIndex.HasValue && user.Configuration.RememberSubtitleSelections)
|
||||
{
|
||||
var index = userData.SubtitleStreamIndex.Value;
|
||||
// Make sure the saved index is still valid
|
||||
if (index == -1 || source.MediaStreams.Any(i => i.Type == MediaStreamType.Subtitle && i.Index == index))
|
||||
{
|
||||
source.DefaultSubtitleStreamIndex = index;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference)
|
||||
? new List<string> { }
|
||||
: new List<string> { user.Configuration.SubtitleLanguagePreference };
|
||||
|
||||
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack);
|
||||
|
||||
var defaultAudioIndex = source.DefaultAudioStreamIndex;
|
||||
var audioLangage = defaultAudioIndex == null
|
||||
? null
|
||||
@ -290,6 +305,26 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
user.Configuration.SubtitleMode, audioLangage);
|
||||
}
|
||||
|
||||
private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user)
|
||||
{
|
||||
if (userData.AudioStreamIndex.HasValue && user.Configuration.RememberAudioSelections)
|
||||
{
|
||||
var index = userData.AudioStreamIndex.Value;
|
||||
// Make sure the saved index is still valid
|
||||
if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index))
|
||||
{
|
||||
source.DefaultAudioStreamIndex = index;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference)
|
||||
? new string[] { }
|
||||
: new[] { user.Configuration.AudioLanguagePreference };
|
||||
|
||||
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack);
|
||||
}
|
||||
|
||||
private IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
|
||||
{
|
||||
return sources.OrderBy(i =>
|
||||
@ -349,11 +384,14 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
var json = _jsonSerializer.SerializeToString(mediaSource);
|
||||
_logger.Debug("Live stream opened: " + json);
|
||||
var clone = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
|
||||
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.UserId))
|
||||
{
|
||||
var user = _userManager.GetUserById(request.UserId);
|
||||
SetUserProperties(clone, user);
|
||||
var item = string.IsNullOrWhiteSpace(request.ItemId)
|
||||
? null
|
||||
: _libraryManager.GetItemById(request.ItemId);
|
||||
SetUserProperties(item, clone, user);
|
||||
}
|
||||
|
||||
return new LiveStreamResponse
|
||||
|
@ -771,6 +771,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
recordPath = Path.ChangeExtension(recordPath, ".mp4");
|
||||
}
|
||||
|
||||
_libraryMonitor.ReportFileSystemChangeBeginning(recordPath);
|
||||
|
||||
recording.Path = recordPath;
|
||||
recording.Status = RecordingStatus.InProgress;
|
||||
recording.DateLastUpdated = DateTime.UtcNow;
|
||||
@ -801,6 +803,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
result.Item2.Release();
|
||||
}
|
||||
|
||||
_libraryMonitor.ReportFileSystemChangeComplete(recordPath, false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
|
@ -116,7 +116,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
videoArgs = "-codec:v:0 copy";
|
||||
}
|
||||
|
||||
var commandLineArgs = "-fflags +genpts -i \"{0}\" -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\"";
|
||||
var commandLineArgs = "-fflags +genpts -async 1 -vsync -1 -i \"{0}\" -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\"";
|
||||
|
||||
if (mediaSource.ReadAtNativeFramerate)
|
||||
{
|
||||
@ -143,7 +143,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
audioChannels = audioStream.Channels ?? audioChannels;
|
||||
}
|
||||
return "-codec:a:0 aac -strict experimental -ab 320000 -ac " + audioChannels.ToString(CultureInfo.InvariantCulture);
|
||||
return "-codec:a:0 aac -strict experimental -ab 320000";
|
||||
}
|
||||
|
||||
private bool EncodeVideo(MediaSourceInfo mediaSource)
|
||||
|
@ -178,7 +178,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
SourceType = info.SourceType,
|
||||
Status = info.Status,
|
||||
ChannelName = channelName,
|
||||
Url = info.Url
|
||||
Url = info.Url,
|
||||
CanReset = info.CanReset
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(info.ChannelId))
|
||||
|
@ -801,11 +801,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(info.ImagePath))
|
||||
{
|
||||
item.SetImagePath(ImageType.Primary, info.ImagePath);
|
||||
item.SetImage(new ItemImageInfo
|
||||
{
|
||||
Path = info.ImagePath,
|
||||
Type = ImageType.Primary,
|
||||
IsPlaceholder = true
|
||||
}, 0);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(info.ImageUrl))
|
||||
{
|
||||
item.SetImagePath(ImageType.Primary, info.ImageUrl);
|
||||
item.SetImage(new ItemImageInfo
|
||||
{
|
||||
Path = info.ImageUrl,
|
||||
Type = ImageType.Primary,
|
||||
IsPlaceholder = true
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2343,7 +2353,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
await provider.Validate(info).ConfigureAwait(false);
|
||||
var configurable = provider as IConfigurableTunerHost;
|
||||
if (configurable != null)
|
||||
{
|
||||
await configurable.Validate(info).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var config = GetConfiguration();
|
||||
|
||||
|
@ -64,7 +64,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
return list;
|
||||
}
|
||||
|
||||
private List<TunerHostInfo> GetTunerHosts()
|
||||
protected virtual List<TunerHostInfo> GetTunerHosts()
|
||||
{
|
||||
return GetConfiguration().TunerHosts
|
||||
.Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))
|
||||
|
@ -20,7 +20,7 @@ using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
public class HdHomerunHost : BaseTunerHost, ITunerHost
|
||||
public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
|
@ -8,19 +8,17 @@ using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommonIO;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
public class M3UTunerHost : BaseTunerHost, ITunerHost
|
||||
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IHttpClient _httpClient;
|
||||
@ -46,65 +44,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
|
||||
protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var urlHash = info.Url.GetMD5().ToString("N");
|
||||
|
||||
// Read the file and display it line by line.
|
||||
using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false)))
|
||||
{
|
||||
return GetChannels(reader, urlHash);
|
||||
}
|
||||
}
|
||||
|
||||
private List<M3UChannel> GetChannels(StreamReader reader, string urlHash)
|
||||
{
|
||||
var channels = new List<M3UChannel>();
|
||||
|
||||
string channnelName = null;
|
||||
string channelNumber = null;
|
||||
string line;
|
||||
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
line = line.Trim();
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("#EXTINF:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
line = line.Substring(8);
|
||||
Logger.Info("Found m3u channel: {0}", line);
|
||||
var parts = line.Split(new[] { ',' }, 2);
|
||||
channelNumber = parts[0];
|
||||
channnelName = parts[1];
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(channelNumber))
|
||||
{
|
||||
channels.Add(new M3UChannel
|
||||
{
|
||||
Name = channnelName,
|
||||
Number = channelNumber,
|
||||
Id = ChannelIdPrefix + urlHash + line.GetMD5().ToString("N"),
|
||||
Path = line
|
||||
});
|
||||
|
||||
channelNumber = null;
|
||||
channnelName = null;
|
||||
}
|
||||
}
|
||||
return channels;
|
||||
return await new M3uParser(Logger, _fileSystem, _httpClient).Parse(info.Url, ChannelIdPrefix, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
|
||||
{
|
||||
var list = GetConfiguration().TunerHosts
|
||||
.Where(i => i.IsEnabled && string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))
|
||||
var list = GetTunerHosts()
|
||||
.Select(i => new LiveTvTunerInfo()
|
||||
{
|
||||
Name = Name,
|
||||
@ -125,18 +70,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
return sources.First();
|
||||
}
|
||||
|
||||
class M3UChannel : ChannelInfo
|
||||
{
|
||||
public string Path { get; set; }
|
||||
|
||||
public M3UChannel()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Validate(TunerHostInfo info)
|
||||
{
|
||||
using (var stream = await GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
|
||||
using (var stream = await new M3uParser(Logger, _fileSystem, _httpClient).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
|
||||
{
|
||||
|
||||
}
|
||||
@ -147,15 +83,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _httpClient.Get(info.Url, cancellationToken);
|
||||
}
|
||||
return Task.FromResult(_fileSystem.OpenRead(info.Url));
|
||||
}
|
||||
|
||||
protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo info, string channelId, CancellationToken cancellationToken)
|
||||
{
|
||||
var urlHash = info.Url.GetMD5().ToString("N");
|
||||
|
@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommonIO;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Logging;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
public class M3uParser
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
public M3uParser(ILogger logger, IFileSystem fileSystem, IHttpClient httpClient)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<List<M3UChannel>> Parse(string url, string channelIdPrefix, CancellationToken cancellationToken)
|
||||
{
|
||||
var urlHash = url.GetMD5().ToString("N");
|
||||
|
||||
// Read the file and display it line by line.
|
||||
using (var reader = new StreamReader(await GetListingsStream(url, cancellationToken).ConfigureAwait(false)))
|
||||
{
|
||||
return GetChannels(reader, urlHash, channelIdPrefix);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Stream> GetListingsStream(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _httpClient.Get(url, cancellationToken);
|
||||
}
|
||||
return Task.FromResult(_fileSystem.OpenRead(url));
|
||||
}
|
||||
|
||||
private List<M3UChannel> GetChannels(StreamReader reader, string urlHash, string channelIdPrefix)
|
||||
{
|
||||
var channels = new List<M3UChannel>();
|
||||
|
||||
string channnelName = null;
|
||||
string channelNumber = null;
|
||||
string line;
|
||||
string imageUrl = null;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
line = line.Trim();
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("#EXTINF:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
line = line.Substring(8);
|
||||
_logger.Info("Found m3u channel: {0}", line);
|
||||
var parts = line.Split(new[] { ',' }, 2);
|
||||
channelNumber = parts[0].Trim().Split(' ')[0] ?? "0";
|
||||
channnelName = FindProperty("tvg-name", line, parts[1]);
|
||||
imageUrl = FindProperty("tvg-logo", line, null);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(channelNumber))
|
||||
{
|
||||
channels.Add(new M3UChannel
|
||||
{
|
||||
Name = channnelName,
|
||||
Number = channelNumber,
|
||||
Id = channelIdPrefix + urlHash + line.GetMD5().ToString("N"),
|
||||
ImageUrl = imageUrl
|
||||
});
|
||||
|
||||
imageUrl = null;
|
||||
channelNumber = null;
|
||||
channnelName = null;
|
||||
}
|
||||
}
|
||||
return channels;
|
||||
}
|
||||
public string FindProperty(string property, string properties, string defaultResult = "")
|
||||
{
|
||||
var reg = new Regex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase);
|
||||
var matches = reg.Matches(properties);
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
if (match.Groups[1].Value == property)
|
||||
{
|
||||
return match.Groups[2].Value;
|
||||
}
|
||||
}
|
||||
return defaultResult;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class M3UChannel : ChannelInfo
|
||||
{
|
||||
public string Path { get; set; }
|
||||
}
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
@ -13,6 +17,7 @@ using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
|
||||
{
|
||||
@ -24,14 +29,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
|
||||
private readonly ILiveTvManager _liveTvManager;
|
||||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IJsonSerializer _json;
|
||||
|
||||
public SatIpDiscovery(IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config, ILogger logger, ILiveTvManager liveTvManager, IHttpClient httpClient)
|
||||
public static SatIpDiscovery Current;
|
||||
|
||||
private readonly List<TunerHostInfo> _discoveredHosts = new List<TunerHostInfo>();
|
||||
|
||||
public List<TunerHostInfo> DiscoveredHosts
|
||||
{
|
||||
get { return _discoveredHosts.ToList(); }
|
||||
}
|
||||
|
||||
public SatIpDiscovery(IDeviceDiscovery deviceDiscovery, IServerConfigurationManager config, ILogger logger, ILiveTvManager liveTvManager, IHttpClient httpClient, IJsonSerializer json)
|
||||
{
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
_liveTvManager = liveTvManager;
|
||||
_httpClient = httpClient;
|
||||
_json = json;
|
||||
Current = this;
|
||||
}
|
||||
|
||||
public void Run()
|
||||
@ -42,7 +59,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
|
||||
void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e)
|
||||
{
|
||||
string st = null;
|
||||
if (e.Headers.TryGetValue("ST", out st) && string.Equals(st, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase))
|
||||
string nt = null;
|
||||
e.Headers.TryGetValue("ST", out st);
|
||||
e.Headers.TryGetValue("NT", out nt);
|
||||
|
||||
if (string.Equals(st, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(nt, "urn:ses-com:device:SatIPServer:1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string location;
|
||||
if (e.Headers.TryGetValue("Location", out location) && !string.IsNullOrWhiteSpace(location))
|
||||
@ -61,26 +83,23 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
|
||||
|
||||
try
|
||||
{
|
||||
var options = GetConfiguration();
|
||||
if (_discoveredHosts.Any(i => string.Equals(i.Type, SatIpHost.DeviceType, StringComparison.OrdinalIgnoreCase) && string.Equals(location, i.Url, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//if (options.TunerHosts.Any(i =>
|
||||
// string.Equals(i.Type, SatIpHost.DeviceType, StringComparison.OrdinalIgnoreCase) &&
|
||||
// UriEquals(i.Url, url)))
|
||||
//{
|
||||
// return;
|
||||
//}
|
||||
_logger.Debug("Will attempt to add SAT device {0}", location);
|
||||
var info = await GetInfo(location, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
//// Strip off the port
|
||||
//url = new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped).TrimEnd('/');
|
||||
_discoveredHosts.Add(info);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
//await TestUrl(url).ConfigureAwait(false);
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
|
||||
//await _liveTvManager.SaveTunerHost(new TunerHostInfo
|
||||
//{
|
||||
// Type = SatIpHost.DeviceType,
|
||||
// Url = url
|
||||
|
||||
//}).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -92,43 +111,141 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TestUrl(string url)
|
||||
{
|
||||
// Test it by pulling down the lineup
|
||||
using (await _httpClient.Get(new HttpRequestOptions
|
||||
{
|
||||
Url = string.Format("{0}/lineup.json", url),
|
||||
CancellationToken = CancellationToken.None
|
||||
}))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private bool UriEquals(string savedUri, string location)
|
||||
{
|
||||
return string.Equals(NormalizeUrl(location), NormalizeUrl(savedUri), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private string NormalizeUrl(string url)
|
||||
{
|
||||
if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
url = "http://" + url;
|
||||
}
|
||||
|
||||
url = url.TrimEnd('/');
|
||||
|
||||
// Strip off the port
|
||||
return new Uri(url).GetComponents(UriComponents.AbsoluteUri & ~UriComponents.Port, UriFormat.UriEscaped);
|
||||
}
|
||||
|
||||
private LiveTvOptions GetConfiguration()
|
||||
{
|
||||
return _config.GetConfiguration<LiveTvOptions>("livetv");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<SatIpTunerHostInfo> GetInfo(string url, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new SatIpTunerHostInfo
|
||||
{
|
||||
Url = url,
|
||||
IsEnabled = true,
|
||||
Type = SatIpHost.DeviceType,
|
||||
Tuners = 1,
|
||||
TunersAvailable = 1
|
||||
};
|
||||
|
||||
using (var stream = await _httpClient.Get(url, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
using (var streamReader = new StreamReader(stream))
|
||||
{
|
||||
// Use XmlReader for best performance
|
||||
using (var reader = XmlReader.Create(streamReader))
|
||||
{
|
||||
reader.MoveToContent();
|
||||
|
||||
// Loop through each element
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "device":
|
||||
using (var subtree = reader.ReadSubtree())
|
||||
{
|
||||
FillFromDeviceNode(result, subtree);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(result.Id))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
// Device hasn't implemented an m3u list
|
||||
if (string.IsNullOrWhiteSpace(result.M3UUrl))
|
||||
{
|
||||
result.IsEnabled = false;
|
||||
}
|
||||
|
||||
else if (!result.M3UUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var fullM3uUrl = url.Substring(0, url.LastIndexOf('/'));
|
||||
result.M3UUrl = fullM3uUrl + "/" + result.M3UUrl.TrimStart('/');
|
||||
}
|
||||
|
||||
_logger.Debug("SAT device result: {0}", _json.SerializeToString(result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void FillFromDeviceNode(SatIpTunerHostInfo info, XmlReader reader)
|
||||
{
|
||||
reader.MoveToContent();
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.LocalName)
|
||||
{
|
||||
case "UDN":
|
||||
{
|
||||
info.Id = reader.ReadElementContentAsString();
|
||||
break;
|
||||
}
|
||||
|
||||
case "friendlyName":
|
||||
{
|
||||
info.FriendlyName = reader.ReadElementContentAsString();
|
||||
break;
|
||||
}
|
||||
|
||||
case "satip:X_SATIPCAP":
|
||||
case "X_SATIPCAP":
|
||||
{
|
||||
// <satip:X_SATIPCAP xmlns:satip="urn:ses-com:satip">DVBS2-2</satip:X_SATIPCAP>
|
||||
var value = reader.ReadElementContentAsString() ?? string.Empty;
|
||||
var parts = value.Split(new[] { '-' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
int intValue;
|
||||
if (int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out intValue))
|
||||
{
|
||||
info.TunersAvailable = intValue;
|
||||
}
|
||||
|
||||
if (int.TryParse(parts[0].Substring(parts[0].Length - 1), NumberStyles.Any, CultureInfo.InvariantCulture, out intValue))
|
||||
{
|
||||
info.Tuners = intValue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "satip:X_SATIPM3U":
|
||||
case "X_SATIPM3U":
|
||||
{
|
||||
// <satip:X_SATIPM3U xmlns:satip="urn:ses-com:satip">/channellist.lua?select=m3u</satip:X_SATIPM3U>
|
||||
info.M3UUrl = reader.ReadElementContentAsString();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SatIpTunerHostInfo : TunerHostInfo
|
||||
{
|
||||
public int Tuners { get; set; }
|
||||
public int TunersAvailable { get; set; }
|
||||
public string M3UUrl { get; set; }
|
||||
public string FriendlyName { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +1,171 @@
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
|
||||
{
|
||||
public class SatIpHost /*: BaseTunerHost*/
|
||||
{
|
||||
//public SatIpHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder)
|
||||
// : base(config, logger, jsonSerializer, mediaEncoder)
|
||||
//{
|
||||
//}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommonIO;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
//protected override Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken)
|
||||
//{
|
||||
// throw new NotImplementedException();
|
||||
//}
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
|
||||
{
|
||||
public class SatIpHost : BaseTunerHost, ITunerHost
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
public SatIpHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient)
|
||||
: base(config, logger, jsonSerializer, mediaEncoder)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
private const string ChannelIdPrefix = "sat_";
|
||||
|
||||
protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken)
|
||||
{
|
||||
var satInfo = (SatIpTunerHostInfo) tuner;
|
||||
|
||||
return await new M3uParser(Logger, _fileSystem, _httpClient).Parse(satInfo.M3UUrl, ChannelIdPrefix, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static string DeviceType
|
||||
{
|
||||
get { return "satip"; }
|
||||
}
|
||||
|
||||
//public override string Type
|
||||
//{
|
||||
// get { return DeviceType; }
|
||||
//}
|
||||
public override string Type
|
||||
{
|
||||
get { return DeviceType; }
|
||||
}
|
||||
|
||||
//protected override Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
|
||||
//{
|
||||
// throw new NotImplementedException();
|
||||
//}
|
||||
protected override async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
|
||||
{
|
||||
var urlHash = tuner.Url.GetMD5().ToString("N");
|
||||
var prefix = ChannelIdPrefix + urlHash;
|
||||
if (!channelId.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
//protected override Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken)
|
||||
//{
|
||||
// throw new NotImplementedException();
|
||||
//}
|
||||
var channels = await GetChannels(tuner, true, cancellationToken).ConfigureAwait(false);
|
||||
var m3uchannels = channels.Cast<M3UChannel>();
|
||||
var channel = m3uchannels.FirstOrDefault(c => string.Equals(c.Id, channelId, StringComparison.OrdinalIgnoreCase));
|
||||
if (channel != null)
|
||||
{
|
||||
var path = channel.Path;
|
||||
MediaProtocol protocol = MediaProtocol.File;
|
||||
if (path.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
protocol = MediaProtocol.Http;
|
||||
}
|
||||
else if (path.StartsWith("rtmp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
protocol = MediaProtocol.Rtmp;
|
||||
}
|
||||
else if (path.StartsWith("rtsp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
protocol = MediaProtocol.Rtsp;
|
||||
}
|
||||
|
||||
//protected override Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
|
||||
//{
|
||||
// throw new NotImplementedException();
|
||||
//}
|
||||
var mediaSource = new MediaSourceInfo
|
||||
{
|
||||
Path = channel.Path,
|
||||
Protocol = protocol,
|
||||
MediaStreams = new List<MediaStream>
|
||||
{
|
||||
new MediaStream
|
||||
{
|
||||
Type = MediaStreamType.Video,
|
||||
// Set the index to -1 because we don't know the exact index of the video stream within the container
|
||||
Index = -1,
|
||||
IsInterlaced = true
|
||||
},
|
||||
new MediaStream
|
||||
{
|
||||
Type = MediaStreamType.Audio,
|
||||
// Set the index to -1 because we don't know the exact index of the audio stream within the container
|
||||
Index = -1
|
||||
|
||||
//protected override bool IsValidChannelId(string channelId)
|
||||
//{
|
||||
// throw new NotImplementedException();
|
||||
//}
|
||||
}
|
||||
},
|
||||
RequiresOpening = false,
|
||||
RequiresClosing = false
|
||||
};
|
||||
|
||||
return new List<MediaSourceInfo> { mediaSource };
|
||||
}
|
||||
return new List<MediaSourceInfo> { };
|
||||
}
|
||||
|
||||
protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken)
|
||||
{
|
||||
var sources = await GetChannelStreamMediaSources(tuner, channelId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return sources.First();
|
||||
}
|
||||
|
||||
protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
|
||||
{
|
||||
var updatedInfo = await SatIpDiscovery.Current.GetInfo(tuner.Url, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return updatedInfo.TunersAvailable > 0;
|
||||
}
|
||||
|
||||
protected override bool IsValidChannelId(string channelId)
|
||||
{
|
||||
return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
protected override List<TunerHostInfo> GetTunerHosts()
|
||||
{
|
||||
return SatIpDiscovery.Current.DiscoveredHosts;
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return "Sat IP"; }
|
||||
}
|
||||
|
||||
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
|
||||
{
|
||||
var list = GetTunerHosts()
|
||||
.SelectMany(i => GetTunerInfos(i, cancellationToken))
|
||||
.ToList();
|
||||
|
||||
return Task.FromResult(list);
|
||||
}
|
||||
|
||||
public List<LiveTvTunerInfo> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var satInfo = (SatIpTunerHostInfo) info;
|
||||
|
||||
var list = new List<LiveTvTunerInfo>();
|
||||
|
||||
for (var i = 0; i < satInfo.Tuners; i++)
|
||||
{
|
||||
list.Add(new LiveTvTunerInfo
|
||||
{
|
||||
Name = satInfo.FriendlyName ?? Name,
|
||||
SourceType = Type,
|
||||
Status = LiveTvTunerStatus.Available,
|
||||
Id = info.Url.GetMD5().ToString("N") + i.ToString(CultureInfo.InvariantCulture),
|
||||
Url = info.Url
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,9 +52,9 @@
|
||||
<Reference Include="Interfaces.IO">
|
||||
<HintPath>..\packages\Interfaces.IO.1.0.0.5\lib\portable-net45+sl4+wp71+win8+wpa81\Interfaces.IO.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="MediaBrowser.Naming, Version=1.0.5884.23751, Culture=neutral, processorArchitecture=MSIL">
|
||||
<Reference Include="MediaBrowser.Naming, Version=1.0.5891.29179, Culture=neutral, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\packages\MediaBrowser.Naming.1.0.0.47\lib\portable-net45+sl4+wp71+win8+wpa81\MediaBrowser.Naming.dll</HintPath>
|
||||
<HintPath>..\packages\MediaBrowser.Naming.1.0.0.48\lib\portable-net45+sl4+wp71+win8+wpa81\MediaBrowser.Naming.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="MoreLinq">
|
||||
<HintPath>..\packages\morelinq.1.4.0\lib\net35\MoreLinq.dll</HintPath>
|
||||
@ -234,6 +234,7 @@
|
||||
<Compile Include="LiveTv\TunerHosts\BaseTunerHost.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunHost.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunDiscovery.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\M3uParser.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\M3UTunerHost.cs" />
|
||||
<Compile Include="LiveTv\ProgramImageProvider.cs" />
|
||||
<Compile Include="LiveTv\RecordingImageProvider.cs" />
|
||||
|
@ -1014,7 +1014,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
|
||||
if (!reader.IsDBNull(31))
|
||||
{
|
||||
item.OfficialRating = reader.GetString(31);
|
||||
item.OfficialRatingDescription = reader.GetString(31);
|
||||
}
|
||||
|
||||
if (!reader.IsDBNull(32))
|
||||
|
@ -56,6 +56,9 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
};
|
||||
|
||||
_connection.RunQueries(queries, Logger);
|
||||
|
||||
_connection.AddColumn(Logger, "userdata", "AudioStreamIndex", "int");
|
||||
_connection.AddColumn(Logger, "userdata", "SubtitleStreamIndex", "int");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -127,7 +130,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
|
||||
using (var cmd = _connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = "replace into userdata (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate)";
|
||||
cmd.CommandText = "replace into userdata (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)";
|
||||
|
||||
cmd.Parameters.Add(cmd, "@key", DbType.String).Value = key;
|
||||
cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId;
|
||||
@ -137,6 +140,8 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
cmd.Parameters.Add(cmd, "@isFavorite", DbType.Boolean).Value = userData.IsFavorite;
|
||||
cmd.Parameters.Add(cmd, "@playbackPositionTicks", DbType.Int64).Value = userData.PlaybackPositionTicks;
|
||||
cmd.Parameters.Add(cmd, "@lastPlayedDate", DbType.DateTime).Value = userData.LastPlayedDate;
|
||||
cmd.Parameters.Add(cmd, "@AudioStreamIndex", DbType.Int32).Value = userData.AudioStreamIndex;
|
||||
cmd.Parameters.Add(cmd, "@SubtitleStreamIndex", DbType.Int32).Value = userData.SubtitleStreamIndex;
|
||||
|
||||
cmd.Transaction = transaction;
|
||||
|
||||
@ -199,7 +204,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
{
|
||||
using (var cmd = _connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = "replace into userdata (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate)";
|
||||
cmd.CommandText = "replace into userdata (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)";
|
||||
|
||||
cmd.Parameters.Add(cmd, "@key", DbType.String).Value = userItemData.Key;
|
||||
cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId;
|
||||
@ -209,6 +214,8 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
cmd.Parameters.Add(cmd, "@isFavorite", DbType.Boolean).Value = userItemData.IsFavorite;
|
||||
cmd.Parameters.Add(cmd, "@playbackPositionTicks", DbType.Int64).Value = userItemData.PlaybackPositionTicks;
|
||||
cmd.Parameters.Add(cmd, "@lastPlayedDate", DbType.DateTime).Value = userItemData.LastPlayedDate;
|
||||
cmd.Parameters.Add(cmd, "@AudioStreamIndex", DbType.Int32).Value = userItemData.AudioStreamIndex;
|
||||
cmd.Parameters.Add(cmd, "@SubtitleStreamIndex", DbType.Int32).Value = userItemData.SubtitleStreamIndex;
|
||||
|
||||
cmd.Transaction = transaction;
|
||||
|
||||
@ -275,7 +282,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
|
||||
using (var cmd = _connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate from userdata where key = @key and userId=@userId";
|
||||
cmd.CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where key = @key and userId=@userId";
|
||||
|
||||
cmd.Parameters.Add(cmd, "@key", DbType.String).Value = key;
|
||||
cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId;
|
||||
@ -310,7 +317,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
|
||||
using (var cmd = _connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate from userdata where userId=@userId";
|
||||
cmd.CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where userId=@userId";
|
||||
|
||||
cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId;
|
||||
|
||||
@ -350,6 +357,16 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
userData.LastPlayedDate = reader.GetDateTime(7).ToUniversalTime();
|
||||
}
|
||||
|
||||
if (!reader.IsDBNull(8))
|
||||
{
|
||||
userData.AudioStreamIndex = reader.GetInt32(8);
|
||||
}
|
||||
|
||||
if (!reader.IsDBNull(9))
|
||||
{
|
||||
userData.SubtitleStreamIndex = reader.GetInt32(9);
|
||||
}
|
||||
|
||||
return userData;
|
||||
}
|
||||
|
||||
|
@ -680,7 +680,7 @@ namespace MediaBrowser.Server.Implementations.Session
|
||||
|
||||
foreach (var user in users)
|
||||
{
|
||||
await OnPlaybackProgress(user.Id, key, libraryItem, info.PositionTicks).ConfigureAwait(false);
|
||||
await OnPlaybackProgress(user, key, libraryItem, info).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -712,15 +712,40 @@ namespace MediaBrowser.Server.Implementations.Session
|
||||
StartIdleCheckTimer();
|
||||
}
|
||||
|
||||
private async Task OnPlaybackProgress(Guid userId, string userDataKey, BaseItem item, long? positionTicks)
|
||||
private async Task OnPlaybackProgress(User user, string userDataKey, BaseItem item, PlaybackProgressInfo info)
|
||||
{
|
||||
var data = _userDataRepository.GetUserData(userId, userDataKey);
|
||||
var data = _userDataRepository.GetUserData(user.Id, userDataKey);
|
||||
|
||||
var positionTicks = info.PositionTicks;
|
||||
|
||||
if (positionTicks.HasValue)
|
||||
{
|
||||
_userDataRepository.UpdatePlayState(item, data, positionTicks.Value);
|
||||
|
||||
await _userDataRepository.SaveUserData(userId, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.None).ConfigureAwait(false);
|
||||
UpdatePlaybackSettings(user, info, data);
|
||||
|
||||
await _userDataRepository.SaveUserData(user.Id, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePlaybackSettings(User user, PlaybackProgressInfo info, UserItemData data)
|
||||
{
|
||||
if (user.Configuration.RememberAudioSelections)
|
||||
{
|
||||
data.AudioStreamIndex = info.AudioStreamIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
data.AudioStreamIndex = null;
|
||||
}
|
||||
|
||||
if (user.Configuration.RememberSubtitleSelections)
|
||||
{
|
||||
data.SubtitleStreamIndex = info.SubtitleStreamIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
data.SubtitleStreamIndex = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1268,7 +1293,17 @@ namespace MediaBrowser.Server.Implementations.Session
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <returns>Task{SessionInfo}.</returns>
|
||||
public async Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
|
||||
public Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request)
|
||||
{
|
||||
return AuthenticateNewSessionInternal(request, true);
|
||||
}
|
||||
|
||||
public Task<AuthenticationResult> CreateNewSession(AuthenticationRequest request)
|
||||
{
|
||||
return AuthenticateNewSessionInternal(request, false);
|
||||
}
|
||||
|
||||
private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)
|
||||
{
|
||||
var user = _userManager.Users
|
||||
.FirstOrDefault(i => string.Equals(request.Username, i.Name, StringComparison.OrdinalIgnoreCase));
|
||||
@ -1281,13 +1316,16 @@ namespace MediaBrowser.Server.Implementations.Session
|
||||
}
|
||||
}
|
||||
|
||||
var result = await _userManager.AuthenticateUser(request.Username, request.PasswordSha1, request.PasswordMd5, request.RemoteEndPoint).ConfigureAwait(false);
|
||||
|
||||
if (!result)
|
||||
if (enforcePassword)
|
||||
{
|
||||
EventHelper.FireEventIfNotNull(AuthenticationFailed, this, new GenericEventArgs<AuthenticationRequest>(request), _logger);
|
||||
var result = await _userManager.AuthenticateUser(request.Username, request.PasswordSha1, request.PasswordMd5, request.RemoteEndPoint).ConfigureAwait(false);
|
||||
|
||||
throw new SecurityException("Invalid user or password entered.");
|
||||
if (!result)
|
||||
{
|
||||
EventHelper.FireEventIfNotNull(AuthenticationFailed, this, new GenericEventArgs<AuthenticationRequest>(request), _logger);
|
||||
|
||||
throw new SecurityException("Invalid user or password entered.");
|
||||
}
|
||||
}
|
||||
|
||||
var token = await GetAuthorizationToken(user.Id.ToString("N"), request.DeviceId, request.App, request.AppVersion, request.DeviceName).ConfigureAwait(false);
|
||||
@ -1310,7 +1348,8 @@ namespace MediaBrowser.Server.Implementations.Session
|
||||
ServerId = _appHost.SystemId
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async Task<string> GetAuthorizationToken(string userId, string deviceId, string app, string appVersion, string deviceName)
|
||||
{
|
||||
var existing = _authRepo.Get(new AuthenticationInfoQuery
|
||||
|
@ -19,6 +19,7 @@ using MediaBrowser.Model.Sync;
|
||||
using MoreLinq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@ -125,7 +126,23 @@ namespace MediaBrowser.Server.Implementations.Sync
|
||||
|
||||
private string GetSyncJobItemName(BaseItem item)
|
||||
{
|
||||
return item.Name;
|
||||
var name = item.Name;
|
||||
var episode = item as Episode;
|
||||
|
||||
if (episode != null)
|
||||
{
|
||||
if (episode.IndexNumber.HasValue)
|
||||
{
|
||||
name = "E" + episode.IndexNumber.Value.ToString(CultureInfo.InvariantCulture) + " - " + name;
|
||||
}
|
||||
|
||||
if (episode.ParentIndexNumber.HasValue)
|
||||
{
|
||||
name = "S" + episode.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture) + ", " + name;
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
public Task UpdateJobStatus(string id)
|
||||
@ -699,7 +716,7 @@ namespace MediaBrowser.Server.Implementations.Sync
|
||||
|
||||
var path = Path.Combine(temporaryPath, filename);
|
||||
|
||||
_fileSystem.CreateDirectory(Path.GetDirectoryName(path));
|
||||
_fileSystem.CreateDirectory(Path.GetDirectoryName(path));
|
||||
|
||||
using (var stream = await _subtitleEncoder.GetSubtitles(streamInfo.ItemId, streamInfo.MediaSourceId, subtitleStreamIndex, subtitleStreamInfo.Format, 0, null, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
|
@ -3,7 +3,7 @@
|
||||
<package id="CommonIO" version="1.0.0.7" targetFramework="net45" />
|
||||
<package id="Emby.XmlTv" version="1.0.0.48" targetFramework="net45" />
|
||||
<package id="Interfaces.IO" version="1.0.0.5" targetFramework="net45" />
|
||||
<package id="MediaBrowser.Naming" version="1.0.0.47" targetFramework="net45" />
|
||||
<package id="MediaBrowser.Naming" version="1.0.0.48" targetFramework="net45" />
|
||||
<package id="Mono.Nat" version="1.2.24.0" targetFramework="net45" />
|
||||
<package id="morelinq" version="1.4.0" targetFramework="net45" />
|
||||
<package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" />
|
||||
|
@ -490,7 +490,7 @@ namespace MediaBrowser.Server.Startup.Common
|
||||
ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LogManager.GetLogger("ChannelManager"), ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, LocalizationManager, HttpClient, ProviderManager);
|
||||
RegisterSingleInstance(ChannelManager);
|
||||
|
||||
MediaSourceManager = new MediaSourceManager(ItemRepository, UserManager, LibraryManager, LogManager.GetLogger("MediaSourceManager"), JsonSerializer, FileSystemManager);
|
||||
MediaSourceManager = new MediaSourceManager(ItemRepository, UserManager, LibraryManager, LogManager.GetLogger("MediaSourceManager"), JsonSerializer, FileSystemManager, UserDataManager);
|
||||
RegisterSingleInstance(MediaSourceManager);
|
||||
|
||||
SessionManager = new SessionManager(UserDataManager, LogManager.GetLogger("SessionManager"), UserRepository, LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, JsonSerializer, this, HttpClient, AuthenticationRepository, DeviceManager, MediaSourceManager);
|
||||
|
@ -140,6 +140,9 @@
|
||||
<Content Include="dashboard-ui\components\remotecontrolautoplay.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="dashboard-ui\devices\windowsphone\wp.css">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="dashboard-ui\legacy\buttonenabled.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
@ -254,9 +257,6 @@
|
||||
<Content Include="dashboard-ui\favorites.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Include="dashboard-ui\legacy\deferred.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<Content Include="dashboard-ui\livetvguideprovider.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
@ -275,10 +275,10 @@
|
||||
<Content Include="dashboard-ui\mysyncsettings.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="dashboard-ui\robots.txt">
|
||||
<Content Include="dashboard-ui\pin.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="dashboard-ui\scripts\actionsheet.js">
|
||||
<Content Include="dashboard-ui\robots.txt">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="dashboard-ui\scripts\globalize.js">
|
||||
@ -317,6 +317,9 @@
|
||||
<Content Include="dashboard-ui\scripts\autoorganizesmart.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="dashboard-ui\scripts\pin.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="dashboard-ui\scripts\searchmenu.js">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
Loading…
Reference in New Issue
Block a user