mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-03-17 23:56:44 +00:00
@@ -30,12 +30,12 @@ namespace MediaBrowser.Server.Implementations.Channels
|
||||
return Task.FromResult<IEnumerable<MediaSourceInfo>>(new List<MediaSourceInfo>());
|
||||
}
|
||||
|
||||
public Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
|
||||
public Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> OpenMediaSource(string openToken, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken)
|
||||
public Task CloseMediaSource(string liveStreamId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -1577,97 +1577,5 @@ namespace MediaBrowser.Server.Implementations.Channels
|
||||
|
||||
return await _libraryManager.GetNamedView(name, "channels", "zz_" + name, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task DownloadChannelItem(BaseItem item, string destination,
|
||||
IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var sources = await GetDynamicMediaSources(item, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var list = sources.Where(i => i.Protocol == MediaProtocol.Http).ToList();
|
||||
|
||||
foreach (var source in list)
|
||||
{
|
||||
await TryDownloadChannelItem(source, item, destination, progress, cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TryDownloadChannelItem(MediaSourceInfo source,
|
||||
BaseItem item,
|
||||
string destination,
|
||||
IProgress<double> progress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
CancellationToken = cancellationToken,
|
||||
Url = source.Path,
|
||||
Progress = new Progress<double>()
|
||||
};
|
||||
|
||||
var channel = GetChannel(item.ChannelId);
|
||||
var channelProvider = GetChannelProvider(channel);
|
||||
var features = channelProvider.GetChannelFeatures();
|
||||
|
||||
if (!features.SupportsContentDownloading)
|
||||
{
|
||||
throw new ArgumentException("The channel does not support downloading.");
|
||||
}
|
||||
|
||||
var limit = features.DailyDownloadLimit;
|
||||
|
||||
foreach (var header in source.RequiredHttpHeaders)
|
||||
{
|
||||
options.RequestHeaders[header.Key] = header.Value;
|
||||
}
|
||||
|
||||
_fileSystem.CreateDirectory(Path.GetDirectoryName(destination));
|
||||
|
||||
// Determine output extension
|
||||
var response = await _httpClient.GetTempFileResponse(options).ConfigureAwait(false);
|
||||
|
||||
if (response.ContentType.StartsWith("text/html"))
|
||||
{
|
||||
throw new HttpException("File not found")
|
||||
{
|
||||
StatusCode = HttpStatusCode.NotFound
|
||||
};
|
||||
}
|
||||
|
||||
if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase) && response.ContentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var extension = response.ContentType.Split('/')
|
||||
.Last()
|
||||
.Replace("quicktime", "mov", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
destination += "." + extension;
|
||||
}
|
||||
else if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) && response.ContentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var extension = response.ContentType.Replace("audio/mpeg", "audio/mp3", StringComparison.OrdinalIgnoreCase)
|
||||
.Split('/')
|
||||
.Last();
|
||||
|
||||
destination += "." + extension;
|
||||
}
|
||||
else
|
||||
{
|
||||
_fileSystem.DeleteFile(response.TempFilePath);
|
||||
|
||||
throw new ApplicationException("Unexpected response type encountered: " + response.ContentType);
|
||||
}
|
||||
|
||||
_fileSystem.CopyFile(response.TempFilePath, destination, true);
|
||||
|
||||
try
|
||||
{
|
||||
_fileSystem.DeleteFile(response.TempFilePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ namespace MediaBrowser.Server.Implementations.Channels
|
||||
|
||||
public string Category
|
||||
{
|
||||
get { return "Channels"; }
|
||||
get { return "Internet Channels"; }
|
||||
}
|
||||
|
||||
public async Task Execute(System.Threading.CancellationToken cancellationToken, IProgress<double> progress)
|
||||
|
||||
@@ -219,7 +219,9 @@ namespace MediaBrowser.Server.Implementations.Collections
|
||||
|
||||
foreach (var itemId in itemIds)
|
||||
{
|
||||
var child = collection.LinkedChildren.FirstOrDefault(i => i.ItemId.HasValue && i.ItemId.Value == itemId);
|
||||
var childItem = _libraryManager.GetItemById(itemId);
|
||||
|
||||
var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value == itemId) || (childItem != null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
if (child == null)
|
||||
{
|
||||
@@ -228,47 +230,15 @@ namespace MediaBrowser.Server.Implementations.Collections
|
||||
|
||||
list.Add(child);
|
||||
|
||||
var childItem = _libraryManager.GetItemById(itemId);
|
||||
|
||||
if (childItem != null)
|
||||
{
|
||||
itemList.Add(childItem);
|
||||
}
|
||||
}
|
||||
|
||||
var shortcutFiles = _fileSystem
|
||||
.GetFilePaths(collection.Path)
|
||||
.Where(i => _fileSystem.IsShortcut(i))
|
||||
.ToList();
|
||||
|
||||
var shortcutFilesToDelete = list.Where(child => !string.IsNullOrWhiteSpace(child.Path) && child.Type == LinkedChildType.Shortcut)
|
||||
.Select(child => shortcutFiles.FirstOrDefault(i => string.Equals(child.Path, _fileSystem.ResolveShortcut(i), StringComparison.OrdinalIgnoreCase)))
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.ToList();
|
||||
|
||||
foreach (var file in shortcutFilesToDelete)
|
||||
foreach (var child in list)
|
||||
{
|
||||
_iLibraryMonitor.ReportFileSystemChangeBeginning(file);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var file in shortcutFilesToDelete)
|
||||
{
|
||||
_fileSystem.DeleteFile(file);
|
||||
}
|
||||
|
||||
foreach (var child in list)
|
||||
{
|
||||
collection.LinkedChildren.Remove(child);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var file in shortcutFilesToDelete)
|
||||
{
|
||||
_iLibraryMonitor.ReportFileSystemChangeComplete(file, false);
|
||||
}
|
||||
collection.LinkedChildren.Remove(child);
|
||||
}
|
||||
|
||||
collection.UpdateRatingToContent();
|
||||
|
||||
@@ -127,7 +127,8 @@ namespace MediaBrowser.Server.Implementations.Connect
|
||||
|
||||
// Seeing block length errors with our server
|
||||
EnableHttpCompression = false,
|
||||
PreferIpv4 = preferIpv4
|
||||
PreferIpv4 = preferIpv4,
|
||||
BufferContent = false
|
||||
|
||||
}).ConfigureAwait(false))
|
||||
{
|
||||
|
||||
@@ -266,7 +266,8 @@ namespace MediaBrowser.Server.Implementations.Connect
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = CancellationToken.None
|
||||
CancellationToken = CancellationToken.None,
|
||||
BufferContent = false
|
||||
};
|
||||
|
||||
options.SetPostData(postData);
|
||||
@@ -314,7 +315,8 @@ namespace MediaBrowser.Server.Implementations.Connect
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = CancellationToken.None
|
||||
CancellationToken = CancellationToken.None,
|
||||
BufferContent = false
|
||||
};
|
||||
|
||||
options.SetPostData(postData);
|
||||
@@ -403,6 +405,14 @@ namespace MediaBrowser.Server.Implementations.Connect
|
||||
|
||||
public async Task<UserLinkResult> LinkUser(string userId, string connectUsername)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
throw new ArgumentNullException("userId");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(connectUsername))
|
||||
{
|
||||
throw new ArgumentNullException("connectUsername");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(ConnectServerId))
|
||||
{
|
||||
await UpdateConnectInfo().ConfigureAwait(false);
|
||||
@@ -422,14 +432,6 @@ namespace MediaBrowser.Server.Implementations.Connect
|
||||
|
||||
private async Task<UserLinkResult> LinkUserInternal(string userId, string connectUsername)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
throw new ArgumentNullException("userId");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(connectUsername))
|
||||
{
|
||||
throw new ArgumentNullException("connectUsername");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(ConnectServerId))
|
||||
{
|
||||
throw new ArgumentNullException("ConnectServerId");
|
||||
@@ -446,11 +448,17 @@ namespace MediaBrowser.Server.Implementations.Connect
|
||||
throw new ArgumentException("The Emby account has been disabled.");
|
||||
}
|
||||
|
||||
var existingUser = _userManager.Users.FirstOrDefault(i => string.Equals(i.ConnectUserId, connectUser.Id) && !string.IsNullOrWhiteSpace(i.ConnectAccessKey));
|
||||
if (existingUser != null)
|
||||
{
|
||||
throw new InvalidOperationException("This connect user is already linked to local user " + existingUser.Name);
|
||||
}
|
||||
|
||||
var user = GetUser(userId);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(user.ConnectUserId))
|
||||
{
|
||||
await RemoveConnect(user, connectUser.Id).ConfigureAwait(false);
|
||||
await RemoveConnect(user, user.ConnectUserId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var url = GetConnectUrl("ServerAuthorizations");
|
||||
@@ -458,7 +466,8 @@ namespace MediaBrowser.Server.Implementations.Connect
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = CancellationToken.None
|
||||
CancellationToken = CancellationToken.None,
|
||||
BufferContent = false
|
||||
};
|
||||
|
||||
var accessToken = Guid.NewGuid().ToString("N");
|
||||
@@ -593,7 +602,8 @@ namespace MediaBrowser.Server.Implementations.Connect
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = CancellationToken.None
|
||||
CancellationToken = CancellationToken.None,
|
||||
BufferContent = false
|
||||
};
|
||||
|
||||
var accessToken = Guid.NewGuid().ToString("N");
|
||||
@@ -646,7 +656,8 @@ namespace MediaBrowser.Server.Implementations.Connect
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = CancellationToken.None
|
||||
CancellationToken = CancellationToken.None,
|
||||
BufferContent = false
|
||||
};
|
||||
|
||||
var postData = new Dictionary<string, string>
|
||||
@@ -720,7 +731,8 @@ namespace MediaBrowser.Server.Implementations.Connect
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
CancellationToken = cancellationToken,
|
||||
Url = url
|
||||
Url = url,
|
||||
BufferContent = false
|
||||
};
|
||||
|
||||
SetServerAccessToken(options);
|
||||
@@ -784,7 +796,8 @@ namespace MediaBrowser.Server.Implementations.Connect
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = cancellationToken
|
||||
CancellationToken = cancellationToken,
|
||||
BufferContent = false
|
||||
};
|
||||
|
||||
SetServerAccessToken(options);
|
||||
@@ -1072,7 +1085,8 @@ namespace MediaBrowser.Server.Implementations.Connect
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = CancellationToken.None
|
||||
CancellationToken = CancellationToken.None,
|
||||
BufferContent = false
|
||||
};
|
||||
|
||||
var postData = new Dictionary<string, string>
|
||||
@@ -1120,7 +1134,8 @@ namespace MediaBrowser.Server.Implementations.Connect
|
||||
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = GetConnectUrl("user/authenticate")
|
||||
Url = GetConnectUrl("user/authenticate"),
|
||||
BufferContent = false
|
||||
};
|
||||
|
||||
options.SetPostData(new Dictionary<string, string>
|
||||
|
||||
@@ -437,6 +437,7 @@ namespace MediaBrowser.Server.Implementations.Dto
|
||||
dto.TrailerCount = taggedItems.Count(i => i is Trailer);
|
||||
dto.MusicVideoCount = taggedItems.Count(i => i is MusicVideo);
|
||||
dto.SeriesCount = taggedItems.Count(i => i is Series);
|
||||
dto.ProgramCount = taggedItems.Count(i => i is LiveTvProgram);
|
||||
dto.SongCount = taggedItems.Count(i => i is Audio);
|
||||
}
|
||||
|
||||
@@ -597,7 +598,7 @@ namespace MediaBrowser.Server.Implementations.Dto
|
||||
dto.Altitude = item.Altitude;
|
||||
dto.IsoSpeedRating = item.IsoSpeedRating;
|
||||
|
||||
var album = item.Album;
|
||||
var album = item.AlbumEntity;
|
||||
|
||||
if (album != null)
|
||||
{
|
||||
@@ -906,11 +907,6 @@ namespace MediaBrowser.Server.Implementations.Dto
|
||||
dto.Keywords = item.Keywords;
|
||||
}
|
||||
|
||||
if (fields.Contains(ItemFields.ProductionLocations))
|
||||
{
|
||||
SetProductionLocations(item, dto);
|
||||
}
|
||||
|
||||
var hasAspectRatio = item as IHasAspectRatio;
|
||||
if (hasAspectRatio != null)
|
||||
{
|
||||
@@ -997,8 +993,11 @@ namespace MediaBrowser.Server.Implementations.Dto
|
||||
}
|
||||
dto.Audio = item.Audio;
|
||||
|
||||
dto.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
|
||||
dto.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
|
||||
if (fields.Contains(ItemFields.Settings))
|
||||
{
|
||||
dto.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
|
||||
dto.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
|
||||
}
|
||||
|
||||
dto.CriticRating = item.CriticRating;
|
||||
|
||||
@@ -1088,10 +1087,9 @@ namespace MediaBrowser.Server.Implementations.Dto
|
||||
|
||||
if (fields.Contains(ItemFields.Taglines))
|
||||
{
|
||||
var hasTagline = item as IHasTaglines;
|
||||
if (hasTagline != null)
|
||||
if (!string.IsNullOrWhiteSpace(item.Tagline))
|
||||
{
|
||||
dto.Taglines = hasTagline.Taglines;
|
||||
dto.Taglines = new List<string> { item.Tagline };
|
||||
}
|
||||
|
||||
if (dto.Taglines == null)
|
||||
@@ -1175,6 +1173,12 @@ namespace MediaBrowser.Server.Implementations.Dto
|
||||
.Except(foundArtists, new DistinctNameComparer())
|
||||
.Select(i =>
|
||||
{
|
||||
// This should not be necessary but we're seeing some cases of it
|
||||
if (string.IsNullOrWhiteSpace(i))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var artist = _libraryManager.GetArtist(i);
|
||||
if (artist != null)
|
||||
{
|
||||
@@ -1419,6 +1423,11 @@ namespace MediaBrowser.Server.Implementations.Dto
|
||||
SetBookProperties(dto, book);
|
||||
}
|
||||
|
||||
if (item.ProductionLocations.Count > 0 || item is Movie)
|
||||
{
|
||||
dto.ProductionLocations = item.ProductionLocations.ToArray();
|
||||
}
|
||||
|
||||
var photo = item as Photo;
|
||||
if (photo != null)
|
||||
{
|
||||
@@ -1508,7 +1517,7 @@ namespace MediaBrowser.Server.Implementations.Dto
|
||||
}
|
||||
}
|
||||
|
||||
private string GetMappedPath(IHasMetadata item)
|
||||
private string GetMappedPath(BaseItem item)
|
||||
{
|
||||
var path = item.Path;
|
||||
|
||||
@@ -1516,40 +1525,12 @@ namespace MediaBrowser.Server.Implementations.Dto
|
||||
|
||||
if (locationType == LocationType.FileSystem || locationType == LocationType.Offline)
|
||||
{
|
||||
foreach (var map in _config.Configuration.PathSubstitutions)
|
||||
{
|
||||
path = _libraryManager.SubstitutePath(path, map.From, map.To);
|
||||
}
|
||||
path = _libraryManager.GetPathAfterNetworkSubstitution(path, item);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private void SetProductionLocations(BaseItem item, BaseItemDto dto)
|
||||
{
|
||||
var hasProductionLocations = item as IHasProductionLocations;
|
||||
|
||||
if (hasProductionLocations != null)
|
||||
{
|
||||
dto.ProductionLocations = hasProductionLocations.ProductionLocations;
|
||||
}
|
||||
|
||||
var person = item as Person;
|
||||
if (person != null)
|
||||
{
|
||||
dto.ProductionLocations = new List<string>();
|
||||
if (!string.IsNullOrEmpty(person.PlaceOfBirth))
|
||||
{
|
||||
dto.ProductionLocations.Add(person.PlaceOfBirth);
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.ProductionLocations == null)
|
||||
{
|
||||
dto.ProductionLocations = new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches the primary image aspect ratio.
|
||||
/// </summary>
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using MediaBrowser.Common.Threading;
|
||||
using MediaBrowser.Model.Events;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.EntryPoints
|
||||
{
|
||||
@@ -17,17 +18,17 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly ISsdpHandler _ssdp;
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
|
||||
private PeriodicTimer _timer;
|
||||
private bool _isStarted;
|
||||
|
||||
public ExternalPortForwarding(ILogManager logmanager, IServerApplicationHost appHost, IServerConfigurationManager config, ISsdpHandler ssdp)
|
||||
public ExternalPortForwarding(ILogManager logmanager, IServerApplicationHost appHost, IServerConfigurationManager config, IDeviceDiscovery deviceDiscovery)
|
||||
{
|
||||
_logger = logmanager.GetLogger("PortMapper");
|
||||
_appHost = appHost;
|
||||
_config = config;
|
||||
_ssdp = ssdp;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
}
|
||||
|
||||
private string _lastConfigIdentifier;
|
||||
@@ -61,7 +62,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
|
||||
|
||||
public void Run()
|
||||
{
|
||||
//NatUtility.Logger = new LogWriter(_logger);
|
||||
NatUtility.Logger = _logger;
|
||||
|
||||
if (_config.Configuration.EnableUPnP)
|
||||
{
|
||||
@@ -93,33 +94,22 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
|
||||
|
||||
_timer = new PeriodicTimer(ClearCreatedRules, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
|
||||
|
||||
_ssdp.MessageReceived += _ssdp_MessageReceived;
|
||||
_deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered;
|
||||
|
||||
_lastConfigIdentifier = GetConfigIdentifier();
|
||||
|
||||
_isStarted = true;
|
||||
}
|
||||
|
||||
private void ClearCreatedRules(object state)
|
||||
private async void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
{
|
||||
_createdRules = new List<string>();
|
||||
_usnsHandled = new List<string>();
|
||||
}
|
||||
|
||||
void _ssdp_MessageReceived(object sender, SsdpMessageEventArgs e)
|
||||
{
|
||||
var endpoint = e.EndPoint as IPEndPoint;
|
||||
|
||||
if (endpoint == null || e.LocalEndPoint == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var info = e.Argument;
|
||||
|
||||
string usn;
|
||||
if (!e.Headers.TryGetValue("USN", out usn)) usn = string.Empty;
|
||||
if (!info.Headers.TryGetValue("USN", out usn)) usn = string.Empty;
|
||||
|
||||
string nt;
|
||||
if (!e.Headers.TryGetValue("NT", out nt)) nt = string.Empty;
|
||||
if (!info.Headers.TryGetValue("NT", out nt)) nt = string.Empty;
|
||||
|
||||
// Filter device type
|
||||
if (usn.IndexOf("WANIPConnection:", StringComparison.OrdinalIgnoreCase) == -1 &&
|
||||
@@ -132,15 +122,45 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
|
||||
|
||||
var identifier = string.IsNullOrWhiteSpace(usn) ? nt : usn;
|
||||
|
||||
if (!_usnsHandled.Contains(identifier))
|
||||
if (info.Location != null && !_usnsHandled.Contains(identifier))
|
||||
{
|
||||
_usnsHandled.Add(identifier);
|
||||
|
||||
_logger.Debug("Calling Nat.Handle on " + identifier);
|
||||
NatUtility.Handle(e.LocalEndPoint.Address, e.Message, endpoint, NatProtocol.Upnp);
|
||||
|
||||
IPAddress address;
|
||||
if (IPAddress.TryParse(info.Location.Host, out address))
|
||||
{
|
||||
// The Handle method doesn't need the port
|
||||
var endpoint = new IPEndPoint(address, info.Location.Port);
|
||||
|
||||
IPAddress localAddress = null;
|
||||
|
||||
try
|
||||
{
|
||||
var localAddressString = await _appHost.GetLocalApiUrl().ConfigureAwait(false);
|
||||
|
||||
if (!IPAddress.TryParse(localAddressString, out localAddress))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NatUtility.Handle(localAddress, info, endpoint, NatProtocol.Upnp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearCreatedRules(object state)
|
||||
{
|
||||
_createdRules = new List<string>();
|
||||
_usnsHandled = new List<string>();
|
||||
}
|
||||
|
||||
void NatUtility_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
var ex = e.ExceptionObject as Exception;
|
||||
@@ -228,7 +248,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
_ssdp.MessageReceived -= _ssdp_MessageReceived;
|
||||
_deviceDiscovery.DeviceDiscovered -= _deviceDiscovery_DeviceDiscovered;
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -377,10 +377,10 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications
|
||||
DisposeLibraryUpdateTimer();
|
||||
}
|
||||
|
||||
if (items.Count == 1)
|
||||
{
|
||||
var item = items.First();
|
||||
items = items.Take(10).ToList();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var notification = new NotificationRequest
|
||||
{
|
||||
NotificationType = NotificationType.NewLibraryContent.ToString()
|
||||
@@ -388,17 +388,6 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications
|
||||
|
||||
notification.Variables["Name"] = GetItemName(item);
|
||||
|
||||
await SendNotification(notification).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var notification = new NotificationRequest
|
||||
{
|
||||
NotificationType = NotificationType.NewLibraryContentMultiple.ToString()
|
||||
};
|
||||
|
||||
notification.Variables["ItemCount"] = items.Count.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
await SendNotification(notification).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,8 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
|
||||
EnableHttpCompression = false,
|
||||
|
||||
LogRequest = false,
|
||||
LogErrors = logErrors
|
||||
LogErrors = logErrors,
|
||||
BufferContent = false
|
||||
};
|
||||
|
||||
options.SetPostData(data);
|
||||
@@ -114,7 +115,8 @@ namespace MediaBrowser.Server.Implementations.EntryPoints
|
||||
EnableHttpCompression = false,
|
||||
|
||||
LogRequest = false,
|
||||
LogErrors = logErrors
|
||||
LogErrors = logErrors,
|
||||
BufferContent = false
|
||||
};
|
||||
|
||||
options.SetPostData(data);
|
||||
|
||||
@@ -19,6 +19,7 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Common.Security;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
@@ -45,16 +46,18 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly IMemoryStreamProvider _memoryStreamProvider;
|
||||
|
||||
public HttpListenerHost(IApplicationHost applicationHost,
|
||||
ILogManager logManager,
|
||||
IServerConfigurationManager config,
|
||||
string serviceName,
|
||||
string defaultRedirectPath, INetworkManager networkManager, params Assembly[] assembliesWithServices)
|
||||
string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamProvider memoryStreamProvider, params Assembly[] assembliesWithServices)
|
||||
: base(serviceName, assembliesWithServices)
|
||||
{
|
||||
DefaultRedirectPath = defaultRedirectPath;
|
||||
_networkManager = networkManager;
|
||||
_memoryStreamProvider = memoryStreamProvider;
|
||||
_config = config;
|
||||
|
||||
_logger = logManager.GetLogger("HttpServer");
|
||||
@@ -88,14 +91,16 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
HostConfig.Instance.DebugMode = false;
|
||||
|
||||
HostConfig.Instance.LogFactory = LogManager.LogFactory;
|
||||
HostConfig.Instance.AllowJsonpRequests = false;
|
||||
|
||||
// The Markdown feature causes slow startup times (5 mins+) on cold boots for some users
|
||||
// Custom format allows images
|
||||
HostConfig.Instance.EnableFeatures = Feature.Csv | Feature.Html | Feature.Json | Feature.Jsv | Feature.Metadata | Feature.Xml | Feature.CustomFormat;
|
||||
HostConfig.Instance.EnableFeatures = Feature.Html | Feature.Json | Feature.Xml | Feature.CustomFormat;
|
||||
|
||||
container.Adapter = _containerAdapter;
|
||||
|
||||
Plugins.Add(new SwaggerFeature());
|
||||
Plugins.RemoveAll(x => x is NativeTypesFeature);
|
||||
//Plugins.Add(new SwaggerFeature());
|
||||
Plugins.Add(new CorsFeature(allowedHeaders: "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization"));
|
||||
|
||||
//Plugins.Add(new AuthFeature(() => new AuthUserSession(), new IAuthProvider[] {
|
||||
@@ -179,7 +184,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
|
||||
private IHttpListener GetListener()
|
||||
{
|
||||
return new WebSocketSharpListener(_logger, CertificatePath);
|
||||
return new WebSocketSharpListener(_logger, CertificatePath, _memoryStreamProvider);
|
||||
}
|
||||
|
||||
private void OnWebSocketConnecting(WebSocketConnectingEventArgs args)
|
||||
@@ -542,8 +547,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new NotImplementedException("Cannot execute handler: " + handler + " at PathInfo: " + httpReq.PathInfo);
|
||||
else
|
||||
{
|
||||
httpRes.Close();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -93,12 +93,14 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (responseHeaders != null)
|
||||
if (responseHeaders == null)
|
||||
{
|
||||
AddResponseHeaders(result, responseHeaders);
|
||||
responseHeaders = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
responseHeaders["Expires"] = "-1";
|
||||
AddResponseHeaders(result, responseHeaders);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -681,29 +683,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the error result.
|
||||
/// </summary>
|
||||
/// <param name="statusCode">The status code.</param>
|
||||
/// <param name="errorMessage">The error message.</param>
|
||||
/// <param name="responseHeaders">The response headers.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
public void ThrowError(int statusCode, string errorMessage, IDictionary<string, string> responseHeaders = null)
|
||||
{
|
||||
var error = new HttpError
|
||||
{
|
||||
Status = statusCode,
|
||||
ErrorCode = errorMessage
|
||||
};
|
||||
|
||||
if (responseHeaders != null)
|
||||
{
|
||||
AddResponseHeaders(error, responseHeaders);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
public object GetAsyncStreamWriter(IAsyncStreamSource streamSource)
|
||||
{
|
||||
return new AsyncStreamWriter(streamSource);
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
using MediaBrowser.Common.Events;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using WebSocketMessageType = MediaBrowser.Model.Net.WebSocketMessageType;
|
||||
using WebSocketState = MediaBrowser.Model.Net.WebSocketState;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Class NativeWebSocket
|
||||
/// </summary>
|
||||
public class NativeWebSocket : IWebSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// The logger
|
||||
/// </summary>
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public event EventHandler<EventArgs> Closed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the web socket.
|
||||
/// </summary>
|
||||
/// <value>The web socket.</value>
|
||||
private System.Net.WebSockets.WebSocket WebSocket { get; set; }
|
||||
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NativeWebSocket" /> class.
|
||||
/// </summary>
|
||||
/// <param name="socket">The socket.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <exception cref="System.ArgumentNullException">socket</exception>
|
||||
public NativeWebSocket(WebSocket socket, ILogger logger)
|
||||
{
|
||||
if (socket == null)
|
||||
{
|
||||
throw new ArgumentNullException("socket");
|
||||
}
|
||||
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException("logger");
|
||||
}
|
||||
|
||||
_logger = logger;
|
||||
WebSocket = socket;
|
||||
|
||||
Receive();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state.
|
||||
/// </summary>
|
||||
/// <value>The state.</value>
|
||||
public WebSocketState State
|
||||
{
|
||||
get
|
||||
{
|
||||
WebSocketState commonState;
|
||||
|
||||
if (!Enum.TryParse(WebSocket.State.ToString(), true, out commonState))
|
||||
{
|
||||
_logger.Warn("Unrecognized WebSocketState: {0}", WebSocket.State.ToString());
|
||||
}
|
||||
|
||||
return commonState;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receives this instance.
|
||||
/// </summary>
|
||||
private async void Receive()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
byte[] bytes;
|
||||
|
||||
try
|
||||
{
|
||||
bytes = await ReceiveBytesAsync(_cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (WebSocketException ex)
|
||||
{
|
||||
_logger.ErrorException("Error receiving web socket message", ex);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (bytes == null)
|
||||
{
|
||||
// Connection closed
|
||||
EventHelper.FireEventIfNotNull(Closed, this, EventArgs.Empty, _logger);
|
||||
break;
|
||||
}
|
||||
|
||||
if (OnReceiveBytes != null)
|
||||
{
|
||||
OnReceiveBytes(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receives the async.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{WebSocketMessageInfo}.</returns>
|
||||
/// <exception cref="System.Net.WebSockets.WebSocketException">Connection closed</exception>
|
||||
private async Task<byte[]> ReceiveBytesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var bytes = new byte[4096];
|
||||
var buffer = new ArraySegment<byte>(bytes);
|
||||
|
||||
var result = await WebSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (result.CloseStatus.HasValue)
|
||||
{
|
||||
_logger.Info("Web socket connection closed by client. Reason: {0}", result.CloseStatus.Value);
|
||||
return null;
|
||||
}
|
||||
|
||||
return buffer.Array;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the async.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes.</param>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SendAsync(byte[] bytes, WebSocketMessageType type, bool endOfMessage, CancellationToken cancellationToken)
|
||||
{
|
||||
System.Net.WebSockets.WebSocketMessageType nativeType;
|
||||
|
||||
if (!Enum.TryParse(type.ToString(), true, out nativeType))
|
||||
{
|
||||
_logger.Warn("Unrecognized WebSocketMessageType: {0}", type.ToString());
|
||||
}
|
||||
|
||||
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token);
|
||||
|
||||
return WebSocket.SendAsync(new ArraySegment<byte>(bytes), nativeType, true, linkedTokenSource.Token);
|
||||
}
|
||||
|
||||
public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken)
|
||||
{
|
||||
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token);
|
||||
|
||||
return WebSocket.SendAsync(new ArraySegment<byte>(bytes), System.Net.WebSockets.WebSocketMessageType.Binary, true, linkedTokenSource.Token);
|
||||
}
|
||||
|
||||
public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken)
|
||||
{
|
||||
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationTokenSource.Token);
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(text);
|
||||
|
||||
return WebSocket.SendAsync(new ArraySegment<byte>(bytes), System.Net.WebSockets.WebSocketMessageType.Text, true, linkedTokenSource.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
_cancellationTokenSource.Cancel();
|
||||
|
||||
WebSocket.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive action.
|
||||
/// </summary>
|
||||
/// <value>The receive action.</value>
|
||||
public Action<byte[]> OnReceiveBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the on receive.
|
||||
/// </summary>
|
||||
/// <value>The on receive.</value>
|
||||
public Action<string> OnReceive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The _supports native web socket
|
||||
/// </summary>
|
||||
private static bool? _supportsNativeWebSocket;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether [supports web sockets].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [supports web sockets]; otherwise, <c>false</c>.</value>
|
||||
public static bool IsSupported
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!_supportsNativeWebSocket.HasValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
new ClientWebSocket();
|
||||
|
||||
_supportsNativeWebSocket = true;
|
||||
}
|
||||
catch (PlatformNotSupportedException)
|
||||
{
|
||||
_supportsNativeWebSocket = false;
|
||||
}
|
||||
}
|
||||
|
||||
return _supportsNativeWebSocket.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,15 +191,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error in range request writer", ex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (OnComplete != null)
|
||||
@@ -251,15 +242,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error in range request writer", ex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (OnComplete != null)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Net;
|
||||
@@ -15,23 +16,18 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
/// <summary>
|
||||
/// Creates the server.
|
||||
/// </summary>
|
||||
/// <param name="applicationHost">The application host.</param>
|
||||
/// <param name="logManager">The log manager.</param>
|
||||
/// <param name="config">The configuration.</param>
|
||||
/// <param name="_networkmanager">The _networkmanager.</param>
|
||||
/// <param name="serverName">Name of the server.</param>
|
||||
/// <param name="defaultRedirectpath">The default redirectpath.</param>
|
||||
/// <returns>IHttpServer.</returns>
|
||||
public static IHttpServer CreateServer(IApplicationHost applicationHost,
|
||||
ILogManager logManager,
|
||||
IServerConfigurationManager config,
|
||||
INetworkManager _networkmanager,
|
||||
IMemoryStreamProvider streamProvider,
|
||||
string serverName,
|
||||
string defaultRedirectpath)
|
||||
{
|
||||
LogManager.LogFactory = new ServerLogFactory(logManager);
|
||||
|
||||
return new HttpListenerHost(applicationHost, logManager, config, serverName, defaultRedirectpath, _networkmanager);
|
||||
return new HttpListenerHost(applicationHost, logManager, config, serverName, defaultRedirectpath, _networkmanager, streamProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,11 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
|
||||
if (boundary == null)
|
||||
return;
|
||||
|
||||
using (var requestStream = GetSubStream(InputStream))
|
||||
using (var requestStream = GetSubStream(InputStream, _memoryStreamProvider))
|
||||
{
|
||||
//DB: 30/01/11 - Hack to get around non-seekable stream and received HTTP request
|
||||
//Not ending with \r\n?
|
||||
var ms = new MemoryStream(32 * 1024);
|
||||
var ms = _memoryStreamProvider.CreateNew(32 * 1024);
|
||||
await requestStream.CopyToAsync(ms).ConfigureAwait(false);
|
||||
|
||||
var input = ms;
|
||||
@@ -229,9 +229,9 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
|
||||
|
||||
async Task LoadWwwForm()
|
||||
{
|
||||
using (Stream input = GetSubStream(InputStream))
|
||||
using (Stream input = GetSubStream(InputStream, _memoryStreamProvider))
|
||||
{
|
||||
using (var ms = new MemoryStream())
|
||||
using (var ms = _memoryStreamProvider.CreateNew())
|
||||
{
|
||||
await input.CopyToAsync(ms).ConfigureAwait(false);
|
||||
ms.Position = 0;
|
||||
|
||||
@@ -9,6 +9,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.IO;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
|
||||
{
|
||||
@@ -18,11 +19,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly string _certificatePath;
|
||||
private readonly IMemoryStreamProvider _memoryStreamProvider;
|
||||
|
||||
public WebSocketSharpListener(ILogger logger, string certificatePath)
|
||||
public WebSocketSharpListener(ILogger logger, string certificatePath, IMemoryStreamProvider memoryStreamProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_certificatePath = certificatePath;
|
||||
_memoryStreamProvider = memoryStreamProvider;
|
||||
}
|
||||
|
||||
public Action<Exception, IRequest> ErrorHandler { get; set; }
|
||||
@@ -148,7 +151,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
|
||||
{
|
||||
var operationName = httpContext.Request.GetOperationName();
|
||||
|
||||
var req = new WebSocketSharpRequest(httpContext, operationName, RequestAttributes.None, _logger);
|
||||
var req = new WebSocketSharpRequest(httpContext, operationName, RequestAttributes.None, _logger, _memoryStreamProvider);
|
||||
req.RequestAttributes = req.GetAttributes();
|
||||
|
||||
return req;
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Funq;
|
||||
using MediaBrowser.Common.IO;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using ServiceStack;
|
||||
using ServiceStack.Host;
|
||||
@@ -16,11 +17,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
|
||||
public Container Container { get; set; }
|
||||
private readonly HttpListenerRequest request;
|
||||
private readonly IHttpResponse response;
|
||||
private readonly IMemoryStreamProvider _memoryStreamProvider;
|
||||
|
||||
public WebSocketSharpRequest(HttpListenerContext httpContext, string operationName, RequestAttributes requestAttributes, ILogger logger)
|
||||
public WebSocketSharpRequest(HttpListenerContext httpContext, string operationName, RequestAttributes requestAttributes, ILogger logger, IMemoryStreamProvider memoryStreamProvider)
|
||||
{
|
||||
this.OperationName = operationName;
|
||||
this.RequestAttributes = requestAttributes;
|
||||
_memoryStreamProvider = memoryStreamProvider;
|
||||
this.request = httpContext.Request;
|
||||
this.response = new WebSocketSharpResponse(logger, httpContext.Response, this);
|
||||
|
||||
@@ -403,7 +406,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
|
||||
set
|
||||
{
|
||||
bufferedStream = value
|
||||
? bufferedStream ?? new MemoryStream(request.InputStream.ReadFully())
|
||||
? bufferedStream ?? _memoryStreamProvider.CreateNew(request.InputStream.ReadFully())
|
||||
: null;
|
||||
}
|
||||
}
|
||||
@@ -447,7 +450,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
|
||||
}
|
||||
}
|
||||
|
||||
static Stream GetSubStream(Stream stream)
|
||||
static Stream GetSubStream(Stream stream, IMemoryStreamProvider streamProvider)
|
||||
{
|
||||
if (stream is MemoryStream)
|
||||
{
|
||||
|
||||
@@ -81,20 +81,12 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp
|
||||
|
||||
public void Write(string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bOutput = System.Text.Encoding.UTF8.GetBytes(text);
|
||||
response.ContentLength64 = bOutput.Length;
|
||||
var bOutput = System.Text.Encoding.UTF8.GetBytes(text);
|
||||
response.ContentLength64 = bOutput.Length;
|
||||
|
||||
var outputStream = response.OutputStream;
|
||||
outputStream.Write(bOutput, 0, bOutput.Length);
|
||||
Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Could not WriteTextToResponse: " + ex.Message, ex);
|
||||
throw;
|
||||
}
|
||||
var outputStream = response.OutputStream;
|
||||
outputStream.Write(bOutput, 0, bOutput.Length);
|
||||
Close();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.IO;
|
||||
using ServiceStack;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
@@ -17,7 +18,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
private ILogger Logger { get; set; }
|
||||
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the source stream.
|
||||
/// </summary>
|
||||
@@ -39,6 +40,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
|
||||
public Action OnComplete { get; set; }
|
||||
public Action OnError { get; set; }
|
||||
private readonly byte[] _bytes;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StreamWriter" /> class.
|
||||
@@ -73,6 +75,17 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
public StreamWriter(byte[] source, string contentType, ILogger logger)
|
||||
: this(new MemoryStream(source), contentType, logger)
|
||||
{
|
||||
if (string.IsNullOrEmpty(contentType))
|
||||
{
|
||||
throw new ArgumentNullException("contentType");
|
||||
}
|
||||
|
||||
_bytes = source;
|
||||
Logger = logger;
|
||||
|
||||
Options["Content-Type"] = contentType;
|
||||
|
||||
Options["Content-Length"] = source.Length.ToString(UsCulture);
|
||||
}
|
||||
|
||||
private const int BufferSize = 81920;
|
||||
@@ -85,9 +98,16 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var src = SourceStream)
|
||||
if (_bytes != null)
|
||||
{
|
||||
src.CopyTo(responseStream, BufferSize);
|
||||
responseStream.Write(_bytes, 0, _bytes.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var src = SourceStream)
|
||||
{
|
||||
src.CopyTo(responseStream, BufferSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -114,9 +134,16 @@ namespace MediaBrowser.Server.Implementations.HttpServer
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var src = SourceStream)
|
||||
if (_bytes != null)
|
||||
{
|
||||
await src.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false);
|
||||
await responseStream.WriteAsync(_bytes, 0, _bytes.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var src = SourceStream)
|
||||
{
|
||||
await src.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -172,32 +172,16 @@ namespace MediaBrowser.Server.Implementations.IO
|
||||
Start();
|
||||
}
|
||||
|
||||
private bool EnableLibraryMonitor
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (ConfigurationManager.Configuration.EnableLibraryMonitor)
|
||||
{
|
||||
case AutoOnOff.Auto:
|
||||
return Environment.OSVersion.Platform == PlatformID.Win32NT;
|
||||
case AutoOnOff.Enabled:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsLibraryMonitorEnabaled(BaseItem item)
|
||||
{
|
||||
var options = LibraryManager.GetLibraryOptions(item);
|
||||
|
||||
if (options != null && options.SchemaVersion >= 1)
|
||||
if (options != null)
|
||||
{
|
||||
return options.EnableRealtimeMonitor;
|
||||
}
|
||||
|
||||
return EnableLibraryMonitor;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
|
||||
@@ -99,8 +99,9 @@ namespace MediaBrowser.Server.Implementations.Intros
|
||||
IncludeItemTypes = new[] { typeof(Trailer).Name },
|
||||
TrailerTypes = trailerTypes.ToArray(),
|
||||
SimilarTo = item,
|
||||
IsPlayed = config.EnableIntrosForWatchedContent ? (bool?) null : false,
|
||||
IsPlayed = config.EnableIntrosForWatchedContent ? (bool?)null : false,
|
||||
MaxParentalRating = config.EnableIntrosParentalControl ? ratingLevel : null,
|
||||
BlockUnratedItems = config.EnableIntrosParentalControl ? new[] { UnratedItem.Trailer } : new UnratedItem[] { },
|
||||
Limit = config.TrailerLimit
|
||||
});
|
||||
|
||||
@@ -110,7 +111,7 @@ namespace MediaBrowser.Server.Implementations.Intros
|
||||
Type = i.SourceType == SourceType.Channel ? ItemWithTrailerType.ChannelTrailer : ItemWithTrailerType.ItemWithTrailer,
|
||||
LibraryManager = _libraryManager
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return GetResult(item, candidates, config);
|
||||
}
|
||||
@@ -197,7 +198,7 @@ namespace MediaBrowser.Server.Implementations.Intros
|
||||
}
|
||||
|
||||
returnResult.AddRange(GetMediaInfoIntrosByTags(allIntros, item.Tags).Take(1));
|
||||
|
||||
|
||||
return returnResult.DistinctBy(i => i.Path, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
catch (IOException)
|
||||
|
||||
@@ -33,7 +33,8 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
|
||||
// Synology
|
||||
"@eaDir",
|
||||
"eaDir"
|
||||
"eaDir",
|
||||
"#recycle"
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ using MediaBrowser.Server.Implementations.Library.Resolvers;
|
||||
using MoreLinq;
|
||||
using SortOrder = MediaBrowser.Model.Entities.SortOrder;
|
||||
using VideoResolver = MediaBrowser.Naming.Video.VideoResolver;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Library
|
||||
{
|
||||
@@ -334,15 +335,6 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the item in library cache.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
private void UpdateItemInLibraryCache(BaseItem item)
|
||||
{
|
||||
RegisterItem(item);
|
||||
}
|
||||
|
||||
public void RegisterItem(BaseItem item)
|
||||
{
|
||||
if (item == null)
|
||||
@@ -695,7 +687,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
|
||||
public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files,
|
||||
IDirectoryService directoryService,
|
||||
Folder parent,
|
||||
Folder parent,
|
||||
LibraryOptions libraryOptions,
|
||||
string collectionType,
|
||||
IItemResolver[] resolvers)
|
||||
@@ -1216,12 +1208,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
if (libraryFolder != null)
|
||||
{
|
||||
info.ItemId = libraryFolder.Id.ToString("N");
|
||||
}
|
||||
|
||||
var collectionFolder = libraryFolder as CollectionFolder;
|
||||
if (collectionFolder != null)
|
||||
{
|
||||
info.LibraryOptions = collectionFolder.GetLibraryOptions();
|
||||
info.LibraryOptions = GetLibraryOptions(libraryFolder);
|
||||
}
|
||||
|
||||
return info;
|
||||
@@ -1490,10 +1477,10 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
|
||||
private void AddUserToQuery(InternalItemsQuery query, User user)
|
||||
{
|
||||
if (query.AncestorIds.Length == 0 &&
|
||||
!query.ParentId.HasValue &&
|
||||
query.ChannelIds.Length == 0 &&
|
||||
query.TopParentIds.Length == 0 &&
|
||||
if (query.AncestorIds.Length == 0 &&
|
||||
!query.ParentId.HasValue &&
|
||||
query.ChannelIds.Length == 0 &&
|
||||
query.TopParentIds.Length == 0 &&
|
||||
string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey)
|
||||
&& query.ItemIds.Length == 0)
|
||||
{
|
||||
@@ -1781,7 +1768,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
|
||||
foreach (var item in list)
|
||||
{
|
||||
UpdateItemInLibraryCache(item);
|
||||
RegisterItem(item);
|
||||
}
|
||||
|
||||
if (ItemAdded != null)
|
||||
@@ -1822,7 +1809,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
|
||||
await ItemRepository.SaveItem(item, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
UpdateItemInLibraryCache(item);
|
||||
RegisterItem(item);
|
||||
|
||||
if (ItemUpdated != null)
|
||||
{
|
||||
@@ -1889,11 +1876,41 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
|
||||
public LibraryOptions GetLibraryOptions(BaseItem item)
|
||||
{
|
||||
var collectionFolder = GetCollectionFolders(item)
|
||||
.OfType<CollectionFolder>()
|
||||
.FirstOrDefault();
|
||||
var collectionFolder = item as CollectionFolder;
|
||||
if (collectionFolder == null)
|
||||
{
|
||||
collectionFolder = GetCollectionFolders(item)
|
||||
.OfType<CollectionFolder>()
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
|
||||
var options = collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
|
||||
|
||||
if (options.SchemaVersion < 3)
|
||||
{
|
||||
options.SaveLocalMetadata = ConfigurationManager.Configuration.SaveLocalMeta;
|
||||
options.EnableInternetProviders = ConfigurationManager.Configuration.EnableInternetProviders;
|
||||
}
|
||||
|
||||
if (options.SchemaVersion < 2)
|
||||
{
|
||||
var chapterOptions = ConfigurationManager.GetConfiguration<ChapterOptions>("chapters");
|
||||
options.ExtractChapterImagesDuringLibraryScan = chapterOptions.ExtractDuringLibraryScan;
|
||||
|
||||
if (collectionFolder != null)
|
||||
{
|
||||
if (string.Equals(collectionFolder.CollectionType, "movies", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
options.EnableChapterImageExtraction = chapterOptions.EnableMovieChapterImageExtraction;
|
||||
}
|
||||
else if (string.Equals(collectionFolder.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
options.EnableChapterImageExtraction = chapterOptions.EnableEpisodeChapterImageExtraction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
public string GetContentType(BaseItem item)
|
||||
@@ -2437,7 +2454,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
|
||||
public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
|
||||
{
|
||||
var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
|
||||
var files = owner.DetectIsInMixedFolder() ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
|
||||
.Where(i => string.Equals(i.Name, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase))
|
||||
.SelectMany(i => _fileSystem.GetFiles(i.FullName, false))
|
||||
.ToList();
|
||||
@@ -2527,7 +2544,59 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
}).OrderBy(i => i.Path).ToList();
|
||||
}
|
||||
|
||||
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
|
||||
{
|
||||
if (ownerItem != null)
|
||||
{
|
||||
var libraryOptions = GetLibraryOptions(ownerItem);
|
||||
if (libraryOptions != null)
|
||||
{
|
||||
foreach (var pathInfo in libraryOptions.PathInfos)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pathInfo.NetworkPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var substitutionResult = SubstitutePathInternal(path, pathInfo.Path, pathInfo.NetworkPath);
|
||||
if (substitutionResult.Item2)
|
||||
{
|
||||
return substitutionResult.Item1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var metadataPath = ConfigurationManager.Configuration.MetadataPath;
|
||||
var metadataNetworkPath = ConfigurationManager.Configuration.MetadataNetworkPath;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath))
|
||||
{
|
||||
var metadataSubstitutionResult = SubstitutePathInternal(path, metadataPath, metadataNetworkPath);
|
||||
if (metadataSubstitutionResult.Item2)
|
||||
{
|
||||
return metadataSubstitutionResult.Item1;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var map in ConfigurationManager.Configuration.PathSubstitutions)
|
||||
{
|
||||
var substitutionResult = SubstitutePathInternal(path, map.From, map.To);
|
||||
if (substitutionResult.Item2)
|
||||
{
|
||||
return substitutionResult.Item1;
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public string SubstitutePath(string path, string from, string to)
|
||||
{
|
||||
return SubstitutePathInternal(path, from, to).Item1;
|
||||
}
|
||||
|
||||
private Tuple<string, bool> SubstitutePathInternal(string path, string from, string to)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
@@ -2542,7 +2611,11 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
throw new ArgumentNullException("to");
|
||||
}
|
||||
|
||||
from = from.Trim();
|
||||
to = to.Trim();
|
||||
|
||||
var newPath = path.Replace(from, to, StringComparison.OrdinalIgnoreCase);
|
||||
var changed = false;
|
||||
|
||||
if (!string.Equals(newPath, path))
|
||||
{
|
||||
@@ -2554,9 +2627,11 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
{
|
||||
newPath = newPath.Replace('/', '\\');
|
||||
}
|
||||
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return newPath;
|
||||
return new Tuple<string, bool>(newPath, changed);
|
||||
}
|
||||
|
||||
private void SetExtraTypeFromFilename(Video item)
|
||||
@@ -2685,7 +2760,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
public void AddVirtualFolder(string name, string collectionType, string[] mediaPaths, LibraryOptions options, bool refreshLibrary)
|
||||
public void AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
@@ -2703,12 +2778,13 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
virtualFolderPath = Path.Combine(rootFolderPath, name);
|
||||
}
|
||||
|
||||
if (mediaPaths != null)
|
||||
var mediaPathInfos = options.PathInfos;
|
||||
if (mediaPathInfos != null)
|
||||
{
|
||||
var invalidpath = mediaPaths.FirstOrDefault(i => !_fileSystem.DirectoryExists(i));
|
||||
var invalidpath = mediaPathInfos.FirstOrDefault(i => !_fileSystem.DirectoryExists(i.Path));
|
||||
if (invalidpath != null)
|
||||
{
|
||||
throw new ArgumentException("The specified path does not exist: " + invalidpath + ".");
|
||||
throw new ArgumentException("The specified path does not exist: " + invalidpath.Path + ".");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2730,11 +2806,11 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
|
||||
CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
|
||||
|
||||
if (mediaPaths != null)
|
||||
if (mediaPathInfos != null)
|
||||
{
|
||||
foreach (var path in mediaPaths)
|
||||
foreach (var path in mediaPathInfos)
|
||||
{
|
||||
AddMediaPath(name, path);
|
||||
AddMediaPathInternal(name, path, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2760,6 +2836,137 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
private bool ValidateNetworkPath(string path)
|
||||
{
|
||||
if (Environment.OSVersion.Platform == PlatformID.Win32NT || !path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Directory.Exists(path);
|
||||
}
|
||||
|
||||
// Without native support for unc, we cannot validate this when running under mono
|
||||
return true;
|
||||
}
|
||||
|
||||
private const string ShortcutFileExtension = ".mblink";
|
||||
private const string ShortcutFileSearch = "*" + ShortcutFileExtension;
|
||||
public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
|
||||
{
|
||||
AddMediaPathInternal(virtualFolderName, pathInfo, true);
|
||||
}
|
||||
|
||||
private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions)
|
||||
{
|
||||
if (pathInfo == null)
|
||||
{
|
||||
throw new ArgumentNullException("path");
|
||||
}
|
||||
|
||||
var path = pathInfo.Path;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
throw new ArgumentNullException("path");
|
||||
}
|
||||
|
||||
if (!_fileSystem.DirectoryExists(path))
|
||||
{
|
||||
throw new DirectoryNotFoundException("The path does not exist.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
|
||||
{
|
||||
throw new DirectoryNotFoundException("The network path does not exist.");
|
||||
}
|
||||
|
||||
var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
|
||||
|
||||
var shortcutFilename = _fileSystem.GetFileNameWithoutExtension(path);
|
||||
|
||||
var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
||||
|
||||
while (_fileSystem.FileExists(lnk))
|
||||
{
|
||||
shortcutFilename += "1";
|
||||
lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
||||
}
|
||||
|
||||
_fileSystem.CreateShortcut(lnk, path);
|
||||
|
||||
RemoveContentTypeOverrides(path);
|
||||
|
||||
if (saveLibraryOptions)
|
||||
{
|
||||
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
|
||||
|
||||
var list = libraryOptions.PathInfos.ToList();
|
||||
list.Add(pathInfo);
|
||||
libraryOptions.PathInfos = list.ToArray();
|
||||
|
||||
SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
|
||||
|
||||
CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
|
||||
{
|
||||
if (pathInfo == null)
|
||||
{
|
||||
throw new ArgumentNullException("path");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath))
|
||||
{
|
||||
throw new DirectoryNotFoundException("The network path does not exist.");
|
||||
}
|
||||
|
||||
var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
|
||||
|
||||
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
|
||||
|
||||
SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
|
||||
|
||||
var list = libraryOptions.PathInfos.ToList();
|
||||
foreach (var originalPathInfo in list)
|
||||
{
|
||||
if (string.Equals(pathInfo.Path, originalPathInfo.Path, StringComparison.Ordinal))
|
||||
{
|
||||
originalPathInfo.NetworkPath = pathInfo.NetworkPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
libraryOptions.PathInfos = list.ToArray();
|
||||
|
||||
CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
|
||||
}
|
||||
|
||||
private void SyncLibraryOptionsToLocations(string virtualFolderPath, LibraryOptions options)
|
||||
{
|
||||
var topLibraryFolders = GetUserRootFolder().Children.ToList();
|
||||
var info = GetVirtualFolderInfo(virtualFolderPath, topLibraryFolders);
|
||||
|
||||
if (info.Locations.Count > 0 && info.Locations.Count != options.PathInfos.Length)
|
||||
{
|
||||
var list = options.PathInfos.ToList();
|
||||
|
||||
foreach (var location in info.Locations)
|
||||
{
|
||||
if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
|
||||
{
|
||||
list.Add(new MediaPathInfo
|
||||
{
|
||||
Path = location
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
options.PathInfos = list.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveVirtualFolder(string name, bool refreshLibrary)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
@@ -2804,38 +3011,6 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
private const string ShortcutFileExtension = ".mblink";
|
||||
private const string ShortcutFileSearch = "*" + ShortcutFileExtension;
|
||||
public void AddMediaPath(string virtualFolderName, string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
throw new ArgumentNullException("path");
|
||||
}
|
||||
|
||||
if (!_fileSystem.DirectoryExists(path))
|
||||
{
|
||||
throw new DirectoryNotFoundException("The path does not exist.");
|
||||
}
|
||||
|
||||
var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
|
||||
|
||||
var shortcutFilename = _fileSystem.GetFileNameWithoutExtension(path);
|
||||
|
||||
var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
||||
|
||||
while (_fileSystem.FileExists(lnk))
|
||||
{
|
||||
shortcutFilename += "1";
|
||||
lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
|
||||
}
|
||||
|
||||
_fileSystem.CreateShortcut(lnk, path);
|
||||
|
||||
RemoveContentTypeOverrides(path);
|
||||
}
|
||||
|
||||
private void RemoveContentTypeOverrides(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
@@ -2872,19 +3047,28 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
}
|
||||
|
||||
var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||
var path = Path.Combine(rootFolderPath, virtualFolderName);
|
||||
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
|
||||
|
||||
if (!_fileSystem.DirectoryExists(path))
|
||||
if (!_fileSystem.DirectoryExists(virtualFolderPath))
|
||||
{
|
||||
throw new DirectoryNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName));
|
||||
}
|
||||
|
||||
var shortcut = Directory.EnumerateFiles(path, ShortcutFileSearch, SearchOption.AllDirectories).FirstOrDefault(f => _fileSystem.ResolveShortcut(f).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
|
||||
var shortcut = Directory.EnumerateFiles(virtualFolderPath, ShortcutFileSearch, SearchOption.AllDirectories).FirstOrDefault(f => _fileSystem.ResolveShortcut(f).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrEmpty(shortcut))
|
||||
{
|
||||
_fileSystem.DeleteFile(shortcut);
|
||||
}
|
||||
|
||||
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
|
||||
|
||||
libraryOptions.PathInfos = libraryOptions
|
||||
.PathInfos
|
||||
.Where(i => !string.Equals(i.Path, mediaPath, StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
|
||||
CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,8 +221,28 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId, bool enablePathSubstitution)
|
||||
public async Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId, string liveStreamId, bool enablePathSubstitution, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(liveStreamId))
|
||||
{
|
||||
return await GetLiveStream(liveStreamId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
//await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
//try
|
||||
//{
|
||||
// var stream = _openStreams.Values.FirstOrDefault(i => string.Equals(i.MediaSource.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// if (stream != null)
|
||||
// {
|
||||
// return stream.MediaSource;
|
||||
// }
|
||||
//}
|
||||
//finally
|
||||
//{
|
||||
// _liveStreamSemaphore.Release();
|
||||
//}
|
||||
|
||||
var sources = await GetPlayackMediaSources(item.Id.ToString("N"), null, enablePathSubstitution, new[] { MediaType.Audio, MediaType.Video },
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
@@ -335,7 +355,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private readonly ConcurrentDictionary<string, LiveStreamInfo> _openStreams = new ConcurrentDictionary<string, LiveStreamInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, LiveStreamInfo> _openStreams = new Dictionary<string, LiveStreamInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
|
||||
|
||||
public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, bool enableAutoClose, CancellationToken cancellationToken)
|
||||
@@ -347,7 +367,9 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
var tuple = GetProvider(request.OpenToken);
|
||||
var provider = tuple.Item1;
|
||||
|
||||
var mediaSource = await provider.OpenMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false);
|
||||
var mediaSourceTuple = await provider.OpenMediaSource(tuple.Item2, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var mediaSource = mediaSourceTuple.Item1;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
|
||||
{
|
||||
@@ -361,9 +383,11 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
Date = DateTime.UtcNow,
|
||||
EnableCloseTimer = enableAutoClose,
|
||||
Id = mediaSource.LiveStreamId,
|
||||
MediaSource = mediaSource
|
||||
MediaSource = mediaSource,
|
||||
DirectStreamProvider = mediaSourceTuple.Item2
|
||||
};
|
||||
_openStreams.AddOrUpdate(mediaSource.LiveStreamId, info, (key, i) => info);
|
||||
|
||||
_openStreams[mediaSource.LiveStreamId] = info;
|
||||
|
||||
if (enableAutoClose)
|
||||
{
|
||||
@@ -394,14 +418,14 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)
|
||||
public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStreamWithDirectStreamProvider(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
throw new ArgumentNullException("id");
|
||||
}
|
||||
|
||||
_logger.Debug("Getting live stream {0}", id);
|
||||
_logger.Debug("Getting already opened live stream {0}", id);
|
||||
|
||||
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -410,7 +434,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
LiveStreamInfo info;
|
||||
if (_openStreams.TryGetValue(id, out info))
|
||||
{
|
||||
return info.MediaSource;
|
||||
return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info.DirectStreamProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -423,6 +447,12 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<MediaSourceInfo> GetLiveStream(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await GetLiveStreamWithDirectStreamProvider(id, cancellationToken).ConfigureAwait(false);
|
||||
return result.Item1;
|
||||
}
|
||||
|
||||
public async Task PingLiveStream(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
@@ -436,7 +466,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Error("Failed to update MediaSource timestamp for {0}", id);
|
||||
_logger.Error("Failed to ping live stream {0}", id);
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -445,17 +475,16 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CloseLiveStreamWithProvider(IMediaSourceProvider provider, string streamId, CancellationToken cancellationToken)
|
||||
private async Task CloseLiveStreamWithProvider(IMediaSourceProvider provider, string streamId)
|
||||
{
|
||||
_logger.Info("Closing live stream {0} with provider {1}", streamId, provider.GetType().Name);
|
||||
|
||||
try
|
||||
{
|
||||
await provider.CloseMediaSource(streamId, cancellationToken).ConfigureAwait(false);
|
||||
await provider.CloseMediaSource(streamId).ConfigureAwait(false);
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -463,37 +492,35 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CloseLiveStream(string id, CancellationToken cancellationToken)
|
||||
public async Task CloseLiveStream(string id)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
throw new ArgumentNullException("id");
|
||||
}
|
||||
|
||||
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
await _liveStreamSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
LiveStreamInfo current;
|
||||
|
||||
if (_openStreams.TryGetValue(id, out current))
|
||||
{
|
||||
_openStreams.Remove(id);
|
||||
current.Closed = true;
|
||||
|
||||
if (current.MediaSource.RequiresClosing)
|
||||
{
|
||||
var tuple = GetProvider(id);
|
||||
|
||||
await CloseLiveStreamWithProvider(tuple.Item1, tuple.Item2, cancellationToken).ConfigureAwait(false);
|
||||
await CloseLiveStreamWithProvider(tuple.Item1, tuple.Item2).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
LiveStreamInfo removed;
|
||||
if (_openStreams.TryRemove(id, out removed))
|
||||
{
|
||||
removed.Closed = true;
|
||||
}
|
||||
|
||||
if (_openStreams.Count == 0)
|
||||
{
|
||||
StopCloseTimer();
|
||||
if (_openStreams.Count == 0)
|
||||
{
|
||||
StopCloseTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -523,7 +550,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
}
|
||||
|
||||
private Timer _closeTimer;
|
||||
private readonly TimeSpan _openStreamMaxAge = TimeSpan.FromSeconds(60);
|
||||
private readonly TimeSpan _openStreamMaxAge = TimeSpan.FromSeconds(180);
|
||||
|
||||
private void StartCloseTimer()
|
||||
{
|
||||
@@ -545,10 +572,20 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
|
||||
private async void CloseTimerCallback(object state)
|
||||
{
|
||||
var infos = _openStreams
|
||||
.Values
|
||||
.Where(i => i.EnableCloseTimer && DateTime.UtcNow - i.Date > _openStreamMaxAge)
|
||||
.ToList();
|
||||
List<LiveStreamInfo> infos;
|
||||
await _liveStreamSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
infos = _openStreams
|
||||
.Values
|
||||
.Where(i => i.EnableCloseTimer && DateTime.UtcNow - i.Date > _openStreamMaxAge)
|
||||
.ToList();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_liveStreamSemaphore.Release();
|
||||
}
|
||||
|
||||
foreach (var info in infos)
|
||||
{
|
||||
@@ -556,7 +593,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
{
|
||||
try
|
||||
{
|
||||
await CloseLiveStream(info.Id, CancellationToken.None).ConfigureAwait(false);
|
||||
await CloseLiveStream(info.Id).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -588,12 +625,10 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
{
|
||||
foreach (var key in _openStreams.Keys.ToList())
|
||||
{
|
||||
var task = CloseLiveStream(key, CancellationToken.None);
|
||||
var task = CloseLiveStream(key);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
_openStreams.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -605,6 +640,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
public string Id;
|
||||
public bool Closed;
|
||||
public MediaSourceInfo MediaSource;
|
||||
public IDirectStreamProvider DirectStreamProvider;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,8 +54,8 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio
|
||||
if (!args.IsDirectory) return null;
|
||||
|
||||
// Avoid mis-identifying top folders
|
||||
if (args.Parent.IsRoot) return null;
|
||||
if (args.HasParent<MusicAlbum>()) return null;
|
||||
if (args.Parent.IsRoot) return null;
|
||||
|
||||
var collectionType = args.GetCollectionType();
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using CommonIO;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio
|
||||
{
|
||||
@@ -18,12 +19,14 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio
|
||||
private readonly ILogger _logger;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
public MusicArtistResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager)
|
||||
public MusicArtistResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager, IServerConfigurationManager config)
|
||||
{
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_libraryManager = libraryManager;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -48,9 +51,6 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio
|
||||
{
|
||||
if (!args.IsDirectory) return null;
|
||||
|
||||
// Avoid mis-identifying top folders
|
||||
if (args.Parent.IsRoot) return null;
|
||||
|
||||
// Don't allow nested artists
|
||||
if (args.HasParent<MusicArtist>() || args.HasParent<MusicAlbum>())
|
||||
{
|
||||
@@ -67,6 +67,19 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Audio
|
||||
return null;
|
||||
}
|
||||
|
||||
if (args.ContainsFileSystemEntryByName("artist.nfo"))
|
||||
{
|
||||
return new MusicArtist();
|
||||
}
|
||||
|
||||
if (_config.Configuration.EnableSimpleArtistDetection)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Avoid mis-identifying top folders
|
||||
if (args.Parent.IsRoot) return null;
|
||||
|
||||
var directoryService = args.DirectoryService;
|
||||
|
||||
var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
|
||||
|
||||
@@ -51,7 +51,6 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers
|
||||
base.SetInitialItemValues(item, args);
|
||||
|
||||
item.IsRoot = args.Parent == null;
|
||||
item.IsPhysicalRoot = args.IsPhysicalRoot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,12 +48,6 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies
|
||||
string collectionType,
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
if (parent != null && parent.Path != null && parent.Path.IndexOf("disney", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
var b = true;
|
||||
var a = b;
|
||||
}
|
||||
|
||||
var result = ResolveMultipleInternal(parent, files, collectionType, directoryService);
|
||||
|
||||
if (result != null)
|
||||
@@ -213,26 +207,22 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies
|
||||
// Find movies with their own folders
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
var files = args.FileSystemChildren
|
||||
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
|
||||
.ToList();
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return FindMovie<MusicVideo>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(collectionType))
|
||||
{
|
||||
// Owned items should just use the plain video type
|
||||
// Owned items will be caught by the plain video resolver
|
||||
if (args.Parent == null)
|
||||
{
|
||||
return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (args.HasParent<Series>())
|
||||
@@ -240,11 +230,21 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.Movies
|
||||
return null;
|
||||
}
|
||||
|
||||
return FindMovie<Movie>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
|
||||
{
|
||||
var files = args.FileSystemChildren
|
||||
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
|
||||
.ToList();
|
||||
|
||||
return FindMovie<Movie>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
|
||||
}
|
||||
}
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var files = args.FileSystemChildren
|
||||
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
|
||||
.ToList();
|
||||
|
||||
return FindMovie<Movie>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,11 +54,20 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV
|
||||
{
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
if (args.HasParent<Series>())
|
||||
if (args.HasParent<Series>() || args.HasParent<Season>())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (args.ContainsFileSystemEntryByName("tvshow.nfo"))
|
||||
{
|
||||
return new Series
|
||||
{
|
||||
Path = args.Path,
|
||||
Name = Path.GetFileName(args.Path)
|
||||
};
|
||||
}
|
||||
|
||||
var collectionType = args.GetCollectionType();
|
||||
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -72,22 +81,20 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (string.IsNullOrWhiteSpace(collectionType))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(collectionType))
|
||||
if (args.Parent.IsRoot)
|
||||
{
|
||||
if (args.Parent.IsRoot)
|
||||
return null;
|
||||
}
|
||||
|
||||
if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, args.GetLibraryOptions(), false))
|
||||
{
|
||||
return new Series
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, args.GetLibraryOptions(), false))
|
||||
{
|
||||
return new Series
|
||||
{
|
||||
Path = args.Path,
|
||||
Name = Path.GetFileName(args.Path)
|
||||
};
|
||||
}
|
||||
Path = args.Path,
|
||||
Name = Path.GetFileName(args.Path)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
var includeItemTypes = (query.IncludeItemTypes ?? new string[] { }).ToList();
|
||||
|
||||
excludeItemTypes.Add(typeof(Year).Name);
|
||||
excludeItemTypes.Add(typeof(Folder).Name);
|
||||
|
||||
if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase)))
|
||||
{
|
||||
|
||||
@@ -61,16 +61,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Repository.SaveUserData(userId, key, userData, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error saving user data", ex);
|
||||
|
||||
throw;
|
||||
}
|
||||
await Repository.SaveUserData(userId, key, userData, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var cacheKey = GetCacheKey(userId, item.Id);
|
||||
@@ -107,18 +98,7 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
await Repository.SaveAllUserData(userId, userData, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error saving user data", ex);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
await Repository.SaveAllUserData(userId, userData, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -289,6 +269,11 @@ namespace MediaBrowser.Server.Implementations.Library
|
||||
positionTicks = 0;
|
||||
}
|
||||
|
||||
if (!item.SupportsPlayedStatus)
|
||||
{
|
||||
positionTicks = 0;
|
||||
data.Played = false;
|
||||
}
|
||||
if (item is Audio)
|
||||
{
|
||||
positionTicks = 0;
|
||||
|
||||
@@ -47,25 +47,60 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
_logger.Info("Copying recording stream to file {0}", targetFile);
|
||||
|
||||
if (mediaSource.RunTimeTicks.HasValue)
|
||||
{
|
||||
// The media source already has a fixed duration
|
||||
// But add another stop 1 minute later just in case the recording gets stuck for any reason
|
||||
var durationToken = new CancellationTokenSource(duration.Add(TimeSpan.FromMinutes(1)));
|
||||
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The media source if infinite so we need to handle stopping ourselves
|
||||
var durationToken = new CancellationTokenSource(duration);
|
||||
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
|
||||
}
|
||||
// The media source if infinite so we need to handle stopping ourselves
|
||||
var durationToken = new CancellationTokenSource(duration);
|
||||
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
|
||||
|
||||
await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
|
||||
await CopyUntilCancelled(response.Content, output, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Info("Recording completed to file {0}", targetFile);
|
||||
}
|
||||
|
||||
private const int BufferSize = 81920;
|
||||
public static Task CopyUntilCancelled(Stream source, Stream target, CancellationToken cancellationToken)
|
||||
{
|
||||
return CopyUntilCancelled(source, target, null, cancellationToken);
|
||||
}
|
||||
public static async Task CopyUntilCancelled(Stream source, Stream target, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var bytesRead = await CopyToAsyncInternal(source, target, BufferSize, onStarted, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
onStarted = null;
|
||||
|
||||
//var position = fs.Position;
|
||||
//_logger.Debug("Streamed {0} bytes to position {1} from file {2}", bytesRead, position, path);
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<int> CopyToAsyncInternal(Stream source, Stream destination, Int32 bufferSize, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
int bytesRead;
|
||||
int totalBytesRead = 0;
|
||||
|
||||
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
|
||||
if (onStarted != null)
|
||||
{
|
||||
onStarted();
|
||||
}
|
||||
onStarted = null;
|
||||
}
|
||||
|
||||
return totalBytesRead;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -67,36 +67,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
|
||||
public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
if (mediaSource.Path.IndexOf("m3u8", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
await RecordWithoutTempFile(mediaSource, targetFile, duration, onStarted, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var tempfile = Path.Combine(_appPaths.TranscodingTempPath, Guid.NewGuid().ToString("N") + ".ts");
|
||||
|
||||
try
|
||||
{
|
||||
await RecordWithTempFile(mediaSource, tempfile, targetFile, duration, onStarted, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(tempfile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error deleting recording temp file", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RecordWithoutTempFile(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
var durationToken = new CancellationTokenSource(duration);
|
||||
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
|
||||
@@ -106,59 +76,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
_logger.Info("Recording completed to file {0}", targetFile);
|
||||
}
|
||||
|
||||
private async Task RecordWithTempFile(MediaSourceInfo mediaSource, string tempFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
var httpRequestOptions = new HttpRequestOptions()
|
||||
{
|
||||
Url = mediaSource.Path
|
||||
};
|
||||
|
||||
httpRequestOptions.BufferContent = false;
|
||||
|
||||
using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false))
|
||||
{
|
||||
_logger.Info("Opened recording stream from tuner provider");
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(tempFile));
|
||||
|
||||
using (var output = _fileSystem.GetFileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read))
|
||||
{
|
||||
//onStarted();
|
||||
|
||||
_logger.Info("Copying recording stream to file {0}", tempFile);
|
||||
|
||||
var bufferMs = 5000;
|
||||
|
||||
if (mediaSource.RunTimeTicks.HasValue)
|
||||
{
|
||||
// The media source already has a fixed duration
|
||||
// But add another stop 1 minute later just in case the recording gets stuck for any reason
|
||||
var durationToken = new CancellationTokenSource(duration.Add(TimeSpan.FromMinutes(1)));
|
||||
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The media source if infinite so we need to handle stopping ourselves
|
||||
var durationToken = new CancellationTokenSource(duration.Add(TimeSpan.FromMilliseconds(bufferMs)));
|
||||
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token;
|
||||
}
|
||||
|
||||
var tempFileTask = response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, cancellationToken);
|
||||
|
||||
// Give the temp file a little time to build up
|
||||
await Task.Delay(bufferMs, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var recordTask = Task.Run(() => RecordFromFile(mediaSource, tempFile, targetFile, duration, onStarted, cancellationToken), cancellationToken);
|
||||
|
||||
await tempFileTask.ConfigureAwait(false);
|
||||
|
||||
await recordTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Info("Recording completed to file {0}", targetFile);
|
||||
}
|
||||
|
||||
private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
_targetPath = targetFile;
|
||||
@@ -200,7 +117,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
|
||||
_logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length);
|
||||
|
||||
process.Exited += (sender, args) => OnFfMpegProcessExited(process);
|
||||
process.Exited += (sender, args) => OnFfMpegProcessExited(process, inputFile);
|
||||
|
||||
process.Start();
|
||||
|
||||
@@ -214,6 +131,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
|
||||
StartStreamingLog(process.StandardError.BaseStream, _logFileStream);
|
||||
|
||||
_logger.Info("ffmpeg recording process started for {0}", _targetPath);
|
||||
|
||||
return _taskCompletionSource.Task;
|
||||
}
|
||||
|
||||
@@ -234,16 +153,33 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
|
||||
var durationParam = " -t " + _mediaEncoder.GetTimeParameter(duration.Ticks);
|
||||
var commandLineArgs = "-fflags +genpts -async 1 -vsync -1 -re -i \"{0}\"{4} -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\"";
|
||||
var inputModifiers = "-fflags +genpts -async 1 -vsync -1";
|
||||
var commandLineArgs = "-i \"{0}\"{4} -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\"";
|
||||
|
||||
long startTimeTicks = 0;
|
||||
//if (mediaSource.DateLiveStreamOpened.HasValue)
|
||||
//{
|
||||
// var elapsed = DateTime.UtcNow - mediaSource.DateLiveStreamOpened.Value;
|
||||
// elapsed -= TimeSpan.FromSeconds(10);
|
||||
// if (elapsed.TotalSeconds >= 0)
|
||||
// {
|
||||
// startTimeTicks = elapsed.Ticks + startTimeTicks;
|
||||
// }
|
||||
//}
|
||||
|
||||
if (mediaSource.ReadAtNativeFramerate)
|
||||
{
|
||||
commandLineArgs = "-re " + commandLineArgs;
|
||||
inputModifiers += " -re";
|
||||
}
|
||||
|
||||
if (startTimeTicks > 0)
|
||||
{
|
||||
inputModifiers = "-ss " + _mediaEncoder.GetTimeParameter(startTimeTicks) + " " + inputModifiers;
|
||||
}
|
||||
|
||||
commandLineArgs = string.Format(commandLineArgs, inputTempFile, targetFile, videoArgs, GetAudioArgs(mediaSource), durationParam);
|
||||
|
||||
return commandLineArgs;
|
||||
return inputModifiers + " " + commandLineArgs;
|
||||
}
|
||||
|
||||
private string GetAudioArgs(MediaSourceInfo mediaSource)
|
||||
@@ -309,8 +245,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
/// <summary>
|
||||
/// Processes the exited.
|
||||
/// </summary>
|
||||
/// <param name="process">The process.</param>
|
||||
private void OnFfMpegProcessExited(Process process)
|
||||
private void OnFfMpegProcessExited(Process process, string inputFile)
|
||||
{
|
||||
_hasExited = true;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
@@ -12,37 +13,55 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
return timer.StartDate.AddSeconds(-timer.PrePaddingSeconds);
|
||||
}
|
||||
|
||||
public static TimerInfo CreateTimer(ProgramInfo parent, SeriesTimerInfo series)
|
||||
public static TimerInfo CreateTimer(ProgramInfo parent, SeriesTimerInfo seriesTimer)
|
||||
{
|
||||
var timer = new TimerInfo();
|
||||
|
||||
timer.ChannelId = parent.ChannelId;
|
||||
timer.Id = (series.Id + parent.Id).GetMD5().ToString("N");
|
||||
timer.Id = (seriesTimer.Id + parent.Id).GetMD5().ToString("N");
|
||||
timer.StartDate = parent.StartDate;
|
||||
timer.EndDate = parent.EndDate;
|
||||
timer.ProgramId = parent.Id;
|
||||
timer.PrePaddingSeconds = series.PrePaddingSeconds;
|
||||
timer.PostPaddingSeconds = series.PostPaddingSeconds;
|
||||
timer.IsPostPaddingRequired = series.IsPostPaddingRequired;
|
||||
timer.IsPrePaddingRequired = series.IsPrePaddingRequired;
|
||||
timer.Priority = series.Priority;
|
||||
timer.PrePaddingSeconds = seriesTimer.PrePaddingSeconds;
|
||||
timer.PostPaddingSeconds = seriesTimer.PostPaddingSeconds;
|
||||
timer.IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired;
|
||||
timer.IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired;
|
||||
timer.KeepUntil = seriesTimer.KeepUntil;
|
||||
timer.Priority = seriesTimer.Priority;
|
||||
timer.Name = parent.Name;
|
||||
timer.Overview = parent.Overview;
|
||||
timer.SeriesTimerId = series.Id;
|
||||
timer.SeriesTimerId = seriesTimer.Id;
|
||||
|
||||
CopyProgramInfoToTimerInfo(parent, timer);
|
||||
|
||||
return timer;
|
||||
}
|
||||
|
||||
public static string GetRecordingName(TimerInfo timer, ProgramInfo info)
|
||||
public static void CopyProgramInfoToTimerInfo(ProgramInfo programInfo, TimerInfo timerInfo)
|
||||
{
|
||||
if (info == null)
|
||||
{
|
||||
return timer.ProgramId;
|
||||
}
|
||||
timerInfo.SeasonNumber = programInfo.SeasonNumber;
|
||||
timerInfo.EpisodeNumber = programInfo.EpisodeNumber;
|
||||
timerInfo.IsMovie = programInfo.IsMovie;
|
||||
timerInfo.IsKids = programInfo.IsKids;
|
||||
timerInfo.IsNews = programInfo.IsNews;
|
||||
timerInfo.IsSports = programInfo.IsSports;
|
||||
timerInfo.ProductionYear = programInfo.ProductionYear;
|
||||
timerInfo.EpisodeTitle = programInfo.EpisodeTitle;
|
||||
timerInfo.OriginalAirDate = programInfo.OriginalAirDate;
|
||||
timerInfo.IsProgramSeries = programInfo.IsSeries;
|
||||
|
||||
timerInfo.HomePageUrl = programInfo.HomePageUrl;
|
||||
timerInfo.CommunityRating = programInfo.CommunityRating;
|
||||
timerInfo.ShortOverview = programInfo.ShortOverview;
|
||||
timerInfo.OfficialRating = programInfo.OfficialRating;
|
||||
timerInfo.IsRepeat = programInfo.IsRepeat;
|
||||
}
|
||||
|
||||
public static string GetRecordingName(TimerInfo info)
|
||||
{
|
||||
var name = info.Name;
|
||||
|
||||
if (info.IsSeries)
|
||||
if (info.IsProgramSeries)
|
||||
{
|
||||
var addHyphen = true;
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using CommonIO;
|
||||
using MediaBrowser.Controller.Power;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
@@ -17,15 +16,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
public class TimerManager : ItemDataProvider<TimerInfo>
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, Timer> _timers = new ConcurrentDictionary<string, Timer>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly IPowerManagement _powerManagement;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired;
|
||||
|
||||
public TimerManager(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger, string dataPath, IPowerManagement powerManagement, ILogger logger1)
|
||||
public TimerManager(IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger, string dataPath, ILogger logger1)
|
||||
: base(fileSystem, jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_powerManagement = powerManagement;
|
||||
_logger = logger1;
|
||||
}
|
||||
|
||||
@@ -35,7 +32,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
foreach (var item in GetAll().ToList())
|
||||
{
|
||||
AddTimer(item);
|
||||
AddOrUpdateSystemTimer(item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,18 +55,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
public override void Update(TimerInfo item)
|
||||
{
|
||||
base.Update(item);
|
||||
|
||||
Timer timer;
|
||||
if (_timers.TryGetValue(item.Id, out timer))
|
||||
{
|
||||
var timespan = RecordingHelper.GetStartTime(item) - DateTime.UtcNow;
|
||||
timer.Change(timespan, TimeSpan.Zero);
|
||||
ScheduleWake(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddTimer(item);
|
||||
}
|
||||
AddOrUpdateSystemTimer(item);
|
||||
}
|
||||
|
||||
public void AddOrUpdate(TimerInfo item, bool resetTimer)
|
||||
@@ -100,13 +86,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
|
||||
base.Add(item);
|
||||
AddTimer(item);
|
||||
ScheduleWake(item);
|
||||
AddOrUpdateSystemTimer(item);
|
||||
}
|
||||
|
||||
private void AddTimer(TimerInfo item)
|
||||
private bool ShouldStartTimer(TimerInfo item)
|
||||
{
|
||||
if (item.Status == RecordingStatus.Completed)
|
||||
if (item.Status == RecordingStatus.Completed ||
|
||||
item.Status == RecordingStatus.Cancelled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void AddOrUpdateSystemTimer(TimerInfo item)
|
||||
{
|
||||
StopTimer(item);
|
||||
|
||||
if (!ShouldStartTimer(item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -120,33 +118,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
return;
|
||||
}
|
||||
|
||||
var timerLength = startDate - now;
|
||||
StartTimer(item, timerLength);
|
||||
var dueTime = startDate - now;
|
||||
StartTimer(item, dueTime);
|
||||
}
|
||||
|
||||
private void ScheduleWake(TimerInfo info)
|
||||
private void StartTimer(TimerInfo item, TimeSpan dueTime)
|
||||
{
|
||||
var startDate = RecordingHelper.GetStartTime(info).AddMinutes(-5);
|
||||
|
||||
try
|
||||
{
|
||||
_powerManagement.ScheduleWake(startDate);
|
||||
_logger.Info("Scheduled system wake timer at {0} (UTC)", startDate);
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error scheduling wake timer", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void StartTimer(TimerInfo item, TimeSpan dueTime)
|
||||
{
|
||||
StopTimer(item);
|
||||
|
||||
var timer = new Timer(TimerCallback, item.Id, dueTime, TimeSpan.Zero);
|
||||
|
||||
if (_timers.TryAdd(item.Id, timer))
|
||||
@@ -179,5 +156,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
|
||||
EventHelper.FireEventIfNotNull(TimerFired, this, new GenericEventArgs<TimerInfo> { Argument = timer }, Logger);
|
||||
}
|
||||
}
|
||||
|
||||
public TimerInfo GetTimer(string id)
|
||||
{
|
||||
return GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.XmlTv.Classes;
|
||||
using Emby.XmlTv.Entities;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
@@ -115,7 +116,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings
|
||||
var reader = new XmlTvReader(path, GetLanguage(), null);
|
||||
|
||||
var results = reader.GetProgrammes(channelNumber, startDateUtc, endDateUtc, cancellationToken);
|
||||
return results.Select(p => new ProgramInfo()
|
||||
return results.Select(p => GetProgramInfo(p, info));
|
||||
}
|
||||
|
||||
private ProgramInfo GetProgramInfo(XmlTvProgram p, ListingsProviderInfo info)
|
||||
{
|
||||
var programInfo = new ProgramInfo
|
||||
{
|
||||
ChannelId = p.ChannelId,
|
||||
EndDate = GetDate(p.EndDate),
|
||||
@@ -141,7 +147,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings
|
||||
OfficialRating = p.Rating != null && !String.IsNullOrEmpty(p.Rating.Value) ? p.Rating.Value : null,
|
||||
CommunityRating = p.StarRating.HasValue ? p.StarRating.Value : (float?)null,
|
||||
SeriesId = p.Episode != null ? p.Title.GetMD5().ToString("N") : null
|
||||
});
|
||||
};
|
||||
|
||||
if (programInfo.IsMovie)
|
||||
{
|
||||
programInfo.IsSeries = false;
|
||||
programInfo.EpisodeNumber = null;
|
||||
programInfo.EpisodeTitle = null;
|
||||
}
|
||||
|
||||
return programInfo;
|
||||
}
|
||||
|
||||
private DateTime GetDate(DateTime date)
|
||||
|
||||
110
MediaBrowser.Server.Implementations/LiveTv/LiveStreamHelper.cs
Normal file
110
MediaBrowser.Server.Implementations/LiveTv/LiveStreamHelper.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Logging;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
{
|
||||
public class LiveStreamHelper
|
||||
{
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger)
|
||||
{
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task AddMediaInfoWithProbe(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)
|
||||
{
|
||||
var originalRuntime = mediaSource.RunTimeTicks;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
|
||||
{
|
||||
InputPath = mediaSource.Path,
|
||||
Protocol = mediaSource.Protocol,
|
||||
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
|
||||
ExtractChapters = false,
|
||||
AnalyzeDurationSections = 2
|
||||
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_logger.Info("Live tv media info probe took {0} seconds", (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
mediaSource.Bitrate = info.Bitrate;
|
||||
mediaSource.Container = info.Container;
|
||||
mediaSource.Formats = info.Formats;
|
||||
mediaSource.MediaStreams = info.MediaStreams;
|
||||
mediaSource.RunTimeTicks = info.RunTimeTicks;
|
||||
mediaSource.Size = info.Size;
|
||||
mediaSource.Timestamp = info.Timestamp;
|
||||
mediaSource.Video3DFormat = info.Video3DFormat;
|
||||
mediaSource.VideoType = info.VideoType;
|
||||
|
||||
mediaSource.DefaultSubtitleStreamIndex = null;
|
||||
|
||||
// Null this out so that it will be treated like a live stream
|
||||
if (!originalRuntime.HasValue)
|
||||
{
|
||||
mediaSource.RunTimeTicks = null;
|
||||
}
|
||||
|
||||
var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Audio);
|
||||
|
||||
if (audioStream == null || audioStream.Index == -1)
|
||||
{
|
||||
mediaSource.DefaultAudioStreamIndex = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
mediaSource.DefaultAudioStreamIndex = audioStream.Index;
|
||||
}
|
||||
|
||||
var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Video);
|
||||
if (videoStream != null)
|
||||
{
|
||||
if (!videoStream.BitRate.HasValue)
|
||||
{
|
||||
var width = videoStream.Width ?? 1920;
|
||||
|
||||
if (width >= 1900)
|
||||
{
|
||||
videoStream.BitRate = 8000000;
|
||||
}
|
||||
|
||||
else if (width >= 1260)
|
||||
{
|
||||
videoStream.BitRate = 3000000;
|
||||
}
|
||||
|
||||
else if (width >= 700)
|
||||
{
|
||||
videoStream.BitRate = 1000000;
|
||||
}
|
||||
}
|
||||
|
||||
// This is coming up false and preventing stream copy
|
||||
videoStream.IsAVC = null;
|
||||
}
|
||||
|
||||
// Try to estimate this
|
||||
if (!mediaSource.Bitrate.HasValue)
|
||||
{
|
||||
var total = mediaSource.MediaStreams.Select(i => i.BitRate ?? 0).Sum();
|
||||
|
||||
if (total > 0)
|
||||
{
|
||||
mediaSource.Bitrate = total;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -52,6 +53,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
PostPaddingSeconds = info.PostPaddingSeconds,
|
||||
IsPostPaddingRequired = info.IsPostPaddingRequired,
|
||||
IsPrePaddingRequired = info.IsPrePaddingRequired,
|
||||
KeepUntil = info.KeepUntil,
|
||||
ExternalChannelId = info.ChannelId,
|
||||
ExternalSeriesTimerId = info.SeriesTimerId,
|
||||
ServiceName = service.Name,
|
||||
@@ -71,6 +73,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
dto.ProgramInfo = _dtoService.GetBaseItemDto(program, new DtoOptions());
|
||||
|
||||
dto.ProgramInfo.TimerId = dto.Id;
|
||||
|
||||
dto.ProgramInfo.SeriesTimerId = dto.SeriesTimerId;
|
||||
}
|
||||
|
||||
@@ -100,6 +103,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
Priority = info.Priority,
|
||||
RecordAnyChannel = info.RecordAnyChannel,
|
||||
RecordAnyTime = info.RecordAnyTime,
|
||||
SkipEpisodesInLibrary = info.SkipEpisodesInLibrary,
|
||||
KeepUpTo = info.KeepUpTo,
|
||||
KeepUntil = info.KeepUntil,
|
||||
RecordNewOnly = info.RecordNewOnly,
|
||||
ExternalChannelId = info.ChannelId,
|
||||
ExternalProgramId = info.ProgramId,
|
||||
@@ -120,6 +126,34 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
|
||||
dto.DayPattern = info.Days == null ? null : GetDayPattern(info.Days);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(info.SeriesId))
|
||||
{
|
||||
var program = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
|
||||
ExternalSeriesId = info.SeriesId,
|
||||
Limit = 1,
|
||||
ImageTypes = new ImageType[] { ImageType.Primary }
|
||||
|
||||
}).FirstOrDefault();
|
||||
|
||||
if (program != null)
|
||||
{
|
||||
var image = program.GetImageInfo(ImageType.Primary, 0);
|
||||
if (image != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
dto.ParentPrimaryImageTag = _imageProcessor.GetImageCacheTag(program, image);
|
||||
dto.ParentPrimaryImageItemId = program.Id.ToString("N");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
@@ -244,6 +278,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
PostPaddingSeconds = dto.PostPaddingSeconds,
|
||||
IsPostPaddingRequired = dto.IsPostPaddingRequired,
|
||||
IsPrePaddingRequired = dto.IsPrePaddingRequired,
|
||||
KeepUntil = dto.KeepUntil,
|
||||
Priority = dto.Priority,
|
||||
SeriesTimerId = dto.ExternalSeriesTimerId,
|
||||
ProgramId = dto.ExternalProgramId,
|
||||
@@ -308,6 +343,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
Priority = dto.Priority,
|
||||
RecordAnyChannel = dto.RecordAnyChannel,
|
||||
RecordAnyTime = dto.RecordAnyTime,
|
||||
SkipEpisodesInLibrary = dto.SkipEpisodesInLibrary,
|
||||
KeepUpTo = dto.KeepUpTo,
|
||||
KeepUntil = dto.KeepUntil,
|
||||
RecordNewOnly = dto.RecordNewOnly,
|
||||
ProgramId = dto.ExternalProgramId,
|
||||
ChannelId = dto.ExternalChannelId,
|
||||
|
||||
@@ -62,9 +62,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
|
||||
private readonly List<ILiveTvService> _services = new List<ILiveTvService>();
|
||||
|
||||
private readonly ConcurrentDictionary<string, LiveStreamData> _openStreams =
|
||||
new ConcurrentDictionary<string, LiveStreamData>();
|
||||
|
||||
private readonly SemaphoreSlim _refreshRecordingsLock = new SemaphoreSlim(1, 1);
|
||||
|
||||
private readonly List<ITunerHost> _tunerHosts = new List<ITunerHost>();
|
||||
@@ -124,9 +121,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
foreach (var service in _services)
|
||||
{
|
||||
service.DataSourceChanged += service_DataSourceChanged;
|
||||
service.RecordingStatusChanged += Service_RecordingStatusChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void Service_RecordingStatusChanged(object sender, RecordingStatusChangedEventArgs e)
|
||||
{
|
||||
_lastRecordingRefreshTime = DateTime.MinValue;
|
||||
}
|
||||
|
||||
public List<ITunerHost> TunerHosts
|
||||
{
|
||||
get { return _tunerHosts; }
|
||||
@@ -151,112 +154,40 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
|
||||
var topFolder = await GetInternalLiveTvFolder(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var channels = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
var internalQuery = new InternalItemsQuery(user)
|
||||
{
|
||||
IsMovie = query.IsMovie,
|
||||
IsNews = query.IsNews,
|
||||
IsKids = query.IsKids,
|
||||
IsSports = query.IsSports,
|
||||
IsSeries = query.IsSeries,
|
||||
IncludeItemTypes = new[] { typeof(LiveTvChannel).Name },
|
||||
SortBy = new[] { ItemSortBy.SortName },
|
||||
TopParentIds = new[] { topFolder.Id.ToString("N") }
|
||||
SortOrder = query.SortOrder ?? SortOrder.Ascending,
|
||||
TopParentIds = new[] { topFolder.Id.ToString("N") },
|
||||
IsFavorite = query.IsFavorite,
|
||||
IsLiked = query.IsLiked,
|
||||
StartIndex = query.StartIndex,
|
||||
Limit = query.Limit
|
||||
};
|
||||
|
||||
}).Cast<LiveTvChannel>();
|
||||
internalQuery.OrderBy.AddRange(query.SortBy.Select(i => new Tuple<string, SortOrder>(i, query.SortOrder ?? SortOrder.Ascending)));
|
||||
|
||||
if (user != null)
|
||||
if (query.EnableFavoriteSorting)
|
||||
{
|
||||
// Avoid implicitly captured closure
|
||||
var currentUser = user;
|
||||
|
||||
channels = channels
|
||||
.Where(i => i.IsVisible(currentUser))
|
||||
.OrderBy(i =>
|
||||
{
|
||||
double number = 0;
|
||||
|
||||
if (!string.IsNullOrEmpty(i.Number))
|
||||
{
|
||||
double.TryParse(i.Number, out number);
|
||||
}
|
||||
|
||||
return number;
|
||||
|
||||
});
|
||||
|
||||
if (query.IsFavorite.HasValue)
|
||||
{
|
||||
var val = query.IsFavorite.Value;
|
||||
|
||||
channels = channels
|
||||
.Where(i => _userDataManager.GetUserData(user, i).IsFavorite == val);
|
||||
}
|
||||
|
||||
if (query.IsLiked.HasValue)
|
||||
{
|
||||
var val = query.IsLiked.Value;
|
||||
|
||||
channels = channels
|
||||
.Where(i =>
|
||||
{
|
||||
var likes = _userDataManager.GetUserData(user, i).Likes;
|
||||
|
||||
return likes.HasValue && likes.Value == val;
|
||||
});
|
||||
}
|
||||
|
||||
if (query.IsDisliked.HasValue)
|
||||
{
|
||||
var val = query.IsDisliked.Value;
|
||||
|
||||
channels = channels
|
||||
.Where(i =>
|
||||
{
|
||||
var likes = _userDataManager.GetUserData(user, i).Likes;
|
||||
|
||||
return likes.HasValue && likes.Value != val;
|
||||
});
|
||||
}
|
||||
internalQuery.OrderBy.Insert(0, new Tuple<string, SortOrder>(ItemSortBy.IsFavoriteOrLiked, SortOrder.Descending));
|
||||
}
|
||||
|
||||
var enableFavoriteSorting = query.EnableFavoriteSorting;
|
||||
|
||||
channels = channels.OrderBy(i =>
|
||||
if (!internalQuery.OrderBy.Any(i => string.Equals(i.Item1, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (enableFavoriteSorting)
|
||||
{
|
||||
var userData = _userDataManager.GetUserData(user, i);
|
||||
|
||||
if (userData.IsFavorite)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (userData.Likes.HasValue)
|
||||
{
|
||||
if (!userData.Likes.Value)
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 2;
|
||||
});
|
||||
|
||||
var allChannels = channels.ToList();
|
||||
IEnumerable<LiveTvChannel> allEnumerable = allChannels;
|
||||
|
||||
if (query.StartIndex.HasValue)
|
||||
{
|
||||
allEnumerable = allEnumerable.Skip(query.StartIndex.Value);
|
||||
internalQuery.OrderBy.Add(new Tuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending));
|
||||
}
|
||||
|
||||
if (query.Limit.HasValue)
|
||||
{
|
||||
allEnumerable = allEnumerable.Take(query.Limit.Value);
|
||||
}
|
||||
var channelResult = _libraryManager.GetItemsResult(internalQuery);
|
||||
|
||||
var result = new QueryResult<LiveTvChannel>
|
||||
{
|
||||
Items = allEnumerable.ToArray(),
|
||||
TotalRecordCount = allChannels.Count
|
||||
Items = channelResult.Items.Cast<LiveTvChannel>().ToArray(),
|
||||
TotalRecordCount = channelResult.TotalRecordCount
|
||||
};
|
||||
|
||||
return result;
|
||||
@@ -298,32 +229,32 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
return result.Items.FirstOrDefault();
|
||||
}
|
||||
|
||||
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
|
||||
|
||||
public async Task<MediaSourceInfo> GetRecordingStream(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
return await GetLiveStream(id, null, false, cancellationToken).ConfigureAwait(false);
|
||||
var info = await GetLiveStream(id, null, false, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return info.Item1;
|
||||
}
|
||||
|
||||
public async Task<MediaSourceInfo> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken)
|
||||
public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStream(string id, string mediaSourceId, CancellationToken cancellationToken)
|
||||
{
|
||||
return await GetLiveStream(id, mediaSourceId, true, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MediaSourceInfo>> GetRecordingMediaSources(string id, CancellationToken cancellationToken)
|
||||
public async Task<IEnumerable<MediaSourceInfo>> GetRecordingMediaSources(IHasMediaSources item, CancellationToken cancellationToken)
|
||||
{
|
||||
var item = await GetInternalRecording(id, cancellationToken).ConfigureAwait(false);
|
||||
var service = GetService(item);
|
||||
var baseItem = (BaseItem)item;
|
||||
var service = GetService(baseItem);
|
||||
|
||||
return await service.GetRecordingStreamMediaSources(id, cancellationToken).ConfigureAwait(false);
|
||||
return await service.GetRecordingStreamMediaSources(baseItem.ExternalId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(string id, CancellationToken cancellationToken)
|
||||
public async Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(IHasMediaSources item, CancellationToken cancellationToken)
|
||||
{
|
||||
var item = GetInternalChannel(id);
|
||||
var service = GetService(item);
|
||||
var baseItem = (LiveTvChannel)item;
|
||||
var service = GetService(baseItem);
|
||||
|
||||
var sources = await service.GetChannelStreamMediaSources(item.ExternalId, cancellationToken).ConfigureAwait(false);
|
||||
var sources = await service.GetChannelStreamMediaSources(baseItem.ExternalId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (sources.Count == 0)
|
||||
{
|
||||
@@ -334,7 +265,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
|
||||
foreach (var source in list)
|
||||
{
|
||||
Normalize(source, service, item.ChannelType == ChannelType.TV);
|
||||
Normalize(source, service, baseItem.ChannelType == ChannelType.TV);
|
||||
}
|
||||
|
||||
return list;
|
||||
@@ -355,79 +286,67 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
return _services.FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private async Task<MediaSourceInfo> GetLiveStream(string id, string mediaSourceId, bool isChannel, CancellationToken cancellationToken)
|
||||
private async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetLiveStream(string id, string mediaSourceId, bool isChannel, CancellationToken cancellationToken)
|
||||
{
|
||||
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (string.Equals(id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mediaSourceId = null;
|
||||
}
|
||||
|
||||
try
|
||||
MediaSourceInfo info;
|
||||
bool isVideo;
|
||||
ILiveTvService service;
|
||||
IDirectStreamProvider directStreamProvider = null;
|
||||
|
||||
if (isChannel)
|
||||
{
|
||||
MediaSourceInfo info;
|
||||
bool isVideo;
|
||||
ILiveTvService service;
|
||||
var channel = GetInternalChannel(id);
|
||||
isVideo = channel.ChannelType == ChannelType.TV;
|
||||
service = GetService(channel);
|
||||
_logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId);
|
||||
|
||||
if (isChannel)
|
||||
var supportsManagedStream = service as ISupportsDirectStreamProvider;
|
||||
if (supportsManagedStream != null)
|
||||
{
|
||||
var channel = GetInternalChannel(id);
|
||||
isVideo = channel.ChannelType == ChannelType.TV;
|
||||
service = GetService(channel);
|
||||
_logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId);
|
||||
info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
|
||||
info.RequiresClosing = true;
|
||||
|
||||
if (info.RequiresClosing)
|
||||
{
|
||||
var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_";
|
||||
|
||||
info.LiveStreamId = idPrefix + info.Id;
|
||||
}
|
||||
var streamInfo = await supportsManagedStream.GetChannelStreamWithDirectStreamProvider(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
|
||||
info = streamInfo.Item1;
|
||||
directStreamProvider = streamInfo.Item2;
|
||||
}
|
||||
else
|
||||
{
|
||||
var recording = await GetInternalRecording(id, cancellationToken).ConfigureAwait(false);
|
||||
isVideo = !string.Equals(recording.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase);
|
||||
service = GetService(recording);
|
||||
|
||||
_logger.Info("Opening recording stream from {0}, external recording Id: {1}", service.Name, recording.ExternalId);
|
||||
info = await service.GetRecordingStream(recording.ExternalId, null, cancellationToken).ConfigureAwait(false);
|
||||
info.RequiresClosing = true;
|
||||
|
||||
if (info.RequiresClosing)
|
||||
{
|
||||
var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_";
|
||||
|
||||
info.LiveStreamId = idPrefix + info.Id;
|
||||
}
|
||||
info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
info.RequiresClosing = true;
|
||||
|
||||
_logger.Info("Live stream info: {0}", _jsonSerializer.SerializeToString(info));
|
||||
Normalize(info, service, isVideo);
|
||||
|
||||
var data = new LiveStreamData
|
||||
if (info.RequiresClosing)
|
||||
{
|
||||
Info = info,
|
||||
IsChannel = isChannel,
|
||||
ItemId = id
|
||||
};
|
||||
var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_";
|
||||
|
||||
_openStreams.AddOrUpdate(info.Id, data, (key, i) => data);
|
||||
|
||||
return info;
|
||||
info.LiveStreamId = idPrefix + info.Id;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
else
|
||||
{
|
||||
_logger.ErrorException("Error getting channel stream", ex);
|
||||
var recording = await GetInternalRecording(id, cancellationToken).ConfigureAwait(false);
|
||||
isVideo = !string.Equals(recording.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase);
|
||||
service = GetService(recording);
|
||||
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_liveStreamSemaphore.Release();
|
||||
_logger.Info("Opening recording stream from {0}, external recording Id: {1}", service.Name, recording.ExternalId);
|
||||
info = await service.GetRecordingStream(recording.ExternalId, null, cancellationToken).ConfigureAwait(false);
|
||||
info.RequiresClosing = true;
|
||||
|
||||
if (info.RequiresClosing)
|
||||
{
|
||||
var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_";
|
||||
|
||||
info.LiveStreamId = idPrefix + info.Id;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Info("Live stream info: {0}", _jsonSerializer.SerializeToString(info));
|
||||
Normalize(info, service, isVideo);
|
||||
|
||||
return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info, directStreamProvider);
|
||||
}
|
||||
|
||||
private void Normalize(MediaSourceInfo mediaSource, ILiveTvService service, bool isVideo)
|
||||
@@ -628,11 +547,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
return item;
|
||||
}
|
||||
|
||||
private async Task<LiveTvProgram> GetProgram(ProgramInfo info, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken)
|
||||
private Tuple<LiveTvProgram, bool, bool> GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken)
|
||||
{
|
||||
var id = _tvDtoService.GetInternalProgramId(serviceName, info.Id);
|
||||
|
||||
var item = _libraryManager.GetItemById(id) as LiveTvProgram;
|
||||
LiveTvProgram item = null;
|
||||
allExistingPrograms.TryGetValue(id, out item);
|
||||
|
||||
var isNew = false;
|
||||
var forceUpdate = false;
|
||||
|
||||
@@ -649,6 +570,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
};
|
||||
}
|
||||
|
||||
var seriesId = info.SeriesId;
|
||||
|
||||
if (!item.ParentId.Equals(channel.Id))
|
||||
{
|
||||
forceUpdate = true;
|
||||
@@ -668,6 +591,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
|
||||
item.EpisodeTitle = info.EpisodeTitle;
|
||||
item.ExternalId = info.Id;
|
||||
item.ExternalSeriesIdLegacy = seriesId;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.Ordinal))
|
||||
{
|
||||
forceUpdate = true;
|
||||
}
|
||||
item.ExternalSeriesId = seriesId;
|
||||
|
||||
item.Genres = info.Genres;
|
||||
item.IsHD = info.IsHD;
|
||||
item.IsKids = info.IsKids;
|
||||
@@ -698,7 +629,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
item.HomePageUrl = info.HomePageUrl;
|
||||
|
||||
item.ProductionYear = info.ProductionYear;
|
||||
item.PremiereDate = info.OriginalAirDate;
|
||||
|
||||
if (!info.IsSeries || info.IsRepeat)
|
||||
{
|
||||
item.PremiereDate = info.OriginalAirDate;
|
||||
}
|
||||
|
||||
item.IndexNumber = info.EpisodeNumber;
|
||||
item.ParentIndexNumber = info.SeasonNumber;
|
||||
@@ -725,13 +660,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
}
|
||||
}
|
||||
|
||||
var isUpdated = false;
|
||||
if (isNew)
|
||||
{
|
||||
await _libraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else if (forceUpdate || string.IsNullOrWhiteSpace(info.Etag))
|
||||
{
|
||||
await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
|
||||
isUpdated = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -741,13 +676,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
if (!string.Equals(etag, item.ExternalEtag, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.ExternalEtag = etag;
|
||||
await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
|
||||
isUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
_providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(_fileSystem));
|
||||
|
||||
return item;
|
||||
return new Tuple<LiveTvProgram, bool, bool>(item, isNew, isUpdated);
|
||||
}
|
||||
|
||||
private async Task<Guid> CreateRecordingRecord(RecordingInfo info, string serviceName, Guid parentFolderId, CancellationToken cancellationToken)
|
||||
@@ -811,6 +744,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
recording.IsRepeat = info.IsRepeat;
|
||||
recording.IsSports = info.IsSports;
|
||||
recording.SeriesTimerId = info.SeriesTimerId;
|
||||
recording.TimerId = info.TimerId;
|
||||
recording.StartDate = info.StartDate;
|
||||
|
||||
if (!dataChanged)
|
||||
@@ -903,8 +837,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
|
||||
var dto = _dtoService.GetBaseItemDto(program, new DtoOptions(), user);
|
||||
|
||||
var list = new List<Tuple<BaseItemDto, string, string>>();
|
||||
list.Add(new Tuple<BaseItemDto, string, string>(dto, program.ServiceName, program.ExternalId));
|
||||
var list = new List<Tuple<BaseItemDto, string, string, string>>();
|
||||
list.Add(new Tuple<BaseItemDto, string, string, string>(dto, program.ServiceName, program.ExternalId, program.ExternalSeriesIdLegacy));
|
||||
|
||||
await AddRecordingInfo(list, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -932,17 +866,41 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
MaxStartDate = query.MaxStartDate,
|
||||
ChannelIds = query.ChannelIds,
|
||||
IsMovie = query.IsMovie,
|
||||
IsSeries = query.IsSeries,
|
||||
IsSports = query.IsSports,
|
||||
IsKids = query.IsKids,
|
||||
IsNews = query.IsNews,
|
||||
Genres = query.Genres,
|
||||
StartIndex = query.StartIndex,
|
||||
Limit = query.Limit,
|
||||
SortBy = query.SortBy,
|
||||
SortOrder = query.SortOrder ?? SortOrder.Ascending,
|
||||
EnableTotalRecordCount = query.EnableTotalRecordCount,
|
||||
TopParentIds = new[] { topFolder.Id.ToString("N") }
|
||||
TopParentIds = new[] { topFolder.Id.ToString("N") },
|
||||
DtoOptions = options
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.SeriesTimerId))
|
||||
{
|
||||
var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery { }, cancellationToken).ConfigureAwait(false);
|
||||
var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.ServiceName, i.Id).ToString("N"), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase));
|
||||
if (seriesTimer != null)
|
||||
{
|
||||
internalQuery.ExternalSeriesId = seriesTimer.SeriesId;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId))
|
||||
{
|
||||
// Better to return nothing than every program in the database
|
||||
return new QueryResult<BaseItemDto>();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Better to return nothing than every program in the database
|
||||
return new QueryResult<BaseItemDto>();
|
||||
}
|
||||
}
|
||||
|
||||
if (query.HasAired.HasValue)
|
||||
{
|
||||
if (query.HasAired.Value)
|
||||
@@ -970,7 +928,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<QueryResult<LiveTvProgram>> GetRecommendedProgramsInternal(RecommendedProgramQuery query, CancellationToken cancellationToken)
|
||||
public async Task<QueryResult<LiveTvProgram>> GetRecommendedProgramsInternal(RecommendedProgramQuery query, DtoOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = _userManager.GetUserById(query.UserId);
|
||||
|
||||
@@ -980,12 +938,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
{
|
||||
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name },
|
||||
IsAiring = query.IsAiring,
|
||||
IsNews = query.IsNews,
|
||||
IsMovie = query.IsMovie,
|
||||
IsSeries = query.IsSeries,
|
||||
IsSports = query.IsSports,
|
||||
IsKids = query.IsKids,
|
||||
EnableTotalRecordCount = query.EnableTotalRecordCount,
|
||||
SortBy = new[] { ItemSortBy.StartDate },
|
||||
TopParentIds = new[] { topFolder.Id.ToString("N") }
|
||||
TopParentIds = new[] { topFolder.Id.ToString("N") },
|
||||
DtoOptions = options
|
||||
};
|
||||
|
||||
if (query.Limit.HasValue)
|
||||
@@ -1009,9 +970,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
|
||||
var programList = programs.ToList();
|
||||
|
||||
var factorChannelWatchCount = (query.IsAiring ?? false) || (query.IsKids ?? false) || (query.IsSports ?? false) || (query.IsMovie ?? false);
|
||||
var factorChannelWatchCount = (query.IsAiring ?? false) || (query.IsKids ?? false) || (query.IsSports ?? false) || (query.IsMovie ?? false) || (query.IsNews ?? false) || (query.IsSeries ?? false);
|
||||
|
||||
programs = programList.OrderBy(i => i.HasImage(ImageType.Primary) ? 0 : 1)
|
||||
programs = programList.OrderBy(i => i.StartDate.Date)
|
||||
.ThenByDescending(i => GetRecommendationScore(i, user.Id, factorChannelWatchCount))
|
||||
.ThenBy(i => i.StartDate);
|
||||
|
||||
@@ -1035,7 +996,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
|
||||
public async Task<QueryResult<BaseItemDto>> GetRecommendedPrograms(RecommendedProgramQuery query, DtoOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
var internalResult = await GetRecommendedProgramsInternal(query, cancellationToken).ConfigureAwait(false);
|
||||
var internalResult = await GetRecommendedProgramsInternal(query, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var user = _userManager.GetUserById(query.UserId);
|
||||
|
||||
@@ -1092,15 +1053,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
return score;
|
||||
}
|
||||
|
||||
private async Task AddRecordingInfo(IEnumerable<Tuple<BaseItemDto, string, string>> programs, CancellationToken cancellationToken)
|
||||
private async Task AddRecordingInfo(IEnumerable<Tuple<BaseItemDto, string, string, string>> programs, CancellationToken cancellationToken)
|
||||
{
|
||||
var timers = new Dictionary<string, List<TimerInfo>>();
|
||||
var seriesTimers = new Dictionary<string, List<SeriesTimerInfo>>();
|
||||
|
||||
foreach (var programTuple in programs)
|
||||
{
|
||||
var program = programTuple.Item1;
|
||||
var serviceName = programTuple.Item2;
|
||||
var externalProgramId = programTuple.Item3;
|
||||
string externalSeriesId = programTuple.Item4;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(serviceName))
|
||||
{
|
||||
@@ -1123,18 +1086,54 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
}
|
||||
|
||||
var timer = timerList.FirstOrDefault(i => string.Equals(i.ProgramId, externalProgramId, StringComparison.OrdinalIgnoreCase));
|
||||
var foundSeriesTimer = false;
|
||||
|
||||
if (timer != null)
|
||||
{
|
||||
program.TimerId = _tvDtoService.GetInternalTimerId(serviceName, timer.Id)
|
||||
.ToString("N");
|
||||
if (timer.Status != RecordingStatus.Cancelled && timer.Status != RecordingStatus.Error)
|
||||
{
|
||||
program.TimerId = _tvDtoService.GetInternalTimerId(serviceName, timer.Id)
|
||||
.ToString("N");
|
||||
|
||||
program.Status = timer.Status.ToString();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(timer.SeriesTimerId))
|
||||
{
|
||||
program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(serviceName, timer.SeriesTimerId)
|
||||
.ToString("N");
|
||||
|
||||
foundSeriesTimer = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundSeriesTimer || string.IsNullOrWhiteSpace(externalSeriesId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
List<SeriesTimerInfo> seriesTimerList;
|
||||
if (!seriesTimers.TryGetValue(serviceName, out seriesTimerList))
|
||||
{
|
||||
try
|
||||
{
|
||||
var tempTimers = await GetService(serviceName).GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
|
||||
seriesTimers[serviceName] = seriesTimerList = tempTimers.ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error getting series timer infos", ex);
|
||||
seriesTimers[serviceName] = seriesTimerList = new List<SeriesTimerInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
var seriesTimer = seriesTimerList.FirstOrDefault(i => string.Equals(i.SeriesId, externalSeriesId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (seriesTimer != null)
|
||||
{
|
||||
program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(serviceName, seriesTimer.Id)
|
||||
.ToString("N");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1267,14 +1266,96 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
var start = DateTime.UtcNow.AddHours(-1);
|
||||
var end = start.AddDays(guideDays);
|
||||
|
||||
var isMovie = false;
|
||||
var isSports = false;
|
||||
var isNews = false;
|
||||
var isKids = false;
|
||||
var iSSeries = false;
|
||||
|
||||
var channelPrograms = await service.GetProgramsAsync(currentChannel.ExternalId, start, end, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
|
||||
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name },
|
||||
ChannelIds = new string[] { currentChannel.Id.ToString("N") }
|
||||
|
||||
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
|
||||
|
||||
var newPrograms = new List<LiveTvProgram>();
|
||||
var updatedPrograms = new List<LiveTvProgram>();
|
||||
|
||||
foreach (var program in channelPrograms)
|
||||
{
|
||||
var programItem = await GetProgram(program, currentChannel, currentChannel.ChannelType, service.Name, cancellationToken).ConfigureAwait(false);
|
||||
var programTuple = GetProgram(program, existingPrograms, currentChannel, currentChannel.ChannelType, service.Name, cancellationToken);
|
||||
var programItem = programTuple.Item1;
|
||||
|
||||
if (programTuple.Item2)
|
||||
{
|
||||
newPrograms.Add(programItem);
|
||||
}
|
||||
else if (programTuple.Item3)
|
||||
{
|
||||
updatedPrograms.Add(programItem);
|
||||
}
|
||||
|
||||
programs.Add(programItem.Id);
|
||||
|
||||
if (program.IsMovie)
|
||||
{
|
||||
isMovie = true;
|
||||
}
|
||||
|
||||
if (program.IsSeries)
|
||||
{
|
||||
iSSeries = true;
|
||||
}
|
||||
|
||||
if (program.IsSports)
|
||||
{
|
||||
isSports = true;
|
||||
}
|
||||
|
||||
if (program.IsNews)
|
||||
{
|
||||
isNews = true;
|
||||
}
|
||||
|
||||
if (program.IsKids)
|
||||
{
|
||||
isKids = true;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("Channel {0} has {1} new programs and {2} updated programs", currentChannel.Name, newPrograms.Count, updatedPrograms.Count);
|
||||
|
||||
if (newPrograms.Count > 0)
|
||||
{
|
||||
await _libraryManager.CreateItems(newPrograms, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// TODO: Do this in bulk
|
||||
foreach (var program in updatedPrograms)
|
||||
{
|
||||
await _libraryManager.UpdateItem(program, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
foreach (var program in newPrograms)
|
||||
{
|
||||
_providerManager.QueueRefresh(program.Id, new MetadataRefreshOptions(_fileSystem));
|
||||
}
|
||||
foreach (var program in updatedPrograms)
|
||||
{
|
||||
_providerManager.QueueRefresh(program.Id, new MetadataRefreshOptions(_fileSystem));
|
||||
}
|
||||
|
||||
currentChannel.IsMovie = isMovie;
|
||||
currentChannel.IsNews = isNews;
|
||||
currentChannel.IsSports = isSports;
|
||||
currentChannel.IsKids = isKids;
|
||||
currentChannel.IsSeries = iSSeries;
|
||||
|
||||
await currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -1361,7 +1442,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
private DateTime _lastRecordingRefreshTime;
|
||||
private async Task RefreshRecordings(CancellationToken cancellationToken)
|
||||
{
|
||||
const int cacheMinutes = 5;
|
||||
const int cacheMinutes = 3;
|
||||
|
||||
if ((DateTime.UtcNow - _lastRecordingRefreshTime).TotalMinutes < cacheMinutes)
|
||||
{
|
||||
@@ -1409,9 +1490,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
}
|
||||
}
|
||||
|
||||
private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, User user)
|
||||
private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, DtoOptions dtoOptions, User user)
|
||||
{
|
||||
if (user == null || (query.IsInProgress ?? false))
|
||||
if (user == null)
|
||||
{
|
||||
return new QueryResult<BaseItem>();
|
||||
}
|
||||
|
||||
if ((query.IsInProgress ?? false))
|
||||
{
|
||||
return new QueryResult<BaseItem>();
|
||||
}
|
||||
@@ -1485,7 +1571,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
EnableTotalRecordCount = query.EnableTotalRecordCount,
|
||||
IncludeItemTypes = includeItemTypes.ToArray(),
|
||||
ExcludeItemTypes = excludeItemTypes.ToArray(),
|
||||
Genres = genres.ToArray()
|
||||
Genres = genres.ToArray(),
|
||||
DtoOptions = dtoOptions
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1556,9 +1643,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
return new QueryResult<BaseItem>();
|
||||
}
|
||||
|
||||
if (_services.Count == 1)
|
||||
if (_services.Count == 1 && !(query.IsInProgress ?? false))
|
||||
{
|
||||
return GetEmbyRecordings(query, user);
|
||||
return GetEmbyRecordings(query, new DtoOptions(), user);
|
||||
}
|
||||
|
||||
await RefreshRecordings(cancellationToken).ConfigureAwait(false);
|
||||
@@ -1609,6 +1696,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
recordings = recordings.Where(i => i.IsMovie == val);
|
||||
}
|
||||
|
||||
if (query.IsNews.HasValue)
|
||||
{
|
||||
var val = query.IsNews.Value;
|
||||
recordings = recordings.Where(i => i.IsNews == val);
|
||||
}
|
||||
|
||||
if (query.IsSeries.HasValue)
|
||||
{
|
||||
var val = query.IsSeries.Value;
|
||||
@@ -1659,7 +1752,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
|
||||
public async Task AddInfoToProgramDto(List<Tuple<BaseItem, BaseItemDto>> tuples, List<ItemFields> fields, User user = null)
|
||||
{
|
||||
var recordingTuples = new List<Tuple<BaseItemDto, string, string>>();
|
||||
var recordingTuples = new List<Tuple<BaseItemDto, string, string, string>>();
|
||||
|
||||
foreach (var tuple in tuples)
|
||||
{
|
||||
@@ -1727,7 +1820,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
dto.ServiceName = serviceName;
|
||||
}
|
||||
|
||||
recordingTuples.Add(new Tuple<BaseItemDto, string, string>(dto, serviceName, program.ExternalId));
|
||||
recordingTuples.Add(new Tuple<BaseItemDto, string, string, string>(dto, serviceName, program.ExternalId, program.ExternalSeriesIdLegacy));
|
||||
}
|
||||
|
||||
await AddRecordingInfo(recordingTuples, CancellationToken.None).ConfigureAwait(false);
|
||||
@@ -1746,6 +1839,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
? null
|
||||
: _tvDtoService.GetInternalSeriesTimerId(service.Name, info.SeriesTimerId).ToString("N");
|
||||
|
||||
dto.TimerId = string.IsNullOrEmpty(info.TimerId)
|
||||
? null
|
||||
: _tvDtoService.GetInternalTimerId(service.Name, info.TimerId).ToString("N");
|
||||
|
||||
dto.StartDate = info.StartDate;
|
||||
dto.RecordingStatus = info.Status;
|
||||
dto.IsRepeat = info.IsRepeat;
|
||||
@@ -1842,6 +1939,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
}
|
||||
}
|
||||
|
||||
if (query.IsScheduled.HasValue)
|
||||
{
|
||||
if (query.IsScheduled.Value)
|
||||
{
|
||||
timers = timers.Where(i => i.Item1.Status == RecordingStatus.New);
|
||||
}
|
||||
else
|
||||
{
|
||||
timers = timers.Where(i => !(i.Item1.Status == RecordingStatus.New));
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(query.ChannelId))
|
||||
{
|
||||
var guid = new Guid(query.ChannelId);
|
||||
@@ -2002,6 +2111,56 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private async Task<QueryResult<SeriesTimerInfo>> GetSeriesTimersInternal(SeriesTimerQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var tasks = _services.Select(async i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var recs = await i.GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
|
||||
return recs.Select(r =>
|
||||
{
|
||||
r.ServiceName = i.Name;
|
||||
return new Tuple<SeriesTimerInfo, ILiveTvService>(r, i);
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error getting recordings", ex);
|
||||
return new List<Tuple<SeriesTimerInfo, ILiveTvService>>();
|
||||
}
|
||||
});
|
||||
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
var timers = results.SelectMany(i => i.ToList());
|
||||
|
||||
if (string.Equals(query.SortBy, "Priority", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
timers = query.SortOrder == SortOrder.Descending ?
|
||||
timers.OrderBy(i => i.Item1.Priority).ThenByStringDescending(i => i.Item1.Name) :
|
||||
timers.OrderByDescending(i => i.Item1.Priority).ThenByString(i => i.Item1.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
timers = query.SortOrder == SortOrder.Descending ?
|
||||
timers.OrderByStringDescending(i => i.Item1.Name) :
|
||||
timers.OrderByString(i => i.Item1.Name);
|
||||
}
|
||||
|
||||
var returnArray = timers
|
||||
.Select(i =>
|
||||
{
|
||||
return i.Item1;
|
||||
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
return new QueryResult<SeriesTimerInfo>
|
||||
{
|
||||
Items = returnArray,
|
||||
TotalRecordCount = returnArray.Length
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<QueryResult<SeriesTimerInfoDto>> GetSeriesTimers(SeriesTimerQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var tasks = _services.Select(async i =>
|
||||
@@ -2146,6 +2305,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
|
||||
var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false);
|
||||
|
||||
info.RecordAnyChannel = true;
|
||||
info.RecordAnyTime = true;
|
||||
info.Days = new List<DayOfWeek>
|
||||
{
|
||||
DayOfWeek.Sunday,
|
||||
DayOfWeek.Monday,
|
||||
DayOfWeek.Tuesday,
|
||||
DayOfWeek.Wednesday,
|
||||
DayOfWeek.Thursday,
|
||||
DayOfWeek.Friday,
|
||||
DayOfWeek.Saturday
|
||||
};
|
||||
|
||||
info.Id = null;
|
||||
|
||||
return new Tuple<SeriesTimerInfo, ILiveTvService>(info, service);
|
||||
@@ -2399,47 +2571,22 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
};
|
||||
}
|
||||
|
||||
class LiveStreamData
|
||||
public async Task CloseLiveStream(string id)
|
||||
{
|
||||
internal MediaSourceInfo Info;
|
||||
internal string ItemId;
|
||||
internal bool IsChannel;
|
||||
}
|
||||
var parts = id.Split(new[] { '_' }, 2);
|
||||
|
||||
public async Task CloseLiveStream(string id, CancellationToken cancellationToken)
|
||||
{
|
||||
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N"), parts[0], StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
try
|
||||
if (service == null)
|
||||
{
|
||||
var parts = id.Split(new[] { '_' }, 2);
|
||||
|
||||
var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N"), parts[0], StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (service == null)
|
||||
{
|
||||
throw new ArgumentException("Service not found.");
|
||||
}
|
||||
|
||||
id = parts[1];
|
||||
|
||||
LiveStreamData data;
|
||||
_openStreams.TryRemove(id, out data);
|
||||
|
||||
_logger.Info("Closing live stream from {0}, stream Id: {1}", service.Name, id);
|
||||
|
||||
await service.CloseLiveStream(id, cancellationToken).ConfigureAwait(false);
|
||||
throw new ArgumentException("Service not found.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error closing live stream", ex);
|
||||
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_liveStreamSemaphore.Release();
|
||||
}
|
||||
id = parts[1];
|
||||
|
||||
_logger.Info("Closing live stream from {0}, stream Id: {1}", service.Name, id);
|
||||
|
||||
await service.CloseLiveStream(id, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public GuideInfo GetGuideInfo()
|
||||
@@ -2462,7 +2609,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private readonly object _disposeLock = new object();
|
||||
private bool _isDisposed = false;
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
@@ -2473,18 +2619,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
if (dispose)
|
||||
{
|
||||
_isDisposed = true;
|
||||
|
||||
lock (_disposeLock)
|
||||
{
|
||||
foreach (var stream in _openStreams.Values.ToList())
|
||||
{
|
||||
var task = CloseLiveStream(stream.Info.Id, CancellationToken.None);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
|
||||
_openStreams.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2620,7 +2754,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
|
||||
public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
|
||||
{
|
||||
info = (TunerHostInfo)_jsonSerializer.DeserializeFromString(_jsonSerializer.SerializeToString(info), typeof(TunerHostInfo));
|
||||
info = _jsonSerializer.DeserializeFromString<TunerHostInfo>(_jsonSerializer.SerializeToString(info));
|
||||
|
||||
var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
@@ -2661,7 +2795,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
|
||||
public async Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings)
|
||||
{
|
||||
info = (ListingsProviderInfo)_jsonSerializer.DeserializeFromString(_jsonSerializer.SerializeToString(info), typeof(ListingsProviderInfo));
|
||||
info = _jsonSerializer.DeserializeFromString< ListingsProviderInfo>(_jsonSerializer.SerializeToString(info));
|
||||
|
||||
var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
@@ -2803,7 +2937,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
feature = "embytvseriesrecordings";
|
||||
}
|
||||
|
||||
if (string.Equals(feature, "dvr", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(feature, "dvr-l", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var config = GetConfiguration();
|
||||
if (config.TunerHosts.Count(i => i.IsEnabled) > 0 &&
|
||||
|
||||
@@ -9,9 +9,11 @@ using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
{
|
||||
@@ -63,12 +65,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
{
|
||||
if (item is ILiveTvRecording)
|
||||
{
|
||||
sources = await _liveTvManager.GetRecordingMediaSources(item.Id.ToString("N"), cancellationToken)
|
||||
sources = await _liveTvManager.GetRecordingMediaSources(item, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
sources = await _liveTvManager.GetChannelMediaSources(item.Id.ToString("N"), cancellationToken)
|
||||
sources = await _liveTvManager.GetChannelMediaSources(item, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -116,17 +118,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
|
||||
public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> OpenMediaSource(string openToken, CancellationToken cancellationToken)
|
||||
{
|
||||
MediaSourceInfo stream;
|
||||
MediaSourceInfo stream = null;
|
||||
const bool isAudio = false;
|
||||
|
||||
var keys = openToken.Split(new[] { StreamIdDelimeter }, 3);
|
||||
var mediaSourceId = keys.Length >= 3 ? keys[2] : null;
|
||||
IDirectStreamProvider directStreamProvider = null;
|
||||
|
||||
if (string.Equals(keys[0], typeof(LiveTvChannel).Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false);
|
||||
var info = await _liveTvManager.GetChannelStream(keys[1], mediaSourceId, cancellationToken).ConfigureAwait(false);
|
||||
stream = info.Item1;
|
||||
directStreamProvider = info.Item2;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -135,14 +140,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
|
||||
try
|
||||
{
|
||||
await AddMediaInfo(stream, isAudio, cancellationToken).ConfigureAwait(false);
|
||||
if (stream.MediaStreams.Any(i => i.Index != -1))
|
||||
{
|
||||
await AddMediaInfo(stream, isAudio, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error probing live tv stream", ex);
|
||||
}
|
||||
|
||||
return stream;
|
||||
return new Tuple<MediaSourceInfo, IDirectStreamProvider>(stream, directStreamProvider);
|
||||
}
|
||||
|
||||
private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)
|
||||
@@ -204,9 +216,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv
|
||||
}
|
||||
}
|
||||
|
||||
public Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken)
|
||||
public Task CloseMediaSource(string liveStreamId)
|
||||
{
|
||||
return _liveTvManager.CloseLiveStream(liveStreamId, cancellationToken);
|
||||
return _liveTvManager.CloseLiveStream(liveStreamId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ using MediaBrowser.Model.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
@@ -17,7 +19,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
public abstract class BaseTunerHost
|
||||
{
|
||||
protected readonly IConfigurationManager Config;
|
||||
protected readonly IServerConfigurationManager Config;
|
||||
protected readonly ILogger Logger;
|
||||
protected IJsonSerializer JsonSerializer;
|
||||
protected readonly IMediaEncoder MediaEncoder;
|
||||
@@ -25,7 +27,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
private readonly ConcurrentDictionary<string, ChannelCache> _channelCache =
|
||||
new ConcurrentDictionary<string, ChannelCache>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
protected BaseTunerHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder)
|
||||
protected BaseTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
Config = config;
|
||||
Logger = logger;
|
||||
@@ -71,7 +73,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ChannelInfo>> GetChannels(CancellationToken cancellationToken)
|
||||
public async Task<IEnumerable<ChannelInfo>> GetChannels(bool enableCache, CancellationToken cancellationToken)
|
||||
{
|
||||
var list = new List<ChannelInfo>();
|
||||
|
||||
@@ -81,7 +83,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
try
|
||||
{
|
||||
var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
|
||||
var channels = await GetChannels(host, enableCache, cancellationToken).ConfigureAwait(false);
|
||||
var newChannels = channels.Where(i => !list.Any(l => string.Equals(i.Id, l.Id, StringComparison.OrdinalIgnoreCase))).ToList();
|
||||
|
||||
list.AddRange(newChannels);
|
||||
@@ -124,12 +126,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
|
||||
foreach (var host in hostsWithChannel)
|
||||
{
|
||||
var resourcePool = GetLock(host.Url);
|
||||
Logger.Debug("GetChannelStreamMediaSources - Waiting on tuner resource pool");
|
||||
|
||||
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
Logger.Debug("GetChannelStreamMediaSources - Unlocked resource pool");
|
||||
|
||||
try
|
||||
{
|
||||
// Check to make sure the tuner is available
|
||||
@@ -155,89 +151,63 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
Logger.Error("Error opening tuner", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
resourcePool.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new List<MediaSourceInfo>();
|
||||
}
|
||||
|
||||
protected abstract Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken);
|
||||
protected abstract Task<LiveStream> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken);
|
||||
|
||||
public async Task<Tuple<MediaSourceInfo, SemaphoreSlim>> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
|
||||
public async Task<LiveStream> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (IsValidChannelId(channelId))
|
||||
if (!IsValidChannelId(channelId))
|
||||
{
|
||||
var hosts = GetTunerHosts();
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
var hostsWithChannel = new List<TunerHostInfo>();
|
||||
var hosts = GetTunerHosts();
|
||||
|
||||
foreach (var host in hosts)
|
||||
var hostsWithChannel = new List<TunerHostInfo>();
|
||||
|
||||
foreach (var host in hosts)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(streamId))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(streamId))
|
||||
{
|
||||
try
|
||||
{
|
||||
var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
hostsWithChannel.Add(host);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Error getting channels", ex);
|
||||
}
|
||||
}
|
||||
else if (streamId.StartsWith(host.Id, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hostsWithChannel = new List<TunerHostInfo> { host };
|
||||
streamId = streamId.Substring(host.Id.Length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var host in hostsWithChannel)
|
||||
{
|
||||
var resourcePool = GetLock(host.Url);
|
||||
Logger.Debug("GetChannelStream - Waiting on tuner resource pool");
|
||||
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
Logger.Debug("GetChannelStream - Unlocked resource pool");
|
||||
try
|
||||
{
|
||||
// Check to make sure the tuner is available
|
||||
// If there's only one tuner, don't bother with the check and just let the tuner be the one to throw an error
|
||||
// If a streamId is specified then availibility has already been checked in GetChannelStreamMediaSources
|
||||
if (string.IsNullOrWhiteSpace(streamId) && hostsWithChannel.Count > 1)
|
||||
var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
if (!await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
Logger.Error("Tuner is not currently available");
|
||||
resourcePool.Release();
|
||||
continue;
|
||||
}
|
||||
hostsWithChannel.Add(host);
|
||||
}
|
||||
|
||||
var stream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (EnableMediaProbing)
|
||||
{
|
||||
await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return new Tuple<MediaSourceInfo, SemaphoreSlim>(stream, resourcePool);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Error opening tuner", ex);
|
||||
|
||||
resourcePool.Release();
|
||||
Logger.Error("Error getting channels", ex);
|
||||
}
|
||||
}
|
||||
else if (streamId.StartsWith(host.Id, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hostsWithChannel = new List<TunerHostInfo> { host };
|
||||
streamId = streamId.Substring(host.Id.Length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var host in hostsWithChannel)
|
||||
{
|
||||
try
|
||||
{
|
||||
var liveStream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false);
|
||||
await liveStream.Open(cancellationToken).ConfigureAwait(false);
|
||||
return liveStream;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error("Error opening tuner", ex);
|
||||
}
|
||||
}
|
||||
|
||||
throw new LiveTvConflictException();
|
||||
@@ -263,117 +233,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
|
||||
protected abstract Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// The _semaphoreLocks
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks = new ConcurrentDictionary<string, SemaphoreSlim>(StringComparer.OrdinalIgnoreCase);
|
||||
/// <summary>
|
||||
/// Gets the lock.
|
||||
/// </summary>
|
||||
/// <param name="url">The filename.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
private SemaphoreSlim GetLock(string url)
|
||||
{
|
||||
return _semaphoreLocks.GetOrAdd(url, key => new SemaphoreSlim(1, 1));
|
||||
}
|
||||
|
||||
private async Task AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio, SemaphoreSlim resourcePool, CancellationToken cancellationToken)
|
||||
{
|
||||
await resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await AddMediaInfoInternal(mediaSource, isAudio, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Leave the resource locked. it will be released upstream
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Release the resource if there's some kind of failure.
|
||||
resourcePool.Release();
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddMediaInfoInternal(MediaSourceInfo mediaSource, bool isAudio, CancellationToken cancellationToken)
|
||||
{
|
||||
var originalRuntime = mediaSource.RunTimeTicks;
|
||||
|
||||
var info = await MediaEncoder.GetMediaInfo(new MediaInfoRequest
|
||||
{
|
||||
InputPath = mediaSource.Path,
|
||||
Protocol = mediaSource.Protocol,
|
||||
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
|
||||
ExtractChapters = false
|
||||
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
mediaSource.Bitrate = info.Bitrate;
|
||||
mediaSource.Container = info.Container;
|
||||
mediaSource.Formats = info.Formats;
|
||||
mediaSource.MediaStreams = info.MediaStreams;
|
||||
mediaSource.RunTimeTicks = info.RunTimeTicks;
|
||||
mediaSource.Size = info.Size;
|
||||
mediaSource.Timestamp = info.Timestamp;
|
||||
mediaSource.Video3DFormat = info.Video3DFormat;
|
||||
mediaSource.VideoType = info.VideoType;
|
||||
|
||||
mediaSource.DefaultSubtitleStreamIndex = null;
|
||||
|
||||
// Null this out so that it will be treated like a live stream
|
||||
if (!originalRuntime.HasValue)
|
||||
{
|
||||
mediaSource.RunTimeTicks = null;
|
||||
}
|
||||
|
||||
var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Audio);
|
||||
|
||||
if (audioStream == null || audioStream.Index == -1)
|
||||
{
|
||||
mediaSource.DefaultAudioStreamIndex = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
mediaSource.DefaultAudioStreamIndex = audioStream.Index;
|
||||
}
|
||||
|
||||
var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == Model.Entities.MediaStreamType.Video);
|
||||
if (videoStream != null)
|
||||
{
|
||||
if (!videoStream.BitRate.HasValue)
|
||||
{
|
||||
var width = videoStream.Width ?? 1920;
|
||||
|
||||
if (width >= 1900)
|
||||
{
|
||||
videoStream.BitRate = 8000000;
|
||||
}
|
||||
|
||||
else if (width >= 1260)
|
||||
{
|
||||
videoStream.BitRate = 3000000;
|
||||
}
|
||||
|
||||
else if (width >= 700)
|
||||
{
|
||||
videoStream.BitRate = 1000000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to estimate this
|
||||
if (!mediaSource.Bitrate.HasValue)
|
||||
{
|
||||
var total = mediaSource.MediaStreams.Select(i => i.BitRate ?? 0).Sum();
|
||||
|
||||
if (total > 0)
|
||||
{
|
||||
mediaSource.Bitrate = total;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract bool IsValidChannelId(string channelId);
|
||||
|
||||
protected LiveTvOptions GetConfiguration()
|
||||
|
||||
@@ -10,6 +10,7 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
@@ -39,13 +40,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
_deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered;
|
||||
}
|
||||
|
||||
void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e)
|
||||
void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
{
|
||||
string server = null;
|
||||
if (e.Headers.TryGetValue("SERVER", out server) && server.IndexOf("HDHomeRun", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
var info = e.Argument;
|
||||
|
||||
if (info.Headers.TryGetValue("SERVER", out server) && server.IndexOf("HDHomeRun", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
string location;
|
||||
if (e.Headers.TryGetValue("Location", out location))
|
||||
if (info.Headers.TryGetValue("Location", out location))
|
||||
{
|
||||
//_logger.Debug("HdHomerun found at {0}", location);
|
||||
|
||||
@@ -85,7 +88,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
using (var stream = await _httpClient.Get(new HttpRequestOptions
|
||||
{
|
||||
Url = string.Format("{0}/discover.json", url),
|
||||
CancellationToken = CancellationToken.None
|
||||
CancellationToken = CancellationToken.None,
|
||||
BufferContent = false
|
||||
}))
|
||||
{
|
||||
var response = _json.DeserializeFromStream<HdHomerunHost.DiscoverResponse>(stream);
|
||||
|
||||
@@ -14,7 +14,10 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommonIO;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Net;
|
||||
@@ -24,11 +27,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
|
||||
{
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
public HdHomerunHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient)
|
||||
public HdHomerunHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IHttpClient httpClient, IFileSystem fileSystem, IServerApplicationHost appHost)
|
||||
: base(config, logger, jsonSerializer, mediaEncoder)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_fileSystem = fileSystem;
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
public string Name
|
||||
@@ -60,20 +67,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
return id;
|
||||
}
|
||||
|
||||
public string ApplyDuration(string streamPath, TimeSpan duration)
|
||||
{
|
||||
streamPath += streamPath.IndexOf('?') == -1 ? "?" : "&";
|
||||
streamPath += "duration=" + Convert.ToInt32(duration.TotalSeconds).ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
return streamPath;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
Url = string.Format("{0}/lineup.json", GetApiUrl(info, false)),
|
||||
CancellationToken = cancellationToken
|
||||
CancellationToken = cancellationToken,
|
||||
BufferContent = false
|
||||
};
|
||||
using (var stream = await _httpClient.Get(options))
|
||||
{
|
||||
@@ -105,8 +105,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
|
||||
private async Task<string> GetModelInfo(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
lock (_modelCache)
|
||||
{
|
||||
DiscoverResponse response;
|
||||
if (_modelCache.TryGetValue(info.Url, out response))
|
||||
{
|
||||
return response.ModelNumber;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using (var stream = await _httpClient.Get(new HttpRequestOptions()
|
||||
@@ -115,11 +125,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
CancellationToken = cancellationToken,
|
||||
CacheLength = TimeSpan.FromDays(1),
|
||||
CacheMode = CacheMode.Unconditional,
|
||||
TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds)
|
||||
TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds),
|
||||
BufferContent = false
|
||||
}))
|
||||
{
|
||||
var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
|
||||
|
||||
lock (_modelCache)
|
||||
{
|
||||
_modelCache[info.Id] = response;
|
||||
}
|
||||
|
||||
return response.ModelNumber;
|
||||
}
|
||||
}
|
||||
@@ -127,8 +143,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
var defaultValue = "HDHR";
|
||||
// HDHR4 doesn't have this api
|
||||
return "HDHR";
|
||||
lock (_modelCache)
|
||||
{
|
||||
_modelCache[info.Id] = new DiscoverResponse
|
||||
{
|
||||
ModelNumber = defaultValue
|
||||
};
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
throw;
|
||||
@@ -143,7 +167,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
Url = string.Format("{0}/tuners.html", GetApiUrl(info, false)),
|
||||
CancellationToken = cancellationToken,
|
||||
TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds)
|
||||
TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds),
|
||||
BufferContent = false
|
||||
}))
|
||||
{
|
||||
var tuners = new List<LiveTvTunerInfo>();
|
||||
@@ -319,18 +344,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
videoBitrate = 1000000;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(videoCodec))
|
||||
var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false);
|
||||
var channel = channels.FirstOrDefault(i => string.Equals(i.Number, channelId, StringComparison.OrdinalIgnoreCase));
|
||||
if (channel != null)
|
||||
{
|
||||
var channels = await GetChannels(info, true, CancellationToken.None).ConfigureAwait(false);
|
||||
var channel = channels.FirstOrDefault(i => string.Equals(i.Number, channelId, StringComparison.OrdinalIgnoreCase));
|
||||
if (channel != null)
|
||||
if (string.IsNullOrWhiteSpace(videoCodec))
|
||||
{
|
||||
videoCodec = channel.VideoCodec;
|
||||
audioCodec = channel.AudioCodec;
|
||||
|
||||
videoBitrate = (channel.IsHD ?? true) ? 15000000 : 2000000;
|
||||
audioBitrate = (channel.IsHD ?? true) ? 448000 : 192000;
|
||||
}
|
||||
audioCodec = channel.AudioCodec;
|
||||
|
||||
if (!videoBitrate.HasValue)
|
||||
{
|
||||
videoBitrate = (channel.IsHD ?? true) ? 15000000 : 2000000;
|
||||
}
|
||||
audioBitrate = (channel.IsHD ?? true) ? 448000 : 192000;
|
||||
}
|
||||
|
||||
// normalize
|
||||
@@ -352,6 +380,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
url += "?transcode=" + profile;
|
||||
}
|
||||
|
||||
var id = profile;
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
id = "native";
|
||||
}
|
||||
id += "_" + url.GetMD5().ToString("N");
|
||||
|
||||
var mediaSource = new MediaSourceInfo
|
||||
{
|
||||
Path = url,
|
||||
@@ -380,14 +415,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
BitRate = audioBitrate
|
||||
}
|
||||
},
|
||||
RequiresOpening = false,
|
||||
RequiresOpening = true,
|
||||
RequiresClosing = false,
|
||||
BufferMs = 0,
|
||||
Container = "ts",
|
||||
Id = profile,
|
||||
SupportsDirectPlay = true,
|
||||
SupportsDirectStream = false,
|
||||
SupportsTranscoding = true
|
||||
Id = id,
|
||||
SupportsDirectPlay = false,
|
||||
SupportsDirectStream = true,
|
||||
SupportsTranscoding = true,
|
||||
IsInfiniteStream = true
|
||||
};
|
||||
|
||||
return mediaSource;
|
||||
@@ -417,18 +453,21 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
|
||||
try
|
||||
{
|
||||
string model = await GetModelInfo(info, cancellationToken).ConfigureAwait(false);
|
||||
model = model ?? string.Empty;
|
||||
|
||||
if (info.AllowHWTranscoding && (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1))
|
||||
if (info.AllowHWTranscoding)
|
||||
{
|
||||
list.Add(await GetMediaSource(info, hdhrId, "heavy").ConfigureAwait(false));
|
||||
string model = await GetModelInfo(info, cancellationToken).ConfigureAwait(false);
|
||||
model = model ?? string.Empty;
|
||||
|
||||
list.Add(await GetMediaSource(info, hdhrId, "internet540").ConfigureAwait(false));
|
||||
list.Add(await GetMediaSource(info, hdhrId, "internet480").ConfigureAwait(false));
|
||||
list.Add(await GetMediaSource(info, hdhrId, "internet360").ConfigureAwait(false));
|
||||
list.Add(await GetMediaSource(info, hdhrId, "internet240").ConfigureAwait(false));
|
||||
list.Add(await GetMediaSource(info, hdhrId, "mobile").ConfigureAwait(false));
|
||||
if ((model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1))
|
||||
{
|
||||
list.Add(await GetMediaSource(info, hdhrId, "heavy").ConfigureAwait(false));
|
||||
|
||||
list.Add(await GetMediaSource(info, hdhrId, "internet540").ConfigureAwait(false));
|
||||
list.Add(await GetMediaSource(info, hdhrId, "internet480").ConfigureAwait(false));
|
||||
list.Add(await GetMediaSource(info, hdhrId, "internet360").ConfigureAwait(false));
|
||||
list.Add(await GetMediaSource(info, hdhrId, "internet240").ConfigureAwait(false));
|
||||
list.Add(await GetMediaSource(info, hdhrId, "mobile").ConfigureAwait(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -449,9 +488,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
|
||||
protected override async Task<LiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.Info("GetChannelStream: channel id: {0}. stream id: {1}", channelId, streamId ?? string.Empty);
|
||||
var profile = streamId.Split('_')[0];
|
||||
|
||||
Logger.Info("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelId, streamId, profile);
|
||||
|
||||
if (!channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -459,7 +500,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
}
|
||||
var hdhrId = GetHdHrIdFromChannelId(channelId);
|
||||
|
||||
return await GetMediaSource(info, hdhrId, streamId).ConfigureAwait(false);
|
||||
var mediaSource = await GetMediaSource(info, hdhrId, profile).ConfigureAwait(false);
|
||||
|
||||
var liveStream = new HdHomerunLiveStream(mediaSource, streamId, _fileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost);
|
||||
liveStream.EnableStreamSharing = true;
|
||||
return liveStream;
|
||||
}
|
||||
|
||||
public async Task Validate(TunerHostInfo info)
|
||||
@@ -469,13 +514,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_modelCache)
|
||||
{
|
||||
_modelCache.Clear();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Test it by pulling down the lineup
|
||||
using (var stream = await _httpClient.Get(new HttpRequestOptions
|
||||
{
|
||||
Url = string.Format("{0}/discover.json", GetApiUrl(info, false)),
|
||||
CancellationToken = CancellationToken.None
|
||||
CancellationToken = CancellationToken.None,
|
||||
BufferContent = false
|
||||
}))
|
||||
{
|
||||
var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream);
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommonIO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
public class HdHomerunLiveStream : LiveStream, IDirectStreamProvider
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IServerApplicationPaths _appPaths;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
private readonly CancellationTokenSource _liveStreamCancellationTokenSource = new CancellationTokenSource();
|
||||
private readonly TaskCompletionSource<bool> _liveStreamTaskCompletionSource = new TaskCompletionSource<bool>();
|
||||
private readonly MulticastStream _multicastStream;
|
||||
|
||||
|
||||
public HdHomerunLiveStream(MediaSourceInfo mediaSource, string originalStreamId, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost)
|
||||
: base(mediaSource)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_appPaths = appPaths;
|
||||
_appHost = appHost;
|
||||
OriginalStreamId = originalStreamId;
|
||||
_multicastStream = new MulticastStream(_logger);
|
||||
}
|
||||
|
||||
protected override async Task OpenInternal(CancellationToken openCancellationToken)
|
||||
{
|
||||
_liveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested();
|
||||
|
||||
var mediaSource = OriginalMediaSource;
|
||||
|
||||
var url = mediaSource.Path;
|
||||
|
||||
_logger.Info("Opening HDHR Live stream from {0}", url);
|
||||
|
||||
var taskCompletionSource = new TaskCompletionSource<bool>();
|
||||
|
||||
StartStreaming(url, taskCompletionSource, _liveStreamCancellationTokenSource.Token);
|
||||
|
||||
//OpenedMediaSource.Protocol = MediaProtocol.File;
|
||||
//OpenedMediaSource.Path = tempFile;
|
||||
//OpenedMediaSource.ReadAtNativeFramerate = true;
|
||||
|
||||
OpenedMediaSource.Path = _appHost.GetLocalApiUrl("localhost") + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
|
||||
OpenedMediaSource.Protocol = MediaProtocol.Http;
|
||||
OpenedMediaSource.SupportsDirectPlay = false;
|
||||
OpenedMediaSource.SupportsDirectStream = true;
|
||||
OpenedMediaSource.SupportsTranscoding = true;
|
||||
|
||||
await taskCompletionSource.Task.ConfigureAwait(false);
|
||||
|
||||
//await Task.Delay(5000).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override Task Close()
|
||||
{
|
||||
_logger.Info("Closing HDHR live stream");
|
||||
_liveStreamCancellationTokenSource.Cancel();
|
||||
|
||||
return _liveStreamTaskCompletionSource.Task;
|
||||
}
|
||||
|
||||
private async Task StartStreaming(string url, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
var isFirstAttempt = true;
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var response = await _httpClient.SendAsync(new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = cancellationToken,
|
||||
BufferContent = false
|
||||
|
||||
}, "GET").ConfigureAwait(false))
|
||||
{
|
||||
_logger.Info("Opened HDHR stream from {0}", url);
|
||||
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.Info("Beginning multicastStream.CopyUntilCancelled");
|
||||
|
||||
Action onStarted = null;
|
||||
if (isFirstAttempt)
|
||||
{
|
||||
onStarted = () => openTaskCompletionSource.TrySetResult(true);
|
||||
}
|
||||
|
||||
await _multicastStream.CopyUntilCancelled(response.Content, onStarted, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (isFirstAttempt)
|
||||
{
|
||||
_logger.ErrorException("Error opening live stream:", ex);
|
||||
openTaskCompletionSource.TrySetException(ex);
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.ErrorException("Error copying live stream, will reopen", ex);
|
||||
}
|
||||
|
||||
isFirstAttempt = false;
|
||||
}
|
||||
|
||||
_liveStreamTaskCompletionSource.TrySetResult(true);
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task CopyToAsync(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
return _multicastStream.CopyToAsync(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,10 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommonIO;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
@@ -23,7 +25,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
public M3UTunerHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient)
|
||||
public M3UTunerHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient)
|
||||
: base(config, logger, jsonSerializer, mediaEncoder)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
@@ -63,11 +65,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
return Task.FromResult(list);
|
||||
}
|
||||
|
||||
protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
|
||||
protected override async Task<LiveStream> GetChannelStream(TunerHostInfo info, string channelId, string streamId, CancellationToken cancellationToken)
|
||||
{
|
||||
var sources = await GetChannelStreamMediaSources(info, channelId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return sources.First();
|
||||
var liveStream = new LiveStream(sources.First());
|
||||
return liveStream;
|
||||
}
|
||||
|
||||
public async Task Validate(TunerHostInfo info)
|
||||
@@ -136,7 +139,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
RequiresOpening = false,
|
||||
RequiresClosing = false,
|
||||
|
||||
ReadAtNativeFramerate = false
|
||||
ReadAtNativeFramerate = false,
|
||||
|
||||
Id = channel.Path.GetMD5().ToString("N"),
|
||||
IsInfiniteStream = true
|
||||
};
|
||||
|
||||
return new List<MediaSourceInfo> { mediaSource };
|
||||
@@ -148,10 +154,5 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public string ApplyDuration(string streamPath, TimeSpan duration)
|
||||
{
|
||||
return streamPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Logging;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
public class MulticastStream
|
||||
{
|
||||
private readonly List<QueueStream> _outputStreams = new List<QueueStream>();
|
||||
private const int BufferSize = 81920;
|
||||
private CancellationToken _cancellationToken;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public MulticastStream(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task CopyUntilCancelled(Stream source, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
_cancellationToken = cancellationToken;
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
byte[] buffer = new byte[BufferSize];
|
||||
|
||||
var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (bytesRead > 0)
|
||||
{
|
||||
byte[] copy = new byte[bytesRead];
|
||||
Buffer.BlockCopy(buffer, 0, copy, 0, bytesRead);
|
||||
|
||||
List<QueueStream> streams = null;
|
||||
|
||||
lock (_outputStreams)
|
||||
{
|
||||
streams = _outputStreams.ToList();
|
||||
}
|
||||
|
||||
foreach (var stream in streams)
|
||||
{
|
||||
stream.Queue(copy);
|
||||
}
|
||||
|
||||
if (onStarted != null)
|
||||
{
|
||||
var onStartedCopy = onStarted;
|
||||
onStarted = null;
|
||||
Task.Run(onStartedCopy);
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task CopyToAsync(Stream stream)
|
||||
{
|
||||
var result = new QueueStream(stream, _logger)
|
||||
{
|
||||
OnFinished = OnFinished
|
||||
};
|
||||
|
||||
lock (_outputStreams)
|
||||
{
|
||||
_outputStreams.Add(result);
|
||||
}
|
||||
|
||||
result.Start(_cancellationToken);
|
||||
|
||||
return result.TaskCompletion.Task;
|
||||
}
|
||||
|
||||
public void RemoveOutputStream(QueueStream stream)
|
||||
{
|
||||
lock (_outputStreams)
|
||||
{
|
||||
_outputStreams.Remove(stream);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFinished(QueueStream queueStream)
|
||||
{
|
||||
RemoveOutputStream(queueStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Logging;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
public class QueueStream
|
||||
{
|
||||
private readonly Stream _outputStream;
|
||||
private readonly ConcurrentQueue<byte[]> _queue = new ConcurrentQueue<byte[]>();
|
||||
private CancellationToken _cancellationToken;
|
||||
public TaskCompletionSource<bool> TaskCompletion { get; private set; }
|
||||
|
||||
public Action<QueueStream> OnFinished { get; set; }
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public QueueStream(Stream outputStream, ILogger logger)
|
||||
{
|
||||
_outputStream = outputStream;
|
||||
_logger = logger;
|
||||
TaskCompletion = new TaskCompletionSource<bool>();
|
||||
}
|
||||
|
||||
public void Queue(byte[] bytes)
|
||||
{
|
||||
_queue.Enqueue(bytes);
|
||||
}
|
||||
|
||||
public void Start(CancellationToken cancellationToken)
|
||||
{
|
||||
_cancellationToken = cancellationToken;
|
||||
Task.Run(() => StartInternal());
|
||||
}
|
||||
|
||||
private byte[] Dequeue()
|
||||
{
|
||||
byte[] bytes;
|
||||
if (_queue.TryDequeue(out bytes))
|
||||
{
|
||||
return bytes;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task StartInternal()
|
||||
{
|
||||
var cancellationToken = _cancellationToken;
|
||||
|
||||
try
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var bytes = Dequeue();
|
||||
if (bytes != null)
|
||||
{
|
||||
await _outputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
TaskCompletion.TrySetResult(true);
|
||||
_logger.Debug("QueueStream complete");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Debug("QueueStream cancelled");
|
||||
TaskCompletion.TrySetCanceled();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error in QueueStream", ex);
|
||||
TaskCompletion.TrySetException(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (OnFinished != null)
|
||||
{
|
||||
OnFinished(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using System.Xml.Linq;
|
||||
using MediaBrowser.Model.Events;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
|
||||
{
|
||||
@@ -50,18 +51,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
|
||||
_deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered;
|
||||
}
|
||||
|
||||
void _deviceDiscovery_DeviceDiscovered(object sender, SsdpMessageEventArgs e)
|
||||
void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
{
|
||||
var info = e.Argument;
|
||||
|
||||
string st = null;
|
||||
string nt = null;
|
||||
e.Headers.TryGetValue("ST", out st);
|
||||
e.Headers.TryGetValue("NT", out nt);
|
||||
info.Headers.TryGetValue("ST", out st);
|
||||
info.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))
|
||||
if (info.Headers.TryGetValue("Location", out location) && !string.IsNullOrWhiteSpace(location))
|
||||
{
|
||||
_logger.Debug("SAT IP found at {0}", location);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using CommonIO;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dto;
|
||||
@@ -16,6 +17,7 @@ using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Server.Implementations.LiveTv.EmbyTV;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
|
||||
{
|
||||
@@ -24,7 +26,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
public SatIpHost(IConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient)
|
||||
public SatIpHost(IServerConfigurationManager config, ILogger logger, IJsonSerializer jsonSerializer, IMediaEncoder mediaEncoder, IFileSystem fileSystem, IHttpClient httpClient)
|
||||
: base(config, logger, jsonSerializer, mediaEncoder)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
@@ -113,11 +115,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp
|
||||
return new List<MediaSourceInfo>();
|
||||
}
|
||||
|
||||
protected override async Task<MediaSourceInfo> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken)
|
||||
protected override async Task<LiveStream> GetChannelStream(TunerHostInfo tuner, string channelId, string streamId, CancellationToken cancellationToken)
|
||||
{
|
||||
var sources = await GetChannelStreamMediaSources(tuner, channelId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return sources.First();
|
||||
var liveStream = new LiveStream(sources.First());
|
||||
|
||||
return liveStream;
|
||||
}
|
||||
|
||||
protected override async Task<bool> IsAvailableInternal(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken)
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
"NotificationOptionTaskFailed": "Scheduled task failure",
|
||||
"NotificationOptionInstallationFailed": "Installation failure",
|
||||
"NotificationOptionNewLibraryContent": "New content added",
|
||||
"NotificationOptionNewLibraryContentMultiple": "New content added (multiple)",
|
||||
"NotificationOptionCameraImageUploaded": "Camera image uploaded",
|
||||
"NotificationOptionUserLockedOut": "User locked out",
|
||||
"NotificationOptionServerRestartRequired": "Server restart required",
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
DE-0,1
|
||||
FSK-0,1
|
||||
DE-6,5
|
||||
FSK-6,5
|
||||
DE-12,7
|
||||
FSK-12,7
|
||||
DE-16,8
|
||||
FSK-16,8
|
||||
DE-18,9
|
||||
FSK-18,9
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
@@ -11,9 +11,10 @@
|
||||
<AssemblyName>MediaBrowser.Server.Implementations</AssemblyName>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion>
|
||||
<ReleaseVersion>
|
||||
</ReleaseVersion>
|
||||
<TargetFrameworkProfile />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
@@ -69,12 +70,12 @@
|
||||
<Reference Include="ServiceStack.Api.Swagger">
|
||||
<HintPath>..\ThirdParty\ServiceStack\ServiceStack.Api.Swagger.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SimpleInjector, Version=3.2.0.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\SimpleInjector.3.2.0\lib\net45\SimpleInjector.dll</HintPath>
|
||||
<Reference Include="SimpleInjector, Version=3.2.2.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\SimpleInjector.3.2.2\lib\net45\SimpleInjector.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="SocketHttpListener, Version=1.0.6063.4624, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\SocketHttpListener.1.0.0.39\lib\net45\SocketHttpListener.dll</HintPath>
|
||||
<Reference Include="SocketHttpListener, Version=1.0.6109.26162, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\SocketHttpListener.1.0.0.40\lib\net45\SocketHttpListener.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
@@ -105,9 +106,6 @@
|
||||
<Reference Include="UniversalDetector">
|
||||
<HintPath>..\ThirdParty\UniversalDetector\UniversalDetector.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Mono.Nat">
|
||||
<HintPath>..\packages\Mono.Nat.1.2.24.0\lib\net40\Mono.Nat.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SharedVersion.cs">
|
||||
@@ -164,7 +162,6 @@
|
||||
<Compile Include="HttpServer\HttpListenerHost.cs" />
|
||||
<Compile Include="HttpServer\HttpResultFactory.cs" />
|
||||
<Compile Include="HttpServer\LoggerUtils.cs" />
|
||||
<Compile Include="HttpServer\NativeWebSocket.cs" />
|
||||
<Compile Include="HttpServer\RangeRequestWriter.cs" />
|
||||
<Compile Include="HttpServer\ResponseFilter.cs" />
|
||||
<Compile Include="HttpServer\Security\AuthService.cs" />
|
||||
@@ -237,6 +234,7 @@
|
||||
<Compile Include="LiveTv\EmbyTV\TimerManager.cs" />
|
||||
<Compile Include="LiveTv\Listings\SchedulesDirect.cs" />
|
||||
<Compile Include="LiveTv\Listings\XmlTvListingsProvider.cs" />
|
||||
<Compile Include="LiveTv\LiveStreamHelper.cs" />
|
||||
<Compile Include="LiveTv\LiveTvConfigurationFactory.cs" />
|
||||
<Compile Include="LiveTv\LiveTvDtoService.cs" />
|
||||
<Compile Include="LiveTv\LiveTvManager.cs" />
|
||||
@@ -244,11 +242,14 @@
|
||||
<Compile Include="LiveTv\TunerHosts\BaseTunerHost.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunHost.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunDiscovery.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\HdHomerun\HdHomerunLiveStream.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\M3uParser.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\M3UTunerHost.cs" />
|
||||
<Compile Include="LiveTv\ProgramImageProvider.cs" />
|
||||
<Compile Include="LiveTv\RecordingImageProvider.cs" />
|
||||
<Compile Include="LiveTv\RefreshChannelsScheduledTask.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\MulticastStream.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\QueueStream.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\SatIp\ChannelScan.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\SatIp\Rtcp\ReportBlock.cs" />
|
||||
<Compile Include="LiveTv\TunerHosts\SatIp\Rtcp\RtcpAppPacket.cs" />
|
||||
@@ -390,6 +391,10 @@
|
||||
<Project>{7EEEB4BB-F3E8-48FC-B4C5-70F0FFF8329B}</Project>
|
||||
<Name>MediaBrowser.Model</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Mono.Nat\Mono.Nat.csproj">
|
||||
<Project>{d7453b88-2266-4805-b39b-2b5a2a33e1ba}</Project>
|
||||
<Name>Mono.Nat</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Localization\Ratings\us.txt" />
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
||||
}
|
||||
|
||||
var libraryOptions = _libraryManager.GetLibraryOptions(video);
|
||||
if (libraryOptions != null && libraryOptions.SchemaVersion >= 2)
|
||||
if (libraryOptions != null)
|
||||
{
|
||||
if (!libraryOptions.EnableChapterImageExtraction)
|
||||
{
|
||||
@@ -70,29 +70,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
||||
}
|
||||
else
|
||||
{
|
||||
var options = _chapterManager.GetConfiguration();
|
||||
|
||||
if (video is Movie)
|
||||
{
|
||||
if (!options.EnableMovieChapterImageExtraction)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (video is Episode)
|
||||
{
|
||||
if (!options.EnableEpisodeChapterImageExtraction)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!options.EnableOtherVideoChapterImageExtraction)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Can't extract images if there are no video streams
|
||||
@@ -160,7 +138,9 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder
|
||||
|
||||
_fileSystem.CreateDirectory(Path.GetDirectoryName(path));
|
||||
|
||||
var tempFile = await _encoder.ExtractVideoImage(inputPath, protocol, video.Video3DFormat, time, cancellationToken).ConfigureAwait(false);
|
||||
var container = video.Container;
|
||||
|
||||
var tempFile = await _encoder.ExtractVideoImage(inputPath, container, protocol, video.Video3DFormat, time, cancellationToken).ConfigureAwait(false);
|
||||
File.Copy(tempFile, path, true);
|
||||
|
||||
try
|
||||
|
||||
@@ -81,7 +81,8 @@ namespace MediaBrowser.Server.Implementations.News
|
||||
{
|
||||
Url = "http://emby.media/community/index.php?/blog/rss/1-media-browser-developers-blog",
|
||||
Progress = new Progress<double>(),
|
||||
UserAgent = "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.42 Safari/537.36"
|
||||
UserAgent = "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.42 Safari/537.36",
|
||||
BufferContent = false
|
||||
};
|
||||
|
||||
using (var stream = await _httpClient.Get(requestOptions).ConfigureAwait(false))
|
||||
|
||||
@@ -89,13 +89,6 @@ namespace MediaBrowser.Server.Implementations.Notifications
|
||||
Variables = new List<string>{"Name"}
|
||||
},
|
||||
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.NewLibraryContentMultiple.ToString(),
|
||||
DefaultTitle = "{ItemCount} new items have been added to your media library.",
|
||||
Variables = new List<string>{"ItemCount"}
|
||||
},
|
||||
|
||||
new NotificationTypeInfo
|
||||
{
|
||||
Type = NotificationType.AudioPlayback.ToString(),
|
||||
|
||||
@@ -4,6 +4,7 @@ using MediaBrowser.Model.Serialization;
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using MediaBrowser.Common.IO;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Persistence
|
||||
{
|
||||
@@ -51,18 +52,16 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
/// <summary>
|
||||
/// Gets a stream from a DataReader at a given ordinal
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <param name="ordinal">The ordinal.</param>
|
||||
/// <returns>Stream.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">reader</exception>
|
||||
public static Stream GetMemoryStream(this IDataReader reader, int ordinal)
|
||||
public static Stream GetMemoryStream(this IDataReader reader, int ordinal, IMemoryStreamProvider streamProvider)
|
||||
{
|
||||
if (reader == null)
|
||||
{
|
||||
throw new ArgumentNullException("reader");
|
||||
}
|
||||
|
||||
var memoryStream = new MemoryStream();
|
||||
var memoryStream = streamProvider.CreateNew();
|
||||
var num = 0L;
|
||||
var array = new byte[4096];
|
||||
long bytes;
|
||||
@@ -132,18 +131,16 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
/// <summary>
|
||||
/// Serializes to bytes.
|
||||
/// </summary>
|
||||
/// <param name="json">The json.</param>
|
||||
/// <param name="obj">The obj.</param>
|
||||
/// <returns>System.Byte[][].</returns>
|
||||
/// <exception cref="System.ArgumentNullException">obj</exception>
|
||||
public static byte[] SerializeToBytes(this IJsonSerializer json, object obj)
|
||||
public static byte[] SerializeToBytes(this IJsonSerializer json, object obj, IMemoryStreamProvider streamProvider)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
throw new ArgumentNullException("obj");
|
||||
}
|
||||
|
||||
using (var stream = new MemoryStream())
|
||||
using (var stream = streamProvider.CreateNew())
|
||||
{
|
||||
json.SerializeToStream(obj, stream);
|
||||
return stream.ToArray();
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Data;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.IO;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Persistence
|
||||
{
|
||||
@@ -18,10 +19,13 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
/// </summary>
|
||||
public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository
|
||||
{
|
||||
public SqliteDisplayPreferencesRepository(ILogManager logManager, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, IDbConnector dbConnector)
|
||||
private readonly IMemoryStreamProvider _memoryStreamProvider;
|
||||
|
||||
public SqliteDisplayPreferencesRepository(ILogManager logManager, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, IDbConnector dbConnector, IMemoryStreamProvider memoryStreamProvider)
|
||||
: base(logManager, dbConnector)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_memoryStreamProvider = memoryStreamProvider;
|
||||
DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db");
|
||||
}
|
||||
|
||||
@@ -82,7 +86,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var serialized = _jsonSerializer.SerializeToBytes(displayPreferences);
|
||||
var serialized = _jsonSerializer.SerializeToBytes(displayPreferences, _memoryStreamProvider);
|
||||
|
||||
using (var connection = await CreateConnection().ConfigureAwait(false))
|
||||
{
|
||||
@@ -166,7 +170,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
foreach (var displayPreference in displayPreferences)
|
||||
{
|
||||
|
||||
var serialized = _jsonSerializer.SerializeToBytes(displayPreference);
|
||||
var serialized = _jsonSerializer.SerializeToBytes(displayPreference, _memoryStreamProvider);
|
||||
|
||||
using (var cmd = connection.CreateCommand())
|
||||
{
|
||||
@@ -246,7 +250,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
{
|
||||
if (reader.Read())
|
||||
{
|
||||
using (var stream = reader.GetMemoryStream(0))
|
||||
using (var stream = reader.GetMemoryStream(0, _memoryStreamProvider))
|
||||
{
|
||||
return _jsonSerializer.DeserializeFromStream<DisplayPreferences>(stream);
|
||||
}
|
||||
@@ -283,7 +287,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
using (var stream = reader.GetMemoryStream(0))
|
||||
using (var stream = reader.GetMemoryStream(0, _memoryStreamProvider))
|
||||
{
|
||||
list.Add(_jsonSerializer.DeserializeFromStream<DisplayPreferences>(stream));
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ using System.Data;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.IO;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Persistence
|
||||
{
|
||||
@@ -18,10 +19,12 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
public class SqliteUserRepository : BaseSqliteRepository, IUserRepository
|
||||
{
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IMemoryStreamProvider _memoryStreamProvider;
|
||||
|
||||
public SqliteUserRepository(ILogManager logManager, IServerApplicationPaths appPaths, IJsonSerializer jsonSerializer, IDbConnector dbConnector) : base(logManager, dbConnector)
|
||||
public SqliteUserRepository(ILogManager logManager, IServerApplicationPaths appPaths, IJsonSerializer jsonSerializer, IDbConnector dbConnector, IMemoryStreamProvider memoryStreamProvider) : base(logManager, dbConnector)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_memoryStreamProvider = memoryStreamProvider;
|
||||
|
||||
DbFilePath = Path.Combine(appPaths.DataPath, "users.db");
|
||||
}
|
||||
@@ -75,7 +78,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var serialized = _jsonSerializer.SerializeToBytes(user);
|
||||
var serialized = _jsonSerializer.SerializeToBytes(user, _memoryStreamProvider);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -150,7 +153,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
|
||||
{
|
||||
var id = reader.GetGuid(0);
|
||||
|
||||
using (var stream = reader.GetMemoryStream(1))
|
||||
using (var stream = reader.GetMemoryStream(1, _memoryStreamProvider))
|
||||
{
|
||||
var user = _jsonSerializer.DeserializeFromStream<User>(stream);
|
||||
user.Id = id;
|
||||
|
||||
@@ -5,6 +5,8 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using CommonIO;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Playlists
|
||||
{
|
||||
@@ -17,7 +19,7 @@ namespace MediaBrowser.Server.Implementations.Playlists
|
||||
|
||||
public override bool IsVisible(User user)
|
||||
{
|
||||
return base.IsVisible(user) && GetChildren(user, false).Any();
|
||||
return base.IsVisible(user) && GetChildren(user, true).Any();
|
||||
}
|
||||
|
||||
protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
|
||||
@@ -37,6 +39,12 @@ namespace MediaBrowser.Server.Implementations.Playlists
|
||||
{
|
||||
get { return Model.Entities.CollectionType.Playlists; }
|
||||
}
|
||||
|
||||
protected override Task<QueryResult<BaseItem>> GetItemsInternal(InternalItemsQuery query)
|
||||
{
|
||||
query.Recursive = false;
|
||||
return base.GetItemsInternal(query);
|
||||
}
|
||||
}
|
||||
|
||||
public class PlaylistsDynamicFolder : IVirtualFolderCreator
|
||||
|
||||
@@ -13,6 +13,7 @@ using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.IO;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.ServerManager
|
||||
{
|
||||
@@ -72,6 +73,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager
|
||||
private readonly List<IWebSocketListener> _webSocketListeners = new List<IWebSocketListener>();
|
||||
|
||||
private bool _disposed;
|
||||
private readonly IMemoryStreamProvider _memoryStreamProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerManager" /> class.
|
||||
@@ -81,7 +83,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="configurationManager">The configuration manager.</param>
|
||||
/// <exception cref="System.ArgumentNullException">applicationHost</exception>
|
||||
public ServerManager(IServerApplicationHost applicationHost, IJsonSerializer jsonSerializer, ILogger logger, IServerConfigurationManager configurationManager)
|
||||
public ServerManager(IServerApplicationHost applicationHost, IJsonSerializer jsonSerializer, ILogger logger, IServerConfigurationManager configurationManager, IMemoryStreamProvider memoryStreamProvider)
|
||||
{
|
||||
if (applicationHost == null)
|
||||
{
|
||||
@@ -100,6 +102,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_applicationHost = applicationHost;
|
||||
ConfigurationManager = configurationManager;
|
||||
_memoryStreamProvider = memoryStreamProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -150,7 +153,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager
|
||||
return;
|
||||
}
|
||||
|
||||
var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger)
|
||||
var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger, _memoryStreamProvider)
|
||||
{
|
||||
OnReceive = ProcessWebSocketMessageReceived,
|
||||
Url = e.Url,
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Collections.Specialized;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.IO;
|
||||
using UniversalDetector;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.ServerManager
|
||||
@@ -19,7 +20,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager
|
||||
public class WebSocketConnection : IWebSocketConnection
|
||||
{
|
||||
public event EventHandler<EventArgs> Closed;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The _socket
|
||||
/// </summary>
|
||||
@@ -35,11 +36,6 @@ namespace MediaBrowser.Server.Implementations.ServerManager
|
||||
/// </summary>
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
/// <summary>
|
||||
/// The _send semaphore
|
||||
/// </summary>
|
||||
private readonly SemaphoreSlim _sendSemaphore = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// The logger
|
||||
/// </summary>
|
||||
@@ -78,7 +74,8 @@ namespace MediaBrowser.Server.Implementations.ServerManager
|
||||
/// </summary>
|
||||
/// <value>The query string.</value>
|
||||
public NameValueCollection QueryString { get; set; }
|
||||
|
||||
private readonly IMemoryStreamProvider _memoryStreamProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
|
||||
/// </summary>
|
||||
@@ -87,7 +84,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager
|
||||
/// <param name="jsonSerializer">The json serializer.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <exception cref="System.ArgumentNullException">socket</exception>
|
||||
public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger)
|
||||
public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger, IMemoryStreamProvider memoryStreamProvider)
|
||||
{
|
||||
if (socket == null)
|
||||
{
|
||||
@@ -113,6 +110,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager
|
||||
_socket.OnReceive = OnReceiveInternal;
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
_logger = logger;
|
||||
_memoryStreamProvider = memoryStreamProvider;
|
||||
|
||||
socket.Closed += socket_Closed;
|
||||
}
|
||||
@@ -149,7 +147,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var ms = new MemoryStream(bytes))
|
||||
using (var ms = _memoryStreamProvider.CreateNew(bytes))
|
||||
{
|
||||
var detector = new CharsetDetector();
|
||||
detector.Feed(ms);
|
||||
@@ -207,7 +205,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager
|
||||
_logger.ErrorException("Error processing web socket message", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message asynchronously.
|
||||
/// </summary>
|
||||
@@ -234,7 +232,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager
|
||||
/// <param name="buffer">The buffer.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task SendAsync(byte[] buffer, CancellationToken cancellationToken)
|
||||
public Task SendAsync(byte[] buffer, CancellationToken cancellationToken)
|
||||
{
|
||||
if (buffer == null)
|
||||
{
|
||||
@@ -243,33 +241,10 @@ namespace MediaBrowser.Server.Implementations.ServerManager
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Per msdn docs, attempting to send simultaneous messages will result in one failing.
|
||||
// This should help us workaround that and ensure all messages get sent
|
||||
await _sendSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await _socket.SendAsync(buffer, true, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Info("WebSocket message to {0} was cancelled", RemoteEndPoint);
|
||||
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error sending WebSocket message {0}", ex, RemoteEndPoint);
|
||||
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sendSemaphore.Release();
|
||||
}
|
||||
return _socket.SendAsync(buffer, true, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task SendAsync(string text, CancellationToken cancellationToken)
|
||||
public Task SendAsync(string text, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
@@ -278,30 +253,7 @@ namespace MediaBrowser.Server.Implementations.ServerManager
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Per msdn docs, attempting to send simultaneous messages will result in one failing.
|
||||
// This should help us workaround that and ensure all messages get sent
|
||||
await _sendSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await _socket.SendAsync(text, true, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Info("WebSocket message to {0} was cancelled", RemoteEndPoint);
|
||||
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ErrorException("Error sending WebSocket message {0}", ex, RemoteEndPoint);
|
||||
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sendSemaphore.Release();
|
||||
}
|
||||
return _socket.SendAsync(text, true, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -75,7 +75,8 @@ namespace MediaBrowser.Server.Implementations.Session
|
||||
await _httpClient.Post(new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
CancellationToken = cancellationToken
|
||||
CancellationToken = cancellationToken,
|
||||
BufferContent = false
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -41,12 +41,7 @@ namespace MediaBrowser.Server.Implementations.Session
|
||||
/// <summary>
|
||||
/// The _user data repository
|
||||
/// </summary>
|
||||
private readonly IUserDataManager _userDataRepository;
|
||||
|
||||
/// <summary>
|
||||
/// The _user repository
|
||||
/// </summary>
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
|
||||
/// <summary>
|
||||
/// The _logger
|
||||
@@ -99,11 +94,10 @@ namespace MediaBrowser.Server.Implementations.Session
|
||||
|
||||
private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
|
||||
|
||||
public SessionManager(IUserDataManager userDataRepository, ILogger logger, IUserRepository userRepository, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager, IDtoService dtoService, IImageProcessor imageProcessor, IJsonSerializer jsonSerializer, IServerApplicationHost appHost, IHttpClient httpClient, IAuthenticationRepository authRepo, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager)
|
||||
public SessionManager(IUserDataManager userDataManager, ILogger logger, ILibraryManager libraryManager, IUserManager userManager, IMusicManager musicManager, IDtoService dtoService, IImageProcessor imageProcessor, IJsonSerializer jsonSerializer, IServerApplicationHost appHost, IHttpClient httpClient, IAuthenticationRepository authRepo, IDeviceManager deviceManager, IMediaSourceManager mediaSourceManager)
|
||||
{
|
||||
_userDataRepository = userDataRepository;
|
||||
_userDataManager = userDataManager;
|
||||
_logger = logger;
|
||||
_userRepository = userRepository;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_musicManager = musicManager;
|
||||
@@ -248,13 +242,11 @@ namespace MediaBrowser.Server.Implementations.Session
|
||||
var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue;
|
||||
user.LastActivityDate = activityDate;
|
||||
|
||||
// Don't log in the db anymore frequently than 10 seconds
|
||||
if ((activityDate - userLastActivityDate).TotalSeconds > 10)
|
||||
if ((activityDate - userLastActivityDate).TotalSeconds > 60)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Save this directly. No need to fire off all the events for this.
|
||||
await _userRepository.SaveUser(user, CancellationToken.None).ConfigureAwait(false);
|
||||
await _userManager.UpdateUser(user).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -294,11 +286,9 @@ namespace MediaBrowser.Server.Implementations.Session
|
||||
var key = GetSessionKey(session.Client, session.DeviceId);
|
||||
|
||||
SessionInfo removed;
|
||||
_activeConnections.TryRemove(key, out removed);
|
||||
|
||||
if (_activeConnections.TryRemove(key, out removed))
|
||||
{
|
||||
OnSessionEnded(removed);
|
||||
}
|
||||
OnSessionEnded(session);
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -307,9 +297,9 @@ namespace MediaBrowser.Server.Implementations.Session
|
||||
}
|
||||
}
|
||||
|
||||
private Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId)
|
||||
private Task<MediaSourceInfo> GetMediaSource(IHasMediaSources item, string mediaSourceId, string liveStreamId)
|
||||
{
|
||||
return _mediaSourceManager.GetMediaSource(item, mediaSourceId, false);
|
||||
return _mediaSourceManager.GetMediaSource(item, mediaSourceId, liveStreamId, false, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -337,7 +327,7 @@ namespace MediaBrowser.Server.Implementations.Session
|
||||
var hasMediaSources = libraryItem as IHasMediaSources;
|
||||
if (hasMediaSources != null)
|
||||
{
|
||||
mediaSource = await GetMediaSource(hasMediaSources, info.MediaSourceId).ConfigureAwait(false);
|
||||
mediaSource = await GetMediaSource(hasMediaSources, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false);
|
||||
|
||||
if (mediaSource != null)
|
||||
{
|
||||
@@ -638,17 +628,21 @@ namespace MediaBrowser.Server.Implementations.Session
|
||||
/// <returns>Task.</returns>
|
||||
private async Task OnPlaybackStart(Guid userId, IHasUserData item)
|
||||
{
|
||||
var data = _userDataRepository.GetUserData(userId, item);
|
||||
var data = _userDataManager.GetUserData(userId, item);
|
||||
|
||||
data.PlayCount++;
|
||||
data.LastPlayedDate = DateTime.UtcNow;
|
||||
|
||||
if (!(item is Video))
|
||||
if (!(item is Video) && item.SupportsPlayedStatus)
|
||||
{
|
||||
data.Played = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
data.Played = false;
|
||||
}
|
||||
|
||||
await _userDataRepository.SaveUserData(userId, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None).ConfigureAwait(false);
|
||||
await _userDataManager.SaveUserData(userId, item, data, UserDataSaveReason.PlaybackStart, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -715,17 +709,17 @@ namespace MediaBrowser.Server.Implementations.Session
|
||||
|
||||
private async Task OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info)
|
||||
{
|
||||
var data = _userDataRepository.GetUserData(user.Id, item);
|
||||
var data = _userDataManager.GetUserData(user.Id, item);
|
||||
|
||||
var positionTicks = info.PositionTicks;
|
||||
|
||||
if (positionTicks.HasValue)
|
||||
{
|
||||
_userDataRepository.UpdatePlayState(item, data, positionTicks.Value);
|
||||
_userDataManager.UpdatePlayState(item, data, positionTicks.Value);
|
||||
|
||||
UpdatePlaybackSettings(user, info, data);
|
||||
|
||||
await _userDataRepository.SaveUserData(user.Id, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.None).ConfigureAwait(false);
|
||||
await _userDataManager.SaveUserData(user.Id, item, data, UserDataSaveReason.PlaybackProgress, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -792,7 +786,7 @@ namespace MediaBrowser.Server.Implementations.Session
|
||||
var hasMediaSources = libraryItem as IHasMediaSources;
|
||||
if (hasMediaSources != null)
|
||||
{
|
||||
mediaSource = await GetMediaSource(hasMediaSources, info.MediaSourceId).ConfigureAwait(false);
|
||||
mediaSource = await GetMediaSource(hasMediaSources, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
info.Item = GetItemInfo(libraryItem, libraryItem, mediaSource);
|
||||
@@ -820,7 +814,7 @@ namespace MediaBrowser.Server.Implementations.Session
|
||||
{
|
||||
try
|
||||
{
|
||||
await _mediaSourceManager.CloseLiveStream(info.LiveStreamId, CancellationToken.None).ConfigureAwait(false);
|
||||
await _mediaSourceManager.CloseLiveStream(info.LiveStreamId).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -851,22 +845,22 @@ namespace MediaBrowser.Server.Implementations.Session
|
||||
|
||||
if (!playbackFailed)
|
||||
{
|
||||
var data = _userDataRepository.GetUserData(userId, item);
|
||||
var data = _userDataManager.GetUserData(userId, item);
|
||||
|
||||
if (positionTicks.HasValue)
|
||||
{
|
||||
playedToCompletion = _userDataRepository.UpdatePlayState(item, data, positionTicks.Value);
|
||||
playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
|
||||
}
|
||||
else
|
||||
else
|
||||
{
|
||||
// If the client isn't able to report this, then we'll just have to make an assumption
|
||||
data.PlayCount++;
|
||||
data.Played = true;
|
||||
data.Played = item.SupportsPlayedStatus;
|
||||
data.PlaybackPositionTicks = 0;
|
||||
playedToCompletion = true;
|
||||
}
|
||||
|
||||
await _userDataRepository.SaveUserData(userId, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None).ConfigureAwait(false);
|
||||
await _userDataManager.SaveUserData(userId, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return playedToCompletion;
|
||||
|
||||
@@ -30,6 +30,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommonIO;
|
||||
using MediaBrowser.Common.IO;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Sync
|
||||
{
|
||||
@@ -51,6 +52,7 @@ namespace MediaBrowser.Server.Implementations.Sync
|
||||
private readonly Func<IMediaSourceManager> _mediaSourceManager;
|
||||
private readonly IJsonSerializer _json;
|
||||
private readonly ITaskManager _taskManager;
|
||||
private readonly IMemoryStreamProvider _memoryStreamProvider;
|
||||
|
||||
private ISyncProvider[] _providers = { };
|
||||
|
||||
@@ -60,7 +62,7 @@ namespace MediaBrowser.Server.Implementations.Sync
|
||||
public event EventHandler<GenericEventArgs<SyncJobItem>> SyncJobItemUpdated;
|
||||
public event EventHandler<GenericEventArgs<SyncJobItem>> SyncJobItemCreated;
|
||||
|
||||
public SyncManager(ILibraryManager libraryManager, ISyncRepository repo, IImageProcessor imageProcessor, ILogger logger, IUserManager userManager, Func<IDtoService> dtoService, IServerApplicationHost appHost, ITVSeriesManager tvSeriesManager, Func<IMediaEncoder> mediaEncoder, IFileSystem fileSystem, Func<ISubtitleEncoder> subtitleEncoder, IConfigurationManager config, IUserDataManager userDataManager, Func<IMediaSourceManager> mediaSourceManager, IJsonSerializer json, ITaskManager taskManager)
|
||||
public SyncManager(ILibraryManager libraryManager, ISyncRepository repo, IImageProcessor imageProcessor, ILogger logger, IUserManager userManager, Func<IDtoService> dtoService, IServerApplicationHost appHost, ITVSeriesManager tvSeriesManager, Func<IMediaEncoder> mediaEncoder, IFileSystem fileSystem, Func<ISubtitleEncoder> subtitleEncoder, IConfigurationManager config, IUserDataManager userDataManager, Func<IMediaSourceManager> mediaSourceManager, IJsonSerializer json, ITaskManager taskManager, IMemoryStreamProvider memoryStreamProvider)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_repo = repo;
|
||||
@@ -78,6 +80,7 @@ namespace MediaBrowser.Server.Implementations.Sync
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_json = json;
|
||||
_taskManager = taskManager;
|
||||
_memoryStreamProvider = memoryStreamProvider;
|
||||
}
|
||||
|
||||
public void AddParts(IEnumerable<ISyncProvider> providers)
|
||||
@@ -95,7 +98,7 @@ namespace MediaBrowser.Server.Implementations.Sync
|
||||
|
||||
public ISyncDataProvider GetDataProvider(IServerSyncProvider provider, SyncTarget target)
|
||||
{
|
||||
return _dataProviders.GetOrAdd(target.Id, key => new TargetDataProvider(provider, target, _appHost, _logger, _json, _fileSystem, _config.CommonApplicationPaths));
|
||||
return _dataProviders.GetOrAdd(target.Id, key => new TargetDataProvider(provider, target, _appHost, _logger, _json, _fileSystem, _config.CommonApplicationPaths, _memoryStreamProvider));
|
||||
}
|
||||
|
||||
public async Task<SyncJobCreationResult> CreateJob(SyncJobRequest request)
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace MediaBrowser.Server.Implementations.Sync
|
||||
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
|
||||
private const string StreamIdDelimeterString = "_";
|
||||
|
||||
public async Task<MediaSourceInfo> OpenMediaSource(string openToken, CancellationToken cancellationToken)
|
||||
public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> OpenMediaSource(string openToken, CancellationToken cancellationToken)
|
||||
{
|
||||
var openKeys = openToken.Split(new[] { StreamIdDelimeterString[0] }, 3);
|
||||
|
||||
@@ -137,7 +137,7 @@ namespace MediaBrowser.Server.Implementations.Sync
|
||||
mediaSource.Protocol = dynamicInfo.Protocol;
|
||||
mediaSource.RequiredHttpHeaders = dynamicInfo.RequiredHttpHeaders;
|
||||
|
||||
return mediaSource;
|
||||
return new Tuple<MediaSourceInfo, IDirectStreamProvider>(mediaSource, null);
|
||||
}
|
||||
|
||||
private void SetStaticMediaSourceInfo(LocalItem item, MediaSourceInfo mediaSource)
|
||||
@@ -150,7 +150,7 @@ namespace MediaBrowser.Server.Implementations.Sync
|
||||
}
|
||||
}
|
||||
|
||||
public Task CloseMediaSource(string liveStreamId, CancellationToken cancellationToken)
|
||||
public Task CloseMediaSource(string liveStreamId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommonIO;
|
||||
using Interfaces.IO;
|
||||
using MediaBrowser.Common.IO;
|
||||
|
||||
namespace MediaBrowser.Server.Implementations.Sync
|
||||
{
|
||||
@@ -28,8 +29,9 @@ namespace MediaBrowser.Server.Implementations.Sync
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IMemoryStreamProvider _memoryStreamProvider;
|
||||
|
||||
public TargetDataProvider(IServerSyncProvider provider, SyncTarget target, IServerApplicationHost appHost, ILogger logger, IJsonSerializer json, IFileSystem fileSystem, IApplicationPaths appPaths)
|
||||
public TargetDataProvider(IServerSyncProvider provider, SyncTarget target, IServerApplicationHost appHost, ILogger logger, IJsonSerializer json, IFileSystem fileSystem, IApplicationPaths appPaths, IMemoryStreamProvider memoryStreamProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_json = json;
|
||||
@@ -37,6 +39,7 @@ namespace MediaBrowser.Server.Implementations.Sync
|
||||
_target = target;
|
||||
_fileSystem = fileSystem;
|
||||
_appPaths = appPaths;
|
||||
_memoryStreamProvider = memoryStreamProvider;
|
||||
_appHost = appHost;
|
||||
}
|
||||
|
||||
@@ -90,7 +93,7 @@ namespace MediaBrowser.Server.Implementations.Sync
|
||||
|
||||
private async Task SaveData(List<LocalItem> items, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var stream = new MemoryStream())
|
||||
using (var stream = _memoryStreamProvider.CreateNew())
|
||||
{
|
||||
_json.SerializeToStream(items, stream);
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Data.SQLite" publicKeyToken="db937bc2d44ff139" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-1.0.94.0" newVersion="1.0.94.0" />
|
||||
<assemblyIdentity name="System.Data.SQLite" publicKeyToken="db937bc2d44ff139" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-1.0.94.0" newVersion="1.0.94.0"/>
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
||||
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1"/></startup></configuration>
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
<package id="ini-parser" version="2.3.0" targetFramework="net45" />
|
||||
<package id="Interfaces.IO" version="1.0.0.5" targetFramework="net45" />
|
||||
<package id="MediaBrowser.Naming" version="1.0.0.55" 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" />
|
||||
<package id="SimpleInjector" version="3.2.0" targetFramework="net45" />
|
||||
<package id="SocketHttpListener" version="1.0.0.39" targetFramework="net45" />
|
||||
<package id="SimpleInjector" version="3.2.2" targetFramework="net45" />
|
||||
<package id="SocketHttpListener" version="1.0.0.40" targetFramework="net45" />
|
||||
</packages>
|
||||
Reference in New Issue
Block a user