Ingest ExternalReviews from K+

Adds a new entity ExternalChapterMetadata, which would allow us to
extend chapters to Recommendations, Ratings, etc in the future
This commit is contained in:
Amelia 2025-04-28 16:19:03 +02:00
parent 749fb24185
commit 052b3f9fe4
No known key found for this signature in database
GPG key ID: D6D0ECE365407EAA
29 changed files with 647 additions and 137 deletions

View file

@ -0,0 +1,9 @@
import {UserReview} from "../_single-module/review-card/user-review";
import {Rating} from "./rating";
export type ChapterDetail = {
rating: number;
hasBeenRated: boolean;
reviews: UserReview[];
ratings: Rating[];
};

View file

@ -5,6 +5,7 @@ import {Chapter} from "../_models/chapter";
import {TextResonse} from "../_types/text-response";
import {UserReview} from "../_single-module/review-card/user-review";
import {Rating} from "../_models/rating";
import {ChapterDetail} from "../_models/chapter-detail";
@Injectable({
providedIn: 'root'
@ -31,8 +32,8 @@ export class ChapterService {
return this.httpClient.post(this.baseUrl + 'chapter/update', chapter, TextResonse);
}
chapterReviews(chapterId: number) {
return this.httpClient.get<Array<UserReview>>(this.baseUrl + 'chapter/review?chapterId='+chapterId);
chapterDetailPlus(seriesId: number, chapterId: number) {
return this.httpClient.get<ChapterDetail>(this.baseUrl + `chapter/chapter-detail-plus?chapterId=${chapterId}&seriesId=${seriesId}`);
}
}

View file

@ -13,13 +13,6 @@ export class ReviewService {
constructor(private httpClient: HttpClient) { }
getReviews(seriesId: number, chapterId?: number) {
if (chapterId) {
return this.httpClient.get<UserReview[]>(this.baseUrl + `review?chapterId=${chapterId}&seriesId=${seriesId}`);
}
return this.httpClient.get<UserReview[]>(this.baseUrl + 'review?seriesId=' + seriesId);
}
deleteReview(seriesId: number, chapterId?: number) {
if (chapterId) {
return this.httpClient.delete(this.baseUrl + `review?chapterId=${chapterId}&seriesId=${seriesId}`);
@ -28,15 +21,15 @@ export class ReviewService {
return this.httpClient.delete(this.baseUrl + 'review?seriesId=' + seriesId);
}
updateReview(seriesId: number, body: string, rating: number, chapterId?: number) {
updateReview(seriesId: number, body: string, chapterId?: number) {
if (chapterId) {
return this.httpClient.post<UserReview>(this.baseUrl + `review?chapterId=${chapterId}&seriesId=${seriesId}`, {
rating, body
return this.httpClient.post<UserReview>(this.baseUrl + `review`, {
seriesId, chapterId, body
});
}
return this.httpClient.post<UserReview>(this.baseUrl + 'review', {
seriesId, rating, body
seriesId, body
});
}

View file

@ -1,12 +1,14 @@
import {ScrobbleProvider} from "../../_services/scrobbling.service";
export enum RatingAuthority {
User = 0,
Critic = 1
}
export interface UserReview {
seriesId: number;
libraryId: number;
volumeId?: number;
chapterId?: number;
rating: number;
hasBeenRated: boolean;
score: number;
username: string;
body: string;
@ -15,4 +17,5 @@ export interface UserReview {
bodyJustText?: string;
siteUrl?: string;
provider: ScrobbleProvider;
authority: RatingAuthority;
}

View file

@ -35,31 +35,21 @@ export class ReviewModalComponent implements OnInit {
protected readonly modal = inject(NgbActiveModal);
private readonly reviewService = inject(ReviewService);
private readonly seriesService = inject(SeriesService);
private readonly cdRef = inject(ChangeDetectorRef);
private readonly confirmService = inject(ConfirmService);
private readonly toastr = inject(ToastrService);
private readonly themeService = inject(ThemeService);
protected readonly minLength = 5;
@Input({required: true}) review!: UserReview;
reviewGroup!: FormGroup;
rating: number = 0;
starColor = this.themeService.getCssVariable('--rating-star-color');
ngOnInit(): void {
this.reviewGroup = new FormGroup({
reviewBody: new FormControl(this.review.body, [Validators.required, Validators.minLength(this.minLength)]),
});
this.rating = this.review.rating;
this.cdRef.markForCheck();
}
updateRating($event: number) {
this.rating = $event;
}
close() {
this.modal.close({success: false, review: this.review, action: ReviewModalCloseAction.Close});
}
@ -79,7 +69,7 @@ export class ReviewModalComponent implements OnInit {
return;
}
this.reviewService.updateReview(this.review.seriesId, model.reviewBody, this.rating, this.review.chapterId).subscribe(review => {
this.reviewService.updateReview(this.review.seriesId, model.reviewBody, this.review.chapterId).subscribe(review => {
this.modal.close({success: true, review: review, action: ReviewModalCloseAction.Edit});
});

View file

@ -28,11 +28,10 @@
<div class="mt-2 mb-2">
@let rating = userRating();
<app-external-rating [seriesId]="series.id"
[ratings]="[]"
[userRating]="rating?.rating || 0"
[hasUserRated]="rating !== undefined && rating.hasBeenRated"
[userRating]="rating"
[hasUserRated]="hasBeenRated"
[libraryType]="libraryType!"
[chapterId]="chapterId"
>

View file

@ -73,10 +73,11 @@ import {ReviewModalComponent} from "../_single-module/review-modal/review-modal.
import {ReviewsComponent} from "../_single-module/reviews/reviews.component";
import {ExternalRatingComponent} from "../series-detail/_components/external-rating/external-rating.component";
import {Rating} from "../_models/rating";
import {ReviewService} from "../_services/review.service";
enum TabID {
Related = 'related-tab',
Reviews = 'review-tab', // Only applicable for books
Reviews = 'review-tab',
Details = 'details-tab'
}
@ -111,8 +112,6 @@ enum TabID {
DatePipe,
DefaultDatePipe,
CoverImageComponent,
CarouselReelComponent,
ReviewCardComponent,
ReviewsComponent,
ExternalRatingComponent
],
@ -165,6 +164,9 @@ export class ChapterDetailComponent implements OnInit {
hasReadingProgress = false;
userReviews: Array<UserReview> = [];
plusReviews: Array<UserReview> = [];
rating: number = 0;
hasBeenRated: boolean = false;
weblinks: Array<string> = [];
activeTabId = TabID.Details;
/**
@ -233,7 +235,7 @@ export class ChapterDetailComponent implements OnInit {
series: this.seriesService.getSeries(this.seriesId),
chapter: this.chapterService.getChapterMetadata(this.chapterId),
libraryType: this.libraryService.getLibraryType(this.libraryId),
reviews: this.chapterService.chapterReviews(this.chapterId),
chapterDetail: this.chapterService.chapterDetailPlus(this.seriesId, this.chapterId),
}).subscribe(results => {
if (results.chapter === null) {
@ -245,8 +247,10 @@ export class ChapterDetailComponent implements OnInit {
this.chapter = results.chapter;
this.weblinks = this.chapter.webLinks.split(',');
this.libraryType = results.libraryType;
this.userReviews = results.reviews.filter(r => !r.isExternal);
this.plusReviews = results.reviews.filter(r => r.isExternal);
this.userReviews = results.chapterDetail.reviews.filter(r => !r.isExternal);
this.plusReviews = results.chapterDetail.reviews.filter(r => r.isExternal);
this.rating = results.chapterDetail.rating;
this.hasBeenRated = results.chapterDetail.hasBeenRated;
this.themeService.setColorScape(this.chapter.primaryColor, this.chapter.secondaryColor);
@ -386,10 +390,6 @@ export class ChapterDetailComponent implements OnInit {
}
}
userRating() {
return this.userReviews.find(r => r.username == this.user?.username && !r.isExternal)
}
protected readonly LibraryType = LibraryType;
protected readonly encodeURIComponent = encodeURIComponent;
}

View file

@ -31,11 +31,10 @@
@if (libraryType !== null && series && volume.chapters.length === 1) {
<div class="mt-2 mb-2">
@let rating = userRating();
<app-external-rating [seriesId]="series.id"
[ratings]="[]"
[userRating]="rating?.rating || 0"
[hasUserRated]="rating !== undefined && rating.hasBeenRated"
[userRating]="rating"
[hasUserRated]="hasBeenRated"
[libraryType]="libraryType"
[chapterId]="volume.chapters[0].id"
/>

View file

@ -83,6 +83,7 @@ import {ReviewsComponent} from "../_single-module/reviews/reviews.component";
import {ExternalRatingComponent} from "../series-detail/_components/external-rating/external-rating.component";
import {User} from "../_models/user";
import {ReviewService} from "../_services/review.service";
import {ChapterService} from "../_services/chapter.service";
enum TabID {
@ -183,7 +184,7 @@ export class VolumeDetailComponent implements OnInit {
private readonly readingListService = inject(ReadingListService);
private readonly messageHub = inject(MessageHubService);
private readonly location = inject(Location);
private readonly reviewService = inject(ReviewService);
private readonly chapterService = inject(ChapterService);
protected readonly AgeRating = AgeRating;
@ -204,8 +205,13 @@ export class VolumeDetailComponent implements OnInit {
libraryType: LibraryType | null = null;
activeTabId = TabID.Chapters;
readingLists: ReadingList[] = [];
// Only populated if the volume has exactly one chapter
userReviews: Array<UserReview> = [];
plusReviews: Array<UserReview> = [];
rating: number = 0;
hasBeenRated: boolean = false;
mobileSeriesImgBackground: string | undefined;
downloadInProgress: boolean = false;
@ -405,9 +411,11 @@ export class VolumeDetailComponent implements OnInit {
this.libraryType = results.libraryType;
if (this.volume.chapters.length === 1) {
this.reviewService.getReviews(this.seriesId, this.volume.chapters[0].id).subscribe(reviews => {
this.userReviews = reviews.filter(r => !r.isExternal);
this.plusReviews = reviews.filter(r => r.isExternal);
this.chapterService.chapterDetailPlus(this.seriesId, this.volume.chapters[0].id).subscribe(detail => {
this.userReviews = detail.reviews.filter(r => !r.isExternal);
this.plusReviews = detail.reviews.filter(r => r.isExternal);
this.rating = detail.rating;
this.hasBeenRated = detail.hasBeenRated;
});
}
@ -692,9 +700,5 @@ export class VolumeDetailComponent implements OnInit {
}
}
userRating() {
return this.userReviews.find(r => r.username === this.user?.username && !r.isExternal);
}
protected readonly encodeURIComponent = encodeURIComponent;
}