@digitalpaws/starr 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.rst +75 -0
- package/data/.nfs.811bbb0d.43c34.4 +0 -0
- package/data/.nfs.81ba4cd0.ffff87804.4 +0 -0
- package/data/.nfs.82d127f7.fffff5704.4 +0 -0
- package/data/.nfs.82d127fe.fffff5704.4 +0 -0
- package/data/.nfs.82d159a5.fffff5aa4.4 +0 -0
- package/data/.nfs.82d159aa.fffff5aa4.4 +0 -0
- package/data/.nfs.82d15a9c.fffff5bb4.4 +0 -0
- package/data/.nfs.82d15aa1.fffff5bb4.4 +0 -0
- package/data/.nfs.82d15c65.fffff5c24.4 +0 -0
- package/data/.nfs.82d1852c.fffff5e24.4 +0 -0
- package/data/.nfs.82d18536.fffff5e24.4 +0 -0
- package/data/.nfs.82d18dbe.fffff5e44.4 +0 -0
- package/data/.nfs.82d18dc6.fffff5e44.4 +0 -0
- package/data/.nfs.8309a3f5.fffff82f4.4 +0 -0
- package/data/.nfs.8309a3fe.fffff82f4.4 +0 -0
- package/data/.nfs.8314122e.fffffb044.4 +0 -0
- package/data/.nfs.83141236.fffffb044.4 +0 -0
- package/data/.nfs.833c4fe9.fffffbc34.4 +0 -0
- package/data/.nfs.833c4ff3.fffffbc34.4 +0 -0
- package/data/logs/starr.log +66244 -0
- package/data/starr.db +0 -0
- package/data/starr.db-shm +0 -0
- package/data/starr.db-wal +0 -0
- package/data/starr.jwt +1 -0
- package/data/starr.key +1 -0
- package/dist/auth/auth-service.d.ts +42 -0
- package/dist/auth/auth-service.d.ts.map +1 -0
- package/dist/auth/auth-service.js +226 -0
- package/dist/auth/auth-service.js.map +1 -0
- package/dist/auth/http-server.d.ts +16 -0
- package/dist/auth/http-server.d.ts.map +1 -0
- package/dist/auth/http-server.js +207 -0
- package/dist/auth/http-server.js.map +1 -0
- package/dist/auth/jwt-secret.d.ts +6 -0
- package/dist/auth/jwt-secret.d.ts.map +1 -0
- package/dist/auth/jwt-secret.js +51 -0
- package/dist/auth/jwt-secret.js.map +1 -0
- package/dist/auth/oidc/oidc-flow.d.ts +60 -0
- package/dist/auth/oidc/oidc-flow.d.ts.map +1 -0
- package/dist/auth/oidc/oidc-flow.js +201 -0
- package/dist/auth/oidc/oidc-flow.js.map +1 -0
- package/dist/auth/oidc/oidc-provider-service.d.ts +59 -0
- package/dist/auth/oidc/oidc-provider-service.d.ts.map +1 -0
- package/dist/auth/oidc/oidc-provider-service.js +124 -0
- package/dist/auth/oidc/oidc-provider-service.js.map +1 -0
- package/dist/auth/oidc/user-service.d.ts +67 -0
- package/dist/auth/oidc/user-service.d.ts.map +1 -0
- package/dist/auth/oidc/user-service.js +177 -0
- package/dist/auth/oidc/user-service.js.map +1 -0
- package/dist/auth/rate-limiter.d.ts +31 -0
- package/dist/auth/rate-limiter.d.ts.map +1 -0
- package/dist/auth/rate-limiter.js +70 -0
- package/dist/auth/rate-limiter.js.map +1 -0
- package/dist/config/config-service.d.ts +83 -0
- package/dist/config/config-service.d.ts.map +1 -0
- package/dist/config/config-service.js +309 -0
- package/dist/config/config-service.js.map +1 -0
- package/dist/config/validate.d.ts +12 -0
- package/dist/config/validate.d.ts.map +1 -0
- package/dist/config/validate.js +140 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/content-types/registry.d.ts +75 -0
- package/dist/content-types/registry.d.ts.map +1 -0
- package/dist/content-types/registry.js +171 -0
- package/dist/content-types/registry.js.map +1 -0
- package/dist/datastore/db.d.ts +9 -0
- package/dist/datastore/db.d.ts.map +1 -0
- package/dist/datastore/db.js +27 -0
- package/dist/datastore/db.js.map +1 -0
- package/dist/datastore/encrypt.d.ts +9 -0
- package/dist/datastore/encrypt.d.ts.map +1 -0
- package/dist/datastore/encrypt.js +84 -0
- package/dist/datastore/encrypt.js.map +1 -0
- package/dist/datastore/migrations.d.ts +10 -0
- package/dist/datastore/migrations.d.ts.map +1 -0
- package/dist/datastore/migrations.js +340 -0
- package/dist/datastore/migrations.js.map +1 -0
- package/dist/datastore/storage.d.ts +4 -0
- package/dist/datastore/storage.d.ts.map +1 -0
- package/dist/datastore/storage.js +60 -0
- package/dist/datastore/storage.js.map +1 -0
- package/dist/events/event-bus.d.ts +15 -0
- package/dist/events/event-bus.d.ts.map +1 -0
- package/dist/events/event-bus.js +140 -0
- package/dist/events/event-bus.js.map +1 -0
- package/dist/events/spine.d.ts +31 -0
- package/dist/events/spine.d.ts.map +1 -0
- package/dist/events/spine.js +68 -0
- package/dist/events/spine.js.map +1 -0
- package/dist/events/trace.d.ts +24 -0
- package/dist/events/trace.d.ts.map +1 -0
- package/dist/events/trace.js +33 -0
- package/dist/events/trace.js.map +1 -0
- package/dist/host.d.ts +65 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +94 -0
- package/dist/host.js.map +1 -0
- package/dist/library/importer.d.ts +29 -0
- package/dist/library/importer.d.ts.map +1 -0
- package/dist/library/importer.js +145 -0
- package/dist/library/importer.js.map +1 -0
- package/dist/library/library-cache.d.ts +46 -0
- package/dist/library/library-cache.d.ts.map +1 -0
- package/dist/library/library-cache.js +152 -0
- package/dist/library/library-cache.js.map +1 -0
- package/dist/library/library-config.d.ts +12 -0
- package/dist/library/library-config.d.ts.map +1 -0
- package/dist/library/library-config.js +10 -0
- package/dist/library/library-config.js.map +1 -0
- package/dist/library/library-scanner.d.ts +62 -0
- package/dist/library/library-scanner.d.ts.map +1 -0
- package/dist/library/library-scanner.js +336 -0
- package/dist/library/library-scanner.js.map +1 -0
- package/dist/library/library-service.d.ts +65 -0
- package/dist/library/library-service.d.ts.map +1 -0
- package/dist/library/library-service.js +197 -0
- package/dist/library/library-service.js.map +1 -0
- package/dist/library/metadata-matcher.d.ts +56 -0
- package/dist/library/metadata-matcher.d.ts.map +1 -0
- package/dist/library/metadata-matcher.js +207 -0
- package/dist/library/metadata-matcher.js.map +1 -0
- package/dist/library/naming.d.ts +8 -0
- package/dist/library/naming.d.ts.map +1 -0
- package/dist/library/naming.js +47 -0
- package/dist/library/naming.js.map +1 -0
- package/dist/library/scan-controller.d.ts +95 -0
- package/dist/library/scan-controller.d.ts.map +1 -0
- package/dist/library/scan-controller.js +379 -0
- package/dist/library/scan-controller.js.map +1 -0
- package/dist/library/source-resolver.d.ts +17 -0
- package/dist/library/source-resolver.d.ts.map +1 -0
- package/dist/library/source-resolver.js +75 -0
- package/dist/library/source-resolver.js.map +1 -0
- package/dist/logging/logger.d.ts +3 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +107 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/main.d.ts +36 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +229 -0
- package/dist/main.js.map +1 -0
- package/dist/plugins/context.d.ts +10 -0
- package/dist/plugins/context.d.ts.map +1 -0
- package/dist/plugins/context.js +110 -0
- package/dist/plugins/context.js.map +1 -0
- package/dist/plugins/discovery.d.ts +2 -0
- package/dist/plugins/discovery.d.ts.map +1 -0
- package/dist/plugins/discovery.js +32 -0
- package/dist/plugins/discovery.js.map +1 -0
- package/dist/plugins/loader.d.ts +9 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +245 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/state/request-intake.d.ts +26 -0
- package/dist/state/request-intake.d.ts.map +1 -0
- package/dist/state/request-intake.js +74 -0
- package/dist/state/request-intake.js.map +1 -0
- package/dist/state/state-store.d.ts +146 -0
- package/dist/state/state-store.d.ts.map +1 -0
- package/dist/state/state-store.js +465 -0
- package/dist/state/state-store.js.map +1 -0
- package/dist/state/trace-store.d.ts +12 -0
- package/dist/state/trace-store.d.ts.map +1 -0
- package/dist/state/trace-store.js +24 -0
- package/dist/state/trace-store.js.map +1 -0
- package/dist/types/plugin.d.ts +93 -0
- package/dist/types/plugin.d.ts.map +1 -0
- package/dist/types/plugin.js +4 -0
- package/dist/types/plugin.js.map +1 -0
- package/dist/web/index.d.ts +10 -0
- package/dist/web/index.d.ts.map +1 -0
- package/dist/web/index.js +153 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/routes/activity.d.ts +6 -0
- package/dist/web/routes/activity.d.ts.map +1 -0
- package/dist/web/routes/activity.js +31 -0
- package/dist/web/routes/activity.js.map +1 -0
- package/dist/web/routes/auth.d.ts +6 -0
- package/dist/web/routes/auth.d.ts.map +1 -0
- package/dist/web/routes/auth.js +142 -0
- package/dist/web/routes/auth.js.map +1 -0
- package/dist/web/routes/config.d.ts +7 -0
- package/dist/web/routes/config.d.ts.map +1 -0
- package/dist/web/routes/config.js +204 -0
- package/dist/web/routes/config.js.map +1 -0
- package/dist/web/routes/content-types.d.ts +6 -0
- package/dist/web/routes/content-types.d.ts.map +1 -0
- package/dist/web/routes/content-types.js +29 -0
- package/dist/web/routes/content-types.js.map +1 -0
- package/dist/web/routes/discovery.d.ts +6 -0
- package/dist/web/routes/discovery.d.ts.map +1 -0
- package/dist/web/routes/discovery.js +111 -0
- package/dist/web/routes/discovery.js.map +1 -0
- package/dist/web/routes/library-import.d.ts +6 -0
- package/dist/web/routes/library-import.d.ts.map +1 -0
- package/dist/web/routes/library-import.js +120 -0
- package/dist/web/routes/library-import.js.map +1 -0
- package/dist/web/routes/library-management.d.ts +6 -0
- package/dist/web/routes/library-management.d.ts.map +1 -0
- package/dist/web/routes/library-management.js +120 -0
- package/dist/web/routes/library-management.js.map +1 -0
- package/dist/web/routes/library.d.ts +6 -0
- package/dist/web/routes/library.d.ts.map +1 -0
- package/dist/web/routes/library.js +28 -0
- package/dist/web/routes/library.js.map +1 -0
- package/dist/web/routes/oidc.d.ts +9 -0
- package/dist/web/routes/oidc.d.ts.map +1 -0
- package/dist/web/routes/oidc.js +203 -0
- package/dist/web/routes/oidc.js.map +1 -0
- package/dist/web/routes/requests.d.ts +6 -0
- package/dist/web/routes/requests.d.ts.map +1 -0
- package/dist/web/routes/requests.js +183 -0
- package/dist/web/routes/requests.js.map +1 -0
- package/dist/web/routes/sse.d.ts +6 -0
- package/dist/web/routes/sse.d.ts.map +1 -0
- package/dist/web/routes/sse.js +54 -0
- package/dist/web/routes/sse.js.map +1 -0
- package/package.json +33 -0
- package/package.json.license +2 -0
- package/src/auth/auth-service.ts +333 -0
- package/src/auth/http-server.ts +249 -0
- package/src/auth/jwt-secret.ts +65 -0
- package/src/auth/oidc/oidc-flow.ts +266 -0
- package/src/auth/oidc/oidc-provider-service.ts +174 -0
- package/src/auth/oidc/user-service.ts +227 -0
- package/src/auth/rate-limiter.ts +85 -0
- package/src/config/config-service.ts +434 -0
- package/src/config/validate.ts +153 -0
- package/src/content-types/registry.ts +194 -0
- package/src/datastore/db.ts +38 -0
- package/src/datastore/encrypt.ts +107 -0
- package/src/datastore/migrations.ts +367 -0
- package/src/datastore/storage.ts +83 -0
- package/src/events/event-bus.ts +180 -0
- package/src/events/spine.ts +87 -0
- package/src/events/trace.ts +38 -0
- package/src/host.ts +130 -0
- package/src/library/importer.ts +174 -0
- package/src/library/library-cache.ts +212 -0
- package/src/library/library-config.ts +22 -0
- package/src/library/library-scanner.ts +404 -0
- package/src/library/library-service.ts +282 -0
- package/src/library/metadata-matcher.ts +285 -0
- package/src/library/naming.ts +59 -0
- package/src/library/scan-controller.ts +490 -0
- package/src/library/source-resolver.ts +92 -0
- package/src/logging/logger.ts +115 -0
- package/src/main.ts +364 -0
- package/src/plugins/context.ts +138 -0
- package/src/plugins/discovery.ts +29 -0
- package/src/plugins/loader.ts +309 -0
- package/src/state/request-intake.ts +101 -0
- package/src/state/state-store.ts +621 -0
- package/src/state/trace-store.ts +27 -0
- package/src/types/plugin.ts +110 -0
- package/src/web/index.ts +182 -0
- package/src/web/routes/activity.ts +41 -0
- package/src/web/routes/auth.ts +170 -0
- package/src/web/routes/config.ts +282 -0
- package/src/web/routes/content-types.ts +42 -0
- package/src/web/routes/discovery.ts +156 -0
- package/src/web/routes/library-import.ts +196 -0
- package/src/web/routes/library-management.ts +140 -0
- package/src/web/routes/library.ts +46 -0
- package/src/web/routes/oidc.ts +260 -0
- package/src/web/routes/requests.ts +250 -0
- package/src/web/routes/sse.ts +92 -0
- package/tests/auth.test.ts +510 -0
- package/tests/config-route.test.ts +345 -0
- package/tests/config.test.ts +612 -0
- package/tests/content-types.test.ts +376 -0
- package/tests/datastore.test.ts +345 -0
- package/tests/e2e.test.ts +430 -0
- package/tests/event-bus.test.ts +925 -0
- package/tests/host.test.ts +449 -0
- package/tests/library-cache.test.ts +171 -0
- package/tests/library-scanner.test.ts +177 -0
- package/tests/library.test.ts +210 -0
- package/tests/loader-context.test.ts +644 -0
- package/tests/logger-jsonl.test.ts +154 -0
- package/tests/metadata-matcher.test.ts +198 -0
- package/tests/oidc.test.ts +218 -0
- package/tests/scan-controller.test.ts +328 -0
- package/tests/source-resolver.test.ts +100 -0
- package/tests/starr-video-integration.test.ts +153 -0
- package/tests/state-store.test.ts +700 -0
- package/tests/trace-history.test.ts +356 -0
- package/tsconfig.json +8 -0
- package/tsconfig.json.license +2 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
5
|
+
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import Database from "better-sqlite3";
|
|
9
|
+
import { createLogger } from "../src/logging/logger.js";
|
|
10
|
+
import { runMigrations, CORE_MIGRATIONS } from "../src/datastore/migrations.js";
|
|
11
|
+
import { LibraryScanner } from "../src/library/library-scanner.js";
|
|
12
|
+
import type { LibraryConfig } from "../src/library/library-config.js";
|
|
13
|
+
|
|
14
|
+
let tmpDir: string;
|
|
15
|
+
let db: Database.Database;
|
|
16
|
+
let scanner: LibraryScanner;
|
|
17
|
+
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
tmpDir = await mkdtemp(path.join(tmpdir(), "starr-scanner-test-"));
|
|
20
|
+
|
|
21
|
+
db = new Database(":memory:");
|
|
22
|
+
db.pragma("journal_mode = WAL");
|
|
23
|
+
const logger = createLogger("test", "silent");
|
|
24
|
+
runMigrations(db, CORE_MIGRATIONS, logger);
|
|
25
|
+
|
|
26
|
+
// Create library structure
|
|
27
|
+
const moviesDir = path.join(tmpDir, "Movies");
|
|
28
|
+
const booksDir = path.join(tmpDir, "Books");
|
|
29
|
+
await mkdir(moviesDir, { recursive: true });
|
|
30
|
+
await mkdir(booksDir, { recursive: true });
|
|
31
|
+
|
|
32
|
+
// Movie 1: Dune (2021)
|
|
33
|
+
const duneDir = path.join(moviesDir, "Dune (2021)");
|
|
34
|
+
await mkdir(duneDir, { recursive: true });
|
|
35
|
+
await writeFile(path.join(duneDir, "Dune (2021) [2160p].mkv"), "movie1");
|
|
36
|
+
await writeFile(path.join(duneDir, "Dune (2021).en.srt"), "subs1");
|
|
37
|
+
|
|
38
|
+
// Movie 2: The Matrix (1999)
|
|
39
|
+
const matrixDir = path.join(moviesDir, "The Matrix (1999)");
|
|
40
|
+
await mkdir(matrixDir, { recursive: true });
|
|
41
|
+
await writeFile(path.join(matrixDir, "The Matrix (1999) [1080p].mp4"), "movie2");
|
|
42
|
+
await writeFile(path.join(matrixDir, "The Matrix (1999).en.srt"), "subs2");
|
|
43
|
+
|
|
44
|
+
// Movie 3: Hidden dir (should be skipped)
|
|
45
|
+
const hiddenDir = path.join(moviesDir, ".hidden_movie");
|
|
46
|
+
await mkdir(hiddenDir, { recursive: true });
|
|
47
|
+
await writeFile(path.join(hiddenDir, "hidden.mkv"), "hidden");
|
|
48
|
+
|
|
49
|
+
// Movie 4: Extras dir (should be skipped)
|
|
50
|
+
const extrasDir = path.join(moviesDir, "Extras");
|
|
51
|
+
await mkdir(extrasDir, { recursive: true });
|
|
52
|
+
await writeFile(path.join(extrasDir, "extra.mkv"), "extra");
|
|
53
|
+
|
|
54
|
+
// Movie 5: Empty dir (no media files)
|
|
55
|
+
const emptyDir = path.join(moviesDir, "Empty Folder");
|
|
56
|
+
await mkdir(emptyDir, { recursive: true });
|
|
57
|
+
await writeFile(path.join(emptyDir, "readme.txt"), "not media");
|
|
58
|
+
|
|
59
|
+
// Book 1: Some Book (use mkv so scanner recognizes it)
|
|
60
|
+
const bookDir = path.join(booksDir, "Some Author", "Some Title");
|
|
61
|
+
await mkdir(bookDir, { recursive: true });
|
|
62
|
+
await writeFile(path.join(bookDir, "Some Title.mkv"), "book1");
|
|
63
|
+
|
|
64
|
+
// Insert a library_paths record to test already-imported detection
|
|
65
|
+
db.prepare(`INSERT INTO tracked_requests (request_id, content_type_id, content_identity, item, owner_user_id, status, library_path)
|
|
66
|
+
VALUES (?,?,?,?,?,?,?)`).run(
|
|
67
|
+
"existing-movie",
|
|
68
|
+
"starr-video/movie@1",
|
|
69
|
+
"id::1",
|
|
70
|
+
JSON.stringify({ title: "Existing", contentTypeId: "starr-video/movie@1", externalIds: {} }),
|
|
71
|
+
"admin",
|
|
72
|
+
"imported",
|
|
73
|
+
path.join(duneDir, "Dune (2021) [2160p].mkv"),
|
|
74
|
+
);
|
|
75
|
+
db.prepare(`INSERT INTO library_paths (request_id, library_path, library_root, imported_at)
|
|
76
|
+
VALUES (?,?,?,?)`).run(
|
|
77
|
+
"existing-movie",
|
|
78
|
+
path.join(duneDir, "Dune (2021) [2160p].mkv"),
|
|
79
|
+
moviesDir,
|
|
80
|
+
"2026-01-01",
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const getConfig = (): LibraryConfig => ({
|
|
84
|
+
defaultLibraryRoot: moviesDir,
|
|
85
|
+
roots: {
|
|
86
|
+
"starr-video/movie@1": moviesDir,
|
|
87
|
+
"starr-books/book@1": booksDir,
|
|
88
|
+
},
|
|
89
|
+
linkType: "copy",
|
|
90
|
+
importSubtitles: true,
|
|
91
|
+
subtitleExtensions: ["srt", "ass"],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
scanner = new LibraryScanner(db, logger, getConfig);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
afterAll(async () => {
|
|
98
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
99
|
+
db.close();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("LibraryScanner", () => {
|
|
103
|
+
it("discovers media groups from library roots", async () => {
|
|
104
|
+
const groups = await scanner.discoverGroups();
|
|
105
|
+
expect(groups.length).toBeGreaterThanOrEqual(2);
|
|
106
|
+
|
|
107
|
+
const dune = groups.find((g) => g.folderName === "Dune (2021)");
|
|
108
|
+
expect(dune).toBeDefined();
|
|
109
|
+
expect(dune!.files).toHaveLength(1);
|
|
110
|
+
expect(dune!.files[0]).toContain("Dune (2021) [2160p].mkv");
|
|
111
|
+
expect(dune!.subtitleFiles).toHaveLength(1);
|
|
112
|
+
expect(dune!.contentTypeId).toBe("starr-video/movie@1");
|
|
113
|
+
|
|
114
|
+
const matrix = groups.find((g) => g.folderName === "The Matrix (1999)");
|
|
115
|
+
expect(matrix).toBeDefined();
|
|
116
|
+
expect(matrix!.files).toHaveLength(1);
|
|
117
|
+
expect(matrix!.contentTypeId).toBe("starr-video/movie@1");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("scans only specified content types when contentTypeIds provided", async () => {
|
|
121
|
+
const groups = await scanner.discoverGroups({ contentTypeIds: ["starr-books/book@1"] });
|
|
122
|
+
// The books root has a nested structure: Some Author/Some Title/
|
|
123
|
+
// The scanner recurses into subdirectories, so this should find the book
|
|
124
|
+
expect(groups.length).toBeGreaterThanOrEqual(1);
|
|
125
|
+
for (const g of groups) {
|
|
126
|
+
expect(g.contentTypeId).toBe("starr-books/book@1");
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("skips hidden directories", async () => {
|
|
131
|
+
const groups = await scanner.discoverGroups();
|
|
132
|
+
const hidden = groups.find((g) => g.folderName.startsWith("."));
|
|
133
|
+
expect(hidden).toBeUndefined();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("skips Extras and Sample directories", async () => {
|
|
137
|
+
const groups = await scanner.discoverGroups();
|
|
138
|
+
const extras = groups.find((g) => g.folderName === "Extras");
|
|
139
|
+
expect(extras).toBeUndefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("skips directories with no media files", async () => {
|
|
143
|
+
const groups = await scanner.discoverGroups();
|
|
144
|
+
const empty = groups.find((g) => g.folderName === "Empty Folder");
|
|
145
|
+
expect(empty).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("detects already-imported directories", async () => {
|
|
149
|
+
const groups = await scanner.discoverGroups();
|
|
150
|
+
const dirs = groups.map((g) => g.directory);
|
|
151
|
+
const existing = scanner.findExisting(dirs);
|
|
152
|
+
const duneDir = groups.find((g) => g.folderName === "Dune (2021)")?.directory;
|
|
153
|
+
expect(duneDir).toBeDefined();
|
|
154
|
+
expect(existing.has(duneDir!)).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("includes subtitle files in discovered groups", async () => {
|
|
158
|
+
const groups = await scanner.discoverGroups();
|
|
159
|
+
const dune = groups.find((g) => g.folderName === "Dune (2021)");
|
|
160
|
+
expect(dune).toBeDefined();
|
|
161
|
+
expect(dune!.subtitleFiles.length).toBeGreaterThan(0);
|
|
162
|
+
expect(dune!.subtitleFiles[0]).toContain(".srt");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("groups nested content directories by item name, not category", async () => {
|
|
166
|
+
// Simulate root/Category/Title/file.mkv structure
|
|
167
|
+
const groups = await scanner.discoverGroups();
|
|
168
|
+
// The test data has: moviesDir/Dune (2021)/file.mkv
|
|
169
|
+
// where moviesDir IS the defaultLibraryRoot
|
|
170
|
+
const dune = groups.find((g) => g.folderName === "Dune (2021)");
|
|
171
|
+
expect(dune).toBeDefined();
|
|
172
|
+
expect(dune!.directory).toContain("Dune (2021)");
|
|
173
|
+
// Ensure no group is accidentally named after the parent category
|
|
174
|
+
const moviesGroup = groups.find((g) => g.folderName === "Movies");
|
|
175
|
+
expect(moviesGroup).toBeUndefined();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// SPDX-License-Identifier: 0BSD
|
|
2
|
+
// SPDX-FileCopyrightText: 2025 Digi <digitalpaws@proton.me>
|
|
3
|
+
|
|
4
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
5
|
+
import { mkdtemp, mkdir, writeFile, rm, copyFile } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import Database from "better-sqlite3";
|
|
9
|
+
import { resolveNaming } from "../src/library/naming.js";
|
|
10
|
+
import { Importer } from "../src/library/importer.js";
|
|
11
|
+
import { EventBusImpl } from "../src/events/event-bus.js";
|
|
12
|
+
import { createLogger } from "../src/logging/logger.js";
|
|
13
|
+
import { runMigrations, CORE_MIGRATIONS } from "../src/datastore/migrations.js";
|
|
14
|
+
import type { NamingRule, ContentItemBase } from "@digitalpaws/starr-types-core";
|
|
15
|
+
|
|
16
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function movieItem(overrides?: Partial<ContentItemBase>): any {
|
|
19
|
+
return {
|
|
20
|
+
contentTypeId: "starr-video/movie@1",
|
|
21
|
+
title: "Dune",
|
|
22
|
+
externalIds: { tmdb: "123", imdb: "tt123" },
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Naming tests (Phase 1c) ─────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
describe("resolveNaming", () => {
|
|
30
|
+
it("resolves default naming: {Title}/{Title}.{ext}", () => {
|
|
31
|
+
const rule: NamingRule = {
|
|
32
|
+
directory: [[{ type: "field", value: "Title" }]],
|
|
33
|
+
filename: [[{ type: "field", value: "Title" }]],
|
|
34
|
+
};
|
|
35
|
+
expect(resolveNaming(rule, movieItem(), "mkv")).toBe("Dune/Dune.mkv");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("resolves multi-segment naming with year and quality", () => {
|
|
39
|
+
const rule: NamingRule = {
|
|
40
|
+
directory: [[{ type: "field", value: "Title" }, { type: "literal", value: " (" }, { type: "field", value: "Year" }, { type: "literal", value: ")" }]],
|
|
41
|
+
filename: [[{ type: "field", value: "Title" }, { type: "literal", value: " (" }, { type: "field", value: "Year" }, { type: "literal", value: ") [" }, { type: "field", value: "Quality" }, { type: "literal", value: "]" }]],
|
|
42
|
+
};
|
|
43
|
+
const item = movieItem({ fields: { Quality: "2160p" }, year: 2021 });
|
|
44
|
+
expect(resolveNaming(rule, item, "mkv")).toBe("Dune (2021)/Dune (2021) [2160p].mkv");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("sanitises problematic characters", () => {
|
|
48
|
+
const item = movieItem({ title: 'Movie: The <Best> "Ever"' });
|
|
49
|
+
const rule: NamingRule = {
|
|
50
|
+
directory: [[{ type: "field", value: "Title" }]],
|
|
51
|
+
filename: [[{ type: "field", value: "Title" }]],
|
|
52
|
+
};
|
|
53
|
+
expect(resolveNaming(rule, item, "mp4")).toBe("Movie The Best Ever/Movie The Best Ever.mp4");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("resolves external ids and ContentType", () => {
|
|
57
|
+
const rule: NamingRule = {
|
|
58
|
+
directory: [[{ type: "field", value: "ContentType" }]],
|
|
59
|
+
filename: [[{ type: "field", value: "tmdb" }]],
|
|
60
|
+
};
|
|
61
|
+
expect(resolveNaming(rule, movieItem(), "mkv")).toBe("starr-video/movie@1/123.mkv");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ── Importer tests (real fs with temp dirs) ─────────────────────────
|
|
66
|
+
|
|
67
|
+
let tmpDirBase: string;
|
|
68
|
+
let counter = 0;
|
|
69
|
+
|
|
70
|
+
beforeAll(async () => {
|
|
71
|
+
tmpDirBase = await mkdtemp(path.join(tmpdir(), "starr-library-test-"));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterAll(async () => {
|
|
75
|
+
await rm(tmpDirBase, { recursive: true, force: true });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Set up a fresh test environment with:
|
|
80
|
+
* - In-memory SQLite database with migrations
|
|
81
|
+
* - Event bus
|
|
82
|
+
* - A temp download directory with a source file
|
|
83
|
+
* - A temp library root
|
|
84
|
+
* - A tracked request in "downloaded" state
|
|
85
|
+
*/
|
|
86
|
+
async function setup() {
|
|
87
|
+
const db = new Database(":memory:");
|
|
88
|
+
db.pragma("journal_mode = WAL");
|
|
89
|
+
db.pragma("foreign_keys = ON");
|
|
90
|
+
const logger = createLogger("test", "silent");
|
|
91
|
+
runMigrations(db, CORE_MIGRATIONS, logger);
|
|
92
|
+
|
|
93
|
+
const impl = new EventBusImpl(logger);
|
|
94
|
+
const coreBus = impl.coreBus();
|
|
95
|
+
|
|
96
|
+
// Temp directories unique per test
|
|
97
|
+
const n = ++counter;
|
|
98
|
+
const downloadDir = path.join(tmpDirBase, `download-${n}`);
|
|
99
|
+
const libraryRoot = path.join(tmpDirBase, `library-${n}`);
|
|
100
|
+
await mkdir(downloadDir, { recursive: true });
|
|
101
|
+
await mkdir(libraryRoot, { recursive: true });
|
|
102
|
+
|
|
103
|
+
// Create a source file
|
|
104
|
+
const sourceFile = path.join(downloadDir, "Dune.mkv");
|
|
105
|
+
await writeFile(sourceFile, "fake media content");
|
|
106
|
+
|
|
107
|
+
// Insert a tracked request
|
|
108
|
+
db.prepare(`INSERT INTO tracked_requests (request_id, content_type_id, content_identity, item, owner_user_id, status)
|
|
109
|
+
VALUES (?,?,?,?,?,?)`).run(
|
|
110
|
+
"req-001",
|
|
111
|
+
"starr-video/movie@1",
|
|
112
|
+
"starr-video/movie@1::tmdb=123",
|
|
113
|
+
JSON.stringify({ title: "Dune", contentTypeId: "starr-video/movie@1", externalIds: { tmdb: "123" } }),
|
|
114
|
+
"admin",
|
|
115
|
+
"downloaded",
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const getConfig = () => ({
|
|
119
|
+
defaultLibraryRoot: libraryRoot,
|
|
120
|
+
roots: {} as Record<string, string>,
|
|
121
|
+
linkType: "copy" as const,
|
|
122
|
+
importSubtitles: false,
|
|
123
|
+
subtitleExtensions: ["srt"],
|
|
124
|
+
});
|
|
125
|
+
const getNamingRule = () => ({
|
|
126
|
+
directory: [[{ type: "field", value: "Title" }]],
|
|
127
|
+
filename: [[{ type: "field", value: "Title" }]],
|
|
128
|
+
} as NamingRule);
|
|
129
|
+
|
|
130
|
+
const importer = new Importer(db, coreBus, logger, getConfig, getNamingRule);
|
|
131
|
+
|
|
132
|
+
return { db, coreBus, importer, sourceFile, libraryRoot };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
describe("Importer.import", () => {
|
|
136
|
+
it("successfully imports a file via copy", async () => {
|
|
137
|
+
const { importer, coreBus, sourceFile, libraryRoot } = await setup();
|
|
138
|
+
const events: string[] = [];
|
|
139
|
+
coreBus.on("import.started", () => events.push("import.started"));
|
|
140
|
+
coreBus.on("import.completed", () => events.push("import.completed"));
|
|
141
|
+
|
|
142
|
+
const result = await importer.import(
|
|
143
|
+
"req-001",
|
|
144
|
+
sourceFile,
|
|
145
|
+
{ title: "Dune", contentTypeId: "starr-video/movie@1", externalIds: { tmdb: "123" } },
|
|
146
|
+
"starr-video/movie@1",
|
|
147
|
+
"mkv",
|
|
148
|
+
"starr-video/movie@1::tmdb=123",
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
expect(result.success).toBe(true);
|
|
152
|
+
expect(result.libraryPath).toBe(path.join(libraryRoot, "Dune", "Dune.mkv"));
|
|
153
|
+
expect(events).toContain("import.started");
|
|
154
|
+
expect(events).toContain("import.completed");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("emits import.failed on error (non-existent source)", async () => {
|
|
158
|
+
const { importer, coreBus, libraryRoot } = await setup();
|
|
159
|
+
const events: string[] = [];
|
|
160
|
+
coreBus.on("import.failed", () => events.push("import.failed"));
|
|
161
|
+
|
|
162
|
+
const result = await importer.import(
|
|
163
|
+
"req-001",
|
|
164
|
+
"/nonexistent/path/file.mkv",
|
|
165
|
+
{ title: "D", contentTypeId: "starr-video/movie@1", externalIds: { tmdb: "123" } },
|
|
166
|
+
"starr-video/movie@1",
|
|
167
|
+
"mkv",
|
|
168
|
+
"starr-video/movie@1::tmdb=123",
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(result.success).toBe(false);
|
|
172
|
+
expect(result.error).toBeDefined();
|
|
173
|
+
expect(events).toContain("import.failed");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("records library_path in the database", async () => {
|
|
177
|
+
const { importer, db, sourceFile, libraryRoot } = await setup();
|
|
178
|
+
|
|
179
|
+
await importer.import(
|
|
180
|
+
"req-001",
|
|
181
|
+
sourceFile,
|
|
182
|
+
{ title: "Dune", contentTypeId: "starr-video/movie@1", externalIds: { tmdb: "123" } },
|
|
183
|
+
"starr-video/movie@1",
|
|
184
|
+
"mkv",
|
|
185
|
+
"starr-video/movie@1::tmdb=123",
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const row = db.prepare("SELECT library_path FROM library_paths WHERE request_id = ?").get("req-001") as any;
|
|
189
|
+
expect(row).toBeDefined();
|
|
190
|
+
expect(row!.library_path).toBe(path.join(libraryRoot, "Dune", "Dune.mkv"));
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("creates the destination file on disk", async () => {
|
|
194
|
+
const { importer, sourceFile, libraryRoot } = await setup();
|
|
195
|
+
|
|
196
|
+
const result = await importer.import(
|
|
197
|
+
"req-001",
|
|
198
|
+
sourceFile,
|
|
199
|
+
{ title: "Dune", contentTypeId: "starr-video/movie@1", externalIds: { tmdb: "123" } },
|
|
200
|
+
"starr-video/movie@1",
|
|
201
|
+
"mkv",
|
|
202
|
+
"starr-video/movie@1::tmdb=123",
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
expect(result.success).toBe(true);
|
|
206
|
+
// Verify the file actually exists
|
|
207
|
+
const { access } = await import("node:fs/promises");
|
|
208
|
+
await expect(access(result.libraryPath!)).resolves.toBeUndefined();
|
|
209
|
+
});
|
|
210
|
+
});
|