using System; using System.Collections.Concurrent; using System.Linq; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Queries; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Devices; 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 JellyfinDbProvider _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(JellyfinDbProvider 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, DeviceOptions options) { await using var dbContext = _dbProvider.CreateContext(); var deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false); if (deviceOptions == null) { deviceOptions = new DeviceOptions(deviceId); dbContext.DeviceOptions.Add(deviceOptions); } deviceOptions.CustomName = options.CustomName; await dbContext.SaveChangesAsync().ConfigureAwait(false); DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs>(new Tuple(deviceId, options))); } /// public async Task CreateDevice(Device device) { await using var dbContext = _dbProvider.CreateContext(); dbContext.Devices.Add(device); await dbContext.SaveChangesAsync().ConfigureAwait(false); return device; } /// public async Task GetDeviceOptions(string deviceId) { await using var dbContext = _dbProvider.CreateContext(); return await dbContext.DeviceOptions .AsQueryable() .FirstOrDefaultAsync(d => d.DeviceId == deviceId) .ConfigureAwait(false); } /// public ClientCapabilities GetCapabilities(string deviceId) { return _capabilitiesMap.TryGetValue(deviceId, out ClientCapabilities? result) ? result : new ClientCapabilities(); } /// public async Task GetDevice(string id) { await using var dbContext = _dbProvider.CreateContext(); var device = await dbContext.Devices .AsQueryable() .Where(d => d.DeviceId == id) .OrderByDescending(d => d.DateLastActivity) .Include(d => d.User) .FirstOrDefaultAsync() .ConfigureAwait(false); var deviceInfo = device == null ? null : ToDeviceInfo(device); return deviceInfo; } /// public async Task> GetDevices(DeviceQuery query) { await using var dbContext = _dbProvider.CreateContext(); var devices = dbContext.Devices.AsQueryable(); if (query.UserId.HasValue) { devices = devices.Where(device => device.UserId == query.UserId.Value); } if (query.DeviceId != null) { devices = devices.Where(device => device.DeviceId == query.DeviceId); } if (query.AccessToken != null) { devices = devices.Where(device => device.AccessToken == query.AccessToken); } var count = await devices.CountAsync().ConfigureAwait(false); if (query.Skip.HasValue) { devices = devices.Skip(query.Skip.Value); } if (query.Limit.HasValue) { devices = devices.Take(query.Limit.Value); } return new QueryResult { Items = await devices.ToListAsync().ConfigureAwait(false), StartIndex = query.Skip ?? 0, TotalRecordCount = count }; } /// public async Task> GetDeviceInfos(DeviceQuery query) { var devices = await GetDevices(query).ConfigureAwait(false); return new QueryResult { Items = devices.Items.Select(device => ToDeviceInfo(device)).ToList(), StartIndex = devices.StartIndex, TotalRecordCount = devices.TotalRecordCount }; } /// public async Task> GetDevicesForUser(Guid? userId, bool? supportsSync) { await using var dbContext = _dbProvider.CreateContext(); var sessions = dbContext.Devices .AsQueryable() .OrderBy(d => d.DeviceId) .ThenByDescending(d => d.DateLastActivity) .AsAsyncEnumerable(); if (supportsSync.HasValue) { sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value); } if (userId.HasValue) { var user = _userManager.GetUserById(userId.Value); sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId)); } var array = await sessions.Select(device => ToDeviceInfo(device)).ToArrayAsync().ConfigureAwait(false); return new QueryResult(array); } /// public async Task DeleteDevice(Device device) { await using var dbContext = _dbProvider.CreateContext(); dbContext.Devices.Remove(device); await dbContext.SaveChangesAsync().ConfigureAwait(false); } /// public bool CanAccessDevice(User user, string deviceId) { if (user == null) { throw new ArgumentException("user not found"); } if (string.IsNullOrEmpty(deviceId)) { throw new ArgumentNullException(nameof(deviceId)); } if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator)) { return true; } return user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase) || !GetCapabilities(deviceId).SupportsPersistentIdentifier; } private DeviceInfo ToDeviceInfo(Device authInfo) { var caps = GetCapabilities(authInfo.DeviceId); return new DeviceInfo { AppName = authInfo.AppName, AppVersion = authInfo.AppVersion, Id = authInfo.DeviceId, LastUserId = authInfo.UserId, LastUserName = authInfo.User.Username, Name = authInfo.DeviceName, DateLastActivity = authInfo.DateLastActivity, IconUrl = caps.IconUrl }; } } }