funoteka 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/.env.example +90 -0
  2. package/CHANGELOG.md +104 -0
  3. package/DEPLOY.md +375 -0
  4. package/LICENSE +21 -0
  5. package/README.md +138 -0
  6. package/deploy/answers.schema.json +151 -0
  7. package/deploy/build.mjs +88 -0
  8. package/deploy/check-publishable.mjs +246 -0
  9. package/deploy/smoke.mjs +681 -0
  10. package/deploy/systemd/funoteka.env.example +41 -0
  11. package/deploy/systemd/funoteka.service +77 -0
  12. package/deploy/windows/funoteka-service.xml +58 -0
  13. package/deploy/windows/install-service.ps1 +158 -0
  14. package/dist/annotation/store.js +67 -0
  15. package/dist/api/admin-export.js +227 -0
  16. package/dist/api/admin-guard.js +181 -0
  17. package/dist/api/admin-library.js +113 -0
  18. package/dist/api/admin.js +1162 -0
  19. package/dist/api/annotation.js +193 -0
  20. package/dist/api/artistinfo.js +140 -0
  21. package/dist/api/audit.js +32 -0
  22. package/dist/api/auth.js +145 -0
  23. package/dist/api/bookmark.js +108 -0
  24. package/dist/api/browse.js +1241 -0
  25. package/dist/api/config-file.js +202 -0
  26. package/dist/api/config.js +285 -0
  27. package/dist/api/cover.js +304 -0
  28. package/dist/api/download.js +112 -0
  29. package/dist/api/envelope.js +162 -0
  30. package/dist/api/extensions.js +43 -0
  31. package/dist/api/genre.js +81 -0
  32. package/dist/api/health.js +35 -0
  33. package/dist/api/history.js +374 -0
  34. package/dist/api/idempotency.js +66 -0
  35. package/dist/api/keys.js +90 -0
  36. package/dist/api/log-file.js +65 -0
  37. package/dist/api/meta.js +1410 -0
  38. package/dist/api/playlist.js +311 -0
  39. package/dist/api/rescan.js +196 -0
  40. package/dist/api/roots.js +161 -0
  41. package/dist/api/router.js +133 -0
  42. package/dist/api/scanner.js +163 -0
  43. package/dist/api/search.js +82 -0
  44. package/dist/api/server.js +643 -0
  45. package/dist/api/settings.js +67 -0
  46. package/dist/api/stream.js +442 -0
  47. package/dist/api/stubs.js +183 -0
  48. package/dist/api/transcode.js +678 -0
  49. package/dist/api/user.js +73 -0
  50. package/dist/api/virtual.js +362 -0
  51. package/dist/api/visibility.js +21 -0
  52. package/dist/artist/apply.js +752 -0
  53. package/dist/artist/credit.js +95 -0
  54. package/dist/artist/folder.js +65 -0
  55. package/dist/artist/name.js +205 -0
  56. package/dist/artist/nfo.js +101 -0
  57. package/dist/artist/translit.js +79 -0
  58. package/dist/bookmark/store.js +54 -0
  59. package/dist/classify/classify.js +405 -0
  60. package/dist/classify/collision-name.js +109 -0
  61. package/dist/classify/folder-name.js +466 -0
  62. package/dist/classify/roles.js +277 -0
  63. package/dist/classify/shelf-name.js +178 -0
  64. package/dist/classify/tree.js +43 -0
  65. package/dist/cli/args.js +169 -0
  66. package/dist/cli/daemon.js +192 -0
  67. package/dist/cli/entry.js +25 -0
  68. package/dist/cli/keys.js +52 -0
  69. package/dist/cli.js +638 -0
  70. package/dist/cover/pick.js +58 -0
  71. package/dist/cover/picture.js +111 -0
  72. package/dist/cue/engine.js +759 -0
  73. package/dist/cue/match.js +142 -0
  74. package/dist/cue/parse.js +182 -0
  75. package/dist/cue/plan.js +286 -0
  76. package/dist/cue/track-name.js +120 -0
  77. package/dist/db/index.js +163 -0
  78. package/dist/db/issue.js +73 -0
  79. package/dist/db/ledger.js +84 -0
  80. package/dist/db/migrations/001_init.sql +196 -0
  81. package/dist/db/migrations/002_cue_track_flexible.sql +35 -0
  82. package/dist/db/migrations/003_cue_metadata.sql +14 -0
  83. package/dist/db/migrations/004_cue_performer.sql +12 -0
  84. package/dist/db/migrations/005_incremental.sql +23 -0
  85. package/dist/db/migrations/006_tags.sql +57 -0
  86. package/dist/db/migrations/007_artist_credit.sql +35 -0
  87. package/dist/db/migrations/008_issue_stage.sql +39 -0
  88. package/dist/db/migrations/009_cue_title.sql +20 -0
  89. package/dist/db/migrations/010_duration_source.sql +31 -0
  90. package/dist/db/migrations/011_track_title_source.sql +28 -0
  91. package/dist/db/migrations/012_file_container.sql +25 -0
  92. package/dist/db/migrations/013_cover_art.sql +28 -0
  93. package/dist/db/migrations/014_album_year.sql +17 -0
  94. package/dist/db/migrations/015_probe_method.sql +14 -0
  95. package/dist/db/migrations/016_tags_method.sql +20 -0
  96. package/dist/db/migrations/017_cover_indirect.sql +23 -0
  97. package/dist/db/migrations/018_release_year.sql +14 -0
  98. package/dist/db/migrations/019_file_tag_name_file_value.sql +20 -0
  99. package/dist/db/migrations/020_analyze_after_index.sql +16 -0
  100. package/dist/db/migrations/021_cue_index_files.sql +15 -0
  101. package/dist/db/migrations/022_sidecar_text.sql +19 -0
  102. package/dist/db/migrations/023_track_credit.sql +23 -0
  103. package/dist/db/migrations/024_playlists.sql +63 -0
  104. package/dist/db/migrations/025_playlist_sequence.sql +27 -0
  105. package/dist/db/migrations/026_playlist_source_file.sql +19 -0
  106. package/dist/db/migrations/027_lookup_indexes.sql +30 -0
  107. package/dist/db/migrations/028_annotations.sql +47 -0
  108. package/dist/db/migrations/029_scan_state_root_run.sql +28 -0
  109. package/dist/db/migrations/030_history.sql +108 -0
  110. package/dist/db/migrations/031_queue_position_and_plays.sql +46 -0
  111. package/dist/db/migrations/032_bookmarks.sql +27 -0
  112. package/dist/db/migrations/033_bookmark_order.sql +14 -0
  113. package/dist/db/migrations/034_junk.sql +33 -0
  114. package/dist/db/migrations/035_api_key.sql +38 -0
  115. package/dist/db/migrations/036_file_tag_first.sql +88 -0
  116. package/dist/db/migrations/037_admin_idempotency.sql +31 -0
  117. package/dist/db/migrations/038_cue_audio_index.sql +24 -0
  118. package/dist/db/sweep.js +109 -0
  119. package/dist/history/store.js +151 -0
  120. package/dist/inventory/inventory.js +294 -0
  121. package/dist/junk/marks.js +143 -0
  122. package/dist/junk/rule.js +93 -0
  123. package/dist/mcp/client.js +48 -0
  124. package/dist/mcp/server.js +175 -0
  125. package/dist/mcp/tools.js +348 -0
  126. package/dist/playlist/files.js +116 -0
  127. package/dist/playlist/import.js +260 -0
  128. package/dist/playlist/store.js +240 -0
  129. package/dist/probe/ffprobe.js +179 -0
  130. package/dist/run.js +120 -0
  131. package/dist/scan/kinds.js +49 -0
  132. package/dist/scan/scan.js +503 -0
  133. package/dist/scan/schedule.js +132 -0
  134. package/dist/scan/settle.js +172 -0
  135. package/dist/scan/walk.js +132 -0
  136. package/dist/search/index.js +41 -0
  137. package/dist/search/query.js +28 -0
  138. package/dist/stream/flac.js +541 -0
  139. package/dist/stream/mpeg.js +179 -0
  140. package/dist/stream/recode.js +393 -0
  141. package/dist/stream/rewrite.js +163 -0
  142. package/dist/stream/segment.js +624 -0
  143. package/dist/tags/apply.js +703 -0
  144. package/dist/tags/encode.js +164 -0
  145. package/dist/tags/first.js +130 -0
  146. package/dist/tags/flac.js +196 -0
  147. package/dist/tags/genres.js +123 -0
  148. package/dist/tags/id3v1.js +209 -0
  149. package/dist/tags/id3v2.js +754 -0
  150. package/dist/tags/mp4.js +534 -0
  151. package/dist/tags/mpeg.js +355 -0
  152. package/dist/tags/ogg.js +463 -0
  153. package/dist/tags/picture.js +71 -0
  154. package/dist/tags/read.js +269 -0
  155. package/dist/tags/types.js +51 -0
  156. package/dist/tags/vorbis-comment.js +155 -0
  157. package/dist/text/encoding.js +384 -0
  158. package/dist/text/site-name.js +37 -0
  159. package/dist/util/child.js +21 -0
  160. package/dist/util/names.js +97 -0
  161. package/funoteka.json.example +27 -0
  162. package/package.json +55 -0
