@anandaramr/obscura 1.0.0 → 1.1.0

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/API.md ADDED
@@ -0,0 +1,93 @@
1
+ # Obscura API
2
+
3
+ Base URL: `/api`
4
+
5
+ All endpoints return JSON unless otherwise noted.
6
+
7
+ ## Files
8
+
9
+ ### List files
10
+
11
+ `GET /api/files`
12
+
13
+ Returns all files sorted by date descending.
14
+
15
+ **Response** `200 OK`
16
+
17
+ ```json
18
+ [
19
+ {
20
+ "id": "4186824008a4299b7587c6902d00e6df",
21
+ "name": "beach_volleyball.mp4",
22
+ "type": "video",
23
+ "date": "2026-05-25T22:15:30.000Z",
24
+ "size": 45102080,
25
+ "isAnimated": false
26
+ },
27
+ {
28
+ "id": "4316e0adec899c7fdd40bb55d47900e4",
29
+ "name": "sunset.jpg",
30
+ "type": "image",
31
+ "date": "2026-05-25T23:11:00.000Z",
32
+ "size": 2048576,
33
+ "isAnimated": false
34
+ }
35
+ ]
36
+ ```
37
+
38
+ ### Get file
39
+
40
+ `GET /api/files/:id`
41
+
42
+ Returns the raw file for rendering in the browser.
43
+
44
+ **Responses**
45
+
46
+ - `200` — raw file bytes
47
+ - `404` — file not found
48
+
49
+ ## Thumbnails
50
+
51
+ ### Get thumbnail
52
+
53
+ `GET /api/thumb/:id`
54
+
55
+ Returns a JPEG thumbnail for the file. Generated on first request and cached to disk. Images below the cache threshold are returned as-is without processing. Thumbnail generation is subject to the configured disk concurrency limit.
56
+
57
+ **Responses**
58
+
59
+ - `200` — JPEG thumbnail (or original file if below cache threshold)
60
+ - `404` — file not found
61
+ - `500` — thumbnail generation failed
62
+
63
+ ## Events
64
+
65
+ ### Subscribe to file changes
66
+
67
+ `GET /api/events`
68
+
69
+ Opens a server-sent events stream. Emits an event whenever a file is added or removed from the gallery directory. Reconnect and re-fetch `/api/files` if the connection drops.
70
+
71
+ **Event shape**
72
+
73
+ `add` — emitted when a file is added to the gallery directory.
74
+
75
+ ```json
76
+ {
77
+ "action": "add",
78
+ "file": {
79
+ "id": "4186824008a4299b7587c6902d00e6df",
80
+ "name": "beach_volleyball.mp4",
81
+ "type": "video",
82
+ "date": "2026-05-25T22:15:30.000Z",
83
+ "size": 45102080,
84
+ "isAnimated": false
85
+ }
86
+ }
87
+ ```
88
+
89
+ `remove` — emitted when a file is removed from the gallery directory.
90
+
91
+ ```json
92
+ { "action": "remove", "file": { "id": "4186824008a4299b7587c6902d00e6df" } }
93
+ ```
package/README.md CHANGED
@@ -1,3 +1,73 @@
1
- # Obscura Media Server
1
+ # Obscura
2
2
 
