Misc Bugfixes (#1373)

* Fixed an issue where signalr cover update events would fire before the covers were updated in db and hence UI would show as if no cover for quite some time.

* Refactored MetadataService to GenerateCovers, as that is what this service does.

* Fixed a bug where list item for books that have 0.X series index wouldn't render on series detail. Added Name updates on volume on scan

* Removed some debug code
This commit is contained in:
Joseph Milazzo 2022-07-13 15:19:00 -04:00 committed by GitHub
parent ea845ca64d
commit 141d10e6da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 66 additions and 36 deletions

View File

@ -1,6 +1,7 @@
 
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using API.Entities;
using API.Entities.Interfaces; using API.Entities.Interfaces;
namespace API.DTOs namespace API.DTOs
@ -8,7 +9,9 @@ namespace API.DTOs
public class VolumeDto : IHasReadTimeEstimate public class VolumeDto : IHasReadTimeEstimate
{ {
public int Id { get; set; } public int Id { get; set; }
/// <inheritdoc cref="Volume.Number"/>
public int Number { get; set; } public int Number { get; set; }
/// <inheritdoc cref="Volume.Name"/>
public string Name { get; set; } public string Name { get; set; }
public int Pages { get; set; } public int Pages { get; set; }
public int PagesRead { get; set; } public int PagesRead { get; set; }

View File

@ -23,20 +23,20 @@ namespace API.Services;
public interface IMetadataService public interface IMetadataService
{ {
/// <summary> /// <summary>
/// Recalculates metadata for all entities in a library. /// Recalculates cover images for all entities in a library.
/// </summary> /// </summary>
/// <param name="libraryId"></param> /// <param name="libraryId"></param>
/// <param name="forceUpdate"></param> /// <param name="forceUpdate"></param>
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
Task RefreshMetadata(int libraryId, bool forceUpdate = false); Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false);
/// <summary> /// <summary>
/// Performs a forced refresh of metadata just for a series and it's nested entities /// Performs a forced refresh of cover images just for a series and it's nested entities
/// </summary> /// </summary>
/// <param name="libraryId"></param> /// <param name="libraryId"></param>
/// <param name="seriesId"></param> /// <param name="seriesId"></param>
/// <param name="forceUpdate">Overrides any cache logic and forces execution</param> /// <param name="forceUpdate">Overrides any cache logic and forces execution</param>
Task RefreshMetadataForSeries(int libraryId, int seriesId, bool forceUpdate = true); Task GenerateCoversForSeries(int libraryId, int seriesId, bool forceUpdate = true);
} }
public class MetadataService : IMetadataService public class MetadataService : IMetadataService
@ -48,6 +48,7 @@ public class MetadataService : IMetadataService
private readonly IReadingItemService _readingItemService; private readonly IReadingItemService _readingItemService;
private readonly IDirectoryService _directoryService; private readonly IDirectoryService _directoryService;
private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst(); private readonly ChapterSortComparerZeroFirst _chapterSortComparerForInChapterSorting = new ChapterSortComparerZeroFirst();
private IList<SignalRMessage> _updateEvents = new List<SignalRMessage>();
public MetadataService(IUnitOfWork unitOfWork, ILogger<MetadataService> logger, public MetadataService(IUnitOfWork unitOfWork, ILogger<MetadataService> logger,
IEventHub eventHub, ICacheHelper cacheHelper, IEventHub eventHub, ICacheHelper cacheHelper,
IReadingItemService readingItemService, IDirectoryService directoryService) IReadingItemService readingItemService, IDirectoryService directoryService)
@ -65,25 +66,27 @@ public class MetadataService : IMetadataService
/// </summary> /// </summary>
/// <param name="chapter"></param> /// <param name="chapter"></param>
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param> /// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
private async Task<bool> UpdateChapterCoverImage(Chapter chapter, bool forceUpdate) private Task<bool> UpdateChapterCoverImage(Chapter chapter, bool forceUpdate)
{ {
var firstFile = chapter.Files.MinBy(x => x.Chapter); var firstFile = chapter.Files.MinBy(x => x.Chapter);
if (!_cacheHelper.ShouldUpdateCoverImage(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage), firstFile, chapter.Created, forceUpdate, chapter.CoverImageLocked)) if (!_cacheHelper.ShouldUpdateCoverImage(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, chapter.CoverImage), firstFile, chapter.Created, forceUpdate, chapter.CoverImageLocked))
return false; return Task.FromResult(false);
if (firstFile == null) return false; if (firstFile == null) return Task.FromResult(false);
_logger.LogDebug("[MetadataService] Generating cover image for {File}", firstFile.FilePath); _logger.LogDebug("[MetadataService] Generating cover image for {File}", firstFile.FilePath);
chapter.CoverImage = _readingItemService.GetCoverImage(firstFile.FilePath, ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId), firstFile.Format); chapter.CoverImage = _readingItemService.GetCoverImage(firstFile.FilePath, ImageService.GetChapterFormat(chapter.Id, chapter.VolumeId), firstFile.Format);
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), false); // await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate,
return true; // MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter), false);
_updateEvents.Add(MessageFactory.CoverUpdateEvent(chapter.Id, MessageFactoryEntityTypes.Chapter));
return Task.FromResult(true);
} }
private void UpdateChapterLastModified(Chapter chapter, bool forceUpdate) private void UpdateChapterLastModified(Chapter chapter, bool forceUpdate)
{ {
var firstFile = chapter.Files.OrderBy(x => x.Chapter).FirstOrDefault(); var firstFile = chapter.Files.MinBy(x => x.Chapter);
if (firstFile == null || _cacheHelper.HasFileNotChangedSinceCreationOrLastScan(chapter, forceUpdate, firstFile)) return; if (firstFile == null || _cacheHelper.HasFileNotChangedSinceCreationOrLastScan(chapter, forceUpdate, firstFile)) return;
firstFile.UpdateLastModified(); firstFile.UpdateLastModified();
@ -94,22 +97,23 @@ public class MetadataService : IMetadataService
/// </summary> /// </summary>
/// <param name="volume"></param> /// <param name="volume"></param>
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param> /// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
private async Task<bool> UpdateVolumeCoverImage(Volume volume, bool forceUpdate) private Task<bool> UpdateVolumeCoverImage(Volume volume, bool forceUpdate)
{ {
// We need to check if Volume coverImage matches first chapters if forceUpdate is false // We need to check if Volume coverImage matches first chapters if forceUpdate is false
if (volume == null || !_cacheHelper.ShouldUpdateCoverImage( if (volume == null || !_cacheHelper.ShouldUpdateCoverImage(
_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, volume.CoverImage), _directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, volume.CoverImage),
null, volume.Created, forceUpdate)) return false; null, volume.Created, forceUpdate)) return Task.FromResult(false);
volume.Chapters ??= new List<Chapter>(); volume.Chapters ??= new List<Chapter>();
var firstChapter = volume.Chapters.MinBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting); var firstChapter = volume.Chapters.MinBy(x => double.Parse(x.Number), _chapterSortComparerForInChapterSorting);
if (firstChapter == null) return false; if (firstChapter == null) return Task.FromResult(false);
volume.CoverImage = firstChapter.CoverImage; volume.CoverImage = firstChapter.CoverImage;
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume), false); //await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume), false);
_updateEvents.Add(MessageFactory.CoverUpdateEvent(volume.Id, MessageFactoryEntityTypes.Volume));
return Task.FromResult(true);
return true;
} }
/// <summary> /// <summary>
@ -117,13 +121,13 @@ public class MetadataService : IMetadataService
/// </summary> /// </summary>
/// <param name="series"></param> /// <param name="series"></param>
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param> /// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
private async Task UpdateSeriesCoverImage(Series series, bool forceUpdate) private Task UpdateSeriesCoverImage(Series series, bool forceUpdate)
{ {
if (series == null) return; if (series == null) return Task.CompletedTask;
if (!_cacheHelper.ShouldUpdateCoverImage(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, series.CoverImage), if (!_cacheHelper.ShouldUpdateCoverImage(_directoryService.FileSystem.Path.Join(_directoryService.CoverImageDirectory, series.CoverImage),
null, series.Created, forceUpdate, series.CoverImageLocked)) null, series.Created, forceUpdate, series.CoverImageLocked))
return; return Task.CompletedTask;
series.Volumes ??= new List<Volume>(); series.Volumes ??= new List<Volume>();
var firstCover = series.Volumes.GetCoverImage(series.Format); var firstCover = series.Volumes.GetCoverImage(series.Format);
@ -143,7 +147,9 @@ public class MetadataService : IMetadataService
} }
} }
series.CoverImage = firstCover?.CoverImage ?? coverImage; series.CoverImage = firstCover?.CoverImage ?? coverImage;
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false); //await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false);
_updateEvents.Add(MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series));
return Task.CompletedTask;
} }
@ -194,18 +200,20 @@ public class MetadataService : IMetadataService
/// <summary> /// <summary>
/// Refreshes Metadata for a whole library /// Refreshes Cover Images for a whole library
/// </summary> /// </summary>
/// <remarks>This can be heavy on memory first run</remarks> /// <remarks>This can be heavy on memory first run</remarks>
/// <param name="libraryId"></param> /// <param name="libraryId"></param>
/// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param> /// <param name="forceUpdate">Force updating cover image even if underlying file has not been modified or chapter already has a cover image</param>
[DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)] [DisableConcurrentExecution(timeoutInSeconds: 60 * 60 * 60)]
[AutomaticRetry(Attempts = 0, OnAttemptsExceeded = AttemptsExceededAction.Delete)] [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Delete)]
public async Task RefreshMetadata(int libraryId, bool forceUpdate = false) public async Task GenerateCoversForLibrary(int libraryId, bool forceUpdate = false)
{ {
var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None); var library = await _unitOfWork.LibraryRepository.GetLibraryForIdAsync(libraryId, LibraryIncludes.None);
_logger.LogInformation("[MetadataService] Beginning metadata refresh of {LibraryName}", library.Name); _logger.LogInformation("[MetadataService] Beginning metadata refresh of {LibraryName}", library.Name);
_updateEvents.Clear();
var chunkInfo = await _unitOfWork.SeriesRepository.GetChunkInfo(library.Id); var chunkInfo = await _unitOfWork.SeriesRepository.GetChunkInfo(library.Id);
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
var totalTime = 0L; var totalTime = 0L;
@ -253,6 +261,8 @@ public class MetadataService : IMetadataService
await _unitOfWork.CommitAsync(); await _unitOfWork.CommitAsync();
await FlushEvents();
_logger.LogInformation( _logger.LogInformation(
"[MetadataService] Processed {SeriesStart} - {SeriesEnd} out of {TotalSeries} series in {ElapsedScanTime} milliseconds for {LibraryName}", "[MetadataService] Processed {SeriesStart} - {SeriesEnd} out of {TotalSeries} series in {ElapsedScanTime} milliseconds for {LibraryName}",
chunk * chunkInfo.ChunkSize, (chunk * chunkInfo.ChunkSize) + nonLibrarySeries.Count, chunkInfo.TotalSize, stopwatch.ElapsedMilliseconds, library.Name); chunk * chunkInfo.ChunkSize, (chunk * chunkInfo.ChunkSize) + nonLibrarySeries.Count, chunkInfo.TotalSize, stopwatch.ElapsedMilliseconds, library.Name);
@ -280,7 +290,7 @@ public class MetadataService : IMetadataService
/// <param name="libraryId"></param> /// <param name="libraryId"></param>
/// <param name="seriesId"></param> /// <param name="seriesId"></param>
/// <param name="forceUpdate">Overrides any cache logic and forces execution</param> /// <param name="forceUpdate">Overrides any cache logic and forces execution</param>
public async Task RefreshMetadataForSeries(int libraryId, int seriesId, bool forceUpdate = true) public async Task GenerateCoversForSeries(int libraryId, int seriesId, bool forceUpdate = true)
{ {
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId); var series = await _unitOfWork.SeriesRepository.GetFullSeriesForSeriesIdAsync(seriesId);
@ -309,8 +319,19 @@ public class MetadataService : IMetadataService
if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync()) if (_unitOfWork.HasChanges() && await _unitOfWork.CommitAsync())
{ {
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false); await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, MessageFactory.CoverUpdateEvent(series.Id, MessageFactoryEntityTypes.Series), false);
await FlushEvents();
} }
_logger.LogInformation("[MetadataService] Updated metadata for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds); _logger.LogInformation("[MetadataService] Updated metadata for {SeriesName} in {ElapsedMilliseconds} milliseconds", series.Name, sw.ElapsedMilliseconds);
} }
private async Task FlushEvents()
{
// Send all events out now that entities are saved
foreach (var updateEvent in _updateEvents)
{
await _eventHub.SendMessageAsync(MessageFactory.CoverUpdate, updateEvent, false);
}
_updateEvents.Clear();
}
} }

