Implement file hash-based library matching and remove fuzzy ASIN matching

Adds file hash-based matching for Audiobookshelf library items to ensure 100% accurate ASIN assignment for RMAB-organized content. Removes fuzzy matching from library availability checks, making all matching ASIN-only to eliminate false positives and race conditions. Updates database schema, processors, and matcher utilities; adds new tests and documentation for the new matching strategy. Removes obsolete scripts, Dockerfile, and related tests; updates docker-compose for test environments.
This commit is contained in:
kikootwo
2026-01-28 10:32:14 -05:00
parent 497849f427
commit a97979358f
111 changed files with 6571 additions and 1426 deletions
@@ -124,6 +124,7 @@ describe('IndexersTab - Auto-load Indexers on Tab Activation', () => {
{
id: 1,
name: 'AudioBook Bay',
protocol: 'torrent',
priority: 10,
seedingTimeMinutes: 4320,
rssEnabled: true,
@@ -132,8 +133,9 @@ describe('IndexersTab - Auto-load Indexers on Tab Activation', () => {
{
id: 2,
name: 'MyAnonaMouse',
protocol: 'usenet',
priority: 15,
seedingTimeMinutes: 10080,
removeAfterProcessing: true,
rssEnabled: false,
categories: [3030],
},
@@ -10,12 +10,14 @@ import { act, fireEvent, render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const swipeHandlers: {
onSwipeStart?: () => void;
onSwiping?: (eventData: { deltaX: number; deltaY: number }) => void;
onSwiped?: (eventData: { deltaX: number; deltaY: number }) => void;
} = {};
vi.mock('react-swipeable', () => ({
useSwipeable: (handlers: any) => {
swipeHandlers.onSwipeStart = handlers.onSwipeStart;
swipeHandlers.onSwiping = handlers.onSwiping;
swipeHandlers.onSwiped = handlers.onSwiped;
return {};
@@ -33,6 +35,7 @@ const recommendation = {
describe('RecommendationCard', () => {
beforeEach(() => {
swipeHandlers.onSwipeStart = undefined;
swipeHandlers.onSwiping = undefined;
swipeHandlers.onSwiped = undefined;
});
@@ -87,6 +90,7 @@ describe('RecommendationCard', () => {
render(<RecommendationCard recommendation={recommendation} onSwipe={onSwipe} />);
act(() => {
swipeHandlers.onSwipeStart?.();
swipeHandlers.onSwiping?.({ deltaX: -80, deltaY: 0 });
});
+131
View File
@@ -223,4 +223,135 @@ describe('Header', () => {
expect(screen.queryByRole('link', { name: 'BookDate' })).not.toBeInTheDocument();
});
it('opens change password modal and closes it', async () => {
const fetchMock = vi.fn().mockResolvedValue({
json: vi.fn().mockResolvedValue({ version: 'v.test' }),
});
vi.stubGlobal('fetch', fetchMock);
renderWithProviders(<Header />, {
auth: {
user: {
id: 'user-4',
plexId: 'plex-4',
username: 'local-admin',
role: 'admin',
authProvider: 'local',
},
isLoading: false,
},
});
const userButton = screen.getByText('local-admin').closest('button');
expect(userButton).not.toBeNull();
await userEvent.click(userButton as HTMLButtonElement);
const changePasswordButton = await screen.findByText('Change Password');
await userEvent.click(changePasswordButton);
expect(await screen.findByRole('heading', { name: 'Change Password' })).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
await waitFor(() => {
expect(screen.queryByRole('heading', { name: 'Change Password' })).not.toBeInTheDocument();
});
});
it('closes the user menu when profile is clicked', async () => {
const fetchMock = vi.fn().mockResolvedValue({
json: vi.fn().mockResolvedValue({ version: 'v.test' }),
});
vi.stubGlobal('fetch', fetchMock);
renderWithProviders(<Header />, {
auth: {
user: {
id: 'user-5',
plexId: 'plex-5',
username: 'reader',
role: 'user',
authProvider: 'local',
},
isLoading: false,
},
});
const userButton = screen.getByText('reader').closest('button');
expect(userButton).not.toBeNull();
await userEvent.click(userButton as HTMLButtonElement);
const profileLink = await screen.findByRole('link', { name: 'Profile' });
await userEvent.click(profileLink);
await waitFor(() => {
expect(screen.queryByText('Logout')).not.toBeInTheDocument();
});
});
it('logs errors when Plex login fails', async () => {
const fetchMock = vi.fn().mockRejectedValue(new Error('login failed'));
const errorMock = vi.spyOn(console, 'error').mockImplementation(() => undefined);
const openMock = vi.spyOn(window, 'open').mockImplementation(() => null);
vi.stubGlobal('fetch', fetchMock);
renderWithProviders(<Header />, { auth: { user: null, isLoading: false } });
await userEvent.click(screen.getByRole('button', { name: /login with plex/i }));
await waitFor(() => {
expect(errorMock).toHaveBeenCalledWith('Login failed:', expect.any(Error));
});
expect(openMock).not.toHaveBeenCalled();
});
it('closes the mobile menu when BookDate is selected', async () => {
localStorage.setItem('accessToken', 'token');
const fetchMock = vi.fn().mockImplementation((input: RequestInfo) => {
if (input === '/api/version') {
return Promise.resolve({
json: vi.fn().mockResolvedValue({ version: 'v.test' }),
});
}
if (input === '/api/bookdate/config') {
return Promise.resolve({
json: vi.fn().mockResolvedValue({
config: { isVerified: true, isEnabled: true },
}),
});
}
return Promise.resolve({
json: vi.fn().mockResolvedValue({}),
});
});
vi.stubGlobal('fetch', fetchMock);
renderWithProviders(<Header />, {
auth: {
user: {
id: 'user-6',
plexId: 'plex-6',
username: 'reader',
role: 'user',
},
isLoading: false,
},
});
const initialBookDateCount = (await screen.findAllByRole('link', { name: 'BookDate' })).length;
await userEvent.click(screen.getByRole('button', { name: 'Toggle menu' }));
const bookDateLinks = await screen.findAllByRole('link', { name: 'BookDate' });
expect(bookDateLinks).toHaveLength(initialBookDateCount + 1);
await userEvent.click(bookDateLinks[bookDateLinks.length - 1]);
await waitFor(async () => {
const remainingLinks = await screen.findAllByRole('link', { name: 'BookDate' });
expect(remainingLinks).toHaveLength(initialBookDateCount);
});
});
});
-38
View File
@@ -1,38 +0,0 @@
/**
* Component: Pagination Tests
* Documentation: documentation/frontend/components.md
*/
// @vitest-environment jsdom
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { Pagination } from '@/components/ui/Pagination';
describe('Pagination', () => {
it('renders nothing when there is only one page', () => {
const { container } = render(<Pagination currentPage={1} totalPages={1} onPageChange={vi.fn()} />);
expect(container.firstChild).toBeNull();
});
it('renders ellipses for large page ranges', () => {
render(<Pagination currentPage={5} totalPages={10} onPageChange={vi.fn()} />);
const ellipses = screen.getAllByText('...');
expect(ellipses.length).toBeGreaterThan(0);
});
it('calls onPageChange for navigation controls', () => {
const onPageChange = vi.fn();
render(<Pagination currentPage={2} totalPages={5} onPageChange={onPageChange} />);
fireEvent.click(screen.getByLabelText('Previous page'));
expect(onPageChange).toHaveBeenCalledWith(1);
fireEvent.click(screen.getByLabelText('Next page'));
expect(onPageChange).toHaveBeenCalledWith(3);
fireEvent.click(screen.getByLabelText('Page 4'));
expect(onPageChange).toHaveBeenCalledWith(4);
});
});