diff --git a/API/Controllers/ServerController.cs b/API/Controllers/ServerController.cs index 2ac68ca61..a770fbc6e 100644 --- a/API/Controllers/ServerController.cs +++ b/API/Controllers/ServerController.cs @@ -42,12 +42,13 @@ public class ServerController : BaseApiController private readonly IUnitOfWork _unitOfWork; private readonly IEasyCachingProviderFactory _cachingProviderFactory; private readonly ILocalizationService _localizationService; + private readonly IEmailService _emailService; public ServerController(ILogger logger, IBackupService backupService, IArchiveService archiveService, IVersionUpdaterService versionUpdaterService, IStatsService statsService, ICleanupService cleanupService, IScannerService scannerService, IAccountService accountService, ITaskScheduler taskScheduler, IUnitOfWork unitOfWork, IEasyCachingProviderFactory cachingProviderFactory, - ILocalizationService localizationService) + ILocalizationService localizationService, IEmailService emailService) { _logger = logger; _backupService = backupService; @@ -61,6 +62,7 @@ public class ServerController : BaseApiController _unitOfWork = unitOfWork; _cachingProviderFactory = cachingProviderFactory; _localizationService = localizationService; + _emailService = emailService; } /// @@ -270,5 +272,22 @@ public class ServerController : BaseApiController return Ok(); } + /// + /// Returns the KavitaEmail version for non-default instances + /// + /// + [Authorize("RequireAdminRole")] + [HttpGet("email-version")] + public async Task> GetEmailVersion() + { + var emailServiceUrl = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)) + .Value; + + if (emailServiceUrl.Equals(EmailService.DefaultApiUrl)) return Ok(null); + + return Ok(await _emailService.GetVersion(emailServiceUrl)); + + } + } diff --git a/API/Services/EmailService.cs b/API/Services/EmailService.cs index 9d128d002..d002e74a4 100644 --- a/API/Services/EmailService.cs +++ b/API/Services/EmailService.cs @@ -26,6 +26,7 @@ public interface IEmailService Task TestConnectivity(string emailUrl, string adminEmail, bool sendEmail); Task IsDefaultEmailService(); Task SendEmailChangeEmail(ConfirmationEmailDto data); + Task GetVersion(string emailUrl); } public class EmailService : IEmailService @@ -94,6 +95,34 @@ public class EmailService : IEmailService } } + public async Task GetVersion(string emailUrl) + { + try + { + var settings = await _unitOfWork.SettingsRepository.GetSettingsDtoAsync(); + var response = await $"{emailUrl}/api/about/version" + .WithHeader("Accept", "application/json") + .WithHeader("User-Agent", "Kavita") + .WithHeader("x-api-key", "MsnvA2DfQqxSK5jh") + .WithHeader("x-kavita-version", BuildInfo.Version) + .WithHeader("x-kavita-installId", settings.InstallId) + .WithHeader("Content-Type", "application/json") + .WithTimeout(TimeSpan.FromSeconds(10)) + .GetStringAsync(); + + if (!string.IsNullOrEmpty(response)) + { + return response.Replace("\"", string.Empty); + } + } + catch (Exception) + { + return null; + } + + return null; + } + public async Task SendConfirmationEmail(ConfirmationEmailDto data) { var emailLink = (await _unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.EmailServiceUrl)).Value; diff --git a/API/Services/SeriesService.cs b/API/Services/SeriesService.cs index 3ef7a14b5..81c73a59e 100644 --- a/API/Services/SeriesService.cs +++ b/API/Services/SeriesService.cs @@ -651,9 +651,10 @@ public class SeriesService : ISeriesService { var series = await _unitOfWork.SeriesRepository.GetSeriesByIdAsync(seriesId, SeriesIncludes.Metadata | SeriesIncludes.Library); if (series == null) throw new KavitaException(await _localizationService.Translate(userId, "series-doesnt-exist")); - var libraryIds = _unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId); - if (!libraryIds.Contains(series.LibraryId)) //// TODO: Rewrite this to use a new method which checks permissions all in the DB to be streamlined and less memory + if (!(await _unitOfWork.UserRepository.HasAccessToSeries(userId, seriesId))) + { throw new UnauthorizedAccessException("user-no-access-library-from-series"); + } if (series?.Metadata.PublicationStatus is not (PublicationStatus.OnGoing or PublicationStatus.Ended) || series.Library.Type == LibraryType.Book) { return new NextExpectedChapterDto() @@ -670,18 +671,32 @@ public class SeriesService : ISeriesService .ToList(); // Calculate the time differences between consecutive chapters - var timeDifferences = chapters - .Select((chapter, index) => new + // var timeDifferences = chapters + // .Select((chapter, index) => new + // { + // ChapterNumber = chapter.Number, + // VolumeNumber = chapter.Volume.Number, + // TimeDifference = index == 0 ? TimeSpan.Zero : (chapter.CreatedUtc - chapters.ElementAt(index - 1).CreatedUtc) + // }) + // .ToList(); + // Quantize time differences: Chapters created within an hour from each other will be treated as one time delta + var timeDifferences = new List(); + DateTime? previousChapterTime = null; + foreach (var chapter in chapters) + { + if (previousChapterTime.HasValue && (chapter.CreatedUtc - previousChapterTime.Value) <= TimeSpan.FromHours(1)) { - ChapterNumber = chapter.Number, - VolumeNumber = chapter.Volume.Number, - TimeDifference = index == 0 ? TimeSpan.Zero : (chapter.CreatedUtc - chapters.ElementAt(index - 1).CreatedUtc) - }) - .ToList(); + continue; // Skip this chapter if it's within an hour of the previous one + } + timeDifferences.Add(chapter.CreatedUtc - previousChapterTime ?? TimeSpan.Zero); + previousChapterTime = chapter.CreatedUtc; + } // Calculate the average time difference between chapters + // var averageTimeDifference = timeDifferences + // .Average(td => td.TimeDifference.TotalDays); var averageTimeDifference = timeDifferences - .Average(td => td.TimeDifference.TotalDays); + .Average(td => td.TotalDays); // Calculate the forecast for when the next chapter is expected var nextChapterExpected = chapters.Any() @@ -693,8 +708,8 @@ public class SeriesService : ISeriesService nextChapterExpected = DateTime.UtcNow + TimeSpan.FromDays(averageTimeDifference); } - var lastChapter = timeDifferences.Last(); - float.TryParse(lastChapter.ChapterNumber, NumberStyles.Number, CultureInfo.InvariantCulture, + var lastChapter = chapters.Last(); + float.TryParse(lastChapter.Number, NumberStyles.Number, CultureInfo.InvariantCulture, out var lastChapterNumber); var result = new NextExpectedChapterDto() @@ -708,7 +723,7 @@ public class SeriesService : ISeriesService if (lastChapterNumber > 0) { result.ChapterNumber = lastChapterNumber + 1; - result.VolumeNumber = lastChapter.VolumeNumber; + result.VolumeNumber = lastChapter.Volume.Number; result.Title = series.Library.Type switch { LibraryType.Manga => await _localizationService.Translate(userId, "chapter-num", @@ -722,8 +737,8 @@ public class SeriesService : ISeriesService } else { - result.VolumeNumber = lastChapter.VolumeNumber + 1; - result.Title = await _localizationService.Translate(userId, "vol-num", + result.VolumeNumber = lastChapter.Volume.Number + 1; + result.Title = await _localizationService.Translate(userId, "volume-num", new object[] {result.VolumeNumber}); } diff --git a/API/Startup.cs b/API/Startup.cs index f09e78a35..4418fe270 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -238,21 +238,8 @@ public class Startup logger.LogInformation("Running Migrations"); - // v0.7.2 - await MigrateLoginRoles.Migrate(unitOfWork, userManager, logger); - - // v0.7.3 - await MigrateRemoveWebPSettingRows.Migrate(unitOfWork, logger); - - // v0.7.4 - await MigrateDisableScrobblingOnComicLibraries.Migrate(unitOfWork, dataContext, logger); - - // v0.7.6 - await MigrateExistingRatings.Migrate(dataContext, logger); - // v0.7.9 await MigrateUserLibrarySideNavStream.Migrate(unitOfWork, dataContext, logger); - await MigrateDashboardStreamNamesToLocaleKeys.Migrate(unitOfWork, dataContext, logger); // Update the version in the DB after all migrations are run var installVersion = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.InstallVersion); diff --git a/UI/Web/src/app/_services/server.service.ts b/UI/Web/src/app/_services/server.service.ts index 543d36aad..3f3a88a25 100644 --- a/UI/Web/src/app/_services/server.service.ts +++ b/UI/Web/src/app/_services/server.service.ts @@ -5,6 +5,7 @@ import {ServerInfoSlim} from '../admin/_models/server-info'; import { UpdateVersionEvent } from '../_models/events/update-version-event'; import { Job } from '../_models/job/job'; import { KavitaMediaError } from '../admin/_models/media-error'; +import {TextResonse} from "../_types/text-response"; @Injectable({ providedIn: 'root' @@ -67,4 +68,8 @@ export class ServerService { clearMediaAlerts() { return this.httpClient.post(this.baseUrl + 'server/clear-media-alerts', {}); } + + getEmailVersion() { + return this.httpClient.get(this.baseUrl + 'server/email-version', TextResonse); + } } diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts index 6eae379ea..a5e986df8 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts @@ -1,13 +1,14 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {ToastrService} from 'ngx-toastr'; -import {take} from 'rxjs'; +import {forkJoin, take} from 'rxjs'; import {EmailTestResult, SettingsService} from '../settings.service'; import {ServerSettings} from '../_models/server-settings'; import {NgbTooltip} from '@ng-bootstrap/ng-bootstrap'; import {NgIf, NgTemplateOutlet} from '@angular/common'; -import {TranslocoModule, TranslocoService} from "@ngneat/transloco"; +import {translate, TranslocoModule, TranslocoService} from "@ngneat/transloco"; import {SafeHtmlPipe} from "../../pipe/safe-html.pipe"; +import {ServerService} from "../../_services/server.service"; @Component({ selector: 'app-manage-email-settings', @@ -22,9 +23,13 @@ export class ManageEmailSettingsComponent implements OnInit { serverSettings!: ServerSettings; settingsForm: FormGroup = new FormGroup({}); link = 'Kavita Email'; + emailVersion: string | null = null; private readonly cdRef = inject(ChangeDetectorRef); + private readonly serverService = inject(ServerService); + private readonly settingsService = inject(SettingsService); + private readonly toastr = inject(ToastrService); - constructor(private settingsService: SettingsService, private toastr: ToastrService, private translocoService: TranslocoService) { } + constructor() { } ngOnInit(): void { this.settingsService.getServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { @@ -33,6 +38,11 @@ export class ManageEmailSettingsComponent implements OnInit { this.settingsForm.addControl('hostName', new FormControl(this.serverSettings.hostName, [])); this.cdRef.markForCheck(); }); + + this.serverService.getEmailVersion().subscribe(version => { + this.emailVersion = version; + this.cdRef.markForCheck(); + }); } resetForm() { @@ -51,7 +61,7 @@ export class ManageEmailSettingsComponent implements OnInit { this.settingsService.updateServerSettings(modelSettings).pipe(take(1)).subscribe((settings: ServerSettings) => { this.serverSettings = settings; this.resetForm(); - this.toastr.success(this.translocoService.translate('toasts.server-settings-updated')); + this.toastr.success(translate('toasts.server-settings-updated')); }, (err: any) => { console.error('error: ', err); }); @@ -61,7 +71,7 @@ export class ManageEmailSettingsComponent implements OnInit { this.settingsService.resetServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { this.serverSettings = settings; this.resetForm(); - this.toastr.success(this.translocoService.translate('toasts.server-settings-updated')); + this.toastr.success(translate('toasts.server-settings-updated')); }, (err: any) => { console.error('error: ', err); }); @@ -71,7 +81,7 @@ export class ManageEmailSettingsComponent implements OnInit { this.settingsService.resetEmailServerSettings().pipe(take(1)).subscribe((settings: ServerSettings) => { this.serverSettings.emailServiceUrl = settings.emailServiceUrl; this.resetForm(); - this.toastr.success(this.translocoService.translate('toasts.email-service-reset')); + this.toastr.success(translate('toasts.email-service-reset')); }, (err: any) => { console.error('error: ', err); }); @@ -79,11 +89,14 @@ export class ManageEmailSettingsComponent implements OnInit { testEmailServiceUrl() { if (this.settingsForm.get('emailServiceUrl')?.value === '') return; - this.settingsService.testEmailServerSettings(this.settingsForm.get('emailServiceUrl')?.value).pipe(take(1)).subscribe(async (result: EmailTestResult) => { + forkJoin([this.settingsService.testEmailServerSettings(this.settingsForm.get('emailServiceUrl')?.value), this.serverService.getEmailVersion()]) + .pipe(take(1)).subscribe(async (results) => { + const result = results[0] as EmailTestResult; if (result.successful) { - this.toastr.success(this.translocoService.translate('toasts.email-service-reachable')); + const version = ('. Kavita Email: ' + results[1] ? 'v' + results[1] : ''); + this.toastr.success(translate('toasts.email-service-reachable') + version); } else { - this.toastr.error(this.translocoService.translate('toasts.email-service-unresponsive') + result.errorMessage); + this.toastr.error(translate('toasts.email-service-unresponsive') + result.errorMessage.split('(')[0]); } }, (err: any) => { diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index d3777cc95..08e20e54d 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -1907,7 +1907,7 @@ "reading-list-imported": "Reading List imported", "incognito-off": "Incognito mode is off. Progress will now start being tracked.", "email-service-reset": "Email Service Reset", - "email-service-reachable": "Email Service was reachable", + "email-service-reachable": "Kavita Email Connection Successful", "email-service-unresponsive": "Email Service Url did not respond.", "refresh-covers-queued": "Refresh covers queued for {{name}}", "library-file-analysis-queued": "Library file analysis queued for {{name}}", diff --git a/openapi.json b/openapi.json index 8ec8a9d53..24cb9c861 100644 --- a/openapi.json +++ b/openapi.json @@ -9455,6 +9455,36 @@ } } }, + "/api/Server/email-version": { + "get": { + "tags": [ + "Server" + ], + "summary": "Returns the KavitaEmail version for non-default instances", + "responses": { + "200": { + "description": "Success", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + }, + "text/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, "/api/Settings/base-url": { "get": { "tags": [