Luke Pulverenti 12 years ago
parent
commit
e56433a0ef

+ 13 - 4
MediaBrowser.Server.Implementations/MediaBrowser.Server.Implementations.csproj

@@ -82,6 +82,12 @@
     </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
+    <Reference Include="System.Data.SQLite">
+      <HintPath>..\packages\System.Data.SQLite.x86.1.0.86.0\lib\net45\System.Data.SQLite.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Data.SQLite.Linq">
+      <HintPath>..\packages\System.Data.SQLite.x86.1.0.86.0\lib\net45\System.Data.SQLite.Linq.dll</HintPath>
+    </Reference>
     <Reference Include="System.Reactive.Core">
       <HintPath>..\packages\Rx-Core.2.1.30214.0\lib\Net45\System.Reactive.Core.dll</HintPath>
     </Reference>
@@ -91,6 +97,7 @@
     <Reference Include="System.Reactive.Linq">
       <HintPath>..\packages\Rx-Linq.2.1.30214.0\lib\Net45\System.Reactive.Linq.dll</HintPath>
     </Reference>
+    <Reference Include="System.Runtime.Serialization" />
     <Reference Include="System.Web" />
     <Reference Include="Microsoft.CSharp" />
     <Reference Include="System.Data" />
@@ -132,6 +139,8 @@
     <Compile Include="Library\UserManager.cs" />
     <Compile Include="Localization\LocalizationManager.cs" />
     <Compile Include="MediaEncoder\MediaEncoder.cs" />
+    <Compile Include="Persistence\SqliteExtensions.cs" />
+    <Compile Include="Persistence\SqliteRepository.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="Providers\ProviderManager.cs" />
     <Compile Include="ScheduledTasks\ArtistValidationTask.cs" />
@@ -162,10 +171,10 @@
     <Compile Include="Sorting\RevenueComparer.cs" />
     <Compile Include="Sorting\RuntimeComparer.cs" />
     <Compile Include="Sorting\SortNameComparer.cs" />
-    <Compile Include="Persistence\JsonDisplayPreferencesRepository.cs" />
-    <Compile Include="Persistence\JsonItemRepository.cs" />
-    <Compile Include="Persistence\JsonUserDataRepository.cs" />
-    <Compile Include="Persistence\JsonUserRepository.cs" />
+    <Compile Include="Persistence\SqliteDisplayPreferencesRepository.cs" />
+    <Compile Include="Persistence\SqliteItemRepository.cs" />
+    <Compile Include="Persistence\SqliteUserDataRepository.cs" />
+    <Compile Include="Persistence\SqliteUserRepository.cs" />
     <Compile Include="Udp\UdpMessageReceivedEventArgs.cs" />
     <Compile Include="Udp\UdpServer.cs" />
     <Compile Include="Updates\InstallationManager.cs" />

+ 0 - 164
MediaBrowser.Server.Implementations/Persistence/JsonDisplayPreferencesRepository.cs

@@ -1,164 +0,0 @@
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.Serialization;
-using System;
-using System.Collections.Concurrent;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Server.Implementations.Persistence
-{
-    public class JsonDisplayPreferencesRepository : IDisplayPreferencesRepository
-    {
-        private readonly ConcurrentDictionary<string, SemaphoreSlim> _fileLocks = new ConcurrentDictionary<string, SemaphoreSlim>();
-
-        private SemaphoreSlim GetLock(string filename)
-        {
-            return _fileLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
-        }
-
-        /// <summary>
-        /// Gets the name of the repository
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name
-        {
-            get
-            {
-                return "Json";
-            }
-        }
-
-        /// <summary>
-        /// The _json serializer
-        /// </summary>
-        private readonly IJsonSerializer _jsonSerializer;
-
-        private readonly string _dataPath;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="JsonUserDataRepository" /> 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">
-        /// jsonSerializer
-        /// or
-        /// appPaths
-        /// </exception>
-        public JsonDisplayPreferencesRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager)
-        {
-            if (jsonSerializer == null)
-            {
-                throw new ArgumentNullException("jsonSerializer");
-            }
-            if (appPaths == null)
-            {
-                throw new ArgumentNullException("appPaths");
-            }
-
-            _jsonSerializer = jsonSerializer;
-            _dataPath = Path.Combine(appPaths.DataPath, "display-preferences");
-        }
-
-        /// <summary>
-        /// Opens the connection to the database
-        /// </summary>
-        /// <returns>Task.</returns>
-        public Task Initialize()
-        {
-            return Task.FromResult(true);
-        }
-
-        /// <summary>
-        /// Save the display preferences associated with an item in the repo
-        /// </summary>
-        /// <param name="displayPreferences">The display preferences.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        /// <exception cref="System.ArgumentNullException">item</exception>
-        public async Task SaveDisplayPreferences(DisplayPreferences displayPreferences, CancellationToken cancellationToken)
-        {
-            if (displayPreferences == null)
-            {
-                throw new ArgumentNullException("displayPreferences");
-            }
-            if (displayPreferences.Id == Guid.Empty)
-            {
-                throw new ArgumentNullException("displayPreferences.Id");
-            }
-            if (cancellationToken == null)
-            {
-                throw new ArgumentNullException("cancellationToken");
-            }
-
-            cancellationToken.ThrowIfCancellationRequested();
-
-            if (!Directory.Exists(_dataPath))
-            {
-                Directory.CreateDirectory(_dataPath);
-            }
-
-            var path = Path.Combine(_dataPath, displayPreferences.Id + ".json");
-
-            var semaphore = GetLock(path);
-
-            await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
-            try
-            {
-                _jsonSerializer.SerializeToFile(displayPreferences, path);
-            }
-            finally
-            {
-                semaphore.Release();
-            }
-        }
-
-        /// <summary>
-        /// Gets the display preferences.
-        /// </summary>
-        /// <param name="displayPreferencesId">The display preferences id.</param>
-        /// <returns>Task{DisplayPreferences}.</returns>
-        /// <exception cref="System.ArgumentNullException">item</exception>
-        public Task<DisplayPreferences> GetDisplayPreferences(Guid displayPreferencesId)
-        {
-            if (displayPreferencesId == Guid.Empty)
-            {
-                throw new ArgumentNullException("displayPreferencesId");
-            }
-
-            return Task.Run(() =>
-            {
-                var path = Path.Combine(_dataPath, displayPreferencesId + ".json");
-
-                try
-                {
-                    return _jsonSerializer.DeserializeFromFile<DisplayPreferences>(path);
-                }
-                catch (IOException)
-                {
-                    // File doesn't exist or is currently bring written to
-                    return null;
-                }
-            });
-        }
-
-        public void Dispose()
-        {
-            // Wait up to two seconds for any existing writes to finish
-            var locks = _fileLocks.Values.ToList()
-                                  .Where(i => i.CurrentCount == 1)
-                                  .Select(i => i.WaitAsync(2000));
-
-            var task = Task.WhenAll(locks);
-
-            Task.WaitAll(task);
-        }
-    }
-}

