Changed book status to smallint. Added media_type to series. Added 'Hanashi Media' regex resolver for searching. Removed 'Fiction' limitation when searching. Added update series to add new volumes. Fixed search when not all volumes would show up.
This commit is contained in:
@ -25,6 +25,7 @@ CREATE TABLE
|
|||||||
-- 3rd party id for this series.
|
-- 3rd party id for this series.
|
||||||
provider_series_id text,
|
provider_series_id text,
|
||||||
series_title text NOT NULL,
|
series_title text NOT NULL,
|
||||||
|
media_type text,
|
||||||
-- 3rd party used to fetch the data for this series.
|
-- 3rd party used to fetch the data for this series.
|
||||||
provider varchar(12) NOT NULL,
|
provider varchar(12) NOT NULL,
|
||||||
added_at timestamp default NULL,
|
added_at timestamp default NULL,
|
||||||
@ -113,7 +114,7 @@ CREATE TABLE
|
|||||||
book_statuses (
|
book_statuses (
|
||||||
user_id uuid,
|
user_id uuid,
|
||||||
book_id uuid,
|
book_id uuid,
|
||||||
state varchar(12),
|
state smallint,
|
||||||
added_at timestamp default NULL,
|
added_at timestamp default NULL,
|
||||||
modified_at timestamp default NULL,
|
modified_at timestamp default NULL,
|
||||||
PRIMARY KEY (user_id, book_id),
|
PRIMARY KEY (user_id, book_id),
|
||||||
|
@ -9,6 +9,7 @@ import { CreateBookDto } from './dto/create-book.dto';
|
|||||||
import { CreateBookOriginDto } from './dto/create-book-origin.dto';
|
import { CreateBookOriginDto } from './dto/create-book-origin.dto';
|
||||||
import { CreateBookStatusDto } from './dto/create-book-status.dto';
|
import { CreateBookStatusDto } from './dto/create-book-status.dto';
|
||||||
import { DeleteBookStatusDto } from './dto/delete-book-status.dto';
|
import { DeleteBookStatusDto } from './dto/delete-book-status.dto';
|
||||||
|
import { SeriesDto } from 'src/series/dto/series.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BooksService {
|
export class BooksService {
|
||||||
@ -62,12 +63,21 @@ export class BooksService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findBooksFromSeries(series: SeriesDto) {
|
||||||
|
return await this.bookRepository.find({
|
||||||
|
where: {
|
||||||
|
providerSeriesId: series.providerSeriesId,
|
||||||
|
provider: series.provider,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async findBookStatusesTrackedBy(userId: UUID): Promise<BookStatusEntity[]> {
|
async findBookStatusesTrackedBy(userId: UUID): Promise<BookStatusEntity[]> {
|
||||||
return await this.bookStatusRepository.createQueryBuilder('s')
|
return await this.bookStatusRepository.createQueryBuilder('s')
|
||||||
.select(['s.book_id', 's.user_id'])
|
.select(['s.book_id', 's.user_id'])
|
||||||
.where('s.user_id = :id', { id: userId })
|
.where('s.user_id = :id', { id: userId })
|
||||||
.innerJoin('s.book', 'b')
|
.innerJoin('s.book', 'b')
|
||||||
.addSelect(['b.book_title', 'b.book_desc', 'b.book_volume', 'b.provider'])
|
.addSelect(['b.book_title', 'b.book_desc', 'b.book_volume', 'b.provider', 'b.providerSeriesId'])
|
||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +111,7 @@ export class BooksService {
|
|||||||
await this.bookStatusRepository.createQueryBuilder()
|
await this.bookStatusRepository.createQueryBuilder()
|
||||||
.insert()
|
.insert()
|
||||||
.values(status)
|
.values(status)
|
||||||
.orUpdate(['user_id', 'book_id'], ['state', 'modified_at'], { skipUpdateIfNoValuesChanged: true })
|
.orUpdate(['state', 'modified_at'], ['user_id', 'book_id'], { skipUpdateIfNoValuesChanged: true })
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ export class CreateBookStatusDto {
|
|||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
state: string;
|
state: number;
|
||||||
|
|
||||||
modifiedAt: Date;
|
modifiedAt: Date;
|
||||||
}
|
}
|
@ -11,8 +11,8 @@ export class BookStatusEntity {
|
|||||||
@PrimaryColumn({ name: 'user_id', type: 'uuid' })
|
@PrimaryColumn({ name: 'user_id', type: 'uuid' })
|
||||||
readonly userId: UUID;
|
readonly userId: UUID;
|
||||||
|
|
||||||
@Column({ name: 'state', type: 'varchar' })
|
@Column({ name: 'state', type: 'smallint' })
|
||||||
state: string;
|
state: number;
|
||||||
|
|
||||||
@Column({ name: 'added_at', type: 'timestamptz', nullable: false })
|
@Column({ name: 'added_at', type: 'timestamptz', nullable: false })
|
||||||
addedAt: Date
|
addedAt: Date
|
||||||
|
@ -26,49 +26,9 @@ export class LibraryConsumer extends WorkerHost {
|
|||||||
msg: 'Started task on queue.',
|
msg: 'Started task on queue.',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (job.name == 'new_series') {
|
||||||
const series: CreateSeriesSubscriptionJobDto = job.data;
|
const series: CreateSeriesSubscriptionJobDto = job.data;
|
||||||
|
const books = await this.search(job, series, false);
|
||||||
let context = this.provider.generateSearchContext(series.provider, series.title) as GoogleSearchContext;
|
|
||||||
//context.intitle = series.title;
|
|
||||||
context.maxResults = '40';
|
|
||||||
context.subject = 'Fiction';
|
|
||||||
|
|
||||||
// Search for the book(s) via the provider.
|
|
||||||
// Up until end of results or after 3 unhelpful pages of results.
|
|
||||||
let results = [];
|
|
||||||
let related = [];
|
|
||||||
let pageSearchedCount = 0;
|
|
||||||
let unhelpfulResultsCount = 0;
|
|
||||||
do {
|
|
||||||
pageSearchedCount += 1;
|
|
||||||
results = await this.provider.search(context);
|
|
||||||
const potential = results.filter(r => r.providerSeriesId == series.providerSeriesId || r.title == series.title);
|
|
||||||
if (potential.length > 0) {
|
|
||||||
related.push.apply(related, potential);
|
|
||||||
} else {
|
|
||||||
unhelpfulResultsCount += 1;
|
|
||||||
}
|
|
||||||
context = context.next();
|
|
||||||
job.updateProgress(pageSearchedCount * 5);
|
|
||||||
} while (results.length >= 40 && unhelpfulResultsCount < 3);
|
|
||||||
|
|
||||||
// Sort & de-duplicate the entries received.
|
|
||||||
const books = related.map(book => this.toScore(book, series))
|
|
||||||
.sort((a, b) => a.result.volume - b.result.volume || b.score - a.score)
|
|
||||||
.filter((_, index, arr) => index == 0 || arr[index - 1].result.volume != arr[index].result.volume);
|
|
||||||
job.updateProgress(25);
|
|
||||||
|
|
||||||
this.logger.debug({
|
|
||||||
class: LibraryConsumer.name,
|
|
||||||
method: this.process.name,
|
|
||||||
job: job,
|
|
||||||
msg: 'Finished searching for book entries.',
|
|
||||||
results: {
|
|
||||||
pages: pageSearchedCount,
|
|
||||||
related_entries: related.length,
|
|
||||||
volumes: books.length,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
for (let book of books) {
|
for (let book of books) {
|
||||||
@ -82,7 +42,7 @@ export class LibraryConsumer extends WorkerHost {
|
|||||||
method: this.process.name,
|
method: this.process.name,
|
||||||
book: book.result,
|
book: book.result,
|
||||||
score: book.score,
|
score: book.score,
|
||||||
msg: 'Failed to add book in background.',
|
msg: 'Failed to add book in background during adding series.',
|
||||||
error: err,
|
error: err,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@ -90,6 +50,44 @@ export class LibraryConsumer extends WorkerHost {
|
|||||||
job.updateProgress(25 + 75 * counter / books.length);
|
job.updateProgress(25 + 75 * counter / books.length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (job.name == 'update_series') {
|
||||||
|
const series: CreateSeriesSubscriptionJobDto = job.data;
|
||||||
|
const existingBooks = await this.library.getBooksFromSeries(series);
|
||||||
|
const existingVolumes = existingBooks.map(b => b.volume);
|
||||||
|
const books = await this.search(job, series, true);
|
||||||
|
|
||||||
|
let counter = 0;
|
||||||
|
for (let book of books) {
|
||||||
|
if (existingVolumes.includes(book.result.volume)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Force the provider's series id to be set, so that we know which series this belongs.
|
||||||
|
book.result.providerSeriesId = series.providerSeriesId;
|
||||||
|
await this.library.addBook(book.result);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error({
|
||||||
|
class: LibraryConsumer.name,
|
||||||
|
method: this.process.name,
|
||||||
|
book: book.result,
|
||||||
|
score: book.score,
|
||||||
|
msg: 'Failed to add book in background during series update.',
|
||||||
|
error: err,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
counter++;
|
||||||
|
job.updateProgress(25 + 75 * counter / books.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.logger.warn({
|
||||||
|
class: LibraryConsumer.name,
|
||||||
|
method: this.process.name,
|
||||||
|
job: job,
|
||||||
|
msg: 'Unknown job name found.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.info({
|
this.logger.info({
|
||||||
class: LibraryConsumer.name,
|
class: LibraryConsumer.name,
|
||||||
@ -101,6 +99,54 @@ export class LibraryConsumer extends WorkerHost {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async search(job: Job, series: CreateSeriesSubscriptionJobDto, newest: boolean): Promise<{ result: BookSearchResultDto, score: number }[]> {
|
||||||
|
let context = this.provider.generateSearchContext(series.provider, series.title) as GoogleSearchContext;
|
||||||
|
context.maxResults = '40';
|
||||||
|
if (newest) {
|
||||||
|
context.orderBy = 'newest';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for the book(s) via the provider.
|
||||||
|
// Up until end of results or after 3 unhelpful pages of results.
|
||||||
|
let results = [];
|
||||||
|
let related = [];
|
||||||
|
let pageSearchedCount = 0;
|
||||||
|
let unhelpfulResultsCount = 0;
|
||||||
|
do {
|
||||||
|
pageSearchedCount += 1;
|
||||||
|
results = await this.provider.search(context);
|
||||||
|
const potential = results.filter((r: BookSearchResultDto) => r.providerSeriesId == series.providerSeriesId || r.title == series.title && r.mediaType == series.mediaType);
|
||||||
|
if (potential.length > 0) {
|
||||||
|
related.push.apply(related, potential);
|
||||||
|
} else {
|
||||||
|
unhelpfulResultsCount += 1;
|
||||||
|
}
|
||||||
|
context = context.next();
|
||||||
|
job.updateProgress(pageSearchedCount * 5);
|
||||||
|
} while (results.length >= context.maxResults && (!newest || unhelpfulResultsCount < 3));
|
||||||
|
|
||||||
|
// Sort & de-duplicate the entries received.
|
||||||
|
const books = related.map(book => this.toScore(book, series))
|
||||||
|
.sort((a, b) => a.result.volume - b.result.volume || b.score - a.score)
|
||||||
|
.filter((_, index, arr) => index == 0 || arr[index - 1].result.volume != arr[index].result.volume);
|
||||||
|
job.updateProgress(25);
|
||||||
|
|
||||||
|
|
||||||
|
this.logger.debug({
|
||||||
|
class: LibraryConsumer.name,
|
||||||
|
method: this.search.name,
|
||||||
|
job: job,
|
||||||
|
msg: 'Finished searching for book entries.',
|
||||||
|
results: {
|
||||||
|
pages: pageSearchedCount,
|
||||||
|
related_entries: related.length,
|
||||||
|
volumes: books.length,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return books;
|
||||||
|
}
|
||||||
|
|
||||||
@OnQueueEvent('failed')
|
@OnQueueEvent('failed')
|
||||||
onFailed(job: Job, err: Error) {
|
onFailed(job: Job, err: Error) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { InjectQueue } from '@nestjs/bullmq';
|
import { InjectQueue } from '@nestjs/bullmq';
|
||||||
import { Body, Controller, Delete, Get, Post, Put, Request, Res, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Patch, Post, Put, Request, Res, UseGuards } from '@nestjs/common';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { Queue } from 'bullmq';
|
import { Queue } from 'bullmq';
|
||||||
import { PinoLogger } from 'nestjs-pino';
|
import { PinoLogger } from 'nestjs-pino';
|
||||||
@ -49,6 +49,44 @@ export class LibraryController {
|
|||||||
provider: body.provider,
|
provider: body.provider,
|
||||||
providerSeriesId: body.providerSeriesId,
|
providerSeriesId: body.providerSeriesId,
|
||||||
title: body.title,
|
title: body.title,
|
||||||
|
mediaType: body.mediaType,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof QueryFailedError) {
|
||||||
|
if (err.driverError.code == '23505') {
|
||||||
|
// Subscription already exist.
|
||||||
|
response.statusCode = 409;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error_message: 'Series subscription already exists.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.statusCode = 500;
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error_message: 'Something went wrong.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('series')
|
||||||
|
async updateSeries(
|
||||||
|
@Request() req,
|
||||||
|
@Body() body: CreateSeriesSubscriptionDto,
|
||||||
|
@Res({ passthrough: true }) response: Response,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await this.library.updateSeries({
|
||||||
|
provider: body.provider,
|
||||||
|
providerSeriesId: body.providerSeriesId,
|
||||||
|
title: body.title,
|
||||||
|
mediaType: body.mediaType,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -149,9 +187,18 @@ export class LibraryController {
|
|||||||
provider: body.provider,
|
provider: body.provider,
|
||||||
providerSeriesId: body.providerSeriesId,
|
providerSeriesId: body.providerSeriesId,
|
||||||
title: body.title,
|
title: body.title,
|
||||||
|
mediaType: body.mediaType,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof QueryFailedError) {
|
if (err instanceof QueryFailedError) {
|
||||||
|
this.logger.error({
|
||||||
|
class: LibraryController.name,
|
||||||
|
method: this.createBook.name,
|
||||||
|
user: req.user,
|
||||||
|
msg: 'Failed to create a series for a book.',
|
||||||
|
error: err,
|
||||||
|
});
|
||||||
|
|
||||||
// Ignore if the series already exist.
|
// Ignore if the series already exist.
|
||||||
if (err.driverError.code != '23505') {
|
if (err.driverError.code != '23505') {
|
||||||
response.statusCode = 500;
|
response.statusCode = 500;
|
||||||
@ -171,6 +218,14 @@ export class LibraryController {
|
|||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof QueryFailedError) {
|
if (err instanceof QueryFailedError) {
|
||||||
|
this.logger.error({
|
||||||
|
class: LibraryController.name,
|
||||||
|
method: this.createBook.name,
|
||||||
|
user: req.user,
|
||||||
|
msg: 'Failed to create book.',
|
||||||
|
error: err,
|
||||||
|
});
|
||||||
|
|
||||||
if (err.driverError.code == '23505') {
|
if (err.driverError.code == '23505') {
|
||||||
// Book exists already.
|
// Book exists already.
|
||||||
response.statusCode = 409;
|
response.statusCode = 409;
|
||||||
@ -179,7 +234,7 @@ export class LibraryController {
|
|||||||
error_message: 'The book has already been added previously.',
|
error_message: 'The book has already been added previously.',
|
||||||
};
|
};
|
||||||
} else if (err.driverError.code == '23503') {
|
} else if (err.driverError.code == '23503') {
|
||||||
// Data dependency is missing.
|
// Series is missing.
|
||||||
response.statusCode = 500;
|
response.statusCode = 500;
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -188,14 +243,6 @@ export class LibraryController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.error({
|
|
||||||
class: LibraryController.name,
|
|
||||||
method: this.createBook.name,
|
|
||||||
user: req.user,
|
|
||||||
msg: 'Failed to create book.',
|
|
||||||
error: err,
|
|
||||||
});
|
|
||||||
|
|
||||||
response.statusCode = 500;
|
response.statusCode = 500;
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
@ -3,10 +3,10 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { Queue } from 'bullmq';
|
import { Queue } from 'bullmq';
|
||||||
import { PinoLogger } from 'nestjs-pino';
|
import { PinoLogger } from 'nestjs-pino';
|
||||||
import { BooksService } from 'src/books/books.service';
|
import { BooksService } from 'src/books/books.service';
|
||||||
import { CreateBookDto } from 'src/books/dto/create-book.dto';
|
|
||||||
import { BookSearchResultDto } from 'src/providers/dto/book-search-result.dto';
|
import { BookSearchResultDto } from 'src/providers/dto/book-search-result.dto';
|
||||||
import { CreateSeriesDto } from 'src/series/dto/create-series.dto';
|
import { CreateSeriesDto } from 'src/series/dto/create-series.dto';
|
||||||
import { SeriesSubscriptionDto } from 'src/series/dto/series-subscription.dto';
|
import { SeriesSubscriptionDto } from 'src/series/dto/series-subscription.dto';
|
||||||
|
import { SeriesDto } from 'src/series/dto/series.dto';
|
||||||
import { SeriesService } from 'src/series/series.service';
|
import { SeriesService } from 'src/series/series.service';
|
||||||
import { BookOriginType } from 'src/shared/enums/book_origin_type';
|
import { BookOriginType } from 'src/shared/enums/book_origin_type';
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ export class LibraryService {
|
|||||||
this.logger.debug({
|
this.logger.debug({
|
||||||
class: LibraryService.name,
|
class: LibraryService.name,
|
||||||
method: this.addSubscription.name,
|
method: this.addSubscription.name,
|
||||||
series: series.providerSeriesId,
|
series: series,
|
||||||
msg: 'Series saved to database.',
|
msg: 'Series saved to database.',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -135,4 +135,12 @@ export class LibraryService {
|
|||||||
|
|
||||||
return bookId;
|
return bookId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateSeries(series: CreateSeriesDto) {
|
||||||
|
return await this.jobs.add('update_series', series);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBooksFromSeries(series: SeriesDto) {
|
||||||
|
return await this.books.findBooksFromSeries(series);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ export class GoogleSearchContext extends SearchContext {
|
|||||||
|
|
||||||
|
|
||||||
generateQueryParams() {
|
generateQueryParams() {
|
||||||
const filterParams = ['maxResults', 'startIndex'];
|
const filterParams = ['maxResults', 'startIndex', 'orderBy'];
|
||||||
const searchParams = ['intitle', 'inauthor', 'inpublisher', 'subject', 'isbn'];
|
const searchParams = ['intitle', 'inauthor', 'inpublisher', 'subject', 'isbn'];
|
||||||
|
|
||||||
const queryParams = filterParams
|
const queryParams = filterParams
|
||||||
@ -21,7 +21,19 @@ export class GoogleSearchContext extends SearchContext {
|
|||||||
...searchParams.map(p => this.params[p] ? p + ':"' + this.params[p] + '"' : ''),
|
...searchParams.map(p => this.params[p] ? p + ':"' + this.params[p] + '"' : ''),
|
||||||
].filter(p => p.length > 0).join('');
|
].filter(p => p.length > 0).join('');
|
||||||
|
|
||||||
return [queryParams, 'q=' + searchQueryParam].filter(q => q.length > 0).join('&');
|
return [queryParams, 'q=' + searchQueryParam].filter(q => q.length > 2).join('&');
|
||||||
|
}
|
||||||
|
|
||||||
|
get orderBy(): 'newest' | 'relevant' {
|
||||||
|
return this.params['orderBy'] as 'newest' | 'relevant' ?? 'relevant';
|
||||||
|
}
|
||||||
|
|
||||||
|
set orderBy(value: 'newest' | 'relevant' | null) {
|
||||||
|
if (!value) {
|
||||||
|
delete this.params['orderBy'];
|
||||||
|
} else {
|
||||||
|
this.params['orderBy'] = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get maxResults(): number {
|
get maxResults(): number {
|
||||||
|
@ -48,6 +48,10 @@ export class BookSearchResultDto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
language: string;
|
language: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
mediaType: string | null;
|
||||||
|
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
categories: string[];
|
categories: string[];
|
||||||
|
@ -59,9 +59,10 @@ export class GoogleService {
|
|||||||
volume: item.volumeInfo.seriesInfo?.bookDisplayNumber ? parseInt(item.volumeInfo.seriesInfo?.bookDisplayNumber, 10) : undefined,
|
volume: item.volumeInfo.seriesInfo?.bookDisplayNumber ? parseInt(item.volumeInfo.seriesInfo?.bookDisplayNumber, 10) : undefined,
|
||||||
publisher: item.volumeInfo.publisher,
|
publisher: item.volumeInfo.publisher,
|
||||||
authors: item.volumeInfo.authors,
|
authors: item.volumeInfo.authors,
|
||||||
categories: item.volumeInfo.categories,
|
categories: item.volumeInfo.categories ?? [],
|
||||||
|
mediaType: null,
|
||||||
maturityRating: item.volumeInfo.maturityRating,
|
maturityRating: item.volumeInfo.maturityRating,
|
||||||
industryIdentifiers: item.volumeInfo.industryIdentifiers ? Object.assign({}, ...item.volumeInfo.industryIdentifiers.map(i => ({ [i.type]: i.identifier }))) : [],
|
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),
|
publishedAt: new Date(item.volumeInfo.publishedDate),
|
||||||
language: item.volumeInfo.language,
|
language: item.volumeInfo.language,
|
||||||
thumbnail: item.volumeInfo.imageLinks?.thumbnail,
|
thumbnail: item.volumeInfo.imageLinks?.thumbnail,
|
||||||
@ -69,8 +70,7 @@ export class GoogleService {
|
|||||||
provider: 'google'
|
provider: 'google'
|
||||||
}
|
}
|
||||||
|
|
||||||
let regex = this.getRegexByPublisher(result.publisher);
|
const regex = this.getRegexByPublisher(result.publisher);
|
||||||
|
|
||||||
const match = result.title.match(regex);
|
const match = result.title.match(regex);
|
||||||
if (match?.groups) {
|
if (match?.groups) {
|
||||||
result.title = match.groups['title'].trim();
|
result.title = match.groups['title'].trim();
|
||||||
@ -79,19 +79,41 @@ export class GoogleService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getRegexByPublisher(publisher: string): RegExp {
|
private getRegexByPublisher(publisher: string): RegExp {
|
||||||
switch (publisher) {
|
switch (publisher) {
|
||||||
case 'J-Novel Club':
|
case 'J-Novel Club':
|
||||||
return /(?<title>.+?):?\sVolume\s(?<volume>\d+)/i;
|
return /^(?<title>.+?):?\sVolume\s(?<volume>\d+)$/i;
|
||||||
case 'Yen On':
|
case 'Yen On':
|
||||||
case 'Yen Press':
|
case 'Yen Press':
|
||||||
case 'Yen Press LLC':
|
case 'Yen Press LLC':
|
||||||
return /(?<title>.+?),?\sVol\.\s(?<volume>\d+)\s\((?<media_type>[\w\s]+)\)/;
|
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:
|
default:
|
||||||
return /(?<title>.+?)(?:,|:|\s\-)?\s(?:Vol(?:\.|ume)?)?\s(?<volume>\d+)/;
|
return /^(?<title>.+?)(?:,|:|\s\-)?\s(?:Vol(?:\.|ume)?)?\s(?<volume>\d+)$/;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,4 +5,8 @@ export class CreateSeriesSubscriptionJobDto extends SeriesSubscriptionDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
mediaType: string;
|
||||||
}
|
}
|
@ -5,4 +5,8 @@ export class CreateSeriesSubscriptionDto extends SeriesDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
mediaType: string;
|
||||||
}
|
}
|
@ -5,4 +5,8 @@ export class CreateSeriesDto extends SeriesDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
mediaType: string;
|
||||||
}
|
}
|
@ -14,6 +14,9 @@ export class SeriesEntity {
|
|||||||
@Column({ name: 'series_title', type: 'text', nullable: false })
|
@Column({ name: 'series_title', type: 'text', nullable: false })
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
@Column({ name: 'media_type', type: 'text', nullable: true })
|
||||||
|
mediaType: string;
|
||||||
|
|
||||||
@Column({ name: 'provider', type: 'text', nullable: false })
|
@Column({ name: 'provider', type: 'text', nullable: false })
|
||||||
provider: string;
|
provider: string;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user