lucille-node 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.md +31 -0
- package/STORAGE.md +419 -0
- package/data/lucille/50a84a5a-6b2b-492e-b279-61eddb392e34_video:test-video-1773549/834/196 +1 -0
- package/data/lucille/dfa790d7-c4f5-43a9-b7ff-f166f0f344c9_video:test-video-1773549/541/230 +1 -0
- package/data/lucille/k/eys +1 -0
- package/data/lucille/pubKey_02b0071b6d90ce7598eabdcfa0596f95359799fd413e300dcbdca823095a/4cf/884 +1 -0
- package/data/lucille/pubKey_030e4e2f145c4b68863ca42868e08b1559c3c9c10774e61c3f4ef9766d0c/df7/e30 +1 -0
- package/data/lucille/user_50a84a5a-6b2b-492e-b279-61eddb/392/e34 +1 -0
- package/data/lucille/user_dfa790d7-c4f5-43a9-b7ff-f166f0/f34/4c9 +1 -0
- package/data/lucille/videos_50a84a5a-6b2b-492e-b279-61eddb/392/e34 +1 -0
- package/data/lucille/videos_dfa790d7-c4f5-43a9-b7ff-f166f0/f34/4c9 +1 -0
- package/integration-test.js +346 -0
- package/lucille.js +498 -0
- package/package.json +29 -0
- package/src/config.js +104 -0
- package/src/persistence/client.js +83 -0
- package/src/persistence/db.js +105 -0
- package/src/seeder.js +112 -0
- package/src/spaces.js +68 -0
- package/src/tracker.js +49 -0
- package/templates/player.html +236 -0
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Lucille
|
|
2
|
+
|
|
3
|
+
Named for the incomporable Lucille Ball, lucille is a part of the 4's Entertainment System providing for streaming, and downloadable video.
|
|
4
|
+
It utilizes bittorrent via the webtorrent protocol to enable P2P capabilites to save bandwidth, and provide scalability that other distributed systems cannot while simultaneously saving on bandwidth costs obviating the need for ad-supported revenue.
|
|
5
|
+
The point is for it to get so popular it breaks.
|
|
6
|
+
Then we'll know we're doing something right.
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
This first iteration of lucille is built on Digital Ocean infrastructure utilizing the following stack:
|
|
11
|
+
|
|
12
|
+
-------------
|
|
13
|
+
|| Storage ||
|
|
14
|
+
|| Seeder ||
|
|
15
|
+
|| Tracker ||
|
|
16
|
+
|| Viewer ||
|
|
17
|
+
-------------
|
|
18
|
+
|
|
19
|
+
If you're unfamiliar torrents, no worries!
|
|
20
|
+
All they mean is that while you're streaming your video, you're enabling others to stream it as well.
|
|
21
|
+
This happens seemlessly for you in the background, and lucille doesn't track whether you do or don't, so feel free to turn it off if you'd like.
|
|
22
|
+
|
|
23
|
+
### Video posters
|
|
24
|
+
|
|
25
|
+
Posters will likely interface with lucille through a client application that will handle the upload and storage of their video.
|
|
26
|
+
The first such application will be part of the shoppe plugin for the federated wiki, and can be found here: https://github.com/planet-nine-app/wiki-plugin-shoppe.
|
|
27
|
+
|
|
28
|
+
### Video watchers
|
|
29
|
+
|
|
30
|
+
Watchers will likewise interface through a client application, and the first one will probably be at the main lucille hub, which is tbd, or in fed wikis as well.
|
|
31
|
+
We shall see.
|
package/STORAGE.md
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
# Storage and Seeding: Build Guide
|
|
2
|
+
|
|
3
|
+
This covers the two backend Node services you need before the wiki and frontend
|
|
4
|
+
can do anything useful: the DO Spaces storage layer and the WebTorrent seeder +
|
|
5
|
+
tracker. Both are designed to run as long-lived fedwiki plugin processes.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Part 1 — DO Spaces (Storage)
|
|
10
|
+
|
|
11
|
+
### What it does
|
|
12
|
+
|
|
13
|
+
Spaces is S3-compatible object storage. It holds the raw video files and
|
|
14
|
+
optionally serves them through a built-in CDN. The seeder pulls from Spaces
|
|
15
|
+
when it first seeds a file; after that, WebTorrent peers take over the majority
|
|
16
|
+
of delivery.
|
|
17
|
+
|
|
18
|
+
### Setup
|
|
19
|
+
|
|
20
|
+
Create a Space in the DigitalOcean control panel. Pick the region closest to
|
|
21
|
+
your users. Enable the CDN endpoint — you'll use this URL to serve the initial
|
|
22
|
+
seed bytes cheaply before P2P kicks in.
|
|
23
|
+
|
|
24
|
+
Generate a Spaces access key under **API → Spaces Keys**. You'll get an access
|
|
25
|
+
key ID and a secret. Store both as environment variables — never in code.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
DO_SPACES_KEY=your_access_key
|
|
29
|
+
DO_SPACES_SECRET=your_secret_key
|
|
30
|
+
DO_SPACES_REGION=nyc3
|
|
31
|
+
DO_SPACES_BUCKET=your-bucket-name
|
|
32
|
+
DO_SPACES_CDN_ENDPOINT=https://your-bucket.nyc3.cdn.digitaloceanspaces.com
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Client setup
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
// spaces.js
|
|
45
|
+
const { S3Client, PutObjectCommand, GetObjectCommand,
|
|
46
|
+
ListObjectsV2Command } = require('@aws-sdk/client-s3')
|
|
47
|
+
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner')
|
|
48
|
+
|
|
49
|
+
const client = new S3Client({
|
|
50
|
+
endpoint: `https://${process.env.DO_SPACES_REGION}.digitaloceanspaces.com`,
|
|
51
|
+
region: process.env.DO_SPACES_REGION,
|
|
52
|
+
credentials: {
|
|
53
|
+
accessKeyId: process.env.DO_SPACES_KEY,
|
|
54
|
+
secretAccessKey: process.env.DO_SPACES_SECRET
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const BUCKET = process.env.DO_SPACES_BUCKET
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Uploading a video
|
|
62
|
+
|
|
63
|
+
Stream the file into Spaces rather than buffering the whole thing in memory.
|
|
64
|
+
For large files you'll want multipart upload, but the SDK handles that
|
|
65
|
+
automatically when you pass a stream body.
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
const fs = require('fs')
|
|
69
|
+
|
|
70
|
+
async function uploadVideo(localPath, key) {
|
|
71
|
+
const stream = fs.createReadStream(localPath)
|
|
72
|
+
const stat = fs.statSync(localPath)
|
|
73
|
+
|
|
74
|
+
await client.send(new PutObjectCommand({
|
|
75
|
+
Bucket: BUCKET,
|
|
76
|
+
Key: key, // e.g. 'videos/my-film.mp4'
|
|
77
|
+
Body: stream,
|
|
78
|
+
ContentLength: stat.size,
|
|
79
|
+
ContentType: 'video/mp4',
|
|
80
|
+
ACL: 'private' // keep private; serve via presigned URL or CDN
|
|
81
|
+
}))
|
|
82
|
+
|
|
83
|
+
console.log(`Uploaded ${key} (${(stat.size / 1e9).toFixed(2)} GB)`)
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Generating a presigned download URL
|
|
88
|
+
|
|
89
|
+
The auth plugin will call this before handing the viewer a magnet link. The
|
|
90
|
+
presigned URL lets the seeder fetch the file without making it fully public.
|
|
91
|
+
Expiry of 1 hour is plenty — the seeder only needs it once at startup.
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
async function presignedGet(key, expiresIn = 3600) {
|
|
95
|
+
const command = new GetObjectCommand({ Bucket: BUCKET, Key: key })
|
|
96
|
+
return getSignedUrl(client, command, { expiresIn })
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Listing available videos
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
async function listVideos(prefix = 'videos/') {
|
|
104
|
+
const response = await client.send(new ListObjectsV2Command({
|
|
105
|
+
Bucket: BUCKET,
|
|
106
|
+
Prefix: prefix
|
|
107
|
+
}))
|
|
108
|
+
return (response.Contents || []).map(obj => ({
|
|
109
|
+
key: obj.Key,
|
|
110
|
+
size: obj.Size,
|
|
111
|
+
lastModified: obj.LastModified
|
|
112
|
+
}))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { uploadVideo, presignedGet, listVideos }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### CDN delivery
|
|
119
|
+
|
|
120
|
+
Once a file is in Spaces, its CDN URL is:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
https://<bucket>.<region>.cdn.digitaloceanspaces.com/<key>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
You can use this CDN URL directly as the download source when creating a
|
|
127
|
+
torrent. The CDN absorbs the initial cold-start bandwidth before P2P takes
|
|
128
|
+
over, and it caches globally so the seeder Droplet's egress bill stays low.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Part 2 — WebTorrent Seeder
|
|
133
|
+
|
|
134
|
+
### What it does
|
|
135
|
+
|
|
136
|
+
A long-lived Node process that seeds every video file, maintains an in-memory
|
|
137
|
+
torrent registry, and exposes a small HTTP API so the auth plugin can get
|
|
138
|
+
magnet links without spawning its own WebTorrent client.
|
|
139
|
+
|
|
140
|
+
### Install
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npm install webtorrent express
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### The seeder process
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
// seeder.js
|
|
150
|
+
const WebTorrent = require('webtorrent')
|
|
151
|
+
const express = require('express')
|
|
152
|
+
const https = require('https')
|
|
153
|
+
const fs = require('fs')
|
|
154
|
+
const path = require('path')
|
|
155
|
+
const os = require('os')
|
|
156
|
+
|
|
157
|
+
const { presignedGet, listVideos } = require('./spaces')
|
|
158
|
+
|
|
159
|
+
const client = new WebTorrent()
|
|
160
|
+
const app = express()
|
|
161
|
+
app.use(express.json())
|
|
162
|
+
|
|
163
|
+
// Map of Spaces key → torrent instance
|
|
164
|
+
const torrents = new Map()
|
|
165
|
+
|
|
166
|
+
// Download a file from a URL into a temp directory and return the local path
|
|
167
|
+
function downloadToTemp(url, filename) {
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
const dest = path.join(os.tmpdir(), filename)
|
|
170
|
+
// Skip if already cached locally
|
|
171
|
+
if (fs.existsSync(dest)) return resolve(dest)
|
|
172
|
+
|
|
173
|
+
const file = fs.createWriteStream(dest)
|
|
174
|
+
https.get(url, res => {
|
|
175
|
+
res.pipe(file)
|
|
176
|
+
file.on('finish', () => file.close(() => resolve(dest)))
|
|
177
|
+
}).on('error', err => {
|
|
178
|
+
fs.unlink(dest, () => {})
|
|
179
|
+
reject(err)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Seed a single video by its Spaces key
|
|
185
|
+
async function seedVideo(key) {
|
|
186
|
+
if (torrents.has(key)) return torrents.get(key)
|
|
187
|
+
|
|
188
|
+
console.log(`Seeding: ${key}`)
|
|
189
|
+
const url = await presignedGet(key)
|
|
190
|
+
const filename = path.basename(key)
|
|
191
|
+
const localPath = await downloadToTemp(url, filename)
|
|
192
|
+
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
client.seed(localPath, {
|
|
195
|
+
// Point peers at your tracker
|
|
196
|
+
announce: [process.env.TRACKER_URL || 'ws://localhost:8000']
|
|
197
|
+
}, torrent => {
|
|
198
|
+
torrents.set(key, torrent)
|
|
199
|
+
console.log(`Seeding ${filename} — magnet: ${torrent.magnetURI}`)
|
|
200
|
+
resolve(torrent)
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Seed everything in Spaces at startup
|
|
206
|
+
async function seedAll() {
|
|
207
|
+
const videos = await listVideos()
|
|
208
|
+
for (const video of videos) {
|
|
209
|
+
await seedVideo(video.key).catch(err =>
|
|
210
|
+
console.error(`Failed to seed ${video.key}:`, err.message)
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// API routes consumed by the auth plugin
|
|
216
|
+
|
|
217
|
+
// GET /magnet?key=videos/my-film.mp4
|
|
218
|
+
app.get('/magnet', async (req, res) => {
|
|
219
|
+
const { key } = req.query
|
|
220
|
+
if (!key) return res.status(400).json({ error: 'key required' })
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const torrent = await seedVideo(key)
|
|
224
|
+
res.json({
|
|
225
|
+
magnetURI: torrent.magnetURI,
|
|
226
|
+
infoHash: torrent.infoHash,
|
|
227
|
+
name: torrent.name
|
|
228
|
+
})
|
|
229
|
+
} catch (err) {
|
|
230
|
+
res.status(500).json({ error: err.message })
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// GET /status — health check and active torrent list
|
|
235
|
+
app.get('/status', (req, res) => {
|
|
236
|
+
const active = []
|
|
237
|
+
for (const [key, torrent] of torrents.entries()) {
|
|
238
|
+
active.push({
|
|
239
|
+
key,
|
|
240
|
+
infoHash: torrent.infoHash,
|
|
241
|
+
peers: torrent.numPeers,
|
|
242
|
+
uploadSpeed: torrent.uploadSpeed,
|
|
243
|
+
downloaded: torrent.downloaded
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
res.json({ torrents: active, totalPeers: client.torrents.reduce((n, t) => n + t.numPeers, 0) })
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
// POST /seed — add a newly uploaded video without restarting
|
|
250
|
+
app.post('/seed', async (req, res) => {
|
|
251
|
+
const { key } = req.body
|
|
252
|
+
if (!key) return res.status(400).json({ error: 'key required' })
|
|
253
|
+
try {
|
|
254
|
+
const torrent = await seedVideo(key)
|
|
255
|
+
res.json({ magnetURI: torrent.magnetURI })
|
|
256
|
+
} catch (err) {
|
|
257
|
+
res.status(500).json({ error: err.message })
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const PORT = process.env.SEEDER_PORT || 7000
|
|
262
|
+
app.listen(PORT, async () => {
|
|
263
|
+
console.log(`Seeder API listening on port ${PORT}`)
|
|
264
|
+
await seedAll()
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// Graceful shutdown — flush torrents before exit
|
|
268
|
+
process.on('SIGTERM', () => {
|
|
269
|
+
console.log('Shutting down seeder...')
|
|
270
|
+
client.destroy(() => process.exit(0))
|
|
271
|
+
})
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Notes on the temp cache
|
|
275
|
+
|
|
276
|
+
The seeder downloads each file from Spaces once and caches it in `/tmp`. On
|
|
277
|
+
a 7TB Storage-Optimized Droplet you could cache everything locally and skip
|
|
278
|
+
the Spaces download on restart entirely — just point `client.seed()` at a
|
|
279
|
+
permanent local path. For smaller Droplets, the tmpdir approach is fine; the
|
|
280
|
+
file is re-downloaded from Spaces on each cold start.
|
|
281
|
+
|
|
282
|
+
If your library is large and cold starts are slow, pre-warm the cache on
|
|
283
|
+
Droplet boot using a startup script that calls `POST /seed` for each key.
|
|
284
|
+
|
|
285
|
+
### Environment variables
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
TRACKER_URL=ws://your-tracker-host:8000
|
|
289
|
+
SEEDER_PORT=7000
|
|
290
|
+
# Plus the DO_SPACES_* vars from Part 1
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Part 3 — Tracker / Signaling Server
|
|
296
|
+
|
|
297
|
+
### What it does
|
|
298
|
+
|
|
299
|
+
A WebTorrent-compatible tracker that brokers peer discovery. Viewers register
|
|
300
|
+
their info-hash here and the tracker tells them who else has the same torrent.
|
|
301
|
+
This is a thin WebSocket server — it does no streaming itself.
|
|
302
|
+
|
|
303
|
+
### Install
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
npm install bittorrent-tracker
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### The tracker process
|
|
310
|
+
|
|
311
|
+
```js
|
|
312
|
+
// tracker.js
|
|
313
|
+
const Tracker = require('bittorrent-tracker').Server
|
|
314
|
+
|
|
315
|
+
const server = new Tracker({
|
|
316
|
+
udp: false, // browsers can't speak UDP; WebRTC/WS only
|
|
317
|
+
http: true, // fallback for non-WebSocket clients
|
|
318
|
+
ws: true, // WebRTC signaling over WebSocket — the main path
|
|
319
|
+
stats: true // enables the /stats endpoint
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
server.on('error', err => console.error('Tracker error:', err.message))
|
|
323
|
+
|
|
324
|
+
server.on('warning', err => console.warn('Tracker warning:', err.message))
|
|
325
|
+
|
|
326
|
+
server.on('listening', () => {
|
|
327
|
+
const wsAddr = server.ws.address()
|
|
328
|
+
const httpAddr = server.http.address()
|
|
329
|
+
console.log(`Tracker WS listening on ws://0.0.0.0:${wsAddr.port}`)
|
|
330
|
+
console.log(`Tracker HTTP listening on http://0.0.0.0:${httpAddr.port}`)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
// Optional: log peer join/leave for debugging
|
|
334
|
+
server.on('start', (addr, params) => {
|
|
335
|
+
console.log(`Peer joined infoHash=${params.info_hash.toString('hex').slice(0,8)}`)
|
|
336
|
+
})
|
|
337
|
+
server.on('stop', (addr, params) => {
|
|
338
|
+
console.log(`Peer left infoHash=${params.info_hash.toString('hex').slice(0,8)}`)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
const PORT = process.env.TRACKER_PORT || 8000
|
|
342
|
+
server.listen(PORT)
|
|
343
|
+
|
|
344
|
+
process.on('SIGTERM', () => server.close(() => process.exit(0)))
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
That's the entire tracker. The `bittorrent-tracker` package does the rest —
|
|
348
|
+
peer list management, announce intervals, scrape responses.
|
|
349
|
+
|
|
350
|
+
### Pointing the seeder at the tracker
|
|
351
|
+
|
|
352
|
+
In `seeder.js` above the `announce` array already reads from `TRACKER_URL`.
|
|
353
|
+
Set it to your tracker's public address:
|
|
354
|
+
|
|
355
|
+
```
|
|
356
|
+
TRACKER_URL=ws://your-tracker-droplet-ip:8000
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Viewers pass the same URL via the magnet link's `&tr=` parameter, which the
|
|
360
|
+
auth plugin appends before returning the magnet URI to the browser.
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Part 4 — Wiring it together
|
|
365
|
+
|
|
366
|
+
### Request flow at stream time
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
viewer pays → auth plugin → GET /magnet?key=videos/film.mp4 → seeder API
|
|
370
|
+
→ seeder returns magnetURI (with tracker URL embedded)
|
|
371
|
+
→ auth plugin returns magnetURI to viewer
|
|
372
|
+
→ viewer's WebTorrent (browser) opens magnet
|
|
373
|
+
→ connects to tracker → discovers seeder + other peers
|
|
374
|
+
→ streams from peer mesh, seeder fills gaps
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Startup order
|
|
378
|
+
|
|
379
|
+
1. Tracker starts first (seeder needs its URL)
|
|
380
|
+
2. Seeder starts, seeds all Spaces content, registers with tracker
|
|
381
|
+
3. Auth plugin starts, ready to serve magnet links
|
|
382
|
+
|
|
383
|
+
### Running as services
|
|
384
|
+
|
|
385
|
+
Both processes are long-lived. On a plain Droplet, use `pm2`:
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
npm install -g pm2
|
|
389
|
+
pm2 start tracker.js --name tracker
|
|
390
|
+
pm2 start seeder.js --name seeder
|
|
391
|
+
pm2 save
|
|
392
|
+
pm2 startup # generates the systemd hook so they survive reboots
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Firewall rules (ufw)
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
ufw allow 7000/tcp # seeder API (internal only — restrict to your auth droplet IP)
|
|
399
|
+
ufw allow 8000/tcp # tracker HTTP
|
|
400
|
+
ufw allow 8000/udp # tracker UDP (optional, not needed for browser clients)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
The seeder API (port 7000) should only be reachable from the auth plugin's
|
|
404
|
+
Droplet, not the public internet. Use DigitalOcean's VPC or a firewall rule
|
|
405
|
+
scoped to your auth Droplet's private IP.
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Part 5 — Deployment checklist
|
|
410
|
+
|
|
411
|
+
- [ ] DO Spaces bucket created, CDN enabled
|
|
412
|
+
- [ ] Spaces access key generated, stored as env vars
|
|
413
|
+
- [ ] Tracker Droplet running `tracker.js`, port 8000 open
|
|
414
|
+
- [ ] Seeder Droplet (Premium CPU-Optimized) running `seeder.js`
|
|
415
|
+
- [ ] `TRACKER_URL` env var set on seeder pointing to tracker's public IP
|
|
416
|
+
- [ ] `POST /seed` called after every new upload to Spaces
|
|
417
|
+
- [ ] Seeder API (port 7000) firewalled to auth Droplet only
|
|
418
|
+
- [ ] `pm2 startup` run on both Droplets so processes survive reboots
|
|
419
|
+
- [ ] `/status` endpoint smoke-tested to confirm peers are connecting
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"title":"test-video-1773549834196","description":"A test video","tags":["test","integration"],"uuid":"50a84a5a-6b2b-492e-b279-61eddb392e34","videoId":"1bc176d66a289f8dc346048841aa54e05a3664f83e830e6189a73b961f565fe4","createdAt":"2026-03-15T04:43:54.199Z"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"title":"test-video-1773549541230","description":"A test video","tags":["test","integration"],"uuid":"dfa790d7-c4f5-43a9-b7ff-f166f0f344c9","videoId":"6b2c45b7f4f89b1bda0d627adfe01287f83b34aeb919faacc6d4dc2b67c56b64","createdAt":"2026-03-15T04:39:01.233Z"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"privateKey":"c1446ecd82f3d199e6a15660f57a2294214ee10b6ebf86431ca6acfa5bd5b23d","pubKey":"035195b65ea792314cba1400189d6dccc0ceb074a592e22500c3e94dead25ecc00"}
|
package/data/lucille/pubKey_02b0071b6d90ce7598eabdcfa0596f95359799fd413e300dcbdca823095a/4cf/884
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
50a84a5a-6b2b-492e-b279-61eddb392e34
|
package/data/lucille/pubKey_030e4e2f145c4b68863ca42868e08b1559c3c9c10774e61c3f4ef9766d0c/df7/e30
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dfa790d7-c4f5-43a9-b7ff-f166f0f344c9
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"pubKey":"02b0071b6d90ce7598eabdcfa0596f95359799fd413e300dcbdca823095a4cf884","videos":{},"uuid":"50a84a5a-6b2b-492e-b279-61eddb392e34"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"pubKey":"030e4e2f145c4b68863ca42868e08b1559c3c9c10774e61c3f4ef9766d0cdf7e30","videos":{},"uuid":"dfa790d7-c4f5-43a9-b7ff-f166f0f344c9"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"test-video-1773549834196":{"title":"test-video-1773549834196","description":"A test video","tags":["test","integration"],"uuid":"50a84a5a-6b2b-492e-b279-61eddb392e34","videoId":"1bc176d66a289f8dc346048841aa54e05a3664f83e830e6189a73b961f565fe4","createdAt":"2026-03-15T04:43:54.199Z"}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"test-video-1773549541230":{"title":"test-video-1773549541230","description":"A test video","tags":["test","integration"],"uuid":"dfa790d7-c4f5-43a9-b7ff-f166f0f344c9","videoId":"6b2c45b7f4f89b1bda0d627adfe01287f83b34aeb919faacc6d4dc2b67c56b64","createdAt":"2026-03-15T04:39:01.233Z"}}
|