+ 0 - 235
MediaBrowser.Server.Implementations/Persistence/JsonItemRepository.cs

@@ -1,235 +0,0 @@
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.IO;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.Serialization;
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Server.Implementations.Persistence
-{
-    public class JsonItemRepository : IItemRepository
-    {
-        private readonly ConcurrentDictionary<string, SemaphoreSlim> _fileLocks = new ConcurrentDictionary<string, SemaphoreSlim>();
-
-        private SemaphoreSlim GetLock(string filename)
-        {
-            return _fileLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
-        }
-        
-        /// <summary>
-        /// Gets the name of the repository
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name
-        {
-            get
-            {
-                return "Json";
-            }
-        }
-
-        /// <summary>
-        /// Gets the json serializer.
-        /// </summary>
-        /// <value>The json serializer.</value>
-        private readonly IJsonSerializer _jsonSerializer;
-
-        private readonly string _criticReviewsPath;
-
-        private readonly FileSystemRepository _itemRepo;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="JsonUserDataRepository" /> 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</exception>
-        public JsonItemRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager)
-        {
-            if (appPaths == null)
-            {
-                throw new ArgumentNullException("appPaths");
-            }
-            if (jsonSerializer == null)
-            {
-                throw new ArgumentNullException("jsonSerializer");
-            }
-
-            _jsonSerializer = jsonSerializer;
-
-            _criticReviewsPath = Path.Combine(appPaths.DataPath, "critic-reviews");
-
-            _itemRepo = new FileSystemRepository(Path.Combine(appPaths.DataPath, "library"));
-        }
-
-        /// <summary>
-        /// Opens the connection to the database
-        /// </summary>
-        /// <returns>Task.</returns>
-        public Task Initialize()
-        {
-            return Task.FromResult(true);
-        }
-
-        /// <summary>
-        /// Save a standard item in the repo
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        /// <exception cref="System.ArgumentNullException">item</exception>
-        public async Task SaveItem(BaseItem item, CancellationToken cancellationToken)
-        {
-            if (item == null)
-            {
-                throw new ArgumentNullException("item");
-            }
-
-            if (!Directory.Exists(_criticReviewsPath))
-            {
-                Directory.CreateDirectory(_criticReviewsPath);
-            }
-
-            var path = _itemRepo.GetResourcePath(item.Id + ".json");
-
-            var parentPath = Path.GetDirectoryName(path);
-            if (!Directory.Exists(parentPath))
-            {
-                Directory.CreateDirectory(parentPath);
-            }
-
-            var semaphore = GetLock(path);
-
-            await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
-            try
-            {
-                _jsonSerializer.SerializeToFile(item, path);
-            }
-            finally
-            {
-                semaphore.Release();
-            }
-        }
-
-        /// <summary>
-        /// Saves the items.
-        /// </summary>
-        /// <param name="items">The items.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        /// <exception cref="System.ArgumentNullException">
-        /// items
-        /// or
-        /// cancellationToken
-        /// </exception>
-        public Task SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken)
-        {
-            if (items == null)
-            {
-                throw new ArgumentNullException("items");
-            }
-
-            if (cancellationToken == null)
-            {
-                throw new ArgumentNullException("cancellationToken");
-            }
-
-            var tasks = items.Select(i => SaveItem(i, cancellationToken));
-
-            return Task.WhenAll(tasks);
-        }
-
-        /// <summary>
-        /// Retrieves the item.
-        /// </summary>
-        /// <param name="id">The id.</param>
-        /// <param name="type">The type.</param>
-        /// <returns>BaseItem.</returns>
-        /// <exception cref="System.ArgumentNullException">id</exception>
-        public BaseItem RetrieveItem(Guid id, Type type)
-        {
-            if (id == Guid.Empty)
-            {
-                throw new ArgumentNullException("id");
-            }
-
-            var path = _itemRepo.GetResourcePath(id + ".json");
-
-            try
-            {
-                return (BaseItem)_jsonSerializer.DeserializeFromFile(type, path);
-            }
-            catch (IOException)
-            {
-                // File doesn't exist or is currently bring written to
-                return null;
-            }
-        }
-
-        /// <summary>
-        /// Gets the critic reviews.
-        /// </summary>
-        /// <param name="itemId">The item id.</param>
-        /// <returns>Task{IEnumerable{ItemReview}}.</returns>
-        public Task<IEnumerable<ItemReview>> GetCriticReviews(Guid itemId)
-        {
-            return Task.Run<IEnumerable<ItemReview>>(() =>
-            {
-                var path = Path.Combine(_criticReviewsPath, itemId + ".json");
-
-                try
-                {
-                    return _jsonSerializer.DeserializeFromFile<List<ItemReview>>(path);
-                }
-                catch (IOException)
-                {
-                    // File doesn't exist or is currently bring written to
-                    return new List<ItemReview>();
-                }
-            });
-        }
-
-        /// <summary>
-        /// Saves the critic reviews.
-        /// </summary>
-        /// <param name="itemId">The item id.</param>
-        /// <param name="criticReviews">The critic reviews.</param>
-        /// <returns>Task.</returns>
-        public Task SaveCriticReviews(Guid itemId, IEnumerable<ItemReview> criticReviews)
-        {
-            return Task.Run(() =>
-            {
-                if (!Directory.Exists(_criticReviewsPath))
-                {
-                    Directory.CreateDirectory(_criticReviewsPath);
-                }
-
-                var path = Path.Combine(_criticReviewsPath, itemId + ".json");
-
-                _jsonSerializer.SerializeToFile(criticReviews.ToList(), path);
-            });
-        }
-
-        public void Dispose()
-        {
-            // Wait up to two seconds for any existing writes to finish
-            var locks = _fileLocks.Values.ToList()
-                                  .Where(i => i.CurrentCount == 1)
-                                  .Select(i => i.WaitAsync(2000));
-
-            var task = Task.WhenAll(locks);
-
-            Task.WaitAll(task);
-        }
-    }
-}

