浏览代码

Merge pull request #2952 from MediaBrowser/dev

Dev
Luke 7 年之前
父节点
当前提交
c8244f4687
共有 34 个文件被更改,包括 618 次插入461 次删除
  1. 1 15
      Emby.Photos/PhotoProvider.cs
  2. 10 3
      Emby.Server.Implementations/ApplicationHost.cs
  3. 1 1
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  4. 15 12
      Emby.Server.Implementations/Devices/DeviceManager.cs
  5. 0 212
      Emby.Server.Implementations/Devices/DeviceRepository.cs
  6. 451 0
      Emby.Server.Implementations/Devices/SqliteDeviceRepository.cs
  7. 1 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  8. 2 0
      Emby.Server.Implementations/HttpClientManager/HttpClientInfo.cs
  9. 5 11
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  10. 6 7
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHttpStream.cs
  11. 3 4
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
  12. 26 8
      Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs
  13. 6 6
      Emby.Server.Implementations/Session/SessionManager.cs
  14. 2 6
      MediaBrowser.Api/Devices/DeviceService.cs
  15. 2 2
      MediaBrowser.Api/LiveTv/LiveTvService.cs
  16. 5 8
      MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs
  17. 4 9
      MediaBrowser.Controller/Devices/IDeviceManager.cs
  18. 4 9
      MediaBrowser.Controller/Devices/IDeviceRepository.cs
  19. 0 6
      MediaBrowser.Controller/Entities/AudioBook.cs
  20. 0 6
      MediaBrowser.Controller/Entities/Book.cs
  21. 0 6
      MediaBrowser.Controller/Entities/Game.cs
  22. 2 1
      MediaBrowser.Controller/LiveTv/LiveTvChannel.cs
  23. 0 1
      MediaBrowser.Controller/MediaBrowser.Controller.csproj
  24. 0 17
      MediaBrowser.Controller/Providers/IHasChangeMonitor.cs
  25. 1 11
      MediaBrowser.Model/Devices/DeviceInfo.cs
  26. 13 16
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  27. 2 2
      MediaBrowser.Model/Dlna/StreamInfo.cs
  28. 47 19
      MediaBrowser.Providers/Manager/MetadataService.cs
  29. 1 15
      MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
  30. 0 27
      MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
  31. 1 15
      MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
  32. 6 0
      MediaBrowser.Providers/Movies/MovieDbSearch.cs
  33. 0 4
      MediaBrowser.Providers/TV/MissingEpisodeProvider.cs
  34. 1 1
      SharedVersion.cs

+ 1 - 15
Emby.Photos/PhotoProvider.cs

@@ -17,7 +17,7 @@ using TagLib.IFD.Tags;
 
 namespace Emby.Photos
 {
-    public class PhotoProvider : ICustomMetadataProvider<Photo>, IHasItemChangeMonitor, IForcedProvider
+    public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider
     {
         private readonly ILogger _logger;
         private readonly IFileSystem _fileSystem;
@@ -177,19 +177,5 @@ namespace Emby.Photos
         {
             get { return "Embedded Information"; }
         }
-
-        public bool HasChanged(IHasMetadata item, IDirectoryService directoryService)
-        {
-            if (item.EnableRefreshOnDateModifiedChange && !string.IsNullOrWhiteSpace(item.Path) && item.LocationType == LocationType.FileSystem)
-            {
-                var file = directoryService.GetFile(item.Path);
-                if (file != null && file.LastWriteTimeUtc != item.DateModified)
-                {
-                    return true;
-                }
-            }
-
-            return false;
-        }
     }
 }

+ 10 - 3
Emby.Server.Implementations/ApplicationHost.cs

@@ -792,6 +792,11 @@ namespace Emby.Server.Implementations
 
         protected abstract IConnectManager CreateConnectManager();
         protected abstract ISyncManager CreateSyncManager();
+        
+        protected virtual IHttpClient CreateHttpClient()
+        {
+            return new HttpClientManager.HttpClientManager(ApplicationPaths, LogManager.GetLogger("HttpClient"), FileSystemManager, MemoryStreamFactory, GetDefaultUserAgent);
+        }
 
         /// <summary>
         /// Registers resources that classes will depend on
@@ -814,7 +819,7 @@ namespace Emby.Server.Implementations
 
             RegisterSingleInstance(FileSystemManager);
 
-            HttpClient = new HttpClientManager.HttpClientManager(ApplicationPaths, LogManager.GetLogger("HttpClient"), FileSystemManager, MemoryStreamFactory, GetDefaultUserAgent);
+            HttpClient = CreateHttpClient();
             RegisterSingleInstance(HttpClient);
 
             RegisterSingleInstance(NetworkManager);
@@ -938,7 +943,9 @@ namespace Emby.Server.Implementations
             ConnectManager = CreateConnectManager();
             RegisterSingleInstance(ConnectManager);
 
-            DeviceManager = new DeviceManager(new DeviceRepository(ApplicationPaths, JsonSerializer, LogManager.GetLogger("DeviceManager"), FileSystemManager), UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager, LogManager.GetLogger("DeviceManager"), NetworkManager);
+            var deviceRepo = new SqliteDeviceRepository(LogManager.GetLogger("DeviceManager"), ServerConfigurationManager, FileSystemManager, JsonSerializer);
+            deviceRepo.Initialize();
+            DeviceManager = new DeviceManager(deviceRepo, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager, LogManager.GetLogger("DeviceManager"), NetworkManager);
             RegisterSingleInstance(DeviceManager);
 
             var newsService = new Emby.Server.Implementations.News.NewsService(ApplicationPaths, JsonSerializer);
@@ -1116,7 +1123,7 @@ namespace Emby.Server.Implementations
             IsoManager.AddParts(list);
         }
 
-        private string GetDefaultUserAgent()
+        protected string GetDefaultUserAgent()
         {
             var name = FormatAttribute(Name);
 

+ 1 - 1
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -3038,8 +3038,8 @@ namespace Emby.Server.Implementations.Data
             {
                 if (orderBy.Count == 0)
                 {
-                    orderBy.Add(new Tuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending));
                     orderBy.Add(new Tuple<string, SortOrder>("SimilarityScore", SortOrder.Descending));
+                    orderBy.Add(new Tuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending));
                     //orderBy.Add(new Tuple<string, SortOrder>(ItemSortBy.Random, SortOrder.Ascending));
                 }
             }

+ 15 - 12
Emby.Server.Implementations/Devices/DeviceManager.cs

@@ -50,7 +50,7 @@ namespace Emby.Server.Implementations.Devices
             _network = network;
         }
 
-        public async Task<DeviceInfo> RegisterDevice(string reportedId, string name, string appName, string appVersion, string usedByUserId)
+        public DeviceInfo RegisterDevice(string reportedId, string name, string appName, string appVersion, string usedByUserId)
         {
             if (string.IsNullOrWhiteSpace(reportedId))
             {
@@ -76,14 +76,16 @@ namespace Emby.Server.Implementations.Devices
 
             device.DateLastModified = DateTime.UtcNow;
 
-            await _repo.SaveDevice(device).ConfigureAwait(false);
+            device.Name = string.IsNullOrWhiteSpace(device.CustomName) ? device.ReportedName : device.CustomName;
+
+            _repo.SaveDevice(device);
 
             return device;
         }
 
-        public Task SaveCapabilities(string reportedId, ClientCapabilities capabilities)
+        public void SaveCapabilities(string reportedId, ClientCapabilities capabilities)
         {
-            return _repo.SaveCapabilities(reportedId, capabilities);
+            _repo.SaveCapabilities(reportedId, capabilities);
         }
 
         public ClientCapabilities GetCapabilities(string reportedId)
@@ -98,13 +100,13 @@ namespace Emby.Server.Implementations.Devices
 
         public QueryResult<DeviceInfo> GetDevices(DeviceQuery query)
         {
-            IEnumerable<DeviceInfo> devices = _repo.GetDevices().OrderByDescending(i => i.DateLastModified);
+            IEnumerable<DeviceInfo> devices = _repo.GetDevices();
 
             if (query.SupportsSync.HasValue)
             {
                 var val = query.SupportsSync.Value;
 
-                devices = devices.Where(i => GetCapabilities(i.Id).SupportsSync == val);
+                devices = devices.Where(i => i.Capabilities.SupportsSync == val);
             }
 
             if (query.SupportsPersistentIdentifier.HasValue)
@@ -113,8 +115,7 @@ namespace Emby.Server.Implementations.Devices
 
                 devices = devices.Where(i =>
                 {
-                    var caps = GetCapabilities(i.Id);
-                    var deviceVal = caps.SupportsPersistentIdentifier;
+                    var deviceVal = i.Capabilities.SupportsPersistentIdentifier;
                     return deviceVal == val;
                 });
             }
@@ -132,9 +133,9 @@ namespace Emby.Server.Implementations.Devices
             };
         }
 