@@ -0,0 +1,183 @@
1
+ import { ApiError, ERROR } from "./envelope.js";
2
+ /**
3
+ * The surface this server answers and does not fill.
4
+ *
5
+ * Everything here is a method a client may ask for, answered **empty by form**
6
+ * rather than refused (requirements:47, «Заглушки»). The difference is not
7
+ * cosmetic: a client that gets `unknown method` shows an error, retries, or
8
+ * decides this server is not Subsonic at all — while a client that gets a
9
+ * well-formed empty answer draws an empty list and carries on. Which of the two
10
+ * a client does with the six methods it calls on startup is the difference
11
+ * between a library that opens and a library that does not.
12
+ *
13
+ * **Every shape below is the specification's, not a guess.** Each was read from
14
+ * the endpoint's own page in `opensubsonic/open-subsonic-api`
15
+ * (`content/en/docs/Endpoints/<Name>.md`) — which names the element a method
16
+ * answers with, and, where an example exists, its fields. The rule the project
17
+ * keeps for the protocol applies here as everywhere: the spec is the source and
18
+ * a shape recalled is a shape invented. The two places the spec is silent are
19
+ * called out at the entry itself rather than filled in quietly.
20
+ *
21
+ * **What a stub is not.** It is not a claim that the server does something. The
22
+ * reads here answer "nothing" and mean it; the *writes* — `createShare`,
23
+ * `deleteUser`, `changePassword` — answer `ok` and change nothing, which is a
24
+ * lie of shape rather than of content, and it is the one the contract asked for
25
+ * by name. `wiki:3639` records what that costs: nothing in this collection is
26
+ * reachable through those methods, and a client that believes it created a share
27
+ * finds no share. The alternative — refusing — is what the contract rejects, and
28
+ * it lands on the same client that a missing method lands on.
29
+ */
30
+ /** An answer whose whole content is that there is none. */
31
+ const nothing = {};
32
+ /**
33
+ * A listing that holds nothing.
34
+ *
35
+ * The child array is *present and empty* rather than absent, and that is the
36
+ * point of the whole module: a client reading `topSongs.song` and finding no
37
+ * field at all is a client reading a shape it does not recognise, while one
38
+ * reading `[]` is a client reading a list of none.
39
+ */
40
+ const none = (container, child) => ({ [container]: { [child]: [] } });
41
+ /**
42
+ * What each method answers, keyed by the name the client asks with.
43
+ *
44
+ * The comments name what the method would hold if this server had it, because a
45
+ * reader of this file is deciding what to build next, and the list is the map of
46
+ * that.
47
+ */
48
+ export const STUBBED = new Map([
49
+ // Words about the music, which this server does not hold. The protocol has two
50
+ // shapes for them — the flat `lyrics` of 1.2.0 and the structured list of the
51
+ // OpenSubsonic extension — and both are answered, because a client asks by
52
+ // version, not by what this server would prefer.
53
+ ['getlyrics', { lyrics: {} }],
54
+ ['getlyricsbysongid', none('lyricsList', 'structuredLyrics')],
55
+ // Reviews and biographies from an external service. This server reads no
56
+ // external service (`requirements:47`, «Норма»), so there is nothing to say —
57
+ // and `getArtistInfo2`, which is about the *collection*, is answered for real
58
+ // beside these.
59
+ ['getalbuminfo', { albumInfo: {} }],
60
+ ['getalbuminfo2', { albumInfo: {} }],
61
+ ['getartistinfo', { artistInfo: {} }],
62
+ // Recomendations. Similarity needs a corpus this server does not compute, and
63
+ // the sonic path needs fingerprints.
64
+ ['getsimilarsongs', none('similarSongs', 'song')],
65
+ ['getsimilarsongs2', none('similarSongs2', 'song')],
66
+ ['gettopsongs', none('topSongs', 'song')],
67
+ ['getsonicsimilartracks', { sonicMatch: [] }],
68
+ ['findsonicpath', { sonicMatch: [] }],
69
+ // Podcasts: a channel, its episodes, and the fetches that keep them current.
70
+ //
71
+ // `downloadPodcastEpisode` is here and **not** among the byte-answering stubs,
72
+ // which its name invites a reader to get wrong — a review axis did. What it
73
+ // does is ask the server to *start* downloading, and the specification says
74
+ // so in its own words: «Request the server to start downloading a given
75
+ // Podcast episode», and «An empty `subsonic-response` element on success».
76
+ // The episode is not the answer; the answer is that the request was taken.
77
+ ['getpodcasts', none('podcasts', 'channel')],
78
+ ['getnewestpodcasts', none('newestPodcasts', 'episode')],
79
+ ['getpodcastepisode', { podcastEpisode: {} }],
80
+ ['refreshpodcasts', nothing],
81
+ ['createpodcastchannel', nothing],
82
+ ['deletepodcastchannel', nothing],
83
+ ['deletepodcastepisode', nothing],
84
+ ['downloadpodcastepisode', nothing],
85
+ // Internet radio: a station is a URL and a name, and this server holds none.
86
+ ['getinternetradiostations', none('internetRadioStations', 'internetRadioStation')],
87
+ ['createinternetradiostation', nothing],
88
+ ['updateinternetradiostation', nothing],
89
+ ['deleteinternetradiostation', nothing],
90
+ // Chat between listeners of one server. There is one listener.
91
+ ['getchatmessages', none('chatMessages', 'chatMessage')],
92
+ ['addchatmessage', nothing],
93
+ // Video: the collection is music, and `getCaptions` below is where the
94
+ // captions of a video that does not exist would go.
95
+ ['getvideos', none('videos', 'video')],
96
+ ['getvideoinfo', { videoInfo: {} }],
97
+ // Shares: a public link to a record. Nothing here is shared.
98
+ ['getshares', none('shares', 'share')],
99
+ ['createshare', none('shares', 'share')],
100
+ ['updateshare', nothing],
101
+ ['deleteshare', nothing],
102
+ // Accounts. `getUser` and `getUsers` beside these both answer for real — the
103
+ // one account this server has, and a list holding it — and the *management* is
104
+ // what is stubbed, which is the contract's line (requirements:47, «Заглушки»).
105
+ // `getUsers` was a stub here too until the operator read the two answers side
106
+ // by side and said which one was wrong; `wiki:3640` records it.
107
+ ['createuser', nothing],
108
+ ['updateuser', nothing],
109
+ ['deleteuser', nothing],
110
+ ['changepassword', nothing],
111
+ // The protocol's first generation, kept for clients that never moved on. Each
112
+ // is the same question as a method this server answers properly — `getAlbumList`
113
+ // against `getAlbumList2`, `search`/`search2` against `search3` — asked in a
114
+ // shape that cannot carry what the collection holds (no `byYear`, no artist
115
+ // credit, no OpenSubsonic fields). Answering emptily is honest here in a way it
116
+ // is not for the modern ones, and it is still better than a client reading
117
+ // "unknown method".
118
+ //
119
+ // `searchResult` is the one shape the specification does **not** define:
120
+ // `Responses/searchResult.md` is a stub of its own, and the endpoint page's
121
+ // example is `// TODO`. The three fields are the legacy v1 ones, and an empty
122
+ // `match` is what a client reads either way.
123
+ ['getalbumlist', none('albumList', 'album')],
124
+ ['search', { searchResult: { offset: 0, total: 0, match: [] } }],
125
+ ['search2', { searchResult2: { artist: [], album: [], song: [] } }],
126
+ // The one entry whose answer depends on what was asked — see `jukebox`.
127
+ ['jukeboxcontrol', jukebox],
128
+ ]);
129
+ /**
130
+ * The jukebox, which is the one stub whose shape depends on what was asked.
131
+ *
132
+ * Declared before the map that holds it so the entry reads as one list; a
133
+ * function declaration hoists, and the reference is resolved when the map is
134
+ * built.
135
+ *
136
+ * `action=get` returns the playlist and every other action returns the status
137
+ * (the specification's own words: «jukeboxStatus for all actions but get,
138
+ * jukeboxPlaylist for get»). A single empty payload would be wrong for one of
139
+ * the two, and a client that called `get` and was handed a status has been told
140
+ * the queue is empty in a field it will not look at. The status is the honest
141
+ * one for a machine that is not playing anything.
142
+ */
143
+ function jukebox(query) {
144
+ const status = { currentIndex: 0, playing: false, gain: 1, position: 0 };
145
+ return query.get('action') === 'get'
146
+ ? { jukeboxPlaylist: { ...status, entry: [] } }
147
+ : { jukeboxStatus: status };
148
+ }
149
+ /**
150
+ * The payload for a stubbed method, or undefined when the name is not one.
151
+ *
152
+ * Folded rather than kept beside the table because it is the same answer with
153
+ * one exception, and a second lookup in `router.ts` would be a second place to
154
+ * forget that the jukebox reads its question.
155
+ */
156
+ export function stubPayload(method, query) {
157
+ const entry = STUBBED.get(method);
158
+ if (entry === undefined)
159
+ return undefined;
160
+ return typeof entry === 'function' ? entry(query) : entry;
161
+ }
162
+ /**
163
+ * The methods that answer with *bytes* rather than with an envelope, and why
164
+ * there are no bytes.
165
+ *
166
+ * `пусто по форме` has no form here: the protocol's answer to these is an image,
167
+ * a caption file or a playlist, and an empty one of those is not an answer — a
168
+ * zero-byte image is a broken picture, where a client expects a picture or a
169
+ * refusal and handles both. The specification says so itself: `getAvatar`
170
+ * «returns the avatar image in binary form on success, or an XML document on
171
+ * error», and `hls` likewise. So the refusal is the protocol's, in the
172
+ * protocol's language, and it is not the "unknown method" a client cannot act
173
+ * on.
174
+ */
175
+ export const STUBBED_BYTES = new Map([
176
+ ['getavatar', 'this server keeps no avatars'],
177
+ ['getcaptions', 'this server holds no video, so there are no captions'],
178
+ ['hls', 'this server does not produce HLS'],
179
+ ]);
180
+ /** The refusal a byte-answering stub throws, before any header is written. */
181
+ export function stubRefusal(reason) {
182
+ return new ApiError(ERROR.notFound, reason);
183
+ }