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 { 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 { 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 /^(?.+?):?\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+)$/; } } }