+ 0 - 189
MediaBrowser.Server.Implementations/Persistence/JsonUserRepository.cs

@@ -1,189 +0,0 @@
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Persistence;
-using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.Serialization;
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace MediaBrowser.Server.Implementations.Persistence
-{
-    public class JsonUserRepository : IUserRepository
-    {
-        private readonly ConcurrentDictionary<string, SemaphoreSlim> _fileLocks = new ConcurrentDictionary<string, SemaphoreSlim>();
-
-        private SemaphoreSlim GetLock(string filename)
-        {
-            return _fileLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
-        }
-        
-        /// <summary>
-        /// Gets the name of the repository
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name
-        {
-            get
-            {
-                return "Json";
-            }
-        }
-
-        /// <summary>
-        /// Gets the json serializer.
-        /// </summary>
-        /// <value>The json serializer.</value>
-        private readonly IJsonSerializer _jsonSerializer;
-
-        private readonly string _dataPath;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="JsonUserRepository"/> 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 JsonUserRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager)
-        {
-            if (appPaths == null)
-            {
-                throw new ArgumentNullException("appPaths");
-            }
-            if (jsonSerializer == null)
-            {
-                throw new ArgumentNullException("jsonSerializer");
-            }
-
-            _jsonSerializer = jsonSerializer;
-
-            _dataPath = Path.Combine(appPaths.DataPath, "users");
-        }
-
-        /// <summary>
-        /// Opens the connection to the database
-        /// </summary>
-        /// <returns>Task.</returns>
-        public Task Initialize()
-        {
-            return Task.FromResult(true);
-        }
-
-        /// <summary>
-        /// Save a user in the repo
-        /// </summary>
-        /// <param name="user">The user.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        /// <exception cref="System.ArgumentNullException">user</exception>
-        public async Task SaveUser(User user, CancellationToken cancellationToken)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException("user");
-            }
-
-            if (cancellationToken == null)
-            {
-                throw new ArgumentNullException("cancellationToken");
-            }
-
-            cancellationToken.ThrowIfCancellationRequested();
-
-            if (!Directory.Exists(_dataPath))
-            {
-                Directory.CreateDirectory(_dataPath);
-            }
-
-            var path = Path.Combine(_dataPath, user.Id + ".json");
-
-            var semaphore = GetLock(path);
-
-            await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
-            try
-            {
-                _jsonSerializer.SerializeToFile(user, path);
-            }
-            finally
-            {
-                semaphore.Release();
-            }
-        }
-
-        /// <summary>
-        /// Retrieve all users from the database
-        /// </summary>
-        /// <returns>IEnumerable{User}.</returns>
-        public IEnumerable<User> RetrieveAllUsers()
-        {
-            try
-            {
-                return Directory.EnumerateFiles(_dataPath, "*.json", SearchOption.TopDirectoryOnly)
-                    .Select(i => _jsonSerializer.DeserializeFromFile<User>(i));
-            }
-            catch (IOException)
-            {
-                return new List<User>();
-            }
-        }
-
-        /// <summary>
-        /// Deletes the user.
-        /// </summary>
-        /// <param name="user">The user.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        /// <exception cref="System.ArgumentNullException">user</exception>
-        public async Task DeleteUser(User user, CancellationToken cancellationToken)
-        {
-            if (user == null)
-            {
-                throw new ArgumentNullException("user");
-            }
-
-            if (cancellationToken == null)
-            {
-                throw new ArgumentNullException("cancellationToken");
-            }
-
-            cancellationToken.ThrowIfCancellationRequested();
-
-            var path = Path.Combine(_dataPath, user.Id + ".json");
-
-            var semaphore = GetLock(path);
-
-            await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
-            try
-            {
-                File.Delete(path);
-            }
-            finally
-            {
-                semaphore.Release();
-            }
-        }
-
-        public void Dispose()
-        {
-            // Wait up to two seconds for any existing writes to finish
-            var locks = _fileLocks.Values.ToList()
-                                  .Where(i => i.CurrentCount == 1)
-                                  .Select(i => i.WaitAsync(2000));
-
-            var task = Task.WhenAll(locks);
-
-            Task.WaitAll(task);
-        }
-    }
-}

+ 209 - 0
MediaBrowser.Server.Implementations/Persistence/SqliteDisplayPreferencesRepository.cs

