Added fine-grained control over search for server use.

This commit is contained in:
Tom
2025-02-20 04:36:48 +00:00
parent a764e1d441
commit cd3ba11924
14 changed files with 677 additions and 149 deletions

View File

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GoogleService } from './google.service';
describe('GoogleService', () => {
let service: GoogleService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [GoogleService],
}).compile();
service = module.get<GoogleService>(GoogleService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@ -0,0 +1,86 @@
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';
@Injectable()
export class GoogleService {
constructor(
private readonly http: HttpService,
) { }
async searchRaw(searchQuery: string): Promise<BookSearchResultDto[]> {
const queryParams = '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(this.transform),
)
);
}
async search(context: GoogleSearchContext): Promise<BookSearchResultDto[]> {
const defaultQueryParams = '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(this.transform),
)
);
}
private transform(response: AxiosResponse): BookSearchResultDto[] {
if (!response.data?.items) {
return [];
}
const items: any[] = response.data.items;
return items.map((item: any) => {
const result: BookSearchResultDto = {
providerBookId: item.id,
providerSeriesId: item.volumeInfo.seriesInfo?.volumeSeries[0].seriesId,
title: item.volumeInfo.title,
desc: item.volumeInfo.description,
volume: parseInt(item.volumeInfo.seriesInfo?.bookDisplayNumber),
publisher: item.volumeInfo.publisher,
authors: item.volumeInfo.authors,
categories: item.volumeInfo.categories,
maturityRating: item.volumeInfo.maturityRating,
industryIdentifiers: Object.assign({}, ...item.volumeInfo.industryIdentifiers.map(i => ({ [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'
}
if (result.providerSeriesId) {
let regex = null;
switch (result.publisher) {
case 'J-Novel Club':
regex = new RegExp(/(?<title>.+?):?\sVolume\s(?<volume>\d+)/);
case 'Yen Press LLC':
regex = new RegExp(/(?<title>.+?),?\sVol\.\s(?<volume>\d+)\s\((?<media_type>\w+)\)/);
default:
regex = new RegExp(/(?<title>.+?)(?:,|:|\s\-)?\s(?:Vol(?:\.|ume)?)?\s(?<volume>\d+)/);
}
const match = result.title.match(regex);
if (match?.groups) {
result.title = match.groups['title'].trim();
if (!result.volume) {
result.volume = parseInt(match.groups['volume']);
}
}
}
return result;
});
}
}