mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
f23afc1ba2
Implement Plex-compatible file-extension coercion to avoid Plex silently ignoring .mp4 (and single-file .m4a) audiobooks (issue #166). Adds a DB migration and configuration key (plex_format_coercion_enabled, default true), exposes a toggle in the setup wizard and Admin Paths settings, and persists/reads the setting in the admin/setup APIs. Introduces src/lib/utils/format-coercion.ts (coerceToPlexCompatible) and related constants in src/lib/constants/audio-formats.ts (PLEX_COMPATIBLE_EXTENSIONS, COERCION_RENAME_MAP, DRM_EXTENSIONS, TRANSCODE_REQUIRED_EXTENSIONS). The organize-files processor now runs coercion after organizing/tagging and before generating the filesHash and triggering scans; coercion is idempotent, never overwrites existing targets, logs warnings on DRM/transcode/permission errors, and is non-fatal. Adds unit tests for the coercion util and updates processor & setup UI tests. Updates documentation (TABLEOFCONTENTS, file-organization, fixes/file-hash-matching, settings-pages) describing behavior, config, and constraints.
120 lines
3.3 KiB
TypeScript
120 lines
3.3 KiB
TypeScript
/**
|
|
* Component: Setup Paths Step Tests
|
|
* Documentation: documentation/setup-wizard.md
|
|
*/
|
|
|
|
// @vitest-environment jsdom
|
|
|
|
import React, { useState } from 'react';
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { PathsStep } from '@/app/setup/steps/PathsStep';
|
|
|
|
const PathsHarness = ({
|
|
onNext,
|
|
onBack,
|
|
initialState,
|
|
}: {
|
|
onNext: () => void;
|
|
onBack: () => void;
|
|
initialState?: Partial<React.ComponentProps<typeof PathsStep>>;
|
|
}) => {
|
|
const [state, setState] = useState({
|
|
downloadDir: '/downloads',
|
|
mediaDir: '/media/audiobooks',
|
|
metadataTaggingEnabled: true,
|
|
plexFormatCoercionEnabled: true,
|
|
chapterMergingEnabled: false,
|
|
...initialState,
|
|
});
|
|
|
|
return (
|
|
<PathsStep
|
|
{...state}
|
|
onUpdate={(field, value) => setState((prev) => ({ ...prev, [field]: value }))}
|
|
onNext={onNext}
|
|
onBack={onBack}
|
|
/>
|
|
);
|
|
};
|
|
|
|
describe('PathsStep', () => {
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it('validates paths and allows navigation on success', async () => {
|
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
success: true,
|
|
message: 'Directories are ready',
|
|
downloadDirValid: true,
|
|
mediaDirValid: true,
|
|
}),
|
|
});
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
const onNext = vi.fn();
|
|
|
|
render(<PathsHarness onNext={onNext} onBack={vi.fn()} />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Validate Paths' }));
|
|
|
|
await waitFor(() => {
|
|
expect(fetchMock).toHaveBeenCalledWith('/api/setup/test-paths', expect.any(Object));
|
|
});
|
|
|
|
expect(screen.getByText('Directories are ready')).toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
|
|
expect(onNext).toHaveBeenCalled();
|
|
});
|
|
|
|
it('requires validation before proceeding', async () => {
|
|
const onNext = vi.fn();
|
|
render(<PathsHarness onNext={onNext} onBack={vi.fn()} />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
|
|
|
|
expect(screen.getByText('Please validate the paths before proceeding')).toBeInTheDocument();
|
|
expect(onNext).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('toggles metadata and chapter merge settings', async () => {
|
|
render(
|
|
<PathsHarness
|
|
onNext={vi.fn()}
|
|
onBack={vi.fn()}
|
|
initialState={{
|
|
metadataTaggingEnabled: false,
|
|
plexFormatCoercionEnabled: false,
|
|
chapterMergingEnabled: false,
|
|
}}
|
|
/>
|
|
);
|
|
|
|
const metadataToggle = screen.getByLabelText('Auto-tag audio files with metadata');
|
|
const chapterToggle = screen.getByLabelText('Auto-merge chapters to M4B');
|
|
|
|
fireEvent.click(metadataToggle);
|
|
fireEvent.click(chapterToggle);
|
|
|
|
expect(metadataToggle).toBeChecked();
|
|
expect(chapterToggle).toBeChecked();
|
|
});
|
|
|
|
it('renders plex format coercion toggle with default checked and toggles state', async () => {
|
|
render(<PathsHarness onNext={vi.fn()} onBack={vi.fn()} />);
|
|
|
|
const coercionToggle = screen.getByLabelText('Coerce file formats for Plex compatibility');
|
|
|
|
expect(coercionToggle).toBeChecked();
|
|
|
|
fireEvent.click(coercionToggle);
|
|
expect(coercionToggle).not.toBeChecked();
|
|
|
|
fireEvent.click(coercionToggle);
|
|
expect(coercionToggle).toBeChecked();
|
|
});
|
|
});
|