@bzenky/spoti 0.2.0 → 0.4.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 (70) hide show
  1. package/README.md +122 -4
  2. package/dist/app.d.ts +23 -1
  3. package/dist/app.js +397 -36
  4. package/dist/app.js.map +1 -1
  5. package/dist/auth/config.d.ts +1 -1
  6. package/dist/auth/config.js +3 -3
  7. package/dist/auth/config.js.map +1 -1
  8. package/dist/cli.js +54 -0
  9. package/dist/cli.js.map +1 -1
  10. package/dist/services/auth.service.d.ts +1 -1
  11. package/dist/services/auth.service.js +9 -3
  12. package/dist/services/auth.service.js.map +1 -1
  13. package/dist/services/catalog.service.d.ts +9 -0
  14. package/dist/services/catalog.service.js +22 -0
  15. package/dist/services/catalog.service.js.map +1 -0
  16. package/dist/services/device.service.js +22 -2
  17. package/dist/services/device.service.js.map +1 -1
  18. package/dist/services/library.service.d.ts +13 -0
  19. package/dist/services/library.service.js +46 -0
  20. package/dist/services/library.service.js.map +1 -0
  21. package/dist/services/mappers.d.ts +10 -0
  22. package/dist/services/mappers.js +104 -0
  23. package/dist/services/mappers.js.map +1 -0
  24. package/dist/services/models.d.ts +45 -0
  25. package/dist/services/player.service.d.ts +4 -1
  26. package/dist/services/player.service.js +21 -14
  27. package/dist/services/player.service.js.map +1 -1
  28. package/dist/services/playlist.service.d.ts +9 -0
  29. package/dist/services/playlist.service.js +33 -0
  30. package/dist/services/playlist.service.js.map +1 -0
  31. package/dist/services/recent.service.d.ts +7 -0
  32. package/dist/services/recent.service.js +24 -0
  33. package/dist/services/recent.service.js.map +1 -0
  34. package/dist/services/search.service.d.ts +5 -1
  35. package/dist/services/search.service.js +32 -17
  36. package/dist/services/search.service.js.map +1 -1
  37. package/dist/services/update.service.d.ts +57 -0
  38. package/dist/services/update.service.js +313 -0
  39. package/dist/services/update.service.js.map +1 -0
  40. package/dist/spotify/client.js +14 -1
  41. package/dist/spotify/client.js.map +1 -1
  42. package/dist/spotify/scopes.d.ts +1 -1
  43. package/dist/spotify/scopes.js +4 -0
  44. package/dist/spotify/scopes.js.map +1 -1
  45. package/dist/spotify/types.d.ts +110 -8
  46. package/dist/storage/config.d.ts +5 -1
  47. package/dist/storage/config.js +21 -1
  48. package/dist/storage/config.js.map +1 -1
  49. package/dist/storage/update-cache.d.ts +29 -0
  50. package/dist/storage/update-cache.js +135 -0
  51. package/dist/storage/update-cache.js.map +1 -0
  52. package/dist/ui/completions.d.ts +3 -0
  53. package/dist/ui/completions.js +237 -0
  54. package/dist/ui/completions.js.map +1 -0
  55. package/dist/ui/interactive-search.d.ts +33 -0
  56. package/dist/ui/interactive-search.js +174 -0
  57. package/dist/ui/interactive-search.js.map +1 -0
  58. package/dist/ui/output.d.ts +10 -1
  59. package/dist/ui/output.js +60 -0
  60. package/dist/ui/output.js.map +1 -1
  61. package/dist/ui/progress.d.ts +6 -0
  62. package/dist/ui/progress.js +25 -0
  63. package/dist/ui/progress.js.map +1 -0
  64. package/dist/ui/prompts.d.ts +12 -1
  65. package/dist/ui/prompts.js +73 -6
  66. package/dist/ui/prompts.js.map +1 -1
  67. package/dist/update-worker.d.ts +9 -0
  68. package/dist/update-worker.js +91 -0
  69. package/dist/update-worker.js.map +1 -0
  70. package/package.json +1 -1