@@ -0,0 +1,209 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Data;
+using System.Data.SQLite;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Persistence
+{
+    /// <summary>
+    /// Class SQLiteDisplayPreferencesRepository
+    /// </summary>
+    public class SqliteDisplayPreferencesRepository : SqliteRepository, IDisplayPreferencesRepository
+    {
+        /// <summary>
+        /// The repository name
+        /// </summary>
+        public const string RepositoryName = "SQLite";
+
+        /// <summary>
+        /// Gets the name of the repository
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name
+        {
+            get
+            {
+                return RepositoryName;
+            }
+        }
+
+        /// <summary>
+        /// The _json serializer
+        /// </summary>
+        private readonly IJsonSerializer _jsonSerializer;
+
+        /// <summary>
+        /// The _app paths
+        /// </summary>
+        private readonly IApplicationPaths _appPaths;
+
+        private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SqliteDisplayPreferencesRepository" /> 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">
+        /// jsonSerializer
+        /// or
+        /// appPaths
+        /// </exception>
+        public SqliteDisplayPreferencesRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager)
+            : base(logManager)
+        {
+            if (jsonSerializer == null)
+            {
+                throw new ArgumentNullException("jsonSerializer");
+            }
+            if (appPaths == null)
+            {
+                throw new ArgumentNullException("appPaths");
+            }
+
+            _jsonSerializer = jsonSerializer;
+            _appPaths = appPaths;
+        }
+
+        /// <summary>
+        /// Opens the connection to the database
+        /// </summary>
+        /// <returns>Task.</returns>
+        public async Task Initialize()
+        {
+            var dbFile = Path.Combine(_appPaths.DataPath, "displaypreferences.db");
+
+            await ConnectToDb(dbFile).ConfigureAwait(false);
+
+            string[] queries = {
+
+                                "create table if not exists displaypreferences (id GUID, data BLOB)",
+                                "create unique index if not exists displaypreferencesindex on displaypreferences (id)",
+                                "create table if not exists schema_version (table_name primary key, version)",
+                                //pragmas
+                                "pragma temp_store = memory"
+                               };
+
+            RunQueries(queries);
+        }
+
+        /// <summary>
+        /// Save the display preferences associated with an item in the repo
+        /// </summary>
+        /// <param name="displayPreferences">The display preferences.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        /// <exception cref="System.ArgumentNullException">item</exception>
+        public async Task SaveDisplayPreferences(DisplayPreferences displayPreferences, CancellationToken cancellationToken)
+        {
+            if (displayPreferences == null)
+            {
+                throw new ArgumentNullException("displayPreferences");
+            }
+            if (displayPreferences.Id == Guid.Empty)
+            {
+                throw new ArgumentNullException("displayPreferences.Id");
+            }
+            if (cancellationToken == null)
+            {
+                throw new ArgumentNullException("cancellationToken");
+            }
+
+            cancellationToken.ThrowIfCancellationRequested();
+
+            var serialized = _jsonSerializer.SerializeToBytes(displayPreferences);
+
+            await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            SQLiteTransaction transaction = null;
+
+            try
+            {
+                transaction = Connection.BeginTransaction();
+
+                using (var cmd = Connection.CreateCommand())
+                {
+                    cmd.CommandText = "replace into displaypreferences (id, data) values (@1, @2)";
+                    cmd.AddParam("@1", displayPreferences.Id);
+                    cmd.AddParam("@2", serialized);
+
+                    cmd.Transaction = transaction;
+
+                    await cmd.ExecuteNonQueryAsync(cancellationToken);
+                }
+
+                transaction.Commit();
+            }
+            catch (OperationCanceledException)
+            {
+                if (transaction != null)
+                {
+                    transaction.Rollback();
+                }
+
+                throw;
+            }
+            catch (Exception e)
+            {
+                Logger.ErrorException("Failed to save display preferences:", e);
+
+                if (transaction != null)
+                {
+                    transaction.Rollback();
+                }
+
+                throw;
+            }
+            finally
+            {
+                if (transaction != null)
+                {
+                    transaction.Dispose();
+                }
+
+                _writeLock.Release();
+            }
+        }
+
+        /// <summary>
+        /// Gets the display preferences.
+        /// </summary>
+        /// <param name="displayPreferencesId">The display preferences id.</param>
+        /// <returns>Task{DisplayPreferences}.</returns>
+        /// <exception cref="System.ArgumentNullException">item</exception>
+        public async Task<DisplayPreferences> GetDisplayPreferences(Guid displayPreferencesId)
+        {
+            if (displayPreferencesId == Guid.Empty)
+            {
+                throw new ArgumentNullException("displayPreferencesId");
+            }
+
+            var cmd = Connection.CreateCommand();
+            cmd.CommandText = "select data from displaypreferences where id = @id";
+
+            var idParam = cmd.Parameters.Add("@id", DbType.Guid);
+            idParam.Value = displayPreferencesId;
+
+            using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow).ConfigureAwait(false))
+            {
+                if (reader.Read())
+                {
+                    using (var stream = GetStream(reader, 0))
+                    {
+                        return _jsonSerializer.DeserializeFromStream<DisplayPreferences>(stream);
+                    }
+                }
+            }
+
+            return null;
+        }
+    }
+}

+ 61 - 0
MediaBrowser.Server.Implementations/Persistence/SqliteExtensions.cs

@@ -0,0 +1,61 @@
+using System;
+using System.Data;
+using System.Data.SQLite;
+
+namespace MediaBrowser.Server.Implementations.Persistence
+{
+    /// <summary>
+    /// Class SQLiteExtensions
+    /// </summary>
+    static class SqliteExtensions
+    {
+        /// <summary>
+        /// Adds the param.
+        /// </summary>
+        /// <param name="cmd">The CMD.</param>
+        /// <param name="param">The param.</param>
+        /// <returns>SQLiteParameter.</returns>
+        /// <exception cref="System.ArgumentNullException"></exception>
+        public static SQLiteParameter AddParam(this SQLiteCommand cmd, string param)
+        {
+            if (string.IsNullOrEmpty(param))
+            {
+                throw new ArgumentNullException();
+            }
+
+            var sqliteParam = new SQLiteParameter(param);
+            cmd.Parameters.Add(sqliteParam);
+            return sqliteParam;
+        }
+
+        /// <summary>
+        /// Adds the param.
+        /// </summary>
+        /// <param name="cmd">The CMD.</param>
+        /// <param name="param">The param.</param>
+        /// <param name="data">The data.</param>
+        /// <returns>SQLiteParameter.</returns>
+        /// <exception cref="System.ArgumentNullException"></exception>
+        public static SQLiteParameter AddParam(this SQLiteCommand cmd, string param, object data)
+        {
+            if (string.IsNullOrEmpty(param))
+            {
+                throw new ArgumentNullException();
+            }
+
+            var sqliteParam = AddParam(cmd, param);
+            sqliteParam.Value = data;
+            return sqliteParam;
+        }
+
+        /// <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 SQLiteConnection conn)
+        {
+            return conn.State == ConnectionState.Open;
+        }
+    }
+}

+ 309 - 0
MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs

