diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 0af4972f74..bde4d0f457 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -98,7 +98,9 @@ namespace MediaBrowser.Controller.Entities public bool? IsCurrentSchema { get; set; } public bool? HasDeadParentId { get; set; } - + public bool? IsOffline { get; set; } + public LocationType? LocationType { get; set; } + public InternalItemsQuery() { Tags = new string[] { }; diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index a4b9bf1202..8fc0aedd3f 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -169,6 +169,13 @@ namespace MediaBrowser.Controller.Persistence /// The query. /// List<System.String>. List GetPeopleNames(InternalPeopleQuery query); + + /// + /// Gets the item ids with path. + /// + /// The query. + /// QueryResult<Tuple<Guid, System.String>>. + QueryResult> GetItemIdsWithPath(InternalItemsQuery query); } } diff --git a/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs b/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs index 0b45c468a5..22b2c7f335 100644 --- a/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs +++ b/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs @@ -1,15 +1,18 @@ -using MediaBrowser.Common.Progress; +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Progress; using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.Entities.Audio; namespace MediaBrowser.Server.Implementations.Persistence { @@ -19,13 +22,15 @@ namespace MediaBrowser.Server.Implementations.Persistence private readonly IItemRepository _itemRepo; private readonly ILogger _logger; private readonly IServerConfigurationManager _config; + private readonly IFileSystem _fileSystem; - public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IServerConfigurationManager config) + public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem) { _libraryManager = libraryManager; _itemRepo = itemRepo; _logger = logger; _config = config; + _fileSystem = fileSystem; } public string Name @@ -46,15 +51,18 @@ namespace MediaBrowser.Server.Implementations.Persistence public async Task Execute(CancellationToken cancellationToken, IProgress progress) { var innerProgress = new ActionableProgress(); - innerProgress.RegisterAction(p => progress.Report(.95 * p)); + innerProgress.RegisterAction(p => progress.Report(.4 * p)); await UpdateToLatestSchema(cancellationToken, innerProgress).ConfigureAwait(false); innerProgress = new ActionableProgress(); - innerProgress.RegisterAction(p => progress.Report(95 + (.05 * p))); - + innerProgress.RegisterAction(p => progress.Report(40 + (.05 * p))); await CleanDeadItems(cancellationToken, innerProgress).ConfigureAwait(false); + progress.Report(45); + innerProgress = new ActionableProgress(); + innerProgress.RegisterAction(p => progress.Report(45 + (.55 * p))); + await CleanDeletedItems(cancellationToken, innerProgress).ConfigureAwait(false); progress.Report(100); } @@ -153,11 +161,60 @@ namespace MediaBrowser.Server.Implementations.Persistence progress.Report(100); } + private async Task CleanDeletedItems(CancellationToken cancellationToken, IProgress progress) + { + var result = _itemRepo.GetItemIdsWithPath(new InternalItemsQuery + { + IsOffline = false, + LocationType = LocationType.FileSystem, + //Limit = limit, + + // 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 } + }); + + var numComplete = 0; + var numItems = result.Items.Length; + + foreach (var item in result.Items) + { + cancellationToken.ThrowIfCancellationRequested(); + + var path = item.Item2; + + try + { + if (!_fileSystem.FileExists(path) && !_fileSystem.DirectoryExists(path)) + { + var libraryItem = _libraryManager.GetItemById(item.Item1); + + await _libraryManager.DeleteItem(libraryItem, new DeleteOptions + { + DeleteFileLocation = false + }); + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.ErrorException("Error in CleanDeletedItems. File {0}", ex, path); + } + + numComplete++; + double percent = numComplete; + percent /= numItems; + progress.Report(percent * 100); + } + } + public IEnumerable GetDefaultTriggers() { return new ITaskTrigger[] { - new IntervalTrigger{ Interval = TimeSpan.FromDays(1)} + new IntervalTrigger{ Interval = TimeSpan.FromDays(7)} }; } } diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs index bd128a16e7..3c06973b46 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs +++ b/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs @@ -72,7 +72,7 @@ namespace MediaBrowser.Server.Implementations.Persistence private IDbCommand _deletePeopleCommand; private IDbCommand _savePersonCommand; - private const int LatestSchemaVersion = 6; + private const int LatestSchemaVersion = 7; /// /// Initializes a new instance of the class. @@ -175,6 +175,7 @@ namespace MediaBrowser.Server.Implementations.Persistence _connection.AddColumn(_logger, "TypedBaseItems", "ForcedSortName", "Text"); _connection.AddColumn(_logger, "TypedBaseItems", "IsOffline", "BIT"); + _connection.AddColumn(_logger, "TypedBaseItems", "LocationType", "Text"); PrepareStatements(); @@ -245,7 +246,8 @@ namespace MediaBrowser.Server.Implementations.Persistence "DateCreated", "DateModified", "ForcedSortName", - "IsOffline" + "IsOffline", + "LocationType" }; _saveItemCommand = _connection.CreateCommand(); _saveItemCommand.CommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns.ToArray()) + ") values ("; @@ -415,6 +417,7 @@ namespace MediaBrowser.Server.Implementations.Persistence _saveItemCommand.GetParameter(index++).Value = item.ForcedSortName; _saveItemCommand.GetParameter(index++).Value = item.IsOffline; + _saveItemCommand.GetParameter(index++).Value = item.LocationType.ToString(); _saveItemCommand.Transaction = transaction; @@ -617,7 +620,7 @@ namespace MediaBrowser.Server.Implementations.Persistence /// Task. public Task SaveCriticReviews(Guid itemId, IEnumerable criticReviews) { - Directory.CreateDirectory(_criticReviewsPath); + Directory.CreateDirectory(_criticReviewsPath); var path = Path.Combine(_criticReviewsPath, itemId + ".json"); @@ -950,6 +953,75 @@ namespace MediaBrowser.Server.Implementations.Persistence } } + public QueryResult> GetItemIdsWithPath(InternalItemsQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + CheckDisposed(); + + using (var cmd = _connection.CreateCommand()) + { + cmd.CommandText = "select guid,path from TypedBaseItems"; + + var whereClauses = GetWhereClauses(query, cmd, false); + + 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 += GetOrderByText(query); + + if (query.Limit.HasValue) + { + cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(CultureInfo.InvariantCulture); + } + + cmd.CommandText += "; select count (guid) from TypedBaseItems" + whereTextWithoutPaging; + + var list = new List>(); + var count = 0; + + _logger.Debug(cmd.CommandText); + + using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + { + while (reader.Read()) + { + var id = reader.GetGuid(0); + string path = null; + + if (!reader.IsDBNull(1)) + { + path = reader.GetString(1); + } + list.Add(new Tuple(id, path)); + } + + if (reader.NextResult() && reader.Read()) + { + count = reader.GetInt32(0); + } + } + + return new QueryResult>() + { + Items = list.ToArray(), + TotalRecordCount = count + }; + } + } + public QueryResult GetItemIds(InternalItemsQuery query) { if (query == null) @@ -1028,6 +1100,16 @@ namespace MediaBrowser.Server.Implementations.Persistence } cmd.Parameters.Add(cmd, "@SchemaVersion", DbType.Int32).Value = LatestSchemaVersion; } + if (query.IsOffline.HasValue) + { + whereClauses.Add("IsOffline=@IsOffline"); + cmd.Parameters.Add(cmd, "@IsOffline", DbType.Boolean).Value = query.IsOffline; + } + if (query.LocationType.HasValue) + { + whereClauses.Add("LocationType=@LocationType"); + cmd.Parameters.Add(cmd, "@LocationType", DbType.String).Value = query.LocationType.Value; + } if (query.IsMovie.HasValue) { whereClauses.Add("IsMovie=@IsMovie");