-        public Task DeleteDevice(string id)
+        public void DeleteDevice(string id)
         {
-            return _repo.DeleteDevice(id);
+            _repo.DeleteDevice(id);
         }
 
         public ContentUploadHistory GetCameraUploadHistory(string deviceId)
@@ -213,14 +214,16 @@ namespace Emby.Server.Implementations.Devices
             get { return Path.Combine(_config.CommonApplicationPaths.DataPath, "camerauploads"); }
         }
 
-        public async Task UpdateDeviceInfo(string id, DeviceOptions options)
+        public void UpdateDeviceInfo(string id, DeviceOptions options)
         {
             var device = GetDevice(id);
 
             device.CustomName = options.CustomName;
             device.CameraUploadPath = options.CameraUploadPath;
 
-            await _repo.SaveDevice(device).ConfigureAwait(false);
+            device.Name = string.IsNullOrWhiteSpace(device.CustomName) ? device.ReportedName : device.CustomName;
+
+            _repo.SaveDevice(device);
 
             EventHelper.FireEventIfNotNull(DeviceOptionsUpdated, this, new GenericEventArgs<DeviceInfo>(device), _logger);
         }

+ 0 - 212
Emby.Server.Implementations/Devices/DeviceRepository.cs

@@ -1,212 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Devices;
-using MediaBrowser.Model.Devices;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Logging;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Session;
-using MediaBrowser.Model.Extensions;
-
-namespace Emby.Server.Implementations.Devices
-{
-    public class DeviceRepository : IDeviceRepository
-    {
-        private readonly object _syncLock = new object();
-
-        private readonly IApplicationPaths _appPaths;
-        private readonly IJsonSerializer _json;
-        private readonly ILogger _logger;
-        private readonly IFileSystem _fileSystem;
-
-        private Dictionary<string, DeviceInfo> _devices;
-
-        public DeviceRepository(IApplicationPaths appPaths, IJsonSerializer json, ILogger logger, IFileSystem fileSystem)
-        {
-            _appPaths = appPaths;
-            _json = json;
-            _logger = logger;
-            _fileSystem = fileSystem;
-        }
-
-        private string GetDevicesPath()
-        {
-            return Path.Combine(_appPaths.DataPath, "devices");
-        }
-
-        private string GetDevicePath(string id)
-        {
-            return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N"));
-        }
-
-        public Task SaveDevice(DeviceInfo device)
-        {
-            var path = Path.Combine(GetDevicePath(device.Id), "device.json");
-            _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path));
-
-            lock (_syncLock)
-            {
-                _json.SerializeToFile(device, path);
-                _devices[device.Id] = device;
-            }
-            return Task.FromResult(true);
-        }
-
-        public Task SaveCapabilities(string reportedId, ClientCapabilities capabilities)
-        {
-            var device = GetDevice(reportedId);
-
-            if (device == null)
-            {
-                throw new ArgumentException("No device has been registed with id " + reportedId);
-            }
-
-            device.Capabilities = capabilities;
-            SaveDevice(device);
-
-            return Task.FromResult(true);
-        }
-
-        public ClientCapabilities GetCapabilities(string reportedId)
-        {
-            var device = GetDevice(reportedId);
-
-            return device == null ? null : device.Capabilities;
-        }
-
-        public DeviceInfo GetDevice(string id)
-        {
-            if (string.IsNullOrWhiteSpace(id))
-            {
-                throw new ArgumentNullException("id");
-            }
-
-            return GetDevices()
-                .FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase));
-        }
-
-        public IEnumerable<DeviceInfo> GetDevices()
-        {
-            lock (_syncLock)
-            {
-                if (_devices == null)
-                {
-                    _devices = new Dictionary<string, DeviceInfo>(StringComparer.OrdinalIgnoreCase);
-
-                    var devices = LoadDevices().ToList();
-                    foreach (var device in devices)
-                    {
-                        _devices[device.Id] = device;
-                    }
-                }
-                return _devices.Values.ToList();
-            }
-        }
-
-        private IEnumerable<DeviceInfo> LoadDevices()
-        {
-            var path = GetDevicesPath();
-
-            try
-            {
-                return _fileSystem
-                    .GetFilePaths(path, true)
-                    .Where(i => string.Equals(Path.GetFileName(i), "device.json", StringComparison.OrdinalIgnoreCase))
-                    .ToList()
-                    .Select(i =>
-                    {
-                        try
-                        {
-                            return _json.DeserializeFromFile<DeviceInfo>(i);
-                        }
-                        catch (Exception ex)
-                        {
-                            _logger.ErrorException("Error reading {0}", ex, i);
-                            return null;
-                        }
-                    })
-                    .Where(i => i != null);
-            }
-            catch (IOException)
-            {
-                return new List<DeviceInfo>();
-            }
-        }
-
-        public Task DeleteDevice(string id)
-        {
-            var path = GetDevicePath(id);
-
-            lock (_syncLock)
-            {
-                try
-                {
-                    _fileSystem.DeleteDirectory(path, true);
-                }
-                catch (IOException)
-                {
-                }
-
-                _devices = null;
-            }
-
-            return Task.FromResult(true);
-        }
-
-        public ContentUploadHistory GetCameraUploadHistory(string deviceId)
-        {
-            var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json");
-
-            lock (_syncLock)
-            {
-                try
-                {
-                    return _json.DeserializeFromFile<ContentUploadHistory>(path);
-                }
-                catch (IOException)
-                {
-                    return new ContentUploadHistory
-                    {
-                        DeviceId = deviceId
-                    };
-                }
-            }
-        }
-
-        public void AddCameraUpload(string deviceId, LocalFileInfo file)
-        {
-            var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json");
-            _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path));
-
-            lock (_syncLock)
-            {
-                ContentUploadHistory history;
-
-                try
-                {
-                    history = _json.DeserializeFromFile<ContentUploadHistory>(path);
-                }
-                catch (IOException)
-                {
-                    history = new ContentUploadHistory
-                    {
-                        DeviceId = deviceId
-                    };
-                }
-
-                history.DeviceId = deviceId;
-
-                var list = history.FilesUploaded.ToList();
-                list.Add(file);
-                history.FilesUploaded = list.ToArray(list.Count);
-
-                _json.SerializeToFile(history, path);
-            }
-        }
-    }
-}

+ 451 - 0
Emby.Server.Implementations/Devices/SqliteDeviceRepository.cs