@@ -0,0 +1,309 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.SQLite;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Persistence
+{
+    /// <summary>
+    /// Class SQLiteItemRepository
+    /// </summary>
+    public class SqliteItemRepository : SqliteRepository, IItemRepository
+    {
+        /// <summary>
+        /// The repository name
+        /// </summary>
+        public const string RepositoryName = "SQLite";
+
+        /// <summary>
+        /// Gets the name of the repository
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name
+        {
+            get
+            {
+                return RepositoryName;
+            }
+        }
+
+        /// <summary>
+        /// Gets the json serializer.
+        /// </summary>
+        /// <value>The json serializer.</value>
+        private readonly IJsonSerializer _jsonSerializer;
+
+        /// <summary>
+        /// The _app paths
+        /// </summary>
+        private readonly IApplicationPaths _appPaths;
+
+        /// <summary>
+        /// The _save item command
+        /// </summary>
+        private SQLiteCommand _saveItemCommand;
+
+        private readonly string _criticReviewsPath;
+
+        /// <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)
+        {
+            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");
+        }
+
+        /// <summary>
+        /// Opens the connection to the database
+        /// </summary>
+        /// <returns>Task.</returns>
+        public async Task Initialize()
+        {
+            var dbFile = Path.Combine(_appPaths.DataPath, "library.db");
+
+            await ConnectToDb(dbFile).ConfigureAwait(false);
+
+            string[] queries = {
+
+                                "create table if not exists baseitems (guid GUID primary key, data BLOB)",
+                                "create index if not exists idx_baseitems on baseitems(guid)",
+                                "create table if not exists schema_version (table_name primary key, version)",
+                                //pragmas
+                                "pragma temp_store = memory"
+                               };
+
+            RunQueries(queries);
+
+            PrepareStatements();
+        }
+
+        /// <summary>
+        /// The _write lock
+        /// </summary>
+        private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
+
+        /// <summary>
+        /// Prepares the statements.
+        /// </summary>
+        private void PrepareStatements()
+        {
+            _saveItemCommand = new SQLiteCommand
+            {
+                CommandText = "replace into baseitems (guid, data) values (@1, @2)"
+            };
+
+            _saveItemCommand.Parameters.Add(new SQLiteParameter("@1"));
+            _saveItemCommand.Parameters.Add(new SQLiteParameter("@2"));
+        }
+
+        /// <summary>
+        /// Save a standard item in the repo
+        /// </summary>
+        /// <param name="item">The item.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        /// <exception cref="System.ArgumentNullException">item</exception>
+        public Task SaveItem(BaseItem item, CancellationToken cancellationToken)
+        {
+            if (item == null)
+            {
+                throw new ArgumentNullException("item");
+            }
+
+            return SaveItems(new[] { item }, cancellationToken);
+        }
+
+        /// <summary>
+        /// Saves the items.
+        /// </summary>
+        /// <param name="items">The items.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        /// <exception cref="System.ArgumentNullException">
+        /// items
+        /// or
+        /// cancellationToken
+        /// </exception>
+        public async Task SaveItems(IEnumerable<BaseItem> items, CancellationToken cancellationToken)
+        {
+            if (items == null)
+            {
+                throw new ArgumentNullException("items");
+            }
+
+            if (cancellationToken == null)
+            {
+                throw new ArgumentNullException("cancellationToken");
+            }
+
+            cancellationToken.ThrowIfCancellationRequested();
+
+            await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            SQLiteTransaction transaction = null;
+
+            try
+            {
+                transaction = Connection.BeginTransaction();
+
+                foreach (var item in items)
+                {
+                    cancellationToken.ThrowIfCancellationRequested();
+
+                    _saveItemCommand.Parameters[0].Value = item.Id;
+                    _saveItemCommand.Parameters[1].Value = _jsonSerializer.SerializeToBytes(item);
+
+                    _saveItemCommand.Transaction = transaction;
+
+                    await _saveItemCommand.ExecuteNonQueryAsync(cancellationToken);
+                }
+
+                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();
+            }
+        }
+
+        /// <summary>
+        /// Internal retrieve from items or users table
+        /// </summary>
+        /// <param name="id">The id.</param>
+        /// <param name="type">The type.</param>
+        /// <returns>BaseItem.</returns>
+        /// <exception cref="System.ArgumentNullException">id</exception>
+        /// <exception cref="System.ArgumentException"></exception>
+        public BaseItem RetrieveItem(Guid id, Type type)
+        {
+            if (id == Guid.Empty)
+            {
+                throw new ArgumentNullException("id");
+            }
+
+            using (var cmd = Connection.CreateCommand())
+            {
+                cmd.CommandText = "select data from baseitems where guid = @guid";
+                var guidParam = cmd.Parameters.Add("@guid", DbType.Guid);
+                guidParam.Value = id;
+
+                using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow))
+                {
+                    if (reader.Read())
+                    {
+                        using (var stream = GetStream(reader, 0))
+                        {
+                            return _jsonSerializer.DeserializeFromStream(stream, type) as BaseItem;
+                        }
+                    }
+                }
+                return null;
+            }
+        }
+
+        /// <summary>
+        /// Gets the critic reviews.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <returns>Task{IEnumerable{ItemReview}}.</returns>
+        public Task<IEnumerable<ItemReview>> GetCriticReviews(Guid itemId)
+        {
+            return Task.Run<IEnumerable<ItemReview>>(() =>
+            {
+
+                try
+                {
+                    var path = Path.Combine(_criticReviewsPath, itemId + ".json");
+
+                    return _jsonSerializer.DeserializeFromFile<List<ItemReview>>(path);
+                }
+                catch (DirectoryNotFoundException)
+                {
+                    return new List<ItemReview>();
+                }
+                catch (FileNotFoundException)
+                {
+                    return new List<ItemReview>();
+                }
+
+            });
+        }
+
+        /// <summary>
+        /// Saves the critic reviews.
+        /// </summary>
+        /// <param name="itemId">The item id.</param>
+        /// <param name="criticReviews">The critic reviews.</param>
+        /// <returns>Task.</returns>
+        public Task SaveCriticReviews(Guid itemId, IEnumerable<ItemReview> criticReviews)
+        {
+            return Task.Run(() =>
+            {
+                if (!Directory.Exists(_criticReviewsPath))
+                {
+                    Directory.CreateDirectory(_criticReviewsPath);
+                }
+
+                var path = Path.Combine(_criticReviewsPath, itemId + ".json");
+
+                _jsonSerializer.SerializeToFile(criticReviews.ToList(), path);
+            });
+        }
+    }
+}

+ 182 - 0
MediaBrowser.Server.Implementations/Persistence/SqliteRepository.cs

