@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 +93 -0
- package/README.md +72 -2
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +42 -0
- package/dist/config.js.map +1 -0
- package/dist/file.d.ts.map +1 -1
- package/dist/file.js +2 -4
- package/dist/file.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/lib-sharp.d.ts +3 -0
- package/dist/lib/lib-sharp.d.ts.map +1 -0
- package/dist/lib/lib-sharp.js +5 -0
- package/dist/lib/lib-sharp.js.map +1 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +134 -126
- package/dist/server.js.map +1 -1
- package/dist/thumbnail.js +2 -2
- package/dist/thumbnail.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +44 -37
- package/public/app.js +93 -45
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
|
|
1
|
+
# Obscura
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
package/dist/config.d.ts
ADDED
|
@@ -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"}
|
package/dist/file.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
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
|
|
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":"AAEA,OAAO,IAAI,MAAM,MAAM,CAAA;AACvB,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,MAAM,MAAM,QAAQ,CAAA;AAC3B,OAAO,
|
|
1
|
+
{"version":3,"file":"file.js","sourceRoot":"","sources":["../src/file.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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
|
-
|
|
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
|
package/dist/server.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
21
|
-
app
|
|
22
|
-
app.use(
|
|
23
|
-
app.use(
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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("
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"
|
|
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
|
|
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
|
|
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);
|
package/dist/thumbnail.js.map
CHANGED
|
@@ -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,
|
|
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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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.
|
|
4
|
-
"description": "A lightweight local media streaming server",
|
|
5
|
-
"main": "dist/
|
|
6
|
-
"type": "module",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
168
|
-
|
|
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 (
|
|
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
|
}
|