@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.
Files changed (180) hide show
  1. package/.changeset/config.json +11 -0
  2. package/.changeset/nice-wolves-thank.md +5 -0
  3. package/.freqhole-versions.json +4 -0
  4. package/.github/copilot-instructions.md +201 -0
  5. package/.github/workflows/changesets.yml +50 -0
  6. package/.github/workflows/npm-publish.yml +124 -0
  7. package/.github/workflows/pr-checks.yml +103 -0
  8. package/README.md +30 -0
  9. package/build-component.js +141 -0
  10. package/build-zip-bundle-lib.js +44 -0
  11. package/config/playwright.config.ts +47 -0
  12. package/config/vite.config.ts +44 -0
  13. package/config/vitest.config.ts +39 -0
  14. package/dist/assets/automerge_wasm_bg-Cik4BF9l.wasm +0 -0
  15. package/dist/assets/index-CbOXzGiA.js +216 -0
  16. package/dist/assets/index-CbOXzGiA.js.map +1 -0
  17. package/dist/assets/index-TvJ6RFpy.css +1 -0
  18. package/dist/assets/midden-DceCrT_L.js +2 -0
  19. package/dist/assets/midden-DceCrT_L.js.map +1 -0
  20. package/dist/assets/midden_bg-BLhfGIU-.wasm +0 -0
  21. package/dist/index.html +55 -0
  22. package/dist/sw.js +134 -0
  23. package/docs/AUTOMERGE_P2P_PLAN.md +233 -0
  24. package/docs/COLLABORATIVE_SHARING_PLAN.md +188 -0
  25. package/docs/E2E_TESTID_PLAN.md +234 -0
  26. package/docs/IROH_P2P_PLAN.md +302 -0
  27. package/docs/ROADMAP.md +695 -0
  28. package/docs/TODO.md +167 -0
  29. package/docs/bundle-embedding-plan.md +134 -0
  30. package/docs/standalone-refactor.md +184 -0
  31. package/e2e/all-playlists.spec.ts +220 -0
  32. package/e2e/audio-player.spec.ts +226 -0
  33. package/e2e/collaborative-features.spec.ts +229 -0
  34. package/e2e/contexts.ts +238 -0
  35. package/e2e/edit-panel.spec.ts +87 -0
  36. package/e2e/fixtures/bare-glitch-1s.m4a +0 -0
  37. package/e2e/fixtures/bare-glitch-1s.mp3 +0 -0
  38. package/e2e/fixtures/bare-glitch-1s.ogg +0 -0
  39. package/e2e/fixtures/chord-stack-3s.wav +0 -0
  40. package/e2e/fixtures/cover-anim.gif +0 -0
  41. package/e2e/fixtures/cover-blue.png +0 -0
  42. package/e2e/fixtures/cover-checkers.png +0 -0
  43. package/e2e/fixtures/cover-gradient.jpg +0 -0
  44. package/e2e/fixtures/cover-mono.gif +0 -0
  45. package/e2e/fixtures/cover-noise.png +0 -0
  46. package/e2e/fixtures/cover-plasma.webp +0 -0
  47. package/e2e/fixtures/cover-portrait.jpg +0 -0
  48. package/e2e/fixtures/cover-red.png +0 -0
  49. package/e2e/fixtures/cover-thumb.jpg +0 -0
  50. package/e2e/fixtures/cover-wide.webp +0 -0
  51. package/e2e/fixtures/generate.mjs +257 -0
  52. package/e2e/fixtures/long-drone-90s.mp3 +0 -0
  53. package/e2e/fixtures/noisy-binaural-8s.mp3 +0 -0
  54. package/e2e/fixtures/tagged-a3-4s.m4a +0 -0
  55. package/e2e/fixtures/tagged-a3-4s.mp3 +0 -0
  56. package/e2e/fixtures/tagged-a3-4s.ogg +0 -0
  57. package/e2e/fixtures/tagged-c5-3s.m4a +0 -0
  58. package/e2e/fixtures/tagged-c5-3s.mp3 +0 -0
  59. package/e2e/fixtures/tagged-c5-3s.ogg +0 -0
  60. package/e2e/fixtures/tagged-f4-6s.m4a +0 -0
  61. package/e2e/fixtures/tagged-f4-6s.mp3 +0 -0
  62. package/e2e/fixtures/tagged-f4-6s.ogg +0 -0
  63. package/e2e/fixtures/tone-220hz-10s.wav +0 -0
  64. package/e2e/fixtures/tone-440hz-2s.wav +0 -0
  65. package/e2e/fixtures/tone-880hz-5s.wav +0 -0
  66. package/e2e/fixtures/tone-stereo-3s.wav +0 -0
  67. package/e2e/fixtures/user-provided/README.md +1 -0
  68. package/e2e/helpers/app.ts +143 -0
  69. package/e2e/helpers/hooks.ts +133 -0
  70. package/e2e/helpers/index.ts +12 -0
  71. package/e2e/helpers/media.ts +125 -0
  72. package/e2e/helpers.ts +10 -0
  73. package/e2e/p2p-collaboration.spec.ts +356 -0
  74. package/e2e/p2p-multi-peer.spec.ts +723 -0
  75. package/e2e/p2p-states.spec.ts +302 -0
  76. package/e2e/playback.spec.ts +56 -0
  77. package/e2e/playlist-crud.spec.ts +126 -0
  78. package/e2e/share-link-autoplay.spec.ts +129 -0
  79. package/e2e/sharing-access.spec.ts +205 -0
  80. package/e2e/sharing.spec.ts +195 -0
  81. package/e2e/song-cache-state.spec.ts +202 -0
  82. package/e2e/zip-bundle.spec.ts +855 -0
  83. package/eslint.config.js +114 -0
  84. package/index.html +54 -0
  85. package/package.json +119 -0
  86. package/public/sw.js +134 -0
  87. package/scripts/use-local.mjs +37 -0
  88. package/scripts/use-published.mjs +37 -0
  89. package/src/App.tsx +9 -0
  90. package/src/cli/check.ts +164 -0
  91. package/src/cli/generate.ts +184 -0
  92. package/src/cli/http.ts +88 -0
  93. package/src/cli/index.ts +65 -0
  94. package/src/cli/init.ts +18 -0
  95. package/src/components/AllPlaylistsPanel.tsx +713 -0
  96. package/src/components/AudioPlayer.tsx +122 -0
  97. package/src/components/MarqueeText.tsx +101 -0
  98. package/src/components/PlaylistCoverModal.tsx +519 -0
  99. package/src/components/PlaylistEditPanel.tsx +803 -0
  100. package/src/components/PlaylistSharePanel.tsx +1020 -0
  101. package/src/components/ShareLinkKnockPanel.tsx +144 -0
  102. package/src/components/SharePanel.tsx +584 -0
  103. package/src/components/SongEditModal.tsx +453 -0
  104. package/src/components/SongEditPanel.tsx +578 -0
  105. package/src/components/SongRow.tsx +689 -0
  106. package/src/components/index.tsx +494 -0
  107. package/src/components/playlist/index.tsx +1203 -0
  108. package/src/context/PlaylistzContext.tsx +74 -0
  109. package/src/dev-hooks.ts +35 -0
  110. package/src/hooks/createDocIndexQuery.ts +53 -0
  111. package/src/hooks/createDocStore.test.ts +303 -0
  112. package/src/hooks/createDocStore.ts +90 -0
  113. package/src/hooks/useDragAndDrop.test.ts +474 -0
  114. package/src/hooks/useDragAndDrop.ts +400 -0
  115. package/src/hooks/useImageModal.test.ts +174 -0
  116. package/src/hooks/useImageModal.ts +201 -0
  117. package/src/hooks/usePlaylistManager.test.ts +453 -0
  118. package/src/hooks/usePlaylistManager.ts +685 -0
  119. package/src/hooks/usePlaylistsQuery.test.tsx +120 -0
  120. package/src/hooks/usePlaylistsQuery.ts +44 -0
  121. package/src/hooks/useSongState.test.ts +236 -0
  122. package/src/hooks/useSongState.ts +114 -0
  123. package/src/hooks/useUIState.ts +71 -0
  124. package/src/index.tsx +18 -0
  125. package/src/services/audioService.dev.ts +22 -0
  126. package/src/services/audioService.test.ts +1226 -0
  127. package/src/services/audioService.ts +1395 -0
  128. package/src/services/automergeRepo.test.ts +269 -0
  129. package/src/services/automergeRepo.ts +226 -0
  130. package/src/services/blobTransferService.dev.ts +119 -0
  131. package/src/services/blobTransferService.test.ts +441 -0
  132. package/src/services/blobTransferService.ts +702 -0
  133. package/src/services/docIndexService.test.ts +179 -0
  134. package/src/services/docIndexService.ts +118 -0
  135. package/src/services/fileProcessingService.test.ts +554 -0
  136. package/src/services/fileProcessingService.ts +239 -0
  137. package/src/services/imageService.test.ts +701 -0
  138. package/src/services/imageService.ts +365 -0
  139. package/src/services/indexedDBService.integration.test.ts +104 -0
  140. package/src/services/indexedDBService.test.ts +202 -0
  141. package/src/services/indexedDBService.ts +436 -0
  142. package/src/services/offlineService.test.ts +661 -0
  143. package/src/services/offlineService.ts +382 -0
  144. package/src/services/p2pService.test.ts +305 -0
  145. package/src/services/p2pService.ts +344 -0
  146. package/src/services/playlistDocService.test.ts +448 -0
  147. package/src/services/playlistDocService.ts +707 -0
  148. package/src/services/playlistDownloadService.test.ts +674 -0
  149. package/src/services/playlistDownloadService.ts +389 -0
  150. package/src/services/sharingService.test.ts +812 -0
  151. package/src/services/sharingService.ts +1073 -0
  152. package/src/services/sharingState.ts +161 -0
  153. package/src/services/songReactivity.test.ts +620 -0
  154. package/src/services/songReactivity.ts +145 -0
  155. package/src/services/standaloneService.test.ts +1025 -0
  156. package/src/services/standaloneService.ts +588 -0
  157. package/src/services/streamingAudioService.test.ts +275 -0
  158. package/src/services/streamingAudioService.ts +166 -0
  159. package/src/styles.css +428 -0
  160. package/src/test-setup.ts +547 -0
  161. package/src/types/global.d.ts +40 -0
  162. package/src/types/playlist.ts +99 -0
  163. package/src/utils/hashUtils.ts +41 -0
  164. package/src/utils/log.ts +97 -0
  165. package/src/utils/m3u.test.ts +172 -0
  166. package/src/utils/m3u.ts +136 -0
  167. package/src/utils/mockData.ts +166 -0
  168. package/src/utils/standaloneTemplates.test.ts +175 -0
  169. package/src/utils/standaloneTemplates.ts +83 -0
  170. package/src/utils/swTemplate.ts +84 -0
  171. package/src/utils/timeUtils.ts +166 -0
  172. package/src/utils/typeGuards.ts +171 -0
  173. package/src/web-component.tsx +98 -0
  174. package/src/zip-bundle/index.ts +7 -0
  175. package/src/zip-bundle/m3u.ts +45 -0
  176. package/src/zip-bundle/types.ts +50 -0
  177. package/src/zip-bundle/utils.ts +33 -0
  178. package/src/zip-bundle/zipBuilder.ts +309 -0
  179. package/tailwind.config.js +55 -0
  180. package/tsconfig.json +43 -0