@@ -0,0 +1,182 @@
+using MediaBrowser.Model.Logging;
+using System;
+using System.Data;
+using System.Data.SQLite;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Persistence
+{
+    /// <summary>
+    /// Class SqliteRepository
+    /// </summary>
+    public abstract class SqliteRepository : IDisposable
+    {
+        /// <summary>
+        /// The db file name
+        /// </summary>
+        protected string DbFileName;
+        /// <summary>
+        /// The connection
+        /// </summary>
+        protected SQLiteConnection Connection;
+
+        /// <summary>
+        /// Gets the logger.
+        /// </summary>
+        /// <value>The logger.</value>
+        protected ILogger Logger { get; private set; }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SqliteRepository" /> class.
+        /// </summary>
+        /// <param name="logManager">The log manager.</param>
+        /// <exception cref="System.ArgumentNullException">logger</exception>
+        protected SqliteRepository(ILogManager logManager)
+        {
+            if (logManager == null)
+            {
+                throw new ArgumentNullException("logManager");
+            }
+
+            Logger = logManager.GetLogger(GetType().Name);
+        }
+
+        /// <summary>
+        /// Connects to DB.
+        /// </summary>
+        /// <param name="dbPath">The db path.</param>
+        /// <returns>Task{System.Boolean}.</returns>
+        /// <exception cref="System.ArgumentNullException">dbPath</exception>
+        protected Task ConnectToDb(string dbPath)
+        {
+            if (string.IsNullOrEmpty(dbPath))
+            {
+                throw new ArgumentNullException("dbPath");
+            }
+
+            DbFileName = dbPath;
+            var connectionstr = new SQLiteConnectionStringBuilder
+            {
+                PageSize = 4096,
+                CacheSize = 40960,
+                SyncMode = SynchronizationModes.Off,
+                DataSource = dbPath,
+                JournalMode = SQLiteJournalModeEnum.Wal
+            };
+
+            Connection = new SQLiteConnection(connectionstr.ConnectionString);
+
+            return Connection.OpenAsync();
+        }
+
+        /// <summary>
+        /// Runs the queries.
+        /// </summary>
+        /// <param name="queries">The queries.</param>
+        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
+        /// <exception cref="System.ArgumentNullException">queries</exception>
+        protected void RunQueries(string[] queries)
+        {
+            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>
+        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+        /// </summary>
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+
+        private readonly object _disposeLock = new object();
+
+        /// <summary>
+        /// Releases unmanaged and - optionally - managed resources.
+        /// </summary>
+        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+        protected virtual void Dispose(bool dispose)
+        {
+            if (dispose)
+            {
+                try
+                {
+                    lock (_disposeLock)
+                    {
+                        if (Connection != null)
+                        {
+                            if (Connection.IsOpen())
+                            {
+                                Connection.Close();
+                            }
+
+                            Connection.Dispose();
+                            Connection = null;
+                        }
+                    }
+                }
+                catch (Exception ex)
+                {
+                    Logger.ErrorException("Error disposing database", ex);
+                }
+            }
+        }
+
+        /// <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>
+        protected static Stream GetStream(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;
+        }
+    }
+}

+ 104 - 64
MediaBrowser.Server.Implementations/Persistence/JsonUserDataRepository.cs → MediaBrowser.Server.Implementations/Persistence/SqliteUserDataRepository.cs

@@ -1,29 +1,29 @@
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Logging;
 using MediaBrowser.Model.Serialization;
 using System;
 using System.Collections.Concurrent;
+using System.Data;
+using System.Data.SQLite;
 using System.IO;
-using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 
 namespace MediaBrowser.Server.Implementations.Persistence
 {
-    public class JsonUserDataRepository : IUserDataRepository
+    public class SqliteUserDataRepository : SqliteRepository, IUserDataRepository
     {
-        private readonly ConcurrentDictionary<string, SemaphoreSlim> _fileLocks = new ConcurrentDictionary<string, SemaphoreSlim>();
-
-        private SemaphoreSlim GetLock(string filename)
-        {
-            return _fileLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1));
-        }
-        
         private readonly ConcurrentDictionary<string, UserItemData> _userData = new ConcurrentDictionary<string, UserItemData>();
 
+        private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
+
+        /// <summary>
+        /// The repository name
+        /// </summary>
+        public const string RepositoryName = "SQLite";
+
         /// <summary>
         /// Gets the name of the repository
         /// </summary>
@@ -32,18 +32,19 @@ namespace MediaBrowser.Server.Implementations.Persistence
         {
             get
             {
-                return "Json";
+                return RepositoryName;
             }
         }
 
         private readonly IJsonSerializer _jsonSerializer;
 
-        private readonly string _dataPath;
-
-        private readonly ILogger _logger;
+        /// <summary>
+        /// The _app paths
+        /// </summary>
+        private readonly IApplicationPaths _appPaths;
 
         /// <summary>
-        /// Initializes a new instance of the <see cref="JsonUserDataRepository" /> class.
+        /// Initializes a new instance of the <see cref="SqliteUserDataRepository"/> class.
         /// </summary>
         /// <param name="appPaths">The app paths.</param>
         /// <param name="jsonSerializer">The json serializer.</param>
@@ -53,7 +54,8 @@ namespace MediaBrowser.Server.Implementations.Persistence
         /// or
         /// appPaths
         /// </exception>
-        public JsonUserDataRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager)
+        public SqliteUserDataRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager)
+            : base(logManager)
         {
             if (jsonSerializer == null)
             {
@@ -64,18 +66,30 @@ namespace MediaBrowser.Server.Implementations.Persistence
                 throw new ArgumentNullException("appPaths");
             }
 
-            _logger = logManager.GetLogger(GetType().Name);
             _jsonSerializer = jsonSerializer;
-            _dataPath = Path.Combine(appPaths.DataPath, "userdata");
+            _appPaths = appPaths;
         }
 
         /// <summary>
         /// Opens the connection to the database
         /// </summary>
         /// <returns>Task.</returns>
