@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
package/docs/TODO.md ADDED
@@ -0,0 +1,167 @@
1
+ # TODO
2
+
3
+ in rough prio
4
+
5
+ ## 0
6
+
7
+ [x] so need to refactor the "standalone" mode a bit; so i want to use playlistz.js convention (as in <script src="playlistz.js"></script> in html) file for one or more playlists data, instead of embedding into .html file, then don't need to template the .html file...
8
+
9
+ [x] some kind of interactive (and also script-able) cli mjs script to take a bunch of audio files + playlist metadata and generate a standalone playlistz.js, maybe also a .html and sw.js generator? then i could just build a freqhole-playlistz.js file. is there a way i could have the same freqhole-playlistz.js be used in a browser like <script type="module" src="freqhole-playlistz.js"></script> as well as in node like `node freqhole-playlistz.js --generate-html` or even start a lil' express static file server?
10
+
11
+ [] freqhole could dispense takeout download linkz that generated playlist data.zip with this (package lib thing?)
12
+ [] some kind of npm or github package lib thing, so prolly CI github action .yml filez to build + deploy npm package, some kind of cli tool, single .html + sw.js file
13
+
14
+ ## 1
15
+
16
+ [x] need more immediate ui feedback for when drag-n-drop is loading playlist
17
+
18
+ [x] play/pause needs some more work so it resumes position. song positions (e.g. the background fill on the song row) shouldn't disappear when pausing song, or skipping to a different track. when playing a track, start playback at last-left-off position, unless that position is >95% of the track time, then can restart at the beginning but don't clear the background position fill until the track starts over again at the beginning (or if the user manually seeks the track time). so play/pause of each song should track per-song playback state, and the global play/pause should track playing and pausing the current song (right now it just starts back at the beginning). there should be code that's doing this? please make sure to dig before adding new code. keep test coverage up.
19
+
20
+ [x] this `<` `>` menu toggle is still not right =? could it be at the bottom of the page, after all the playlist songz? orrrr could it be only shown in the "modal" window view? so like edit buttonz on playlist will bring open a modal that's sized to the playlist size but include the `<` `>` menu toggle button then can show the sidebar stuff then! OH! the edit modalz could be in the sidebar content area! then there could be a `<` toggle button to current all-playlistz, search, + new playlist stuff!! yesss.
21
+
22
+ i'd like to re-work the edit playlist + edit song modals and how this `<` `>` sidebar menu toggle works.
23
+
24
+ so first, the edit playlist modal doesn't need to be a "modal", how i'd like it to work is by dropping out the song rows and then rendering a panel in the space where the song rows were that's the same content as the edit playlist modal. i'd like to animate the transitions here, so that the first, say 5 song rows drop out one by one, and then the animaton ramps up and clears them all out, so like one by one (in quick succession) each row falls straight down out of view. then the edit panel expands to fill in the space. when it's close, the reverse happens, panel shrinks down and song rows go back up.
25
+
26
+ when a song edit modal opens, in a similar way, the selected song row stays and all the other song rows + playlist header either drops down (if after/below the current song) or goes up and out if above the current song, including the playlist header section, then the edit song panel fills into the now-open space. when exiting, the animation goes in reverse, so the panel shrinks down to close and the other rows come back in from above or below.
27
+
28
+ right, so then it's only when one of these edit panels are open, that i want to show sidebar `<` `>` menu toggle button and sidebar contents. i guess also want to show the sidebar if there's 0 playlists.
29
+
30
+ ## 2
31
+
32
+ [x] edit playlist form controlz + idb persistence for how background image filter is set
33
+ [] does sw.js need more work?
34
+
35
+ ## 3
36
+
37
+ [] i guess it could have a midden iroh wasm client...
38
+
39
+ so this is a bigger ask: i'd like to introduce iroh p2p networking into playlistz. i'd like for it to be interoperable with the existing freqhole setup i've got already over in tomb/client/midden, and i think playlistz would also need to use the freqhole-api-client over in tomb/client-codegen/freqhole-api-client. the idea, for now is that playlistz can just use `file:../tomb/...` npm package ref for freqhole-api-client and midden (or we can just cp midden's dist output; i'll look into publishing these two packages at some point, maybe, for now just want to use file: path link). the playlistz data model will need some changes to fit better with freqhole's playlist schema, namely supportig multiple cover and song images (with one having a primary property), song image "waveform" types, playlist + song entity url linkz, and song lyrics.
40
+
41
+ for the playlistz app interface, i want to add a new "share" icon button to the playlist, after the zip download icon button, that will open a share panel (similar to the edit playlist panel), that will let a user do an initial midden endpoint setup, so setup things like a name, avatar image, some settings to set if all playlists are public or if they require knock access requests.
42
+
43
+ i'd like to start a new "freqhole-playlistz" ALPN that both freqhole and the playlistz web app can use to communicate. spume can track and generate base64 share links that contain node ids and some other helpful info to display a share link, e.g. https://spume.freqhole.net/#?share=eyJpIjoiOTRjZGZlMjhjMWE1MDJhOCIsImsiOiJwbGF5bGlzdCIsInMiOnsibiI6IjBkMGRlNTQwY2E1NWMzMzdkMzZhNmEwN2M3N2I1NGMwYTJhMjM0NDgyOWNiMzE0MDczY2U1YTA5NGVkZGQyNDAifSwidCI6ImRhbmNlMnRoaXMiLCJ2IjoxfQ so i'd like playlistz app to have a similar way to share. the idea would be playlistz app can read freqhole playlistz that are shared with it (and the user has proper access and the ability to send knock requests). otherwise the playlistz web app can share it's playlistz with other playlistz app users (e.g. without freqhole) so either anyone (if public) or require an access knock request (want to borrow as much from freqhole's knock stuff as possible here). the browser node will need to do like accept_bi on the iroh endpoint so it can process incoming requests from other nodes.
44
+
45
+ while i don't plan for this playlistz app to run on the same domain as spume, i want it to be able to read spume's indexed db, opfs, and iroh secret key if it is. so ideally we'd be able to share a lot of the same indexed db logic as well. in some sensible way, so that tomb/\* code doesn't have to know about playlistz app, so maybe some this has to come out of freqhole-api-client, or just have the same indexed and opfs key and schemas so it will interop-- gonna need to find a delicate balance here.
46
+
47
+ so, then, first i'd like you to dig around the tomb/ and playlistz/ code to get a better sense of how this will be implemented in these codebases and then start a new .md file in playlistz/docs/ with a plan for implementing all this stuff and track progress as we work thru and complete all the work, keep open questions out of the plan doc and let's just have a conversation about anything that comes up. ideally you would include as much technical details as possible in the plan doc, and then dispatch claude sonnet 4.6 subagents for as much work here as possible. this is a lot of "feature" but also a lot of the code needed to do this has pretty much been worked out in tomb/, so a big part of this is will be digging around the code there and finding good ways to share it in playlistz/ app, which might mean some refactoring or just re-implementing the same patterns in playlistz/ if it's too much of a chore to refactor and share (ideally i'd think a good amount of code from tomb could be shared, atleast in terms of biz logic, of course playlistz will have it's own presentational aesthetic and frontend ui implementation)
48
+
49
+ FREQHOLE_ALPN.to_vec(),
50
+ AUTOMERGE_ALPN.to_vec(),
51
+ FRIENDZ_ALPN.to_vec(),
52
+ ADMIN_ALPN.to_vec(),
53
+ EVENTS_ALPN.to_vec(),
54
+ iroh_blobs::ALPN.to_vec(),
55
+
56
+ in tomb/client/midden/src/lib.rs
57
+
58
+ # THU, JUN 11, 2026
59
+
60
+ so i guess if we could do webauthn-rs passkey stuff (in a https:// web browser) then it could be perhaps possible for two nodes to validate a user? like if the charnel tauri app and rathole cli could be, more or less, a hub server, where users can register their passkey for that user, then with passkey validation, they could write a new peer node id device for access? like a https://spume.freqhole.net/?query_auth_node_id=abcdef123456..&return_to_node_id=0202aaabbbdcba.. could open, init a new iroh endpoint that calls the query auth node id and validates passkey, then inserts a new allowed peer node for self, and then sends some kind of success/fail token back to the return_to node?
61
+
62
+ [] double click playlist in sidebar to start playing it.
63
+ [] move share modal from sidebar to main page content, try to emulate the edit playlist layout, so will need to animate out the current playlist details and then show like:
64
+
65
+ ```
66
+ [share modal, where the playlist header is, with transparent background so current playlist background image comes thru. x (close icon button in top right)]
67
+
68
+ [$(playlist img) $(playlisttitle) `<` `>` buttons to show share detail panel for stuff related to playlist, currently this is only the share link text input + copy to clipboard button; will include other stuff here in the near future]
69
+ ```
70
+
71
+ so then also move the share button from the sidebar nav, back into the playlist detail action buttons: after the edit button, before the download button.
72
+
73
+ ---
74
+
75
+ okay, so i'm hoping you can help me work on the playlistz/ code here in this workspace. what i'm hoping to do is re-work the layout and navigation to be a bit more consistent. so currently, the main layout for a playlist is like:
76
+
77
+ a playlist header section
78
+
79
+ ```
80
+ PLAYLIST_NAME (editable div) | |
81
+ PLAYLIST_DESCRIPTION (also editable div) | cover |
82
+ song count + total time | img |
83
+ PLAY/PAUSE BUTTON [ACTION BUTTONS]
84
+ ```
85
+
86
+ and then under that song rows. when the edit mode is toggled, the header stays in place, and the song rows section switched out to the edit panel and the edit button in the action buttons section of the header gets a magenta border style. this is working great and how i'd like it to work.
87
+
88
+ however, then, the "all playlists" view and action button, and the share view and action button don't quite work like this.
89
+
90
+ the share view should be easy, we just need to keep the playlist header section in place, like the edit mode does it.
91
+
92
+ and then a similar thing for the all playlists view, the currently selected playlist header section then just becomes a row w/ sticky top + bottom position. and then the other playlists are added as rows also with the same header section as row. currently the row view here for all playlists is much different, which i want to change, and re-use the same playlist header section for row view as much as possible. it will probably need some variation when in row-form. having a dedicated row for adding a playlist is great, and i'd like to keep that, it should also be sticky bottom position (and play well with the other active playlist sticky row).
93
+
94
+ then the all playlists view and share view don't need `x` close or headers shown.
95
+
96
+ so thoughts about this? i really hope it's clear what i'm asking here, we'll probably need to refactor the edit playlist + song row container into a more generic app layout structure of files and components and hooks etc. needed to support this. do you have any questions for me about this work?
97
+
98
+ ---
99
+
100
+ some issues now with the narrow mobile views? i think the playlist header
101
+
102
+ 71144
103
+
104
+ ---
105
+
106
+ some more things:
107
+
108
+ there should be a search query input somewhere, i think on the `new playlist` row in all playlists view? it needs so many playlists to show, let's remove that, and show a search input all the time. the search input can also take share links, then we can remove the `open a share link` and `browse a peer's playlistz` in the share panel (need to make it so it can figure out if a knock is needed)-- the search bar should be able to detect a node_id or a share link and handle accordingly.
109
+
110
+ for now, i want to hide the `node id:` display in the share panel.
111
+
112
+ also when there's no knocks, we don't need to show any empty state, like:
113
+
114
+ ```
115
+ knock inbox
116
+ no pending knockz
117
+ ```
118
+
119
+ when browsing a users playlists, it should work the same way all playlists does in addition to a new field, after the date last modified in the playlists rows, that includes the display name of the remote user (and possibly an avatar, can we work this into the iroh sharing? might be more involved.); remember we need to keep the currently selected playlist visible in this all playlists view.
120
+
121
+ i also want to work more on a "collaborative" playlist sharing setting, where other users can do all the automerge syncing stuff so all the peers that are allowed to edit the playlist, sync together. otherwise, if not a collaborative playlist (or a peer that isn't allowed to collaborate in editing the playlist) should not send their automerge docs changes back. however, i do want to be able to let the peer keep getting playlist edits. it would be nice to have some ui feedback for when a playlist is in this "subscribed" mode and will get updates, i suppose then what to do if the user makes local changes to a subscribed playlist and then new changes come in? i feel like we should just let automerge sync, in the even there's any conflicts, it'd be great to show something in the share edit panel that let's the user either: 1. accept all the incoming changes (their local changes will get overwritten) or 2. add the remote playlist as a new playlist, and then unsubscribe their local copy from the remote automerge document since it's now a standalone fork. every playlist is gonna need it's own list of peer nodes that can collaborate.
122
+
123
+ i think also, each playlist is gonna need to have it's sharing mode explicitly set and started, so by default a playlist should not be shared until the user explicity chooses something from the "who can browse my playlistz?" options, which would be `public` (anyone can read and sync updates, but not make edits), knock first. and then i'm not quite sure where the new "collaborative" toggle would come in, or how it would work. maybe it could work like this:
124
+
125
+ so when a playlist creator shares a playlist with another user, say it's "knock first" setting, they paste the link into the search input, or just click it, then they go thru the knock flow (oh by the way the knock requests should also include a short free-form message text with the label "say who you are and mention something only the admin would know (but no passwords or secrets!)"), then when the playlist is added the UI is in, like read-only mode, so no new songs can get added, the name or description can't be edited, no new images or drag and drop reordering. in the edit menu, we can add two buttons, one that will turn the playlist into just a local fork copy that's basically like a user's own playlist and the other is a "collaborate" button that then will issue another knock access request to be a collaborator.
126
+
127
+ so then in the playlist creator's share panel, need to be able to accept both kinds of knock requests, and manage who's collaborators.
128
+
129
+ for now i want to keep the global share endpoint on/off toggle and display name, but i'd like to move the display name to the top of the share panel, and have the name just be a short text (like a pill badge) and then it can be clicked to open a text-input that can change the display name and then `x` to close (will auto set the displayname on key change events so no save button needed), perhaps this is where a user could set an avatar image? see skein/ for examples on how i handle profile avatars?
130
+
131
+ ---
132
+
133
+ remote "playlist deleted" toast
134
+
135
+ ✗registration failed: failed to start registration
136
+
137
+ 6d8bdc333c74279db9bb090d9f2d61c0ec7b79e1da223e4accc4435adb1e6ae7
138
+ 2914-whoosh-snail-koala-yodel
139
+
140
+ https://www.youtube.com/watch?v=HJgjZ6vbHI0&list=RDHJgjZ6vbHI0&start_radio=1
141
+
142
+ if there's precheck config, add review step to add music fetch from url flow so user can confirm what yt-dlp will fetch
143
+
144
+ route passkeys over p2p! browser users can use passkey to auth with a remote (in addition to knock access requests and invite codes). also can link out to spume.freqhole.net so charnel app users (desktop or android apps) so users can use browser for passkey.
145
+
146
+ ---
147
+
148
+ [x] update freqhole.net docs for recent features
149
+
150
+ [x] yank ci job that tries to update .mdx file updated at, just have the single script that reads git commits (force?) update them all in one go, i can just run this locally on my machine where i have the git history (unlike ci).
151
+
152
+ [x] after deleting a playlist, need to route to a different playlist (or show empty state if the last playlist was deleted).
153
+
154
+ [x] is it possible to register a new passkey with a web browser client that already has access to a remote? i guess i just need to test this more.
155
+
156
+ [x] users should be able to generate their own account-link invite codes. should be set to pretty quick expire time (say maybe around 30mins or 1hr). users should also be able to update their own usernames (but not to a username that's already in use in the db, so need friendly message for this case: "ohey, that handle is already taken; if you ask the admin nicely, they might be able to help."). perhaps there's a user profile view in the settings, linked where the "manage passkeys" is, so remove the "manage passkeys" link there and move the link to the username + role (after the "singed in as") and then add a new settings view, we can probably just add the passkey stuff from `#/settings/remotes/$(remote_name)/passkeys` to this new user profile view that can do all this stuff?
157
+
158
+ [ ] sync all music from library to another remote (i.e. push all music files, images, and most all metadata to a remote, de-dupe by blake3 or sha256 hashes). where in the ui to add this? maybe it's a rathole thing? would be nice in spume so collections built up in web browser can get synced to a more stable remote. >----
159
+ ------> OR MAYBE: like "federation" so two freqhole instances can share up to all the sqlite (or indexed db) db data? but otherwise selectively pick & choose: like share these users, passkeys, or these tags, or just user favorites and playlists, etc etc etc.
160
+
161
+ [x] publish freqhole-api-client and midden to npm so it's easier to use in playlistz. so need to setup @freqhole npm org package namespace and update package names, and then publish once from local machine, then add ci to publish new packages on tag (avoid draft releases). >----> NEED TO PUBLISH ONCE FROM LOCALHOST:
162
+
163
+ [ ] playlistz <-> freqhole playlist sync
164
+
165
+ [ ] has the time come to improve the last_seen_at user peer nodes? would make gossip and updating peerz when playlist changes a bit easier
166
+
167
+ [ ] graph viz has bugz where sometimes a selected artist causes the graph to collapse and then the selected artist node disappears and a bunch of hub nodes are rendered but it's, like real wonky.
@@ -0,0 +1,134 @@
1
+ # bundle embedding plan
2
+
3
+ the zip bundle needs to include `freqhole-playlistz.js` and `freqhole-playlistz-cli.mjs`
4
+ so that the standalone `index.html` is fully self-contained (no network required).
5
+
6
+ ## current approach (problems)
7
+
8
+ `buildPlaylistZip` fetches the bundle at zip-build time:
9
+
10
+ ```ts
11
+ fetch(`${window.location.origin}/freqhole-playlistz.js`);
12
+ ```
13
+
14
+ problems:
15
+
16
+ - in vite dev mode, `origin/freqhole-playlistz.js` may not be served unless
17
+ `npm run build` has been run and the dev server is set to serve `dist/`
18
+ - in spume/charnel (tauri), the origin is `tauri://localhost` and the asset
19
+ may or may not be registered depending on tauri's asset protocol config
20
+ - the caller must pass `appBundleUrl` to override; the fallback is fragile
21
+ - the cli build path (`freqhole-playlistz-cli.mjs`) has the same fragility
22
+
23
+ ## option A: seed zip baked into the package (recommended)
24
+
25
+ as part of the playlistz package build (`npm run build`), generate a
26
+ `dist/bundle-seed.zip` that contains only the two js files:
27
+
28
+ ```
29
+ dist/bundle-seed.zip
30
+ freqhole-playlistz.js
31
+ freqhole-playlistz-cli.mjs
32
+ ```
33
+
34
+ `buildPlaylistZip` opens this seed zip and adds playlist content (audio, images,
35
+ html, playlistz.js) into it, rather than starting from scratch. this means:
36
+
37
+ - the bundle is always available as a package asset (no network request)
38
+ - callers pass nothing extra - it just works
39
+ - the zip builder imports the seed as a base64-encoded or raw buffer included
40
+ in the package (via `?url` or `?raw` vite import, or `fs.readFileSync` in node)
41
+
42
+ ### implementation steps
43
+
44
+ 1. add a `build:seed-zip` npm script that runs after `build:standalone`:
45
+
46
+ ```
47
+ node scripts/build-seed-zip.mjs
48
+ ```
49
+
50
+ reads `dist/freqhole-playlistz.js` + `dist/freqhole-playlistz-cli.mjs`,
51
+ builds a zip, writes to `dist/bundle-seed.zip`
52
+
53
+ 2. in `zipBuilder.ts`, import the seed zip:
54
+
55
+ ```ts
56
+ // vite: import as url so it can be fetched from the package
57
+ import seedZipUrl from "../../dist/bundle-seed.zip?url";
58
+ // or in node/cli context: read via fs
59
+ ```
60
+
61
+ then `JSZip.loadAsync(await fetch(seedZipUrl).then(r => r.arrayBuffer()))` to
62
+ get a base zip, then add content into it.
63
+
64
+ 3. the cli path can read the seed zip via `fs.readFileSync` relative to the
65
+ package root - no network request needed.
66
+
67
+ ### dev-mode handling
68
+
69
+ in vite dev mode, `?url` imports still work (vite serves them as assets). the
70
+ seed zip is static content, so vite just serves the file. **callers no longer
71
+ need to pass `appBundleUrl`** at all.
72
+
73
+ ---
74
+
75
+ ## option B: fetch from cdn / freqhole.net
76
+
77
+ provide fallback urls for npm cdn and a self-hosted mirror:
78
+
79
+ ```ts
80
+ const BUNDLE_URLS = [
81
+ `${window.location.origin}/freqhole-playlistz.js`, // local server (preferred)
82
+ "https://cdn.freqhole.net/playlistz/0.0.1/freqhole-playlistz.js",
83
+ "https://unpkg.com/@freqhole/playlistz@0.0.1/dist/freqhole-playlistz.js",
84
+ "https://cdn.jsdelivr.net/npm/@freqhole/playlistz@0.0.1/dist/freqhole-playlistz.js",
85
+ ];
86
+ ```
87
+
88
+ problems with option B:
89
+
90
+ - requires publishing the package to npm (or maintaining a cdn)
91
+ - requires a network request at zip-build time (breaks offline use)
92
+ - version pinning is manual - easy to get stale bundles
93
+ - does not solve the vite dev mode problem
94
+
95
+ option B is useful as a **last-resort fallback** (e.g. if the seed zip is
96
+ somehow missing from the package), not as the primary mechanism.
97
+
98
+ ---
99
+
100
+ ## option C: embed the bundle as a base64 string at build time
101
+
102
+ embed the bundle bytes directly into the js bundle via a vite/rollup plugin or
103
+ a `?raw` import followed by btoa. this avoids a separate zip asset.
104
+
105
+ problems:
106
+
107
+ - doubles the package size (bundle is ~8MB; base64 is ~11MB)
108
+ - the embedded bundle cannot be updated without rebuilding the package
109
+ - not practical for the cli path
110
+
111
+ ---
112
+
113
+ ## recommended approach
114
+
115
+ **primary**: option A (seed zip baked into the package).
116
+
117
+ **fallback order when seed zip is unavailable**:
118
+
119
+ 1. try `${window.location.origin}/freqhole-playlistz.js` (local server)
120
+ 2. warn and omit the bundle from the zip (zip still works for playing
121
+ on an http server that serves the bundle separately)
122
+
123
+ ---
124
+
125
+ ## vite dev mode notes
126
+
127
+ in dev mode (`npm run dev`), vite does not serve `dist/`. to test the zip
128
+ bundle download in dev mode, either:
129
+
130
+ - run `npm run build:standalone` first (generates `dist/freqhole-playlistz.js`)
131
+ and configure vite to serve `dist/` as a static folder
132
+ - or use option A's seed zip which vite serves as a static asset import
133
+
134
+ this is tracked as a known limitation until option A is implemented.
@@ -0,0 +1,184 @@
1
+ # standalone refactor plan
2
+
3
+ ## goal
4
+
5
+ refactor the standalone build from a single self-contained `freqhole-playlistz.html` (with all js/css inlined) into:
6
+
7
+ - `freqhole-playlistz.js` - dual-use ESM bundle: registers web component in browser, runs as CLI in Node
8
+ - `playlistz.js` - data file (generated by cli or freqhole takeout); sets `window.__PLAYLISTZ__`
9
+ - `index.html` - static, minimal, generated once by cli; never needs to be re-templated
10
+ - `sw.js` - static service worker, generated by cli
11
+
12
+ the `index.html` becomes truly static - only `playlistz.js` and `data/` change when playlist content changes.
13
+
14
+ `playlistz.js` is generated by two different paths that share the same template functions:
15
+
16
+ - **in-app download**: the existing "download zip" button generates `playlistz.js` + `index.html` + `sw.js` and bundles them into a zip
17
+ - **cli**: `npx freqhole-playlistz --init` / `--generate-data` writes the same files to disk
18
+
19
+ both paths import the shared template functions from `src/utils/standaloneTemplates.ts`.
20
+
21
+ ## output directory layout
22
+
23
+ ```
24
+ output/
25
+ freqhole-playlistz.js # app bundle (from npm or built locally)
26
+ playlistz.js # playlist data (generated by cli)
27
+ index.html # static shell (generated by cli)
28
+ sw.js # service worker (generated by cli)
29
+ data/
30
+ song1.mp3
31
+ song2.mp3
32
+ playlist-cover.jpg
33
+ song1-cover.jpg
34
+ ```
35
+
36
+ ## freqhole-playlistz.js structure
37
+
38
+ the built file wraps everything in environment guards:
39
+
40
+ ```js
41
+ // css injector + solid web component bundle, guarded for browser
42
+ const isBrowser =
43
+ typeof window !== "undefined" && typeof customElements !== "undefined";
44
+ if (isBrowser) {
45
+ // inject compiled css into document.head at runtime
46
+ (() => {
47
+ const s = document.createElement("style");
48
+ s.textContent = "...";
49
+ document.head.appendChild(s);
50
+ })();
51
+ // solid web component registration
52
+ // ...vite-built bundle...
53
+ }
54
+
55
+ // node cli entrypoint
56
+ const isNode = typeof process !== "undefined" && process.versions?.node;
57
+ if (isNode) {
58
+ const { fileURLToPath } = await import("node:url");
59
+ const isMain = process.argv[1] === fileURLToPath(import.meta.url);
60
+ if (isMain) {
61
+ // cli dispatch
62
+ }
63
+ }
64
+ ```
65
+
66
+ format must be ESM (`format: 'es'`) to support `import.meta.url` and top-level `await` in both environments.
67
+
68
+ ## playlistz.js data format
69
+
70
+ supports one or more playlists:
71
+
72
+ ```js
73
+ window.__PLAYLISTZ__ = [
74
+ {
75
+ playlist: { id, title, description, rev, imageExtension, imageMimeType, safeFilename },
76
+ songs: [ { id, title, artist, album, duration, originalFilename, fileSize, sha, ... }, ... ]
77
+ },
78
+ // ...more playlists
79
+ ];
80
+ ```
81
+
82
+ the structure is validated with Zod schemas defined in `src/utils/standaloneTemplates.ts`. this is used:
83
+
84
+ - in `standaloneService.ts` when the web component reads `window.__PLAYLISTZ__` at init
85
+ - in the cli when reading a user-provided `playlistz.js` or generating one from audio files
86
+ - in the download zip service when assembling the export bundle
87
+
88
+ the web component checks `window.__PLAYLISTZ__` on init and calls `initializeStandalonePlaylist()` for each entry. `standaloneService.ts` already handles single-playlist init - needs to be extended to loop over multiple entries.
89
+
90
+ ## shared template utilities (`src/utils/standaloneTemplates.ts`)
91
+
92
+ a new file that both the browser app and the Node cli can import. browser-compatible (no Node-only APIs). exports:
93
+
94
+ - `FreqholePlaylistSchema` - Zod schema for a single `{ playlist, songs }` entry (replaces raw `StandaloneData` interface in `standaloneService.ts`)
95
+ - `FreqholePlaylistzSchema` - Zod schema for `window.__PLAYLISTZ__`, i.e. `z.array(FreqholePlaylistSchema)`
96
+ - `generatePlaylistzJs(playlists)` - serializes the entries array to `window.__PLAYLISTZ__ = [...];
97
+ ` string
98
+ - `generateIndexHtml()` - returns the minimal static `index.html` as a string; template is read from `src/templates/index.html` at Vite build time and inlined as a string constant
99
+ - `generateSwJs()` - returns `sw.js` content as a string; `public/sw.js` is read at Vite build time and inlined as a string constant
100
+
101
+ both `generateIndexHtml()` and `generateSwJs()` just return pre-baked string constants - no runtime file I/O in browser or Node.
102
+
103
+ the download zip service (`playlistDownloadService.ts`) already generates the zip bundle - it will call these functions instead of its own templating. the cli calls the same functions and writes files to disk.
104
+
105
+ ## changes required
106
+
107
+ ### phase 1 - build refactor (`build-component.js`)
108
+
109
+ - [x] output format changed to `'es'` for vite rollup, then post-processed to IIFE by esbuild
110
+ - [x] css minified via esbuild `transform` with `loader: 'css'`, newlines stripped, injected as a string constant prepended to the browser bundle
111
+ - [x] css asset removed from bundle output (no separate `.css` file)
112
+ - [x] emit `freqhole-playlistz.js` instead of inlining js into html
113
+ - [x] browser bundle wrapped in `if (isBrowser) { ... }` shell
114
+ - [x] `if (isNode) { ... }` cli shell appended (stub for phase 6)
115
+ - [x] emit static `index.html` from `generateIndexHtml()`
116
+ - [x] emit `sw.js` from `readServiceWorker()`
117
+ - [x] removed `generateStandaloneHtml()` from `build-component.js`
118
+ - [x] esbuild `transform` post-pass: minifies js + css together, produces `.map` sourcemap file, outputs IIFE format (no `type="module"` needed, works on `file://` urls)
119
+ - [x] `index.html` uses plain `<script src>` (not `type="module"`) for `file://` compatibility
120
+
121
+ ### phase 2 - shared template utilities (`src/utils/standaloneTemplates.ts`)
122
+
123
+ - [x] `zod` added as a dependency
124
+ - [x] `FreqholePlaylistSchema` and `FreqholePlaylistzSchema` defined; inferred types exported
125
+ - [x] `generatePlaylistzJs(playlists)` implemented
126
+ - [x] `generateIndexHtml()` implemented (plain `<script src>`, no `type="module"`)
127
+ - [x] `generateSwJs()` implemented via `?raw` import of `public/sw.js`
128
+ - [x] unit tests for all three generators and schema parse/reject cases
129
+
130
+ ### phase 3 - web component init (`web-component.tsx`, `components/index.tsx`)
131
+
132
+ - [x] `connectedCallback` checks `window.__PLAYLISTZ__`, parses with `FreqholePlaylistzSchema`, calls `initializeStandalonePlaylist()` for each entry
133
+ - [x] `window.STANDALONE_MODE` / `window.DEFERRED_PLAYLIST_DATA` mechanism removed
134
+ - [x] `global.d.ts` updated with `__PLAYLISTZ__` type declaration
135
+
136
+ ### phase 4 - multi-playlist standalone support (`standaloneService.ts`)
137
+
138
+ - [x] raw interfaces replaced with Zod-inferred types from `standaloneTemplates.ts`; `StandaloneData` kept as a backwards-compat alias
139
+ - [x] `initializeAllStandalonePlaylists(playlists)` added as array wrapper
140
+ - [x] sha-based smart update logic unchanged
141
+ - [x] tests updated with multi-playlist init case
142
+
143
+ ### phase 5 - download zip refactor (`playlistDownloadService.ts`)
144
+
145
+ - [x] `generatePlaylistzJs()`, `generateIndexHtml()`, `generateSwJs()` used instead of inline templating
146
+ - [x] zip layout: `playlistz.js` + `index.html` + `sw.js` in root, audio/image files in `data/`; `freqhole-playlistz.js` not included
147
+ - [x] no more `fetch('./sw.js')` network call during zip generation
148
+ - [x] download service tests updated
149
+
150
+ ### phase 6 - cli (`dist/freqhole-playlistz-cli.mjs`)
151
+
152
+ - [x] two-file output: `freqhole-playlistz.js` (browser IIFE, no Node globals) + `freqhole-playlistz-cli.mjs` (Node ESM, no browser globals)
153
+ - [x] `--help` - usage
154
+ - [x] `--http [dir]` - static file server with range request support; defaults to `./data`; respects `PORT` env var
155
+ - [x] `--check [file]` - validates `playlistz.js` (defaults to `./playlistz.js`); field-level error messages; checks audio + image files exist on disk; skips `http://`/`https://` urls
156
+ - [x] `--init <dir>` - writes `index.html` + `sw.js` (baked in at build time) to target dir
157
+ - [x] `--generate-data <audio-dir>` - reads audio files, generates `playlistz.js` via `generatePlaylistzJs()`
158
+
159
+ cli source lives in `src/cli/` with modules: `index.ts` (dispatch), `http.ts` (server), `check.ts` (validator), `init.ts` (file writer).
160
+
161
+ ### phase 7 - npm package setup
162
+
163
+ - [x] add `"bin": { "freqhole-playlistz": "./dist/freqhole-playlistz.js" }` to `package.json`
164
+ - [x] add `"type": "module"` to `package.json` if not already set
165
+ - [x] verify `npx freqhole-playlistz --help` works from a clean install
166
+
167
+ ## testing considerations
168
+
169
+ - existing `standaloneService.test.ts` covers single-playlist init - extend with multi-playlist cases
170
+ - `standaloneTemplates.ts`: unit tests for all three generators and schema parse/reject cases
171
+ - new unit tests for cli arg parsing and file generation functions
172
+ - the http range request server should have tests for partial content responses
173
+ - `web-component.tsx` init path needs tests for `window.__PLAYLISTZ__` detection
174
+ - `playlistDownloadService.ts`: verify zip output contains correct files with correct content
175
+
176
+ ## progress
177
+
178
+ - [x] phase 1 - build refactor
179
+ - [x] phase 2 - shared template utilities + zod schemas
180
+ - [x] phase 3 - web component init
181
+ - [x] phase 4 - multi-playlist standalone support
182
+ - [x] phase 5 - download zip refactor
183
+ - [x] phase 6 - cli (`--http`, `--check`, `--init`)
184
+ - [x] phase 7 - npm package setup