Serialize longs as strings to prevent precision loss (d'oh!)

This commit is contained in:
TheOtherP 2025-07-06 10:41:35 +02:00
parent a38d813b6e
commit 7caae7324f
12 changed files with 88 additions and 64 deletions

View File

@ -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;
}
});

View File

@ -263,7 +263,6 @@
*ngFor="let downloader of enabledDownloaders"
[searchResult]="groupedResult.result"
[downloader]="downloader"
[alwaysAsk]="false"
(downloadComplete)="onDownloadComplete($event)">
</app-addable-nzb>
</div>

View File

@ -83,7 +83,7 @@ export class SearchResultsComponent implements OnInit {
groupedResults: GroupedResult[] = [];
// Selection state
selectedResultsIds: Set<number> = new Set();
selectedResultsIds: Set<string> = new Set();
lastSelectedIndex: number = -1;
lastSelectionAction: "select" | "unselect" | null = null;
showIndexerStatuses = false;

View File

@ -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<DownloadResponse> {
return this.sendNzbAddCommand(downloader, searchResults, category);
download(downloader: Downloader, searchResults: SearchResultDl[]): Observable<DownloadResponse> {
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<DownloadResponse>(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);
}
}
/**

View File

@ -54,7 +54,7 @@ export interface IndexerSearchMetaData {
}
export interface SearchResultWebTO {
searchResultId: number;
searchResultId: string;
title: string;
link: string;
guid: string;

View File

@ -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;

View File

@ -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<Long> addedIds;
@JsonSerialize(contentUsing = ToStringSerializer.class)
private Collection<Long> missedIds;
}

View File

@ -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<Long> addedIds;
@JsonSerialize(contentUsing = ToStringSerializer.class)
private Collection<Long> missedIds;
}

View File

@ -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;

View File

@ -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;

View File

@ -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
}

View File

@ -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;