-        public Task Initialize()
+        public async Task Initialize()
         {
-            return Task.FromResult(true);
+            var dbFile = Path.Combine(_appPaths.DataPath, "userdata.db");
+
+            await ConnectToDb(dbFile).ConfigureAwait(false);
+
+            string[] queries = {
+
+                                "create table if not exists userdata (key nvarchar, userId GUID, data BLOB)",
+                                "create unique index if not exists userdataindex on userdata (key, userId)",
+                                "create table if not exists schema_version (table_name primary key, version)",
+                                //pragmas
+                                "pragma temp_store = memory"
+                               };
+
+            RunQueries(queries);
         }
 
         /// <summary>
@@ -118,12 +132,14 @@ namespace MediaBrowser.Server.Implementations.Persistence
             {
                 await PersistUserData(userId, key, userData, cancellationToken).ConfigureAwait(false);
 
+                var newValue = userData;
+
                 // Once it succeeds, put it into the dictionary to make it available to everyone else
-                _userData.AddOrUpdate(GetInternalKey(userId, key), userData, delegate { return userData; });
+                _userData.AddOrUpdate(GetInternalKey(userId, key), newValue, delegate { return newValue; });
             }
             catch (Exception ex)
             {
-                _logger.ErrorException("Error saving user data", ex);
+                Logger.ErrorException("Error saving user data", ex);
 
                 throw;
             }
@@ -152,25 +168,60 @@ namespace MediaBrowser.Server.Implementations.Persistence
         {
             cancellationToken.ThrowIfCancellationRequested();
 
-            var path = GetUserDataPath(userId, key);
+            var serialized = _jsonSerializer.SerializeToBytes(userData);
 
-            var parentPath = Path.GetDirectoryName(path);
-            if (!Directory.Exists(parentPath))
-            {
-                Directory.CreateDirectory(parentPath);
-            }
+            cancellationToken.ThrowIfCancellationRequested();
 
-            var semaphore = GetLock(path);
+            await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
 
-            await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
+            SQLiteTransaction transaction = null;
 
             try
             {
-                _jsonSerializer.SerializeToFile(userData, path);
+                transaction = Connection.BeginTransaction();
+
+                using (var cmd = Connection.CreateCommand())
+                {
+                    cmd.CommandText = "replace into userdata (key, userId, data) values (@1, @2, @3)";
+                    cmd.AddParam("@1", key);
+                    cmd.AddParam("@2", userId);
+                    cmd.AddParam("@3", serialized);
+
+                    cmd.Transaction = transaction;
+
+                    await cmd.ExecuteNonQueryAsync(cancellationToken);
+                }
+
+                transaction.Commit();
+            }
+            catch (OperationCanceledException)
+            {
+                if (transaction != null)
+                {
+                    transaction.Rollback();
+                }
+
+                throw;
+            }
+            catch (Exception e)
+            {
+                Logger.ErrorException("Failed to save user data:", e);
+
+                if (transaction != null)
+                {
+                    transaction.Rollback();
+                }
+
+                throw;
             }
             finally
             {
-                semaphore.Release();
+                if (transaction != null)
+                {
+                    transaction.Dispose();
+                }
+
+                _writeLock.Release();
             }
         }
 
@@ -207,40 +258,29 @@ namespace MediaBrowser.Server.Implementations.Persistence
         /// <returns>Task{UserItemData}.</returns>
         private UserItemData RetrieveUserData(Guid userId, string key)
         {
-            var path = GetUserDataPath(userId, key);
-
-            try
-            {
-                return _jsonSerializer.DeserializeFromFile<UserItemData>(path);
-            }
-            catch (IOException)
+            using (var cmd = Connection.CreateCommand())
             {
-                // File doesn't exist or is currently bring written to
-                return new UserItemData { UserId = userId };
+                cmd.CommandText = "select data from userdata where key = @key and userId=@userId";
+
+                var idParam = cmd.Parameters.Add("@key", DbType.String);
+                idParam.Value = key;
+
+                var userIdParam = cmd.Parameters.Add("@userId", DbType.Guid);
+                userIdParam.Value = userId;
+
+                using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow))
+                {
+                    if (reader.Read())
+                    {
+                        using (var stream = GetStream(reader, 0))
+                        {
+                            return _jsonSerializer.DeserializeFromStream<UserItemData>(stream);
+                        }
+                    }
+                }
+
+                return new UserItemData();
             }
         }
-
-        private string GetUserDataPath(Guid userId, string key)
-        {
-            var userFolder = Path.Combine(_dataPath, userId.ToString());
-
-            var keyHash = key.GetMD5().ToString();
-
-            var prefix = keyHash.Substring(0, 1);
-
-            return Path.Combine(userFolder, prefix, keyHash + ".json");
-        }
-
-        public void Dispose()
-        {
-            // Wait up to two seconds for any existing writes to finish
-            var locks = _fileLocks.Values.ToList()
-                                  .Where(i => i.CurrentCount == 1)
-                                  .Select(i => i.WaitAsync(2000));
-
-            var task = Task.WhenAll(locks);
-
-            Task.WaitAll(task);
-        }
     }
 }

+ 271 - 0
MediaBrowser.Server.Implementations/Persistence/SqliteUserRepository.cs