@@ -0,0 +1,237 @@
1
+ const bashCompletion = `# bash completion for spoti
2
+ _spoti_completion() {
3
+ local cur command
4
+ cur="\${COMP_WORDS[COMP_CWORD]}"
5
+ command="\${COMP_WORDS[1]}"
6
+
7
+ local commands="setup login logout status config interactive now pause resume next previous devices device seek volume queue shuffle repeat album artist playlists playlist liked like unlike recent update search play completion i p pa r np q s vol dev devs pl pls rep rec n prev"
8
+ local global_options="-h --help -V --version"
9
+
10
+ if (( COMP_CWORD == 1 )); then
11
+ COMPREPLY=( $(compgen -W "$commands $global_options" -- "$cur") )
12
+ return
13
+ fi
14
+
15
+ case "$command" in
16
+ config)
17
+ if (( COMP_CWORD == 2 )); then
18
+ COMPREPLY=( $(compgen -W "get set reset path unset -h --help" -- "$cur") )
19
+ elif (( COMP_CWORD == 3 )) && [[ "\${COMP_WORDS[2]}" == get || "\${COMP_WORDS[2]}" == set || "\${COMP_WORDS[2]}" == unset ]]; then
20
+ COMPREPLY=( $(compgen -W "spotifyClientId watchAfterPlay refreshIntervalMs" -- "$cur") )
21
+ elif (( COMP_CWORD == 4 )) && [[ "\${COMP_WORDS[2]}" == set && "\${COMP_WORDS[3]}" == watchAfterPlay ]]; then
22
+ COMPREPLY=( $(compgen -W "true false" -- "$cur") )
23
+ fi
24
+ ;;
25
+ completion)
26
+ COMPREPLY=( $(compgen -W "bash zsh fish -h --help" -- "$cur") )
27
+ ;;
28
+ now|np)
29
+ COMPREPLY=( $(compgen -W "-w --watch -h --help" -- "$cur") )
30
+ ;;
31
+ queue|q|album|artist|playlist|pl)
32
+ COMPREPLY=( $(compgen -W "--first -h --help" -- "$cur") )
33
+ ;;
34
+ playlists|pls|liked|recent|rec|search|s)
35
+ COMPREPLY=( $(compgen -W "-l --limit -h --help" -- "$cur") )
36
+ ;;
37
+ update)
38
+ COMPREPLY=( $(compgen -W "--check -h --help" -- "$cur") )
39
+ ;;
40
+ play|p)
41
+ COMPREPLY=( $(compgen -W "track album artist playlist --first --watch --no-watch -h --help" -- "$cur") )
42
+ ;;
43
+ shuffle)
44
+ COMPREPLY=( $(compgen -W "on off -h --help" -- "$cur") )
45
+ ;;
46
+ repeat|rep)
47
+ COMPREPLY=( $(compgen -W "off track context -h --help" -- "$cur") )
48
+ ;;
49
+ *)
50
+ COMPREPLY=( $(compgen -W "-h --help" -- "$cur") )
51
+ ;;
52
+ esac
53
+ }
54
+
55
+ complete -F _spoti_completion spoti
56
+ `;
57
+ const zshCompletion = `#compdef spoti
58
+ # zsh completion for spoti
59
+
60
+ _spoti() {
61
+ local context state line
62
+ local -a commands
63
+ commands=(
64
+ 'setup:save your Spotify application client ID'
65
+ 'login:log in to Spotify'
66
+ 'logout:remove locally stored Spotify credentials'
67
+ 'status:show authentication status'
68
+ 'config:view and update spoti configuration'
69
+ 'interactive:open the keyboard-driven Spotify search'
70
+ 'now:show the current Spotify playback'
71
+ 'pause:pause playback'
72
+ 'resume:resume playback'
73
+ 'next:skip to the next track'
74
+ 'previous:return to the previous track'
75
+ 'devices:list available Spotify Connect devices'
76
+ 'device:transfer playback to a Spotify Connect device'
77
+ 'seek:seek within the current track'
78
+ 'volume:set or adjust the active device volume'
79
+ 'queue:show the playback queue or add a searched track'
80
+ 'shuffle:turn playback shuffle on or off'
81
+ 'repeat:set the playback repeat mode'
82
+ 'album:search for and show an album'
83
+ 'artist:search for and show an artist'
84
+ 'playlists:list your Spotify playlists'
85
+ 'playlist:show one of your Spotify playlists'
86
+ 'liked:list your liked tracks'
87
+ 'like:add the current track to your Spotify library'
88
+ 'unlike:remove the current track from your Spotify library'
89
+ 'recent:show recently played tracks'
90
+ 'update:check for or install the latest spoti version'
91
+ 'search:search Spotify tracks'
92
+ 'play:play a track, album, artist, or playlist'
93
+ 'completion:print a static shell completion script'
94
+ 'i:alias for interactive'
95
+ 'p:alias for play'
96
+ 'pa:alias for pause'
97
+ 'r:alias for resume'
98
+ 'np:alias for now'
99
+ 'q:alias for queue'
100
+ 's:alias for search'
101
+ 'vol:alias for volume'
102
+ 'dev:alias for device'
103
+ 'devs:alias for devices'
104
+ 'pl:alias for playlist'
105
+ 'pls:alias for playlists'
106
+ 'rep:alias for repeat'
107
+ 'rec:alias for recent'
108
+ 'n:alias for next'
109
+ 'prev:alias for previous'
110
+ )
111
+
112
+ _arguments -C \\
113
+ '(-h --help)'{-h,--help}'[display help]' \\
114
+ '(-V --version)'{-V,--version}'[display version]' \
115
+ '1:command:->command' \
116
+ '*::argument:->arguments'
117
+
118
+ case "$state" in
119
+ command)
120
+ _describe 'spoti command' commands
121
+ ;;
122
+ arguments)
123
+ case "$words[2]" in
124
+ config)
125
+ local -a config_commands
126
+ config_commands=(
127
+ 'get:read a configuration value'
128
+ 'set:set a configuration value'
129
+ 'reset:reset configuration to defaults'
130
+ 'path:show the configuration file path'
131
+ 'unset:remove a configuration value'
132
+ )
133
+ if (( CURRENT == 3 )); then
134
+ _describe 'config command' config_commands
135
+ elif [[ "$words[3]" == get || "$words[3]" == set || "$words[3]" == unset ]]; then
136
+ _values 'configuration key' spotifyClientId watchAfterPlay refreshIntervalMs
137
+ fi
138
+ ;;
139
+ completion)
140
+ _values 'shell' bash zsh fish
141
+ ;;
142
+ now|np)
143
+ _arguments '(-w --watch)'{-w,--watch}'[continuously refresh playback information]'
144
+ ;;
145
+ queue|q|album|artist|playlist|pl)
146
+ _arguments '--first[select the first result without prompting]' '*:query:'
147
+ ;;
148
+ playlists|pls|liked|recent|rec|search|s)
149
+ _arguments '(-l --limit)'{-l,--limit}'[maximum number of results]:number:' '*:query:'
150
+ ;;
151
+ update)
152
+ _arguments '--check[check without installing]'
153
+ ;;
154
+ play|p)
155
+ _arguments '--first[play the first result without prompting]' '--watch[continuously refresh playback information]' '--no-watch[return after starting playback]' '1:type:(track album artist playlist)' '*:query:'
156
+ ;;
157
+ shuffle)
158
+ _values 'shuffle state' on off
159
+ ;;
160
+ repeat|rep)
161
+ _values 'repeat mode' off track context
162
+ ;;
163
+ esac
164
+ ;;
165
+ esac
166
+ }
167
+
168
+ _spoti "$@"
169
+ `;
170
+ const fishCompletion = `# fish completion for spoti
171
+ complete -c spoti -f
172
+ complete -c spoti -s h -l help -d 'Display help'
173
+ complete -c spoti -s V -l version -d 'Display version'
174
+
175
+ function __spoti_needs_command
176
+ not __fish_seen_subcommand_from setup login logout status config interactive now pause resume next previous devices device seek volume queue shuffle repeat album artist playlists playlist liked like unlike recent update search play completion i p pa r np q s vol dev devs pl pls rep rec n prev
177
+ end
178
+ complete -c spoti -n __spoti_needs_command -a setup -d 'Save your Spotify application client ID'
179
+ complete -c spoti -n __spoti_needs_command -a login -d 'Log in to Spotify'
180
+ complete -c spoti -n __spoti_needs_command -a logout -d 'Remove locally stored Spotify credentials'
181
+ complete -c spoti -n __spoti_needs_command -a status -d 'Show authentication status'
182
+ complete -c spoti -n __spoti_needs_command -a config -d 'View and update configuration'
183
+ complete -c spoti -n __spoti_needs_command -a interactive -d 'Open the keyboard-driven Spotify search'
184
+ complete -c spoti -n __spoti_needs_command -a now -d 'Show current playback'
185
+ complete -c spoti -n __spoti_needs_command -a pause -d 'Pause playback'
186
+ complete -c spoti -n __spoti_needs_command -a resume -d 'Resume playback'
187
+ complete -c spoti -n __spoti_needs_command -a next -d 'Skip to the next track'
188
+ complete -c spoti -n __spoti_needs_command -a previous -d 'Return to the previous track'
189
+ complete -c spoti -n __spoti_needs_command -a devices -d 'List Spotify Connect devices'
190
+ complete -c spoti -n __spoti_needs_command -a device -d 'Transfer playback to a device'
191
+ complete -c spoti -n __spoti_needs_command -a seek -d 'Seek within the current track'
192
+ complete -c spoti -n __spoti_needs_command -a volume -d 'Set or adjust volume'
193
+ complete -c spoti -n __spoti_needs_command -a queue -d 'Show or add to the queue'
194
+ complete -c spoti -n __spoti_needs_command -a shuffle -d 'Turn shuffle on or off'
195
+ complete -c spoti -n __spoti_needs_command -a repeat -d 'Set repeat mode'
196
+ complete -c spoti -n __spoti_needs_command -a album -d 'Search for an album'
197
+ complete -c spoti -n __spoti_needs_command -a artist -d 'Search for an artist'
198
+ complete -c spoti -n __spoti_needs_command -a playlists -d 'List your playlists'
199
+ complete -c spoti -n __spoti_needs_command -a playlist -d 'Show a playlist'
200
+ complete -c spoti -n __spoti_needs_command -a liked -d 'List liked tracks'
201
+ complete -c spoti -n __spoti_needs_command -a like -d 'Like the current track'
202
+ complete -c spoti -n __spoti_needs_command -a unlike -d 'Unlike the current track'
203
+ complete -c spoti -n __spoti_needs_command -a recent -d 'Show recently played tracks'
204
+ complete -c spoti -n __spoti_needs_command -a update -d 'Check for or install updates'
205
+ complete -c spoti -n __spoti_needs_command -a search -d 'Search Spotify tracks'
206
+ complete -c spoti -n __spoti_needs_command -a play -d 'Play a track, album, artist, or playlist'
207
+ complete -c spoti -n __spoti_needs_command -a completion -d 'Print a shell completion script'
208
+ complete -c spoti -n __spoti_needs_command -a 'i p pa r np q s vol dev devs pl pls rep rec n prev' -d 'Command alias'
209
+
210
+ complete -c spoti -n '__fish_seen_subcommand_from config; and not __fish_seen_subcommand_from get set reset path unset' -a 'get set reset path unset'
211
+ complete -c spoti -n '__fish_seen_subcommand_from config; and __fish_seen_subcommand_from get set unset' -a 'spotifyClientId watchAfterPlay refreshIntervalMs'
212
+ complete -c spoti -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish'
213
+ complete -c spoti -n '__fish_seen_subcommand_from now np' -s w -l watch -d 'Continuously refresh playback information'
214
+ complete -c spoti -n '__fish_seen_subcommand_from queue q album artist playlist pl' -l first -d 'Select the first result without prompting'
215
+ complete -c spoti -n '__fish_seen_subcommand_from playlists pls liked recent rec search s' -s l -l limit -r -d 'Maximum number of results'
216
+ complete -c spoti -n '__fish_seen_subcommand_from update' -l check -d 'Check without installing'
217
+ complete -c spoti -n '__fish_seen_subcommand_from play p' -l first -d 'Play the first result without prompting'
218
+ complete -c spoti -n '__fish_seen_subcommand_from play p' -l watch -d 'Continuously refresh playback information'
219
+ complete -c spoti -n '__fish_seen_subcommand_from play p' -l no-watch -d 'Return after starting playback'
220
+ complete -c spoti -n '__fish_seen_subcommand_from play p' -a 'track album artist playlist'
221
+ complete -c spoti -n '__fish_seen_subcommand_from shuffle' -a 'on off'
222
+ complete -c spoti -n '__fish_seen_subcommand_from repeat rep' -a 'off track context'
223
+ `;
224
+ /** Return a self-contained completion definition without running the spoti CLI. */
225
+ export function generateCompletionScript(shell) {
226
+ switch (shell) {
227
+ case 'bash':
228
+ return bashCompletion;
229
+ case 'zsh':
230
+ return zshCompletion;
231
+ case 'fish':
232
+ return fishCompletion;
233
+ default:
234
+ throw new Error(`Unsupported shell: ${String(shell)}`);
235
+ }
236
+ }
237
+ //# sourceMappingURL=completions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completions.js","sourceRoot":"","sources":["../../src/ui/completions.ts"],"names":[],"mappings":"AAEA,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuDtB,CAAC;AAEF,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgHrB,CAAC;AAEF,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqDtB,CAAC;AAEF,mFAAmF;AACnF,MAAM,UAAU,wBAAwB,CAAC,KAAsB;IAC7D,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,MAAM;YACT,OAAO,cAAc,CAAC;QACxB,KAAK,KAAK;YACR,OAAO,aAAa,CAAC;QACvB,KAAK,MAAM;YACT,OAAO,cAAc,CAAC;QACxB;YACE,MAAM,IAAI,KAAK,CAAC,sBAAsB,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC"}
@@ -0,0 +1,33 @@
1
+ import type { PlayerService } from '../services/player.service.js';
2
+ import type { SearchService } from '../services/search.service.js';
3
+ export type SearchCategory = 'track' | 'album' | 'artist' | 'playlist';
4
+ export type InteractiveSearchKey = {
5
+ type: 'character';
6
+ value: string;
7
+ } | {
8
+ type: 'backspace' | 'enter' | 'escape' | 'up' | 'down' | 'left' | 'right' | 'tab' | 'other';
9
+ };
10
+ export interface InteractiveSearchTerminal {
11
+ readonly isTTY: boolean;
12
+ setup(): void;
13
+ cleanup(): void;
14
+ readKey(): Promise<InteractiveSearchKey>;
15
+ write(value: string): void;
16
+ }
17
+ export interface InteractiveSearchServices {
18
+ search: Pick<SearchService, 'searchTracks' | 'searchAlbums' | 'searchArtists' | 'searchPlaylists'>;
19
+ player: Pick<PlayerService, 'playTrack' | 'playContext'>;
20
+ }
21
+ export interface InteractiveSearchOptions extends InteractiveSearchServices {
22
+ terminal?: InteractiveSearchTerminal;
23
+ limit?: number;
24
+ }
25
+ export type InteractiveSearchResult = {
26
+ status: 'not-interactive' | 'cancelled';
27
+ } | {
28
+ status: 'played';
29
+ category: SearchCategory;
30
+ uri: string;
31
+ };
32
+ export declare function runInteractiveSearch(options: InteractiveSearchOptions): Promise<InteractiveSearchResult>;
33
+ export declare function createNodeInteractiveSearchTerminal(): InteractiveSearchTerminal;
@@ -0,0 +1,174 @@
1
+ import { emitKeypressEvents } from 'node:readline';
2
+ import { stdin, stdout } from 'node:process';
3
+ import { formatAlbum, formatArtist, formatPlaylist, formatTrack } from './output.js';
4
+ const CATEGORIES = ['track', 'album', 'artist', 'playlist'];
5
+ const CATEGORY_LABELS = {
6
+ track: 'Tracks',
7
+ album: 'Albums',
8
+ artist: 'Artists',
9
+ playlist: 'Playlists',
10
+ };
11
+ const CLEAR_SCREEN = '\u001B[2J\u001B[H';
12
+ export async function runInteractiveSearch(options) {
13
+ const terminal = options.terminal ?? createNodeInteractiveSearchTerminal();
14
+ if (!terminal.isTTY)
15
+ return { status: 'not-interactive' };
16
+ let query = '';
17
+ let category = 'track';
18
+ let items = null;
19
+ let selectedIndex = 0;
20
+ terminal.setup();
21
+ try {
22
+ render(terminal, { query, category, items, selectedIndex });
23
+ while (true) {
24
+ const key = await terminal.readKey();
25
+ if (key.type === 'escape')
26
+ return { status: 'cancelled' };
27
+ if (items === null) {
28
+ if (key.type === 'character')
29
+ query += key.value;
30
+ else if (key.type === 'backspace')
31
+ query = query.slice(0, -1);
32
+ else if (isCategoryKey(key))
33
+ category = moveCategory(category, key.type === 'left' ? -1 : 1);
34
+ else if (key.type === 'enter' && query.trim()) {
35
+ renderSearching(terminal, query, category);
36
+ items = await search(options.search, category, query, options.limit);
37
+ selectedIndex = 0;
38
+ }
39
+ }
40
+ else if (isCategoryKey(key)) {
41
+ category = moveCategory(category, key.type === 'left' ? -1 : 1);
42
+ renderSearching(terminal, query, category);
43
+ items = await search(options.search, category, query, options.limit);
44
+ selectedIndex = 0;
45
+ }
46
+ else if (key.type === 'up' && items.length > 0) {
47
+ selectedIndex = (selectedIndex - 1 + items.length) % items.length;
48
+ }
49
+ else if (key.type === 'down' && items.length > 0) {
50
+ selectedIndex = (selectedIndex + 1) % items.length;
51
+ }
52
+ else if (key.type === 'enter' && items.length > 0) {
53
+ const selected = items[selectedIndex];
54
+ if (!selected)
55
+ continue;
56
+ if (category === 'track')
57
+ await options.player.playTrack(selected.uri);
58
+ else
59
+ await options.player.playContext(selected.uri);
60
+ return { status: 'played', category, uri: selected.uri };
61
+ }
62
+ render(terminal, { query, category, items, selectedIndex });
63
+ }
64
+ }
65
+ finally {
66
+ terminal.cleanup();
67
+ }
68
+ }
69
+ export function createNodeInteractiveSearchTerminal() {
70
+ let wasRaw = false;
71
+ return {
72
+ isTTY: Boolean(stdin.isTTY && stdout.isTTY),
73
+ setup() {
74
+ wasRaw = Boolean(stdin.isRaw);
75
+ emitKeypressEvents(stdin);
76
+ stdin.setRawMode(true);
77
+ stdin.resume();
78
+ stdout.write('\u001B[?1049h\u001B[?25l');
79
+ },
80
+ cleanup() {
81
+ if (stdin.isTTY)
82
+ stdin.setRawMode(wasRaw);
83
+ stdin.pause();
84
+ stdout.write('\u001B[?25h\u001B[?1049l');
85
+ },
86
+ readKey() {
87
+ return new Promise((resolve) => {
88
+ const listener = (character, key = {}) => {
89
+ resolve(mapNodeKey(character, key));
90
+ };
91
+ stdin.once('keypress', listener);
92
+ });
93
+ },
94
+ write(value) {
95
+ stdout.write(value);
96
+ },
97
+ };
98
+ }
99
+ function render(terminal, state) {
100
+ const categoryBar = CATEGORIES.map((entry) => entry === state.category ? `[${CATEGORY_LABELS[entry]}]` : CATEGORY_LABELS[entry]).join(' ');
101
+ const lines = [
102
+ 'Spotify search',
103
+ '',
104
+ `${categoryBar}`,
105
+ '',
106
+ `Search: ${state.query}${state.items === null ? '▌' : ''}`,
107
+ '',
108
+ ];
109
+ if (state.items === null) {
110
+ lines.push('Type a query and press Enter. Tab switches category; Esc exits.');
111
+ }
112
+ else if (state.items.length === 0) {
113
+ lines.push(`No ${CATEGORY_LABELS[state.category].toLowerCase()} found for "${state.query.trim()}".`);
114
+ lines.push('', 'Tab switches category; Esc exits.');
115
+ }
116
+ else {
117
+ lines.push(...state.items.map((item, index) => `${index === state.selectedIndex ? '›' : ' '} ${formatItem(item, state.category)}`), '', '↑/↓ selects; Tab switches category; Enter plays; Esc exits.');
118
+ }
119
+ terminal.write(`${CLEAR_SCREEN}${lines.join('\n')}\n`);
120
+ }
121
+ function renderSearching(terminal, query, category) {
122
+ terminal.write(`${CLEAR_SCREEN}Searching ${CATEGORY_LABELS[category].toLowerCase()} for "${query.trim()}"…\n`);
123
+ }
124
+ async function search(service, category, query, limit) {
125
+ if (category === 'track')
126
+ return service.searchTracks(query, limit);
127
+ if (category === 'album')
128
+ return service.searchAlbums(query, limit);
129
+ if (category === 'artist')
130
+ return service.searchArtists(query, limit);
131
+ return service.searchPlaylists(query, limit);
132
+ }
133
+ function formatItem(item, category) {
134
+ if (category === 'track')
135
+ return formatTrack(item);
136
+ if (category === 'album')
137
+ return formatAlbum(item);
138
+ if (category === 'artist')
139
+ return formatArtist(item);
140
+ return formatPlaylist(item);
141
+ }
142
+ function isCategoryKey(key) {
143
+ return key.type === 'tab' || key.type === 'left' || key.type === 'right';
144
+ }
145
+ function moveCategory(category, offset) {
146
+ const currentIndex = CATEGORIES.indexOf(category);
147
+ return CATEGORIES[(currentIndex + offset + CATEGORIES.length) % CATEGORIES.length] ?? 'track';
148
+ }
149
+ function mapNodeKey(character, key) {
150
+ if (key.name === 'escape' || (key.ctrl && key.name === 'c'))
151
+ return { type: 'escape' };
152
+ if (key.name === 'return' || key.name === 'enter')
153
+ return { type: 'enter' };
154
+ if (key.name === 'backspace')
155
+ return { type: 'backspace' };
156
+ if (key.name === 'up')
157
+ return { type: 'up' };
158
+ if (key.name === 'down')
159
+ return { type: 'down' };
160
+ if (key.name === 'left')
161
+ return { type: 'left' };
162
+ if (key.name === 'right')
163
+ return { type: 'right' };
164
+ if (key.name === 'tab')
165
+ return { type: 'tab' };
166
+ if (!key.ctrl && !key.meta && character && isPrintable(character)) {
167
+ return { type: 'character', value: character };
168
+ }
169
+ return { type: 'other' };
170
+ }
171
+ function isPrintable(value) {
172
+ return Array.from(value).every((character) => character >= ' ' && character !== '\u007F');
173
+ }
174
+ //# sourceMappingURL=interactive-search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interactive-search.js","sourceRoot":"","sources":["../../src/ui/interactive-search.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { CurrentPlayback, Track } from '../services/models.js';
1
+ import type { Album, AlbumDetail, Artist, CurrentPlayback, Playlist, PlaylistDetail, RecentlyPlayedTrack, SavedTrack, Track } from '../services/models.js';
2
2
  export interface Output {
3
3
  log(message: string): void;
4
4
  error(message: string): void;
@@ -6,3 +6,12 @@ export interface Output {
6
6
  export declare const consoleOutput: Output;
7
7
  export declare function formatPlayback(playback: CurrentPlayback): string;
8
8
  export declare function formatTrack(track: Track, index?: number): string;
9
+ export declare function formatAlbum(album: Album, index?: number): string;
10
+ export declare function formatAlbumDetail(album: AlbumDetail): string;
11
+ export declare function formatArtist(artist: Artist, index?: number): string;
12
+ export declare function formatArtistDetail(artist: Artist): string;
13
+ export declare function formatPlaylist(playlist: Playlist, index?: number): string;
14
+ export declare function formatPlaylistOverview(playlist: Playlist): string;
15
+ export declare function formatPlaylistDetail(playlist: PlaylistDetail): string;
16
+ export declare function formatSavedTrack(item: SavedTrack, index?: number): string;
17
+ export declare function formatRecentlyPlayed(item: RecentlyPlayedTrack, index?: number): string;
package/dist/ui/output.js CHANGED
@@ -18,4 +18,64 @@ export function formatTrack(track, index) {
18
18
  const prefix = index === undefined ? '' : `${index + 1}. `;
19
19
  return `${prefix}${track.name} — ${track.artists.join(', ')} · ${track.album}`;
20
20
  }
21
+ export function formatAlbum(album, index) {
22
+ const prefix = index === undefined ? '' : `${index + 1}. `;
23
+ const release = album.releaseDate ? ` · ${album.releaseDate}` : '';
24
+ return `${prefix}${album.name} — ${album.artists.join(', ')} · ${album.totalTracks} tracks${release}`;
25
+ }
26
+ export function formatAlbumDetail(album) {
27
+ const lines = [formatAlbum(album), '', 'Tracks:'];
28
+ if (album.tracks.length === 0)
29
+ lines.push('No playable tracks found.');
30
+ else
31
+ lines.push(...album.tracks.map((track, index) => formatTrack(track, index)));
32
+ if (album.tracks.length < album.totalTracks) {
33
+ lines.push(`Showing ${album.tracks.length} of ${album.totalTracks} tracks.`);
34
+ }
35
+ if (album.externalUrl)
36
+ lines.push('', `Spotify: ${album.externalUrl}`);
37
+ return lines.join('\n');
38
+ }
39
+ export function formatArtist(artist, index) {
40
+ const prefix = index === undefined ? '' : `${index + 1}. `;
41
+ return `${prefix}${artist.name}`;
42
+ }
43
+ export function formatArtistDetail(artist) {
44
+ const lines = [formatArtist(artist)];
45
+ if (artist.externalUrl)
46
+ lines.push(`Spotify: ${artist.externalUrl}`);
47
+ return lines.join('\n');
48
+ }
49
+ export function formatPlaylist(playlist, index) {
50
+ const prefix = index === undefined ? '' : `${index + 1}. `;
51
+ const itemLabel = playlist.totalTracks === 1 ? 'item' : 'items';
52
+ return `${prefix}${playlist.name} — ${playlist.ownerName} · ${playlist.totalTracks} ${itemLabel}`;
53
+ }
54
+ export function formatPlaylistOverview(playlist) {
55
+ const lines = [formatPlaylist(playlist)];
56
+ if (playlist.description)
57
+ lines.push(playlist.description);
58
+ if (playlist.externalUrl)
59
+ lines.push(`Spotify: ${playlist.externalUrl}`);
60
+ return lines.join('\n');
61
+ }
62
+ export function formatPlaylistDetail(playlist) {
63
+ const lines = [formatPlaylist(playlist)];
64
+ if (playlist.description)
65
+ lines.push(playlist.description);
66
+ lines.push('', 'Tracks:');
67
+ if (playlist.tracks.length === 0)
68
+ lines.push('No playable tracks found.');
69
+ else
70
+ lines.push(...playlist.tracks.map((track, index) => formatTrack(track, index)));
71
+ if (playlist.externalUrl)
72
+ lines.push('', `Spotify: ${playlist.externalUrl}`);
73
+ return lines.join('\n');
74
+ }
75
+ export function formatSavedTrack(item, index) {
76
+ return `${formatTrack(item.track, index)} · liked ${item.addedAt}`;
77
+ }
78
+ export function formatRecentlyPlayed(item, index) {
79
+ return `${formatTrack(item.track, index)} · played ${item.playedAt}`;
80
+ }
21
81
  //# sourceMappingURL=output.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/ui/output.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAOrE,MAAM,CAAC,MAAM,aAAa,GAAW;IACnC,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;IACtC,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC;CAC3C,CAAC;AAEF,MAAM,UAAU,cAAc,CAAC,QAAyB;IACtD,MAAM,SAAS,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;IACjD,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAC1E,OAAO;QACL,GAAG,SAAS,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE;QACrC,GAAG,OAAO,MAAM,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE;QACtC,EAAE;QACF,GAAG,cAAc,CAAC,QAAQ,CAAC,IAAI,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,cAAc,CAAC,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE;KACrI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAY,EAAE,KAAc;IACtD,MAAM,MAAM,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,IAAI,CAAC;IAC3D,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC,IAAI,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC;AACjF,CAAC"}
1
+ {"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/ui/output.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ export type ProgressRunner = <Result>(label: string, task: () => Promise<Result>) => Promise<Result>;
2
+ export interface ProgressTerminal {
3
+ readonly isTTY: boolean;
4
+ write(value: string): void;
5
+ }
6
+ export declare function withProgress<Result>(label: string, task: () => Promise<Result>, terminal?: ProgressTerminal): Promise<Result>;
@@ -0,0 +1,25 @@
1
+ import { stderr } from 'node:process';
2
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
3
+ const defaultTerminal = {
4
+ isTTY: Boolean(stderr.isTTY),
5
+ write: (value) => stderr.write(value),
6
+ };
7
+ export async function withProgress(label, task, terminal = defaultTerminal) {
8
+ if (!terminal.isTTY)
9
+ return task();
10
+ let frameIndex = 0;
11
+ terminal.write(`${frames[frameIndex]} ${label}`);
12
+ const timer = setInterval(() => {
13
+ frameIndex = (frameIndex + 1) % frames.length;
14
+ terminal.write(`\r${frames[frameIndex]} ${label}`);
15
+ }, 80);
16
+ timer.unref();
17
+ try {
18
+ return await task();
19
+ }
20
+ finally {
21
+ clearInterval(timer);
22
+ terminal.write('\r\u001B[2K');
23
+ }
24
+ }
25
+ //# sourceMappingURL=progress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"progress.js","sourceRoot":"","sources":["../../src/ui/progress.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAYtC,MAAM,MAAM,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU,CAAC;AAE3E,MAAM,eAAe,GAAqB;IACxC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;IAC5B,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC;CACtC,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,KAAa,EACb,IAA2B,EAC3B,WAA6B,eAAe;IAE5C,IAAI,CAAC,QAAQ,CAAC,KAAK;QAAE,OAAO,IAAI,EAAE,CAAC;IAEnC,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,QAAQ,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,UAAU,GAAG,CAAC,UAAU,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;QAC9C,QAAQ,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,UAAU,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC;IACrD,CAAC,EAAE,EAAE,CAAC,CAAC;IACP,KAAK,CAAC,KAAK,EAAE,CAAC;IAEd,IAAI,CAAC;QACH,OAAO,MAAM,IAAI,EAAE,CAAC;IACtB,CAAC;YAAS,CAAC;QACT,aAAa,CAAC,KAAK,CAAC,CAAC;QACrB,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;IAChC,CAAC;AACH,CAAC"}
@@ -1,2 +1,13 @@
1
- import type { Track } from '../services/models.js';
1
+ import type { Album, Artist, Playlist, Track } from '../services/models.js';
2
2
  export declare function selectTrack(tracks: Track[]): Promise<Track | null>;
3
+ export declare function selectAlbum(albums: Album[]): Promise<Album | null>;
4
+ export declare function selectArtist(artists: Artist[]): Promise<Artist | null>;
5
+ export declare function selectPlaylist(playlists: Playlist[]): Promise<Playlist | null>;
6
+ export type AlbumAction = 'play-album' | 'play-track';
7
+ export type ArtistAction = 'play-artist' | 'select-album';
8
+ export type PlaylistAction = 'play-playlist';
9
+ export declare function selectAlbumAction(): Promise<AlbumAction | null>;
10
+ export declare function selectArtistAction(): Promise<ArtistAction | null>;
11
+ export declare function selectPlaylistAction(): Promise<PlaylistAction | null>;
12
+ export declare function promptSpotifyClientId(): Promise<string | null>;
13
+ export declare function confirmUpdate(currentVersion: string, latestVersion: string): Promise<boolean>;