diff --git a/API/Services/Tasks/ScannerService.cs b/API/Services/Tasks/ScannerService.cs index e80955b29..5be532867 100644 --- a/API/Services/Tasks/ScannerService.cs +++ b/API/Services/Tasks/ScannerService.cs @@ -185,6 +185,9 @@ namespace API.Services.Tasks } _logger.LogInformation("[ScannerService] Beginning file scan on {LibraryName}", library.Name); + await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibraryProgress, + MessageFactory.ScanLibraryProgressEvent(libraryId, 0, string.Empty)); + var scanner = new ParseScannedFiles(_bookService, _logger); var series = scanner.ScanLibrariesForSeries(library.Type, library.Folders.Select(fp => fp.Path), out var totalFiles, out var scanElapsedTime); @@ -212,7 +215,8 @@ namespace API.Services.Tasks await CleanupAbandonedChapters(); BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, false)); - await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibrary, MessageFactory.ScanLibraryEvent(libraryId, "complete")); + await _messageHub.Clients.All.SendAsync(SignalREvents.ScanLibraryProgress, + MessageFactory.ScanLibraryProgressEvent(libraryId, 100, string.Empty)); } /// @@ -277,7 +281,10 @@ namespace API.Services.Tasks // Now, we only have to deal with series that exist on disk. Let's recalculate the volumes for each series var librarySeries = cleanedSeries.ToList(); - Parallel.ForEach(librarySeries, (series) => { UpdateSeries(series, parsedSeries); }); + Parallel.ForEach(librarySeries, (series) => + { + UpdateSeries(series, parsedSeries); + }); await _unitOfWork.CommitAsync(); _logger.LogInformation( diff --git a/API/SignalR/MessageFactory.cs b/API/SignalR/MessageFactory.cs index 8e107cd9a..4485392b0 100644 --- a/API/SignalR/MessageFactory.cs +++ b/API/SignalR/MessageFactory.cs @@ -59,6 +59,22 @@ namespace API.SignalR }; } + public static SignalRMessage ScanLibraryProgressEvent(int libraryId, int progress, string seriesName) + { + return new SignalRMessage() + { + Name = SignalREvents.ScanLibrary, + Body = new + { + LibraryId = libraryId, + Progress = progress, + SeriesName = seriesName + } + }; + } + + + public static SignalRMessage RefreshMetadataEvent(int libraryId, int seriesId) { return new SignalRMessage() diff --git a/API/SignalR/MessageHub.cs b/API/SignalR/MessageHub.cs index 262e5b3bb..2b3cd96cc 100644 --- a/API/SignalR/MessageHub.cs +++ b/API/SignalR/MessageHub.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using API.Extensions; +using API.SignalR.Presence; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; @@ -13,8 +15,14 @@ namespace API.SignalR [Authorize] public class MessageHub : Hub { + private readonly IPresenceTracker _tracker; private static readonly HashSet Connections = new HashSet(); + public MessageHub(IPresenceTracker tracker) + { + _tracker = tracker; + } + public static bool IsConnected { get @@ -33,6 +41,12 @@ namespace API.SignalR Connections.Add(Context.ConnectionId); } + await _tracker.UserConnected(Context.User.GetUsername(), Context.ConnectionId); + + var currentUsers = await PresenceTracker.GetOnlineUsers(); + await Clients.All.SendAsync(SignalREvents.OnlineUsers, currentUsers); + + await base.OnConnectedAsync(); } @@ -43,6 +57,12 @@ namespace API.SignalR Connections.Remove(Context.ConnectionId); } + await _tracker.UserDisconnected(Context.User.GetUsername(), Context.ConnectionId); + + var currentUsers = await PresenceTracker.GetOnlineUsers(); + await Clients.All.SendAsync(SignalREvents.OnlineUsers, currentUsers); + + await base.OnDisconnectedAsync(exception); } } diff --git a/API/SignalR/SignalREvents.cs b/API/SignalR/SignalREvents.cs index 6799780ff..0d4c124ca 100644 --- a/API/SignalR/SignalREvents.cs +++ b/API/SignalR/SignalREvents.cs @@ -8,6 +8,7 @@ public const string ScanLibrary = "ScanLibrary"; public const string SeriesAdded = "SeriesAdded"; public const string SeriesRemoved = "SeriesRemoved"; - + public const string ScanLibraryProgress = "ScanLibraryProgress"; + public const string OnlineUsers = "OnlineUsers"; } } diff --git a/API/Startup.cs b/API/Startup.cs index ee26e2d2b..e933e08dc 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -147,7 +147,6 @@ namespace API { endpoints.MapControllers(); endpoints.MapHub("hubs/messages"); - endpoints.MapHub("hubs/presence"); endpoints.MapHangfireDashboard(); endpoints.MapFallbackToController("Index", "Fallback"); }); diff --git a/UI/Web/src/app/_models/events/scan-library-event.ts b/UI/Web/src/app/_models/events/scan-library-event.ts deleted file mode 100644 index b0c663502..000000000 --- a/UI/Web/src/app/_models/events/scan-library-event.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ScanLibraryEvent { - libraryId: number; - stage: 'complete'; -} \ No newline at end of file diff --git a/UI/Web/src/app/_models/events/scan-library-progress-event.ts b/UI/Web/src/app/_models/events/scan-library-progress-event.ts new file mode 100644 index 000000000..41a5afbbe --- /dev/null +++ b/UI/Web/src/app/_models/events/scan-library-progress-event.ts @@ -0,0 +1,4 @@ +export interface ScanLibraryProgressEvent { + libraryId: number; + progress: number; +} \ No newline at end of file diff --git a/UI/Web/src/app/_services/account.service.ts b/UI/Web/src/app/_services/account.service.ts index c467cb88d..a58147cab 100644 --- a/UI/Web/src/app/_services/account.service.ts +++ b/UI/Web/src/app/_services/account.service.ts @@ -8,7 +8,6 @@ import { User } from '../_models/user'; import * as Sentry from "@sentry/angular"; import { Router } from '@angular/router'; import { MessageHubService } from './message-hub.service'; -import { PresenceHubService } from './presence-hub.service'; @Injectable({ providedIn: 'root' @@ -26,7 +25,7 @@ export class AccountService implements OnDestroy { private readonly onDestroy = new Subject(); constructor(private httpClient: HttpClient, private router: Router, - private messageHub: MessageHubService, private presenceHub: PresenceHubService) {} + private messageHub: MessageHubService) {} ngOnDestroy(): void { this.onDestroy.next(); @@ -52,7 +51,6 @@ export class AccountService implements OnDestroy { if (user) { this.setCurrentUser(user); this.messageHub.createHubConnection(user, this.hasAdminRole(user)); - this.presenceHub.createHubConnection(user); } }), takeUntil(this.onDestroy) @@ -85,7 +83,6 @@ export class AccountService implements OnDestroy { // Upon logout, perform redirection this.router.navigateByUrl('/login'); this.messageHub.stopHubConnection(); - this.presenceHub.stopHubConnection(); } register(model: {username: string, password: string, isAdmin?: boolean}) { diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index 0cdfb3cfe..867e2e0f8 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -6,6 +6,7 @@ import { take } from 'rxjs/operators'; import { BookmarksModalComponent } from '../cards/_modals/bookmarks-modal/bookmarks-modal.component'; import { AddToListModalComponent, ADD_FLOW } from '../reading-list/_modals/add-to-list-modal/add-to-list-modal.component'; import { EditReadingListModalComponent } from '../reading-list/_modals/edit-reading-list-modal/edit-reading-list-modal.component'; +import { ConfirmService } from '../shared/confirm.service'; import { Chapter } from '../_models/chapter'; import { Library } from '../_models/library'; import { ReadingList } from '../_models/reading-list'; @@ -35,7 +36,8 @@ export class ActionService implements OnDestroy { private readingListModalRef: NgbModalRef | null = null; constructor(private libraryService: LibraryService, private seriesService: SeriesService, - private readerService: ReaderService, private toastr: ToastrService, private modalService: NgbModal) { } + private readerService: ReaderService, private toastr: ToastrService, private modalService: NgbModal, + private confirmService: ConfirmService) { } ngOnDestroy() { this.onDestroy.next(); @@ -66,11 +68,15 @@ export class ActionService implements OnDestroy { * @param callback Optional callback to perform actions after API completes * @returns */ - refreshMetadata(library: Partial, callback?: LibraryActionCallback) { + async refreshMetadata(library: Partial, callback?: LibraryActionCallback) { if (!library.hasOwnProperty('id') || library.id === undefined) { return; } + if (!await this.confirmService.confirm('Refresh metadata will force all cover images and metadata to be recalculated. This is a heavy operation. Are you sure you don\'t want to perform a Scan instead?')) { + return; + } + this.libraryService.refreshMetadata(library?.id).pipe(take(1)).subscribe((res: any) => { this.toastr.success('Scan started for ' + library.name); if (callback) { @@ -128,7 +134,11 @@ export class ActionService implements OnDestroy { * @param series Series, must have libraryId, id and name populated * @param callback Optional callback to perform actions after API completes */ - refreshMetdata(series: Series, callback?: SeriesActionCallback) { + async refreshMetdata(series: Series, callback?: SeriesActionCallback) { + if (!await this.confirmService.confirm('Refresh metadata will force all cover images and metadata to be recalculated. This is a heavy operation. Are you sure you don\'t want to perform a Scan instead?')) { + return; + } + this.seriesService.refreshMetadata(series).pipe(take(1)).subscribe((res: any) => { this.toastr.success('Refresh started for ' + series.name); if (callback) { diff --git a/UI/Web/src/app/_services/message-hub.service.ts b/UI/Web/src/app/_services/message-hub.service.ts index cfeb13828..d49f5076b 100644 --- a/UI/Web/src/app/_services/message-hub.service.ts +++ b/UI/Web/src/app/_services/message-hub.service.ts @@ -3,20 +3,21 @@ import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { User } from '@sentry/angular'; import { ToastrService } from 'ngx-toastr'; -import { ReplaySubject } from 'rxjs'; +import { BehaviorSubject, ReplaySubject } from 'rxjs'; import { environment } from 'src/environments/environment'; import { UpdateNotificationModalComponent } from '../shared/update-notification/update-notification-modal.component'; import { RefreshMetadataEvent } from '../_models/events/refresh-metadata-event'; -import { ScanLibraryEvent } from '../_models/events/scan-library-event'; +import { ScanLibraryProgressEvent } from '../_models/events/scan-library-progress-event'; import { ScanSeriesEvent } from '../_models/events/scan-series-event'; import { SeriesAddedEvent } from '../_models/events/series-added-event'; export enum EVENTS { UpdateAvailable = 'UpdateAvailable', ScanSeries = 'ScanSeries', - ScanLibrary = 'ScanLibrary', RefreshMetadata = 'RefreshMetadata', - SeriesAdded = 'SeriesAdded' + SeriesAdded = 'SeriesAdded', + ScanLibraryProgress = 'ScanLibraryProgress', + OnlineUsers = 'OnlineUsers' } export interface Message { @@ -35,8 +36,11 @@ export class MessageHubService { private messagesSource = new ReplaySubject>(1); public messages$ = this.messagesSource.asObservable(); + private onlineUsersSource = new BehaviorSubject([]); + onlineUsers$ = this.onlineUsersSource.asObservable(); + public scanSeries: EventEmitter = new EventEmitter(); - public scanLibrary: EventEmitter = new EventEmitter(); + public scanLibrary: EventEmitter = new EventEmitter(); public seriesAdded: EventEmitter = new EventEmitter(); public refreshMetadata: EventEmitter = new EventEmitter(); @@ -60,10 +64,11 @@ export class MessageHubService { .start() .catch(err => console.error(err)); - this.hubConnection.on('receiveMessage', body => { - //console.log('[Hub] Body: ', body); + this.hubConnection.on(EVENTS.OnlineUsers, (usernames: string[]) => { + this.onlineUsersSource.next(usernames); }); + this.hubConnection.on(EVENTS.ScanSeries, resp => { this.messagesSource.next({ event: EVENTS.ScanSeries, @@ -72,9 +77,9 @@ export class MessageHubService { this.scanSeries.emit(resp.body); }); - this.hubConnection.on(EVENTS.ScanLibrary, resp => { + this.hubConnection.on(EVENTS.ScanLibraryProgress, resp => { this.messagesSource.next({ - event: EVENTS.ScanLibrary, + event: EVENTS.ScanLibraryProgress, payload: resp.body }); this.scanLibrary.emit(resp.body); diff --git a/UI/Web/src/app/_services/presence-hub.service.ts b/UI/Web/src/app/_services/presence-hub.service.ts deleted file mode 100644 index f0fe970d1..000000000 --- a/UI/Web/src/app/_services/presence-hub.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr'; -import { User } from '@sentry/angular'; -import { ToastrService } from 'ngx-toastr'; -import { BehaviorSubject } from 'rxjs'; -import { environment } from 'src/environments/environment'; - -@Injectable({ - providedIn: 'root' -}) -export class PresenceHubService { - - hubUrl = environment.hubUrl; - private hubConnection!: HubConnection; - private onlineUsersSource = new BehaviorSubject([]); - onlineUsers$ = this.onlineUsersSource.asObservable(); - - constructor(private toatsr: ToastrService) { } - - createHubConnection(user: User) { - this.hubConnection = new HubConnectionBuilder() - .withUrl(this.hubUrl + 'presence', { - accessTokenFactory: () => user.token - }) - .withAutomaticReconnect() - .build(); - - this.hubConnection - .start() - .catch(err => console.error(err)); - - this.hubConnection.on('GetOnlineUsers', (usernames: string[]) => { - this.onlineUsersSource.next(usernames); - }); - } - - stopHubConnection() { - this.hubConnection.stop().catch(err => console.error(err)); - } -} diff --git a/UI/Web/src/app/admin/manage-library/manage-library.component.html b/UI/Web/src/app/admin/manage-library/manage-library.component.html index 2a82cb003..476e6b161 100644 --- a/UI/Web/src/app/admin/manage-library/manage-library.component.html +++ b/UI/Web/src/app/admin/manage-library/manage-library.component.html @@ -7,7 +7,10 @@
  • - {{library.name | titlecase}} + {{library.name | titlecase}}  +
    + Scan for {{library.name}} in progress +
    diff --git a/UI/Web/src/app/admin/manage-library/manage-library.component.ts b/UI/Web/src/app/admin/manage-library/manage-library.component.ts index f3353ed7f..70f137f2d 100644 --- a/UI/Web/src/app/admin/manage-library/manage-library.component.ts +++ b/UI/Web/src/app/admin/manage-library/manage-library.component.ts @@ -4,8 +4,10 @@ import { ToastrService } from 'ngx-toastr'; import { Subject } from 'rxjs'; import { take, takeUntil } from 'rxjs/operators'; import { ConfirmService } from 'src/app/shared/confirm.service'; +import { ScanLibraryProgressEvent } from 'src/app/_models/events/scan-library-progress-event'; import { Library, LibraryType } from 'src/app/_models/library'; import { LibraryService } from 'src/app/_services/library.service'; +import { MessageHubService } from 'src/app/_services/message-hub.service'; import { LibraryEditorModalComponent } from '../_modals/library-editor-modal/library-editor-modal.component'; @Component({ @@ -22,13 +24,21 @@ export class ManageLibraryComponent implements OnInit, OnDestroy { * If a deletion is in progress for a library */ deletionInProgress: boolean = false; + scanInProgress: {[key: number]: boolean} = {}; private readonly onDestroy = new Subject(); - constructor(private modalService: NgbModal, private libraryService: LibraryService, private toastr: ToastrService, private confirmService: ConfirmService) { } + constructor(private modalService: NgbModal, private libraryService: LibraryService, + private toastr: ToastrService, private confirmService: ConfirmService, + private hubService: MessageHubService) { } ngOnInit(): void { this.getLibraries(); + + this.hubService.scanLibrary.subscribe((event: ScanLibraryProgressEvent) => { + + this.scanInProgress[event.libraryId] = event.progress !== 100; + }); } ngOnDestroy() { diff --git a/UI/Web/src/app/admin/manage-users/manage-users.component.html b/UI/Web/src/app/admin/manage-users/manage-users.component.html index 5aff35f12..2f54e76ef 100644 --- a/UI/Web/src/app/admin/manage-users/manage-users.component.html +++ b/UI/Web/src/app/admin/manage-users/manage-users.component.html @@ -9,7 +9,7 @@
  • - {{member.username | titlecase}} (You) + {{member.username | titlecase}} (You)
    diff --git a/UI/Web/src/app/admin/manage-users/manage-users.component.ts b/UI/Web/src/app/admin/manage-users/manage-users.component.ts index fd3d50f62..b76a45d2e 100644 --- a/UI/Web/src/app/admin/manage-users/manage-users.component.ts +++ b/UI/Web/src/app/admin/manage-users/manage-users.component.ts @@ -1,6 +1,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { take, takeUntil } from 'rxjs/operators'; +import { take } from 'rxjs/operators'; import { MemberService } from 'src/app/_services/member.service'; import { Member } from 'src/app/_models/member'; import { User } from 'src/app/_models/user'; @@ -10,8 +10,8 @@ import { ToastrService } from 'ngx-toastr'; import { ResetPasswordModalComponent } from '../_modals/reset-password-modal/reset-password-modal.component'; import { ConfirmService } from 'src/app/shared/confirm.service'; import { EditRbsModalComponent } from '../_modals/edit-rbs-modal/edit-rbs-modal.component'; -import { PresenceHubService } from 'src/app/_services/presence-hub.service'; import { Subject } from 'rxjs'; +import { MessageHubService } from 'src/app/_services/message-hub.service'; @Component({ selector: 'app-manage-users', @@ -34,7 +34,7 @@ export class ManageUsersComponent implements OnInit, OnDestroy { private modalService: NgbModal, private toastr: ToastrService, private confirmService: ConfirmService, - public presence: PresenceHubService) { + public messageHub: MessageHubService) { this.accountService.currentUser$.pipe(take(1)).subscribe((user: User) => { this.loggedInUsername = user.username; }); diff --git a/UI/Web/src/app/app.component.ts b/UI/Web/src/app/app.component.ts index c3f86569f..949bf0534 100644 --- a/UI/Web/src/app/app.component.ts +++ b/UI/Web/src/app/app.component.ts @@ -5,7 +5,6 @@ import { AccountService } from './_services/account.service'; import { LibraryService } from './_services/library.service'; import { MessageHubService } from './_services/message-hub.service'; import { NavService } from './_services/nav.service'; -import { PresenceHubService } from './_services/presence-hub.service'; import { StatsService } from './_services/stats.service'; import { filter } from 'rxjs/operators'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; @@ -19,7 +18,7 @@ export class AppComponent implements OnInit { constructor(private accountService: AccountService, public navService: NavService, private statsService: StatsService, private messageHub: MessageHubService, - private presenceHub: PresenceHubService, private libraryService: LibraryService, private router: Router, private ngbModal: NgbModal) { + private libraryService: LibraryService, private router: Router, private ngbModal: NgbModal) { // Close any open modals when a route change occurs router.events @@ -48,7 +47,6 @@ export class AppComponent implements OnInit { if (user) { this.navService.setDarkMode(user.preferences.siteDarkMode); this.messageHub.createHubConnection(user, this.accountService.hasAdminRole(user)); - this.presenceHub.createHubConnection(user); this.libraryService.getLibraryNames().pipe(take(1)).subscribe(() => {/* No Operation */}); } else { this.navService.setDarkMode(true); diff --git a/UI/Web/src/app/library-detail/library-detail.component.ts b/UI/Web/src/app/library-detail/library-detail.component.ts index 409ea9267..f192664c1 100644 --- a/UI/Web/src/app/library-detail/library-detail.component.ts +++ b/UI/Web/src/app/library-detail/library-detail.component.ts @@ -1,11 +1,10 @@ import { Component, HostListener, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; -import { take, takeWhile } from 'rxjs/operators'; +import { debounceTime, take, takeWhile } from 'rxjs/operators'; import { BulkSelectionService } from '../cards/bulk-selection.service'; import { UpdateFilterEvent } from '../cards/card-detail-layout/card-detail-layout.component'; import { KEY_CODES } from '../shared/_services/utility.service'; -import { RefreshMetadataEvent } from '../_models/events/refresh-metadata-event'; import { SeriesAddedEvent } from '../_models/events/series-added-event'; import { Library } from '../_models/library'; import { Pagination } from '../_models/pagination'; @@ -81,8 +80,7 @@ export class LibraryDetailComponent implements OnInit { } ngOnInit(): void { - - this.hubService.seriesAdded.pipe(takeWhile(event => event.libraryId === this.libraryId)).subscribe((event: SeriesAddedEvent) => { + this.hubService.seriesAdded.pipe(takeWhile(event => event.libraryId === this.libraryId), debounceTime(6000)).subscribe((event: SeriesAddedEvent) => { this.loadPage(); }); }