diff --git a/frontend/src/App/AppUpdatedModalContent.js b/frontend/src/App/AppUpdatedModalContent.js index c08dc6177..045d74007 100644 --- a/frontend/src/App/AppUpdatedModalContent.js +++ b/frontend/src/App/AppUpdatedModalContent.js @@ -10,9 +10,43 @@ import ModalFooter from 'Components/Modal/ModalFooter'; import UpdateChanges from 'System/Updates/UpdateChanges'; import styles from './AppUpdatedModalContent.css'; +function mergeUpdates(items, version, prevVersion) { + let installedIndex = items.findIndex((u) => u.version === version); + let installedPreviouslyIndex = items.findIndex((u) => u.version === prevVersion); + + if (installedIndex === -1) { + installedIndex = 0; + } + + if (installedPreviouslyIndex === -1) { + installedPreviouslyIndex = items.length; + } else if (installedPreviouslyIndex === installedIndex && items.size()) { + installedPreviouslyIndex += 1; + } + + const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex); + + if (!appliedUpdates.length) { + return null; + } + + const appliedChanges = { new: [], fixed: [] }; + appliedUpdates.forEach((u) => { + if (u.changes) { + appliedChanges.new.push(... u.changes.new); + appliedChanges.fixed.push(... u.changes.fixed); + } + }); + + const mergedUpdate = Object.assign({}, appliedUpdates[0], { changes: appliedChanges }); + + return mergedUpdate; +} + function AppUpdatedModalContent(props) { const { version, + prevVersion, isPopulated, error, items, @@ -20,7 +54,7 @@ function AppUpdatedModalContent(props) { onModalClose } = props; - const update = items[0]; + const update = mergeUpdates(items, version, prevVersion); return ( @@ -30,7 +64,7 @@ function AppUpdatedModalContent(props) {
- Version {version} of Sonarr has been installed, in order to get the latest changes you'll need to reload Sonarr. + Sonarr has been updated to version {version}, in order to get the latest changes you'll need to reload Sonarr.
{ @@ -88,6 +122,7 @@ function AppUpdatedModalContent(props) { AppUpdatedModalContent.propTypes = { version: PropTypes.string.isRequired, + prevVersion: PropTypes.string, isPopulated: PropTypes.bool.isRequired, error: PropTypes.object, items: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/frontend/src/App/AppUpdatedModalContentConnector.js b/frontend/src/App/AppUpdatedModalContentConnector.js index b252868ce..4100ee674 100644 --- a/frontend/src/App/AppUpdatedModalContentConnector.js +++ b/frontend/src/App/AppUpdatedModalContentConnector.js @@ -8,8 +8,9 @@ import AppUpdatedModalContent from './AppUpdatedModalContent'; function createMapStateToProps() { return createSelector( (state) => state.app.version, + (state) => state.app.prevVersion, (state) => state.system.updates, - (version, updates) => { + (version, prevVersion, updates) => { const { isPopulated, error, @@ -18,6 +19,7 @@ function createMapStateToProps() { return { version, + prevVersion, isPopulated, error, items diff --git a/frontend/src/Store/Actions/appActions.js b/frontend/src/Store/Actions/appActions.js index 81c61ca80..d19bffe06 100644 --- a/frontend/src/Store/Actions/appActions.js +++ b/frontend/src/Store/Actions/appActions.js @@ -117,6 +117,9 @@ export const reducers = createHandleActions({ }; if (state.version !== version) { + if (!state.prevVersion) { + newState.prevVersion = state.version; + } newState.isUpdated = true; } diff --git a/frontend/src/System/Updates/Updates.js b/frontend/src/System/Updates/Updates.js index 87890df2f..4573b45f3 100644 --- a/frontend/src/System/Updates/Updates.js +++ b/frontend/src/System/Updates/Updates.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import React, { Component, Fragment } from 'react'; import { icons, kinds } from 'Helpers/Props'; import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import SpinnerButton from 'Components/Link/SpinnerButton'; import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; @@ -30,6 +31,8 @@ class Updates extends Component { updateMechanism, updateMechanismMessage, shortDateFormat, + longDateFormat, + timeFormat, onInstallLatestPress } = this.props; @@ -134,7 +137,12 @@ class Updates extends Component {
{update.version}
-
{formatDate(update.releaseDate, shortDateFormat)}
+
+ {formatDate(update.releaseDate, shortDateFormat)} +
{ update.branch === 'master' ? @@ -151,11 +159,24 @@ class Updates extends Component { : null } + + { + update.version !== currentVersion && update.installedOn ? + : + null + }
{ @@ -215,6 +236,8 @@ Updates.propTypes = { updateMechanism: PropTypes.string, updateMechanismMessage: PropTypes.string, shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, onInstallLatestPress: PropTypes.func.isRequired }; diff --git a/frontend/src/System/Updates/UpdatesConnector.js b/frontend/src/System/Updates/UpdatesConnector.js index d93b3ba4a..dcb46d91a 100644 --- a/frontend/src/System/Updates/UpdatesConnector.js +++ b/frontend/src/System/Updates/UpdatesConnector.js @@ -45,7 +45,9 @@ function createMapStateToProps() { isInstallingUpdate, updateMechanism: generalSettings.item.updateMechanism, updateMechanismMessage: status.packageUpdateMechanismMessage, - shortDateFormat: uiSettings.shortDateFormat + shortDateFormat: uiSettings.shortDateFormat, + longDateFormat: uiSettings.longDateFormat, + timeFormat: uiSettings.timeFormat }; } ); diff --git a/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs b/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs index 1a2f757ca..bd70447d0 100644 --- a/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs +++ b/src/NzbDrone.Core.Test/UpdateTests/UpdatePackageProviderFixture.cs @@ -44,7 +44,7 @@ namespace NzbDrone.Core.Test.UpdateTests { const string branch = "master"; UseRealHttp(); - var recent = Subject.GetRecentUpdates(branch, new Version(2, 0)); + var recent = Subject.GetRecentUpdates(branch, new Version(2, 0), null); recent.Should().NotBeEmpty(); recent.Should().OnlyContain(c => c.Hash.IsNotNullOrWhiteSpace()); diff --git a/src/NzbDrone.Core/Datastore/Converters/SystemVersionConverter.cs b/src/NzbDrone.Core/Datastore/Converters/SystemVersionConverter.cs new file mode 100644 index 000000000..68bfbe217 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Converters/SystemVersionConverter.cs @@ -0,0 +1,41 @@ +using System; +using Marr.Data.Converters; +using Marr.Data.Mapping; + +namespace NzbDrone.Core.Datastore.Converters +{ + public class SystemVersionConverter : IConverter + { + public object FromDB(ConverterContext context) + { + if (context.DbValue is string version) + { + return Version.Parse(version); + } + + return null; + } + + public object FromDB(ColumnMap map, object dbValue) + { + if (dbValue is string version) + { + return Version.Parse(version); + } + + return null; + } + + public object ToDB(object clrValue) + { + if (clrValue is Version version) + { + return version.ToString(); + } + + return DBNull.Value; + } + + public Type DbType => typeof(String); + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Datastore/Migration/141_add_update_history.cs b/src/NzbDrone.Core/Datastore/Migration/141_add_update_history.cs new file mode 100644 index 000000000..dd6ae0237 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/141_add_update_history.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Data; +using FluentMigrator; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Serializer; +using NzbDrone.Core.Datastore.Converters; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(141)] + public class add_update_history : NzbDroneMigrationBase + { + protected override void LogDbUpgrade() + { + Create.TableForModel("UpdateHistory") + .WithColumn("Date").AsDateTime().NotNullable().Indexed() + .WithColumn("Version").AsString().NotNullable() + .WithColumn("EventType").AsInt32().NotNullable(); + + Insert.IntoTable("UpdateHistory") + .Row(new + { + Date = new UtcConverter().ToDB(DateTime.UtcNow).ToString(), + Version = BuildInfo.Version.ToString(), + EventType = 2 + }); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index df9c97602..32c778d8f 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -39,6 +39,7 @@ using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Languages; using NzbDrone.Core.Profiles.Languages; using NzbDrone.Core.Profiles.Releases; +using NzbDrone.Core.Update.History; namespace NzbDrone.Core.Datastore { @@ -139,6 +140,8 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("DownloadHistory") .AutoMapChildModels(); + + Mapper.Entity().RegisterModel("UpdateHistory"); } private static void RegisterMappers() @@ -168,6 +171,7 @@ namespace NzbDrone.Core.Datastore MapRepository.Instance.RegisterTypeConverter(typeof(Command), new CommandConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(TimeSpan), new TimeSpanConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(TimeSpan?), new TimeSpanConverter()); + MapRepository.Instance.RegisterTypeConverter(typeof(Version), new SystemVersionConverter()); } private static void RegisterProviderSettingConverter() diff --git a/src/NzbDrone.Core/Update/Events/UpdateInstalledEvent.cs b/src/NzbDrone.Core/Update/Events/UpdateInstalledEvent.cs new file mode 100644 index 000000000..d16b8095c --- /dev/null +++ b/src/NzbDrone.Core/Update/Events/UpdateInstalledEvent.cs @@ -0,0 +1,17 @@ +using System; +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Update.History.Events +{ + public class UpdateInstalledEvent : IEvent + { + public Version PreviousVerison { get; set; } + public Version NewVersion { get; set; } + + public UpdateInstalledEvent(Version previousVersion, Version newVersion) + { + PreviousVerison = previousVersion; + NewVersion = newVersion; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Update/History/UpdateHistory.cs b/src/NzbDrone.Core/Update/History/UpdateHistory.cs new file mode 100644 index 000000000..62a3a6fc0 --- /dev/null +++ b/src/NzbDrone.Core/Update/History/UpdateHistory.cs @@ -0,0 +1,12 @@ +using System; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Update.History +{ + public class UpdateHistory : ModelBase + { + public DateTime Date { get; set; } + public Version Version { get; set; } + public UpdateHistoryEventType EventType { get; set; } + } +} diff --git a/src/NzbDrone.Core/Update/History/UpdateHistoryEventType.cs b/src/NzbDrone.Core/Update/History/UpdateHistoryEventType.cs new file mode 100644 index 000000000..2ad99d624 --- /dev/null +++ b/src/NzbDrone.Core/Update/History/UpdateHistoryEventType.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Core.Update.History +{ + public enum UpdateHistoryEventType + { + Unknown = 0, + Initiated = 1, + Installed = 2 + } +} diff --git a/src/NzbDrone.Core/Update/History/UpdateHistoryRepository.cs b/src/NzbDrone.Core/Update/History/UpdateHistoryRepository.cs new file mode 100644 index 000000000..20ea5a239 --- /dev/null +++ b/src/NzbDrone.Core/Update/History/UpdateHistoryRepository.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Update.History +{ + public interface IUpdateHistoryRepository : IBasicRepository + { + UpdateHistory LastInstalled(); + UpdateHistory PreviouslyInstalled(); + List InstalledSince(DateTime dateTime); + } + + public class UpdateHistoryRepository : BasicRepository, IUpdateHistoryRepository + { + public UpdateHistoryRepository(ILogDatabase logDatabase, IEventAggregator eventAggregator) + : base(logDatabase, eventAggregator) + { + + } + + public UpdateHistory LastInstalled() + { + var history = Query.Where(v => v.EventType == UpdateHistoryEventType.Installed) + .OrderBy(v => v.Date) + .Take(1) + .FirstOrDefault(); + + return history; + } + + public UpdateHistory PreviouslyInstalled() + { + var history = Query.Where(v => v.EventType == UpdateHistoryEventType.Installed) + .OrderBy(v => v.Date) + .Skip(1) + .Take(1) + .FirstOrDefault(); + + return history; + } + + public List InstalledSince(DateTime dateTime) + { + var history = Query.Where(v => v.EventType == UpdateHistoryEventType.Installed && v.Date >= dateTime) + .OrderBy(v => v.Date) + .ToList(); + + return history; + } + } +} diff --git a/src/NzbDrone.Core/Update/History/UpdateHistoryService.cs b/src/NzbDrone.Core/Update/History/UpdateHistoryService.cs new file mode 100644 index 000000000..8d11ea054 --- /dev/null +++ b/src/NzbDrone.Core/Update/History/UpdateHistoryService.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Update.History.Events; + +namespace NzbDrone.Core.Update.History +{ + public interface IUpdateHistoryService + { + Version PreviouslyInstalled(); + List InstalledSince(DateTime dateTime); + } + + public class UpdateHistoryService : IUpdateHistoryService, IHandle, IHandleAsync + { + private readonly IUpdateHistoryRepository _repository; + private readonly IEventAggregator _eventAggregator; + private Version _prevVersion; + + public UpdateHistoryService(IUpdateHistoryRepository repository, IEventAggregator eventAggregator) + { + _repository = repository; + _eventAggregator = eventAggregator; + } + + public Version PreviouslyInstalled() + { + var history = _repository.PreviouslyInstalled(); + + return history?.Version; + } + + public List InstalledSince(DateTime dateTime) + { + return _repository.InstalledSince(dateTime); + } + + public void Handle(ApplicationStartedEvent message) + { + if (BuildInfo.Version.Major == 10) + { + // Don't save dev versions, they change constantly + return; + } + + var history = _repository.LastInstalled(); + + if (history == null || history.Version != BuildInfo.Version) + { + _prevVersion = history.Version; + + _repository.Insert(new UpdateHistory + { + Date = DateTime.UtcNow, + Version = BuildInfo.Version, + EventType = UpdateHistoryEventType.Installed + }); + } + } + + public void HandleAsync(ApplicationStartedEvent message) + { + if (_prevVersion != null) + { + _eventAggregator.PublishEvent(new UpdateInstalledEvent(_prevVersion, BuildInfo.Version)); + } + } + } +} diff --git a/src/NzbDrone.Core/Update/RecentUpdateProvider.cs b/src/NzbDrone.Core/Update/RecentUpdateProvider.cs index 6fcaf42c2..42b338f5f 100644 --- a/src/NzbDrone.Core/Update/RecentUpdateProvider.cs +++ b/src/NzbDrone.Core/Update/RecentUpdateProvider.cs @@ -1,6 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Update.History; namespace NzbDrone.Core.Update { @@ -13,18 +15,23 @@ namespace NzbDrone.Core.Update { private readonly IConfigFileProvider _configFileProvider; private readonly IUpdatePackageProvider _updatePackageProvider; + private readonly IUpdateHistoryService _updateHistoryService; public RecentUpdateProvider(IConfigFileProvider configFileProvider, - IUpdatePackageProvider updatePackageProvider) + IUpdatePackageProvider updatePackageProvider, + IUpdateHistoryService updateHistoryService) { _configFileProvider = configFileProvider; _updatePackageProvider = updatePackageProvider; + _updateHistoryService = updateHistoryService; } public List GetRecentUpdatePackages() { var branch = _configFileProvider.Branch; - return _updatePackageProvider.GetRecentUpdates(branch, BuildInfo.Version); + var version = BuildInfo.Version; + var prevVersion = _updateHistoryService.PreviouslyInstalled(); + return _updatePackageProvider.GetRecentUpdates(branch, version, prevVersion); } } } diff --git a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs index d1cca39fc..829efb192 100644 --- a/src/NzbDrone.Core/Update/UpdatePackageProvider.cs +++ b/src/NzbDrone.Core/Update/UpdatePackageProvider.cs @@ -10,7 +10,7 @@ namespace NzbDrone.Core.Update public interface IUpdatePackageProvider { UpdatePackage GetLatestUpdate(string branch, Version currentVersion); - List GetRecentUpdates(string branch, Version currentVersion); + List GetRecentUpdates(string branch, Version currentVersion, Version previousVersion = null); } public class UpdatePackageProvider : IUpdatePackageProvider @@ -49,8 +49,8 @@ namespace NzbDrone.Core.Update return update.UpdatePackage; } - - public List GetRecentUpdates(string branch, Version currentVersion) + + public List GetRecentUpdates(string branch, Version currentVersion, Version previousVersion) { var request = _requestBuilder.Create() .Resource("/update/{branch}/changes") @@ -59,6 +59,11 @@ namespace NzbDrone.Core.Update .AddQueryParam("runtimeVer", _platformInfo.Version) .SetSegment("branch", branch); + if (previousVersion != null && previousVersion != currentVersion) + { + request.AddQueryParam("prevVersion", previousVersion); + } + if (_analyticsService.IsEnabled) { // Send if the system is active so we know which versions to deprecate/ignore diff --git a/src/Sonarr.Api.V3/Update/UpdateModule.cs b/src/Sonarr.Api.V3/Update/UpdateModule.cs index 76c357be1..6c5ec785a 100644 --- a/src/Sonarr.Api.V3/Update/UpdateModule.cs +++ b/src/Sonarr.Api.V3/Update/UpdateModule.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Update; +using NzbDrone.Core.Update.History; using Sonarr.Http; namespace Sonarr.Api.V3.Update @@ -9,10 +11,12 @@ namespace Sonarr.Api.V3.Update public class UpdateModule : SonarrRestModule { private readonly IRecentUpdateProvider _recentUpdateProvider; + private readonly IUpdateHistoryService _updateHistoryService; - public UpdateModule(IRecentUpdateProvider recentUpdateProvider) + public UpdateModule(IRecentUpdateProvider recentUpdateProvider, IUpdateHistoryService updateHistoryService) { _recentUpdateProvider = recentUpdateProvider; + _updateHistoryService = updateHistoryService; GetResourceAll = GetRecentUpdates; } @@ -38,6 +42,18 @@ namespace Sonarr.Api.V3.Update { installed.Installed = true; } + + var installDates = _updateHistoryService.InstalledSince(resources.Last().ReleaseDate) + .DistinctBy(v => v.Version) + .ToDictionary(v => v.Version); + + foreach (var resource in resources) + { + if (installDates.TryGetValue(resource.Version, out var installDate)) + { + resource.InstalledOn = installDate.Date; + } + } } return resources; diff --git a/src/Sonarr.Api.V3/Update/UpdateResource.cs b/src/Sonarr.Api.V3/Update/UpdateResource.cs index e01ad966d..d23d0dc1c 100644 --- a/src/Sonarr.Api.V3/Update/UpdateResource.cs +++ b/src/Sonarr.Api.V3/Update/UpdateResource.cs @@ -17,6 +17,7 @@ namespace Sonarr.Api.V3.Update public string FileName { get; set; } public string Url { get; set; } public bool Installed { get; set; } + public DateTime? InstalledOn { get; set; } public bool Installable { get; set; } public bool Latest { get; set; } public UpdateChanges Changes { get; set; }