diff --git a/core/ui-src/hydra-ng/src/app/components/addable-nzb/addable-nzb.component.ts b/core/ui-src/hydra-ng/src/app/components/addable-nzb/addable-nzb.component.ts index cc55c6fb5..596b63ef1 100644 --- a/core/ui-src/hydra-ng/src/app/components/addable-nzb/addable-nzb.component.ts +++ b/core/ui-src/hydra-ng/src/app/components/addable-nzb/addable-nzb.component.ts @@ -1,9 +1,6 @@ import {Component, EventEmitter, Input, Output} from "@angular/core"; -import {NgbModal} from "@ng-bootstrap/ng-bootstrap"; -import {take} from "rxjs/operators"; -import {Downloader, DownloaderService, SearchResultDl} from "../../services/downloader.service"; +import {Downloader, DownloaderService} from "../../services/downloader.service"; import {SearchResultWebTO} from "../../services/search.service"; -import {CategorySelectionModalComponent} from "../category-selection-modal/category-selection-modal.component"; @Component({ @@ -15,18 +12,15 @@ import {CategorySelectionModalComponent} from "../category-selection-modal/categ export class AddableNzbComponent { @Input() searchResult!: SearchResultWebTO; @Input() downloader!: Downloader; - @Input() alwaysAsk: boolean = false; @Output() downloadComplete = new EventEmitter<{ successful: boolean, message?: string }>(); cssClass: string = ""; isDownloading: boolean = false; constructor( - private downloaderService: DownloaderService, - private modalService: NgbModal + private downloaderService: DownloaderService ) { this.updateCssClass(); - } ngOnChanges(): void { @@ -57,55 +51,26 @@ export class AddableNzbComponent { } this.isDownloading = true; - const originalClass = this.cssClass; this.cssClass = "nzb-spinning"; - // If alwaysAsk or no default category, open modal - if (this.alwaysAsk || !this.downloader.defaultCategory) { - this.downloaderService.getCategories(this.downloader).pipe(take(1)).subscribe({ - next: (categories) => { - const modalRef = this.modalService.open(CategorySelectionModalComponent, {size: "sm"}); - modalRef.componentInstance.categories = categories; - modalRef.result.then((selectedCategory: string) => { - this.doDownload(this.searchResult, selectedCategory); - }, () => { - this.cssClass = originalClass; - this.isDownloading = false; - }); - }, - error: () => { - this.cssClass = this.buildCssClass("-error"); - this.downloadComplete.emit({ - successful: false, - message: "Failed to load categories from downloader." - }); - this.isDownloading = false; - } - }); - } else { - this.doDownload(this.searchResult, this.downloader.defaultCategory); - } + this.doDownload(); } - - private buildSearchResultDl(searchResult: SearchResultWebTO): SearchResultDl { - return { - searchResultId: this.searchResult.searchResultId, - originalCategory: this.searchResult.originalCategory, - mappedCategory: this.searchResult.category - }; - } - private buildCssClass(postfix: string) { let baseClass = this.getCssClass(this.downloader.downloaderType); return baseClass + " " + baseClass + postfix; } - private doDownload(searchResult: SearchResultWebTO, category: string) { - this.downloaderService.download(this.downloader, [this.buildSearchResultDl(searchResult)], category) + private doDownload() { + const searchResultDl = { + searchResultId: this.searchResult.searchResultId, + originalCategory: this.searchResult.originalCategory, + mappedCategory: this.searchResult.category + }; + this.downloaderService.download(this.downloader, [searchResultDl]) .subscribe({ next: (response) => { if ( - response.successful && response.addedIds?.includes(searchResult.searchResultId) + response.successful && response.addedIds?.includes(searchResultDl.searchResultId) ) { this.cssClass = this.buildCssClass("-success"); this.downloadComplete.emit({successful: true}); @@ -115,12 +80,20 @@ export class AddableNzbComponent { } this.isDownloading = false; }, - error: () => { + error: (error) => { this.cssClass = this.buildCssClass("-error"); - this.downloadComplete.emit({ - successful: false, - message: "An unexpected error occurred while trying to contact NZBHydra or add the NZB." - }); + if (error.message === "Category selection cancelled") { + this.cssClass = this.getCssClass(this.downloader.downloaderType); + this.downloadComplete.emit({ + successful: false, + message: "Download cancelled by user." + }); + } else { + this.downloadComplete.emit({ + successful: false, + message: "An unexpected error occurred while trying to contact NZBHydra or add the NZB." + }); + } this.isDownloading = false; } }); diff --git a/core/ui-src/hydra-ng/src/app/components/search-results/search-results.component.html b/core/ui-src/hydra-ng/src/app/components/search-results/search-results.component.html index 86405a4b3..74bc25595 100644 --- a/core/ui-src/hydra-ng/src/app/components/search-results/search-results.component.html +++ b/core/ui-src/hydra-ng/src/app/components/search-results/search-results.component.html @@ -263,7 +263,6 @@ *ngFor="let downloader of enabledDownloaders" [searchResult]="groupedResult.result" [downloader]="downloader" - [alwaysAsk]="false" (downloadComplete)="onDownloadComplete($event)"> diff --git a/core/ui-src/hydra-ng/src/app/components/search-results/search-results.component.ts b/core/ui-src/hydra-ng/src/app/components/search-results/search-results.component.ts index cd623a4de..b6c797715 100644 --- a/core/ui-src/hydra-ng/src/app/components/search-results/search-results.component.ts +++ b/core/ui-src/hydra-ng/src/app/components/search-results/search-results.component.ts @@ -83,7 +83,7 @@ export class SearchResultsComponent implements OnInit { groupedResults: GroupedResult[] = []; // Selection state - selectedResultsIds: Set = new Set(); + selectedResultsIds: Set = new Set(); lastSelectedIndex: number = -1; lastSelectionAction: "select" | "unselect" | null = null; showIndexerStatuses = false; diff --git a/core/ui-src/hydra-ng/src/app/services/downloader.service.ts b/core/ui-src/hydra-ng/src/app/services/downloader.service.ts index 40ce1cd95..3cedf5d6a 100644 --- a/core/ui-src/hydra-ng/src/app/services/downloader.service.ts +++ b/core/ui-src/hydra-ng/src/app/services/downloader.service.ts @@ -1,7 +1,9 @@ import {HttpClient} from "@angular/common/http"; import {Injectable} from "@angular/core"; +import {NgbModal} from "@ng-bootstrap/ng-bootstrap"; import {Observable, throwError} from "rxjs"; -import {catchError, map} from "rxjs/operators"; +import {catchError, map, switchMap, take} from "rxjs/operators"; +import {CategorySelectionModalComponent} from "../components/category-selection-modal/category-selection-modal.component"; import {ConfigService} from "./config.service"; export interface Downloader { @@ -13,7 +15,7 @@ export interface Downloader { } export interface SearchResultDl { - searchResultId: number; + searchResultId: string; originalCategory: string; mappedCategory: string; } @@ -26,7 +28,7 @@ export interface DownloadRequest { export interface DownloadResponse { successful: boolean; - addedIds?: number[]; + addedIds?: string[]; message?: string; } @@ -36,7 +38,8 @@ export interface DownloadResponse { export class DownloaderService { constructor( private http: HttpClient, - private configService: ConfigService + private configService: ConfigService, + private modalService: NgbModal ) { } @@ -55,9 +58,36 @@ export class DownloaderService { /** * Download NZB to specified downloader + * Opens category selection modal if no default category is set */ - download(downloader: Downloader, searchResults: SearchResultDl[], category: string): Observable { - return this.sendNzbAddCommand(downloader, searchResults, category); + download(downloader: Downloader, searchResults: SearchResultDl[]): Observable { + let category = downloader.defaultCategory; + if (!category) { + return this.getCategories(downloader).pipe( + take(1), + switchMap(categories => { + const modalRef = this.modalService.open(CategorySelectionModalComponent, {size: "sm"}); + modalRef.componentInstance.categories = categories; + + return new Observable(observer => { + modalRef.result.then((selectedCategory: string) => { + this.sendNzbAddCommand(downloader, searchResults, selectedCategory).subscribe({ + next: (response) => observer.next(response), + error: (error) => observer.error(error) + }); + }, () => { + observer.error(new Error("Category selection cancelled")); + }); + }); + }), + catchError(error => { + console.error("Error fetching categories:", error); + return throwError(() => error); + }) + ); + } else { + return this.sendNzbAddCommand(downloader, searchResults, category); + } } /** diff --git a/core/ui-src/hydra-ng/src/app/services/search.service.ts b/core/ui-src/hydra-ng/src/app/services/search.service.ts index e899ee1ac..efdfe9e9d 100644 --- a/core/ui-src/hydra-ng/src/app/services/search.service.ts +++ b/core/ui-src/hydra-ng/src/app/services/search.service.ts @@ -54,7 +54,7 @@ export interface IndexerSearchMetaData { } export interface SearchResultWebTO { - searchResultId: number; + searchResultId: string; title: string; link: string; guid: string; diff --git a/shared/mapping/src/main/java/org/nzbhydra/downloading/AddFilesRequest.java b/shared/mapping/src/main/java/org/nzbhydra/downloading/AddFilesRequest.java index f12333a03..32510479c 100644 --- a/shared/mapping/src/main/java/org/nzbhydra/downloading/AddFilesRequest.java +++ b/shared/mapping/src/main/java/org/nzbhydra/downloading/AddFilesRequest.java @@ -1,5 +1,7 @@ package org.nzbhydra.downloading; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -23,7 +25,7 @@ public class AddFilesRequest { @NoArgsConstructor @AllArgsConstructor public static class SearchResult { - + @JsonSerialize(using = ToStringSerializer.class) private Long searchResultId; private String originalCategory; private String mappedCategory; diff --git a/shared/mapping/src/main/java/org/nzbhydra/downloading/FileZipResponse.java b/shared/mapping/src/main/java/org/nzbhydra/downloading/FileZipResponse.java index a0d8a3f7c..3ca613996 100644 --- a/shared/mapping/src/main/java/org/nzbhydra/downloading/FileZipResponse.java +++ b/shared/mapping/src/main/java/org/nzbhydra/downloading/FileZipResponse.java @@ -16,6 +16,8 @@ package org.nzbhydra.downloading; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -32,6 +34,8 @@ public class FileZipResponse { private boolean successful; private String zipFilepath; private String message; + @JsonSerialize(contentUsing = ToStringSerializer.class) private Collection addedIds; + @JsonSerialize(contentUsing = ToStringSerializer.class) private Collection missedIds; } diff --git a/shared/mapping/src/main/java/org/nzbhydra/downloading/downloaders/AddNzbsResponse.java b/shared/mapping/src/main/java/org/nzbhydra/downloading/downloaders/AddNzbsResponse.java index 573ec4c52..e0a286e6e 100644 --- a/shared/mapping/src/main/java/org/nzbhydra/downloading/downloaders/AddNzbsResponse.java +++ b/shared/mapping/src/main/java/org/nzbhydra/downloading/downloaders/AddNzbsResponse.java @@ -16,6 +16,8 @@ package org.nzbhydra.downloading.downloaders; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -33,6 +35,8 @@ public class AddNzbsResponse { */ private boolean successful; private String message; + @JsonSerialize(contentUsing = ToStringSerializer.class) private Collection addedIds; + @JsonSerialize(contentUsing = ToStringSerializer.class) private Collection missedIds; } diff --git a/shared/mapping/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderStatus.java b/shared/mapping/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderStatus.java index a49bf74b2..4eeb19daf 100644 --- a/shared/mapping/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderStatus.java +++ b/shared/mapping/src/main/java/org/nzbhydra/downloading/downloaders/DownloaderStatus.java @@ -16,6 +16,8 @@ package org.nzbhydra.downloading.downloaders; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.google.common.collect.Iterables; import lombok.AllArgsConstructor; import lombok.Builder; @@ -49,8 +51,9 @@ public class DownloaderStatus { private int elementsInQueue; private String downloadingTitle; + @JsonSerialize(using = ToStringSerializer.class) private long downloadingTitleRemainingSizeKilobytes; - + @JsonSerialize(using = ToStringSerializer.class) private long downloadingTitleRemainingTimeSeconds; private int downloadingTitlePercentFinished; diff --git a/shared/mapping/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSearchMetaData.java b/shared/mapping/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSearchMetaData.java index 94affeb3f..fac1ce405 100644 --- a/shared/mapping/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSearchMetaData.java +++ b/shared/mapping/src/main/java/org/nzbhydra/searching/dtoseventsenums/IndexerSearchMetaData.java @@ -16,6 +16,8 @@ package org.nzbhydra.searching.dtoseventsenums; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import lombok.Data; import org.nzbhydra.springnative.ReflectionMarker; @@ -31,6 +33,7 @@ public class IndexerSearchMetaData { private int numberOfAvailableResults; private int numberOfFoundResults; private int offset; + @JsonSerialize(using = ToStringSerializer.class) private long responseTime; private boolean totalResultsKnown; private boolean wasSuccessful; diff --git a/shared/mapping/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchRequestParameters.java b/shared/mapping/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchRequestParameters.java index 1160bdb31..26bf9dcbd 100644 --- a/shared/mapping/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchRequestParameters.java +++ b/shared/mapping/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchRequestParameters.java @@ -16,6 +16,8 @@ package org.nzbhydra.searching.dtoseventsenums; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -53,7 +55,7 @@ public class SearchRequestParameters { private Integer season; private String episode; - + @JsonSerialize(using = ToStringSerializer.class) private long searchRequestId; //Sent by the GUI to identify this search when getting updates for it } diff --git a/shared/mapping/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchResultWebTO.java b/shared/mapping/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchResultWebTO.java index d2531933e..3b3e07348 100644 --- a/shared/mapping/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchResultWebTO.java +++ b/shared/mapping/src/main/java/org/nzbhydra/searching/dtoseventsenums/SearchResultWebTO.java @@ -16,6 +16,8 @@ package org.nzbhydra.searching.dtoseventsenums; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import lombok.Builder; import lombok.Getter; import lombok.Setter; @@ -27,7 +29,6 @@ import lombok.extern.jackson.Jacksonized; @Jacksonized public class SearchResultWebTO { - private String age; private Boolean age_precise; private String category; @@ -36,6 +37,7 @@ public class SearchResultWebTO { private Integer comments; private String details_link; private String downloadType; + @JsonSerialize(using = ToStringSerializer.class) private Long epoch; private Integer files; private Integer grabs; @@ -49,8 +51,10 @@ public class SearchResultWebTO { private String link; private String originalCategory; private String poster; + @JsonSerialize(using = ToStringSerializer.class) private Long searchResultId; private String source; + @JsonSerialize(using = ToStringSerializer.class) private Long size; private String title; private String season;