diff options
Diffstat (limited to 'MediaBrowser.Server.Implementations')
123 files changed, 9819 insertions, 5113 deletions
diff --git a/MediaBrowser.Server.Implementations/Activity/ActivityRepository.cs b/MediaBrowser.Server.Implementations/Activity/ActivityRepository.cs index 85ab76182..c992def39 100644 --- a/MediaBrowser.Server.Implementations/Activity/ActivityRepository.cs +++ b/MediaBrowser.Server.Implementations/Activity/ActivityRepository.cs @@ -15,54 +15,26 @@ namespace MediaBrowser.Server.Implementations.Activity { public class ActivityRepository : BaseSqliteRepository, IActivityRepository { - private IDbConnection _connection; - private readonly IServerApplicationPaths _appPaths; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private IDbCommand _saveActivityCommand; - - public ActivityRepository(ILogManager logManager, IServerApplicationPaths appPaths) - : base(logManager) + public ActivityRepository(ILogManager logManager, IServerApplicationPaths appPaths, IDbConnector connector) + : base(logManager, connector) { - _appPaths = appPaths; + DbFilePath = Path.Combine(appPaths.DataPath, "activitylog.db"); } public async Task Initialize() { - var dbFile = Path.Combine(_appPaths.DataPath, "activitylog.db"); - - _connection = await SqliteExtensions.ConnectToDb(dbFile, Logger).ConfigureAwait(false); - - string[] queries = { + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + string[] queries = { "create table if not exists ActivityLogEntries (Id GUID PRIMARY KEY, Name TEXT, Overview TEXT, ShortOverview TEXT, Type TEXT, ItemId TEXT, UserId TEXT, DateCreated DATETIME, LogSeverity TEXT)", - "create index if not exists idx_ActivityLogEntries on ActivityLogEntries(Id)", - - //pragmas - "pragma temp_store = memory", - - "pragma shrink_memory" + "create index if not exists idx_ActivityLogEntries on ActivityLogEntries(Id)" }; - _connection.RunQueries(queries, Logger); - - PrepareStatements(); - } - - private void PrepareStatements() - { - _saveActivityCommand = _connection.CreateCommand(); - _saveActivityCommand.CommandText = "replace into ActivityLogEntries (Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Id, @Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)"; - - _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@Id"); - _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@Name"); - _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@Overview"); - _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@ShortOverview"); - _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@Type"); - _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@ItemId"); - _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@UserId"); - _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@DateCreated"); - _saveActivityCommand.Parameters.Add(_saveActivityCommand, "@LogSeverity"); + connection.RunQueries(queries, Logger); + } } private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLogEntries"; @@ -79,128 +51,145 @@ namespace MediaBrowser.Server.Implementations.Activity throw new ArgumentNullException("entry"); } - await WriteLock.WaitAsync().ConfigureAwait(false); + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + using (var saveActivityCommand = connection.CreateCommand()) + { + saveActivityCommand.CommandText = "replace into ActivityLogEntries (Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Id, @Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)"; - IDbTransaction transaction = null; + saveActivityCommand.Parameters.Add(saveActivityCommand, "@Id"); + saveActivityCommand.Parameters.Add(saveActivityCommand, "@Name"); + saveActivityCommand.Parameters.Add(saveActivityCommand, "@Overview"); + saveActivityCommand.Parameters.Add(saveActivityCommand, "@ShortOverview"); + saveActivityCommand.Parameters.Add(saveActivityCommand, "@Type"); + saveActivityCommand.Parameters.Add(saveActivityCommand, "@ItemId"); + saveActivityCommand.Parameters.Add(saveActivityCommand, "@UserId"); + saveActivityCommand.Parameters.Add(saveActivityCommand, "@DateCreated"); + saveActivityCommand.Parameters.Add(saveActivityCommand, "@LogSeverity"); - try - { - transaction = _connection.BeginTransaction(); + IDbTransaction transaction = null; - var index = 0; + try + { + transaction = connection.BeginTransaction(); - _saveActivityCommand.GetParameter(index++).Value = new Guid(entry.Id); - _saveActivityCommand.GetParameter(index++).Value = entry.Name; - _saveActivityCommand.GetParameter(index++).Value = entry.Overview; - _saveActivityCommand.GetParameter(index++).Value = entry.ShortOverview; - _saveActivityCommand.GetParameter(index++).Value = entry.Type; - _saveActivityCommand.GetParameter(index++).Value = entry.ItemId; - _saveActivityCommand.GetParameter(index++).Value = entry.UserId; - _saveActivityCommand.GetParameter(index++).Value = entry.Date; - _saveActivityCommand.GetParameter(index++).Value = entry.Severity.ToString(); + var index = 0; - _saveActivityCommand.Transaction = transaction; + saveActivityCommand.GetParameter(index++).Value = new Guid(entry.Id); + saveActivityCommand.GetParameter(index++).Value = entry.Name; + saveActivityCommand.GetParameter(index++).Value = entry.Overview; + saveActivityCommand.GetParameter(index++).Value = entry.ShortOverview; + saveActivityCommand.GetParameter(index++).Value = entry.Type; + saveActivityCommand.GetParameter(index++).Value = entry.ItemId; + saveActivityCommand.GetParameter(index++).Value = entry.UserId; + saveActivityCommand.GetParameter(index++).Value = entry.Date; + saveActivityCommand.GetParameter(index++).Value = entry.Severity.ToString(); - _saveActivityCommand.ExecuteNonQuery(); + saveActivityCommand.Transaction = transaction; - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } + saveActivityCommand.ExecuteNonQuery(); - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save record:", e); + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } - if (transaction != null) - { - transaction.Rollback(); - } + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save record:", e); - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } + if (transaction != null) + { + transaction.Rollback(); + } - WriteLock.Release(); + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } } } public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit) { - using (var cmd = _connection.CreateCommand()) + using (var connection = CreateConnection(true).Result) { - cmd.CommandText = BaseActivitySelectText; - - var whereClauses = new List<string>(); - - if (minDate.HasValue) + using (var cmd = connection.CreateCommand()) { - whereClauses.Add("DateCreated>=@DateCreated"); - cmd.Parameters.Add(cmd, "@DateCreated", DbType.Date).Value = minDate.Value; - } + cmd.CommandText = BaseActivitySelectText; - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + var whereClauses = new List<string>(); - if (startIndex.HasValue && startIndex.Value > 0) - { - var pagingWhereText = whereClauses.Count == 0 ? + if (minDate.HasValue) + { + whereClauses.Add("DateCreated>=@DateCreated"); + cmd.Parameters.Add(cmd, "@DateCreated", DbType.Date).Value = minDate.Value; + } + + var whereTextWithoutPaging = whereClauses.Count == 0 ? string.Empty : " where " + string.Join(" AND ", whereClauses.ToArray()); - - whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM ActivityLogEntries {0} ORDER BY DateCreated DESC LIMIT {1})", - pagingWhereText, - startIndex.Value.ToString(_usCulture))); - } - var whereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - cmd.CommandText += whereText; + if (startIndex.HasValue && startIndex.Value > 0) + { + var pagingWhereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); - cmd.CommandText += " ORDER BY DateCreated DESC"; + whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM ActivityLogEntries {0} ORDER BY DateCreated DESC LIMIT {1})", + pagingWhereText, + startIndex.Value.ToString(_usCulture))); + } - if (limit.HasValue) - { - cmd.CommandText += " LIMIT " + limit.Value.ToString(_usCulture); - } + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); - cmd.CommandText += "; select count (Id) from ActivityLogEntries" + whereTextWithoutPaging; + cmd.CommandText += whereText; - var list = new List<ActivityLogEntry>(); - var count = 0; + cmd.CommandText += " ORDER BY DateCreated DESC"; - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) - { - while (reader.Read()) + if (limit.HasValue) { - list.Add(GetEntry(reader)); + cmd.CommandText += " LIMIT " + limit.Value.ToString(_usCulture); } - if (reader.NextResult() && reader.Read()) + cmd.CommandText += "; select count (Id) from ActivityLogEntries" + whereTextWithoutPaging; + + var list = new List<ActivityLogEntry>(); + var count = 0; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) { - count = reader.GetInt32(0); + while (reader.Read()) + { + list.Add(GetEntry(reader)); + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } } - } - return new QueryResult<ActivityLogEntry>() - { - Items = list.ToArray(), - TotalRecordCount = count - }; + return new QueryResult<ActivityLogEntry>() + { + Items = list.ToArray(), + TotalRecordCount = count + }; + } } } @@ -260,19 +249,5 @@ namespace MediaBrowser.Server.Implementations.Activity return info; } - - protected override void CloseConnection() - { - if (_connection != null) - { - if (_connection.IsOpen()) - { - _connection.Close(); - } - - _connection.Dispose(); - _connection = null; - } - } } } diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelImageProvider.cs b/MediaBrowser.Server.Implementations/Channels/ChannelImageProvider.cs index f13c71c6d..c98f71ce2 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelImageProvider.cs +++ b/MediaBrowser.Server.Implementations/Channels/ChannelImageProvider.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.Server.Implementations.Channels return ((ChannelManager)_channelManager).GetChannelProvider(channel); } - public bool HasChanged(IHasMetadata item, MetadataStatus status, IDirectoryService directoryService) + public bool HasChanged(IHasMetadata item, IDirectoryService directoryService) { return GetSupportedImages(item).Any(i => !item.HasImage(i)); } diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs index a206c1925..41592865c 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs +++ b/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs @@ -133,7 +133,7 @@ namespace MediaBrowser.Server.Implementations.Channels if (query.IsFavorite.HasValue) { var val = query.IsFavorite.Value; - channels = channels.Where(i => _userDataManager.GetUserData(user.Id, i.GetUserDataKey()).IsFavorite == val) + channels = channels.Where(i => _userDataManager.GetUserData(user, i).IsFavorite == val) .ToList(); } @@ -191,7 +191,7 @@ namespace MediaBrowser.Server.Implementations.Channels var dtoOptions = new DtoOptions(); - var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user) + var returnItems = (await _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user).ConfigureAwait(false)) .ToArray(); var result = new QueryResult<BaseItemDto> @@ -596,7 +596,7 @@ namespace MediaBrowser.Server.Implementations.Channels var dtoOptions = new DtoOptions(); - var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user) + var returnItems = (await _dtoService.GetBaseItemDtos(items, dtoOptions, user).ConfigureAwait(false)) .ToArray(); var result = new QueryResult<BaseItemDto> @@ -863,7 +863,7 @@ namespace MediaBrowser.Server.Implementations.Channels var dtoOptions = new DtoOptions(); - var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user) + var returnItems = (await _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user).ConfigureAwait(false)) .ToArray(); var result = new QueryResult<BaseItemDto> @@ -1012,7 +1012,7 @@ namespace MediaBrowser.Server.Implementations.Channels var dtoOptions = new DtoOptions(); - var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user) + var returnItems = (await _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user).ConfigureAwait(false)) .ToArray(); var result = new QueryResult<BaseItemDto> @@ -1172,8 +1172,7 @@ namespace MediaBrowser.Server.Implementations.Channels { items = ApplyFilters(items, query.Filters, user); - var sortBy = query.SortBy.Length == 0 ? new[] { ItemSortBy.SortName } : query.SortBy; - items = _libraryManager.Sort(items, user, sortBy, query.SortOrder ?? SortOrder.Ascending); + items = _libraryManager.Sort(items, user, query.SortBy, query.SortOrder ?? SortOrder.Ascending); var all = items.ToList(); var totalCount = totalCountFromProvider ?? all.Count; @@ -1250,10 +1249,22 @@ namespace MediaBrowser.Server.Implementations.Channels { item = GetItemById<MusicAlbum>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); } + else if (info.FolderType == ChannelFolderType.MusicArtist) + { + item = GetItemById<MusicArtist>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); + } else if (info.FolderType == ChannelFolderType.PhotoAlbum) { item = GetItemById<PhotoAlbum>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); } + else if (info.FolderType == ChannelFolderType.Series) + { + item = GetItemById<Series>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); + } + else if (info.FolderType == ChannelFolderType.Season) + { + item = GetItemById<Season>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); + } else { item = GetItemById<Folder>(info.Id, channelProvider.Name, channelProvider.DataVersion, out isNew); @@ -1307,6 +1318,28 @@ namespace MediaBrowser.Server.Implementations.Channels item.OfficialRating = info.OfficialRating; item.DateCreated = info.DateCreated ?? DateTime.UtcNow; item.Tags = info.Tags; + item.HomePageUrl = info.HomePageUrl; + } + else if (info.Type == ChannelItemType.Folder && info.FolderType == ChannelFolderType.Container) + { + // At least update names of container folders + if (item.Name != info.Name) + { + item.Name = info.Name; + forceUpdate = true; + } + } + + var hasArtists = item as IHasArtist; + if (hasArtists != null) + { + hasArtists.Artists = info.Artists; + } + + var hasAlbumArtists = item as IHasAlbumArtist; + if (hasAlbumArtists != null) + { + hasAlbumArtists.AlbumArtists = info.AlbumArtists; } var trailer = item as Trailer; @@ -1406,7 +1439,8 @@ namespace MediaBrowser.Server.Implementations.Channels throw new ArgumentNullException("channel"); } - var result = GetAllChannels().FirstOrDefault(i => string.Equals(GetInternalChannelId(i.Name).ToString("N"), channel.ChannelId, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Name, channel.Name, StringComparison.OrdinalIgnoreCase)); + var result = GetAllChannels() + .FirstOrDefault(i => string.Equals(GetInternalChannelId(i.Name).ToString("N"), channel.ChannelId, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Name, channel.Name, StringComparison.OrdinalIgnoreCase)); if (result == null) { @@ -1436,7 +1470,7 @@ namespace MediaBrowser.Server.Implementations.Channels case ItemFilter.IsFavoriteOrLikes: return items.Where(item => { - var userdata = _userDataManager.GetUserData(user.Id, item.GetUserDataKey()); + var userdata = _userDataManager.GetUserData(user, item); if (userdata == null) { @@ -1452,7 +1486,7 @@ namespace MediaBrowser.Server.Implementations.Channels case ItemFilter.Likes: return items.Where(item => { - var userdata = _userDataManager.GetUserData(user.Id, item.GetUserDataKey()); + var userdata = _userDataManager.GetUserData(user, item); return userdata != null && userdata.Likes.HasValue && userdata.Likes.Value; }); @@ -1460,7 +1494,7 @@ namespace MediaBrowser.Server.Implementations.Channels case ItemFilter.Dislikes: return items.Where(item => { - var userdata = _userDataManager.GetUserData(user.Id, item.GetUserDataKey()); + var userdata = _userDataManager.GetUserData(user, item); return userdata != null && userdata.Likes.HasValue && !userdata.Likes.Value; }); @@ -1468,7 +1502,7 @@ namespace MediaBrowser.Server.Implementations.Channels case ItemFilter.IsFavorite: return items.Where(item => { - var userdata = _userDataManager.GetUserData(user.Id, item.GetUserDataKey()); + var userdata = _userDataManager.GetUserData(user, item); return userdata != null && userdata.IsFavorite; }); @@ -1476,7 +1510,7 @@ namespace MediaBrowser.Server.Implementations.Channels case ItemFilter.IsResumable: return items.Where(item => { - var userdata = _userDataManager.GetUserData(user.Id, item.GetUserDataKey()); + var userdata = _userDataManager.GetUserData(user, item); return userdata != null && userdata.PlaybackPositionTicks > 0; }); diff --git a/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs b/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs index 561d46229..3e33066ae 100644 --- a/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs +++ b/MediaBrowser.Server.Implementations/Collections/ManualCollectionsFolder.cs @@ -20,7 +20,7 @@ namespace MediaBrowser.Server.Implementations.Collections public bool IsHiddenFromUser(User user) { - return !user.Configuration.DisplayCollectionsView; + return !ConfigurationManager.Configuration.DisplayCollectionsView; } public override string CollectionType diff --git a/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs b/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs index 7db457c6e..e8669bbc2 100644 --- a/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -95,13 +95,9 @@ namespace MediaBrowser.Server.Implementations.Configuration { metadataPath = GetInternalMetadataPath(); } - else if (Configuration.EnableCustomPathSubFolders) - { - metadataPath = Path.Combine(Configuration.MetadataPath, "metadata"); - } else { - metadataPath = Configuration.MetadataPath; + metadataPath = Path.Combine(Configuration.MetadataPath, "metadata"); } ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = metadataPath; diff --git a/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs b/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs index ea12e332d..28a62c012 100644 --- a/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs +++ b/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs @@ -41,14 +41,15 @@ namespace MediaBrowser.Server.Implementations.Connect public void Run() { - Task.Run(() => LoadCachedAddress()); + LoadCachedAddress(); _timer = new PeriodicTimer(TimerCallback, null, TimeSpan.FromSeconds(5), TimeSpan.FromHours(3)); + ((ConnectManager)_connectManager).Start(); } private readonly string[] _ipLookups = { - "http://bot.whatismyipaddress.com", + "http://bot.whatismyipaddress.com", "https://connect.emby.media/service/ip" }; @@ -78,17 +79,18 @@ namespace MediaBrowser.Server.Implementations.Connect } // If this produced an ipv6 address, try again - if (validIpAddress == null || validIpAddress.AddressFamily == AddressFamily.InterNetworkV6) + if (validIpAddress != null && validIpAddress.AddressFamily == AddressFamily.InterNetworkV6) { foreach (var ipLookupUrl in _ipLookups) { try { - validIpAddress = await GetIpAddress(ipLookupUrl, true).ConfigureAwait(false); + var newAddress = await GetIpAddress(ipLookupUrl, true).ConfigureAwait(false); // Try to find the ipv4 address, if present - if (validIpAddress.AddressFamily == AddressFamily.InterNetwork) + if (newAddress.AddressFamily == AddressFamily.InterNetwork) { + validIpAddress = newAddress; break; } } @@ -162,6 +164,8 @@ namespace MediaBrowser.Server.Implementations.Connect { var path = CacheFilePath; + _logger.Info("Loading data from {0}", path); + try { var endpoint = _fileSystem.ReadAllText(path, Encoding.UTF8); diff --git a/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs b/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs index 9ed67f77e..24750de94 100644 --- a/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs +++ b/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs @@ -24,6 +24,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Common.Extensions; namespace MediaBrowser.Server.Implementations.Connect { @@ -62,6 +63,17 @@ namespace MediaBrowser.Server.Implementations.Connect { var address = _config.Configuration.WanDdns; + if (!string.IsNullOrWhiteSpace(address)) + { + try + { + address = new Uri(address).Host; + } + catch + { + } + } + if (string.IsNullOrWhiteSpace(address) && DiscoveredWanIpAddress != null) { if (DiscoveredWanIpAddress.AddressFamily == AddressFamily.InterNetworkV6) @@ -127,11 +139,14 @@ namespace MediaBrowser.Server.Implementations.Connect _securityManager = securityManager; _fileSystem = fileSystem; - _config.ConfigurationUpdated += _config_ConfigurationUpdated; - LoadCachedData(); } + internal void Start() + { + _config.ConfigurationUpdated += _config_ConfigurationUpdated; + } + internal void OnWanAddressResolved(IPAddress address) { DiscoveredWanIpAddress = address; @@ -165,7 +180,7 @@ namespace MediaBrowser.Server.Implementations.Connect try { - var localAddress = _appHost.LocalApiUrl; + var localAddress = await _appHost.GetLocalApiUrl().ConfigureAwait(false); var hasExistingRecord = !string.IsNullOrWhiteSpace(ConnectServerId) && !string.IsNullOrWhiteSpace(ConnectAccessKey); @@ -205,24 +220,26 @@ namespace MediaBrowser.Server.Implementations.Connect } private string _lastReportedIdentifier; - private string GetConnectReportingIdentifier() + private async Task<string> GetConnectReportingIdentifier() { - return GetConnectReportingIdentifier(_appHost.LocalApiUrl, WanApiAddress); + var url = await _appHost.GetLocalApiUrl().ConfigureAwait(false); + return GetConnectReportingIdentifier(url, WanApiAddress); } private string GetConnectReportingIdentifier(string localAddress, string remoteAddress) { return (remoteAddress ?? string.Empty) + (localAddress ?? string.Empty); } - void _config_ConfigurationUpdated(object sender, EventArgs e) + async void _config_ConfigurationUpdated(object sender, EventArgs e) { // If info hasn't changed, don't report anything - if (string.Equals(_lastReportedIdentifier, GetConnectReportingIdentifier(), StringComparison.OrdinalIgnoreCase)) + var connectIdentifier = await GetConnectReportingIdentifier().ConfigureAwait(false); + if (string.Equals(_lastReportedIdentifier, connectIdentifier, StringComparison.OrdinalIgnoreCase)) { return; } - UpdateConnectInfo(); + await UpdateConnectInfo().ConfigureAwait(false); } private async Task CreateServerRegistration(string wanApiAddress, string localAddress) @@ -237,8 +254,8 @@ namespace MediaBrowser.Server.Implementations.Connect var postData = new Dictionary<string, string> { - {"name", _appHost.FriendlyName}, - {"url", wanApiAddress}, + {"name", _appHost.FriendlyName}, + {"url", wanApiAddress}, {"systemId", _appHost.SystemId} }; @@ -345,6 +362,8 @@ namespace MediaBrowser.Server.Implementations.Connect { var path = CacheFilePath; + _logger.Info("Loading data from {0}", path); + try { lock (_dataFileLock) @@ -544,9 +563,22 @@ namespace MediaBrowser.Server.Implementations.Connect } catch (HttpException ex) { - if (!ex.StatusCode.HasValue || - ex.StatusCode.Value != HttpStatusCode.NotFound || - !Validator.EmailIsValid(connectUsername)) + if (!ex.StatusCode.HasValue) + { + throw; + } + + // If they entered a username, then whatever the error is just throw it, for example, user not found + if (!Validator.EmailIsValid(connectUsername)) + { + if (ex.StatusCode.Value == HttpStatusCode.NotFound) + { + throw new ResourceNotFoundException(); + } + throw; + } + + if (ex.StatusCode.Value != HttpStatusCode.NotFound) { throw; } diff --git a/MediaBrowser.Server.Implementations/Connect/Responses.cs b/MediaBrowser.Server.Implementations/Connect/Responses.cs index e7c3f8154..f86527829 100644 --- a/MediaBrowser.Server.Implementations/Connect/Responses.cs +++ b/MediaBrowser.Server.Implementations/Connect/Responses.cs @@ -60,7 +60,6 @@ namespace MediaBrowser.Server.Implementations.Connect { return new ConnectUserPreferences { - GroupMoviesIntoBoxSets = config.GroupMoviesIntoBoxSets, PlayDefaultAudioTrack = config.PlayDefaultAudioTrack, SubtitleMode = config.SubtitleMode, PreferredAudioLanguages = string.IsNullOrWhiteSpace(config.AudioLanguagePreference) ? new string[] { } : new[] { config.AudioLanguagePreference }, diff --git a/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs b/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs index 947933561..3dfc04c26 100644 --- a/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs +++ b/MediaBrowser.Server.Implementations/Devices/CameraUploadsFolder.cs @@ -28,6 +28,7 @@ namespace MediaBrowser.Server.Implementations.Devices return base.IsVisible(user) && HasChildren(); } + [IgnoreDataMember] public override string CollectionType { get { return Model.Entities.CollectionType.Photos; } diff --git a/MediaBrowser.Server.Implementations/Devices/DeviceManager.cs b/MediaBrowser.Server.Implementations/Devices/DeviceManager.cs index 6b1af8d2d..c3db9140c 100644 --- a/MediaBrowser.Server.Implementations/Devices/DeviceManager.cs +++ b/MediaBrowser.Server.Implementations/Devices/DeviceManager.cs @@ -51,6 +51,11 @@ namespace MediaBrowser.Server.Implementations.Devices public async Task<DeviceInfo> RegisterDevice(string reportedId, string name, string appName, string appVersion, string usedByUserId) { + if (string.IsNullOrWhiteSpace(reportedId)) + { + throw new ArgumentNullException("reportedId"); + } + var device = GetDevice(reportedId) ?? new DeviceInfo { Id = reportedId diff --git a/MediaBrowser.Server.Implementations/Devices/DeviceRepository.cs b/MediaBrowser.Server.Implementations/Devices/DeviceRepository.cs index 368d21322..6e67af82b 100644 --- a/MediaBrowser.Server.Implementations/Devices/DeviceRepository.cs +++ b/MediaBrowser.Server.Implementations/Devices/DeviceRepository.cs @@ -23,7 +23,7 @@ namespace MediaBrowser.Server.Implementations.Devices private readonly ILogger _logger; private readonly IFileSystem _fileSystem; - private List<DeviceInfo> _devices; + private Dictionary<string, DeviceInfo> _devices; public DeviceRepository(IApplicationPaths appPaths, IJsonSerializer json, ILogger logger, IFileSystem fileSystem) { @@ -46,12 +46,12 @@ namespace MediaBrowser.Server.Implementations.Devices public Task SaveDevice(DeviceInfo device) { var path = Path.Combine(GetDevicePath(device.Id), "device.json"); - _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); lock (_syncLock) { _json.SerializeToFile(device, path); - _devices = null; + _devices[device.Id] = device; } return Task.FromResult(true); } @@ -95,9 +95,15 @@ namespace MediaBrowser.Server.Implementations.Devices { if (_devices == null) { - _devices = LoadDevices().ToList(); + _devices = new Dictionary<string, DeviceInfo>(StringComparer.OrdinalIgnoreCase); + + var devices = LoadDevices().ToList(); + foreach (var device in devices) + { + _devices[device.Id] = device; + } } - return _devices.ToList(); + return _devices.Values.ToList(); } } @@ -144,7 +150,7 @@ namespace MediaBrowser.Server.Implementations.Devices catch (DirectoryNotFoundException) { } - + _devices = null; } @@ -174,7 +180,7 @@ namespace MediaBrowser.Server.Implementations.Devices public void AddCameraUpload(string deviceId, LocalFileInfo file) { var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); - _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); lock (_syncLock) { diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/MediaBrowser.Server.Implementations/Dto/DtoService.cs index 234e15a66..616625bc9 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/MediaBrowser.Server.Implementations/Dto/DtoService.cs @@ -86,8 +86,18 @@ namespace MediaBrowser.Server.Implementations.Dto return GetBaseItemDto(item, options, user, owner); } - public IEnumerable<BaseItemDto> GetBaseItemDtos(IEnumerable<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null) + public async Task<List<BaseItemDto>> GetBaseItemDtos(IEnumerable<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null) { + if (items == null) + { + throw new ArgumentNullException("items"); + } + + if (options == null) + { + throw new ArgumentNullException("options"); + } + var syncJobItems = GetSyncedItemProgress(options); var syncDictionary = GetSyncedItemProgressDictionary(syncJobItems); @@ -97,7 +107,7 @@ namespace MediaBrowser.Server.Implementations.Dto foreach (var item in items) { - var dto = GetBaseItemDtoInternal(item, options, syncDictionary, user, owner); + var dto = await GetBaseItemDtoInternal(item, options, syncDictionary, user, owner).ConfigureAwait(false); var tvChannel = item as LiveTvChannel; if (tvChannel != null) @@ -115,11 +125,10 @@ namespace MediaBrowser.Server.Implementations.Dto { if (options.Fields.Contains(ItemFields.ItemCounts)) { - var itemFilter = byName.GetItemFilter(); - - var libraryItems = user != null ? - user.RootFolder.GetRecursiveChildren(user, itemFilter) : - _libraryManager.RootFolder.GetRecursiveChildren(itemFilter); + var libraryItems = byName.GetTaggedItems(new InternalItemsQuery(user) + { + Recursive = true + }); SetItemByNameInfo(item, dto, libraryItems.ToList(), user); } @@ -132,8 +141,7 @@ namespace MediaBrowser.Server.Implementations.Dto if (programTuples.Count > 0) { - var task = _livetvManager().AddInfoToProgramDto(programTuples, options.Fields, user); - Task.WaitAll(task); + await _livetvManager().AddInfoToProgramDto(programTuples, options.Fields, user).ConfigureAwait(false); } if (channelTuples.Count > 0) @@ -160,7 +168,7 @@ namespace MediaBrowser.Server.Implementations.Dto { var syncProgress = GetSyncedItemProgress(options); - var dto = GetBaseItemDtoInternal(item, options, GetSyncedItemProgressDictionary(syncProgress), user, owner); + var dto = GetBaseItemDtoInternal(item, options, GetSyncedItemProgressDictionary(syncProgress), user, owner).Result; var tvChannel = item as LiveTvChannel; if (tvChannel != null) { @@ -194,24 +202,13 @@ namespace MediaBrowser.Server.Implementations.Dto private List<BaseItem> GetTaggedItems(IItemByName byName, User user) { - var person = byName as Person; - - if (person != null) + var items = byName.GetTaggedItems(new InternalItemsQuery(user) { - var items = _libraryManager.GetItemList(new InternalItemsQuery(user) - { - Person = byName.Name - - }, new string[] { }); + Recursive = true - return items.ToList(); - } - - var itemFilter = byName.GetItemFilter(); + }).ToList(); - return user != null ? - user.RootFolder.GetRecursiveChildren(user, itemFilter).ToList() : - _libraryManager.RootFolder.GetRecursiveChildren(itemFilter).ToList(); + return items; } private SyncedItemProgress[] GetSyncedItemProgress(DtoOptions options) @@ -282,7 +279,7 @@ namespace MediaBrowser.Server.Implementations.Dto else if (dto.HasSyncJob.Value) { - dto.SyncStatus = SyncJobItemStatus.Queued; + dto.SyncStatus = syncProgress.Where(i => string.Equals(i.ItemId, dto.Id, StringComparison.OrdinalIgnoreCase)).Select(i => i.Status).Max(); } } } @@ -307,12 +304,12 @@ namespace MediaBrowser.Server.Implementations.Dto else if (dto.HasSyncJob.Value) { - dto.SyncStatus = SyncJobItemStatus.Queued; + dto.SyncStatus = syncProgress.Where(i => string.Equals(i.ItemId, dto.Id, StringComparison.OrdinalIgnoreCase)).Select(i => i.Status).Max(); } } } - private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, Dictionary<string, SyncedItemProgress> syncProgress, User user = null, BaseItem owner = null) + private async Task<BaseItemDto> GetBaseItemDtoInternal(BaseItem item, DtoOptions options, Dictionary<string, SyncedItemProgress> syncProgress, User user = null, BaseItem owner = null) { var fields = options.Fields; @@ -361,7 +358,7 @@ namespace MediaBrowser.Server.Implementations.Dto if (user != null) { - AttachUserSpecificInfo(dto, item, user, fields, syncProgress); + await AttachUserSpecificInfo(dto, item, user, fields, syncProgress).ConfigureAwait(false); } var hasMediaSources = item as IHasMediaSources; @@ -397,12 +394,6 @@ namespace MediaBrowser.Server.Implementations.Dto collectionFolder.GetViewType(user); } - var playlist = item as Playlist; - if (playlist != null) - { - AttachLinkedChildImages(dto, playlist, user, options); - } - if (fields.Contains(ItemFields.CanDelete)) { dto.CanDelete = user == null @@ -434,9 +425,9 @@ namespace MediaBrowser.Server.Implementations.Dto { var syncProgress = GetSyncedItemProgress(options); - var dto = GetBaseItemDtoInternal(item, options, GetSyncedItemProgressDictionary(syncProgress), user); + var dto = GetBaseItemDtoInternal(item, options, GetSyncedItemProgressDictionary(syncProgress), user).Result; - if (options.Fields.Contains(ItemFields.ItemCounts)) + if (taggedItems != null && options.Fields.Contains(ItemFields.ItemCounts)) { SetItemByNameInfo(item, dto, taggedItems, user); } @@ -483,35 +474,47 @@ namespace MediaBrowser.Server.Implementations.Dto /// <param name="user">The user.</param> /// <param name="fields">The fields.</param> /// <param name="syncProgress">The synchronize progress.</param> - private void AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, List<ItemFields> fields, Dictionary<string, SyncedItemProgress> syncProgress) + private async Task AttachUserSpecificInfo(BaseItemDto dto, BaseItem item, User user, List<ItemFields> fields, Dictionary<string, SyncedItemProgress> syncProgress) { if (item.IsFolder) { - var userData = _userDataRepository.GetUserData(user.Id, item.GetUserDataKey()); + var folder = (Folder)item; - // Skip the user data manager because we've already looped through the recursive tree and don't want to do it twice - // TODO: Improve in future - dto.UserData = GetUserItemDataDto(userData); + if (item.SourceType == SourceType.Library && folder.SupportsUserDataFromChildren && fields.Contains(ItemFields.SyncInfo)) + { + // Skip the user data manager because we've already looped through the recursive tree and don't want to do it twice + // TODO: Improve in future + dto.UserData = GetUserItemDataDto(_userDataRepository.GetUserData(user, item)); - var folder = (Folder)item; + await SetSpecialCounts(folder, user, dto, fields, syncProgress).ConfigureAwait(false); + + dto.UserData.Played = dto.UserData.PlayedPercentage.HasValue && + dto.UserData.PlayedPercentage.Value >= 100; + } + else + { + dto.UserData = await _userDataRepository.GetUserDataDto(item, dto, user).ConfigureAwait(false); + } if (item.SourceType == SourceType.Library) { dto.ChildCount = GetChildCount(folder, user); + } - // These are just far too slow. - if (!(folder is UserRootFolder) && !(folder is UserView) && !(folder is ICollectionFolder)) - { - SetSpecialCounts(folder, user, dto, fields, syncProgress); - } + if (fields.Contains(ItemFields.CumulativeRunTimeTicks)) + { + dto.CumulativeRunTimeTicks = item.RunTimeTicks; } - dto.UserData.Played = dto.UserData.PlayedPercentage.HasValue && dto.UserData.PlayedPercentage.Value >= 100; + if (fields.Contains(ItemFields.DateLastMediaAdded)) + { + dto.DateLastMediaAdded = folder.DateLastMediaAdded; + } } else { - dto.UserData = _userDataRepository.GetUserDataDto(item, user); + dto.UserData = _userDataRepository.GetUserDataDto(item, user).Result; } dto.PlayAccess = item.GetPlayAccess(user); @@ -526,7 +529,7 @@ namespace MediaBrowser.Server.Implementations.Dto if (season != null) { - dto.SeasonUserData = _userDataRepository.GetUserDataDto(season, user); + dto.SeasonUserData = await _userDataRepository.GetUserDataDto(season, user).ConfigureAwait(false); } } } @@ -546,8 +549,14 @@ namespace MediaBrowser.Server.Implementations.Dto private int GetChildCount(Folder folder, User user) { - return folder.GetChildren(user, true) - .Count(); + // Right now this is too slow to calculate for top level folders on a per-user basis + // Just return something so that apps that are expecting a value won't think the folders are empty + if (folder is ICollectionFolder || folder is UserView) + { + return new Random().Next(1, 10); + } + + return folder.GetChildCount(user); } /// <summary> @@ -626,9 +635,12 @@ namespace MediaBrowser.Server.Implementations.Dto { if (!string.IsNullOrEmpty(item.Album)) { - var parentAlbum = _libraryManager.RootFolder - .GetRecursiveChildren(i => i is MusicAlbum && string.Equals(i.Name, item.Album, StringComparison.OrdinalIgnoreCase)) - .FirstOrDefault(); + var parentAlbum = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, + Name = item.Album + + }).FirstOrDefault(); if (parentAlbum != null) { @@ -651,29 +663,11 @@ namespace MediaBrowser.Server.Implementations.Dto dto.GameSystem = item.GameSystemName; } - private List<string> GetBackdropImageTags(BaseItem item, int limit) - { - return GetCacheTags(item, ImageType.Backdrop, limit).ToList(); - } - - private List<string> GetScreenshotImageTags(BaseItem item, int limit) + private List<string> GetImageTags(BaseItem item, List<ItemImageInfo> images) { - var hasScreenshots = item as IHasScreenshots; - if (hasScreenshots == null) - { - return new List<string>(); - } - return GetCacheTags(item, ImageType.Screenshot, limit).ToList(); - } - - private IEnumerable<string> GetCacheTags(BaseItem item, ImageType type, int limit) - { - return item.GetImages(type) - // Convert to a list now in case GetImageCacheTag is slow - .ToList() + return images .Select(p => GetImageCacheTag(item, p)) .Where(i => i != null) - .Take(limit) .ToList(); } @@ -839,53 +833,6 @@ namespace MediaBrowser.Server.Implementations.Dto } /// <summary> - /// If an item does not any backdrops, this can be used to find the first parent that does have one - /// </summary> - /// <param name="item">The item.</param> - /// <param name="owner">The owner.</param> - /// <returns>BaseItem.</returns> - private BaseItem GetParentBackdropItem(BaseItem item, BaseItem owner) - { - var parent = item.GetParent() ?? owner; - - while (parent != null) - { - if (parent.GetImages(ImageType.Backdrop).Any()) - { - return parent; - } - - parent = parent.GetParent(); - } - - return null; - } - - /// <summary> - /// If an item does not have a logo, this can be used to find the first parent that does have one - /// </summary> - /// <param name="item">The item.</param> - /// <param name="type">The type.</param> - /// <param name="owner">The owner.</param> - /// <returns>BaseItem.</returns> - private BaseItem GetParentImageItem(BaseItem item, ImageType type, BaseItem owner) - { - var parent = item.GetParent() ?? owner; - - while (parent != null) - { - if (parent.HasImage(type)) - { - return parent; - } - - parent = parent.GetParent(); - } - - return null; - } - - /// <summary> /// Gets the chapter info dto. /// </summary> /// <param name="chapterInfo">The chapter info.</param> @@ -905,7 +852,7 @@ namespace MediaBrowser.Server.Implementations.Dto { Path = chapterInfo.ImagePath, Type = ImageType.Chapter, - DateModified = _fileSystem.GetLastWriteTimeUtc(chapterInfo.ImagePath) + DateModified = chapterInfo.ImageDateModified }); } @@ -946,6 +893,7 @@ namespace MediaBrowser.Server.Implementations.Dto dto.LockData = item.IsLocked; dto.ForcedSortName = item.ForcedSortName; } + dto.Container = item.Container; var hasBudget = item as IHasBudget; if (hasBudget != null) @@ -975,30 +923,12 @@ namespace MediaBrowser.Server.Implementations.Dto if (fields.Contains(ItemFields.Tags)) { - var hasTags = item as IHasTags; - if (hasTags != null) - { - dto.Tags = hasTags.Tags; - } - - if (dto.Tags == null) - { - dto.Tags = new List<string>(); - } + dto.Tags = item.Tags; } if (fields.Contains(ItemFields.Keywords)) { - var hasTags = item as IHasKeywords; - if (hasTags != null) - { - dto.Keywords = hasTags.Keywords; - } - - if (dto.Keywords == null) - { - dto.Keywords = new List<string>(); - } + dto.Keywords = item.Keywords; } if (fields.Contains(ItemFields.ProductionLocations)) @@ -1033,7 +963,7 @@ namespace MediaBrowser.Server.Implementations.Dto var backdropLimit = options.GetImageLimit(ImageType.Backdrop); if (backdropLimit > 0) { - dto.BackdropImageTags = GetBackdropImageTags(item, backdropLimit); + dto.BackdropImageTags = GetImageTags(item, item.GetImages(ImageType.Backdrop).Take(backdropLimit).ToList()); } if (fields.Contains(ItemFields.ScreenshotImageTags)) @@ -1041,7 +971,7 @@ namespace MediaBrowser.Server.Implementations.Dto var screenshotLimit = options.GetImageLimit(ImageType.Screenshot); if (screenshotLimit > 0) { - dto.ScreenshotImageTags = GetScreenshotImageTags(item, screenshotLimit); + dto.ScreenshotImageTags = GetImageTags(item, item.GetImages(ImageType.Screenshot).Take(screenshotLimit).ToList()); } } @@ -1070,6 +1000,7 @@ namespace MediaBrowser.Server.Implementations.Dto dto.Id = GetDtoId(item); dto.IndexNumber = item.IndexNumber; + dto.ParentIndexNumber = item.ParentIndexNumber; dto.IsFolder = item.IsFolder; dto.MediaType = item.MediaType; dto.LocationType = item.LocationType; @@ -1082,15 +1013,11 @@ namespace MediaBrowser.Server.Implementations.Dto dto.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode; dto.PreferredMetadataLanguage = item.PreferredMetadataLanguage; - var hasCriticRating = item as IHasCriticRating; - if (hasCriticRating != null) - { - dto.CriticRating = hasCriticRating.CriticRating; + dto.CriticRating = item.CriticRating; - if (fields.Contains(ItemFields.CriticRatingSummary)) - { - dto.CriticRatingSummary = hasCriticRating.CriticRatingSummary; - } + if (fields.Contains(ItemFields.CriticRatingSummary)) + { + dto.CriticRatingSummary = item.CriticRatingSummary; } var hasTrailers = item as IHasTrailers; @@ -1126,76 +1053,26 @@ namespace MediaBrowser.Server.Implementations.Dto dto.Overview = item.Overview; } - if (fields.Contains(ItemFields.ShortOverview)) + if (fields.Contains(ItemFields.OriginalTitle)) { - var hasShortOverview = item as IHasShortOverview; - if (hasShortOverview != null) - { - dto.ShortOverview = hasShortOverview.ShortOverview; - } + dto.OriginalTitle = item.OriginalTitle; } - // If there are no backdrops, indicate what parent has them in case the Ui wants to allow inheritance - if (backdropLimit > 0 && dto.BackdropImageTags.Count == 0) + if (fields.Contains(ItemFields.ShortOverview)) { - var parentWithBackdrop = GetParentBackdropItem(item, owner); - - if (parentWithBackdrop != null) - { - dto.ParentBackdropItemId = GetDtoId(parentWithBackdrop); - dto.ParentBackdropImageTags = GetBackdropImageTags(parentWithBackdrop, backdropLimit); - } + dto.ShortOverview = item.ShortOverview; } if (fields.Contains(ItemFields.ParentId)) { - var displayParent = item.DisplayParent; - if (displayParent != null) + var displayParentId = item.DisplayParentId; + if (displayParentId.HasValue) { - dto.ParentId = GetDtoId(displayParent); + dto.ParentId = displayParentId.Value.ToString("N"); } } - dto.ParentIndexNumber = item.ParentIndexNumber; - - // If there is no logo, indicate what parent has one in case the Ui wants to allow inheritance - if (!dto.HasLogo && options.GetImageLimit(ImageType.Logo) > 0) - { - var parentWithLogo = GetParentImageItem(item, ImageType.Logo, owner); - - if (parentWithLogo != null) - { - dto.ParentLogoItemId = GetDtoId(parentWithLogo); - - dto.ParentLogoImageTag = GetImageCacheTag(parentWithLogo, ImageType.Logo); - } - } - - // If there is no art, indicate what parent has one in case the Ui wants to allow inheritance - if (!dto.HasArtImage && options.GetImageLimit(ImageType.Art) > 0) - { - var parentWithImage = GetParentImageItem(item, ImageType.Art, owner); - - if (parentWithImage != null) - { - dto.ParentArtItemId = GetDtoId(parentWithImage); - - dto.ParentArtImageTag = GetImageCacheTag(parentWithImage, ImageType.Art); - } - } - - // If there is no thumb, indicate what parent has one in case the Ui wants to allow inheritance - if (!dto.HasThumb && options.GetImageLimit(ImageType.Thumb) > 0) - { - var parentWithImage = GetParentImageItem(item, ImageType.Thumb, owner); - - if (parentWithImage != null) - { - dto.ParentThumbItemId = GetDtoId(parentWithImage); - - dto.ParentThumbImageTag = GetImageCacheTag(parentWithImage, ImageType.Thumb); - } - } + AddInheritedImages(dto, item, options, owner); if (fields.Contains(ItemFields.Path)) { @@ -1287,26 +1164,22 @@ namespace MediaBrowser.Server.Implementations.Dto { dto.Artists = hasArtist.Artists; - dto.ArtistItems = hasArtist - .Artists + var artistItems = _libraryManager.GetArtists(new InternalItemsQuery + { + EnableTotalRecordCount = false, + ItemIds = new[] { item.Id.ToString("N") } + }); + + dto.ArtistItems = artistItems.Items .Select(i => { - try - { - var artist = _libraryManager.GetArtist(i); - return new NameIdPair - { - Name = artist.Name, - Id = artist.Id.ToString("N") - }; - } - catch (Exception ex) + var artist = i.Item1; + return new NameIdPair { - _logger.ErrorException("Error getting artist", ex); - return null; - } + Name = artist.Name, + Id = artist.Id.ToString("N") + }; }) - .Where(i => i != null) .ToList(); } @@ -1315,26 +1188,22 @@ namespace MediaBrowser.Server.Implementations.Dto { dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); - dto.AlbumArtists = hasAlbumArtist - .AlbumArtists + var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery + { + EnableTotalRecordCount = false, + ItemIds = new[] { item.Id.ToString("N") } + }); + + dto.AlbumArtists = artistItems.Items .Select(i => { - try + var artist = i.Item1; + return new NameIdPair { - var artist = _libraryManager.GetArtist(i); - return new NameIdPair - { - Name = artist.Name, - Id = artist.Id.ToString("N") - }; - } - catch (Exception ex) - { - _logger.ErrorException("Error getting album artist", ex); - return null; - } + Name = artist.Name, + Id = artist.Id.ToString("N") + }; }) - .Where(i => i != null) .ToList(); } @@ -1358,9 +1227,10 @@ namespace MediaBrowser.Server.Implementations.Dto if (fields.Contains(ItemFields.MediaSourceCount)) { - if (video.MediaSourceCount != 1) + var mediaSourceCount = video.MediaSourceCount; + if (mediaSourceCount != 1) { - dto.MediaSourceCount = video.MediaSourceCount; + dto.MediaSourceCount = mediaSourceCount; } } @@ -1412,6 +1282,7 @@ namespace MediaBrowser.Server.Implementations.Dto if (episode != null) { dto.IndexNumberEnd = episode.IndexNumberEnd; + dto.SeriesName = episode.SeriesName; if (fields.Contains(ItemFields.AlternateEpisodeNumbers)) { @@ -1427,23 +1298,46 @@ namespace MediaBrowser.Server.Implementations.Dto dto.AirsBeforeSeasonNumber = episode.AirsBeforeSeasonNumber; } - var episodeSeason = episode.Season; - if (episodeSeason != null) + var seasonId = episode.SeasonId; + if (seasonId.HasValue) { - dto.SeasonId = episodeSeason.Id.ToString("N"); + dto.SeasonId = seasonId.Value.ToString("N"); + } + + dto.SeasonName = episode.SeasonName; - if (fields.Contains(ItemFields.SeasonName)) + var seriesId = episode.SeriesId; + if (seriesId.HasValue) + { + dto.SeriesId = seriesId.Value.ToString("N"); + } + + Series episodeSeries = null; + + if (fields.Contains(ItemFields.SeriesGenres)) + { + episodeSeries = episodeSeries ?? episode.Series; + if (episodeSeries != null) { - dto.SeasonName = episodeSeason.Name; + dto.SeriesGenres = episodeSeries.Genres.ToList(); } } - if (fields.Contains(ItemFields.SeriesGenres)) + //if (fields.Contains(ItemFields.SeriesPrimaryImage)) { - var episodeseries = episode.Series; - if (episodeseries != null) + episodeSeries = episodeSeries ?? episode.Series; + if (episodeSeries != null) { - dto.SeriesGenres = episodeseries.Genres.ToList(); + dto.SeriesPrimaryImageTag = GetImageCacheTag(episodeSeries, ImageType.Primary); + } + } + + if (fields.Contains(ItemFields.SeriesStudio)) + { + episodeSeries = episodeSeries ?? episode.Series; + if (episodeSeries != null) + { + dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault(); } } } @@ -1456,59 +1350,36 @@ namespace MediaBrowser.Server.Implementations.Dto dto.AirTime = series.AirTime; dto.SeriesStatus = series.Status; - if (fields.Contains(ItemFields.Settings)) - { - dto.DisplaySpecialsWithSeasons = series.DisplaySpecialsWithSeasons; - } - dto.AnimeSeriesIndex = series.AnimeSeriesIndex; } - if (episode != null) + // Add SeasonInfo + var season = item as Season; + if (season != null) { - series = episode.Series; + dto.SeriesName = season.SeriesName; - if (series != null) + var seriesId = season.SeriesId; + if (seriesId.HasValue) { - dto.SeriesId = GetDtoId(series); - dto.SeriesName = series.Name; - - if (fields.Contains(ItemFields.AirTime)) - { - dto.AirTime = series.AirTime; - } - - if (options.GetImageLimit(ImageType.Thumb) > 0) - { - dto.SeriesThumbImageTag = GetImageCacheTag(series, ImageType.Thumb); - } + dto.SeriesId = seriesId.Value.ToString("N"); + } - if (options.GetImageLimit(ImageType.Primary) > 0) - { - dto.SeriesPrimaryImageTag = GetImageCacheTag(series, ImageType.Primary); - } + series = null; - if (fields.Contains(ItemFields.SeriesStudio)) + if (fields.Contains(ItemFields.SeriesStudio)) + { + series = series ?? season.Series; + if (series != null) { dto.SeriesStudio = series.Studios.FirstOrDefault(); } } - } - // Add SeasonInfo - var season = item as Season; - if (season != null) - { - series = season.Series; - - if (series != null) + if (fields.Contains(ItemFields.SeriesPrimaryImage)) { - dto.SeriesId = GetDtoId(series); - dto.SeriesName = series.Name; - dto.AirTime = series.AirTime; - dto.SeriesStudio = series.Studios.FirstOrDefault(); - - if (options.GetImageLimit(ImageType.Primary) > 0) + series = series ?? season.Series; + if (series != null) { dto.SeriesPrimaryImageTag = GetImageCacheTag(series, ImageType.Primary); } @@ -1559,42 +1430,74 @@ namespace MediaBrowser.Server.Implementations.Dto } } - private void AttachLinkedChildImages(BaseItemDto dto, Folder folder, User user, DtoOptions options) + private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem owner) { - List<BaseItem> linkedChildren = null; - + var logoLimit = options.GetImageLimit(ImageType.Logo); + var artLimit = options.GetImageLimit(ImageType.Art); + var thumbLimit = options.GetImageLimit(ImageType.Thumb); var backdropLimit = options.GetImageLimit(ImageType.Backdrop); - if (backdropLimit > 0 && dto.BackdropImageTags.Count == 0) + if (logoLimit == 0 && artLimit == 0 && thumbLimit == 0 && backdropLimit == 0) { - linkedChildren = user == null - ? folder.GetRecursiveChildren().ToList() - : folder.GetRecursiveChildren(user).ToList(); + return; + } - var parentWithBackdrop = linkedChildren.FirstOrDefault(i => i.GetImages(ImageType.Backdrop).Any()); + BaseItem parent = null; + var isFirst = true; - if (parentWithBackdrop != null) + while (((!dto.HasLogo && logoLimit > 0) || (!dto.HasArtImage && artLimit > 0) || (!dto.HasThumb && thumbLimit > 0) || parent is Series) && + (parent = parent ?? (isFirst ? item.GetParent() ?? owner : parent)) != null) + { + if (parent == null) { - dto.ParentBackdropItemId = GetDtoId(parentWithBackdrop); - dto.ParentBackdropImageTags = GetBackdropImageTags(parentWithBackdrop, backdropLimit); + break; } - } - if (!dto.ImageTags.ContainsKey(ImageType.Primary) && options.GetImageLimit(ImageType.Primary) > 0) - { - if (linkedChildren == null) + var allImages = parent.ImageInfos; + + if (logoLimit > 0 && !dto.HasLogo && dto.ParentLogoItemId == null) { - linkedChildren = user == null - ? folder.GetRecursiveChildren().ToList() - : folder.GetRecursiveChildren(user).ToList(); + var image = allImages.FirstOrDefault(i => i.Type == ImageType.Logo); + + if (image != null) + { + dto.ParentLogoItemId = GetDtoId(parent); + dto.ParentLogoImageTag = GetImageCacheTag(parent, image); + } } - var parentWithImage = linkedChildren.FirstOrDefault(i => i.GetImages(ImageType.Primary).Any()); + if (artLimit > 0 && !dto.HasArtImage && dto.ParentArtItemId == null) + { + var image = allImages.FirstOrDefault(i => i.Type == ImageType.Art); - if (parentWithImage != null) + if (image != null) + { + dto.ParentArtItemId = GetDtoId(parent); + dto.ParentArtImageTag = GetImageCacheTag(parent, image); + } + } + if (thumbLimit > 0 && !dto.HasThumb && (dto.ParentThumbItemId == null || parent is Series)) { - dto.ParentPrimaryImageItemId = GetDtoId(parentWithImage); - dto.ParentPrimaryImageTag = GetImageCacheTag(parentWithImage, ImageType.Primary); + var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb); + + if (image != null) + { + dto.ParentThumbItemId = GetDtoId(parent); + dto.ParentThumbImageTag = GetImageCacheTag(parent, image); + } + } + if (backdropLimit > 0 && !dto.HasBackdrop) + { + var images = allImages.Where(i => i.Type == ImageType.Backdrop).Take(backdropLimit).ToList(); + + if (images.Count > 0) + { + dto.ParentBackdropItemId = GetDtoId(parent); + dto.ParentBackdropImageTags = GetImageTags(parent, images); + } } + + isFirst = false; + parent = parent.GetParent(); } } @@ -1649,39 +1552,27 @@ namespace MediaBrowser.Server.Implementations.Dto /// <param name="fields">The fields.</param> /// <param name="syncProgress">The synchronize progress.</param> /// <returns>Task.</returns> - private void SetSpecialCounts(Folder folder, User user, BaseItemDto dto, List<ItemFields> fields, Dictionary<string, SyncedItemProgress> syncProgress) + private async Task SetSpecialCounts(Folder folder, User user, BaseItemDto dto, List<ItemFields> fields, Dictionary<string, SyncedItemProgress> syncProgress) { var recursiveItemCount = 0; var unplayed = 0; - long runtime = 0; - DateTime? dateLastMediaAdded = null; double totalPercentPlayed = 0; double totalSyncPercent = 0; - var addSyncInfo = fields.Contains(ItemFields.SyncInfo); - var children = folder.GetItems(new InternalItemsQuery + var children = await folder.GetItems(new InternalItemsQuery { IsFolder = false, Recursive = true, ExcludeLocationTypes = new[] { LocationType.Virtual }, User = user - }).Result.Items; + }).ConfigureAwait(false); // Loop through each recursive child - foreach (var child in children) + foreach (var child in children.Items) { - if (!dateLastMediaAdded.HasValue) - { - dateLastMediaAdded = child.DateCreated; - } - else - { - dateLastMediaAdded = new[] { dateLastMediaAdded.Value, child.DateCreated }.Max(); - } - - var userdata = _userDataRepository.GetUserData(user.Id, child.GetUserDataKey()); + var userdata = _userDataRepository.GetUserData(user, child); recursiveItemCount++; @@ -1709,28 +1600,23 @@ namespace MediaBrowser.Server.Implementations.Dto unplayed++; } - runtime += child.RunTimeTicks ?? 0; - - if (addSyncInfo) + double percent = 0; + SyncedItemProgress syncItemProgress; + if (syncProgress.TryGetValue(child.Id.ToString("N"), out syncItemProgress)) { - double percent = 0; - SyncedItemProgress syncItemProgress; - if (syncProgress.TryGetValue(child.Id.ToString("N"), out syncItemProgress)) + switch (syncItemProgress.Status) { - switch (syncItemProgress.Status) - { - case SyncJobItemStatus.Synced: - percent = 100; - break; - case SyncJobItemStatus.Converting: - case SyncJobItemStatus.ReadyToTransfer: - case SyncJobItemStatus.Transferring: - percent = 50; - break; - } + case SyncJobItemStatus.Synced: + percent = 100; + break; + case SyncJobItemStatus.Converting: + case SyncJobItemStatus.ReadyToTransfer: + case SyncJobItemStatus.Transferring: + percent = 50; + break; } - totalSyncPercent += percent; } + totalSyncPercent += percent; } dto.RecursiveItemCount = recursiveItemCount; @@ -1740,25 +1626,12 @@ namespace MediaBrowser.Server.Implementations.Dto { dto.UserData.PlayedPercentage = totalPercentPlayed / recursiveItemCount; - if (addSyncInfo) + var pct = totalSyncPercent / recursiveItemCount; + if (pct > 0) { - var pct = totalSyncPercent / recursiveItemCount; - if (pct > 0) - { - dto.SyncPercent = pct; - } + dto.SyncPercent = pct; } } - - if (runtime > 0 && fields.Contains(ItemFields.CumulativeRunTimeTicks)) - { - dto.CumulativeRunTimeTicks = runtime; - } - - if (fields.Contains(ItemFields.DateLastMediaAdded)) - { - dto.DateLastMediaAdded = dateLastMediaAdded; - } } /// <summary> diff --git a/MediaBrowser.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs b/MediaBrowser.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs index df6a9e654..d5f265dda 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs @@ -8,6 +8,9 @@ using MediaBrowser.Model.Tasks; using System; using System.Linq; using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.LiveTv; namespace MediaBrowser.Server.Implementations.EntryPoints { @@ -18,16 +21,18 @@ namespace MediaBrowser.Server.Implementations.EntryPoints private readonly ITaskManager _iTaskManager; private readonly ISessionManager _sessionManager; private readonly IServerConfigurationManager _config; + private readonly ILiveTvManager _liveTvManager; private Timer _timer; - public AutomaticRestartEntryPoint(IServerApplicationHost appHost, ILogger logger, ITaskManager iTaskManager, ISessionManager sessionManager, IServerConfigurationManager config) + public AutomaticRestartEntryPoint(IServerApplicationHost appHost, ILogger logger, ITaskManager iTaskManager, ISessionManager sessionManager, IServerConfigurationManager config, ILiveTvManager liveTvManager) { _appHost = appHost; _logger = logger; _iTaskManager = iTaskManager; _sessionManager = sessionManager; _config = config; + _liveTvManager = liveTvManager; } public void Run() @@ -44,34 +49,55 @@ namespace MediaBrowser.Server.Implementations.EntryPoints if (_appHost.HasPendingRestart) { - _timer = new Timer(TimerCallback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); + _timer = new Timer(TimerCallback, null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); } } - private void TimerCallback(object state) + private async void TimerCallback(object state) { - if (_config.Configuration.EnableAutomaticRestart && IsIdle()) + if (_config.Configuration.EnableAutomaticRestart) { - DisposeTimer(); + var isIdle = await IsIdle().ConfigureAwait(false); - try - { - _appHost.Restart(); - } - catch (Exception ex) + if (isIdle) { - _logger.ErrorException("Error restarting server", ex); + DisposeTimer(); + + try + { + _appHost.Restart(); + } + catch (Exception ex) + { + _logger.ErrorException("Error restarting server", ex); + } } } } - private bool IsIdle() + private async Task<bool> IsIdle() { if (_iTaskManager.ScheduledTasks.Any(i => i.State != TaskState.Idle)) { return false; } + if (_liveTvManager.Services.Count == 1) + { + try + { + var timers = await _liveTvManager.GetTimers(new TimerQuery(), CancellationToken.None).ConfigureAwait(false); + if (timers.Items.Any(i => i.Status == RecordingStatus.InProgress)) + { + return false; + } + } + catch (Exception ex) + { + _logger.ErrorException("Error getting timers", ex); + } + } + var now = DateTime.UtcNow; return !_sessionManager.Sessions.Any(i => (now - i.LastActivityDate).TotalMinutes < 30); diff --git a/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index 95763c43f..50ad3cfbc 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -50,8 +50,6 @@ namespace MediaBrowser.Server.Implementations.EntryPoints void _config_ConfigurationUpdated(object sender, EventArgs e) { - _config.ConfigurationUpdated -= _config_ConfigurationUpdated; - if (!string.Equals(_lastConfigIdentifier, GetConfigIdentifier(), StringComparison.OrdinalIgnoreCase)) { if (_isStarted) @@ -95,7 +93,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints NatUtility.UnhandledException += NatUtility_UnhandledException; NatUtility.StartDiscovery(); - _timer = new PeriodicTimer(s => _createdRules = new List<string>(), null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); + _timer = new PeriodicTimer(ClearCreatedRules, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); _ssdp.MessageReceived += _ssdp_MessageReceived; @@ -104,12 +102,43 @@ namespace MediaBrowser.Server.Implementations.EntryPoints _isStarted = true; } + private void ClearCreatedRules(object state) + { + _createdRules = new List<string>(); + _usnsHandled = new List<string>(); + } + void _ssdp_MessageReceived(object sender, SsdpMessageEventArgs e) { var endpoint = e.EndPoint as IPEndPoint; - if (endpoint != null && e.LocalEndPoint != null) + if (endpoint == null || e.LocalEndPoint == null) { + return; + } + + string usn; + if (!e.Headers.TryGetValue("USN", out usn)) usn = string.Empty; + + string nt; + if (!e.Headers.TryGetValue("NT", out nt)) nt = string.Empty; + + // Filter device type + if (usn.IndexOf("WANIPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && + nt.IndexOf("WANIPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && + usn.IndexOf("WANPPPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && + nt.IndexOf("WANPPPConnection:", StringComparison.OrdinalIgnoreCase) == -1) + { + return; + } + + var identifier = string.IsNullOrWhiteSpace(usn) ? nt : usn; + + if (!_usnsHandled.Contains(identifier)) + { + _usnsHandled.Add(identifier); + + _logger.Debug("Calling Nat.Handle on " + identifier); NatUtility.Handle(e.LocalEndPoint.Address, e.Message, endpoint, NatProtocol.Upnp); } } @@ -153,6 +182,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints } private List<string> _createdRules = new List<string>(); + private List<string> _usnsHandled = new List<string>(); private void CreateRules(INatDevice device) { // On some systems the device discovered event seems to fire repeatedly @@ -226,30 +256,5 @@ namespace MediaBrowser.Server.Implementations.EntryPoints _isStarted = false; } } - - private class LogWriter : TextWriter - { - private readonly ILogger _logger; - - public LogWriter(ILogger logger) - { - _logger = logger; - } - - public override Encoding Encoding - { - get { return Encoding.UTF8; } - } - - public override void WriteLine(string format, params object[] arg) - { - _logger.Debug(format, arg); - } - - public override void WriteLine(string value) - { - _logger.Debug(value); - } - } } -} +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs b/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs index 918110226..e84b66c5a 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/Notifications/Notifications.cs @@ -22,6 +22,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.Entities.TV; namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications { @@ -116,7 +117,8 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications var notification = new NotificationRequest { - NotificationType = type + NotificationType = type, + Url = e.Argument.infoUrl }; notification.Variables["Version"] = e.Argument.versionStr; @@ -213,6 +215,12 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications return; } + var video = e.Item as Video; + if (video != null && video.IsThemeMedia) + { + return; + } + var type = GetPlaybackNotificationType(item.MediaType); SendPlaybackNotification(type, e); @@ -228,6 +236,12 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications return; } + var video = e.Item as Video; + if (video != null && video.IsThemeMedia) + { + return; + } + var type = GetPlaybackStoppedNotificationType(item.MediaType); SendPlaybackNotification(type, e); @@ -334,12 +348,17 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications private bool FilterItem(BaseItem item) { - if (!item.IsFolder && item.LocationType == LocationType.Virtual) + if (item.IsFolder) { return false; } - if (item is IItemByName && !(item is MusicArtist)) + if (item.LocationType == LocationType.Virtual) + { + return false; + } + + if (item is IItemByName) { return false; } @@ -387,6 +406,18 @@ namespace MediaBrowser.Server.Implementations.EntryPoints.Notifications public static string GetItemName(BaseItem item) { var name = item.Name; + var episode = item as Episode; + if (episode != null) + { + if (episode.IndexNumber.HasValue) + { + name = string.Format("Ep{0} - {1}", episode.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), name); + } + if (episode.ParentIndexNumber.HasValue) + { + name = string.Format("S{0}, {1}", episode.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture), name); + } + } var hasSeries = item as IHasSeries; diff --git a/MediaBrowser.Server.Implementations/EntryPoints/RecordingNotifier.cs b/MediaBrowser.Server.Implementations/EntryPoints/RecordingNotifier.cs new file mode 100644 index 000000000..cc4ef1972 --- /dev/null +++ b/MediaBrowser.Server.Implementations/EntryPoints/RecordingNotifier.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.Server.Implementations.EntryPoints +{ + public class RecordingNotifier : IServerEntryPoint + { + private readonly ILiveTvManager _liveTvManager; + private readonly ISessionManager _sessionManager; + private readonly IUserManager _userManager; + private readonly ILogger _logger; + + public RecordingNotifier(ISessionManager sessionManager, IUserManager userManager, ILogger logger, ILiveTvManager liveTvManager) + { + _sessionManager = sessionManager; + _userManager = userManager; + _logger = logger; + _liveTvManager = liveTvManager; + } + + public void Run() + { + _liveTvManager.TimerCancelled += _liveTvManager_TimerCancelled; + _liveTvManager.SeriesTimerCancelled += _liveTvManager_SeriesTimerCancelled; + _liveTvManager.TimerCreated += _liveTvManager_TimerCreated; + _liveTvManager.SeriesTimerCreated += _liveTvManager_SeriesTimerCreated; + } + + private void _liveTvManager_SeriesTimerCreated(object sender, Model.Events.GenericEventArgs<TimerEventInfo> e) + { + SendMessage("SeriesTimerCreated", e.Argument); + } + + private void _liveTvManager_TimerCreated(object sender, Model.Events.GenericEventArgs<TimerEventInfo> e) + { + SendMessage("TimerCreated", e.Argument); + } + + private void _liveTvManager_SeriesTimerCancelled(object sender, Model.Events.GenericEventArgs<TimerEventInfo> e) + { + SendMessage("SeriesTimerCancelled", e.Argument); + } + + private void _liveTvManager_TimerCancelled(object sender, Model.Events.GenericEventArgs<TimerEventInfo> e) + { + SendMessage("TimerCancelled", e.Argument); + } + + private async void SendMessage(string name, TimerEventInfo info) + { + var users = _userManager.Users.Where(i => i.Policy.EnableLiveTvAccess).ToList(); + + foreach (var user in users) + { + try + { + await _sessionManager.SendMessageToUserSessions<TimerEventInfo>(user.Id.ToString("N"), name, info, CancellationToken.None); + } + catch (Exception ex) + { + _logger.ErrorException("Error sending message", ex); + } + } + } + + public void Dispose() + { + _liveTvManager.TimerCancelled -= _liveTvManager_TimerCancelled; + _liveTvManager.SeriesTimerCancelled -= _liveTvManager_SeriesTimerCancelled; + _liveTvManager.TimerCreated -= _liveTvManager_TimerCreated; + _liveTvManager.SeriesTimerCreated -= _liveTvManager_SeriesTimerCreated; + } + } +} diff --git a/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs b/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs index 7e22efb23..7b3a7a30d 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs @@ -18,7 +18,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints private readonly IHttpClient _httpClient; private readonly IUserManager _userManager; private readonly ILogger _logger; - private const string MbAdminUrl = "http://www.mb3admin.com/admin/"; + private const string MbAdminUrl = "https://www.mb3admin.com/admin/"; public UsageReporter(IApplicationHost applicationHost, IHttpClient httpClient, IUserManager userManager, ILogger logger) { diff --git a/MediaBrowser.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/MediaBrowser.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs index b059e4144..b616b7761 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs +++ b/MediaBrowser.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -119,7 +119,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints .DistinctBy(i => i.Id) .Select(i => { - var dto = _userDataManager.GetUserDataDto(i, user); + var dto = _userDataManager.GetUserDataDto(i, user).Result; dto.ItemId = i.Id.ToString("N"); return dto; }) diff --git a/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs b/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs index e45df3f4a..2109f8d59 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs +++ b/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs @@ -116,7 +116,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization premiereDate, options, overwriteExisting, - false, + false, result, cancellationToken).ConfigureAwait(false); } @@ -202,7 +202,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization null, options, true, - request.RememberCorrection, + request.RememberCorrection, result, cancellationToken).ConfigureAwait(false); @@ -219,7 +219,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization DateTime? premiereDate, AutoOrganizeOptions options, bool overwriteExisting, - bool rememberCorrection, + bool rememberCorrection, FileOrganizationResult result, CancellationToken cancellationToken) { @@ -242,7 +242,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization premiereDate, options, overwriteExisting, - rememberCorrection, + rememberCorrection, result, cancellationToken); } @@ -255,7 +255,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization DateTime? premiereDate, AutoOrganizeOptions options, bool overwriteExisting, - bool rememberCorrection, + bool rememberCorrection, FileOrganizationResult result, CancellationToken cancellationToken) { @@ -304,7 +304,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization if (otherDuplicatePaths.Count > 0) { - var msg = string.Format("File '{0}' already exists as '{1}', stopping organization", sourcePath, otherDuplicatePaths); + var msg = string.Format("File '{0}' already exists as these:'{1}'. Stopping organization", sourcePath, string.Join("', '", otherDuplicatePaths)); _logger.Info(msg); result.Status = FileSortingStatus.SkippedExisting; result.StatusMessage = msg; @@ -356,6 +356,11 @@ namespace MediaBrowser.Server.Implementations.FileOrganization private void SaveSmartMatchString(string matchString, Series series, AutoOrganizeOptions options) { + if (string.IsNullOrEmpty(matchString) || matchString.Length < 3) + { + return; + } + SmartMatchInfo info = options.SmartMatchInfos.FirstOrDefault(i => string.Equals(i.ItemName, series.Name, StringComparison.OrdinalIgnoreCase)); if (info == null) @@ -536,7 +541,11 @@ namespace MediaBrowser.Server.Implementations.FileOrganization result.ExtractedName = nameWithoutYear; result.ExtractedYear = yearInName; - var series = _libraryManager.RootFolder.GetRecursiveChildren(i => i is Series) + var series = _libraryManager.GetItemList(new Controller.Entities.InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Series).Name }, + Recursive = true + }) .Cast<Series>() .Select(i => NameUtils.GetMatchScore(nameWithoutYear, yearInName, i)) .Where(i => i.Item2 > 0) @@ -550,10 +559,13 @@ namespace MediaBrowser.Server.Implementations.FileOrganization if (info != null) { - series = _libraryManager.RootFolder - .GetRecursiveChildren(i => i is Series) - .Cast<Series>() - .FirstOrDefault(i => string.Equals(i.Name, info.ItemName, StringComparison.OrdinalIgnoreCase)); + series = _libraryManager.GetItemList(new Controller.Entities.InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Series).Name }, + Recursive = true, + Name = info.ItemName + + }).Cast<Series>().FirstOrDefault(); } } diff --git a/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs b/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs index 0e8a60612..60d515e12 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs +++ b/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs @@ -95,7 +95,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization return _repo.Delete(resultId); } - private AutoOrganizeOptions GetAutoOrganizeptions() + private AutoOrganizeOptions GetAutoOrganizeOptions() { return _config.GetAutoOrganizeOptions(); } @@ -112,7 +112,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager); - await organizer.OrganizeEpisodeFile(result.OriginalPath, GetAutoOrganizeptions(), true, CancellationToken.None) + await organizer.OrganizeEpisodeFile(result.OriginalPath, GetAutoOrganizeOptions(), true, CancellationToken.None) .ConfigureAwait(false); } @@ -126,7 +126,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization var organizer = new EpisodeFileOrganizer(this, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager); - await organizer.OrganizeWithCorrection(request, GetAutoOrganizeptions(), CancellationToken.None).ConfigureAwait(false); + await organizer.OrganizeWithCorrection(request, GetAutoOrganizeOptions(), CancellationToken.None).ConfigureAwait(false); } public QueryResult<SmartMatchInfo> GetSmartMatchInfos(FileOrganizationResultQuery query) @@ -136,7 +136,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization throw new ArgumentNullException("query"); } - var options = GetAutoOrganizeptions(); + var options = GetAutoOrganizeOptions(); var items = options.SmartMatchInfos.Skip(query.StartIndex ?? 0).Take(query.Limit ?? Int32.MaxValue).ToArray(); @@ -159,7 +159,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization throw new ArgumentNullException("matchString"); } - var options = GetAutoOrganizeptions(); + var options = GetAutoOrganizeOptions(); SmartMatchInfo info = options.SmartMatchInfos.FirstOrDefault(i => string.Equals(i.ItemName, itemName)); diff --git a/MediaBrowser.Server.Implementations/HttpServer/AsyncStreamWriterFunc.cs b/MediaBrowser.Server.Implementations/HttpServer/AsyncStreamWriterFunc.cs new file mode 100644 index 000000000..5aa01c706 --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/AsyncStreamWriterFunc.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using ServiceStack; +using ServiceStack.Web; + +namespace MediaBrowser.Server.Implementations.HttpServer +{ + public class AsyncStreamWriterFunc : IStreamWriter, IAsyncStreamWriter, IHasOptions + { + /// <summary> + /// Gets or sets the source stream. + /// </summary> + /// <value>The source stream.</value> + private Func<Stream, Task> Writer { get; set; } + + /// <summary> + /// Gets the options. + /// </summary> + /// <value>The options.</value> + public IDictionary<string, string> Options { get; private set; } + + public Action OnComplete { get; set; } + public Action OnError { get; set; } + + /// <summary> + /// Initializes a new instance of the <see cref="StreamWriter" /> class. + /// </summary> + public AsyncStreamWriterFunc(Func<Stream, Task> writer, IDictionary<string, string> headers) + { + Writer = writer; + + if (headers == null) + { + headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + } + Options = headers; + } + + /// <summary> + /// Writes to. + /// </summary> + /// <param name="responseStream">The response stream.</param> + public void WriteTo(Stream responseStream) + { + var task = Writer(responseStream); + Task.WaitAll(task); + } + + public async Task WriteToAsync(Stream responseStream) + { + await Writer(responseStream).ConfigureAwait(false); + } + } +} diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs index 3e4f4a70c..17e4793cb 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -106,7 +106,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer } }); - HostContext.GlobalResponseFilters.Add(new ResponseFilter(_logger, () => _config.Configuration.DenyIFrameEmbedding).FilterResponse); + HostContext.GlobalResponseFilters.Add(new ResponseFilter(_logger).FilterResponse); } public override void OnAfterInit() @@ -179,6 +179,11 @@ namespace MediaBrowser.Server.Implementations.HttpServer private void OnWebSocketConnecting(WebSocketConnectingEventArgs args) { + if (_disposed) + { + return; + } + if (WebSocketConnecting != null) { WebSocketConnecting(this, args); @@ -187,6 +192,11 @@ namespace MediaBrowser.Server.Implementations.HttpServer private void OnWebSocketConnected(WebSocketConnectEventArgs args) { + if (_disposed) + { + return; + } + if (WebSocketConnected != null) { WebSocketConnected(this, args); @@ -325,12 +335,19 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// <param name="httpReq">The HTTP req.</param> /// <param name="url">The URL.</param> /// <returns>Task.</returns> - protected Task RequestHandler(IHttpRequest httpReq, Uri url) + protected async Task RequestHandler(IHttpRequest httpReq, Uri url) { var date = DateTime.Now; var httpRes = httpReq.Response; + if (_disposed) + { + httpRes.StatusCode = 503; + httpRes.Close(); + return ; + } + var operationName = httpReq.OperationName; var localPath = url.LocalPath; @@ -344,6 +361,19 @@ namespace MediaBrowser.Server.Implementations.HttpServer LoggerUtils.LogRequest(_logger, urlToLog, httpReq.HttpMethod, httpReq.UserAgent); } + if (string.Equals(localPath, "/emby/", StringComparison.OrdinalIgnoreCase) || + string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase)) + { + httpRes.RedirectToUrl(DefaultRedirectPath); + return; + } + if (string.Equals(localPath, "/emby", StringComparison.OrdinalIgnoreCase) || + string.Equals(localPath, "/mediabrowser", StringComparison.OrdinalIgnoreCase)) + { + httpRes.RedirectToUrl("emby/" + DefaultRedirectPath); + return; + } + if (string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase) || string.Equals(localPath, "/mediabrowser", StringComparison.OrdinalIgnoreCase) || localPath.IndexOf("mediabrowser/web", StringComparison.OrdinalIgnoreCase) != -1 || @@ -359,45 +389,35 @@ namespace MediaBrowser.Server.Implementations.HttpServer httpRes.Write("<!doctype html><html><head><title>Emby</title></head><body>Please update your Emby bookmark to <a href=\"" + newUrl + "\">" + newUrl + "</a></body></html>"); httpRes.Close(); - return Task.FromResult(true); + return; } } - if (string.Equals(localPath, "/emby/", StringComparison.OrdinalIgnoreCase)) - { - httpRes.RedirectToUrl(DefaultRedirectPath); - return Task.FromResult(true); - } - if (string.Equals(localPath, "/emby", StringComparison.OrdinalIgnoreCase)) - { - httpRes.RedirectToUrl("emby/" + DefaultRedirectPath); - return Task.FromResult(true); - } if (string.Equals(localPath, "/web", StringComparison.OrdinalIgnoreCase)) { httpRes.RedirectToUrl(DefaultRedirectPath); - return Task.FromResult(true); + return; } if (string.Equals(localPath, "/web/", StringComparison.OrdinalIgnoreCase)) { httpRes.RedirectToUrl("../" + DefaultRedirectPath); - return Task.FromResult(true); + return; } if (string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)) { httpRes.RedirectToUrl(DefaultRedirectPath); - return Task.FromResult(true); + return; } if (string.IsNullOrEmpty(localPath)) { httpRes.RedirectToUrl("/" + DefaultRedirectPath); - return Task.FromResult(true); + return; } if (string.Equals(localPath, "/emby/pin", StringComparison.OrdinalIgnoreCase)) { httpRes.RedirectToUrl("web/pin.html"); - return Task.FromResult(true); + return; } if (!string.IsNullOrWhiteSpace(GlobalResponse)) @@ -407,7 +427,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer httpRes.Write(GlobalResponse); httpRes.Close(); - return Task.FromResult(true); + return; } var handler = HttpHandlerFactory.GetHandler(httpReq); @@ -423,13 +443,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer httpReq.OperationName = operationName = restHandler.RestPath.RequestType.GetOperationName(); } - var task = serviceStackHandler.ProcessRequestAsync(httpReq, httpRes, operationName); - - task.ContinueWith(x => httpRes.Close(), TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.AttachedToParent); - //Matches Exceptions handled in HttpListenerBase.InitTask() - - task.ContinueWith(x => + try + { + await serviceStackHandler.ProcessRequestAsync(httpReq, httpRes, operationName).ConfigureAwait(false); + } + finally { + httpRes.Close(); var statusCode = httpRes.StatusCode; var duration = DateTime.Now - date; @@ -438,13 +458,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer { LoggerUtils.LogResponse(_logger, statusCode, urlToLog, remoteIp, duration); } - - }, TaskContinuationOptions.None); - return task; + } } - return new NotImplementedException("Cannot execute handler: " + handler + " at PathInfo: " + httpReq.PathInfo) - .AsTaskException(); + throw new NotImplementedException("Cannot execute handler: " + handler + " at PathInfo: " + httpReq.PathInfo); } /// <summary> diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs index 6cedaa6a9..c0a2a5eb3 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -294,7 +294,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer return null; } - public object GetStaticFileResult(IRequest requestContext, + public Task<object> GetStaticFileResult(IRequest requestContext, string path, FileShare fileShare = FileShare.Read) { @@ -310,7 +310,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer }); } - public object GetStaticFileResult(IRequest requestContext, + public Task<object> GetStaticFileResult(IRequest requestContext, StaticFileResultOptions options) { var path = options.Path; @@ -331,7 +331,11 @@ namespace MediaBrowser.Server.Implementations.HttpServer options.ContentType = MimeTypes.GetMimeType(path); } - options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path); + if (!options.DateLastModified.HasValue) + { + options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path); + } + var cacheKey = path + options.DateLastModified.Value.Ticks; options.CacheKey = cacheKey.GetMD5(); @@ -351,7 +355,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer return _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, fileShare); } - public object GetStaticResult(IRequest requestContext, + public Task<object> GetStaticResult(IRequest requestContext, Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, @@ -372,7 +376,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer }); } - public object GetStaticResult(IRequest requestContext, StaticResultOptions options) + public async Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options) { var cacheKey = options.CacheKey; options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); @@ -398,7 +402,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer } var compress = ShouldCompressResponse(requestContext, contentType); - var hasOptions = GetStaticResult(requestContext, options, compress).Result; + var hasOptions = await GetStaticResult(requestContext, options, compress).ConfigureAwait(false); AddResponseHeaders(hasOptions, options.ResponseHeaders); return hasOptions; @@ -699,5 +703,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer throw error; } + + public object GetAsyncStreamWriter(Func<Stream, Task> streamWriter, IDictionary<string, string> responseHeaders = null) + { + return new AsyncStreamWriterFunc(streamWriter, responseHeaders); + } } }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs b/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs index ce8100025..bfbb228ed 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs @@ -35,7 +35,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer public static void LogResponse(ILogger logger, int statusCode, string url, string endPoint, TimeSpan duration) { var durationMs = duration.TotalMilliseconds; - var logSuffix = durationMs >= 1000 ? "ms (slow)" : "ms"; + var logSuffix = durationMs >= 1000 && durationMs < 60000 ? "ms (slow)" : "ms"; logger.Info("HTTP Response {0} to {1}. Time: {2}{3}. {4}", statusCode, endPoint, Convert.ToInt32(durationMs).ToString(CultureInfo.InvariantCulture), logSuffix, url); } diff --git a/MediaBrowser.Server.Implementations/HttpServer/NetListener/HttpListenerServer.cs b/MediaBrowser.Server.Implementations/HttpServer/NetListener/HttpListenerServer.cs deleted file mode 100644 index 31c0e87b3..000000000 --- a/MediaBrowser.Server.Implementations/HttpServer/NetListener/HttpListenerServer.cs +++ /dev/null @@ -1,285 +0,0 @@ -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Logging; -using ServiceStack; -using ServiceStack.Host.HttpListener; -using ServiceStack.Web; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.HttpServer.NetListener -{ - public class HttpListenerServer : IHttpListener - { - private readonly ILogger _logger; - private HttpListener _listener; - private readonly ManualResetEventSlim _listenForNextRequest = new ManualResetEventSlim(false); - - public Action<Exception, IRequest> ErrorHandler { get; set; } - public Action<WebSocketConnectEventArgs> WebSocketHandler { get; set; } - public Func<IHttpRequest, Uri, Task> RequestHandler { get; set; } - - private readonly Action<string> _endpointListener; - - public HttpListenerServer(ILogger logger, Action<string> endpointListener) - { - _logger = logger; - _endpointListener = endpointListener; - } - - private List<string> UrlPrefixes { get; set; } - - public void Start(IEnumerable<string> urlPrefixes) - { - UrlPrefixes = urlPrefixes.ToList(); - - if (_listener == null) - _listener = new HttpListener(); - - //HostContext.Config.HandlerFactoryPath = ListenerRequest.GetHandlerPathIfAny(UrlPrefixes.First()); - - foreach (var prefix in UrlPrefixes) - { - _logger.Info("Adding HttpListener prefix " + prefix); - _listener.Prefixes.Add(prefix); - } - - _listener.Start(); - - Task.Factory.StartNew(Listen, TaskCreationOptions.LongRunning); - } - - private bool IsListening - { - get { return _listener != null && _listener.IsListening; } - } - - // Loop here to begin processing of new requests. - private void Listen() - { - while (IsListening) - { - if (_listener == null) return; - _listenForNextRequest.Reset(); - - try - { - _listener.BeginGetContext(ListenerCallback, _listener); - _listenForNextRequest.Wait(); - } - catch (Exception ex) - { - _logger.Error("Listen()", ex); - return; - } - if (_listener == null) return; - } - } - - // Handle the processing of a request in here. - private void ListenerCallback(IAsyncResult asyncResult) - { - _listenForNextRequest.Set(); - - var listener = asyncResult.AsyncState as HttpListener; - HttpListenerContext context; - - if (listener == null) return; - var isListening = listener.IsListening; - - try - { - if (!isListening) - { - _logger.Debug("Ignoring ListenerCallback() as HttpListener is no longer listening"); return; - } - // The EndGetContext() method, as with all Begin/End asynchronous methods in the .NET Framework, - // blocks until there is a request to be processed or some type of data is available. - context = listener.EndGetContext(asyncResult); - } - catch (Exception ex) - { - // You will get an exception when httpListener.Stop() is called - // because there will be a thread stopped waiting on the .EndGetContext() - // method, and again, that is just the way most Begin/End asynchronous - // methods of the .NET Framework work. - var errMsg = ex + ": " + IsListening; - _logger.Warn(errMsg); - return; - } - - Task.Factory.StartNew(() => InitTask(context)); - } - - private void InitTask(HttpListenerContext context) - { - try - { - var task = this.ProcessRequestAsync(context); - task.ContinueWith(x => HandleError(x.Exception, context), TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.AttachedToParent); - - if (task.Status == TaskStatus.Created) - { - task.RunSynchronously(); - } - } - catch (Exception ex) - { - HandleError(ex, context); - } - } - - private Task ProcessRequestAsync(HttpListenerContext context) - { - var request = context.Request; - - LogHttpRequest(request); - - if (request.IsWebSocketRequest) - { - return ProcessWebSocketRequest(context); - } - - if (string.IsNullOrEmpty(context.Request.RawUrl)) - return ((object)null).AsTaskResult(); - - var operationName = context.Request.GetOperationName(); - - var httpReq = GetRequest(context, operationName); - - return RequestHandler(httpReq, request.Url); - } - - /// <summary> - /// Processes the web socket request. - /// </summary> - /// <param name="ctx">The CTX.</param> - /// <returns>Task.</returns> - private async Task ProcessWebSocketRequest(HttpListenerContext ctx) - { -#if !__MonoCS__ - try - { - var webSocketContext = await ctx.AcceptWebSocketAsync(null).ConfigureAwait(false); - - if (WebSocketHandler != null) - { - WebSocketHandler(new WebSocketConnectEventArgs - { - WebSocket = new NativeWebSocket(webSocketContext.WebSocket, _logger), - Endpoint = ctx.Request.RemoteEndPoint.ToString() - }); - } - } - catch (Exception ex) - { - _logger.ErrorException("AcceptWebSocketAsync error", ex); - ctx.Response.StatusCode = 500; - ctx.Response.Close(); - } -#endif - } - - private void HandleError(Exception ex, HttpListenerContext context) - { - var operationName = context.Request.GetOperationName(); - var httpReq = GetRequest(context, operationName); - - if (ErrorHandler != null) - { - ErrorHandler(ex, httpReq); - } - } - - private static ListenerRequest GetRequest(HttpListenerContext httpContext, string operationName) - { - var req = new ListenerRequest(httpContext, operationName, RequestAttributes.None); - req.RequestAttributes = req.GetAttributes(); - - return req; - } - - /// <summary> - /// Logs the HTTP request. - /// </summary> - /// <param name="request">The request.</param> - private void LogHttpRequest(HttpListenerRequest request) - { - var endpoint = request.LocalEndPoint; - - if (endpoint != null) - { - var address = endpoint.ToString(); - - _endpointListener(address); - } - - LogRequest(_logger, request); - } - - /// <summary> - /// Logs the request. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="request">The request.</param> - private static void LogRequest(ILogger logger, HttpListenerRequest request) - { - var log = new StringBuilder(); - - var logHeaders = true; - - if (logHeaders) - { - var headers = string.Join(",", request.Headers.AllKeys.Where(i => !string.Equals(i, "cookie", StringComparison.OrdinalIgnoreCase) && !string.Equals(i, "Referer", StringComparison.OrdinalIgnoreCase)).Select(k => k + "=" + request.Headers[k])); - - log.AppendLine("Ip: " + request.RemoteEndPoint + ". Headers: " + headers); - } - - var type = request.IsWebSocketRequest ? "Web Socket" : "HTTP " + request.HttpMethod; - - logger.LogMultiline(type + " " + request.Url, LogSeverity.Debug, log); - } - - public void Stop() - { - if (_listener != null) - { - foreach (var prefix in UrlPrefixes) - { - _listener.Prefixes.Remove(prefix); - } - - _listener.Close(); - } - } - - public void Dispose() - { - Dispose(true); - } - - private bool _disposed; - private readonly object _disposeLock = new object(); - protected virtual void Dispose(bool disposing) - { - if (_disposed) return; - - lock (_disposeLock) - { - if (_disposed) return; - - if (disposing) - { - Stop(); - } - - //release unmanaged resources here... - _disposed = true; - } - } - } -}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs b/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs index 020856886..7ac92408b 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -5,10 +5,12 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; +using System.Threading.Tasks; +using ServiceStack; namespace MediaBrowser.Server.Implementations.HttpServer { - public class RangeRequestWriter : IStreamWriter, IHttpResult + public class RangeRequestWriter : IStreamWriter, IAsyncStreamWriter, IHttpResult { /// <summary> /// Gets or sets the source stream. @@ -39,6 +41,9 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// </summary> private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + public Func<IDisposable> ResultScope { get; set; } + public List<Cookie> Cookies { get; private set; } + /// <summary> /// Additional HTTP Headers /// </summary> @@ -81,6 +86,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer Options["Accept-Ranges"] = "bytes"; StatusCode = HttpStatusCode.PartialContent; + Cookies = new List<Cookie>(); SetRangeValues(); } @@ -165,16 +171,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// <param name="responseStream">The response stream.</param> public void WriteTo(Stream responseStream) { - WriteToInternal(responseStream); - } - - /// <summary> - /// Writes to async. - /// </summary> - /// <param name="responseStream">The response stream.</param> - /// <returns>Task.</returns> - private void WriteToInternal(Stream responseStream) - { try { // Headers only @@ -233,6 +229,66 @@ namespace MediaBrowser.Server.Implementations.HttpServer } } + public async Task WriteToAsync(Stream responseStream) + { + try + { + // Headers only + if (IsHeadRequest) + { + return; + } + + using (var source = SourceStream) + { + // If the requested range is "0-", we can optimize by just doing a stream copy + if (RangeEnd >= TotalContentLength - 1) + { + await source.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false); + } + else + { + await CopyToInternalAsync(source, responseStream, RangeLength).ConfigureAwait(false); + } + } + } + catch (IOException ex) + { + throw; + } + catch (Exception ex) + { + _logger.ErrorException("Error in range request writer", ex); + throw; + } + finally + { + if (OnComplete != null) + { + OnComplete(); + } + } + } + + private async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength) + { + var array = new byte[BufferSize]; + int count; + while ((count = await source.ReadAsync(array, 0, array.Length).ConfigureAwait(false)) != 0) + { + var bytesToCopy = Math.Min(count, copyLength); + + await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy)).ConfigureAwait(false); + + copyLength -= bytesToCopy; + + if (copyLength <= 0) + { + break; + } + } + } + public string ContentType { get; set; } public IRequest RequestContext { get; set; } diff --git a/MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs b/MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs index f993d4437..ee05702f4 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs @@ -12,12 +12,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer { private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); private readonly ILogger _logger; - private readonly Func<bool> _denyIframeEmbedding; - public ResponseFilter(ILogger logger, Func<bool> denyIframeEmbedding) + public ResponseFilter(ILogger logger) { _logger = logger; - _denyIframeEmbedding = denyIframeEmbedding; } /// <summary> @@ -31,11 +29,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer // Try to prevent compatibility view res.AddHeader("X-UA-Compatible", "IE=Edge"); - if (_denyIframeEmbedding()) - { - res.AddHeader("X-Frame-Options", "SAMEORIGIN"); - } - var exception = dto as Exception; if (exception != null) diff --git a/MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index 357f5c976..bc3e7b163 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -104,6 +104,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security { info.DeviceId = tokenInfo.DeviceId; } + if (string.IsNullOrWhiteSpace(info.Version)) + { + info.Version = tokenInfo.AppVersion; + } } else { diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs index ed9e17b6b..bfa65ac6b 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs @@ -3,7 +3,9 @@ using System.Collections.Specialized; using System.Globalization; using System.IO; using System.Text; +using System.Threading.Tasks; using System.Web; +using ServiceStack; using ServiceStack.Web; namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp @@ -31,53 +33,54 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp return header.Substring(ap + 1, end - ap - 1); } - void LoadMultiPart() + async Task LoadMultiPart() { string boundary = GetParameter(ContentType, "; boundary="); if (boundary == null) return; - var input = GetSubStream(InputStream); + using (var requestStream = GetSubStream(InputStream)) + { + //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); + await requestStream.CopyToAsync(ms).ConfigureAwait(false); - //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); - input.CopyTo(ms); - input = ms; - ms.WriteByte((byte)'\r'); - ms.WriteByte((byte)'\n'); + var input = ms; + ms.WriteByte((byte)'\r'); + ms.WriteByte((byte)'\n'); - input.Position = 0; + input.Position = 0; - //Uncomment to debug - //var content = new StreamReader(ms).ReadToEnd(); - //Console.WriteLine(boundary + "::" + content); - //input.Position = 0; + //Uncomment to debug + //var content = new StreamReader(ms).ReadToEnd(); + //Console.WriteLine(boundary + "::" + content); + //input.Position = 0; - var multi_part = new HttpMultipart(input, boundary, ContentEncoding); + var multi_part = new HttpMultipart(input, boundary, ContentEncoding); - HttpMultipart.Element e; - while ((e = multi_part.ReadNextElement()) != null) - { - if (e.Filename == null) + HttpMultipart.Element e; + while ((e = multi_part.ReadNextElement()) != null) { - byte[] copy = new byte[e.Length]; + if (e.Filename == null) + { + byte[] copy = new byte[e.Length]; - input.Position = e.Start; - input.Read(copy, 0, (int)e.Length); + input.Position = e.Start; + input.Read(copy, 0, (int)e.Length); - form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy)); - } - else - { - // - // We use a substream, as in 2.x we will support large uploads streamed to disk, - // - HttpPostedFile sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length); - files.AddFile(e.Name, sub); + form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy)); + } + else + { + // + // We use a substream, as in 2.x we will support large uploads streamed to disk, + // + HttpPostedFile sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length); + files.AddFile(e.Name, sub); + } } } - EndSubStream(input); } public NameValueCollection Form @@ -90,10 +93,15 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp files = new HttpFileCollection(); if (IsContentType("multipart/form-data", true)) - LoadMultiPart(); - else if ( - IsContentType("application/x-www-form-urlencoded", true)) - LoadWwwForm(); + { + var task = LoadMultiPart(); + Task.WaitAll(task); + } + else if (IsContentType("application/x-www-form-urlencoded", true)) + { + var task = LoadWwwForm(); + Task.WaitAll(task); + } form.Protect(); } @@ -116,6 +124,21 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp } } + public string Accept + { + get + { + return string.IsNullOrEmpty(request.Headers[HttpHeaders.Accept]) ? null : request.Headers[HttpHeaders.Accept]; + } + } + + public string Authorization + { + get + { + return string.IsNullOrEmpty(request.Headers[HttpHeaders.Authorization]) ? null : request.Headers[HttpHeaders.Authorization]; + } + } protected bool validate_cookies, validate_query_string, validate_form; protected bool checked_cookies, checked_query_string, checked_form; @@ -204,50 +227,50 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp return String.Compare(ContentType, ct, true, Helpers.InvariantCulture) == 0; } - - - - - void LoadWwwForm() + async Task LoadWwwForm() { using (Stream input = GetSubStream(InputStream)) { - using (StreamReader s = new StreamReader(input, ContentEncoding)) + using (var ms = new MemoryStream()) { - StringBuilder key = new StringBuilder(); - StringBuilder value = new StringBuilder(); - int c; + await input.CopyToAsync(ms).ConfigureAwait(false); + ms.Position = 0; - while ((c = s.Read()) != -1) + using (StreamReader s = new StreamReader(ms, ContentEncoding)) { - if (c == '=') + StringBuilder key = new StringBuilder(); + StringBuilder value = new StringBuilder(); + int c; + + while ((c = s.Read()) != -1) { - value.Length = 0; - while ((c = s.Read()) != -1) + if (c == '=') { - if (c == '&') + value.Length = 0; + while ((c = s.Read()) != -1) + { + if (c == '&') + { + AddRawKeyValue(key, value); + break; + } + else + value.Append((char)c); + } + if (c == -1) { AddRawKeyValue(key, value); - break; + return; } - else - value.Append((char)c); } - if (c == -1) - { + else if (c == '&') AddRawKeyValue(key, value); - return; - } + else + key.Append((char)c); } - else if (c == '&') + if (c == -1) AddRawKeyValue(key, value); - else - key.Append((char)c); } - if (c == -1) - AddRawKeyValue(key, value); - - EndSubStream(input); } } } diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs index 30849d441..dc2aec3e1 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs @@ -22,7 +22,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp this.OperationName = operationName; this.RequestAttributes = requestAttributes; this.request = httpContext.Request; - this.response = new WebSocketSharpResponse(logger, httpContext.Response); + this.response = new WebSocketSharpResponse(logger, httpContext.Response, this); this.RequestPreferences = new RequestPreferences(this); } @@ -134,12 +134,89 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp get { return remoteIp ?? - (remoteIp = XForwardedFor ?? - (NormalizeIp(XRealIp) ?? + (remoteIp = (CheckBadChars(XForwardedFor)) ?? + (NormalizeIp(CheckBadChars(XRealIp)) ?? (request.RemoteEndPoint != null ? NormalizeIp(request.RemoteEndPoint.Address.ToString()) : null))); } } + private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 }; + + // + // CheckBadChars - throws on invalid chars to be not found in header name/value + // + internal static string CheckBadChars(string name) + { + if (name == null || name.Length == 0) + { + return name; + } + + // VALUE check + //Trim spaces from both ends + name = name.Trim(HttpTrimCharacters); + + //First, check for correctly formed multi-line value + //Second, check for absenece of CTL characters + int crlf = 0; + for (int i = 0; i < name.Length; ++i) + { + char c = (char)(0x000000ff & (uint)name[i]); + switch (crlf) + { + case 0: + if (c == '\r') + { + crlf = 1; + } + else if (c == '\n') + { + // Technically this is bad HTTP. But it would be a breaking change to throw here. + // Is there an exploit? + crlf = 2; + } + else if (c == 127 || (c < ' ' && c != '\t')) + { + throw new ArgumentException("net_WebHeaderInvalidControlChars"); + } + break; + + case 1: + if (c == '\n') + { + crlf = 2; + break; + } + throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); + + case 2: + if (c == ' ' || c == '\t') + { + crlf = 0; + break; + } + throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); + } + } + if (crlf != 0) + { + throw new ArgumentException("net_WebHeaderInvalidCRLFChars"); + } + return name; + } + + internal static bool ContainsNonAsciiChars(string token) + { + for (int i = 0; i < token.Length; ++i) + { + if ((token[i] < 0x20) || (token[i] > 0x7e)) + { + return true; + } + } + return false; + } + private string NormalizeIp(string ip) { if (!string.IsNullOrWhiteSpace(ip)) @@ -388,10 +465,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp return stream; } - static void EndSubStream(Stream stream) - { - } - public static string GetHandlerPathIfAny(string listenerUrl) { if (listenerUrl == null) return null; diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs index 171dacb22..e08be8bd1 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Net; using MediaBrowser.Model.Logging; @@ -14,14 +15,17 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp private readonly ILogger _logger; private readonly HttpListenerResponse response; - public WebSocketSharpResponse(ILogger logger, HttpListenerResponse response) + public WebSocketSharpResponse(ILogger logger, HttpListenerResponse response, IRequest request) { _logger = logger; this.response = response; + Items = new Dictionary<string, object>(); + Request = request; } + public IRequest Request { get; private set; } public bool UseBufferedStream { get; set; } - + public Dictionary<string, object> Items { get; private set; } public object OriginalResponse { get { return response; } @@ -58,6 +62,11 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp response.AddHeader(name, value); } + public string GetHeader(string name) + { + return response.Headers[name]; + } + public void Redirect(string url) { response.Redirect(url); @@ -142,5 +151,9 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp } public bool KeepAlive { get; set; } + + public void ClearCookies() + { + } } } diff --git a/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs b/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs index a756f4aa8..f5906f6b7 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs @@ -4,13 +4,15 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Threading.Tasks; +using ServiceStack; namespace MediaBrowser.Server.Implementations.HttpServer { /// <summary> /// Class StreamWriter /// </summary> - public class StreamWriter : IStreamWriter, IHasOptions + public class StreamWriter : IStreamWriter, IAsyncStreamWriter, IHasOptions { private ILogger Logger { get; set; } @@ -73,30 +75,49 @@ namespace MediaBrowser.Server.Implementations.HttpServer { } + // 256k + private const int BufferSize = 262144; + /// <summary> /// Writes to. /// </summary> /// <param name="responseStream">The response stream.</param> public void WriteTo(Stream responseStream) { - WriteToInternal(responseStream); + try + { + using (var src = SourceStream) + { + src.CopyTo(responseStream, BufferSize); + } + } + catch (Exception ex) + { + Logger.ErrorException("Error streaming data", ex); + + if (OnError != null) + { + OnError(); + } + + throw; + } + finally + { + if (OnComplete != null) + { + OnComplete(); + } + } } - // 256k - private const int BufferSize = 262144; - - /// <summary> - /// Writes to async. - /// </summary> - /// <param name="responseStream">The response stream.</param> - /// <returns>Task.</returns> - private void WriteToInternal(Stream responseStream) + public async Task WriteToAsync(Stream responseStream) { try { using (var src = SourceStream) { - src.CopyTo(responseStream, BufferSize); + await src.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false); } } catch (Exception ex) @@ -107,7 +128,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer { OnError(); } - + throw; } finally diff --git a/MediaBrowser.Server.Implementations/HttpServer/SwaggerService.cs b/MediaBrowser.Server.Implementations/HttpServer/SwaggerService.cs index aeaac80e8..d91f316d6 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/SwaggerService.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/SwaggerService.cs @@ -25,7 +25,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer var requestedFile = Path.Combine(swaggerDirectory, request.ResourceName.Replace('/', Path.DirectorySeparatorChar)); - return ResultFactory.GetStaticFileResult(Request, requestedFile); + return ResultFactory.GetStaticFileResult(Request, requestedFile).Result; } /// <summary> diff --git a/MediaBrowser.Server.Implementations/IO/FileRefresher.cs b/MediaBrowser.Server.Implementations/IO/FileRefresher.cs new file mode 100644 index 000000000..f48beacb5 --- /dev/null +++ b/MediaBrowser.Server.Implementations/IO/FileRefresher.cs @@ -0,0 +1,289 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommonIO; +using MediaBrowser.Common.Events; +using MediaBrowser.Common.ScheduledTasks; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Logging; +using MediaBrowser.Server.Implementations.ScheduledTasks; + +namespace MediaBrowser.Server.Implementations.IO +{ + public class FileRefresher : IDisposable + { + private ILogger Logger { get; set; } + private ITaskManager TaskManager { get; set; } + private ILibraryManager LibraryManager { get; set; } + private IServerConfigurationManager ConfigurationManager { get; set; } + private readonly IFileSystem _fileSystem; + private readonly List<string> _affectedPaths = new List<string>(); + private Timer _timer; + private readonly object _timerLock = new object(); + public string Path { get; private set; } + + public event EventHandler<EventArgs> Completed; + + public FileRefresher(string path, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ITaskManager taskManager, ILogger logger) + { + logger.Debug("New file refresher created for {0}", path); + Path = path; + + _fileSystem = fileSystem; + ConfigurationManager = configurationManager; + LibraryManager = libraryManager; + TaskManager = taskManager; + Logger = logger; + AddPath(path); + } + + private void AddAffectedPath(string path) + { + if (!_affectedPaths.Contains(path, StringComparer.Ordinal)) + { + _affectedPaths.Add(path); + } + } + + public void AddPath(string path) + { + lock (_timerLock) + { + AddAffectedPath(path); + } + RestartTimer(); + } + + public void RestartTimer() + { + lock (_timerLock) + { + if (_timer == null) + { + _timer = new Timer(OnTimerCallback, null, TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1)); + } + else + { + _timer.Change(TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1)); + } + } + } + + public void ResetPath(string path, string affectedFile) + { + lock (_timerLock) + { + Logger.Debug("Resetting file refresher from {0} to {1}", Path, path); + + Path = path; + AddAffectedPath(path); + + if (!string.IsNullOrWhiteSpace(affectedFile)) + { + AddAffectedPath(affectedFile); + } + } + RestartTimer(); + } + + private async void OnTimerCallback(object state) + { + List<string> paths; + + lock (_timerLock) + { + paths = _affectedPaths.ToList(); + } + + // Extend the timer as long as any of the paths are still being written to. + if (paths.Any(IsFileLocked)) + { + Logger.Info("Timer extended."); + RestartTimer(); + return; + } + + Logger.Debug("Timer stopped."); + + DisposeTimer(); + EventHelper.FireEventIfNotNull(Completed, this, EventArgs.Empty, Logger); + + try + { + await ProcessPathChanges(paths.ToList()).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.ErrorException("Error processing directory changes", ex); + } + } + + private async Task ProcessPathChanges(List<string> paths) + { + var itemsToRefresh = paths + .Select(GetAffectedBaseItem) + .Where(item => item != null) + .Distinct() + .ToList(); + + foreach (var p in paths) + { + Logger.Info(p + " reports change."); + } + + // If the root folder changed, run the library task so the user can see it + if (itemsToRefresh.Any(i => i is AggregateFolder)) + { + TaskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>(); + return; + } + + foreach (var item in itemsToRefresh) + { + Logger.Info(item.Name + " (" + item.Path + ") will be refreshed."); + + try + { + await item.ChangedExternally().ConfigureAwait(false); + } + catch (IOException ex) + { + // For now swallow and log. + // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable) + // Should we remove it from it's parent? + Logger.ErrorException("Error refreshing {0}", ex, item.Name); + } + catch (Exception ex) + { + Logger.ErrorException("Error refreshing {0}", ex, item.Name); + } + } + } + + /// <summary> + /// Gets the affected base item. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>BaseItem.</returns> + private BaseItem GetAffectedBaseItem(string path) + { + BaseItem item = null; + + while (item == null && !string.IsNullOrEmpty(path)) + { + item = LibraryManager.FindByPath(path, null); + + path = System.IO.Path.GetDirectoryName(path); + } + + if (item != null) + { + // If the item has been deleted find the first valid parent that still exists + while (!_fileSystem.DirectoryExists(item.Path) && !_fileSystem.FileExists(item.Path)) + { + item = item.GetParent(); + + if (item == null) + { + break; + } + } + } + + return item; + } + + private bool IsFileLocked(string path) + { + if (Environment.OSVersion.Platform != PlatformID.Win32NT) + { + // Causing lockups on linux + return false; + } + + try + { + var data = _fileSystem.GetFileSystemInfo(path); + + if (!data.Exists + || data.IsDirectory + + // Opening a writable stream will fail with readonly files + || data.Attributes.HasFlag(FileAttributes.ReadOnly)) + { + return false; + } + } + catch (IOException) + { + return false; + } + catch (Exception ex) + { + Logger.ErrorException("Error getting file system info for: {0}", ex, path); + return false; + } + + // In order to determine if the file is being written to, we have to request write access + // But if the server only has readonly access, this is going to cause this entire algorithm to fail + // So we'll take a best guess about our access level + var requestedFileAccess = ConfigurationManager.Configuration.SaveLocalMeta + ? FileAccess.ReadWrite + : FileAccess.Read; + + try + { + using (_fileSystem.GetFileStream(path, FileMode.Open, requestedFileAccess, FileShare.ReadWrite)) + { + //file is not locked + return false; + } + } + catch (DirectoryNotFoundException) + { + // File may have been deleted + return false; + } + catch (FileNotFoundException) + { + // File may have been deleted + return false; + } + catch (IOException) + { + //the file is unavailable because it is: + //still being written to + //or being processed by another thread + //or does not exist (has already been processed) + Logger.Debug("{0} is locked.", path); + return true; + } + catch (Exception ex) + { + Logger.ErrorException("Error determining if file is locked: {0}", ex, path); + return false; + } + } + + private void DisposeTimer() + { + lock (_timerLock) + { + if (_timer != null) + { + _timer.Dispose(); + } + } + } + + public void Dispose() + { + DisposeTimer(); + } + } +} diff --git a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs index 2c0257c5f..99cb80cb2 100644 --- a/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs +++ b/MediaBrowser.Server.Implementations/IO/LibraryMonitor.cs @@ -26,13 +26,9 @@ namespace MediaBrowser.Server.Implementations.IO /// </summary> private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase); /// <summary> - /// The update timer - /// </summary> - private Timer _updateTimer; - /// <summary> /// The affected paths /// </summary> - private readonly ConcurrentDictionary<string, string> _affectedPaths = new ConcurrentDictionary<string, string>(); + private readonly List<FileRefresher> _activeRefreshers = new List<FileRefresher>(); /// <summary> /// A dynamic list of paths that should be ignored. Added to during our own file sytem modifications. @@ -44,8 +40,8 @@ namespace MediaBrowser.Server.Implementations.IO /// </summary> private readonly IReadOnlyList<string> _alwaysIgnoreFiles = new List<string> { - "thumbs.db", - "small.jpg", + "thumbs.db", + "small.jpg", "albumart.jpg", // WMC temp recording directories that will constantly be written to @@ -54,11 +50,6 @@ namespace MediaBrowser.Server.Implementations.IO }; /// <summary> - /// The timer lock - /// </summary> - private readonly object _timerLock = new object(); - - /// <summary> /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope. /// </summary> /// <param name="path">The path.</param> @@ -93,7 +84,7 @@ namespace MediaBrowser.Server.Implementations.IO // This is an arbitraty amount of time, but delay it because file system writes often trigger events long after the file was actually written to. // Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds // But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata - await Task.Delay(25000).ConfigureAwait(false); + await Task.Delay(45000).ConfigureAwait(false); string val; _tempIgnoredPaths.TryRemove(path, out val); @@ -251,7 +242,7 @@ namespace MediaBrowser.Server.Implementations.IO /// <exception cref="System.ArgumentNullException">path</exception> private static bool ContainsParentFolder(IEnumerable<string> lst, string path) { - if (string.IsNullOrEmpty(path)) + if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentNullException("path"); } @@ -463,226 +454,58 @@ namespace MediaBrowser.Server.Implementations.IO if (monitorPath) { // Avoid implicitly captured closure - var affectedPath = path; - _affectedPaths.AddOrUpdate(path, path, (key, oldValue) => affectedPath); + CreateRefresher(path); } - - RestartTimer(); } - private void RestartTimer() + private void CreateRefresher(string path) { - lock (_timerLock) - { - if (_updateTimer == null) - { - _updateTimer = new Timer(TimerStopped, null, TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1)); - } - else - { - _updateTimer.Change(TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1)); - } - } - } + var parentPath = Path.GetDirectoryName(path); - /// <summary> - /// Timers the stopped. - /// </summary> - /// <param name="stateInfo">The state info.</param> - private async void TimerStopped(object stateInfo) - { - // Extend the timer as long as any of the paths are still being written to. - if (_affectedPaths.Any(p => IsFileLocked(p.Key))) + lock (_activeRefreshers) { - Logger.Info("Timer extended."); - RestartTimer(); - return; - } - - Logger.Debug("Timer stopped."); - - DisposeTimer(); - - var paths = _affectedPaths.Keys.ToList(); - _affectedPaths.Clear(); - - try - { - await ProcessPathChanges(paths).ConfigureAwait(false); - } - catch (Exception ex) - { - Logger.ErrorException("Error processing directory changes", ex); - } - } - - private bool IsFileLocked(string path) - { - if (Environment.OSVersion.Platform != PlatformID.Win32NT) - { - // Causing lockups on linux - return false; - } - - try - { - var data = _fileSystem.GetFileSystemInfo(path); - - if (!data.Exists - || data.IsDirectory - - // Opening a writable stream will fail with readonly files - || data.Attributes.HasFlag(FileAttributes.ReadOnly)) + var refreshers = _activeRefreshers.ToList(); + foreach (var refresher in refreshers) { - return false; - } - } - catch (IOException) - { - return false; - } - catch (Exception ex) - { - Logger.ErrorException("Error getting file system info for: {0}", ex, path); - return false; - } - - // In order to determine if the file is being written to, we have to request write access - // But if the server only has readonly access, this is going to cause this entire algorithm to fail - // So we'll take a best guess about our access level - var requestedFileAccess = ConfigurationManager.Configuration.SaveLocalMeta - ? FileAccess.ReadWrite - : FileAccess.Read; + // Path is already being refreshed + if (string.Equals(path, refresher.Path, StringComparison.Ordinal)) + { + refresher.RestartTimer(); + return; + } - try - { - using (_fileSystem.GetFileStream(path, FileMode.Open, requestedFileAccess, FileShare.ReadWrite)) - { - if (_updateTimer != null) + // Parent folder is already being refreshed + if (_fileSystem.ContainsSubPath(refresher.Path, path)) { - //file is not locked - return false; + refresher.AddPath(path); + return; } - } - } - catch (DirectoryNotFoundException) - { - // File may have been deleted - return false; - } - catch (FileNotFoundException) - { - // File may have been deleted - return false; - } - catch (IOException) - { - //the file is unavailable because it is: - //still being written to - //or being processed by another thread - //or does not exist (has already been processed) - Logger.Debug("{0} is locked.", path); - return true; - } - catch (Exception ex) - { - Logger.ErrorException("Error determining if file is locked: {0}", ex, path); - return false; - } - return false; - } + // New path is a parent + if (_fileSystem.ContainsSubPath(path, refresher.Path)) + { + refresher.ResetPath(path, null); + return; + } - private void DisposeTimer() - { - lock (_timerLock) - { - if (_updateTimer != null) - { - _updateTimer.Dispose(); - _updateTimer = null; + // They are siblings. Rebase the refresher to the parent folder. + if (string.Equals(parentPath, Path.GetDirectoryName(refresher.Path), StringComparison.Ordinal)) + { + refresher.ResetPath(parentPath, path); + return; + } } - } - } - - /// <summary> - /// Processes the path changes. - /// </summary> - /// <param name="paths">The paths.</param> - /// <returns>Task.</returns> - private async Task ProcessPathChanges(List<string> paths) - { - var itemsToRefresh = paths - .Select(GetAffectedBaseItem) - .Where(item => item != null) - .Distinct() - .ToList(); - - foreach (var p in paths) - { - Logger.Info(p + " reports change."); - } - - // If the root folder changed, run the library task so the user can see it - if (itemsToRefresh.Any(i => i is AggregateFolder)) - { - TaskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>(); - return; - } - - foreach (var item in itemsToRefresh) - { - Logger.Info(item.Name + " (" + item.Path + ") will be refreshed."); - try - { - await item.ChangedExternally().ConfigureAwait(false); - } - catch (IOException ex) - { - // For now swallow and log. - // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable) - // Should we remove it from it's parent? - Logger.ErrorException("Error refreshing {0}", ex, item.Name); - } - catch (Exception ex) - { - Logger.ErrorException("Error refreshing {0}", ex, item.Name); - } + var newRefresher = new FileRefresher(path, _fileSystem, ConfigurationManager, LibraryManager, TaskManager, Logger); + newRefresher.Completed += NewRefresher_Completed; + _activeRefreshers.Add(newRefresher); } } - /// <summary> - /// Gets the affected base item. - /// </summary> - /// <param name="path">The path.</param> - /// <returns>BaseItem.</returns> - private BaseItem GetAffectedBaseItem(string path) + private void NewRefresher_Completed(object sender, EventArgs e) { - BaseItem item = null; - - while (item == null && !string.IsNullOrEmpty(path)) - { - item = LibraryManager.FindByPath(path); - - path = Path.GetDirectoryName(path); - } - - if (item != null) - { - // If the item has been deleted find the first valid parent that still exists - while (!_fileSystem.DirectoryExists(item.Path) && !_fileSystem.FileExists(item.Path)) - { - item = item.GetParent(); - - if (item == null) - { - break; - } - } - } - - return item; + var refresher = (FileRefresher)sender; + DisposeRefresher(refresher); } /// <summary> @@ -713,10 +536,29 @@ namespace MediaBrowser.Server.Implementations.IO watcher.Dispose(); } - DisposeTimer(); - _fileSystemWatchers.Clear(); - _affectedPaths.Clear(); + DisposeRefreshers(); + } + + private void DisposeRefresher(FileRefresher refresher) + { + lock (_activeRefreshers) + { + refresher.Dispose(); + _activeRefreshers.Remove(refresher); + } + } + + private void DisposeRefreshers() + { + lock (_activeRefreshers) + { + foreach (var refresher in _activeRefreshers.ToList()) + { + refresher.Dispose(); + } + _activeRefreshers.Clear(); + } } /// <summary> diff --git a/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs b/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs index 49012c65a..7c7a535cd 100644 --- a/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs +++ b/MediaBrowser.Server.Implementations/Intros/DefaultIntroProvider.cs @@ -63,16 +63,8 @@ namespace MediaBrowser.Server.Implementations.Intros ? null : _localization.GetRatingLevel(item.OfficialRating); - var random = new Random(Environment.TickCount + Guid.NewGuid().GetHashCode()); - var candidates = new List<ItemWithTrailer>(); - var itemPeople = _libraryManager.GetPeople(item); - var allPeople = _libraryManager.GetPeople(new InternalPeopleQuery - { - AppearsInItemId = item.Id - }); - var trailerTypes = new List<TrailerType>(); if (config.EnableIntrosFromMoviesInLibrary) @@ -105,26 +97,25 @@ namespace MediaBrowser.Server.Implementations.Intros var trailerResult = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = new[] { typeof(Trailer).Name }, - TrailerTypes = trailerTypes.ToArray() + TrailerTypes = trailerTypes.ToArray(), + SimilarTo = item, + IsPlayed = config.EnableIntrosForWatchedContent ? (bool?) null : false, + MaxParentalRating = config.EnableIntrosParentalControl ? ratingLevel : null, + Limit = config.TrailerLimit }); candidates.AddRange(trailerResult.Select(i => new ItemWithTrailer { Item = i, Type = i.SourceType == SourceType.Channel ? ItemWithTrailerType.ChannelTrailer : ItemWithTrailerType.ItemWithTrailer, - User = user, - WatchingItem = item, - WatchingItemPeople = itemPeople, - AllPeople = allPeople, - Random = random, LibraryManager = _libraryManager })); } - return GetResult(item, candidates, config, ratingLevel); + return GetResult(item, candidates, config); } - private IEnumerable<IntroInfo> GetResult(BaseItem item, IEnumerable<ItemWithTrailer> candidates, CinemaModeConfiguration config, int? ratingLevel) + private IEnumerable<IntroInfo> GetResult(BaseItem item, IEnumerable<ItemWithTrailer> candidates, CinemaModeConfiguration config) { var customIntros = !string.IsNullOrWhiteSpace(config.CustomIntroPath) ? GetCustomIntros(config) : @@ -134,48 +125,12 @@ namespace MediaBrowser.Server.Implementations.Intros GetMediaInfoIntros(config, item) : new List<IntroInfo>(); - var trailerLimit = config.TrailerLimit; - // Avoid implicitly captured closure - return candidates.Where(i => - { - if (config.EnableIntrosParentalControl && !FilterByParentalRating(ratingLevel, i.Item)) - { - return false; - } - - if (!config.EnableIntrosForWatchedContent && i.IsPlayed) - { - return false; - } - return !IsDuplicate(item, i.Item); - }) - .OrderByDescending(i => i.Score) - .ThenBy(i => Guid.NewGuid()) - .ThenByDescending(i => i.IsPlayed ? 0 : 1) - .Select(i => i.IntroInfo) - .Take(trailerLimit) + return candidates.Select(i => i.IntroInfo) .Concat(customIntros.Take(1)) .Concat(mediaInfoIntros); } - private bool IsDuplicate(BaseItem playingContent, BaseItem test) - { - var id = playingContent.GetProviderId(MetadataProviders.Imdb); - if (!string.IsNullOrWhiteSpace(id) && string.Equals(id, test.GetProviderId(MetadataProviders.Imdb), StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - id = playingContent.GetProviderId(MetadataProviders.Tmdb); - if (!string.IsNullOrWhiteSpace(id) && string.Equals(id, test.GetProviderId(MetadataProviders.Tmdb), StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - return false; - } - private CinemaModeConfiguration GetOptions() { return _serverConfig.GetConfiguration<CinemaModeConfiguration>("cinemamode"); @@ -346,102 +301,6 @@ namespace MediaBrowser.Server.Implementations.Intros return list.Distinct(StringComparer.OrdinalIgnoreCase); } - private bool FilterByParentalRating(int? ratingLevel, BaseItem item) - { - // Only content rated same or lower - if (ratingLevel.HasValue) - { - var level = string.IsNullOrWhiteSpace(item.OfficialRating) - ? (int?)null - : _localization.GetRatingLevel(item.OfficialRating); - - return level.HasValue && level.Value <= ratingLevel.Value; - } - - return true; - } - - internal static int GetSimiliarityScore(BaseItem item1, List<PersonInfo> item1People, List<PersonInfo> allPeople, BaseItem item2, Random random, ILibraryManager libraryManager) - { - var points = 0; - - if (!string.IsNullOrEmpty(item1.OfficialRating) && string.Equals(item1.OfficialRating, item2.OfficialRating, StringComparison.OrdinalIgnoreCase)) - { - points += 10; - } - - // Find common genres - points += item1.Genres.Where(i => item2.Genres.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10); - - // Find common tags - points += GetTags(item1).Where(i => GetTags(item2).Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10); - - // Find common keywords - points += GetKeywords(item1).Where(i => GetKeywords(item2).Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10); - - // Find common studios - points += item1.Studios.Where(i => item2.Studios.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 5); - - var item2PeopleNames = allPeople.Where(i => i.ItemId == item2.Id) - .Select(i => i.Name) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .DistinctNames() - .ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); - - points += item1People.Where(i => item2PeopleNames.ContainsKey(i.Name)).Sum(i => - { - if (string.Equals(i.Type, PersonType.Director, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase)) - { - return 5; - } - if (string.Equals(i.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Actor, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - if (string.Equals(i.Type, PersonType.Composer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Composer, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - if (string.Equals(i.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - if (string.Equals(i.Type, PersonType.Writer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - - return 1; - }); - - // Add some randomization so that you're not always seeing the same ones for a given movie - points += random.Next(0, 50); - - return points; - } - - private static IEnumerable<string> GetTags(BaseItem item) - { - var hasTags = item as IHasTags; - if (hasTags != null) - { - return hasTags.Tags; - } - - return new List<string>(); - } - - private static IEnumerable<string> GetKeywords(BaseItem item) - { - var hasTags = item as IHasKeywords; - if (hasTags != null) - { - return hasTags.Keywords; - } - - return new List<string>(); - } - public IEnumerable<string> GetAllIntroFiles() { return GetCustomIntroFiles(GetOptions(), true, true); @@ -461,39 +320,8 @@ namespace MediaBrowser.Server.Implementations.Intros { internal BaseItem Item; internal ItemWithTrailerType Type; - internal User User; - internal BaseItem WatchingItem; - internal List<PersonInfo> WatchingItemPeople; - internal List<PersonInfo> AllPeople; - internal Random Random; internal ILibraryManager LibraryManager; - private bool? _isPlayed; - public bool IsPlayed - { - get - { - if (!_isPlayed.HasValue) - { - _isPlayed = Item.IsPlayed(User); - } - return _isPlayed.Value; - } - } - - private int? _score; - public int Score - { - get - { - if (!_score.HasValue) - { - _score = GetSimiliarityScore(WatchingItem, WatchingItemPeople, AllPeople, Item, Random, LibraryManager); - } - return _score.Value; - } - } - public IntroInfo IntroInfo { get diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs index 28671fb7c..712ea4ef3 100644 --- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs +++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs @@ -33,6 +33,9 @@ using System.Net; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Model.Channels; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Library; using MediaBrowser.Model.Net; @@ -143,6 +146,7 @@ namespace MediaBrowser.Server.Implementations.Library private readonly Func<ILibraryMonitor> _libraryMonitorFactory; private readonly Func<IProviderManager> _providerManagerFactory; private readonly Func<IUserViewManager> _userviewManager; + public bool IsScanRunning { get; private set; } /// <summary> /// The _library items cache @@ -305,9 +309,14 @@ namespace MediaBrowser.Server.Implementations.Library /// <returns>Task.</returns> private async Task UpdateSeasonZeroNames(string newName, CancellationToken cancellationToken) { - var seasons = RootFolder.GetRecursiveChildren(i => i is Season) - .Cast<Season>() - .Where(i => i.IndexNumber.HasValue && i.IndexNumber.Value == 0 && !string.Equals(i.Name, newName, StringComparison.Ordinal)) + var seasons = GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Season).Name }, + Recursive = true, + IndexNumber = 0 + + }).Cast<Season>() + .Where(i => !string.Equals(i.Name, newName, StringComparison.Ordinal)) .ToList(); foreach (var season in seasons) @@ -345,10 +354,6 @@ namespace MediaBrowser.Server.Implementations.Library private void RegisterItem(Guid id, BaseItem item) { - if (item.SourceType != SourceType.Library) - { - return; - } if (item is IItemByName) { if (!(item is MusicArtist)) @@ -356,10 +361,25 @@ namespace MediaBrowser.Server.Implementations.Library return; } } - //if (!(item is Folder)) - //{ - // return; - //} + + if (item.IsFolder) + { + if (!(item is ICollectionFolder) && !(item is UserView) && !(item is Channel)) + { + if (item.SourceType != SourceType.Library) + { + return; + } + } + } + else + { + if (item is Photo) + { + return; + } + } + LibraryItemsCache.AddOrUpdate(id, item, delegate { return item; }); } @@ -514,38 +534,6 @@ namespace MediaBrowser.Server.Implementations.Library return key.GetMD5(); } - public IEnumerable<BaseItem> ReplaceVideosWithPrimaryVersions(IEnumerable<BaseItem> items) - { - if (items == null) - { - throw new ArgumentNullException("items"); - } - - var dict = new Dictionary<Guid, BaseItem>(); - - foreach (var item in items) - { - var video = item as Video; - - if (video != null) - { - if (video.PrimaryVersionId.HasValue) - { - var primary = GetItemById(video.PrimaryVersionId.Value) as Video; - - if (primary != null) - { - dict[primary.Id] = primary; - continue; - } - } - } - dict[item.Id] = item; - } - - return dict.Values; - } - /// <summary> /// Ensure supplied item has only one instance throughout /// </summary> @@ -800,27 +788,22 @@ namespace MediaBrowser.Server.Implementations.Library return _userRootFolder; } - public BaseItem FindByPath(string path) + public BaseItem FindByPath(string path, bool? isFolder) { + // If this returns multiple items it could be tricky figuring out which one is correct. + // In most cases, the newest one will be and the others obsolete but not yet cleaned up + var query = new InternalItemsQuery { - Path = path + Path = path, + IsFolder = isFolder, + SortBy = new[] { ItemSortBy.DateCreated }, + SortOrder = SortOrder.Descending, + Limit = 1 }; - // Only use the database result if there's exactly one item, otherwise we run the risk of returning old data that hasn't been cleaned yet. - var items = GetItemIds(query).Select(GetItemById).Where(i => i != null).ToArray(); - - if (items.Length == 1) - { - return items[0]; - } - - if (items.Length == 0) - { - return null; - } - - return RootFolder.FindByPath(path); + return GetItemList(query) + .FirstOrDefault(); } /// <summary> @@ -929,7 +912,10 @@ namespace MediaBrowser.Server.Implementations.Library throw new ArgumentNullException("name"); } - var validFilename = _fileSystem.GetValidFilename(name).Trim(); + // Trim the period at the end because windows will have a hard time with that + var validFilename = _fileSystem.GetValidFilename(name) + .Trim() + .TrimEnd('.'); string subFolderPrefix = null; @@ -951,31 +937,23 @@ namespace MediaBrowser.Server.Implementations.Library Path.Combine(path, validFilename) : Path.Combine(path, subFolderPrefix, validFilename); - var id = GetNewItemId(fullPath, type); - - BaseItem obj; - - if (!_libraryItemsCache.TryGetValue(id, out obj)) - { - obj = CreateItemByName<T>(fullPath, name, id); - - RegisterItem(id, obj); - } - - return obj as T; + return CreateItemByName<T>(fullPath, name); } - private T CreateItemByName<T>(string path, string name, Guid id) + private T CreateItemByName<T>(string path, string name) where T : BaseItem, new() { - var isArtist = typeof(T) == typeof(MusicArtist); - - if (isArtist) + if (typeof(T) == typeof(MusicArtist)) { - var existing = RootFolder - .GetRecursiveChildren(i => i is T && NameExtensions.AreEqual(i.Name, name)) - .Cast<T>() - .FirstOrDefault(); + var existing = GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(T).Name }, + Name = name + + }).Cast<MusicArtist>() + .OrderBy(i => i.IsAccessedByName ? 1 : 0) + .Cast<T>() + .FirstOrDefault(); if (existing != null) { @@ -983,6 +961,8 @@ namespace MediaBrowser.Server.Implementations.Library } } + var id = GetNewItemId(path, typeof(T)); + var item = GetItemById(id) as T; if (item == null) @@ -996,11 +976,6 @@ namespace MediaBrowser.Server.Implementations.Library Path = path }; - if (isArtist) - { - (item as MusicArtist).IsAccessedByName = true; - } - var task = CreateItem(item, CancellationToken.None); Task.WaitAll(task); } @@ -1102,6 +1077,7 @@ namespace MediaBrowser.Server.Implementations.Library /// <returns>Task.</returns> public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken) { + IsScanRunning = true; _libraryMonitorFactory().Stop(); try @@ -1111,6 +1087,7 @@ namespace MediaBrowser.Server.Implementations.Library finally { _libraryMonitorFactory().Start(); + IsScanRunning = false; } } @@ -1289,6 +1266,8 @@ namespace MediaBrowser.Server.Implementations.Library item = RetrieveItem(id); + //_logger.Debug("GetitemById {0}", id); + if (item != null) { RegisterItem(item); @@ -1297,59 +1276,162 @@ namespace MediaBrowser.Server.Implementations.Library return item; } - public BaseItem GetMemoryItemById(Guid id) + public IEnumerable<BaseItem> GetItemList(InternalItemsQuery query) { - if (id == Guid.Empty) + if (query.Recursive && query.ParentId.HasValue) { - throw new ArgumentNullException("id"); + var parent = GetItemById(query.ParentId.Value); + if (parent != null) + { + SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent }); + query.ParentId = null; + } } - BaseItem item; + if (query.User != null) + { + AddUserToQuery(query, query.User); + } - LibraryItemsCache.TryGetValue(id, out item); + return ItemRepository.GetItemList(query); + } - return item; + public IEnumerable<BaseItem> GetItemList(InternalItemsQuery query, IEnumerable<string> parentIds) + { + var parents = parentIds.Select(i => GetItemById(new Guid(i))).Where(i => i != null).ToList(); + + SetTopParentIdsOrAncestors(query, parents); + + if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0) + { + if (query.User != null) + { + AddUserToQuery(query, query.User); + } + } + + return ItemRepository.GetItemList(query); } - public IEnumerable<BaseItem> GetItemList(InternalItemsQuery query) + public QueryResult<BaseItem> QueryItems(InternalItemsQuery query) { if (query.User != null) { AddUserToQuery(query, query.User); } - var result = ItemRepository.GetItemIdsList(query); + if (query.EnableTotalRecordCount) + { + return ItemRepository.GetItems(query); + } - return result.Select(GetItemById).Where(i => i != null); + return new QueryResult<BaseItem> + { + Items = ItemRepository.GetItemList(query).ToArray() + }; } - public QueryResult<BaseItem> QueryItems(InternalItemsQuery query) + public List<Guid> GetItemIds(InternalItemsQuery query) { if (query.User != null) { AddUserToQuery(query, query.User); } - return ItemRepository.GetItems(query); + return ItemRepository.GetItemIdsList(query); } - public List<Guid> GetItemIds(InternalItemsQuery query) + public QueryResult<Tuple<BaseItem, ItemCounts>> GetStudios(InternalItemsQuery query) { if (query.User != null) { AddUserToQuery(query, query.User); } - return ItemRepository.GetItemIdsList(query); + SetTopParentOrAncestorIds(query); + return ItemRepository.GetStudios(query); } - public IEnumerable<BaseItem> GetItemList(InternalItemsQuery query, IEnumerable<string> parentIds) + public QueryResult<Tuple<BaseItem, ItemCounts>> GetGenres(InternalItemsQuery query) { - var parents = parentIds.Select(i => GetItemById(new Guid(i))).Where(i => i != null).ToList(); + if (query.User != null) + { + AddUserToQuery(query, query.User); + } - SetTopParentIdsOrAncestors(query, parents); + SetTopParentOrAncestorIds(query); + return ItemRepository.GetGenres(query); + } - return GetItemIds(query).Select(GetItemById).Where(i => i != null); + public QueryResult<Tuple<BaseItem, ItemCounts>> GetGameGenres(InternalItemsQuery query) + { + if (query.User != null) + { + AddUserToQuery(query, query.User); + } + + SetTopParentOrAncestorIds(query); + return ItemRepository.GetGameGenres(query); + } + + public QueryResult<Tuple<BaseItem, ItemCounts>> GetMusicGenres(InternalItemsQuery query) + { + if (query.User != null) + { + AddUserToQuery(query, query.User); + } + + SetTopParentOrAncestorIds(query); + return ItemRepository.GetMusicGenres(query); + } + + public QueryResult<Tuple<BaseItem, ItemCounts>> GetArtists(InternalItemsQuery query) + { + if (query.User != null) + { + AddUserToQuery(query, query.User); + } + + SetTopParentOrAncestorIds(query); + return ItemRepository.GetArtists(query); + } + + private void SetTopParentOrAncestorIds(InternalItemsQuery query) + { + if (query.AncestorIds.Length == 0) + { + return; + } + + var parents = query.AncestorIds.Select(i => GetItemById(new Guid(i))).ToList(); + + if (parents.All(i => + { + if (i is ICollectionFolder || i is UserView) + { + return true; + } + + //_logger.Debug("Query requires ancestor query due to type: " + i.GetType().Name); + return false; + + })) + { + // Optimize by querying against top level views + query.TopParentIds = parents.SelectMany(i => GetTopParentsForQuery(i, query.User)).Select(i => i.Id.ToString("N")).ToArray(); + query.AncestorIds = new string[] { }; + } + } + + public QueryResult<Tuple<BaseItem, ItemCounts>> GetAlbumArtists(InternalItemsQuery query) + { + if (query.User != null) + { + AddUserToQuery(query, query.User); + } + + SetTopParentOrAncestorIds(query); + return ItemRepository.GetAlbumArtists(query); } public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query) @@ -1369,24 +1451,17 @@ namespace MediaBrowser.Server.Implementations.Library AddUserToQuery(query, query.User); } - var initialResult = ItemRepository.GetItemIds(query); + if (query.EnableTotalRecordCount) + { + return ItemRepository.GetItems(query); + } return new QueryResult<BaseItem> { - TotalRecordCount = initialResult.TotalRecordCount, - Items = initialResult.Items.Select(GetItemById).Where(i => i != null).ToArray() + Items = ItemRepository.GetItemList(query).ToArray() }; } - public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query, IEnumerable<string> parentIds) - { - var parents = parentIds.Select(i => GetItemById(new Guid(i))).Where(i => i != null).ToList(); - - SetTopParentIdsOrAncestors(query, parents); - - return GetItemsResult(query); - } - private void SetTopParentIdsOrAncestors(InternalItemsQuery query, List<BaseItem> parents) { if (parents.All(i => @@ -1396,7 +1471,7 @@ namespace MediaBrowser.Server.Implementations.Library return true; } - _logger.Debug("Query requires ancestor query due to type: " + i.GetType().Name); + //_logger.Debug("Query requires ancestor query due to type: " + i.GetType().Name); return false; })) @@ -1413,7 +1488,7 @@ 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)) { var userViews = _userviewManager().GetUserViews(new UserViewQuery { @@ -1438,8 +1513,13 @@ namespace MediaBrowser.Server.Implementations.Library } if (string.Equals(view.ViewType, CollectionType.Channels)) { - // TODO: Return channels - return new[] { view }; + var channelResult = BaseItem.ChannelManager.GetChannelsInternal(new ChannelQuery + { + UserId = user.Id.ToString("N") + + }, CancellationToken.None).Result; + + return channelResult.Items; } // Translate view into folders @@ -1465,8 +1545,12 @@ namespace MediaBrowser.Server.Implementations.Library // Handle grouping if (user != null && !string.IsNullOrWhiteSpace(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType)) { - var collectionFolders = user.RootFolder.GetChildren(user, true).OfType<CollectionFolder>().Where(i => string.IsNullOrWhiteSpace(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase)); - return collectionFolders.SelectMany(i => GetTopParentsForQuery(i, user)); + return user.RootFolder + .GetChildren(user, true) + .OfType<CollectionFolder>() + .Where(i => string.IsNullOrWhiteSpace(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase)) + .Where(i => user.IsFolderGrouped(i.Id)) + .SelectMany(i => GetTopParentsForQuery(i, user)); } return new BaseItem[] { }; } @@ -1847,7 +1931,7 @@ namespace MediaBrowser.Server.Implementations.Library private string GetContentTypeOverride(string path, bool inherit) { - var nameValuePair = ConfigurationManager.Configuration.ContentTypes.FirstOrDefault(i => string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase) || (inherit && _fileSystem.ContainsSubPath(i.Name, path))); + var nameValuePair = ConfigurationManager.Configuration.ContentTypes.FirstOrDefault(i => string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase) || (inherit && !string.IsNullOrWhiteSpace(i.Name) && _fileSystem.ContainsSubPath(i.Name, path))); if (nameValuePair != null) { return nameValuePair.Value; @@ -2322,7 +2406,7 @@ namespace MediaBrowser.Server.Implementations.Library public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) { - var files = fileSystemChildren.Where(i => i.IsDirectory) + var files = owner.IsInMixedFolder ? 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(); @@ -2361,6 +2445,7 @@ namespace MediaBrowser.Server.Implementations.Library } video.ExtraType = ExtraType.Trailer; + video.TrailerTypes = new List<TrailerType> { TrailerType.LocalTrailer }; return video; @@ -2575,5 +2660,205 @@ namespace MediaBrowser.Server.Implementations.Library throw new InvalidOperationException(); } + + public void AddVirtualFolder(string name, string collectionType, string[] mediaPaths, bool refreshLibrary) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException("name"); + } + + name = _fileSystem.GetValidFilename(name); + + var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + + var virtualFolderPath = Path.Combine(rootFolderPath, name); + while (_fileSystem.DirectoryExists(virtualFolderPath)) + { + name += "1"; + virtualFolderPath = Path.Combine(rootFolderPath, name); + } + + if (mediaPaths != null) + { + var invalidpath = mediaPaths.FirstOrDefault(i => !_fileSystem.DirectoryExists(i)); + if (invalidpath != null) + { + throw new ArgumentException("The specified path does not exist: " + invalidpath + "."); + } + } + + _libraryMonitorFactory().Stop(); + + try + { + _fileSystem.CreateDirectory(virtualFolderPath); + + if (!string.IsNullOrEmpty(collectionType)) + { + var path = Path.Combine(virtualFolderPath, collectionType + ".collection"); + + using (File.Create(path)) + { + + } + } + + if (mediaPaths != null) + { + foreach (var path in mediaPaths) + { + AddMediaPath(name, path); + } + } + } + finally + { + Task.Run(() => + { + // No need to start if scanning the library because it will handle it + if (refreshLibrary) + { + ValidateMediaLibrary(new Progress<double>(), CancellationToken.None); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + var task = Task.Delay(1000); + // Have to block here to allow exceptions to bubble + Task.WaitAll(task); + + _libraryMonitorFactory().Start(); + } + }); + } + } + + public void RemoveVirtualFolder(string name, bool refreshLibrary) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException("name"); + } + + var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + + var path = Path.Combine(rootFolderPath, name); + + if (!_fileSystem.DirectoryExists(path)) + { + throw new DirectoryNotFoundException("The media folder does not exist"); + } + + _libraryMonitorFactory().Stop(); + + try + { + _fileSystem.DeleteDirectory(path, true); + } + finally + { + Task.Run(() => + { + // No need to start if scanning the library because it will handle it + if (refreshLibrary) + { + ValidateMediaLibrary(new Progress<double>(), CancellationToken.None); + } + else + { + // Need to add a delay here or directory watchers may still pick up the changes + var task = Task.Delay(1000); + // Have to block here to allow exceptions to bubble + Task.WaitAll(task); + + _libraryMonitorFactory().Start(); + } + }); + } + } + + 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)) + { + throw new ArgumentNullException("path"); + } + + var removeList = new List<NameValuePair>(); + + foreach (var contentType in ConfigurationManager.Configuration.ContentTypes) + { + if (string.Equals(path, contentType.Name, StringComparison.OrdinalIgnoreCase) + || _fileSystem.ContainsSubPath(path, contentType.Name)) + { + removeList.Add(contentType); + } + } + + if (removeList.Count > 0) + { + ConfigurationManager.Configuration.ContentTypes = ConfigurationManager.Configuration.ContentTypes + .Except(removeList) + .ToArray(); + + ConfigurationManager.SaveConfiguration(); + } + } + + public void RemoveMediaPath(string virtualFolderName, string mediaPath) + { + if (string.IsNullOrWhiteSpace(mediaPath)) + { + throw new ArgumentNullException("mediaPath"); + } + + var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + var path = Path.Combine(rootFolderPath, virtualFolderName); + + if (!_fileSystem.DirectoryExists(path)) + { + 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)); + + if (!string.IsNullOrEmpty(shortcut)) + { + _fileSystem.DeleteFile(shortcut); + } + } } }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Library/LocalTrailerPostScanTask.cs b/MediaBrowser.Server.Implementations/Library/LocalTrailerPostScanTask.cs index 96d570ef9..78107b82d 100644 --- a/MediaBrowser.Server.Implementations/Library/LocalTrailerPostScanTask.cs +++ b/MediaBrowser.Server.Implementations/Library/LocalTrailerPostScanTask.cs @@ -6,6 +6,8 @@ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; namespace MediaBrowser.Server.Implementations.Library { @@ -22,18 +24,24 @@ namespace MediaBrowser.Server.Implementations.Library public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) { - var items = _libraryManager.RootFolder - .GetRecursiveChildren(i => i is IHasTrailers) - .Cast<IHasTrailers>() - .ToList(); + var items = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(BoxSet).Name, typeof(Game).Name, typeof(Movie).Name, typeof(Series).Name }, + Recursive = true + + }).OfType<IHasTrailers>().ToList(); + + var trailerTypes = Enum.GetNames(typeof(TrailerType)) + .Select(i => (TrailerType)Enum.Parse(typeof(TrailerType), i, true)) + .Except(new[] { TrailerType.LocalTrailer }) + .ToArray(); var trailers = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = new[] { typeof(Trailer).Name }, - ExcludeTrailerTypes = new[] - { - TrailerType.LocalTrailer - } + TrailerTypes = trailerTypes, + Recursive = true + }).ToArray(); var numComplete = 0; diff --git a/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs b/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs index 95f5cb0e1..4f3fe1bf3 100644 --- a/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs +++ b/MediaBrowser.Server.Implementations/Library/MediaSourceManager.cs @@ -69,10 +69,6 @@ namespace MediaBrowser.Server.Implementations.Library if (stream.IsTextSubtitleStream) { - if (string.Equals(stream.Codec, "ass", StringComparison.OrdinalIgnoreCase)) - { - return false; - } return true; } @@ -175,13 +171,6 @@ namespace MediaBrowser.Server.Implementations.Library source.SupportsTranscoding = false; } } - else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) - { - if (!user.Policy.EnableVideoPlaybackTranscoding) - { - source.SupportsTranscoding = false; - } - } } } @@ -267,15 +256,17 @@ namespace MediaBrowser.Server.Implementations.Library private void SetUserProperties(IHasUserData item, MediaSourceInfo source, User user) { - var userData = item == null ? new UserItemData() : _userDataManager.GetUserData(user.Id, item.GetUserDataKey()); + var userData = item == null ? new UserItemData() : _userDataManager.GetUserData(user, item); + + var allowRememberingSelection = item == null || item.EnableRememberingTrackSelections; - SetDefaultAudioStreamIndex(source, userData, user); - SetDefaultSubtitleStreamIndex(source, userData, user); + SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection); + SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection); } - private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user) + private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) { - if (userData.SubtitleStreamIndex.HasValue && user.Configuration.RememberSubtitleSelections && user.Configuration.SubtitleMode != SubtitlePlaybackMode.None) + if (userData.SubtitleStreamIndex.HasValue && user.Configuration.RememberSubtitleSelections && user.Configuration.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection) { var index = userData.SubtitleStreamIndex.Value; // Make sure the saved index is still valid @@ -304,9 +295,9 @@ namespace MediaBrowser.Server.Implementations.Library user.Configuration.SubtitleMode, audioLangage); } - private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user) + private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection) { - if (userData.AudioStreamIndex.HasValue && user.Configuration.RememberAudioSelections) + if (userData.AudioStreamIndex.HasValue && user.Configuration.RememberAudioSelections && allowRememberingSelection) { var index = userData.AudioStreamIndex.Value; // Make sure the saved index is still valid diff --git a/MediaBrowser.Server.Implementations/Library/MusicManager.cs b/MediaBrowser.Server.Implementations/Library/MusicManager.cs index aad7c112b..3ff434898 100644 --- a/MediaBrowser.Server.Implementations/Library/MusicManager.cs +++ b/MediaBrowser.Server.Implementations/Library/MusicManager.cs @@ -30,7 +30,10 @@ namespace MediaBrowser.Server.Implementations.Library public IEnumerable<Audio> GetInstantMixFromArtist(MusicArtist artist, User user) { var genres = user.RootFolder - .GetRecursiveChildren(user, i => i is Audio) + .GetRecursiveChildren(user, new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { typeof(Audio).Name } + }) .Cast<Audio>() .Where(i => i.HasAnyArtist(artist.Name)) .SelectMany(i => i.Genres) @@ -43,7 +46,10 @@ namespace MediaBrowser.Server.Implementations.Library public IEnumerable<Audio> GetInstantMixFromAlbum(MusicAlbum item, User user) { var genres = item - .GetRecursiveChildren(user, i => i is Audio) + .GetRecursiveChildren(user, new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { typeof(Audio).Name } + }) .Cast<Audio>() .SelectMany(i => i.Genres) .Concat(item.Genres) @@ -55,7 +61,10 @@ namespace MediaBrowser.Server.Implementations.Library public IEnumerable<Audio> GetInstantMixFromFolder(Folder item, User user) { var genres = item - .GetRecursiveChildren(user, i => i is Audio) + .GetRecursiveChildren(user, new InternalItemsQuery(user) + { + IncludeItemTypes = new[] {typeof(Audio).Name} + }) .Cast<Audio>() .SelectMany(i => i.Genres) .Concat(item.Genres) @@ -67,7 +76,10 @@ namespace MediaBrowser.Server.Implementations.Library public IEnumerable<Audio> GetInstantMixFromPlaylist(Playlist item, User user) { var genres = item - .GetRecursiveChildren(user, i => i is Audio) + .GetRecursiveChildren(user, new InternalItemsQuery(user) + { + IncludeItemTypes = new[] { typeof(Audio).Name } + }) .Cast<Audio>() .SelectMany(i => i.Genres) .Concat(item.Genres) @@ -86,7 +98,7 @@ namespace MediaBrowser.Server.Implementations.Library Genres = genreList.ToArray() - }, new string[] { }); + }); var genresDictionary = genreList.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); diff --git a/MediaBrowser.Server.Implementations/Library/ResolverHelper.cs b/MediaBrowser.Server.Implementations/Library/ResolverHelper.cs index 60e7e2df3..9f949db92 100644 --- a/MediaBrowser.Server.Implementations/Library/ResolverHelper.cs +++ b/MediaBrowser.Server.Implementations/Library/ResolverHelper.cs @@ -44,7 +44,6 @@ namespace MediaBrowser.Server.Implementations.Library // Make sure DateCreated and DateModified have values var fileInfo = directoryService.GetFile(item.Path); - item.DateModified = fileSystem.GetLastWriteTimeUtc(fileInfo); SetDateCreated(item, fileSystem, fileInfo); EnsureName(item, fileInfo); @@ -80,7 +79,7 @@ namespace MediaBrowser.Server.Implementations.Library item.GetParents().Any(i => i.IsLocked); // Make sure DateCreated and DateModified have values - EnsureDates(fileSystem, item, args, true); + EnsureDates(fileSystem, item, args); } /// <summary> @@ -125,8 +124,7 @@ namespace MediaBrowser.Server.Implementations.Library /// <param name="fileSystem">The file system.</param> /// <param name="item">The item.</param> /// <param name="args">The args.</param> - /// <param name="includeCreationTime">if set to <c>true</c> [include creation time].</param> - private static void EnsureDates(IFileSystem fileSystem, BaseItem item, ItemResolveArgs args, bool includeCreationTime) + private static void EnsureDates(IFileSystem fileSystem, BaseItem item, ItemResolveArgs args) { if (fileSystem == null) { @@ -148,12 +146,7 @@ namespace MediaBrowser.Server.Implementations.Library if (childData != null) { - if (includeCreationTime) - { - SetDateCreated(item, fileSystem, childData); - } - - item.DateModified = fileSystem.GetLastWriteTimeUtc(childData); + SetDateCreated(item, fileSystem, childData); } else { @@ -161,21 +154,13 @@ namespace MediaBrowser.Server.Implementations.Library if (fileData.Exists) { - if (includeCreationTime) - { - SetDateCreated(item, fileSystem, fileData); - } - item.DateModified = fileSystem.GetLastWriteTimeUtc(fileData); + SetDateCreated(item, fileSystem, fileData); } } } else { - if (includeCreationTime) - { - SetDateCreated(item, fileSystem, args.FileInfo); - } - item.DateModified = fileSystem.GetLastWriteTimeUtc(args.FileInfo); + SetDateCreated(item, fileSystem, args.FileInfo); } } diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 9edd3f83f..703a33856 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -126,7 +126,7 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers } else { - var videoInfo = parser.ResolveFile(args.Path); + var videoInfo = parser.Resolve(args.Path, false, false); if (videoInfo == null) { diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/PhotoResolver.cs index 8beb03b71..9dd30edde 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -5,15 +5,19 @@ using MediaBrowser.Model.Entities; using System; using System.IO; using System.Linq; +using CommonIO; namespace MediaBrowser.Server.Implementations.Library.Resolvers { public class PhotoResolver : ItemResolver<Photo> { private readonly IImageProcessor _imageProcessor; - public PhotoResolver(IImageProcessor imageProcessor) + private readonly ILibraryManager _libraryManager; + + public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager) { _imageProcessor = imageProcessor; + _libraryManager = libraryManager; } /// <summary> @@ -23,20 +27,45 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers /// <returns>Trailer.</returns> protected override Photo Resolve(ItemResolveArgs args) { - // Must be an image file within a photo collection - if (string.Equals(args.GetCollectionType(), CollectionType.Photos, StringComparison.OrdinalIgnoreCase) && - !args.IsDirectory && - IsImageFile(args.Path, _imageProcessor)) + if (!args.IsDirectory) { - return new Photo + // Must be an image file within a photo collection + var collectionType = args.GetCollectionType(); + + if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) || + string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)) { - Path = args.Path - }; + if (IsImageFile(args.Path, _imageProcessor)) + { + var filename = Path.GetFileNameWithoutExtension(args.Path); + + // Make sure the image doesn't belong to a video file + if (args.DirectoryService.GetFiles(Path.GetDirectoryName(args.Path)).Any(i => IsOwnedByMedia(i, filename))) + { + return null; + } + + return new Photo + { + Path = args.Path + }; + } + } } return null; } + private bool IsOwnedByMedia(FileSystemMetadata file, string imageFilename) + { + if (_libraryManager.IsVideoFile(file.FullName) && imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file.Name), StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + return false; + } + private static readonly string[] IgnoreFiles = { "folder", @@ -44,7 +73,8 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers "landscape", "fanart", "backdrop", - "poster" + "poster", + "cover" }; internal static bool IsImageFile(string path, IImageProcessor imageProcessor) diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index e62049821..7b8832c59 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -1,6 +1,9 @@ -using MediaBrowser.Controller.Entities.TV; +using System; +using System.IO; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using System.Linq; +using MediaBrowser.Model.Entities; namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV { @@ -28,7 +31,6 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV } var season = parent as Season; - // Just in case the user decided to nest episodes. // Not officially supported but in some cases we can handle it. if (season == null) @@ -37,10 +39,31 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV } // If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something - if (season != null || args.HasParent<Series>()) + // Also handle flat tv folders + if (season != null || + string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) || + args.HasParent<Series>()) { var episode = ResolveVideo<Episode>(args, false); + if (episode != null) + { + var series = parent as Series; + if (series == null) + { + series = parent.GetParents().OfType<Series>().FirstOrDefault(); + } + + if (series != null) + { + episode.SeriesId = series.Id; + } + if (season != null) + { + episode.SeasonId = season.Id; + } + } + return episode; } diff --git a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index 7d13b11ad..eeac1345e 100644 --- a/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/MediaBrowser.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -38,10 +38,12 @@ namespace MediaBrowser.Server.Implementations.Library.Resolvers.TV if (args.Parent is Series && args.IsDirectory) { var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); - + var series = ((Series)args.Parent); + var season = new Season { - IndexNumber = new SeasonPathParser(namingOptions, new RegexProvider()).Parse(args.Path, true, true).SeasonNumber + IndexNumber = new SeasonPathParser(namingOptions, new RegexProvider()).Parse(args.Path, true, true).SeasonNumber, + SeriesId = series.Id }; if (season.IndexNumber.HasValue && season.IndexNumber.Value == 0) diff --git a/MediaBrowser.Server.Implementations/Library/SearchEngine.cs b/MediaBrowser.Server.Implementations/Library/SearchEngine.cs index 276fc329f..cf6f070d0 100644 --- a/MediaBrowser.Server.Implementations/Library/SearchEngine.cs +++ b/MediaBrowser.Server.Implementations/Library/SearchEngine.cs @@ -87,13 +87,16 @@ namespace MediaBrowser.Server.Implementations.Library { var searchTerm = query.SearchTerm; + if (searchTerm != null) + { + searchTerm = searchTerm.Trim().RemoveDiacritics(); + } + if (string.IsNullOrWhiteSpace(searchTerm)) { throw new ArgumentNullException("searchTerm"); } - searchTerm = searchTerm.RemoveDiacritics(); - var terms = GetWords(searchTerm); var hints = new List<Tuple<BaseItem, string, int>>(); @@ -119,7 +122,7 @@ namespace MediaBrowser.Server.Implementations.Library AddIfMissing(excludeItemTypes, typeof(MusicGenre).Name); } - if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase))) + if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase))) { if (!query.IncludeMedia) { @@ -165,7 +168,7 @@ namespace MediaBrowser.Server.Implementations.Library Limit = query.Limit, IncludeItemsByName = true - }, new string[] { }); + }); // Add search hints based on item name hints.AddRange(mediaItems.Where(IncludeInSearch).Select(item => diff --git a/MediaBrowser.Server.Implementations/Library/UserDataManager.cs b/MediaBrowser.Server.Implementations/Library/UserDataManager.cs index ae737d244..715f3c522 100644 --- a/MediaBrowser.Server.Implementations/Library/UserDataManager.cs +++ b/MediaBrowser.Server.Implementations/Library/UserDataManager.cs @@ -10,6 +10,7 @@ using MediaBrowser.Model.Logging; using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -22,7 +23,8 @@ namespace MediaBrowser.Server.Implementations.Library { public event EventHandler<UserDataSaveEventArgs> UserDataSaved; - private readonly ConcurrentDictionary<string, UserItemData> _userData = new ConcurrentDictionary<string, UserItemData>(); + private readonly ConcurrentDictionary<string, UserItemData> _userData = + new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase); private readonly ILogger _logger; private readonly IServerConfigurationManager _config; @@ -56,27 +58,28 @@ namespace MediaBrowser.Server.Implementations.Library cancellationToken.ThrowIfCancellationRequested(); - var key = item.GetUserDataKey(); + var keys = item.GetUserDataKeys(); - try + foreach (var key in keys) { - await Repository.SaveUserData(userId, key, userData, cancellationToken).ConfigureAwait(false); - - var newValue = userData; + try + { + await Repository.SaveUserData(userId, key, userData, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error saving user data", ex); - // Once it succeeds, put it into the dictionary to make it available to everyone else - _userData.AddOrUpdate(GetCacheKey(userId, key), newValue, delegate { return newValue; }); + throw; + } } - catch (Exception ex) - { - _logger.ErrorException("Error saving user data", ex); - throw; - } + var cacheKey = GetCacheKey(userId, item.Id); + _userData.AddOrUpdate(cacheKey, userData, (k, v) => userData); EventHelper.FireEventIfNotNull(UserDataSaved, this, new UserDataSaveEventArgs { - Key = key, + Keys = keys, UserData = userData, SaveReason = reason, UserId = userId, @@ -116,7 +119,7 @@ namespace MediaBrowser.Server.Implementations.Library throw; } - + } /// <summary> @@ -134,51 +137,86 @@ namespace MediaBrowser.Server.Implementations.Library return Repository.GetAllUserData(userId); } - /// <summary> - /// Gets the user data. - /// </summary> - /// <param name="userId">The user id.</param> - /// <param name="key">The key.</param> - /// <returns>Task{UserItemData}.</returns> - public UserItemData GetUserData(Guid userId, string key) + public UserItemData GetUserData(Guid userId, Guid itemId, List<string> keys) { if (userId == Guid.Empty) { throw new ArgumentNullException("userId"); } - if (string.IsNullOrEmpty(key)) + if (keys == null) + { + throw new ArgumentNullException("keys"); + } + if (keys.Count == 0) { - throw new ArgumentNullException("key"); + throw new ArgumentException("UserData keys cannot be empty."); } - return _userData.GetOrAdd(GetCacheKey(userId, key), keyName => GetUserDataFromRepository(userId, key)); + var cacheKey = GetCacheKey(userId, itemId); + + return _userData.GetOrAdd(cacheKey, k => GetUserDataInternal(userId, keys)); } - public UserItemData GetUserDataFromRepository(Guid userId, string key) + private UserItemData GetUserDataInternal(Guid userId, List<string> keys) { - var data = Repository.GetUserData(userId, key); + var userData = Repository.GetUserData(userId, keys); - return data; + if (userData != null) + { + return userData; + } + + if (keys.Count > 0) + { + return new UserItemData + { + UserId = userId, + Key = keys[0] + }; + } + + return null; } /// <summary> /// Gets the internal key. /// </summary> - /// <param name="userId">The user id.</param> - /// <param name="key">The key.</param> /// <returns>System.String.</returns> - private string GetCacheKey(Guid userId, string key) + private string GetCacheKey(Guid userId, Guid itemId) + { + return userId.ToString("N") + itemId.ToString("N"); + } + + public UserItemData GetUserData(IHasUserData user, IHasUserData item) + { + return GetUserData(user.Id, item); + } + + public UserItemData GetUserData(string userId, IHasUserData item) + { + return GetUserData(new Guid(userId), item); + } + + public UserItemData GetUserData(Guid userId, IHasUserData item) { - return userId + key; + return GetUserData(userId, item.Id, item.GetUserDataKeys()); } - public UserItemDataDto GetUserDataDto(IHasUserData item, User user) + public async Task<UserItemDataDto> GetUserDataDto(IHasUserData item, User user) { - var userData = GetUserData(user.Id, item.GetUserDataKey()); + var userData = GetUserData(user.Id, item); var dto = GetUserItemDataDto(userData); - item.FillUserDataDtoValues(dto, userData, user); + await item.FillUserDataDtoValues(dto, userData, null, user).ConfigureAwait(false); + return dto; + } + + public async Task<UserItemDataDto> GetUserDataDto(IHasUserData item, BaseItemDto itemDto, User user) + { + var userData = GetUserData(user.Id, item); + var dto = GetUserItemDataDto(userData); + await item.FillUserDataDtoValues(dto, userData, itemDto, user).ConfigureAwait(false); return dto; } @@ -261,10 +299,5 @@ namespace MediaBrowser.Server.Implementations.Library return playedToCompletion; } - - public UserItemData GetUserData(string userId, string key) - { - return GetUserData(new Guid(userId), key); - } } } diff --git a/MediaBrowser.Server.Implementations/Library/UserManager.cs b/MediaBrowser.Server.Implementations/Library/UserManager.cs index c1807efe9..6456d7f81 100644 --- a/MediaBrowser.Server.Implementations/Library/UserManager.cs +++ b/MediaBrowser.Server.Implementations/Library/UserManager.cs @@ -352,6 +352,7 @@ namespace MediaBrowser.Server.Implementations.Library users.Add(user); user.Policy.IsAdministrator = true; + user.Policy.EnableContentDeletion = true; user.Policy.EnableRemoteControlOfOtherUsers = true; await UpdateUserPolicy(user, user.Policy, false).ConfigureAwait(false); } @@ -728,7 +729,7 @@ namespace MediaBrowser.Server.Implementations.Library var text = new StringBuilder(); - var localAddress = _appHost.LocalApiUrl ?? string.Empty; + var localAddress = _appHost.GetLocalApiUrl().Result ?? string.Empty; text.AppendLine("Use your web browser to visit:"); text.AppendLine(string.Empty); diff --git a/MediaBrowser.Server.Implementations/Library/UserViewManager.cs b/MediaBrowser.Server.Implementations/Library/UserViewManager.cs index 9f6e39b46..319e715c3 100644 --- a/MediaBrowser.Server.Implementations/Library/UserViewManager.cs +++ b/MediaBrowser.Server.Implementations/Library/UserViewManager.cs @@ -105,7 +105,7 @@ namespace MediaBrowser.Server.Implementations.Library } } - if (user.Configuration.DisplayFoldersView) + if (_config.Configuration.EnableFolderView) { var name = _localizationManager.GetLocalizedString("ViewType" + CollectionType.Folders); list.Add(await _libraryManager.GetNamedView(name, CollectionType.Folders, string.Empty, cancellationToken).ConfigureAwait(false)); @@ -121,7 +121,7 @@ namespace MediaBrowser.Server.Implementations.Library var channels = channelResult.Items; - if (!user.Configuration.DisplayChannelsInline && channels.Length > 0) + if (user.Configuration.EnableChannelView && channels.Length > 0) { list.Add(await _channelManager.GetInternalChannelFolder(cancellationToken).ConfigureAwait(false)); } @@ -202,23 +202,7 @@ namespace MediaBrowser.Server.Implementations.Library { var user = _userManager.GetUserById(request.UserId); - var includeTypes = request.IncludeItemTypes; - - var currentUser = user; - - var libraryItems = GetItemsForLatestItems(user, request.ParentId, includeTypes, request.Limit ?? 10).Where(i => - { - if (request.IsPlayed.HasValue) - { - var val = request.IsPlayed.Value; - if (i is Video && i.IsPlayed(currentUser) != val) - { - return false; - } - } - - return true; - }); + var libraryItems = GetItemsForLatestItems(user, request); var list = new List<Tuple<BaseItem, List<BaseItem>>>(); @@ -254,8 +238,13 @@ namespace MediaBrowser.Server.Implementations.Library return list; } - private IEnumerable<BaseItem> GetItemsForLatestItems(User user, string parentId, string[] includeItemTypes, int limit) + private IEnumerable<BaseItem> GetItemsForLatestItems(User user, LatestItemsQuery request) { + var parentId = request.ParentId; + + var includeItemTypes = request.IncludeItemTypes; + var limit = request.Limit ?? 10; + var parentIds = string.IsNullOrEmpty(parentId) ? new string[] { } : new[] { parentId }; @@ -276,7 +265,12 @@ namespace MediaBrowser.Server.Implementations.Library var excludeItemTypes = includeItemTypes.Length == 0 ? new[] { - typeof(Person).Name, typeof(Studio).Name, typeof(Year).Name, typeof(GameGenre).Name, typeof(MusicGenre).Name, typeof(Genre).Name + typeof(Person).Name, + typeof(Studio).Name, + typeof(Year).Name, + typeof(GameGenre).Name, + typeof(MusicGenre).Name, + typeof(Genre).Name } : new string[] { }; @@ -288,8 +282,9 @@ namespace MediaBrowser.Server.Implementations.Library IsFolder = includeItemTypes.Length == 0 ? false : (bool?)null, ExcludeItemTypes = excludeItemTypes, ExcludeLocationTypes = new[] { LocationType.Virtual }, - Limit = limit * 20, - ExcludeSourceTypes = parentIds.Length == 0 ? new[] { SourceType.Channel, SourceType.LiveTV } : new SourceType[] { } + Limit = limit * 5, + ExcludeSourceTypes = parentIds.Length == 0 ? new[] { SourceType.Channel, SourceType.LiveTV } : new SourceType[] { }, + IsPlayed = request.IsPlayed }, parentIds); } diff --git a/MediaBrowser.Server.Implementations/Library/Validators/StudiosValidator.cs b/MediaBrowser.Server.Implementations/Library/Validators/StudiosValidator.cs index c122d64d3..c1803b5e4 100644 --- a/MediaBrowser.Server.Implementations/Library/Validators/StudiosValidator.cs +++ b/MediaBrowser.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -35,7 +35,7 @@ namespace MediaBrowser.Server.Implementations.Library.Validators /// <returns>Task.</returns> public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) { - var items = _libraryManager.RootFolder.GetRecursiveChildren() + var items = _libraryManager.RootFolder.GetRecursiveChildren(i => true) .SelectMany(i => i.Studios) .DistinctNames() .ToList(); @@ -73,28 +73,6 @@ namespace MediaBrowser.Server.Implementations.Library.Validators progress.Report(percent); } - var allIds = _libraryManager.GetItemIds(new InternalItemsQuery - { - IncludeItemTypes = new[] { typeof(Studio).Name } - }); - - var invalidIds = allIds - .Except(validIds) - .ToList(); - - foreach (var id in invalidIds) - { - cancellationToken.ThrowIfCancellationRequested(); - - var item = _libraryManager.GetItemById(id); - - await _libraryManager.DeleteItem(item, new DeleteOptions - { - DeleteFileLocation = false - - }).ConfigureAwait(false); - } - progress.Report(100); } } diff --git a/MediaBrowser.Server.Implementations/Library/Validators/YearsPostScanTask.cs b/MediaBrowser.Server.Implementations/Library/Validators/YearsPostScanTask.cs index 5ea5fb254..7f52a4506 100644 --- a/MediaBrowser.Server.Implementations/Library/Validators/YearsPostScanTask.cs +++ b/MediaBrowser.Server.Implementations/Library/Validators/YearsPostScanTask.cs @@ -20,16 +20,12 @@ namespace MediaBrowser.Server.Implementations.Library.Validators public async Task Run(IProgress<double> progress, CancellationToken cancellationToken) { - var allYears = _libraryManager.RootFolder.GetRecursiveChildren(i => i.ProductionYear.HasValue) - .Select(i => i.ProductionYear ?? -1) - .Where(i => i > 0) - .Distinct() - .ToList(); - - var count = allYears.Count; + var yearNumber = 1900; + var maxYear = DateTime.UtcNow.Year + 3; + var count = maxYear - yearNumber + 1; var numComplete = 0; - foreach (var yearNumber in allYears) + while (yearNumber < maxYear) { try { @@ -53,6 +49,7 @@ namespace MediaBrowser.Server.Implementations.Library.Validators percent *= 100; progress.Report(percent); + yearNumber++; } } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs index 24d38a63e..23560b1aa 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/ChannelImageProvider.cs @@ -41,7 +41,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var service = _liveTvManager.Services.FirstOrDefault(i => string.Equals(i.Name, liveTvItem.ServiceName, StringComparison.OrdinalIgnoreCase)); - if (service != null) + if (service != null && !item.HasImage(ImageType.Primary)) { try { @@ -77,7 +77,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv get { return 0; } } - public bool HasChanged(IHasMetadata item, MetadataStatus status, IDirectoryService directoryService) + public bool HasChanged(IHasMetadata item, IDirectoryService directoryService) { return GetSupportedImages(item).Any(i => !item.HasImage(i)); } diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index d33b2c51d..b21aa904b 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -23,6 +23,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _fileSystem = fileSystem; } + public string GetOutputPath(MediaSourceInfo mediaSource, string targetFile) + { + return targetFile; + } + public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { var httpRequestOptions = new HttpRequestOptions() @@ -40,14 +45,27 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { onStarted(); - _logger.Info("Copying recording stream to file stream"); + _logger.Info("Copying recording stream to file {0}", targetFile); - var durationToken = new CancellationTokenSource(duration); - var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + 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, linkedToken).ConfigureAwait(false); + await response.Content.CopyToAsync(output, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false); } } + + _logger.Info("Recording completed to file {0}", targetFile); } } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 60ff23b04..8f56554f1 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -26,13 +26,16 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Power; using Microsoft.Win32; namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { - public class EmbyTV : ILiveTvService, IHasRegistrationInfo, IDisposable + public class EmbyTV : ILiveTvService, ISupportsNewTimerIds, IHasRegistrationInfo, IDisposable { private readonly IApplicationHost _appHpst; private readonly ILogger _logger; @@ -40,7 +43,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV private readonly IServerConfigurationManager _config; private readonly IJsonSerializer _jsonSerializer; - private readonly ItemDataProvider<RecordingInfo> _recordingProvider; private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider; private readonly TimerManager _timerProvider; @@ -56,6 +58,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV public static EmbyTV Current; + 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, ISecurityManager security, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, IFileOrganizationService organizationService, IMediaEncoder mediaEncoder, IPowerManagement powerManagement) { Current = this; @@ -74,10 +82,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _liveTvManager = (LiveTvManager)liveTvManager; _jsonSerializer = jsonSerializer; - _recordingProvider = new ItemDataProvider<RecordingInfo>(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "recordings"), (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)); _seriesTimerProvider = new SeriesTimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers")); _timerProvider = new TimerManager(fileSystem, jsonSerializer, _logger, Path.Combine(DataPath, "timers"), powerManagement, _logger); _timerProvider.TimerFired += _timerProvider_TimerFired; + + _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated; + } + + private void _config_NamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) + { + if (string.Equals(e.Key, "livetv", StringComparison.OrdinalIgnoreCase)) + { + OnRecordingFoldersChanged(); + } } public void Start() @@ -85,6 +102,124 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _timerProvider.RestartTimers(); SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; + CreateRecordingFolders(); + } + + private void OnRecordingFoldersChanged() + { + CreateRecordingFolders(); + } + + internal void CreateRecordingFolders() + { + try + { + CreateRecordingFoldersInternal(); + } + catch (Exception ex) + { + _logger.ErrorException("Error creating recording folders", ex); + } + } + + internal void CreateRecordingFoldersInternal() + { + var recordingFolders = GetRecordingFolders(); + + var virtualFolders = _libraryManager.GetVirtualFolders() + .ToList(); + + var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList(); + + var pathsAdded = new List<string>(); + + foreach (var recordingFolder in recordingFolders) + { + var pathsToCreate = recordingFolder.Locations + .Where(i => !allExistingPaths.Contains(i, StringComparer.OrdinalIgnoreCase)) + .ToList(); + + if (pathsToCreate.Count == 0) + { + continue; + } + + try + { + _libraryManager.AddVirtualFolder(recordingFolder.Name, recordingFolder.CollectionType, pathsToCreate.ToArray(), true); + } + catch (Exception ex) + { + _logger.ErrorException("Error creating virtual folder", ex); + } + + pathsAdded.AddRange(pathsToCreate); + } + + var config = GetConfiguration(); + + var pathsToRemove = config.MediaLocationsCreated + .Except(recordingFolders.SelectMany(i => i.Locations)) + .ToList(); + + if (pathsAdded.Count > 0 || pathsToRemove.Count > 0) + { + pathsAdded.InsertRange(0, config.MediaLocationsCreated); + config.MediaLocationsCreated = pathsAdded.Except(pathsToRemove).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + _config.SaveConfiguration("livetv", config); + } + + foreach (var path in pathsToRemove) + { + RemovePathFromLibrary(path); + } + } + + private void RemovePathFromLibrary(string path) + { + _logger.Debug("Removing path from library: {0}", path); + + var requiresRefresh = false; + var virtualFolders = _libraryManager.GetVirtualFolders() + .ToList(); + + foreach (var virtualFolder in virtualFolders) + { + if (!virtualFolder.Locations.Contains(path, StringComparer.OrdinalIgnoreCase)) + { + continue; + } + + if (virtualFolder.Locations.Count == 1) + { + // remove entire virtual folder + try + { + _libraryManager.RemoveVirtualFolder(virtualFolder.Name, true); + } + catch (Exception ex) + { + _logger.ErrorException("Error removing virtual folder", ex); + } + } + else + { + try + { + _libraryManager.RemoveMediaPath(virtualFolder.Name, path); + requiresRefresh = true; + } + catch (Exception ex) + { + _logger.ErrorException("Error removing media path", ex); + } + } + } + + if (requiresRefresh) + { + _libraryManager.ValidateMediaLibrary(new Progress<Double>(), CancellationToken.None); + } } void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e) @@ -97,13 +232,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } - public event EventHandler DataSourceChanged; - - public event EventHandler<RecordingStatusChangedEventArgs> RecordingStatusChanged; - - private readonly ConcurrentDictionary<string, ActiveRecordingInfo> _activeRecordings = - new ConcurrentDictionary<string, ActiveRecordingInfo>(StringComparer.OrdinalIgnoreCase); - public string Name { get { return "Emby"; } @@ -114,6 +242,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "livetv"); } } + private string DefaultRecordingPath + { + get + { + return Path.Combine(DataPath, "recordings"); + } + } + + private string RecordingPath + { + get + { + var path = GetConfiguration().RecordingPath; + + return string.IsNullOrWhiteSpace(path) + ? DefaultRecordingPath + : path; + } + } + public string HomePageUrl { get { return "http://emby.media"; } @@ -234,6 +382,29 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return list; } + public async Task<List<ChannelInfo>> GetChannelsForListingsProvider(ListingsProviderInfo listingsProvider, CancellationToken cancellationToken) + { + var list = new List<ChannelInfo>(); + + foreach (var hostInstance in _liveTvManager.TunerHosts) + { + try + { + var channels = await hostInstance.GetChannels(cancellationToken).ConfigureAwait(false); + + list.AddRange(channels); + } + catch (Exception ex) + { + _logger.ErrorException("Error getting channels", ex); + } + } + + return list + .Where(i => IsListingProviderEnabledForTuner(listingsProvider, i.TunerHostId)) + .ToList(); + } + public Task<IEnumerable<ChannelInfo>> GetChannelsAsync(CancellationToken cancellationToken) { return GetChannelsAsync(false, cancellationToken); @@ -280,59 +451,29 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return Task.FromResult(true); } - public async Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken) + public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken) { - var remove = _recordingProvider.GetAll().FirstOrDefault(i => string.Equals(i.Id, recordingId, StringComparison.OrdinalIgnoreCase)); - if (remove != null) - { - if (!string.IsNullOrWhiteSpace(remove.TimerId)) - { - var enableDelay = _activeRecordings.ContainsKey(remove.TimerId); - - CancelTimerInternal(remove.TimerId); - - if (enableDelay) - { - // A hack yes, but need to make sure the file is closed before attempting to delete it - await Task.Delay(3000, cancellationToken).ConfigureAwait(false); - } - } - - if (!string.IsNullOrWhiteSpace(remove.Path)) - { - try - { - _fileSystem.DeleteFile(remove.Path); - } - catch (DirectoryNotFoundException) - { + return Task.FromResult(true); + } - } - catch (FileNotFoundException) - { + public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken) + { + return CreateTimer(info, cancellationToken); + } - } - catch (Exception ex) - { - _logger.ErrorException("Error deleting recording file {0}", ex, remove.Path); - } - } - _recordingProvider.Delete(remove); - } - else - { - throw new ResourceNotFoundException("Recording not found: " + recordingId); - } + public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) + { + return CreateSeriesTimer(info, cancellationToken); } - public Task CreateTimerAsync(TimerInfo info, CancellationToken cancellationToken) + public Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken) { info.Id = Guid.NewGuid().ToString("N"); _timerProvider.Add(info); - return Task.FromResult(0); + return Task.FromResult(info.Id); } - public async Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) + public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken) { info.Id = Guid.NewGuid().ToString("N"); @@ -362,6 +503,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _seriesTimerProvider.Add(info); await UpdateTimersForSeriesTimer(epgData, info, false).ConfigureAwait(false); + + return info.Id; } public async Task UpdateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) @@ -424,29 +567,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV public async Task<IEnumerable<RecordingInfo>> GetRecordingsAsync(CancellationToken cancellationToken) { - var recordings = _recordingProvider.GetAll().ToList(); - var updated = false; - - foreach (var recording in recordings) - { - if (recording.Status == RecordingStatus.InProgress) - { - if (string.IsNullOrWhiteSpace(recording.TimerId) || !_activeRecordings.ContainsKey(recording.TimerId)) - { - recording.Status = RecordingStatus.Cancelled; - recording.DateLastUpdated = DateTime.UtcNow; - _recordingProvider.Update(recording); - updated = true; - } - } - } - - if (updated) - { - recordings = _recordingProvider.GetAll().ToList(); - } - - return recordings; + return new List<RecordingInfo>(); } public Task<IEnumerable<TimerInfo>> GetTimersAsync(CancellationToken cancellationToken) @@ -462,9 +583,20 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { PostPaddingSeconds = Math.Max(config.PostPaddingSeconds, 0), PrePaddingSeconds = Math.Max(config.PrePaddingSeconds, 0), - RecordAnyChannel = false, - RecordAnyTime = false, - RecordNewOnly = false + RecordAnyChannel = true, + RecordAnyTime = true, + RecordNewOnly = false, + + Days = new List<DayOfWeek> + { + DayOfWeek.Sunday, + DayOfWeek.Monday, + DayOfWeek.Tuesday, + DayOfWeek.Wednesday, + DayOfWeek.Thursday, + DayOfWeek.Friday, + DayOfWeek.Saturday + } }; if (program != null) @@ -528,7 +660,16 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _logger.Debug("Getting programs for channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty); - var programs = await provider.Item1.GetProgramsAsync(provider.Item2, channel.Number, channel.Name, startDateUtc, endDateUtc, cancellationToken) + var channelMappings = GetChannelMappings(provider.Item2); + var channelNumber = channel.Number; + string mappedChannelNumber; + if (channelMappings.TryGetValue(channelNumber, out mappedChannelNumber)) + { + _logger.Debug("Found mapped channel on provider {0}. Tuner channel number: {1}, Mapped channel number: {2}", provider.Item1.Name, channelNumber, mappedChannelNumber); + channelNumber = mappedChannelNumber; + } + + var programs = await provider.Item1.GetProgramsAsync(provider.Item2, channelNumber, channel.Name, startDateUtc, endDateUtc, cancellationToken) .ConfigureAwait(false); var list = programs.ToList(); @@ -550,6 +691,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return new List<ProgramInfo>(); } + private Dictionary<string, string> GetChannelMappings(ListingsProviderInfo info) + { + var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + foreach (var mapping in info.ChannelMappings) + { + dict[mapping.Name] = mapping.Value; + } + + return dict; + } + private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders() { return GetConfiguration().ListingProviders @@ -591,7 +744,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV throw new ApplicationException("Tuner not found."); } - private async Task<Tuple<MediaSourceInfo, SemaphoreSlim>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken) + private async Task<Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>> GetChannelStreamInternal(string channelId, string streamId, CancellationToken cancellationToken) { _logger.Info("Streaming Channel " + channelId); @@ -599,7 +752,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { try { - return await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false); + var result = await hostInstance.GetChannelStream(channelId, streamId, cancellationToken).ConfigureAwait(false); + + return new Tuple<MediaSourceInfo, ITunerHost, SemaphoreSlim>(result.Item1, hostInstance, result.Item2); } catch (Exception e) { @@ -693,6 +848,106 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } + private string GetRecordingPath(TimerInfo timer, ProgramInfo info) + { + var recordPath = RecordingPath; + var config = GetConfiguration(); + + if (info.IsMovie) + { + var customRecordingPath = config.MovieRecordingPath; + var allowSubfolder = true; + if (!string.IsNullOrWhiteSpace(customRecordingPath)) + { + allowSubfolder = string.Equals(customRecordingPath, recordPath, StringComparison.OrdinalIgnoreCase); + recordPath = customRecordingPath; + } + + if (allowSubfolder && config.EnableRecordingSubfolders) + { + recordPath = Path.Combine(recordPath, "Movies"); + } + + var folderName = _fileSystem.GetValidFilename(info.Name).Trim(); + if (info.ProductionYear.HasValue) + { + folderName += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; + } + recordPath = Path.Combine(recordPath, folderName); + } + else if (info.IsSeries) + { + var customRecordingPath = config.SeriesRecordingPath; + var allowSubfolder = true; + if (!string.IsNullOrWhiteSpace(customRecordingPath)) + { + allowSubfolder = string.Equals(customRecordingPath, recordPath, StringComparison.OrdinalIgnoreCase); + recordPath = customRecordingPath; + } + + if (allowSubfolder && config.EnableRecordingSubfolders) + { + 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) + ")"; + } + + if (Directory.Exists(Path.Combine(recordPath, folderName))) + { + recordPath = Path.Combine(recordPath, folderName); + } + else + { + recordPath = Path.Combine(recordPath, folderNameWithYear); + } + + if (info.SeasonNumber.HasValue) + { + folderName = string.Format("Season {0}", info.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture)); + recordPath = Path.Combine(recordPath, folderName); + } + } + else if (info.IsKids) + { + if (config.EnableRecordingSubfolders) + { + recordPath = Path.Combine(recordPath, "Kids"); + } + + var folderName = _fileSystem.GetValidFilename(info.Name).Trim(); + if (info.ProductionYear.HasValue) + { + folderName += " (" + info.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")"; + } + recordPath = Path.Combine(recordPath, folderName); + } + else if (info.IsSports) + { + if (config.EnableRecordingSubfolders) + { + recordPath = Path.Combine(recordPath, "Sports"); + } + recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(info.Name).Trim()); + } + else + { + if (config.EnableRecordingSubfolders) + { + recordPath = Path.Combine(recordPath, "Other"); + } + recordPath = Path.Combine(recordPath, _fileSystem.GetValidFilename(info.Name).Trim()); + } + + var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts"; + + return Path.Combine(recordPath, recordingFileName); + } + private async Task RecordStream(TimerInfo timer, DateTime recordingEndDate, ActiveRecordingInfo activeRecordingInfo, CancellationToken cancellationToken) { if (timer == null) @@ -722,161 +977,102 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV throw new InvalidOperationException(string.Format("Program with Id {0} not found", timer.ProgramId)); } - var recordPath = RecordingPath; - - if (info.IsMovie) - { - recordPath = Path.Combine(recordPath, "Movies", _fileSystem.GetValidFilename(info.Name).Trim()); - } - else if (info.IsSeries) - { - recordPath = Path.Combine(recordPath, "Series", _fileSystem.GetValidFilename(info.Name).Trim()); - } - else if (info.IsKids) - { - recordPath = Path.Combine(recordPath, "Kids", _fileSystem.GetValidFilename(info.Name).Trim()); - } - else if (info.IsSports) - { - recordPath = Path.Combine(recordPath, "Sports", _fileSystem.GetValidFilename(info.Name).Trim()); - } - else - { - recordPath = Path.Combine(recordPath, "Other", _fileSystem.GetValidFilename(info.Name).Trim()); - } - - var recordingFileName = _fileSystem.GetValidFilename(RecordingHelper.GetRecordingName(timer, info)).Trim() + ".ts"; - - recordPath = Path.Combine(recordPath, recordingFileName); - - var recordingId = info.Id.GetMD5().ToString("N"); - var recording = _recordingProvider.GetAll().FirstOrDefault(x => string.Equals(x.Id, recordingId, StringComparison.OrdinalIgnoreCase)); - - if (recording == null) - { - recording = new RecordingInfo - { - ChannelId = info.ChannelId, - Id = recordingId, - StartDate = info.StartDate, - EndDate = info.EndDate, - Genres = info.Genres, - IsKids = info.IsKids, - IsLive = info.IsLive, - IsMovie = info.IsMovie, - IsHD = info.IsHD, - IsNews = info.IsNews, - IsPremiere = info.IsPremiere, - IsSeries = info.IsSeries, - IsSports = info.IsSports, - IsRepeat = !info.IsPremiere, - Name = info.Name, - EpisodeTitle = info.EpisodeTitle, - ProgramId = info.Id, - ImagePath = info.ImagePath, - ImageUrl = info.ImageUrl, - OriginalAirDate = info.OriginalAirDate, - Status = RecordingStatus.Scheduled, - Overview = info.Overview, - SeriesTimerId = timer.SeriesTimerId, - TimerId = timer.Id, - ShowId = info.ShowId - }; - _recordingProvider.AddOrUpdate(recording); - } + var recordPath = GetRecordingPath(timer, info); + var recordingStatus = RecordingStatus.New; + var isResourceOpen = false; + SemaphoreSlim semaphore = null; try { var result = await GetChannelStreamInternal(timer.ChannelId, null, CancellationToken.None).ConfigureAwait(false); + isResourceOpen = true; + semaphore = result.Item3; var mediaStreamInfo = result.Item1; - var isResourceOpen = true; - // Unfortunately due to the semaphore we have to have a nested try/finally - try - { - // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg - //await Task.Delay(3000, cancellationToken).ConfigureAwait(false); + // HDHR doesn't seem to release the tuner right away after first probing with ffmpeg + //await Task.Delay(3000, cancellationToken).ConfigureAwait(false); - var duration = recordingEndDate - DateTime.UtcNow; + var recorder = await GetRecorder().ConfigureAwait(false); - var recorder = await GetRecorder().ConfigureAwait(false); + recordPath = recorder.GetOutputPath(mediaStreamInfo, recordPath); + recordPath = EnsureFileUnique(recordPath, timer.Id); - if (recorder is EncodedRecorder) - { - recordPath = Path.ChangeExtension(recordPath, ".mp4"); - } - recordPath = EnsureFileUnique(recordPath, timer.Id); - _fileSystem.CreateDirectory(Path.GetDirectoryName(recordPath)); - activeRecordingInfo.Path = recordPath; + _libraryMonitor.ReportFileSystemChangeBeginning(recordPath); + _fileSystem.CreateDirectory(Path.GetDirectoryName(recordPath)); + activeRecordingInfo.Path = recordPath; - _libraryMonitor.ReportFileSystemChangeBeginning(recordPath); + var duration = recordingEndDate - DateTime.UtcNow; - recording.Path = recordPath; - recording.Status = RecordingStatus.InProgress; - recording.DateLastUpdated = DateTime.UtcNow; - _recordingProvider.AddOrUpdate(recording); + _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"); - _logger.Info("Writing file to path: " + recordPath); - _logger.Info("Opening recording stream from tuner provider"); + Action onStarted = () => + { + timer.Status = RecordingStatus.InProgress; + _timerProvider.AddOrUpdate(timer, false); - Action onStarted = () => - { - result.Item2.Release(); - isResourceOpen = false; - }; + result.Item3.Release(); + isResourceOpen = false; + }; - await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken).ConfigureAwait(false); + var pathWithDuration = result.Item2.ApplyDuration(mediaStreamInfo.Path, duration); - recording.Status = RecordingStatus.Completed; - _logger.Info("Recording completed: {0}", recordPath); - } - finally + // If it supports supplying duration via url + if (!string.Equals(pathWithDuration, mediaStreamInfo.Path, StringComparison.OrdinalIgnoreCase)) { - if (isResourceOpen) - { - result.Item2.Release(); - } - - _libraryMonitor.ReportFileSystemChangeComplete(recordPath, false); + mediaStreamInfo.Path = pathWithDuration; + mediaStreamInfo.RunTimeTicks = duration.Ticks; } + + await recorder.Record(mediaStreamInfo, recordPath, duration, onStarted, cancellationToken).ConfigureAwait(false); + + recordingStatus = RecordingStatus.Completed; + _logger.Info("Recording completed: {0}", recordPath); } catch (OperationCanceledException) { _logger.Info("Recording stopped: {0}", recordPath); - recording.Status = RecordingStatus.Completed; + recordingStatus = RecordingStatus.Completed; } catch (Exception ex) { _logger.ErrorException("Error recording to {0}", ex, recordPath); - recording.Status = RecordingStatus.Error; + recordingStatus = RecordingStatus.Error; } finally { + if (isResourceOpen && semaphore != null) + { + semaphore.Release(); + } + + _libraryMonitor.ReportFileSystemChangeComplete(recordPath, true); + ActiveRecordingInfo removed; _activeRecordings.TryRemove(timer.Id, out removed); } - recording.DateLastUpdated = DateTime.UtcNow; - _recordingProvider.AddOrUpdate(recording); - - if (recording.Status == RecordingStatus.Completed) + if (recordingStatus == RecordingStatus.Completed) { - OnSuccessfulRecording(recording); + timer.Status = RecordingStatus.Completed; _timerProvider.Delete(timer); + + OnSuccessfulRecording(info.IsSeries, recordPath); } else if (DateTime.UtcNow < timer.EndDate) { const int retryIntervalSeconds = 60; _logger.Info("Retrying recording in {0} seconds.", retryIntervalSeconds); - _timerProvider.StartTimer(timer, TimeSpan.FromSeconds(retryIntervalSeconds)); + timer.Status = RecordingStatus.New; + timer.StartDate = DateTime.UtcNow.AddSeconds(retryIntervalSeconds); + _timerProvider.AddOrUpdate(timer); } else { _timerProvider.Delete(timer); - _recordingProvider.Delete(recording); } } @@ -916,24 +1112,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV private async Task<IRecorder> GetRecorder() { - if (GetConfiguration().EnableRecordingEncoding) + var config = GetConfiguration(); + + if (config.EnableRecordingEncoding) { var regInfo = await _security.GetRegistrationStatus("embytvrecordingconversion").ConfigureAwait(false); if (regInfo.IsValid) { - return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer); + return new EncodedRecorder(_logger, _fileSystem, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, config, _httpClient); } } return new DirectRecorder(_logger, _httpClient, _fileSystem); } - private async void OnSuccessfulRecording(RecordingInfo recording) + private async void OnSuccessfulRecording(bool isSeries, string path) { if (GetConfiguration().EnableAutoOrganize) { - if (recording.IsSeries) + if (isSeries) { try { @@ -943,12 +1141,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV var organize = new EpisodeFileOrganizer(_organizationService, _config, _fileSystem, _logger, _libraryManager, _libraryMonitor, _providerManager); - var result = await organize.OrganizeEpisodeFile(recording.Path, CancellationToken.None).ConfigureAwait(false); - - if (result.Status == FileSortingStatus.Success) - { - _recordingProvider.Delete(recording); - } + var result = await organize.OrganizeEpisodeFile(path, CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { @@ -972,18 +1165,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV return epgData.FirstOrDefault(p => Math.Abs(startDateTicks - p.StartDate.Ticks) <= TimeSpan.FromMinutes(3).Ticks); } - private string RecordingPath - { - get - { - var path = GetConfiguration().RecordingPath; - - return string.IsNullOrWhiteSpace(path) - ? Path.Combine(DataPath, "recordings") - : path; - } - } - private LiveTvOptions GetConfiguration() { return _config.GetConfiguration<LiveTvOptions>("livetv"); @@ -991,7 +1172,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV private async Task UpdateTimersForSeriesTimer(List<ProgramInfo> epgData, SeriesTimerInfo seriesTimer, bool deleteInvalidTimers) { - var newTimers = GetTimersForSeries(seriesTimer, epgData, _recordingProvider.GetAll()).ToList(); + var newTimers = GetTimersForSeries(seriesTimer, epgData, true).ToList(); var registration = await GetRegistrationInfo("seriesrecordings").ConfigureAwait(false); @@ -1005,7 +1186,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV if (deleteInvalidTimers) { - var allTimers = GetTimersForSeries(seriesTimer, epgData, new List<RecordingInfo>()) + var allTimers = GetTimersForSeries(seriesTimer, epgData, false) .Select(i => i.Id) .ToList(); @@ -1021,7 +1202,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } - private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer, IEnumerable<ProgramInfo> allPrograms, IReadOnlyList<RecordingInfo> currentRecordings) + private IEnumerable<TimerInfo> GetTimersForSeries(SeriesTimerInfo seriesTimer, + IEnumerable<ProgramInfo> allPrograms, + bool filterByCurrentRecordings) { if (seriesTimer == null) { @@ -1031,28 +1214,80 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { throw new ArgumentNullException("allPrograms"); } - if (currentRecordings == null) - { - throw new ArgumentNullException("currentRecordings"); - } // Exclude programs that have already ended allPrograms = allPrograms.Where(i => i.EndDate > DateTime.UtcNow && i.StartDate > DateTime.UtcNow); allPrograms = GetProgramsForSeries(seriesTimer, allPrograms); - var recordingShowIds = currentRecordings.Select(i => i.ProgramId).Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); - - allPrograms = allPrograms.Where(i => !recordingShowIds.Contains(i.Id, StringComparer.OrdinalIgnoreCase)); + if (filterByCurrentRecordings) + { + allPrograms = allPrograms.Where(i => !IsProgramAlreadyInLibrary(i)); + } return allPrograms.Select(i => RecordingHelper.CreateTimer(i, seriesTimer)); } + private bool IsProgramAlreadyInLibrary(ProgramInfo program) + { + if ((program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) || !string.IsNullOrWhiteSpace(program.EpisodeTitle)) + { + var seriesIds = _libraryManager.GetItemIds(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Series).Name }, + Name = program.Name + + }).Select(i => i.ToString("N")).ToArray(); + + if (seriesIds.Length == 0) + { + return false; + } + + if (program.EpisodeNumber.HasValue && program.SeasonNumber.HasValue) + { + var result = _libraryManager.GetItemsResult(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Episode).Name }, + ParentIndexNumber = program.SeasonNumber.Value, + IndexNumber = program.EpisodeNumber.Value, + AncestorIds = seriesIds, + ExcludeLocationTypes = new[] { LocationType.Virtual } + }); + + if (result.TotalRecordCount > 0) + { + return true; + } + } + + if (!string.IsNullOrWhiteSpace(program.EpisodeTitle)) + { + var result = _libraryManager.GetItemsResult(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Episode).Name }, + Name = program.EpisodeTitle, + AncestorIds = seriesIds, + ExcludeLocationTypes = new[] { LocationType.Virtual } + }); + + if (result.TotalRecordCount > 0) + { + return true; + } + } + } + + return false; + } + 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) @@ -1065,8 +1300,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV allPrograms = allPrograms.Where(epg => string.Equals(epg.ChannelId, seriesTimer.ChannelId, StringComparison.OrdinalIgnoreCase)); } - allPrograms = allPrograms.Where(i => seriesTimer.Days.Contains(i.StartDate.ToLocalTime().DayOfWeek)); - if (string.IsNullOrWhiteSpace(seriesTimer.SeriesId)) { _logger.Error("seriesTimer.SeriesId is null. Cannot find programs for series"); @@ -1132,6 +1365,47 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV }); } + public List<VirtualFolderInfo> GetRecordingFolders() + { + var list = new List<VirtualFolderInfo>(); + + var defaultFolder = RecordingPath; + var defaultName = "Recordings"; + + if (Directory.Exists(defaultFolder)) + { + list.Add(new VirtualFolderInfo + { + Locations = new List<string> { defaultFolder }, + Name = defaultName + }); + } + + var customPath = GetConfiguration().MovieRecordingPath; + if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath)) + { + list.Add(new VirtualFolderInfo + { + Locations = new List<string> { customPath }, + Name = "Recorded Movies", + CollectionType = CollectionType.Movies + }); + } + + customPath = GetConfiguration().SeriesRecordingPath; + if ((!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase)) && Directory.Exists(customPath)) + { + list.Add(new VirtualFolderInfo + { + Locations = new List<string> { customPath }, + Name = "Recorded Series", + CollectionType = CollectionType.TvShows + }); + } + + return list; + } + class ActiveRecordingInfo { public string Path { get; set; } diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTVRegistration.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTVRegistration.cs new file mode 100644 index 000000000..675fca325 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EmbyTVRegistration.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using MediaBrowser.Common.Security; + +namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV +{ + public class EmbyTVRegistration : IRequiresRegistration + { + private readonly ISecurityManager _securityManager; + + public static EmbyTVRegistration Instance; + + public EmbyTVRegistration(ISecurityManager securityManager) + { + _securityManager = securityManager; + Instance = this; + } + + private bool? _isXmlTvEnabled; + + public Task LoadRegistrationInfoAsync() + { + _isXmlTvEnabled = null; + return Task.FromResult(true); + } + + public async Task<bool> EnableXmlTv() + { + if (!_isXmlTvEnabled.HasValue) + { + var info = await _securityManager.GetRegistrationStatus("xmltv").ConfigureAwait(false); + _isXmlTvEnabled = info.IsValid; + } + return _isXmlTvEnabled.Value; + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 69cc8ebf7..5e428e6f0 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -8,10 +8,13 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using CommonIO; -using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; @@ -21,8 +24,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { private readonly ILogger _logger; private readonly IFileSystem _fileSystem; + private readonly IHttpClient _httpClient; private readonly IMediaEncoder _mediaEncoder; - private readonly IApplicationPaths _appPaths; + private readonly IServerApplicationPaths _appPaths; + private readonly LiveTvOptions _liveTvOptions; private bool _hasExited; private Stream _logFileStream; private string _targetPath; @@ -30,17 +35,99 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV private readonly IJsonSerializer _json; private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>(); - public EncodedRecorder(ILogger logger, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IApplicationPaths appPaths, IJsonSerializer json) + public EncodedRecorder(ILogger logger, IFileSystem fileSystem, IMediaEncoder mediaEncoder, IServerApplicationPaths appPaths, IJsonSerializer json, LiveTvOptions liveTvOptions, IHttpClient httpClient) { _logger = logger; _fileSystem = fileSystem; _mediaEncoder = mediaEncoder; _appPaths = appPaths; _json = json; + _liveTvOptions = liveTvOptions; + _httpClient = httpClient; + } + + public string GetOutputPath(MediaSourceInfo mediaSource, string targetFile) + { + return Path.ChangeExtension(targetFile, ".mp4"); } public async Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { + var tempfile = Path.Combine(_appPaths.TranscodingTempPath, Guid.NewGuid().ToString("N") + ".ts"); + + try + { + await RecordInternal(mediaSource, tempfile, targetFile, duration, onStarted, cancellationToken) + .ConfigureAwait(false); + } + finally + { + try + { + File.Delete(tempfile); + } + catch (Exception ex) + { + _logger.ErrorException("Error deleting recording temp file", ex); + } + } + } + + public async Task RecordInternal(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; _fileSystem.CreateDirectory(Path.GetDirectoryName(targetFile)); @@ -52,12 +139,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV UseShellExecute = false, // Must consume both stdout and stderr or deadlocks may occur - RedirectStandardOutput = true, + //RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = true, FileName = _mediaEncoder.EncoderPath, - Arguments = GetCommandLineArgs(mediaSource, targetFile, duration), + Arguments = GetCommandLineArgs(mediaSource, inputFile, targetFile, duration), WindowStyle = ProcessWindowStyle.Hidden, ErrorDialog = false @@ -78,7 +165,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV _logFileStream = _fileSystem.GetFileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, true); var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); - await _logFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationToken).ConfigureAwait(false); + _logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length); process.Exited += (sender, args) => OnFfMpegProcessExited(process); @@ -87,24 +174,24 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV cancellationToken.Register(Stop); // MUST read both stdout and stderr asynchronously or a deadlock may occurr - process.BeginOutputReadLine(); + //process.BeginOutputReadLine(); onStarted(); // 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); - await _taskCompletionSource.Task.ConfigureAwait(false); + return _taskCompletionSource.Task; } - private string GetCommandLineArgs(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration) + private string GetCommandLineArgs(MediaSourceInfo mediaSource, string inputTempFile, string targetFile, TimeSpan duration) { string videoArgs; if (EncodeVideo(mediaSource)) { var maxBitrate = 25000000; videoArgs = string.Format( - "-codec:v:0 libx264 -force_key_frames expr:gte(t,n_forced*5) {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -vsync vfr -profile:v high -level 41", + "-codec:v:0 libx264 -force_key_frames expr:gte(t,n_forced*5) {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -vsync -1 -profile:v high -level 41", GetOutputSizeParam(), maxBitrate.ToString(CultureInfo.InvariantCulture)); } @@ -113,23 +200,31 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV videoArgs = "-codec:v:0 copy"; } - var commandLineArgs = "-fflags +genpts -async 1 -vsync -1 -i \"{0}\" -t {4} -sn {2} -map_metadata -1 -threads 0 {3} -y \"{1}\""; + 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}\""; if (mediaSource.ReadAtNativeFramerate) { commandLineArgs = "-re " + commandLineArgs; } - commandLineArgs = string.Format(commandLineArgs, mediaSource.Path, targetFile, videoArgs, GetAudioArgs(mediaSource), _mediaEncoder.GetTimeParameter(duration.Ticks)); + commandLineArgs = string.Format(commandLineArgs, inputTempFile, targetFile, videoArgs, GetAudioArgs(mediaSource), durationParam); return commandLineArgs; } private string GetAudioArgs(MediaSourceInfo mediaSource) { - var copyAudio = new[] { "aac", "mp3" }; + // do not copy aac because many players have difficulty with aac_latm + var copyAudio = new[] { "mp3" }; var mediaStreams = mediaSource.MediaStreams ?? new List<MediaStream>(); - if (mediaStreams.Any(i => i.Type == MediaStreamType.Audio && copyAudio.Contains(i.Codec, StringComparer.OrdinalIgnoreCase))) + var inputAudioCodec = mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Select(i => i.Codec).FirstOrDefault() ?? string.Empty; + + if (copyAudio.Contains(inputAudioCodec, StringComparer.OrdinalIgnoreCase)) + { + return "-codec:a:0 copy"; + } + if (_liveTvOptions.EnableOriginalAudioWithEncodedRecordings && !string.Equals(inputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase)) { return "-codec:a:0 copy"; } @@ -175,9 +270,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV //process.Kill(); _process.StandardInput.WriteLine("q"); - - // Need to wait because killing is asynchronous - _process.WaitForExit(5000); } catch (Exception ex) { diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs index 268a4f751..5706b6ae9 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs @@ -17,5 +17,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> Task Record(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken); + + string GetOutputPath(MediaSourceInfo mediaSource, string targetFile); } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index 5d462f106..423358906 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading; using CommonIO; using MediaBrowser.Controller.Power; +using MediaBrowser.Model.LiveTv; namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV { @@ -71,6 +72,26 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } + public void AddOrUpdate(TimerInfo item, bool resetTimer) + { + if (resetTimer) + { + AddOrUpdate(item); + return; + } + + var list = GetAll().ToList(); + + if (!list.Any(i => EqualityComparer(i, item))) + { + base.Add(item); + } + else + { + base.Update(item); + } + } + public override void Add(TimerInfo item) { if (string.IsNullOrWhiteSpace(item.Id)) @@ -85,6 +106,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV private void AddTimer(TimerInfo item) { + if (item.Status == RecordingStatus.Completed) + { + return; + } + var startDate = RecordingHelper.GetStartTime(item); var now = DateTime.UtcNow; @@ -117,15 +143,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.EmbyTV } } - public void StartTimer(TimerInfo item, TimeSpan length) + public void StartTimer(TimerInfo item, TimeSpan dueTime) { StopTimer(item); - var timer = new Timer(TimerCallback, item.Id, length, TimeSpan.Zero); + var timer = new Timer(TimerCallback, item.Id, dueTime, TimeSpan.Zero); if (_timers.TryAdd(item.Id, timer)) { - _logger.Info("Creating recording timer for {0}, {1}. Timer will fire in {2} minutes", item.Id, item.Name, length.TotalMinutes.ToString(CultureInfo.InvariantCulture)); + _logger.Info("Creating recording timer for {0}, {1}. Timer will fire in {2} minutes", item.Id, item.Name, dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture)); } else { diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index ae2a85090..e37109c14 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -506,8 +506,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings } private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms( - ListingsProviderInfo info, - List<string> programIds, + ListingsProviderInfo info, + List<string> programIds, CancellationToken cancellationToken) { var imageIdString = "["; @@ -564,7 +564,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings try { - using (Stream responce = await Get(options, false, info).ConfigureAwait(false)) + using (Stream responce = await Get(options, false, info).ConfigureAwait(false)) { var root = _jsonSerializer.DeserializeFromStream<List<ScheduleDirect.Headends>>(responce); @@ -666,58 +666,60 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings } } - private async Task<HttpResponseInfo> Post(HttpRequestOptions options, - bool enableRetry, - ListingsProviderInfo providerInfo) + private async Task<HttpResponseInfo> Post(HttpRequestOptions options, + bool enableRetry, + ListingsProviderInfo providerInfo) { try { return await _httpClient.Post(options).ConfigureAwait(false); - } - catch (HttpException ex) - { - _tokens.Clear(); - - if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500) - { - enableRetry = false; - } - - if (!enableRetry) { - throw; - } - } - - var newToken = await GetToken (providerInfo, options.CancellationToken).ConfigureAwait (false); - options.RequestHeaders ["token"] = newToken; - return await Post (options, false, providerInfo).ConfigureAwait (false); + } + catch (HttpException ex) + { + _tokens.Clear(); + + if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500) + { + enableRetry = false; + } + + if (!enableRetry) + { + throw; + } + } + + var newToken = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false); + options.RequestHeaders["token"] = newToken; + return await Post(options, false, providerInfo).ConfigureAwait(false); } - private async Task<Stream> Get(HttpRequestOptions options, - bool enableRetry, - ListingsProviderInfo providerInfo) + private async Task<Stream> Get(HttpRequestOptions options, + bool enableRetry, + ListingsProviderInfo providerInfo) { try { return await _httpClient.Get(options).ConfigureAwait(false); - } - catch (HttpException ex) - { - _tokens.Clear(); - - if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500) - { - enableRetry = false; - } - - if (!enableRetry) { - throw; - } - } - - var newToken = await GetToken (providerInfo, options.CancellationToken).ConfigureAwait (false); - options.RequestHeaders ["token"] = newToken; - return await Get (options, false, providerInfo).ConfigureAwait (false); + } + catch (HttpException ex) + { + _tokens.Clear(); + + if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500) + { + enableRetry = false; + } + + if (!enableRetry) + { + throw; + } + } + + var newToken = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false); + options.RequestHeaders["token"] = newToken; + return await Get(options, false, providerInfo).ConfigureAwait(false); } private async Task<string> GetTokenInternal(string username, string password, @@ -734,7 +736,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings //_logger.Info("Obtaining token from Schedules Direct from addres: " + httpOptions.Url + " with body " + // httpOptions.RequestContent); - using (var responce = await Post(httpOptions, false, null).ConfigureAwait(false)) + using (var responce = await Post(httpOptions, false, null).ConfigureAwait(false)) { var root = _jsonSerializer.DeserializeFromStream<ScheduleDirect.Token>(responce.Content); if (root.message == "OK") @@ -816,7 +818,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings try { - using (var response = await Get(options, false, null).ConfigureAwait(false)) + using (var response = await Get(options, false, null).ConfigureAwait(false)) { var root = _jsonSerializer.DeserializeFromStream<ScheduleDirect.Lineups>(response); @@ -869,6 +871,75 @@ namespace MediaBrowser.Server.Implementations.LiveTv.Listings return GetHeadends(info, country, location, CancellationToken.None); } + public async Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken) + { + var listingsId = info.ListingsId; + if (string.IsNullOrWhiteSpace(listingsId)) + { + throw new Exception("ListingsId required"); + } + + await AddMetadata(info, new List<ChannelInfo>(), cancellationToken).ConfigureAwait(false); + + var token = await GetToken(info, cancellationToken); + + if (string.IsNullOrWhiteSpace(token)) + { + throw new Exception("token required"); + } + + var httpOptions = new HttpRequestOptions() + { + Url = ApiUrl + "/lineups/" + listingsId, + UserAgent = UserAgent, + CancellationToken = cancellationToken, + LogErrorResponseBody = true, + // The data can be large so give it some extra time + TimeoutMs = 60000 + }; + + httpOptions.RequestHeaders["token"] = token; + + var list = new List<ChannelInfo>(); + + using (var response = await Get(httpOptions, true, info).ConfigureAwait(false)) + { + var root = _jsonSerializer.DeserializeFromStream<ScheduleDirect.Channel>(response); + _logger.Info("Found " + root.map.Count + " channels on the lineup on ScheduleDirect"); + _logger.Info("Mapping Stations to Channel"); + foreach (ScheduleDirect.Map map in root.map) + { + var channelNumber = map.logicalChannelNumber; + + if (string.IsNullOrWhiteSpace(channelNumber)) + { + channelNumber = map.channel; + } + if (string.IsNullOrWhiteSpace(channelNumber)) + { + channelNumber = map.atscMajor + "." + map.atscMinor; + } + channelNumber = channelNumber.TrimStart('0'); + + var name = channelNumber; + var station = GetStation(listingsId, channelNumber, null); + + if (station != null) + { + name = station.name; + } + + list.Add(new ChannelInfo + { + Number = channelNumber, + Name = name + }); + } + } + + return list; + } + public class ScheduleDirect { public class Token diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs deleted file mode 100644 index ac316f9a1..000000000 --- a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTv.cs +++ /dev/null @@ -1,44 +0,0 @@ -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.LiveTv; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.LiveTv.Listings -{ - public class XmlTv : IListingsProvider - { - public string Name - { - get { return "XmlTV"; } - } - - public string Type - { - get { return "xmltv"; } - } - - public Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, string channelName, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public async Task AddMetadata(ListingsProviderInfo info, List<ChannelInfo> channels, CancellationToken cancellationToken) - { - // Might not be needed - } - - public async Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings) - { - // Check that the path or url is valid. If not, throw a file not found exception - } - - public Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location) - { - // In theory this should never be called because there is always only one lineup - throw new NotImplementedException(); - } - } -} diff --git a/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs new file mode 100644 index 000000000..1e82e3fce --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -0,0 +1,219 @@ +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.LiveTv; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Emby.XmlTv.Classes; +using Emby.XmlTv.Entities; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.Server.Implementations.LiveTv.Listings +{ + public class XmlTvListingsProvider : IListingsProvider + { + private readonly IServerConfigurationManager _config; + private readonly IHttpClient _httpClient; + private readonly ILogger _logger; + + public XmlTvListingsProvider(IServerConfigurationManager config, IHttpClient httpClient, ILogger logger) + { + _config = config; + _httpClient = httpClient; + _logger = logger; + } + + public string Name + { + get { return "XmlTV"; } + } + + public string Type + { + get { return "xmltv"; } + } + + private string GetLanguage() + { + return _config.Configuration.PreferredMetadataLanguage; + } + + private async Task<string> GetXml(string path, CancellationToken cancellationToken) + { + _logger.Info("xmltv path: {0}", path); + + if (!path.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + { + return path; + } + + var cacheFilename = DateTime.UtcNow.DayOfYear.ToString(CultureInfo.InvariantCulture) + "-" + DateTime.UtcNow.Hour.ToString(CultureInfo.InvariantCulture) + ".xml"; + var cacheFile = Path.Combine(_config.ApplicationPaths.CachePath, "xmltv", cacheFilename); + if (File.Exists(cacheFile)) + { + return cacheFile; + } + + _logger.Info("Downloading xmltv listings from {0}", path); + + var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = path, + Progress = new Progress<Double>(), + DecompressionMethod = DecompressionMethods.GZip, + + // It's going to come back gzipped regardless of this value + // So we need to make sure the decompression method is set to gzip + EnableHttpCompression = true + + }).ConfigureAwait(false); + + Directory.CreateDirectory(Path.GetDirectoryName(cacheFile)); + + using (var stream = File.OpenRead(tempFile)) + { + using (var reader = new StreamReader(stream, Encoding.UTF8)) + { + using (var fileStream = File.OpenWrite(cacheFile)) + { + using (var writer = new StreamWriter(fileStream)) + { + while (!reader.EndOfStream) + { + writer.WriteLine(reader.ReadLine()); + } + } + } + } + } + + _logger.Debug("Returning xmltv path {0}", cacheFile); + return cacheFile; + } + + public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelNumber, string channelName, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) + { + if (!await EmbyTV.EmbyTVRegistration.Instance.EnableXmlTv().ConfigureAwait(false)) + { + var length = endDateUtc - startDateUtc; + if (length.TotalDays > 1) + { + endDateUtc = startDateUtc.AddDays(1); + } + } + + var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false); + var reader = new XmlTvReader(path, GetLanguage(), null); + + var results = reader.GetProgrammes(channelNumber, startDateUtc, endDateUtc, cancellationToken); + return results.Select(p => new ProgramInfo() + { + ChannelId = p.ChannelId, + EndDate = GetDate(p.EndDate), + EpisodeNumber = p.Episode == null ? null : p.Episode.Episode, + EpisodeTitle = p.Episode == null ? null : p.Episode.Title, + Genres = p.Categories, + Id = String.Format("{0}_{1:O}", p.ChannelId, p.StartDate), // Construct an id from the channel and start date, + StartDate = GetDate(p.StartDate), + Name = p.Title, + Overview = p.Description, + ShortOverview = p.Description, + ProductionYear = !p.CopyrightDate.HasValue ? (int?)null : p.CopyrightDate.Value.Year, + SeasonNumber = p.Episode == null ? null : p.Episode.Series, + IsSeries = p.Episode != null, + IsRepeat = p.IsRepeat, + IsPremiere = p.Premiere != null, + IsKids = p.Categories.Any(c => info.KidsCategories.Contains(c, StringComparer.InvariantCultureIgnoreCase)), + IsMovie = p.Categories.Any(c => info.MovieCategories.Contains(c, StringComparer.InvariantCultureIgnoreCase)), + IsNews = p.Categories.Any(c => info.NewsCategories.Contains(c, StringComparer.InvariantCultureIgnoreCase)), + IsSports = p.Categories.Any(c => info.SportsCategories.Contains(c, StringComparer.InvariantCultureIgnoreCase)), + ImageUrl = p.Icon != null && !String.IsNullOrEmpty(p.Icon.Source) ? p.Icon.Source : null, + HasImage = p.Icon != null && !String.IsNullOrEmpty(p.Icon.Source), + 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 + }); + } + + private DateTime GetDate(DateTime date) + { + if (date.Kind != DateTimeKind.Utc) + { + date = DateTime.SpecifyKind(date, DateTimeKind.Utc); + } + return date; + } + + public async Task AddMetadata(ListingsProviderInfo info, List<ChannelInfo> channels, CancellationToken cancellationToken) + { + // Add the channel image url + var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false); + var reader = new XmlTvReader(path, GetLanguage(), null); + var results = reader.GetChannels().ToList(); + + if (channels != null) + { + channels.ForEach(c => + { + var channelNumber = info.GetMappedChannel(c.Number); + var match = results.FirstOrDefault(r => string.Equals(r.Id, channelNumber, StringComparison.OrdinalIgnoreCase)); + + if (match != null && match.Icon != null && !String.IsNullOrEmpty(match.Icon.Source)) + { + c.ImageUrl = match.Icon.Source; + } + }); + } + } + + public Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings) + { + // Assume all urls are valid. check files for existence + if (!info.Path.StartsWith("http", StringComparison.OrdinalIgnoreCase) && !File.Exists(info.Path)) + { + throw new FileNotFoundException("Could not find the XmlTv file specified:", info.Path); + } + + return Task.FromResult(true); + } + + public async Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location) + { + // In theory this should never be called because there is always only one lineup + var path = await GetXml(info.Path, CancellationToken.None).ConfigureAwait(false); + var reader = new XmlTvReader(path, GetLanguage(), null); + var results = reader.GetChannels(); + + // Should this method be async? + return results.Select(c => new NameIdPair() { Id = c.Id, Name = c.DisplayName }).ToList(); + } + + public async Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken) + { + // In theory this should never be called because there is always only one lineup + var path = await GetXml(info.Path, cancellationToken).ConfigureAwait(false); + var reader = new XmlTvReader(path, GetLanguage(), null); + var results = reader.GetChannels(); + + // Should this method be async? + return results.Select(c => new ChannelInfo() + { + Id = c.Id, + Name = c.DisplayName, + ImageUrl = c.Icon != null && !String.IsNullOrEmpty(c.Icon.Source) ? c.Icon.Source : null, + Number = c.Id + + }).ToList(); + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs index 3849f44ab..64af35a9a 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvManager.cs @@ -30,6 +30,8 @@ using System.Threading.Tasks; using CommonIO; using IniParser; using IniParser.Model; +using MediaBrowser.Common.Events; +using MediaBrowser.Model.Events; namespace MediaBrowser.Server.Implementations.LiveTv { @@ -64,6 +66,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv private readonly List<IListingsProvider> _listingProviders = new List<IListingsProvider>(); private readonly IFileSystem _fileSystem; + public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled; + public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled; + public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated; + public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated; + public LiveTvManager(IApplicationHost appHost, IServerConfigurationManager config, ILogger logger, IItemRepository itemRepo, IImageProcessor imageProcessor, IUserDataManager userDataManager, IDtoService dtoService, IUserManager userManager, ILibraryManager libraryManager, ITaskManager taskManager, ILocalizationManager localization, IJsonSerializer jsonSerializer, IProviderManager providerManager, IFileSystem fileSystem) { _config = config; @@ -133,9 +140,13 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId); + var topFolder = await GetInternalLiveTvFolder(cancellationToken).ConfigureAwait(false); + var channels = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { typeof(LiveTvChannel).Name } + IncludeItemTypes = new[] { typeof(LiveTvChannel).Name }, + SortBy = new[] { ItemSortBy.SortName }, + TopParentIds = new[] { topFolder.Id.ToString("N") } }).Cast<LiveTvChannel>(); @@ -164,7 +175,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var val = query.IsFavorite.Value; channels = channels - .Where(i => _userDataManager.GetUserData(user.Id, i.GetUserDataKey()).IsFavorite == val); + .Where(i => _userDataManager.GetUserData(user, i).IsFavorite == val); } if (query.IsLiked.HasValue) @@ -174,7 +185,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv channels = channels .Where(i => { - var likes = _userDataManager.GetUserData(user.Id, i.GetUserDataKey()).Likes; + var likes = _userDataManager.GetUserData(user, i).Likes; return likes.HasValue && likes.Value == val; }); @@ -187,7 +198,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv channels = channels .Where(i => { - var likes = _userDataManager.GetUserData(user.Id, i.GetUserDataKey()).Likes; + var likes = _userDataManager.GetUserData(user, i).Likes; return likes.HasValue && likes.Value != val; }); @@ -200,7 +211,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv { if (enableFavoriteSorting) { - var userData = _userDataManager.GetUserData(user.Id, i.GetUserDataKey()); + var userData = _userDataManager.GetUserData(user, i); if (userData.IsFavorite) { @@ -511,14 +522,22 @@ namespace MediaBrowser.Server.Implementations.LiveTv if (!(service is EmbyTV.EmbyTV)) { // We can't trust that we'll be able to direct stream it through emby server, no matter what the provider says - mediaSource.SupportsDirectStream = true; + mediaSource.SupportsDirectStream = false; mediaSource.SupportsTranscoding = true; + foreach (var stream in mediaSource.MediaStreams) + { + if (stream.Type == MediaStreamType.Video && string.IsNullOrWhiteSpace(stream.NalLengthSize)) + { + stream.NalLengthSize = "0"; + } + } } } private async Task<LiveTvChannel> GetChannel(ChannelInfo channelInfo, string serviceName, Guid parentFolderId, CancellationToken cancellationToken) { var isNew = false; + var forceUpdate = false; var id = _tvDtoService.GetInternalChannelId(serviceName, channelInfo.Id); @@ -568,10 +587,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv if (!string.IsNullOrWhiteSpace(channelInfo.ImagePath)) { item.SetImagePath(ImageType.Primary, channelInfo.ImagePath); + forceUpdate = true; } else if (!string.IsNullOrWhiteSpace(channelInfo.ImageUrl)) { item.SetImagePath(ImageType.Primary, channelInfo.ImageUrl); + forceUpdate = true; } } @@ -580,9 +601,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv item.Name = channelInfo.Name; } + if (isNew) + { + await _libraryManager.CreateItem(item, cancellationToken).ConfigureAwait(false); + } + else if (forceUpdate) + { + await _libraryManager.UpdateItem(item, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); + } + await item.RefreshMetadata(new MetadataRefreshOptions(_fileSystem) { - ForceSave = isNew + ForceSave = isNew || forceUpdate }, cancellationToken); @@ -626,7 +656,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv item.Audio = info.Audio; item.ChannelId = channel.Id.ToString("N"); item.CommunityRating = item.CommunityRating ?? info.CommunityRating; - item.EndDate = info.EndDate; + item.EpisodeTitle = info.EpisodeTitle; item.ExternalId = info.Id; item.Genres = info.Genres; @@ -643,7 +673,19 @@ namespace MediaBrowser.Server.Implementations.LiveTv item.OfficialRating = item.OfficialRating ?? info.OfficialRating; item.Overview = item.Overview ?? info.Overview; item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks; + + if (item.StartDate != info.StartDate) + { + forceUpdate = true; + } item.StartDate = info.StartDate; + + if (item.EndDate != info.EndDate) + { + forceUpdate = true; + } + item.EndDate = info.EndDate; + item.HomePageUrl = info.HomePageUrl; item.ProductionYear = info.ProductionYear; @@ -864,6 +906,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId); + var topFolder = await GetInternalLiveTvFolder(cancellationToken).ConfigureAwait(false); + + if (query.SortBy.Length == 0) + { + // Unless something else was specified, order by start date to take advantage of a specialized index + query.SortBy = new[] { ItemSortBy.StartDate }; + } + var internalQuery = new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, @@ -879,7 +929,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv StartIndex = query.StartIndex, Limit = query.Limit, SortBy = query.SortBy, - SortOrder = query.SortOrder ?? SortOrder.Ascending + SortOrder = query.SortOrder ?? SortOrder.Ascending, + EnableTotalRecordCount = query.EnableTotalRecordCount, + TopParentIds = new[] { topFolder.Id.ToString("N") } }; if (query.HasAired.HasValue) @@ -896,7 +948,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var queryResult = _libraryManager.QueryItems(internalQuery); - var returnArray = _dtoService.GetBaseItemDtos(queryResult.Items, options, user).ToArray(); + var returnArray = (await _dtoService.GetBaseItemDtos(queryResult.Items, options, user).ConfigureAwait(false)).ToArray(); var result = new QueryResult<BaseItemDto> { @@ -911,15 +963,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv { var user = _userManager.GetUserById(query.UserId); + var topFolder = await GetInternalLiveTvFolder(cancellationToken).ConfigureAwait(false); + var internalQuery = new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, IsAiring = query.IsAiring, IsMovie = query.IsMovie, IsSports = query.IsSports, - IsKids = query.IsKids + IsKids = query.IsKids, + EnableTotalRecordCount = query.EnableTotalRecordCount, + SortBy = new[] { ItemSortBy.StartDate }, + TopParentIds = new[] { topFolder.Id.ToString("N") } }; + if (query.Limit.HasValue) + { + internalQuery.Limit = Math.Max(query.Limit.Value * 4, 200); + } + if (query.HasAired.HasValue) { if (query.HasAired.Value) @@ -936,15 +998,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv var programList = programs.ToList(); - var genres = programList.SelectMany(i => i.Genres) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .DistinctNames() - .Select(i => _libraryManager.GetGenre(i)) - .DistinctBy(i => i.Id) - .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase); + var factorChannelWatchCount = (query.IsAiring ?? false) || (query.IsKids ?? false) || (query.IsSports ?? false) || (query.IsMovie ?? false); programs = programList.OrderBy(i => i.HasImage(ImageType.Primary) ? 0 : 1) - .ThenByDescending(i => GetRecommendationScore(i, user.Id, genres)) + .ThenByDescending(i => GetRecommendationScore(i, user.Id, factorChannelWatchCount)) .ThenBy(i => i.StartDate); if (query.Limit.HasValue) @@ -971,7 +1028,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var user = _userManager.GetUserById(query.UserId); - var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user).ToArray(); + var returnArray = (await _dtoService.GetBaseItemDtos(internalResult.Items, options, user).ConfigureAwait(false)).ToArray(); var result = new QueryResult<BaseItemDto> { @@ -982,7 +1039,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv return result; } - private int GetRecommendationScore(LiveTvProgram program, Guid userId, Dictionary<string, Genre> genres) + private int GetRecommendationScore(LiveTvProgram program, Guid userId, bool factorChannelWatchCount) { var score = 0; @@ -998,7 +1055,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var channel = GetInternalChannel(program.ChannelId); - var channelUserdata = _userDataManager.GetUserData(userId, channel.GetUserDataKey()); + var channelUserdata = _userDataManager.GetUserData(userId, channel); if (channelUserdata.Likes ?? false) { @@ -1014,41 +1071,12 @@ namespace MediaBrowser.Server.Implementations.LiveTv score += 3; } - score += GetGenreScore(program.Genres, userId, genres); - - return score; - } - - private int GetGenreScore(IEnumerable<string> programGenres, Guid userId, Dictionary<string, Genre> genres) - { - return programGenres.Select(i => + if (factorChannelWatchCount) { - var score = 0; - - Genre genre; - - if (genres.TryGetValue(i, out genre)) - { - var genreUserdata = _userDataManager.GetUserData(userId, genre.GetUserDataKey()); - - if (genreUserdata.Likes ?? false) - { - score++; - } - else if (!(genreUserdata.Likes ?? true)) - { - score--; - } - - if (genreUserdata.IsFavorite) - { - score += 2; - } - } - - return score; + score += channelUserdata.PlayCount; + } - }).Sum(); + return score; } private async Task AddRecordingInfo(IEnumerable<Tuple<BaseItemDto, string, string>> programs, CancellationToken cancellationToken) @@ -1104,6 +1132,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv private async Task RefreshChannelsInternal(IProgress<double> progress, CancellationToken cancellationToken) { + EmbyTV.EmbyTV.Current.CreateRecordingFolders(); + var numComplete = 0; double progressPerService = _services.Count == 0 ? 0 @@ -1184,8 +1214,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv var item = await GetChannel(channelInfo.Item2, channelInfo.Item1, parentFolderId, cancellationToken).ConfigureAwait(false); list.Add(item); - - _libraryManager.RegisterItem(item); } catch (OperationCanceledException) { @@ -1256,11 +1284,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv private async Task CleanDatabaseInternal(List<Guid> currentIdList, string[] validTypes, IProgress<double> progress, CancellationToken cancellationToken) { - var list = _itemRepo.GetItemIds(new InternalItemsQuery + var list = _itemRepo.GetItemIdsList(new InternalItemsQuery { IncludeItemTypes = validTypes - }).Items.ToList(); + }).ToList(); var numComplete = 0; @@ -1373,6 +1401,40 @@ namespace MediaBrowser.Server.Implementations.LiveTv } } + private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, User user) + { + if (user == null || (query.IsInProgress ?? false)) + { + return new QueryResult<BaseItem>(); + } + + var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders() + .SelectMany(i => i.Locations) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Select(i => _libraryManager.FindByPath(i, true)) + .Where(i => i != null) + .Where(i => i.IsVisibleStandalone(user)) + .ToList(); + + if (folders.Count == 0) + { + return new QueryResult<BaseItem>(); + } + + return _libraryManager.GetItemsResult(new InternalItemsQuery(user) + { + MediaTypes = new[] { MediaType.Video }, + Recursive = true, + AncestorIds = folders.Select(i => i.Id.ToString("N")).ToArray(), + IsFolder = false, + ExcludeLocationTypes = new[] { LocationType.Virtual }, + Limit = Math.Min(200, query.Limit ?? int.MaxValue), + SortBy = new[] { ItemSortBy.DateCreated }, + SortOrder = SortOrder.Descending, + EnableTotalRecordCount = query.EnableTotalRecordCount + }); + } + public async Task<QueryResult<BaseItem>> GetInternalRecordings(RecordingQuery query, CancellationToken cancellationToken) { var user = string.IsNullOrEmpty(query.UserId) ? null : _userManager.GetUserById(query.UserId); @@ -1381,6 +1443,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv return new QueryResult<BaseItem>(); } + if (_services.Count == 1) + { + return GetEmbyRecordings(query, user); + } + await RefreshRecordings(cancellationToken).ConfigureAwait(false); var internalQuery = new InternalItemsQuery(user) @@ -1393,7 +1460,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv internalQuery.ChannelIds = new[] { query.ChannelId }; } - var queryResult = _libraryManager.GetItemList(internalQuery, new string[] { }); + var queryResult = _libraryManager.GetItemList(internalQuery); IEnumerable<ILiveTvRecording> recordings = queryResult.Cast<ILiveTvRecording>(); if (!string.IsNullOrWhiteSpace(query.Id)) @@ -1506,6 +1573,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv { dto.ChannelName = channel.Name; dto.MediaType = channel.MediaType; + dto.ChannelNumber = channel.Number; if (channel.HasImage(ImageType.Primary)) { @@ -1596,7 +1664,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var internalResult = await GetInternalRecordings(query, cancellationToken).ConfigureAwait(false); - var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user).ToArray(); + var returnArray = (await _dtoService.GetBaseItemDtos(internalResult.Items, options, user).ConfigureAwait(false)).ToArray(); return new QueryResult<BaseItemDto> { @@ -1623,6 +1691,18 @@ namespace MediaBrowser.Server.Implementations.LiveTv var results = await Task.WhenAll(tasks).ConfigureAwait(false); var timers = results.SelectMany(i => i.ToList()); + if (query.IsActive.HasValue) + { + if (query.IsActive.Value) + { + timers = timers.Where(i => i.Item1.Status == RecordingStatus.InProgress); + } + else + { + timers = timers.Where(i => i.Item1.Status != RecordingStatus.InProgress); + } + } + if (!string.IsNullOrEmpty(query.ChannelId)) { var guid = new Guid(query.ChannelId); @@ -1724,6 +1804,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv await service.CancelTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); _lastRecordingRefreshTime = DateTime.MinValue; + + EventHelper.QueueEventIfNotNull(TimerCancelled, this, new GenericEventArgs<TimerEventInfo> + { + Argument = new TimerEventInfo + { + Id = id + } + }, _logger); } public async Task CancelSeriesTimer(string id) @@ -1739,6 +1827,14 @@ namespace MediaBrowser.Server.Implementations.LiveTv await service.CancelSeriesTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); _lastRecordingRefreshTime = DateTime.MinValue; + + EventHelper.QueueEventIfNotNull(SeriesTimerCancelled, this, new GenericEventArgs<TimerEventInfo> + { + Argument = new TimerEventInfo + { + Id = id + } + }, _logger); } public async Task<BaseItemDto> GetRecording(string id, DtoOptions options, CancellationToken cancellationToken, User user = null) @@ -1835,9 +1931,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv MaxStartDate = now, MinEndDate = now, Limit = channelIds.Length, - SortBy = new[] { "StartDate" } + SortBy = new[] { "StartDate" }, + TopParentIds = new[] { GetInternalLiveTvFolder(CancellationToken.None).Result.Id.ToString("N") } - }, new string[] { }).ToList(); + }).ToList(); foreach (var tuple in tuples) { @@ -1845,6 +1942,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var channel = tuple.Item2; dto.Number = channel.Number; + dto.ChannelNumber = channel.Number; dto.ChannelType = channel.ChannelType; dto.ServiceName = GetService(channel).Name; @@ -1923,10 +2021,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv var defaults = await GetNewTimerDefaultsInternal(cancellationToken, program).ConfigureAwait(false); var info = _tvDtoService.GetSeriesTimerInfoDto(defaults.Item1, defaults.Item2, null); - info.Days = new List<DayOfWeek> - { - program.StartDate.ToLocalTime().DayOfWeek - }; + info.Days = defaults.Item1.Days; info.DayPattern = _tvDtoService.GetDayPattern(info.Days); @@ -1957,9 +2052,29 @@ namespace MediaBrowser.Server.Implementations.LiveTv var defaultValues = await service.GetNewTimerDefaultsAsync(cancellationToken).ConfigureAwait(false); info.Priority = defaultValues.Priority; - await service.CreateTimerAsync(info, cancellationToken).ConfigureAwait(false); + string newTimerId = null; + var supportsNewTimerIds = service as ISupportsNewTimerIds; + if (supportsNewTimerIds != null) + { + newTimerId = await supportsNewTimerIds.CreateTimer(info, cancellationToken).ConfigureAwait(false); + newTimerId = _tvDtoService.GetInternalTimerId(timer.ServiceName, newTimerId).ToString("N"); + } + else + { + await service.CreateTimerAsync(info, cancellationToken).ConfigureAwait(false); + } + _lastRecordingRefreshTime = DateTime.MinValue; _logger.Info("New recording scheduled"); + + EventHelper.QueueEventIfNotNull(TimerCreated, this, new GenericEventArgs<TimerEventInfo> + { + Argument = new TimerEventInfo + { + ProgramId = _tvDtoService.GetInternalProgramId(timer.ServiceName, info.ProgramId).ToString("N"), + Id = newTimerId + } + }, _logger); } public async Task CreateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken) @@ -1972,8 +2087,28 @@ namespace MediaBrowser.Server.Implementations.LiveTv var defaultValues = await service.GetNewTimerDefaultsAsync(cancellationToken).ConfigureAwait(false); info.Priority = defaultValues.Priority; - await service.CreateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false); + string newTimerId = null; + var supportsNewTimerIds = service as ISupportsNewTimerIds; + if (supportsNewTimerIds != null) + { + newTimerId = await supportsNewTimerIds.CreateSeriesTimer(info, cancellationToken).ConfigureAwait(false); + newTimerId = _tvDtoService.GetInternalSeriesTimerId(timer.ServiceName, newTimerId).ToString("N"); + } + else + { + await service.CreateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false); + } + _lastRecordingRefreshTime = DateTime.MinValue; + + EventHelper.QueueEventIfNotNull(SeriesTimerCreated, this, new GenericEventArgs<TimerEventInfo> + { + Argument = new TimerEventInfo + { + ProgramId = _tvDtoService.GetInternalProgramId(timer.ServiceName, info.ProgramId).ToString("N"), + Id = newTimerId + } + }, _logger); } public async Task UpdateTimer(TimerInfoDto timer, CancellationToken cancellationToken) @@ -2048,7 +2183,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv }, cancellationToken).ConfigureAwait(false); - var recordings = recordingResult.Items.Cast<ILiveTvRecording>().ToList(); + var recordings = recordingResult.Items.OfType<ILiveTvRecording>().ToList(); var groups = new List<BaseItemDto>(); @@ -2389,6 +2524,79 @@ namespace MediaBrowser.Server.Implementations.LiveTv return info; } + public void DeleteListingsProvider(string id) + { + var config = GetConfiguration(); + + config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToList(); + + _config.SaveConfiguration("livetv", config); + _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>(); + } + + public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber) + { + var config = GetConfiguration(); + + var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase)); + listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray(); + + if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase)) + { + var list = listingsProviderInfo.ChannelMappings.ToList(); + list.Add(new NameValuePair + { + Name = tunerChannelNumber, + Value = providerChannelNumber + }); + listingsProviderInfo.ChannelMappings = list.ToArray(); + } + + _config.SaveConfiguration("livetv", config); + + var tunerChannels = await GetChannelsForListingsProvider(providerId, CancellationToken.None) + .ConfigureAwait(false); + + var providerChannels = await GetChannelsFromListingsProviderData(providerId, CancellationToken.None) + .ConfigureAwait(false); + + var mappings = listingsProviderInfo.ChannelMappings.ToList(); + + var tunerChannelMappings = + tunerChannels.Select(i => GetTunerChannelMapping(i, mappings, providerChannels)).ToList(); + + _taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>(); + + return tunerChannelMappings.First(i => string.Equals(i.Number, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)); + } + + public TunerChannelMapping GetTunerChannelMapping(ChannelInfo channel, List<NameValuePair> mappings, List<ChannelInfo> providerChannels) + { + var result = new TunerChannelMapping + { + Name = channel.Number + " " + channel.Name, + Number = channel.Number + }; + + var mapping = mappings.FirstOrDefault(i => string.Equals(i.Name, channel.Number, StringComparison.OrdinalIgnoreCase)); + var providerChannelNumber = channel.Number; + + if (mapping != null) + { + providerChannelNumber = mapping.Value; + } + + var providerChannel = providerChannels.FirstOrDefault(i => string.Equals(i.Number, providerChannelNumber, StringComparison.OrdinalIgnoreCase)); + + if (providerChannel != null) + { + result.ProviderChannelNumber = providerChannel.Number; + result.ProviderChannelName = providerChannel.Name; + } + + return result; + } + public Task<List<NameIdPair>> GetLineups(string providerType, string providerId, string country, string location) { var config = GetConfiguration(); @@ -2450,7 +2658,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv public List<NameValuePair> GetSatIniMappings() { - var names = GetType().Assembly.GetManifestResourceNames().Where(i => i.IndexOf("SatIp.ini.satellite", StringComparison.OrdinalIgnoreCase) != -1).ToList(); + var names = GetType().Assembly.GetManifestResourceNames().Where(i => i.IndexOf("SatIp.ini", StringComparison.OrdinalIgnoreCase) != -1).ToList(); return names.Select(GetSatIniMappings).Where(i => i != null).DistinctBy(i => i.Value.Split('|')[0]).ToList(); } @@ -2472,13 +2680,34 @@ namespace MediaBrowser.Server.Implementations.LiveTv return null; } + var srch = "SatIp.ini."; + var filename = Path.GetFileName(resource); + return new NameValuePair { Name = satType1 + " " + satType2, - Value = satType2 + "|" + Path.GetFileName(resource) + Value = satType2 + "|" + filename.Substring(filename.IndexOf(srch) + srch.Length) }; } } } + + public Task<List<ChannelInfo>> GetSatChannelScanResult(TunerHostInfo info, CancellationToken cancellationToken) + { + return new TunerHosts.SatIp.ChannelScan(_logger).Scan(info, cancellationToken); + } + + public Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken) + { + var info = GetConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); + return EmbyTV.EmbyTV.Current.GetChannelsForListingsProvider(info, cancellationToken); + } + + public Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken) + { + var info = GetConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); + var provider = _listingProviders.First(i => string.Equals(i.Type, info.Type, StringComparison.OrdinalIgnoreCase)); + return provider.GetChannels(info, cancellationToken); + } } }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index d3bb87bc7..cdba1873e 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -83,7 +83,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv } var list = sources.ToList(); - var serverUrl = _appHost.LocalApiUrl; + var serverUrl = await _appHost.GetLocalApiUrl().ConfigureAwait(false); foreach (var source in list) { diff --git a/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs index ab8ec720b..3f0538bd0 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/ProgramImageProvider.cs @@ -74,7 +74,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv } } - public bool HasChanged(IHasMetadata item, MetadataStatus status, IDirectoryService directoryService) + public bool HasChanged(IHasMetadata item, IDirectoryService directoryService) { var liveTvItem = item as LiveTvProgram; diff --git a/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs b/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs index fce3223ea..25678c29d 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/RecordingImageProvider.cs @@ -68,7 +68,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv get { return 0; } } - public bool HasChanged(IHasMetadata item, MetadataStatus status, IDirectoryService directoryService) + public bool HasChanged(IHasMetadata item, IDirectoryService directoryService) { var liveTvItem = item as ILiveTvRecording; diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 02a8d6938..9bb5b4fd7 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -224,7 +224,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts var stream = await GetChannelStream(host, channelId, streamId, cancellationToken).ConfigureAwait(false); - //await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false); + if (EnableMediaProbing) + { + await AddMediaInfo(stream, false, resourcePool, cancellationToken).ConfigureAwait(false); + } + return new Tuple<MediaSourceInfo, SemaphoreSlim>(stream, resourcePool); } catch (Exception ex) @@ -239,6 +243,11 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts throw new LiveTvConflictException(); } + protected virtual bool EnableMediaProbing + { + get { return false; } + } + protected async Task<bool> IsAvailable(TunerHostInfo tuner, string channelId, CancellationToken cancellationToken) { try @@ -268,6 +277,25 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts 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; diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index db7f6f86c..69b6fb5a9 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -17,6 +17,7 @@ using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Net; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun { @@ -59,7 +60,15 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun return id; } - protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken) + 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 { @@ -68,45 +77,61 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun }; using (var stream = await _httpClient.Get(options)) { - var root = JsonSerializer.DeserializeFromStream<List<Channels>>(stream); + var lineup = JsonSerializer.DeserializeFromStream<List<Channels>>(stream) ?? new List<Channels>(); - if (root != null) + if (info.ImportFavoritesOnly) { - var result = root.Select(i => new ChannelInfo - { - Name = i.GuideName, - Number = i.GuideNumber.ToString(CultureInfo.InvariantCulture), - Id = GetChannelId(info, i), - IsFavorite = i.Favorite, - TunerHostId = info.Id + lineup = lineup.Where(i => i.Favorite).ToList(); + } - }); + return lineup.Where(i => !i.DRM).ToList(); + } + } - if (info.ImportFavoritesOnly) - { - result = result.Where(i => i.IsFavorite ?? true).ToList(); - } + protected override async Task<IEnumerable<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken) + { + var lineup = await GetLineup(info, cancellationToken).ConfigureAwait(false); - return result; - } - return new List<ChannelInfo>(); - } + return lineup.Select(i => new ChannelInfo + { + Name = i.GuideName, + Number = i.GuideNumber.ToString(CultureInfo.InvariantCulture), + Id = GetChannelId(info, i), + IsFavorite = i.Favorite, + TunerHostId = info.Id, + IsHD = i.HD == 1, + AudioCodec = i.AudioCodec, + VideoCodec = i.VideoCodec + }); } private async Task<string> GetModelInfo(TunerHostInfo info, CancellationToken cancellationToken) { - using (var stream = await _httpClient.Get(new HttpRequestOptions() + try { - Url = string.Format("{0}/discover.json", GetApiUrl(info, false)), - CancellationToken = cancellationToken, - CacheLength = TimeSpan.FromDays(1), - CacheMode = CacheMode.Unconditional, - TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds) - })) + using (var stream = await _httpClient.Get(new HttpRequestOptions() + { + Url = string.Format("{0}/discover.json", GetApiUrl(info, false)), + CancellationToken = cancellationToken, + CacheLength = TimeSpan.FromDays(1), + CacheMode = CacheMode.Unconditional, + TimeoutMs = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds) + })) + { + var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream); + + return response.ModelNumber; + } + } + catch (HttpException ex) { - var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream); + if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound) + { + // HDHR4 doesn't have this api + return "HDHR"; + } - return response.ModelNumber; + throw; } } @@ -226,19 +251,24 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun { public string GuideNumber { get; set; } public string GuideName { get; set; } + public string VideoCodec { get; set; } + public string AudioCodec { get; set; } public string URL { get; set; } public bool Favorite { get; set; } public bool DRM { get; set; } + public int HD { get; set; } } - private MediaSourceInfo GetMediaSource(TunerHostInfo info, string channelId, string profile) + private async Task<MediaSourceInfo> GetMediaSource(TunerHostInfo info, string channelId, string profile) { int? width = null; int? height = null; bool isInterlaced = true; - var videoCodec = !string.IsNullOrWhiteSpace(GetEncodingOptions().HardwareAccelerationType) ? null : "mpeg2video"; + string videoCodec = null; + string audioCodec = "ac3"; int? videoBitrate = null; + int? audioBitrate = null; if (string.Equals(profile, "mobile", StringComparison.OrdinalIgnoreCase)) { @@ -256,18 +286,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun videoCodec = "h264"; videoBitrate = 15000000; } - else if (string.Equals(profile, "internet720", StringComparison.OrdinalIgnoreCase)) - { - width = 1280; - height = 720; - isInterlaced = false; - videoCodec = "h264"; - videoBitrate = 8000000; - } else if (string.Equals(profile, "internet540", StringComparison.OrdinalIgnoreCase)) { - width = 1280; - height = 720; + width = 960; + height = 546; isInterlaced = false; videoCodec = "h264"; videoBitrate = 2500000; @@ -297,6 +319,32 @@ 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) + { + videoCodec = channel.VideoCodec; + audioCodec = channel.AudioCodec; + + videoBitrate = (channel.IsHD ?? true) ? 15000000 : 2000000; + audioBitrate = (channel.IsHD ?? true) ? 448000 : 192000; + } + } + + // normalize + if (string.Equals(videoCodec, "mpeg2", StringComparison.OrdinalIgnoreCase)) + { + videoCodec = "mpeg2video"; + } + + string nal = null; + if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) + { + nal = "0"; + } + var url = GetApiUrl(info, true) + "/auto/v" + channelId; if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase)) @@ -319,16 +367,17 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun Codec = videoCodec, Width = width, Height = height, - BitRate = videoBitrate - + BitRate = videoBitrate, + NalLengthSize = nal + }, new MediaStream { Type = MediaStreamType.Audio, // Set the index to -1 because we don't know the exact index of the audio stream within the container Index = -1, - Codec = "ac3", - BitRate = 192000 + Codec = audioCodec, + BitRate = audioBitrate } }, RequiresOpening = false, @@ -337,7 +386,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun Container = "ts", Id = profile, SupportsDirectPlay = true, - SupportsDirectStream = true, + SupportsDirectStream = false, SupportsTranscoding = true }; @@ -364,21 +413,22 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun } var hdhrId = GetHdHrIdFromChannelId(channelId); - list.Add(GetMediaSource(info, hdhrId, "native")); + list.Add(await GetMediaSource(info, hdhrId, "native").ConfigureAwait(false)); try { string model = await GetModelInfo(info, cancellationToken).ConfigureAwait(false); model = model ?? string.Empty; - if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1) + if (info.AllowHWTranscoding && (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)) { - list.Insert(0, GetMediaSource(info, hdhrId, "heavy")); + list.Add(await GetMediaSource(info, hdhrId, "heavy").ConfigureAwait(false)); - list.Add(GetMediaSource(info, hdhrId, "internet480")); - list.Add(GetMediaSource(info, hdhrId, "internet360")); - list.Add(GetMediaSource(info, hdhrId, "internet240")); - list.Add(GetMediaSource(info, hdhrId, "mobile")); + 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 (Exception ex) @@ -409,7 +459,7 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun } var hdhrId = GetHdHrIdFromChannelId(channelId); - return GetMediaSource(info, hdhrId, streamId); + return await GetMediaSource(info, hdhrId, streamId).ConfigureAwait(false); } public async Task Validate(TunerHostInfo info) @@ -419,16 +469,29 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.HdHomerun return; } - // Test it by pulling down the lineup - using (var stream = await _httpClient.Get(new HttpRequestOptions + try { - Url = string.Format("{0}/discover.json", GetApiUrl(info, false)), - CancellationToken = CancellationToken.None - })) + // 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 + })) + { + var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream); + + info.DeviceId = response.DeviceID; + } + } + catch (HttpException ex) { - var response = JsonSerializer.DeserializeFromStream<DiscoverResponse>(stream); + if (ex.StatusCode.HasValue && ex.StatusCode.Value == System.Net.HttpStatusCode.NotFound) + { + // HDHR4 doesn't have this api + return; + } - info.DeviceId = response.DeviceID; + throw; } } diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 523f14dfc..2a974b545 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -134,7 +134,9 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts } }, RequiresOpening = false, - RequiresClosing = false + RequiresClosing = false, + + ReadAtNativeFramerate = true }; return new List<MediaSourceInfo> { mediaSource }; @@ -146,5 +148,10 @@ 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/SatIp/ChannelScan.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/ChannelScan.cs new file mode 100644 index 000000000..fdeae25b0 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/ChannelScan.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using IniParser; +using IniParser.Model; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Logging; +using MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp +{ + public class ChannelScan + { + private readonly ILogger _logger; + + public ChannelScan(ILogger logger) + { + _logger = logger; + } + + public async Task<List<ChannelInfo>> Scan(TunerHostInfo info, CancellationToken cancellationToken) + { + var ini = info.SourceA.Split('|')[1]; + var resource = GetType().Assembly.GetManifestResourceNames().FirstOrDefault(i => i.EndsWith(ini, StringComparison.OrdinalIgnoreCase)); + + _logger.Info("Opening ini file {0}", resource); + var list = new List<ChannelInfo>(); + + using (var stream = GetType().Assembly.GetManifestResourceStream(resource)) + { + using (var reader = new StreamReader(stream)) + { + var parser = new StreamIniDataParser(); + var data = parser.ReadData(reader); + + var count = GetInt(data, "DVB", "0", 0); + + _logger.Info("DVB Count: {0}", count); + + var index = 1; + var source = "1"; + + while (index <= count) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (var rtspSession = new RtspSession(info.Url, _logger)) + { + float percent = count == 0 ? 0 : (float)(index) / count; + percent = Math.Max(percent * 100, 100); + + //SetControlPropertyThreadSafe(pgbSearchResult, "Value", (int)percent); + var strArray = data["DVB"][index.ToString(CultureInfo.InvariantCulture)].Split(','); + + string tuning; + if (strArray[4] == "S2") + { + tuning = string.Format("src={0}&freq={1}&pol={2}&sr={3}&fec={4}&msys=dvbs2&mtype={5}&plts=on&ro=0.35&pids=0,16,17,18,20", source, strArray[0], strArray[1].ToLower(), strArray[2].ToLower(), strArray[3], strArray[5].ToLower()); + } + else + { + tuning = string.Format("src={0}&freq={1}&pol={2}&sr={3}&fec={4}&msys=dvbs&mtype={5}&pids=0,16,17,18,20", source, strArray[0], strArray[1].ToLower(), strArray[2], strArray[3], strArray[5].ToLower()); + } + + rtspSession.Setup(tuning, "unicast"); + + rtspSession.Play(string.Empty); + + int signallevel; + int signalQuality; + rtspSession.Describe(out signallevel, out signalQuality); + + await Task.Delay(500).ConfigureAwait(false); + index++; + } + } + } + } + + return list; + } + + private int GetInt(IniData data, string s1, string s2, int defaultValue) + { + var value = data[s1][s2]; + int numericValue; + if (int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out numericValue)) + { + return numericValue; + } + + return defaultValue; + } + } + + public class SatChannel + { + // TODO: Add properties + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspMethod.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspMethod.cs new file mode 100644 index 000000000..5f286f1db --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspMethod.cs @@ -0,0 +1,88 @@ +/* + Copyright (C) <2007-2016> <Kay Diefenthal> + + SatIp.RtspSample is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + SatIp.RtspSample is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SatIp.RtspSample. If not, see <http://www.gnu.org/licenses/>. +*/ + +using System.Collections.Generic; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp +{ + /// <summary> + /// Standard RTSP request methods. + /// </summary> + public sealed class RtspMethod + { + public override int GetHashCode() + { + return (_name != null ? _name.GetHashCode() : 0); + } + + private readonly string _name; + private static readonly IDictionary<string, RtspMethod> _values = new Dictionary<string, RtspMethod>(); + + public static readonly RtspMethod Describe = new RtspMethod("DESCRIBE"); + public static readonly RtspMethod Announce = new RtspMethod("ANNOUNCE"); + public static readonly RtspMethod GetParameter = new RtspMethod("GET_PARAMETER"); + public static readonly RtspMethod Options = new RtspMethod("OPTIONS"); + public static readonly RtspMethod Pause = new RtspMethod("PAUSE"); + public static readonly RtspMethod Play = new RtspMethod("PLAY"); + public static readonly RtspMethod Record = new RtspMethod("RECORD"); + public static readonly RtspMethod Redirect = new RtspMethod("REDIRECT"); + public static readonly RtspMethod Setup = new RtspMethod("SETUP"); + public static readonly RtspMethod SetParameter = new RtspMethod("SET_PARAMETER"); + public static readonly RtspMethod Teardown = new RtspMethod("TEARDOWN"); + + private RtspMethod(string name) + { + _name = name; + _values.Add(name, this); + } + + public override string ToString() + { + return _name; + } + + public override bool Equals(object obj) + { + var method = obj as RtspMethod; + if (method != null && this == method) + { + return true; + } + return false; + } + + public static ICollection<RtspMethod> Values + { + get { return _values.Values; } + } + + public static explicit operator RtspMethod(string name) + { + RtspMethod value; + if (!_values.TryGetValue(name, out value)) + { + return null; + } + return value; + } + + public static implicit operator string(RtspMethod method) + { + return method._name; + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspRequest.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspRequest.cs new file mode 100644 index 000000000..600eda02d --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspRequest.cs @@ -0,0 +1,140 @@ +/* + Copyright (C) <2007-2016> <Kay Diefenthal> + + SatIp.RtspSample is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + SatIp.RtspSample is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SatIp.RtspSample. If not, see <http://www.gnu.org/licenses/>. +*/ + +using System.Collections.Generic; +using System.Text; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp +{ + /// <summary> + /// A simple class that can be used to serialise RTSP requests. + /// </summary> + public class RtspRequest + { + private readonly RtspMethod _method; + private readonly string _uri; + private readonly int _majorVersion; + private readonly int _minorVersion; + private IDictionary<string, string> _headers = new Dictionary<string, string>(); + private string _body = string.Empty; + + /// <summary> + /// Initialise a new instance of the <see cref="RtspRequest"/> class. + /// </summary> + /// <param name="method">The request method.</param> + /// <param name="uri">The request URI</param> + /// <param name="majorVersion">The major version number.</param> + /// <param name="minorVersion">The minor version number.</param> + public RtspRequest(RtspMethod method, string uri, int majorVersion, int minorVersion) + { + _method = method; + _uri = uri; + _majorVersion = majorVersion; + _minorVersion = minorVersion; + } + + /// <summary> + /// Get the request method. + /// </summary> + public RtspMethod Method + { + get + { + return _method; + } + } + + /// <summary> + /// Get the request URI. + /// </summary> + public string Uri + { + get + { + return _uri; + } + } + + /// <summary> + /// Get the request major version number. + /// </summary> + public int MajorVersion + { + get + { + return _majorVersion; + } + } + + /// <summary> + /// Get the request minor version number. + /// </summary> + public int MinorVersion + { + get + { + return _minorVersion; + } + } + + /// <summary> + /// Get or set the request headers. + /// </summary> + public IDictionary<string, string> Headers + { + get + { + return _headers; + } + set + { + _headers = value; + } + } + + /// <summary> + /// Get or set the request body. + /// </summary> + public string Body + { + get + { + return _body; + } + set + { + _body = value; + } + } + + /// <summary> + /// Serialise this request. + /// </summary> + /// <returns>raw request bytes</returns> + public byte[] Serialise() + { + var request = new StringBuilder(); + request.AppendFormat("{0} {1} RTSP/{2}.{3}\r\n", _method, _uri, _majorVersion, _minorVersion); + foreach (var header in _headers) + { + request.AppendFormat("{0}: {1}\r\n", header.Key, header.Value); + } + request.AppendFormat("\r\n{0}", _body); + return Encoding.UTF8.GetBytes(request.ToString()); + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspResponse.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspResponse.cs new file mode 100644 index 000000000..97290623b --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspResponse.cs @@ -0,0 +1,149 @@ +/* + Copyright (C) <2007-2016> <Kay Diefenthal> + + SatIp.RtspSample is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + SatIp.RtspSample is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SatIp.RtspSample. If not, see <http://www.gnu.org/licenses/>. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp +{ + /// <summary> + /// A simple class that can be used to deserialise RTSP responses. + /// </summary> + public class RtspResponse + { + private static readonly Regex RegexStatusLine = new Regex(@"RTSP/(\d+)\.(\d+)\s+(\d+)\s+([^.]+?)\r\n(.*)", RegexOptions.Singleline); + + private int _majorVersion = 1; + private int _minorVersion; + private RtspStatusCode _statusCode; + private string _reasonPhrase; + private IDictionary<string, string> _headers; + private string _body; + + /// <summary> + /// Initialise a new instance of the <see cref="RtspResponse"/> class. + /// </summary> + private RtspResponse() + { + } + + /// <summary> + /// Get the response major version number. + /// </summary> + public int MajorVersion + { + get + { + return _majorVersion; + } + } + + /// <summary> + /// Get the response minor version number. + /// </summary> + public int MinorVersion + { + get + { + return _minorVersion; + } + } + + /// <summary> + /// Get the response status code. + /// </summary> + public RtspStatusCode StatusCode + { + get + { + return _statusCode; + } + } + + /// <summary> + /// Get the response reason phrase. + /// </summary> + public string ReasonPhrase + { + get + { + return _reasonPhrase; + } + } + + /// <summary> + /// Get the response headers. + /// </summary> + public IDictionary<string, string> Headers + { + get + { + return _headers; + } + } + + /// <summary> + /// Get the response body. + /// </summary> + public string Body + { + get + { + return _body; + } + set + { + _body = value; + } + } + + /// <summary> + /// Deserialise/parse an RTSP response. + /// </summary> + /// <param name="responseBytes">The raw response bytes.</param> + /// <param name="responseByteCount">The number of valid bytes in the response.</param> + /// <returns>a response object</returns> + public static RtspResponse Deserialise(byte[] responseBytes, int responseByteCount) + { + var response = new RtspResponse(); + var responseString = Encoding.UTF8.GetString(responseBytes, 0, responseByteCount); + + var m = RegexStatusLine.Match(responseString); + if (m.Success) + { + response._majorVersion = int.Parse(m.Groups[1].Captures[0].Value); + response._minorVersion = int.Parse(m.Groups[2].Captures[0].Value); + response._statusCode = (RtspStatusCode)int.Parse(m.Groups[3].Captures[0].Value); + response._reasonPhrase = m.Groups[4].Captures[0].Value; + responseString = m.Groups[5].Captures[0].Value; + } + + var sections = responseString.Split(new[] { "\r\n\r\n" }, StringSplitOptions.None); + response._body = sections[1]; + var headers = sections[0].Split(new[] { "\r\n" }, StringSplitOptions.None); + response._headers = new Dictionary<string, string>(); + foreach (var headerInfo in headers.Select(header => header.Split(':'))) + { + response._headers.Add(headerInfo[0], headerInfo[1].Trim()); + } + return response; + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspSession.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspSession.cs new file mode 100644 index 000000000..71b3f8a18 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspSession.cs @@ -0,0 +1,688 @@ +/* + Copyright (C) <2007-2016> <Kay Diefenthal> + + SatIp.RtspSample is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + SatIp.RtspSample is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SatIp.RtspSample. If not, see <http://www.gnu.org/licenses/>. +*/ + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Text.RegularExpressions; +using MediaBrowser.Model.Logging; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp +{ + public class RtspSession : IDisposable + { + #region Private Fields + private static readonly Regex RegexRtspSessionHeader = new Regex(@"\s*([^\s;]+)(;timeout=(\d+))?"); + private const int DefaultRtspSessionTimeout = 30; // unit = s + private static readonly Regex RegexDescribeResponseSignalInfo = new Regex(@";tuner=\d+,(\d+),(\d+),(\d+),", RegexOptions.Singleline | RegexOptions.IgnoreCase); + private string _address; + private string _rtspSessionId; + + public string RtspSessionId + { + get { return _rtspSessionId; } + set { _rtspSessionId = value; } + } + private int _rtspSessionTimeToLive = 0; + private string _rtspStreamId; + private int _clientRtpPort; + private int _clientRtcpPort; + private int _serverRtpPort; + private int _serverRtcpPort; + private int _rtpPort; + private int _rtcpPort; + private string _rtspStreamUrl; + private string _destination; + private string _source; + private string _transport; + private int _signalLevel; + private int _signalQuality; + private Socket _rtspSocket; + private int _rtspSequenceNum = 1; + private bool _disposed = false; + private readonly ILogger _logger; + #endregion + + #region Constructor + + public RtspSession(string address, ILogger logger) + { + if (string.IsNullOrWhiteSpace(address)) + { + throw new ArgumentNullException("address"); + } + + _address = address; + _logger = logger; + + _logger.Info("Creating RtspSession with url {0}", address); + } + ~RtspSession() + { + Dispose(false); + } + #endregion + + #region Properties + + #region Rtsp + + public string RtspStreamId + { + get { return _rtspStreamId; } + set { if (_rtspStreamId != value) { _rtspStreamId = value; OnPropertyChanged("RtspStreamId"); } } + } + public string RtspStreamUrl + { + get { return _rtspStreamUrl; } + set { if (_rtspStreamUrl != value) { _rtspStreamUrl = value; OnPropertyChanged("RtspStreamUrl"); } } + } + + public int RtspSessionTimeToLive + { + get + { + if (_rtspSessionTimeToLive == 0) + _rtspSessionTimeToLive = DefaultRtspSessionTimeout; + return _rtspSessionTimeToLive * 1000 - 20; + } + set { if (_rtspSessionTimeToLive != value) { _rtspSessionTimeToLive = value; OnPropertyChanged("RtspSessionTimeToLive"); } } + } + + #endregion + + #region Rtp Rtcp + + /// <summary> + /// The LocalEndPoint Address + /// </summary> + public string Destination + { + get + { + if (string.IsNullOrEmpty(_destination)) + { + var result = ""; + var host = Dns.GetHostName(); + var hostentry = Dns.GetHostEntry(host); + foreach (var ip in hostentry.AddressList.Where(ip => ip.AddressFamily == AddressFamily.InterNetwork)) + { + result = ip.ToString(); + } + + _destination = result; + } + return _destination; + } + set + { + if (_destination != value) + { + _destination = value; + OnPropertyChanged("Destination"); + } + } + } + + /// <summary> + /// The RemoteEndPoint Address + /// </summary> + public string Source + { + get { return _source; } + set + { + if (_source != value) + { + _source = value; + OnPropertyChanged("Source"); + } + } + } + + /// <summary> + /// The Media Data Delivery RemoteEndPoint Port if we use Unicast + /// </summary> + public int ServerRtpPort + { + get + { + return _serverRtpPort; + } + set { if (_serverRtpPort != value) { _serverRtpPort = value; OnPropertyChanged("ServerRtpPort"); } } + } + + /// <summary> + /// The Media Metadata Delivery RemoteEndPoint Port if we use Unicast + /// </summary> + public int ServerRtcpPort + { + get { return _serverRtcpPort; } + set { if (_serverRtcpPort != value) { _serverRtcpPort = value; OnPropertyChanged("ServerRtcpPort"); } } + } + + /// <summary> + /// The Media Data Delivery LocalEndPoint Port if we use Unicast + /// </summary> + public int ClientRtpPort + { + get { return _clientRtpPort; } + set { if (_clientRtpPort != value) { _clientRtpPort = value; OnPropertyChanged("ClientRtpPort"); } } + } + + /// <summary> + /// The Media Metadata Delivery LocalEndPoint Port if we use Unicast + /// </summary> + public int ClientRtcpPort + { + get { return _clientRtcpPort; } + set { if (_clientRtcpPort != value) { _clientRtcpPort = value; OnPropertyChanged("ClientRtcpPort"); } } + } + + /// <summary> + /// The Media Data Delivery RemoteEndPoint Port if we use Multicast + /// </summary> + public int RtpPort + { + get { return _rtpPort; } + set { if (_rtpPort != value) { _rtpPort = value; OnPropertyChanged("RtpPort"); } } + } + + /// <summary> + /// The Media Meta Delivery RemoteEndPoint Port if we use Multicast + /// </summary> + public int RtcpPort + { + get { return _rtcpPort; } + set { if (_rtcpPort != value) { _rtcpPort = value; OnPropertyChanged("RtcpPort"); } } + } + + #endregion + + public string Transport + { + get + { + if (string.IsNullOrEmpty(_transport)) + { + _transport = "unicast"; + } + return _transport; + } + set + { + if (_transport != value) + { + _transport = value; + OnPropertyChanged("Transport"); + } + } + } + public int SignalLevel + { + get { return _signalLevel; } + set { if (_signalLevel != value) { _signalLevel = value; OnPropertyChanged("SignalLevel"); } } + } + public int SignalQuality + { + get { return _signalQuality; } + set { if (_signalQuality != value) { _signalQuality = value; OnPropertyChanged("SignalQuality"); } } + } + + #endregion + + #region Private Methods + + private void ProcessSessionHeader(string sessionHeader, string response) + { + if (!string.IsNullOrEmpty(sessionHeader)) + { + var m = RegexRtspSessionHeader.Match(sessionHeader); + if (!m.Success) + { + _logger.Error("Failed to tune, RTSP {0} response session header {1} format not recognised", response, sessionHeader); + } + _rtspSessionId = m.Groups[1].Captures[0].Value; + _rtspSessionTimeToLive = m.Groups[3].Captures.Count == 1 ? int.Parse(m.Groups[3].Captures[0].Value) : DefaultRtspSessionTimeout; + } + } + private void ProcessTransportHeader(string transportHeader) + { + if (!string.IsNullOrEmpty(transportHeader)) + { + var transports = transportHeader.Split(','); + foreach (var transport in transports) + { + if (transport.Trim().StartsWith("RTP/AVP")) + { + var sections = transport.Split(';'); + foreach (var section in sections) + { + var parts = section.Split('='); + if (parts[0].Equals("server_port")) + { + var ports = parts[1].Split('-'); + _serverRtpPort = int.Parse(ports[0]); + _serverRtcpPort = int.Parse(ports[1]); + } + else if (parts[0].Equals("destination")) + { + _destination = parts[1]; + } + else if (parts[0].Equals("port")) + { + var ports = parts[1].Split('-'); + _rtpPort = int.Parse(ports[0]); + _rtcpPort = int.Parse(ports[1]); + } + else if (parts[0].Equals("ttl")) + { + _rtspSessionTimeToLive = int.Parse(parts[1]); + } + else if (parts[0].Equals("source")) + { + _source = parts[1]; + } + else if (parts[0].Equals("client_port")) + { + var ports = parts[1].Split('-'); + var rtp = int.Parse(ports[0]); + var rtcp = int.Parse(ports[1]); + //if (!rtp.Equals(_rtpPort)) + //{ + // Logger.Error("SAT>IP base: server specified RTP client port {0} instead of {1}", rtp, _rtpPort); + //} + //if (!rtcp.Equals(_rtcpPort)) + //{ + // Logger.Error("SAT>IP base: server specified RTCP client port {0} instead of {1}", rtcp, _rtcpPort); + //} + _rtpPort = rtp; + _rtcpPort = rtcp; + } + } + } + } + } + } + private void Connect() + { + _rtspSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + var ip = IPAddress.Parse(_address); + var rtspEndpoint = new IPEndPoint(ip, 554); + _rtspSocket.Connect(rtspEndpoint); + } + private void Disconnect() + { + if (_rtspSocket != null && _rtspSocket.Connected) + { + _rtspSocket.Shutdown(SocketShutdown.Both); + _rtspSocket.Close(); + } + } + private void SendRequest(RtspRequest request) + { + if (_rtspSocket == null) + { + Connect(); + } + try + { + request.Headers.Add("CSeq", _rtspSequenceNum.ToString()); + _rtspSequenceNum++; + byte[] requestBytes = request.Serialise(); + if (_rtspSocket != null) + { + var requestBytesCount = _rtspSocket.Send(requestBytes, requestBytes.Length, SocketFlags.None); + if (requestBytesCount < 1) + { + + } + } + } + catch (Exception e) + { + _logger.Error(e.Message); + } + } + private void ReceiveResponse(out RtspResponse response) + { + response = null; + var responseBytesCount = 0; + byte[] responseBytes = new byte[1024]; + try + { + responseBytesCount = _rtspSocket.Receive(responseBytes, responseBytes.Length, SocketFlags.None); + response = RtspResponse.Deserialise(responseBytes, responseBytesCount); + string contentLengthString; + int contentLength = 0; + if (response.Headers.TryGetValue("Content-Length", out contentLengthString)) + { + contentLength = int.Parse(contentLengthString); + if ((string.IsNullOrEmpty(response.Body) && contentLength > 0) || response.Body.Length < contentLength) + { + if (response.Body == null) + { + response.Body = string.Empty; + } + while (responseBytesCount > 0 && response.Body.Length < contentLength) + { + responseBytesCount = _rtspSocket.Receive(responseBytes, responseBytes.Length, SocketFlags.None); + response.Body += System.Text.Encoding.UTF8.GetString(responseBytes, 0, responseBytesCount); + } + } + } + } + catch (SocketException) + { + } + } + + #endregion + + #region Public Methods + + public RtspStatusCode Setup(string query, string transporttype) + { + + RtspRequest request; + RtspResponse response; + //_rtspClient = new RtspClient(_rtspDevice.ServerAddress); + if ((_rtspSocket == null)) + { + Connect(); + } + if (string.IsNullOrEmpty(_rtspSessionId)) + { + request = new RtspRequest(RtspMethod.Setup, string.Format("rtsp://{0}:{1}/?{2}", _address, 554, query), 1, 0); + switch (transporttype) + { + case "multicast": + request.Headers.Add("Transport", string.Format("RTP/AVP;multicast")); + break; + case "unicast": + var activeTcpConnections = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections(); + var usedPorts = new HashSet<int>(); + foreach (var connection in activeTcpConnections) + { + usedPorts.Add(connection.LocalEndPoint.Port); + } + for (var port = 40000; port <= 65534; port += 2) + { + if (!usedPorts.Contains(port) && !usedPorts.Contains(port + 1)) + { + + _clientRtpPort = port; + _clientRtcpPort = port + 1; + break; + } + } + request.Headers.Add("Transport", string.Format("RTP/AVP;unicast;client_port={0}-{1}", _clientRtpPort, _clientRtcpPort)); + break; + } + } + else + { + request = new RtspRequest(RtspMethod.Setup, string.Format("rtsp://{0}:{1}/?{2}", _address, 554, query), 1, 0); + switch (transporttype) + { + case "multicast": + request.Headers.Add("Transport", string.Format("RTP/AVP;multicast")); + break; + case "unicast": + request.Headers.Add("Transport", string.Format("RTP/AVP;unicast;client_port={0}-{1}", _clientRtpPort, _clientRtcpPort)); + break; + } + + } + SendRequest(request); + ReceiveResponse(out response); + + //if (_rtspClient.SendRequest(request, out response) != RtspStatusCode.Ok) + //{ + // Logger.Error("Failed to tune, non-OK RTSP SETUP status code {0} {1}", response.StatusCode, response.ReasonPhrase); + //} + if (!response.Headers.TryGetValue("com.ses.streamID", out _rtspStreamId)) + { + _logger.Error(string.Format("Failed to tune, not able to locate Stream ID header in RTSP SETUP response")); + } + string sessionHeader; + if (!response.Headers.TryGetValue("Session", out sessionHeader)) + { + _logger.Error(string.Format("Failed to tune, not able to locate Session header in RTSP SETUP response")); + } + ProcessSessionHeader(sessionHeader, "Setup"); + string transportHeader; + if (!response.Headers.TryGetValue("Transport", out transportHeader)) + { + _logger.Error(string.Format("Failed to tune, not able to locate Transport header in RTSP SETUP response")); + } + ProcessTransportHeader(transportHeader); + return response.StatusCode; + } + + public RtspStatusCode Play(string query) + { + if ((_rtspSocket == null)) + { + Connect(); + } + //_rtspClient = new RtspClient(_rtspDevice.ServerAddress); + RtspResponse response; + string data; + if (string.IsNullOrEmpty(query)) + { + data = string.Format("rtsp://{0}:{1}/stream={2}", _address, + 554, _rtspStreamId); + } + else + { + data = string.Format("rtsp://{0}:{1}/stream={2}?{3}", _address, + 554, _rtspStreamId, query); + } + var request = new RtspRequest(RtspMethod.Play, data, 1, 0); + request.Headers.Add("Session", _rtspSessionId); + SendRequest(request); + ReceiveResponse(out response); + //if (_rtspClient.SendRequest(request, out response) != RtspStatusCode.Ok) + //{ + // Logger.Error("Failed to tune, non-OK RTSP SETUP status code {0} {1}", response.StatusCode, response.ReasonPhrase); + //} + //Logger.Info("RtspSession-Play : \r\n {0}", response); + string sessionHeader; + if (!response.Headers.TryGetValue("Session", out sessionHeader)) + { + _logger.Error(string.Format("Failed to tune, not able to locate Session header in RTSP Play response")); + } + ProcessSessionHeader(sessionHeader, "Play"); + string rtpinfoHeader; + if (!response.Headers.TryGetValue("RTP-Info", out rtpinfoHeader)) + { + _logger.Error(string.Format("Failed to tune, not able to locate Rtp-Info header in RTSP Play response")); + } + return response.StatusCode; + } + + public RtspStatusCode Options() + { + if ((_rtspSocket == null)) + { + Connect(); + } + //_rtspClient = new RtspClient(_rtspDevice.ServerAddress); + RtspRequest request; + RtspResponse response; + + + if (string.IsNullOrEmpty(_rtspSessionId)) + { + request = new RtspRequest(RtspMethod.Options, string.Format("rtsp://{0}:{1}/", _address, 554), 1, 0); + } + else + { + request = new RtspRequest(RtspMethod.Options, string.Format("rtsp://{0}:{1}/", _address, 554), 1, 0); + request.Headers.Add("Session", _rtspSessionId); + } + SendRequest(request); + ReceiveResponse(out response); + //if (_rtspClient.SendRequest(request, out response) != RtspStatusCode.Ok) + //{ + // Logger.Error("Failed to tune, non-OK RTSP SETUP status code {0} {1}", response.StatusCode, response.ReasonPhrase); + //} + //Logger.Info("RtspSession-Options : \r\n {0}", response); + string sessionHeader; + if (!response.Headers.TryGetValue("Session", out sessionHeader)) + { + _logger.Error(string.Format("Failed to tune, not able to locate session header in RTSP Options response")); + } + ProcessSessionHeader(sessionHeader, "Options"); + string optionsHeader; + if (!response.Headers.TryGetValue("Public", out optionsHeader)) + { + _logger.Error(string.Format("Failed to tune, not able to Options header in RTSP Options response")); + } + return response.StatusCode; + } + + public RtspStatusCode Describe(out int level, out int quality) + { + if ((_rtspSocket == null)) + { + Connect(); + } + //_rtspClient = new RtspClient(_rtspDevice.ServerAddress); + RtspRequest request; + RtspResponse response; + level = 0; + quality = 0; + + if (string.IsNullOrEmpty(_rtspSessionId)) + { + request = new RtspRequest(RtspMethod.Describe, string.Format("rtsp://{0}:{1}/", _address, 554), 1, 0); + request.Headers.Add("Accept", "application/sdp"); + + } + else + { + request = new RtspRequest(RtspMethod.Describe, string.Format("rtsp://{0}:{1}/stream={2}", _address, 554, _rtspStreamId), 1, 0); + request.Headers.Add("Accept", "application/sdp"); + request.Headers.Add("Session", _rtspSessionId); + } + SendRequest(request); + ReceiveResponse(out response); + //if (_rtspClient.SendRequest(request, out response) != RtspStatusCode.Ok) + //{ + // Logger.Error("Failed to tune, non-OK RTSP Describe status code {0} {1}", response.StatusCode, response.ReasonPhrase); + //} + //Logger.Info("RtspSession-Describe : \r\n {0}", response); + string sessionHeader; + if (!response.Headers.TryGetValue("Session", out sessionHeader)) + { + _logger.Error(string.Format("Failed to tune, not able to locate session header in RTSP Describe response")); + } + ProcessSessionHeader(sessionHeader, "Describe"); + var m = RegexDescribeResponseSignalInfo.Match(response.Body); + if (m.Success) + { + + //isSignalLocked = m.Groups[2].Captures[0].Value.Equals("1"); + level = int.Parse(m.Groups[1].Captures[0].Value) * 100 / 255; // level: 0..255 => 0..100 + quality = int.Parse(m.Groups[3].Captures[0].Value) * 100 / 15; // quality: 0..15 => 0..100 + + } + /* + v=0 + o=- 1378633020884883 1 IN IP4 192.168.2.108 + s=SatIPServer:1 4 + t=0 0 + a=tool:idl4k + m=video 52780 RTP/AVP 33 + c=IN IP4 0.0.0.0 + b=AS:5000 + a=control:stream=4 + a=fmtp:33 ver=1.0;tuner=1,0,0,0,12344,h,dvbs2,,off,,22000,34;pids=0,100,101,102,103,106 + =sendonly + */ + + + return response.StatusCode; + } + + public RtspStatusCode TearDown() + { + if ((_rtspSocket == null)) + { + Connect(); + } + //_rtspClient = new RtspClient(_rtspDevice.ServerAddress); + RtspResponse response; + + var request = new RtspRequest(RtspMethod.Teardown, string.Format("rtsp://{0}:{1}/stream={2}", _address, 554, _rtspStreamId), 1, 0); + request.Headers.Add("Session", _rtspSessionId); + SendRequest(request); + ReceiveResponse(out response); + //if (_rtspClient.SendRequest(request, out response) != RtspStatusCode.Ok) + //{ + // Logger.Error("Failed to tune, non-OK RTSP Teardown status code {0} {1}", response.StatusCode, response.ReasonPhrase); + //} + return response.StatusCode; + } + + #endregion + + #region Public Events + + public event PropertyChangedEventHandler PropertyChanged; + + #endregion + + #region Protected Methods + + protected void OnPropertyChanged(string name) + { + //var handler = PropertyChanged; + //if (handler != null) + //{ + // handler(this, new PropertyChangedEventArgs(name)); + //} + } + + #endregion + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this);//Disconnect(); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + TearDown(); + Disconnect(); + } + } + _disposed = true; + } + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspStatusCode.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspStatusCode.cs new file mode 100644 index 000000000..6d6d50623 --- /dev/null +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/Rtsp/RtspStatusCode.cs @@ -0,0 +1,251 @@ +/* + Copyright (C) <2007-2016> <Kay Diefenthal> + + SatIp.RtspSample is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + SatIp.RtspSample is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with SatIp.RtspSample. If not, see <http://www.gnu.org/licenses/>. +*/ + +using System.ComponentModel; + +namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp.Rtsp +{ + /// <summary> + /// Standard RTSP status codes. + /// </summary> + public enum RtspStatusCode + { + /// <summary> + /// 100 continue + /// </summary> + Continue = 100, + + /// <summary> + /// 200 OK + /// </summary> + [Description("Okay")] + Ok = 200, + /// <summary> + /// 201 created + /// </summary> + Created = 201, + + /// <summary> + /// 250 low on storage space + /// </summary> + [Description("Low On Storage Space")] + LowOnStorageSpace = 250, + + /// <summary> + /// 300 multiple choices + /// </summary> + [Description("Multiple Choices")] + MultipleChoices = 300, + /// <summary> + /// 301 moved permanently + /// </summary> + [Description("Moved Permanently")] + MovedPermanently = 301, + /// <summary> + /// 302 moved temporarily + /// </summary> + [Description("Moved Temporarily")] + MovedTemporarily = 302, + /// <summary> + /// 303 see other + /// </summary> + [Description("See Other")] + SeeOther = 303, + /// <summary> + /// 304 not modified + /// </summary> + [Description("Not Modified")] + NotModified = 304, + /// <summary> + /// 305 use proxy + /// </summary> + [Description("Use Proxy")] + UseProxy = 305, + + /// <summary> + /// 400 bad request + /// </summary> + [Description("Bad Request")] + BadRequest = 400, + /// <summary> + /// 401 unauthorised + /// </summary> + Unauthorised = 401, + /// <summary> + /// 402 payment required + /// </summary> + [Description("Payment Required")] + PaymentRequired = 402, + /// <summary> + /// 403 forbidden + /// </summary> + Forbidden = 403, + /// <summary> + /// 404 not found + /// </summary> + [Description("Not Found")] + NotFound = 404, + /// <summary> + /// 405 method not allowed + /// </summary> + [Description("Method Not Allowed")] + MethodNotAllowed = 405, + /// <summary> + /// 406 not acceptable + /// </summary> + [Description("Not Acceptable")] + NotAcceptable = 406, + /// <summary> + /// 407 proxy authentication required + /// </summary> + [Description("Proxy Authentication Required")] + ProxyAuthenticationRequred = 407, + /// <summary> + /// 408 request time-out + /// </summary> + [Description("Request Time-Out")] + RequestTimeOut = 408, + + /// <summary> + /// 410 gone + /// </summary> + Gone = 410, + /// <summary> + /// 411 length required + /// </summary> + [Description("Length Required")] + LengthRequired = 411, + /// <summary> + /// 412 precondition failed + /// </summary> + [Description("Precondition Failed")] + PreconditionFailed = 412, + /// <summary> + /// 413 request entity too large + /// </summary> + [Description("Request Entity Too Large")] + RequestEntityTooLarge = 413, + /// <summary> + /// 414 request URI too large + /// </summary> + [Description("Request URI Too Large")] + RequestUriTooLarge = 414, + /// <summary> + /// 415 unsupported media type + /// </summary> + [Description("Unsupported Media Type")] + UnsupportedMediaType = 415, + + /// <summary> + /// 451 parameter not understood + /// </summary> + [Description("Parameter Not Understood")] + ParameterNotUnderstood = 451, + /// <summary> + /// 452 conference not found + /// </summary> + [Description("Conference Not Found")] + ConferenceNotFound = 452, + /// <summary> + /// 453 not enough bandwidth + /// </summary> + [Description("Not Enough Bandwidth")] + NotEnoughBandwidth = 453, + /// <summary> + /// 454 session not found + /// </summary> + [Description("Session Not Found")] + SessionNotFound = 454, + /// <summary> + /// 455 method not valid in this state + /// </summary> + [Description("Method Not Valid In This State")] + MethodNotValidInThisState = 455, + /// <summary> + /// 456 header field not valid for this resource + /// </summary> + [Description("Header Field Not Valid For This Resource")] + HeaderFieldNotValidForThisResource = 456, + /// <summary> + /// 457 invalid range + /// </summary> + [Description("Invalid Range")] + InvalidRange = 457, + /// <summary> + /// 458 parameter is read-only + /// </summary> + [Description("Parameter Is Read-Only")] + ParameterIsReadOnly = 458, + /// <summary> + /// 459 aggregate operation not allowed + /// </summary> + [Description("Aggregate Operation Not Allowed")] + AggregateOperationNotAllowed = 459, + /// <summary> + /// 460 only aggregate operation allowed + /// </summary> + [Description("Only Aggregate Operation Allowed")] + OnlyAggregateOperationAllowed = 460, + /// <summary> + /// 461 unsupported transport + /// </summary> + [Description("Unsupported Transport")] + UnsupportedTransport = 461, + /// <summary> + /// 462 destination unreachable + /// </summary> + [Description("Destination Unreachable")] + DestinationUnreachable = 462, + + /// <summary> + /// 500 internal server error + /// </summary> + [Description("Internal Server Error")] + InternalServerError = 500, + /// <summary> + /// 501 not implemented + /// </summary> + [Description("Not Implemented")] + NotImplemented = 501, + /// <summary> + /// 502 bad gateway + /// </summary> + [Description("Bad Gateway")] + BadGateway = 502, + /// <summary> + /// 503 service unavailable + /// </summary> + [Description("Service Unavailable")] + ServiceUnavailable = 503, + /// <summary> + /// 504 gateway time-out + /// </summary> + [Description("Gateway Time-Out")] + GatewayTimeOut = 504, + /// <summary> + /// 505 RTSP version not supported + /// </summary> + [Description("RTSP Version Not Supported")] + RtspVersionNotSupported = 505, + + /// <summary> + /// 551 option not supported + /// </summary> + [Description("Option Not Supported")] + OptionNotSupported = 551 + } +} diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs index da1894bb7..d0a55966f 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpDiscovery.cs @@ -15,6 +15,7 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Extensions; +using System.Xml.Linq; namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp { @@ -171,58 +172,86 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp public async Task<SatIpTunerHostInfo> GetInfo(string url, CancellationToken cancellationToken) { + Uri locationUri = new Uri(url); + string devicetype = ""; + string friendlyname = ""; + string uniquedevicename = ""; + string manufacturer = ""; + string manufacturerurl = ""; + string modelname = ""; + string modeldescription = ""; + string modelnumber = ""; + string modelurl = ""; + string serialnumber = ""; + string presentationurl = ""; + string capabilities = ""; + string m3u = ""; + var document = XDocument.Load(locationUri.AbsoluteUri); + var xnm = new XmlNamespaceManager(new NameTable()); + XNamespace n1 = "urn:ses-com:satip"; + XNamespace n0 = "urn:schemas-upnp-org:device-1-0"; + xnm.AddNamespace("root", n0.NamespaceName); + xnm.AddNamespace("satip:", n1.NamespaceName); + if (document.Root != null) + { + var deviceElement = document.Root.Element(n0 + "device"); + if (deviceElement != null) + { + var devicetypeElement = deviceElement.Element(n0 + "deviceType"); + if (devicetypeElement != null) + devicetype = devicetypeElement.Value; + var friendlynameElement = deviceElement.Element(n0 + "friendlyName"); + if (friendlynameElement != null) + friendlyname = friendlynameElement.Value; + var manufactureElement = deviceElement.Element(n0 + "manufacturer"); + if (manufactureElement != null) + manufacturer = manufactureElement.Value; + var manufactureurlElement = deviceElement.Element(n0 + "manufacturerURL"); + if (manufactureurlElement != null) + manufacturerurl = manufactureurlElement.Value; + var modeldescriptionElement = deviceElement.Element(n0 + "modelDescription"); + if (modeldescriptionElement != null) + modeldescription = modeldescriptionElement.Value; + var modelnameElement = deviceElement.Element(n0 + "modelName"); + if (modelnameElement != null) + modelname = modelnameElement.Value; + var modelnumberElement = deviceElement.Element(n0 + "modelNumber"); + if (modelnumberElement != null) + modelnumber = modelnumberElement.Value; + var modelurlElement = deviceElement.Element(n0 + "modelURL"); + if (modelurlElement != null) + modelurl = modelurlElement.Value; + var serialnumberElement = deviceElement.Element(n0 + "serialNumber"); + if (serialnumberElement != null) + serialnumber = serialnumberElement.Value; + var uniquedevicenameElement = deviceElement.Element(n0 + "UDN"); + if (uniquedevicenameElement != null) uniquedevicename = uniquedevicenameElement.Value; + var presentationUrlElement = deviceElement.Element(n0 + "presentationURL"); + if (presentationUrlElement != null) presentationurl = presentationUrlElement.Value; + var capabilitiesElement = deviceElement.Element(n1 + "X_SATIPCAP"); + if (capabilitiesElement != null) capabilities = capabilitiesElement.Value; + var m3uElement = deviceElement.Element(n1 + "X_SATIPM3U"); + if (m3uElement != null) m3u = m3uElement.Value; + } + } + var result = new SatIpTunerHostInfo { Url = url, + Id = uniquedevicename, IsEnabled = true, Type = SatIpHost.DeviceType, Tuners = 1, - TunersAvailable = 1 + TunersAvailable = 1, + M3UUrl = m3u }; - using (var stream = await _httpClient.Get(url, cancellationToken).ConfigureAwait(false)) - { - using (var streamReader = new StreamReader(stream)) - { - // Use XmlReader for best performance - using (var reader = XmlReader.Create(streamReader)) - { - reader.MoveToContent(); - - // Loop through each element - while (reader.Read()) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "device": - using (var subtree = reader.ReadSubtree()) - { - FillFromDeviceNode(result, subtree); - } - break; - default: - reader.Skip(); - break; - } - } - } - } - } - } - - if (string.IsNullOrWhiteSpace(result.DeviceId)) + result.FriendlyName = friendlyname; + if (string.IsNullOrWhiteSpace(result.Id)) { throw new NotImplementedException(); } - // Device hasn't implemented an m3u list - if (string.IsNullOrWhiteSpace(result.M3UUrl)) - { - result.IsEnabled = false; - } - else if (!result.M3UUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { var fullM3uUrl = url.Substring(0, url.LastIndexOf('/')); @@ -233,66 +262,6 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp return result; } - - private void FillFromDeviceNode(SatIpTunerHostInfo info, XmlReader reader) - { - reader.MoveToContent(); - - while (reader.Read()) - { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.LocalName) - { - case "UDN": - { - info.DeviceId = reader.ReadElementContentAsString(); - break; - } - - case "friendlyName": - { - info.FriendlyName = reader.ReadElementContentAsString(); - break; - } - - case "satip:X_SATIPCAP": - case "X_SATIPCAP": - { - // <satip:X_SATIPCAP xmlns:satip="urn:ses-com:satip">DVBS2-2</satip:X_SATIPCAP> - var value = reader.ReadElementContentAsString() ?? string.Empty; - var parts = value.Split(new[] { '-' }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length == 2) - { - int intValue; - if (int.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out intValue)) - { - info.TunersAvailable = intValue; - } - - if (int.TryParse(parts[0].Substring(parts[0].Length - 1), NumberStyles.Any, CultureInfo.InvariantCulture, out intValue)) - { - info.Tuners = intValue; - } - } - break; - } - - case "satip:X_SATIPM3U": - case "X_SATIPM3U": - { - // <satip:X_SATIPM3U xmlns:satip="urn:ses-com:satip">/channellist.lua?select=m3u</satip:X_SATIPM3U> - info.M3UUrl = reader.ReadElementContentAsString(); - break; - } - - default: - reader.Skip(); - break; - } - } - } - } } public class SatIpTunerHostInfo : TunerHostInfo diff --git a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs index 46a2a8524..1e571c84f 100644 --- a/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs +++ b/MediaBrowser.Server.Implementations/LiveTv/TunerHosts/SatIp/SatIpHost.cs @@ -40,7 +40,8 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp return await new M3uParser(Logger, _fileSystem, _httpClient).Parse(tuner.M3UUrl, ChannelIdPrefix, tuner.Id, cancellationToken).ConfigureAwait(false); } - return new List<ChannelInfo>(); + var channels = await new ChannelScan(Logger).Scan(tuner, cancellationToken).ConfigureAwait(false); + return channels; } public static string DeviceType @@ -163,5 +164,10 @@ namespace MediaBrowser.Server.Implementations.LiveTv.TunerHosts.SatIp return list; } + + public string ApplyDuration(string streamPath, TimeSpan duration) + { + return streamPath; + } } } diff --git a/MediaBrowser.Server.Implementations/Localization/Core/ca.json b/MediaBrowser.Server.Implementations/Localization/Core/ca.json index 7cec213d3..7ca8e1553 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/ca.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/ca.json @@ -1,5 +1,5 @@ { - "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", + "DbUpgradeMessage": "Si et plau espera mentre la teva base de dades del Servidor Emby \u00e9s actualitzada. {0}% completat.", "AppDeviceValues": "App: {0}, Dispositiu: {1}", "UserDownloadingItemWithValues": "{0} est\u00e0 descarregant {1}", "FolderTypeMixed": "Contingut barrejat", @@ -19,11 +19,11 @@ "LabelChapterName": "Cap\u00edtol {0}", "NameSeasonNumber": "Temporada {0}", "LabelExit": "Sortir", - "LabelVisitCommunity": "Visita la comunitat", + "LabelVisitCommunity": "Visita la Comunitat", "LabelGithub": "Github", "LabelApiDocumentation": "Documentaci\u00f3 de l'API", - "LabelDeveloperResources": "Recursos per a programadors", - "LabelBrowseLibrary": "Examina la biblioteca", + "LabelDeveloperResources": "Recursos per a Desenvolupadors", + "LabelBrowseLibrary": "Examina la Biblioteca", "LabelConfigureServer": "Configura Emby", "LabelRestartServer": "Reiniciar Servidor", "CategorySync": "Sync", @@ -131,7 +131,7 @@ "HeaderType": "Type", "HeaderSeverity": "Severity", "HeaderUser": "Usuari", - "HeaderName": "Name", + "HeaderName": "Nom", "HeaderDate": "Data", "HeaderPremiereDate": "Premiere Date", "HeaderDateAdded": "Data afegida", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/en-US.json b/MediaBrowser.Server.Implementations/Localization/Core/en-US.json index 0d5b5c4aa..5e2f98c09 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/en-US.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/en-US.json @@ -1,178 +1,179 @@ { - "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", - "AppDeviceValues": "App: {0}, Device: {1}", - "UserDownloadingItemWithValues": "{0} is downloading {1}", - "FolderTypeMixed": "Mixed content", - "FolderTypeMovies": "Movies", - "FolderTypeMusic": "Music", - "FolderTypeAdultVideos": "Adult videos", - "FolderTypePhotos": "Photos", - "FolderTypeMusicVideos": "Music videos", - "FolderTypeHomeVideos": "Home videos", - "FolderTypeGames": "Games", - "FolderTypeBooks": "Books", - "FolderTypeTvShows": "TV", - "FolderTypeInherit": "Inherit", - "HeaderCastCrew": "Cast & Crew", - "HeaderPeople": "People", - "ValueSpecialEpisodeName": "Special - {0}", - "LabelChapterName": "Chapter {0}", - "NameSeasonNumber": "Season {0}", - "LabelExit": "Exit", - "LabelVisitCommunity": "Visit Community", - "LabelGithub": "Github", - "LabelApiDocumentation": "Api Documentation", - "LabelDeveloperResources": "Developer Resources", - "LabelBrowseLibrary": "Browse Library", - "LabelConfigureServer": "Configure Emby", - "LabelRestartServer": "Restart Server", - "CategorySync": "Sync", - "CategoryUser": "User", - "CategorySystem": "System", - "CategoryApplication": "Application", - "CategoryPlugin": "Plugin", - "NotificationOptionPluginError": "Plugin failure", - "NotificationOptionApplicationUpdateAvailable": "Application update available", - "NotificationOptionApplicationUpdateInstalled": "Application update installed", - "NotificationOptionPluginUpdateInstalled": "Plugin update installed", - "NotificationOptionPluginInstalled": "Plugin installed", - "NotificationOptionPluginUninstalled": "Plugin uninstalled", - "NotificationOptionVideoPlayback": "Video playback started", - "NotificationOptionAudioPlayback": "Audio playback started", - "NotificationOptionGamePlayback": "Game playback started", - "NotificationOptionVideoPlaybackStopped": "Video playback stopped", - "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", - "NotificationOptionGamePlaybackStopped": "Game playback stopped", - "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", - "ViewTypePlaylists": "Playlists", - "ViewTypeMovies": "Movies", - "ViewTypeTvShows": "TV", - "ViewTypeGames": "Games", - "ViewTypeMusic": "Music", - "ViewTypeMusicGenres": "Genres", - "ViewTypeMusicArtists": "Artists", - "ViewTypeBoxSets": "Collections", - "ViewTypeChannels": "Channels", - "ViewTypeLiveTV": "Live TV", - "ViewTypeLiveTvNowPlaying": "Now Airing", - "ViewTypeLatestGames": "Latest Games", - "ViewTypeRecentlyPlayedGames": "Recently Played", - "ViewTypeGameFavorites": "Favorites", - "ViewTypeGameSystems": "Game Systems", - "ViewTypeGameGenres": "Genres", - "ViewTypeTvResume": "Resume", - "ViewTypeTvNextUp": "Next Up", - "ViewTypeTvLatest": "Latest", - "ViewTypeTvShowSeries": "Series", - "ViewTypeTvGenres": "Genres", - "ViewTypeTvFavoriteSeries": "Favorite Series", - "ViewTypeTvFavoriteEpisodes": "Favorite Episodes", - "ViewTypeMovieResume": "Resume", - "ViewTypeMovieLatest": "Latest", - "ViewTypeMovieMovies": "Movies", - "ViewTypeMovieCollections": "Collections", - "ViewTypeMovieFavorites": "Favorites", - "ViewTypeMovieGenres": "Genres", - "ViewTypeMusicLatest": "Latest", - "ViewTypeMusicPlaylists": "Playlists", - "ViewTypeMusicAlbums": "Albums", - "ViewTypeMusicAlbumArtists": "Album Artists", - "HeaderOtherDisplaySettings": "Display Settings", - "ViewTypeMusicSongs": "Songs", - "ViewTypeMusicFavorites": "Favorites", - "ViewTypeMusicFavoriteAlbums": "Favorite Albums", - "ViewTypeMusicFavoriteArtists": "Favorite Artists", - "ViewTypeMusicFavoriteSongs": "Favorite Songs", - "ViewTypeFolders": "Folders", - "ViewTypeLiveTvRecordingGroups": "Recordings", - "ViewTypeLiveTvChannels": "Channels", - "ScheduledTaskFailedWithName": "{0} failed", - "LabelRunningTimeValue": "Running time: {0}", - "ScheduledTaskStartedWithName": "{0} started", - "VersionNumber": "Version {0}", - "PluginInstalledWithName": "{0} was installed", - "PluginUpdatedWithName": "{0} was updated", - "PluginUninstalledWithName": "{0} was uninstalled", - "ItemAddedWithName": "{0} was added to the library", - "ItemRemovedWithName": "{0} was removed from the library", - "LabelIpAddressValue": "Ip address: {0}", - "DeviceOnlineWithName": "{0} is connected", - "UserOnlineFromDevice": "{0} is online from {1}", - "ProviderValue": "Provider: {0}", - "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", - "UserCreatedWithName": "User {0} has been created", - "UserPasswordChangedWithName": "Password has been changed for user {0}", - "UserDeletedWithName": "User {0} has been deleted", - "MessageServerConfigurationUpdated": "Server configuration has been updated", - "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", - "MessageApplicationUpdated": "Emby Server has been updated", - "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", - "AuthenticationSucceededWithUserName": "{0} successfully authenticated", - "DeviceOfflineWithName": "{0} has disconnected", - "UserLockedOutWithName": "User {0} has been locked out", - "UserOfflineFromDevice": "{0} has disconnected from {1}", - "UserStartedPlayingItemWithValues": "{0} has started playing {1}", - "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", - "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "HeaderUnidentified": "Unidentified", - "HeaderImagePrimary": "Primary", - "HeaderImageBackdrop": "Backdrop", - "HeaderImageLogo": "Logo", - "HeaderUserPrimaryImage": "User Image", - "HeaderOverview": "Overview", - "HeaderShortOverview": "Short Overview", - "HeaderType": "Type", - "HeaderSeverity": "Severity", - "HeaderUser": "User", - "HeaderName": "Name", - "HeaderDate": "Date", - "HeaderPremiereDate": "Premiere Date", - "HeaderDateAdded": "Date Added", - "HeaderReleaseDate": "Release date", - "HeaderRuntime": "Runtime", - "HeaderPlayCount": "Play Count", - "HeaderSeason": "Season", - "HeaderSeasonNumber": "Season number", - "HeaderSeries": "Series:", - "HeaderNetwork": "Network", - "HeaderYear": "Year:", - "HeaderYears": "Years:", - "HeaderParentalRating": "Parental Rating", - "HeaderCommunityRating": "Community rating", - "HeaderTrailers": "Trailers", - "HeaderSpecials": "Specials", - "HeaderGameSystems": "Game Systems", - "HeaderPlayers": "Players:", - "HeaderAlbumArtists": "Album Artists", - "HeaderAlbums": "Albums", - "HeaderDisc": "Disc", - "HeaderTrack": "Track", - "HeaderAudio": "Audio", - "HeaderVideo": "Video", - "HeaderEmbeddedImage": "Embedded image", - "HeaderResolution": "Resolution", - "HeaderSubtitles": "Subtitles", - "HeaderGenres": "Genres", - "HeaderCountries": "Countries", - "HeaderStatus": "Status", - "HeaderTracks": "Tracks", - "HeaderMusicArtist": "Music artist", - "HeaderLocked": "Locked", - "HeaderStudios": "Studios", - "HeaderActor": "Actors", - "HeaderComposer": "Composers", - "HeaderDirector": "Directors", - "HeaderGuestStar": "Guest star", - "HeaderProducer": "Producers", - "HeaderWriter": "Writers", - "HeaderParentalRatings": "Parental Ratings", - "HeaderCommunityRatings": "Community ratings", - "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." + "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", + "AppDeviceValues": "App: {0}, Device: {1}", + "UserDownloadingItemWithValues": "{0} is downloading {1}", + "FolderTypeMixed": "Mixed content", + "FolderTypeMovies": "Movies", + "FolderTypeMusic": "Music", + "FolderTypeAdultVideos": "Adult videos", + "FolderTypePhotos": "Photos", + "FolderTypeMusicVideos": "Music videos", + "FolderTypeHomeVideos": "Home videos", + "FolderTypeGames": "Games", + "FolderTypeBooks": "Books", + "FolderTypeTvShows": "TV", + "FolderTypeInherit": "Inherit", + "HeaderCastCrew": "Cast & Crew", + "HeaderPeople": "People", + "ValueSpecialEpisodeName": "Special - {0}", + "LabelChapterName": "Chapter {0}", + "NameSeasonUnknown": "Season Unknown", + "NameSeasonNumber": "Season {0}", + "LabelExit": "Exit", + "LabelVisitCommunity": "Visit Community", + "LabelGithub": "Github", + "LabelApiDocumentation": "Api Documentation", + "LabelDeveloperResources": "Developer Resources", + "LabelBrowseLibrary": "Browse Library", + "LabelConfigureServer": "Configure Emby", + "LabelRestartServer": "Restart Server", + "CategorySync": "Sync", + "CategoryUser": "User", + "CategorySystem": "System", + "CategoryApplication": "Application", + "CategoryPlugin": "Plugin", + "NotificationOptionPluginError": "Plugin failure", + "NotificationOptionApplicationUpdateAvailable": "Application update available", + "NotificationOptionApplicationUpdateInstalled": "Application update installed", + "NotificationOptionPluginUpdateInstalled": "Plugin update installed", + "NotificationOptionPluginInstalled": "Plugin installed", + "NotificationOptionPluginUninstalled": "Plugin uninstalled", + "NotificationOptionVideoPlayback": "Video playback started", + "NotificationOptionAudioPlayback": "Audio playback started", + "NotificationOptionGamePlayback": "Game playback started", + "NotificationOptionVideoPlaybackStopped": "Video playback stopped", + "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", + "NotificationOptionGamePlaybackStopped": "Game playback stopped", + "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", + "ViewTypePlaylists": "Playlists", + "ViewTypeMovies": "Movies", + "ViewTypeTvShows": "TV", + "ViewTypeGames": "Games", + "ViewTypeMusic": "Music", + "ViewTypeMusicGenres": "Genres", + "ViewTypeMusicArtists": "Artists", + "ViewTypeBoxSets": "Collections", + "ViewTypeChannels": "Channels", + "ViewTypeLiveTV": "Live TV", + "ViewTypeLiveTvNowPlaying": "Now Airing", + "ViewTypeLatestGames": "Latest Games", + "ViewTypeRecentlyPlayedGames": "Recently Played", + "ViewTypeGameFavorites": "Favorites", + "ViewTypeGameSystems": "Game Systems", + "ViewTypeGameGenres": "Genres", + "ViewTypeTvResume": "Resume", + "ViewTypeTvNextUp": "Next Up", + "ViewTypeTvLatest": "Latest", + "ViewTypeTvShowSeries": "Series", + "ViewTypeTvGenres": "Genres", + "ViewTypeTvFavoriteSeries": "Favorite Series", + "ViewTypeTvFavoriteEpisodes": "Favorite Episodes", + "ViewTypeMovieResume": "Resume", + "ViewTypeMovieLatest": "Latest", + "ViewTypeMovieMovies": "Movies", + "ViewTypeMovieCollections": "Collections", + "ViewTypeMovieFavorites": "Favorites", + "ViewTypeMovieGenres": "Genres", + "ViewTypeMusicLatest": "Latest", + "ViewTypeMusicPlaylists": "Playlists", + "ViewTypeMusicAlbums": "Albums", + "ViewTypeMusicAlbumArtists": "Album Artists", + "HeaderOtherDisplaySettings": "Display Settings", + "ViewTypeMusicSongs": "Songs", + "ViewTypeMusicFavorites": "Favorites", + "ViewTypeMusicFavoriteAlbums": "Favorite Albums", + "ViewTypeMusicFavoriteArtists": "Favorite Artists", + "ViewTypeMusicFavoriteSongs": "Favorite Songs", + "ViewTypeFolders": "Folders", + "ViewTypeLiveTvRecordingGroups": "Recordings", + "ViewTypeLiveTvChannels": "Channels", + "ScheduledTaskFailedWithName": "{0} failed", + "LabelRunningTimeValue": "Running time: {0}", + "ScheduledTaskStartedWithName": "{0} started", + "VersionNumber": "Version {0}", + "PluginInstalledWithName": "{0} was installed", + "PluginUpdatedWithName": "{0} was updated", + "PluginUninstalledWithName": "{0} was uninstalled", + "ItemAddedWithName": "{0} was added to the library", + "ItemRemovedWithName": "{0} was removed from the library", + "LabelIpAddressValue": "Ip address: {0}", + "DeviceOnlineWithName": "{0} is connected", + "UserOnlineFromDevice": "{0} is online from {1}", + "ProviderValue": "Provider: {0}", + "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", + "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", + "UserCreatedWithName": "User {0} has been created", + "UserPasswordChangedWithName": "Password has been changed for user {0}", + "UserDeletedWithName": "User {0} has been deleted", + "MessageServerConfigurationUpdated": "Server configuration has been updated", + "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", + "MessageApplicationUpdated": "Emby Server has been updated", + "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", + "AuthenticationSucceededWithUserName": "{0} successfully authenticated", + "DeviceOfflineWithName": "{0} has disconnected", + "UserLockedOutWithName": "User {0} has been locked out", + "UserOfflineFromDevice": "{0} has disconnected from {1}", + "UserStartedPlayingItemWithValues": "{0} has started playing {1}", + "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", + "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", + "HeaderUnidentified": "Unidentified", + "HeaderImagePrimary": "Primary", + "HeaderImageBackdrop": "Backdrop", + "HeaderImageLogo": "Logo", + "HeaderUserPrimaryImage": "User Image", + "HeaderOverview": "Overview", + "HeaderShortOverview": "Short Overview", + "HeaderType": "Type", + "HeaderSeverity": "Severity", + "HeaderUser": "User", + "HeaderName": "Name", + "HeaderDate": "Date", + "HeaderPremiereDate": "Premiere Date", + "HeaderDateAdded": "Date Added", + "HeaderReleaseDate": "Release date", + "HeaderRuntime": "Runtime", + "HeaderPlayCount": "Play Count", + "HeaderSeason": "Season", + "HeaderSeasonNumber": "Season number", + "HeaderSeries": "Series:", + "HeaderNetwork": "Network", + "HeaderYear": "Year:", + "HeaderYears": "Years:", + "HeaderParentalRating": "Parental Rating", + "HeaderCommunityRating": "Community rating", + "HeaderTrailers": "Trailers", + "HeaderSpecials": "Specials", + "HeaderGameSystems": "Game Systems", + "HeaderPlayers": "Players:", + "HeaderAlbumArtists": "Album Artists", + "HeaderAlbums": "Albums", + "HeaderDisc": "Disc", + "HeaderTrack": "Track", + "HeaderAudio": "Audio", + "HeaderVideo": "Video", + "HeaderEmbeddedImage": "Embedded image", + "HeaderResolution": "Resolution", + "HeaderSubtitles": "Subtitles", + "HeaderGenres": "Genres", + "HeaderCountries": "Countries", + "HeaderStatus": "Status", + "HeaderTracks": "Tracks", + "HeaderMusicArtist": "Music artist", + "HeaderLocked": "Locked", + "HeaderStudios": "Studios", + "HeaderActor": "Actors", + "HeaderComposer": "Composers", + "HeaderDirector": "Directors", + "HeaderGuestStar": "Guest star", + "HeaderProducer": "Producers", + "HeaderWriter": "Writers", + "HeaderParentalRatings": "Parental Ratings", + "HeaderCommunityRatings": "Community ratings", + "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/es.json b/MediaBrowser.Server.Implementations/Localization/Core/es.json index 2a715a5b3..d1a56240d 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/es.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/es.json @@ -1,7 +1,7 @@ { - "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", - "AppDeviceValues": "App: {0}, Device: {1}", - "UserDownloadingItemWithValues": "{0} is downloading {1}", + "DbUpgradeMessage": "Por favor espere mientras la base de datos de su servidor Emby se actualiza. {0}% completado.", + "AppDeviceValues": "Aplicaci\u00f3n: {0}, Dispositivo: {1}", + "UserDownloadingItemWithValues": "{0} est\u00e1 descargando {1}", "FolderTypeMixed": "Contenido mezclado", "FolderTypeMovies": "Peliculas", "FolderTypeMusic": "Musica", @@ -14,10 +14,10 @@ "FolderTypeTvShows": "TV", "FolderTypeInherit": "Heredado", "HeaderCastCrew": "Reparto y equipo t\u00e9cnico", - "HeaderPeople": "People", - "ValueSpecialEpisodeName": "Special - {0}", + "HeaderPeople": "Gente", + "ValueSpecialEpisodeName": "Especial - {0}", "LabelChapterName": "Cap\u00edtulo {0}", - "NameSeasonNumber": "Season {0}", + "NameSeasonNumber": "Temporada {0}", "LabelExit": "Salir", "LabelVisitCommunity": "Visitar la comunidad", "LabelGithub": "Github", @@ -50,77 +50,77 @@ "NotificationOptionCameraImageUploaded": "Imagen de camara se a carcado", "NotificationOptionUserLockedOut": "Usuario bloqueado", "NotificationOptionServerRestartRequired": "Se requiere el reinicio del servidor", - "ViewTypePlaylists": "Playlists", + "ViewTypePlaylists": "Listas de reproducci\u00f3n", "ViewTypeMovies": "Pel\u00edculas", "ViewTypeTvShows": "TV", "ViewTypeGames": "Juegos", "ViewTypeMusic": "M\u00fasica", - "ViewTypeMusicGenres": "Genres", - "ViewTypeMusicArtists": "Artists", + "ViewTypeMusicGenres": "G\u00e9neros", + "ViewTypeMusicArtists": "Artistas", "ViewTypeBoxSets": "Colecciones", "ViewTypeChannels": "Canales", "ViewTypeLiveTV": "Tv en vivo", - "ViewTypeLiveTvNowPlaying": "Now Airing", - "ViewTypeLatestGames": "Latest Games", - "ViewTypeRecentlyPlayedGames": "Recently Played", - "ViewTypeGameFavorites": "Favorites", - "ViewTypeGameSystems": "Game Systems", - "ViewTypeGameGenres": "Genres", - "ViewTypeTvResume": "Resume", - "ViewTypeTvNextUp": "Next Up", - "ViewTypeTvLatest": "Latest", + "ViewTypeLiveTvNowPlaying": "Transmiti\u00e9ndose ahora", + "ViewTypeLatestGames": "\u00daltimos juegos", + "ViewTypeRecentlyPlayedGames": "Reproducido recientemente", + "ViewTypeGameFavorites": "Favoritos", + "ViewTypeGameSystems": "Sistemas de juego", + "ViewTypeGameGenres": "G\u00e9neros", + "ViewTypeTvResume": "Reanudar", + "ViewTypeTvNextUp": "Pr\u00f3ximamente", + "ViewTypeTvLatest": "\u00daltimas", "ViewTypeTvShowSeries": "Series", - "ViewTypeTvGenres": "Genres", - "ViewTypeTvFavoriteSeries": "Favorite Series", - "ViewTypeTvFavoriteEpisodes": "Favorite Episodes", - "ViewTypeMovieResume": "Resume", - "ViewTypeMovieLatest": "Latest", - "ViewTypeMovieMovies": "Movies", - "ViewTypeMovieCollections": "Collections", - "ViewTypeMovieFavorites": "Favorites", - "ViewTypeMovieGenres": "Genres", - "ViewTypeMusicLatest": "Latest", - "ViewTypeMusicPlaylists": "Playlists", - "ViewTypeMusicAlbums": "Albums", - "ViewTypeMusicAlbumArtists": "Album Artists", + "ViewTypeTvGenres": "G\u00e9neros", + "ViewTypeTvFavoriteSeries": "Series favoritas", + "ViewTypeTvFavoriteEpisodes": "Episodios favoritos", + "ViewTypeMovieResume": "Reanudar", + "ViewTypeMovieLatest": "\u00daltimas", + "ViewTypeMovieMovies": "Pel\u00edculas", + "ViewTypeMovieCollections": "Colecciones", + "ViewTypeMovieFavorites": "Favoritos", + "ViewTypeMovieGenres": "G\u00e9neros", + "ViewTypeMusicLatest": "\u00daltimas", + "ViewTypeMusicPlaylists": "Lista", + "ViewTypeMusicAlbums": "\u00c1lbumes", + "ViewTypeMusicAlbumArtists": "\u00c1lbumes de artistas", "HeaderOtherDisplaySettings": "Configuraci\u00f3n de pantalla", - "ViewTypeMusicSongs": "Songs", - "ViewTypeMusicFavorites": "Favorites", - "ViewTypeMusicFavoriteAlbums": "Favorite Albums", - "ViewTypeMusicFavoriteArtists": "Favorite Artists", - "ViewTypeMusicFavoriteSongs": "Favorite Songs", - "ViewTypeFolders": "Folders", - "ViewTypeLiveTvRecordingGroups": "Recordings", - "ViewTypeLiveTvChannels": "Channels", - "ScheduledTaskFailedWithName": "{0} failed", - "LabelRunningTimeValue": "Running time: {0}", - "ScheduledTaskStartedWithName": "{0} started", + "ViewTypeMusicSongs": "Canciones", + "ViewTypeMusicFavorites": "Favoritos", + "ViewTypeMusicFavoriteAlbums": "\u00c1lbumes favoritos", + "ViewTypeMusicFavoriteArtists": "Artistas favoritos", + "ViewTypeMusicFavoriteSongs": "Canciones favoritas", + "ViewTypeFolders": "Carpetas", + "ViewTypeLiveTvRecordingGroups": "Grabaciones", + "ViewTypeLiveTvChannels": "Canales", + "ScheduledTaskFailedWithName": "{0} fall\u00f3", + "LabelRunningTimeValue": "Tiempo de ejecuci\u00f3n: {0}", + "ScheduledTaskStartedWithName": "{0} iniciado", "VersionNumber": "Versi\u00f3n {0}", - "PluginInstalledWithName": "{0} was installed", - "PluginUpdatedWithName": "{0} was updated", - "PluginUninstalledWithName": "{0} was uninstalled", - "ItemAddedWithName": "{0} was added to the library", - "ItemRemovedWithName": "{0} was removed from the library", - "LabelIpAddressValue": "Ip address: {0}", - "DeviceOnlineWithName": "{0} is connected", - "UserOnlineFromDevice": "{0} is online from {1}", - "ProviderValue": "Provider: {0}", - "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", - "UserConfigurationUpdatedWithName": "User configuration has been updated for {0}", - "UserCreatedWithName": "User {0} has been created", - "UserPasswordChangedWithName": "Password has been changed for user {0}", - "UserDeletedWithName": "User {0} has been deleted", - "MessageServerConfigurationUpdated": "Server configuration has been updated", - "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", - "MessageApplicationUpdated": "Emby Server has been updated", - "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", - "AuthenticationSucceededWithUserName": "{0} successfully authenticated", - "DeviceOfflineWithName": "{0} has disconnected", - "UserLockedOutWithName": "User {0} has been locked out", - "UserOfflineFromDevice": "{0} has disconnected from {1}", - "UserStartedPlayingItemWithValues": "{0} has started playing {1}", - "UserStoppedPlayingItemWithValues": "{0} has stopped playing {1}", - "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", + "PluginInstalledWithName": "{0} ha sido instalado", + "PluginUpdatedWithName": "{0} ha sido actualizado", + "PluginUninstalledWithName": "{0} ha sido desinstalado", + "ItemAddedWithName": "{0} ha sido a\u00f1adido a la biblioteca", + "ItemRemovedWithName": "{0} se ha eliminado de la biblioteca", + "LabelIpAddressValue": "Direcci\u00f3n IP: {0}", + "DeviceOnlineWithName": "{0} est\u00e1 conectado", + "UserOnlineFromDevice": "{0} est\u00e1 conectado desde {1}", + "ProviderValue": "Proveedor: {0}", + "SubtitlesDownloadedForItem": "Subt\u00edtulos descargados para {0}", + "UserConfigurationUpdatedWithName": "Se ha actualizado la configuraci\u00f3n de usuario para {0}", + "UserCreatedWithName": "Se ha creado el usuario {0}", + "UserPasswordChangedWithName": "Contrase\u00f1a cambiada al usuario {0}", + "UserDeletedWithName": "El usuario {0} ha sido eliminado", + "MessageServerConfigurationUpdated": "Se ha actualizado la configuraci\u00f3n del servidor", + "MessageNamedServerConfigurationUpdatedWithValue": "Se ha actualizado la secci\u00f3n {0} de la configuraci\u00f3n del servidor", + "MessageApplicationUpdated": "Se ha actualizado el servidor Emby", + "FailedLoginAttemptWithUserName": "Intento de inicio de sesi\u00f3n fallido desde {0}", + "AuthenticationSucceededWithUserName": "{0} se ha autenticado satisfactoriamente", + "DeviceOfflineWithName": "{0} se ha desconectado", + "UserLockedOutWithName": "El usuario {0} ha sido bloqueado", + "UserOfflineFromDevice": "{0} se ha desconectado de {1}", + "UserStartedPlayingItemWithValues": "{0} ha empezado a reproducir {1}", + "UserStoppedPlayingItemWithValues": "{0} ha parado de reproducir {1}", + "SubtitleDownloadFailureForItem": "Fallo en la descarga de subt\u00edtulos para {0}", "HeaderUnidentified": "Unidentified", "HeaderImagePrimary": "Primary", "HeaderImageBackdrop": "Backdrop", @@ -159,20 +159,20 @@ "HeaderEmbeddedImage": "Embedded image", "HeaderResolution": "Resolution", "HeaderSubtitles": "Subt\u00edtulos", - "HeaderGenres": "Genres", - "HeaderCountries": "Countries", + "HeaderGenres": "G\u00e9neros", + "HeaderCountries": "Paises", "HeaderStatus": "Estado", "HeaderTracks": "Tracks", "HeaderMusicArtist": "Music artist", "HeaderLocked": "Locked", - "HeaderStudios": "Studios", + "HeaderStudios": "Estudios", "HeaderActor": "Actors", "HeaderComposer": "Composers", "HeaderDirector": "Directors", "HeaderGuestStar": "Guest star", "HeaderProducer": "Producers", "HeaderWriter": "Writers", - "HeaderParentalRatings": "Parental Ratings", + "HeaderParentalRatings": "Clasificaci\u00f3n parental", "HeaderCommunityRatings": "Community ratings", "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/hu.json b/MediaBrowser.Server.Implementations/Localization/Core/hu.json index b175ae6c1..2b9d28d8c 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/hu.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/hu.json @@ -33,10 +33,10 @@ "CategoryPlugin": "B\u0151v\u00edtm\u00e9ny", "NotificationOptionPluginError": "B\u0151v\u00edtm\u00e9ny hiba", "NotificationOptionApplicationUpdateAvailable": "Friss\u00edt\u00e9s el\u00e9rhet\u0151", - "NotificationOptionApplicationUpdateInstalled": "Friss\u00edt\u00e9s telep\u00edtve", - "NotificationOptionPluginUpdateInstalled": "B\u0151v\u00edtm\u00e9ny friss\u00edtve", + "NotificationOptionApplicationUpdateInstalled": "Program friss\u00edt\u00e9s telep\u00edtve", + "NotificationOptionPluginUpdateInstalled": "B\u0151v\u00edtm\u00e9ny friss\u00edt\u00e9s telep\u00edtve", "NotificationOptionPluginInstalled": "B\u0151v\u00edtm\u00e9ny telep\u00edtve", - "NotificationOptionPluginUninstalled": "B\u0151v\u00edtm\u00e9ny t\u00f6r\u00f6lve", + "NotificationOptionPluginUninstalled": "B\u0151v\u00edtm\u00e9ny elt\u00e1vol\u00edtva", "NotificationOptionVideoPlayback": "Vide\u00f3 elind\u00edtva", "NotificationOptionAudioPlayback": "Zene elind\u00edtva", "NotificationOptionGamePlayback": "J\u00e1t\u00e9k elind\u00edtva", @@ -169,8 +169,8 @@ "HeaderActor": "Sz\u00edn\u00e9szek", "HeaderComposer": "Zeneszerz\u0151k", "HeaderDirector": "Rendez\u0151k", - "HeaderGuestStar": "Guest star", - "HeaderProducer": "Producers", + "HeaderGuestStar": "Vend\u00e9g szt\u00e1r", + "HeaderProducer": "Producerek", "HeaderWriter": "\u00cdr\u00f3k", "HeaderParentalRatings": "Korhat\u00e1r besorol\u00e1s", "HeaderCommunityRatings": "K\u00f6z\u00f6ss\u00e9gi \u00e9rt\u00e9kel\u00e9sek", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/nl.json b/MediaBrowser.Server.Implementations/Localization/Core/nl.json index a83182ee8..2818fbf6a 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/nl.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/nl.json @@ -1,5 +1,5 @@ { - "DbUpgradeMessage": "Even geduld svp terwijl de Emby Server database ge-upgrade wordt. {0}% gereed.", + "DbUpgradeMessage": "Een ogenblik geduld terwijl uw Emby Server-database wordt bijgewerkt. {0}% voltooid.", "AppDeviceValues": "App: {0}, Apparaat: {1}", "UserDownloadingItemWithValues": "{0} download {1}", "FolderTypeMixed": "Gemengde inhoud", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/ru.json b/MediaBrowser.Server.Implementations/Localization/Core/ru.json index fa68aadb9..62fe3b496 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/ru.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/ru.json @@ -2,17 +2,17 @@ "DbUpgradeMessage": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0431\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0430 \u0432\u0430\u0448\u0435\u043c Emby Server \u043c\u043e\u0434\u0435\u0440\u043d\u0438\u0437\u0438\u0440\u0443\u0435\u0442\u0441\u044f. {0} % \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e.", "AppDeviceValues": "\u041f\u0440\u0438\u043b.: {0}, \u0423\u0441\u0442\u0440.: {1}", "UserDownloadingItemWithValues": "{0} \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442 {1}", - "FolderTypeMixed": "\u0420\u0430\u0437\u043d\u043e\u0442\u0438\u043f\u043d\u043e\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435", + "FolderTypeMixed": "\u0421\u043c\u0435\u0448\u0430\u043d\u043d\u043e\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435", "FolderTypeMovies": "\u041a\u0438\u043d\u043e", "FolderTypeMusic": "\u041c\u0443\u0437\u044b\u043a\u0430", - "FolderTypeAdultVideos": "\u0412\u0438\u0434\u0435\u043e \u0434\u043b\u044f \u0432\u0437\u0440\u043e\u0441\u043b\u044b\u0445", - "FolderTypePhotos": "\u0424\u043e\u0442\u043e\u0433\u0440\u0430\u0444\u0438\u0438", - "FolderTypeMusicVideos": "\u041c\u0443\u0437\u044b\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0432\u0438\u0434\u0435\u043e", - "FolderTypeHomeVideos": "\u0414\u043e\u043c\u0430\u0448\u043d\u0438\u0435 \u0432\u0438\u0434\u0435\u043e", + "FolderTypeAdultVideos": "\u0414\u043b\u044f \u0432\u0437\u0440\u043e\u0441\u043b\u044b\u0445", + "FolderTypePhotos": "\u0424\u043e\u0442\u043e", + "FolderTypeMusicVideos": "\u041c\u0443\u0437. \u0432\u0438\u0434\u0435\u043e", + "FolderTypeHomeVideos": "\u0414\u043e\u043c. \u0432\u0438\u0434\u0435\u043e", "FolderTypeGames": "\u0418\u0433\u0440\u044b", - "FolderTypeBooks": "\u041a\u043d\u0438\u0433\u0438", + "FolderTypeBooks": "\u041b\u0438\u0442\u0435\u0440\u0430\u0442\u0443\u0440\u0430", "FolderTypeTvShows": "\u0422\u0412", - "FolderTypeInherit": "\u041d\u0430\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u0435", + "FolderTypeInherit": "\u041d\u0430\u0441\u043b\u0435\u0434\u0443\u0435\u043c\u044b\u0439", "HeaderCastCrew": "\u0421\u043d\u0438\u043c\u0430\u043b\u0438\u0441\u044c \u0438 \u0441\u043d\u0438\u043c\u0430\u043b\u0438", "HeaderPeople": "\u041b\u044e\u0434\u0438", "ValueSpecialEpisodeName": "\u0421\u043f\u0435\u0446\u044d\u043f\u0438\u0437\u043e\u0434 - {0}", @@ -163,7 +163,7 @@ "HeaderCountries": "\u0421\u0442\u0440\u0430\u043d\u044b", "HeaderStatus": "\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435", "HeaderTracks": "\u0414\u043e\u0440-\u043a\u0438", - "HeaderMusicArtist": "\u0418\u0441\u043f. \u043c\u0443\u0437\u044b\u043a\u0438", + "HeaderMusicArtist": "\u041c\u0443\u0437. \u0438\u0441\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c", "HeaderLocked": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e", "HeaderStudios": "\u0421\u0442\u0443\u0434\u0438\u0438", "HeaderActor": "\u0410\u043a\u0442\u0451\u0440\u044b", diff --git a/MediaBrowser.Server.Implementations/Localization/Core/sv.json b/MediaBrowser.Server.Implementations/Localization/Core/sv.json index f52f656d4..4a6565aff 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/sv.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/sv.json @@ -1,7 +1,7 @@ { - "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", + "DbUpgradeMessage": "V\u00e4nligen v\u00e4nta medan databasen p\u00e5 din Emby Server uppgraderas. {0}% klar", "AppDeviceValues": "App: {0}, enhet: {1}", - "UserDownloadingItemWithValues": "{0} is downloading {1}", + "UserDownloadingItemWithValues": "{0} laddar ned {1}", "FolderTypeMixed": "Blandat inneh\u00e5ll", "FolderTypeMovies": "Filmer", "FolderTypeMusic": "Musik", @@ -15,18 +15,18 @@ "FolderTypeInherit": "\u00c4rv", "HeaderCastCrew": "Rollista & bes\u00e4ttning", "HeaderPeople": "Personer", - "ValueSpecialEpisodeName": "Special - {0}", + "ValueSpecialEpisodeName": "Specialavsnitt - {0}", "LabelChapterName": "Kapitel {0}", - "NameSeasonNumber": "Season {0}", + "NameSeasonNumber": "S\u00e4song {0}", "LabelExit": "Avsluta", "LabelVisitCommunity": "Bes\u00f6k v\u00e5rt diskussionsforum", "LabelGithub": "Github", - "LabelApiDocumentation": "Api Dokumentation", + "LabelApiDocumentation": "Api-dokumentation", "LabelDeveloperResources": "Resurser f\u00f6r utvecklare", "LabelBrowseLibrary": "Bl\u00e4ddra i biblioteket", - "LabelConfigureServer": "Configure Emby", + "LabelConfigureServer": "Konfigurera Emby", "LabelRestartServer": "Starta om servern", - "CategorySync": "Sync", + "CategorySync": "Synkronisera", "CategoryUser": "Anv\u00e4ndare", "CategorySystem": "System", "CategoryApplication": "App", @@ -47,10 +47,10 @@ "NotificationOptionInstallationFailed": "Fel vid installation", "NotificationOptionNewLibraryContent": "Nytt inneh\u00e5ll har tillkommit", "NotificationOptionNewLibraryContentMultiple": "Nytillkommet inneh\u00e5ll finns (flera objekt)", - "NotificationOptionCameraImageUploaded": "Camera image uploaded", - "NotificationOptionUserLockedOut": "User locked out", + "NotificationOptionCameraImageUploaded": "Kaberabild uppladdad", + "NotificationOptionUserLockedOut": "Anv\u00e4ndare har l\u00e5sts ute", "NotificationOptionServerRestartRequired": "Servern m\u00e5ste startas om", - "ViewTypePlaylists": "Playlists", + "ViewTypePlaylists": "Spellistor", "ViewTypeMovies": "Filmer", "ViewTypeTvShows": "TV", "ViewTypeGames": "Spel", @@ -80,10 +80,10 @@ "ViewTypeMovieFavorites": "Favoriter", "ViewTypeMovieGenres": "Genrer", "ViewTypeMusicLatest": "Nytillkommet", - "ViewTypeMusicPlaylists": "Playlists", + "ViewTypeMusicPlaylists": "Spellistor", "ViewTypeMusicAlbums": "Album", "ViewTypeMusicAlbumArtists": "Albumartister", - "HeaderOtherDisplaySettings": "Visningsinst\u00e4llningar", + "HeaderOtherDisplaySettings": "Visningsalternativ", "ViewTypeMusicSongs": "L\u00e5tar", "ViewTypeMusicFavorites": "Favoriter", "ViewTypeMusicFavoriteAlbums": "Favoritalbum", @@ -112,45 +112,45 @@ "UserDeletedWithName": "Anv\u00e4ndaren {0} har tagits bort", "MessageServerConfigurationUpdated": "Server konfigurationen har uppdaterats", "MessageNamedServerConfigurationUpdatedWithValue": "Serverinst\u00e4llningarnas del {0} ar uppdaterats", - "MessageApplicationUpdated": "Emby Server has been updated", + "MessageApplicationUpdated": "Emby Server har uppdaterats", "FailedLoginAttemptWithUserName": "Misslyckat inloggningsf\u00f6rs\u00f6k fr\u00e5n {0}", "AuthenticationSucceededWithUserName": "{0} har autentiserats", "DeviceOfflineWithName": "{0} har avbrutit anslutningen", - "UserLockedOutWithName": "User {0} has been locked out", - "UserOfflineFromDevice": "{0} har kopplats bort fr\u00e5n {1}", + "UserLockedOutWithName": "Anv\u00e4ndare {0} har l\u00e5sts ute", + "UserOfflineFromDevice": "{0} har avbrutit anslutningen fr\u00e5n {1}", "UserStartedPlayingItemWithValues": "{0} har p\u00e5b\u00f6rjat uppspelning av {1}", "UserStoppedPlayingItemWithValues": "{0} har avslutat uppspelning av {1}", "SubtitleDownloadFailureForItem": "Nerladdning av undertexter f\u00f6r {0} misslyckades", - "HeaderUnidentified": "Unidentified", - "HeaderImagePrimary": "Primary", - "HeaderImageBackdrop": "Backdrop", - "HeaderImageLogo": "Logo", - "HeaderUserPrimaryImage": "User Image", - "HeaderOverview": "Overview", - "HeaderShortOverview": "Short Overview", - "HeaderType": "Type", + "HeaderUnidentified": "Oidentifierad", + "HeaderImagePrimary": "Huvudbild", + "HeaderImageBackdrop": "Bakgrundsbild", + "HeaderImageLogo": "Logotyp", + "HeaderUserPrimaryImage": "Anv\u00e4ndarbild", + "HeaderOverview": "\u00d6versikt", + "HeaderShortOverview": "Kort \u00f6versikt", + "HeaderType": "Typ", "HeaderSeverity": "Severity", "HeaderUser": "Anv\u00e4ndare", "HeaderName": "Namn", "HeaderDate": "Datum", - "HeaderPremiereDate": "Premiere Date", + "HeaderPremiereDate": "Premi\u00e4rdatum", "HeaderDateAdded": "Date Added", "HeaderReleaseDate": "Premi\u00e4rdatum:", "HeaderRuntime": "Speltid", - "HeaderPlayCount": "Play Count", + "HeaderPlayCount": "Antal spelningar", "HeaderSeason": "S\u00e4song", "HeaderSeasonNumber": "S\u00e4songsnummer:", - "HeaderSeries": "Series:", + "HeaderSeries": "Serie:", "HeaderNetwork": "TV-bolag", - "HeaderYear": "Year:", - "HeaderYears": "Years:", + "HeaderYear": "\u00c5r:", + "HeaderYears": "\u00c5r:", "HeaderParentalRating": "Parental Rating", "HeaderCommunityRating": "Anv\u00e4ndaromd\u00f6me", "HeaderTrailers": "Trailers", - "HeaderSpecials": "Specialer", + "HeaderSpecials": "Specialavsnitt", "HeaderGameSystems": "Game Systems", - "HeaderPlayers": "Players:", - "HeaderAlbumArtists": "Album Artists", + "HeaderPlayers": "Spelare:", + "HeaderAlbumArtists": "Albumartister", "HeaderAlbums": "Album", "HeaderDisc": "Skiva", "HeaderTrack": "Sp\u00e5r", @@ -163,16 +163,16 @@ "HeaderCountries": "L\u00e4nder", "HeaderStatus": "Status", "HeaderTracks": "Sp\u00e5r", - "HeaderMusicArtist": "Music artist", - "HeaderLocked": "Locked", + "HeaderMusicArtist": "Musikartist", + "HeaderLocked": "L\u00e5st", "HeaderStudios": "Studior", - "HeaderActor": "Actors", - "HeaderComposer": "Composers", - "HeaderDirector": "Directors", - "HeaderGuestStar": "Guest star", - "HeaderProducer": "Producers", - "HeaderWriter": "Writers", + "HeaderActor": "Sk\u00e5despelare", + "HeaderComposer": "Komposit\u00f6rer", + "HeaderDirector": "Regiss\u00f6r", + "HeaderGuestStar": "G\u00e4startist", + "HeaderProducer": "Producenter", + "HeaderWriter": "F\u00f6rfattare", "HeaderParentalRatings": "Parental Ratings", "HeaderCommunityRatings": "Community ratings", - "StartupEmbyServerIsLoading": "Emby Server is loading. Please try again shortly." + "StartupEmbyServerIsLoading": "Emby Server startar. V\u00e4nligen f\u00f6rs\u00f6k igen om en kort stund." }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Localization/Core/zh-TW.json b/MediaBrowser.Server.Implementations/Localization/Core/zh-TW.json index 7f289fc1f..b711aab1f 100644 --- a/MediaBrowser.Server.Implementations/Localization/Core/zh-TW.json +++ b/MediaBrowser.Server.Implementations/Localization/Core/zh-TW.json @@ -1,5 +1,5 @@ { - "DbUpgradeMessage": "Please wait while your Emby Server database is upgraded. {0}% complete.", + "DbUpgradeMessage": "\u8acb\u7a0d\u5019\uff0cEmby\u4f3a\u670d\u5668\u8cc7\u6599\u5eab\u6b63\u5728\u66f4\u65b0...\uff08\u5df2\u5b8c\u6210{0}%\uff09", "AppDeviceValues": "App: {0}, Device: {1}", "UserDownloadingItemWithValues": "{0} is downloading {1}", "FolderTypeMixed": "Mixed content", @@ -19,13 +19,13 @@ "LabelChapterName": "Chapter {0}", "NameSeasonNumber": "Season {0}", "LabelExit": "\u96e2\u958b", - "LabelVisitCommunity": "\u8a2a\u554f\u793e\u5340", - "LabelGithub": "Github", - "LabelApiDocumentation": "Api Documentation", - "LabelDeveloperResources": "Developer Resources", - "LabelBrowseLibrary": "\u700f\u89bd\u5a92\u9ad4\u5eab", - "LabelConfigureServer": "Configure Emby", - "LabelRestartServer": "\u91cd\u65b0\u555f\u52d5\u4f3a\u5668\u670d", + "LabelVisitCommunity": "\u8a2a\u554f\u793e\u7fa4", + "LabelGithub": "GitHub", + "LabelApiDocumentation": "API\u8aaa\u660e\u6587\u4ef6", + "LabelDeveloperResources": "\u958b\u767c\u4eba\u54e1\u5c08\u5340", + "LabelBrowseLibrary": "\u700f\u89bd\u5a92\u9ad4\u6ac3", + "LabelConfigureServer": "Emby\u8a2d\u5b9a", + "LabelRestartServer": "\u91cd\u65b0\u555f\u52d5\u4f3a\u670d\u5668", "CategorySync": "Sync", "CategoryUser": "User", "CategorySystem": "System", @@ -59,7 +59,7 @@ "ViewTypeMusicArtists": "Artists", "ViewTypeBoxSets": "Collections", "ViewTypeChannels": "Channels", - "ViewTypeLiveTV": "Live TV", + "ViewTypeLiveTV": "\u96fb\u8996", "ViewTypeLiveTvNowPlaying": "Now Airing", "ViewTypeLatestGames": "Latest Games", "ViewTypeRecentlyPlayedGames": "Recently Played", diff --git a/MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs b/MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs index 0c627d751..ec544dd70 100644 --- a/MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs +++ b/MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs @@ -12,6 +12,7 @@ using System.IO; using System.Linq; using System.Reflection; using CommonIO; +using MediaBrowser.Model.Logging; namespace MediaBrowser.Server.Implementations.Localization { @@ -35,6 +36,7 @@ namespace MediaBrowser.Server.Implementations.Localization private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; /// <summary> /// Initializes a new instance of the <see cref="LocalizationManager" /> class. @@ -42,11 +44,12 @@ namespace MediaBrowser.Server.Implementations.Localization /// <param name="configurationManager">The configuration manager.</param> /// <param name="fileSystem">The file system.</param> /// <param name="jsonSerializer">The json serializer.</param> - public LocalizationManager(IServerConfigurationManager configurationManager, IFileSystem fileSystem, IJsonSerializer jsonSerializer) + public LocalizationManager(IServerConfigurationManager configurationManager, IFileSystem fileSystem, IJsonSerializer jsonSerializer, ILogger logger) { _configurationManager = configurationManager; _fileSystem = fileSystem; _jsonSerializer = jsonSerializer; + _logger = logger; ExtractAll(); } @@ -75,7 +78,10 @@ namespace MediaBrowser.Server.Implementations.Localization { using (var stream = type.Assembly.GetManifestResourceStream(resource)) { - using (var fs = _fileSystem.GetFileStream(Path.Combine(localizationPath, filename), FileMode.Create, FileAccess.Write, FileShare.Read)) + var target = Path.Combine(localizationPath, filename); + _logger.Info("Extracting ratings to {0}", target); + + using (var fs = _fileSystem.GetFileStream(target, FileMode.Create, FileAccess.Write, FileShare.Read)) { stream.CopyTo(fs); } diff --git a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj index 97f090ab2..0f91d5285 100644 --- a/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj +++ b/MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj @@ -46,18 +46,19 @@ <HintPath>..\packages\CommonIO.1.0.0.9\lib\net45\CommonIO.dll</HintPath> </Reference> <Reference Include="Emby.XmlTv, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\Emby.XmlTv.1.0.0.48\lib\net45\Emby.XmlTv.dll</HintPath> + <HintPath>..\packages\Emby.XmlTv.1.0.0.55\lib\net45\Emby.XmlTv.dll</HintPath> + <Private>True</Private> </Reference> - <Reference Include="INIFileParser"> - <HintPath>..\packages\ini-parser.2.2.4\lib\net20\INIFileParser.dll</HintPath> + <Reference Include="INIFileParser, Version=2.3.0.0, Culture=neutral, PublicKeyToken=79af7b307b65cf3c, processorArchitecture=MSIL"> + <HintPath>..\packages\ini-parser.2.3.0\lib\net20\INIFileParser.dll</HintPath> + <Private>True</Private> </Reference> <Reference Include="Interfaces.IO"> <HintPath>..\packages\Interfaces.IO.1.0.0.5\lib\portable-net45+sl4+wp71+win8+wpa81\Interfaces.IO.dll</HintPath> </Reference> - <Reference Include="MediaBrowser.Naming, Version=1.0.5917.1514, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\MediaBrowser.Naming.1.0.0.49\lib\portable-net45+sl4+wp71+win8+wpa81\MediaBrowser.Naming.dll</HintPath> + <Reference Include="MediaBrowser.Naming, Version=1.0.6046.32295, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\MediaBrowser.Naming.1.0.0.53\lib\portable-net45+sl4+wp71+win8+wpa81\MediaBrowser.Naming.dll</HintPath> + <Private>True</Private> </Reference> <Reference Include="MoreLinq"> <HintPath>..\packages\morelinq.1.4.0\lib\net35\MoreLinq.dll</HintPath> @@ -68,15 +69,16 @@ <Reference Include="ServiceStack.Api.Swagger"> <HintPath>..\ThirdParty\ServiceStack\ServiceStack.Api.Swagger.dll</HintPath> </Reference> - <Reference Include="SocketHttpListener, Version=1.0.5908.28560, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\SocketHttpListener.1.0.0.29\lib\net45\SocketHttpListener.dll</HintPath> + <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> + <Private>True</Private> + </Reference> + <Reference Include="SocketHttpListener, Version=1.0.6046.26351, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\SocketHttpListener.1.0.0.35\lib\net45\SocketHttpListener.dll</HintPath> + <Private>True</Private> </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> - <Reference Include="System.Data.SQLite"> - <HintPath>..\ThirdParty\System.Data.SQLite.ManagedOnly\1.0.94.0\System.Data.SQLite.dll</HintPath> - </Reference> <Reference Include="Microsoft.CSharp" /> <Reference Include="System.Data" /> <Reference Include="System.Net" /> @@ -99,6 +101,7 @@ <Reference Include="ServiceStack.Text"> <HintPath>..\ThirdParty\ServiceStack.Text\ServiceStack.Text.dll</HintPath> </Reference> + <Reference Include="System.Xml.Linq" /> <Reference Include="UniversalDetector"> <HintPath>..\ThirdParty\UniversalDetector\UniversalDetector.dll</HintPath> </Reference> @@ -139,6 +142,7 @@ <Compile Include="EntryPoints\LoadRegistrations.cs" /> <Compile Include="EntryPoints\Notifications\Notifications.cs" /> <Compile Include="EntryPoints\Notifications\WebSocketNotifier.cs" /> + <Compile Include="EntryPoints\RecordingNotifier.cs" /> <Compile Include="EntryPoints\RefreshUsersMetadata.cs" /> <Compile Include="EntryPoints\UsageEntryPoint.cs" /> <Compile Include="Connect\ConnectEntryPoint.cs" /> @@ -152,6 +156,7 @@ <Compile Include="EntryPoints\ServerEventNotifier.cs" /> <Compile Include="EntryPoints\UserDataChangeNotifier.cs" /> <Compile Include="FileOrganization\OrganizerScheduledTask.cs" /> + <Compile Include="HttpServer\AsyncStreamWriterFunc.cs" /> <Compile Include="HttpServer\IHttpListener.cs" /> <Compile Include="HttpServer\Security\AuthorizationContext.cs" /> <Compile Include="HttpServer\ContainerAdapter.cs" /> @@ -178,6 +183,7 @@ <Compile Include="HttpServer\SocketSharp\WebSocketSharpRequest.cs" /> <Compile Include="HttpServer\SocketSharp\WebSocketSharpResponse.cs" /> <Compile Include="Intros\DefaultIntroProvider.cs" /> + <Compile Include="IO\FileRefresher.cs" /> <Compile Include="IO\LibraryMonitor.cs" /> <Compile Include="Library\CoreResolutionIgnoreRule.cs" /> <Compile Include="Library\LibraryManager.cs" /> @@ -221,6 +227,7 @@ <Compile Include="LiveTv\ChannelImageProvider.cs" /> <Compile Include="LiveTv\EmbyTV\DirectRecorder.cs" /> <Compile Include="LiveTv\EmbyTV\EmbyTV.cs" /> + <Compile Include="LiveTv\EmbyTV\EmbyTVRegistration.cs" /> <Compile Include="LiveTv\EmbyTV\EncodedRecorder.cs" /> <Compile Include="LiveTv\EmbyTV\EntryPoint.cs" /> <Compile Include="LiveTv\EmbyTV\IRecorder.cs" /> @@ -229,7 +236,7 @@ <Compile Include="LiveTv\EmbyTV\SeriesTimerManager.cs" /> <Compile Include="LiveTv\EmbyTV\TimerManager.cs" /> <Compile Include="LiveTv\Listings\SchedulesDirect.cs" /> - <Compile Include="LiveTv\Listings\XmlTv.cs" /> + <Compile Include="LiveTv\Listings\XmlTvListingsProvider.cs" /> <Compile Include="LiveTv\LiveTvConfigurationFactory.cs" /> <Compile Include="LiveTv\LiveTvDtoService.cs" /> <Compile Include="LiveTv\LiveTvManager.cs" /> @@ -242,6 +249,12 @@ <Compile Include="LiveTv\ProgramImageProvider.cs" /> <Compile Include="LiveTv\RecordingImageProvider.cs" /> <Compile Include="LiveTv\RefreshChannelsScheduledTask.cs" /> + <Compile Include="LiveTv\TunerHosts\SatIp\ChannelScan.cs" /> + <Compile Include="LiveTv\TunerHosts\SatIp\Rtsp\RtspMethod.cs" /> + <Compile Include="LiveTv\TunerHosts\SatIp\Rtsp\RtspRequest.cs" /> + <Compile Include="LiveTv\TunerHosts\SatIp\Rtsp\RtspResponse.cs" /> + <Compile Include="LiveTv\TunerHosts\SatIp\Rtsp\RtspSession.cs" /> + <Compile Include="LiveTv\TunerHosts\SatIp\Rtsp\RtspStatusCode.cs" /> <Compile Include="LiveTv\TunerHosts\SatIp\SatIpHost.cs" /> <Compile Include="LiveTv\TunerHosts\SatIp\SatIpDiscovery.cs" /> <Compile Include="Localization\LocalizationManager.cs" /> @@ -250,6 +263,8 @@ <Compile Include="Notifications\IConfigurableNotificationService.cs" /> <Compile Include="Persistence\BaseSqliteRepository.cs" /> <Compile Include="Persistence\CleanDatabaseScheduledTask.cs" /> + <Compile Include="Persistence\DataExtensions.cs" /> + <Compile Include="Persistence\IDbConnector.cs" /> <Compile Include="Persistence\MediaStreamColumns.cs" /> <Compile Include="Social\SharingManager.cs" /> <Compile Include="Social\SharingRepository.cs" /> @@ -264,10 +279,8 @@ <Compile Include="Notifications\InternalNotificationService.cs" /> <Compile Include="Notifications\NotificationConfigurationFactory.cs" /> <Compile Include="Notifications\NotificationManager.cs" /> - <Compile Include="Persistence\SqliteExtensions.cs" /> <Compile Include="Persistence\SqliteFileOrganizationRepository.cs" /> <Compile Include="Notifications\SqliteNotificationsRepository.cs" /> - <Compile Include="Persistence\SqliteProviderInfoRepository.cs" /> <Compile Include="Persistence\TypeMapper.cs" /> <Compile Include="Photos\BaseDynamicImageProvider.cs" /> <Compile Include="Playlists\ManualPlaylistsFolder.cs" /> @@ -381,7 +394,103 @@ <EmbeddedResource Include="Localization\Ratings\ru.txt" /> </ItemGroup> <ItemGroup> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\backbone-min.js"> + <Link>swagger-ui\lib\backbone-min.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\handlebars-2.0.0.js"> + <Link>swagger-ui\lib\handlebars-2.0.0.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\highlight.7.3.pack.js"> + <Link>swagger-ui\lib\highlight.7.3.pack.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\jquery-1.8.0.min.js"> + <Link>swagger-ui\lib\jquery-1.8.0.min.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\jquery.ba-bbq.min.js"> + <Link>swagger-ui\lib\jquery.ba-bbq.min.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\jquery.slideto.min.js"> + <Link>swagger-ui\lib\jquery.slideto.min.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\jquery.wiggle.min.js"> + <Link>swagger-ui\lib\jquery.wiggle.min.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\marked.js"> + <Link>swagger-ui\lib\marked.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\shred.bundle.js"> + <Link>swagger-ui\lib\shred.bundle.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\swagger-client.js"> + <Link>swagger-ui\lib\swagger-client.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\swagger-oauth.js"> + <Link>swagger-ui\lib\swagger-oauth.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\underscore-min.js"> + <Link>swagger-ui\lib\underscore-min.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\o2c.html"> + <Link>swagger-ui\o2c.html</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\patch.js"> + <Link>swagger-ui\patch.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\swagger-ui.js"> + <Link>swagger-ui\swagger-ui.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\swagger-ui.min.js"> + <Link>swagger-ui\swagger-ui.min.js</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> <EmbeddedResource Include="Localization\countries.json" /> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\fonts\droid-sans-v6-latin-700.eot"> + <Link>swagger-ui\fonts\droid-sans-v6-latin-700.eot</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\fonts\droid-sans-v6-latin-700.ttf"> + <Link>swagger-ui\fonts\droid-sans-v6-latin-700.ttf</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\fonts\droid-sans-v6-latin-700.woff"> + <Link>swagger-ui\fonts\droid-sans-v6-latin-700.woff</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\fonts\droid-sans-v6-latin-700.woff2"> + <Link>swagger-ui\fonts\droid-sans-v6-latin-700.woff2</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\fonts\droid-sans-v6-latin-regular.eot"> + <Link>swagger-ui\fonts\droid-sans-v6-latin-regular.eot</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\fonts\droid-sans-v6-latin-regular.ttf"> + <Link>swagger-ui\fonts\droid-sans-v6-latin-regular.ttf</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\fonts\droid-sans-v6-latin-regular.woff"> + <Link>swagger-ui\fonts\droid-sans-v6-latin-regular.woff</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\fonts\droid-sans-v6-latin-regular.woff2"> + <Link>swagger-ui\fonts\droid-sans-v6-latin-regular.woff2</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> <None Include="app.config" /> <EmbeddedResource Include="Localization\Core\core.json" /> <EmbeddedResource Include="Localization\Core\ar.json" /> @@ -598,10 +707,30 @@ <EmbeddedResource Include="Localization\Ratings\ca.txt" /> </ItemGroup> <ItemGroup> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\css\reset.css"> + <Link>swagger-ui\css\reset.css</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> <Content Include="..\ThirdParty\ServiceStack\swagger-ui\css\screen.css"> <Link>swagger-ui\css\screen.css</Link> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\css\typography.css"> + <Link>swagger-ui\css\typography.css</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\fonts\droid-sans-v6-latin-700.svg"> + <Link>swagger-ui\fonts\droid-sans-v6-latin-700.svg</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\fonts\droid-sans-v6-latin-regular.svg"> + <Link>swagger-ui\fonts\droid-sans-v6-latin-regular.svg</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> + <Content Include="..\ThirdParty\ServiceStack\swagger-ui\images\explorer_icons.png"> + <Link>swagger-ui\images\explorer_icons.png</Link> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </Content> <Content Include="..\ThirdParty\ServiceStack\swagger-ui\images\logo_small.png"> <Link>swagger-ui\images\logo_small.png</Link> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> @@ -622,64 +751,14 @@ <Link>swagger-ui\index.html</Link> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> - <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\backbone-min.js"> - <Link>swagger-ui\lib\backbone-min.js</Link> - <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - </Content> - <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\handlebars-1.0.0.js"> - <Link>swagger-ui\lib\handlebars-1.0.0.js</Link> - <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - </Content> - <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\highlight.7.3.pack.js"> - <Link>swagger-ui\lib\highlight.7.3.pack.js</Link> - <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - </Content> - <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\jquery-1.8.0.min.js"> - <Link>swagger-ui\lib\jquery-1.8.0.min.js</Link> - <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - </Content> - <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\jquery.ba-bbq.min.js"> - <Link>swagger-ui\lib\jquery.ba-bbq.min.js</Link> - <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - </Content> - <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\jquery.slideto.min.js"> - <Link>swagger-ui\lib\jquery.slideto.min.js</Link> - <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - </Content> - <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\jquery.wiggle.min.js"> - <Link>swagger-ui\lib\jquery.wiggle.min.js</Link> - <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - </Content> - <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\shred.bundle.js"> - <Link>swagger-ui\lib\shred.bundle.js</Link> - <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - </Content> <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\shred\content.js"> <Link>swagger-ui\lib\shred\content.js</Link> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> - <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\swagger.js"> - <Link>swagger-ui\lib\swagger.js</Link> - <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - </Content> - <Content Include="..\ThirdParty\ServiceStack\swagger-ui\lib\underscore-min.js"> - <Link>swagger-ui\lib\underscore-min.js</Link> - <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - </Content> - <Content Include="..\ThirdParty\ServiceStack\swagger-ui\swagger-ui.js"> - <Link>swagger-ui\swagger-ui.js</Link> - <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - </Content> - <Content Include="..\ThirdParty\ServiceStack\swagger-ui\swagger-ui.min.js"> - <Link>swagger-ui\swagger-ui.min.js</Link> - <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> - </Content> <EmbeddedResource Include="Localization\iso6392.txt" /> <EmbeddedResource Include="Localization\Ratings\be.txt" /> </ItemGroup> - <ItemGroup> - <Folder Include="HttpServer\NetListener\" /> - </ItemGroup> + <ItemGroup /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. Other similar extension points exist, see Microsoft.Common.targets. diff --git a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs index a7b0d61c7..7f709d084 100644 --- a/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/MediaBrowser.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -139,15 +139,20 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder { _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); - using (var stream = await _encoder.ExtractVideoImage(inputPath, protocol, video.Video3DFormat, time, cancellationToken).ConfigureAwait(false)) + var tempFile = await _encoder.ExtractVideoImage(inputPath, protocol, video.Video3DFormat, time, cancellationToken).ConfigureAwait(false); + File.Copy(tempFile, path, true); + + try + { + File.Delete(tempFile); + } + catch { - using (var fileStream = _fileSystem.GetFileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, true)) - { - await stream.CopyToAsync(fileStream).ConfigureAwait(false); - } + } chapter.ImagePath = path; + chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path); changesMade = true; } catch (Exception ex) @@ -166,6 +171,7 @@ namespace MediaBrowser.Server.Implementations.MediaEncoder else if (!string.Equals(path, chapter.ImagePath, StringComparison.OrdinalIgnoreCase)) { chapter.ImagePath = path; + chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path); changesMade = true; } } diff --git a/MediaBrowser.Server.Implementations/Notifications/SqliteNotificationsRepository.cs b/MediaBrowser.Server.Implementations/Notifications/SqliteNotificationsRepository.cs index 7302431e1..be8c6d48d 100644 --- a/MediaBrowser.Server.Implementations/Notifications/SqliteNotificationsRepository.cs +++ b/MediaBrowser.Server.Implementations/Notifications/SqliteNotificationsRepository.cs @@ -15,73 +15,28 @@ namespace MediaBrowser.Server.Implementations.Notifications { public class SqliteNotificationsRepository : BaseSqliteRepository, INotificationsRepository { - private IDbConnection _connection; - private readonly IServerApplicationPaths _appPaths; + public SqliteNotificationsRepository(ILogManager logManager, IServerApplicationPaths appPaths, IDbConnector dbConnector) : base(logManager, dbConnector) + { + DbFilePath = Path.Combine(appPaths.DataPath, "notifications.db"); + } public event EventHandler<NotificationUpdateEventArgs> NotificationAdded; public event EventHandler<NotificationReadEventArgs> NotificationsMarkedRead; public event EventHandler<NotificationUpdateEventArgs> NotificationUpdated; - private IDbCommand _replaceNotificationCommand; - private IDbCommand _markReadCommand; - private IDbCommand _markAllReadCommand; - - public SqliteNotificationsRepository(ILogManager logManager, IServerApplicationPaths appPaths) - : base(logManager) - { - _appPaths = appPaths; - } - public async Task Initialize() { - var dbFile = Path.Combine(_appPaths.DataPath, "notifications.db"); - - _connection = await SqliteExtensions.ConnectToDb(dbFile, Logger).ConfigureAwait(false); - - string[] queries = { + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + string[] queries = { "create table if not exists Notifications (Id GUID NOT NULL, UserId GUID NOT NULL, Date DATETIME NOT NULL, Name TEXT NOT NULL, Description TEXT, Url TEXT, Level TEXT NOT NULL, IsRead BOOLEAN NOT NULL, Category TEXT NOT NULL, RelatedId TEXT, PRIMARY KEY (Id, UserId))", - "create index if not exists idx_Notifications on Notifications(Id, UserId)", - - //pragmas - "pragma temp_store = memory", - - "pragma shrink_memory" + "create index if not exists idx_Notifications1 on Notifications(Id)", + "create index if not exists idx_Notifications2 on Notifications(UserId)" }; - _connection.RunQueries(queries, Logger); - - PrepareStatements(); - } - - private void PrepareStatements() - { - _replaceNotificationCommand = _connection.CreateCommand(); - _replaceNotificationCommand.CommandText = "replace into Notifications (Id, UserId, Date, Name, Description, Url, Level, IsRead, Category, RelatedId) values (@Id, @UserId, @Date, @Name, @Description, @Url, @Level, @IsRead, @Category, @RelatedId)"; - - _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@Id"); - _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@UserId"); - _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@Date"); - _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@Name"); - _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@Description"); - _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@Url"); - _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@Level"); - _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@IsRead"); - _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@Category"); - _replaceNotificationCommand.Parameters.Add(_replaceNotificationCommand, "@RelatedId"); - - _markReadCommand = _connection.CreateCommand(); - _markReadCommand.CommandText = "update Notifications set IsRead=@IsRead where Id=@Id and UserId=@UserId"; - - _markReadCommand.Parameters.Add(_replaceNotificationCommand, "@UserId"); - _markReadCommand.Parameters.Add(_replaceNotificationCommand, "@IsRead"); - _markReadCommand.Parameters.Add(_replaceNotificationCommand, "@Id"); - - _markAllReadCommand = _connection.CreateCommand(); - _markAllReadCommand.CommandText = "update Notifications set IsRead=@IsRead where UserId=@UserId"; - - _markAllReadCommand.Parameters.Add(_replaceNotificationCommand, "@UserId"); - _markAllReadCommand.Parameters.Add(_replaceNotificationCommand, "@IsRead"); + connection.RunQueries(queries, Logger); + } } /// <summary> @@ -93,49 +48,52 @@ namespace MediaBrowser.Server.Implementations.Notifications { var result = new NotificationResult(); - using (var cmd = _connection.CreateCommand()) + using (var connection = CreateConnection(true).Result) { - var clauses = new List<string>(); - - if (query.IsRead.HasValue) + using (var cmd = connection.CreateCommand()) { - clauses.Add("IsRead=@IsRead"); - cmd.Parameters.Add(cmd, "@IsRead", DbType.Boolean).Value = query.IsRead.Value; - } + var clauses = new List<string>(); - clauses.Add("UserId=@UserId"); - cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = new Guid(query.UserId); + if (query.IsRead.HasValue) + { + clauses.Add("IsRead=@IsRead"); + cmd.Parameters.Add(cmd, "@IsRead", DbType.Boolean).Value = query.IsRead.Value; + } - var whereClause = " where " + string.Join(" And ", clauses.ToArray()); + clauses.Add("UserId=@UserId"); + cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = new Guid(query.UserId); - cmd.CommandText = string.Format("select count(Id) from Notifications{0};select Id,UserId,Date,Name,Description,Url,Level,IsRead,Category,RelatedId from Notifications{0} order by IsRead asc, Date desc", whereClause); + var whereClause = " where " + string.Join(" And ", clauses.ToArray()); - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) - { - if (reader.Read()) - { - result.TotalRecordCount = reader.GetInt32(0); - } + cmd.CommandText = string.Format("select count(Id) from Notifications{0};select Id,UserId,Date,Name,Description,Url,Level,IsRead,Category,RelatedId from Notifications{0} order by IsRead asc, Date desc", whereClause); - if (reader.NextResult()) + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) { - var notifications = GetNotifications(reader); - - if (query.StartIndex.HasValue) + if (reader.Read()) { - notifications = notifications.Skip(query.StartIndex.Value); + result.TotalRecordCount = reader.GetInt32(0); } - if (query.Limit.HasValue) + if (reader.NextResult()) { - notifications = notifications.Take(query.Limit.Value); - } + var notifications = GetNotifications(reader); + + if (query.StartIndex.HasValue) + { + notifications = notifications.Skip(query.StartIndex.Value); + } + + if (query.Limit.HasValue) + { + notifications = notifications.Take(query.Limit.Value); + } - result.Notifications = notifications.ToArray(); + result.Notifications = notifications.ToArray(); + } } - } - return result; + return result; + } } } @@ -143,31 +101,34 @@ namespace MediaBrowser.Server.Implementations.Notifications { var result = new NotificationsSummary(); - using (var cmd = _connection.CreateCommand()) + using (var connection = CreateConnection(true).Result) { - cmd.CommandText = "select Level from Notifications where UserId=@UserId and IsRead=@IsRead"; - - cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = new Guid(userId); - cmd.Parameters.Add(cmd, "@IsRead", DbType.Boolean).Value = false; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + using (var cmd = connection.CreateCommand()) { - var levels = new List<NotificationLevel>(); + cmd.CommandText = "select Level from Notifications where UserId=@UserId and IsRead=@IsRead"; - while (reader.Read()) + cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = new Guid(userId); + cmd.Parameters.Add(cmd, "@IsRead", DbType.Boolean).Value = false; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) { - levels.Add(GetLevel(reader, 0)); - } + var levels = new List<NotificationLevel>(); + + while (reader.Read()) + { + levels.Add(GetLevel(reader, 0)); + } - result.UnreadCount = levels.Count; + result.UnreadCount = levels.Count; - if (levels.Count > 0) - { - result.MaxUnreadNotificationLevel = levels.Max(); + if (levels.Count > 0) + { + result.MaxUnreadNotificationLevel = levels.Max(); + } } - } - return result; + return result; + } } } @@ -178,10 +139,14 @@ namespace MediaBrowser.Server.Implementations.Notifications /// <returns>IEnumerable{Notification}.</returns> private IEnumerable<Notification> GetNotifications(IDataReader reader) { + var list = new List<Notification>(); + while (reader.Read()) { - yield return GetNotification(reader); + list.Add(GetNotification(reader)); } + + return list; } private Notification GetNotification(IDataReader reader) @@ -272,59 +237,74 @@ namespace MediaBrowser.Server.Implementations.Notifications cancellationToken.ThrowIfCancellationRequested(); - await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - IDbTransaction transaction = null; - - try + using (var connection = await CreateConnection().ConfigureAwait(false)) { - transaction = _connection.BeginTransaction(); + using (var replaceNotificationCommand = connection.CreateCommand()) + { + replaceNotificationCommand.CommandText = "replace into Notifications (Id, UserId, Date, Name, Description, Url, Level, IsRead, Category, RelatedId) values (@Id, @UserId, @Date, @Name, @Description, @Url, @Level, @IsRead, @Category, @RelatedId)"; + + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@Id"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@UserId"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@Date"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@Name"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@Description"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@Url"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@Level"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@IsRead"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@Category"); + replaceNotificationCommand.Parameters.Add(replaceNotificationCommand, "@RelatedId"); + + IDbTransaction transaction = null; + + try + { + transaction = connection.BeginTransaction(); - _replaceNotificationCommand.GetParameter(0).Value = new Guid(notification.Id); - _replaceNotificationCommand.GetParameter(1).Value = new Guid(notification.UserId); - _replaceNotificationCommand.GetParameter(2).Value = notification.Date.ToUniversalTime(); - _replaceNotificationCommand.GetParameter(3).Value = notification.Name; - _replaceNotificationCommand.GetParameter(4).Value = notification.Description; - _replaceNotificationCommand.GetParameter(5).Value = notification.Url; - _replaceNotificationCommand.GetParameter(6).Value = notification.Level.ToString(); - _replaceNotificationCommand.GetParameter(7).Value = notification.IsRead; - _replaceNotificationCommand.GetParameter(8).Value = string.Empty; - _replaceNotificationCommand.GetParameter(9).Value = string.Empty; + replaceNotificationCommand.GetParameter(0).Value = new Guid(notification.Id); + replaceNotificationCommand.GetParameter(1).Value = new Guid(notification.UserId); + replaceNotificationCommand.GetParameter(2).Value = notification.Date.ToUniversalTime(); + replaceNotificationCommand.GetParameter(3).Value = notification.Name; + replaceNotificationCommand.GetParameter(4).Value = notification.Description; + replaceNotificationCommand.GetParameter(5).Value = notification.Url; + replaceNotificationCommand.GetParameter(6).Value = notification.Level.ToString(); + replaceNotificationCommand.GetParameter(7).Value = notification.IsRead; + replaceNotificationCommand.GetParameter(8).Value = string.Empty; + replaceNotificationCommand.GetParameter(9).Value = string.Empty; - _replaceNotificationCommand.Transaction = transaction; + replaceNotificationCommand.Transaction = transaction; - _replaceNotificationCommand.ExecuteNonQuery(); + replaceNotificationCommand.ExecuteNonQuery(); - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save notification:", e); + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save notification:", e); - if (transaction != null) - { - transaction.Rollback(); - } + if (transaction != null) + { + transaction.Rollback(); + } - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } } - - WriteLock.Release(); } } @@ -365,51 +345,58 @@ namespace MediaBrowser.Server.Implementations.Notifications { cancellationToken.ThrowIfCancellationRequested(); - await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + using (var markAllReadCommand = connection.CreateCommand()) + { + markAllReadCommand.CommandText = "update Notifications set IsRead=@IsRead where UserId=@UserId"; - IDbTransaction transaction = null; + markAllReadCommand.Parameters.Add(markAllReadCommand, "@UserId"); + markAllReadCommand.Parameters.Add(markAllReadCommand, "@IsRead"); - try - { - cancellationToken.ThrowIfCancellationRequested(); + IDbTransaction transaction = null; - transaction = _connection.BeginTransaction(); + try + { + cancellationToken.ThrowIfCancellationRequested(); - _markAllReadCommand.GetParameter(0).Value = new Guid(userId); - _markAllReadCommand.GetParameter(1).Value = isRead; + transaction = connection.BeginTransaction(); - _markAllReadCommand.ExecuteNonQuery(); + markAllReadCommand.GetParameter(0).Value = new Guid(userId); + markAllReadCommand.GetParameter(1).Value = isRead; - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } + markAllReadCommand.ExecuteNonQuery(); - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save notification:", e); + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } - if (transaction != null) - { - transaction.Rollback(); - } + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save notification:", e); - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } + if (transaction != null) + { + transaction.Rollback(); + } - WriteLock.Release(); + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } } } @@ -417,72 +404,66 @@ namespace MediaBrowser.Server.Implementations.Notifications { cancellationToken.ThrowIfCancellationRequested(); - await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - IDbTransaction transaction = null; - - try + using (var connection = await CreateConnection().ConfigureAwait(false)) { - cancellationToken.ThrowIfCancellationRequested(); + using (var markReadCommand = connection.CreateCommand()) + { + markReadCommand.CommandText = "update Notifications set IsRead=@IsRead where Id=@Id and UserId=@UserId"; - transaction = _connection.BeginTransaction(); + markReadCommand.Parameters.Add(markReadCommand, "@UserId"); + markReadCommand.Parameters.Add(markReadCommand, "@IsRead"); + markReadCommand.Parameters.Add(markReadCommand, "@Id"); - _markReadCommand.GetParameter(0).Value = new Guid(userId); - _markReadCommand.GetParameter(1).Value = isRead; + IDbTransaction transaction = null; - foreach (var id in notificationIdList) - { - _markReadCommand.GetParameter(2).Value = id; + try + { + cancellationToken.ThrowIfCancellationRequested(); - _markReadCommand.Transaction = transaction; + transaction = connection.BeginTransaction(); - _markReadCommand.ExecuteNonQuery(); - } + markReadCommand.GetParameter(0).Value = new Guid(userId); + markReadCommand.GetParameter(1).Value = isRead; - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } + foreach (var id in notificationIdList) + { + markReadCommand.GetParameter(2).Value = id; - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save notification:", e); + markReadCommand.Transaction = transaction; - if (transaction != null) - { - transaction.Rollback(); - } + markReadCommand.ExecuteNonQuery(); + } - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } - WriteLock.Release(); - } - } + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save notification:", e); - protected override void CloseConnection() - { - if (_connection != null) - { - if (_connection.IsOpen()) - { - _connection.Close(); - } + if (transaction != null) + { + transaction.Rollback(); + } - _connection.Dispose(); - _connection = null; + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } } } } diff --git a/MediaBrowser.Server.Implementations/Persistence/BaseSqliteRepository.cs b/MediaBrowser.Server.Implementations/Persistence/BaseSqliteRepository.cs index 395907844..233ab56fe 100644 --- a/MediaBrowser.Server.Implementations/Persistence/BaseSqliteRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/BaseSqliteRepository.cs @@ -8,14 +8,36 @@ namespace MediaBrowser.Server.Implementations.Persistence { public abstract class BaseSqliteRepository : IDisposable { - protected readonly SemaphoreSlim WriteLock = new SemaphoreSlim(1, 1); + protected SemaphoreSlim WriteLock = new SemaphoreSlim(1, 1); + protected readonly IDbConnector DbConnector; protected ILogger Logger; - protected BaseSqliteRepository(ILogManager logManager) + protected string DbFilePath { get; set; } + + protected BaseSqliteRepository(ILogManager logManager, IDbConnector dbConnector) { + DbConnector = dbConnector; Logger = logManager.GetLogger(GetType().Name); } + protected virtual bool EnableConnectionPooling + { + get { return true; } + } + + protected virtual async Task<IDbConnection> CreateConnection(bool isReadOnly = false) + { + var connection = await DbConnector.Connect(DbFilePath, false, true).ConfigureAwait(false); + + connection.RunQueries(new[] + { + "pragma temp_store = memory" + + }, Logger); + + return connection; + } + private bool _disposed; protected void CheckDisposed() { @@ -84,6 +106,9 @@ namespace MediaBrowser.Server.Implementations.Persistence } } - protected abstract void CloseConnection(); + protected virtual void CloseConnection() + { + + } } -} +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs b/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs index 3c8a0ffeb..bf2afb5ac 100644 --- a/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs @@ -15,6 +15,7 @@ using System.Threading.Tasks; using CommonIO; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Net; using MediaBrowser.Server.Implementations.ScheduledTasks; @@ -32,7 +33,7 @@ namespace MediaBrowser.Server.Implementations.Persistence private readonly ILocalizationManager _localization; private readonly ITaskManager _taskManager; - public const int MigrationVersion = 20; + public const int MigrationVersion = 23; public static bool EnableUnavailableMessage = false; public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IHttpServer httpServer, ILocalizationManager localization, ITaskManager taskManager) @@ -110,6 +111,12 @@ namespace MediaBrowser.Server.Implementations.Persistence _config.SaveConfiguration(); } + if (_config.Configuration.SchemaVersion < SqliteItemRepository.LatestSchemaVersion) + { + _config.Configuration.SchemaVersion = SqliteItemRepository.LatestSchemaVersion; + _config.SaveConfiguration(); + } + if (EnableUnavailableMessage) { EnableUnavailableMessage = false; @@ -139,7 +146,8 @@ namespace MediaBrowser.Server.Implementations.Persistence { var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery { - IsCurrentSchema = false + IsCurrentSchema = false, + ExcludeItemTypes = new[] { typeof(LiveTvProgram).Name } }); var numComplete = 0; @@ -147,6 +155,8 @@ namespace MediaBrowser.Server.Implementations.Persistence _logger.Debug("Upgrading schema for {0} items", numItems); + var list = new List<BaseItem>(); + foreach (var itemId in itemIds) { cancellationToken.ThrowIfCancellationRequested(); @@ -158,27 +168,50 @@ namespace MediaBrowser.Server.Implementations.Persistence if (item != null) { - try - { - await _itemRepo.SaveItem(item, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - _logger.ErrorException("Error saving item", ex); - } + list.Add(item); } } + if (list.Count >= 1000) + { + try + { + await _itemRepo.SaveItems(list, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.ErrorException("Error saving item", ex); + } + + list.Clear(); + } + numComplete++; double percent = numComplete; percent /= numItems; progress.Report(percent * 100); } + if (list.Count > 0) + { + try + { + await _itemRepo.SaveItems(list, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.ErrorException("Error saving item", ex); + } + } + progress.Report(100); } @@ -230,14 +263,14 @@ namespace MediaBrowser.Server.Implementations.Persistence // These have their own cleanup routines ExcludeItemTypes = new[] { - typeof(Person).Name, - typeof(Genre).Name, - typeof(MusicGenre).Name, - typeof(GameGenre).Name, - typeof(Studio).Name, - typeof(Year).Name, - typeof(Channel).Name, - typeof(AggregateFolder).Name, + typeof(Person).Name, + typeof(Genre).Name, + typeof(MusicGenre).Name, + typeof(GameGenre).Name, + typeof(Studio).Name, + typeof(Year).Name, + typeof(Channel).Name, + typeof(AggregateFolder).Name, typeof(CollectionFolder).Name } }); @@ -307,8 +340,8 @@ namespace MediaBrowser.Server.Implementations.Persistence public IEnumerable<ITaskTrigger> GetDefaultTriggers() { - return new ITaskTrigger[] - { + return new ITaskTrigger[] + { new IntervalTrigger{ Interval = TimeSpan.FromHours(24)} }; } diff --git a/MediaBrowser.Server.Implementations/Persistence/DataExtensions.cs b/MediaBrowser.Server.Implementations/Persistence/DataExtensions.cs new file mode 100644 index 000000000..61ce6e351 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Persistence/DataExtensions.cs @@ -0,0 +1,185 @@ +using System.Text; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using System; +using System.Data; +using System.IO; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Persistence +{ + static class DataExtensions + { + /// <summary> + /// Determines whether the specified conn is open. + /// </summary> + /// <param name="conn">The conn.</param> + /// <returns><c>true</c> if the specified conn is open; otherwise, <c>false</c>.</returns> + public static bool IsOpen(this IDbConnection conn) + { + return conn.State == ConnectionState.Open; + } + + public static IDataParameter GetParameter(this IDbCommand cmd, int index) + { + return (IDataParameter)cmd.Parameters[index]; + } + + public static IDataParameter Add(this IDataParameterCollection paramCollection, IDbCommand cmd, string name, DbType type) + { + var param = cmd.CreateParameter(); + + param.ParameterName = name; + param.DbType = type; + + paramCollection.Add(param); + + return param; + } + + public static IDataParameter Add(this IDataParameterCollection paramCollection, IDbCommand cmd, string name) + { + var param = cmd.CreateParameter(); + + param.ParameterName = name; + + paramCollection.Add(param); + + return param; + } + + + /// <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) + { + if (reader == null) + { + throw new ArgumentNullException("reader"); + } + + var memoryStream = new MemoryStream(); + var num = 0L; + var array = new byte[4096]; + long bytes; + do + { + bytes = reader.GetBytes(ordinal, num, array, 0, array.Length); + memoryStream.Write(array, 0, (int)bytes); + num += bytes; + } + while (bytes > 0L); + memoryStream.Position = 0; + return memoryStream; + } + + /// <summary> + /// Runs the queries. + /// </summary> + /// <param name="connection">The connection.</param> + /// <param name="queries">The queries.</param> + /// <param name="logger">The logger.</param> + /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + /// <exception cref="System.ArgumentNullException">queries</exception> + public static void RunQueries(this IDbConnection connection, string[] queries, ILogger logger) + { + if (queries == null) + { + throw new ArgumentNullException("queries"); + } + + using (var tran = connection.BeginTransaction()) + { + try + { + using (var cmd = connection.CreateCommand()) + { + foreach (var query in queries) + { + cmd.Transaction = tran; + cmd.CommandText = query; + cmd.ExecuteNonQuery(); + } + } + + tran.Commit(); + } + catch (Exception e) + { + logger.ErrorException("Error running queries", e); + tran.Rollback(); + throw; + } + } + } + + public static void Attach(IDbConnection db, string path, string alias) + { + using (var cmd = db.CreateCommand()) + { + cmd.CommandText = string.Format("attach @dbPath as {0};", alias); + cmd.Parameters.Add(cmd, "@dbPath", DbType.String); + cmd.GetParameter(0).Value = path; + + cmd.ExecuteNonQuery(); + } + } + + /// <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) + { + if (obj == null) + { + throw new ArgumentNullException("obj"); + } + + using (var stream = new MemoryStream()) + { + json.SerializeToStream(obj, stream); + return stream.ToArray(); + } + } + + public static void AddColumn(this IDbConnection connection, ILogger logger, string table, string columnName, string type) + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(" + table + ")"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, columnName, StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table " + table); + builder.AppendLine("add column " + columnName + " " + type); + + connection.RunQueries(new[] { builder.ToString() }, logger); + } + } +}
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Persistence/IDbConnector.cs b/MediaBrowser.Server.Implementations/Persistence/IDbConnector.cs new file mode 100644 index 000000000..596cf8407 --- /dev/null +++ b/MediaBrowser.Server.Implementations/Persistence/IDbConnector.cs @@ -0,0 +1,10 @@ +using System.Data; +using System.Threading.Tasks; + +namespace MediaBrowser.Server.Implementations.Persistence +{ + public interface IDbConnector + { + Task<IDbConnection> Connect(string dbPath, bool isReadOnly, bool enablePooling = false, int? cacheSize = null); + } +} diff --git a/MediaBrowser.Server.Implementations/Persistence/MediaStreamColumns.cs b/MediaBrowser.Server.Implementations/Persistence/MediaStreamColumns.cs index 211c77107..1d9be2e0d 100644 --- a/MediaBrowser.Server.Implementations/Persistence/MediaStreamColumns.cs +++ b/MediaBrowser.Server.Implementations/Persistence/MediaStreamColumns.cs @@ -21,14 +21,18 @@ namespace MediaBrowser.Server.Implementations.Persistence AddPixelFormatColumnCommand(); AddBitDepthCommand(); AddIsAnamorphicColumn(); - AddIsCabacColumn(); AddKeyFramesColumn(); AddRefFramesCommand(); AddCodecTagColumn(); AddCommentColumn(); + AddNalColumn(); + AddIsAvcColumn(); + AddTitleColumn(); + AddTimeBaseColumn(); + AddCodecTimeBaseColumn(); } - private void AddCommentColumn() + private void AddIsAvcColumn() { using (var cmd = _connection.CreateCommand()) { @@ -42,7 +46,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { var name = reader.GetString(1); - if (string.Equals(name, "Comment", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(name, "IsAvc", StringComparison.OrdinalIgnoreCase)) { return; } @@ -54,12 +58,12 @@ namespace MediaBrowser.Server.Implementations.Persistence var builder = new StringBuilder(); builder.AppendLine("alter table mediastreams"); - builder.AppendLine("add column Comment TEXT"); + builder.AppendLine("add column IsAvc BIT NULL"); _connection.RunQueries(new[] { builder.ToString() }, _logger); } - private void AddCodecTagColumn() + private void AddTimeBaseColumn() { using (var cmd = _connection.CreateCommand()) { @@ -73,7 +77,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { var name = reader.GetString(1); - if (string.Equals(name, "CodecTag", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(name, "TimeBase", StringComparison.OrdinalIgnoreCase)) { return; } @@ -85,12 +89,12 @@ namespace MediaBrowser.Server.Implementations.Persistence var builder = new StringBuilder(); builder.AppendLine("alter table mediastreams"); - builder.AppendLine("add column CodecTag TEXT"); + builder.AppendLine("add column TimeBase TEXT"); _connection.RunQueries(new[] { builder.ToString() }, _logger); } - private void AddPixelFormatColumnCommand() + private void AddCodecTimeBaseColumn() { using (var cmd = _connection.CreateCommand()) { @@ -104,7 +108,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { var name = reader.GetString(1); - if (string.Equals(name, "PixelFormat", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(name, "CodecTimeBase", StringComparison.OrdinalIgnoreCase)) { return; } @@ -116,12 +120,12 @@ namespace MediaBrowser.Server.Implementations.Persistence var builder = new StringBuilder(); builder.AppendLine("alter table mediastreams"); - builder.AppendLine("add column PixelFormat TEXT"); + builder.AppendLine("add column CodecTimeBase TEXT"); _connection.RunQueries(new[] { builder.ToString() }, _logger); } - private void AddBitDepthCommand() + private void AddTitleColumn() { using (var cmd = _connection.CreateCommand()) { @@ -135,7 +139,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { var name = reader.GetString(1); - if (string.Equals(name, "BitDepth", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(name, "Title", StringComparison.OrdinalIgnoreCase)) { return; } @@ -147,12 +151,12 @@ namespace MediaBrowser.Server.Implementations.Persistence var builder = new StringBuilder(); builder.AppendLine("alter table mediastreams"); - builder.AppendLine("add column BitDepth INT NULL"); + builder.AppendLine("add column Title TEXT"); _connection.RunQueries(new[] { builder.ToString() }, _logger); } - private void AddRefFramesCommand() + private void AddNalColumn() { using (var cmd = _connection.CreateCommand()) { @@ -166,7 +170,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { var name = reader.GetString(1); - if (string.Equals(name, "RefFrames", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(name, "NalLengthSize", StringComparison.OrdinalIgnoreCase)) { return; } @@ -178,12 +182,74 @@ namespace MediaBrowser.Server.Implementations.Persistence var builder = new StringBuilder(); builder.AppendLine("alter table mediastreams"); - builder.AppendLine("add column RefFrames INT NULL"); + builder.AppendLine("add column NalLengthSize TEXT"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddCommentColumn() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "Comment", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column Comment TEXT"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddCodecTagColumn() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "CodecTag", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column CodecTag TEXT"); _connection.RunQueries(new[] { builder.ToString() }, _logger); } - private void AddIsCabacColumn() + private void AddPixelFormatColumnCommand() { using (var cmd = _connection.CreateCommand()) { @@ -197,7 +263,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { var name = reader.GetString(1); - if (string.Equals(name, "IsCabac", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(name, "PixelFormat", StringComparison.OrdinalIgnoreCase)) { return; } @@ -209,7 +275,69 @@ namespace MediaBrowser.Server.Implementations.Persistence var builder = new StringBuilder(); builder.AppendLine("alter table mediastreams"); - builder.AppendLine("add column IsCabac BIT NULL"); + builder.AppendLine("add column PixelFormat TEXT"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddBitDepthCommand() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "BitDepth", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column BitDepth INT NULL"); + + _connection.RunQueries(new[] { builder.ToString() }, _logger); + } + + private void AddRefFramesCommand() + { + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "PRAGMA table_info(mediastreams)"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + if (!reader.IsDBNull(1)) + { + var name = reader.GetString(1); + + if (string.Equals(name, "RefFrames", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + } + } + } + + var builder = new StringBuilder(); + + builder.AppendLine("alter table mediastreams"); + builder.AppendLine("add column RefFrames INT NULL"); _connection.RunQueries(new[] { builder.ToString() }, _logger); } diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs index 45e0304c1..40970dbe4 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs @@ -18,12 +18,11 @@ namespace MediaBrowser.Server.Implementations.Persistence /// </summary> public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository { - private IDbConnection _connection; - - public SqliteDisplayPreferencesRepository(ILogManager logManager, IJsonSerializer jsonSerializer, IApplicationPaths appPaths) : base(logManager) + public SqliteDisplayPreferencesRepository(ILogManager logManager, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, IDbConnector dbConnector) + : base(logManager, dbConnector) { _jsonSerializer = jsonSerializer; - _appPaths = appPaths; + DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db"); } /// <summary> @@ -44,32 +43,21 @@ namespace MediaBrowser.Server.Implementations.Persistence private readonly IJsonSerializer _jsonSerializer; /// <summary> - /// The _app paths - /// </summary> - private readonly IApplicationPaths _appPaths; - - /// <summary> /// Opens the connection to the database /// </summary> /// <returns>Task.</returns> public async Task Initialize() { - var dbFile = Path.Combine(_appPaths.DataPath, "displaypreferences.db"); - - _connection = await SqliteExtensions.ConnectToDb(dbFile, Logger).ConfigureAwait(false); - - string[] queries = { + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + string[] queries = { "create table if not exists userdisplaypreferences (id GUID, userId GUID, client text, data BLOB)", - "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)", - - //pragmas - "pragma temp_store = memory", - - "pragma shrink_memory" + "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)" }; - _connection.RunQueries(queries, Logger); + connection.RunQueries(queries, Logger); + } } /// <summary> @@ -96,58 +84,57 @@ namespace MediaBrowser.Server.Implementations.Persistence var serialized = _jsonSerializer.SerializeToBytes(displayPreferences); - await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - IDbTransaction transaction = null; - - try + using (var connection = await CreateConnection().ConfigureAwait(false)) { - transaction = _connection.BeginTransaction(); + IDbTransaction transaction = null; - using (var cmd = _connection.CreateCommand()) + try { - cmd.CommandText = "replace into userdisplaypreferences (id, userid, client, data) values (@1, @2, @3, @4)"; + transaction = connection.BeginTransaction(); - cmd.Parameters.Add(cmd, "@1", DbType.Guid).Value = new Guid(displayPreferences.Id); - cmd.Parameters.Add(cmd, "@2", DbType.Guid).Value = userId; - cmd.Parameters.Add(cmd, "@3", DbType.String).Value = client; - cmd.Parameters.Add(cmd, "@4", DbType.Binary).Value = serialized; + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "replace into userdisplaypreferences (id, userid, client, data) values (@1, @2, @3, @4)"; - cmd.Transaction = transaction; + cmd.Parameters.Add(cmd, "@1", DbType.Guid).Value = new Guid(displayPreferences.Id); + cmd.Parameters.Add(cmd, "@2", DbType.Guid).Value = userId; + cmd.Parameters.Add(cmd, "@3", DbType.String).Value = client; + cmd.Parameters.Add(cmd, "@4", DbType.Binary).Value = serialized; - cmd.ExecuteNonQuery(); - } + cmd.Transaction = transaction; - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) + cmd.ExecuteNonQuery(); + } + + transaction.Commit(); + } + catch (OperationCanceledException) { - transaction.Rollback(); + if (transaction != null) + { + transaction.Rollback(); + } + + throw; } + catch (Exception e) + { + Logger.ErrorException("Failed to save display preferences:", e); - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save display preferences:", e); + if (transaction != null) + { + transaction.Rollback(); + } - if (transaction != null) - { - transaction.Rollback(); + throw; } - - throw; - } - finally - { - if (transaction != null) + finally { - transaction.Dispose(); + if (transaction != null) + { + transaction.Dispose(); + } } - - WriteLock.Release(); } } @@ -168,64 +155,63 @@ namespace MediaBrowser.Server.Implementations.Persistence cancellationToken.ThrowIfCancellationRequested(); - await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - IDbTransaction transaction = null; - - try + using (var connection = await CreateConnection().ConfigureAwait(false)) { - transaction = _connection.BeginTransaction(); + IDbTransaction transaction = null; - foreach (var displayPreference in displayPreferences) + try { + transaction = connection.BeginTransaction(); - var serialized = _jsonSerializer.SerializeToBytes(displayPreference); - - using (var cmd = _connection.CreateCommand()) + foreach (var displayPreference in displayPreferences) { - cmd.CommandText = "replace into userdisplaypreferences (id, userid, client, data) values (@1, @2, @3, @4)"; - cmd.Parameters.Add(cmd, "@1", DbType.Guid).Value = new Guid(displayPreference.Id); - cmd.Parameters.Add(cmd, "@2", DbType.Guid).Value = userId; - cmd.Parameters.Add(cmd, "@3", DbType.String).Value = displayPreference.Client; - cmd.Parameters.Add(cmd, "@4", DbType.Binary).Value = serialized; + var serialized = _jsonSerializer.SerializeToBytes(displayPreference); - cmd.Transaction = transaction; + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "replace into userdisplaypreferences (id, userid, client, data) values (@1, @2, @3, @4)"; - cmd.ExecuteNonQuery(); + cmd.Parameters.Add(cmd, "@1", DbType.Guid).Value = new Guid(displayPreference.Id); + cmd.Parameters.Add(cmd, "@2", DbType.Guid).Value = userId; + cmd.Parameters.Add(cmd, "@3", DbType.String).Value = displayPreference.Client; + cmd.Parameters.Add(cmd, "@4", DbType.Binary).Value = serialized; + + cmd.Transaction = transaction; + + cmd.ExecuteNonQuery(); + } } - } - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) + transaction.Commit(); + } + catch (OperationCanceledException) { - transaction.Rollback(); + if (transaction != null) + { + transaction.Rollback(); + } + + throw; } + catch (Exception e) + { + Logger.ErrorException("Failed to save display preferences:", e); - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save display preferences:", e); + if (transaction != null) + { + transaction.Rollback(); + } - if (transaction != null) - { - transaction.Rollback(); + throw; } - - throw; - } - finally - { - if (transaction != null) + finally { - transaction.Dispose(); + if (transaction != null) + { + transaction.Dispose(); + } } - - WriteLock.Release(); } } @@ -246,28 +232,33 @@ namespace MediaBrowser.Server.Implementations.Persistence var guidId = displayPreferencesId.GetMD5(); - var cmd = _connection.CreateCommand(); - cmd.CommandText = "select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"; - - cmd.Parameters.Add(cmd, "@id", DbType.Guid).Value = guidId; - cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; - cmd.Parameters.Add(cmd, "@client", DbType.String).Value = client; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + using (var connection = CreateConnection(true).Result) { - if (reader.Read()) + using (var cmd = connection.CreateCommand()) { - using (var stream = reader.GetMemoryStream(0)) + cmd.CommandText = "select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"; + + cmd.Parameters.Add(cmd, "@id", DbType.Guid).Value = guidId; + cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; + cmd.Parameters.Add(cmd, "@client", DbType.String).Value = client; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) { - return _jsonSerializer.DeserializeFromStream<DisplayPreferences>(stream); + if (reader.Read()) + { + using (var stream = reader.GetMemoryStream(0)) + { + return _jsonSerializer.DeserializeFromStream<DisplayPreferences>(stream); + } + } } + + return new DisplayPreferences + { + Id = guidId.ToString("N") + }; } } - - return new DisplayPreferences - { - Id = guidId.ToString("N") - }; } /// <summary> @@ -278,36 +269,30 @@ namespace MediaBrowser.Server.Implementations.Persistence /// <exception cref="System.ArgumentNullException">item</exception> public IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId) { + var list = new List<DisplayPreferences>(); - var cmd = _connection.CreateCommand(); - cmd.CommandText = "select data from userdisplaypreferences where userId=@userId"; - - cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + using (var connection = CreateConnection(true).Result) { - while (reader.Read()) + using (var cmd = connection.CreateCommand()) { - using (var stream = reader.GetMemoryStream(0)) + cmd.CommandText = "select data from userdisplaypreferences where userId=@userId"; + + cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) { - yield return _jsonSerializer.DeserializeFromStream<DisplayPreferences>(stream); + while (reader.Read()) + { + using (var stream = reader.GetMemoryStream(0)) + { + list.Add(_jsonSerializer.DeserializeFromStream<DisplayPreferences>(stream)); + } + } } } } - } - - protected override void CloseConnection() - { - if (_connection != null) - { - if (_connection.IsOpen()) - { - _connection.Close(); - } - _connection.Dispose(); - _connection = null; - } + return list; } public Task SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken) diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs index 4fb1e07dd..d5b582da5 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs @@ -1,218 +1,58 @@ -using System.Text; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using System; +using System; +using System.Collections.Generic; using System.Data; using System.Data.SQLite; -using System.IO; +using System.Linq; +using System.Text; using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Logging; namespace MediaBrowser.Server.Implementations.Persistence { /// <summary> /// Class SQLiteExtensions /// </summary> - static class SqliteExtensions + public static class SqliteExtensions { /// <summary> - /// Determines whether the specified conn is open. - /// </summary> - /// <param name="conn">The conn.</param> - /// <returns><c>true</c> if the specified conn is open; otherwise, <c>false</c>.</returns> - public static bool IsOpen(this IDbConnection conn) - { - return conn.State == ConnectionState.Open; - } - - public static IDataParameter GetParameter(this IDbCommand cmd, int index) - { - return (IDataParameter)cmd.Parameters[index]; - } - - public static IDataParameter Add(this IDataParameterCollection paramCollection, IDbCommand cmd, string name, DbType type) - { - var param = cmd.CreateParameter(); - - param.ParameterName = name; - param.DbType = type; - - paramCollection.Add(param); - - return param; - } - - public static IDataParameter Add(this IDataParameterCollection paramCollection, IDbCommand cmd, string name) - { - var param = cmd.CreateParameter(); - - param.ParameterName = name; - - paramCollection.Add(param); - - return param; - } - - - /// <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) - { - if (reader == null) - { - throw new ArgumentNullException("reader"); - } - - var memoryStream = new MemoryStream(); - var num = 0L; - var array = new byte[4096]; - long bytes; - do - { - bytes = reader.GetBytes(ordinal, num, array, 0, array.Length); - memoryStream.Write(array, 0, (int)bytes); - num += bytes; - } - while (bytes > 0L); - memoryStream.Position = 0; - return memoryStream; - } - - /// <summary> - /// Runs the queries. - /// </summary> - /// <param name="connection">The connection.</param> - /// <param name="queries">The queries.</param> - /// <param name="logger">The logger.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - /// <exception cref="System.ArgumentNullException">queries</exception> - public static void RunQueries(this IDbConnection connection, string[] queries, ILogger logger) - { - if (queries == null) - { - throw new ArgumentNullException("queries"); - } - - using (var tran = connection.BeginTransaction()) - { - try - { - using (var cmd = connection.CreateCommand()) - { - foreach (var query in queries) - { - cmd.Transaction = tran; - cmd.CommandText = query; - cmd.ExecuteNonQuery(); - } - } - - tran.Commit(); - } - catch (Exception e) - { - logger.ErrorException("Error running queries", e); - tran.Rollback(); - throw; - } - } - } - - /// <summary> /// Connects to db. /// </summary> - /// <param name="dbPath">The db path.</param> - /// <param name="logger">The logger.</param> - /// <returns>Task{IDbConnection}.</returns> - /// <exception cref="System.ArgumentNullException">dbPath</exception> - public static async Task<IDbConnection> ConnectToDb(string dbPath, ILogger logger) + public static async Task<IDbConnection> ConnectToDb(string dbPath, bool isReadOnly, bool enablePooling, int? cacheSize, ILogger logger) { if (string.IsNullOrEmpty(dbPath)) { throw new ArgumentNullException("dbPath"); } - logger.Info("Sqlite {0} opening {1}", SQLiteConnection.SQLiteVersion, dbPath); + SQLiteConnection.SetMemoryStatus(false); var connectionstr = new SQLiteConnectionStringBuilder { PageSize = 4096, - CacheSize = 2000, - SyncMode = SynchronizationModes.Full, + CacheSize = cacheSize ?? 2000, + SyncMode = SynchronizationModes.Normal, DataSource = dbPath, - JournalMode = SQLiteJournalModeEnum.Wal - }; - - var connection = new SQLiteConnection(connectionstr.ConnectionString); + JournalMode = SQLiteJournalModeEnum.Wal, - await connection.OpenAsync().ConfigureAwait(false); - - return connection; - } - - public static void Attach(IDbConnection db, string path, string alias) - { - using (var cmd = db.CreateCommand()) - { - cmd.CommandText = string.Format("attach '{0}' as {1};", path, alias); - cmd.ExecuteNonQuery(); - } - } - - /// <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) - { - if (obj == null) - { - throw new ArgumentNullException("obj"); - } + // This is causing crashing under linux + Pooling = enablePooling && Environment.OSVersion.Platform == PlatformID.Win32NT, + ReadOnly = isReadOnly + }; - using (var stream = new MemoryStream()) - { - json.SerializeToStream(obj, stream); - return stream.ToArray(); - } - } + var connectionString = connectionstr.ConnectionString; - public static void AddColumn(this IDbConnection connection, ILogger logger, string table, string columnName, string type) - { - using (var cmd = connection.CreateCommand()) + if (!enablePooling) { - cmd.CommandText = "PRAGMA table_info(" + table + ")"; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) - { - while (reader.Read()) - { - if (!reader.IsDBNull(1)) - { - var name = reader.GetString(1); - - if (string.Equals(name, columnName, StringComparison.OrdinalIgnoreCase)) - { - return; - } - } - } - } + logger.Info("Sqlite {0} opening {1}", SQLiteConnection.SQLiteVersion, connectionString); } - var builder = new StringBuilder(); + var connection = new SQLiteConnection(connectionString); - builder.AppendLine("alter table " + table); - builder.AppendLine("add column " + columnName + " " + type); + await connection.OpenAsync().ConfigureAwait(false); - connection.RunQueries(new[] { builder.ToString() }, logger); + return connection; } } -}
\ No newline at end of file +} diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs index 2d5aad04d..7a5e00090 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteFileOrganizationRepository.cs @@ -16,19 +16,11 @@ namespace MediaBrowser.Server.Implementations.Persistence { public class SqliteFileOrganizationRepository : BaseSqliteRepository, IFileOrganizationRepository, IDisposable { - private IDbConnection _connection; - - private readonly IServerApplicationPaths _appPaths; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private IDbCommand _saveResultCommand; - private IDbCommand _deleteResultCommand; - private IDbCommand _deleteAllCommand; - - public SqliteFileOrganizationRepository(ILogManager logManager, IServerApplicationPaths appPaths) : base(logManager) + public SqliteFileOrganizationRepository(ILogManager logManager, IServerApplicationPaths appPaths, IDbConnector connector) : base(logManager, connector) { - _appPaths = appPaths; + DbFilePath = Path.Combine(appPaths.DataPath, "fileorganization.db"); } /// <summary> @@ -37,53 +29,16 @@ namespace MediaBrowser.Server.Implementations.Persistence /// <returns>Task.</returns> public async Task Initialize() { - var dbFile = Path.Combine(_appPaths.DataPath, "fileorganization.db"); - - _connection = await SqliteExtensions.ConnectToDb(dbFile, Logger).ConfigureAwait(false); - - string[] queries = { + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + string[] queries = { "create table if not exists FileOrganizerResults (ResultId GUID PRIMARY KEY, OriginalPath TEXT, TargetPath TEXT, FileLength INT, OrganizationDate datetime, Status TEXT, OrganizationType TEXT, StatusMessage TEXT, ExtractedName TEXT, ExtractedYear int null, ExtractedSeasonNumber int null, ExtractedEpisodeNumber int null, ExtractedEndingEpisodeNumber, DuplicatePaths TEXT int null)", - "create index if not exists idx_FileOrganizerResults on FileOrganizerResults(ResultId)", - - //pragmas - "pragma temp_store = memory", - - "pragma shrink_memory" + "create index if not exists idx_FileOrganizerResults on FileOrganizerResults(ResultId)" }; - _connection.RunQueries(queries, Logger); - - PrepareStatements(); - } - - private void PrepareStatements() - { - _saveResultCommand = _connection.CreateCommand(); - _saveResultCommand.CommandText = "replace into FileOrganizerResults (ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths) values (@ResultId, @OriginalPath, @TargetPath, @FileLength, @OrganizationDate, @Status, @OrganizationType, @StatusMessage, @ExtractedName, @ExtractedYear, @ExtractedSeasonNumber, @ExtractedEpisodeNumber, @ExtractedEndingEpisodeNumber, @DuplicatePaths)"; - - _saveResultCommand.Parameters.Add(_saveResultCommand, "@ResultId"); - _saveResultCommand.Parameters.Add(_saveResultCommand, "@OriginalPath"); - _saveResultCommand.Parameters.Add(_saveResultCommand, "@TargetPath"); - _saveResultCommand.Parameters.Add(_saveResultCommand, "@FileLength"); - _saveResultCommand.Parameters.Add(_saveResultCommand, "@OrganizationDate"); - _saveResultCommand.Parameters.Add(_saveResultCommand, "@Status"); - _saveResultCommand.Parameters.Add(_saveResultCommand, "@OrganizationType"); - _saveResultCommand.Parameters.Add(_saveResultCommand, "@StatusMessage"); - _saveResultCommand.Parameters.Add(_saveResultCommand, "@ExtractedName"); - _saveResultCommand.Parameters.Add(_saveResultCommand, "@ExtractedYear"); - _saveResultCommand.Parameters.Add(_saveResultCommand, "@ExtractedSeasonNumber"); - _saveResultCommand.Parameters.Add(_saveResultCommand, "@ExtractedEpisodeNumber"); - _saveResultCommand.Parameters.Add(_saveResultCommand, "@ExtractedEndingEpisodeNumber"); - _saveResultCommand.Parameters.Add(_saveResultCommand, "@DuplicatePaths"); - - _deleteResultCommand = _connection.CreateCommand(); - _deleteResultCommand.CommandText = "delete from FileOrganizerResults where ResultId = @ResultId"; - - _deleteResultCommand.Parameters.Add(_saveResultCommand, "@ResultId"); - - _deleteAllCommand = _connection.CreateCommand(); - _deleteAllCommand.CommandText = "delete from FileOrganizerResults"; + connection.RunQueries(queries, Logger); + } } public async Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken) @@ -95,65 +50,84 @@ namespace MediaBrowser.Server.Implementations.Persistence cancellationToken.ThrowIfCancellationRequested(); - await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - IDbTransaction transaction = null; - - try - { - transaction = _connection.BeginTransaction(); - - var index = 0; - - _saveResultCommand.GetParameter(index++).Value = new Guid(result.Id); - _saveResultCommand.GetParameter(index++).Value = result.OriginalPath; - _saveResultCommand.GetParameter(index++).Value = result.TargetPath; - _saveResultCommand.GetParameter(index++).Value = result.FileSize; - _saveResultCommand.GetParameter(index++).Value = result.Date; - _saveResultCommand.GetParameter(index++).Value = result.Status.ToString(); - _saveResultCommand.GetParameter(index++).Value = result.Type.ToString(); - _saveResultCommand.GetParameter(index++).Value = result.StatusMessage; - _saveResultCommand.GetParameter(index++).Value = result.ExtractedName; - _saveResultCommand.GetParameter(index++).Value = result.ExtractedYear; - _saveResultCommand.GetParameter(index++).Value = result.ExtractedSeasonNumber; - _saveResultCommand.GetParameter(index++).Value = result.ExtractedEpisodeNumber; - _saveResultCommand.GetParameter(index++).Value = result.ExtractedEndingEpisodeNumber; - _saveResultCommand.GetParameter(index).Value = string.Join("|", result.DuplicatePaths.ToArray()); - - _saveResultCommand.Transaction = transaction; - - _saveResultCommand.ExecuteNonQuery(); - - transaction.Commit(); - } - catch (OperationCanceledException) + using (var connection = await CreateConnection().ConfigureAwait(false)) { - if (transaction != null) + using (var saveResultCommand = connection.CreateCommand()) { - transaction.Rollback(); - } + saveResultCommand.CommandText = "replace into FileOrganizerResults (ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths) values (@ResultId, @OriginalPath, @TargetPath, @FileLength, @OrganizationDate, @Status, @OrganizationType, @StatusMessage, @ExtractedName, @ExtractedYear, @ExtractedSeasonNumber, @ExtractedEpisodeNumber, @ExtractedEndingEpisodeNumber, @DuplicatePaths)"; + + saveResultCommand.Parameters.Add(saveResultCommand, "@ResultId"); + saveResultCommand.Parameters.Add(saveResultCommand, "@OriginalPath"); + saveResultCommand.Parameters.Add(saveResultCommand, "@TargetPath"); + saveResultCommand.Parameters.Add(saveResultCommand, "@FileLength"); + saveResultCommand.Parameters.Add(saveResultCommand, "@OrganizationDate"); + saveResultCommand.Parameters.Add(saveResultCommand, "@Status"); + saveResultCommand.Parameters.Add(saveResultCommand, "@OrganizationType"); + saveResultCommand.Parameters.Add(saveResultCommand, "@StatusMessage"); + saveResultCommand.Parameters.Add(saveResultCommand, "@ExtractedName"); + saveResultCommand.Parameters.Add(saveResultCommand, "@ExtractedYear"); + saveResultCommand.Parameters.Add(saveResultCommand, "@ExtractedSeasonNumber"); + saveResultCommand.Parameters.Add(saveResultCommand, "@ExtractedEpisodeNumber"); + saveResultCommand.Parameters.Add(saveResultCommand, "@ExtractedEndingEpisodeNumber"); + saveResultCommand.Parameters.Add(saveResultCommand, "@DuplicatePaths"); + + IDbTransaction transaction = null; + + try + { + transaction = connection.BeginTransaction(); + + var index = 0; + + saveResultCommand.GetParameter(index++).Value = new Guid(result.Id); + saveResultCommand.GetParameter(index++).Value = result.OriginalPath; + saveResultCommand.GetParameter(index++).Value = result.TargetPath; + saveResultCommand.GetParameter(index++).Value = result.FileSize; + saveResultCommand.GetParameter(index++).Value = result.Date; + saveResultCommand.GetParameter(index++).Value = result.Status.ToString(); + saveResultCommand.GetParameter(index++).Value = result.Type.ToString(); + saveResultCommand.GetParameter(index++).Value = result.StatusMessage; + saveResultCommand.GetParameter(index++).Value = result.ExtractedName; + saveResultCommand.GetParameter(index++).Value = result.ExtractedYear; + saveResultCommand.GetParameter(index++).Value = result.ExtractedSeasonNumber; + saveResultCommand.GetParameter(index++).Value = result.ExtractedEpisodeNumber; + saveResultCommand.GetParameter(index++).Value = result.ExtractedEndingEpisodeNumber; + saveResultCommand.GetParameter(index).Value = string.Join("|", result.DuplicatePaths.ToArray()); + + saveResultCommand.Transaction = transaction; + + saveResultCommand.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save FileOrganizationResult:", e); + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save FileOrganizationResult:", e); - if (transaction != null) - { - transaction.Rollback(); - } + if (transaction != null) + { + transaction.Rollback(); + } - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } } - - WriteLock.Release(); } } @@ -164,100 +138,110 @@ namespace MediaBrowser.Server.Implementations.Persistence throw new ArgumentNullException("id"); } - await WriteLock.WaitAsync().ConfigureAwait(false); + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + using (var deleteResultCommand = connection.CreateCommand()) + { + deleteResultCommand.CommandText = "delete from FileOrganizerResults where ResultId = @ResultId"; - IDbTransaction transaction = null; + deleteResultCommand.Parameters.Add(deleteResultCommand, "@ResultId"); - try - { - transaction = _connection.BeginTransaction(); + IDbTransaction transaction = null; - _deleteResultCommand.GetParameter(0).Value = new Guid(id); + try + { + transaction = connection.BeginTransaction(); - _deleteResultCommand.Transaction = transaction; + deleteResultCommand.GetParameter(0).Value = new Guid(id); - _deleteResultCommand.ExecuteNonQuery(); + deleteResultCommand.Transaction = transaction; - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } + deleteResultCommand.ExecuteNonQuery(); - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to delete FileOrganizationResult:", e); + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } - if (transaction != null) - { - transaction.Rollback(); - } + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to delete FileOrganizationResult:", e); - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } + if (transaction != null) + { + transaction.Rollback(); + } - WriteLock.Release(); + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } } } public async Task DeleteAll() { - await WriteLock.WaitAsync().ConfigureAwait(false); + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "delete from FileOrganizerResults"; - IDbTransaction transaction = null; + IDbTransaction transaction = null; - try - { - transaction = _connection.BeginTransaction(); - - _deleteAllCommand.Transaction = transaction; + try + { + transaction = connection.BeginTransaction(); - _deleteAllCommand.ExecuteNonQuery(); + cmd.Transaction = transaction; - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } + cmd.ExecuteNonQuery(); - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to delete results", e); + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } - if (transaction != null) - { - transaction.Rollback(); - } + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to delete results", e); - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } + if (transaction != null) + { + transaction.Rollback(); + } - WriteLock.Release(); + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } } } - + public QueryResult<FileOrganizationResult> GetResults(FileOrganizationResultQuery query) { if (query == null) @@ -265,46 +249,49 @@ namespace MediaBrowser.Server.Implementations.Persistence throw new ArgumentNullException("query"); } - using (var cmd = _connection.CreateCommand()) + using (var connection = CreateConnection(true).Result) { - cmd.CommandText = "SELECT ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths from FileOrganizerResults"; - - if (query.StartIndex.HasValue && query.StartIndex.Value > 0) + using (var cmd = connection.CreateCommand()) { - cmd.CommandText += string.Format(" WHERE ResultId NOT IN (SELECT ResultId FROM FileOrganizerResults ORDER BY OrganizationDate desc LIMIT {0})", - query.StartIndex.Value.ToString(_usCulture)); - } + cmd.CommandText = "SELECT ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths from FileOrganizerResults"; - cmd.CommandText += " ORDER BY OrganizationDate desc"; + if (query.StartIndex.HasValue && query.StartIndex.Value > 0) + { + cmd.CommandText += string.Format(" WHERE ResultId NOT IN (SELECT ResultId FROM FileOrganizerResults ORDER BY OrganizationDate desc LIMIT {0})", + query.StartIndex.Value.ToString(_usCulture)); + } - if (query.Limit.HasValue) - { - cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); - } + cmd.CommandText += " ORDER BY OrganizationDate desc"; + + if (query.Limit.HasValue) + { + cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); + } - cmd.CommandText += "; select count (ResultId) from FileOrganizerResults"; + cmd.CommandText += "; select count (ResultId) from FileOrganizerResults"; - var list = new List<FileOrganizationResult>(); - var count = 0; + var list = new List<FileOrganizationResult>(); + var count = 0; - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) - { - while (reader.Read()) + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) { - list.Add(GetResult(reader)); + while (reader.Read()) + { + list.Add(GetResult(reader)); + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } } - if (reader.NextResult() && reader.Read()) + return new QueryResult<FileOrganizationResult>() { - count = reader.GetInt32(0); - } + Items = list.ToArray(), + TotalRecordCount = count + }; } - - return new QueryResult<FileOrganizationResult>() - { - Items = list.ToArray(), - TotalRecordCount = count - }; } } @@ -315,24 +302,27 @@ namespace MediaBrowser.Server.Implementations.Persistence throw new ArgumentNullException("id"); } - var guid = new Guid(id); - - using (var cmd = _connection.CreateCommand()) + using (var connection = CreateConnection(true).Result) { - cmd.CommandText = "select ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths from FileOrganizerResults where ResultId=@Id"; + var guid = new Guid(id); - cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + using (var cmd = connection.CreateCommand()) { - if (reader.Read()) + cmd.CommandText = "select ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths from FileOrganizerResults where ResultId=@Id"; + + cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) { - return GetResult(reader); + if (reader.Read()) + { + return GetResult(reader); + } } } - } - return null; + return null; + } } public FileOrganizationResult GetResult(IDataReader reader) @@ -414,19 +404,5 @@ namespace MediaBrowser.Server.Implementations.Persistence return result; } - - protected override void CloseConnection() - { - if (_connection != null) - { - if (_connection.IsOpen()) - { - _connection.Close(); - } - - _connection.Dispose(); - _connection = null; - } - } } } diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs index af275faee..63dd29e0d 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs @@ -1,4 +1,3 @@ -using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; @@ -16,10 +15,14 @@ using System.Globalization; using System.IO; using System.Linq; using System.Runtime.Serialization; +using System.Text; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Playlists; +using MediaBrowser.Model.Dto; using MediaBrowser.Model.LiveTv; namespace MediaBrowser.Server.Implementations.Persistence @@ -54,7 +57,7 @@ namespace MediaBrowser.Server.Implementations.Persistence /// <summary> /// The _app paths /// </summary> - private readonly IApplicationPaths _appPaths; + private readonly IServerConfigurationManager _config; /// <summary> /// The _save item command @@ -77,79 +80,110 @@ namespace MediaBrowser.Server.Implementations.Persistence private IDbCommand _deleteAncestorsCommand; private IDbCommand _saveAncestorCommand; + private IDbCommand _deleteUserDataKeysCommand; + private IDbCommand _saveUserDataKeysCommand; + + private IDbCommand _deleteItemValuesCommand; + private IDbCommand _saveItemValuesCommand; + + private IDbCommand _deleteProviderIdsCommand; + private IDbCommand _saveProviderIdsCommand; + + private IDbCommand _deleteImagesCommand; + private IDbCommand _saveImagesCommand; + private IDbCommand _updateInheritedRatingCommand; + private IDbCommand _updateInheritedTagsCommand; - private const int LatestSchemaVersion = 53; + public const int LatestSchemaVersion = 108; /// <summary> /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class. /// </summary> - /// <param name="appPaths">The app paths.</param> - /// <param name="jsonSerializer">The json serializer.</param> - /// <param name="logManager">The log manager.</param> - /// <exception cref="System.ArgumentNullException"> - /// appPaths - /// or - /// jsonSerializer - /// </exception> - public SqliteItemRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager) - : base(logManager) + public SqliteItemRepository(IServerConfigurationManager config, IJsonSerializer jsonSerializer, ILogManager logManager, IDbConnector connector) + : base(logManager, connector) { - if (appPaths == null) + if (config == null) { - throw new ArgumentNullException("appPaths"); + throw new ArgumentNullException("config"); } if (jsonSerializer == null) { throw new ArgumentNullException("jsonSerializer"); } - _appPaths = appPaths; + _config = config; _jsonSerializer = jsonSerializer; - _criticReviewsPath = Path.Combine(_appPaths.DataPath, "critic-reviews"); + _criticReviewsPath = Path.Combine(_config.ApplicationPaths.DataPath, "critic-reviews"); + DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); } private const string ChaptersTableName = "Chapters2"; + protected override async Task<IDbConnection> CreateConnection(bool isReadOnly = false) + { + var cacheSize = _config.Configuration.SqliteCacheSize; + if (cacheSize <= 0) + { + cacheSize = Math.Min(Environment.ProcessorCount * 50000, 200000); + } + + var connection = await DbConnector.Connect(DbFilePath, false, false, 0 - cacheSize).ConfigureAwait(false); + + connection.RunQueries(new[] + { + "pragma temp_store = memory", + "pragma default_temp_store = memory", + "PRAGMA locking_mode=EXCLUSIVE" + + }, Logger); + + return connection; + } + /// <summary> /// Opens the connection to the database /// </summary> /// <returns>Task.</returns> - public async Task Initialize() + public async Task Initialize(SqliteUserDataRepository userDataRepo) { - var dbFile = Path.Combine(_appPaths.DataPath, "library.db"); - - _connection = await SqliteExtensions.ConnectToDb(dbFile, Logger).ConfigureAwait(false); + _connection = await CreateConnection(false).ConfigureAwait(false); var createMediaStreamsTableCommand - = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, IsCabac BIT NULL, CodecTag TEXT NULL, Comment TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))"; + = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))"; string[] queries = { "create table if not exists TypedBaseItems (guid GUID primary key, type TEXT, data BLOB, ParentId GUID, Path TEXT)", - "create index if not exists idx_TypedBaseItems on TypedBaseItems(guid)", - "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)", - "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)", "create table if not exists AncestorIds (ItemId GUID, AncestorId GUID, AncestorIdText TEXT, PRIMARY KEY (ItemId, AncestorId))", "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)", "create index if not exists idx_AncestorIds2 on AncestorIds(AncestorIdText)", - + + "create table if not exists UserDataKeys (ItemId GUID, UserDataKey TEXT Priority INT, PRIMARY KEY (ItemId, UserDataKey))", + + "create table if not exists ItemValues (ItemId GUID, Type INT, Value TEXT, CleanValue TEXT)", + + "create table if not exists ProviderIds (ItemId GUID, Name TEXT, Value TEXT, PRIMARY KEY (ItemId, Name))", + // covering index + "create index if not exists Idx_ProviderIds1 on ProviderIds(ItemId,Name,Value)", + + "create table if not exists Images (ItemId GUID NOT NULL, Path TEXT NOT NULL, ImageType INT NOT NULL, DateModified DATETIME, IsPlaceHolder BIT NOT NULL, SortOrder INT)", + "create index if not exists idx_Images on Images(ItemId)", + "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)", - "create index if not exists idxPeopleItemId on People(ItemId)", + + "drop index if exists idxPeopleItemId", + "create index if not exists idxPeopleItemId1 on People(ItemId,ListOrder)", "create index if not exists idxPeopleName on People(Name)", "create table if not exists "+ChaptersTableName+" (ItemId GUID, ChapterIndex INT, StartPositionTicks BIGINT, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", - "create index if not exists idx_"+ChaptersTableName+" on "+ChaptersTableName+"(ItemId, ChapterIndex)", createMediaStreamsTableCommand, - "create index if not exists idx_mediastreams on mediastreams(ItemId, StreamIndex)", - //pragmas - "pragma temp_store = memory", + "create index if not exists idx_mediastreams1 on mediastreams(ItemId)", - "pragma shrink_memory" }; _connection.RunQueries(queries, Logger); @@ -223,84 +257,95 @@ namespace MediaBrowser.Server.Implementations.Persistence _connection.AddColumn(Logger, "TypedBaseItems", "TrailerTypes", "Text"); _connection.AddColumn(Logger, "TypedBaseItems", "CriticRating", "Float"); _connection.AddColumn(Logger, "TypedBaseItems", "CriticRatingSummary", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "InheritedTags", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "CleanName", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "PresentationUniqueKey", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "SlugName", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "OriginalTitle", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "PrimaryVersionId", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "DateLastMediaAdded", "DATETIME"); + _connection.AddColumn(Logger, "TypedBaseItems", "Album", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "IsVirtualItem", "BIT"); + _connection.AddColumn(Logger, "TypedBaseItems", "SeriesName", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "UserDataKey", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "SeasonName", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "SeasonId", "GUID"); + _connection.AddColumn(Logger, "TypedBaseItems", "SeriesId", "GUID"); + _connection.AddColumn(Logger, "TypedBaseItems", "SeriesSortName", "Text"); + + _connection.AddColumn(Logger, "UserDataKeys", "Priority", "INT"); + _connection.AddColumn(Logger, "ItemValues", "CleanValue", "Text"); + + _connection.AddColumn(Logger, ChaptersTableName, "ImageDateModified", "DATETIME"); + + string[] postQueries = + + { + // obsolete + "drop index if exists idx_TypedBaseItems", + "drop index if exists idx_mediastreams", + "drop index if exists idx_"+ChaptersTableName, + "drop index if exists idx_UserDataKeys1", + "drop index if exists idx_UserDataKeys2", + "drop index if exists idx_TypeTopParentId3", + "drop index if exists idx_TypeTopParentId2", + "drop index if exists idx_TypeTopParentId4", + "drop index if exists idx_Type", + "drop index if exists idx_TypeTopParentId", + "drop index if exists idx_GuidType", + "drop index if exists idx_TopParentId", + "drop index if exists idx_TypeTopParentId6", + "drop index if exists idx_ItemValues2", + "drop index if exists Idx_ProviderIds", + "drop index if exists idx_ItemValues3", + "drop index if exists idx_ItemValues4", + "drop index if exists idx_ItemValues5", + + "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)", + "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)", + + "create index if not exists idx_PresentationUniqueKey on TypedBaseItems(PresentationUniqueKey)", + "create index if not exists idx_GuidTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,Type,IsFolder,IsVirtualItem)", + //"create index if not exists idx_GuidMediaTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,MediaType,IsFolder,IsVirtualItem)", + "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)", + + // covering index + "create index if not exists idx_TopParentIdGuid on TypedBaseItems(TopParentId,Guid)", + + // live tv programs + "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)", + + // covering index for getitemvalues + "create index if not exists idx_TypeTopParentIdGuid on TypedBaseItems(Type,TopParentId,Guid)", + + // used by movie suggestions + "create index if not exists idx_TypeTopParentIdGroup on TypedBaseItems(Type,TopParentId,PresentationUniqueKey)", + "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)", + + // latest items + "create index if not exists idx_TypeTopParentId9 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey,DateCreated)", + "create index if not exists idx_TypeTopParentId8 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem,PresentationUniqueKey,DateCreated)", + + // resume + "create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)", + + // items by name + "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)", + "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)", + + // covering index + "create index if not exists idx_UserDataKeys3 on UserDataKeys(ItemId,Priority,UserDataKey)" + }; + + _connection.RunQueries(postQueries, Logger); PrepareStatements(); new MediaStreamColumns(_connection, Logger).AddColumns(); - var chapterDbFile = Path.Combine(_appPaths.DataPath, "chapters.db"); - if (File.Exists(chapterDbFile)) - { - MigrateChapters(chapterDbFile); - } - - var mediaStreamsDbFile = Path.Combine(_appPaths.DataPath, "mediainfo.db"); - if (File.Exists(mediaStreamsDbFile)) - { - MigrateMediaStreams(mediaStreamsDbFile); - } - } - - private void MigrateMediaStreams(string file) - { - try - { - var backupFile = file + ".bak"; - File.Copy(file, backupFile, true); - SqliteExtensions.Attach(_connection, backupFile, "MediaInfoOld"); - - var columns = string.Join(",", _mediaStreamSaveColumns); - - string[] queries = { - "REPLACE INTO mediastreams("+columns+") SELECT "+columns+" FROM MediaInfoOld.mediastreams;" - }; - - _connection.RunQueries(queries, Logger); - } - catch (Exception ex) - { - Logger.ErrorException("Error migrating media info database", ex); - } - finally - { - TryDeleteFile(file); - } - } - - private void MigrateChapters(string file) - { - try - { - var backupFile = file + ".bak"; - File.Copy(file, backupFile, true); - SqliteExtensions.Attach(_connection, backupFile, "ChaptersOld"); - - string[] queries = { - "REPLACE INTO "+ChaptersTableName+"(ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath) SELECT ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath FROM ChaptersOld.Chapters;" - }; - - _connection.RunQueries(queries, Logger); - } - catch (Exception ex) - { - Logger.ErrorException("Error migrating chapter database", ex); - } - finally - { - TryDeleteFile(file); - } - } - - private void TryDeleteFile(string file) - { - try - { - File.Delete(file); - } - catch (Exception ex) - { - Logger.ErrorException("Error deleting file {0}", ex, file); - } + DataExtensions.Attach(_connection, Path.Combine(_config.ApplicationPaths.DataPath, "userdata_v2.db"), "UserDataDb"); + await userDataRepo.Initialize(_connection, WriteLock).ConfigureAwait(false); + //await Vacuum(_connection).ConfigureAwait(false); } private readonly string[] _retriveItemColumns = @@ -355,7 +400,19 @@ namespace MediaBrowser.Server.Implementations.Persistence "Studios", "Tags", "SourceType", - "TrailerTypes" + "TrailerTypes", + "OriginalTitle", + "PrimaryVersionId", + "DateLastMediaAdded", + "Album", + "CriticRating", + "CriticRatingSummary", + "IsVirtualItem", + "SeriesName", + "SeasonName", + "SeasonId", + "SeriesId", + "SeriesSortName" }; private readonly string[] _mediaStreamSaveColumns = @@ -385,9 +442,13 @@ namespace MediaBrowser.Server.Implementations.Persistence "BitDepth", "IsAnamorphic", "RefFrames", - "IsCabac", "CodecTag", - "Comment" + "Comment", + "NalLengthSize", + "IsAvc", + "Title", + "TimeBase", + "CodecTimeBase" }; /// <summary> @@ -400,7 +461,7 @@ namespace MediaBrowser.Server.Implementations.Persistence "guid", "type", "data", - "Path", + "Path", "StartDate", "EndDate", "ChannelId", @@ -459,7 +520,22 @@ namespace MediaBrowser.Server.Implementations.Persistence "SourceType", "TrailerTypes", "CriticRating", - "CriticRatingSummary" + "CriticRatingSummary", + "InheritedTags", + "CleanName", + "PresentationUniqueKey", + "SlugName", + "OriginalTitle", + "PrimaryVersionId", + "DateLastMediaAdded", + "Album", + "IsVirtualItem", + "SeriesName", + "UserDataKey", + "SeasonName", + "SeasonId", + "SeriesId", + "SeriesSortName" }; _saveItemCommand = _connection.CreateCommand(); _saveItemCommand.CommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns.ToArray()) + ") values ("; @@ -518,6 +594,7 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@StartPositionTicks"); _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@Name"); _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ImagePath"); + _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ImageDateModified"); // MediaStreams _deleteStreamsCommand = _connection.CreateCommand(); @@ -537,8 +614,61 @@ namespace MediaBrowser.Server.Implementations.Persistence _updateInheritedRatingCommand = _connection.CreateCommand(); _updateInheritedRatingCommand.CommandText = "Update TypedBaseItems set InheritedParentalRatingValue=@InheritedParentalRatingValue where Guid=@Guid"; - _updateInheritedRatingCommand.Parameters.Add(_updateInheritedRatingCommand, "@InheritedParentalRatingValue"); _updateInheritedRatingCommand.Parameters.Add(_updateInheritedRatingCommand, "@Guid"); + _updateInheritedRatingCommand.Parameters.Add(_updateInheritedRatingCommand, "@InheritedParentalRatingValue"); + + _updateInheritedTagsCommand = _connection.CreateCommand(); + _updateInheritedTagsCommand.CommandText = "Update TypedBaseItems set InheritedTags=@InheritedTags where Guid=@Guid"; + _updateInheritedTagsCommand.Parameters.Add(_updateInheritedTagsCommand, "@Guid"); + _updateInheritedTagsCommand.Parameters.Add(_updateInheritedTagsCommand, "@InheritedTags"); + + // user data + _deleteUserDataKeysCommand = _connection.CreateCommand(); + _deleteUserDataKeysCommand.CommandText = "delete from UserDataKeys where ItemId=@Id"; + _deleteUserDataKeysCommand.Parameters.Add(_deleteUserDataKeysCommand, "@Id"); + + _saveUserDataKeysCommand = _connection.CreateCommand(); + _saveUserDataKeysCommand.CommandText = "insert into UserDataKeys (ItemId, UserDataKey, Priority) values (@ItemId, @UserDataKey, @Priority)"; + _saveUserDataKeysCommand.Parameters.Add(_saveUserDataKeysCommand, "@ItemId"); + _saveUserDataKeysCommand.Parameters.Add(_saveUserDataKeysCommand, "@UserDataKey"); + _saveUserDataKeysCommand.Parameters.Add(_saveUserDataKeysCommand, "@Priority"); + + // item values + _deleteItemValuesCommand = _connection.CreateCommand(); + _deleteItemValuesCommand.CommandText = "delete from ItemValues where ItemId=@Id"; + _deleteItemValuesCommand.Parameters.Add(_deleteItemValuesCommand, "@Id"); + + _saveItemValuesCommand = _connection.CreateCommand(); + _saveItemValuesCommand.CommandText = "insert into ItemValues (ItemId, Type, Value, CleanValue) values (@ItemId, @Type, @Value, @CleanValue)"; + _saveItemValuesCommand.Parameters.Add(_saveItemValuesCommand, "@ItemId"); + _saveItemValuesCommand.Parameters.Add(_saveItemValuesCommand, "@Type"); + _saveItemValuesCommand.Parameters.Add(_saveItemValuesCommand, "@Value"); + _saveItemValuesCommand.Parameters.Add(_saveItemValuesCommand, "@CleanValue"); + + // provider ids + _deleteProviderIdsCommand = _connection.CreateCommand(); + _deleteProviderIdsCommand.CommandText = "delete from ProviderIds where ItemId=@Id"; + _deleteProviderIdsCommand.Parameters.Add(_deleteProviderIdsCommand, "@Id"); + + _saveProviderIdsCommand = _connection.CreateCommand(); + _saveProviderIdsCommand.CommandText = "insert into ProviderIds (ItemId, Name, Value) values (@ItemId, @Name, @Value)"; + _saveProviderIdsCommand.Parameters.Add(_saveProviderIdsCommand, "@ItemId"); + _saveProviderIdsCommand.Parameters.Add(_saveProviderIdsCommand, "@Name"); + _saveProviderIdsCommand.Parameters.Add(_saveProviderIdsCommand, "@Value"); + + // images + _deleteImagesCommand = _connection.CreateCommand(); + _deleteImagesCommand.CommandText = "delete from Images where ItemId=@Id"; + _deleteImagesCommand.Parameters.Add(_deleteImagesCommand, "@Id"); + + _saveImagesCommand = _connection.CreateCommand(); + _saveImagesCommand.CommandText = "insert into Images (ItemId, ImageType, Path, DateModified, IsPlaceHolder, SortOrder) values (@ItemId, @ImageType, @Path, @DateModified, @IsPlaceHolder, @SortOrder)"; + _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@ItemId"); + _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@ImageType"); + _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@Path"); + _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@DateModified"); + _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@IsPlaceHolder"); + _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@SortOrder"); } /// <summary> @@ -696,7 +826,15 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveItemCommand.GetParameter(index++).Value = item.DateLastRefreshed; } - _saveItemCommand.GetParameter(index++).Value = item.DateLastSaved; + if (item.DateLastSaved == default(DateTime)) + { + _saveItemCommand.GetParameter(index++).Value = null; + } + else + { + _saveItemCommand.GetParameter(index++).Value = item.DateLastSaved; + } + _saveItemCommand.GetParameter(index++).Value = item.IsInMixedFolder; _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.LockedFields.Select(i => i.ToString()).ToArray()); _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.Studios.ToArray()); @@ -712,7 +850,15 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveItemCommand.GetParameter(index++).Value = item.ServiceName; - _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.Tags.ToArray()); + if (item.Tags.Count > 0) + { + _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.Tags.ToArray()); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + _saveItemCommand.GetParameter(index++).Value = item.IsFolder; _saveItemCommand.GetParameter(index++).Value = item.GetBlockUnratedType().ToString(); @@ -741,7 +887,7 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveItemCommand.GetParameter(index++).Value = item.SourceType.ToString(); var trailer = item as Trailer; - if (trailer != null) + if (trailer != null && trailer.TrailerTypes.Count > 0) { _saveItemCommand.GetParameter(index++).Value = string.Join("|", trailer.TrailerTypes.Select(i => i.ToString()).ToArray()); } @@ -752,7 +898,89 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveItemCommand.GetParameter(index++).Value = item.CriticRating; _saveItemCommand.GetParameter(index++).Value = item.CriticRatingSummary; - + + var inheritedTags = item.GetInheritedTags(); + if (inheritedTags.Count > 0) + { + _saveItemCommand.GetParameter(index++).Value = string.Join("|", inheritedTags.ToArray()); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + if (string.IsNullOrWhiteSpace(item.Name)) + { + _saveItemCommand.GetParameter(index++).Value = null; + } + else + { + _saveItemCommand.GetParameter(index++).Value = item.Name.RemoveDiacritics(); + } + + _saveItemCommand.GetParameter(index++).Value = item.PresentationUniqueKey; + _saveItemCommand.GetParameter(index++).Value = item.SlugName; + _saveItemCommand.GetParameter(index++).Value = item.OriginalTitle; + + var video = item as Video; + if (video != null) + { + _saveItemCommand.GetParameter(index++).Value = video.PrimaryVersionId; + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + var folder = item as Folder; + if (folder != null && folder.DateLastMediaAdded.HasValue) + { + _saveItemCommand.GetParameter(index++).Value = folder.DateLastMediaAdded.Value; + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + _saveItemCommand.GetParameter(index++).Value = item.Album; + + _saveItemCommand.GetParameter(index++).Value = item.IsVirtualItem || (!item.IsFolder && item.LocationType == LocationType.Virtual); + + var hasSeries = item as IHasSeries; + if (hasSeries != null) + { + _saveItemCommand.GetParameter(index++).Value = hasSeries.FindSeriesName(); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + } + + _saveItemCommand.GetParameter(index++).Value = item.GetUserDataKeys().FirstOrDefault(); + + var episode = item as Episode; + if (episode != null) + { + _saveItemCommand.GetParameter(index++).Value = episode.FindSeasonName(); + _saveItemCommand.GetParameter(index++).Value = episode.FindSeasonId(); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; + } + + if (hasSeries != null) + { + _saveItemCommand.GetParameter(index++).Value = hasSeries.FindSeriesId(); + _saveItemCommand.GetParameter(index++).Value = hasSeries.FindSeriesSortName(); + } + else + { + _saveItemCommand.GetParameter(index++).Value = null; + _saveItemCommand.GetParameter(index++).Value = null; + } + _saveItemCommand.Transaction = transaction; _saveItemCommand.ExecuteNonQuery(); @@ -761,6 +989,11 @@ namespace MediaBrowser.Server.Implementations.Persistence { UpdateAncestors(item.Id, item.GetAncestorIds().Distinct().ToList(), transaction); } + + UpdateUserDataKeys(item.Id, item.GetUserDataKeys().Distinct(StringComparer.OrdinalIgnoreCase).ToList(), transaction); + UpdateImages(item.Id, item.ImageInfos, transaction); + UpdateProviderIds(item.Id, item.ProviderIds, transaction); + UpdateItemValues(item.Id, GetItemValuesToSave(item), transaction); } transaction.Commit(); @@ -836,7 +1069,7 @@ namespace MediaBrowser.Server.Implementations.Persistence if (type == null) { - Logger.Debug("Unknown type {0}", typeString); + //Logger.Debug("Unknown type {0}", typeString); return null; } @@ -1125,6 +1358,102 @@ namespace MediaBrowser.Server.Implementations.Persistence } } + var index = 51; + + if (!reader.IsDBNull(index)) + { + item.OriginalTitle = reader.GetString(index); + } + index++; + + var video = item as Video; + if (video != null) + { + if (!reader.IsDBNull(index)) + { + video.PrimaryVersionId = reader.GetString(index); + } + } + index++; + + var folder = item as Folder; + if (folder != null && !reader.IsDBNull(index)) + { + folder.DateLastMediaAdded = reader.GetDateTime(index).ToUniversalTime(); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.Album = reader.GetString(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.CriticRating = reader.GetFloat(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.CriticRatingSummary = reader.GetString(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.IsVirtualItem = reader.GetBoolean(index); + } + index++; + + var hasSeries = item as IHasSeries; + if (hasSeries != null) + { + if (!reader.IsDBNull(index)) + { + hasSeries.SeriesName = reader.GetString(index); + } + } + index++; + + var episode = item as Episode; + if (episode != null) + { + if (!reader.IsDBNull(index)) + { + episode.SeasonName = reader.GetString(index); + } + index++; + if (!reader.IsDBNull(index)) + { + episode.SeasonId = reader.GetGuid(index); + } + } + else + { + index++; + } + index++; + + if (hasSeries != null) + { + if (!reader.IsDBNull(index)) + { + hasSeries.SeriesId = reader.GetGuid(index); + } + } + index++; + + if (hasSeries != null) + { + if (!reader.IsDBNull(index)) + { + hasSeries.SeriesSortName = reader.GetString(index); + } + } + index++; + return item; } @@ -1182,10 +1511,11 @@ namespace MediaBrowser.Server.Implementations.Persistence { throw new ArgumentNullException("id"); } + var list = new List<ChapterInfo>(); using (var cmd = _connection.CreateCommand()) { - cmd.CommandText = "select StartPositionTicks,Name,ImagePath from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"; + cmd.CommandText = "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"; cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = id; @@ -1193,10 +1523,12 @@ namespace MediaBrowser.Server.Implementations.Persistence { while (reader.Read()) { - yield return GetChapter(reader); + list.Add(GetChapter(reader)); } } } + + return list; } /// <summary> @@ -1216,7 +1548,7 @@ namespace MediaBrowser.Server.Implementations.Persistence using (var cmd = _connection.CreateCommand()) { - cmd.CommandText = "select StartPositionTicks,Name,ImagePath from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"; + cmd.CommandText = "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"; cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = id; cmd.Parameters.Add(cmd, "@ChapterIndex", DbType.Int32).Value = index; @@ -1254,6 +1586,11 @@ namespace MediaBrowser.Server.Implementations.Persistence chapter.ImagePath = reader.GetString(2); } + if (!reader.IsDBNull(3)) + { + chapter.ImageDateModified = reader.GetDateTime(3).ToUniversalTime(); + } + return chapter; } @@ -1271,7 +1608,7 @@ namespace MediaBrowser.Server.Implementations.Persistence /// or /// cancellationToken /// </exception> - public async Task SaveChapters(Guid id, IEnumerable<ChapterInfo> chapters, CancellationToken cancellationToken) + public async Task SaveChapters(Guid id, List<ChapterInfo> chapters, CancellationToken cancellationToken) { CheckDisposed(); @@ -1313,6 +1650,7 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveChapterCommand.GetParameter(2).Value = chapter.StartPositionTicks; _saveChapterCommand.GetParameter(3).Value = chapter.Name; _saveChapterCommand.GetParameter(4).Value = chapter.ImagePath; + _saveChapterCommand.GetParameter(5).Value = chapter.ImageDateModified; _saveChapterCommand.Transaction = transaction; @@ -1368,37 +1706,167 @@ namespace MediaBrowser.Server.Implementations.Persistence } } - public IEnumerable<BaseItem> GetItemsOfType(Type type) + private bool EnableJoinUserData(InternalItemsQuery query) { - if (type == null) + if (query.User == null) { - throw new ArgumentNullException("type"); + return false; } - CheckDisposed(); + if (query.SimilarTo != null && query.User != null) + { + return true; + } - using (var cmd = _connection.CreateCommand()) + if (query.SortBy != null && query.SortBy.Length > 0) + { + 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; + } + } + + if (query.IsFavoriteOrLiked.HasValue) { - cmd.CommandText = "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems where type = @type"; + return true; + } - cmd.Parameters.Add(cmd, "@type", DbType.String).Value = type.FullName; + if (query.IsFavorite.HasValue) + { + return true; + } - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) - { - while (reader.Read()) - { - var item = GetItem(reader); + if (query.IsResumable.HasValue) + { + return true; + } - if (item != null) - { - yield return item; - } - } - } + if (query.IsPlayed.HasValue) + { + return true; + } + + if (query.IsLiked.HasValue) + { + return true; } + + return false; } - public IEnumerable<BaseItem> GetItemList(InternalItemsQuery query) + private string[] GetFinalColumnsToSelect(InternalItemsQuery query, string[] startColumns, IDbCommand cmd) + { + var list = startColumns.ToList(); + + if (EnableJoinUserData(query)) + { + list.Add("UserDataDb.UserData.UserId"); + list.Add("UserDataDb.UserData.lastPlayedDate"); + list.Add("UserDataDb.UserData.playbackPositionTicks"); + list.Add("UserDataDb.UserData.playcount"); + list.Add("UserDataDb.UserData.isFavorite"); + list.Add("UserDataDb.UserData.played"); + list.Add("UserDataDb.UserData.rating"); + } + + if (query.SimilarTo != null) + { + var item = query.SimilarTo; + + var builder = new StringBuilder(); + builder.Append("("); + + builder.Append("((OfficialRating=@ItemOfficialRating) * 10)"); + //builder.Append("+ ((ProductionYear=@ItemProductionYear) * 10)"); + + builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 10 Then 2 Else 0 End )"); + builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 2 Else 0 End )"); + + //// genres + builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and Type=2 and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId and type=2)) * 10)"); + + //// tags + builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and Type=4 and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId and type=4)) * 10)"); + + builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and Type=5 and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId and type=5)) * 10)"); + + builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and Type=3 and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId and type=3)) * 3)"); + + //builder.Append("+ ((Select count(Name) from People where ItemId=Guid and Name in (select Name from People where ItemId=@SimilarItemId)) * 3)"); + + ////builder.Append("(select group_concat((Select Name from People where ItemId=Guid and Name in (Select Name from People where ItemId=@SimilarItemId)), '|'))"); + + builder.Append(") as SimilarityScore"); + + list.Add(builder.ToString()); + cmd.Parameters.Add(cmd, "@ItemOfficialRating", DbType.String).Value = item.OfficialRating; + cmd.Parameters.Add(cmd, "@ItemProductionYear", DbType.Int32).Value = item.ProductionYear ?? 0; + cmd.Parameters.Add(cmd, "@SimilarItemId", DbType.Guid).Value = item.Id; + + var excludeIds = query.ExcludeItemIds.ToList(); + excludeIds.Add(item.Id.ToString("N")); + query.ExcludeItemIds = excludeIds.ToArray(); + + query.ExcludeProviderIds = item.ProviderIds; + } + + return list.ToArray(); + } + + private string GetJoinUserDataText(InternalItemsQuery query) + { + if (!EnableJoinUserData(query)) + { + return string.Empty; + } + + if (_config.Configuration.SchemaVersion >= 96) + { + return " left join UserDataDb.UserData on UserDataKey=UserDataDb.UserData.Key And (UserId=@UserId)"; + } + + return " left join UserDataDb.UserData on (select UserDataKey from UserDataKeys where ItemId=Guid order by Priority LIMIT 1)=UserDataDb.UserData.Key And (UserId=@UserId)"; + } + + private string GetGroupBy(InternalItemsQuery query) + { + var groups = new List<string>(); + + if (EnableGroupByPresentationUniqueKey(query)) + { + groups.Add("PresentationUniqueKey"); + } + + if (groups.Count > 0) + { + return " Group by " + string.Join(",", groups.ToArray()); + } + + return string.Empty; + } + + private string GetFromText(string alias = "A") + { + return " from TypedBaseItems " + alias; + } + + public List<BaseItem> GetItemList(InternalItemsQuery query) { if (query == null) { @@ -1407,11 +1875,27 @@ namespace MediaBrowser.Server.Implementations.Persistence CheckDisposed(); + var now = DateTime.UtcNow; + + var list = new List<BaseItem>(); + + // Hack for right now since we currently don't support filtering out these duplicates within a query + if (query.Limit.HasValue && query.EnableGroupByMetadataKey) + { + query.Limit = query.Limit.Value + 4; + } + using (var cmd = _connection.CreateCommand()) { - cmd.CommandText = "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems"; + cmd.CommandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns, cmd)) + GetFromText(); + cmd.CommandText += GetJoinUserDataText(query); + + if (EnableJoinUserData(query)) + { + cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = query.User.Id; + } - var whereClauses = GetWhereClauses(query, cmd, true); + var whereClauses = GetWhereClauses(query, cmd); var whereText = whereClauses.Count == 0 ? string.Empty : @@ -1419,27 +1903,115 @@ namespace MediaBrowser.Server.Implementations.Persistence cmd.CommandText += whereText; + cmd.CommandText += GetGroupBy(query); + cmd.CommandText += GetOrderByText(query); - if (query.Limit.HasValue) + if (query.Limit.HasValue || query.StartIndex.HasValue) { - cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(CultureInfo.InvariantCulture); - } + var offset = query.StartIndex ?? 0; - //Logger.Debug(cmd.CommandText); + if (query.Limit.HasValue || offset > 0) + { + cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + } + + if (offset > 0) + { + cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + } + } using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) { + LogQueryTime("GetItemList", cmd, now); + while (reader.Read()) { var item = GetItem(reader); if (item != null) { - yield return item; + list.Add(item); } } } } + + // Hack for right now since we currently don't support filtering out these duplicates within a query + if (query.EnableGroupByMetadataKey) + { + var limit = query.Limit ?? int.MaxValue; + limit -= 4; + var newList = new List<BaseItem>(); + + foreach (var item in list) + { + AddItem(newList, item); + + if (newList.Count >= limit) + { + break; + } + } + + list = newList; + } + + return list; + } + + private void AddItem(List<BaseItem> items, BaseItem newItem) + { + var providerIds = newItem.ProviderIds.ToList(); + + for (var i = 0; i < items.Count; i++) + { + var item = items[i]; + + foreach (var providerId in providerIds) + { + if (providerId.Key == MetadataProviders.TmdbCollection.ToString()) + { + continue; + } + if (item.GetProviderId(providerId.Key) == providerId.Value) + { + if (newItem.SourceType == SourceType.Library) + { + items[i] = newItem; + } + return; + } + } + } + + items.Add(newItem); + } + + private void LogQueryTime(string methodName, IDbCommand cmd, DateTime startDate) + { + var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds; + + var slowThreshold = 1000; + +#if DEBUG + slowThreshold = 50; +#endif + + if (elapsed >= slowThreshold) + { + Logger.Debug("{2} query time (slow): {0}ms. Query: {1}", + Convert.ToInt32(elapsed), + cmd.CommandText, + methodName); + } + else + { + //Logger.Debug("{2} query time: {0}ms. Query: {1}", + // Convert.ToInt32(elapsed), + // cmd.CommandText, + // methodName); + } } public QueryResult<BaseItem> GetItems(InternalItemsQuery query) @@ -1451,52 +2023,109 @@ namespace MediaBrowser.Server.Implementations.Persistence CheckDisposed(); + if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0)) + { + var list = GetItemList(query); + return new QueryResult<BaseItem> + { + Items = list.ToArray(), + TotalRecordCount = list.Count + }; + } + + var now = DateTime.UtcNow; + using (var cmd = _connection.CreateCommand()) { - cmd.CommandText = "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems"; + cmd.CommandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns, cmd)) + GetFromText(); + cmd.CommandText += GetJoinUserDataText(query); - var whereClauses = GetWhereClauses(query, cmd, false); + if (EnableJoinUserData(query)) + { + cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = query.User.Id; + } + + var whereClauses = GetWhereClauses(query, cmd); var whereTextWithoutPaging = whereClauses.Count == 0 ? string.Empty : " where " + string.Join(" AND ", whereClauses.ToArray()); - whereClauses = GetWhereClauses(query, cmd, true); - var whereText = whereClauses.Count == 0 ? string.Empty : " where " + string.Join(" AND ", whereClauses.ToArray()); cmd.CommandText += whereText; + cmd.CommandText += GetGroupBy(query); + cmd.CommandText += GetOrderByText(query); - if (query.Limit.HasValue) + if (query.Limit.HasValue || query.StartIndex.HasValue) { - cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(CultureInfo.InvariantCulture); + var offset = query.StartIndex ?? 0; + + if (query.Limit.HasValue || offset > 0) + { + cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + } + + if (offset > 0) + { + cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + } } - cmd.CommandText += "; select count (guid) from TypedBaseItems" + whereTextWithoutPaging; + cmd.CommandText += ";"; - //Logger.Debug(cmd.CommandText); + var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; + + if (isReturningZeroItems) + { + cmd.CommandText = ""; + } + + if (EnableGroupByPresentationUniqueKey(query)) + { + cmd.CommandText += " select count (distinct PresentationUniqueKey)" + GetFromText(); + } + else + { + cmd.CommandText += " select count (guid)" + GetFromText(); + } + + cmd.CommandText += GetJoinUserDataText(query); + cmd.CommandText += whereTextWithoutPaging; var list = new List<BaseItem>(); var count = 0; using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) { - while (reader.Read()) + LogQueryTime("GetItems", cmd, now); + + if (isReturningZeroItems) { - var item = GetItem(reader); - if (item != null) + if (reader.Read()) { - list.Add(item); + count = reader.GetInt32(0); } } - - if (reader.NextResult() && reader.Read()) + else { - count = reader.GetInt32(0); + while (reader.Read()) + { + var item = GetItem(reader); + if (item != null) + { + list.Add(item); + } + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } } } @@ -1510,33 +2139,109 @@ namespace MediaBrowser.Server.Implementations.Persistence private string GetOrderByText(InternalItemsQuery query) { + if (query.SimilarTo != null) + { + if (query.SortBy == null || query.SortBy.Length == 0) + { + if (query.User != null) + { + query.SortBy = new[] { ItemSortBy.IsPlayed, "SimilarityScore", ItemSortBy.Random }; + } + else + { + query.SortBy = new[] { "SimilarityScore", ItemSortBy.Random }; + } + query.SortOrder = SortOrder.Descending; + } + } + if (query.SortBy == null || query.SortBy.Length == 0) { return string.Empty; } - var sortOrder = query.SortOrder == SortOrder.Descending ? "DESC" : "ASC"; + var isAscending = query.SortOrder != SortOrder.Descending; - return " ORDER BY " + string.Join(",", query.SortBy.Select(i => MapOrderByField(i) + " " + sortOrder).ToArray()); + return " ORDER BY " + string.Join(",", query.SortBy.Select(i => + { + var columnMap = MapOrderByField(i, query); + var columnAscending = isAscending; + if (columnMap.Item2) + { + columnAscending = !columnAscending; + } + + var sortOrder = columnAscending ? "ASC" : "DESC"; + + return columnMap.Item1 + " " + sortOrder; + }).ToArray()); } - private string MapOrderByField(string name) + private Tuple<string, bool> MapOrderByField(string name, InternalItemsQuery query) { if (string.Equals(name, ItemSortBy.AirTime, StringComparison.OrdinalIgnoreCase)) { // TODO - return "SortName"; + return new Tuple<string, bool>("SortName", false); } if (string.Equals(name, ItemSortBy.Runtime, StringComparison.OrdinalIgnoreCase)) { - return "RuntimeTicks"; + return new Tuple<string, bool>("RuntimeTicks", false); } if (string.Equals(name, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase)) { - return "RANDOM()"; + return new Tuple<string, bool>("RANDOM()", false); + } + if (string.Equals(name, ItemSortBy.DatePlayed, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple<string, bool>("LastPlayedDate", false); + } + if (string.Equals(name, ItemSortBy.PlayCount, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple<string, bool>("PlayCount", false); + } + if (string.Equals(name, ItemSortBy.IsFavoriteOrLiked, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple<string, bool>("IsFavorite", true); + } + if (string.Equals(name, ItemSortBy.IsFolder, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple<string, bool>("IsFolder", true); + } + if (string.Equals(name, ItemSortBy.IsPlayed, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple<string, bool>("played", true); + } + if (string.Equals(name, ItemSortBy.IsUnplayed, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple<string, bool>("played", false); + } + if (string.Equals(name, ItemSortBy.DateLastContentAdded, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple<string, bool>("DateLastMediaAdded", false); + } + if (string.Equals(name, ItemSortBy.Artist, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple<string, bool>("(select CleanValue from itemvalues where ItemId=Guid and Type=0 LIMIT 1)", false); + } + if (string.Equals(name, ItemSortBy.AlbumArtist, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple<string, bool>("(select CleanValue from itemvalues where ItemId=Guid and Type=1 LIMIT 1)", false); + } + if (string.Equals(name, ItemSortBy.OfficialRating, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple<string, bool>("ParentalRatingValue", false); + } + if (string.Equals(name, ItemSortBy.Studio, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple<string, bool>("(select CleanValue from itemvalues where ItemId=Guid and Type=3 LIMIT 1)", false); + } + if (string.Equals(name, ItemSortBy.SeriesDatePlayed, StringComparison.OrdinalIgnoreCase)) + { + return new Tuple<string, bool>("(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where B.Guid in (Select ItemId from AncestorIds where AncestorId in (select guid from typedbaseitems c where C.Type = 'MediaBrowser.Controller.Entities.TV.Series' And C.Guid in (Select AncestorId from AncestorIds where ItemId=A.Guid))))", false); } - return name; + return new Tuple<string, bool>(name, false); } public List<Guid> GetItemIdsList(InternalItemsQuery query) @@ -1548,11 +2253,19 @@ namespace MediaBrowser.Server.Implementations.Persistence CheckDisposed(); + var now = DateTime.UtcNow; + using (var cmd = _connection.CreateCommand()) { - cmd.CommandText = "select guid from TypedBaseItems"; + cmd.CommandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" }, cmd)) + GetFromText(); + cmd.CommandText += GetJoinUserDataText(query); + + if (EnableJoinUserData(query)) + { + cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = query.User.Id; + } - var whereClauses = GetWhereClauses(query, cmd, true); + var whereClauses = GetWhereClauses(query, cmd); var whereText = whereClauses.Count == 0 ? string.Empty : @@ -1560,19 +2273,31 @@ namespace MediaBrowser.Server.Implementations.Persistence cmd.CommandText += whereText; + cmd.CommandText += GetGroupBy(query); + cmd.CommandText += GetOrderByText(query); - if (query.Limit.HasValue) + if (query.Limit.HasValue || query.StartIndex.HasValue) { - cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(CultureInfo.InvariantCulture); + var offset = query.StartIndex ?? 0; + + if (query.Limit.HasValue || offset > 0) + { + cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + } + + if (offset > 0) + { + cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + } } var list = new List<Guid>(); - //Logger.Debug(cmd.CommandText); - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) { + LogQueryTime("GetItemIdsList", cmd, now); + while (reader.Read()) { list.Add(reader.GetGuid(0)); @@ -1596,25 +2321,35 @@ namespace MediaBrowser.Server.Implementations.Persistence { cmd.CommandText = "select guid,path from TypedBaseItems"; - var whereClauses = GetWhereClauses(query, cmd, false); + var whereClauses = GetWhereClauses(query, cmd); var whereTextWithoutPaging = whereClauses.Count == 0 ? string.Empty : " where " + string.Join(" AND ", whereClauses.ToArray()); - whereClauses = GetWhereClauses(query, cmd, true); - var whereText = whereClauses.Count == 0 ? string.Empty : " where " + string.Join(" AND ", whereClauses.ToArray()); cmd.CommandText += whereText; + cmd.CommandText += GetGroupBy(query); + cmd.CommandText += GetOrderByText(query); - if (query.Limit.HasValue) + if (query.Limit.HasValue || query.StartIndex.HasValue) { - cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(CultureInfo.InvariantCulture); + var offset = query.StartIndex ?? 0; + + if (query.Limit.HasValue || offset > 0) + { + cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + } + + if (offset > 0) + { + cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + } } cmd.CommandText += "; select count (guid) from TypedBaseItems" + whereTextWithoutPaging; @@ -1661,17 +2396,29 @@ namespace MediaBrowser.Server.Implementations.Persistence CheckDisposed(); - using (var cmd = _connection.CreateCommand()) + if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0)) { - cmd.CommandText = "select guid from TypedBaseItems"; + var list = GetItemIdsList(query); + return new QueryResult<Guid> + { + Items = list.ToArray(), + TotalRecordCount = list.Count + }; + } - var whereClauses = GetWhereClauses(query, cmd, false); + var now = DateTime.UtcNow; - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" }, cmd)) + GetFromText(); + + var whereClauses = GetWhereClauses(query, cmd); + cmd.CommandText += GetJoinUserDataText(query); - whereClauses = GetWhereClauses(query, cmd, true); + if (EnableJoinUserData(query)) + { + cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = query.User.Id; + } var whereText = whereClauses.Count == 0 ? string.Empty : @@ -1679,22 +2426,44 @@ namespace MediaBrowser.Server.Implementations.Persistence cmd.CommandText += whereText; + cmd.CommandText += GetGroupBy(query); + cmd.CommandText += GetOrderByText(query); - if (query.Limit.HasValue) + if (query.Limit.HasValue || query.StartIndex.HasValue) { - cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(CultureInfo.InvariantCulture); + var offset = query.StartIndex ?? 0; + + if (query.Limit.HasValue || offset > 0) + { + cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + } + + if (offset > 0) + { + cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + } } - cmd.CommandText += "; select count (guid) from TypedBaseItems" + whereTextWithoutPaging; + if (EnableGroupByPresentationUniqueKey(query)) + { + cmd.CommandText += "; select count (distinct PresentationUniqueKey)" + GetFromText(); + } + else + { + cmd.CommandText += "; select count (guid)" + GetFromText(); + } + + cmd.CommandText += GetJoinUserDataText(query); + cmd.CommandText += whereText; var list = new List<Guid>(); var count = 0; - //Logger.Debug(cmd.CommandText); - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) { + LogQueryTime("GetItemIds", cmd, now); + while (reader.Read()) { list.Add(reader.GetGuid(0)); @@ -1714,10 +2483,14 @@ namespace MediaBrowser.Server.Implementations.Persistence } } - private List<string> GetWhereClauses(InternalItemsQuery query, IDbCommand cmd, bool addPaging) + private List<string> GetWhereClauses(InternalItemsQuery query, IDbCommand cmd, string paramSuffix = "") { var whereClauses = new List<string>(); + if (EnableJoinUserData(query)) + { + //whereClauses.Add("(UserId is null or UserId=@UserId)"); + } if (query.IsCurrentSchema.HasValue) { if (query.IsCurrentSchema.Value) @@ -1747,7 +2520,24 @@ namespace MediaBrowser.Server.Implementations.Persistence } if (query.IsMovie.HasValue) { - whereClauses.Add("IsMovie=@IsMovie"); + 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"); + } + else + { + whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)"); + } cmd.Parameters.Add(cmd, "@IsMovie", DbType.Boolean).Value = query.IsMovie; } if (query.IsKids.HasValue) @@ -1769,8 +2559,8 @@ namespace MediaBrowser.Server.Implementations.Persistence var includeTypes = query.IncludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray(); if (includeTypes.Length == 1) { - whereClauses.Add("type=@type"); - cmd.Parameters.Add(cmd, "@type", DbType.String).Value = includeTypes[0]; + whereClauses.Add("type=@type" + paramSuffix); + cmd.Parameters.Add(cmd, "@type" + paramSuffix, DbType.String).Value = includeTypes[0]; } else if (includeTypes.Length > 1) { @@ -1813,6 +2603,12 @@ namespace MediaBrowser.Server.Implementations.Persistence cmd.Parameters.Add(cmd, "@Path", DbType.String).Value = query.Path; } + if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) + { + whereClauses.Add("PresentationUniqueKey=@PresentationUniqueKey"); + cmd.Parameters.Add(cmd, "@PresentationUniqueKey", DbType.String).Value = query.PresentationUniqueKey; + } + if (query.MinCommunityRating.HasValue) { whereClauses.Add("CommunityRating>=@MinCommunityRating"); @@ -1837,11 +2633,21 @@ namespace MediaBrowser.Server.Implementations.Persistence // cmd.Parameters.Add(cmd, "@MaxPlayers", DbType.Int32).Value = query.MaxPlayers.Value; //} + if (query.IndexNumber.HasValue) + { + whereClauses.Add("IndexNumber=@IndexNumber"); + cmd.Parameters.Add(cmd, "@IndexNumber", DbType.Int32).Value = query.IndexNumber.Value; + } if (query.ParentIndexNumber.HasValue) { - whereClauses.Add("ParentIndexNumber=@MinEndDate"); + whereClauses.Add("ParentIndexNumber=@ParentIndexNumber"); cmd.Parameters.Add(cmd, "@ParentIndexNumber", DbType.Int32).Value = query.ParentIndexNumber.Value; } + if (query.ParentIndexNumberNotEquals.HasValue) + { + whereClauses.Add("(ParentIndexNumber<>@ParentIndexNumberNotEquals or ParentIndexNumber is null)"); + cmd.Parameters.Add(cmd, "@ParentIndexNumberNotEquals", DbType.Int32).Value = query.ParentIndexNumberNotEquals.Value; + } if (query.MinEndDate.HasValue) { whereClauses.Add("EndDate>=@MinEndDate"); @@ -1913,20 +2719,6 @@ namespace MediaBrowser.Server.Implementations.Persistence whereClauses.Add(clause); } - if (query.ExcludeTrailerTypes.Length > 0) - { - var clauses = new List<string>(); - var index = 0; - foreach (var type in query.ExcludeTrailerTypes) - { - clauses.Add("TrailerTypes not like @TrailerTypes" + index); - cmd.Parameters.Add(cmd, "@TrailerTypes" + index, DbType.String).Value = "%" + type + "%"; - index++; - } - var clause = "(" + string.Join(" AND ", clauses.ToArray()) + ")"; - whereClauses.Add(clause); - } - if (query.IsAiring.HasValue) { if (query.IsAiring.Value) @@ -1944,16 +2736,176 @@ namespace MediaBrowser.Server.Implementations.Persistence } } + if (query.PersonIds.Length > 0) + { + // Todo: improve without having to do this + query.Person = query.PersonIds.Select(i => RetrieveItem(new Guid(i))).Where(i => i != null).Select(i => i.Name).FirstOrDefault(); + } + if (!string.IsNullOrWhiteSpace(query.Person)) { whereClauses.Add("Guid in (select ItemId from People where Name=@PersonName)"); cmd.Parameters.Add(cmd, "@PersonName", DbType.String).Value = query.Person; } + if (!string.IsNullOrWhiteSpace(query.SlugName)) + { + whereClauses.Add("SlugName=@SlugName"); + cmd.Parameters.Add(cmd, "@SlugName", DbType.String).Value = query.SlugName; + } + + if (!string.IsNullOrWhiteSpace(query.MinSortName)) + { + whereClauses.Add("SortName>=@MinSortName"); + cmd.Parameters.Add(cmd, "@MinSortName", DbType.String).Value = query.MinSortName; + } + + if (!string.IsNullOrWhiteSpace(query.Name)) + { + whereClauses.Add("CleanName=@Name"); + cmd.Parameters.Add(cmd, "@Name", DbType.String).Value = query.Name.RemoveDiacritics(); + } + if (!string.IsNullOrWhiteSpace(query.NameContains)) { - whereClauses.Add("Name like @NameContains"); - cmd.Parameters.Add(cmd, "@NameContains", DbType.String).Value = "%" + query.NameContains + "%"; + whereClauses.Add("CleanName like @NameContains"); + cmd.Parameters.Add(cmd, "@NameContains", DbType.String).Value = "%" + query.NameContains.RemoveDiacritics() + "%"; + } + if (!string.IsNullOrWhiteSpace(query.NameStartsWith)) + { + whereClauses.Add("SortName like @NameStartsWith"); + cmd.Parameters.Add(cmd, "@NameStartsWith", DbType.String).Value = query.NameStartsWith + "%"; + } + if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater)) + { + whereClauses.Add("SortName >= @NameStartsWithOrGreater"); + // lowercase this because SortName is stored as lowercase + cmd.Parameters.Add(cmd, "@NameStartsWithOrGreater", DbType.String).Value = query.NameStartsWithOrGreater.ToLower(); + } + if (!string.IsNullOrWhiteSpace(query.NameLessThan)) + { + whereClauses.Add("SortName < @NameLessThan"); + // lowercase this because SortName is stored as lowercase + cmd.Parameters.Add(cmd, "@NameLessThan", DbType.String).Value = query.NameLessThan.ToLower(); + } + + if (query.ImageTypes.Length > 0 && _config.Configuration.SchemaVersion >= 87) + { + var requiredImageIndex = 0; + + foreach (var requiredImage in query.ImageTypes) + { + var paramName = "@RequiredImageType" + requiredImageIndex; + whereClauses.Add("(select path from images where ItemId=Guid and ImageType=" + paramName + " limit 1) not null"); + cmd.Parameters.Add(cmd, paramName, DbType.Int32).Value = (int)requiredImage; + requiredImageIndex++; + } + } + + if (query.IsLiked.HasValue) + { + if (query.IsLiked.Value) + { + whereClauses.Add("rating>=@UserRating"); + cmd.Parameters.Add(cmd, "@UserRating", DbType.Double).Value = UserItemData.MinLikeValue; + } + else + { + whereClauses.Add("(rating is null or rating<@UserRating)"); + cmd.Parameters.Add(cmd, "@UserRating", DbType.Double).Value = UserItemData.MinLikeValue; + } + } + + if (query.IsFavoriteOrLiked.HasValue) + { + if (query.IsFavoriteOrLiked.Value) + { + whereClauses.Add("IsFavorite=@IsFavoriteOrLiked"); + } + else + { + whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavoriteOrLiked)"); + } + cmd.Parameters.Add(cmd, "@IsFavoriteOrLiked", DbType.Boolean).Value = query.IsFavoriteOrLiked.Value; + } + + if (query.IsFavorite.HasValue) + { + if (query.IsFavorite.Value) + { + whereClauses.Add("IsFavorite=@IsFavorite"); + } + else + { + whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavorite)"); + } + cmd.Parameters.Add(cmd, "@IsFavorite", DbType.Boolean).Value = query.IsFavorite.Value; + } + + if (EnableJoinUserData(query)) + { + if (query.IsPlayed.HasValue) + { + if (query.IsPlayed.Value) + { + whereClauses.Add("(played=@IsPlayed)"); + } + else + { + whereClauses.Add("(played is null or played=@IsPlayed)"); + } + cmd.Parameters.Add(cmd, "@IsPlayed", DbType.Boolean).Value = query.IsPlayed.Value; + } + } + + if (query.IsResumable.HasValue) + { + if (query.IsResumable.Value) + { + whereClauses.Add("playbackPositionTicks > 0"); + } + else + { + whereClauses.Add("(playbackPositionTicks is null or playbackPositionTicks = 0)"); + } + } + + if (query.ArtistNames.Length > 0) + { + var clauses = new List<string>(); + var index = 0; + foreach (var artist in query.ArtistNames) + { + clauses.Add("@ArtistName" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type <= 1)"); + cmd.Parameters.Add(cmd, "@ArtistName" + index, DbType.String).Value = artist.RemoveDiacritics(); + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); + } + + if (query.ExcludeArtistIds.Length > 0) + { + var clauses = new List<string>(); + var index = 0; + foreach (var artistId in query.ExcludeArtistIds) + { + var artistItem = RetrieveItem(new Guid(artistId)); + if (artistItem != null) + { + clauses.Add("@ExcludeArtistName" + index + " not in (select CleanValue from itemvalues where ItemId=Guid and Type <= 1)"); + cmd.Parameters.Add(cmd, "@ExcludeArtistName" + index, DbType.String).Value = artistItem.Name.RemoveDiacritics(); + index++; + } + } + var clause = "(" + string.Join(" AND ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); + } + + if (query.GenreIds.Length > 0) + { + // Todo: improve without having to do this + query.Genres = query.GenreIds.Select(i => RetrieveItem(new Guid(i))).Where(i => i != null).Select(i => i.Name).ToArray(); } if (query.Genres.Length > 0) @@ -1962,8 +2914,8 @@ namespace MediaBrowser.Server.Implementations.Persistence var index = 0; foreach (var item in query.Genres) { - clauses.Add("Genres like @Genres" + index); - cmd.Parameters.Add(cmd, "@Genres" + index, DbType.String).Value = "%" + item + "%"; + clauses.Add("@Genre" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=2)"); + cmd.Parameters.Add(cmd, "@Genre" + index, DbType.String).Value = item.RemoveDiacritics(); index++; } var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; @@ -1976,22 +2928,56 @@ namespace MediaBrowser.Server.Implementations.Persistence var index = 0; foreach (var item in query.Tags) { - clauses.Add("Tags like @Tags" + index); - cmd.Parameters.Add(cmd, "@Tags" + index, DbType.String).Value = "%" + item + "%"; + clauses.Add("@Tag" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=4)"); + cmd.Parameters.Add(cmd, "@Tag" + index, DbType.String).Value = item.RemoveDiacritics(); index++; } var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; whereClauses.Add(clause); } + if (query.StudioIds.Length > 0) + { + // Todo: improve without having to do this + query.Studios = query.StudioIds.Select(i => RetrieveItem(new Guid(i))).Where(i => i != null).Select(i => i.Name).ToArray(); + } + if (query.Studios.Length > 0) { var clauses = new List<string>(); var index = 0; foreach (var item in query.Studios) { - clauses.Add("Studios like @Studios" + index); - cmd.Parameters.Add(cmd, "@Studios" + index, DbType.String).Value = "%" + item + "%"; + clauses.Add("@Studio" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=3)"); + cmd.Parameters.Add(cmd, "@Studio" + index, DbType.String).Value = item.RemoveDiacritics(); + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); + } + + if (query.Keywords.Length > 0) + { + var clauses = new List<string>(); + var index = 0; + foreach (var item in query.Keywords) + { + clauses.Add("@Keyword" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=5)"); + cmd.Parameters.Add(cmd, "@Keyword" + index, DbType.String).Value = item.RemoveDiacritics(); + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); + } + + if (query.OfficialRatings.Length > 0) + { + var clauses = new List<string>(); + var index = 0; + foreach (var item in query.OfficialRatings) + { + clauses.Add("OfficialRating=@OfficialRating" + index); + cmd.Parameters.Add(cmd, "@OfficialRating" + index, DbType.String).Value = item; index++; } var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; @@ -2056,8 +3042,15 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.LocationTypes.Length == 1) { - whereClauses.Add("LocationType=@LocationType"); - cmd.Parameters.Add(cmd, "@LocationType", DbType.String).Value = query.LocationTypes[0].ToString(); + if (query.LocationTypes[0] == LocationType.Virtual && _config.Configuration.SchemaVersion >= 90) + { + query.IsVirtualItem = true; + } + else + { + whereClauses.Add("LocationType=@LocationType"); + cmd.Parameters.Add(cmd, "@LocationType", DbType.String).Value = query.LocationTypes[0].ToString(); + } } else if (query.LocationTypes.Length > 1) { @@ -2067,8 +3060,15 @@ namespace MediaBrowser.Server.Implementations.Persistence } if (query.ExcludeLocationTypes.Length == 1) { - whereClauses.Add("LocationType<>@ExcludeLocationTypes"); - cmd.Parameters.Add(cmd, "@ExcludeLocationTypes", DbType.String).Value = query.ExcludeLocationTypes[0].ToString(); + if (query.ExcludeLocationTypes[0] == LocationType.Virtual && _config.Configuration.SchemaVersion >= 90) + { + query.IsVirtualItem = false; + } + else + { + whereClauses.Add("LocationType<>@ExcludeLocationTypes"); + cmd.Parameters.Add(cmd, "@ExcludeLocationTypes", DbType.String).Value = query.ExcludeLocationTypes[0].ToString(); + } } else if (query.ExcludeLocationTypes.Length > 1) { @@ -2076,10 +3076,55 @@ namespace MediaBrowser.Server.Implementations.Persistence whereClauses.Add("LocationType not in (" + val + ")"); } + if (query.IsVirtualItem.HasValue) + { + if (_config.Configuration.SchemaVersion >= 90) + { + whereClauses.Add("IsVirtualItem=@IsVirtualItem"); + cmd.Parameters.Add(cmd, "@IsVirtualItem", DbType.Boolean).Value = query.IsVirtualItem.Value; + } + else if (!query.IsVirtualItem.Value) + { + whereClauses.Add("LocationType<>'Virtual'"); + } + } + if (query.IsUnaired.HasValue) + { + if (query.IsUnaired.Value) + { + whereClauses.Add("PremiereDate >= DATETIME('now')"); + } + else + { + whereClauses.Add("PremiereDate < DATETIME('now')"); + } + } + if (query.IsMissing.HasValue && _config.Configuration.SchemaVersion >= 90) + { + if (query.IsMissing.Value) + { + whereClauses.Add("(IsVirtualItem=1 AND PremiereDate < DATETIME('now'))"); + } + else + { + whereClauses.Add("(IsVirtualItem=0 OR PremiereDate >= DATETIME('now'))"); + } + } + if (query.IsVirtualUnaired.HasValue && _config.Configuration.SchemaVersion >= 90) + { + if (query.IsVirtualUnaired.Value) + { + whereClauses.Add("(IsVirtualItem=1 AND PremiereDate >= DATETIME('now'))"); + } + else + { + whereClauses.Add("(IsVirtualItem=0 OR PremiereDate < DATETIME('now'))"); + } + } if (query.MediaTypes.Length == 1) { whereClauses.Add("MediaType=@MediaTypes"); - cmd.Parameters.Add(cmd, "@MediaTypes", DbType.String).Value = query.MediaTypes[0].ToString(); + cmd.Parameters.Add(cmd, "@MediaTypes", DbType.String).Value = query.MediaTypes[0]; } if (query.MediaTypes.Length > 1) { @@ -2087,8 +3132,96 @@ namespace MediaBrowser.Server.Implementations.Persistence whereClauses.Add("MediaType in (" + val + ")"); } + if (query.ItemIds.Length > 0) + { + var excludeIds = new List<string>(); - var enableItemsByName = query.IncludeItemsByName ?? query.IncludeItemTypes.Length > 0; + var index = 0; + foreach (var id in query.ItemIds) + { + excludeIds.Add("Guid = @IncludeId" + index); + cmd.Parameters.Add(cmd, "@IncludeId" + index, DbType.Guid).Value = new Guid(id); + index++; + } + + whereClauses.Add(string.Join(" OR ", excludeIds.ToArray())); + } + if (query.ExcludeItemIds.Length > 0) + { + var excludeIds = new List<string>(); + + var index = 0; + foreach (var id in query.ExcludeItemIds) + { + excludeIds.Add("Guid <> @ExcludeId" + index); + cmd.Parameters.Add(cmd, "@ExcludeId" + index, DbType.Guid).Value = new Guid(id); + index++; + } + + whereClauses.Add(string.Join(" AND ", excludeIds.ToArray())); + } + + if (query.ExcludeProviderIds.Count > 0) + { + var excludeIds = new List<string>(); + + var index = 0; + foreach (var pair in query.ExcludeProviderIds) + { + if (string.Equals(pair.Key, MetadataProviders.TmdbCollection.ToString(), StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var paramName = "@ExcludeProviderId" + index; + excludeIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")"); + cmd.Parameters.Add(cmd, paramName, DbType.String).Value = pair.Value; + index++; + } + + whereClauses.Add(string.Join(" AND ", excludeIds.ToArray())); + } + + if (query.HasImdbId.HasValue) + { + var fn = query.HasImdbId.Value ? "<>" : "="; + whereClauses.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = 'Imdb'), '') " + fn + " '')"); + } + + if (query.HasTmdbId.HasValue) + { + var fn = query.HasTmdbId.Value ? "<>" : "="; + whereClauses.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = 'Tmdb'), '') " + fn + " '')"); + } + + if (query.HasTvdbId.HasValue) + { + var fn = query.HasTvdbId.Value ? "<>" : "="; + whereClauses.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = 'Tvdb'), '') " + fn + " '')"); + } + + if (query.AlbumNames.Length > 0) + { + var clause = "("; + + var index = 0; + foreach (var name in query.AlbumNames) + { + if (index > 0) + { + clause += " OR "; + } + clause += "Album=@AlbumName" + index; + cmd.Parameters.Add(cmd, "@AlbumName" + index, DbType.String).Value = name; + index++; + } + + clause += ")"; + whereClauses.Add(clause); + } + + //var enableItemsByName = query.IncludeItemsByName ?? query.IncludeItemTypes.Length > 0; + var enableItemsByName = query.IncludeItemsByName ?? false; if (query.TopParentIds.Length == 1) { @@ -2128,6 +3261,12 @@ namespace MediaBrowser.Server.Implementations.Persistence var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + new Guid(i).ToString("N") + "'").ToArray()); whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause)); } + if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey)) + { + var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey"; + whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause)); + cmd.Parameters.Add(cmd, "@AncestorWithPresentationUniqueKey", DbType.String).Value = query.AncestorWithPresentationUniqueKey; + } if (query.BlockUnratedItems.Length == 1) { @@ -2143,28 +3282,58 @@ namespace MediaBrowser.Server.Implementations.Persistence var excludeTagIndex = 0; foreach (var excludeTag in query.ExcludeTags) { - whereClauses.Add("Tags not like @excludeTag" + excludeTagIndex); + whereClauses.Add("(Tags is null OR Tags not like @excludeTag" + excludeTagIndex + ")"); cmd.Parameters.Add(cmd, "@excludeTag" + excludeTagIndex, DbType.String).Value = "%" + excludeTag + "%"; excludeTagIndex++; } - if (addPaging) + excludeTagIndex = 0; + foreach (var excludeTag in query.ExcludeInheritedTags) { - if (query.StartIndex.HasValue && query.StartIndex.Value > 0) - { - var pagingWhereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + whereClauses.Add("(InheritedTags is null OR InheritedTags not like @excludeInheritedTag" + excludeTagIndex + ")"); + cmd.Parameters.Add(cmd, "@excludeInheritedTag" + excludeTagIndex, DbType.String).Value = "%" + excludeTag + "%"; + excludeTagIndex++; + } - var orderBy = GetOrderByText(query); + return whereClauses; + } - whereClauses.Add(string.Format("guid NOT IN (SELECT guid FROM TypedBaseItems {0}" + orderBy + " LIMIT {1})", - pagingWhereText, - query.StartIndex.Value.ToString(CultureInfo.InvariantCulture))); - } + private bool EnableGroupByPresentationUniqueKey(InternalItemsQuery query) + { + if (!query.GroupByPresentationUniqueKey) + { + return false; } - return whereClauses; + if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) + { + return false; + } + + if (query.User == null) + { + return false; + } + + if (query.IncludeItemTypes.Length == 0) + { + return true; + } + + var types = new[] { + typeof(Episode).Name, + typeof(Video).Name , + typeof(Movie).Name , + typeof(MusicVideo).Name , + typeof(Series).Name , + typeof(Season).Name }; + + if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase))) + { + return true; + } + + return false; } private static readonly Type[] KnownTypes = @@ -2208,6 +3377,88 @@ namespace MediaBrowser.Server.Implementations.Persistence public async Task UpdateInheritedValues(CancellationToken cancellationToken) { + await UpdateInheritedParentalRating(cancellationToken).ConfigureAwait(false); + await UpdateInheritedTags(cancellationToken).ConfigureAwait(false); + } + + private async Task UpdateInheritedTags(CancellationToken cancellationToken) + { + var newValues = new List<Tuple<Guid, string>>(); + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select Guid,InheritedTags,(select group_concat(Tags, '|') from TypedBaseItems where (guid=outer.guid) OR (guid in (Select AncestorId from AncestorIds where ItemId=Outer.guid))) as NewInheritedTags from typedbaseitems as Outer where NewInheritedTags <> InheritedTags"; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) + { + var id = reader.GetGuid(0); + string value = reader.IsDBNull(2) ? null : reader.GetString(2); + + newValues.Add(new Tuple<Guid, string>(id, value)); + } + } + } + + Logger.Debug("UpdateInheritedTags - {0} rows", newValues.Count); + if (newValues.Count == 0) + { + return; + } + + await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); + + IDbTransaction transaction = null; + + try + { + transaction = _connection.BeginTransaction(); + + foreach (var item in newValues) + { + _updateInheritedTagsCommand.GetParameter(0).Value = item.Item1; + _updateInheritedTagsCommand.GetParameter(1).Value = item.Item2; + + _updateInheritedTagsCommand.Transaction = transaction; + _updateInheritedTagsCommand.ExecuteNonQuery(); + } + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Error running query:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + + WriteLock.Release(); + } + } + + private async Task UpdateInheritedParentalRating(CancellationToken cancellationToken) + { var newValues = new List<Tuple<Guid, int>>(); using (var cmd = _connection.CreateCommand()) @@ -2226,6 +3477,7 @@ namespace MediaBrowser.Server.Implementations.Persistence } } + Logger.Debug("UpdateInheritedParentalRatings - {0} rows", newValues.Count); if (newValues.Count == 0) { return; @@ -2283,7 +3535,7 @@ namespace MediaBrowser.Server.Implementations.Persistence private static Dictionary<string, string[]> GetTypeMapDictionary() { - var dict = new Dictionary<string, string[]>(); + var dict = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase); foreach (var t in KnownTypes) { @@ -2348,6 +3600,26 @@ namespace MediaBrowser.Server.Implementations.Persistence _deleteAncestorsCommand.Transaction = transaction; _deleteAncestorsCommand.ExecuteNonQuery(); + // Delete user data keys + _deleteUserDataKeysCommand.GetParameter(0).Value = id; + _deleteUserDataKeysCommand.Transaction = transaction; + _deleteUserDataKeysCommand.ExecuteNonQuery(); + + // Delete item values + _deleteItemValuesCommand.GetParameter(0).Value = id; + _deleteItemValuesCommand.Transaction = transaction; + _deleteItemValuesCommand.ExecuteNonQuery(); + + // Delete provider ids + _deleteProviderIdsCommand.GetParameter(0).Value = id; + _deleteProviderIdsCommand.Transaction = transaction; + _deleteProviderIdsCommand.ExecuteNonQuery(); + + // Delete images + _deleteImagesCommand.GetParameter(0).Value = id; + _deleteImagesCommand.Transaction = transaction; + _deleteImagesCommand.ExecuteNonQuery(); + // Delete the item _deleteItemCommand.GetParameter(0).Value = id; _deleteItemCommand.Transaction = transaction; @@ -2540,6 +3812,490 @@ namespace MediaBrowser.Server.Implementations.Persistence } } + public QueryResult<Tuple<BaseItem, ItemCounts>> GetArtists(InternalItemsQuery query) + { + return GetItemValues(query, 0, typeof(MusicArtist).FullName); + } + + public QueryResult<Tuple<BaseItem, ItemCounts>> GetAlbumArtists(InternalItemsQuery query) + { + return GetItemValues(query, 1, typeof(MusicArtist).FullName); + } + + public QueryResult<Tuple<BaseItem, ItemCounts>> GetStudios(InternalItemsQuery query) + { + return GetItemValues(query, 3, typeof(Studio).FullName); + } + + public QueryResult<Tuple<BaseItem, ItemCounts>> GetGenres(InternalItemsQuery query) + { + return GetItemValues(query, 2, typeof(Genre).FullName); + } + + public QueryResult<Tuple<BaseItem, ItemCounts>> GetGameGenres(InternalItemsQuery query) + { + return GetItemValues(query, 2, typeof(GameGenre).FullName); + } + + public QueryResult<Tuple<BaseItem, ItemCounts>> GetMusicGenres(InternalItemsQuery query) + { + return GetItemValues(query, 2, typeof(MusicGenre).FullName); + } + + private QueryResult<Tuple<BaseItem, ItemCounts>> GetItemValues(InternalItemsQuery query, int itemValueType, string returnType) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + if (!query.Limit.HasValue) + { + query.EnableTotalRecordCount = false; + } + + CheckDisposed(); + + var now = DateTime.UtcNow; + + using (var cmd = _connection.CreateCommand()) + { + var itemCountColumns = new List<Tuple<string, string>>(); + + var typesToCount = query.IncludeItemTypes.ToList(); + + if (typesToCount.Count > 0) + { + var itemCountColumnQuery = "select group_concat(type, '|')" + GetFromText("B"); + + var typeSubQuery = new InternalItemsQuery(query.User) + { + ExcludeItemTypes = query.ExcludeItemTypes, + IncludeItemTypes = query.IncludeItemTypes, + MediaTypes = query.MediaTypes, + AncestorIds = query.AncestorIds, + ExcludeItemIds = query.ExcludeItemIds, + ItemIds = query.ItemIds, + TopParentIds = query.TopParentIds, + ParentId = query.ParentId, + IsPlayed = query.IsPlayed + }; + var whereClauses = GetWhereClauses(typeSubQuery, cmd, "itemTypes"); + + whereClauses.Add("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND Type=@ItemValueType)"); + + var typeWhereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + itemCountColumnQuery += typeWhereText; + + //itemCountColumnQuery += ")"; + + itemCountColumns.Add(new Tuple<string, string>("itemTypes", "(" + itemCountColumnQuery + ") as itemTypes")); + } + + var columns = _retriveItemColumns.ToList(); + columns.AddRange(itemCountColumns.Select(i => i.Item2).ToArray()); + + cmd.CommandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, columns.ToArray(), cmd)) + GetFromText(); + cmd.CommandText += GetJoinUserDataText(query); + + var innerQuery = new InternalItemsQuery(query.User) + { + ExcludeItemTypes = query.ExcludeItemTypes, + IncludeItemTypes = query.IncludeItemTypes, + MediaTypes = query.MediaTypes, + AncestorIds = query.AncestorIds, + ExcludeItemIds = query.ExcludeItemIds, + ItemIds = query.ItemIds, + TopParentIds = query.TopParentIds, + ParentId = query.ParentId, + IsPlayed = query.IsPlayed + }; + + var innerWhereClauses = GetWhereClauses(innerQuery, cmd); + + var innerWhereText = innerWhereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", innerWhereClauses.ToArray()); + + var whereText = " where Type=@SelectType"; + + if (typesToCount.Count == 0) + { + whereText += " And CleanName In (Select CleanValue from ItemValues where Type=@ItemValueType AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))"; + } + else + { + //whereText += " And itemTypes not null"; + whereText += " And CleanName In (Select CleanValue from ItemValues where Type=@ItemValueType AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))"; + } + + var outerQuery = new InternalItemsQuery(query.User) + { + IsFavorite = query.IsFavorite, + IsFavoriteOrLiked = query.IsFavoriteOrLiked, + IsLiked = query.IsLiked, + IsLocked = query.IsLocked, + NameLessThan = query.NameLessThan, + NameStartsWith = query.NameStartsWith, + NameStartsWithOrGreater = query.NameStartsWithOrGreater, + AlbumArtistStartsWithOrGreater = query.AlbumArtistStartsWithOrGreater, + Tags = query.Tags, + OfficialRatings = query.OfficialRatings, + Genres = query.GenreIds, + Years = query.Years + }; + + var outerWhereClauses = GetWhereClauses(outerQuery, cmd); + + whereText += outerWhereClauses.Count == 0 ? + string.Empty : + " AND " + string.Join(" AND ", outerWhereClauses.ToArray()); + //cmd.CommandText += GetGroupBy(query); + + cmd.CommandText += whereText; + cmd.CommandText += " group by PresentationUniqueKey"; + + cmd.Parameters.Add(cmd, "@SelectType", DbType.String).Value = returnType; + cmd.Parameters.Add(cmd, "@ItemValueType", DbType.Int32).Value = itemValueType; + + if (EnableJoinUserData(query)) + { + cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = query.User.Id; + } + + cmd.CommandText += " order by SortName"; + + if (query.Limit.HasValue || query.StartIndex.HasValue) + { + var offset = query.StartIndex ?? 0; + + if (query.Limit.HasValue || offset > 0) + { + cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); + } + + if (offset > 0) + { + cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + } + } + + cmd.CommandText += ";"; + + var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; + + if (isReturningZeroItems) + { + cmd.CommandText = ""; + } + + if (query.EnableTotalRecordCount) + { + cmd.CommandText += "select count (distinct PresentationUniqueKey)" + GetFromText(); + + cmd.CommandText += GetJoinUserDataText(query); + cmd.CommandText += whereText; + } + else + { + cmd.CommandText = cmd.CommandText.TrimEnd(';'); + } + + var list = new List<Tuple<BaseItem, ItemCounts>>(); + var count = 0; + + var commandBehavior = isReturningZeroItems || !query.EnableTotalRecordCount + ? (CommandBehavior.SequentialAccess | CommandBehavior.SingleResult) + : CommandBehavior.SequentialAccess; + + //Logger.Debug("GetItemValues: " + cmd.CommandText); + + using (var reader = cmd.ExecuteReader(commandBehavior)) + { + LogQueryTime("GetItemValues", cmd, now); + + if (isReturningZeroItems) + { + if (reader.Read()) + { + count = reader.GetInt32(0); + } + } + else + { + while (reader.Read()) + { + var item = GetItem(reader); + if (item != null) + { + var countStartColumn = columns.Count - 1; + + list.Add(new Tuple<BaseItem, ItemCounts>(item, GetItemCounts(reader, countStartColumn, typesToCount))); + } + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } + } + } + + if (count == 0) + { + count = list.Count; + } + + return new QueryResult<Tuple<BaseItem, ItemCounts>> + { + Items = list.ToArray(), + TotalRecordCount = count + }; + + } + } + + private ItemCounts GetItemCounts(IDataReader reader, int countStartColumn, List<string> typesToCount) + { + var counts = new ItemCounts(); + + if (typesToCount.Count == 0) + { + return counts; + } + + var typeString = reader.IsDBNull(countStartColumn) ? null : reader.GetString(countStartColumn); + + if (string.IsNullOrWhiteSpace(typeString)) + { + return counts; + } + + var allTypes = typeString.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) + .ToLookup(i => i).ToList(); + + foreach (var type in allTypes) + { + var value = type.ToList().Count; + var typeName = type.Key; + + if (string.Equals(typeName, typeof(Series).FullName, StringComparison.OrdinalIgnoreCase)) + { + counts.SeriesCount = value; + } + else if (string.Equals(typeName, typeof(Episode).FullName, StringComparison.OrdinalIgnoreCase)) + { + counts.EpisodeCount = value; + } + else if (string.Equals(typeName, typeof(Movie).FullName, StringComparison.OrdinalIgnoreCase)) + { + counts.MovieCount = value; + } + else if (string.Equals(typeName, typeof(MusicAlbum).FullName, StringComparison.OrdinalIgnoreCase)) + { + counts.AlbumCount = value; + } + else if (string.Equals(typeName, typeof(Audio).FullName, StringComparison.OrdinalIgnoreCase)) + { + counts.SongCount = value; + } + else if (string.Equals(typeName, typeof(Game).FullName, StringComparison.OrdinalIgnoreCase)) + { + counts.GameCount = value; + } + else if (string.Equals(typeName, typeof(Trailer).FullName, StringComparison.OrdinalIgnoreCase)) + { + counts.TrailerCount = value; + } + counts.ItemCount += value; + } + + return counts; + } + + private List<Tuple<int, string>> GetItemValuesToSave(BaseItem item) + { + var list = new List<Tuple<int, string>>(); + + var hasArtist = item as IHasArtist; + if (hasArtist != null) + { + list.AddRange(hasArtist.Artists.Select(i => new Tuple<int, string>(0, i))); + } + + var hasAlbumArtist = item as IHasAlbumArtist; + if (hasAlbumArtist != null) + { + list.AddRange(hasAlbumArtist.AlbumArtists.Select(i => new Tuple<int, string>(1, i))); + } + + list.AddRange(item.Genres.Select(i => new Tuple<int, string>(2, i))); + list.AddRange(item.Studios.Select(i => new Tuple<int, string>(3, i))); + list.AddRange(item.Tags.Select(i => new Tuple<int, string>(4, i))); + list.AddRange(item.Keywords.Select(i => new Tuple<int, string>(5, i))); + + return list; + } + + private void UpdateImages(Guid itemId, List<ItemImageInfo> images, IDbTransaction transaction) + { + if (itemId == Guid.Empty) + { + throw new ArgumentNullException("itemId"); + } + + if (images == null) + { + throw new ArgumentNullException("images"); + } + + CheckDisposed(); + + // First delete + _deleteImagesCommand.GetParameter(0).Value = itemId; + _deleteImagesCommand.Transaction = transaction; + + _deleteImagesCommand.ExecuteNonQuery(); + + var index = 0; + foreach (var image in images) + { + _saveImagesCommand.GetParameter(0).Value = itemId; + _saveImagesCommand.GetParameter(1).Value = image.Type; + _saveImagesCommand.GetParameter(2).Value = image.Path; + + if (image.DateModified == default(DateTime)) + { + _saveImagesCommand.GetParameter(3).Value = null; + } + else + { + _saveImagesCommand.GetParameter(3).Value = image.DateModified; + } + + _saveImagesCommand.GetParameter(4).Value = image.IsPlaceholder; + _saveImagesCommand.GetParameter(5).Value = index; + + _saveImagesCommand.Transaction = transaction; + + _saveImagesCommand.ExecuteNonQuery(); + index++; + } + } + + private void UpdateProviderIds(Guid itemId, Dictionary<string, string> values, IDbTransaction transaction) + { + if (itemId == Guid.Empty) + { + throw new ArgumentNullException("itemId"); + } + + if (values == null) + { + throw new ArgumentNullException("values"); + } + + // Just in case there might be case-insensitive duplicates, strip them out now + var newValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + foreach (var pair in values) + { + newValues[pair.Key] = pair.Value; + } + + CheckDisposed(); + + // First delete + _deleteProviderIdsCommand.GetParameter(0).Value = itemId; + _deleteProviderIdsCommand.Transaction = transaction; + + _deleteProviderIdsCommand.ExecuteNonQuery(); + + foreach (var pair in newValues) + { + _saveProviderIdsCommand.GetParameter(0).Value = itemId; + _saveProviderIdsCommand.GetParameter(1).Value = pair.Key; + _saveProviderIdsCommand.GetParameter(2).Value = pair.Value; + _saveProviderIdsCommand.Transaction = transaction; + + _saveProviderIdsCommand.ExecuteNonQuery(); + } + } + + private void UpdateItemValues(Guid itemId, List<Tuple<int, string>> values, IDbTransaction transaction) + { + if (itemId == Guid.Empty) + { + throw new ArgumentNullException("itemId"); + } + + if (values == null) + { + throw new ArgumentNullException("keys"); + } + + CheckDisposed(); + + // First delete + _deleteItemValuesCommand.GetParameter(0).Value = itemId; + _deleteItemValuesCommand.Transaction = transaction; + + _deleteItemValuesCommand.ExecuteNonQuery(); + + foreach (var pair in values) + { + _saveItemValuesCommand.GetParameter(0).Value = itemId; + _saveItemValuesCommand.GetParameter(1).Value = pair.Item1; + _saveItemValuesCommand.GetParameter(2).Value = pair.Item2; + if (pair.Item2 == null) + { + _saveItemValuesCommand.GetParameter(3).Value = null; + } + else + { + _saveItemValuesCommand.GetParameter(3).Value = pair.Item2.RemoveDiacritics(); + } + _saveItemValuesCommand.Transaction = transaction; + + _saveItemValuesCommand.ExecuteNonQuery(); + } + } + + private void UpdateUserDataKeys(Guid itemId, List<string> keys, IDbTransaction transaction) + { + if (itemId == Guid.Empty) + { + throw new ArgumentNullException("itemId"); + } + + if (keys == null) + { + throw new ArgumentNullException("keys"); + } + + CheckDisposed(); + + // First delete + _deleteUserDataKeysCommand.GetParameter(0).Value = itemId; + _deleteUserDataKeysCommand.Transaction = transaction; + + _deleteUserDataKeysCommand.ExecuteNonQuery(); + var index = 0; + + foreach (var key in keys) + { + _saveUserDataKeysCommand.GetParameter(0).Value = itemId; + _saveUserDataKeysCommand.GetParameter(1).Value = key; + _saveUserDataKeysCommand.GetParameter(2).Value = index; + index++; + _saveUserDataKeysCommand.Transaction = transaction; + + _saveUserDataKeysCommand.ExecuteNonQuery(); + } + } + public async Task UpdatePeople(Guid itemId, List<PersonInfo> people) { if (itemId == Guid.Empty) @@ -2656,6 +4412,8 @@ namespace MediaBrowser.Server.Implementations.Persistence throw new ArgumentNullException("query"); } + var list = new List<MediaStream>(); + using (var cmd = _connection.CreateCommand()) { var cmdText = "select " + string.Join(",", _mediaStreamSaveColumns) + " from mediastreams where"; @@ -2683,13 +4441,15 @@ namespace MediaBrowser.Server.Implementations.Persistence { while (reader.Read()) { - yield return GetMediaStream(reader); + list.Add(GetMediaStream(reader)); } } } + + return list; } - public async Task SaveMediaStreams(Guid id, IEnumerable<MediaStream> streams, CancellationToken cancellationToken) + public async Task SaveMediaStreams(Guid id, List<MediaStream> streams, CancellationToken cancellationToken) { CheckDisposed(); @@ -2755,10 +4515,15 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveStreamCommand.GetParameter(index++).Value = stream.BitDepth; _saveStreamCommand.GetParameter(index++).Value = stream.IsAnamorphic; _saveStreamCommand.GetParameter(index++).Value = stream.RefFrames; - _saveStreamCommand.GetParameter(index++).Value = stream.IsCabac; _saveStreamCommand.GetParameter(index++).Value = stream.CodecTag; _saveStreamCommand.GetParameter(index++).Value = stream.Comment; + _saveStreamCommand.GetParameter(index++).Value = stream.NalLengthSize; + _saveStreamCommand.GetParameter(index++).Value = stream.IsAVC; + _saveStreamCommand.GetParameter(index++).Value = stream.Title; + + _saveStreamCommand.GetParameter(index++).Value = stream.TimeBase; + _saveStreamCommand.GetParameter(index++).Value = stream.CodecTimeBase; _saveStreamCommand.Transaction = transaction; _saveStreamCommand.ExecuteNonQuery(); @@ -2909,17 +4674,37 @@ namespace MediaBrowser.Server.Implementations.Persistence if (!reader.IsDBNull(25)) { - item.IsCabac = reader.GetBoolean(25); + item.CodecTag = reader.GetString(25); } if (!reader.IsDBNull(26)) { - item.CodecTag = reader.GetString(26); + item.Comment = reader.GetString(26); } if (!reader.IsDBNull(27)) { - item.Comment = reader.GetString(27); + item.NalLengthSize = reader.GetString(27); + } + + if (!reader.IsDBNull(28)) + { + item.IsAVC = reader.GetBoolean(28); + } + + if (!reader.IsDBNull(29)) + { + item.Title = reader.GetString(29); + } + + if (!reader.IsDBNull(30)) + { + item.TimeBase = reader.GetString(30); + } + + if (!reader.IsDBNull(31)) + { + item.CodecTimeBase = reader.GetString(31); } return item; diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteProviderInfoRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteProviderInfoRepository.cs deleted file mode 100644 index dbceda727..000000000 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteProviderInfoRepository.cs +++ /dev/null @@ -1,248 +0,0 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Logging; -using System; -using System.Data; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Server.Implementations.Persistence -{ - public class SqliteProviderInfoRepository : BaseSqliteRepository, IProviderRepository - { - private IDbConnection _connection; - - private IDbCommand _saveStatusCommand; - private readonly IApplicationPaths _appPaths; - - public SqliteProviderInfoRepository(ILogManager logManager, IApplicationPaths appPaths) : base(logManager) - { - _appPaths = appPaths; - } - - /// <summary> - /// Gets the name of the repository - /// </summary> - /// <value>The name.</value> - public string Name - { - get - { - return "SQLite"; - } - } - - /// <summary> - /// Opens the connection to the database - /// </summary> - /// <returns>Task.</returns> - public async Task Initialize() - { - var dbFile = Path.Combine(_appPaths.DataPath, "refreshinfo.db"); - - _connection = await SqliteExtensions.ConnectToDb(dbFile, Logger).ConfigureAwait(false); - - string[] queries = { - - "create table if not exists MetadataStatus (ItemId GUID PRIMARY KEY, DateLastMetadataRefresh datetime, DateLastImagesRefresh datetime, ItemDateModified DateTimeNull)", - "create index if not exists idx_MetadataStatus on MetadataStatus(ItemId)", - - //pragmas - "pragma temp_store = memory", - - "pragma shrink_memory" - }; - - _connection.RunQueries(queries, Logger); - - AddItemDateModifiedCommand(); - - PrepareStatements(); - } - - private static readonly string[] StatusColumns = - { - "ItemId", - "DateLastMetadataRefresh", - "DateLastImagesRefresh", - "ItemDateModified" - }; - - private void AddItemDateModifiedCommand() - { - using (var cmd = _connection.CreateCommand()) - { - cmd.CommandText = "PRAGMA table_info(MetadataStatus)"; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) - { - while (reader.Read()) - { - if (!reader.IsDBNull(1)) - { - var name = reader.GetString(1); - - if (string.Equals(name, "ItemDateModified", StringComparison.OrdinalIgnoreCase)) - { - return; - } - } - } - } - } - - var builder = new StringBuilder(); - - builder.AppendLine("alter table MetadataStatus"); - builder.AppendLine("add column ItemDateModified DateTime NULL"); - - _connection.RunQueries(new[] { builder.ToString() }, Logger); - } - - /// <summary> - /// Prepares the statements. - /// </summary> - private void PrepareStatements() - { - _saveStatusCommand = _connection.CreateCommand(); - - _saveStatusCommand.CommandText = string.Format("replace into MetadataStatus ({0}) values ({1})", - string.Join(",", StatusColumns), - string.Join(",", StatusColumns.Select(i => "@" + i).ToArray())); - - foreach (var col in StatusColumns) - { - _saveStatusCommand.Parameters.Add(_saveStatusCommand, "@" + col); - } - } - - public MetadataStatus GetMetadataStatus(Guid itemId) - { - if (itemId == Guid.Empty) - { - throw new ArgumentNullException("itemId"); - } - - using (var cmd = _connection.CreateCommand()) - { - var cmdText = "select " + string.Join(",", StatusColumns) + " from MetadataStatus where"; - - cmdText += " ItemId=@ItemId"; - cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = itemId; - - cmd.CommandText = cmdText; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) - { - while (reader.Read()) - { - return GetStatus(reader); - } - - return null; - } - } - } - - private MetadataStatus GetStatus(IDataReader reader) - { - var result = new MetadataStatus - { - ItemId = reader.GetGuid(0) - }; - - if (!reader.IsDBNull(1)) - { - result.DateLastMetadataRefresh = reader.GetDateTime(1).ToUniversalTime(); - } - - if (!reader.IsDBNull(2)) - { - result.DateLastImagesRefresh = reader.GetDateTime(2).ToUniversalTime(); - } - - if (!reader.IsDBNull(3)) - { - result.ItemDateModified = reader.GetDateTime(3).ToUniversalTime(); - } - - return result; - } - - public async Task SaveMetadataStatus(MetadataStatus status, CancellationToken cancellationToken) - { - if (status == null) - { - throw new ArgumentNullException("status"); - } - - cancellationToken.ThrowIfCancellationRequested(); - - await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - IDbTransaction transaction = null; - - try - { - transaction = _connection.BeginTransaction(); - - _saveStatusCommand.GetParameter(0).Value = status.ItemId; - _saveStatusCommand.GetParameter(1).Value = status.DateLastMetadataRefresh; - _saveStatusCommand.GetParameter(2).Value = status.DateLastImagesRefresh; - _saveStatusCommand.GetParameter(3).Value = status.ItemDateModified; - - _saveStatusCommand.Transaction = transaction; - - _saveStatusCommand.ExecuteNonQuery(); - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save provider info:", e); - - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - - WriteLock.Release(); - } - } - - protected override void CloseConnection() - { - if (_connection != null) - { - if (_connection.IsOpen()) - { - _connection.Close(); - } - - _connection.Dispose(); - _connection = null; - } - } - } -} diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs index 63c41c71f..62d9e7634 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs @@ -5,7 +5,9 @@ using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; using System.Data; +using System.Globalization; using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -14,11 +16,15 @@ namespace MediaBrowser.Server.Implementations.Persistence public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository { private IDbConnection _connection; - private readonly IApplicationPaths _appPaths; - public SqliteUserDataRepository(ILogManager logManager, IApplicationPaths appPaths) : base(logManager) + public SqliteUserDataRepository(ILogManager logManager, IApplicationPaths appPaths, IDbConnector connector) : base(logManager, connector) { - _appPaths = appPaths; + DbFilePath = Path.Combine(appPaths.DataPath, "userdata_v2.db"); + } + + protected override bool EnableConnectionPooling + { + get { return false; } } /// <summary> @@ -33,21 +39,42 @@ namespace MediaBrowser.Server.Implementations.Persistence } } + protected override async Task<IDbConnection> CreateConnection(bool isReadOnly = false) + { + var connection = await DbConnector.Connect(DbFilePath, false, false, 10000).ConfigureAwait(false); + + connection.RunQueries(new[] + { + "pragma temp_store = memory" + + }, Logger); + + return connection; + } + /// <summary> /// Opens the connection to the database /// </summary> /// <returns>Task.</returns> - public async Task Initialize() + public async Task Initialize(IDbConnection connection, SemaphoreSlim writeLock) { - var dbFile = Path.Combine(_appPaths.DataPath, "userdata_v2.db"); - - _connection = await SqliteExtensions.ConnectToDb(dbFile, Logger).ConfigureAwait(false); + WriteLock.Dispose(); + WriteLock = writeLock; + _connection = connection; string[] queries = { - "create table if not exists userdata (key nvarchar, userId GUID, rating float null, played bit, playCount int, isFavorite bit, playbackPositionTicks bigint, lastPlayedDate datetime null)", + "create table if not exists UserDataDb.userdata (key nvarchar, userId GUID, rating float null, played bit, playCount int, isFavorite bit, playbackPositionTicks bigint, lastPlayedDate datetime null)", + + "drop index if exists UserDataDb.idx_userdata", + "drop index if exists UserDataDb.idx_userdata1", + "drop index if exists UserDataDb.idx_userdata2", + "drop index if exists UserDataDb.userdataindex1", - "create unique index if not exists userdataindex on userdata (key, userId)", + "create unique index if not exists UserDataDb.userdataindex on userdata (key, userId)", + "create index if not exists UserDataDb.userdataindex2 on userdata (key, userId, played)", + "create index if not exists UserDataDb.userdataindex3 on userdata (key, userId, playbackPositionTicks)", + "create index if not exists UserDataDb.userdataindex4 on userdata (key, userId, isFavorite)", //pragmas "pragma temp_store = memory", @@ -295,11 +322,54 @@ namespace MediaBrowser.Server.Implementations.Persistence } } - return new UserItemData + return null; + } + } + + public UserItemData GetUserData(Guid userId, List<string> keys) + { + if (userId == Guid.Empty) + { + throw new ArgumentNullException("userId"); + } + if (keys == null) + { + throw new ArgumentNullException("keys"); + } + + using (var cmd = _connection.CreateCommand()) + { + var index = 0; + var userdataKeys = new List<string>(); + var builder = new StringBuilder(); + foreach (var key in keys) + { + var paramName = "@Key" + index; + userdataKeys.Add("Key =" + paramName); + cmd.Parameters.Add(cmd, paramName, DbType.String).Value = key; + builder.Append(" WHEN Key=" + paramName + " THEN " + index); + index++; + break; + } + + var keyText = string.Join(" OR ", userdataKeys.ToArray()); + + cmd.CommandText = "select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where userId=@userId AND (" + keyText + ") "; + + cmd.CommandText += " ORDER BY (Case " + builder + " Else " + keys.Count.ToString(CultureInfo.InvariantCulture) + " End )"; + cmd.CommandText += " LIMIT 1"; + + cmd.Parameters.Add(cmd, "@userId", DbType.Guid).Value = userId; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) { - UserId = userId, - Key = key - }; + if (reader.Read()) + { + return ReadRow(reader); + } + } + + return null; } } @@ -370,18 +440,14 @@ namespace MediaBrowser.Server.Implementations.Persistence return userData; } - protected override void CloseConnection() + protected override void Dispose(bool dispose) { - if (_connection != null) - { - if (_connection.IsOpen()) - { - _connection.Close(); - } + // handled by library database + } - _connection.Dispose(); - _connection = null; - } + protected override void CloseConnection() + { + // handled by library database } } }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs index 9bd7e47f3..25ab60ca5 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs @@ -17,14 +17,13 @@ namespace MediaBrowser.Server.Implementations.Persistence /// </summary> public class SqliteUserRepository : BaseSqliteRepository, IUserRepository { - private IDbConnection _connection; - private readonly IServerApplicationPaths _appPaths; private readonly IJsonSerializer _jsonSerializer; - public SqliteUserRepository(ILogManager logManager, IServerApplicationPaths appPaths, IJsonSerializer jsonSerializer) : base(logManager) + public SqliteUserRepository(ILogManager logManager, IServerApplicationPaths appPaths, IJsonSerializer jsonSerializer, IDbConnector dbConnector) : base(logManager, dbConnector) { - _appPaths = appPaths; _jsonSerializer = jsonSerializer; + + DbFilePath = Path.Combine(appPaths.DataPath, "users.db"); } /// <summary> @@ -45,23 +44,19 @@ namespace MediaBrowser.Server.Implementations.Persistence /// <returns>Task.</returns> public async Task Initialize() { - var dbFile = Path.Combine(_appPaths.DataPath, "users.db"); - - _connection = await SqliteExtensions.ConnectToDb(dbFile, Logger).ConfigureAwait(false); - - string[] queries = { + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + string[] queries = { "create table if not exists users (guid GUID primary key, data BLOB)", "create index if not exists idx_users on users(guid)", "create table if not exists schema_version (table_name primary key, version)", - //pragmas - "pragma temp_store = memory", - "pragma shrink_memory" }; - _connection.RunQueries(queries, Logger); + connection.RunQueries(queries, Logger); + } } /// <summary> @@ -84,55 +79,54 @@ namespace MediaBrowser.Server.Implementations.Persistence cancellationToken.ThrowIfCancellationRequested(); - await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - IDbTransaction transaction = null; - - try + using (var connection = await CreateConnection().ConfigureAwait(false)) { - transaction = _connection.BeginTransaction(); + IDbTransaction transaction = null; - using (var cmd = _connection.CreateCommand()) + try { - cmd.CommandText = "replace into users (guid, data) values (@1, @2)"; - cmd.Parameters.Add(cmd, "@1", DbType.Guid).Value = user.Id; - cmd.Parameters.Add(cmd, "@2", DbType.Binary).Value = serialized; + transaction = connection.BeginTransaction(); - cmd.Transaction = transaction; + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "replace into users (guid, data) values (@1, @2)"; + cmd.Parameters.Add(cmd, "@1", DbType.Guid).Value = user.Id; + cmd.Parameters.Add(cmd, "@2", DbType.Binary).Value = serialized; - cmd.ExecuteNonQuery(); - } + cmd.Transaction = transaction; - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) + cmd.ExecuteNonQuery(); + } + + transaction.Commit(); + } + catch (OperationCanceledException) { - transaction.Rollback(); + if (transaction != null) + { + transaction.Rollback(); + } + + throw; } + catch (Exception e) + { + Logger.ErrorException("Failed to save user:", e); - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save user:", e); + if (transaction != null) + { + transaction.Rollback(); + } - if (transaction != null) - { - transaction.Rollback(); + throw; } - - throw; - } - finally - { - if (transaction != null) + finally { - transaction.Dispose(); + if (transaction != null) + { + transaction.Dispose(); + } } - - WriteLock.Release(); } } @@ -142,25 +136,32 @@ namespace MediaBrowser.Server.Implementations.Persistence /// <returns>IEnumerable{User}.</returns> public IEnumerable<User> RetrieveAllUsers() { - using (var cmd = _connection.CreateCommand()) - { - cmd.CommandText = "select guid,data from users"; + var list = new List<User>(); - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + using (var connection = CreateConnection(true).Result) + { + using (var cmd = connection.CreateCommand()) { - while (reader.Read()) - { - var id = reader.GetGuid(0); + cmd.CommandText = "select guid,data from users"; - using (var stream = reader.GetMemoryStream(1)) + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + { + while (reader.Read()) { - var user = _jsonSerializer.DeserializeFromStream<User>(stream); - user.Id = id; - yield return user; + var id = reader.GetGuid(0); + + using (var stream = reader.GetMemoryStream(1)) + { + var user = _jsonSerializer.DeserializeFromStream<User>(stream); + user.Id = id; + list.Add(user); + } } } } } + + return list; } /// <summary> @@ -179,69 +180,54 @@ namespace MediaBrowser.Server.Implementations.Persistence cancellationToken.ThrowIfCancellationRequested(); - await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - IDbTransaction transaction = null; - - try + using (var connection = await CreateConnection().ConfigureAwait(false)) { - transaction = _connection.BeginTransaction(); + IDbTransaction transaction = null; - using (var cmd = _connection.CreateCommand()) + try { - cmd.CommandText = "delete from users where guid=@guid"; - - cmd.Parameters.Add(cmd, "@guid", DbType.Guid).Value = user.Id; + transaction = connection.BeginTransaction(); - cmd.Transaction = transaction; + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "delete from users where guid=@guid"; - cmd.ExecuteNonQuery(); - } + cmd.Parameters.Add(cmd, "@guid", DbType.Guid).Value = user.Id; - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } + cmd.Transaction = transaction; - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to delete user:", e); + cmd.ExecuteNonQuery(); + } - if (transaction != null) - { - transaction.Rollback(); + transaction.Commit(); } - - throw; - } - finally - { - if (transaction != null) + catch (OperationCanceledException) { - transaction.Dispose(); + if (transaction != null) + { + transaction.Rollback(); + } + + throw; } + catch (Exception e) + { + Logger.ErrorException("Failed to delete user:", e); - WriteLock.Release(); - } - } + if (transaction != null) + { + transaction.Rollback(); + } - protected override void CloseConnection() - { - if (_connection != null) - { - if (_connection.IsOpen()) + throw; + } + finally { - _connection.Close(); + if (transaction != null) + { + transaction.Dispose(); + } } - - _connection.Dispose(); - _connection = null; } } } diff --git a/MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs b/MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs index 4a69646f8..abf0f3425 100644 --- a/MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs +++ b/MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs @@ -13,11 +13,12 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Model.Configuration; namespace MediaBrowser.Server.Implementations.Photos { - public abstract class BaseDynamicImageProvider<T> : IHasChangeMonitor, IForcedProvider, ICustomMetadataProvider<T>, IHasOrder + public abstract class BaseDynamicImageProvider<T> : IHasItemChangeMonitor, IForcedProvider, ICustomMetadataProvider<T>, IHasOrder where T : IHasMetadata { protected IFileSystem FileSystem { get; private set; } @@ -109,6 +110,21 @@ namespace MediaBrowser.Server.Implementations.Photos protected async Task<ItemUpdateType> FetchAsync(IHasImages item, ImageType imageType, MetadataRefreshOptions options, CancellationToken cancellationToken) { + var image = item.GetImageInfo(imageType, 0); + + if (image != null) + { + if (!image.IsLocalFile) + { + return ItemUpdateType.None; + } + + if (!FileSystem.ContainsSubPath(item.GetInternalMetadataPath(), image.Path)) + { + return ItemUpdateType.None; + } + } + var items = await GetItemsWithImages(item).ConfigureAwait(false); return await FetchToFileInternal(item, items, imageType, cancellationToken).ConfigureAwait(false); @@ -232,7 +248,7 @@ namespace MediaBrowser.Server.Implementations.Photos { return await CreateSquareCollage(item, itemsWithImages, outputPath).ConfigureAwait(false); } - if (item is Playlist) + if (item is Playlist || item is MusicGenre) { return await CreateSquareCollage(item, itemsWithImages, outputPath).ConfigureAwait(false); } @@ -247,7 +263,7 @@ namespace MediaBrowser.Server.Implementations.Photos get { return 7; } } - public bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date) + public bool HasChanged(IHasMetadata item, IDirectoryService directoryServicee) { if (!Supports(item)) { diff --git a/MediaBrowser.Server.Implementations/Playlists/PlaylistImageProvider.cs b/MediaBrowser.Server.Implementations/Playlists/PlaylistImageProvider.cs index bdb73ea38..5b234d0c6 100644 --- a/MediaBrowser.Server.Implementations/Playlists/PlaylistImageProvider.cs +++ b/MediaBrowser.Server.Implementations/Playlists/PlaylistImageProvider.cs @@ -12,6 +12,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Querying; namespace MediaBrowser.Server.Implementations.Playlists { @@ -65,4 +67,36 @@ namespace MediaBrowser.Server.Implementations.Playlists return Task.FromResult(GetFinalItems(items)); } } + + public class MusicGenreImageProvider : BaseDynamicImageProvider<MusicGenre> + { + private readonly ILibraryManager _libraryManager; + + public MusicGenreImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor) + { + _libraryManager = libraryManager; + } + + protected override Task<List<BaseItem>> GetItemsWithImages(IHasImages item) + { + var items = _libraryManager.GetItemList(new InternalItemsQuery + { + Genres = new[] { item.Name }, + IncludeItemTypes = new[] { typeof(MusicAlbum).Name, typeof(MusicVideo).Name, typeof(Audio).Name }, + SortBy = new[] { ItemSortBy.Random }, + Limit = 4, + Recursive = true, + ImageTypes = new[] { ImageType.Primary } + + }).ToList(); + + return Task.FromResult(GetFinalItems(items)); + } + + //protected override Task<string> CreateImage(IHasImages item, List<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex) + //{ + // return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary); + //} + } + } diff --git a/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs b/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs index 06ef05951..ba1559bd0 100644 --- a/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs +++ b/MediaBrowser.Server.Implementations/Playlists/PlaylistManager.cs @@ -158,7 +158,7 @@ namespace MediaBrowser.Server.Implementations.Playlists return path; } - private IEnumerable<BaseItem> GetPlaylistItems(IEnumerable<string> itemIds, string playlistMediaType, User user) + private Task<IEnumerable<BaseItem>> GetPlaylistItems(IEnumerable<string> itemIds, string playlistMediaType, User user) { var items = itemIds.Select(i => _libraryManager.GetItemById(i)).Where(i => i != null); @@ -183,7 +183,7 @@ namespace MediaBrowser.Server.Implementations.Playlists var list = new List<LinkedChild>(); - var items = GetPlaylistItems(itemIds, playlist.MediaType, user) + var items = (await GetPlaylistItems(itemIds, playlist.MediaType, user).ConfigureAwait(false)) .Where(i => i.SupportsAddingToPlaylist) .ToList(); @@ -247,15 +247,18 @@ namespace MediaBrowser.Server.Implementations.Playlists return; } - if (newIndex > oldIndex) - { - newIndex--; - } - var item = playlist.LinkedChildren[oldIndex]; playlist.LinkedChildren.Remove(item); - playlist.LinkedChildren.Insert(newIndex, item); + + if (newIndex >= playlist.LinkedChildren.Count) + { + playlist.LinkedChildren.Add(item); + } + else + { + playlist.LinkedChildren.Insert(newIndex, item); + } await playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); } diff --git a/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs b/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs index e50de7bac..607a043a6 100644 --- a/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs +++ b/MediaBrowser.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs @@ -12,6 +12,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using CommonIO; +using MediaBrowser.Model.Entities; namespace MediaBrowser.Server.Implementations.ScheduledTasks { @@ -85,8 +86,13 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks /// <returns>Task.</returns> public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress) { - var videos = _libraryManager.RootFolder.GetRecursiveChildren(i => i is Video) - .Cast<Video>() + var videos = _libraryManager.GetItemList(new InternalItemsQuery + { + MediaTypes = new[] { MediaType.Video }, + IsFolder = false, + Recursive = true + }) + .OfType<Video>() .ToList(); var numComplete = 0; @@ -97,7 +103,7 @@ namespace MediaBrowser.Server.Implementations.ScheduledTasks try { - previouslyFailedImages = _fileSystem.ReadAllText(failHistoryPath) + previouslyFailedImages = _fileSystem.ReadAllText(failHistoryPath) .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) .ToList(); } diff --git a/MediaBrowser.Server.Implementations/Security/AuthenticationRepository.cs b/MediaBrowser.Server.Implementations/Security/AuthenticationRepository.cs index b932f0cac..74a552dcc 100644 --- a/MediaBrowser.Server.Implementations/Security/AuthenticationRepository.cs +++ b/MediaBrowser.Server.Implementations/Security/AuthenticationRepository.cs @@ -15,57 +15,30 @@ namespace MediaBrowser.Server.Implementations.Security { public class AuthenticationRepository : BaseSqliteRepository, IAuthenticationRepository { - private IDbConnection _connection; private readonly IServerApplicationPaths _appPaths; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private IDbCommand _saveInfoCommand; - - public AuthenticationRepository(ILogManager logManager, IServerApplicationPaths appPaths) - : base(logManager) + public AuthenticationRepository(ILogManager logManager, IServerApplicationPaths appPaths, IDbConnector connector) + : base(logManager, connector) { _appPaths = appPaths; + DbFilePath = Path.Combine(appPaths.DataPath, "authentication.db"); } public async Task Initialize() { - var dbFile = Path.Combine(_appPaths.DataPath, "authentication.db"); - - _connection = await SqliteExtensions.ConnectToDb(dbFile, Logger).ConfigureAwait(false); - - string[] queries = { + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + string[] queries = { "create table if not exists AccessTokens (Id GUID PRIMARY KEY, AccessToken TEXT NOT NULL, DeviceId TEXT, AppName TEXT, AppVersion TEXT, DeviceName TEXT, UserId TEXT, IsActive BIT, DateCreated DATETIME NOT NULL, DateRevoked DATETIME)", - "create index if not exists idx_AccessTokens on AccessTokens(Id)", - - //pragmas - "pragma temp_store = memory", - - "pragma shrink_memory" + "create index if not exists idx_AccessTokens on AccessTokens(Id)" }; - _connection.RunQueries(queries, Logger); + connection.RunQueries(queries, Logger); - _connection.AddColumn(Logger, "AccessTokens", "AppVersion", "TEXT"); - - PrepareStatements(); - } - - private void PrepareStatements() - { - _saveInfoCommand = _connection.CreateCommand(); - _saveInfoCommand.CommandText = "replace into AccessTokens (Id, AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, IsActive, DateCreated, DateRevoked) values (@Id, @AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @IsActive, @DateCreated, @DateRevoked)"; - - _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@Id"); - _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@AccessToken"); - _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@DeviceId"); - _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@AppName"); - _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@AppVersion"); - _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@DeviceName"); - _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@UserId"); - _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@IsActive"); - _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@DateCreated"); - _saveInfoCommand.Parameters.Add(_saveInfoCommand, "@DateRevoked"); + connection.AddColumn(Logger, "AccessTokens", "AppVersion", "TEXT"); + } } public Task Create(AuthenticationInfo info, CancellationToken cancellationToken) @@ -84,61 +57,76 @@ namespace MediaBrowser.Server.Implementations.Security cancellationToken.ThrowIfCancellationRequested(); - await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - IDbTransaction transaction = null; - - try + using (var connection = await CreateConnection().ConfigureAwait(false)) { - transaction = _connection.BeginTransaction(); + using (var saveInfoCommand = connection.CreateCommand()) + { + saveInfoCommand.CommandText = "replace into AccessTokens (Id, AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, IsActive, DateCreated, DateRevoked) values (@Id, @AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @IsActive, @DateCreated, @DateRevoked)"; + + saveInfoCommand.Parameters.Add(saveInfoCommand, "@Id"); + saveInfoCommand.Parameters.Add(saveInfoCommand, "@AccessToken"); + saveInfoCommand.Parameters.Add(saveInfoCommand, "@DeviceId"); + saveInfoCommand.Parameters.Add(saveInfoCommand, "@AppName"); + saveInfoCommand.Parameters.Add(saveInfoCommand, "@AppVersion"); + saveInfoCommand.Parameters.Add(saveInfoCommand, "@DeviceName"); + saveInfoCommand.Parameters.Add(saveInfoCommand, "@UserId"); + saveInfoCommand.Parameters.Add(saveInfoCommand, "@IsActive"); + saveInfoCommand.Parameters.Add(saveInfoCommand, "@DateCreated"); + saveInfoCommand.Parameters.Add(saveInfoCommand, "@DateRevoked"); + + IDbTransaction transaction = null; + + try + { + transaction = connection.BeginTransaction(); - var index = 0; + var index = 0; - _saveInfoCommand.GetParameter(index++).Value = new Guid(info.Id); - _saveInfoCommand.GetParameter(index++).Value = info.AccessToken; - _saveInfoCommand.GetParameter(index++).Value = info.DeviceId; - _saveInfoCommand.GetParameter(index++).Value = info.AppName; - _saveInfoCommand.GetParameter(index++).Value = info.AppVersion; - _saveInfoCommand.GetParameter(index++).Value = info.DeviceName; - _saveInfoCommand.GetParameter(index++).Value = info.UserId; - _saveInfoCommand.GetParameter(index++).Value = info.IsActive; - _saveInfoCommand.GetParameter(index++).Value = info.DateCreated; - _saveInfoCommand.GetParameter(index++).Value = info.DateRevoked; + saveInfoCommand.GetParameter(index++).Value = new Guid(info.Id); + saveInfoCommand.GetParameter(index++).Value = info.AccessToken; + saveInfoCommand.GetParameter(index++).Value = info.DeviceId; + saveInfoCommand.GetParameter(index++).Value = info.AppName; + saveInfoCommand.GetParameter(index++).Value = info.AppVersion; + saveInfoCommand.GetParameter(index++).Value = info.DeviceName; + saveInfoCommand.GetParameter(index++).Value = info.UserId; + saveInfoCommand.GetParameter(index++).Value = info.IsActive; + saveInfoCommand.GetParameter(index++).Value = info.DateCreated; + saveInfoCommand.GetParameter(index++).Value = info.DateRevoked; - _saveInfoCommand.Transaction = transaction; + saveInfoCommand.Transaction = transaction; - _saveInfoCommand.ExecuteNonQuery(); + saveInfoCommand.ExecuteNonQuery(); - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save record:", e); + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save record:", e); - if (transaction != null) - { - transaction.Rollback(); - } + if (transaction != null) + { + transaction.Rollback(); + } - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } } - - WriteLock.Release(); } } @@ -151,101 +139,104 @@ namespace MediaBrowser.Server.Implementations.Security throw new ArgumentNullException("query"); } - using (var cmd = _connection.CreateCommand()) + using (var connection = CreateConnection(true).Result) { - cmd.CommandText = BaseSelectText; - - var whereClauses = new List<string>(); - - var startIndex = query.StartIndex ?? 0; - - if (!string.IsNullOrWhiteSpace(query.AccessToken)) + using (var cmd = connection.CreateCommand()) { - whereClauses.Add("AccessToken=@AccessToken"); - cmd.Parameters.Add(cmd, "@AccessToken", DbType.String).Value = query.AccessToken; - } + cmd.CommandText = BaseSelectText; - if (!string.IsNullOrWhiteSpace(query.UserId)) - { - whereClauses.Add("UserId=@UserId"); - cmd.Parameters.Add(cmd, "@UserId", DbType.String).Value = query.UserId; - } + var whereClauses = new List<string>(); - if (!string.IsNullOrWhiteSpace(query.DeviceId)) - { - whereClauses.Add("DeviceId=@DeviceId"); - cmd.Parameters.Add(cmd, "@DeviceId", DbType.String).Value = query.DeviceId; - } + var startIndex = query.StartIndex ?? 0; - if (query.IsActive.HasValue) - { - whereClauses.Add("IsActive=@IsActive"); - cmd.Parameters.Add(cmd, "@IsActive", DbType.Boolean).Value = query.IsActive.Value; - } + if (!string.IsNullOrWhiteSpace(query.AccessToken)) + { + whereClauses.Add("AccessToken=@AccessToken"); + cmd.Parameters.Add(cmd, "@AccessToken", DbType.String).Value = query.AccessToken; + } - if (query.HasUser.HasValue) - { - if (query.HasUser.Value) + if (!string.IsNullOrWhiteSpace(query.UserId)) { - whereClauses.Add("UserId not null"); + whereClauses.Add("UserId=@UserId"); + cmd.Parameters.Add(cmd, "@UserId", DbType.String).Value = query.UserId; } - else + + if (!string.IsNullOrWhiteSpace(query.DeviceId)) { - whereClauses.Add("UserId is null"); + whereClauses.Add("DeviceId=@DeviceId"); + cmd.Parameters.Add(cmd, "@DeviceId", DbType.String).Value = query.DeviceId; } - } - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + if (query.IsActive.HasValue) + { + whereClauses.Add("IsActive=@IsActive"); + cmd.Parameters.Add(cmd, "@IsActive", DbType.Boolean).Value = query.IsActive.Value; + } - if (startIndex > 0) - { - var pagingWhereText = whereClauses.Count == 0 ? + if (query.HasUser.HasValue) + { + if (query.HasUser.Value) + { + whereClauses.Add("UserId not null"); + } + else + { + whereClauses.Add("UserId is null"); + } + } + + var whereTextWithoutPaging = whereClauses.Count == 0 ? string.Empty : " where " + string.Join(" AND ", whereClauses.ToArray()); - whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM AccessTokens {0} ORDER BY DateCreated LIMIT {1})", - pagingWhereText, - startIndex.ToString(_usCulture))); - } + if (startIndex > 0) + { + var pagingWhereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); - var whereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM AccessTokens {0} ORDER BY DateCreated LIMIT {1})", + pagingWhereText, + startIndex.ToString(_usCulture))); + } - cmd.CommandText += whereText; + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); - cmd.CommandText += " ORDER BY DateCreated"; + cmd.CommandText += whereText; - if (query.Limit.HasValue) - { - cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); - } + cmd.CommandText += " ORDER BY DateCreated"; - cmd.CommandText += "; select count (Id) from AccessTokens" + whereTextWithoutPaging; + if (query.Limit.HasValue) + { + cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); + } - var list = new List<AuthenticationInfo>(); - var count = 0; + cmd.CommandText += "; select count (Id) from AccessTokens" + whereTextWithoutPaging; - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) - { - while (reader.Read()) + var list = new List<AuthenticationInfo>(); + var count = 0; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) { - list.Add(Get(reader)); + while (reader.Read()) + { + list.Add(Get(reader)); + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } } - if (reader.NextResult() && reader.Read()) + return new QueryResult<AuthenticationInfo>() { - count = reader.GetInt32(0); - } + Items = list.ToArray(), + TotalRecordCount = count + }; } - - return new QueryResult<AuthenticationInfo>() - { - Items = list.ToArray(), - TotalRecordCount = count - }; } } @@ -256,24 +247,27 @@ namespace MediaBrowser.Server.Implementations.Security throw new ArgumentNullException("id"); } - var guid = new Guid(id); - - using (var cmd = _connection.CreateCommand()) + using (var connection = CreateConnection(true).Result) { - cmd.CommandText = BaseSelectText + " where Id=@Id"; - - cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid; + var guid = new Guid(id); - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + using (var cmd = connection.CreateCommand()) { - if (reader.Read()) + cmd.CommandText = BaseSelectText + " where Id=@Id"; + + cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid; + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) { - return Get(reader); + if (reader.Read()) + { + return Get(reader); + } } } - } - return null; + return null; + } } private AuthenticationInfo Get(IDataReader reader) @@ -319,19 +313,5 @@ namespace MediaBrowser.Server.Implementations.Security return info; } - - protected override void CloseConnection() - { - if (_connection != null) - { - if (_connection.IsOpen()) - { - _connection.Close(); - } - - _connection.Dispose(); - _connection = null; - } - } } } diff --git a/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs b/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs index 8719f5448..33d106916 100644 --- a/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs +++ b/MediaBrowser.Server.Implementations/ServerManager/ServerManager.cs @@ -71,6 +71,8 @@ namespace MediaBrowser.Server.Implementations.ServerManager /// <value>The web socket listeners.</value> private readonly List<IWebSocketListener> _webSocketListeners = new List<IWebSocketListener>(); + private bool _disposed; + /// <summary> /// Initializes a new instance of the <see cref="ServerManager" /> class. /// </summary> @@ -143,6 +145,11 @@ namespace MediaBrowser.Server.Implementations.ServerManager /// <param name="e">The <see cref="WebSocketConnectEventArgs" /> instance containing the event data.</param> void HttpServer_WebSocketConnected(object sender, WebSocketConnectEventArgs e) { + if (_disposed) + { + return; + } + var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger) { OnReceive = ProcessWebSocketMessageReceived, @@ -164,6 +171,11 @@ namespace MediaBrowser.Server.Implementations.ServerManager /// <param name="result">The result.</param> private async void ProcessWebSocketMessageReceived(WebSocketMessageInfo result) { + if (_disposed) + { + return; + } + //_logger.Debug("Websocket message received: {0}", result.MessageType); var tasks = _webSocketListeners.Select(i => Task.Run(async () => @@ -244,6 +256,11 @@ namespace MediaBrowser.Server.Implementations.ServerManager throw new ArgumentNullException("dataFunction"); } + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name); + } + cancellationToken.ThrowIfCancellationRequested(); var connectionsList = connections.Where(s => s.State == WebSocketState.Open).ToList(); @@ -301,6 +318,8 @@ namespace MediaBrowser.Server.Implementations.ServerManager /// </summary> public void Dispose() { + _disposed = true; + Dispose(true); GC.SuppressFinalize(this); } diff --git a/MediaBrowser.Server.Implementations/Session/SessionManager.cs b/MediaBrowser.Server.Implementations/Session/SessionManager.cs index 88f11c368..84aab5e1f 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionManager.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionManager.cs @@ -404,6 +404,10 @@ namespace MediaBrowser.Server.Implementations.Session /// <returns>SessionInfo.</returns> private async Task<SessionInfo> GetSessionInfo(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user) { + if (string.IsNullOrWhiteSpace(deviceId)) + { + throw new ArgumentNullException("deviceId"); + } var key = GetSessionKey(appName, deviceId); await _sessionLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); @@ -601,11 +605,9 @@ namespace MediaBrowser.Server.Implementations.Session if (libraryItem != null) { - var key = libraryItem.GetUserDataKey(); - foreach (var user in users) { - await OnPlaybackStart(user.Id, key, libraryItem).ConfigureAwait(false); + await OnPlaybackStart(user.Id, libraryItem).ConfigureAwait(false); } } @@ -632,12 +634,11 @@ namespace MediaBrowser.Server.Implementations.Session /// Called when [playback start]. /// </summary> /// <param name="userId">The user identifier.</param> - /// <param name="userDataKey">The user data key.</param> /// <param name="item">The item.</param> /// <returns>Task.</returns> - private async Task OnPlaybackStart(Guid userId, string userDataKey, IHasUserData item) + private async Task OnPlaybackStart(Guid userId, IHasUserData item) { - var data = _userDataRepository.GetUserData(userId, userDataKey); + var data = _userDataRepository.GetUserData(userId, item); data.PlayCount++; data.LastPlayedDate = DateTime.UtcNow; @@ -676,11 +677,9 @@ namespace MediaBrowser.Server.Implementations.Session if (libraryItem != null) { - var key = libraryItem.GetUserDataKey(); - foreach (var user in users) { - await OnPlaybackProgress(user, key, libraryItem, info).ConfigureAwait(false); + await OnPlaybackProgress(user, libraryItem, info).ConfigureAwait(false); } } @@ -714,9 +713,9 @@ namespace MediaBrowser.Server.Implementations.Session StartIdleCheckTimer(); } - private async Task OnPlaybackProgress(User user, string userDataKey, BaseItem item, PlaybackProgressInfo info) + private async Task OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info) { - var data = _userDataRepository.GetUserData(user.Id, userDataKey); + var data = _userDataRepository.GetUserData(user.Id, item); var positionTicks = info.PositionTicks; @@ -811,11 +810,9 @@ namespace MediaBrowser.Server.Implementations.Session if (libraryItem != null) { - var key = libraryItem.GetUserDataKey(); - foreach (var user in users) { - playedToCompletion = await OnPlaybackStopped(user.Id, key, libraryItem, info.PositionTicks, info.Failed).ConfigureAwait(false); + playedToCompletion = await OnPlaybackStopped(user.Id, libraryItem, info.PositionTicks, info.Failed).ConfigureAwait(false); } } @@ -848,14 +845,14 @@ namespace MediaBrowser.Server.Implementations.Session await SendPlaybackStoppedNotification(session, CancellationToken.None).ConfigureAwait(false); } - private async Task<bool> OnPlaybackStopped(Guid userId, string userDataKey, BaseItem item, long? positionTicks, bool playbackFailed) + private async Task<bool> OnPlaybackStopped(Guid userId, BaseItem item, long? positionTicks, bool playbackFailed) { bool playedToCompletion = false; if (!playbackFailed) { - var data = _userDataRepository.GetUserData(userId, userDataKey); - + var data = _userDataRepository.GetUserData(userId, item); + if (positionTicks.HasValue) { playedToCompletion = _userDataRepository.UpdatePlayState(item, data, positionTicks.Value); @@ -935,7 +932,7 @@ namespace MediaBrowser.Server.Implementations.Session return session.SessionController.SendGeneralCommand(command, cancellationToken); } - public Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken) + public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken) { var session = GetSessionToRemoteControl(sessionId); @@ -953,7 +950,14 @@ namespace MediaBrowser.Server.Implementations.Session } else { - items = command.ItemIds.SelectMany(i => TranslateItemForPlayback(i, user)) + var list = new List<BaseItem>(); + foreach (var itemId in command.ItemIds) + { + var subItems = await TranslateItemForPlayback(itemId, user).ConfigureAwait(false); + list.AddRange(subItems); + } + + items = list .Where(i => i.LocationType != LocationType.Virtual) .ToList(); } @@ -1016,10 +1020,10 @@ namespace MediaBrowser.Server.Implementations.Session command.ControllingUserId = controllingSession.UserId.Value.ToString("N"); } - return session.SessionController.SendPlayCommand(command, cancellationToken); + await session.SessionController.SendPlayCommand(command, cancellationToken).ConfigureAwait(false); } - private IEnumerable<BaseItem> TranslateItemForPlayback(string id, User user) + private async Task<List<BaseItem>> TranslateItemForPlayback(string id, User user) { var item = _libraryManager.GetItemById(id); @@ -1033,29 +1037,34 @@ namespace MediaBrowser.Server.Implementations.Session if (byName != null) { - var itemFilter = byName.GetItemFilter(); - - var items = user == null ? - _libraryManager.RootFolder.GetRecursiveChildren(i => !i.IsFolder && itemFilter(i)) : - user.RootFolder.GetRecursiveChildren(user, i => !i.IsFolder && itemFilter(i)); + var items = byName.GetTaggedItems(new InternalItemsQuery(user) + { + IsFolder = false, + Recursive = true + }); return FilterToSingleMediaType(items) - .OrderBy(i => i.SortName); + .OrderBy(i => i.SortName) + .ToList(); } if (item.IsFolder) { var folder = (Folder)item; - var items = user == null ? - folder.GetRecursiveChildren(i => !i.IsFolder) : - folder.GetRecursiveChildren(user, i => !i.IsFolder); + var itemsResult = await folder.GetItems(new InternalItemsQuery(user) + { + Recursive = true, + IsFolder = false + + }).ConfigureAwait(false); - return FilterToSingleMediaType(items) - .OrderBy(i => i.SortName); + return FilterToSingleMediaType(itemsResult.Items) + .OrderBy(i => i.SortName) + .ToList(); } - return new[] { item }; + return new List<BaseItem> { item }; } private IEnumerable<BaseItem> FilterToSingleMediaType(IEnumerable<BaseItem> items) @@ -1125,11 +1134,11 @@ namespace MediaBrowser.Server.Implementations.Session /// </summary> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - public Task SendRestartRequiredNotification(CancellationToken cancellationToken) + public async Task SendRestartRequiredNotification(CancellationToken cancellationToken) { var sessions = Sessions.Where(i => i.IsActive && i.SessionController != null).ToList(); - var info = _appHost.GetSystemInfo(); + var info = await _appHost.GetSystemInfo().ConfigureAwait(false); var tasks = sessions.Select(session => Task.Run(async () => { @@ -1144,7 +1153,7 @@ namespace MediaBrowser.Server.Implementations.Session }, cancellationToken)); - return Task.WhenAll(tasks); + await Task.WhenAll(tasks).ConfigureAwait(false); } /// <summary> @@ -1374,8 +1383,8 @@ namespace MediaBrowser.Server.Implementations.Session ServerId = _appHost.SystemId }; } - - + + private async Task<string> GetAuthorizationToken(string userId, string deviceId, string app, string appVersion, string deviceName) { var existing = _authRepo.Get(new AuthenticationInfoQuery @@ -1451,7 +1460,7 @@ namespace MediaBrowser.Server.Implementations.Session } } - public async Task RevokeUserTokens(string userId) + public async Task RevokeUserTokens(string userId, string currentAccessToken) { var existing = _authRepo.Get(new AuthenticationInfoQuery { @@ -1461,7 +1470,10 @@ namespace MediaBrowser.Server.Implementations.Session foreach (var info in existing.Items) { - await Logout(info.AccessToken).ConfigureAwait(false); + if (!string.Equals(currentAccessToken, info.AccessToken, StringComparison.OrdinalIgnoreCase)) + { + await Logout(info.AccessToken).ConfigureAwait(false); + } } } @@ -1752,6 +1764,11 @@ namespace MediaBrowser.Server.Implementations.Session public void ReportNowViewingItem(string sessionId, string itemId) { + if (string.IsNullOrWhiteSpace(itemId)) + { + throw new ArgumentNullException("itemId"); + } + var item = _libraryManager.GetItemById(new Guid(itemId)); var info = GetItemInfo(item, null, null); diff --git a/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs b/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs index 602bc4876..ddd7ba53a 100644 --- a/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/MediaBrowser.Server.Implementations/Session/SessionWebSocketListener.cs @@ -230,7 +230,12 @@ namespace MediaBrowser.Server.Implementations.Session { var vals = message.Data.Split('|'); - _sessionManager.ReportNowViewingItem(session.Id, vals[1]); + var itemId = vals[1]; + + if (!string.IsNullOrWhiteSpace(itemId)) + { + _sessionManager.ReportNowViewingItem(session.Id, itemId); + } } } diff --git a/MediaBrowser.Server.Implementations/Social/SharingManager.cs b/MediaBrowser.Server.Implementations/Social/SharingManager.cs index 326b2893c..95f0ece0c 100644 --- a/MediaBrowser.Server.Implementations/Social/SharingManager.cs +++ b/MediaBrowser.Server.Implementations/Social/SharingManager.cs @@ -43,7 +43,7 @@ namespace MediaBrowser.Server.Implementations.Social throw new ResourceNotFoundException(); } - var externalUrl = _appHost.GetSystemInfo().WanAddress; + var externalUrl = (await _appHost.GetSystemInfo().ConfigureAwait(false)).WanAddress; if (string.IsNullOrWhiteSpace(externalUrl)) { @@ -58,7 +58,7 @@ namespace MediaBrowser.Server.Implementations.Social UserId = userId }; - AddShareInfo(info); + AddShareInfo(info, externalUrl); await _repository.CreateShare(info).ConfigureAwait(false); @@ -74,17 +74,15 @@ namespace MediaBrowser.Server.Implementations.Social { var info = _repository.GetShareInfo(id); - AddShareInfo(info); + AddShareInfo(info, _appHost.GetSystemInfo().Result.WanAddress); return info; } - private void AddShareInfo(SocialShareInfo info) + private void AddShareInfo(SocialShareInfo info, string externalUrl) { - var externalUrl = _appHost.GetSystemInfo().WanAddress; - info.ImageUrl = externalUrl + "/Social/Shares/Public/" + info.Id + "/Image"; - info.Url = externalUrl + "/web/shared.html?id=" + info.Id; + info.Url = externalUrl + "/emby/web/shared.html?id=" + info.Id; var item = _libraryManager.GetItemById(info.ItemId); diff --git a/MediaBrowser.Server.Implementations/Social/SharingRepository.cs b/MediaBrowser.Server.Implementations/Social/SharingRepository.cs index d6d7f021a..c4243c1a7 100644 --- a/MediaBrowser.Server.Implementations/Social/SharingRepository.cs +++ b/MediaBrowser.Server.Implementations/Social/SharingRepository.cs @@ -12,14 +12,10 @@ namespace MediaBrowser.Server.Implementations.Social { public class SharingRepository : BaseSqliteRepository { - private IDbConnection _connection; - private IDbCommand _saveShareCommand; - private readonly IApplicationPaths _appPaths; - - public SharingRepository(ILogManager logManager, IApplicationPaths appPaths) - : base(logManager) + public SharingRepository(ILogManager logManager, IApplicationPaths appPaths, IDbConnector dbConnector) + : base(logManager, dbConnector) { - _appPaths = appPaths; + DbFilePath = Path.Combine(appPaths.DataPath, "shares.db"); } /// <summary> @@ -28,38 +24,18 @@ namespace MediaBrowser.Server.Implementations.Social /// <returns>Task.</returns> public async Task Initialize() { - var dbFile = Path.Combine(_appPaths.DataPath, "shares.db"); - - _connection = await SqliteExtensions.ConnectToDb(dbFile, Logger).ConfigureAwait(false); - - string[] queries = { + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + string[] queries = { "create table if not exists Shares (Id GUID, ItemId TEXT, UserId TEXT, ExpirationDate DateTime, PRIMARY KEY (Id))", "create index if not exists idx_Shares on Shares(Id)", - //pragmas - "pragma temp_store = memory", - "pragma shrink_memory" }; - _connection.RunQueries(queries, Logger); - - PrepareStatements(); - } - - /// <summary> - /// Prepares the statements. - /// </summary> - private void PrepareStatements() - { - _saveShareCommand = _connection.CreateCommand(); - _saveShareCommand.CommandText = "replace into Shares (Id, ItemId, UserId, ExpirationDate) values (@Id, @ItemId, @UserId, @ExpirationDate)"; - - _saveShareCommand.Parameters.Add(_saveShareCommand, "@Id"); - _saveShareCommand.Parameters.Add(_saveShareCommand, "@ItemId"); - _saveShareCommand.Parameters.Add(_saveShareCommand, "@UserId"); - _saveShareCommand.Parameters.Add(_saveShareCommand, "@ExpirationDate"); + connection.RunQueries(queries, Logger); + } } public async Task CreateShare(SocialShareInfo info) @@ -77,53 +53,62 @@ namespace MediaBrowser.Server.Implementations.Social cancellationToken.ThrowIfCancellationRequested(); - await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - IDbTransaction transaction = null; - - try - { - transaction = _connection.BeginTransaction(); - - _saveShareCommand.GetParameter(0).Value = new Guid(info.Id); - _saveShareCommand.GetParameter(1).Value = info.ItemId; - _saveShareCommand.GetParameter(2).Value = info.UserId; - _saveShareCommand.GetParameter(3).Value = info.ExpirationDate; - - _saveShareCommand.Transaction = transaction; - - _saveShareCommand.ExecuteNonQuery(); - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) + using (var connection = await CreateConnection().ConfigureAwait(false)) { - Logger.ErrorException("Failed to save share:", e); - - if (transaction != null) + using (var saveShareCommand = connection.CreateCommand()) { - transaction.Rollback(); + saveShareCommand.CommandText = "replace into Shares (Id, ItemId, UserId, ExpirationDate) values (@Id, @ItemId, @UserId, @ExpirationDate)"; + + saveShareCommand.Parameters.Add(saveShareCommand, "@Id"); + saveShareCommand.Parameters.Add(saveShareCommand, "@ItemId"); + saveShareCommand.Parameters.Add(saveShareCommand, "@UserId"); + saveShareCommand.Parameters.Add(saveShareCommand, "@ExpirationDate"); + + IDbTransaction transaction = null; + + try + { + transaction = connection.BeginTransaction(); + + saveShareCommand.GetParameter(0).Value = new Guid(info.Id); + saveShareCommand.GetParameter(1).Value = info.ItemId; + saveShareCommand.GetParameter(2).Value = info.UserId; + saveShareCommand.GetParameter(3).Value = info.ExpirationDate; + + saveShareCommand.Transaction = transaction; + + saveShareCommand.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save share:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - - WriteLock.Release(); } } @@ -134,20 +119,23 @@ namespace MediaBrowser.Server.Implementations.Social throw new ArgumentNullException("id"); } - var cmd = _connection.CreateCommand(); - cmd.CommandText = "select Id, ItemId, UserId, ExpirationDate from Shares where id = @id"; + using (var connection = CreateConnection(true).Result) + { + var cmd = connection.CreateCommand(); + cmd.CommandText = "select Id, ItemId, UserId, ExpirationDate from Shares where id = @id"; - cmd.Parameters.Add(cmd, "@id", DbType.Guid).Value = new Guid(id); + cmd.Parameters.Add(cmd, "@id", DbType.Guid).Value = new Guid(id); - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) - { - if (reader.Read()) + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) { - return GetSocialShareInfo(reader); + if (reader.Read()) + { + return GetSocialShareInfo(reader); + } } - } - return null; + return null; + } } private SocialShareInfo GetSocialShareInfo(IDataReader reader) @@ -164,21 +152,7 @@ namespace MediaBrowser.Server.Implementations.Social public async Task DeleteShare(string id) { - - } - protected override void CloseConnection() - { - if (_connection != null) - { - if (_connection.IsOpen()) - { - _connection.Close(); - } - - _connection.Dispose(); - _connection = null; - } } } } diff --git a/MediaBrowser.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/MediaBrowser.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs index 70cf805cf..91abbe34c 100644 --- a/MediaBrowser.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs +++ b/MediaBrowser.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs @@ -49,8 +49,8 @@ namespace MediaBrowser.Server.Implementations.Sorting private int Compare(Episode x, Episode y) { - var isXSpecial = (x.PhysicalSeasonNumber ?? -1) == 0; - var isYSpecial = (y.PhysicalSeasonNumber ?? -1) == 0; + var isXSpecial = (x.ParentIndexNumber ?? -1) == 0; + var isYSpecial = (y.ParentIndexNumber ?? -1) == 0; if (isXSpecial && isYSpecial) { @@ -74,7 +74,7 @@ namespace MediaBrowser.Server.Implementations.Sorting { // http://thetvdb.com/wiki/index.php?title=Special_Episodes - var xSeason = x.PhysicalSeasonNumber ?? -1; + var xSeason = x.ParentIndexNumber ?? -1; var ySeason = y.AirsAfterSeasonNumber ?? y.AirsBeforeSeasonNumber ?? -1; if (xSeason != ySeason) @@ -142,8 +142,8 @@ namespace MediaBrowser.Server.Implementations.Sorting private int CompareEpisodes(Episode x, Episode y) { - var xValue = (x.PhysicalSeasonNumber ?? -1) * 1000 + (x.IndexNumber ?? -1); - var yValue = (y.PhysicalSeasonNumber ?? -1) * 1000 + (y.IndexNumber ?? -1); + var xValue = (x.ParentIndexNumber ?? -1) * 1000 + (x.IndexNumber ?? -1); + var yValue = (y.ParentIndexNumber ?? -1) * 1000 + (y.IndexNumber ?? -1); return xValue.CompareTo(yValue); } diff --git a/MediaBrowser.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/MediaBrowser.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs index 68cd44ec9..e8c78b8e7 100644 --- a/MediaBrowser.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs +++ b/MediaBrowser.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs @@ -49,13 +49,13 @@ namespace MediaBrowser.Server.Implementations.Sorting if (folder != null) { - return folder.GetRecursiveChildren(User, i => !i.IsFolder) - .Select(i => i.DateCreated) - .OrderByDescending(i => i) - .FirstOrDefault(); + if (folder.DateLastMediaAdded.HasValue) + { + return folder.DateLastMediaAdded.Value; + } } - return x.DateCreated; + return DateTime.MinValue; } /// <summary> diff --git a/MediaBrowser.Server.Implementations/Sorting/DatePlayedComparer.cs b/MediaBrowser.Server.Implementations/Sorting/DatePlayedComparer.cs index c881591be..3edf23020 100644 --- a/MediaBrowser.Server.Implementations/Sorting/DatePlayedComparer.cs +++ b/MediaBrowser.Server.Implementations/Sorting/DatePlayedComparer.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.Server.Implementations.Sorting /// <returns>DateTime.</returns> private DateTime GetDate(BaseItem x) { - var userdata = UserDataRepository.GetUserData(User.Id, x.GetUserDataKey()); + var userdata = UserDataRepository.GetUserData(User, x); if (userdata != null && userdata.LastPlayedDate.HasValue) { diff --git a/MediaBrowser.Server.Implementations/Sorting/PlayCountComparer.cs b/MediaBrowser.Server.Implementations/Sorting/PlayCountComparer.cs index 1bc5261b4..8b14efffc 100644 --- a/MediaBrowser.Server.Implementations/Sorting/PlayCountComparer.cs +++ b/MediaBrowser.Server.Implementations/Sorting/PlayCountComparer.cs @@ -34,7 +34,7 @@ namespace MediaBrowser.Server.Implementations.Sorting /// <returns>DateTime.</returns> private int GetValue(BaseItem x) { - var userdata = UserDataRepository.GetUserData(User.Id, x.GetUserDataKey()); + var userdata = UserDataRepository.GetUserData(User, x); return userdata == null ? 0 : userdata.PlayCount; } diff --git a/MediaBrowser.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/MediaBrowser.Server.Implementations/Sorting/SeriesSortNameComparer.cs index 4efc3218b..6bc1264a4 100644 --- a/MediaBrowser.Server.Implementations/Sorting/SeriesSortNameComparer.cs +++ b/MediaBrowser.Server.Implementations/Sorting/SeriesSortNameComparer.cs @@ -1,5 +1,4 @@ using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; using System; @@ -21,28 +20,9 @@ namespace MediaBrowser.Server.Implementations.Sorting private string GetValue(BaseItem item) { - Series series = null; + var hasSeries = item as IHasSeries; - var season = item as Season; - - if (season != null) - { - series = season.Series; - } - - var episode = item as Episode; - - if (episode != null) - { - series = episode.Series; - } - - if (series == null) - { - series = item as Series; - } - - return series != null ? series.SortName : null; + return hasSeries != null ? hasSeries.SeriesSortName : null; } /// <summary> diff --git a/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs b/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs index 01334c121..e120d3a4d 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncJobProcessor.cs @@ -149,14 +149,9 @@ namespace MediaBrowser.Server.Implementations.Sync { var job = _syncRepo.GetJob(id); - return UpdateJobStatus(job); - } - - private Task UpdateJobStatus(SyncJob job) - { if (job == null) { - throw new ArgumentNullException("job"); + return Task.FromResult(true); } var result = _syncManager.GetJobItems(new SyncJobItemQuery @@ -239,10 +234,22 @@ namespace MediaBrowser.Server.Implementations.Sync public async Task<IEnumerable<BaseItem>> GetItemsForSync(SyncCategory? category, string parentId, IEnumerable<string> itemIds, User user, bool unwatchedOnly) { - var items = category.HasValue ? - await GetItemsForSync(category.Value, parentId, user).ConfigureAwait(false) : - itemIds.SelectMany(i => GetItemsForSync(i, user)); + var list = new List<BaseItem>(); + if (category.HasValue) + { + list = (await GetItemsForSync(category.Value, parentId, user).ConfigureAwait(false)).ToList(); + } + else + { + foreach (var itemId in itemIds) + { + var subList = await GetItemsForSync(itemId, user).ConfigureAwait(false); + list.AddRange(subList); + } + } + + IEnumerable<BaseItem> items = list; items = items.Where(_syncManager.SupportsSync); if (unwatchedOnly) @@ -319,7 +326,7 @@ namespace MediaBrowser.Server.Implementations.Sync return result.Items; } - private IEnumerable<BaseItem> GetItemsForSync(string id, User user) + private async Task<List<BaseItem>> GetItemsForSync(string id, User user) { var item = _libraryManager.GetItemById(id); @@ -331,38 +338,35 @@ namespace MediaBrowser.Server.Implementations.Sync var itemByName = item as IItemByName; if (itemByName != null) { - var itemByNameFilter = itemByName.GetItemFilter(); - - return user.RootFolder - .GetRecursiveChildren(user, i => !i.IsFolder && itemByNameFilter(i)); - } - - var series = item as Series; - if (series != null) - { - return series.GetEpisodes(user, false, false); - } - - var season = item as Season; - if (season != null) - { - return season.GetEpisodes(user, false, false); + return itemByName.GetTaggedItems(new InternalItemsQuery(user) + { + IsFolder = false, + Recursive = true + }).ToList(); } if (item.IsFolder) { var folder = (Folder)item; - var items = folder.GetRecursiveChildren(user, i => !i.IsFolder); + var itemsResult = await folder.GetItems(new InternalItemsQuery(user) + { + Recursive = true, + IsFolder = false + + }).ConfigureAwait(false); + + var items = itemsResult.Items; if (!folder.IsPreSorted) { - items = items.OrderBy(i => i.SortName); + items = _libraryManager.Sort(items, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending) + .ToArray(); } - return items; + return items.ToList(); } - return new[] { item }; + return new List<BaseItem> { item }; } private async Task EnsureSyncJobItems(string targetId, CancellationToken cancellationToken) @@ -476,14 +480,12 @@ namespace MediaBrowser.Server.Implementations.Sync if (jobItem != null) { - var job = _syncRepo.GetJob(jobItem.JobId); if (jobItem.Status != SyncJobItemStatus.Cancelled) { - await ProcessJobItem(job, jobItem, enableConversion, innerProgress, cancellationToken).ConfigureAwait(false); + await ProcessJobItem(jobItem, enableConversion, innerProgress, cancellationToken).ConfigureAwait(false); } - job = _syncRepo.GetJob(jobItem.JobId); - await UpdateJobStatus(job).ConfigureAwait(false); + await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); } numComplete++; @@ -493,8 +495,13 @@ namespace MediaBrowser.Server.Implementations.Sync } } - private async Task ProcessJobItem(SyncJob job, SyncJobItem jobItem, bool enableConversion, IProgress<double> progress, CancellationToken cancellationToken) + private async Task ProcessJobItem(SyncJobItem jobItem, bool enableConversion, IProgress<double> progress, CancellationToken cancellationToken) { + if (jobItem == null) + { + throw new ArgumentNullException("jobItem"); + } + var item = _libraryManager.GetItemById(jobItem.ItemId); if (item == null) { @@ -507,6 +514,7 @@ namespace MediaBrowser.Server.Implementations.Sync jobItem.Progress = 0; var syncOptions = _config.GetSyncOptions(); + var job = _syncManager.GetJob(jobItem.JobId); var user = _userManager.GetUserById(job.UserId); if (user == null) { @@ -521,7 +529,7 @@ namespace MediaBrowser.Server.Implementations.Sync { AddMetadata = false, ItemId = jobItem.ItemId, - TargetId = job.TargetId, + TargetId = jobItem.TargetId, Statuses = new[] { SyncJobItemStatus.Converting, SyncJobItemStatus.Queued, SyncJobItemStatus.ReadyToTransfer, SyncJobItemStatus.Synced, SyncJobItemStatus.Transferring } }); @@ -531,7 +539,7 @@ namespace MediaBrowser.Server.Implementations.Sync if (duplicateJobItems.Count > 0) { - var syncProvider = _syncManager.GetSyncProvider(jobItem, job) as IHasDuplicateCheck; + var syncProvider = _syncManager.GetSyncProvider(jobItem) as IHasDuplicateCheck; if (!duplicateJobItems.Any(i => AllowDuplicateJobItem(syncProvider, i, jobItem))) { @@ -545,12 +553,12 @@ namespace MediaBrowser.Server.Implementations.Sync var video = item as Video; if (video != null) { - await Sync(jobItem, job, video, user, enableConversion, syncOptions, progress, cancellationToken).ConfigureAwait(false); + await Sync(jobItem, video, user, enableConversion, syncOptions, progress, cancellationToken).ConfigureAwait(false); } else if (item is Audio) { - await Sync(jobItem, job, (Audio)item, user, enableConversion, syncOptions, progress, cancellationToken).ConfigureAwait(false); + await Sync(jobItem, (Audio)item, user, enableConversion, syncOptions, progress, cancellationToken).ConfigureAwait(false); } else if (item is Photo) @@ -574,8 +582,9 @@ namespace MediaBrowser.Server.Implementations.Sync return true; } - private async Task Sync(SyncJobItem jobItem, SyncJob job, Video item, User user, bool enableConversion, SyncOptions syncOptions, IProgress<double> progress, CancellationToken cancellationToken) + private async Task Sync(SyncJobItem jobItem, Video item, User user, bool enableConversion, SyncOptions syncOptions, IProgress<double> progress, CancellationToken cancellationToken) { + var job = _syncManager.GetJob(jobItem.JobId); var jobOptions = _syncManager.GetVideoOptions(jobItem, job); var conversionOptions = new VideoOptions { @@ -587,7 +596,7 @@ namespace MediaBrowser.Server.Implementations.Sync conversionOptions.ItemId = item.Id.ToString("N"); conversionOptions.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, false, user).ToList(); - var streamInfo = new StreamBuilder(_logger).BuildVideoItem(conversionOptions); + var streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildVideoItem(conversionOptions); var mediaSource = streamInfo.MediaSource; // No sense creating external subs if we're already burning one into the video @@ -616,7 +625,7 @@ namespace MediaBrowser.Server.Implementations.Sync { // Save the job item now since conversion could take a while await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - await UpdateJobStatus(job).ConfigureAwait(false); + await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); try { @@ -630,7 +639,7 @@ namespace MediaBrowser.Server.Implementations.Sync { jobItem.Progress = pct / 2; await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - await UpdateJobStatus(job).ConfigureAwait(false); + await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); } }); @@ -642,7 +651,8 @@ namespace MediaBrowser.Server.Implementations.Sync }, innerProgress, cancellationToken); - _syncManager.OnConversionComplete(jobItem, job); + jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; + _syncManager.OnConversionComplete(jobItem); } catch (OperationCanceledException) { @@ -678,6 +688,7 @@ namespace MediaBrowser.Server.Implementations.Sync throw new InvalidOperationException(string.Format("Cannot direct stream {0} protocol", mediaSource.Protocol)); } + jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; jobItem.MediaSource = mediaSource; } @@ -756,7 +767,7 @@ namespace MediaBrowser.Server.Implementations.Sync _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); - using (var stream = await _subtitleEncoder.GetSubtitles(streamInfo.ItemId, streamInfo.MediaSourceId, subtitleStreamIndex, subtitleStreamInfo.Format, 0, null, cancellationToken).ConfigureAwait(false)) + using (var stream = await _subtitleEncoder.GetSubtitles(streamInfo.ItemId, streamInfo.MediaSourceId, subtitleStreamIndex, subtitleStreamInfo.Format, 0, null, false, cancellationToken).ConfigureAwait(false)) { using (var fs = _fileSystem.GetFileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, true)) { @@ -775,8 +786,9 @@ namespace MediaBrowser.Server.Implementations.Sync private const int DatabaseProgressUpdateIntervalSeconds = 2; - private async Task Sync(SyncJobItem jobItem, SyncJob job, Audio item, User user, bool enableConversion, SyncOptions syncOptions, IProgress<double> progress, CancellationToken cancellationToken) + private async Task Sync(SyncJobItem jobItem, Audio item, User user, bool enableConversion, SyncOptions syncOptions, IProgress<double> progress, CancellationToken cancellationToken) { + var job = _syncManager.GetJob(jobItem.JobId); var jobOptions = _syncManager.GetAudioOptions(jobItem, job); var conversionOptions = new AudioOptions { @@ -788,7 +800,7 @@ namespace MediaBrowser.Server.Implementations.Sync conversionOptions.ItemId = item.Id.ToString("N"); conversionOptions.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, false, user).ToList(); - var streamInfo = new StreamBuilder(_logger).BuildAudioItem(conversionOptions); + var streamInfo = new StreamBuilder(_mediaEncoder, _logger).BuildAudioItem(conversionOptions); var mediaSource = streamInfo.MediaSource; jobItem.MediaSourceId = streamInfo.MediaSourceId; @@ -803,7 +815,7 @@ namespace MediaBrowser.Server.Implementations.Sync jobItem.Status = SyncJobItemStatus.Converting; await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - await UpdateJobStatus(job).ConfigureAwait(false); + await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); try { @@ -817,7 +829,7 @@ namespace MediaBrowser.Server.Implementations.Sync { jobItem.Progress = pct / 2; await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); - await UpdateJobStatus(job).ConfigureAwait(false); + await UpdateJobStatus(jobItem.JobId).ConfigureAwait(false); } }); @@ -828,7 +840,8 @@ namespace MediaBrowser.Server.Implementations.Sync }, innerProgress, cancellationToken); - _syncManager.OnConversionComplete(jobItem, job); + jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; + _syncManager.OnConversionComplete(jobItem); } catch (OperationCanceledException) { @@ -864,6 +877,7 @@ namespace MediaBrowser.Server.Implementations.Sync throw new InvalidOperationException(string.Format("Cannot direct stream {0} protocol", mediaSource.Protocol)); } + jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; jobItem.MediaSource = mediaSource; } @@ -880,6 +894,7 @@ namespace MediaBrowser.Server.Implementations.Sync jobItem.Progress = 50; jobItem.Status = SyncJobItemStatus.ReadyToTransfer; + jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); } @@ -889,6 +904,7 @@ namespace MediaBrowser.Server.Implementations.Sync jobItem.Progress = 50; jobItem.Status = SyncJobItemStatus.ReadyToTransfer; + jobItem.ItemDateModifiedTicks = item.DateModified.Ticks; await _syncManager.UpdateSyncJobItemInternal(jobItem).ConfigureAwait(false); } diff --git a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs b/MediaBrowser.Server.Implementations/Sync/SyncManager.cs index 2effad2f7..38edc3024 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncManager.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncManager.cs @@ -687,7 +687,7 @@ namespace MediaBrowser.Server.Implementations.Sync private Task ReportOfflinePlayedItem(UserAction action) { var item = _libraryManager.GetItemById(action.ItemId); - var userData = _userDataManager.GetUserData(new Guid(action.UserId), item.GetUserDataKey()); + var userData = _userDataManager.GetUserData(action.UserId, item); userData.LastPlayedDate = action.Date; _userDataManager.UpdatePlayState(item, userData, action.PositionTicks); @@ -775,6 +775,13 @@ namespace MediaBrowser.Server.Implementations.Sync removeFromDevice = true; } } + else if (libraryItem != null && libraryItem.DateModified.Ticks != jobItem.ItemDateModifiedTicks && jobItem.ItemDateModifiedTicks > 0) + { + _logger.Info("Setting status to Queued for {0} because the media has been modified since the original sync.", jobItem.ItemId); + jobItem.Status = SyncJobItemStatus.Queued; + jobItem.Progress = 0; + requiresSaving = true; + } } else { @@ -881,6 +888,13 @@ namespace MediaBrowser.Server.Implementations.Sync removeFromDevice = true; } } + else if (libraryItem != null && libraryItem.DateModified.Ticks != jobItem.ItemDateModifiedTicks && jobItem.ItemDateModifiedTicks > 0) + { + _logger.Info("Setting status to Queued for {0} because the media has been modified since the original sync.", jobItem.ItemId); + jobItem.Status = SyncJobItemStatus.Queued; + jobItem.Progress = 0; + requiresSaving = true; + } } else { @@ -1126,7 +1140,7 @@ namespace MediaBrowser.Server.Implementations.Sync return options; } - public ISyncProvider GetSyncProvider(SyncJobItem jobItem, SyncJob job) + public ISyncProvider GetSyncProvider(SyncJobItem jobItem) { foreach (var provider in _providers) { @@ -1323,9 +1337,9 @@ namespace MediaBrowser.Server.Implementations.Sync return list; } - protected internal void OnConversionComplete(SyncJobItem item, SyncJob job) + protected internal void OnConversionComplete(SyncJobItem item) { - var syncProvider = GetSyncProvider(item, job); + var syncProvider = GetSyncProvider(item); if (syncProvider is AppSyncProvider) { return; diff --git a/MediaBrowser.Server.Implementations/Sync/SyncRepository.cs b/MediaBrowser.Server.Implementations/Sync/SyncRepository.cs index 39153526a..a1ed66a99 100644 --- a/MediaBrowser.Server.Implementations/Sync/SyncRepository.cs +++ b/MediaBrowser.Server.Implementations/Sync/SyncRepository.cs @@ -18,156 +18,44 @@ namespace MediaBrowser.Server.Implementations.Sync { public class SyncRepository : BaseSqliteRepository, ISyncRepository { - private IDbConnection _connection; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private IDbCommand _insertJobCommand; - private IDbCommand _updateJobCommand; - private IDbCommand _deleteJobCommand; - - private IDbCommand _deleteJobItemsCommand; - private IDbCommand _insertJobItemCommand; - private IDbCommand _updateJobItemCommand; - private readonly IJsonSerializer _json; - private readonly IServerApplicationPaths _appPaths; - public SyncRepository(ILogManager logManager, IJsonSerializer json, IServerApplicationPaths appPaths) - : base(logManager) + public SyncRepository(ILogManager logManager, IJsonSerializer json, IServerApplicationPaths appPaths, IDbConnector connector) + : base(logManager, connector) { _json = json; - _appPaths = appPaths; + DbFilePath = Path.Combine(appPaths.DataPath, "sync14.db"); } public async Task Initialize() { - var dbFile = Path.Combine(_appPaths.DataPath, "sync14.db"); - - _connection = await SqliteExtensions.ConnectToDb(dbFile, Logger).ConfigureAwait(false); - - string[] queries = { + using (var connection = await CreateConnection().ConfigureAwait(false)) + { + string[] queries = { "create table if not exists SyncJobs (Id GUID PRIMARY KEY, TargetId TEXT NOT NULL, Name TEXT NOT NULL, Profile TEXT, Quality TEXT, Bitrate INT, Status TEXT NOT NULL, Progress FLOAT, UserId TEXT NOT NULL, ItemIds TEXT NOT NULL, Category TEXT, ParentId TEXT, UnwatchedOnly BIT, ItemLimit INT, SyncNewContent BIT, DateCreated DateTime, DateLastModified DateTime, ItemCount int)", "create index if not exists idx_SyncJobs on SyncJobs(Id)", + "create index if not exists idx_SyncJobs1 on SyncJobs(TargetId)", - "create table if not exists SyncJobItems (Id GUID PRIMARY KEY, ItemId TEXT, ItemName TEXT, MediaSourceId TEXT, JobId TEXT, TemporaryPath TEXT, OutputPath TEXT, Status TEXT, TargetId TEXT, DateCreated DateTime, Progress FLOAT, AdditionalFiles TEXT, MediaSource TEXT, IsMarkedForRemoval BIT, JobItemIndex INT)", - "create index if not exists idx_SyncJobItems on SyncJobs(Id)", - - //pragmas - "pragma temp_store = memory", + "create table if not exists SyncJobItems (Id GUID PRIMARY KEY, ItemId TEXT, ItemName TEXT, MediaSourceId TEXT, JobId TEXT, TemporaryPath TEXT, OutputPath TEXT, Status TEXT, TargetId TEXT, DateCreated DateTime, Progress FLOAT, AdditionalFiles TEXT, MediaSource TEXT, IsMarkedForRemoval BIT, JobItemIndex INT, ItemDateModifiedTicks BIGINT)", + "create index if not exists idx_SyncJobItems1 on SyncJobItems(Id)", + "create index if not exists idx_SyncJobItems2 on SyncJobItems(TargetId)", "pragma shrink_memory" }; - _connection.RunQueries(queries, Logger); + connection.RunQueries(queries, Logger); - _connection.AddColumn(Logger, "SyncJobs", "Profile", "TEXT"); - _connection.AddColumn(Logger, "SyncJobs", "Bitrate", "INT"); - - PrepareStatements(); - } - - private void PrepareStatements() - { - // _deleteJobCommand - _deleteJobCommand = _connection.CreateCommand(); - _deleteJobCommand.CommandText = "delete from SyncJobs where Id=@Id"; - _deleteJobCommand.Parameters.Add(_deleteJobCommand, "@Id"); - - // _deleteJobItemsCommand - _deleteJobItemsCommand = _connection.CreateCommand(); - _deleteJobItemsCommand.CommandText = "delete from SyncJobItems where JobId=@JobId"; - _deleteJobItemsCommand.Parameters.Add(_deleteJobItemsCommand, "@JobId"); - - // _insertJobCommand - _insertJobCommand = _connection.CreateCommand(); - _insertJobCommand.CommandText = "insert into SyncJobs (Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount) values (@Id, @TargetId, @Name, @Profile, @Quality, @Bitrate, @Status, @Progress, @UserId, @ItemIds, @Category, @ParentId, @UnwatchedOnly, @ItemLimit, @SyncNewContent, @DateCreated, @DateLastModified, @ItemCount)"; - - _insertJobCommand.Parameters.Add(_insertJobCommand, "@Id"); - _insertJobCommand.Parameters.Add(_insertJobCommand, "@TargetId"); - _insertJobCommand.Parameters.Add(_insertJobCommand, "@Name"); - _insertJobCommand.Parameters.Add(_insertJobCommand, "@Profile"); - _insertJobCommand.Parameters.Add(_insertJobCommand, "@Quality"); - _insertJobCommand.Parameters.Add(_insertJobCommand, "@Bitrate"); - _insertJobCommand.Parameters.Add(_insertJobCommand, "@Status"); - _insertJobCommand.Parameters.Add(_insertJobCommand, "@Progress"); - _insertJobCommand.Parameters.Add(_insertJobCommand, "@UserId"); - _insertJobCommand.Parameters.Add(_insertJobCommand, "@ItemIds"); - _insertJobCommand.Parameters.Add(_insertJobCommand, "@Category"); - _insertJobCommand.Parameters.Add(_insertJobCommand, "@ParentId"); - _insertJobCommand.Parameters.Add(_insertJobCommand, "@UnwatchedOnly"); - _insertJobCommand.Parameters.Add(_insertJobCommand, "@ItemLimit"); - _insertJobCommand.Parameters.Add(_insertJobCommand, "@SyncNewContent"); - _insertJobCommand.Parameters.Add(_insertJobCommand, "@DateCreated"); - _insertJobCommand.Parameters.Add(_insertJobCommand, "@DateLastModified"); - _insertJobCommand.Parameters.Add(_insertJobCommand, "@ItemCount"); - - // _updateJobCommand - _updateJobCommand = _connection.CreateCommand(); - _updateJobCommand.CommandText = "update SyncJobs set TargetId=@TargetId,Name=@Name,Profile=@Profile,Quality=@Quality,Bitrate=@Bitrate,Status=@Status,Progress=@Progress,UserId=@UserId,ItemIds=@ItemIds,Category=@Category,ParentId=@ParentId,UnwatchedOnly=@UnwatchedOnly,ItemLimit=@ItemLimit,SyncNewContent=@SyncNewContent,DateCreated=@DateCreated,DateLastModified=@DateLastModified,ItemCount=@ItemCount where Id=@ID"; - - _updateJobCommand.Parameters.Add(_updateJobCommand, "@Id"); - _updateJobCommand.Parameters.Add(_updateJobCommand, "@TargetId"); - _updateJobCommand.Parameters.Add(_updateJobCommand, "@Name"); - _updateJobCommand.Parameters.Add(_updateJobCommand, "@Profile"); - _updateJobCommand.Parameters.Add(_updateJobCommand, "@Quality"); - _updateJobCommand.Parameters.Add(_updateJobCommand, "@Bitrate"); - _updateJobCommand.Parameters.Add(_updateJobCommand, "@Status"); - _updateJobCommand.Parameters.Add(_updateJobCommand, "@Progress"); - _updateJobCommand.Parameters.Add(_updateJobCommand, "@UserId"); - _updateJobCommand.Parameters.Add(_updateJobCommand, "@ItemIds"); - _updateJobCommand.Parameters.Add(_updateJobCommand, "@Category"); - _updateJobCommand.Parameters.Add(_updateJobCommand, "@ParentId"); - _updateJobCommand.Parameters.Add(_updateJobCommand, "@UnwatchedOnly"); - _updateJobCommand.Parameters.Add(_updateJobCommand, "@ItemLimit"); - _updateJobCommand.Parameters.Add(_updateJobCommand, "@SyncNewContent"); - _updateJobCommand.Parameters.Add(_updateJobCommand, "@DateCreated"); - _updateJobCommand.Parameters.Add(_updateJobCommand, "@DateLastModified"); - _updateJobCommand.Parameters.Add(_updateJobCommand, "@ItemCount"); - - // _insertJobItemCommand - _insertJobItemCommand = _connection.CreateCommand(); - _insertJobItemCommand.CommandText = "insert into SyncJobItems (Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex) values (@Id, @ItemId, @ItemName, @MediaSourceId, @JobId, @TemporaryPath, @OutputPath, @Status, @TargetId, @DateCreated, @Progress, @AdditionalFiles, @MediaSource, @IsMarkedForRemoval, @JobItemIndex)"; - - _insertJobItemCommand.Parameters.Add(_insertJobItemCommand, "@Id"); - _insertJobItemCommand.Parameters.Add(_insertJobItemCommand, "@ItemId"); - _insertJobItemCommand.Parameters.Add(_insertJobItemCommand, "@ItemName"); - _insertJobItemCommand.Parameters.Add(_insertJobItemCommand, "@MediaSourceId"); - _insertJobItemCommand.Parameters.Add(_insertJobItemCommand, "@JobId"); - _insertJobItemCommand.Parameters.Add(_insertJobItemCommand, "@TemporaryPath"); - _insertJobItemCommand.Parameters.Add(_insertJobItemCommand, "@OutputPath"); - _insertJobItemCommand.Parameters.Add(_insertJobItemCommand, "@Status"); - _insertJobItemCommand.Parameters.Add(_insertJobItemCommand, "@TargetId"); - _insertJobItemCommand.Parameters.Add(_insertJobItemCommand, "@DateCreated"); - _insertJobItemCommand.Parameters.Add(_insertJobItemCommand, "@Progress"); - _insertJobItemCommand.Parameters.Add(_insertJobItemCommand, "@AdditionalFiles"); - _insertJobItemCommand.Parameters.Add(_insertJobItemCommand, "@MediaSource"); - _insertJobItemCommand.Parameters.Add(_insertJobItemCommand, "@IsMarkedForRemoval"); - _insertJobItemCommand.Parameters.Add(_insertJobItemCommand, "@JobItemIndex"); - - // _updateJobItemCommand - _updateJobItemCommand = _connection.CreateCommand(); - _updateJobItemCommand.CommandText = "update SyncJobItems set ItemId=@ItemId,ItemName=@ItemName,MediaSourceId=@MediaSourceId,JobId=@JobId,TemporaryPath=@TemporaryPath,OutputPath=@OutputPath,Status=@Status,TargetId=@TargetId,DateCreated=@DateCreated,Progress=@Progress,AdditionalFiles=@AdditionalFiles,MediaSource=@MediaSource,IsMarkedForRemoval=@IsMarkedForRemoval,JobItemIndex=@JobItemIndex where Id=@Id"; - - _updateJobItemCommand.Parameters.Add(_updateJobItemCommand, "@Id"); - _updateJobItemCommand.Parameters.Add(_updateJobItemCommand, "@ItemId"); - _updateJobItemCommand.Parameters.Add(_updateJobItemCommand, "@ItemName"); - _updateJobItemCommand.Parameters.Add(_updateJobItemCommand, "@MediaSourceId"); - _updateJobItemCommand.Parameters.Add(_updateJobItemCommand, "@JobId"); - _updateJobItemCommand.Parameters.Add(_updateJobItemCommand, "@TemporaryPath"); - _updateJobItemCommand.Parameters.Add(_updateJobItemCommand, "@OutputPath"); - _updateJobItemCommand.Parameters.Add(_updateJobItemCommand, "@Status"); - _updateJobItemCommand.Parameters.Add(_updateJobItemCommand, "@TargetId"); - _updateJobItemCommand.Parameters.Add(_updateJobItemCommand, "@DateCreated"); - _updateJobItemCommand.Parameters.Add(_updateJobItemCommand, "@Progress"); - _updateJobItemCommand.Parameters.Add(_updateJobItemCommand, "@AdditionalFiles"); - _updateJobItemCommand.Parameters.Add(_updateJobItemCommand, "@MediaSource"); - _updateJobItemCommand.Parameters.Add(_updateJobItemCommand, "@IsMarkedForRemoval"); - _updateJobItemCommand.Parameters.Add(_updateJobItemCommand, "@JobItemIndex"); + connection.AddColumn(Logger, "SyncJobs", "Profile", "TEXT"); + connection.AddColumn(Logger, "SyncJobs", "Bitrate", "INT"); + connection.AddColumn(Logger, "SyncJobItems", "ItemDateModifiedTicks", "BIGINT"); + } } private const string BaseJobSelectText = "select Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount from SyncJobs"; - private const string BaseJobItemSelectText = "select Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex from SyncJobItems"; + private const string BaseJobItemSelectText = "select Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex, ItemDateModifiedTicks from SyncJobItems"; public SyncJob GetJob(string id) { @@ -177,7 +65,7 @@ namespace MediaBrowser.Server.Implementations.Sync } CheckDisposed(); - + var guid = new Guid(id); if (guid == Guid.Empty) @@ -185,22 +73,25 @@ namespace MediaBrowser.Server.Implementations.Sync throw new ArgumentNullException("id"); } - using (var cmd = _connection.CreateCommand()) + using (var connection = CreateConnection(true).Result) { - cmd.CommandText = BaseJobSelectText + " where Id=@Id"; + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = BaseJobSelectText + " where Id=@Id"; - cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid; + cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid; - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) - { - if (reader.Read()) + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) { - return GetJob(reader); + if (reader.Read()) + { + return GetJob(reader); + } } } - } - return null; + return null; + } } private SyncJob GetJob(IDataReader reader) @@ -278,15 +169,15 @@ namespace MediaBrowser.Server.Implementations.Sync public Task Create(SyncJob job) { - return InsertOrUpdate(job, _insertJobCommand); + return InsertOrUpdate(job, true); } public Task Update(SyncJob job) { - return InsertOrUpdate(job, _updateJobCommand); + return InsertOrUpdate(job, false); } - private async Task InsertOrUpdate(SyncJob job, IDbCommand cmd) + private async Task InsertOrUpdate(SyncJob job, bool insert) { if (job == null) { @@ -294,70 +185,119 @@ namespace MediaBrowser.Server.Implementations.Sync } CheckDisposed(); - - await WriteLock.WaitAsync().ConfigureAwait(false); - - IDbTransaction transaction = null; - try + using (var connection = await CreateConnection().ConfigureAwait(false)) { - transaction = _connection.BeginTransaction(); - - var index = 0; - - cmd.GetParameter(index++).Value = new Guid(job.Id); - cmd.GetParameter(index++).Value = job.TargetId; - cmd.GetParameter(index++).Value = job.Name; - cmd.GetParameter(index++).Value = job.Profile; - cmd.GetParameter(index++).Value = job.Quality; - cmd.GetParameter(index++).Value = job.Bitrate; - cmd.GetParameter(index++).Value = job.Status.ToString(); - cmd.GetParameter(index++).Value = job.Progress; - cmd.GetParameter(index++).Value = job.UserId; - cmd.GetParameter(index++).Value = string.Join(",", job.RequestedItemIds.ToArray()); - cmd.GetParameter(index++).Value = job.Category; - cmd.GetParameter(index++).Value = job.ParentId; - cmd.GetParameter(index++).Value = job.UnwatchedOnly; - cmd.GetParameter(index++).Value = job.ItemLimit; - cmd.GetParameter(index++).Value = job.SyncNewContent; - cmd.GetParameter(index++).Value = job.DateCreated; - cmd.GetParameter(index++).Value = job.DateLastModified; - cmd.GetParameter(index++).Value = job.ItemCount; - - cmd.Transaction = transaction; + using (var cmd = connection.CreateCommand()) + { + if (insert) + { + cmd.CommandText = "insert into SyncJobs (Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount) values (@Id, @TargetId, @Name, @Profile, @Quality, @Bitrate, @Status, @Progress, @UserId, @ItemIds, @Category, @ParentId, @UnwatchedOnly, @ItemLimit, @SyncNewContent, @DateCreated, @DateLastModified, @ItemCount)"; + + cmd.Parameters.Add(cmd, "@Id"); + cmd.Parameters.Add(cmd, "@TargetId"); + cmd.Parameters.Add(cmd, "@Name"); + cmd.Parameters.Add(cmd, "@Profile"); + cmd.Parameters.Add(cmd, "@Quality"); + cmd.Parameters.Add(cmd, "@Bitrate"); + cmd.Parameters.Add(cmd, "@Status"); + cmd.Parameters.Add(cmd, "@Progress"); + cmd.Parameters.Add(cmd, "@UserId"); + cmd.Parameters.Add(cmd, "@ItemIds"); + cmd.Parameters.Add(cmd, "@Category"); + cmd.Parameters.Add(cmd, "@ParentId"); + cmd.Parameters.Add(cmd, "@UnwatchedOnly"); + cmd.Parameters.Add(cmd, "@ItemLimit"); + cmd.Parameters.Add(cmd, "@SyncNewContent"); + cmd.Parameters.Add(cmd, "@DateCreated"); + cmd.Parameters.Add(cmd, "@DateLastModified"); + cmd.Parameters.Add(cmd, "@ItemCount"); + } + else + { + cmd.CommandText = "update SyncJobs set TargetId=@TargetId,Name=@Name,Profile=@Profile,Quality=@Quality,Bitrate=@Bitrate,Status=@Status,Progress=@Progress,UserId=@UserId,ItemIds=@ItemIds,Category=@Category,ParentId=@ParentId,UnwatchedOnly=@UnwatchedOnly,ItemLimit=@ItemLimit,SyncNewContent=@SyncNewContent,DateCreated=@DateCreated,DateLastModified=@DateLastModified,ItemCount=@ItemCount where Id=@Id"; + + cmd.Parameters.Add(cmd, "@Id"); + cmd.Parameters.Add(cmd, "@TargetId"); + cmd.Parameters.Add(cmd, "@Name"); + cmd.Parameters.Add(cmd, "@Profile"); + cmd.Parameters.Add(cmd, "@Quality"); + cmd.Parameters.Add(cmd, "@Bitrate"); + cmd.Parameters.Add(cmd, "@Status"); + cmd.Parameters.Add(cmd, "@Progress"); + cmd.Parameters.Add(cmd, "@UserId"); + cmd.Parameters.Add(cmd, "@ItemIds"); + cmd.Parameters.Add(cmd, "@Category"); + cmd.Parameters.Add(cmd, "@ParentId"); + cmd.Parameters.Add(cmd, "@UnwatchedOnly"); + cmd.Parameters.Add(cmd, "@ItemLimit"); + cmd.Parameters.Add(cmd, "@SyncNewContent"); + cmd.Parameters.Add(cmd, "@DateCreated"); + cmd.Parameters.Add(cmd, "@DateLastModified"); + cmd.Parameters.Add(cmd, "@ItemCount"); + } - cmd.ExecuteNonQuery(); + IDbTransaction transaction = null; - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } + try + { + transaction = connection.BeginTransaction(); + + var index = 0; + + cmd.GetParameter(index++).Value = new Guid(job.Id); + cmd.GetParameter(index++).Value = job.TargetId; + cmd.GetParameter(index++).Value = job.Name; + cmd.GetParameter(index++).Value = job.Profile; + cmd.GetParameter(index++).Value = job.Quality; + cmd.GetParameter(index++).Value = job.Bitrate; + cmd.GetParameter(index++).Value = job.Status.ToString(); + cmd.GetParameter(index++).Value = job.Progress; + cmd.GetParameter(index++).Value = job.UserId; + cmd.GetParameter(index++).Value = string.Join(",", job.RequestedItemIds.ToArray()); + cmd.GetParameter(index++).Value = job.Category; + cmd.GetParameter(index++).Value = job.ParentId; + cmd.GetParameter(index++).Value = job.UnwatchedOnly; + cmd.GetParameter(index++).Value = job.ItemLimit; + cmd.GetParameter(index++).Value = job.SyncNewContent; + cmd.GetParameter(index++).Value = job.DateCreated; + cmd.GetParameter(index++).Value = job.DateLastModified; + cmd.GetParameter(index++).Value = job.ItemCount; + + cmd.Transaction = transaction; + + cmd.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save record:", e); + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save record:", e); - if (transaction != null) - { - transaction.Rollback(); - } + if (transaction != null) + { + transaction.Rollback(); + } - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } } - - WriteLock.Release(); } } @@ -369,56 +309,66 @@ namespace MediaBrowser.Server.Implementations.Sync } CheckDisposed(); - - await WriteLock.WaitAsync().ConfigureAwait(false); - IDbTransaction transaction = null; - - try - { - transaction = _connection.BeginTransaction(); - - var index = 0; - - _deleteJobCommand.GetParameter(index++).Value = new Guid(id); - _deleteJobCommand.Transaction = transaction; - _deleteJobCommand.ExecuteNonQuery(); - - index = 0; - _deleteJobItemsCommand.GetParameter(index++).Value = id; - _deleteJobItemsCommand.Transaction = transaction; - _deleteJobItemsCommand.ExecuteNonQuery(); - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) + using (var connection = await CreateConnection().ConfigureAwait(false)) { - Logger.ErrorException("Failed to save record:", e); - - if (transaction != null) + using (var deleteJobCommand = connection.CreateCommand()) { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); + using (var deleteJobItemsCommand = connection.CreateCommand()) + { + IDbTransaction transaction = null; + + try + { + // _deleteJobCommand + deleteJobCommand.CommandText = "delete from SyncJobs where Id=@Id"; + deleteJobCommand.Parameters.Add(deleteJobCommand, "@Id"); + + transaction = connection.BeginTransaction(); + + deleteJobCommand.GetParameter(0).Value = new Guid(id); + deleteJobCommand.Transaction = transaction; + deleteJobCommand.ExecuteNonQuery(); + + // _deleteJobItemsCommand + deleteJobItemsCommand.CommandText = "delete from SyncJobItems where JobId=@JobId"; + deleteJobItemsCommand.Parameters.Add(deleteJobItemsCommand, "@JobId"); + + deleteJobItemsCommand.GetParameter(0).Value = id; + deleteJobItemsCommand.Transaction = transaction; + deleteJobItemsCommand.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save record:", e); + + if (transaction != null) + { + transaction.Rollback(); + } + + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } + } } - - WriteLock.Release(); } } @@ -430,83 +380,86 @@ namespace MediaBrowser.Server.Implementations.Sync } CheckDisposed(); - - using (var cmd = _connection.CreateCommand()) + + using (var connection = CreateConnection(true).Result) { - cmd.CommandText = BaseJobSelectText; + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = BaseJobSelectText; - var whereClauses = new List<string>(); + var whereClauses = new List<string>(); - if (query.Statuses.Length > 0) - { - var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); + if (query.Statuses.Length > 0) + { + var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); - whereClauses.Add(string.Format("Status in ({0})", statuses)); - } - if (!string.IsNullOrWhiteSpace(query.TargetId)) - { - whereClauses.Add("TargetId=@TargetId"); - cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; - } - if (!string.IsNullOrWhiteSpace(query.UserId)) - { - whereClauses.Add("UserId=@UserId"); - cmd.Parameters.Add(cmd, "@UserId", DbType.String).Value = query.UserId; - } - if (query.SyncNewContent.HasValue) - { - whereClauses.Add("SyncNewContent=@SyncNewContent"); - cmd.Parameters.Add(cmd, "@SyncNewContent", DbType.Boolean).Value = query.SyncNewContent.Value; - } + whereClauses.Add(string.Format("Status in ({0})", statuses)); + } + if (!string.IsNullOrWhiteSpace(query.TargetId)) + { + whereClauses.Add("TargetId=@TargetId"); + cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; + } + if (!string.IsNullOrWhiteSpace(query.UserId)) + { + whereClauses.Add("UserId=@UserId"); + cmd.Parameters.Add(cmd, "@UserId", DbType.String).Value = query.UserId; + } + if (query.SyncNewContent.HasValue) + { + whereClauses.Add("SyncNewContent=@SyncNewContent"); + cmd.Parameters.Add(cmd, "@SyncNewContent", DbType.Boolean).Value = query.SyncNewContent.Value; + } - cmd.CommandText += " mainTable"; + cmd.CommandText += " mainTable"; - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); - var startIndex = query.StartIndex ?? 0; - if (startIndex > 0) - { - whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobs ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC LIMIT {0})", - startIndex.ToString(_usCulture))); - } + var startIndex = query.StartIndex ?? 0; + if (startIndex > 0) + { + whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobs ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC LIMIT {0})", + startIndex.ToString(_usCulture))); + } - if (whereClauses.Count > 0) - { - cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); - } + if (whereClauses.Count > 0) + { + cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); + } - cmd.CommandText += " ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC"; + cmd.CommandText += " ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC"; - if (query.Limit.HasValue) - { - cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); - } + if (query.Limit.HasValue) + { + cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); + } - cmd.CommandText += "; select count (Id) from SyncJobs" + whereTextWithoutPaging; + cmd.CommandText += "; select count (Id) from SyncJobs" + whereTextWithoutPaging; - var list = new List<SyncJob>(); - var count = 0; + var list = new List<SyncJob>(); + var count = 0; - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) - { - while (reader.Read()) + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) { - list.Add(GetJob(reader)); + while (reader.Read()) + { + list.Add(GetJob(reader)); + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } } - if (reader.NextResult() && reader.Read()) + return new QueryResult<SyncJob>() { - count = reader.GetInt32(0); - } + Items = list.ToArray(), + TotalRecordCount = count + }; } - - return new QueryResult<SyncJob>() - { - Items = list.ToArray(), - TotalRecordCount = count - }; } } @@ -518,25 +471,28 @@ namespace MediaBrowser.Server.Implementations.Sync } CheckDisposed(); - + var guid = new Guid(id); - using (var cmd = _connection.CreateCommand()) + using (var connection = CreateConnection(true).Result) { - cmd.CommandText = BaseJobItemSelectText + " where Id=@Id"; + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = BaseJobItemSelectText + " where Id=@Id"; - cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid; + cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid; - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) - { - if (reader.Read()) + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) { - return GetJobItem(reader); + if (reader.Read()) + { + return GetJobItem(reader); + } } } - } - return null; + return null; + } } private QueryResult<T> GetJobItemReader<T>(SyncJobItemQuery query, string baseSelectText, Func<IDataReader, T> itemFactory) @@ -546,81 +502,84 @@ namespace MediaBrowser.Server.Implementations.Sync throw new ArgumentNullException("query"); } - using (var cmd = _connection.CreateCommand()) + using (var connection = CreateConnection(true).Result) { - cmd.CommandText = baseSelectText; + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = baseSelectText; - var whereClauses = new List<string>(); + var whereClauses = new List<string>(); - if (!string.IsNullOrWhiteSpace(query.JobId)) - { - whereClauses.Add("JobId=@JobId"); - cmd.Parameters.Add(cmd, "@JobId", DbType.String).Value = query.JobId; - } - if (!string.IsNullOrWhiteSpace(query.ItemId)) - { - whereClauses.Add("ItemId=@ItemId"); - cmd.Parameters.Add(cmd, "@ItemId", DbType.String).Value = query.ItemId; - } - if (!string.IsNullOrWhiteSpace(query.TargetId)) - { - whereClauses.Add("TargetId=@TargetId"); - cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; - } + if (!string.IsNullOrWhiteSpace(query.JobId)) + { + whereClauses.Add("JobId=@JobId"); + cmd.Parameters.Add(cmd, "@JobId", DbType.String).Value = query.JobId; + } + if (!string.IsNullOrWhiteSpace(query.ItemId)) + { + whereClauses.Add("ItemId=@ItemId"); + cmd.Parameters.Add(cmd, "@ItemId", DbType.String).Value = query.ItemId; + } + if (!string.IsNullOrWhiteSpace(query.TargetId)) + { + whereClauses.Add("TargetId=@TargetId"); + cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; + } - if (query.Statuses.Length > 0) - { - var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); + if (query.Statuses.Length > 0) + { + var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); - whereClauses.Add(string.Format("Status in ({0})", statuses)); - } + whereClauses.Add(string.Format("Status in ({0})", statuses)); + } - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); - var startIndex = query.StartIndex ?? 0; - if (startIndex > 0) - { - whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobItems ORDER BY JobItemIndex, DateCreated LIMIT {0})", - startIndex.ToString(_usCulture))); - } + var startIndex = query.StartIndex ?? 0; + if (startIndex > 0) + { + whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobItems ORDER BY JobItemIndex, DateCreated LIMIT {0})", + startIndex.ToString(_usCulture))); + } - if (whereClauses.Count > 0) - { - cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); - } + if (whereClauses.Count > 0) + { + cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); + } - cmd.CommandText += " ORDER BY JobItemIndex, DateCreated"; + cmd.CommandText += " ORDER BY JobItemIndex, DateCreated"; - if (query.Limit.HasValue) - { - cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); - } + if (query.Limit.HasValue) + { + cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); + } - cmd.CommandText += "; select count (Id) from SyncJobItems" + whereTextWithoutPaging; + cmd.CommandText += "; select count (Id) from SyncJobItems" + whereTextWithoutPaging; - var list = new List<T>(); - var count = 0; + var list = new List<T>(); + var count = 0; - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) - { - while (reader.Read()) + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) { - list.Add(itemFactory(reader)); + while (reader.Read()) + { + list.Add(itemFactory(reader)); + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } } - if (reader.NextResult() && reader.Read()) + return new QueryResult<T>() { - count = reader.GetInt32(0); - } + Items = list.ToArray(), + TotalRecordCount = count + }; } - - return new QueryResult<T>() - { - Items = list.ToArray(), - TotalRecordCount = count - }; } } @@ -636,15 +595,15 @@ namespace MediaBrowser.Server.Implementations.Sync public Task Create(SyncJobItem jobItem) { - return InsertOrUpdate(jobItem, _insertJobItemCommand); + return InsertOrUpdate(jobItem, true); } public Task Update(SyncJobItem jobItem) { - return InsertOrUpdate(jobItem, _updateJobItemCommand); + return InsertOrUpdate(jobItem, false); } - private async Task InsertOrUpdate(SyncJobItem jobItem, IDbCommand cmd) + private async Task InsertOrUpdate(SyncJobItem jobItem, bool insert) { if (jobItem == null) { @@ -652,67 +611,114 @@ namespace MediaBrowser.Server.Implementations.Sync } CheckDisposed(); - - await WriteLock.WaitAsync().ConfigureAwait(false); - - IDbTransaction transaction = null; - try + using (var connection = await CreateConnection().ConfigureAwait(false)) { - transaction = _connection.BeginTransaction(); - - var index = 0; - - cmd.GetParameter(index++).Value = new Guid(jobItem.Id); - cmd.GetParameter(index++).Value = jobItem.ItemId; - cmd.GetParameter(index++).Value = jobItem.ItemName; - cmd.GetParameter(index++).Value = jobItem.MediaSourceId; - cmd.GetParameter(index++).Value = jobItem.JobId; - cmd.GetParameter(index++).Value = jobItem.TemporaryPath; - cmd.GetParameter(index++).Value = jobItem.OutputPath; - cmd.GetParameter(index++).Value = jobItem.Status.ToString(); - cmd.GetParameter(index++).Value = jobItem.TargetId; - cmd.GetParameter(index++).Value = jobItem.DateCreated; - cmd.GetParameter(index++).Value = jobItem.Progress; - cmd.GetParameter(index++).Value = _json.SerializeToString(jobItem.AdditionalFiles); - cmd.GetParameter(index++).Value = jobItem.MediaSource == null ? null : _json.SerializeToString(jobItem.MediaSource); - cmd.GetParameter(index++).Value = jobItem.IsMarkedForRemoval; - cmd.GetParameter(index++).Value = jobItem.JobItemIndex; - - cmd.Transaction = transaction; + using (var cmd = connection.CreateCommand()) + { + if (insert) + { + cmd.CommandText = "insert into SyncJobItems (Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex, ItemDateModifiedTicks) values (@Id, @ItemId, @ItemName, @MediaSourceId, @JobId, @TemporaryPath, @OutputPath, @Status, @TargetId, @DateCreated, @Progress, @AdditionalFiles, @MediaSource, @IsMarkedForRemoval, @JobItemIndex, @ItemDateModifiedTicks)"; + + cmd.Parameters.Add(cmd, "@Id"); + cmd.Parameters.Add(cmd, "@ItemId"); + cmd.Parameters.Add(cmd, "@ItemName"); + cmd.Parameters.Add(cmd, "@MediaSourceId"); + cmd.Parameters.Add(cmd, "@JobId"); + cmd.Parameters.Add(cmd, "@TemporaryPath"); + cmd.Parameters.Add(cmd, "@OutputPath"); + cmd.Parameters.Add(cmd, "@Status"); + cmd.Parameters.Add(cmd, "@TargetId"); + cmd.Parameters.Add(cmd, "@DateCreated"); + cmd.Parameters.Add(cmd, "@Progress"); + cmd.Parameters.Add(cmd, "@AdditionalFiles"); + cmd.Parameters.Add(cmd, "@MediaSource"); + cmd.Parameters.Add(cmd, "@IsMarkedForRemoval"); + cmd.Parameters.Add(cmd, "@JobItemIndex"); + cmd.Parameters.Add(cmd, "@ItemDateModifiedTicks"); + } + else + { + // cmd + cmd.CommandText = "update SyncJobItems set ItemId=@ItemId,ItemName=@ItemName,MediaSourceId=@MediaSourceId,JobId=@JobId,TemporaryPath=@TemporaryPath,OutputPath=@OutputPath,Status=@Status,TargetId=@TargetId,DateCreated=@DateCreated,Progress=@Progress,AdditionalFiles=@AdditionalFiles,MediaSource=@MediaSource,IsMarkedForRemoval=@IsMarkedForRemoval,JobItemIndex=@JobItemIndex,ItemDateModifiedTicks=@ItemDateModifiedTicks where Id=@Id"; + + cmd.Parameters.Add(cmd, "@Id"); + cmd.Parameters.Add(cmd, "@ItemId"); + cmd.Parameters.Add(cmd, "@ItemName"); + cmd.Parameters.Add(cmd, "@MediaSourceId"); + cmd.Parameters.Add(cmd, "@JobId"); + cmd.Parameters.Add(cmd, "@TemporaryPath"); + cmd.Parameters.Add(cmd, "@OutputPath"); + cmd.Parameters.Add(cmd, "@Status"); + cmd.Parameters.Add(cmd, "@TargetId"); + cmd.Parameters.Add(cmd, "@DateCreated"); + cmd.Parameters.Add(cmd, "@Progress"); + cmd.Parameters.Add(cmd, "@AdditionalFiles"); + cmd.Parameters.Add(cmd, "@MediaSource"); + cmd.Parameters.Add(cmd, "@IsMarkedForRemoval"); + cmd.Parameters.Add(cmd, "@JobItemIndex"); + cmd.Parameters.Add(cmd, "@ItemDateModifiedTicks"); + } - cmd.ExecuteNonQuery(); + IDbTransaction transaction = null; - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } + try + { + transaction = connection.BeginTransaction(); + + var index = 0; + + cmd.GetParameter(index++).Value = new Guid(jobItem.Id); + cmd.GetParameter(index++).Value = jobItem.ItemId; + cmd.GetParameter(index++).Value = jobItem.ItemName; + cmd.GetParameter(index++).Value = jobItem.MediaSourceId; + cmd.GetParameter(index++).Value = jobItem.JobId; + cmd.GetParameter(index++).Value = jobItem.TemporaryPath; + cmd.GetParameter(index++).Value = jobItem.OutputPath; + cmd.GetParameter(index++).Value = jobItem.Status.ToString(); + cmd.GetParameter(index++).Value = jobItem.TargetId; + cmd.GetParameter(index++).Value = jobItem.DateCreated; + cmd.GetParameter(index++).Value = jobItem.Progress; + cmd.GetParameter(index++).Value = _json.SerializeToString(jobItem.AdditionalFiles); + cmd.GetParameter(index++).Value = jobItem.MediaSource == null ? null : _json.SerializeToString(jobItem.MediaSource); + cmd.GetParameter(index++).Value = jobItem.IsMarkedForRemoval; + cmd.GetParameter(index++).Value = jobItem.JobItemIndex; + cmd.GetParameter(index++).Value = jobItem.ItemDateModifiedTicks; + + cmd.Transaction = transaction; + + cmd.ExecuteNonQuery(); + + transaction.Commit(); + } + catch (OperationCanceledException) + { + if (transaction != null) + { + transaction.Rollback(); + } - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save record:", e); + throw; + } + catch (Exception e) + { + Logger.ErrorException("Failed to save record:", e); - if (transaction != null) - { - transaction.Rollback(); - } + if (transaction != null) + { + transaction.Rollback(); + } - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); + throw; + } + finally + { + if (transaction != null) + { + transaction.Dispose(); + } + } } - - WriteLock.Release(); } } @@ -782,6 +788,11 @@ namespace MediaBrowser.Server.Implementations.Sync info.IsMarkedForRemoval = reader.GetBoolean(13); info.JobItemIndex = reader.GetInt32(14); + if (!reader.IsDBNull(15)) + { + info.ItemDateModifiedTicks = reader.GetInt64(15); + } + return info; } @@ -798,19 +809,5 @@ namespace MediaBrowser.Server.Implementations.Sync return item; } - - protected override void CloseConnection() - { - if (_connection != null) - { - if (_connection.IsOpen()) - { - _connection.Close(); - } - - _connection.Dispose(); - _connection = null; - } - } } } diff --git a/MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs b/MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs index 3e43ebe9b..ddc1de9cd 100644 --- a/MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs +++ b/MediaBrowser.Server.Implementations/TV/TVSeriesManager.cs @@ -7,6 +7,7 @@ using MediaBrowser.Model.Querying; using System; using System.Collections.Generic; using System.Linq; +using MediaBrowser.Controller.Configuration; namespace MediaBrowser.Server.Implementations.TV { @@ -15,12 +16,14 @@ namespace MediaBrowser.Server.Implementations.TV private readonly IUserManager _userManager; private readonly IUserDataManager _userDataManager; private readonly ILibraryManager _libraryManager; + private readonly IServerConfigurationManager _config; - public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager) + public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager, IServerConfigurationManager config) { _userManager = userManager; _userDataManager = userDataManager; _libraryManager = libraryManager; + _config = config; } public QueryResult<BaseItem> GetNextUp(NextUpQuery request) @@ -32,16 +35,36 @@ namespace MediaBrowser.Server.Implementations.TV throw new ArgumentException("User not found"); } - var parentIds = string.IsNullOrEmpty(request.ParentId) - ? new string[] { } - : new[] { request.ParentId }; + var parentIdGuid = string.IsNullOrWhiteSpace(request.ParentId) ? (Guid?)null : new Guid(request.ParentId); + + string presentationUniqueKey = null; + int? limit = null; + if (!string.IsNullOrWhiteSpace(request.SeriesId)) + { + var series = _libraryManager.GetItemById(request.SeriesId); + + if (series != null) + { + presentationUniqueKey = GetUniqueSeriesKey(series); + limit = 1; + } + } + + if (string.IsNullOrWhiteSpace(presentationUniqueKey) && limit.HasValue) + { + limit = limit.Value + 10; + } var items = _libraryManager.GetItemList(new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(Series).Name }, - SortOrder = SortOrder.Ascending + SortOrder = SortOrder.Ascending, + PresentationUniqueKey = presentationUniqueKey, + Limit = limit, + ParentId = parentIdGuid, + Recursive = true - }, parentIds).Cast<Series>(); + }).Cast<Series>(); // Avoid implicitly captured closure var episodes = GetNextUpEpisodes(request, user, items); @@ -58,10 +81,30 @@ namespace MediaBrowser.Server.Implementations.TV throw new ArgumentException("User not found"); } + string presentationUniqueKey = null; + int? limit = null; + if (!string.IsNullOrWhiteSpace(request.SeriesId)) + { + var series = _libraryManager.GetItemById(request.SeriesId); + + if (series != null) + { + presentationUniqueKey = GetUniqueSeriesKey(series); + limit = 1; + } + } + + if (string.IsNullOrWhiteSpace(presentationUniqueKey) && limit.HasValue) + { + limit = limit.Value + 10; + } + var items = _libraryManager.GetItemList(new InternalItemsQuery(user) { IncludeItemTypes = new[] { typeof(Series).Name }, - SortOrder = SortOrder.Ascending + SortOrder = SortOrder.Ascending, + PresentationUniqueKey = presentationUniqueKey, + Limit = limit }, parentsFolders.Select(i => i.Id.ToString("N"))).Cast<Series>(); @@ -76,32 +119,40 @@ namespace MediaBrowser.Server.Implementations.TV // Avoid implicitly captured closure var currentUser = user; - return FilterSeries(request, series) - .AsParallel() + var allNextUp = series .Select(i => GetNextUp(i, currentUser)) + .Where(i => i.Item1 != null) // Include if an episode was found, and either the series is not unwatched or the specific series was requested - .Where(i => i.Item1 != null && (!i.Item3 || !string.IsNullOrWhiteSpace(request.SeriesId))) - .OrderByDescending(i => - { - var episode = i.Item1; + .OrderByDescending(i => i.Item2) + .ThenByDescending(i => i.Item1.PremiereDate ?? DateTime.MinValue) + .ToList(); - var seriesUserData = _userDataManager.GetUserData(user.Id, episode.Series.GetUserDataKey()); + // If viewing all next up for all series, remove first episodes + if (string.IsNullOrWhiteSpace(request.SeriesId)) + { + var withoutFirstEpisode = allNextUp + .Where(i => !i.Item3) + .ToList(); - if (seriesUserData.IsFavorite) - { - return 2; - } + // But if that returns empty, keep those first episodes (avoid completely empty view) + if (withoutFirstEpisode.Count > 0) + { + allNextUp = withoutFirstEpisode; + } + } - if (seriesUserData.Likes.HasValue) - { - return seriesUserData.Likes.Value ? 1 : -1; - } + return allNextUp + .Select(i => i.Item1) + .Take(request.Limit ?? int.MaxValue); + } - return 0; - }) - .ThenByDescending(i => i.Item2) - .ThenByDescending(i => i.Item1.PremiereDate ?? DateTime.MinValue) - .Select(i => i.Item1); + private string GetUniqueSeriesKey(BaseItem series) + { + if (_config.Configuration.SchemaVersion < 97) + { + return series.Id.ToString("N"); + } + return series.PresentationUniqueKey; } /// <summary> @@ -112,64 +163,43 @@ namespace MediaBrowser.Server.Implementations.TV /// <returns>Task{Episode}.</returns> private Tuple<Episode, DateTime, bool> GetNextUp(Series series, User user) { - // Get them in display order, then reverse - var allEpisodes = series.GetSeasons(user, true, true) - .Where(i => !i.IndexNumber.HasValue || i.IndexNumber.Value != 0) - .SelectMany(i => i.GetEpisodes(user)) - .Reverse() - .ToList(); - - Episode lastWatched = null; - var lastWatchedDate = DateTime.MinValue; - Episode nextUp = null; - - var includeMissing = user.Configuration.DisplayMissingEpisodes; - - // Go back starting with the most recent episodes - foreach (var episode in allEpisodes) + var lastWatchedEpisode = _libraryManager.GetItemList(new InternalItemsQuery(user) { - var userData = _userDataManager.GetUserData(user.Id, episode.GetUserDataKey()); + AncestorWithPresentationUniqueKey = GetUniqueSeriesKey(series), + IncludeItemTypes = new[] { typeof(Episode).Name }, + SortBy = new[] { ItemSortBy.SortName }, + SortOrder = SortOrder.Descending, + IsPlayed = true, + Limit = 1, + ParentIndexNumberNotEquals = 0 - if (userData.Played) - { - if (lastWatched != null || nextUp == null) - { - break; - } - - lastWatched = episode; - lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue; - } - else - { - if (!episode.IsVirtualUnaired && (!episode.IsMissingEpisode || includeMissing)) - { - nextUp = episode; - } - } - } + }).FirstOrDefault(); - if (lastWatched != null) + var firstUnwatchedEpisode = _libraryManager.GetItemList(new InternalItemsQuery(user) { - return new Tuple<Episode, DateTime, bool>(nextUp, lastWatchedDate, false); - } - - var firstEpisode = allEpisodes.LastOrDefault(i => !i.IsVirtualUnaired && (!i.IsMissingEpisode || includeMissing) && !i.IsPlayed(user)); - - // Return the first episode - return new Tuple<Episode, DateTime, bool>(firstEpisode, DateTime.MinValue, true); - } - - private IEnumerable<Series> FilterSeries(NextUpQuery request, IEnumerable<Series> items) - { - if (!string.IsNullOrWhiteSpace(request.SeriesId)) + AncestorWithPresentationUniqueKey = GetUniqueSeriesKey(series), + IncludeItemTypes = new[] { typeof(Episode).Name }, + SortBy = new[] { ItemSortBy.SortName }, + SortOrder = SortOrder.Ascending, + Limit = 1, + IsPlayed = false, + IsVirtualItem = false, + ParentIndexNumberNotEquals = 0, + MinSortName = lastWatchedEpisode == null ? null : lastWatchedEpisode.SortName + + }).Cast<Episode>().FirstOrDefault(); + + if (lastWatchedEpisode != null && firstUnwatchedEpisode != null) { - var id = new Guid(request.SeriesId); + var userData = _userDataManager.GetUserData(user, lastWatchedEpisode); + + var lastWatchedDate = userData.LastPlayedDate ?? DateTime.MinValue.AddDays(1); - items = items.Where(i => i.Id == id); + return new Tuple<Episode, DateTime, bool>(firstUnwatchedEpisode, lastWatchedDate, false); } - return items; + // Return the first episode + return new Tuple<Episode, DateTime, bool>(firstUnwatchedEpisode, DateTime.MinValue, true); } private QueryResult<BaseItem> GetResult(IEnumerable<BaseItem> items, int? totalRecordLimit, NextUpQuery query) diff --git a/MediaBrowser.Server.Implementations/Udp/UdpServer.cs b/MediaBrowser.Server.Implementations/Udp/UdpServer.cs index 40c4deb19..32992b9b2 100644 --- a/MediaBrowser.Server.Implementations/Udp/UdpServer.cs +++ b/MediaBrowser.Server.Implementations/Udp/UdpServer.cs @@ -96,20 +96,20 @@ namespace MediaBrowser.Server.Implementations.Udp private async void RespondToV1Message(string endpoint, Encoding encoding) { - var localAddress = _appHost.LocalApiUrl; + var localUrl = await _appHost.GetLocalApiUrl().ConfigureAwait(false); - if (!string.IsNullOrEmpty(localAddress)) + if (!string.IsNullOrEmpty(localUrl)) { // This is how we did the old v1 search, so need to strip off the protocol - var index = localAddress.IndexOf("://", StringComparison.OrdinalIgnoreCase); + var index = localUrl.IndexOf("://", StringComparison.OrdinalIgnoreCase); if (index != -1) { - localAddress = localAddress.Substring(index + 3); + localUrl = localUrl.Substring(index + 3); } // Send a response back with our ip address and port - var response = String.Format("MediaBrowserServer|{0}", localAddress); + var response = String.Format("MediaBrowserServer|{0}", localUrl); await SendAsync(Encoding.UTF8.GetBytes(response), endpoint); } @@ -121,7 +121,7 @@ namespace MediaBrowser.Server.Implementations.Udp private async void RespondToV2Message(string endpoint, Encoding encoding) { - var localUrl = _appHost.LocalApiUrl; + var localUrl = await _appHost.GetLocalApiUrl().ConfigureAwait(false); if (!string.IsNullOrEmpty(localUrl)) { diff --git a/MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs b/MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs index a66884f89..29716d33e 100644 --- a/MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs +++ b/MediaBrowser.Server.Implementations/UserViews/CollectionFolderImageProvider.cs @@ -54,11 +54,6 @@ namespace MediaBrowser.Server.Implementations.UserViews { return series; } - var episodeSeason = episode.Season; - if (episodeSeason != null) - { - return episodeSeason; - } return episode; } diff --git a/MediaBrowser.Server.Implementations/UserViews/DynamicImageProvider.cs b/MediaBrowser.Server.Implementations/UserViews/DynamicImageProvider.cs index 911dbb0cb..ea4da19b2 100644 --- a/MediaBrowser.Server.Implementations/UserViews/DynamicImageProvider.cs +++ b/MediaBrowser.Server.Implementations/UserViews/DynamicImageProvider.cs @@ -86,11 +86,6 @@ namespace MediaBrowser.Server.Implementations.UserViews { return series; } - var episodeSeason = episode.Season; - if (episodeSeason != null) - { - return episodeSeason; - } return episode; } @@ -153,7 +148,8 @@ namespace MediaBrowser.Server.Implementations.UserViews CollectionType.HomeVideos, CollectionType.BoxSets, CollectionType.Playlists, - CollectionType.Photos + CollectionType.Photos, + string.Empty }; return collectionStripViewTypes.Contains(view.ViewType ?? string.Empty); diff --git a/MediaBrowser.Server.Implementations/packages.config b/MediaBrowser.Server.Implementations/packages.config index 66aede029..9ae0a126a 100644 --- a/MediaBrowser.Server.Implementations/packages.config +++ b/MediaBrowser.Server.Implementations/packages.config @@ -1,12 +1,13 @@ <?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="CommonIO" version="1.0.0.9" targetFramework="net45" />
- <package id="Emby.XmlTv" version="1.0.0.48" targetFramework="net45" />
- <package id="ini-parser" version="2.2.4" targetFramework="net45" />
+ <package id="Emby.XmlTv" version="1.0.0.55" targetFramework="net45" />
+ <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.49" targetFramework="net45" />
+ <package id="MediaBrowser.Naming" version="1.0.0.53" 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="SocketHttpListener" version="1.0.0.29" targetFramework="net45" />
+ <package id="SimpleInjector" version="3.2.0" targetFramework="net45" />
+ <package id="SocketHttpListener" version="1.0.0.35" targetFramework="net45" />
</packages>
\ No newline at end of file |