@@ -0,0 +1,451 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using Emby.Server.Implementations.Data;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.Logging;
+using SQLitePCL.pretty;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Controller.Devices;
+using MediaBrowser.Model.Devices;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Controller.Configuration;
+
+namespace Emby.Server.Implementations.Devices
+{
+    public class SqliteDeviceRepository : BaseSqliteRepository, IDeviceRepository
+    {
+        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        protected IFileSystem FileSystem { get; private set; }
+        private readonly object _syncLock = new object();
+        private readonly IJsonSerializer _json;
+        private IServerApplicationPaths _appPaths;
+
+        public SqliteDeviceRepository(ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IJsonSerializer json)
+            : base(logger)
+        {
+            var appPaths = config.ApplicationPaths;
+
+            DbFilePath = Path.Combine(appPaths.DataPath, "devices.db");
+            FileSystem = fileSystem;
+            _json = json;
+            _appPaths = appPaths;
+        }
+
+        public void Initialize()
+        {
+            try
+            {
+                InitializeInternal();
+            }
+            catch (Exception ex)
+            {
+                Logger.ErrorException("Error loading database file. Will reset and retry.", ex);
+
+                FileSystem.DeleteFile(DbFilePath);
+
+                InitializeInternal();
+            }
+        }
+
+        private void InitializeInternal()
+        {
+            using (var connection = CreateConnection())
+            {
+                RunDefaultInitialization(connection);
+
+                string[] queries = {
+                    "create table if not exists Devices (Id TEXT PRIMARY KEY, Name TEXT, ReportedName TEXT, CustomName TEXT, CameraUploadPath TEXT, LastUserName TEXT, AppName TEXT, AppVersion TEXT, LastUserId TEXT, DateLastModified DATETIME, Capabilities TEXT)",
+                    "create index if not exists idx_id on Devices(Id)"
+                               };
+
+                connection.RunQueries(queries);
+
+                MigrateDevices();
+            }
+        }
+
+        private void MigrateDevices()
+        {
+            List<string> files;
+            try
+            {
+                files = FileSystem
+                       .GetFilePaths(GetDevicesPath(), true)
+                       .Where(i => string.Equals(Path.GetFileName(i), "device.json", StringComparison.OrdinalIgnoreCase))
+                       .ToList();
+            }
+            catch (IOException)
+            {
+                return;
+            }
+
+            foreach (var file in files)
+            {
+                try
+                {
+                    var device = _json.DeserializeFromFile<DeviceInfo>(file);
+
+                    device.Name = string.IsNullOrWhiteSpace(device.CustomName) ? device.ReportedName : device.CustomName;
+
+                    SaveDevice(device);
+                }
+                catch (Exception ex)
+                {
+                    Logger.ErrorException("Error reading {0}", ex, file);
+                }
+                finally
+                {
+                    try
+                    {
+                        FileSystem.DeleteFile(file);
+                    }
+                    catch (IOException)
+                    {
+                        try
+                        {
+                            FileSystem.MoveFile(file, Path.ChangeExtension(file, ".old"));
+                        }
+                        catch (IOException)
+                        {
+                        }
+                    }
+                }
+            }
+        }
+
+        private const string BaseSelectText = "select Id, Name, ReportedName, CustomName, CameraUploadPath, LastUserName, AppName, AppVersion, LastUserId, DateLastModified, Capabilities from Devices";
+
+        public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
+        {
+            using (WriteLock.Write())
+            {
+                using (var connection = CreateConnection())
+                {
+                    connection.RunInTransaction(db =>
+                    {
+                        using (var statement = db.PrepareStatement("update devices set Capabilities=@Capabilities where Id=@Id"))
+                        {
+                            statement.TryBind("@Id", deviceId);
+
+                            if (capabilities == null)
+                            {
+                                statement.TryBindNull("@Capabilities");
+                            }
+                            else
+                            {
+                                statement.TryBind("@Capabilities", _json.SerializeToString(capabilities));
+                            }
+
+                            statement.MoveNext();
+                        }
+                    }, TransactionMode);
+                }
+            }
+        }
+
+        public void SaveDevice(DeviceInfo entry)
+        {
+            if (entry == null)
+            {
+                throw new ArgumentNullException("entry");
+            }
+
+            using (WriteLock.Write())
+            {
+                using (var connection = CreateConnection())
+                {
+                    connection.RunInTransaction(db =>
+                    {
+                        using (var statement = db.PrepareStatement("replace into Devices (Id, Name, ReportedName, CustomName, CameraUploadPath, LastUserName, AppName, AppVersion, LastUserId, DateLastModified, Capabilities) values (@Id, @Name, @ReportedName, @CustomName, @CameraUploadPath, @LastUserName, @AppName, @AppVersion, @LastUserId, @DateLastModified, @Capabilities)"))
+                        {
+                            statement.TryBind("@Id", entry.Id);
+                            statement.TryBind("@Name", entry.Name);
+                            statement.TryBind("@ReportedName", entry.ReportedName);
+                            statement.TryBind("@CustomName", entry.CustomName);
+                            statement.TryBind("@CameraUploadPath", entry.CameraUploadPath);
+                            statement.TryBind("@LastUserName", entry.LastUserName);
+                            statement.TryBind("@AppName", entry.AppName);
+                            statement.TryBind("@AppVersion", entry.AppVersion);
+                            statement.TryBind("@DateLastModified", entry.DateLastModified);
+
+                            if (entry.Capabilities == null)
+                            {
+                                statement.TryBindNull("@Capabilities");
+                            }
+                            else
+                            {
+                                statement.TryBind("@Capabilities", _json.SerializeToString(entry.Capabilities));
+                            }
+
+                            statement.MoveNext();
+                        }
+                    }, TransactionMode);
+                }
+            }
+        }
+
+        public DeviceInfo GetDevice(string id)
+        {
+            using (WriteLock.Read())
+            {
+                using (var connection = CreateConnection(true))
+                {
+                    var statementTexts = new List<string>();
+                    statementTexts.Add(BaseSelectText + " where Id=@Id");
+
+                    return connection.RunInTransaction(db =>
+                    {
+                        var statements = PrepareAllSafe(db, statementTexts).ToList();
+
+                        using (var statement = statements[0])
+                        {
+                            statement.TryBind("@Id", id);
+
+                            foreach (var row in statement.ExecuteQuery())
+                            {
+                                return GetEntry(row);
+                            }
+                        }
+
+                        return null;
+
+                    }, ReadTransactionMode);
+                }
+            }
+        }
+
+        public List<DeviceInfo> GetDevices()
+        {
+            using (WriteLock.Read())
+            {
+                using (var connection = CreateConnection(true))
+                {
+                    var statementTexts = new List<string>();
+                    statementTexts.Add(BaseSelectText + " order by DateLastModified desc");
+
+                    return connection.RunInTransaction(db =>
+                    {
+                        var list = new List<DeviceInfo>();
+
+                        var statements = PrepareAllSafe(db, statementTexts).ToList();
+
+                        using (var statement = statements[0])
+                        {
+                            foreach (var row in statement.ExecuteQuery())
+                            {
+                                list.Add(GetEntry(row));
+                            }
+                        }
+
+                        return list;
+
+                    }, ReadTransactionMode);
+                }
+            }
+        }
+
+        public ClientCapabilities GetCapabilities(string id)
+        {
+            using (WriteLock.Read())
+            {
+                using (var connection = CreateConnection(true))
+                {
+                    var statementTexts = new List<string>();
+                    statementTexts.Add("Select Capabilities from Devices where Id=@Id");
+
+                    return connection.RunInTransaction(db =>
+                    {
+                        var statements = PrepareAllSafe(db, statementTexts).ToList();
+
+                        using (var statement = statements[0])
+                        {
+                            statement.TryBind("@Id", id);
+
+                            foreach (var row in statement.ExecuteQuery())
+                            {
+                                if (row[0].SQLiteType != SQLiteType.Null)
+                                {
+                                    return _json.DeserializeFromString<ClientCapabilities>(row.GetString(0));
+                                }
+                            }
+                        }
+
+                        return null;
+
+                    }, ReadTransactionMode);
+                }
+            }
+        }
+
+        private DeviceInfo GetEntry(IReadOnlyList<IResultSetValue> reader)
+        {
+            var index = 0;
+
+            var info = new DeviceInfo
+            {
+                Id = reader.GetString(index)
+            };
+
+            index++;
+            if (reader[index].SQLiteType != SQLiteType.Null)
+            {
+                info.Name = reader.GetString(index);
+            }
+
+            index++;
+            if (reader[index].SQLiteType != SQLiteType.Null)
+            {
+                info.ReportedName = reader.GetString(index);
+            }
+
+            index++;
+            if (reader[index].SQLiteType != SQLiteType.Null)
+            {
+                info.CustomName = reader.GetString(index);
+            }
+
+            index++;
+            if (reader[index].SQLiteType != SQLiteType.Null)
+            {
+                info.CameraUploadPath = reader.GetString(index);
+            }
+
+            index++;
+            if (reader[index].SQLiteType != SQLiteType.Null)
+            {
+                info.LastUserName = reader.GetString(index);
+            }
+
+            index++;
+            if (reader[index].SQLiteType != SQLiteType.Null)
+            {
+                info.AppName = reader.GetString(index);
+            }
+
+            index++;
+            if (reader[index].SQLiteType != SQLiteType.Null)
+            {
+                info.AppVersion = reader.GetString(index);
+            }
+
+            index++;
+            if (reader[index].SQLiteType != SQLiteType.Null)
+            {
+                info.LastUserId = reader.GetString(index);
+            }
+
+            index++;
+            if (reader[index].SQLiteType != SQLiteType.Null)
+            {
+                info.DateLastModified = reader[index].ReadDateTime();
+            }
+
+            index++;
+            if (reader[index].SQLiteType != SQLiteType.Null)
+            {
+                info.Capabilities = _json.DeserializeFromString<ClientCapabilities>(reader.GetString(index));
+            }
+
+            return info;
+        }
+
+        private string GetDevicesPath()
+        {
+            return Path.Combine(_appPaths.DataPath, "devices");
+        }
+
+        private string GetDevicePath(string id)
+        {
+            return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N"));
+        }
+
+        public ContentUploadHistory GetCameraUploadHistory(string deviceId)
+        {
+            var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json");
+
+            lock (_syncLock)
+            {
+                try
+                {
+                    return _json.DeserializeFromFile<ContentUploadHistory>(path);
+                }
+                catch (IOException)
+                {
+                    return new ContentUploadHistory
+                    {
+                        DeviceId = deviceId
+                    };
+                }
+            }
+        }
+
+        public void AddCameraUpload(string deviceId, LocalFileInfo file)
+        {
+            var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json");
+            FileSystem.CreateDirectory(FileSystem.GetDirectoryName(path));
+
+            lock (_syncLock)
+            {
+                ContentUploadHistory history;
+
+                try
+                {
+                    history = _json.DeserializeFromFile<ContentUploadHistory>(path);
+                }
+                catch (IOException)
+                {
+                    history = new ContentUploadHistory
+                    {
+                        DeviceId = deviceId
+                    };
+                }
+
+                history.DeviceId = deviceId;
+
+                var list = history.FilesUploaded.ToList();
+                list.Add(file);
+                history.FilesUploaded = list.ToArray(list.Count);
+
+                _json.SerializeToFile(history, path);
+            }
+        }
+
+        public void DeleteDevice(string id)
+        {
+            using (WriteLock.Write())
+            {
+                using (var connection = CreateConnection())
+                {
+                    connection.RunInTransaction(db =>
+                    {
+                        using (var statement = db.PrepareStatement("delete from devices where Id=@Id"))
+                        {
+                            statement.TryBind("@Id", id);
+
+                            statement.MoveNext();
+                        }
+                    }, TransactionMode);
+                }
+            }
+
+            var path = GetDevicePath(id);
+
+            lock (_syncLock)
+            {
+                try
+                {
+                    FileSystem.DeleteDirectory(path, true);
+                }
+                catch (IOException)
+                {
+                }
+            }
+        }
+    }
+}

