120 lines
4.7 KiB
TypeScript
120 lines
4.7 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { BookSearchResultDto } from '../dto/book-search-result.dto';
|
|
import { HttpService } from '@nestjs/axios';
|
|
import { firstValueFrom, map, timeout } from 'rxjs';
|
|
import { AxiosResponse } from 'axios';
|
|
import { GoogleSearchContext } from '../contexts/google.search.context';
|
|
|
|
@Injectable()
|
|
export class GoogleService {
|
|
constructor(
|
|
private readonly http: HttpService,
|
|
) { }
|
|
|
|
async searchRaw(searchQuery: string): Promise<BookSearchResultDto[]> {
|
|
const queryParams = 'langRestrict=en&printType=books&maxResults=10&fields=items(kind,id,volumeInfo(title,description,authors,publisher,publishedDate,industryIdentifiers,language,categories,maturityRating,imageLinks,canonicalVolumeLink,seriesInfo))&q=';
|
|
|
|
return await firstValueFrom(
|
|
this.http.get('https://www.googleapis.com/books/v1/volumes?' + queryParams + searchQuery)
|
|
.pipe(
|
|
timeout({ first: 5000 }),
|
|
map(value => this.transform(value)),
|
|
)
|
|
);
|
|
}
|
|
|
|
async search(context: GoogleSearchContext): Promise<BookSearchResultDto[]> {
|
|
if (!context) {
|
|
return null;
|
|
}
|
|
|
|
const defaultQueryParams = 'langRestrict=en&printType=books&fields=items(kind,id,volumeInfo(title,description,authors,publisher,publishedDate,industryIdentifiers,language,categories,maturityRating,imageLinks,canonicalVolumeLink,seriesInfo))';
|
|
const customQueryParams = context.generateQueryParams();
|
|
|
|
return await firstValueFrom(
|
|
this.http.get('https://www.googleapis.com/books/v1/volumes?' + defaultQueryParams + '&' + customQueryParams)
|
|
.pipe(
|
|
timeout({ first: 5000 }),
|
|
map(value => this.transform(value)),
|
|
)
|
|
);
|
|
}
|
|
|
|
private transform(response: AxiosResponse): BookSearchResultDto[] {
|
|
if (!response.data?.items) {
|
|
return [];
|
|
}
|
|
|
|
return response.data.items
|
|
//.filter(item => item.volumeInfo?.canonicalVolumeLink?.startsWith('https://play.google.com/store/books/details'))
|
|
.map(item => this.extract(item));
|
|
}
|
|
|
|
private extract(item: any): BookSearchResultDto {
|
|
const result: BookSearchResultDto = {
|
|
providerBookId: item.id,
|
|
providerSeriesId: item.volumeInfo.seriesInfo?.volumeSeries[0].seriesId,
|
|
title: item.volumeInfo.title,
|
|
desc: item.volumeInfo.description,
|
|
volume: item.volumeInfo.seriesInfo?.bookDisplayNumber ? parseInt(item.volumeInfo.seriesInfo?.bookDisplayNumber, 10) : undefined,
|
|
publisher: item.volumeInfo.publisher,
|
|
authors: item.volumeInfo.authors,
|
|
categories: item.volumeInfo.categories ?? [],
|
|
mediaType: null,
|
|
maturityRating: item.volumeInfo.maturityRating,
|
|
industryIdentifiers: item.volumeInfo.industryIdentifiers ? Object.assign({}, ...item.volumeInfo.industryIdentifiers.map(i => i.type == 'OTHER' ? { [i.identifier.split(':')[0]]: i.identifier.split(':')[1] } : { [i.type]: i.identifier })) : [],
|
|
publishedAt: new Date(item.volumeInfo.publishedDate),
|
|
language: item.volumeInfo.language,
|
|
thumbnail: item.volumeInfo.imageLinks?.thumbnail,
|
|
url: item.volumeInfo.canonicalVolumeLink,
|
|
provider: 'google'
|
|
}
|
|
|
|
const regex = this.getRegexByPublisher(result.publisher);
|
|
const match = result.title.match(regex);
|
|
if (match?.groups) {
|
|
result.title = match.groups['title'].trim();
|
|
if (!result.volume || isNaN(result.volume)) {
|
|
result.volume = parseInt(match.groups['volume'], 10);
|
|
}
|
|
}
|
|
|
|
if (match?.groups && 'media_type' in match.groups) {
|
|
result.mediaType = match.groups['media_type'];
|
|
} else if (result.categories.includes('Comics & Graphic Novels')) {
|
|
result.mediaType = 'Comics & Graphic Novels';
|
|
} else if (result.categories.includes('Fiction') || result.categories.includes('Young Adult Fiction')) {
|
|
result.mediaType = 'Novel';
|
|
} else {
|
|
result.mediaType = 'Book';
|
|
}
|
|
|
|
if (result.mediaType) {
|
|
if (result.mediaType.toLowerCase() == "light novel") {
|
|
result.mediaType = 'Light Novel';
|
|
} else if (result.mediaType.toLowerCase() == 'manga') {
|
|
result.mediaType = 'Manga';
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private getRegexByPublisher(publisher: string): RegExp {
|
|
switch (publisher) {
|
|
case 'J-Novel Club':
|
|
return /^(?<title>.+?):?\sVolume\s(?<volume>\d+)$/i;
|
|
case 'Yen On':
|
|
case 'Yen Press':
|
|
case 'Yen Press LLC':
|
|
return /^(?<title>.+?)(?:,?\sVol\.\s(?<volume>\d+))?\s\((?<media_type>[\w\s]+)\)$/;
|
|
case 'Hanashi Media':
|
|
return /^(?<title>.+?)\s\((?<media_type>[\w\s]+)\),?\sVol\.\s(?<volume>\d+)$/
|
|
case 'Regin\'s Chronicles':
|
|
return /^(?<title>.+?)\s\((?<media_type>[\w\s]+)\)(?<subtitle>\:\s.+?)?$/
|
|
default:
|
|
return /^(?<title>.+?)(?:,|:|\s\-)?\s(?:Vol(?:\.|ume)?)?\s(?<volume>\d+)$/;
|
|
}
|
|
}
|
|
}
|