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
@@ -40,7 +40,7 @@ describe('Admin Prowlarr indexers route', () => {
it('returns indexers with saved config', async () => {
prowlarrMock.getIndexers.mockResolvedValueOnce([{ id: 1, name: 'Indexer', protocol: 'torrent' }]);
configServiceMock.get.mockResolvedValueOnce(JSON.stringify([{ id: 1, name: 'Indexer', priority: 5, seedingTimeMinutes: 10 }]));
configServiceMock.get.mockResolvedValueOnce(JSON.stringify([{ id: 1, name: 'Indexer', protocol: 'torrent', priority: 5, seedingTimeMinutes: 10 }]));
configServiceMock.get.mockResolvedValueOnce('[]');
const { GET } = await import('@/app/api/admin/settings/prowlarr/indexers/route');
@@ -53,7 +53,7 @@ describe('Admin Prowlarr indexers route', () => {
it('saves indexer configuration', async () => {
authRequest.json.mockResolvedValue({
indexers: [{ id: 1, name: 'Indexer', enabled: true, priority: 10, seedingTimeMinutes: 0 }],
indexers: [{ id: 1, name: 'Indexer', protocol: 'torrent', enabled: true, priority: 10, seedingTimeMinutes: 0 }],
flagConfigs: [],
});
+1 -1
View File
@@ -64,7 +64,7 @@ describe('Audiobooks search torrents route', () => {
it('returns ranked results with rank order', async () => {
authRequest.json.mockResolvedValue({ title: 'Title', author: 'Author' });
configServiceMock.get
.mockResolvedValueOnce(JSON.stringify([{ id: 1, name: 'Indexer', priority: 10 }]))
.mockResolvedValueOnce(JSON.stringify([{ id: 1, name: 'Indexer', protocol: 'torrent', priority: 10 }]))
.mockResolvedValueOnce(null);
groupIndexersMock.mockReturnValue([{ categories: [1], indexerIds: [1] }]);
+42
View File
@@ -159,6 +159,48 @@ describe('Auth misc routes', () => {
expect(payload.registrationEnabled).toBe(true);
expect(payload.oidcProviderName).toBe('MyOIDC');
});
it('shows local provider when registration is enabled even without existing users', async () => {
configServiceMock.get
.mockResolvedValueOnce('audiobookshelf') // backend mode
.mockResolvedValueOnce(null) // indexer type
.mockResolvedValueOnce(null) // prowlarr url
.mockResolvedValueOnce('false') // oidc enabled
.mockResolvedValueOnce('true') // registration enabled
.mockResolvedValueOnce('SSO'); // oidc provider name
prismaMock.user.count.mockResolvedValueOnce(0); // No local users exist
const { GET } = await import('@/app/api/auth/providers/route');
const response = await GET();
const payload = await response.json();
expect(payload.backendMode).toBe('audiobookshelf');
expect(payload.providers).toContain('local'); // Should include 'local' for registration
expect(payload.registrationEnabled).toBe(true);
expect(payload.hasLocalUsers).toBe(false);
});
it('does not show local provider when registration is disabled and no users exist', async () => {
configServiceMock.get
.mockResolvedValueOnce('audiobookshelf') // backend mode
.mockResolvedValueOnce(null) // indexer type
.mockResolvedValueOnce(null) // prowlarr url
.mockResolvedValueOnce('false') // oidc enabled
.mockResolvedValueOnce('false') // registration disabled
.mockResolvedValueOnce('SSO'); // oidc provider name
prismaMock.user.count.mockResolvedValueOnce(0); // No local users exist
const { GET } = await import('@/app/api/auth/providers/route');
const response = await GET();
const payload = await response.json();
expect(payload.backendMode).toBe('audiobookshelf');
expect(payload.providers).not.toContain('local'); // Should NOT include 'local'
expect(payload.registrationEnabled).toBe(false);
expect(payload.hasLocalUsers).toBe(false);
});
});