using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Dtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Devices; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; using Microsoft.EntityFrameworkCore; namespace Jellyfin.Server.Implementations.Devices { /// /// Manages the creation, updating, and retrieval of devices. /// public class DeviceManager : IDeviceManager { private readonly IDbContextFactory _dbProvider; private readonly IUserManager _userManager; private readonly ConcurrentDictionary _capabilitiesMap = new(); private readonly ConcurrentDictionary _devices; private readonly ConcurrentDictionary _deviceOptions; /// /// Initializes a new instance of the class. /// /// The database provider. /// The user manager. public DeviceManager(IDbContextFactory dbProvider, IUserManager userManager) { _dbProvider = dbProvider; _userManager = userManager; _devices = new ConcurrentDictionary(); _deviceOptions = new ConcurrentDictionary(); using var dbContext = _dbProvider.CreateDbContext(); foreach (var device in dbContext.Devices .OrderBy(d => d.Id) .AsEnumerable()) { _devices.TryAdd(device.Id, device); } foreach (var deviceOption in dbContext.DeviceOptions .OrderBy(d => d.Id) .AsEnumerable()) { _deviceOptions.TryAdd(deviceOption.DeviceId, deviceOption); } } /// public event EventHandler>>? DeviceOptionsUpdated; /// public void SaveCapabilities(string deviceId, ClientCapabilities capabilities) { _capabilitiesMap[deviceId] = capabilities; } /// public async Task UpdateDeviceOptions(string deviceId, string? deviceName) { DeviceOptions? deviceOptions; var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { deviceOptions = await dbContext.DeviceOptions.FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false); if (deviceOptions is null) { deviceOptions = new DeviceOptions(deviceId); dbContext.DeviceOptions.Add(deviceOptions); } deviceOptions.CustomName = deviceName; await dbContext.SaveChangesAsync().ConfigureAwait(false); } _deviceOptions[deviceId] = deviceOptions; DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs>(new Tuple(deviceId, deviceOptions))); } /// public async Task CreateDevice(Device device) { var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { dbContext.Devices.Add(device); await dbContext.SaveChangesAsync().ConfigureAwait(false); _devices.TryAdd(device.Id, device); } return device; } /// public DeviceOptionsDto? GetDeviceOptions(string deviceId) { if (_deviceOptions.TryGetValue(deviceId, out var deviceOptions)) { return ToDeviceOptionsDto(deviceOptions); } return null; } /// public ClientCapabilities GetCapabilities(string? deviceId) { if (deviceId is null) { return new(); } return _capabilitiesMap.TryGetValue(deviceId, out ClientCapabilities? result) ? result : new(); } /// public DeviceInfoDto? GetDevice(string id) { var device = _devices.Values.Where(d => d.DeviceId == id).OrderByDescending(d => d.DateLastActivity).FirstOrDefault(); _deviceOptions.TryGetValue(id, out var deviceOption); var deviceInfo = device is null ? null : ToDeviceInfo(device, deviceOption); return deviceInfo is null ? null : ToDeviceInfoDto(deviceInfo); } /// public QueryResult GetDevices(DeviceQuery query) { IEnumerable devices = _devices.Values .Where(device => !query.UserId.HasValue || device.UserId.Equals(query.UserId.Value)) .Where(device => query.DeviceId is null || device.DeviceId == query.DeviceId) .Where(device => query.AccessToken is null || device.AccessToken == query.AccessToken) .OrderBy(d => d.Id) .ToList(); var count = devices.Count(); if (query.Skip.HasValue) { devices = devices.Skip(query.Skip.Value); } if (query.Limit.HasValue) { devices = devices.Take(query.Limit.Value); } return new QueryResult(query.Skip, count, devices.ToList()); } /// public QueryResult GetDeviceInfos(DeviceQuery query) { var devices = GetDevices(query); return new QueryResult( devices.StartIndex, devices.TotalRecordCount, devices.Items.Select(device => ToDeviceInfo(device)).ToList()); } /// public QueryResult GetDevicesForUser(Guid? userId) { IEnumerable devices = _devices.Values .OrderByDescending(d => d.DateLastActivity) .ThenBy(d => d.DeviceId); if (!userId.IsNullOrEmpty()) { var user = _userManager.GetUserById(userId.Value); if (user is null) { throw new ResourceNotFoundException(); } devices = devices.Where(i => CanAccessDevice(user, i.DeviceId)); } var array = devices.Select(device => { _deviceOptions.TryGetValue(device.DeviceId, out var option); return ToDeviceInfo(device, option); }) .Select(ToDeviceInfoDto) .ToArray(); return new QueryResult(array); } /// public async Task DeleteDevice(Device device) { _devices.TryRemove(device.Id, out _); var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { dbContext.Devices.Remove(device); await dbContext.SaveChangesAsync().ConfigureAwait(false); } } /// public async Task UpdateDevice(Device device) { var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { dbContext.Devices.Update(device); await dbContext.SaveChangesAsync().ConfigureAwait(false); } _devices[device.Id] = device; } /// public bool CanAccessDevice(User user, string deviceId) { ArgumentNullException.ThrowIfNull(user); ArgumentException.ThrowIfNullOrEmpty(deviceId); if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator)) { return true; } return user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparison.OrdinalIgnoreCase) || !GetCapabilities(deviceId).SupportsPersistentIdentifier; } private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null) { var caps = GetCapabilities(authInfo.DeviceId); var user = _userManager.GetUserById(authInfo.UserId) ?? throw new ResourceNotFoundException("User with UserId " + authInfo.UserId + " not found"); return new() { AppName = authInfo.AppName, AppVersion = authInfo.AppVersion, Id = authInfo.DeviceId, LastUserId = authInfo.UserId, LastUserName = user.Username, Name = authInfo.DeviceName, DateLastActivity = authInfo.DateLastActivity, IconUrl = caps.IconUrl, CustomName = options?.CustomName, }; } private DeviceOptionsDto ToDeviceOptionsDto(DeviceOptions options) { return new() { Id = options.Id, DeviceId = options.DeviceId, CustomName = options.CustomName, }; } private DeviceInfoDto ToDeviceInfoDto(DeviceInfo info) { return new() { Name = info.Name, CustomName = info.CustomName, AccessToken = info.AccessToken, Id = info.Id, LastUserName = info.LastUserName, AppName = info.AppName, AppVersion = info.AppVersion, LastUserId = info.LastUserId, DateLastActivity = info.DateLastActivity, Capabilities = ToClientCapabilitiesDto(info.Capabilities), IconUrl = info.IconUrl }; } /// public ClientCapabilitiesDto ToClientCapabilitiesDto(ClientCapabilities capabilities) { return new() { PlayableMediaTypes = capabilities.PlayableMediaTypes, SupportedCommands = capabilities.SupportedCommands, SupportsMediaControl = capabilities.SupportsMediaControl, SupportsPersistentIdentifier = capabilities.SupportsPersistentIdentifier, DeviceProfile = capabilities.DeviceProfile, AppStoreUrl = capabilities.AppStoreUrl, IconUrl = capabilities.IconUrl }; } } }