using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; using System; using System.Collections.Generic; using System.Data; using System.Globalization; using System.IO; using System.Linq; using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.Persistence { /// /// Class SQLiteItemRepository /// public class SqliteItemRepository : IItemRepository { private IDbConnection _connection; private readonly ILogger _logger; private readonly TypeMapper _typeMapper = new TypeMapper(); /// /// Gets the name of the repository /// /// The name. public string Name { get { return "SQLite"; } } /// /// Gets the json serializer. /// /// The json serializer. private readonly IJsonSerializer _jsonSerializer; /// /// The _app paths /// private readonly IApplicationPaths _appPaths; /// /// The _save item command /// private IDbCommand _saveItemCommand; private readonly string _criticReviewsPath; private SqliteChapterRepository _chapterRepository; private SqliteMediaStreamsRepository _mediaStreamsRepository; private IDbCommand _deleteChildrenCommand; private IDbCommand _saveChildrenCommand; private IDbCommand _deleteItemCommand; private IDbCommand _deletePeopleCommand; private IDbCommand _savePersonCommand; private const int LatestSchemaVersion = 7; /// /// Initializes a new instance of the class. /// /// The app paths. /// The json serializer. /// The log manager. /// /// appPaths /// or /// jsonSerializer /// public SqliteItemRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager) { if (appPaths == null) { throw new ArgumentNullException("appPaths"); } if (jsonSerializer == null) { throw new ArgumentNullException("jsonSerializer"); } _appPaths = appPaths; _jsonSerializer = jsonSerializer; _criticReviewsPath = Path.Combine(_appPaths.DataPath, "critic-reviews"); _logger = logManager.GetLogger(GetType().Name); var chapterDbFile = Path.Combine(_appPaths.DataPath, "chapters.db"); var chapterConnection = SqliteExtensions.ConnectToDb(chapterDbFile, _logger).Result; _chapterRepository = new SqliteChapterRepository(chapterConnection, logManager); var mediaStreamsDbFile = Path.Combine(_appPaths.DataPath, "mediainfo.db"); var mediaStreamsConnection = SqliteExtensions.ConnectToDb(mediaStreamsDbFile, _logger).Result; _mediaStreamsRepository = new SqliteMediaStreamsRepository(mediaStreamsConnection, logManager); } /// /// Opens the connection to the database /// /// Task. public async Task Initialize() { var dbFile = Path.Combine(_appPaths.DataPath, "library.db"); _connection = await SqliteExtensions.ConnectToDb(dbFile, _logger).ConfigureAwait(false); string[] queries = { "create table if not exists TypedBaseItems (guid GUID primary key, type TEXT, data BLOB)", "create index if not exists idx_TypedBaseItems on TypedBaseItems(guid)", "create table if not exists ChildrenIds (ParentId GUID, ItemId GUID, PRIMARY KEY (ParentId, ItemId))", "create index if not exists idx_ChildrenIds on ChildrenIds(ParentId,ItemId)", "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)", //pragmas "pragma temp_store = memory", "pragma shrink_memory" }; _connection.RunQueries(queries, _logger); _connection.AddColumn(_logger, "TypedBaseItems", "Path", "Text"); _connection.AddColumn(_logger, "TypedBaseItems", "StartDate", "DATETIME"); _connection.AddColumn(_logger, "TypedBaseItems", "EndDate", "DATETIME"); _connection.AddColumn(_logger, "TypedBaseItems", "ChannelId", "Text"); _connection.AddColumn(_logger, "TypedBaseItems", "IsMovie", "BIT"); _connection.AddColumn(_logger, "TypedBaseItems", "IsSports", "BIT"); _connection.AddColumn(_logger, "TypedBaseItems", "IsKids", "BIT"); _connection.AddColumn(_logger, "TypedBaseItems", "CommunityRating", "Float"); _connection.AddColumn(_logger, "TypedBaseItems", "CustomRating", "Text"); _connection.AddColumn(_logger, "TypedBaseItems", "IndexNumber", "INT"); _connection.AddColumn(_logger, "TypedBaseItems", "IsLocked", "BIT"); _connection.AddColumn(_logger, "TypedBaseItems", "Name", "Text"); _connection.AddColumn(_logger, "TypedBaseItems", "OfficialRating", "Text"); _connection.AddColumn(_logger, "TypedBaseItems", "MediaType", "Text"); _connection.AddColumn(_logger, "TypedBaseItems", "Overview", "Text"); _connection.AddColumn(_logger, "TypedBaseItems", "ParentIndexNumber", "INT"); _connection.AddColumn(_logger, "TypedBaseItems", "PremiereDate", "DATETIME"); _connection.AddColumn(_logger, "TypedBaseItems", "ProductionYear", "INT"); _connection.AddColumn(_logger, "TypedBaseItems", "ParentId", "GUID"); _connection.AddColumn(_logger, "TypedBaseItems", "Genres", "Text"); _connection.AddColumn(_logger, "TypedBaseItems", "ParentalRatingValue", "INT"); _connection.AddColumn(_logger, "TypedBaseItems", "SchemaVersion", "INT"); _connection.AddColumn(_logger, "TypedBaseItems", "SortName", "Text"); _connection.AddColumn(_logger, "TypedBaseItems", "RunTimeTicks", "BIGINT"); _connection.AddColumn(_logger, "TypedBaseItems", "OfficialRatingDescription", "Text"); _connection.AddColumn(_logger, "TypedBaseItems", "HomePageUrl", "Text"); _connection.AddColumn(_logger, "TypedBaseItems", "VoteCount", "INT"); _connection.AddColumn(_logger, "TypedBaseItems", "DisplayMediaType", "Text"); _connection.AddColumn(_logger, "TypedBaseItems", "DateCreated", "DATETIME"); _connection.AddColumn(_logger, "TypedBaseItems", "DateModified", "DATETIME"); _connection.AddColumn(_logger, "TypedBaseItems", "ForcedSortName", "Text"); _connection.AddColumn(_logger, "TypedBaseItems", "IsOffline", "BIT"); _connection.AddColumn(_logger, "TypedBaseItems", "LocationType", "Text"); PrepareStatements(); _mediaStreamsRepository.Initialize(); _chapterRepository.Initialize(); } /// /// The _write lock /// private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1); private readonly string[] _retriveItemColumns = { "type", "data", "StartDate", "EndDate", "IsOffline", "ChannelId", "IsMovie", "IsSports", "IsKids", "CommunityRating", "CustomRating", "IndexNumber", "IsLocked" }; /// /// Prepares the statements. /// private void PrepareStatements() { var saveColumns = new List { "guid", "type", "data", "Path", "StartDate", "EndDate", "ChannelId", "IsKids", "IsMovie", "IsSports", "CommunityRating", "CustomRating", "IndexNumber", "IsLocked", "Name", "OfficialRating", "MediaType", "Overview", "ParentIndexNumber", "PremiereDate", "ProductionYear", "ParentId", "Genres", "ParentalRatingValue", "SchemaVersion", "SortName", "RunTimeTicks", "OfficialRatingDescription", "HomePageUrl", "VoteCount", "DisplayMediaType", "DateCreated", "DateModified", "ForcedSortName", "IsOffline", "LocationType" }; _saveItemCommand = _connection.CreateCommand(); _saveItemCommand.CommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns.ToArray()) + ") values ("; for (var i = 1; i <= saveColumns.Count; i++) { if (i > 1) { _saveItemCommand.CommandText += ","; } _saveItemCommand.CommandText += "@" + i.ToString(CultureInfo.InvariantCulture); _saveItemCommand.Parameters.Add(_saveItemCommand, "@" + i.ToString(CultureInfo.InvariantCulture)); } _saveItemCommand.CommandText += ")"; _deleteChildrenCommand = _connection.CreateCommand(); _deleteChildrenCommand.CommandText = "delete from ChildrenIds where ParentId=@ParentId"; _deleteChildrenCommand.Parameters.Add(_deleteChildrenCommand, "@ParentId"); _deleteItemCommand = _connection.CreateCommand(); _deleteItemCommand.CommandText = "delete from TypedBaseItems where guid=@Id"; _deleteItemCommand.Parameters.Add(_deleteItemCommand, "@Id"); _saveChildrenCommand = _connection.CreateCommand(); _saveChildrenCommand.CommandText = "replace into ChildrenIds (ParentId, ItemId) values (@ParentId, @ItemId)"; _saveChildrenCommand.Parameters.Add(_saveChildrenCommand, "@ParentId"); _saveChildrenCommand.Parameters.Add(_saveChildrenCommand, "@ItemId"); _deletePeopleCommand = _connection.CreateCommand(); _deletePeopleCommand.CommandText = "delete from People where ItemId=@Id"; _deletePeopleCommand.Parameters.Add(_deletePeopleCommand, "@Id"); _savePersonCommand = _connection.CreateCommand(); _savePersonCommand.CommandText = "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values (@ItemId, @Name, @Role, @PersonType, @SortOrder, @ListOrder)"; _savePersonCommand.Parameters.Add(_savePersonCommand, "@ItemId"); _savePersonCommand.Parameters.Add(_savePersonCommand, "@Name"); _savePersonCommand.Parameters.Add(_savePersonCommand, "@Role"); _savePersonCommand.Parameters.Add(_savePersonCommand, "@PersonType"); _savePersonCommand.Parameters.Add(_savePersonCommand, "@SortOrder"); _savePersonCommand.Parameters.Add(_savePersonCommand, "@ListOrder"); } /// /// Save a standard item in the repo /// /// The item. /// The cancellation token. /// Task. /// item public Task SaveItem(BaseItem item, CancellationToken cancellationToken) { if (item == null) { throw new ArgumentNullException("item"); } return SaveItems(new[] { item }, cancellationToken); } /// /// Saves the items. /// /// The items. /// The cancellation token. /// Task. /// /// items /// or /// cancellationToken /// public async Task SaveItems(IEnumerable items, CancellationToken cancellationToken) { if (items == null) { throw new ArgumentNullException("items"); } cancellationToken.ThrowIfCancellationRequested(); CheckDisposed(); await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); IDbTransaction transaction = null; try { transaction = _connection.BeginTransaction(); foreach (var item in items) { cancellationToken.ThrowIfCancellationRequested(); var index = 0; _saveItemCommand.GetParameter(index++).Value = item.Id; _saveItemCommand.GetParameter(index++).Value = item.GetType().FullName; _saveItemCommand.GetParameter(index++).Value = _jsonSerializer.SerializeToBytes(item); _saveItemCommand.GetParameter(index++).Value = item.Path; var hasStartDate = item as IHasStartDate; if (hasStartDate != null) { _saveItemCommand.GetParameter(index++).Value = hasStartDate.StartDate; } else { _saveItemCommand.GetParameter(index++).Value = null; } _saveItemCommand.GetParameter(index++).Value = item.EndDate; _saveItemCommand.GetParameter(index++).Value = item.ChannelId; var hasProgramAttributes = item as IHasProgramAttributes; if (hasProgramAttributes != null) { _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsKids; _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsMovie; _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsSports; } else { _saveItemCommand.GetParameter(index++).Value = null; _saveItemCommand.GetParameter(index++).Value = null; _saveItemCommand.GetParameter(index++).Value = null; } _saveItemCommand.GetParameter(index++).Value = item.CommunityRating; _saveItemCommand.GetParameter(index++).Value = item.CustomRating; _saveItemCommand.GetParameter(index++).Value = item.IndexNumber; _saveItemCommand.GetParameter(index++).Value = item.IsLocked; _saveItemCommand.GetParameter(index++).Value = item.Name; _saveItemCommand.GetParameter(index++).Value = item.OfficialRating; _saveItemCommand.GetParameter(index++).Value = item.MediaType; _saveItemCommand.GetParameter(index++).Value = item.Overview; _saveItemCommand.GetParameter(index++).Value = item.ParentIndexNumber; _saveItemCommand.GetParameter(index++).Value = item.PremiereDate; _saveItemCommand.GetParameter(index++).Value = item.ProductionYear; if (item.ParentId == Guid.Empty) { _saveItemCommand.GetParameter(index++).Value = null; } else { _saveItemCommand.GetParameter(index++).Value = item.ParentId; } _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.Genres.ToArray()); _saveItemCommand.GetParameter(index++).Value = item.GetParentalRatingValue(); _saveItemCommand.GetParameter(index++).Value = LatestSchemaVersion; _saveItemCommand.GetParameter(index++).Value = item.SortName; _saveItemCommand.GetParameter(index++).Value = item.RunTimeTicks; _saveItemCommand.GetParameter(index++).Value = item.OfficialRatingDescription; _saveItemCommand.GetParameter(index++).Value = item.HomePageUrl; _saveItemCommand.GetParameter(index++).Value = item.VoteCount; _saveItemCommand.GetParameter(index++).Value = item.DisplayMediaType; _saveItemCommand.GetParameter(index++).Value = item.DateCreated; _saveItemCommand.GetParameter(index++).Value = item.DateModified; _saveItemCommand.GetParameter(index++).Value = item.ForcedSortName; _saveItemCommand.GetParameter(index++).Value = item.IsOffline; _saveItemCommand.GetParameter(index++).Value = item.LocationType.ToString(); _saveItemCommand.Transaction = transaction; _saveItemCommand.ExecuteNonQuery(); } transaction.Commit(); } catch (OperationCanceledException) { if (transaction != null) { transaction.Rollback(); } throw; } catch (Exception e) { _logger.ErrorException("Failed to save items:", e); if (transaction != null) { transaction.Rollback(); } throw; } finally { if (transaction != null) { transaction.Dispose(); } _writeLock.Release(); } } /// /// Internal retrieve from items or users table /// /// The id. /// BaseItem. /// id /// public BaseItem RetrieveItem(Guid id) { if (id == Guid.Empty) { throw new ArgumentNullException("id"); } CheckDisposed(); using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems where guid = @guid"; cmd.Parameters.Add(cmd, "@guid", DbType.Guid).Value = id; using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) { if (reader.Read()) { return GetItem(reader); } } return null; } } private BaseItem GetItem(IDataReader reader) { var typeString = reader.GetString(0); var type = _typeMapper.GetType(typeString); if (type == null) { _logger.Debug("Unknown type {0}", typeString); return null; } BaseItem item; using (var stream = reader.GetMemoryStream(1)) { try { item = _jsonSerializer.DeserializeFromStream(stream, type) as BaseItem; if (item == null) { return null; } } catch (SerializationException ex) { _logger.ErrorException("Error deserializing item", ex); return null; } } if (!reader.IsDBNull(2)) { var hasStartDate = item as IHasStartDate; if (hasStartDate != null) { hasStartDate.StartDate = reader.GetDateTime(2).ToUniversalTime(); } } if (!reader.IsDBNull(3)) { item.EndDate = reader.GetDateTime(3).ToUniversalTime(); } if (!reader.IsDBNull(4)) { item.IsOffline = reader.GetBoolean(4); } if (!reader.IsDBNull(5)) { item.ChannelId = reader.GetString(5); } var hasProgramAttributes = item as IHasProgramAttributes; if (hasProgramAttributes != null) { if (!reader.IsDBNull(6)) { hasProgramAttributes.IsMovie = reader.GetBoolean(6); } if (!reader.IsDBNull(7)) { hasProgramAttributes.IsSports = reader.GetBoolean(7); } if (!reader.IsDBNull(8)) { hasProgramAttributes.IsKids = reader.GetBoolean(8); } } if (!reader.IsDBNull(9)) { item.CommunityRating = reader.GetFloat(9); } if (!reader.IsDBNull(10)) { item.CustomRating = reader.GetString(10); } if (!reader.IsDBNull(11)) { item.IndexNumber = reader.GetInt32(11); } if (!reader.IsDBNull(12)) { item.IsLocked = reader.GetBoolean(12); } return item; } /// /// Gets the critic reviews. /// /// The item id. /// Task{IEnumerable{ItemReview}}. public IEnumerable GetCriticReviews(Guid itemId) { try { var path = Path.Combine(_criticReviewsPath, itemId + ".json"); return _jsonSerializer.DeserializeFromFile>(path); } catch (DirectoryNotFoundException) { return new List(); } catch (FileNotFoundException) { return new List(); } } private readonly Task _cachedTask = Task.FromResult(true); /// /// Saves the critic reviews. /// /// The item id. /// The critic reviews. /// Task. public Task SaveCriticReviews(Guid itemId, IEnumerable criticReviews) { Directory.CreateDirectory(_criticReviewsPath); var path = Path.Combine(_criticReviewsPath, itemId + ".json"); _jsonSerializer.SerializeToFile(criticReviews.ToList(), path); return _cachedTask; } /// /// Gets chapters for an item /// /// The id. /// IEnumerable{ChapterInfo}. /// id public IEnumerable GetChapters(Guid id) { CheckDisposed(); return _chapterRepository.GetChapters(id); } /// /// Gets a single chapter for an item /// /// The id. /// The index. /// ChapterInfo. /// id public ChapterInfo GetChapter(Guid id, int index) { CheckDisposed(); return _chapterRepository.GetChapter(id, index); } /// /// Saves the chapters. /// /// The id. /// The chapters. /// The cancellation token. /// Task. /// /// id /// or /// chapters /// or /// cancellationToken /// public Task SaveChapters(Guid id, IEnumerable chapters, CancellationToken cancellationToken) { CheckDisposed(); return _chapterRepository.SaveChapters(id, chapters, cancellationToken); } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private readonly object _disposeLock = new object(); private bool _disposed; private void CheckDisposed() { if (_disposed) { throw new ObjectDisposedException(GetType().Name + " has been disposed and cannot be accessed."); } } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool dispose) { if (dispose) { _disposed = true; try { lock (_disposeLock) { _writeLock.Wait(); if (_connection != null) { if (_connection.IsOpen()) { _connection.Close(); } _connection.Dispose(); _connection = null; } if (_chapterRepository != null) { _chapterRepository.Dispose(); _chapterRepository = null; } if (_mediaStreamsRepository != null) { _mediaStreamsRepository.Dispose(); _mediaStreamsRepository = null; } } } catch (Exception ex) { _logger.ErrorException("Error disposing database", ex); } } } public IEnumerable GetChildren(Guid parentId) { if (parentId == Guid.Empty) { throw new ArgumentNullException("parentId"); } CheckDisposed(); using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "select ItemId from ChildrenIds where ParentId = @ParentId"; cmd.Parameters.Add(cmd, "@ParentId", DbType.Guid).Value = parentId; using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) { while (reader.Read()) { yield return reader.GetGuid(0); } } } } public IEnumerable GetChildrenItems(Guid parentId) { if (parentId == Guid.Empty) { throw new ArgumentNullException("parentId"); } CheckDisposed(); using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems where guid in (select ItemId from ChildrenIds where ParentId = @ParentId)"; cmd.Parameters.Add(cmd, "@ParentId", DbType.Guid).Value = parentId; using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) { while (reader.Read()) { var item = GetItem(reader); if (item != null) { yield return item; } } } } } public IEnumerable GetItemsOfType(Type type) { if (type == null) { throw new ArgumentNullException("type"); } CheckDisposed(); using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems where type = @type"; cmd.Parameters.Add(cmd, "@type", DbType.String).Value = type.FullName; using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) { while (reader.Read()) { var item = GetItem(reader); if (item != null) { yield return item; } } } } } public QueryResult GetItems(InternalItemsQuery query) { if (query == null) { throw new ArgumentNullException("query"); } CheckDisposed(); using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "select " + string.Join(",", _retriveItemColumns) + " 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; _logger.Debug(cmd.CommandText); var list = new List(); var count = 0; using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) { while (reader.Read()) { var item = GetItem(reader); if (item != null) { list.Add(item); } } if (reader.NextResult() && reader.Read()) { count = reader.GetInt32(0); } } return new QueryResult() { Items = list.ToArray(), TotalRecordCount = count }; } } private string GetOrderByText(InternalItemsQuery query) { if (query.SortBy == null || query.SortBy.Length == 0) { return string.Empty; } var sortOrder = query.SortOrder == SortOrder.Descending ? "DESC" : "ASC"; return " ORDER BY " + string.Join(",", query.SortBy.Select(i => MapOrderByField(i) + " " + sortOrder).ToArray()); } private string MapOrderByField(string name) { return name; } public List GetItemIdsList(InternalItemsQuery query) { if (query == null) { throw new ArgumentNullException("query"); } CheckDisposed(); using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "select guid from TypedBaseItems"; var 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); } var list = new List(); _logger.Debug(cmd.CommandText); using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) { while (reader.Read()) { list.Add(reader.GetGuid(0)); } } return list; } } 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) { throw new ArgumentNullException("query"); } CheckDisposed(); using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "select guid 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()) { list.Add(reader.GetGuid(0)); } if (reader.NextResult() && reader.Read()) { count = reader.GetInt32(0); } } return new QueryResult() { Items = list.ToArray(), TotalRecordCount = count }; } } private List GetWhereClauses(InternalItemsQuery query, IDbCommand cmd, bool addPaging) { var whereClauses = new List(); if (query.IsCurrentSchema.HasValue) { if (query.IsCurrentSchema.Value) { whereClauses.Add("(SchemaVersion not null AND SchemaVersion=@SchemaVersion)"); } else { whereClauses.Add("(SchemaVersion is null or SchemaVersion<>@SchemaVersion)"); } 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"); cmd.Parameters.Add(cmd, "@IsMovie", DbType.Boolean).Value = query.IsMovie; } if (query.IsKids.HasValue) { whereClauses.Add("IsKids=@IsKids"); cmd.Parameters.Add(cmd, "@IsKids", DbType.Boolean).Value = query.IsKids; } if (query.IsSports.HasValue) { whereClauses.Add("IsSports=@IsSports"); cmd.Parameters.Add(cmd, "@IsSports", DbType.Boolean).Value = query.IsSports; } 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]; } else if (includeTypes.Length > 1) { var inClause = string.Join(",", includeTypes.Select(i => "'" + i + "'").ToArray()); whereClauses.Add(string.Format("type in ({0})", inClause)); } var excludeTypes = query.ExcludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray(); if (excludeTypes.Length == 1) { whereClauses.Add("type<>@type"); cmd.Parameters.Add(cmd, "@type", DbType.String).Value = excludeTypes[0]; } else if (excludeTypes.Length > 1) { var inClause = string.Join(",", excludeTypes.Select(i => "'" + i + "'").ToArray()); whereClauses.Add(string.Format("type not in ({0})", inClause)); } if (query.ChannelIds.Length == 1) { whereClauses.Add("ChannelId=@ChannelId"); cmd.Parameters.Add(cmd, "@ChannelId", DbType.String).Value = query.ChannelIds[0]; } if (query.ChannelIds.Length > 1) { var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i + "'").ToArray()); whereClauses.Add(string.Format("ChannelId in ({0})", inClause)); } if (query.MinEndDate.HasValue) { whereClauses.Add("EndDate>=@MinEndDate"); cmd.Parameters.Add(cmd, "@MinEndDate", DbType.Date).Value = query.MinEndDate.Value; } if (query.MaxEndDate.HasValue) { whereClauses.Add("EndDate<=@MaxEndDate"); cmd.Parameters.Add(cmd, "@MaxEndDate", DbType.Date).Value = query.MaxEndDate.Value; } if (query.MinStartDate.HasValue) { whereClauses.Add("StartDate>=@MinStartDate"); cmd.Parameters.Add(cmd, "@MinStartDate", DbType.Date).Value = query.MinStartDate.Value; } if (query.MaxStartDate.HasValue) { whereClauses.Add("StartDate<=@MaxStartDate"); cmd.Parameters.Add(cmd, "@MaxStartDate", DbType.Date).Value = query.MaxStartDate.Value; } if (query.IsAiring.HasValue) { if (query.IsAiring.Value) { whereClauses.Add("StartDate<=@MaxStartDate"); cmd.Parameters.Add(cmd, "@MaxStartDate", DbType.Date).Value = DateTime.UtcNow; whereClauses.Add("EndDate>=@MinEndDate"); cmd.Parameters.Add(cmd, "@MinEndDate", DbType.Date).Value = DateTime.UtcNow; } else { whereClauses.Add("(StartDate>@IsAiringDate OR EndDate < @IsAiringDate)"); cmd.Parameters.Add(cmd, "@IsAiringDate", DbType.Date).Value = DateTime.UtcNow; } } 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.NameContains)) { whereClauses.Add("Name like @NameContains"); cmd.Parameters.Add(cmd, "@NameContains", DbType.String).Value = "%" + query.NameContains + "%"; } if (query.Genres.Length > 0) { var genres = new List(); var index = 0; foreach (var genre in query.Genres) { genres.Add("Genres like @Genres" + index); cmd.Parameters.Add(cmd, "@Genres" + index, DbType.String).Value = "%" + genre + "%"; index++; } var genreCaluse = "(" + string.Join(" OR ", genres.ToArray()) + ")"; whereClauses.Add(genreCaluse); } if (query.MaxParentalRating.HasValue) { whereClauses.Add("(ParentalRatingValue is NULL OR ParentalRatingValue<=@MaxParentalRating)"); cmd.Parameters.Add(cmd, "@MaxParentalRating", DbType.Int32).Value = query.MaxParentalRating.Value; } if (query.HasParentalRating.HasValue) { if (query.HasParentalRating.Value) { whereClauses.Add("ParentalRatingValue NOT NULL"); } else { whereClauses.Add("ParentalRatingValue IS NULL"); } } if (query.HasDeadParentId.HasValue) { if (query.HasDeadParentId.Value) { whereClauses.Add("ParentId NOT NULL AND ParentId NOT IN (select guid from TypedBaseItems)"); } } if (addPaging) { if (query.StartIndex.HasValue && query.StartIndex.Value > 0) { var pagingWhereText = whereClauses.Count == 0 ? string.Empty : " where " + string.Join(" AND ", whereClauses.ToArray()); var orderBy = GetOrderByText(query); whereClauses.Add(string.Format("guid NOT IN (SELECT guid FROM TypedBaseItems {0}" + orderBy + " LIMIT {1})", pagingWhereText, query.StartIndex.Value.ToString(CultureInfo.InvariantCulture))); } } return whereClauses; } private static readonly Type[] KnownTypes = { typeof(LiveTvProgram), typeof(LiveTvChannel), typeof(LiveTvVideoRecording), typeof(LiveTvAudioRecording), typeof(Series), typeof(LiveTvAudioRecording), typeof(LiveTvVideoRecording), typeof(Audio), typeof(MusicAlbum), typeof(MusicArtist), typeof(MusicGenre), typeof(MusicVideo), typeof(Movie), typeof(BoxSet), typeof(Episode), typeof(Season), typeof(Series), typeof(Book), typeof(CollectionFolder), typeof(Folder), typeof(Game), typeof(GameGenre), typeof(GameSystem), typeof(Genre), typeof(Person), typeof(Photo), typeof(PhotoAlbum), typeof(Studio), typeof(UserRootFolder), typeof(UserView), typeof(Video), typeof(Year) }; private static Dictionary GetTypeMapDictionary() { var dict = new Dictionary(); foreach (var t in KnownTypes) { dict[t.Name] = new[] { t.FullName }; } dict["Recording"] = new[] { typeof(LiveTvAudioRecording).FullName, typeof(LiveTvVideoRecording).FullName }; dict["Program"] = new[] { typeof(LiveTvProgram).FullName }; dict["TvChannel"] = new[] { typeof(LiveTvChannel).FullName }; return dict; } // Not crazy about having this all the way down here, but at least it's in one place readonly Dictionary _types = GetTypeMapDictionary(); private IEnumerable MapIncludeItemTypes(string value) { string[] result; if (_types.TryGetValue(value, out result)) { return result; } return new[] { value }; } public async Task DeleteItem(Guid id, CancellationToken cancellationToken) { if (id == Guid.Empty) { throw new ArgumentNullException("id"); } CheckDisposed(); await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); IDbTransaction transaction = null; try { transaction = _connection.BeginTransaction(); // First delete children _deleteChildrenCommand.GetParameter(0).Value = id; _deleteChildrenCommand.Transaction = transaction; _deleteChildrenCommand.ExecuteNonQuery(); // Delete people _deletePeopleCommand.GetParameter(0).Value = id; _deletePeopleCommand.Transaction = transaction; _deletePeopleCommand.ExecuteNonQuery(); // Delete the item _deleteItemCommand.GetParameter(0).Value = id; _deleteItemCommand.Transaction = transaction; _deleteItemCommand.ExecuteNonQuery(); transaction.Commit(); } catch (OperationCanceledException) { if (transaction != null) { transaction.Rollback(); } throw; } catch (Exception e) { _logger.ErrorException("Failed to save children:", e); if (transaction != null) { transaction.Rollback(); } throw; } finally { if (transaction != null) { transaction.Dispose(); } _writeLock.Release(); } } public async Task SaveChildren(Guid parentId, IEnumerable children, CancellationToken cancellationToken) { if (parentId == Guid.Empty) { throw new ArgumentNullException("parentId"); } if (children == null) { throw new ArgumentNullException("children"); } CheckDisposed(); await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); IDbTransaction transaction = null; try { transaction = _connection.BeginTransaction(); // First delete _deleteChildrenCommand.GetParameter(0).Value = parentId; _deleteChildrenCommand.Transaction = transaction; _deleteChildrenCommand.ExecuteNonQuery(); foreach (var id in children) { cancellationToken.ThrowIfCancellationRequested(); _saveChildrenCommand.GetParameter(0).Value = parentId; _saveChildrenCommand.GetParameter(1).Value = id; _saveChildrenCommand.Transaction = transaction; _saveChildrenCommand.ExecuteNonQuery(); } transaction.Commit(); } catch (OperationCanceledException) { if (transaction != null) { transaction.Rollback(); } throw; } catch (Exception e) { _logger.ErrorException("Failed to save children:", e); if (transaction != null) { transaction.Rollback(); } throw; } finally { if (transaction != null) { transaction.Dispose(); } _writeLock.Release(); } } public IEnumerable GetMediaStreams(MediaStreamQuery query) { CheckDisposed(); return _mediaStreamsRepository.GetMediaStreams(query); } public Task SaveMediaStreams(Guid id, IEnumerable streams, CancellationToken cancellationToken) { CheckDisposed(); return _mediaStreamsRepository.SaveMediaStreams(id, streams, cancellationToken); } public List GetPeopleNames(InternalPeopleQuery query) { if (query == null) { throw new ArgumentNullException("query"); } CheckDisposed(); using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "select Distinct Name from People"; var whereClauses = GetPeopleWhereClauses(query, cmd); if (whereClauses.Count > 0) { cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); } cmd.CommandText += " order by ListOrder"; var list = new List(); using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) { while (reader.Read()) { list.Add(reader.GetString(0)); } } return list; } } public List GetPeople(InternalPeopleQuery query) { if (query == null) { throw new ArgumentNullException("query"); } CheckDisposed(); using (var cmd = _connection.CreateCommand()) { cmd.CommandText = "select ItemId, Name, Role, PersonType, SortOrder from People"; var whereClauses = GetPeopleWhereClauses(query, cmd); if (whereClauses.Count > 0) { cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); } cmd.CommandText += " order by ListOrder"; var list = new List(); using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) { while (reader.Read()) { list.Add(GetPerson(reader)); } } return list; } } private List GetPeopleWhereClauses(InternalPeopleQuery query, IDbCommand cmd) { var whereClauses = new List(); if (query.ItemId != Guid.Empty) { whereClauses.Add("ItemId=@ItemId"); cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = query.ItemId; } if (query.AppearsInItemId != Guid.Empty) { whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)"); cmd.Parameters.Add(cmd, "@AppearsInItemId", DbType.Guid).Value = query.AppearsInItemId; } if (query.PersonTypes.Count == 1) { whereClauses.Add("PersonType=@PersonType"); cmd.Parameters.Add(cmd, "@PersonType", DbType.String).Value = query.PersonTypes[0]; } if (query.PersonTypes.Count > 1) { var val = string.Join(",", query.PersonTypes.Select(i => "'" + i + "'").ToArray()); whereClauses.Add("PersonType in (" + val + ")"); } if (query.ExcludePersonTypes.Count == 1) { whereClauses.Add("PersonType<>@PersonType"); cmd.Parameters.Add(cmd, "@PersonType", DbType.String).Value = query.ExcludePersonTypes[0]; } if (query.ExcludePersonTypes.Count > 1) { var val = string.Join(",", query.ExcludePersonTypes.Select(i => "'" + i + "'").ToArray()); whereClauses.Add("PersonType not in (" + val + ")"); } if (query.MaxListOrder.HasValue) { whereClauses.Add("ListOrder<=@MaxListOrder"); cmd.Parameters.Add(cmd, "@MaxListOrder", DbType.Int32).Value = query.MaxListOrder.Value; } if (!string.IsNullOrWhiteSpace(query.NameContains)) { whereClauses.Add("Name like @NameContains"); cmd.Parameters.Add(cmd, "@NameContains", DbType.String).Value = "%" + query.NameContains + "%"; } return whereClauses; } public async Task UpdatePeople(Guid itemId, List people) { if (itemId == Guid.Empty) { throw new ArgumentNullException("itemId"); } if (people == null) { throw new ArgumentNullException("people"); } CheckDisposed(); var cancellationToken = CancellationToken.None; await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); IDbTransaction transaction = null; try { transaction = _connection.BeginTransaction(); // First delete _deletePeopleCommand.GetParameter(0).Value = itemId; _deletePeopleCommand.Transaction = transaction; _deletePeopleCommand.ExecuteNonQuery(); var listIndex = 0; foreach (var person in people) { cancellationToken.ThrowIfCancellationRequested(); _savePersonCommand.GetParameter(0).Value = itemId; _savePersonCommand.GetParameter(1).Value = person.Name; _savePersonCommand.GetParameter(2).Value = person.Role; _savePersonCommand.GetParameter(3).Value = person.Type; _savePersonCommand.GetParameter(4).Value = person.SortOrder; _savePersonCommand.GetParameter(5).Value = listIndex; _savePersonCommand.Transaction = transaction; _savePersonCommand.ExecuteNonQuery(); listIndex++; } transaction.Commit(); } catch (OperationCanceledException) { if (transaction != null) { transaction.Rollback(); } throw; } catch (Exception e) { _logger.ErrorException("Failed to save people:", e); if (transaction != null) { transaction.Rollback(); } throw; } finally { if (transaction != null) { transaction.Dispose(); } _writeLock.Release(); } } private PersonInfo GetPerson(IDataReader reader) { var item = new PersonInfo(); item.ItemId = reader.GetGuid(0); item.Name = reader.GetString(1); if (!reader.IsDBNull(2)) { item.Role = reader.GetString(2); } if (!reader.IsDBNull(3)) { item.Type = reader.GetString(3); } if (!reader.IsDBNull(4)) { item.SortOrder = reader.GetInt32(4); } return item; } } }