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.
- package/.env.example +90 -0
- package/CHANGELOG.md +104 -0
- package/DEPLOY.md +375 -0
- package/LICENSE +21 -0
- package/README.md +138 -0
- package/deploy/answers.schema.json +151 -0
- package/deploy/build.mjs +88 -0
- package/deploy/check-publishable.mjs +246 -0
- package/deploy/smoke.mjs +681 -0
- package/deploy/systemd/funoteka.env.example +41 -0
- package/deploy/systemd/funoteka.service +77 -0
- package/deploy/windows/funoteka-service.xml +58 -0
- package/deploy/windows/install-service.ps1 +158 -0
- package/dist/annotation/store.js +67 -0
- package/dist/api/admin-export.js +227 -0
- package/dist/api/admin-guard.js +181 -0
- package/dist/api/admin-library.js +113 -0
- package/dist/api/admin.js +1162 -0
- package/dist/api/annotation.js +193 -0
- package/dist/api/artistinfo.js +140 -0
- package/dist/api/audit.js +32 -0
- package/dist/api/auth.js +145 -0
- package/dist/api/bookmark.js +108 -0
- package/dist/api/browse.js +1241 -0
- package/dist/api/config-file.js +202 -0
- package/dist/api/config.js +285 -0
- package/dist/api/cover.js +304 -0
- package/dist/api/download.js +112 -0
- package/dist/api/envelope.js +162 -0
- package/dist/api/extensions.js +43 -0
- package/dist/api/genre.js +81 -0
- package/dist/api/health.js +35 -0
- package/dist/api/history.js +374 -0
- package/dist/api/idempotency.js +66 -0
- package/dist/api/keys.js +90 -0
- package/dist/api/log-file.js +65 -0
- package/dist/api/meta.js +1410 -0
- package/dist/api/playlist.js +311 -0
- package/dist/api/rescan.js +196 -0
- package/dist/api/roots.js +161 -0
- package/dist/api/router.js +133 -0
- package/dist/api/scanner.js +163 -0
- package/dist/api/search.js +82 -0
- package/dist/api/server.js +643 -0
- package/dist/api/settings.js +67 -0
- package/dist/api/stream.js +442 -0
- package/dist/api/stubs.js +183 -0
- package/dist/api/transcode.js +678 -0
- package/dist/api/user.js +73 -0
- package/dist/api/virtual.js +362 -0
- package/dist/api/visibility.js +21 -0
- package/dist/artist/apply.js +752 -0
- package/dist/artist/credit.js +95 -0
- package/dist/artist/folder.js +65 -0
- package/dist/artist/name.js +205 -0
- package/dist/artist/nfo.js +101 -0
- package/dist/artist/translit.js +79 -0
- package/dist/bookmark/store.js +54 -0
- package/dist/classify/classify.js +405 -0
- package/dist/classify/collision-name.js +109 -0
- package/dist/classify/folder-name.js +466 -0
- package/dist/classify/roles.js +277 -0
- package/dist/classify/shelf-name.js +178 -0
- package/dist/classify/tree.js +43 -0
- package/dist/cli/args.js +169 -0
- package/dist/cli/daemon.js +192 -0
- package/dist/cli/entry.js +25 -0
- package/dist/cli/keys.js +52 -0
- package/dist/cli.js +638 -0
- package/dist/cover/pick.js +58 -0
- package/dist/cover/picture.js +111 -0
- package/dist/cue/engine.js +759 -0
- package/dist/cue/match.js +142 -0
- package/dist/cue/parse.js +182 -0
- package/dist/cue/plan.js +286 -0
- package/dist/cue/track-name.js +120 -0
- package/dist/db/index.js +163 -0
- package/dist/db/issue.js +73 -0
- package/dist/db/ledger.js +84 -0
- package/dist/db/migrations/001_init.sql +196 -0
- package/dist/db/migrations/002_cue_track_flexible.sql +35 -0
- package/dist/db/migrations/003_cue_metadata.sql +14 -0
- package/dist/db/migrations/004_cue_performer.sql +12 -0
- package/dist/db/migrations/005_incremental.sql +23 -0
- package/dist/db/migrations/006_tags.sql +57 -0
- package/dist/db/migrations/007_artist_credit.sql +35 -0
- package/dist/db/migrations/008_issue_stage.sql +39 -0
- package/dist/db/migrations/009_cue_title.sql +20 -0
- package/dist/db/migrations/010_duration_source.sql +31 -0
- package/dist/db/migrations/011_track_title_source.sql +28 -0
- package/dist/db/migrations/012_file_container.sql +25 -0
- package/dist/db/migrations/013_cover_art.sql +28 -0
- package/dist/db/migrations/014_album_year.sql +17 -0
- package/dist/db/migrations/015_probe_method.sql +14 -0
- package/dist/db/migrations/016_tags_method.sql +20 -0
- package/dist/db/migrations/017_cover_indirect.sql +23 -0
- package/dist/db/migrations/018_release_year.sql +14 -0
- package/dist/db/migrations/019_file_tag_name_file_value.sql +20 -0
- package/dist/db/migrations/020_analyze_after_index.sql +16 -0
- package/dist/db/migrations/021_cue_index_files.sql +15 -0
- package/dist/db/migrations/022_sidecar_text.sql +19 -0
- package/dist/db/migrations/023_track_credit.sql +23 -0
- package/dist/db/migrations/024_playlists.sql +63 -0
- package/dist/db/migrations/025_playlist_sequence.sql +27 -0
- package/dist/db/migrations/026_playlist_source_file.sql +19 -0
- package/dist/db/migrations/027_lookup_indexes.sql +30 -0
- package/dist/db/migrations/028_annotations.sql +47 -0
- package/dist/db/migrations/029_scan_state_root_run.sql +28 -0
- package/dist/db/migrations/030_history.sql +108 -0
- package/dist/db/migrations/031_queue_position_and_plays.sql +46 -0
- package/dist/db/migrations/032_bookmarks.sql +27 -0
- package/dist/db/migrations/033_bookmark_order.sql +14 -0
- package/dist/db/migrations/034_junk.sql +33 -0
- package/dist/db/migrations/035_api_key.sql +38 -0
- package/dist/db/migrations/036_file_tag_first.sql +88 -0
- package/dist/db/migrations/037_admin_idempotency.sql +31 -0
- package/dist/db/migrations/038_cue_audio_index.sql +24 -0
- package/dist/db/sweep.js +109 -0
- package/dist/history/store.js +151 -0
- package/dist/inventory/inventory.js +294 -0
- package/dist/junk/marks.js +143 -0
- package/dist/junk/rule.js +93 -0
- package/dist/mcp/client.js +48 -0
- package/dist/mcp/server.js +175 -0
- package/dist/mcp/tools.js +348 -0
- package/dist/playlist/files.js +116 -0
- package/dist/playlist/import.js +260 -0
- package/dist/playlist/store.js +240 -0
- package/dist/probe/ffprobe.js +179 -0
- package/dist/run.js +120 -0
- package/dist/scan/kinds.js +49 -0
- package/dist/scan/scan.js +503 -0
- package/dist/scan/schedule.js +132 -0
- package/dist/scan/settle.js +172 -0
- package/dist/scan/walk.js +132 -0
- package/dist/search/index.js +41 -0
- package/dist/search/query.js +28 -0
- package/dist/stream/flac.js +541 -0
- package/dist/stream/mpeg.js +179 -0
- package/dist/stream/recode.js +393 -0
- package/dist/stream/rewrite.js +163 -0
- package/dist/stream/segment.js +624 -0
- package/dist/tags/apply.js +703 -0
- package/dist/tags/encode.js +164 -0
- package/dist/tags/first.js +130 -0
- package/dist/tags/flac.js +196 -0
- package/dist/tags/genres.js +123 -0
- package/dist/tags/id3v1.js +209 -0
- package/dist/tags/id3v2.js +754 -0
- package/dist/tags/mp4.js +534 -0
- package/dist/tags/mpeg.js +355 -0
- package/dist/tags/ogg.js +463 -0
- package/dist/tags/picture.js +71 -0
- package/dist/tags/read.js +269 -0
- package/dist/tags/types.js +51 -0
- package/dist/tags/vorbis-comment.js +155 -0
- package/dist/text/encoding.js +384 -0
- package/dist/text/site-name.js +37 -0
- package/dist/util/child.js +21 -0
- package/dist/util/names.js +97 -0
- package/funoteka.json.example +27 -0
- 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
|
+
}
|