Add rootless Podman fixes, and others

improve container startup for rootless Podman, plus related refactors and tests. Key changes:

- Add/modify Audiobookshelf-related code and wiring (src/lib/services/audiobookshelf/api.ts, library service refs) and update documentation TABLEOFCONTENTS to reference ABS implementation.
- Detect user namespace in docker/unified app-start.sh and redis-start.sh and skip gosu when running in rootless Podman to preserve UID mapping; improve startup logging and verification.
- Add utility/service files (auth-token-cache.service.ts, credential-migration.service.ts, cleanup-helpers.ts) and corresponding tests; update chapter-merger and metadata-tagger utilities/tests.
- Update many admin/auth API routes and tests to reflect changes in settings and integrations.
- Remove large AI agent and Audiobookshelf implementation guide docs (AGENTS.md and the implementation guide) and add README note about AI-assisted workflow.

These changes enable Audiobookshelf backend mode, improve compatibility with rootless container runtimes, and include cleanup/refactor work and unit tests.
This commit is contained in:
kikootwo
2026-02-04 14:05:28 -05:00
parent 2ef9ac7be1
commit a0f2ba680d
42 changed files with 1843 additions and 3820 deletions
+85
View File
@@ -113,4 +113,89 @@ describe('metadata tagger', () => {
mockExecFailure('not installed');
await expect(checkFfmpegAvailable()).resolves.toBe(false);
});
describe('metadata escaping', () => {
it('does NOT escape single quotes (they are literal in double-quoted shell strings)', async () => {
fsMock.access.mockResolvedValue(undefined);
mockExecSuccess('done');
await tagAudioFileMetadata('/tmp/book.m4b', {
title: "It's Not Her",
author: "Author's Name",
});
const command = execMock.mock.calls[0][0] as string;
// Single quotes should appear as-is, NOT escaped with backslash
expect(command).toContain('-metadata title="It\'s Not Her"');
expect(command).not.toContain("It\\'s"); // No backslash-escaped single quotes
expect(command).toContain('-metadata album_artist="Author\'s Name"');
});
it('escapes double quotes in metadata values', async () => {
fsMock.access.mockResolvedValue(undefined);
mockExecSuccess('done');
await tagAudioFileMetadata('/tmp/book.m4b', {
title: 'Book "Title"',
author: 'Author',
});
const command = execMock.mock.calls[0][0] as string;
expect(command).toContain('-metadata title="Book \\"Title\\""');
});
it('escapes backticks to prevent command substitution', async () => {
fsMock.access.mockResolvedValue(undefined);
mockExecSuccess('done');
await tagAudioFileMetadata('/tmp/book.m4b', {
title: 'Book `test`',
author: 'Author',
});
const command = execMock.mock.calls[0][0] as string;
expect(command).toContain('-metadata title="Book \\`test\\`"');
});
it('escapes dollar signs to prevent variable expansion', async () => {
fsMock.access.mockResolvedValue(undefined);
mockExecSuccess('done');
await tagAudioFileMetadata('/tmp/book.m4b', {
title: 'Book $100',
author: 'Author',
});
const command = execMock.mock.calls[0][0] as string;
expect(command).toContain('-metadata title="Book \\$100"');
});
it('escapes backslashes before other characters', async () => {
fsMock.access.mockResolvedValue(undefined);
mockExecSuccess('done');
await tagAudioFileMetadata('/tmp/book.m4b', {
title: 'Path\\to\\book',
author: 'Author',
});
const command = execMock.mock.calls[0][0] as string;
expect(command).toContain('-metadata title="Path\\\\to\\\\book"');
});
it('handles complex titles with multiple special characters', async () => {
fsMock.access.mockResolvedValue(undefined);
mockExecSuccess('done');
await tagAudioFileMetadata('/tmp/book.m4b', {
title: "Don't Say \"Hello\" for $5",
author: "O'Brien",
});
const command = execMock.mock.calls[0][0] as string;
// Single quotes literal, double quotes escaped, dollar escaped
expect(command).toContain('-metadata title="Don\'t Say \\"Hello\\" for \\$5"');
expect(command).toContain('-metadata album_artist="O\'Brien"');
});
});
});