3
- Core documentation and CLI guides will be published with the upcoming stable release.
3
+ A self-hosted media gallery server for local networks. Given a directory, Obscura indexes your photos and videos, generates thumbnails, and exposes a web interface accessible to any device on the same network.
4
+
5
+ ## Prerequisites
6
+
7
+ Before running Obscura, ensure you have the following software installed on your machine:
8
+
9
+ 1. **Node.js** (v18.x or higher recommended)
10
+ 2. **npm** (comes bundled with Node)
11
+
12
+ ## Installation
13
+
14
+ You can install Obscura globally to invoke it directly from your terminal, or execute it directly using `npx`.
15
+
16
+ ### Global Installation
17
+
18
+ ```bash
19
+ npm install -g @anandaramr/obscura
20
+
21
+ ```
22
+
23
+ ### Direct Execution (Without Installation)
24
+
25
+ ```bash
26
+ npx @anandaramr/obscura [directory]
27
+
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ To spin up Obscura, pass the path to your media gallery folder as the default argument:
33
+
34
+ ```bash
35
+ obscura /path/to/your/media/gallery
36
+
37
+ ```
38
+
39
+ ### CLI Command Options
40
+
41
+ You can fully customize how the server binds, hosts, and reads from disk using flag configurations:
42
+
43
+ ```bash
44
+ Usage: obscura [options] [directory]
45
+
46
+ Lightweight self-hosted media gallery server for local networks
47
+
48
+ Arguments:
49
+ directory Directory to serve (default: ".")
50
+
51
+ Options:
52
+ -v, --version output the version number
53
+ -a, --address <ip> Address to bind to (default: "0.0.0.0")
54
+ -p, --port <number> Port to listen to (default: "4963")
55
+ --disk-concurrency <number> Maximum number of concurrent disk operations (default: "3")
56
+ -h, --help display help for command
57
+
58
+ ```
59
+
60
+ ## Environment Variables
61
+
62
+ Obscura natively honors standard system environment variables or values declared inside a local `.env` file at the root of the application directory:
63
+
64
+ | Environment Variable | Description |
65
+ | --- | --- |
66
+ | `DIRECTORY` | Absolute or relative path to target media directory |
67
+ | `ADDRESS` | Network IP address to bind server instance onto |
68
+ | `PORT` | Local network port to open for the server instance |
69
+ | `DISK_CONCURRENCY` | Maximum allowed parallel disk I/O operational ceiling
70
+
71
+ ## API
72
+
73
+ Obscura exposes a REST API for building custom clients. See [API.md](./API.md) for full documentation.
@@ -0,0 +1,14 @@
1
+ export declare const defaults: {
2
+ DIRECTORY: string;
3
+ THUMBS_DIR: string;
4
+ ADDRESS: string;
5
+ PORT: number;
6
+ THUMB_SIZE: number;
7
+ IMG_CACHE_THRESHOLD: number;
8
+ DISK_CONCURRENCY: number;
9
+ };
10
+ export declare function parsePort(p: string): number;
11
+ export declare function parseDiskConcurrency(conc: string): number;
12
+ export declare function parseDirectory(dir: string): string;
13
+ export declare function parseAddress(ip: string): string;
14
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,QAAQ;;;;;;;;CASpB,CAAA;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,UAMlC;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,UAMhD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,UAOzC;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,UAKtC"}
package/dist/config.js ADDED
@@ -0,0 +1,42 @@
1
+ import { InvalidArgumentError, InvalidOptionArgumentError } from "commander";
2
+ import path from "path";
3
+ import { validateDirectory } from "./file.js";
4
+ import net from "net";
5
+ export const defaults = {
6
+ DIRECTORY: ".",
7
+ THUMBS_DIR: "thumbs",
8
+ ADDRESS: "0.0.0.0",
9
+ PORT: 4963,
10
+ THUMB_SIZE: 400,
11
+ IMG_CACHE_THRESHOLD: 1024 * 1024,
12
+ DISK_CONCURRENCY: 3
13
+ };
14
+ export function parsePort(p) {
15
+ const port = Number(p);
16
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
17
+ throw new InvalidOptionArgumentError(`Port should be a number between 1 and 65535`);
18
+ }
19
+ return port;
20
+ }
21
+ export function parseDiskConcurrency(conc) {
22
+ const diskConcurrency = Number(conc);
23
+ if (!Number.isInteger(diskConcurrency) || diskConcurrency < 1) {
24
+ throw new InvalidOptionArgumentError(`Disk concurrency should be a number > 0`);
25
+ }
26
+ return diskConcurrency;
27
+ }
28
+ export function parseDirectory(dir) {
29
+ const galleryDir = path.resolve(process.cwd(), dir);
30
+ const { error } = validateDirectory(galleryDir);
31
+ if (error) {
32
+ throw new InvalidArgumentError(error);
33
+ }
34
+ return galleryDir;
35
+ }
36
+ export function parseAddress(ip) {
37
+ if (!ip || (!net.isIPv4(ip) && ip !== 'localhost')) {
38
+ throw new InvalidOptionArgumentError(`Invalid IP address \"${ip}\". Should be an IPv4 address`);
39
+ }
40
+ return ip;
41
+ }
42
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,0BAA0B,EAAE,MAAM,WAAW,CAAA;AAC5E,OAAO,IAAI,MAAM,MAAM,CAAA;AACvB,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAC7C,OAAO,GAAG,MAAM,KAAK,CAAA;AAErB,MAAM,CAAC,MAAM,QAAQ,GAAG;IACpB,SAAS,EAAE,GAAG;IACd,UAAU,EAAE,QAAQ;IACpB,OAAO,EAAE,SAAS;IAClB,IAAI,EAAE,IAAI;IAEV,UAAU,EAAE,GAAG;IACf,mBAAmB,EAAE,IAAI,GAAG,IAAI;IAChC,gBAAgB,EAAE,CAAC;CACtB,CAAA;AAED,MAAM,UAAU,SAAS,CAAC,CAAS;IAC/B,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;IACtB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,KAAK,EAAE,CAAC;QACtD,MAAM,IAAI,0BAA0B,CAAC,6CAA6C,CAAC,CAAA;IACvF,CAAC;IACD,OAAO,IAAI,CAAA;AACf,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC7C,MAAM,eAAe,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA;IACpC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,eAAe,CAAC,IAAI,eAAe,GAAG,CAAC,EAAE,CAAC;QAC5D,MAAM,IAAI,0BAA0B,CAAC,yCAAyC,CAAC,CAAA;IACnF,CAAC;IACD,OAAO,eAAe,CAAA;AAC1B,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,GAAW;IACtC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,GAAG,CAAC,CAAA;IACnD,MAAM,EAAE,KAAK,EAAE,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAA;IAC/C,IAAI,KAAK,EAAE,CAAC;QACR,MAAM,IAAI,oBAAoB,CAAC,KAAK,CAAC,CAAA;IACzC,CAAC;IACD,OAAO,UAAU,CAAA;AACrB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,EAAU;IACnC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,WAAW,CAAC,EAAE,CAAC;QACjD,MAAM,IAAI,0BAA0B,CAAC,wBAAwB,EAAE,+BAA+B,CAAC,CAAA;IACnG,CAAC;IACD,OAAO,EAAE,CAAA;AACb,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAkCtD,wBAAgB,iBAAiB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAc/E;AAED,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAqBtF;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,UAE5C"}
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAgCtD,wBAAgB,iBAAiB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAc/E;AAED,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAqBtF;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,UAE5C"}
package/dist/file.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
3
  import crypto from "crypto";
