aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Server.Implementations
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Server.Implementations')
-rw-r--r--MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs4
-rw-r--r--MediaBrowser.Server.Implementations/Channels/ChannelManager.cs92
-rw-r--r--MediaBrowser.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs2
-rw-r--r--MediaBrowser.Server.Implementations/Collections/CollectionManager.cs40
-rw-r--r--MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs3
-rw-r--r--MediaBrowser.Server.Implementations/Connect/ConnectManager.cs51
-rw-r--r--MediaBrowser.Server.Implementations/Dto/DtoService.cs63
-rw-r--r--MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs66
-rw-r--r--MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs17
-rw-r--r--MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs6
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs19
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs31
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs240
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs18
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs10
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs8
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs7
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs9
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs18
-rw-r--r--MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs37
-rw-r--r--MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs20
-rw-r--r--MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs7
-rw-r--r--MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs3
-rw-r--r--MediaBrowser.Server.Implementations/Library/LibraryManager.cs324
-rw-r--r--MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs104
-rw-r--r--MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs2
-rw-r--r--MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs21
-rw-r--r--MediaBrowser.Server.Implementations/Library/Resolvers/FolderResolver.cs1
-rw-r--r--MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs30
-rw-r--r--MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs35
-rw-r--r--MediaBrowser.Server.Implementations/Library/SearchEngine.cs1
-rw-r--r--MediaBrowser.Server.Implementations/Library/UserDataManager.cs29
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs65
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs977
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs113
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs47
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs72
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs19
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveStreamHelper.cs110
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs38
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs614
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs30
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs221
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs12
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs127
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs145
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs19
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs96
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs93
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs11
-rw-r--r--MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs10
-rw-r--r--MediaBrowser.Server.Implementations/Localization/Core/en-US.json1
-rw-r--r--MediaBrowser.Server.Implementations/Localization/Ratings/de.txt5
-rw-r--r--MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj25
-rw-r--r--MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs30
-rw-r--r--MediaBrowser.Server.Implementations/News/NewsEntryPoint.cs3
-rw-r--r--MediaBrowser.Server.Implementations/Notifications/CoreNotificationTypes.cs7
-rw-r--r--MediaBrowser.Server.Implementations/Persistence/DataExtensions.cs13
-rw-r--r--MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs14
-rw-r--r--MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs947
-rw-r--r--MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs9
-rw-r--r--MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs10
-rw-r--r--MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs7
-rw-r--r--MediaBrowser.Server.Implementations/ServerManager/WebSocketConnection.cs72
-rw-r--r--MediaBrowser.Server.Implementations/Session/HttpSessionController.cs3
-rw-r--r--MediaBrowser.Server.Implementations/Session/SessionManager.cs60
-rw-r--r--MediaBrowser.Server.Implementations/Sync/SyncManager.cs7
-rw-r--r--MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs6
-rw-r--r--MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs7
-rw-r--r--MediaBrowser.Server.Implementations/app.config8
-rw-r--r--MediaBrowser.Server.Implementations/packages.config5
71 files changed, 3458 insertions, 1918 deletions
diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs b/MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs
index 3239b20b2..fae78b9bc 100644
--- a/MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs
+++ b/MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs
@@ -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();
}
diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs
index b76cf46b0..1369efae1 100644
--- a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs
+++ b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs
@@ -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
- {
-
- }
- }
}
} \ No newline at end of file
diff --git a/MediaBrowser.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs b/MediaBrowser.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
index 005bbb852..5ac3da3db 100644
--- a/MediaBrowser.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
+++ b/MediaBrowser.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
@@ -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)
diff --git a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs b/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs
index 1b6c44c5e..cb2bd645d 100644
--- a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs
+++ b/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs
@@ -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)
- {
- _iLibraryMonitor.ReportFileSystemChangeBeginning(file);
- }
-
- try
- {
- foreach (var file in shortcutFilesToDelete)
- {
- _fileSystem.DeleteFile(file);
- }
-
- foreach (var child in list)
- {
- collection.LinkedChildren.Remove(child);
- }
- }
- finally
+ foreach (var child in list)
{
- foreach (var file in shortcutFilesToDelete)
- {
- _iLibraryMonitor.ReportFileSystemChangeComplete(file, false);
- }
+ collection.LinkedChildren.Remove(child);
}
collection.UpdateRatingToContent();
diff --git a/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs b/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs
index 2b2373a47..f9eff3c92 100644
--- a/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs
+++ b/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs
@@ -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))
{
diff --git a/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs b/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs
index 45cb2f57d..d7c1b0da0 100644
--- a/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs
+++ b/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs
@@ -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>
diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs
index 9284f4fc7..a0f7aa999 100644
--- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs
+++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs
@@ -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>
diff --git a/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
index 280bec65b..1021d8823 100644
--- a/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ b/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
@@ -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)
- {
- _createdRules = new List<string>();
- _usnsHandled = new List<string>();
- }
-
- void _ssdp_MessageReceived(object sender, SsdpMessageEventArgs e)
+ private async void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> 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
{
diff --git a/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs b/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs
index e84b66c5a..f7fe707da 100644
--- a/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs
+++ b/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs
@@ -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()
@@ -390,17 +390,6 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications
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);
- }
}
public static string GetItemName(BaseItem item)
diff --git a/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs b/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs
index 7b3a7a30d..e445300e4 100644
--- a/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs
+++ b/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs
@@ -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);
diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs
index 8e46f8f03..9ec671908 100644
--- a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs
@@ -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>
diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs
index c55e98388..10d6f7493 100644
--- a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs
+++ b/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs
@@ -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);
diff --git a/MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs b/MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs
deleted file mode 100644
index cac2f8e09..000000000
--- a/MediaBrowser.Server.Implementations/HttpServer/NativeWebSocket.cs
+++ /dev/null
@@ -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;
- }
- }
- }
-}
diff --git a/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs b/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs
index 488c630fe..4b94095f5 100644
--- a/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs
+++ b/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs
@@ -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)
diff --git a/MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs b/MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs
index cc351f6b3..8a7c14eb6 100644
--- a/MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs
+++ b/MediaBrowser.Server.Implementations/HttpServer/ServerFactory.cs
@@ -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);
}
}
}
diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs
index bfa65ac6b..d20dd7ec0 100644
--- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs
+++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs
@@ -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;
diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs
index bcc081eb1..b090c97c6 100644
--- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs
+++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs
@@ -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;
diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs
index dc2aec3e1..b5c8d0107 100644
--- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs
+++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs
@@ -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)
{
diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs
index e08be8bd1..a58645ec5 100644
--- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs
+++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs
@@ -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()
diff --git a/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs b/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs
index ae408f8d6..5f122fb96 100644
--- a/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs
+++ b/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs
@@ -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 responseStream.WriteAsync(_bytes, 0, _bytes.Length);
+ }
+ else
{
- await src.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false);
+ using (var src = SourceStream)
+ {
+ await src.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false);
+ }
}
}
catch (Exception ex)
diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs
index dffaec458..76f0e6a1d 100644
--- a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs
+++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs
@@ -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()
diff --git a/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs b/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs
index 4a43befed..9b23d5be4 100644
--- a/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs
+++ b/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs
@@ -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)
diff --git a/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index ba7e33890..b550d1dda 100644
--- a/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -33,7 +33,8 @@ namespace MediaBrowser.Server.Implementations.Library
// Synology
"@eaDir",
- "eaDir"
+ "eaDir",
+ "#recycle"
};
diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs
index 442e2ebe5..b2302cf86 100644
--- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs
+++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs
@@ -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();
+ }
+
+ 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;
- return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
+ 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,8 +2544,60 @@ 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))
{
throw new ArgumentNullException("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);
}
}
} \ No newline at end of file
diff --git a/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs b/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs
index 4a533ff93..e7bfe56f2 100644
--- a/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs
+++ b/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs
@@ -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;
}
}
} \ No newline at end of file
diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index 1a8295800..7f35fc3ea 100644
--- a/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -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();
diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
index e819af06f..686105ddb 100644
--- a/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
+++ b/MediaBrowser.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -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);
diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/FolderResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/FolderResolver.cs
index 34237622d..ff07c5282 100644
--- a/MediaBrowser.Server.Implementations/Library/Resolvers/FolderResolver.cs
+++ b/MediaBrowser.Server.Implementations/Library/Resolvers/FolderResolver.cs
@@ -51,7 +51,6 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers
base.SetInitialItemValues(item, args);
item.IsRoot = args.Parent == null;
- item.IsPhysicalRoot = args.IsPhysicalRoot;
}
}
}
diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index ee9533d2a..c3d5f3441 100644
--- a/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/MediaBrowser.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -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);
}
diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index 4c6254330..c82825007 100644
--- a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -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 null;
+ }
+
+ if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, args.GetLibraryOptions(), false))
+ {
+ return new Series
{
- return new Series
- {
- Path = args.Path,
- Name = Path.GetFileName(args.Path)
- };
- }
+ Path = args.Path,
+ Name = Path.GetFileName(args.Path)
+ };
}
}
}
diff --git a/MediaBrowser.Server.Implementations/Library/SearchEngine.cs b/MediaBrowser.Server.Implementations/Library/SearchEngine.cs
index a6d6b5cb8..40cca7bab 100644
--- a/MediaBrowser.Server.Implementations/Library/SearchEngine.cs
+++ b/MediaBrowser.Server.Implementations/Library/SearchEngine.cs
@@ -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)))
{
diff --git a/MediaBrowser.Server.Implementations/Library/UserDataManager.cs b/MediaBrowser.Server.Implementations/Library/UserDataManager.cs
index 307cf4cd2..9ee65a57c 100644
--- a/MediaBrowser.Server.Implementations/Library/UserDataManager.cs
+++ b/MediaBrowser.Server.Implementations/Library/UserDataManager.cs
@@ -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;
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
index b21aa904b..0f8c15e71 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs
@@ -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;
- }
-
- await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false);
+ // 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 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;
+ }
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 8fa34109d..136a9e195 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -22,20 +22,25 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using System.Xml;
using CommonIO;
+using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Power;
using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.FileOrganization;
using Microsoft.Win32;
namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
- public class EmbyTV : ILiveTvService, ISupportsNewTimerIds, IDisposable
+ public class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable
{
- private readonly IApplicationHost _appHpst;
+ private readonly IServerApplicationHost _appHost;
private readonly ILogger _logger;
private readonly IHttpClient _httpClient;
private readonly IServerConfigurationManager _config;
@@ -55,17 +60,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
public static EmbyTV Current;
- public event EventHandler DataSourceChanged { add { } remove { } }
- public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged { add { } remove { } }
+ public event EventHandler DataSourceChanged;
+ public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged;
private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings =
new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase);
- public EmbyTV(IApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService, IMediaEncoder mediaEncoder, IPowerManagement powerManagement)
+ public EmbyTV(IServerApplicationHost appHost, ILogger logger, IJsonSerializer jsonSerializer, IHttpClient httpClient, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService, IMediaEncoder mediaEncoder)
{
Current = this;
- _appHpst = appHost;
+ _appHost = appHost;
_logger = logger;
_httpClient = httpClient;
_config = config;
@@ -79,7 +84,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_jsonSerializer = jsonSerializer;
_seriesTimerProvider = new SeriesTimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers"));
- _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), powerManagement, _logger);
+ _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), _logger);
_timerProvider.TimerFired += _timerProvider_TimerFired;
_config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated;
@@ -140,9 +145,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
continue;
}
+ var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo { Path = i }).ToArray();
+
+ var libraryOptions = new LibraryOptions
+ {
+ PathInfos = mediaPathInfos
+ };
try
{
- _libraryManager.AddVirtualFolder(recordingFolder.Name, recordingFolder.CollectionType, pathsToCreate.ToArray(), new LibraryOptions(), true);
+ _libraryManager.AddVirtualFolder(recordingFolder.Name, recordingFolder.CollectionType, libraryOptions, true);
}
catch (Exception ex)
{
@@ -284,7 +295,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
status.Tuners = list;
status.Status = LiveTvServiceStatus.Ok;
- status.Version = _appHpst.ApplicationVersion.ToString();
+ status.Version = _appHost.ApplicationVersion.ToString();
status.IsVisible = false;
return status;
}
@@ -321,27 +332,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
if (DateTime.UtcNow > timer.EndDate && !_activeRecordings.ContainsKey(timer.Id))
{
- _timerProvider.Delete(timer);
+ OnTimerOutOfDate(timer);
}
}
}
- private List<ChannelInfo> _channelCache = null;
- private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken)
+ private void OnTimerOutOfDate(TimerInfo timer)
{
- if (enableCache && _channelCache != null)
- {
-
- return _channelCache.ToList();
- }
+ _timerProvider.Delete(timer);
+ }
+ private async Task<IEnumerable<ChannelInfo>> GetChannelsAsync(bool enableCache, CancellationToken cancellationToken)
+ {
var list = new List<ChannelInfo>();
foreach (var hostInstance in _liveTvManager.TunerHosts)
{
try
{
- var channels = await hostInstance.GetChannels(cancellationToken).ConfigureAwait(false);
+ var channels = await hostInstance.GetChannels(enableCache, cancellationToken).ConfigureAwait(false);
list.AddRange(channels);
}
@@ -374,7 +383,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
- _channelCache = list.ToList();
return list;
}
@@ -386,7 +394,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
try
{
- var channels = await hostInstance.GetChannels(cancellationToken).ConfigureAwait(false);
+ var channels = await hostInstance.GetChannels(false, cancellationToken).ConfigureAwait(false);
list.AddRange(channels);
}
@@ -415,7 +423,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
foreach (var timer in timers)
{
- CancelTimerInternal(timer.Id);
+ CancelTimerInternal(timer.Id, true);
}
var remove = _seriesTimerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase));
@@ -426,12 +434,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return Task.FromResult(true);
}
- private void CancelTimerInternal(string timerId)
+ private void CancelTimerInternal(string timerId, bool isSeriesCancelled)
{
- var remove = _timerProvider.GetAll().FirstOrDefault(r => string.Equals(r.Id, timerId, StringComparison.OrdinalIgnoreCase));
- if (remove != null)
+ var timer = _timerProvider.GetTimer(timerId);
+ if (timer != null)
{
- _timerProvider.Delete(remove);
+ if (string.IsNullOrWhiteSpace(timer.SeriesTimerId) || isSeriesCancelled)
+ {
+ _timerProvider.Delete(timer);
+ }
+ else
+ {
+ timer.Status = RecordingStatus.Cancelled;
+ _timerProvider.AddOrUpdate(timer, false);
+ }
}
ActiveRecordingInfo activeRecordingInfo;
@@ -443,7 +459,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
public Task CancelTimerAsync(string timerId, CancellationToken cancellationToken)
{
- CancelTimerInternal(timerId);
+ CancelTimerInternal(timerId, false);
return Task.FromResult(true);
}
@@ -452,21 +468,57 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return Task.FromResult(true);
}
- public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
+ public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
{
- return CreateTimer(info, cancellationToken);
+ throw new NotImplementedException();
}
- public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken)
+ public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
{
- return CreateSeriesTimer(info, cancellationToken);
+ throw new NotImplementedException();
}
- public Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken)
+ public Task<string> CreateTimer(TimerInfo timer, CancellationToken cancellationToken)
{
- info.Id = Guid.NewGuid().ToString("N");
- _timerProvider.Add(info);
- return Task.FromResult(info.Id);
+ var existingTimer = _timerProvider.GetAll()
+ .FirstOrDefault(i => string.Equals(timer.ProgramId, i.ProgramId, StringComparison.OrdinalIgnoreCase));
+
+ if (existingTimer != null)
+ {
+ if (existingTimer.Status == RecordingStatus.Cancelled ||
+ existingTimer.Status == RecordingStatus.Completed)
+ {
+ existingTimer.Status = RecordingStatus.New;
+ _timerProvider.Update(existingTimer);
+ return Task.FromResult(existingTimer.Id);
+ }
+ else
+ {
+ throw new ArgumentException("A scheduled recording already exists for this program.");
+ }
+ }
+
+ timer.Id = Guid.NewGuid().ToString("N");
+
+ ProgramInfo programInfo = null;
+
+ if (!string.IsNullOrWhiteSpace(timer.ProgramId))
+ {
+ programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
+ }
+ if (programInfo == null)
+ {
+ _logger.Info("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
+ programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
+ }
+
+ if (programInfo != null)
+ {
+ RecordingHelper.CopyProgramInfoToTimerInfo(programInfo, timer);
+ }
+
+ _timerProvider.Add(timer);
+ return Task.FromResult(timer.Id);
}
public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken)
@@ -520,6 +572,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
instance.RecordAnyChannel = info.RecordAnyChannel;
instance.RecordAnyTime = info.RecordAnyTime;
instance.RecordNewOnly = info.RecordNewOnly;
+ instance.SkipEpisodesInLibrary = info.SkipEpisodesInLibrary;
+ instance.KeepUpTo = info.KeepUpTo;
+ instance.KeepUntil = info.KeepUntil;
instance.StartDate = info.StartDate;
_seriesTimerProvider.Update(instance);
@@ -540,12 +595,55 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
- public Task UpdateTimerAsync(TimerInfo info, CancellationToken cancellationToken)
+ public Task UpdateTimerAsync(TimerInfo updatedTimer, CancellationToken cancellationToken)
{
- _timerProvider.Update(info);
+ var existingTimer = _timerProvider.GetTimer(updatedTimer.Id);
+
+ if (existingTimer == null)
+ {
+ throw new ResourceNotFoundException();
+ }
+
+ // Only update if not currently active
+ ActiveRecordingInfo activeRecordingInfo;
+ if (!_activeRecordings.TryGetValue(updatedTimer.Id, out activeRecordingInfo))
+ {
+ existingTimer.PrePaddingSeconds = updatedTimer.PrePaddingSeconds;
+ existingTimer.PostPaddingSeconds = updatedTimer.PostPaddingSeconds;
+ existingTimer.IsPostPaddingRequired = updatedTimer.IsPostPaddingRequired;
+ existingTimer.IsPrePaddingRequired = updatedTimer.IsPrePaddingRequired;
+ }
+
return Task.FromResult(true);
}
+ private void UpdateExistingTimerWithNewMetadata(TimerInfo existingTimer, TimerInfo updatedTimer)
+ {
+ // Update the program info but retain the status
+ existingTimer.ChannelId = updatedTimer.ChannelId;
+ existingTimer.CommunityRating = updatedTimer.CommunityRating;
+ existingTimer.EndDate = updatedTimer.EndDate;
+ existingTimer.EpisodeNumber = updatedTimer.EpisodeNumber;
+ existingTimer.EpisodeTitle = updatedTimer.EpisodeTitle;
+ existingTimer.Genres = updatedTimer.Genres;
+ existingTimer.HomePageUrl = updatedTimer.HomePageUrl;
+ existingTimer.IsKids = updatedTimer.IsKids;
+ existingTimer.IsNews = updatedTimer.IsNews;
+ existingTimer.IsMovie = updatedTimer.IsMovie;
+ existingTimer.IsProgramSeries = updatedTimer.IsProgramSeries;
+ existingTimer.IsRepeat = updatedTimer.IsRepeat;
+ existingTimer.IsSports = updatedTimer.IsSports;
+ existingTimer.Name = updatedTimer.Name;
+ existingTimer.OfficialRating = updatedTimer.OfficialRating;
+ existingTimer.OriginalAirDate = updatedTimer.OriginalAirDate;
+ existingTimer.Overview = updatedTimer.Overview;
+ existingTimer.ProductionYear = updatedTimer.ProductionYear;
+ existingTimer.ProgramId = updatedTimer.ProgramId;
+ existingTimer.SeasonNumber = updatedTimer.SeasonNumber;
+ existingTimer.ShortOverview = updatedTimer.ShortOverview;
+ existingTimer.StartDate = updatedTimer.StartDate;
+ }
+
public Task<ImageStream> GetChannelImageAsync(string channelId, CancellationToken cancellationToken)
{
throw new NotImplementedException();
@@ -563,12 +661,76 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
public async Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken)
{
- return new List<RecordingInfo>();
+ return _activeRecordings.Values.ToList().Select(GetRecordingInfo).ToList();
+ }
+
+ public string GetActiveRecordingPath(string id)
+ {
+ ActiveRecordingInfo info;
+
+ if (_activeRecordings.TryGetValue(id, out info))
+ {
+ return info.Path;
+ }
+ return null;
+ }
+
+ private RecordingInfo GetRecordingInfo(ActiveRecordingInfo info)
+ {
+ var timer = info.Timer;
+ var program = info.Program;
+
+ var result = new RecordingInfo
+ {
+ ChannelId = timer.ChannelId,
+ CommunityRating = timer.CommunityRating,
+ DateLastUpdated = DateTime.UtcNow,
+ EndDate = timer.EndDate,
+ EpisodeTitle = timer.EpisodeTitle,
+ Genres = timer.Genres,
+ Id = "recording" + timer.Id,
+ IsKids = timer.IsKids,
+ IsMovie = timer.IsMovie,
+ IsNews = timer.IsNews,
+ IsRepeat = timer.IsRepeat,
+ IsSeries = timer.IsProgramSeries,
+ IsSports = timer.IsSports,
+ Name = timer.Name,
+ OfficialRating = timer.OfficialRating,
+ OriginalAirDate = timer.OriginalAirDate,
+ Overview = timer.Overview,
+ ProgramId = timer.ProgramId,
+ SeriesTimerId = timer.SeriesTimerId,
+ StartDate = timer.StartDate,
+ Status = RecordingStatus.InProgress,
+ TimerId = timer.Id
+ };
+
+ if (program != null)
+ {
+ result.Audio = program.Audio;
+ result.ImagePath = program.ImagePath;
+ result.ImageUrl = program.ImageUrl;
+ result.IsHD = program.IsHD;
+ result.IsLive = program.IsLive;
+ result.IsPremiere = program.IsPremiere;
+ result.ShowId = program.ShowId;
+ }
+
+ return result;
}
public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken)
{
- return Task.FromResult((IEnumerable<TimerInfo>)_timerProvider.GetAll());
+ var excludeStatues = new List<RecordingStatus>
+ {
+ RecordingStatus.Completed
+ };
+
+ var timers = _timerProvider.GetAll()
+ .Where(i => !excludeStatues.Contains(i.Status));
+
+ return Task.FromResult(timers);
}
public Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null)
@@ -581,7 +743,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
PrePaddingSeconds = Math.Max(config.PrePaddingSeconds, 0),
RecordAnyChannel = true,
RecordAnyTime = true,
- RecordNewOnly = false,
+ RecordNewOnly = true,
Days = new List<DayOfWeek>
{
@@ -601,6 +763,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
defaults.ProgramId = program.Id;
}
+ defaults.SkipEpisodesInLibrary = true;
+ defaults.KeepUntil = KeepUntil.UntilDeleted;
+
return Task.FromResult(defaults);
}
@@ -717,46 +882,108 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
throw new NotImplementedException();
}
+ private readonly SemaphoreSlim _liveStreamsSemaphore = new SemaphoreSlim(1, 1);
+ private readonly List<LiveStream> _liveStreams = new List<LiveStream>();
+
public async Task<MediaSourceInfo> GetChannelStream(string channelId, string streamId, CancellationToken cancellationToken)
{
- _logger.Info("Streaming Channel " + channelId);
+ var result = await GetChannelStreamWithDirectStreamProvider(channelId, streamId, cancellationToken).ConfigureAwait(false);
- foreach (var hostInstance in _liveTvManager.TunerHosts)
- {
- try
- {
- var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
+ return result.Item1;
+ }
- result.Item2.Release();
+ public async Task<Tuple<MediaSourceInfo, IDirectStreamProvider>> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, CancellationToken cancellationToken)
+ {
+ var result = await GetChannelStreamInternal(channelId, streamId, cancellationToken).ConfigureAwait(false);
- return result.Item1;
- }
- catch (Exception e)
- {
- _logger.ErrorException("Error getting channel stream", e);
- }
+ return new Tuple<MediaSourceInfo, IDirectStreamProvider>(result.Item2, result.Item1 as IDirectStreamProvider);
+ }
+
+ private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, bool enableStreamSharing)
+ {
+ var json = _jsonSerializer.SerializeToString(mediaSource);
+ mediaSource = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
+
+ mediaSource.Id = Guid.NewGuid().ToString("N") + "_" + mediaSource.Id;
+
+ //if (mediaSource.DateLiveStreamOpened.HasValue && enableStreamSharing)
+ //{
+ // var ticks = (DateTime.UtcNow - mediaSource.DateLiveStreamOpened.Value).Ticks - TimeSpan.FromSeconds(10).Ticks;
+ // ticks = Math.Max(0, ticks);
+ // mediaSource.Path += "?t=" + ticks.ToString(CultureInfo.InvariantCulture) + "&s=" + mediaSource.DateLiveStreamOpened.Value.Ticks.ToString(CultureInfo.InvariantCulture);
+ //}
+
+ return mediaSource;
+ }
+
+ public async Task<LiveStream> GetLiveStream(string uniqueId)
+ {
+ await _liveStreamsSemaphore.WaitAsync().ConfigureAwait(false);
+
+ try
+ {
+ return _liveStreams
+ .FirstOrDefault(i => string.Equals(i.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase));
+ }
+ finally
+ {
+ _liveStreamsSemaphore.Release();
}
- throw new ApplicationException("Tuner not found.");
}
- private async Task<Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken)
+ private async Task<Tuple<LiveStream, MediaSourceInfo, ITunerHost>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken)
{
_logger.Info("Streaming Channel " + channelId);
- foreach (var hostInstance in _liveTvManager.TunerHosts)
+ await _liveStreamsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ var result = _liveStreams.FirstOrDefault(i => string.Equals(i.OriginalStreamId, streamId, StringComparison.OrdinalIgnoreCase));
+
+ if (result != null && result.EnableStreamSharing)
{
- try
- {
- var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
+ var openedMediaSource = CloneMediaSource(result.OpenedMediaSource, result.EnableStreamSharing);
+ result.SharedStreamIds.Add(openedMediaSource.Id);
+ _liveStreamsSemaphore.Release();
- return new Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>(result.Item1, hostInstance, result.Item2);
- }
- catch (Exception e)
+ _logger.Info("Live stream {0} consumer count is now {1}", streamId, result.ConsumerCount);
+
+ return new Tuple<LiveStream, MediaSourceInfo, ITunerHost>(result, openedMediaSource, result.TunerHost);
+ }
+
+ try
+ {
+ foreach (var hostInstance in _liveTvManager.TunerHosts)
{
- _logger.ErrorException("Error getting channel stream", e);
+ try
+ {
+ result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false);
+
+ var openedMediaSource = CloneMediaSource(result.OpenedMediaSource, result.EnableStreamSharing);
+
+ result.SharedStreamIds.Add(openedMediaSource.Id);
+ _liveStreams.Add(result);
+
+ result.TunerHost = hostInstance;
+ result.OriginalStreamId = streamId;
+
+ _logger.Info("Returning mediasource streamId {0}, mediaSource.Id {1}, mediaSource.LiveStreamId {2}",
+ streamId, openedMediaSource.Id, openedMediaSource.LiveStreamId);
+
+ return new Tuple<LiveStream, MediaSourceInfo, ITunerHost>(result, openedMediaSource, hostInstance);
+ }
+ catch (FileNotFoundException)
+ {
+ }
+ catch (OperationCanceledException)
+ {
+ }
}
}
+ finally
+ {
+ _liveStreamsSemaphore.Release();
+ }
throw new ApplicationException("Tuner not found.");
}
@@ -783,14 +1010,82 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
throw new NotImplementedException();
}
- public Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(string recordingId, CancellationToken cancellationToken)
+ public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(string recordingId, CancellationToken cancellationToken)
{
- throw new NotImplementedException();
+ ActiveRecordingInfo info;
+
+ recordingId = recordingId.Replace("recording", string.Empty);
+
+ if (_activeRecordings.TryGetValue(recordingId, out info))
+ {
+ var stream = new MediaSourceInfo
+ {
+ Path = _appHost.GetLocalApiUrl("localhost") + "/LiveTv/LiveRecordings/" + recordingId + "/stream",
+ Id = recordingId,
+ SupportsDirectPlay = false,
+ SupportsDirectStream = true,
+ SupportsTranscoding = true,
+ IsInfiniteStream = true,
+ RequiresOpening = false,
+ RequiresClosing = false,
+ Protocol = Model.MediaInfo.MediaProtocol.Http,
+ BufferMs = 0
+ };
+
+ var isAudio = false;
+ await new LiveStreamHelper(_mediaEncoder, _logger).AddMediaInfoWithProbe(stream, isAudio, cancellationToken).ConfigureAwait(false);
+
+ return new List<MediaSourceInfo>
+ {
+ stream
+ };
+ }
+
+ throw new FileNotFoundException();
}
- public Task CloseLiveStream(string id, CancellationToken cancellationToken)
+ public async Task CloseLiveStream(string id, CancellationToken cancellationToken)
{
- return Task.FromResult(0);
+ // Ignore the consumer id
+ //id = id.Substring(id.IndexOf('_') + 1);
+
+ await _liveStreamsSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+ try
+ {
+ var stream = _liveStreams.FirstOrDefault(i => i.SharedStreamIds.Contains(id));
+ if (stream != null)
+ {
+ stream.SharedStreamIds.Remove(id);
+
+ _logger.Info("Live stream {0} consumer count is now {1}", id, stream.ConsumerCount);
+
+ if (stream.ConsumerCount <= 0)
+ {
+ _liveStreams.Remove(stream);
+
+ _logger.Info("Closing live stream {0}", id);
+
+ await stream.Close().ConfigureAwait(false);
+ _logger.Info("Live stream {0} closed successfully", id);
+ }
+ }
+ else
+ {
+ _logger.Warn("Live stream not found: {0}, unable to close", id);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error closing live stream", ex);
+ }
+ finally
+ {
+ _liveStreamsSemaphore.Release();
+ }
}
public Task RecordLiveStream(string id, CancellationToken cancellationToken)
@@ -815,14 +1110,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
if (recordingEndDate <= DateTime.UtcNow)
{
- _logger.Warn("Recording timer fired for timer {0}, Id: {1}, but the program has already ended.", timer.Name, timer.Id);
+ _logger.Warn("Recording timer fired for updatedTimer {0}, Id: {1}, but the program has already ended.", timer.Name, timer.Id);
+ OnTimerOutOfDate(timer);
return;
}
var activeRecordingInfo = new ActiveRecordingInfo
{
CancellationTokenSource = new CancellationTokenSource(),
- TimerId = timer.Id
+ Timer = timer
};
if (_activeRecordings.TryAdd(timer.Id, activeRecordingInfo))
@@ -844,12 +1140,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
}
- private string GetRecordingPath(TimerInfo timer, ProgramInfo info)
+ private string GetRecordingPath(TimerInfo timer, out string seriesPath)
{
var recordPath = RecordingPath;
var config = GetConfiguration();
+ seriesPath = null;
- if (info.IsSeries)
+ if (timer.IsProgramSeries)
{
var customRecordingPath = config.SeriesRecordingPath;
var allowSubfolder = true;
@@ -864,29 +1161,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
recordPath = Path.Combine(recordPath, "Series");
}
- var folderName = _fileSystem.GetValidFilename(info.Name).Trim();
- var folderNameWithYear = folderName;
- if (info.ProductionYear.HasValue)
- {
- folderNameWithYear += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
- }
+ var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
- if (Directory.Exists(Path.Combine(recordPath, folderName)))
- {
- recordPath = Path.Combine(recordPath, folderName);
- }
- else
- {
- recordPath = Path.Combine(recordPath, folderNameWithYear);
- }
+ // Can't use the year here in the folder name because it is the year of the episode, not the series.
+ recordPath = Path.Combine(recordPath, folderName);
+
+ seriesPath = recordPath;
- if (info.SeasonNumber.HasValue)
+ if (timer.SeasonNumber.HasValue)
{
- folderName = string.Format("Season {0}", info.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
+ folderName = string.Format("Season {0}", timer.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture));
recordPath = Path.Combine(recordPath, folderName);
}
}
- else if (info.IsMovie)
+ else if (timer.IsMovie)
{
var customRecordingPath = config.MovieRecordingPath;
var allowSubfolder = true;
@@ -901,34 +1189,34 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
recordPath = Path.Combine(recordPath, "Movies");
}
- var folderName = _fileSystem.GetValidFilename(info.Name).Trim();
- if (info.ProductionYear.HasValue)
+ var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
+ if (timer.ProductionYear.HasValue)
{
- folderName += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
+ folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
}
recordPath = Path.Combine(recordPath, folderName);
}
- else if (info.IsKids)
+ else if (timer.IsKids)
{
if (config.EnableRecordingSubfolders)
{
recordPath = Path.Combine(recordPath, "Kids");
}
- var folderName = _fileSystem.GetValidFilename(info.Name).Trim();
- if (info.ProductionYear.HasValue)
+ var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
+ if (timer.ProductionYear.HasValue)
{
- folderName += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
+ folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
}
recordPath = Path.Combine(recordPath, folderName);
}
- else if (info.IsSports)
+ else if (timer.IsSports)
{
if (config.EnableRecordingSubfolders)
{
recordPath = Path.Combine(recordPath, "Sports");
}
- recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(info.Name).Trim());
+ recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim());
}
else
{
@@ -936,54 +1224,57 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
{
recordPath = Path.Combine(recordPath, "Other");
}
- recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(info.Name).Trim());
+ recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(timer.Name).Trim());
}
- var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts";
+ var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer)).Trim() + ".ts";
return Path.Combine(recordPath, recordingFileName);
}
- private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
+ private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate,
+ ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken)
{
if (timer == null)
{
throw new ArgumentNullException("timer");
}
- ProgramInfo info = null;
+ ProgramInfo programInfo = null;
- if (string.IsNullOrWhiteSpace(timer.ProgramId))
+ if (!string.IsNullOrWhiteSpace(timer.ProgramId))
{
- _logger.Info("Timer {0} has null programId", timer.Id);
+ programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
}
- else
- {
- info = GetProgramInfoFromCache(timer.ChannelId, timer.ProgramId);
- }
-
- if (info == null)
+ if (programInfo == null)
{
_logger.Info("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
- info = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
+ programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
}
- if (info == null)
+ if (programInfo != null)
{
- throw new InvalidOperationException(string.Format("Program with Id {0} not found", timer.ProgramId));
+ RecordingHelper.CopyProgramInfoToTimerInfo(programInfo, timer);
+ activeRecordingInfo.Program = programInfo;
}
- var recordPath = GetRecordingPath(timer, info);
+ string seriesPath = null;
+ var recordPath = GetRecordingPath(timer, out seriesPath);
var recordingStatus = RecordingStatus.New;
- var isResourceOpen = false;
- SemaphoreSlim semaphore = null;
+
+ string liveStreamId = null;
+
+ OnRecordingStatusChanged();
try
{
- var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false);
- isResourceOpen = true;
- semaphore = result.Item3;
- var mediaStreamInfo = result.Item1;
+ var allMediaSources = await GetChannelStreamMediaSources(timer.ChannelId, CancellationToken.None).ConfigureAwait(false);
+
+ var liveStreamInfo = await GetChannelStreamInternal(timer.ChannelId, allMediaSources[0].Id, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ var mediaStreamInfo = liveStreamInfo.Item2;
+ liveStreamId = mediaStreamInfo.Id;
// HDHR doesn't seem to release the tuner right away after first probing with ffmpeg
//await Task.Delay(3000, cancellationToken).ConfigureAwait(false);
@@ -1000,7 +1291,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
var duration = recordingEndDate - DateTime.UtcNow;
- _logger.Info("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
+ _logger.Info("Beginning recording. Will record for {0} minutes.",
+ duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
_logger.Info("Writing file to path: " + recordPath);
_logger.Info("Opening recording stream from tuner provider");
@@ -1010,20 +1302,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
timer.Status = RecordingStatus.InProgress;
_timerProvider.AddOrUpdate(timer, false);
- result.Item3.Release();
- isResourceOpen = false;
+ SaveNfo(timer, recordPath, seriesPath);
+ EnforceKeepUpTo(timer);
};
- var pathWithDuration = result.Item2.ApplyDuration(mediaStreamInfo.Path, duration);
-
- // If it supports supplying duration via url
- if (!string.Equals(pathWithDuration, mediaStreamInfo.Path, StringComparison.OrdinalIgnoreCase))
- {
- mediaStreamInfo.Path = pathWithDuration;
- mediaStreamInfo.RunTimeTicks = duration.Ticks;
- }
-
- await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken).ConfigureAwait(false);
+ await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken)
+ .ConfigureAwait(false);
recordingStatus = RecordingStatus.Completed;
_logger.Info("Recording completed: {0}", recordPath);
@@ -1038,28 +1322,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
_logger.ErrorException("Error recording to {0}", ex, recordPath);
recordingStatus = RecordingStatus.Error;
}
- finally
+
+ if (!string.IsNullOrWhiteSpace(liveStreamId))
{
- if (isResourceOpen && semaphore != null)
+ try
{
- semaphore.Release();
+ await CloseLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error closing live stream", ex);
}
-
- _libraryManager.UnRegisterIgnoredPath(recordPath);
- _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true);
-
- ActiveRecordingInfo removed;
- _activeRecordings.TryRemove(timer.Id, out removed);
}
- if (recordingStatus == RecordingStatus.Completed)
- {
- timer.Status = RecordingStatus.Completed;
- _timerProvider.Delete(timer);
+ _libraryManager.UnRegisterIgnoredPath(recordPath);
+ _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true);
- OnSuccessfulRecording(info.IsSeries, recordPath);
- }
- else if (DateTime.UtcNow < timer.EndDate)
+ ActiveRecordingInfo removed;
+ _activeRecordings.TryRemove(timer.Id, out removed);
+
+ if (recordingStatus != RecordingStatus.Completed && DateTime.UtcNow < timer.EndDate)
{
const int retryIntervalSeconds = 60;
_logger.Info("Retrying recording in {0} seconds.", retryIntervalSeconds);
@@ -1068,10 +1350,123 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
timer.StartDate = DateTime.UtcNow.AddSeconds(retryIntervalSeconds);
_timerProvider.AddOrUpdate(timer);
}
+ else if (File.Exists(recordPath))
+ {
+ timer.RecordingPath = recordPath;
+ timer.Status = RecordingStatus.Completed;
+ _timerProvider.AddOrUpdate(timer, false);
+ OnSuccessfulRecording(timer, recordPath);
+ }
else
{
_timerProvider.Delete(timer);
}
+
+ OnRecordingStatusChanged();
+ }
+
+ private void OnRecordingStatusChanged()
+ {
+ EventHelper.FireEventIfNotNull(RecordingStatusChanged, this, new RecordingStatusChangedEventArgs
+ {
+
+ }, _logger);
+ }
+
+ private async void EnforceKeepUpTo(TimerInfo timer)
+ {
+ if (string.IsNullOrWhiteSpace(timer.SeriesTimerId))
+ {
+ return;
+ }
+
+ var seriesTimerId = timer.SeriesTimerId;
+ var seriesTimer = _seriesTimerProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, seriesTimerId, StringComparison.OrdinalIgnoreCase));
+
+ if (seriesTimer == null || seriesTimer.KeepUpTo <= 1)
+ {
+ return;
+ }
+
+ if (_disposed)
+ {
+ return;
+ }
+
+ await _recordingDeleteSemaphore.WaitAsync().ConfigureAwait(false);
+
+ try
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ var timersToDelete = _timerProvider.GetAll()
+ .Where(i => i.Status == RecordingStatus.Completed && !string.IsNullOrWhiteSpace(i.RecordingPath))
+ .Where(i => string.Equals(i.SeriesTimerId, seriesTimerId, StringComparison.OrdinalIgnoreCase))
+ .OrderByDescending(i => i.EndDate)
+ .Where(i => File.Exists(i.RecordingPath))
+ .Skip(seriesTimer.KeepUpTo - 1)
+ .ToList();
+
+ await DeleteLibraryItemsForTimers(timersToDelete).ConfigureAwait(false);
+ }
+ finally
+ {
+ _recordingDeleteSemaphore.Release();
+ }
+ }
+
+ private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1);
+ private async Task DeleteLibraryItemsForTimers(List<TimerInfo> timers)
+ {
+ foreach (var timer in timers)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ try
+ {
+ await DeleteLibraryItemForTimer(timer).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error deleting recording", ex);
+ }
+ }
+ }
+
+ private async Task DeleteLibraryItemForTimer(TimerInfo timer)
+ {
+ var libraryItem = _libraryManager.FindByPath(timer.RecordingPath, false);
+
+ if (libraryItem != null)
+ {
+ await _libraryManager.DeleteItem(libraryItem, new DeleteOptions
+ {
+ DeleteFileLocation = true
+ });
+ }
+ else
+ {
+ try
+ {
+ File.Delete(timer.RecordingPath);
+ }
+ catch (DirectoryNotFoundException)
+ {
+
+ }
+ catch (FileNotFoundException)
+ {
+
+ }
+ }
+
+ _timerProvider.Delete(timer);
}
private string EnsureFileUnique(string path, string timerId)
@@ -1099,7 +1494,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return true;
}
- var hasRecordingAtPath = _activeRecordings.Values.ToList().Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.TimerId, timerId, StringComparison.OrdinalIgnoreCase));
+ var hasRecordingAtPath = _activeRecordings
+ .Values
+ .ToList()
+ .Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase));
if (hasRecordingAtPath)
{
@@ -1125,30 +1523,180 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return new DirectRecorder(_logger, _httpClient, _fileSystem);
}
- private async void OnSuccessfulRecording(bool isSeries, string path)
+ private async void OnSuccessfulRecording(TimerInfo timer, string path)
{
- if (GetConfiguration().EnableAutoOrganize)
+ if (timer.IsProgramSeries && GetConfiguration().EnableAutoOrganize)
{
- if (isSeries)
+ try
{
- try
+ // this is to account for the library monitor holding a lock for additional time after the change is complete.
+ // ideally this shouldn't be hard-coded
+ await Task.Delay(30000).ConfigureAwait(false);
+
+ var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager);
+
+ var result = await organize.OrganizeEpisodeFile(path, _config.GetAutoOrganizeOptions(), false, CancellationToken.None).ConfigureAwait(false);
+
+ if (result.Status == FileSortingStatus.Success)
{
- // this is to account for the library monitor holding a lock for additional time after the change is complete.
- // ideally this shouldn't be hard-coded
- await Task.Delay(30000).ConfigureAwait(false);
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error processing new recording", ex);
+ }
+ }
+ }
- var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager);
+ private void SaveNfo(TimerInfo timer, string recordingPath, string seriesPath)
+ {
+ try
+ {
+ if (timer.IsProgramSeries)
+ {
+ SaveSeriesNfo(timer, recordingPath, seriesPath);
+ }
+ else if (!timer.IsMovie || timer.IsSports || timer.IsNews)
+ {
+ SaveVideoNfo(timer, recordingPath);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.ErrorException("Error saving nfo", ex);
+ }
+ }
+
+ private void SaveSeriesNfo(TimerInfo timer, string recordingPath, string seriesPath)
+ {
+ var nfoPath = Path.Combine(seriesPath, "tvshow.nfo");
+
+ if (File.Exists(nfoPath))
+ {
+ return;
+ }
- var result = await organize.OrganizeEpisodeFile(path, _config.GetAutoOrganizeOptions(), false, CancellationToken.None).ConfigureAwait(false);
+ using (var stream = _fileSystem.GetFileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
+ {
+ var settings = new XmlWriterSettings
+ {
+ Indent = true,
+ Encoding = Encoding.UTF8,
+ CloseOutput = false
+ };
+
+ using (XmlWriter writer = XmlWriter.Create(stream, settings))
+ {
+ writer.WriteStartDocument(true);
+ writer.WriteStartElement("tvshow");
+
+ if (!string.IsNullOrWhiteSpace(timer.Name))
+ {
+ writer.WriteElementString("title", timer.Name);
}
- catch (Exception ex)
+
+ writer.WriteEndElement();
+ writer.WriteEndDocument();
+ }
+ }
+ }
+
+ public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
+ private void SaveVideoNfo(TimerInfo timer, string recordingPath)
+ {
+ var nfoPath = Path.ChangeExtension(recordingPath, ".nfo");
+
+ if (File.Exists(nfoPath))
+ {
+ return;
+ }
+
+ using (var stream = _fileSystem.GetFileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
+ {
+ var settings = new XmlWriterSettings
+ {
+ Indent = true,
+ Encoding = Encoding.UTF8,
+ CloseOutput = false
+ };
+
+ using (XmlWriter writer = XmlWriter.Create(stream, settings))
+ {
+ writer.WriteStartDocument(true);
+ writer.WriteStartElement("movie");
+
+ if (!string.IsNullOrWhiteSpace(timer.Name))
+ {
+ writer.WriteElementString("title", timer.Name);
+ }
+
+ writer.WriteElementString("dateadded", DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat));
+
+ if (timer.ProductionYear.HasValue)
+ {
+ writer.WriteElementString("year", timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture));
+ }
+ if (!string.IsNullOrEmpty(timer.OfficialRating))
+ {
+ writer.WriteElementString("mpaa", timer.OfficialRating);
+ }
+
+ var overview = (timer.Overview ?? string.Empty)
+ .StripHtml()
+ .Replace("&quot;", "'");
+
+ writer.WriteElementString("plot", overview);
+ writer.WriteElementString("lockdata", true.ToString().ToLower());
+
+ if (timer.CommunityRating.HasValue)
+ {
+ writer.WriteElementString("rating", timer.CommunityRating.Value.ToString(CultureInfo.InvariantCulture));
+ }
+
+ if (timer.IsSports)
+ {
+ AddGenre(timer.Genres, "Sports");
+ }
+ if (timer.IsKids)
+ {
+ AddGenre(timer.Genres, "Kids");
+ AddGenre(timer.Genres, "Children");
+ }
+ if (timer.IsNews)
+ {
+ AddGenre(timer.Genres, "News");
+ }
+
+ foreach (var genre in timer.Genres)
+ {
+ writer.WriteElementString("genre", genre);
+ }
+
+ if (!string.IsNullOrWhiteSpace(timer.ShortOverview))
+ {
+ writer.WriteElementString("outline", timer.ShortOverview);
+ }
+
+ if (!string.IsNullOrWhiteSpace(timer.HomePageUrl))
{
- _logger.ErrorException("Error processing new recording", ex);
+ writer.WriteElementString("website", timer.HomePageUrl);
}
+
+ writer.WriteEndElement();
+ writer.WriteEndDocument();
}
}
}
+ private void AddGenre(List<string> genres, string genre)
+ {
+ if (!genres.Contains(genre, StringComparer.OrdinalIgnoreCase))
+ {
+ genres.Add(genre);
+ }
+ }
+
private ProgramInfo GetProgramInfoFromCache(string channelId, string programId)
{
var epgData = GetEpgDataForChannel(channelId);
@@ -1168,41 +1716,101 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return _config.GetConfiguration<LiveTvOptions>("livetv");
}
+ private bool ShouldCancelTimerForSeriesTimer(SeriesTimerInfo seriesTimer, TimerInfo timer)
+ {
+ if (!seriesTimer.RecordAnyTime)
+ {
+ if (Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - timer.StartDate.TimeOfDay.Ticks) >= TimeSpan.FromMinutes(5).Ticks)
+ {
+ return true;
+ }
+
+ if (!seriesTimer.Days.Contains(timer.StartDate.ToLocalTime().DayOfWeek))
+ {
+ return true;
+ }
+ }
+
+ if (seriesTimer.RecordNewOnly && timer.IsRepeat)
+ {
+ return true;
+ }
+
+ if (!seriesTimer.RecordAnyChannel && !string.Equals(timer.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ return seriesTimer.SkipEpisodesInLibrary && IsProgramAlreadyInLibrary(timer);
+ }
+
private async Task UpdateTimersForSeriesTimer(List<ProgramInfo> epgData, SeriesTimerInfo seriesTimer, bool deleteInvalidTimers)
{
- var newTimers = GetTimersForSeries(seriesTimer, epgData, true).ToList();
-
+ var allTimers = GetTimersForSeries(seriesTimer, epgData)
+ .ToList();
+
var registration = await _liveTvManager.GetRegistrationInfo("seriesrecordings").ConfigureAwait(false);
if (registration.IsValid)
{
- foreach (var timer in newTimers)
+ foreach (var timer in allTimers)
{
- _timerProvider.AddOrUpdate(timer);
+ var existingTimer = _timerProvider.GetTimer(timer.Id);
+
+ if (existingTimer == null)
+ {
+ if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
+ {
+ timer.Status = RecordingStatus.Cancelled;
+ }
+ _timerProvider.Add(timer);
+ }
+ else
+ {
+ // Only update if not currently active
+ ActiveRecordingInfo activeRecordingInfo;
+ if (!_activeRecordings.TryGetValue(timer.Id, out activeRecordingInfo))
+ {
+ UpdateExistingTimerWithNewMetadata(existingTimer, timer);
+
+ if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
+ {
+ existingTimer.Status = RecordingStatus.Cancelled;
+ }
+
+ existingTimer.SeriesTimerId = seriesTimer.Id;
+ _timerProvider.Update(existingTimer);
+ }
+ }
}
}
if (deleteInvalidTimers)
{
- var allTimers = GetTimersForSeries(seriesTimer, epgData, false)
+ var allTimerIds = allTimers
.Select(i => i.Id)
.ToList();
+ var deleteStatuses = new List<RecordingStatus>
+ {
+ RecordingStatus.New
+ };
+
var deletes = _timerProvider.GetAll()
.Where(i => string.Equals(i.SeriesTimerId, seriesTimer.Id, StringComparison.OrdinalIgnoreCase))
- .Where(i => !allTimers.Contains(i.Id, StringComparer.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow)
+ .Where(i => !allTimerIds.Contains(i.Id, StringComparer.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow)
+ .Where(i => deleteStatuses.Contains(i.Status))
.ToList();
foreach (var timer in deletes)
{
- await CancelTimerAsync(timer.Id, CancellationToken.None).ConfigureAwait(false);
+ CancelTimerInternal(timer.Id, false);
}
}
}
private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer,
- IEnumerable<ProgramInfo> allPrograms,
- bool filterByCurrentRecordings)
+ IEnumerable<ProgramInfo> allPrograms)
{
if (seriesTimer == null)
{
@@ -1214,19 +1822,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
}
// Exclude programs that have already ended
- allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow && i.StartDate > DateTime.UtcNow);
+ allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow);
allPrograms = GetProgramsForSeries(seriesTimer, allPrograms);
- if (filterByCurrentRecordings)
- {
- allPrograms = allPrograms.Where(i => !IsProgramAlreadyInLibrary(i));
- }
-
return allPrograms.Select(i => RecordingHelper.CreateTimer(i, seriesTimer));
}
- private bool IsProgramAlreadyInLibrary(ProgramInfo program)
+ private bool IsProgramAlreadyInLibrary(TimerInfo program)
{
if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.EpisodeTitle))
{
@@ -1281,23 +1884,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
private IEnumerable<ProgramInfo> GetProgramsForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms)
{
- if (!seriesTimer.RecordAnyTime)
- {
- allPrograms = allPrograms.Where(epg => Math.Abs(seriesTimer.StartDate.TimeOfDay.Ticks - epg.StartDate.TimeOfDay.Ticks) < TimeSpan.FromMinutes(5).Ticks);
-
- allPrograms = allPrograms.Where(i => seriesTimer.Days.Contains(i.StartDate.ToLocalTime().DayOfWeek));
- }
-
- if (seriesTimer.RecordNewOnly)
- {
- allPrograms = allPrograms.Where(epg => !epg.IsRepeat);
- }
-
- if (!seriesTimer.RecordAnyChannel)
- {
- allPrograms = allPrograms.Where(epg => string.Equals(epg.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase));
- }
-
if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId))
{
_logger.Error("seriesTimer.SeriesId is null. Cannot find programs for series");
@@ -1341,8 +1927,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
return channelIds.SelectMany(GetEpgDataForChannel).ToList();
}
+ private bool _disposed;
public void Dispose()
{
+ _disposed = true;
foreach (var pair in _activeRecordings.ToList())
{
pair.Value.CancellationTokenSource.Cancel();
@@ -1393,7 +1981,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV
class ActiveRecordingInfo
{
public string Path { get; set; }
- public string TimerId { get; set; }
+ public TimerInfo Timer { get; set; }
+ public ProgramInfo Program { get; set; }
public CancellationTokenSource CancellationTokenSource { get; set; }
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
index 75ad3de59..3e9d186e3 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
@@ -68,36 +68,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;
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
index 9485e0325..f7b4b3fde 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs
@@ -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;
diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
index 423358906..bddce0420 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
@@ -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 bool ShouldStartTimer(TimerInfo item)
+ {
+ if (item.Status == RecordingStatus.Completed ||
+ item.Status == RecordingStatus.Cancelled)
+ {
+ return false;
+ }
+
+ return true;
}
- private void AddTimer(TimerInfo item)
+ private void AddOrUpdateSystemTimer(TimerInfo item)
{
- if (item.Status == RecordingStatus.Completed)
+ 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));
+ }
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
index d1d8df2e8..d3549aef5 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
@@ -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)
diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveStreamHelper.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveStreamHelper.cs
new file mode 100644
index 000000000..336c32bae
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/LiveStreamHelper.cs
@@ -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;
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
index 683377c61..8c46b4597 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvDtoService.cs
@@ -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,
diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
index b3ced55a5..bac9789b5 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -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;
+ info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false);
+ }
+ info.RequiresClosing = true;
- if (info.RequiresClosing)
- {
- var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_";
+ if (info.RequiresClosing)
+ {
+ var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_";
- info.LiveStreamId = idPrefix + info.Id;
- }
+ info.LiveStreamId = idPrefix + info.Id;
}
+ }
+ else
+ {
+ var recording = await GetInternalRecording(id, cancellationToken).ConfigureAwait(false);
+ isVideo = !string.Equals(recording.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase);
+ service = GetService(recording);
- _logger.Info("Live stream info: {0}", _jsonSerializer.SerializeToString(info));
- Normalize(info, service, isVideo);
+ _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;
- var data = new LiveStreamData
+ if (info.RequiresClosing)
{
- Info = info,
- IsChannel = isChannel,
- ItemId = id
- };
-
- _openStreams.AddOrUpdate(info.Id, data, (key, i) => data);
+ var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_";
- return info;
+ info.LiveStreamId = idPrefix + info.Id;
+ }
}
- catch (Exception ex)
- {
- _logger.ErrorException("Error getting channel stream", ex);
- throw;
- }
- finally
- {
- _liveStreamSemaphore.Release();
- }
+ _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);
+ throw new ArgumentException("Service not found.");
+ }
- _logger.Info("Closing live stream from {0}, stream Id: {1}", service.Name, id);
+ id = parts[1];
- await service.CloseLiveStream(id, cancellationToken).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.ErrorException("Error closing live stream", ex);
+ _logger.Info("Closing live stream from {0}, stream Id: {1}", service.Name, id);
- throw;
- }
- finally
- {
- _liveStreamSemaphore.Release();
- }
+ 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 &&
diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
index cdba1873e..a62796036 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
@@ -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);
}
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index 9bb5b4fd7..0fe74798f 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -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();
-
- var hostsWithChannel = new List<TunerHostInfo>();
+ throw new FileNotFoundException();
+ }
- foreach (var host in hosts)
- {
- if (string.IsNullOrWhiteSpace(streamId))
- {
- try
- {
- var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
+ var hosts = GetTunerHosts();
- 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;
- }
- }
+ var hostsWithChannel = new List<TunerHostInfo>();
- foreach (var host in hostsWithChannel)
+ foreach (var host in hosts)
+ {
+ if (string.IsNullOrWhiteSpace(streamId))
{
- 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)
- {
- if (!await IsAvailable(host, channelId, cancellationToken).ConfigureAwait(false))
- {
- Logger.Error("Tuner is not currently available");
- resourcePool.Release();
- continue;
- }
- }
-
- var stream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false);
+ var channels = await GetChannels(host, true, cancellationToken).ConfigureAwait(false);
- if (EnableMediaProbing)
+ if (channels.Any(i => string.Equals(i.Id, channelId, StringComparison.OrdinalIgnoreCase)))
{
- await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false);
+ hostsWithChannel.Add(host);
}
-
- 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()
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs
index 9ba1c60cc..cd168ba58 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunDiscovery.cs
@@ -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);
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index fd4775938..97d52836d 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -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;
+ }
+ audioCodec = channel.AudioCodec;
+ if (!videoBitrate.HasValue)
+ {
videoBitrate = (channel.IsHD ?? true) ? 15000000 : 2000000;
- audioBitrate = (channel.IsHD ?? true) ? 448000 : 192000;
}
+ 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);
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs
new file mode 100644
index 000000000..60222415c
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunLiveStream.cs
@@ -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);
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index 5c508aacd..b03feefe4 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -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;
- }
}
}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs
new file mode 100644
index 000000000..8ff3fd6c1
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/MulticastStream.cs
@@ -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);
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs
new file mode 100644
index 000000000..c1566b900
--- /dev/null
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/QueueStream.cs
@@ -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);
+ }
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs
index cb0e573da..a0b8ef5f7 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs
@@ -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);
diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs
index b1e349a86..81deb2995 100644
--- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs
+++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs
@@ -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)
diff --git a/MediaBrowser.Server.Implementations/Localization/Core/en-US.json b/MediaBrowser.Server.Implementations/Localization/Core/en-US.json
index 5e2f98c09..bc0dc236d 100644
--- a/MediaBrowser.Server.Implementations/Localization/Core/en-US.json
+++ b/MediaBrowser.Server.Implementations/Localization/Core/en-US.json
@@ -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",
diff --git a/MediaBrowser.Server.Implementations/Localization/Ratings/de.txt b/MediaBrowser.Server.Implementations/Localization/Ratings/de.txt
index 723807509..ad1f18619 100644
--- a/MediaBrowser.Server.Implementations/Localization/Ratings/de.txt
+++ b/MediaBrowser.Server.Implementations/Localization/Ratings/de.txt
@@ -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 \ No newline at end of file
diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
index 8850f3d35..f01a107df 100644
--- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
+++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj
@@ -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" />
diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs
index 7d0841fa6..21e847c68 100644
--- a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs
+++ b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs
@@ -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
diff --git a/MediaBrowser.Server.Implementations/News/NewsEntryPoint.cs b/MediaBrowser.Server.Implementations/News/NewsEntryPoint.cs
index 969541fbc..c15af6f70 100644
--- a/MediaBrowser.Server.Implementations/News/NewsEntryPoint.cs
+++ b/MediaBrowser.Server.Implementations/News/NewsEntryPoint.cs
@@ -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))
diff --git a/MediaBrowser.Server.Implementations/Notifications/CoreNotificationTypes.cs b/MediaBrowser.Server.Implementations/Notifications/CoreNotificationTypes.cs
index 98d3672fa..8ca667739 100644
--- a/MediaBrowser.Server.Implementations/Notifications/CoreNotificationTypes.cs
+++ b/MediaBrowser.Server.Implementations/Notifications/CoreNotificationTypes.cs
@@ -91,13 +91,6 @@ namespace MediaBrowser.Server.Implementations.Notifications
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(),
DefaultTitle = "{UserName} is playing {ItemName} on {DeviceName}.",
Variables = new List<string>{"UserName", "ItemName", "DeviceName", "AppName"}
diff --git a/MediaBrowser.Server.Implementations/Persistence/DataExtensions.cs b/MediaBrowser.Server.Implementations/Persistence/DataExtensions.cs
index a16d23700..028465354 100644
--- a/MediaBrowser.Server.Implementations/Persistence/DataExtensions.cs
+++ b/MediaBrowser.Server.Implementations/Persistence/DataExtensions.cs
@@ -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();
diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs
index 40970dbe4..1726a77a6 100644
--- a/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs
+++ b/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs
@@ -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));
}
diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs
index 5ece3dd82..2235bfe0d 100644
--- a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs
+++ b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs
@@ -19,11 +19,15 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.IO;
using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.LiveTv;
+using MediaBrowser.Server.Implementations.Devices;
+using MediaBrowser.Server.Implementations.Playlists;
namespace MediaBrowser.Server.Implementations.Persistence
{
@@ -95,11 +99,12 @@ namespace MediaBrowser.Server.Implementations.Persistence
private IDbCommand _updateInheritedTagsCommand;
public const int LatestSchemaVersion = 109;
+ private readonly IMemoryStreamProvider _memoryStreamProvider;
/// <summary>
/// Initializes a new instance of the <see cref="SqliteItemRepository"/> class.
/// </summary>
- public SqliteItemRepository(IServerConfigurationManager config, IJsonSerializer jsonSerializer, ILogManager logManager, IDbConnector connector)
+ public SqliteItemRepository(IServerConfigurationManager config, IJsonSerializer jsonSerializer, ILogManager logManager, IDbConnector connector, IMemoryStreamProvider memoryStreamProvider)
: base(logManager, connector)
{
if (config == null)
@@ -113,6 +118,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
_config = config;
_jsonSerializer = jsonSerializer;
+ _memoryStreamProvider = memoryStreamProvider;
_criticReviewsPath = Path.Combine(_config.ApplicationPaths.DataPath, "critic-reviews");
DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
@@ -125,7 +131,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
var cacheSize = _config.Configuration.SqliteCacheSize;
if (cacheSize <= 0)
{
- cacheSize = Math.Min(Environment.ProcessorCount * 50000, 200000);
+ cacheSize = Math.Min(Environment.ProcessorCount * 50000, 100000);
}
var connection = await DbConnector.Connect(DbFilePath, false, false, 0 - cacheSize).ConfigureAwait(false);
@@ -270,6 +276,15 @@ namespace MediaBrowser.Server.Implementations.Persistence
_connection.AddColumn(Logger, "TypedBaseItems", "SeasonId", "GUID");
_connection.AddColumn(Logger, "TypedBaseItems", "SeriesId", "GUID");
_connection.AddColumn(Logger, "TypedBaseItems", "SeriesSortName", "Text");
+ _connection.AddColumn(Logger, "TypedBaseItems", "ExternalSeriesId", "Text");
+ _connection.AddColumn(Logger, "TypedBaseItems", "ShortOverview", "Text");
+ _connection.AddColumn(Logger, "TypedBaseItems", "Tagline", "Text");
+ _connection.AddColumn(Logger, "TypedBaseItems", "Keywords", "Text");
+ _connection.AddColumn(Logger, "TypedBaseItems", "ProviderIds", "Text");
+ _connection.AddColumn(Logger, "TypedBaseItems", "Images", "Text");
+ _connection.AddColumn(Logger, "TypedBaseItems", "ProductionLocations", "Text");
+ _connection.AddColumn(Logger, "TypedBaseItems", "ThemeSongIds", "Text");
+ _connection.AddColumn(Logger, "TypedBaseItems", "ThemeVideoIds", "Text");
_connection.AddColumn(Logger, "UserDataKeys", "Priority", "INT");
_connection.AddColumn(Logger, "ItemValues", "CleanValue", "Text");
@@ -412,7 +427,17 @@ namespace MediaBrowser.Server.Implementations.Persistence
"SeriesId",
"SeriesSortName",
"PresentationUniqueKey",
- "InheritedParentalRatingValue"
+ "InheritedParentalRatingValue",
+ "InheritedTags",
+ "ExternalSeriesId",
+ "ShortOverview",
+ "Tagline",
+ "Keywords",
+ "ProviderIds",
+ "Images",
+ "ProductionLocations",
+ "ThemeSongIds",
+ "ThemeVideoIds"
};
private readonly string[] _mediaStreamSaveColumns =
@@ -534,7 +559,16 @@ namespace MediaBrowser.Server.Implementations.Persistence
"SeasonName",
"SeasonId",
"SeriesId",
- "SeriesSortName"
+ "SeriesSortName",
+ "ExternalSeriesId",
+ "ShortOverview",
+ "Tagline",
+ "Keywords",
+ "ProviderIds",
+ "Images",
+ "ProductionLocations",
+ "ThemeSongIds",
+ "ThemeVideoIds"
};
_saveItemCommand = _connection.CreateCommand();
_saveItemCommand.CommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns.ToArray()) + ") values (";
@@ -720,7 +754,15 @@ namespace MediaBrowser.Server.Implementations.Persistence
_saveItemCommand.GetParameter(index++).Value = item.Id;
_saveItemCommand.GetParameter(index++).Value = item.GetType().FullName;
- _saveItemCommand.GetParameter(index++).Value = _jsonSerializer.SerializeToBytes(item);
+
+ if (TypeRequiresDeserialization(item.GetType()))
+ {
+ _saveItemCommand.GetParameter(index++).Value = _jsonSerializer.SerializeToBytes(item, _memoryStreamProvider);
+ }
+ else
+ {
+ _saveItemCommand.GetParameter(index++).Value = null;
+ }
_saveItemCommand.GetParameter(index++).Value = item.Path;
@@ -974,6 +1016,49 @@ namespace MediaBrowser.Server.Implementations.Persistence
_saveItemCommand.GetParameter(index++).Value = null;
}
+ _saveItemCommand.GetParameter(index++).Value = item.ExternalSeriesId;
+ _saveItemCommand.GetParameter(index++).Value = item.ShortOverview;
+ _saveItemCommand.GetParameter(index++).Value = item.Tagline;
+
+ if (item.Keywords.Count > 0)
+ {
+ _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.Keywords.ToArray());
+ }
+ else
+ {
+ _saveItemCommand.GetParameter(index++).Value = null;
+ }
+
+ _saveItemCommand.GetParameter(index++).Value = SerializeProviderIds(item);
+ _saveItemCommand.GetParameter(index++).Value = SerializeImages(item);
+
+ if (item.ProductionLocations.Count > 0)
+ {
+ _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.ProductionLocations.ToArray());
+ }
+ else
+ {
+ _saveItemCommand.GetParameter(index++).Value = null;
+ }
+
+ if (item.ThemeSongIds.Count > 0)
+ {
+ _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.ThemeSongIds.Select(i => i.ToString("N")).ToArray());
+ }
+ else
+ {
+ _saveItemCommand.GetParameter(index++).Value = null;
+ }
+
+ if (item.ThemeVideoIds.Count > 0)
+ {
+ _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.ThemeVideoIds.Select(i => i.ToString("N")).ToArray());
+ }
+ else
+ {
+ _saveItemCommand.GetParameter(index++).Value = null;
+ }
+
_saveItemCommand.Transaction = transaction;
_saveItemCommand.ExecuteNonQuery();
@@ -1022,6 +1107,99 @@ namespace MediaBrowser.Server.Implementations.Persistence
}
}
+ private string SerializeProviderIds(BaseItem item)
+ {
+ var ids = item.ProviderIds.ToList();
+
+ if (ids.Count == 0)
+ {
+ return null;
+ }
+
+ return string.Join("|", ids.Select(i => i.Key + "=" + i.Value).ToArray());
+ }
+
+ private void DeserializeProviderIds(string value, BaseItem item)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return;
+ }
+
+ if (item.ProviderIds.Count > 0)
+ {
+ return;
+ }
+
+ var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+
+ foreach (var part in parts)
+ {
+ var idParts = part.Split('=');
+
+ item.SetProviderId(idParts[0], idParts[1]);
+ }
+ }
+
+ private string SerializeImages(BaseItem item)
+ {
+ var images = item.ImageInfos.ToList();
+
+ if (images.Count == 0)
+ {
+ return null;
+ }
+
+ return string.Join("|", images.Select(ToValueString).ToArray());
+ }
+
+ private void DeserializeImages(string value, BaseItem item)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return;
+ }
+
+ if (item.ImageInfos.Count > 0)
+ {
+ return;
+ }
+
+ var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+
+ foreach (var part in parts)
+ {
+ item.ImageInfos.Add(ItemImageInfoFromValueString(part));
+ }
+ }
+
+ public string ToValueString(ItemImageInfo image)
+ {
+ var delimeter = "*";
+
+ return (image.Path ?? string.Empty) +
+ delimeter +
+ image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) +
+ delimeter +
+ image.Type +
+ delimeter +
+ image.IsPlaceholder;
+ }
+
+ public ItemImageInfo ItemImageInfoFromValueString(string value)
+ {
+ var parts = value.Split(new[] { '*' }, StringSplitOptions.RemoveEmptyEntries);
+
+ var image = new ItemImageInfo();
+
+ image.Path = parts[0];
+ image.DateModified = new DateTime(long.Parse(parts[1], CultureInfo.InvariantCulture), DateTimeKind.Utc);
+ image.Type = (ImageType)Enum.Parse(typeof(ImageType), parts[2], true);
+ image.IsPlaceholder = string.Equals(parts[3], true.ToString(), StringComparison.OrdinalIgnoreCase);
+
+ return image;
+ }
+
/// <summary>
/// Internal retrieve from items or users table
/// </summary>
@@ -1056,6 +1234,91 @@ namespace MediaBrowser.Server.Implementations.Persistence
private BaseItem GetItem(IDataReader reader)
{
+ return GetItem(reader, new InternalItemsQuery());
+ }
+
+ private bool TypeRequiresDeserialization(Type type)
+ {
+ if (_config.Configuration.SkipDeserializationForBasicTypes)
+ {
+ if (type == typeof(MusicGenre))
+ {
+ return false;
+ }
+ if (type == typeof(GameGenre))
+ {
+ return false;
+ }
+ if (type == typeof(Genre))
+ {
+ return false;
+ }
+ if (type == typeof(Studio))
+ {
+ return false;
+ }
+ if (type == typeof(Year))
+ {
+ return false;
+ }
+ if (type == typeof(Book))
+ {
+ return false;
+ }
+ if (type == typeof(Person))
+ {
+ return false;
+ }
+ if (type == typeof(RecordingGroup))
+ {
+ return false;
+ }
+ if (type == typeof(Channel))
+ {
+ return false;
+ }
+ if (type == typeof(ManualCollectionsFolder))
+ {
+ return false;
+ }
+ if (type == typeof(CameraUploadsFolder))
+ {
+ return false;
+ }
+ if (type == typeof(PlaylistsFolder))
+ {
+ return false;
+ }
+ if (type == typeof(UserRootFolder))
+ {
+ return false;
+ }
+ if (type == typeof(PhotoAlbum))
+ {
+ return false;
+ }
+ if (type == typeof(Season))
+ {
+ return false;
+ }
+ if (type == typeof(MusicArtist))
+ {
+ return false;
+ }
+ }
+ if (_config.Configuration.SkipDeserializationForPrograms)
+ {
+ if (type == typeof(LiveTvProgram))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private BaseItem GetItem(IDataReader reader, InternalItemsQuery query)
+ {
var typeString = reader.GetString(0);
var type = _typeMapper.GetType(typeString);
@@ -1069,34 +1332,37 @@ namespace MediaBrowser.Server.Implementations.Persistence
BaseItem item = null;
- using (var stream = reader.GetMemoryStream(1))
+ if (TypeRequiresDeserialization(type))
{
- try
- {
- item = _jsonSerializer.DeserializeFromStream(stream, type) as BaseItem;
- }
- catch (SerializationException ex)
- {
- Logger.ErrorException("Error deserializing item", ex);
- }
-
- if (item == null)
+ using (var stream = reader.GetMemoryStream(1, _memoryStreamProvider))
{
try
{
- item = Activator.CreateInstance(type) as BaseItem;
+ item = _jsonSerializer.DeserializeFromStream(stream, type) as BaseItem;
}
- catch
+ catch (SerializationException ex)
{
+ Logger.ErrorException("Error deserializing item", ex);
}
}
+ }
- if (item == null)
+ if (item == null)
+ {
+ try
+ {
+ item = Activator.CreateInstance(type) as BaseItem;
+ }
+ catch
{
- return null;
}
}
+ if (item == null)
+ {
+ return null;
+ }
+
if (!reader.IsDBNull(2))
{
var hasStartDate = item as IHasStartDate;
@@ -1170,194 +1436,275 @@ namespace MediaBrowser.Server.Implementations.Persistence
}
}
- if (!reader.IsDBNull(15))
- {
- item.CommunityRating = reader.GetFloat(15);
- }
+ var index = 15;
- if (!reader.IsDBNull(16))
+ if (!reader.IsDBNull(index))
{
- item.CustomRating = reader.GetString(16);
+ item.CommunityRating = reader.GetFloat(index);
}
+ index++;
- if (!reader.IsDBNull(17))
+ if (query.HasField(ItemFields.CustomRating))
{
- item.IndexNumber = reader.GetInt32(17);
+ if (!reader.IsDBNull(index))
+ {
+ item.CustomRating = reader.GetString(index);
+ }
+ index++;
}
- if (!reader.IsDBNull(18))
+ if (!reader.IsDBNull(index))
{
- item.IsLocked = reader.GetBoolean(18);
+ item.IndexNumber = reader.GetInt32(index);
}
+ index++;
- if (!reader.IsDBNull(19))
+ if (query.HasField(ItemFields.Settings))
{
- item.PreferredMetadataLanguage = reader.GetString(19);
- }
+ if (!reader.IsDBNull(index))
+ {
+ item.IsLocked = reader.GetBoolean(index);
+ }
+ index++;
- if (!reader.IsDBNull(20))
- {
- item.PreferredMetadataCountryCode = reader.GetString(20);
+ if (!reader.IsDBNull(index))
+ {
+ item.PreferredMetadataLanguage = reader.GetString(index);
+ }
+ index++;
+
+ if (!reader.IsDBNull(index))
+ {
+ item.PreferredMetadataCountryCode = reader.GetString(index);
+ }
+ index++;
}
- if (!reader.IsDBNull(21))
+ if (!reader.IsDBNull(index))
{
- item.IsHD = reader.GetBoolean(21);
+ item.IsHD = reader.GetBoolean(index);
}
+ index++;
- if (!reader.IsDBNull(22))
+ if (!reader.IsDBNull(index))
{
- item.ExternalEtag = reader.GetString(22);
+ item.ExternalEtag = reader.GetString(index);
}
+ index++;
- if (!reader.IsDBNull(23))
+ if (!reader.IsDBNull(index))
{
- item.DateLastRefreshed = reader.GetDateTime(23).ToUniversalTime();
+ item.DateLastRefreshed = reader.GetDateTime(index).ToUniversalTime();
}
+ index++;
- if (!reader.IsDBNull(24))
+ if (!reader.IsDBNull(index))
{
- item.Name = reader.GetString(24);
+ item.Name = reader.GetString(index);
}
+ index++;
- if (!reader.IsDBNull(25))
+ if (!reader.IsDBNull(index))
{
- item.Path = reader.GetString(25);
+ item.Path = reader.GetString(index);
}
+ index++;
- if (!reader.IsDBNull(26))
+ if (!reader.IsDBNull(index))
{
- item.PremiereDate = reader.GetDateTime(26).ToUniversalTime();
+ item.PremiereDate = reader.GetDateTime(index).ToUniversalTime();
}
+ index++;
- if (!reader.IsDBNull(27))
+ if (query.HasField(ItemFields.Overview))
{
- item.Overview = reader.GetString(27);
+ if (!reader.IsDBNull(index))
+ {
+ item.Overview = reader.GetString(index);
+ }
+ index++;
}
- if (!reader.IsDBNull(28))
+ if (!reader.IsDBNull(index))
{
- item.ParentIndexNumber = reader.GetInt32(28);
+ item.ParentIndexNumber = reader.GetInt32(index);
}
+ index++;
- if (!reader.IsDBNull(29))
+ if (!reader.IsDBNull(index))
{
- item.ProductionYear = reader.GetInt32(29);
+ item.ProductionYear = reader.GetInt32(index);
}
+ index++;
- if (!reader.IsDBNull(30))
+ if (!reader.IsDBNull(index))
{
- item.OfficialRating = reader.GetString(30);
+ item.OfficialRating = reader.GetString(index);
}
+ index++;
- if (!reader.IsDBNull(31))
+ if (query.HasField(ItemFields.OfficialRatingDescription))
{
- item.OfficialRatingDescription = reader.GetString(31);
+ if (!reader.IsDBNull(index))
+ {
+ item.OfficialRatingDescription = reader.GetString(index);
+ }
+ index++;
}
- if (!reader.IsDBNull(32))
+ if (query.HasField(ItemFields.HomePageUrl))
{
- item.HomePageUrl = reader.GetString(32);
+ if (!reader.IsDBNull(index))
+ {
+ item.HomePageUrl = reader.GetString(index);
+ }
+ index++;
}
- if (!reader.IsDBNull(33))
+ if (query.HasField(ItemFields.DisplayMediaType))
{
- item.DisplayMediaType = reader.GetString(33);
+ if (!reader.IsDBNull(index))
+ {
+ item.DisplayMediaType = reader.GetString(index);
+ }
+ index++;
}
- if (!reader.IsDBNull(34))
+ if (query.HasField(ItemFields.SortName))
{
- item.ForcedSortName = reader.GetString(34);
+ if (!reader.IsDBNull(index))
+ {
+ item.ForcedSortName = reader.GetString(index);
+ }
+ index++;
}
- if (!reader.IsDBNull(35))
+ if (!reader.IsDBNull(index))
{
- item.RunTimeTicks = reader.GetInt64(35);
+ item.RunTimeTicks = reader.GetInt64(index);
}
+ index++;
- if (!reader.IsDBNull(36))
+ if (query.HasField(ItemFields.VoteCount))
{
- item.VoteCount = reader.GetInt32(36);
+ if (!reader.IsDBNull(index))
+ {
+ item.VoteCount = reader.GetInt32(index);
+ }
+ index++;
}
- if (!reader.IsDBNull(37))
+ if (query.HasField(ItemFields.DateCreated))
{
- item.DateCreated = reader.GetDateTime(37).ToUniversalTime();
+ if (!reader.IsDBNull(index))
+ {
+ item.DateCreated = reader.GetDateTime(index).ToUniversalTime();
+ }
+ index++;
}
- if (!reader.IsDBNull(38))
+ if (!reader.IsDBNull(index))
{
- item.DateModified = reader.GetDateTime(38).ToUniversalTime();
+ item.DateModified = reader.GetDateTime(index).ToUniversalTime();
}
+ index++;
- item.Id = reader.GetGuid(39);
+ item.Id = reader.GetGuid(index);
+ index++;
- if (!reader.IsDBNull(40))
+ if (query.HasField(ItemFields.Genres))
{
- item.Genres = reader.GetString(40).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
+ if (!reader.IsDBNull(index))
+ {
+ item.Genres = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
+ }
+ index++;
}
- if (!reader.IsDBNull(41))
+ if (!reader.IsDBNull(index))
{
- item.ParentId = reader.GetGuid(41);
+ item.ParentId = reader.GetGuid(index);
}
+ index++;
- if (!reader.IsDBNull(42))
+ if (!reader.IsDBNull(index))
{
- item.Audio = (ProgramAudio)Enum.Parse(typeof(ProgramAudio), reader.GetString(42), true);
+ item.Audio = (ProgramAudio)Enum.Parse(typeof(ProgramAudio), reader.GetString(index), true);
}
+ index++;
- if (!reader.IsDBNull(43))
+ if (query.HasField(ItemFields.ServiceName))
{
- item.ServiceName = reader.GetString(43);
+ if (!reader.IsDBNull(index))
+ {
+ item.ServiceName = reader.GetString(index);
+ }
+ index++;
}
- if (!reader.IsDBNull(44))
+ if (!reader.IsDBNull(index))
{
- item.IsInMixedFolder = reader.GetBoolean(44);
+ item.IsInMixedFolder = reader.GetBoolean(index);
}
+ index++;
- if (!reader.IsDBNull(45))
+ if (!reader.IsDBNull(index))
{
- item.DateLastSaved = reader.GetDateTime(45).ToUniversalTime();
+ item.DateLastSaved = reader.GetDateTime(index).ToUniversalTime();
}
+ index++;
- if (!reader.IsDBNull(46))
+ if (query.HasField(ItemFields.Settings))
{
- item.LockedFields = reader.GetString(46).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => (MetadataFields)Enum.Parse(typeof(MetadataFields), i, true)).ToList();
+ if (!reader.IsDBNull(index))
+ {
+ item.LockedFields = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => (MetadataFields)Enum.Parse(typeof(MetadataFields), i, true)).ToList();
+ }
+ index++;
}
- if (!reader.IsDBNull(47))
+ if (query.HasField(ItemFields.Studios))
{
- item.Studios = reader.GetString(47).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
+ if (!reader.IsDBNull(index))
+ {
+ item.Studios = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
+ }
+ index++;
}
- if (!reader.IsDBNull(48))
+ if (query.HasField(ItemFields.Tags))
{
- item.Tags = reader.GetString(48).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
+ if (!reader.IsDBNull(index))
+ {
+ item.Tags = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
+ }
+ index++;
}
- if (!reader.IsDBNull(49))
+ if (!reader.IsDBNull(index))
{
- item.SourceType = (SourceType)Enum.Parse(typeof(SourceType), reader.GetString(49), true);
+ item.SourceType = (SourceType)Enum.Parse(typeof(SourceType), reader.GetString(index), true);
}
+ index++;
var trailer = item as Trailer;
if (trailer != null)
{
- if (!reader.IsDBNull(50))
+ if (!reader.IsDBNull(index))
{
- trailer.TrailerTypes = reader.GetString(50).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => (TrailerType)Enum.Parse(typeof(TrailerType), i, true)).ToList();
+ trailer.TrailerTypes = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => (TrailerType)Enum.Parse(typeof(TrailerType), i, true)).ToList();
}
}
+ index++;
- var index = 51;
-
- if (!reader.IsDBNull(index))
+ if (query.HasField(ItemFields.OriginalTitle))
{
- item.OriginalTitle = reader.GetString(index);
+ if (!reader.IsDBNull(index))
+ {
+ item.OriginalTitle = reader.GetString(index);
+ }
+ index++;
}
- index++;
var video = item as Video;
if (video != null)
@@ -1369,12 +1716,15 @@ namespace MediaBrowser.Server.Implementations.Persistence
}
index++;
- var folder = item as Folder;
- if (folder != null && !reader.IsDBNull(index))
+ if (query.HasField(ItemFields.DateLastMediaAdded))
{
- folder.DateLastMediaAdded = reader.GetDateTime(index).ToUniversalTime();
+ var folder = item as Folder;
+ if (folder != null && !reader.IsDBNull(index))
+ {
+ folder.DateLastMediaAdded = reader.GetDateTime(index).ToUniversalTime();
+ }
+ index++;
}
- index++;
if (!reader.IsDBNull(index))
{
@@ -1388,11 +1738,14 @@ namespace MediaBrowser.Server.Implementations.Persistence
}
index++;
- if (!reader.IsDBNull(index))
+ if (query.HasField(ItemFields.CriticRatingSummary))
{
- item.CriticRatingSummary = reader.GetString(index);
+ if (!reader.IsDBNull(index))
+ {
+ item.CriticRatingSummary = reader.GetString(index);
+ }
+ index++;
}
- index++;
if (!reader.IsDBNull(index))
{
@@ -1459,6 +1812,105 @@ namespace MediaBrowser.Server.Implementations.Persistence
}
index++;
+ if (!reader.IsDBNull(index))
+ {
+ item.InheritedTags = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
+ }
+ index++;
+
+ if (!reader.IsDBNull(index))
+ {
+ item.ExternalSeriesId = reader.GetString(index);
+ }
+ index++;
+
+ if (query.HasField(ItemFields.ShortOverview))
+ {
+ if (!reader.IsDBNull(index))
+ {
+ item.ShortOverview = reader.GetString(index);
+ }
+ index++;
+ }
+
+ if (query.HasField(ItemFields.Taglines))
+ {
+ if (!reader.IsDBNull(index))
+ {
+ item.Tagline = reader.GetString(index);
+ }
+ index++;
+ }
+
+ if (query.HasField(ItemFields.Keywords))
+ {
+ if (!reader.IsDBNull(index))
+ {
+ item.Keywords = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
+ }
+ index++;
+ }
+
+ if (!reader.IsDBNull(index))
+ {
+ DeserializeProviderIds(reader.GetString(index), item);
+ }
+ index++;
+
+ if (query.DtoOptions.EnableImages)
+ {
+ if (!reader.IsDBNull(index))
+ {
+ DeserializeImages(reader.GetString(index), item);
+ }
+ index++;
+ }
+
+ if (query.HasField(ItemFields.ProductionLocations))
+ {
+ if (!reader.IsDBNull(index))
+ {
+ item.ProductionLocations = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList();
+ }
+ index++;
+ }
+
+ if (query.HasField(ItemFields.ThemeSongIds))
+ {
+ if (!reader.IsDBNull(index))
+ {
+ item.ThemeSongIds = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => new Guid(i)).ToList();
+ }
+ index++;
+ }
+
+ if (query.HasField(ItemFields.ThemeVideoIds))
+ {
+ if (!reader.IsDBNull(index))
+ {
+ item.ThemeVideoIds = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).Select(i => new Guid(i)).ToList();
+ }
+ index++;
+ }
+
+ if (string.IsNullOrWhiteSpace(item.Tagline))
+ {
+ var movie = item as Movie;
+ if (movie != null && movie.Taglines.Count > 0)
+ {
+ movie.Tagline = movie.Taglines[0];
+ }
+ }
+
+ if (type == typeof(Person) && item.ProductionLocations.Count == 0)
+ {
+ var person = (Person)item;
+ if (!string.IsNullOrWhiteSpace(person.PlaceOfBirth))
+ {
+ item.ProductionLocations = new List<string> { person.PlaceOfBirth };
+ }
+ }
+
return item;
}
@@ -1723,28 +2175,28 @@ namespace MediaBrowser.Server.Implementations.Persistence
return true;
}
- if (query.SortBy != null && query.SortBy.Length > 0)
+ var sortingFields = query.SortBy.ToList();
+ sortingFields.AddRange(query.OrderBy.Select(i => i.Item1));
+
+ if (sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked, StringComparer.OrdinalIgnoreCase))
{
- if (query.SortBy.Contains(ItemSortBy.IsFavoriteOrLiked, StringComparer.OrdinalIgnoreCase))
- {
- return true;
- }
- if (query.SortBy.Contains(ItemSortBy.IsPlayed, StringComparer.OrdinalIgnoreCase))
- {
- return true;
- }
- if (query.SortBy.Contains(ItemSortBy.IsUnplayed, StringComparer.OrdinalIgnoreCase))
- {
- return true;
- }
- if (query.SortBy.Contains(ItemSortBy.PlayCount, StringComparer.OrdinalIgnoreCase))
- {
- return true;
- }
- if (query.SortBy.Contains(ItemSortBy.DatePlayed, StringComparer.OrdinalIgnoreCase))
- {
- return true;
- }
+ return true;
+ }
+ if (sortingFields.Contains(ItemSortBy.IsPlayed, StringComparer.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ if (sortingFields.Contains(ItemSortBy.IsUnplayed, StringComparer.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ if (sortingFields.Contains(ItemSortBy.PlayCount, StringComparer.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ if (sortingFields.Contains(ItemSortBy.DatePlayed, StringComparer.OrdinalIgnoreCase))
+ {
+ return true;
}
if (query.IsFavoriteOrLiked.HasValue)
@@ -1775,10 +2227,52 @@ namespace MediaBrowser.Server.Implementations.Persistence
return false;
}
+ private List<ItemFields> allFields = Enum.GetNames(typeof(ItemFields))
+ .Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true))
+ .ToList();
+
+ private IEnumerable<string> GetColumnNamesFromField(ItemFields field)
+ {
+ if (field == ItemFields.Settings)
+ {
+ return new[] { "IsLocked", "PreferredMetadataCountryCode", "PreferredMetadataLanguage", "LockedFields" };
+ }
+ if (field == ItemFields.ServiceName)
+ {
+ return new[] { "ExternalServiceId" };
+ }
+ if (field == ItemFields.SortName)
+ {
+ return new[] { "ForcedSortName" };
+ }
+ if (field == ItemFields.Taglines)
+ {
+ return new[] { "Tagline" };
+ }
+
+ return new[] { field.ToString() };
+ }
+
private string[] GetFinalColumnsToSelect(InternalItemsQuery query, string[] startColumns, IDbCommand cmd)
{
var list = startColumns.ToList();
+ foreach (var field in allFields)
+ {
+ if (!query.HasField(field))
+ {
+ foreach (var fieldToRemove in GetColumnNamesFromField(field).ToList())
+ {
+ list.Remove(fieldToRemove);
+ }
+ }
+ }
+
+ if (!query.DtoOptions.EnableImages)
+ {
+ list.Remove("Images");
+ }
+
if (EnableJoinUserData(query))
{
list.Add("UserDataDb.UserData.UserId");
@@ -1933,7 +2427,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
while (reader.Read())
{
- var item = GetItem(reader);
+ var item = GetItem(reader, query);
if (item != null)
{
list.Add(item);
@@ -2120,7 +2614,7 @@ namespace MediaBrowser.Server.Implementations.Persistence
{
while (reader.Read())
{
- var item = GetItem(reader);
+ var item = GetItem(reader, query);
if (item != null)
{
list.Add(item);
@@ -2144,34 +2638,41 @@ namespace MediaBrowser.Server.Implementations.Persistence
private string GetOrderByText(InternalItemsQuery query)
{
+ var orderBy = query.OrderBy.ToList();
+ var enableOrderInversion = true;
+
+ if (orderBy.Count == 0)
+ {
+ orderBy.AddRange(query.SortBy.Select(i => new Tuple<string, SortOrder>(i, query.SortOrder)));
+ }
+ else
+ {
+ enableOrderInversion = false;
+ }
+
if (query.SimilarTo != null)
{
- if (query.SortBy == null || query.SortBy.Length == 0)
+ if (orderBy.Count == 0)
{
- if (query.User != null)
- {
- query.SortBy = new[] { "SimilarityScore", ItemSortBy.Random };
- }
- else
- {
- query.SortBy = new[] { "SimilarityScore", ItemSortBy.Random };
- }
+ orderBy.Add(new Tuple<string, SortOrder>("SimilarityScore", SortOrder.Descending));
+ orderBy.Add(new Tuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending));
query.SortOrder = SortOrder.Descending;
+ enableOrderInversion = false;
}
}
- if (query.SortBy == null || query.SortBy.Length == 0)
+ query.OrderBy = orderBy;
+
+ if (orderBy.Count == 0)
{
return string.Empty;
}
- var isAscending = query.SortOrder != SortOrder.Descending;
-
- return " ORDER BY " + string.Join(",", query.SortBy.Select(i =>
+ return " ORDER BY " + string.Join(",", orderBy.Select(i =>
{
- var columnMap = MapOrderByField(i, query);
- var columnAscending = isAscending;
- if (columnMap.Item2)
+ var columnMap = MapOrderByField(i.Item1, query);
+ var columnAscending = i.Item2 == SortOrder.Ascending;
+ if (columnMap.Item2 && enableOrderInversion)
{
columnAscending = !columnAscending;
}
@@ -2523,38 +3024,116 @@ namespace MediaBrowser.Server.Implementations.Persistence
whereClauses.Add("IsOffline=@IsOffline");
cmd.Parameters.Add(cmd, "@IsOffline", DbType.Boolean).Value = query.IsOffline;
}
- if (query.IsMovie.HasValue)
+
+ var exclusiveProgramAttribtues = !(query.IsMovie ?? true) ||
+ !(query.IsSports ?? true) ||
+ !(query.IsKids ?? true) ||
+ !(query.IsNews ?? true) ||
+ !(query.IsSeries ?? true);
+
+ if (exclusiveProgramAttribtues)
{
- var alternateTypes = new List<string>();
- if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Movie).Name))
+ if (query.IsMovie.HasValue)
+ {
+ var alternateTypes = new List<string>();
+ if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Movie).Name))
+ {
+ alternateTypes.Add(typeof(Movie).FullName);
+ }
+ if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Trailer).Name))
+ {
+ alternateTypes.Add(typeof(Trailer).FullName);
+ }
+
+ if (alternateTypes.Count == 0)
+ {
+ whereClauses.Add("IsMovie=@IsMovie");
+ cmd.Parameters.Add(cmd, "@IsMovie", DbType.Boolean).Value = query.IsMovie;
+ }
+ else
+ {
+ whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)");
+ cmd.Parameters.Add(cmd, "@IsMovie", DbType.Boolean).Value = query.IsMovie;
+ }
+ }
+ if (query.IsSeries.HasValue)
{
- alternateTypes.Add(typeof(Movie).FullName);
+ whereClauses.Add("IsSeries=@IsSeries");
+ cmd.Parameters.Add(cmd, "@IsSeries", DbType.Boolean).Value = query.IsSeries;
}
- if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Trailer).Name))
+ if (query.IsNews.HasValue)
{
- alternateTypes.Add(typeof(Trailer).FullName);
+ whereClauses.Add("IsNews=@IsNews");
+ cmd.Parameters.Add(cmd, "@IsNews", DbType.Boolean).Value = query.IsNews;
}
-
- if (alternateTypes.Count == 0)
+ if (query.IsKids.HasValue)
{
- whereClauses.Add("IsMovie=@IsMovie");
+ whereClauses.Add("IsKids=@IsKids");
+ cmd.Parameters.Add(cmd, "@IsKids", DbType.Boolean).Value = query.IsKids;
}
- else
+ if (query.IsSports.HasValue)
{
- whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)");
+ whereClauses.Add("IsSports=@IsSports");
+ cmd.Parameters.Add(cmd, "@IsSports", DbType.Boolean).Value = query.IsSports;
}
- cmd.Parameters.Add(cmd, "@IsMovie", DbType.Boolean).Value = query.IsMovie;
}
- if (query.IsKids.HasValue)
+ else
{
- whereClauses.Add("IsKids=@IsKids");
- cmd.Parameters.Add(cmd, "@IsKids", DbType.Boolean).Value = query.IsKids;
+ var programAttribtues = new List<string>();
+ if (query.IsMovie ?? false)
+ {
+ var alternateTypes = new List<string>();
+ if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Movie).Name))
+ {
+ alternateTypes.Add(typeof(Movie).FullName);
+ }
+ if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Trailer).Name))
+ {
+ alternateTypes.Add(typeof(Trailer).FullName);
+ }
+
+ if (alternateTypes.Count == 0)
+ {
+ programAttribtues.Add("IsMovie=@IsMovie");
+ }
+ else
+ {
+ programAttribtues.Add("(IsMovie is null OR IsMovie=@IsMovie)");
+ }
+
+ cmd.Parameters.Add(cmd, "@IsMovie", DbType.Boolean).Value = true;
+ }
+ if (query.IsSports ?? false)
+ {
+ programAttribtues.Add("IsSports=@IsSports");
+ cmd.Parameters.Add(cmd, "@IsSports", DbType.Boolean).Value = true;
+ }
+ if (query.IsNews ?? false)
+ {
+ programAttribtues.Add("IsNews=@IsNews");
+ cmd.Parameters.Add(cmd, "@IsNews", DbType.Boolean).Value = true;
+ }
+ if (query.IsSeries ?? false)
+ {
+ programAttribtues.Add("IsSeries=@IsSeries");
+ cmd.Parameters.Add(cmd, "@IsSeries", DbType.Boolean).Value = true;
+ }
+ if (query.IsKids ?? false)
+ {
+ programAttribtues.Add("IsKids=@IsKids");
+ cmd.Parameters.Add(cmd, "@IsKids", DbType.Boolean).Value = true;
+ }
+ if (programAttribtues.Count > 0)
+ {
+ whereClauses.Add("(" + string.Join(" OR ", programAttribtues.ToArray()) + ")");
+ }
}
- if (query.IsSports.HasValue)
+
+ if (query.SimilarTo != null)
{
- whereClauses.Add("IsSports=@IsSports");
- cmd.Parameters.Add(cmd, "@IsSports", DbType.Boolean).Value = query.IsSports;
+ whereClauses.Add("SimilarityScore > 0");
}
+
if (query.IsFolder.HasValue)
{
whereClauses.Add("IsFolder=@IsFolder");
@@ -2626,6 +3205,18 @@ namespace MediaBrowser.Server.Implementations.Persistence
cmd.Parameters.Add(cmd, "@MinIndexNumber", DbType.Int32).Value = query.MinIndexNumber.Value;
}
+ if (query.MinDateCreated.HasValue)
+ {
+ whereClauses.Add("DateCreated>=@MinDateCreated");
+ cmd.Parameters.Add(cmd, "@MinDateCreated", DbType.DateTime).Value = query.MinDateCreated.Value;
+ }
+
+ if (query.MinDateLastSaved.HasValue)
+ {
+ whereClauses.Add("DateLastSaved>=@MinDateLastSaved");
+ cmd.Parameters.Add(cmd, "@MinDateLastSaved", DbType.DateTime).Value = query.MinDateLastSaved.Value;
+ }
+
//if (query.MinPlayers.HasValue)
//{
// whereClauses.Add("Players>=@MinPlayers");
@@ -2765,6 +3356,12 @@ namespace MediaBrowser.Server.Implementations.Persistence
cmd.Parameters.Add(cmd, "@MinSortName", DbType.String).Value = query.MinSortName;
}
+ if (!string.IsNullOrWhiteSpace(query.ExternalSeriesId))
+ {
+ whereClauses.Add("ExternalSeriesId=@ExternalSeriesId");
+ cmd.Parameters.Add(cmd, "@ExternalSeriesId", DbType.String).Value = query.ExternalSeriesId;
+ }
+
if (!string.IsNullOrWhiteSpace(query.Name))
{
whereClauses.Add("CleanName=@Name");
@@ -3235,6 +3832,28 @@ namespace MediaBrowser.Server.Implementations.Persistence
clause += ")";
whereClauses.Add(clause);
}
+ if (query.HasThemeSong.HasValue)
+ {
+ if (query.HasThemeSong.Value)
+ {
+ whereClauses.Add("ThemeSongIds not null");
+ }
+ else
+ {
+ whereClauses.Add("ThemeSongIds is null");
+ }
+ }
+ if (query.HasThemeVideo.HasValue)
+ {
+ if (query.HasThemeVideo.Value)
+ {
+ whereClauses.Add("ThemeVideoIds not null");
+ }
+ else
+ {
+ whereClauses.Add("ThemeVideoIds is null");
+ }
+ }
//var enableItemsByName = query.IncludeItemsByName ?? query.IncludeItemTypes.Length > 0;
var enableItemsByName = query.IncludeItemsByName ?? false;
diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs
index 25ab60ca5..31fa78806 100644
--- a/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs
+++ b/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs
@@ -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;
diff --git a/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs b/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs
index ff0e4a0e0..63dfe20b2 100644
--- a/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs
+++ b/MediaBrowser.Server.Implementations/Playlists/ManualPlaylistsFolder.cs
@@ -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
diff --git a/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs b/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs
index 33d106916..893592fa3 100644
--- a/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs
+++ b/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs
@@ -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,
diff --git a/MediaBrowser.Server.Implementations/ServerManager/WebSocketConnection.cs b/MediaBrowser.Server.Implementations/ServerManager/WebSocketConnection.cs
index 2adf3e86a..60b04cf82 100644
--- a/MediaBrowser.Server.Implementations/ServerManager/WebSocketConnection.cs
+++ b/MediaBrowser.Server.Implementations/ServerManager/WebSocketConnection.cs
@@ -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>
@@ -36,11 +37,6 @@ namespace MediaBrowser.Server.Implementations.ServerManager
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
/// <summary>
- /// The _send semaphore
- /// </summary>
- private readonly SemaphoreSlim _sendSemaphore = new SemaphoreSlim(1, 1);
-
- /// <summary>
/// The logger
/// </summary>
private readonly ILogger _logger;
@@ -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>
diff --git a/MediaBrowser.Server.Implementations/Session/HttpSessionController.cs b/MediaBrowser.Server.Implementations/Session/HttpSessionController.cs
index b841f0216..f54c452cc 100644
--- a/MediaBrowser.Server.Implementations/Session/HttpSessionController.cs
+++ b/MediaBrowser.Server.Implementations/Session/HttpSessionController.cs
@@ -75,7 +75,8 @@ namespace MediaBrowser.Server.Implementations.Session
await _httpClient.Post(new HttpRequestOptions
{
Url = url,
- CancellationToken = cancellationToken
+ CancellationToken = cancellationToken,
+ BufferContent = false
}).ConfigureAwait(false);
}
diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs
index afcdf9d90..e898a6abd 100644
--- a/MediaBrowser.Server.Implementations/Session/SessionManager.cs
+++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs
@@ -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;
diff --git a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs b/MediaBrowser.Server.Implementations/Sync/SyncManager.cs
index 1278a40a4..c523ec7bd 100644
--- a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs
+++ b/MediaBrowser.Server.Implementations/Sync/SyncManager.cs
@@ -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)
diff --git a/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs b/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs
index a2b5851ac..e0553b1b1 100644
--- a/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs
+++ b/MediaBrowser.Server.Implementations/Sync/SyncedMediaSourceProvider.cs
@@ -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();
}
diff --git a/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs b/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs
index 106dc9115..32a600371 100644
--- a/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs
+++ b/MediaBrowser.Server.Implementations/Sync/TargetDataProvider.cs
@@ -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);
diff --git a/MediaBrowser.Server.Implementations/app.config b/MediaBrowser.Server.Implementations/app.config
index 14f2f055f..77b8b9218 100644
--- a/MediaBrowser.Server.Implementations/app.config
+++ b/MediaBrowser.Server.Implementations/app.config
@@ -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> \ No newline at end of file
+<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.1"/></startup></configuration>
diff --git a/MediaBrowser.Server.Implementations/packages.config b/MediaBrowser.Server.Implementations/packages.config
index 746dc7f62..043257fc8 100644
--- a/MediaBrowser.Server.Implementations/packages.config
+++ b/MediaBrowser.Server.Implementations/packages.config
@@ -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> \ No newline at end of file