+ 1 - 1
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -67,7 +67,7 @@
     <Compile Include="Devices\CameraUploadsFolder.cs" />
     <Compile Include="Devices\DeviceId.cs" />
     <Compile Include="Devices\DeviceManager.cs" />
-    <Compile Include="Devices\DeviceRepository.cs" />
+    <Compile Include="Devices\SqliteDeviceRepository.cs" />
     <Compile Include="Diagnostics\CommonProcess.cs" />
     <Compile Include="Diagnostics\ProcessFactory.cs" />
     <Compile Include="Dto\DtoService.cs" />

+ 2 - 0
Emby.Server.Implementations/HttpClientManager/HttpClientInfo.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Net.Http;
 
 namespace Emby.Server.Implementations.HttpClientManager
 {
@@ -12,5 +13,6 @@ namespace Emby.Server.Implementations.HttpClientManager
         /// </summary>
         /// <value>The last timeout.</value>
         public DateTime LastTimeout { get; set; }
+        public HttpClient HttpClient { get; set; }
     }
 }

+ 5 - 11
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -96,23 +96,17 @@ namespace Emby.Server.Implementations.Library
             return GetMediaStreamsForItem(list);
         }
 
-        private List<MediaStream> GetMediaStreamsForItem(IEnumerable<MediaStream> streams)
+        private List<MediaStream> GetMediaStreamsForItem(List<MediaStream> streams)
         {
-            var list = streams.ToList();
-
-            var subtitleStreams = list
-                .Where(i => i.Type == MediaStreamType.Subtitle)
-                .ToList();
-
-            if (subtitleStreams.Count > 0)
+            foreach (var stream in streams)
             {
-                foreach (var subStream in subtitleStreams)
+                if (stream.Type == MediaStreamType.Subtitle)
                 {
-                    subStream.SupportsExternalStream = StreamSupportsExternalStream(subStream);
+                    stream.SupportsExternalStream = StreamSupportsExternalStream(stream);
                 }
             }
 
-            return list;
+            return streams;
         }
 
         public async Task<IEnumerable<MediaSourceInfo>> GetPlayackMediaSources(string id, string userId, bool enablePathSubstitution, string[] supportedLiveMediaTypes, CancellationToken cancellationToken)

+ 6 - 7
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHttpStream.cs

@@ -22,7 +22,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         private readonly IHttpClient _httpClient;
         private readonly IServerApplicationHost _appHost;
 
-        private readonly CancellationTokenSource _liveStreamCancellationTokenSource = new CancellationTokenSource();
         private readonly TaskCompletionSource<bool> _liveStreamTaskCompletionSource = new TaskCompletionSource<bool>();
 
         public HdHomerunHttpStream(MediaSourceInfo mediaSource, string originalStreamId, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost, IEnvironmentInfo environment)
@@ -35,7 +34,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
         protected override Task OpenInternal(CancellationToken openCancellationToken)
         {
-            _liveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested();
+            LiveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested();
 
             var mediaSource = OriginalMediaSource;
 
@@ -45,7 +44,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
             var taskCompletionSource = new TaskCompletionSource<bool>();
 
-            StartStreaming(url, taskCompletionSource, _liveStreamCancellationTokenSource.Token);
+            StartStreaming(url, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
 
             //OpenedMediaSource.Protocol = MediaProtocol.File;
             //OpenedMediaSource.Path = tempFile;
@@ -65,12 +64,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             //await Task.Delay(5000).ConfigureAwait(false);
         }
 
-        public override Task Close()
+        public override async Task Close()
         {
             Logger.Info("Closing HDHR live stream");
-            _liveStreamCancellationTokenSource.Cancel();
+            LiveStreamCancellationTokenSource.Cancel();
 
-            return _liveStreamTaskCompletionSource.Task;
+            await _liveStreamTaskCompletionSource.Task.ConfigureAwait(false);
+            await DeleteTempFile(TempFilePath).ConfigureAwait(false);
         }
 
         private Task StartStreaming(string url, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
@@ -112,7 +112,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                 }
 
                 _liveStreamTaskCompletionSource.TrySetResult(true);
-                await DeleteTempFile(TempFilePath).ConfigureAwait(false);
             });
         }
 

+ 3 - 4
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs

@@ -26,7 +26,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         private readonly IServerApplicationHost _appHost;
         private readonly ISocketFactory _socketFactory;
 
