mirror of
https://github.com/booklore-app/booklore.git
synced 2026-02-18 00:17:53 +01:00
fix(book-rule-evaluator): fix file type handling and add mapping for specific formats to fix magic shelve based on filetype (#2480)
* fix(book-rule-evaluator): fix file type handling and add mapping for specific formats to fix magic shelve based on filetype Signed-off-by: Balázs Szücs <bszucs1209@gmail.com> * fix(book-rule-evaluator): update test to simplify book creation for group rule evaluation Signed-off-by: Balázs Szücs <bszucs1209@gmail.com> --------- Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
import {beforeEach, describe, expect, it} from 'vitest';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {BookRuleEvaluatorService} from './book-rule-evaluator.service';
|
||||
import {Book, ReadStatus} from '../../book/model/book.model';
|
||||
import {GroupRule} from '../component/magic-shelf-component';
|
||||
|
||||
describe('BookRuleEvaluatorService', () => {
|
||||
let service: BookRuleEvaluatorService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(BookRuleEvaluatorService);
|
||||
});
|
||||
|
||||
const createBook = (overrides: Partial<Book> = {}): Book => ({
|
||||
id: 1,
|
||||
bookType: 'EPUB',
|
||||
libraryId: 1,
|
||||
libraryName: 'Test Library',
|
||||
fileName: 'test.epub',
|
||||
filePath: '/path/to/test.epub',
|
||||
readStatus: ReadStatus.UNREAD,
|
||||
shelves: [],
|
||||
metadata: {
|
||||
bookId: 1,
|
||||
title: 'Test Book',
|
||||
authors: ['Test Author'],
|
||||
categories: ['Fiction'],
|
||||
language: 'en'
|
||||
},
|
||||
...overrides
|
||||
});
|
||||
|
||||
describe('fileType filtering', () => {
|
||||
it('should filter EPUB books correctly when rule uses "epub"', () => {
|
||||
const book = createBook({ bookType: 'EPUB' });
|
||||
const group: GroupRule = {
|
||||
name: 'test',
|
||||
type: 'group',
|
||||
join: 'and',
|
||||
rules: [
|
||||
{
|
||||
field: 'fileType',
|
||||
operator: 'equals',
|
||||
value: 'epub'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = service.evaluateGroup(book, group);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter PDF books correctly when rule uses "pdf"', () => {
|
||||
const book = createBook({ bookType: 'PDF' });
|
||||
const group: GroupRule = {
|
||||
name: 'test',
|
||||
type: 'group',
|
||||
join: 'and',
|
||||
rules: [
|
||||
{
|
||||
field: 'fileType',
|
||||
operator: 'equals',
|
||||
value: 'pdf'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = service.evaluateGroup(book, group);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle CBX books correctly for cbr, cbz, and cb7 rules', () => {
|
||||
const book = createBook({ bookType: 'CBX' });
|
||||
|
||||
// Test CBR
|
||||
const cbrGroup: GroupRule = {
|
||||
name: 'test',
|
||||
type: 'group',
|
||||
join: 'and',
|
||||
rules: [
|
||||
{
|
||||
field: 'fileType',
|
||||
operator: 'equals',
|
||||
value: 'cbr'
|
||||
}
|
||||
]
|
||||
};
|
||||
expect(service.evaluateGroup(book, cbrGroup)).toBe(true);
|
||||
|
||||
// Test CBZ
|
||||
const cbzGroup: GroupRule = {
|
||||
name: 'test',
|
||||
type: 'group',
|
||||
join: 'and',
|
||||
rules: [
|
||||
{
|
||||
field: 'fileType',
|
||||
operator: 'equals',
|
||||
value: 'cbz'
|
||||
}
|
||||
]
|
||||
};
|
||||
expect(service.evaluateGroup(book, cbzGroup)).toBe(true);
|
||||
|
||||
// Test CB7
|
||||
const cb7Group: GroupRule = {
|
||||
name: 'test',
|
||||
type: 'group',
|
||||
join: 'and',
|
||||
rules: [
|
||||
{
|
||||
field: 'fileType',
|
||||
operator: 'equals',
|
||||
value: 'cb7'
|
||||
}
|
||||
]
|
||||
};
|
||||
expect(service.evaluateGroup(book, cb7Group)).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle not_equals operator correctly', () => {
|
||||
const epubBook = createBook({ bookType: 'EPUB' });
|
||||
const group: GroupRule = {
|
||||
name: 'test',
|
||||
type: 'group',
|
||||
join: 'and',
|
||||
rules: [
|
||||
{
|
||||
field: 'fileType',
|
||||
operator: 'not_equals',
|
||||
value: 'pdf'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = service.evaluateGroup(epubBook, group);
|
||||
expect(result).toBe(true);
|
||||
|
||||
// Test the opposite case
|
||||
const pdfBook = createBook({ bookType: 'PDF' });
|
||||
const result2 = service.evaluateGroup(pdfBook, group);
|
||||
expect(result2).toBe(false);
|
||||
});
|
||||
|
||||
it('should filter EPUB books correctly when rule uses "not_equals" with "epub"', () => {
|
||||
const epubBook = createBook({ bookType: 'EPUB' });
|
||||
const pdfBook = createBook({ bookType: 'PDF' });
|
||||
|
||||
const group: GroupRule = {
|
||||
name: 'test',
|
||||
type: 'group',
|
||||
join: 'and',
|
||||
rules: [
|
||||
{
|
||||
field: 'fileType',
|
||||
operator: 'not_equals',
|
||||
value: 'epub'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// EPUB book should not match when rule is "not_equals epub"
|
||||
expect(service.evaluateGroup(epubBook, group)).toBe(false);
|
||||
|
||||
// PDF book should match when rule is "not_equals epub"
|
||||
expect(service.evaluateGroup(pdfBook, group)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluateGroup', () => {
|
||||
it('should evaluate group rules with AND logic', () => {
|
||||
const book = createBook({
|
||||
bookType: 'EPUB'
|
||||
});
|
||||
|
||||
const group: GroupRule = {
|
||||
name: 'test',
|
||||
type: 'group',
|
||||
join: 'and',
|
||||
rules: [
|
||||
{ field: 'fileType', operator: 'equals', value: 'epub' },
|
||||
{ field: 'language', operator: 'equals', value: 'en' }
|
||||
]
|
||||
};
|
||||
|
||||
const result = service.evaluateGroup(book, group);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should evaluate group rules with OR logic', () => {
|
||||
const book = createBook({ bookType: 'PDF' });
|
||||
|
||||
const group: GroupRule = {
|
||||
name: 'test',
|
||||
type: 'group',
|
||||
join: 'or',
|
||||
rules: [
|
||||
{ field: 'fileType', operator: 'equals', value: 'epub' },
|
||||
{ field: 'fileType', operator: 'equals', value: 'pdf' }
|
||||
]
|
||||
};
|
||||
|
||||
const result = service.evaluateGroup(book, group);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Book } from '../../book/model/book.model';
|
||||
import { GroupRule, Rule, RuleField } from '../component/magic-shelf-component';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Book} from '../../book/model/book.model';
|
||||
import {GroupRule, Rule, RuleField} from '../component/magic-shelf-component';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class BookRuleEvaluatorService {
|
||||
@@ -35,6 +35,20 @@ export class BookRuleEvaluatorService {
|
||||
const ruleStart = normalize(rule.valueStart);
|
||||
const ruleEnd = normalize(rule.valueEnd);
|
||||
|
||||
const mapFileTypeValue = (uiValue: string): string => {
|
||||
const lowerValue = uiValue.toLowerCase();
|
||||
switch (lowerValue) {
|
||||
case 'cbr':
|
||||
case 'cbz':
|
||||
case 'cb7':
|
||||
return 'cbx';
|
||||
case 'azw':
|
||||
return 'azw3';
|
||||
default:
|
||||
return lowerValue;
|
||||
}
|
||||
};
|
||||
|
||||
const getArrayField = (field: RuleField): string[] => {
|
||||
switch (field) {
|
||||
case 'authors':
|
||||
@@ -48,7 +62,7 @@ export class BookRuleEvaluatorService {
|
||||
case 'readStatus':
|
||||
return [String(book.readStatus ?? 'UNSET').toLowerCase()];
|
||||
case 'fileType':
|
||||
return [String(this.getFileExtension(book.fileName) ?? '').toLowerCase()];
|
||||
return [String(book.bookType ?? '').toLowerCase()];
|
||||
case 'library':
|
||||
return [String(book.libraryId)];
|
||||
case 'shelf':
|
||||
@@ -73,9 +87,21 @@ export class BookRuleEvaluatorService {
|
||||
};
|
||||
|
||||
const isNumericIdField = rule.field === 'library' || rule.field === 'shelf';
|
||||
const isFileTypeField = rule.field === 'fileType';
|
||||
|
||||
const ruleList = Array.isArray(rule.value)
|
||||
? rule.value.map(v => isNumericIdField ? String(v) : String(v).toLowerCase())
|
||||
: (rule.value ? [isNumericIdField ? String(rule.value) : String(rule.value).toLowerCase()] : []);
|
||||
? rule.value.map(v => {
|
||||
if (isNumericIdField) return String(v);
|
||||
const lowerValue = String(v).toLowerCase();
|
||||
return isFileTypeField ? mapFileTypeValue(lowerValue) : lowerValue;
|
||||
})
|
||||
: (rule.value ? [
|
||||
isNumericIdField
|
||||
? String(rule.value)
|
||||
: isFileTypeField
|
||||
? mapFileTypeValue(String(rule.value).toLowerCase())
|
||||
: String(rule.value).toLowerCase()
|
||||
] : []);
|
||||
|
||||
switch (rule.operator) {
|
||||
case 'equals':
|
||||
@@ -85,6 +111,10 @@ export class BookRuleEvaluatorService {
|
||||
if (value instanceof Date && ruleVal instanceof Date) {
|
||||
return value.getTime() === ruleVal.getTime();
|
||||
}
|
||||
if (isFileTypeField && typeof ruleVal === 'string') {
|
||||
const mappedRuleVal = mapFileTypeValue(ruleVal.toLowerCase());
|
||||
return value === mappedRuleVal;
|
||||
}
|
||||
return value === ruleVal;
|
||||
|
||||
case 'not_equals':
|
||||
@@ -94,6 +124,10 @@ export class BookRuleEvaluatorService {
|
||||
if (value instanceof Date && ruleVal instanceof Date) {
|
||||
return value.getTime() !== ruleVal.getTime();
|
||||
}
|
||||
if (isFileTypeField && typeof ruleVal === 'string') {
|
||||
const mappedRuleVal = mapFileTypeValue(ruleVal.toLowerCase());
|
||||
return value !== mappedRuleVal;
|
||||
}
|
||||
return value !== ruleVal;
|
||||
|
||||
case 'contains':
|
||||
@@ -204,7 +238,7 @@ export class BookRuleEvaluatorService {
|
||||
case 'readStatus':
|
||||
return book.readStatus ?? 'UNSET';
|
||||
case 'fileType':
|
||||
return this.getFileExtension(book.fileName)?.toLowerCase() ?? null;
|
||||
return book.bookType?.toLowerCase() ?? null;
|
||||
case 'fileSize':
|
||||
return book.fileSizeKb;
|
||||
case 'metadataScore':
|
||||
@@ -263,11 +297,4 @@ export class BookRuleEvaluatorService {
|
||||
return (book as Record<string, unknown>)[field];
|
||||
}
|
||||
}
|
||||
|
||||
private getFileExtension(filePath?: string): string | null {
|
||||
if (!filePath) return null;
|
||||
const parts = filePath.split('.');
|
||||
if (parts.length < 2) return null;
|
||||
return parts.pop() ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user