@freqhole/playlistz 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/.changeset/config.json +11 -0
- package/.changeset/nice-wolves-thank.md +5 -0
- package/.freqhole-versions.json +4 -0
- package/.github/copilot-instructions.md +201 -0
- package/.github/workflows/changesets.yml +50 -0
- package/.github/workflows/npm-publish.yml +124 -0
- package/.github/workflows/pr-checks.yml +103 -0
- package/README.md +30 -0
- package/build-component.js +141 -0
- package/build-zip-bundle-lib.js +44 -0
- package/config/playwright.config.ts +47 -0
- package/config/vite.config.ts +44 -0
- package/config/vitest.config.ts +39 -0
- package/dist/assets/automerge_wasm_bg-Cik4BF9l.wasm +0 -0
- package/dist/assets/index-CbOXzGiA.js +216 -0
- package/dist/assets/index-CbOXzGiA.js.map +1 -0
- package/dist/assets/index-TvJ6RFpy.css +1 -0
- package/dist/assets/midden-DceCrT_L.js +2 -0
- package/dist/assets/midden-DceCrT_L.js.map +1 -0
- package/dist/assets/midden_bg-BLhfGIU-.wasm +0 -0
- package/dist/index.html +55 -0
- package/dist/sw.js +134 -0
- package/docs/AUTOMERGE_P2P_PLAN.md +233 -0
- package/docs/COLLABORATIVE_SHARING_PLAN.md +188 -0
- package/docs/E2E_TESTID_PLAN.md +234 -0
- package/docs/IROH_P2P_PLAN.md +302 -0
- package/docs/ROADMAP.md +695 -0
- package/docs/TODO.md +167 -0
- package/docs/bundle-embedding-plan.md +134 -0
- package/docs/standalone-refactor.md +184 -0
- package/e2e/all-playlists.spec.ts +220 -0
- package/e2e/audio-player.spec.ts +226 -0
- package/e2e/collaborative-features.spec.ts +229 -0
- package/e2e/contexts.ts +238 -0
- package/e2e/edit-panel.spec.ts +87 -0
- package/e2e/fixtures/bare-glitch-1s.m4a +0 -0
- package/e2e/fixtures/bare-glitch-1s.mp3 +0 -0
- package/e2e/fixtures/bare-glitch-1s.ogg +0 -0
- package/e2e/fixtures/chord-stack-3s.wav +0 -0
- package/e2e/fixtures/cover-anim.gif +0 -0
- package/e2e/fixtures/cover-blue.png +0 -0
- package/e2e/fixtures/cover-checkers.png +0 -0
- package/e2e/fixtures/cover-gradient.jpg +0 -0
- package/e2e/fixtures/cover-mono.gif +0 -0
- package/e2e/fixtures/cover-noise.png +0 -0
- package/e2e/fixtures/cover-plasma.webp +0 -0
- package/e2e/fixtures/cover-portrait.jpg +0 -0
- package/e2e/fixtures/cover-red.png +0 -0
- package/e2e/fixtures/cover-thumb.jpg +0 -0
- package/e2e/fixtures/cover-wide.webp +0 -0
- package/e2e/fixtures/generate.mjs +257 -0
- package/e2e/fixtures/long-drone-90s.mp3 +0 -0
- package/e2e/fixtures/noisy-binaural-8s.mp3 +0 -0
- package/e2e/fixtures/tagged-a3-4s.m4a +0 -0
- package/e2e/fixtures/tagged-a3-4s.mp3 +0 -0
- package/e2e/fixtures/tagged-a3-4s.ogg +0 -0
- package/e2e/fixtures/tagged-c5-3s.m4a +0 -0
- package/e2e/fixtures/tagged-c5-3s.mp3 +0 -0
- package/e2e/fixtures/tagged-c5-3s.ogg +0 -0
- package/e2e/fixtures/tagged-f4-6s.m4a +0 -0
- package/e2e/fixtures/tagged-f4-6s.mp3 +0 -0
- package/e2e/fixtures/tagged-f4-6s.ogg +0 -0
- package/e2e/fixtures/tone-220hz-10s.wav +0 -0
- package/e2e/fixtures/tone-440hz-2s.wav +0 -0
- package/e2e/fixtures/tone-880hz-5s.wav +0 -0
- package/e2e/fixtures/tone-stereo-3s.wav +0 -0
- package/e2e/fixtures/user-provided/README.md +1 -0
- package/e2e/helpers/app.ts +143 -0
- package/e2e/helpers/hooks.ts +133 -0
- package/e2e/helpers/index.ts +12 -0
- package/e2e/helpers/media.ts +125 -0
- package/e2e/helpers.ts +10 -0
- package/e2e/p2p-collaboration.spec.ts +356 -0
- package/e2e/p2p-multi-peer.spec.ts +723 -0
- package/e2e/p2p-states.spec.ts +302 -0
- package/e2e/playback.spec.ts +56 -0
- package/e2e/playlist-crud.spec.ts +126 -0
- package/e2e/share-link-autoplay.spec.ts +129 -0
- package/e2e/sharing-access.spec.ts +205 -0
- package/e2e/sharing.spec.ts +195 -0
- package/e2e/song-cache-state.spec.ts +202 -0
- package/e2e/zip-bundle.spec.ts +855 -0
- package/eslint.config.js +114 -0
- package/index.html +54 -0
- package/package.json +119 -0
- package/public/sw.js +134 -0
- package/scripts/use-local.mjs +37 -0
- package/scripts/use-published.mjs +37 -0
- package/src/App.tsx +9 -0
- package/src/cli/check.ts +164 -0
- package/src/cli/generate.ts +184 -0
- package/src/cli/http.ts +88 -0
- package/src/cli/index.ts +65 -0
- package/src/cli/init.ts +18 -0
- package/src/components/AllPlaylistsPanel.tsx +713 -0
- package/src/components/AudioPlayer.tsx +122 -0
- package/src/components/MarqueeText.tsx +101 -0
- package/src/components/PlaylistCoverModal.tsx +519 -0
- package/src/components/PlaylistEditPanel.tsx +803 -0
- package/src/components/PlaylistSharePanel.tsx +1020 -0
- package/src/components/ShareLinkKnockPanel.tsx +144 -0
- package/src/components/SharePanel.tsx +584 -0
- package/src/components/SongEditModal.tsx +453 -0
- package/src/components/SongEditPanel.tsx +578 -0
- package/src/components/SongRow.tsx +689 -0
- package/src/components/index.tsx +494 -0
- package/src/components/playlist/index.tsx +1203 -0
- package/src/context/PlaylistzContext.tsx +74 -0
- package/src/dev-hooks.ts +35 -0
- package/src/hooks/createDocIndexQuery.ts +53 -0
- package/src/hooks/createDocStore.test.ts +303 -0
- package/src/hooks/createDocStore.ts +90 -0
- package/src/hooks/useDragAndDrop.test.ts +474 -0
- package/src/hooks/useDragAndDrop.ts +400 -0
- package/src/hooks/useImageModal.test.ts +174 -0
- package/src/hooks/useImageModal.ts +201 -0
- package/src/hooks/usePlaylistManager.test.ts +453 -0
- package/src/hooks/usePlaylistManager.ts +685 -0
- package/src/hooks/usePlaylistsQuery.test.tsx +120 -0
- package/src/hooks/usePlaylistsQuery.ts +44 -0
- package/src/hooks/useSongState.test.ts +236 -0
- package/src/hooks/useSongState.ts +114 -0
- package/src/hooks/useUIState.ts +71 -0
- package/src/index.tsx +18 -0
- package/src/services/audioService.dev.ts +22 -0
- package/src/services/audioService.test.ts +1226 -0
- package/src/services/audioService.ts +1395 -0
- package/src/services/automergeRepo.test.ts +269 -0
- package/src/services/automergeRepo.ts +226 -0
- package/src/services/blobTransferService.dev.ts +119 -0
- package/src/services/blobTransferService.test.ts +441 -0
- package/src/services/blobTransferService.ts +702 -0
- package/src/services/docIndexService.test.ts +179 -0
- package/src/services/docIndexService.ts +118 -0
- package/src/services/fileProcessingService.test.ts +554 -0
- package/src/services/fileProcessingService.ts +239 -0
- package/src/services/imageService.test.ts +701 -0
- package/src/services/imageService.ts +365 -0
- package/src/services/indexedDBService.integration.test.ts +104 -0
- package/src/services/indexedDBService.test.ts +202 -0
- package/src/services/indexedDBService.ts +436 -0
- package/src/services/offlineService.test.ts +661 -0
- package/src/services/offlineService.ts +382 -0
- package/src/services/p2pService.test.ts +305 -0
- package/src/services/p2pService.ts +344 -0
- package/src/services/playlistDocService.test.ts +448 -0
- package/src/services/playlistDocService.ts +707 -0
- package/src/services/playlistDownloadService.test.ts +674 -0
- package/src/services/playlistDownloadService.ts +389 -0
- package/src/services/sharingService.test.ts +812 -0
- package/src/services/sharingService.ts +1073 -0
- package/src/services/sharingState.ts +161 -0
- package/src/services/songReactivity.test.ts +620 -0
- package/src/services/songReactivity.ts +145 -0
- package/src/services/standaloneService.test.ts +1025 -0
- package/src/services/standaloneService.ts +588 -0
- package/src/services/streamingAudioService.test.ts +275 -0
- package/src/services/streamingAudioService.ts +166 -0
- package/src/styles.css +428 -0
- package/src/test-setup.ts +547 -0
- package/src/types/global.d.ts +40 -0
- package/src/types/playlist.ts +99 -0
- package/src/utils/hashUtils.ts +41 -0
- package/src/utils/log.ts +97 -0
- package/src/utils/m3u.test.ts +172 -0
- package/src/utils/m3u.ts +136 -0
- package/src/utils/mockData.ts +166 -0
- package/src/utils/standaloneTemplates.test.ts +175 -0
- package/src/utils/standaloneTemplates.ts +83 -0
- package/src/utils/swTemplate.ts +84 -0
- package/src/utils/timeUtils.ts +166 -0
- package/src/utils/typeGuards.ts +171 -0
- package/src/web-component.tsx +98 -0
- package/src/zip-bundle/index.ts +7 -0
- package/src/zip-bundle/m3u.ts +45 -0
- package/src/zip-bundle/types.ts +50 -0
- package/src/zip-bundle/utils.ts +33 -0
- package/src/zip-bundle/zipBuilder.ts +309 -0
- package/tailwind.config.js +55 -0
- package/tsconfig.json +43 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as crypto from "node:crypto";
|
|
4
|
+
import { FreqholePlaylistzSchema, generatePlaylistzJs, type FreqholePlaylist, type FreqholePlaylistz } from "../utils/standaloneTemplates.js";
|
|
5
|
+
import { parseM3U, serializeM3U } from "../utils/m3u.js";
|
|
6
|
+
|
|
7
|
+
// deterministic uuid v5 from a string (dns namespace)
|
|
8
|
+
function uuidv5(name: string): string {
|
|
9
|
+
const ns = Buffer.from("6ba7b8109dad11d180b400c04fd430c8", "hex");
|
|
10
|
+
const hash = crypto.createHash("sha1").update(Buffer.concat([ns, Buffer.from(name)])).digest();
|
|
11
|
+
hash[6] = (hash[6]! & 0x0f) | 0x50;
|
|
12
|
+
hash[8] = (hash[8]! & 0x3f) | 0x80;
|
|
13
|
+
const h = hash.toString("hex");
|
|
14
|
+
return `${h.slice(0,8)}-${h.slice(8,12)}-${h.slice(12,16)}-${h.slice(16,20)}-${h.slice(20,32)}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function extOf(filePath: string): string {
|
|
18
|
+
return path.extname(filePath).toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function mimeForExt(ext: string): string {
|
|
22
|
+
const map: Record<string, string> = {
|
|
23
|
+
".mp3": "audio/mpeg", ".m4a": "audio/mp4", ".wav": "audio/wav",
|
|
24
|
+
".flac": "audio/flac", ".ogg": "audio/ogg", ".webm": "audio/webm",
|
|
25
|
+
};
|
|
26
|
+
return map[ext] ?? "audio/mpeg";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// load existing playlistz.js if present, return parsed array or empty
|
|
30
|
+
function loadExistingPlaylistz(playlistzPath: string): FreqholePlaylistz {
|
|
31
|
+
if (!fs.existsSync(playlistzPath)) return [];
|
|
32
|
+
try {
|
|
33
|
+
const src = fs.readFileSync(playlistzPath, "utf-8");
|
|
34
|
+
const attrMatch = src.match(/setAttribute\s*\(\s*'data-playlistz'\s*,\s*("(?:[^"\\]|\\.)*")\s*\)/);
|
|
35
|
+
if (!attrMatch) {
|
|
36
|
+
console.warn("existing playlistz.js has unrecognised format - treating as empty");
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const raw = JSON.parse(JSON.parse(attrMatch[1]!));
|
|
40
|
+
const parsed = FreqholePlaylistzSchema.safeParse(raw);
|
|
41
|
+
if (parsed.success) return parsed.data;
|
|
42
|
+
console.warn("existing playlistz.js failed schema validation - treating as empty");
|
|
43
|
+
return [];
|
|
44
|
+
} catch {
|
|
45
|
+
console.warn("could not parse existing playlistz.js - treating as empty");
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function generateData(dir: string): void {
|
|
51
|
+
const resolved = path.resolve(dir);
|
|
52
|
+
if (!fs.existsSync(resolved)) {
|
|
53
|
+
console.error(`directory not found: ${resolved}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// find .m3u8 files in dir or data/ subdir
|
|
58
|
+
const candidates = [
|
|
59
|
+
...fs.readdirSync(resolved).filter((f) => f.endsWith(".m3u8")).map((f) => path.join(resolved, f)),
|
|
60
|
+
...(fs.existsSync(path.join(resolved, "data"))
|
|
61
|
+
? fs.readdirSync(path.join(resolved, "data")).filter((f) => f.endsWith(".m3u8")).map((f) => path.join(resolved, "data", f))
|
|
62
|
+
: []),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
if (candidates.length === 0) {
|
|
66
|
+
console.error(`no .m3u8 files found in ${resolved} or ${resolved}/data/`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const playlistzPath = path.join(resolved, "playlistz.js");
|
|
71
|
+
const existing = loadExistingPlaylistz(playlistzPath);
|
|
72
|
+
|
|
73
|
+
let updated = 0;
|
|
74
|
+
|
|
75
|
+
for (const m3uPath of candidates) {
|
|
76
|
+
console.log(`processing: ${path.relative(resolved, m3uPath)}`);
|
|
77
|
+
|
|
78
|
+
const content = fs.readFileSync(m3uPath, "utf-8");
|
|
79
|
+
const parsed = parseM3U(content);
|
|
80
|
+
const m3uDir = path.dirname(m3uPath);
|
|
81
|
+
|
|
82
|
+
if (!parsed.title) {
|
|
83
|
+
console.warn(` skipping: no # Playlist: header found`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// assign stable id from title if not present
|
|
88
|
+
const isNew = !parsed.id;
|
|
89
|
+
if (isNew) {
|
|
90
|
+
parsed.id = uuidv5(parsed.title);
|
|
91
|
+
parsed.rev = 0;
|
|
92
|
+
console.log(` new playlist - assigned id: ${parsed.id}`);
|
|
93
|
+
} else {
|
|
94
|
+
// increment rev on each generate run to signal standaloneService to re-check
|
|
95
|
+
parsed.rev = (parsed.rev ?? 0) + 1;
|
|
96
|
+
console.log(` existing playlist - id: ${parsed.id}, rev: ${parsed.rev}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// write id + rev back to m3u8
|
|
100
|
+
fs.writeFileSync(m3uPath, serializeM3U(parsed), "utf-8");
|
|
101
|
+
console.log(` updated: ${path.relative(resolved, m3uPath)}`);
|
|
102
|
+
|
|
103
|
+
// resolve playlist cover image metadata
|
|
104
|
+
const coverExt = parsed.playlistImageFile ? extOf(parsed.playlistImageFile) : undefined;
|
|
105
|
+
const coverMime = coverExt === ".gif" ? "image/gif"
|
|
106
|
+
: coverExt === ".png" ? "image/png"
|
|
107
|
+
: coverExt === ".webp" ? "image/webp"
|
|
108
|
+
: coverExt ? "image/jpeg"
|
|
109
|
+
: undefined;
|
|
110
|
+
|
|
111
|
+
// build songs array
|
|
112
|
+
const songs = parsed.songs.map((s, i) => {
|
|
113
|
+
// audio file path is relative to the m3u8 dir, e.g. "data/01-song.mp3"
|
|
114
|
+
const audioFilename = path.basename(s.audioFile);
|
|
115
|
+
const audioExt = extOf(audioFilename);
|
|
116
|
+
|
|
117
|
+
// image: strip "data/" prefix and figure out extension
|
|
118
|
+
const imageBasename = s.imageFile ? path.basename(s.imageFile) : undefined;
|
|
119
|
+
const imageExt = imageBasename ? extOf(imageBasename) : undefined;
|
|
120
|
+
const imageMime = !imageExt ? undefined
|
|
121
|
+
: imageExt === ".gif" ? "image/gif"
|
|
122
|
+
: imageExt === ".png" ? "image/png"
|
|
123
|
+
: imageExt === ".webp" ? "image/webp"
|
|
124
|
+
: "image/jpeg";
|
|
125
|
+
|
|
126
|
+
// check file exists on disk
|
|
127
|
+
const audioPath = path.join(m3uDir, audioFilename);
|
|
128
|
+
if (!fs.existsSync(audioPath)) {
|
|
129
|
+
console.warn(` warn: audio file not found: ${audioFilename}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
id: uuidv5(`${parsed.id}:${i}:${audioFilename}`),
|
|
134
|
+
title: s.title || audioFilename,
|
|
135
|
+
artist: s.artist || "",
|
|
136
|
+
album: s.album || "",
|
|
137
|
+
duration: s.duration,
|
|
138
|
+
originalFilename: audioFilename,
|
|
139
|
+
filePath: `data/${audioFilename}`,
|
|
140
|
+
safeFilename: audioFilename,
|
|
141
|
+
fileSize: fs.existsSync(audioPath) ? fs.statSync(audioPath).size : 0,
|
|
142
|
+
mimeType: mimeForExt(audioExt),
|
|
143
|
+
imageExtension: imageExt ?? undefined,
|
|
144
|
+
imageMimeType: imageMime,
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const entry: FreqholePlaylist = {
|
|
149
|
+
playlist: {
|
|
150
|
+
// parsed.id is always assigned above (uuidv5 from title when new)
|
|
151
|
+
id: parsed.id!,
|
|
152
|
+
title: parsed.title,
|
|
153
|
+
description: parsed.description || undefined,
|
|
154
|
+
rev: parsed.rev,
|
|
155
|
+
imageExtension: coverExt ?? undefined,
|
|
156
|
+
imageMimeType: coverMime,
|
|
157
|
+
},
|
|
158
|
+
songs,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// upsert into existing data: replace by id or append
|
|
162
|
+
const idx = existing.findIndex((e) => e.playlist.id === parsed.id);
|
|
163
|
+
if (idx >= 0) {
|
|
164
|
+
existing[idx] = entry;
|
|
165
|
+
console.log(` updated entry in playlistz.js`);
|
|
166
|
+
} else {
|
|
167
|
+
existing.push(entry);
|
|
168
|
+
console.log(` added new entry to playlistz.js`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
updated++;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (updated === 0) {
|
|
175
|
+
console.log("no playlists were updated");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// write playlistz.js (new attribute-based format)
|
|
180
|
+
const output = generatePlaylistzJs(existing);
|
|
181
|
+
fs.writeFileSync(playlistzPath, output, "utf-8");
|
|
182
|
+
console.log(`\nwrote: ${playlistzPath}`);
|
|
183
|
+
console.log(` ${existing.length} playlist(s) total`);
|
|
184
|
+
}
|
package/src/cli/http.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as http from "node:http";
|
|
4
|
+
|
|
5
|
+
const MIME: Record<string, string> = {
|
|
6
|
+
".html": "text/html",
|
|
7
|
+
".js": "application/javascript",
|
|
8
|
+
".css": "text/css",
|
|
9
|
+
".json": "application/json",
|
|
10
|
+
".mp3": "audio/mpeg",
|
|
11
|
+
".m4a": "audio/mp4",
|
|
12
|
+
".wav": "audio/wav",
|
|
13
|
+
".flac": "audio/flac",
|
|
14
|
+
".ogg": "audio/ogg",
|
|
15
|
+
".webm": "audio/webm",
|
|
16
|
+
".jpg": "image/jpeg",
|
|
17
|
+
".jpeg": "image/jpeg",
|
|
18
|
+
".png": "image/png",
|
|
19
|
+
".gif": "image/gif",
|
|
20
|
+
".webp": "image/webp",
|
|
21
|
+
".map": "application/json",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function mimeFor(filePath: string): string {
|
|
25
|
+
return MIME[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function serveHttp(dir: string): void {
|
|
29
|
+
const root = path.resolve(dir);
|
|
30
|
+
if (!fs.existsSync(root)) {
|
|
31
|
+
console.error(`directory not found: ${root}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const server = http.createServer((req, res) => {
|
|
36
|
+
const rawPath = req.url?.split("?")[0] ?? "/";
|
|
37
|
+
const urlPath = decodeURIComponent(rawPath);
|
|
38
|
+
const filePath = path.join(root, urlPath === "/" ? "index.html" : urlPath);
|
|
39
|
+
|
|
40
|
+
// prevent path traversal
|
|
41
|
+
if (!filePath.startsWith(root + path.sep) && filePath !== root) {
|
|
42
|
+
res.writeHead(403);
|
|
43
|
+
res.end("forbidden");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
|
|
48
|
+
res.writeHead(404);
|
|
49
|
+
res.end("not found");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const stat = fs.statSync(filePath);
|
|
54
|
+
const total = stat.size;
|
|
55
|
+
const mime = mimeFor(filePath);
|
|
56
|
+
const rangeHeader = req.headers["range"];
|
|
57
|
+
|
|
58
|
+
if (rangeHeader) {
|
|
59
|
+
const [, rangeStr] = rangeHeader.split("=");
|
|
60
|
+
const [startStr, endStr] = (rangeStr ?? "").split("-");
|
|
61
|
+
const start = parseInt(startStr ?? "0", 10);
|
|
62
|
+
const end = endStr ? parseInt(endStr, 10) : total - 1;
|
|
63
|
+
const chunkSize = end - start + 1;
|
|
64
|
+
|
|
65
|
+
res.writeHead(206, {
|
|
66
|
+
"Content-Range": `bytes ${start}-${end}/${total}`,
|
|
67
|
+
"Accept-Ranges": "bytes",
|
|
68
|
+
"Content-Length": chunkSize,
|
|
69
|
+
"Content-Type": mime,
|
|
70
|
+
});
|
|
71
|
+
fs.createReadStream(filePath, { start, end }).pipe(res);
|
|
72
|
+
} else {
|
|
73
|
+
res.writeHead(200, {
|
|
74
|
+
"Content-Length": total,
|
|
75
|
+
"Content-Type": mime,
|
|
76
|
+
"Accept-Ranges": "bytes",
|
|
77
|
+
});
|
|
78
|
+
fs.createReadStream(filePath).pipe(res);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const port = parseInt(process.env["PORT"] ?? "8080", 10);
|
|
83
|
+
server.listen(port, () => {
|
|
84
|
+
console.log(`serving ${root}`);
|
|
85
|
+
console.log(`http://localhost:${port}`);
|
|
86
|
+
console.log("ctrl+c to stop");
|
|
87
|
+
});
|
|
88
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { serveHttp } from "./http.js";
|
|
2
|
+
import { checkFile } from "./check.js";
|
|
3
|
+
import { initDir } from "./init.js";
|
|
4
|
+
import { generateData } from "./generate.js";
|
|
5
|
+
|
|
6
|
+
const HELP = `
|
|
7
|
+
freqhole-playlistz cli
|
|
8
|
+
|
|
9
|
+
usage:
|
|
10
|
+
freqhole-playlistz --http [dir] serve dir over http with range requests (default: .)
|
|
11
|
+
freqhole-playlistz --check [file] validate playlistz.js structure + check files on disk (default: ./playlistz.js)
|
|
12
|
+
freqhole-playlistz --init <dir> write index.html + sw.js to dir
|
|
13
|
+
freqhole-playlistz --generate-data <dir> parse .m3u8 file(s) in dir, generate/update playlistz.js
|
|
14
|
+
freqhole-playlistz --help show this help
|
|
15
|
+
|
|
16
|
+
env:
|
|
17
|
+
PORT port for --http server (default: 8080)
|
|
18
|
+
`.trim();
|
|
19
|
+
|
|
20
|
+
export function runCli(argv: string[]): void {
|
|
21
|
+
const args = argv.slice(2);
|
|
22
|
+
const cmd = args[0];
|
|
23
|
+
|
|
24
|
+
if (!cmd || cmd === "--help" || cmd === "-h") {
|
|
25
|
+
console.log(HELP);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (cmd === "--http") {
|
|
30
|
+
serveHttp(args[1] ?? ".");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (cmd === "--check") {
|
|
35
|
+
checkFile(args[1] ?? "./playlistz.js");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (cmd === "--init") {
|
|
40
|
+
const dir = args[1];
|
|
41
|
+
if (!dir) {
|
|
42
|
+
console.error("usage: freqhole-playlistz --init <dir>");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
initDir(dir);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (cmd === "--generate-data") {
|
|
50
|
+
const dir = args[1];
|
|
51
|
+
if (!dir) {
|
|
52
|
+
console.error("usage: freqhole-playlistz --generate-data <dir>");
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
generateData(dir);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.error(`unknown command: ${cmd}`);
|
|
60
|
+
console.error("run with --help for usage");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// self-executing entrypoint when run directly via node
|
|
65
|
+
runCli(process.argv);
|
package/src/cli/init.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
// these placeholder strings are replaced at build time by build-component.js
|
|
5
|
+
// with the actual file contents read from the source tree
|
|
6
|
+
const INDEX_HTML = "__INDEX_HTML__";
|
|
7
|
+
const SW_JS = "__SW_JS__";
|
|
8
|
+
|
|
9
|
+
export function initDir(dir: string): void {
|
|
10
|
+
const resolved = path.resolve(dir);
|
|
11
|
+
fs.mkdirSync(resolved, { recursive: true });
|
|
12
|
+
|
|
13
|
+
fs.writeFileSync(path.join(resolved, "index.html"), INDEX_HTML, "utf-8");
|
|
14
|
+
console.log(`wrote ${path.join(resolved, "index.html")}`);
|
|
15
|
+
|
|
16
|
+
fs.writeFileSync(path.join(resolved, "sw.js"), SW_JS, "utf-8");
|
|
17
|
+
console.log(`wrote ${path.join(resolved, "sw.js")}`);
|
|
18
|
+
}
|