-        private readonly CancellationTokenSource _liveStreamCancellationTokenSource = new CancellationTokenSource();
         private readonly TaskCompletionSource<bool> _liveStreamTaskCompletionSource = new TaskCompletionSource<bool>();
         private readonly IHdHomerunChannelCommands _channelCommands;
         private readonly int _numTuners;
@@ -45,7 +44,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
         protected override Task OpenInternal(CancellationToken openCancellationToken)
         {
-            _liveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested();
+            LiveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested();
 
             var mediaSource = OriginalMediaSource;
 
@@ -56,7 +55,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
             var taskCompletionSource = new TaskCompletionSource<bool>();
 
-            StartStreaming(uri.Host, localPort, taskCompletionSource, _liveStreamCancellationTokenSource.Token);
+            StartStreaming(uri.Host, localPort, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
 
             //OpenedMediaSource.Protocol = MediaProtocol.File;
             //OpenedMediaSource.Path = tempFile;
@@ -76,7 +75,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         public override Task Close()
         {
             Logger.Info("Closing HDHR UDP live stream");
-            _liveStreamCancellationTokenSource.Cancel();
+            LiveStreamCancellationTokenSource.Cancel();
 
             return _liveStreamTaskCompletionSource.Task;
         }

+ 26 - 8
Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs

@@ -32,6 +32,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
         protected readonly string TempFilePath;
         protected readonly ILogger Logger;
+        protected readonly CancellationTokenSource LiveStreamCancellationTokenSource = new CancellationTokenSource();
 
         public LiveStream(MediaSourceInfo mediaSource, IEnvironmentInfo environment, IFileSystem fileSystem, ILogger logger, IServerApplicationPaths appPaths)
         {
@@ -80,6 +81,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 FileSystem.DeleteFile(path);
                 return;
             }
+            catch (DirectoryNotFoundException)
+            {
+                return;
+            }
+            catch (FileNotFoundException)
+            {
+                return;
+            }
             catch
             {
 
@@ -96,6 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
         public async Task CopyToAsync(Stream stream, CancellationToken cancellationToken)
         {
+            cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token).Token;
+
             var allowAsync = false;//Environment.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows;
             // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
 
@@ -110,16 +121,27 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         private static async Task CopyTo(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken)
         {
             byte[] buffer = new byte[bufferSize];
-            while (true)
+
+            var eofCount = 0;
+            var emptyReadLimit = 1000;
+
+            while (eofCount < emptyReadLimit)
             {
                 cancellationToken.ThrowIfCancellationRequested();
 
-                var read = source.Read(buffer, 0, buffer.Length);
+                var bytesRead = source.Read(buffer, 0, buffer.Length);
 
-                if (read > 0)
+                if (bytesRead == 0)
+                {
+                    eofCount++;
+                    await Task.Delay(10, cancellationToken).ConfigureAwait(false);
+                }
+                else
                 {
+                    eofCount = 0;
+
                     //await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false);
-                    destination.Write(buffer, 0, read);
+                    destination.Write(buffer, 0, bytesRead);
 
                     if (onStarted != null)
                     {
@@ -127,10 +149,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                         onStarted = null;
                     }
                 }
-                else
-                {
-                    await Task.Delay(10).ConfigureAwait(false);
-                }
             }
         }
 

+ 6 - 6
Emby.Server.Implementations/Session/SessionManager.cs

@@ -437,7 +437,7 @@ namespace Emby.Server.Implementations.Session
                     if (!string.IsNullOrEmpty(deviceId))
                     {
                         var userIdString = userId.HasValue ? userId.Value.ToString("N") : null;
-                        device = await _deviceManager.RegisterDevice(deviceId, deviceName, appName, appVersion, userIdString).ConfigureAwait(false);
+                        device = _deviceManager.RegisterDevice(deviceId, deviceName, appName, appVersion, userIdString);
                     }
                 }
 
@@ -446,7 +446,7 @@ namespace Emby.Server.Implementations.Session
                 if (device == null)
                 {
                     var userIdString = userId.HasValue ? userId.Value.ToString("N") : null;
-                    device = await _deviceManager.RegisterDevice(deviceId, deviceName, appName, appVersion, userIdString).ConfigureAwait(false);
+                    device = _deviceManager.RegisterDevice(deviceId, deviceName, appName, appVersion, userIdString);
                 }
 
                 if (device != null)
@@ -1567,7 +1567,7 @@ namespace Emby.Server.Implementations.Session
             ReportCapabilities(session, capabilities, true);
         }
 
-        private async void ReportCapabilities(SessionInfo session,
+        private void ReportCapabilities(SessionInfo session,
             ClientCapabilities capabilities,
             bool saveCapabilities)
         {
@@ -1593,7 +1593,7 @@ namespace Emby.Server.Implementations.Session
             {
                 try
                 {
-                    await SaveCapabilities(session.DeviceId, capabilities).ConfigureAwait(false);
+                    SaveCapabilities(session.DeviceId, capabilities);
                 }
                 catch (Exception ex)
                 {
@@ -1607,9 +1607,9 @@ namespace Emby.Server.Implementations.Session
             return _deviceManager.GetCapabilities(deviceId);
         }
 
-        private Task SaveCapabilities(string deviceId, ClientCapabilities capabilities)
+        private void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
         {
-            return _deviceManager.SaveCapabilities(deviceId, capabilities);
+            _deviceManager.SaveCapabilities(deviceId, capabilities);
         }
 
         public SessionInfoDto GetSessionInfoDto(SessionInfo session)

+ 2 - 6
MediaBrowser.Api/Devices/DeviceService.cs

@@ -85,13 +85,11 @@ namespace MediaBrowser.Api.Devices
 
         public void Post(PostDeviceOptions request)
         {
-            var task = _deviceManager.UpdateDeviceInfo(request.Id, new DeviceOptions
+            _deviceManager.UpdateDeviceInfo(request.Id, new DeviceOptions
             {
                 CustomName = request.CustomName,
                 CameraUploadPath = request.CameraUploadPath
             });
-
-            Task.WaitAll(task);
         }
 
         public object Get(GetDeviceInfo request)
@@ -116,9 +114,7 @@ namespace MediaBrowser.Api.Devices
 
         public void Delete(DeleteDevice request)
         {
-            var task = _deviceManager.DeleteDevice(request.Id);
-
-            Task.WaitAll(task);
+            _deviceManager.DeleteDevice(request.Id);
         }
 
         public void Post(PostCameraUpload request)

+ 2 - 2
MediaBrowser.Api/LiveTv/LiveTvService.cs

@@ -734,7 +734,7 @@ namespace MediaBrowser.Api.LiveTv
 
             outputHeaders["Content-Type"] = Model.Net.MimeTypes.GetMimeType(path);
 
-            return new ProgressiveFileCopier(_fileSystem, path, outputHeaders, Logger, _environment, CancellationToken.None)
+            return new ProgressiveFileCopier(_fileSystem, path, outputHeaders, Logger, _environment)
             {
                 AllowEndOfFile = false
             };
@@ -753,7 +753,7 @@ namespace MediaBrowser.Api.LiveTv
 
             outputHeaders["Content-Type"] = Model.Net.MimeTypes.GetMimeType("file." + request.Container);
 
-            return new ProgressiveFileCopier(directStreamProvider, outputHeaders, Logger, _environment, CancellationToken.None)
+            return new ProgressiveFileCopier(directStreamProvider, outputHeaders, Logger, _environment)
             {
                 AllowEndOfFile = false
             };

+ 5 - 8
MediaBrowser.Api/LiveTv/ProgressiveFileCopier.cs

@@ -16,7 +16,6 @@ namespace MediaBrowser.Api.LiveTv
         private readonly IFileSystem _fileSystem;
         private readonly ILogger _logger;
         private readonly string _path;
-        private readonly CancellationToken _cancellationToken;
         private readonly Dictionary<string, string> _outputHeaders;
 
         const int StreamCopyToBufferSize = 81920;
@@ -28,22 +27,20 @@ namespace MediaBrowser.Api.LiveTv
         private readonly IDirectStreamProvider _directStreamProvider;
         private readonly IEnvironmentInfo _environment;
 
-        public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary<string, string> outputHeaders, ILogger logger, IEnvironmentInfo environment, CancellationToken cancellationToken)
+        public ProgressiveFileCopier(IFileSystem fileSystem, string path, Dictionary<string, string> outputHeaders, ILogger logger, IEnvironmentInfo environment)
         {
             _fileSystem = fileSystem;
             _path = path;
             _outputHeaders = outputHeaders;
             _logger = logger;
-            _cancellationToken = cancellationToken;
             _environment = environment;
         }
 
-        public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, Dictionary<string, string> outputHeaders, ILogger logger, IEnvironmentInfo environment, CancellationToken cancellationToken)
+        public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, Dictionary<string, string> outputHeaders, ILogger logger, IEnvironmentInfo environment)
         {
             _directStreamProvider = directStreamProvider;
             _outputHeaders = outputHeaders;
             _logger = logger;
-            _cancellationToken = cancellationToken;
             _environment = environment;
         }
 
@@ -69,8 +66,6 @@ namespace MediaBrowser.Api.LiveTv
 
         public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
         {
-            cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken).Token;
-
             if (_directStreamProvider != null)
             {
                 await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
@@ -89,7 +84,9 @@ namespace MediaBrowser.Api.LiveTv
                     inputStream.Position = StartPosition;
                 }
 
-                while (eofCount < 20 || !AllowEndOfFile)
+                var emptyReadLimit = AllowEndOfFile ? 20 : 100;
+
+                while (eofCount < emptyReadLimit)
                 {
                     int bytesRead;
                     if (allowAsyncFileRead)

+ 4 - 9
MediaBrowser.Controller/Devices/IDeviceManager.cs

@@ -28,7 +28,7 @@ namespace MediaBrowser.Controller.Devices
         /// <param name="appVersion">The application version.</param>
         /// <param name="usedByUserId">The used by user identifier.</param>
         /// <returns>Task.</returns>
-        Task<DeviceInfo> RegisterDevice(string reportedId, string name, string appName, string appVersion, string usedByUserId);
+        DeviceInfo RegisterDevice(string reportedId, string name, string appName, string appVersion, string usedByUserId);
 
         /// <summary>
         /// Saves the capabilities.
@@ -36,7 +36,7 @@ namespace MediaBrowser.Controller.Devices
         /// <param name="reportedId">The reported identifier.</param>
         /// <param name="capabilities">The capabilities.</param>
         /// <returns>Task.</returns>
-        Task SaveCapabilities(string reportedId, ClientCapabilities capabilities);
+        void SaveCapabilities(string reportedId, ClientCapabilities capabilities);
 
         /// <summary>
         /// Gets the capabilities.
@@ -58,7 +58,7 @@ namespace MediaBrowser.Controller.Devices
         /// <param name="id">The identifier.</param>
         /// <param name="options">The options.</param>
         /// <returns>Task.</returns>
-        Task UpdateDeviceInfo(string id, DeviceOptions options);
+        void UpdateDeviceInfo(string id, DeviceOptions options);
 
         /// <summary>
         /// Gets the devices.
@@ -67,12 +67,7 @@ namespace MediaBrowser.Controller.Devices
         /// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns>
         QueryResult<DeviceInfo> GetDevices(DeviceQuery query);
 
-        /// <summary>
-        /// Deletes the device.
-        /// </summary>
-        /// <param name="id">The identifier.</param>
-        /// <returns>Task.</returns>
-        Task DeleteDevice(string id);
+        void DeleteDevice(string id);
 
         /// <summary>
         /// Gets the upload history.

+ 4 - 9
MediaBrowser.Controller/Devices/IDeviceRepository.cs

@@ -1,7 +1,6 @@
 using MediaBrowser.Model.Devices;
 using MediaBrowser.Model.Session;
 using System.Collections.Generic;
-using System.Threading.Tasks;
 
 namespace MediaBrowser.Controller.Devices
 {
@@ -12,7 +11,7 @@ namespace MediaBrowser.Controller.Devices
         /// </summary>
         /// <param name="device">The device.</param>
         /// <returns>Task.</returns>
-        Task SaveDevice(DeviceInfo device);
+        void SaveDevice(DeviceInfo device);
 
         /// <summary>
         /// Saves the capabilities.
@@ -20,7 +19,7 @@ namespace MediaBrowser.Controller.Devices
         /// <param name="id">The identifier.</param>
         /// <param name="capabilities">The capabilities.</param>
         /// <returns>Task.</returns>
-        Task SaveCapabilities(string id, ClientCapabilities capabilities);
+        void SaveCapabilities(string id, ClientCapabilities capabilities);
 
         /// <summary>
         /// Gets the capabilities.
@@ -36,18 +35,14 @@ namespace MediaBrowser.Controller.Devices
         /// <returns>DeviceInfo.</returns>
         DeviceInfo GetDevice(string id);
 
-        /// <summary>
-        /// Gets the devices.
-        /// </summary>
-        /// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns>
-        IEnumerable<DeviceInfo> GetDevices();
+        List<DeviceInfo> GetDevices();
 
         /// <summary>
         /// Deletes the device.
         /// </summary>
         /// <param name="id">The identifier.</param>
         /// <returns>Task.</returns>
-        Task DeleteDevice(string id);
+        void DeleteDevice(string id);
 
         /// <summary>
         /// Gets the upload history.

+ 0 - 6
MediaBrowser.Controller/Entities/AudioBook.cs

@@ -51,12 +51,6 @@ namespace MediaBrowser.Controller.Entities
             return null;
         }
 
-        [IgnoreDataMember]
-        public override bool EnableRefreshOnDateModifiedChange
-        {
-            get { return true; }
-        }
-
         public Guid? FindSeriesId()
         {
             return SeriesId;

+ 0 - 6
MediaBrowser.Controller/Entities/Book.cs

@@ -38,12 +38,6 @@ namespace MediaBrowser.Controller.Entities
             return SeriesPresentationUniqueKey;
         }
 
-        [IgnoreDataMember]
-        public override bool EnableRefreshOnDateModifiedChange
-        {
-            get { return true; }
-        }
-
         public Guid? FindSeriesId()
         {
             return SeriesId;

+ 0 - 6
MediaBrowser.Controller/Entities/Game.cs

@@ -28,12 +28,6 @@ namespace MediaBrowser.Controller.Entities
                    locationType != LocationType.Virtual;
         }
 
-        [IgnoreDataMember]
-        public override bool EnableRefreshOnDateModifiedChange
-        {
-            get { return true; }
-        }
-
         [IgnoreDataMember]
         public override bool SupportsThemeMedia
         {

+ 2 - 1
MediaBrowser.Controller/LiveTv/LiveTvChannel.cs

@@ -136,7 +136,8 @@ namespace MediaBrowser.Controller.LiveTv
                 Name = Name,
                 Path = Path,
                 RunTimeTicks = RunTimeTicks,
-                Type = MediaSourceType.Placeholder
+                Type = MediaSourceType.Placeholder,
+                IsInfiniteStream = RunTimeTicks == null
             };
 
             list.Add(info);

+ 0 - 1
MediaBrowser.Controller/MediaBrowser.Controller.csproj

@@ -220,7 +220,6 @@
     <Compile Include="Providers\IExternalId.cs" />
     <Compile Include="Providers\IExtrasProvider.cs" />
     <Compile Include="Providers\IForcedProvider.cs" />
-    <Compile Include="Providers\IHasChangeMonitor.cs" />
     <Compile Include="Entities\IHasMetadata.cs" />
     <Compile Include="Providers\IHasItemChangeMonitor.cs" />
     <Compile Include="Providers\IHasLookupInfo.cs" />

+ 0 - 17
MediaBrowser.Controller/Providers/IHasChangeMonitor.cs

@@ -1,17 +0,0 @@
-using MediaBrowser.Controller.Entities;
-using System;
-
-namespace MediaBrowser.Controller.Providers
-{
-    public interface IHasChangeMonitor
-    {
-        /// <summary>
-        /// Determines whether the specified item has changed.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <param name="directoryService">The directory service.</param>
-        /// <param name="date">The date.</param>
-        /// <returns><c>true</c> if the specified item has changed; otherwise, <c>false</c>.</returns>
-        bool HasChanged(IHasMetadata item, IDirectoryService directoryService, DateTime date);
-    }
-}

+ 1 - 11
MediaBrowser.Model/Devices/DeviceInfo.cs

@@ -21,17 +21,7 @@ namespace MediaBrowser.Model.Devices
         /// <value>The camera upload path.</value>
         public string CameraUploadPath { get; set; }
 
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name
-        {
-            get
-            {
-                return string.IsNullOrEmpty(CustomName) ? ReportedName : CustomName;
-            }
-        }
+        public string Name { get; set; }
 
         /// <summary>
         /// Gets or sets the identifier.

+ 13 - 16
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -686,7 +686,7 @@ namespace MediaBrowser.Model.Dlna
 
                     if (subtitleStream != null)
                     {
-                        SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, null, null);
+                        SubtitleProfile subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, directPlay.Value, _transcoderSupport, null, null);
 
                         playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
                         playlistItem.SubtitleFormat = subtitleProfile.Format;
@@ -728,7 +728,7 @@ namespace MediaBrowser.Model.Dlna
 
                 if (subtitleStream != null)
                 {
-                    SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Protocol, transcodingProfile.Container);
+                    SubtitleProfile subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, PlayMethod.Transcode, _transcoderSupport, transcodingProfile.Protocol, transcodingProfile.Container);
 
                     playlistItem.SubtitleDeliveryMethod = subtitleProfile.Method;
                     playlistItem.SubtitleFormat = subtitleProfile.Format;
@@ -1054,15 +1054,6 @@ namespace MediaBrowser.Model.Dlna
 
             string videoCodec = videoStream == null ? null : videoStream.Codec;
 
-            if (string.IsNullOrEmpty(videoCodec))
-            {
-                _logger.Info("Profile: {0}, DirectPlay=false. Reason=Unknown video codec. Path: {1}",
-                    profile.Name ?? "Unknown Profile",
-                    mediaSource.Path ?? "Unknown path");
-
-                return new Tuple<PlayMethod?, List<TranscodeReason>>(null, new List<TranscodeReason> { TranscodeReason.UnknownVideoStreamInfo });
-            }
-
             conditions = new List<ProfileCondition>();
             foreach (CodecProfile i in profile.CodecProfiles)
             {
@@ -1189,7 +1180,7 @@ namespace MediaBrowser.Model.Dlna
         {
             if (subtitleStream != null)
             {
-                SubtitleProfile subtitleProfile = GetSubtitleProfile(subtitleStream, options.Profile.SubtitleProfiles, playMethod, _transcoderSupport, null, null);
+                SubtitleProfile subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, playMethod, _transcoderSupport, null, null);
 
                 if (subtitleProfile.Method != SubtitleDeliveryMethod.External && subtitleProfile.Method != SubtitleDeliveryMethod.Embed)
                 {
@@ -1208,7 +1199,7 @@ namespace MediaBrowser.Model.Dlna
             return new Tuple<bool, TranscodeReason?>(result, TranscodeReason.ContainerBitrateExceedsLimit);
         }
 
-        public static SubtitleProfile GetSubtitleProfile(MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, string transcodingSubProtocol, string transcodingContainer)
+        public static SubtitleProfile GetSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, string transcodingSubProtocol, string transcodingContainer)
         {
             if (!subtitleStream.IsExternal && (playMethod != PlayMethod.Transcode || !string.Equals(transcodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase)))
             {
@@ -1262,8 +1253,8 @@ namespace MediaBrowser.Model.Dlna
             }
 
             // Look for an external or hls profile that matches the stream type (text/graphical) and doesn't require conversion
-            return GetExternalSubtitleProfile(subtitleStream, subtitleProfiles, playMethod, transcoderSupport, false) ??
-                GetExternalSubtitleProfile(subtitleStream, subtitleProfiles, playMethod, transcoderSupport, true) ??
+            return GetExternalSubtitleProfile(mediaSource, subtitleStream, subtitleProfiles, playMethod, transcoderSupport, false) ??
+                GetExternalSubtitleProfile(mediaSource, subtitleStream, subtitleProfiles, playMethod, transcoderSupport, true) ??
                 new SubtitleProfile
                 {
                     Method = SubtitleDeliveryMethod.Encode,
@@ -1299,7 +1290,7 @@ namespace MediaBrowser.Model.Dlna
             return false;
         }
 
-        private static SubtitleProfile GetExternalSubtitleProfile(MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, bool allowConversion)
+        private static SubtitleProfile GetExternalSubtitleProfile(MediaSourceInfo mediaSource, MediaStream subtitleStream, SubtitleProfile[] subtitleProfiles, PlayMethod playMethod, ITranscoderSupport transcoderSupport, bool allowConversion)
         {
             foreach (SubtitleProfile profile in subtitleProfiles)
             {
@@ -1338,6 +1329,12 @@ namespace MediaBrowser.Model.Dlna
                         continue;
                     }
 
+                    // TODO: Build this into subtitleStream.SupportsExternalStream
+                    if (mediaSource.IsInfiniteStream)
+                    {
+                        continue;
+                    }
+
                     if (subtitleStream.IsTextSubtitleStream && subtitleStream.SupportsExternalStream && subtitleStream.SupportsSubtitleConversionTo(profile.Format))
                     {
                         return profile;

+ 2 - 2
MediaBrowser.Model/Dlna/StreamInfo.cs

@@ -466,7 +466,7 @@ namespace MediaBrowser.Model.Dlna
 
         private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport)
         {
-            SubtitleProfile subtitleProfile = StreamBuilder.GetSubtitleProfile(stream, subtitleProfiles, PlayMethod, transcoderSupport, SubProtocol, Container);
+            SubtitleProfile subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, SubProtocol, Container);
             SubtitleStreamInfo info = new SubtitleStreamInfo
             {
                 IsForced = stream.IsForced,
@@ -480,7 +480,7 @@ namespace MediaBrowser.Model.Dlna
 
             if (info.DeliveryMethod == SubtitleDeliveryMethod.External)
             {
-                if (MediaSource.Protocol == MediaProtocol.File || !StringHelper.EqualsIgnoreCase(stream.Codec, subtitleProfile.Format))
+                if (MediaSource.Protocol == MediaProtocol.File || !StringHelper.EqualsIgnoreCase(stream.Codec, subtitleProfile.Format) || !stream.IsExternal)
                 {
                     info.Url = string.Format("{0}/Videos/{1}/{2}/Subtitles/{3}/{4}/Stream.{5}",
                         baseUrl,

+ 47 - 19
MediaBrowser.Providers/Manager/MetadataService.cs

@@ -13,6 +13,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Configuration;
 using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.MediaInfo;
 
 namespace MediaBrowser.Providers.Manager
 {
@@ -37,6 +38,28 @@ namespace MediaBrowser.Providers.Manager
             LibraryManager = libraryManager;
         }
 
+        private bool RequiresRefresh(IHasMetadata item, IDirectoryService directoryService)
+        {
+            if (item.RequiresRefresh())
+            {
+                return true;
+            }
+
+            if (item.SupportsLocalMetadata)
+            {
+                var video = item as Video;
+
+                if (video != null && !video.IsPlaceHolder)
+                {
+                    return !video.SubtitleFiles
+                        .SequenceEqual(SubtitleResolver.GetSubtitleFiles(video, directoryService, FileSystem, false)
+                        .OrderBy(i => i), StringComparer.OrdinalIgnoreCase);
+                }
+            }
+
+            return false;
+        }
+
         public async Task<ItemUpdateType> RefreshMetadata(IHasMetadata item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
         {
             var itemOfType = (TItemType)item;
@@ -47,19 +70,35 @@ namespace MediaBrowser.Providers.Manager
 
             var libraryOptions = LibraryManager.GetLibraryOptions((BaseItem)item);
 
-            if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
+            DateTime? newDateModified = null;
+            if (item.LocationType == LocationType.FileSystem)
             {
-                // TODO: If this returns true, should we instead just change metadata refresh mode to Full?
-                requiresRefresh = item.RequiresRefresh();
+                var file = refreshOptions.DirectoryService.GetFile(item.Path);
+                if (file != null)
+                {
+                    newDateModified = file.LastWriteTimeUtc;
+                    if (item.EnableRefreshOnDateModifiedChange)
+                    {
+                        if (newDateModified != item.DateModified)
+                        {
+                            Logger.Debug("Date modified for {0}. Old date {1} new date {2} Id {3}", item.Path, item.DateModified, newDateModified, item.Id);
+                            requiresRefresh = true;
+                        }
+                    }
+                }
             }
 
-            if (!requiresRefresh &&
-                libraryOptions.AutomaticRefreshIntervalDays > 0 &&
-                (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= libraryOptions.AutomaticRefreshIntervalDays)
+            if (!requiresRefresh && libraryOptions.AutomaticRefreshIntervalDays > 0 && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= libraryOptions.AutomaticRefreshIntervalDays)
             {
                 requiresRefresh = true;
             }
 
+            if (!requiresRefresh && refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
+            {
+                // TODO: If this returns true, should we instead just change metadata refresh mode to Full?
+                requiresRefresh = RequiresRefresh(item, refreshOptions.DirectoryService);
+            }
+
             var itemImageProvider = new ItemImageProvider(Logger, ProviderManager, ServerConfigurationManager, FileSystem);
             var localImagesFailed = false;
 
@@ -145,20 +184,9 @@ namespace MediaBrowser.Providers.Manager
             var beforeSaveResult = BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh, updateType);
             updateType = updateType | beforeSaveResult;
 
-            if (item.LocationType == LocationType.FileSystem)
+            if (newDateModified.HasValue)
             {
-                var file = refreshOptions.DirectoryService.GetFile(item.Path);
-                if (file != null)
-                {
-                    var fileLastWriteTime = file.LastWriteTimeUtc;
-                    if (item.EnableRefreshOnDateModifiedChange && fileLastWriteTime != item.DateModified)
-                    {
-                        Logger.Debug("Date modified for {0}. Old date {1} new date {2} Id {3}", item.Path, item.DateModified, fileLastWriteTime, item.Id);
-                        requiresRefresh = true;
-                    }
-
-                    item.DateModified = fileLastWriteTime;
-                }
+                item.DateModified = newDateModified.Value;
             }
 
             // Save if changes were made, or it's never been saved before

+ 1 - 15
MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs

@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.MediaInfo
     /// <summary>
     /// Uses ffmpeg to create video images
     /// </summary>
-    public class AudioImageProvider : IDynamicImageProvider, IHasItemChangeMonitor
+    public class AudioImageProvider : IDynamicImageProvider
     {
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IServerConfigurationManager _config;
@@ -134,19 +134,5 @@ namespace MediaBrowser.Providers.MediaInfo
 
             return item.LocationType == LocationType.FileSystem && audio != null;
         }
-
-        public bool HasChanged(IHasMetadata item, IDirectoryService directoryService)
-        {
-            if (item.EnableRefreshOnDateModifiedChange && !string.IsNullOrWhiteSpace(item.Path) && item.LocationType == LocationType.FileSystem)
-            {
-                var file = directoryService.GetFile(item.Path);
-                if (file != null && file.LastWriteTimeUtc != item.DateModified)
-                {
-                    return true;
-                }
-            }
-
-            return false;
-        }
     }
 }

+ 0 - 27
MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs

@@ -34,7 +34,6 @@ namespace MediaBrowser.Providers.MediaInfo
         ICustomMetadataProvider<Audio>,
         ICustomMetadataProvider<AudioPodcast>,
         ICustomMetadataProvider<AudioBook>,
-        IHasItemChangeMonitor,
         IHasOrder,
         IForcedProvider,
         IPreRefreshProvider
@@ -180,32 +179,6 @@ namespace MediaBrowser.Providers.MediaInfo
             return prober.Probe(item, cancellationToken);
         }
 
-        public bool HasChanged(IHasMetadata item, IDirectoryService directoryService)
-        {
-            if (item.EnableRefreshOnDateModifiedChange && !string.IsNullOrWhiteSpace(item.Path) && item.LocationType == LocationType.FileSystem)
-            {
-                var file = directoryService.GetFile(item.Path);
-                if (file != null && file.LastWriteTimeUtc != item.DateModified)
-                {
-                    return true;
-                }
-            }
-
-            if (item.SupportsLocalMetadata)
-            {
-                var video = item as Video;
-
-                if (video != null && !video.IsPlaceHolder)
-                {
-                    return !video.SubtitleFiles
-                        .SequenceEqual(SubtitleResolver.GetSubtitleFiles(video, directoryService, _fileSystem, false)
-                        .OrderBy(i => i), StringComparer.OrdinalIgnoreCase);
-                }
-            }
-
-            return false;
-        }
-
         public int Order
         {
             get

+ 1 - 15
MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs

@@ -16,7 +16,7 @@ using System.Threading.Tasks;
 
 namespace MediaBrowser.Providers.MediaInfo
 {
-    public class VideoImageProvider : IDynamicImageProvider, IHasItemChangeMonitor, IHasOrder
+    public class VideoImageProvider : IDynamicImageProvider, IHasOrder
     {
         private readonly IMediaEncoder _mediaEncoder;
         private readonly ILogger _logger;
@@ -149,19 +149,5 @@ namespace MediaBrowser.Providers.MediaInfo
                 return 100;
             }
         }
-
-        public bool HasChanged(IHasMetadata item, IDirectoryService directoryService)
-        {
-            if (item.EnableRefreshOnDateModifiedChange && !string.IsNullOrWhiteSpace(item.Path) && item.LocationType == LocationType.FileSystem)
-            {
-                var file = directoryService.GetFile(item.Path);
-                if (file != null && file.LastWriteTimeUtc != item.DateModified)
-                {
-                    return true;
-                }
-            }
-
-            return false;
-        }
     }
 }

+ 6 - 0
MediaBrowser.Providers/Movies/MovieDbSearch.cs

@@ -100,6 +100,12 @@ namespace MediaBrowser.Providers.Movies
                 name = name.Replace("!", " ");
                 name = name.Replace("?", " ");
 
+                var parenthIndex = name.IndexOf('(');
+                if (parenthIndex != -1)
+                {
+                    name = name.Substring(0, parenthIndex);
+                }
+
                 name = name.Trim();
 
                 // Search again if the new name is different

+ 0 - 4
MediaBrowser.Providers/TV/MissingEpisodeProvider.cs

@@ -350,8 +350,6 @@ namespace MediaBrowser.Providers.TV
 
             foreach (var episodeToRemove in episodesToRemove.Select(e => e.Episode))
             {
-                _logger.Info("Removing missing/unaired episode {0} {1}x{2}", episodeToRemove.Series.Name, episodeToRemove.ParentIndexNumber, episodeToRemove.IndexNumber);
-
                 await episodeToRemove.Delete(new DeleteOptions
                 {
                     DeleteFileLocation = true
@@ -418,8 +416,6 @@ namespace MediaBrowser.Providers.TV
 
             foreach (var seasonToRemove in seasonsToRemove)
             {
-                _logger.Info("Removing virtual season {0} {1}", seasonToRemove.Series.Name, seasonToRemove.IndexNumber);
-
                 await seasonToRemove.Delete(new DeleteOptions
                 {
                     DeleteFileLocation = true

+ 1 - 1
SharedVersion.cs

@@ -1,3 +1,3 @@
 using System.Reflection;
 
-[assembly: AssemblyVersion("3.2.33.7")]
+[assembly: AssemblyVersion("3.2.33.8")]