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
-131
View File
@@ -1,131 +0,0 @@
/**
* Component: Pagination Component
* Documentation: documentation/frontend/components.md
*/
'use client';
import React from 'react';
interface PaginationProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
className?: string;
}
export function Pagination({ currentPage, totalPages, onPageChange, className = '' }: PaginationProps) {
if (totalPages <= 1) {
return null;
}
const generatePageNumbers = () => {
const pages: (number | string)[] = [];
const maxVisible = 7; // Show max 7 page buttons
if (totalPages <= maxVisible) {
// Show all pages if total is less than max
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
// Always show first page
pages.push(1);
if (currentPage > 3) {
pages.push('...');
}
// Show pages around current page
const start = Math.max(2, currentPage - 1);
const end = Math.min(totalPages - 1, currentPage + 1);
for (let i = start; i <= end; i++) {
pages.push(i);
}
if (currentPage < totalPages - 2) {
pages.push('...');
}
// Always show last page
pages.push(totalPages);
}
return pages;
};
const pageNumbers = generatePageNumbers();
return (
<div className={`flex items-center justify-center gap-2 ${className}`}>
{/* Previous Button */}
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
className="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600
bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300
hover:bg-gray-50 dark:hover:bg-gray-700
disabled:opacity-50 disabled:cursor-not-allowed
transition-colors"
aria-label="Previous page"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
{/* Page Numbers */}
<div className="flex items-center gap-1">
{pageNumbers.map((page, index) => {
if (page === '...') {
return (
<span
key={`ellipsis-${index}`}
className="px-3 py-2 text-gray-500 dark:text-gray-400"
>
...
</span>
);
}
const pageNum = page as number;
const isActive = pageNum === currentPage;
return (
<button
key={pageNum}
onClick={() => onPageChange(pageNum)}
className={`px-4 py-2 rounded-lg font-medium transition-colors
${
isActive
? 'bg-blue-600 text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
aria-label={`Page ${pageNum}`}
aria-current={isActive ? 'page' : undefined}
>
{pageNum}
</button>
);
})}
</div>
{/* Next Button */}
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600
bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300
hover:bg-gray-50 dark:hover:bg-gray-700
disabled:opacity-50 disabled:cursor-not-allowed
transition-colors"
aria-label="Next page"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
);
}
-11
View File
@@ -192,14 +192,3 @@ function ToastItem({ toast, onRemove }: { toast: Toast; onRemove: (id: string) =
);
}
/**
* Confirmation Dialog Hook
*/
export function useConfirm() {
return useCallback((message: string): Promise<boolean> => {
return new Promise((resolve) => {
const result = window.confirm(message);
resolve(result);
});
}, []);
}