diff options
27 files changed, 814 insertions, 730 deletions
diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index 4685a03ac..3983824a3 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -4,11 +4,10 @@ using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; @@ -30,7 +29,7 @@ namespace Emby.Server.Implementations.Activity /// </summary> public sealed class ActivityLogEntryPoint : IServerEntryPoint { - private readonly ILogger _logger; + private readonly ILogger<ActivityLogEntryPoint> _logger; private readonly IInstallationManager _installationManager; private readonly ISessionManager _sessionManager; private readonly ITaskManager _taskManager; @@ -38,14 +37,12 @@ namespace Emby.Server.Implementations.Activity private readonly ILocalizationManager _localization; private readonly ISubtitleManager _subManager; private readonly IUserManager _userManager; - private readonly IDeviceManager _deviceManager; /// <summary> /// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class. /// </summary> /// <param name="logger">The logger.</param> /// <param name="sessionManager">The session manager.</param> - /// <param name="deviceManager">The device manager.</param> /// <param name="taskManager">The task manager.</param> /// <param name="activityManager">The activity manager.</param> /// <param name="localization">The localization manager.</param> @@ -55,7 +52,6 @@ namespace Emby.Server.Implementations.Activity public ActivityLogEntryPoint( ILogger<ActivityLogEntryPoint> logger, ISessionManager sessionManager, - IDeviceManager deviceManager, ITaskManager taskManager, IActivityManager activityManager, ILocalizationManager localization, @@ -65,7 +61,6 @@ namespace Emby.Server.Implementations.Activity { _logger = logger; _sessionManager = sessionManager; - _deviceManager = deviceManager; _taskManager = taskManager; _activityManager = activityManager; _localization = localization; @@ -99,52 +94,38 @@ namespace Emby.Server.Implementations.Activity _userManager.UserPolicyUpdated += OnUserPolicyUpdated; _userManager.UserLockedOut += OnUserLockedOut; - _deviceManager.CameraImageUploaded += OnCameraImageUploaded; - return Task.CompletedTask; } - private void OnCameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e) + private async void OnUserLockedOut(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("CameraImageUploadedFrom"), - e.Argument.Device.Name), - Type = NotificationType.CameraImageUploaded.ToString() - }); - } - - private void OnUserLockedOut(object sender, GenericEventArgs<User> e) - { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserLockedOutWithName"), - e.Argument.Name), - Type = NotificationType.UserLockedOut.ToString(), - UserId = e.Argument.Id - }); + await CreateLogEntry(new ActivityLog( + string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("UserLockedOutWithName"), + e.Argument.Name), + NotificationType.UserLockedOut.ToString(), + e.Argument.Id)) + .ConfigureAwait(false); } - private void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) + private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"), e.Provider, Notifications.NotificationEntryPoint.GetItemName(e.Item)), - Type = "SubtitleDownloadFailure", + "SubtitleDownloadFailure", + Guid.Empty) + { ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture), ShortOverview = e.Exception.Message - }); + }).ConfigureAwait(false); } - private void OnPlaybackStopped(object sender, PlaybackStopEventArgs e) + private async void OnPlaybackStopped(object sender, PlaybackStopEventArgs e) { var item = e.MediaInfo; @@ -167,20 +148,19 @@ namespace Emby.Server.Implementations.Activity var user = e.Users[0]; - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), user.Name, GetItemName(item), e.DeviceName), - Type = GetPlaybackStoppedNotificationType(item.MediaType), - UserId = user.Id - }); + GetPlaybackStoppedNotificationType(item.MediaType), + user.Id)) + .ConfigureAwait(false); } - private void OnPlaybackStart(object sender, PlaybackProgressEventArgs e) + private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e) { var item = e.MediaInfo; @@ -203,17 +183,16 @@ namespace Emby.Server.Implementations.Activity var user = e.Users.First(); - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserStartedPlayingItemWithValues"), user.Name, GetItemName(item), e.DeviceName), - Type = GetPlaybackNotificationType(item.MediaType), - UserId = user.Id - }); + GetPlaybackNotificationType(item.MediaType), + user.Id)) + .ConfigureAwait(false); } private static string GetItemName(BaseItemDto item) @@ -263,7 +242,7 @@ namespace Emby.Server.Implementations.Activity return null; } - private void OnSessionEnded(object sender, SessionEventArgs e) + private async void OnSessionEnded(object sender, SessionEventArgs e) { var session = e.SessionInfo; @@ -272,110 +251,108 @@ namespace Emby.Server.Implementations.Activity return; } - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserOfflineFromDevice"), session.UserName, session.DeviceName), - Type = "SessionEnded", + "SessionEnded", + session.UserId) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("LabelIpAddressValue"), session.RemoteEndPoint), - UserId = session.UserId - }); + }).ConfigureAwait(false); } - private void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e) + private async void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e) { var user = e.Argument.User; - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("AuthenticationSucceededWithUserName"), user.Name), - Type = "AuthenticationSucceeded", + "AuthenticationSucceeded", + user.Id) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("LabelIpAddressValue"), e.Argument.SessionInfo.RemoteEndPoint), - UserId = user.Id - }); + }).ConfigureAwait(false); } - private void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e) + private async void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("FailedLoginAttemptWithUserName"), e.Argument.Username), - Type = "AuthenticationFailed", + "AuthenticationFailed", + Guid.Empty) + { + LogSeverity = LogLevel.Error, ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("LabelIpAddressValue"), e.Argument.RemoteEndPoint), - Severity = LogLevel.Error - }); + }).ConfigureAwait(false); } - private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e) + private async void OnUserPolicyUpdated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserPolicyUpdatedWithName"), e.Argument.Name), - Type = "UserPolicyUpdated", - UserId = e.Argument.Id - }); + "UserPolicyUpdated", + e.Argument.Id)) + .ConfigureAwait(false); } - private void OnUserDeleted(object sender, GenericEventArgs<User> e) + private async void OnUserDeleted(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDeletedWithName"), e.Argument.Name), - Type = "UserDeleted" - }); + "UserDeleted", + Guid.Empty)) + .ConfigureAwait(false); } - private void OnUserPasswordChanged(object sender, GenericEventArgs<User> e) + private async void OnUserPasswordChanged(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserPasswordChangedWithName"), e.Argument.Name), - Type = "UserPasswordChanged", - UserId = e.Argument.Id - }); + "UserPasswordChanged", + e.Argument.Id)) + .ConfigureAwait(false); } - private void OnUserCreated(object sender, GenericEventArgs<User> e) + private async void OnUserCreated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserCreatedWithName"), e.Argument.Name), - Type = "UserCreated", - UserId = e.Argument.Id - }); + "UserCreated", + e.Argument.Id)) + .ConfigureAwait(false); } - private void OnSessionStarted(object sender, SessionEventArgs e) + private async void OnSessionStarted(object sender, SessionEventArgs e) { var session = e.SessionInfo; @@ -384,87 +361,90 @@ namespace Emby.Server.Implementations.Activity return; } - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserOnlineFromDevice"), session.UserName, session.DeviceName), - Type = "SessionStarted", + "SessionStarted", + session.UserId) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("LabelIpAddressValue"), - session.RemoteEndPoint), - UserId = session.UserId - }); + session.RemoteEndPoint) + }).ConfigureAwait(false); } - private void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e) + private async void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("PluginUpdatedWithName"), e.Argument.Item1.Name), - Type = NotificationType.PluginUpdateInstalled.ToString(), + NotificationType.PluginUpdateInstalled.ToString(), + Guid.Empty) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("VersionNumber"), e.Argument.Item2.version), Overview = e.Argument.Item2.changelog - }); + }).ConfigureAwait(false); } - private void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e) + private async void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("PluginUninstalledWithName"), e.Argument.Name), - Type = NotificationType.PluginUninstalled.ToString() - }); + NotificationType.PluginUninstalled.ToString(), + Guid.Empty)) + .ConfigureAwait(false); } - private void OnPluginInstalled(object sender, GenericEventArgs<VersionInfo> e) + private async void OnPluginInstalled(object sender, GenericEventArgs<VersionInfo> e) { - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("PluginInstalledWithName"), e.Argument.name), - Type = NotificationType.PluginInstalled.ToString(), + NotificationType.PluginInstalled.ToString(), + Guid.Empty) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("VersionNumber"), e.Argument.version) - }); + }).ConfigureAwait(false); } - private void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e) + private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e) { var installationInfo = e.InstallationInfo; - CreateLogEntry(new ActivityLogEntry - { - Name = string.Format( + await CreateLogEntry(new ActivityLog( + string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("NameInstallFailed"), installationInfo.Name), - Type = NotificationType.InstallationFailed.ToString(), + NotificationType.InstallationFailed.ToString(), + Guid.Empty) + { ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("VersionNumber"), installationInfo.Version), Overview = e.Exception.Message - }); + }).ConfigureAwait(false); } - private void OnTaskCompleted(object sender, TaskCompletionEventArgs e) + private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e) { var result = e.Result; var task = e.Task; @@ -495,22 +475,20 @@ namespace Emby.Server.Implementations.Activity vals.Add(e.Result.LongErrorMessage); } - CreateLogEntry(new ActivityLogEntry + await CreateLogEntry(new ActivityLog( + string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name), + NotificationType.TaskFailed.ToString(), + Guid.Empty) { - Name = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("ScheduledTaskFailedWithName"), - task.Name), - Type = NotificationType.TaskFailed.ToString(), + LogSeverity = LogLevel.Error, Overview = string.Join(Environment.NewLine, vals), - ShortOverview = runningTime, - Severity = LogLevel.Error - }); + ShortOverview = runningTime + }).ConfigureAwait(false); } } - private void CreateLogEntry(ActivityLogEntry entry) - => _activityManager.Create(entry); + private async Task CreateLogEntry(ActivityLog entry) + => await _activityManager.CreateAsync(entry).ConfigureAwait(false); /// <inheritdoc /> public void Dispose() @@ -537,8 +515,6 @@ namespace Emby.Server.Implementations.Activity _userManager.UserDeleted -= OnUserDeleted; _userManager.UserPolicyUpdated -= OnUserPolicyUpdated; _userManager.UserLockedOut -= OnUserLockedOut; - - _deviceManager.CameraImageUploaded -= OnCameraImageUploaded; } /// <summary> @@ -566,7 +542,7 @@ namespace Emby.Server.Implementations.Activity { int months = days / DaysInMonth; values.Add(CreateValueString(months, "month")); - days %= DaysInMonth; + days = days % DaysInMonth; } // Number of days diff --git a/Emby.Server.Implementations/Activity/ActivityManager.cs b/Emby.Server.Implementations/Activity/ActivityManager.cs deleted file mode 100644 index 81bebae3d..000000000 --- a/Emby.Server.Implementations/Activity/ActivityManager.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Activity; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Querying; - -namespace Emby.Server.Implementations.Activity -{ - /// <summary> - /// The activity log manager. - /// </summary> - public class ActivityManager : IActivityManager - { - private readonly IActivityRepository _repo; - private readonly IUserManager _userManager; - - /// <summary> - /// Initializes a new instance of the <see cref="ActivityManager"/> class. - /// </summary> - /// <param name="repo">The activity repository.</param> - /// <param name="userManager">The user manager.</param> - public ActivityManager(IActivityRepository repo, IUserManager userManager) - { - _repo = repo; - _userManager = userManager; - } - - /// <inheritdoc /> - public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated; - - public void Create(ActivityLogEntry entry) - { - entry.Date = DateTime.UtcNow; - - _repo.Create(entry); - - EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(entry)); - } - - /// <inheritdoc /> - public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit) - { - var result = _repo.GetActivityLogEntries(minDate, hasUserId, startIndex, limit); - - foreach (var item in result.Items) - { - if (item.UserId == Guid.Empty) - { - continue; - } - - var user = _userManager.GetUserById(item.UserId); - - if (user != null) - { - var dto = _userManager.GetUserDto(user); - item.UserPrimaryImageTag = dto.PrimaryImageTag; - } - } - - return result; - } - - /// <inheritdoc /> - public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit) - { - return GetActivityLogEntries(minDate, null, startIndex, limit); - } - } -} diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs deleted file mode 100644 index 22796ba3f..000000000 --- a/Emby.Server.Implementations/Activity/ActivityRepository.cs +++ /dev/null @@ -1,308 +0,0 @@ -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.Activity; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Querying; -using Microsoft.Extensions.Logging; -using SQLitePCL.pretty; - -namespace Emby.Server.Implementations.Activity -{ - /// <summary> - /// The activity log repository. - /// </summary> - public class ActivityRepository : BaseSqliteRepository, IActivityRepository - { - private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLog"; - - private readonly IFileSystem _fileSystem; - - /// <summary> - /// Initializes a new instance of the <see cref="ActivityRepository"/> class. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="appPaths">The server application paths.</param> - /// <param name="fileSystem">The filesystem.</param> - public ActivityRepository(ILogger<ActivityRepository> logger, IServerApplicationPaths appPaths, IFileSystem fileSystem) - : base(logger) - { - DbFilePath = Path.Combine(appPaths.DataPath, "activitylog.db"); - _fileSystem = fileSystem; - } - - /// <summary> - /// Initializes the <see cref="ActivityRepository"/>. - /// </summary> - public void Initialize() - { - try - { - InitializeInternal(); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error loading database file. Will reset and retry."); - - _fileSystem.DeleteFile(DbFilePath); - - InitializeInternal(); - } - } - - private void InitializeInternal() - { - using var connection = GetConnection(); - connection.RunQueries(new[] - { - "create table if not exists ActivityLog (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, Overview TEXT, ShortOverview TEXT, Type TEXT NOT NULL, ItemId TEXT, UserId TEXT, DateCreated DATETIME NOT NULL, LogSeverity TEXT NOT NULL)", - "drop index if exists idx_ActivityLogEntries" - }); - - TryMigrate(connection); - } - - private void TryMigrate(ManagedConnection connection) - { - try - { - if (TableExists(connection, "ActivityLogEntries")) - { - connection.RunQueries(new[] - { - "INSERT INTO ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) SELECT Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity FROM ActivityLogEntries", - "drop table if exists ActivityLogEntries" - }); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Error migrating activity log database"); - } - } - - /// <inheritdoc /> - public void Create(ActivityLogEntry entry) - { - if (entry == null) - { - throw new ArgumentNullException(nameof(entry)); - } - - using var connection = GetConnection(); - connection.RunInTransaction(db => - { - using var statement = db.PrepareStatement("insert into ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)"); - statement.TryBind("@Name", entry.Name); - - statement.TryBind("@Overview", entry.Overview); - statement.TryBind("@ShortOverview", entry.ShortOverview); - statement.TryBind("@Type", entry.Type); - statement.TryBind("@ItemId", entry.ItemId); - - if (entry.UserId.Equals(Guid.Empty)) - { - statement.TryBindNull("@UserId"); - } - else - { - statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture)); - } - - statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue()); - statement.TryBind("@LogSeverity", entry.Severity.ToString()); - - statement.MoveNext(); - }, TransactionMode); - } - - /// <summary> - /// Adds the provided <see cref="ActivityLogEntry"/> to this repository. - /// </summary> - /// <param name="entry">The activity log entry.</param> - /// <exception cref="ArgumentNullException">If entry is null.</exception> - public void Update(ActivityLogEntry entry) - { - if (entry == null) - { - throw new ArgumentNullException(nameof(entry)); - } - - using var connection = GetConnection(); - connection.RunInTransaction(db => - { - using var statement = db.PrepareStatement("Update ActivityLog set Name=@Name,Overview=@Overview,ShortOverview=@ShortOverview,Type=@Type,ItemId=@ItemId,UserId=@UserId,DateCreated=@DateCreated,LogSeverity=@LogSeverity where Id=@Id"); - statement.TryBind("@Id", entry.Id); - - statement.TryBind("@Name", entry.Name); - statement.TryBind("@Overview", entry.Overview); - statement.TryBind("@ShortOverview", entry.ShortOverview); - statement.TryBind("@Type", entry.Type); - statement.TryBind("@ItemId", entry.ItemId); - - if (entry.UserId.Equals(Guid.Empty)) - { - statement.TryBindNull("@UserId"); - } - else - { - statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture)); - } - - statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue()); - statement.TryBind("@LogSeverity", entry.Severity.ToString()); - - statement.MoveNext(); - }, TransactionMode); - } - - /// <inheritdoc /> - public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit) - { - var commandText = BaseActivitySelectText; - var whereClauses = new List<string>(); - - if (minDate.HasValue) - { - whereClauses.Add("DateCreated>=@DateCreated"); - } - - if (hasUserId.HasValue) - { - whereClauses.Add(hasUserId.Value ? "UserId not null" : "UserId is null"); - } - - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - if (startIndex.HasValue && startIndex.Value > 0) - { - var pagingWhereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - whereClauses.Add( - string.Format( - CultureInfo.InvariantCulture, - "Id NOT IN (SELECT Id FROM ActivityLog {0} ORDER BY DateCreated DESC LIMIT {1})", - pagingWhereText, - startIndex.Value)); - } - - var whereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - commandText += whereText; - - commandText += " ORDER BY DateCreated DESC"; - - if (limit.HasValue) - { - commandText += " LIMIT " + limit.Value.ToString(CultureInfo.InvariantCulture); - } - - var statementTexts = new[] - { - commandText, - "select count (Id) from ActivityLog" + whereTextWithoutPaging - }; - - var list = new List<ActivityLogEntry>(); - var result = new QueryResult<ActivityLogEntry>(); - - using var connection = GetConnection(true); - connection.RunInTransaction( - db => - { - var statements = PrepareAll(db, statementTexts).ToList(); - - using (var statement = statements[0]) - { - if (minDate.HasValue) - { - statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); - } - - list.AddRange(statement.ExecuteQuery().Select(GetEntry)); - } - - using (var statement = statements[1]) - { - if (minDate.HasValue) - { - statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); - } - - result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); - } - }, - ReadTransactionMode); - - result.Items = list; - return result; - } - - private static ActivityLogEntry GetEntry(IReadOnlyList<IResultSetValue> reader) - { - var index = 0; - - var info = new ActivityLogEntry - { - Id = reader[index].ToInt64() - }; - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Name = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Overview = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.ShortOverview = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Type = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.ItemId = reader[index].ToString(); - } - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.UserId = new Guid(reader[index].ToString()); - } - - index++; - info.Date = reader[index].ReadDateTime(); - - index++; - if (reader[index].SQLiteType != SQLiteType.Null) - { - info.Severity = Enum.Parse<LogLevel>(reader[index].ToString(), true); - } - - return info; - } - } -} diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 61e3faf5c..371b5a5b9 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -22,7 +22,6 @@ using Emby.Dlna.Ssdp; using Emby.Drawing; using Emby.Notifications; using Emby.Photos; -using Emby.Server.Implementations.Activity; using Emby.Server.Implementations.Archiving; using Emby.Server.Implementations.Channels; using Emby.Server.Implementations.Collections; @@ -47,6 +46,8 @@ using Emby.Server.Implementations.Session; using Emby.Server.Implementations.SocketSharp; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; +using Jellyfin.Server.Implementations; +using Jellyfin.Server.Implementations.Activity; using MediaBrowser.Api; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -102,6 +103,7 @@ using MediaBrowser.WebDashboard.Api; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; @@ -545,6 +547,13 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>(); + // TODO: properly set up scoping and switch to AddDbContextPool + serviceCollection.AddDbContext<JellyfinDb>( + options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"), + ServiceLifetime.Transient); + + serviceCollection.AddSingleton<JellyfinDbProvider>(); + serviceCollection.AddSingleton(_fileSystemManager); serviceCollection.AddSingleton<TvdbClientManager>(); @@ -655,7 +664,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); - serviceCollection.AddSingleton<IActivityRepository, ActivityRepository>(); serviceCollection.AddSingleton<IActivityManager, ActivityManager>(); serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>(); @@ -688,7 +696,6 @@ namespace Emby.Server.Implementations ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize(); ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize(); ((SqliteUserRepository)Resolve<IUserRepository>()).Initialize(); - ((ActivityRepository)Resolve<IActivityRepository>()).Initialize(); SetStaticProperties(); diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs index 579cb895e..7017be6eb 100644 --- a/Emby.Server.Implementations/Devices/DeviceManager.cs +++ b/Emby.Server.Implementations/Devices/DeviceManager.cs @@ -34,7 +34,6 @@ namespace Emby.Server.Implementations.Devices private readonly IJsonSerializer _json; private readonly IUserManager _userManager; private readonly IFileSystem _fileSystem; - private readonly ILibraryMonitor _libraryMonitor; private readonly IServerConfigurationManager _config; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localizationManager; @@ -43,9 +42,6 @@ namespace Emby.Server.Implementations.Devices public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated; - public event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded; - - private readonly object _cameraUploadSyncLock = new object(); private readonly object _capabilitiesSyncLock = new object(); public DeviceManager( @@ -55,13 +51,11 @@ namespace Emby.Server.Implementations.Devices ILocalizationManager localizationManager, IUserManager userManager, IFileSystem fileSystem, - ILibraryMonitor libraryMonitor, IServerConfigurationManager config) { _json = json; _userManager = userManager; _fileSystem = fileSystem; - _libraryMonitor = libraryMonitor; _config = config; _libraryManager = libraryManager; _localizationManager = localizationManager; @@ -194,105 +188,6 @@ namespace Emby.Server.Implementations.Devices return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N", CultureInfo.InvariantCulture)); } - public ContentUploadHistory GetCameraUploadHistory(string deviceId) - { - var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); - - lock (_cameraUploadSyncLock) - { - try - { - return _json.DeserializeFromFile<ContentUploadHistory>(path); - } - catch (IOException) - { - return new ContentUploadHistory - { - DeviceId = deviceId - }; - } - } - } - - public async Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file) - { - var device = GetDevice(deviceId, false); - var uploadPathInfo = GetUploadPath(device); - - var path = uploadPathInfo.Item1; - - if (!string.IsNullOrWhiteSpace(file.Album)) - { - path = Path.Combine(path, _fileSystem.GetValidFilename(file.Album)); - } - - path = Path.Combine(path, file.Name); - path = Path.ChangeExtension(path, MimeTypes.ToExtension(file.MimeType) ?? "jpg"); - - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - await EnsureLibraryFolder(uploadPathInfo.Item2, uploadPathInfo.Item3).ConfigureAwait(false); - - _libraryMonitor.ReportFileSystemChangeBeginning(path); - - try - { - using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - await stream.CopyToAsync(fs).ConfigureAwait(false); - } - - AddCameraUpload(deviceId, file); - } - finally - { - _libraryMonitor.ReportFileSystemChangeComplete(path, true); - } - - if (CameraImageUploaded != null) - { - CameraImageUploaded?.Invoke(this, new GenericEventArgs<CameraImageUploadInfo> - { - Argument = new CameraImageUploadInfo - { - Device = device, - FileInfo = file - } - }); - } - } - - private void AddCameraUpload(string deviceId, LocalFileInfo file) - { - var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json"); - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - lock (_cameraUploadSyncLock) - { - 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(); - - _json.SerializeToFile(history, path); - } - } - internal Task EnsureLibraryFolder(string path, string name) { var existingFolders = _libraryManager diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 44fc932e3..dccbe2a9a 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <!-- ProjectGuid is only included as a requirement for SonarQube analysis --> <PropertyGroup> @@ -9,6 +9,7 @@ <ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" /> <ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" /> <ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" /> + <ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> @@ -50,7 +51,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFramework>netcoreapp3.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/Jellyfin.Data/Entities/ActivityLog.cs b/Jellyfin.Data/Entities/ActivityLog.cs new file mode 100644 index 000000000..df3026a77 --- /dev/null +++ b/Jellyfin.Data/Entities/ActivityLog.cs @@ -0,0 +1,150 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Data.Entities +{ + public partial class ActivityLog + { + partial void Init(); + + /// <summary> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </summary> + protected ActivityLog() + { + Init(); + } + + /// <summary> + /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. + /// </summary> + public static ActivityLog CreateActivityLogUnsafe() + { + return new ActivityLog(); + } + + /// <summary> + /// Public constructor with required data + /// </summary> + /// <param name="name"></param> + /// <param name="type"></param> + /// <param name="userId"></param> + /// <param name="datecreated"></param> + /// <param name="logSeverity"></param> + public ActivityLog(string name, string type, Guid userId) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentNullException(nameof(type)); + } + + this.Name = name; + this.Type = type; + this.UserId = userId; + this.DateCreated = DateTime.UtcNow; + this.LogSeverity = LogLevel.Trace; + + Init(); + } + + /// <summary> + /// Static create function (for use in LINQ queries, etc.) + /// </summary> + /// <param name="name"></param> + /// <param name="type"></param> + /// <param name="userId"></param> + /// <param name="datecreated"></param> + /// <param name="logseverity"></param> + public static ActivityLog Create(string name, string type, Guid userId) + { + return new ActivityLog(name, type, userId); + } + + /************************************************************************* + * Properties + *************************************************************************/ + + /// <summary> + /// Identity, Indexed, Required + /// </summary> + [Key] + [Required] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Required, Max length = 512 + /// </summary> + [Required] + [MaxLength(512)] + [StringLength(512)] + public string Name { get; set; } + + /// <summary> + /// Max length = 512 + /// </summary> + [MaxLength(512)] + [StringLength(512)] + public string Overview { get; set; } + + /// <summary> + /// Max length = 512 + /// </summary> + [MaxLength(512)] + [StringLength(512)] + public string ShortOverview { get; set; } + + /// <summary> + /// Required, Max length = 256 + /// </summary> + [Required] + [MaxLength(256)] + [StringLength(256)] + public string Type { get; set; } + + /// <summary> + /// Required + /// </summary> + [Required] + public Guid UserId { get; set; } + + /// <summary> + /// Max length = 256 + /// </summary> + [MaxLength(256)] + [StringLength(256)] + public string ItemId { get; set; } + + /// <summary> + /// Required + /// </summary> + [Required] + public DateTime DateCreated { get; set; } + + /// <summary> + /// Required + /// </summary> + [Required] + public LogLevel LogSeverity { get; set; } + + /// <summary> + /// Required, ConcurrencyToken. + /// </summary> + [ConcurrencyCheck] + [Required] + public uint RowVersion { get; set; } + + public void OnSavingChanges() + { + RowVersion++; + } + } +} + diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 73ea593b0..8eae366ba 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -1,12 +1,30 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> + <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + + <!-- Code analysers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="2.2.4" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.4" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.3" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.3" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.3"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> </ItemGroup> </Project> diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs new file mode 100644 index 000000000..531b529dc --- /dev/null +++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.Querying; + +namespace Jellyfin.Server.Implementations.Activity +{ + /// <summary> + /// Manages the storage and retrieval of <see cref="ActivityLog"/> instances. + /// </summary> + public class ActivityManager : IActivityManager + { + private readonly JellyfinDbProvider _provider; + + /// <summary> + /// Initializes a new instance of the <see cref="ActivityManager"/> class. + /// </summary> + /// <param name="provider">The Jellyfin database provider.</param> + public ActivityManager(JellyfinDbProvider provider) + { + _provider = provider; + } + + /// <inheritdoc/> + public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated; + + /// <inheritdoc/> + public void Create(ActivityLog entry) + { + using var dbContext = _provider.CreateContext(); + dbContext.ActivityLogs.Add(entry); + dbContext.SaveChanges(); + + EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry))); + } + + /// <inheritdoc/> + public async Task CreateAsync(ActivityLog entry) + { + using var dbContext = _provider.CreateContext(); + await dbContext.ActivityLogs.AddAsync(entry); + await dbContext.SaveChangesAsync().ConfigureAwait(false); + + EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry))); + } + + /// <inheritdoc/> + public QueryResult<ActivityLogEntry> GetPagedResult( + Func<IQueryable<ActivityLog>, IEnumerable<ActivityLog>> func, + int? startIndex, + int? limit) + { + using var dbContext = _provider.CreateContext(); + + var result = func.Invoke(dbContext.ActivityLogs).AsQueryable(); + + if (startIndex.HasValue) + { + result = result.Where(entry => entry.Id >= startIndex.Value); + } + + if (limit.HasValue) + { + result = result.OrderByDescending(entry => entry.DateCreated).Take(limit.Value); + } + + // This converts the objects from the new database model to the old for compatibility with the existing API. + var list = result.Select(entry => ConvertToOldModel(entry)).ToList(); + + return new QueryResult<ActivityLogEntry>() + { + Items = list, + TotalRecordCount = list.Count + }; + } + + /// <inheritdoc/> + public QueryResult<ActivityLogEntry> GetPagedResult(int? startIndex, int? limit) + { + return GetPagedResult(logs => logs, startIndex, limit); + } + + private static ActivityLogEntry ConvertToOldModel(ActivityLog entry) + { + return new ActivityLogEntry + { + Id = entry.Id, + Name = entry.Name, + Overview = entry.Overview, + ShortOverview = entry.ShortOverview, + Type = entry.Type, + ItemId = entry.ItemId, + UserId = entry.UserId, + Date = entry.DateCreated, + Severity = entry.LogSeverity + }; + } + } +} diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 76343edf9..6fc8d251b 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -15,6 +15,7 @@ namespace Jellyfin.Server.Implementations /// <inheritdoc/> public partial class JellyfinDb : DbContext { + public virtual DbSet<ActivityLog> ActivityLogs { get; set; } /*public virtual DbSet<Artwork> Artwork { get; set; } public virtual DbSet<Book> Books { get; set; } public virtual DbSet<BookMetadata> BookMetadata { get; set; } @@ -49,6 +50,7 @@ namespace Jellyfin.Server.Implementations public virtual DbSet<Preference> Preferences { get; set; } public virtual DbSet<ProviderMapping> ProviderMappings { get; set; } public virtual DbSet<Rating> Ratings { get; set; } + /// <summary> /// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to /// store review ratings, not age ratings @@ -93,8 +95,10 @@ namespace Jellyfin.Server.Implementations modelBuilder.HasDefaultSchema("jellyfin"); /*modelBuilder.Entity<Artwork>().HasIndex(t => t.Kind); + modelBuilder.Entity<Genre>().HasIndex(t => t.Name) .IsUnique(); + modelBuilder.Entity<LibraryItem>().HasIndex(t => t.UrlId) .IsUnique();*/ diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs new file mode 100644 index 000000000..8fdeab088 --- /dev/null +++ b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Jellyfin.Server.Implementations +{ + /// <summary> + /// Factory class for generating new <see cref="JellyfinDb"/> instances. + /// </summary> + public class JellyfinDbProvider + { + private readonly IServiceProvider _serviceProvider; + + /// <summary> + /// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class. + /// </summary> + /// <param name="serviceProvider">The application's service provider.</param> + public JellyfinDbProvider(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + serviceProvider.GetService<JellyfinDb>().Database.Migrate(); + } + + /// <summary> + /// Creates a new <see cref="JellyfinDb"/> context. + /// </summary> + /// <returns>The newly created context.</returns> + public JellyfinDb CreateContext() + { + return _serviceProvider.GetService<JellyfinDb>(); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.Designer.cs new file mode 100644 index 000000000..e1ee9b34a --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.Designer.cs @@ -0,0 +1,73 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1601 + +// <auto-generated /> +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDb))] + [Migration("20200502231229_InitialSchema")] + partial class InitialSchema + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "3.1.3"); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<string>("ItemId") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property<int>("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property<string>("Overview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("ShortOverview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property<string>("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ActivityLog"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.cs b/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.cs new file mode 100644 index 000000000..42fac865c --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/20200502231229_InitialSchema.cs @@ -0,0 +1,46 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1601 + +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Jellyfin.Server.Implementations.Migrations +{ + public partial class InitialSchema : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "jellyfin"); + + migrationBuilder.CreateTable( + name: "ActivityLog", + schema: "jellyfin", + columns: table => new + { + Id = table.Column<int>(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column<string>(maxLength: 512, nullable: false), + Overview = table.Column<string>(maxLength: 512, nullable: true), + ShortOverview = table.Column<string>(maxLength: 512, nullable: true), + Type = table.Column<string>(maxLength: 256, nullable: false), + UserId = table.Column<Guid>(nullable: false), + ItemId = table.Column<string>(maxLength: 256, nullable: true), + DateCreated = table.Column<DateTime>(nullable: false), + LogSeverity = table.Column<int>(nullable: false), + RowVersion = table.Column<uint>(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ActivityLog", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ActivityLog", + schema: "jellyfin"); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs new file mode 100644 index 000000000..23a0fdc78 --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/DesignTimeJellyfinDbFactory.cs @@ -0,0 +1,23 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1601 + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace Jellyfin.Server.Implementations.Migrations +{ + /// <summary> + /// The design time factory for <see cref="JellyfinDb"/>. + /// This is only used for the creation of migrations and not during runtime. + /// </summary> + internal class DesignTimeJellyfinDbFactory : IDesignTimeDbContextFactory<JellyfinDb> + { + public JellyfinDb CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder<JellyfinDb>(); + optionsBuilder.UseSqlite("Data Source=jellyfin.db"); + + return new JellyfinDb(optionsBuilder.Options); + } + } +} diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs new file mode 100644 index 000000000..27f5fe24b --- /dev/null +++ b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs @@ -0,0 +1,68 @@ +// <auto-generated /> +using System; +using Jellyfin.Server.Implementations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Jellyfin.Server.Implementations.Migrations +{ + [DbContext(typeof(JellyfinDb))] + partial class JellyfinDbModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("jellyfin") + .HasAnnotation("ProductVersion", "3.1.3"); + + modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property<DateTime>("DateCreated") + .HasColumnType("TEXT"); + + b.Property<string>("ItemId") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property<int>("LogSeverity") + .HasColumnType("INTEGER"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property<string>("Overview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property<uint>("RowVersion") + .IsConcurrencyToken() + .HasColumnType("INTEGER"); + + b.Property<string>("ShortOverview") + .HasColumnType("TEXT") + .HasMaxLength(512); + + b.Property<string>("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property<Guid>("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("ActivityLog"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 88114d999..4194070aa 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -13,6 +13,9 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <Nullable>enable</Nullable> + + <!-- Used for generating migrations for EF Core --> + <GenerateRuntimeConfigurationFiles>True</GenerateRuntimeConfigurationFiles> </PropertyGroup> <ItemGroup> @@ -41,6 +44,10 @@ <ItemGroup> <PackageReference Include="CommandLineParser" Version="2.7.82" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.3"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.3" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.3" /> <PackageReference Include="prometheus-net" Version="3.5.0" /> diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index ca1748282..c7fa2af0c 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -17,7 +17,8 @@ namespace Jellyfin.Server.Migrations private static readonly Type[] _migrationTypes = { typeof(Routines.DisableTranscodingThrottling), - typeof(Routines.CreateUserLoggingConfigFile) + typeof(Routines.CreateUserLoggingConfigFile), + typeof(Routines.MigrateActivityLogDb) }; /// <summary> diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs new file mode 100644 index 000000000..faa163d19 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Emby.Server.Implementations.Data; +using Jellyfin.Data.Entities; +using Jellyfin.Server.Implementations; +using MediaBrowser.Controller; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SQLitePCL.pretty; + +namespace Jellyfin.Server.Migrations.Routines +{ + /// <summary> + /// The migration routine for migrating the activity log database to EF Core. + /// </summary> + public class MigrateActivityLogDb : IMigrationRoutine + { + private const string DbFilename = "activitylog.db"; + + private readonly ILogger<MigrateActivityLogDb> _logger; + private readonly JellyfinDbProvider _provider; + private readonly IServerApplicationPaths _paths; + + /// <summary> + /// Initializes a new instance of the <see cref="MigrateActivityLogDb"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="paths">The server application paths.</param> + /// <param name="provider">The database provider.</param> + public MigrateActivityLogDb(ILogger<MigrateActivityLogDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider) + { + _logger = logger; + _provider = provider; + _paths = paths; + } + + /// <inheritdoc/> + public Guid Id => Guid.Parse("3793eb59-bc8c-456c-8b9f-bd5a62a42978"); + + /// <inheritdoc/> + public string Name => "MigrateActivityLogDatabase"; + + /// <inheritdoc/> + public void Perform() + { + var logLevelDictionary = new Dictionary<string, LogLevel>(StringComparer.OrdinalIgnoreCase) + { + { "None", LogLevel.None }, + { "Trace", LogLevel.Trace }, + { "Debug", LogLevel.Debug }, + { "Information", LogLevel.Information }, + { "Info", LogLevel.Information }, + { "Warn", LogLevel.Warning }, + { "Warning", LogLevel.Warning }, + { "Error", LogLevel.Error }, + { "Critical", LogLevel.Critical } + }; + + var dataPath = _paths.DataPath; + using (var connection = SQLite3.Open( + Path.Combine(dataPath, DbFilename), + ConnectionFlags.ReadOnly, + null)) + { + _logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin."); + using var dbContext = _provider.CreateContext(); + + var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id ASC"); + + // Make sure that the database is empty in case of failed migration due to power outages, etc. + dbContext.ActivityLogs.RemoveRange(dbContext.ActivityLogs); + dbContext.SaveChanges(); + // Reset the autoincrement counter + dbContext.Database.ExecuteSqlRaw("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'ActivityLog';"); + dbContext.SaveChanges(); + + foreach (var entry in queryResult) + { + var newEntry = new ActivityLog( + entry[1].ToString(), + entry[4].ToString(), + entry[6].SQLiteType == SQLiteType.Null ? Guid.Empty : Guid.Parse(entry[6].ToString())) + { + DateCreated = entry[7].ReadDateTime(), + LogSeverity = logLevelDictionary[entry[8].ToString()] + }; + + if (entry[2].SQLiteType != SQLiteType.Null) + { + newEntry.Overview = entry[2].ToString(); + } + + if (entry[3].SQLiteType != SQLiteType.Null) + { + newEntry.ShortOverview = entry[3].ToString(); + } + + if (entry[5].SQLiteType != SQLiteType.Null) + { + newEntry.ItemId = entry[5].ToString(); + } + + // Since code references the Id of the entries, this needs to be inserted in order. + // In order to do that, this is needed because EF Core doesn't provide a way to guarantee ordering for bulk inserts. + dbContext.ActivityLogs.Add(newEntry); + dbContext.SaveChanges(); + } + } + + try + { + File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old")); + } + catch (IOException e) + { + _logger.LogError(e, "Error renaming legacy activity log database to 'activitylog.db.old'"); + } + } + } +} diff --git a/MediaBrowser.Api/Devices/DeviceService.cs b/MediaBrowser.Api/Devices/DeviceService.cs index 7004a2559..53eb9667d 100644 --- a/MediaBrowser.Api/Devices/DeviceService.cs +++ b/MediaBrowser.Api/Devices/DeviceService.cs @@ -1,5 +1,4 @@ using System.IO; -using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Net; @@ -116,11 +115,6 @@ namespace MediaBrowser.Api.Devices return _deviceManager.GetDeviceOptions(request.Id); } - public object Get(GetCameraUploads request) - { - return ToOptimizedResult(_deviceManager.GetCameraUploadHistory(request.DeviceId)); - } - public void Delete(DeleteDevice request) { var sessions = _authRepo.Get(new AuthenticationInfoQuery @@ -134,35 +128,5 @@ namespace MediaBrowser.Api.Devices _sessionManager.Logout(session); } } - - public Task Post(PostCameraUpload request) - { - var deviceId = Request.QueryString["DeviceId"]; - var album = Request.QueryString["Album"]; - var id = Request.QueryString["Id"]; - var name = Request.QueryString["Name"]; - var req = Request.Response.HttpContext.Request; - - if (req.HasFormContentType) - { - var file = req.Form.Files.Count == 0 ? null : req.Form.Files[0]; - - return _deviceManager.AcceptCameraUpload(deviceId, file.OpenReadStream(), new LocalFileInfo - { - MimeType = file.ContentType, - Album = album, - Name = name, - Id = id - }); - } - - return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo - { - MimeType = Request.ContentType, - Album = album, - Name = name, - Id = id - }); - } } } diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index a54640b2f..93852e970 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -759,13 +759,12 @@ namespace MediaBrowser.Api.Library { try { - _activityManager.Create(new ActivityLogEntry + _activityManager.Create(new Jellyfin.Data.Entities.ActivityLog( + string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Name, item.Name), + "UserDownloadingContent", + auth.UserId) { - Name = string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Name, item.Name), - Type = "UserDownloadingContent", ShortOverview = string.Format(_localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device), - UserId = auth.UserId - }); } catch diff --git a/MediaBrowser.Api/System/ActivityLogService.cs b/MediaBrowser.Api/System/ActivityLogService.cs index f95fa7ca0..f2c37d711 100644 --- a/MediaBrowser.Api/System/ActivityLogService.cs +++ b/MediaBrowser.Api/System/ActivityLogService.cs @@ -1,5 +1,3 @@ -using System; -using System.Globalization; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Activity; @@ -49,11 +47,7 @@ namespace MediaBrowser.Api.System public object Get(GetActivityLogs request) { - DateTime? minDate = string.IsNullOrWhiteSpace(request.MinDate) ? - (DateTime?)null : - DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); - - var result = _activityManager.GetActivityLogEntries(minDate, request.HasUserId, request.StartIndex, request.Limit); + var result = _activityManager.GetPagedResult(request.StartIndex, request.Limit); return ToOptimizedResult(result); } diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 77d567631..4256bdb10 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -12,11 +12,6 @@ namespace MediaBrowser.Controller.Devices public interface IDeviceManager { /// <summary> - /// Occurs when [camera image uploaded]. - /// </summary> - event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded; - - /// <summary> /// Saves the capabilities. /// </summary> /// <param name="reportedId">The reported identifier.</param> @@ -46,22 +41,6 @@ namespace MediaBrowser.Controller.Devices QueryResult<DeviceInfo> GetDevices(DeviceQuery query); /// <summary> - /// Gets the upload history. - /// </summary> - /// <param name="deviceId">The device identifier.</param> - /// <returns>ContentUploadHistory.</returns> - ContentUploadHistory GetCameraUploadHistory(string deviceId); - - /// <summary> - /// Accepts the upload. - /// </summary> - /// <param name="deviceId">The device identifier.</param> - /// <param name="stream">The stream.</param> - /// <param name="file">The file.</param> - /// <returns>Task.</returns> - Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file); - - /// <summary> /// Determines whether this instance [can access device] the specified user identifier. /// </summary> bool CanAccessDevice(User user, string deviceId); diff --git a/MediaBrowser.Model/Activity/ActivityLogEntry.cs b/MediaBrowser.Model/Activity/ActivityLogEntry.cs index 80f01b66e..5ab904394 100644 --- a/MediaBrowser.Model/Activity/ActivityLogEntry.cs +++ b/MediaBrowser.Model/Activity/ActivityLogEntry.cs @@ -59,6 +59,7 @@ namespace MediaBrowser.Model.Activity /// Gets or sets the user primary image tag. /// </summary> /// <value>The user primary image tag.</value> + [Obsolete("UserPrimaryImageTag is not used.")] public string UserPrimaryImageTag { get; set; } /// <summary> diff --git a/MediaBrowser.Model/Activity/IActivityManager.cs b/MediaBrowser.Model/Activity/IActivityManager.cs index f336f5272..6742dc8fc 100644 --- a/MediaBrowser.Model/Activity/IActivityManager.cs +++ b/MediaBrowser.Model/Activity/IActivityManager.cs @@ -1,6 +1,10 @@ #pragma warning disable CS1591 using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Jellyfin.Data.Entities; using MediaBrowser.Model.Events; using MediaBrowser.Model.Querying; @@ -10,10 +14,15 @@ namespace MediaBrowser.Model.Activity { event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated; - void Create(ActivityLogEntry entry); + void Create(ActivityLog entry); - QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit); + Task CreateAsync(ActivityLog entry); - QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? x, int? y); + QueryResult<ActivityLogEntry> GetPagedResult(int? startIndex, int? limit); + + QueryResult<ActivityLogEntry> GetPagedResult( + Func<IQueryable<ActivityLog>, IEnumerable<ActivityLog>> func, + int? startIndex, + int? limit); } } diff --git a/MediaBrowser.Model/Activity/IActivityRepository.cs b/MediaBrowser.Model/Activity/IActivityRepository.cs deleted file mode 100644 index 66144ec47..000000000 --- a/MediaBrowser.Model/Activity/IActivityRepository.cs +++ /dev/null @@ -1,14 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Model.Querying; - -namespace MediaBrowser.Model.Activity -{ - public interface IActivityRepository - { - void Create(ActivityLogEntry entry); - - QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? z, int? startIndex, int? limit); - } -} diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index b41d0af1d..5c6e313e0 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -37,6 +37,9 @@ <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\Jellyfin.Data\Jellyfin.Data.csproj" /> + </ItemGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 6b9fed83b..e100c0b1c 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -66,9 +66,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementat EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Controller.Tests", "tests\Jellyfin.Controller.Tests\Jellyfin.Controller.Tests.csproj", "{462584F7-5023-4019-9EAC-B98CA458C0A0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Data", "Jellyfin.Data\Jellyfin.Data.csproj", "{F03299F2-469F-40EF-A655-3766F97A5702}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Data", "Jellyfin.Data\Jellyfin.Data.csproj", "{F03299F2-469F-40EF-A655-3766F97A5702}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{DAE48069-6D86-4BA6-B148-D1D49B6DDA52}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Server.Implementations", "Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj", "{22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.Api.Tests", "tests\MediaBrowser.Api.Tests\MediaBrowser.Api.Tests.csproj", "{7C93C84F-105C-48E5-A878-406FA0A5B296}" EndProject Global @@ -185,10 +186,10 @@ Global {F03299F2-469F-40EF-A655-3766F97A5702}.Debug|Any CPU.Build.0 = Debug|Any CPU {F03299F2-469F-40EF-A655-3766F97A5702}.Release|Any CPU.ActiveCfg = Release|Any CPU {F03299F2-469F-40EF-A655-3766F97A5702}.Release|Any CPU.Build.0 = Release|Any CPU - {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Debug|Any CPU.Build.0 = Debug|Any CPU - {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Release|Any CPU.ActiveCfg = Release|Any CPU - {22C7DA3A-94F2-4E86-9CE6-86AB02B4F843}.Release|Any CPU.Build.0 = Release|Any CPU + {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DAE48069-6D86-4BA6-B148-D1D49B6DDA52}.Release|Any CPU.Build.0 = Release|Any CPU {7C93C84F-105C-48E5-A878-406FA0A5B296}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7C93C84F-105C-48E5-A878-406FA0A5B296}.Debug|Any CPU.Build.0 = Debug|Any CPU {7C93C84F-105C-48E5-A878-406FA0A5B296}.Release|Any CPU.ActiveCfg = Release|Any CPU |