@@ -0,0 +1,271 @@
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.SQLite;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace MediaBrowser.Server.Implementations.Persistence
+{
+    /// <summary>
+    /// Class SQLiteUserRepository
+    /// </summary>
+    public class SqliteUserRepository : SqliteRepository, IUserRepository
+    {
+        /// <summary>
+        /// The repository name
+        /// </summary>
+        public const string RepositoryName = "SQLite";
+
+        private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
+
+        /// <summary>
+        /// Gets the name of the repository
+        /// </summary>
+        /// <value>The name.</value>
+        public string Name
+        {
+            get
+            {
+                return RepositoryName;
+            }
+        }
+
+        /// <summary>
+        /// Gets the json serializer.
+        /// </summary>
+        /// <value>The json serializer.</value>
+        private readonly IJsonSerializer _jsonSerializer;
+
+        /// <summary>
+        /// The _app paths
+        /// </summary>
+        private readonly IApplicationPaths _appPaths;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SqliteUserRepository" /> 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</exception>
+        public SqliteUserRepository(IApplicationPaths appPaths, IJsonSerializer jsonSerializer, ILogManager logManager)
+            : base(logManager)
+        {
+            if (appPaths == null)
+            {
+                throw new ArgumentNullException("appPaths");
+            }
+            if (jsonSerializer == null)
+            {
+                throw new ArgumentNullException("jsonSerializer");
+            }
+
+            _appPaths = appPaths;
+            _jsonSerializer = jsonSerializer;
+        }
+
+        /// <summary>
+        /// Opens the connection to the database
+        /// </summary>
+        /// <returns>Task.</returns>
+        public async Task Initialize()
+        {
+            var dbFile = Path.Combine(_appPaths.DataPath, "users.db");
+
+            await ConnectToDb(dbFile).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"
+                               };
+
+            RunQueries(queries);
+        }
+
+        /// <summary>
+        /// Save a user in the repo
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        /// <exception cref="System.ArgumentNullException">user</exception>
+        public async Task SaveUser(User user, CancellationToken cancellationToken)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException("user");
+            }
+
+            if (cancellationToken == null)
+            {
+                throw new ArgumentNullException("cancellationToken");
+            }
+
+            cancellationToken.ThrowIfCancellationRequested();
+
+            var serialized = _jsonSerializer.SerializeToBytes(user);
+
+            cancellationToken.ThrowIfCancellationRequested();
+
+            await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            SQLiteTransaction transaction = null;
+
+            try
+            {
+                transaction = Connection.BeginTransaction();
+
+                using (var cmd = Connection.CreateCommand())
+                {
+                    cmd.CommandText = "replace into users (guid, data) values (@1, @2)";
+                    cmd.AddParam("@1", user.Id);
+                    cmd.AddParam("@2", serialized);
+
+                    cmd.Transaction = transaction;
+
+                    await cmd.ExecuteNonQueryAsync(cancellationToken);
+                }
+
+                transaction.Commit();
+            }
+            catch (OperationCanceledException)
+            {
+                if (transaction != null)
+                {
+                    transaction.Rollback();
+                }
+
+                throw;
+            }
+            catch (Exception e)
+            {
+                Logger.ErrorException("Failed to save user:", e);
+
+                if (transaction != null)
+                {
+                    transaction.Rollback();
+                }
+
+                throw;
+            }
+            finally
+            {
+                if (transaction != null)
+                {
+                    transaction.Dispose();
+                }
+
+                _writeLock.Release();
+            }
+        }
+
+        /// <summary>
+        /// Retrieve all users from the database
+        /// </summary>
+        /// <returns>IEnumerable{User}.</returns>
+        public IEnumerable<User> RetrieveAllUsers()
+        {
+            using (var cmd = Connection.CreateCommand())
+            {
+                cmd.CommandText = "select data from users";
+
+                using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult))
+                {
+                    while (reader.Read())
+                    {
+                        using (var stream = GetStream(reader, 0))
+                        {
+                            var user = _jsonSerializer.DeserializeFromStream<User>(stream);
+                            yield return user;
+                        }
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Deletes the user.
+        /// </summary>
+        /// <param name="user">The user.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        /// <exception cref="System.ArgumentNullException">user</exception>
+        public async Task DeleteUser(User user, CancellationToken cancellationToken)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException("user");
+            }
+
+            if (cancellationToken == null)
+            {
+                throw new ArgumentNullException("cancellationToken");
+            }
+
+            cancellationToken.ThrowIfCancellationRequested();
+
+            await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+            SQLiteTransaction transaction = null;
+
+            try
+            {
+                transaction = Connection.BeginTransaction();
+
+                using (var cmd = Connection.CreateCommand())
+                {
+                    cmd.CommandText = "delete from users where guid=@guid";
+
+                    var guidParam = cmd.Parameters.Add("@guid", DbType.Guid);
+                    guidParam.Value = user.Id;
+
+                    cmd.Transaction = transaction;
+
+                    await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
+                }
+
+                transaction.Commit();
+            }
+            catch (OperationCanceledException)
+            {
+                if (transaction != null)
+                {
+                    transaction.Rollback();
+                }
+
+                throw;
+            }
+            catch (Exception e)
+            {
+                Logger.ErrorException("Failed to delete user:", e);
+
+                if (transaction != null)
+                {
+                    transaction.Rollback();
+                }
+
+                throw;
+            }
+            finally
+            {
+                if (transaction != null)
+                {
+                    transaction.Dispose();
+                }
+
+                _writeLock.Release();
+            }
+        }
+    }
+}

+ 1 - 0
MediaBrowser.Server.Implementations/packages.config

@@ -14,4 +14,5 @@
   <package id="ServiceStack.Redis" version="3.9.43" targetFramework="net45" />
   <package id="ServiceStack.Text" version="3.9.45" targetFramework="net45" />
   <package id="SharpZipLib" version="0.86.0" targetFramework="net45" />
+  <package id="System.Data.SQLite.x86" version="1.0.86.0" targetFramework="net45" />
 </packages>

+ 4 - 4
MediaBrowser.ServerApplication/ApplicationHost.cs

@@ -244,16 +244,16 @@ namespace MediaBrowser.ServerApplication
             ZipClient = new DotNetZipClient();
             RegisterSingleInstance(ZipClient);
 
-            UserDataRepository = new JsonUserDataRepository(ApplicationPaths, JsonSerializer, LogManager);
+            UserDataRepository = new SqliteUserDataRepository(ApplicationPaths, JsonSerializer, LogManager);
             RegisterSingleInstance(UserDataRepository);
 
-            UserRepository = new JsonUserRepository(ApplicationPaths, JsonSerializer, LogManager);
+            UserRepository = new SqliteUserRepository(ApplicationPaths, JsonSerializer, LogManager);
             RegisterSingleInstance(UserRepository);
 
-            DisplayPreferencesRepository = new JsonDisplayPreferencesRepository(ApplicationPaths, JsonSerializer, LogManager);
+            DisplayPreferencesRepository = new SqliteDisplayPreferencesRepository(ApplicationPaths, JsonSerializer, LogManager);
             RegisterSingleInstance(DisplayPreferencesRepository);
 
-            ItemRepository = new JsonItemRepository(ApplicationPaths, JsonSerializer, LogManager);
+            ItemRepository = new SqliteItemRepository(ApplicationPaths, JsonSerializer, LogManager);
             RegisterSingleInstance(ItemRepository);
 
             UserManager = new UserManager(Logger, ServerConfigurationManager);