4
- import sharp from "sharp";
4
+ import libSharp from "./lib/lib-sharp.js";
5
5
  const IMAGE_EXTS = [
6
6
  ".jpg",
7
7
  ".jpeg",
@@ -20,8 +20,6 @@ const VIDEO_EXTS = [
20
20
  ".mkv",
21
21
  ".webm",
22
22
  ".m4v",
23
- ".ts",
24
- ".mts",
25
23
  ".m2ts",
26
24
  ];
27
25
  const ALWAYS_ANIMATED_EXTS = new Set([".gif"]);
@@ -66,7 +64,7 @@ export function generateHash(fullPath) {
66
64
  }
67
65
  async function isAnimated(filePath) {
68
66
  try {
69
- const metadata = await sharp(filePath).metadata();
67
+ const metadata = await libSharp(filePath).metadata();
70
68
  return (metadata.pages ?? 1) > 1;
71
69
  }
72
70
  catch (_) {
package/dist/file.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"file.js","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"file.js","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ import dotenv from "dotenv";
3
+ dotenv.config({ quiet: true });
4
+ import { Command } from "commander";
5
+ import manifest from "../package.json" with { type: "json" };
6
+ import { defaults, parseAddress, parseDirectory, parseDiskConcurrency, parsePort } from "./config.js";
7
+ import startServer from "./server.js";
8
+ const program = new Command();
9
+ program
10
+ .name("obscura")
11
+ .description("Lightweight self-hosted media gallery server for local networks")
12
+ .version(manifest.version, "-v, --version", "output the version number");
13
+ program
14
+ .argument("[directory]", "Directory to serve", process.env.DIRECTORY ?? defaults.DIRECTORY)
15
+ .option("-a, --address <ip>", "Address to bind to", process.env.ADDRESS ?? defaults.ADDRESS)
16
+ .option("-p, --port <number>", "Port to listen to", process.env.PORT ?? defaults.PORT.toString())
17
+ .option("--disk-concurrency <number>", "Maximum number of concurrent disk operations", process.env.DISK_CONCURRENCY ?? defaults.DISK_CONCURRENCY.toString())
18
+ .action(async (directory, options) => {
19
+ const galleryDir = parseDirectory(directory);
20
+ const port = parsePort(options.port);
21
+ const diskConcurrency = parseDiskConcurrency(options.diskConcurrency);
22
+ const address = parseAddress(options.address);
23
+ try {
24
+ const config = {
25
+ galleryDir: galleryDir,
26
+ address: address,
27
+ port: port,
28
+ thumbSize: defaults.THUMB_SIZE,
29
+ imgCacheThreshold: defaults.IMG_CACHE_THRESHOLD,
30
+ diskConcurrency: diskConcurrency
31
+ };
32
+ const close = await startServer(config);
33
+ process.on("SIGINT", async () => {
34
+ await close();
35
+ process.exit(0);
36
+ });
37
+ }
38
+ catch (error) {
39
+ console.error(`\x1b[31m[Obscura Startup Error]`);
40
+ console.error(`> ${error}\x1b[0m`);
41
+ process.exit(1);
42
+ }
43
+ });
44
+ program.parse(process.argv);
45
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,MAAM,MAAM,QAAQ,CAAA;AAC3B,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;AAE9B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,QAAQ,MAAM,iBAAiB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AAC5D,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACrG,OAAO,WAAW,MAAM,aAAa,CAAA;AAGrC,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;AAE7B,OAAO;KACF,IAAI,CAAC,SAAS,CAAC;KACf,WAAW,CAAC,iEAAiE,CAAC;KAC9E,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,eAAe,EAAE,2BAA2B,CAAC,CAAA;AAE5E,OAAO;KACF,QAAQ,CAAC,aAAa,EAAE,oBAAoB,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,QAAQ,CAAC,SAAS,CAAC;KAC1F,MAAM,CAAC,oBAAoB,EAAE,oBAAoB,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,QAAQ,CAAC,OAAO,CAAC;KAC3F,MAAM,CACH,qBAAqB,EACrB,mBAAmB,EACnB,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,CAC/C;KACA,MAAM,CACH,6BAA6B,EAC7B,8CAA8C,EAC9C,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CACvE;KACA,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE;IACjC,MAAM,UAAU,GAAG,cAAc,CAAC,SAAS,CAAC,CAAA;IAC5C,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACpC,MAAM,eAAe,GAAG,oBAAoB,CAAC,OAAO,CAAC,eAAe,CAAC,CAAA;IACrE,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IAE7C,IAAI,CAAC;QACD,MAAM,MAAM,GAAiB;YACzB,UAAU,EAAE,UAAU;YACtB,OAAO,EAAE,OAAO;YAChB,IAAI,EAAE,IAAI;YACV,SAAS,EAAE,QAAQ,CAAC,UAAU;YAC9B,iBAAiB,EAAE,QAAQ,CAAC,mBAAmB;YAC/C,eAAe,EAAE,eAAe;SACnC,CAAA;QAED,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAA;QAEvC,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;YAC5B,MAAM,KAAK,EAAE,CAAA;YACb,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACnB,CAAC,CAAC,CAAA;IACN,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAA;QAChD,OAAO,CAAC,KAAK,CAAC,KAAK,KAAK,SAAS,CAAC,CAAA;QAClC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACnB,CAAC;AACL,CAAC,CAAC,CAAA;AAEN,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA"}
@@ -0,0 +1,3 @@
1
+ import libSharp from "sharp";
2
+ export default libSharp;
3
+ //# sourceMappingURL=lib-sharp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lib-sharp.d.ts","sourceRoot":"","sources":["../../src/lib/lib-sharp.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,OAAO,CAAA;AAK5B,eAAe,QAAQ,CAAA"}
@@ -0,0 +1,5 @@
1
+ import libSharp from "sharp";
2
+ // Avoids keeping files open unnecessarily
3
+ libSharp.cache(false);
4
+ export default libSharp;
5
+ //# sourceMappingURL=lib-sharp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lib-sharp.js","sourceRoot":"","sources":["../../src/lib/lib-sharp.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,OAAO,CAAA;AAE5B,0CAA0C;AAC1C,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;AAErB,eAAe,QAAQ,CAAA"}
package/dist/server.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- #!/usr/bin/env node
2
- export {};
1
+ import type { ServerConfig } from "./types.js";
2
+ export default function startServer(config: ServerConfig): Promise<() => Promise<void>>;
3
3
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAoC,YAAY,EAAa,MAAM,YAAY,CAAA;AAe3F,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,MAAM,EAAE,YAAY,iBAgH3B,OAAO,CAAC,IAAI,CAAC,EAkCzC"}
package/dist/server.js CHANGED
@@ -1,148 +1,156 @@
1
- #!/usr/bin/env node
2
1
  import express from "express";
3
2
  import os from "os";
4
3
  import chokidar from "chokidar";
5
4
  import pLimit from "p-limit";
6
- import dotenv from "dotenv";
7
- dotenv.config({ quiet: true });
8
5
  import path from "path";
9
6
  import fs from "fs";
10
- import sharp from "sharp";
11
- sharp.cache(false);
12
- import { validateDirectory, parseFileMetadata, generateHash } from "./file.js";
13
- import { DEFAULT_PORT, DEFAULT_ADDRESS, DEFAULT_DIRECTORY, DEFAULT_THUMB_LIMIT, DEFAULT_THUMB_SIZE, DEFAULT_THUMB_THRESHOLD, } from "./defaults.js";
7
+ import { parseFileMetadata, generateHash } from "./file.js";
14
8
  import logger from "./logger.js";
15
9
  import { generateImageThumbnail, generateVideoThumbnail } from "./thumbnail.js";
16
10
  import { insertSorted } from "./utils.js";
17
- import { fileURLToPath } from 'url';
11
+ import { fileURLToPath } from "url";
12
+ import { defaults } from "./config.js";
18
13
  const __filename = fileURLToPath(import.meta.url);
19
14
  const PROJECT_ROOT = path.resolve(path.dirname(__filename), "..");
20
- const app = express();
21
- app.use(express.json());
22
- app.use(logger());
23
- app.use(express.static(path.join(PROJECT_ROOT, "public")));
24
- const GALLERY_DIR = path.resolve(process.cwd(), process.argv[2] || process.env.GALLERY_DIR || DEFAULT_DIRECTORY);
25
- const ADDRESS = process.argv[3] || process.env.ADDRESS || DEFAULT_ADDRESS;
26
- const PORT = parseInt(process.argv[4] || "") || parseInt(process.env.PORT || "") || DEFAULT_PORT;
27
- const THUMB_THRESHOLD = parseInt(process.env.THUMB_THRESHOLD || "") || DEFAULT_THUMB_THRESHOLD;
28
- const THUMB_SIZE = parseInt(process.env.THUMB_SIZE || "") || DEFAULT_THUMB_SIZE;
29
- const THUMB_LIMIT = parseInt(process.env.THUMB_LIMIT || "") || DEFAULT_THUMB_LIMIT;
30
- const THUMBS_DIR = path.join(PROJECT_ROOT, "thumbs");
31
- if (!fs.existsSync(THUMBS_DIR))
32
- fs.mkdirSync(THUMBS_DIR, { recursive: true });
33
- const limit = pLimit(THUMB_LIMIT);
34
- const { error } = validateDirectory(GALLERY_DIR);
35
- if (error) {
36
- console.error(`\x1b[31m[Obscura Startup Error]\x1b[0m ${error}`);
37
- console.error(`Please provide a valid media directory path.`);
38
- process.exit(1);
15
+ function createApp() {
16
+ const app = express();
17
+ app.use(express.json());
18
+ app.use(logger());
19
+ app.use(express.static(path.join(PROJECT_ROOT, "public")));
20
+ return app;
39
21
  }
40
- let filesMap = new Map();
41
- let sortedFiles = [];
42
- const watcher = chokidar.watch(GALLERY_DIR, {
43
- ignored: /(^|[\/\\])\../,
44
- persistent: true,
45
- ignoreInitial: false,
46
- });
47
- let clients = [];
48
- let isBooting = true;
49
- function broadcastToUsers(action, fileData) {
50
- clients.forEach((client) => {
51
- client.res.write(`data: ${JSON.stringify({ action, file: fileData })}\n\n`);
22
+ export default function startServer(config) {
23
+ const limit = pLimit(config.diskConcurrency);
24
+ const THUMBS_DIR = path.resolve(PROJECT_ROOT, defaults.THUMBS_DIR);
25
+ if (!fs.existsSync(THUMBS_DIR))
26
+ fs.mkdirSync(THUMBS_DIR, { recursive: true });
27
+ let filesMap = new Map();
28
+ let sortedFiles = [];
29
+ const watcher = chokidar.watch(config.galleryDir, {
30
+ ignored: /(^|[\/\\])\../,
31
+ persistent: true,
32
+ ignoreInitial: false
52
33
  });
53
- }
54
- watcher.on("add", async (filePath) => {
55
- const fileData = await parseFileMetadata(filePath);
56
- if (!fileData)
57
- return;
58
- const isExisting = filesMap.has(fileData.id);
59
- filesMap.set(fileData.id, fileData);
60
- if (isExisting)
61
- return;
62
- const { id, name, type, date, size, isAnimated } = fileData;
63
- const clientFileData = { id, name, type, date, size, isAnimated };
64
- insertSorted(sortedFiles, clientFileData, (file) => new Date(file.date).getTime(), (a, b) => b - a);
65
- if (isBooting)
66
- return;
67
- broadcastToUsers("add", clientFileData);
68
- });
69
- watcher.on("unlink", (filePath) => {
70
- const fileId = generateHash(filePath);
71
- if (filesMap.has(fileId)) {
72
- filesMap.delete(fileId);
73
- sortedFiles = sortedFiles.filter((file) => file.id !== fileId);
74
- broadcastToUsers("remove", { id: fileId });
75
- const thumbPath = path.join(THUMBS_DIR, `${fileId}.jpg`);
76
- fs.unlink(thumbPath, () => { });
34
+ let clients = [];
35
+ function broadcastToUsers(action, fileData) {
36
+ clients.forEach(client => {
37
+ client.res.write(`data: ${JSON.stringify({ action, file: fileData })}\n\n`);
38
+ });
77
39
  }
78
- });
79
- watcher.on("ready", () => {
80
- isBooting = false;
81
- });
82
- app.get("/api/files", (req, res) => {
83
- res.status(200).json(sortedFiles);
84
- });
85
- app.get("/api/files/:id", (req, res) => {
86
- const file = filesMap.get(req.params.id);
87
- if (!file)
88
- return res.sendStatus(404);
89
- res.sendFile(file.path);
90
- });
91
- app.get("/api/events", (req, res) => {
92
- res.setHeader("Content-Type", "text/event-stream");
93
- res.setHeader("Cache-Control", "no-cache");
94
- res.setHeader("Connection", "keep-alive");
95
- const clientId = Date.now();
96
- clients.push({ id: clientId, res });
97
- req.on("close", () => {
98
- clients = clients.filter((client) => client.id !== clientId);
40
+ let isBooting = true;
41
+ watcher.on("add", async (filePath) => {
42
+ const fileData = await parseFileMetadata(filePath);
43
+ if (!fileData)
44
+ return;
45
+ const isExisting = filesMap.has(fileData.id);
46
+ filesMap.set(fileData.id, fileData);
47
+ if (isExisting)
48
+ return;
49
+ const { id, name, type, date, size, isAnimated } = fileData;
50
+ const clientFileData = { id, name, type, date, size, isAnimated };
51
+ insertSorted(sortedFiles, clientFileData, file => new Date(file.date).getTime(), (a, b) => b - a);
52
+ if (isBooting)
53
+ return;
54
+ broadcastToUsers("add", clientFileData);
99
55
  });
100
- });
101
- app.get("/api/thumb/:id", async (req, res) => {
102
- const file = filesMap.get(req.params.id);
103
- if (!file)
104
- return res.sendStatus(404);
105
- if (shouldAvoidCaching(file)) {
106
- return res.sendFile(file.path);
107
- }
108
- const thumbPath = path.join(THUMBS_DIR, `${file.id}.jpg`);
109
- try {
110
- await limit(async () => {
111
- // check again after waiting in queue
112
- if (fs.existsSync(thumbPath))
56
+ watcher.on("unlink", filePath => {
57
+ const fileId = generateHash(filePath);
58
+ if (filesMap.has(fileId)) {
59
+ filesMap.delete(fileId);
60
+ sortedFiles = sortedFiles.filter(file => file.id !== fileId);
61
+ broadcastToUsers("remove", { id: fileId });
62
+ const thumbPath = getThumbPath(THUMBS_DIR, fileId);
63
+ fs.unlink(thumbPath, () => { });
64
+ }
65
+ });
66
+ watcher.on("ready", () => {
67
+ isBooting = false;
68
+ });
69
+ const app = createApp();
70
+ app.get("/api/files", (req, res) => {
71
+ res.status(200).json(sortedFiles);
72
+ });
73
+ app.get("/api/files/:id", (req, res) => {
74
+ const file = filesMap.get(req.params.id);
75
+ if (!file)
76
+ return res.sendStatus(404);
77
+ res.sendFile(file.path);
78
+ });
79
+ app.get("/api/events", (req, res) => {
80
+ res.setHeader("Content-Type", "text/event-stream");
81
+ res.setHeader("Cache-Control", "no-cache");
82
+ res.setHeader("Connection", "keep-alive");
83
+ res.flushHeaders();
84
+ const clientId = Date.now();
85
+ clients.push({ id: clientId, res });
86
+ req.on("close", () => {
87
+ clients = clients.filter(client => client.id !== clientId);
88
+ });
89
+ });
90
+ app.get("/api/thumb/:id", async (req, res) => {
91
+ const file = filesMap.get(req.params.id);
92
+ if (!file)
93
+ return res.sendStatus(404);
94
+ if (shouldAvoidCaching(file, config.imgCacheThreshold)) {
95
+ return res.sendFile(file.path);
96
+ }
97
+ const thumbPath = getThumbPath(THUMBS_DIR, file.id);
98
+ try {
99
+ await limit(async () => {
100
+ if (fs.existsSync(thumbPath))
101
+ return;
102
+ if (file.type === "image") {
103
+ await generateImageThumbnail(file, thumbPath, config.thumbSize);
104
+ }
105
+ else {
106
+ await generateVideoThumbnail(file, thumbPath, config.thumbSize);
107
+ }
108
+ });
109
+ res.sendFile(path.resolve(thumbPath));
110
+ }
111
+ catch (err) {
112
+ console.error(`Error while creating thumbnail: ${err}`);
113
+ res.sendStatus(500);
114
+ }
115
+ });
116
+ return new Promise((resolve, reject) => {
117
+ const server = app.listen(config.port, config.address, error => {
118
+ if (error) {
119
+ reject(error);
113
120
  return;
114
- if (file.type === "image") {
115
- await generateImageThumbnail(file, thumbPath, THUMB_SIZE);
116
121
  }
117
- else {
118
- await generateVideoThumbnail(file, thumbPath, THUMB_SIZE);
122
+ console.log(`Obscura running at ${config.address}:${config.port}`);
123
+ console.log(`Serving media from \x1b[36m${config.galleryDir}\x1b[0m\n`);
124
+ const isAllInterfaces = config.address === "0.0.0.0";
125
+ if (!isAllInterfaces) {
126
+ logAddress(config.address, config.port);
127
+ return;
119
128
  }
120
- });
121
- res.sendFile(path.resolve(thumbPath));
122
- }
123
- catch (err) {
124
- console.error(`Error while creating thumbnail: ${err}`);
125
- res.sendStatus(500);
126
- }
127
- });
128
- app.listen(PORT, ADDRESS, (error) => {
129
- if (error) {
130
- console.error(`\x1b[31m[Obscura Startup Error]\x1b[0m ${error.message}`);
131
- process.exit(1);
132
- }
133
- console.log(`Obscura running at ${ADDRESS}:${PORT}`);
134
- console.log(`Serving media from \x1b[36m${GALLERY_DIR}\x1b[0m\n`);
135
- const interfaces = os.networkInterfaces();
136
- Object.entries(interfaces).forEach(([name, addresses]) => {
137
- addresses
138
- ?.filter((addr) => addr.family === "IPv4")
139
- .forEach((addr) => {
140
- console.log(`- \x1b[36mhttp://${addr.address}:${PORT}\x1b[0m \t [${name}]`);
129
+ const interfaces = os.networkInterfaces();
130
+ Object.entries(interfaces).forEach(([name, addresses]) => {
131
+ addresses
132
+ ?.filter(addr => addr.family === "IPv4")
133
+ .forEach(addr => {
134
+ logAddress(addr.address, config.port, name);
135
+ });
136
+ });
137
+ console.log("\n");
138
+ resolve(() => {
139
+ return new Promise(res => {
140
+ watcher.close().then(() => server.close(() => res()));
141
+ });
142
+ });
141
143
  });
142
144
  });
143
- console.log("\n");
144
- });
145
- function shouldAvoidCaching(file) {
146
- return file.type == "image" && (file.size < THUMB_THRESHOLD && !file.isAnimated);
145
+ }
146
+ function logAddress(addr, port, name) {
147
+ const url = `http://${addr}:${port}`;
148
+ console.log(`- \x1b[36m${url.padEnd(30)}\x1b[0m ${name ? "[" + name + "]" : ""}`);
149
+ }
150
+ function getThumbPath(thumbsDir, fileId) {
151
+ return path.join(thumbsDir, `${fileId}.jpg`);
152
+ }
153
+ function shouldAvoidCaching(file, threshold) {
154
+ return file.type == "image" && file.size < threshold && !file.isAnimated;
147
155
  }
148
156
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/dist/thumbnail.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { execFile } from "child_process";
2
- import sharp from "sharp";
2
+ import libSharp from "./lib/lib-sharp.js";
3
3
  import ffmpeg from "ffmpeg-static";
4
4
  const ffmpegPath = typeof ffmpeg === "string" ? ffmpeg : ffmpeg.default;
5
5
  export function generateVideoThumbnail(file, thumbPath, thumbSize) {
@@ -23,7 +23,7 @@ export function generateVideoThumbnail(file, thumbPath, thumbSize) {
23
23
  });
24
24
  }
25
25
  export async function generateImageThumbnail(file, thumbPath, thumbSize) {
26
- await sharp(file.path, { animated: false, page: 0 })
26
+ await libSharp(file.path, { animated: false, page: 0 })
27
27
  .resize(thumbSize, thumbSize, { fit: "cover" })
28
28
  .jpeg({ quality: 80 })
29
29
  .toFile(thumbPath);
@@ -1 +1 @@
1
- {"version":3,"file":"thumbnail.js","sourceRoot":"","sources":["../src/thumbnail.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,MAAM,MAAM,eAAe,CAAA;AAElC,MAAM,UAAU,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,MAAc,CAAC,OAAO,CAAA;AAEhF,MAAM,UAAU,sBAAsB,CAAC,IAAkB,EAAE,SAAiB,EAAE,SAAiB;IAC3F,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACzC,QAAQ,CACJ,UAAU,EACV;YACI,IAAI;YACJ,IAAI,CAAC,IAAI;YACT,WAAW;YACX,GAAG;YACH,KAAK;YACL,SAAS,SAAS,IAAI,SAAS,8CAA8C,SAAS,IAAI,SAAS,QAAQ,SAAS,WAAW,SAAS,KAAK;YAC7I,MAAM;YACN,GAAG;YACH,SAAS;SACZ,EACD,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;YACtB,IAAI,GAAG,EAAE,CAAC;gBACN,OAAO,MAAM,CAAC,IAAI,KAAK,CAAC,iBAAiB,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;YAC5D,CAAC;YACD,OAAO,EAAE,CAAA;QACb,CAAC,CACJ,CAAA;IACL,CAAC,CAAC,CAAA;AACN,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,IAAkB,EAAE,SAAiB,EAAE,SAAiB;IACjG,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;SAC/C,MAAM,CAAC,SAAS,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;SAC9C,IAAI,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;SACrB,MAAM,CAAC,SAAS,CAAC,CAAA;AAC1B,CAAC"}
1
+ {"version":3,"file":"thumbnail.js","sourceRoot":"","sources":["../src/thumbnail.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,QAAQ,MAAM,oBAAoB,CAAA;AACzC,OAAO,MAAM,MAAM,eAAe,CAAA;AAElC,MAAM,UAAU,GAAG,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,MAAc,CAAC,OAAO,CAAA;AAEhF,MAAM,UAAU,sBAAsB,CAAC,IAAkB,EAAE,SAAiB,EAAE,SAAiB;IAC3F,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACzC,QAAQ,CACJ,UAAU,EACV;YACI,IAAI;YACJ,IAAI,CAAC,IAAI;YACT,WAAW;YACX,GAAG;YACH,KAAK;YACL,SAAS,SAAS,IAAI,SAAS,8CAA8C,SAAS,IAAI,SAAS,QAAQ,SAAS,WAAW,SAAS,KAAK;YAC7I,MAAM;YACN,GAAG;YACH,SAAS;SACZ,EACD,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;YACtB,IAAI,GAAG,EAAE,CAAC;gBACN,OAAO,MAAM,CAAC,IAAI,KAAK,CAAC,iBAAiB,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;YAC5D,CAAC;YACD,OAAO,EAAE,CAAA;QACb,CAAC,CACJ,CAAA;IACL,CAAC,CAAC,CAAA;AACN,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,IAAkB,EAAE,SAAiB,EAAE,SAAiB;IACjG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;SAClD,MAAM,CAAC,SAAS,EAAE,SAAS,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;SAC9C,IAAI,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;SACrB,MAAM,CAAC,SAAS,CAAC,CAAA;AAC1B,CAAC"}
package/dist/types.d.ts CHANGED
@@ -18,4 +18,12 @@ export interface Result<T, S> {
18
18
  result?: T;
19
19
  error?: S;
20
20
  }
21
+ export interface ServerConfig {
22
+ galleryDir: string;
23
+ address: string;
24
+ port: number;
25
+ thumbSize: number;
26
+ imgCacheThreshold: number;
27
+ diskConcurrency: number;
28
+ }
21
29
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAEvC,MAAM,WAAW,YAAY;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,IAAI,EAAE,IAAI,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,OAAO,CAAA;CACtB;AAED,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY,CAAC,CAAA;AAE5G,MAAM,WAAW,SAAS;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,QAAQ,CAAA;CAChB;AAED,MAAM,WAAW,MAAM,CAAC,CAAC,EAAE,CAAC;IACxB,MAAM,CAAC,EAAE,CAAC,CAAA;IACV,KAAK,CAAC,EAAE,CAAC,CAAA;CACZ"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAEvC,MAAM,WAAW,YAAY;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACvB,IAAI,EAAE,IAAI,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,OAAO,CAAA;CACtB;AAED,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,YAAY,CAAC,CAAA;AAE5G,MAAM,WAAW,SAAS;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,QAAQ,CAAA;CAChB;AAED,MAAM,WAAW,MAAM,CAAC,CAAC,EAAE,CAAC;IACxB,MAAM,CAAC,EAAE,CAAC,CAAA;IACV,KAAK,CAAC,EAAE,CAAC,CAAA;CACZ;AAED,MAAM,WAAW,YAAY;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAA;CAC1B"}
package/package.json CHANGED
@@ -1,37 +1,44 @@
1
- {
2
- "name": "@anandaramr/obscura",
3
- "version": "1.0.0",
4
- "description": "A lightweight local media streaming server",
5
- "main": "dist/server.js",
6
- "type": "module",
7
- "bin": {
8
- "obscura": "dist/server.js"
9
- },
10
- "files": [
11
- "dist/**/*",
12
- "public/**/*"
13
- ],
14
- "scripts": {
15
- "dev": "tsx watch src/server.ts",
16
- "build": "tsc",
17
- "start": "node dist/server.js",
18
- "prepublishOnly": "npm run build"
19
- },
20
- "dependencies": {
21
- "chokidar": "^5.0.0",
22
- "dotenv": "^17.4.2",
23
- "express": "^5.2.1",
24
- "ffmpeg-static": "^5.3.0",
25
- "morgan": "^1.10.1",
26
- "p-limit": "^7.3.0",
27
- "sharp": "^0.34.5"
28
- },
29
- "devDependencies": {
30
- "@types/express": "^5.0.6",
31
- "@types/morgan": "^1.9.10",
32
- "@types/node": "^25.9.1",
33
- "@types/sharp": "^0.31.1",
34
- "tsx": "^4.22.3",
35
- "typescript": "^6.0.3"
36
- }
37
- }
1
+ {
2
+ "name": "@anandaramr/obscura",
3
+ "version": "1.1.0",
4
+ "description": "A lightweight local media streaming server",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/anandaramr/obscura.git"
10
+ },
11
+ "bin": {
12
+ "obscura": "dist/index.js"
13
+ },
14
+ "files": [
15
+ "dist/**/*",
16
+ "public/**/*",
17
+ "API.md",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "dev": "tsx watch src/index.ts",
22
+ "build": "tsc",
23
+ "start": "node dist/index.js",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "dependencies": {
27
+ "chokidar": "^5.0.0",
28
+ "commander": "^14.0.3",
29
+ "dotenv": "^17.4.2",
30
+ "express": "^5.2.1",
31
+ "ffmpeg-static": "^5.3.0",
32
+ "morgan": "^1.10.1",
33
+ "p-limit": "^7.3.0",
34
+ "sharp": "^0.34.5"
35
+ },
36
+ "devDependencies": {
37
+ "@types/express": "^5.0.6",
38
+ "@types/morgan": "^1.9.10",
39
+ "@types/node": "^25.9.1",
40
+ "@types/sharp": "^0.31.1",
41
+ "tsx": "^4.22.3",
42
+ "typescript": "^6.0.3"
43
+ }
44
+ }
package/public/app.js CHANGED
@@ -2,20 +2,18 @@ let files = []
2
2
  let isShuffled = false
3
3
  let elementMap = new Map()
4
4
 
5
- function onVideoPlay(fileId) {
6
- clearTimeout(cacheTTLMap.get(fileId))
7
- cacheTTLMap.delete(fileId)
8
- }
9
-
10
- function onVideoPause(fileId, vid) {
11
- const timer = setTimeout(() => {
12
- vid.removeAttribute('src')
13
- vid.load()
14
- cacheTTLMap.delete(fileId)
15
- }, VIDEO_CACHE_TTL)
5
+ const VIDEO_CACHE_TTL = 60 * 1000
6
+ let cacheTTLMap = new Map()
16
7
 
17
- cacheTTLMap.set(fileId, timer)
18
- }
8
+ let retryQueue = []
9
+ window.addEventListener('connected', () => {
10
+ if (retryQueue.length) {
11
+ for (const retry of retryQueue) {
12
+ retry()
13
+ }
14
+ retryQueue = []
15
+ }
16
+ })
19
17
 
20
18
  init()
21
19
 
@@ -23,7 +21,7 @@ async function init() {
23
21
  const res = await fetch('/api/files')
24
22
  files = await res.json()
25
23
 
26
- params = new URLSearchParams(window.location.search)
24
+ const params = new URLSearchParams(window.location.search)
27
25
  isShuffled = params.has('shuffle')
28
26
  const fileList = isShuffled ? shuffleArray(files) : files
29
27
 
@@ -53,6 +51,10 @@ eventSource.onmessage = evt => {
53
51
  }
54
52
  }
55
53
 
54
+ eventSource.onopen = () => {
55
+ window.dispatchEvent(new Event('connected'))
56
+ }
57
+
56
58
  let previewWindow = []
57
59
  const observer = new IntersectionObserver(
58
60
  entries => {
@@ -69,30 +71,6 @@ const observer = new IntersectionObserver(
69
71
  { threshold: 1 }
70
72
  )
71
73
 
72
- let currentPreview = null
73
- function onVisible(preview) {
74
- const vid = preview.getElementsByClassName('video-preview')[0]
75
- previewWindow.push(preview)
76
- previewWindow.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top)
77
-
78
- vid.onended = evt => {
79
- const idx = previewWindow.indexOf(preview)
80
- stopMobilePreview(idx)
81
- startMobilePreview((idx + 1) % previewWindow.length)
82
- }
83
-
84
- if (!currentPreview) startMobilePreview(0)
85
- }
86
-
87
- function onEndOfVisibility(preview) {
88
- const idx = previewWindow.indexOf(preview)
89
- const isCurrentlyPlaying = currentPreview == preview
90
-
91
- stopMobilePreview(idx)
92
- previewWindow.splice(idx, 1)
93
- if (isCurrentlyPlaying && previewWindow.length) startMobilePreview(idx % previewWindow.length)
94
- }
95
-
96
74
  function insertGridItem(file, grid, prepend = false) {
97
75
  const preview = document.createElement('a')
98
76
 
@@ -104,6 +82,13 @@ function insertGridItem(file, grid, prepend = false) {
104
82
  media.src = `/api/thumb/${file.id}`
105
83
  media.loading = 'lazy'
106
84
  media.className = file.type === 'image' ? 'img' : 'video'
85
+ media.onerror = () => {
86
+ if (!isServerReachable()) {
87
+ retryQueue.push(() => {
88
+ media.src = media.src
89
+ })
90
+ }
91
+ }
107
92
  preview.appendChild(media)
108
93
 
109
94
  if (file.type != 'image') {
@@ -138,7 +123,7 @@ function insertGridItem(file, grid, prepend = false) {
138
123
  } else if (file.isAnimated) {
139
124
  const icon = document.createElement('span')
140
125
  icon.className = 'live-indicator'
141
- icon.innerText = "LIVE"
126
+ icon.innerText = 'LIVE'
142
127
  preview.appendChild(icon)
143
128
 
144
129
  preview.onmouseenter = () => {
@@ -146,7 +131,7 @@ function insertGridItem(file, grid, prepend = false) {
146
131
  media.src = `/api/files/${file.id}`
147
132
  }
148
133
  }
149
-
134
+
150
135
  preview.onmouseleave = () => {
151
136
  if (!isMobileDevice()) {
152
137
  media.src = `/api/thumb/${file.id}`
@@ -164,8 +149,67 @@ function insertGridItem(file, grid, prepend = false) {
164
149
  elementMap.set(file.id, preview)
165
150
  }
166
151
 
167
- const VIDEO_CACHE_TTL = 60 * 1000
168
- let cacheTTLMap = new Map()
152
+ function isServerReachable() {
153
+ return eventSource.readyState === EventSource.OPEN
154
+ }
155
+
156
+ function onVideoPlay(fileId) {
157
+ clearTimeout(cacheTTLMap.get(fileId))
158
+ cacheTTLMap.delete(fileId)
159
+ }
160
+
161
+ function onVideoPause(fileId, vid) {
162
+ // Allow browser to free memory if the video hasn't been
163
+ // played for a specified amount of time
164
+ const timer = setTimeout(() => {
165
+ vid.removeAttribute('src')
166
+ vid.load()
167
+ cacheTTLMap.delete(fileId)
168
+ }, VIDEO_CACHE_TTL)
169
+
170
+ cacheTTLMap.set(fileId, timer)
171
+ }
172
+
173
+ let currentPreview = null
174
+ function onVisible(preview) {
175
+ const vid = preview.getElementsByClassName('video-preview')[0]
176
+ if (!vid) return
177
+
178
+ addToPreviewWindow(preview)
179
+ vid.onended = evt => {
180
+ const idx = previewWindow.indexOf(preview)
181
+ if (previewWindow.length == 1) {
182
+ vid.play().catch(err => console.log(`Play interrupted: ${err}`))
183
+ return
184
+ }
185
+
186
+ stopMobilePreview(idx)
187
+ startMobilePreview((idx + 1) % previewWindow.length)
188
+ }
189
+
190
+ if (!currentPreview) startMobilePreview(0)
191
+ }
192
+
193
+ const getPosition = element => element.getBoundingClientRect().top
194
+ function addToPreviewWindow(preview) {
195
+ // inserts elements sorted according to their relative position
196
+ // ASSUMES elements are added only during scroll and no elements
197
+ // would be added between
198
+ if (previewWindow.length && getPosition(preview) < getPosition(previewWindow[0])) {
199
+ previewWindow.unshift(preview)
200
+ } else {
201
+ previewWindow.push(preview)
202
+ }
203
+ }
204
+
205
+ function onEndOfVisibility(preview) {
206
+ const idx = previewWindow.indexOf(preview)
207
+ const isCurrentlyPlaying = currentPreview == preview
208
+
209
+ stopMobilePreview(idx)
210
+ previewWindow.splice(idx, 1)
211
+ if (isCurrentlyPlaying && previewWindow.length) startMobilePreview(idx % previewWindow.length)
212
+ }
169
213
 
170
214
  function stopMobilePreview(idx) {
171
215
  const preview = previewWindow[idx]
@@ -194,7 +238,7 @@ function startMobilePreview(idx) {
194
238
  function stopVideoPreview(vid, id, media, icon) {
195
239
  vid.pause()
196
240
  onVideoPause(id, vid)
197
-
241
+
198
242
  vid.classList.add('fade')
199
243
  media.classList.remove('fade')
200
244
  icon.classList.remove('blink')
@@ -212,7 +256,7 @@ function startVideoPreview(vid, id, media, icon) {
212
256
  { once: true }
213
257
  )
214
258
 
215
- if (!vid.src) {
259
+ if (needsVideoLoading(vid)) {
216
260
  vid.src = `/api/files/${id}`
217
261
  vid.load()
218
262
  }
@@ -220,11 +264,15 @@ function startVideoPreview(vid, id, media, icon) {
220
264
  vid.classList.remove('fade')
221
265
  media.classList.add('fade')
222
266
 
223
- vid.currentTime = 0
224
267
  onVideoPlay(id)
268
+ vid.currentTime = 0
225
269
  vid.play().catch(err => console.log('Play interrupted:', err))
226
270
  }
227
271
 
272
+ function needsVideoLoading(vid) {
273
+ return !vid.src || vid.networkState === HTMLMediaElement.NETWORK_NO_SOURCE || vid.error
274
+ }
275
+
228
276
  function isMobileDevice() {
229
277
  return !window.matchMedia('(min-width: 769px)').matches
230
278
  }