using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Jellyfin.Data; using Jellyfin.Data.Dtos; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.Entities; using Jellyfin.Database.Implementations.Entities.Security; using Jellyfin.Database.Implementations.Enums; 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(); /// /// Initializes a new instance of the class. /// /// The database provider. /// The user manager. public DeviceManager(IDbContextFactory dbProvider, IUserManager userManager) { _dbProvider = dbProvider; _userManager = userManager; } /// 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); } 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); } return device; } /// public async Task GetDeviceOptions(string deviceId) { var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { var deviceOptions = await dbContext.DeviceOptions .AsNoTracking() .FirstOrDefaultAsync(dev => dev.DeviceId == deviceId) .ConfigureAwait(false); return deviceOptions is null ? null : ToDeviceOptionsDto(deviceOptions); } } /// public ClientCapabilities GetCapabilities(string? deviceId) { if (deviceId is null) { return new(); } return _capabilitiesMap.TryGetValue(deviceId, out ClientCapabilities? result) ? result : new(); } /// public async Task GetDevice(string id) { var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { var device = await dbContext.Devices .AsNoTracking() .Where(d => d.DeviceId == id) .OrderByDescending(d => d.DateLastActivity) .FirstOrDefaultAsync() .ConfigureAwait(false); if (device is null) { return null; } var deviceOption = await dbContext.DeviceOptions .AsNoTracking() .FirstOrDefaultAsync(dev => dev.DeviceId == id) .ConfigureAwait(false); return ToDeviceInfoDto(ToDeviceInfo(device, deviceOption)); } } /// public async Task> GetDevices(DeviceQuery query) { var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { IQueryable filtered = dbContext.Devices.AsNoTracking(); if (query.UserId.HasValue) { filtered = filtered.Where(device => device.UserId.Equals(query.UserId.Value)); } if (query.DeviceId is not null) { filtered = filtered.Where(device => device.DeviceId == query.DeviceId); } if (query.AccessToken is not null) { filtered = filtered.Where(device => device.AccessToken == query.AccessToken); } // Every filter is an exact match on an indexed column and the table holds one row per // user and device, so paging the materialised set costs less than a second round trip. var matched = await filtered.OrderBy(d => d.Id).ToListAsync().ConfigureAwait(false); IEnumerable devices = matched; if (query.Skip.HasValue) { devices = devices.Skip(query.Skip.Value); } if (query.Limit.HasValue && query.Limit.Value > 0) { devices = devices.Take(query.Limit.Value); } return new QueryResult(query.Skip, matched.Count, devices.ToList()); } } /// public async Task> GetDeviceInfos(DeviceQuery query) { var devices = await GetDevices(query).ConfigureAwait(false); return new QueryResult( devices.StartIndex, devices.TotalRecordCount, devices.Items.Select(device => ToDeviceInfo(device)).ToList()); } /// public async Task> GetDevicesForUser(Guid? userId) { var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { IEnumerable devices = await dbContext.Devices .AsNoTracking() .OrderByDescending(d => d.DateLastActivity) .ThenBy(d => d.DeviceId) .ToListAsync() .ConfigureAwait(false); 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 options = await dbContext.DeviceOptions .AsNoTracking() .ToDictionaryAsync(dev => dev.DeviceId) .ConfigureAwait(false); var array = devices.Select(device => { options.TryGetValue(device.DeviceId, out var option); return ToDeviceInfo(device, option); }) .Select(ToDeviceInfoDto) .ToArray(); return new QueryResult(array); } } /// public async Task DeleteDevice(Device device) { var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { await dbContext.Devices .Where(d => d.Id == device.Id) .ExecuteDeleteAsync() .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); } } /// 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 }; } } }