View File

@ -188,7 +188,7 @@ public class TaskScheduler : ITaskScheduler
} }
_logger.LogInformation("Enqueuing library metadata refresh for: {LibraryId}", libraryId); _logger.LogInformation("Enqueuing library metadata refresh for: {LibraryId}", libraryId);
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, forceUpdate)); BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForLibrary(libraryId, forceUpdate));
} }
public void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false) public void RefreshSeriesMetadata(int libraryId, int seriesId, bool forceUpdate = false)
@ -200,7 +200,7 @@ public class TaskScheduler : ITaskScheduler
} }
_logger.LogInformation("Enqueuing series metadata refresh for: {SeriesId}", seriesId); _logger.LogInformation("Enqueuing series metadata refresh for: {SeriesId}", seriesId);
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, seriesId, forceUpdate)); BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForSeries(libraryId, seriesId, forceUpdate));
} }
public void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false) public void ScanSeries(int libraryId, int seriesId, bool forceUpdate = false)

View File

@ -192,7 +192,7 @@ public class ScannerService : IScannerService
await CleanupDbEntities(); await CleanupDbEntities();
BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds)); BackgroundJob.Enqueue(() => _cacheService.CleanupChapters(chapterIds));
BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.TempDirectory)); BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.TempDirectory));
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadataForSeries(libraryId, series.Id, false)); BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForSeries(libraryId, series.Id, false));
BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(libraryId, series.Id, false)); BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanSeries(libraryId, series.Id, false));
} }
@ -327,7 +327,7 @@ public class ScannerService : IScannerService
await CleanupDbEntities(); await CleanupDbEntities();
BackgroundJob.Enqueue(() => _metadataService.RefreshMetadata(libraryId, false)); BackgroundJob.Enqueue(() => _metadataService.GenerateCoversForLibrary(libraryId, false));
BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanLibrary(libraryId, false)); BackgroundJob.Enqueue(() => _wordCountAnalyzerService.ScanLibrary(libraryId, false));
BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.TempDirectory)); BackgroundJob.Enqueue(() => _directoryService.ClearDirectory(_directoryService.TempDirectory));
} }
@ -804,6 +804,8 @@ public class ScannerService : IScannerService
_unitOfWork.VolumeRepository.Add(volume); _unitOfWork.VolumeRepository.Add(volume);
} }
volume.Name = volumeNumber;
_logger.LogDebug("[ScannerService] Parsing {SeriesName} - Volume {VolumeNumber}", series.Name, volume.Name); _logger.LogDebug("[ScannerService] Parsing {SeriesName} - Volume {VolumeNumber}", series.Name, volume.Name);
var infos = parsedInfos.Where(p => p.Volumes == volumeNumber).ToArray(); var infos = parsedInfos.Where(p => p.Volumes == volumeNumber).ToArray();
UpdateChapters(series, volume, infos); UpdateChapters(series, volume, infos);