@@ -0,0 +1,239 @@
1
+ // file processing service for audio filez
2
+ // can handle file validation, metadata extraction, and processing!
3
+
4
+ import { extractAlbumArt, processPlaylistCover } from "./imageService.js";
5
+ import type {
6
+ AudioMetadata,
7
+ FileUploadResult,
8
+ Song,
9
+ } from "../types/playlist.js";
10
+
11
+ // check if file is a supported audio format
12
+ export function isAudioFile(file: File): boolean {
13
+ return file.type.startsWith("audio/");
14
+ }
15
+
16
+ // validate file size (default 100MB limit)
17
+ export function validateFileSize(file: File, maxSizeMB = 100): boolean {
18
+ const maxBytes = maxSizeMB * 1024 * 1024;
19
+ return file.size <= maxBytes;
20
+ }
21
+
22
+ // extract basic metadata from file name
23
+ function extractMetadataFromFilename(filename: string): Partial<AudioMetadata> {
24
+ // Remove file extension
25
+ const nameWithoutExt = filename.replace(/\.[^/.]+$/, "");
26
+
27
+ // Common patterns: "Artist - Title", "Artist - Album - Title", "Title"
28
+ const dashSplit = nameWithoutExt.split(" - ");
29
+
30
+ if (dashSplit.length === 2) {
31
+ return {
32
+ artist: dashSplit[0]?.trim(),
33
+ title: dashSplit[1]?.trim(),
34
+ };
35
+ } else if (dashSplit.length === 3) {
36
+ return {
37
+ artist: dashSplit[0]?.trim(),
38
+ album: dashSplit[1]?.trim(),
39
+ title: dashSplit[2]?.trim(),
40
+ };
41
+ } else {
42
+ return {
43
+ title: nameWithoutExt.trim(),
44
+ };
45
+ }
46
+ }
47
+
48
+ // Extract audio duration using Web Audio API
49
+ async function extractDuration(file: File): Promise<number> {
50
+ return new Promise((resolve) => {
51
+ try {
52
+ const audio = new Audio();
53
+ const url = URL.createObjectURL(file);
54
+
55
+ audio.addEventListener("loadedmetadata", () => {
56
+ URL.revokeObjectURL(url);
57
+ resolve(audio.duration || 0);
58
+ });
59
+
60
+ audio.addEventListener("error", (e) => {
61
+ URL.revokeObjectURL(url);
62
+ console.warn("could not extract duration from audio file:", e);
63
+ resolve(0); // Don't reject, just return 0
64
+ });
65
+
66
+ audio.src = url;
67
+ } catch (error) {
68
+ console.warn("failed to create blob URL for duration extraction:", error);
69
+ resolve(0); // Return 0 duration if blob URL creation fails
70
+ }
71
+ });
72
+ }
73
+
74
+ // Extract cover art from file using ID3 tags and create both thumbnail and full-size versions
75
+ async function extractCoverArt(
76
+ file: File
77
+ ): Promise<
78
+ | { fullSizeData: ArrayBuffer; thumbnailData: ArrayBuffer; type: string }
79
+ | undefined
80
+ > {
81
+ try {
82
+ const result = await extractAlbumArt(file);
83
+ if (result.success && result.albumArt) {
84
+ // Convert blob URL to File object for processing
85
+ const response = await fetch(result.albumArt);
86
+ const arrayBuffer = await response.arrayBuffer();
87
+ const blob = new Blob([arrayBuffer], { type: "image/jpeg" });
88
+ const imageFile = new File([blob], "albumart.jpg", {
89
+ type: "image/jpeg",
90
+ });
91
+
92
+ // Clean up the blob URL
93
+ URL.revokeObjectURL(result.albumArt);
94
+
95
+ // Process the image to create both full-size and thumbnail versions
96
+ const processResult = await processPlaylistCover(imageFile);
97
+
98
+ if (
99
+ processResult.success &&
100
+ processResult.imageData &&
101
+ processResult.thumbnailData
102
+ ) {
103
+ return {
104
+ fullSizeData: processResult.imageData,
105
+ thumbnailData: processResult.thumbnailData,
106
+ type: processResult.metadata?.format || "image/jpeg",
107
+ };
108
+ }
109
+
110
+ // Fallback: if processing fails, use original as both
111
+ return {
112
+ fullSizeData: arrayBuffer,
113
+ thumbnailData: arrayBuffer,
114
+ type: "image/jpeg",
115
+ };
116
+ }
117
+ return undefined;
118
+ } catch (error) {
119
+ console.warn(
120
+ `o noz! could not extract album art from ${file.name}:`,
121
+ error
122
+ );
123
+ return undefined;
124
+ }
125
+ }
126
+
127
+ // Main metadata extraction function
128
+ export async function extractMetadata(file: File): Promise<AudioMetadata> {
129
+ const filenameMetadata = extractMetadataFromFilename(file.name);
130
+
131
+ try {
132
+ const duration = await extractDuration(file);
133
+ const coverArt = await extractCoverArt(file);
134
+
135
+ return {
136
+ title: filenameMetadata.title || "unknown title",
137
+ artist: filenameMetadata.artist || "unknown artist",
138
+ album: filenameMetadata.album || "unknown album",
139
+ duration,
140
+ coverArtData: coverArt?.fullSizeData,
141
+ coverArtThumbnailData: coverArt?.thumbnailData,
142
+ coverArtType: coverArt?.type,
143
+ };
144
+ } catch (error) {
145
+ console.error("Error extracting metadata:", error);
146
+ return {
147
+ title: filenameMetadata.title || "unknown title",
148
+ artist: filenameMetadata.artist || "unknown artist",
149
+ album: filenameMetadata.album || "unknown album",
150
+ duration: 0,
151
+ };
152
+ }
153
+ }
154
+
155
+ // Process single file upload
156
+ export async function processAudioFile(file: File): Promise<FileUploadResult> {
157
+ try {
158
+ // Validate file type
159
+ if (!isAudioFile(file)) {
160
+ return {
161
+ success: false,
162
+ error: `unsupported file type: ${file.type}. Please upload an audio file.`,
163
+ };
164
+ }
165
+
166
+ // Validate file size
167
+ if (!validateFileSize(file)) {
168
+ return {
169
+ success: false,
170
+ error: `file too large: ${Math.round(file.size / 1024 / 1024)}MB. maximum size is 100MB.`,
171
+ };
172
+ }
173
+
174
+ // Extract metadata
175
+ const metadata = await extractMetadata(file);
176
+
177
+ // Create blob URL for audio playback with error handling
178
+ let blobUrl: string | undefined;
179
+ try {
180
+ blobUrl = URL.createObjectURL(file);
181
+ } catch (error) {
182
+ console.warn("Failed to create blob URL for file:", file.name, error);
183
+ // Continue without blob URL - it can be created later when needed
184
+ }
185
+
186
+ return {
187
+ success: true,
188
+ song: {
189
+ id: "", // Will be set by the database service
190
+ file,
191
+ blobUrl,
192
+ title: metadata.title || "unknown title",
193
+ artist: metadata.artist || "unknown artist",
194
+ album: metadata.album || "unknown album",
195
+ duration: metadata.duration || 0,
196
+ position: 0, // Will be set when adding to playlist
197
+ imageData: metadata.coverArtData,
198
+ thumbnailData: metadata.coverArtThumbnailData,
199
+ imageType: metadata.coverArtType,
200
+ fileSize: file.size,
201
+ createdAt: Date.now(),
202
+ updatedAt: Date.now(),
203
+ playlistId: "", // Will be set when adding to playlist
204
+ } as Song,
205
+ };
206
+ } catch (error) {
207
+ console.error("onoz! error processing audio file:", error);
208
+ return {
209
+ success: false,
210
+ error: `error processing file: ${error instanceof Error ? error.message : "unknown error"}`,
211
+ };
212
+ }
213
+ }
214
+
215
+ // Process multiple files
216
+ export async function processAudioFiles(
217
+ files: FileList | File[]
218
+ ): Promise<FileUploadResult[]> {
219
+ const fileArray = Array.from(files);
220
+ const results: FileUploadResult[] = [];
221
+
222
+ // Process files in parallel but limit concurrency to avoid overwhelming the browser
223
+ const BATCH_SIZE = 3;
224
+
225
+ for (let i = 0; i < fileArray.length; i += BATCH_SIZE) {
226
+ const batch = fileArray.slice(i, i + BATCH_SIZE);
227
+ const batchResults = await Promise.all(
228
+ batch.map((file) => processAudioFile(file))
229
+ );
230
+ results.push(...batchResults);
231
+ }
232
+
233
+ return results;
234
+ }
235
+
236
+ // Filter files to only include audio files
237
+ export function filterAudioFiles(files: FileList | File[]): File[] {
238
+ return Array.from(files).filter(isAudioFile);
239
+ }