View File

@ -185,7 +185,7 @@
<ng-template #volumeListLayout> <ng-template #volumeListLayout>
<ng-container *ngFor="let volume of scroll.viewPortItems; let idx = index; trackBy: trackByVolumeIdentity"> <ng-container *ngFor="let volume of scroll.viewPortItems; let idx = index; trackBy: trackByVolumeIdentity">
<app-list-item [imageUrl]="imageService.getVolumeCoverImage(volume.id)" <app-list-item [imageUrl]="imageService.getVolumeCoverImage(volume.id)"
[seriesName]="series.name" [entity]="volume" *ngIf="volume.number != 0" [seriesName]="series.name" [entity]="volume"
[actions]="volumeActions" [libraryType]="libraryType" imageWidth="130px" imageHeight="" [actions]="volumeActions" [libraryType]="libraryType" imageWidth="130px" imageHeight=""
[pagesRead]="volume.pagesRead" [totalPages]="volume.pages" (read)="openVolume(volume)" [pagesRead]="volume.pagesRead" [totalPages]="volume.pages" (read)="openVolume(volume)"
[blur]="user?.preferences?.blurUnreadSummaries || false"> [blur]="user?.preferences?.blurUnreadSummaries || false">
@ -194,7 +194,6 @@
</ng-container> </ng-container>
</app-list-item> </app-list-item>
</ng-container> </ng-container>
</ng-template> </ng-template>
</virtual-scroller> </virtual-scroller>
</ng-template> </ng-template>
@ -203,7 +202,7 @@
<li [ngbNavItem]="TabID.Chapters" *ngIf="chapters.length > 0"> <li [ngbNavItem]="TabID.Chapters" *ngIf="chapters.length > 0">
<a ngbNavLink>{{utilityService.formatChapterName(libraryType) + 's'}}</a> <a ngbNavLink>{{utilityService.formatChapterName(libraryType) + 's'}}</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<virtual-scroller #scroll [items]="chapters" [parentScroll]="scrollingBlock"> <virtual-scroller #scroll [items]="chapters" [parentScroll]="scrollingBlock">
<ng-container *ngIf="renderMode === PageLayoutMode.Cards; else chapterListLayout"> <ng-container *ngIf="renderMode === PageLayoutMode.Cards; else chapterListLayout">
<div class="card-container row g-0" #container> <div class="card-container row g-0" #container>
<div *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity"> <div *ngFor="let item of scroll.viewPortItems; let idx = index; trackBy: trackByChapterIdentity">
@ -227,7 +226,7 @@
[pagesRead]="chapter.pagesRead" [totalPages]="chapter.pages" (read)="openChapter(chapter)" [pagesRead]="chapter.pagesRead" [totalPages]="chapter.pages" (read)="openChapter(chapter)"
[includeVolume]="true" [blur]="user?.preferences?.blurUnreadSummaries || false"> [includeVolume]="true" [blur]="user?.preferences?.blurUnreadSummaries || false">
<ng-container title> <ng-container title>
<app-entity-title [libraryType]="libraryType" [entity]="chapter" [seriesName]="series.name" [includeVolume]="true" [prioritizeTitleName]="false"></app-entity-title> <app-entity-title [libraryType]="libraryType" [entity]="chapter" [seriesName]="series.name" [includeVolume]="true" [prioritizeTitleName]="false"></app-entity-title>
</ng-container> </ng-container>
</app-list-item> </app-list-item>
</div> </div>
@ -262,6 +261,9 @@
<ng-container title> <ng-container title>
{{chapter.title || chapter.range}} {{chapter.title || chapter.range}}
</ng-container> </ng-container>
<!-- <ng-container title>
<app-entity-title [libraryType]="libraryType" [entity]="chapter" [seriesName]="series.name" [includeVolume]="true" [prioritizeTitleName]="true"></app-entity-title>
</ng-container> -->
</app-list-item> </app-list-item>
</ng-container> </ng-container>
</ng-template> </ng-template>

View File

@ -120,9 +120,11 @@ export class DownloadService {
} }
return of(0); return of(0);
}), switchMap(async (size) => { }), switchMap(async (size) => {
return await this.confirmSize(size, entityType); return await this.confirmSize(size, entityType);
}) })
).pipe(filter(wantsToDownload => wantsToDownload), switchMap(() => { ).pipe(filter(wantsToDownload => {
return wantsToDownload;
}), switchMap(() => {
return downloadCall.pipe( return downloadCall.pipe(
tap((d) => { tap((d) => {
if (callback) callback(d); if (callback) callback(d);