spotify_cli/rpc/
dispatch.rs

1//! RPC method dispatcher
2//!
3//! Maps JSON-RPC methods to CLI command handlers.
4//! Full CLI-RPC parity: everything available in CLI is available via RPC.
5
6use tracing::debug;
7
8use crate::cli::commands::{self, ArtistQuery, ArtistView, SearchFilters, SearchOptions};
9use crate::io::output::{ErrorKind, Response};
10
11use super::protocol::RpcRequest;
12
13/// Command dispatcher
14pub struct Dispatcher;
15
16impl Dispatcher {
17    pub fn new() -> Self {
18        Self
19    }
20
21    /// Dispatch an RPC request to the appropriate handler
22    pub async fn dispatch(&self, request: &RpcRequest) -> Response {
23        debug!(method = %request.method, "Dispatching RPC request");
24
25        let params = request.params.as_ref();
26
27        match request.method.as_str() {
28            // ============================================================
29            // Daemon
30            // ============================================================
31            "ping" => Response::success(200, "pong"),
32            "version" => Response::success_with_payload(
33                200,
34                "Version info",
35                serde_json::json!({
36                    "version": env!("CARGO_PKG_VERSION"),
37                    "name": env!("CARGO_PKG_NAME"),
38                }),
39            ),
40
41            // ============================================================
42            // Auth
43            // ============================================================
44            "auth.login" => {
45                let force = params
46                    .and_then(|p| p.get("force"))
47                    .and_then(|v| v.as_bool())
48                    .unwrap_or(false);
49                commands::auth_login(force).await
50            }
51            "auth.logout" => commands::auth_logout().await,
52            "auth.refresh" => commands::auth_refresh().await,
53            "auth.status" => commands::auth_status().await,
54
55            // ============================================================
56            // Player
57            // ============================================================
58            "player.status" => {
59                let id_only = params
60                    .and_then(|p| p.get("id_only"))
61                    .and_then(|v| v.as_str());
62                commands::player_status(id_only).await
63            }
64            "player.play" => {
65                let uri = params.and_then(|p| p.get("uri")).and_then(|v| v.as_str());
66                let pin = params.and_then(|p| p.get("pin")).and_then(|v| v.as_str());
67                commands::player_play(uri, pin).await
68            }
69            "player.pause" => commands::player_pause().await,
70            "player.toggle" => commands::player_toggle().await,
71            "player.next" => commands::player_next().await,
72            "player.previous" => commands::player_previous().await,
73            "player.seek" => {
74                let position = params
75                    .and_then(|p| p.get("position"))
76                    .and_then(|v| v.as_str())
77                    .unwrap_or("0");
78                commands::player_seek(position).await
79            }
80            "player.volume" => {
81                let percent = params
82                    .and_then(|p| p.get("percent"))
83                    .and_then(|v| v.as_u64())
84                    .unwrap_or(50) as u8;
85                commands::player_volume(percent).await
86            }
87            "player.shuffle" => {
88                let state = params
89                    .and_then(|p| p.get("state"))
90                    .and_then(|v| v.as_str())
91                    .unwrap_or("on");
92                commands::player_shuffle(state).await
93            }
94            "player.repeat" => {
95                let mode = params
96                    .and_then(|p| p.get("mode"))
97                    .and_then(|v| v.as_str())
98                    .unwrap_or("off");
99                commands::player_repeat(mode).await
100            }
101            "player.devices" => commands::player_devices_list().await,
102            "player.transfer" => {
103                let device = params
104                    .and_then(|p| p.get("device"))
105                    .and_then(|v| v.as_str());
106                if let Some(dev) = device {
107                    commands::player_devices_transfer(dev).await
108                } else {
109                    Response::err(400, "Missing 'device' parameter", ErrorKind::Validation)
110                }
111            }
112            "player.recent" => commands::player_recent().await,
113
114            // ============================================================
115            // Queue
116            // ============================================================
117            "queue.list" => commands::player_queue_list().await,
118            "queue.add" => {
119                let uri = params.and_then(|p| p.get("uri")).and_then(|v| v.as_str());
120                let now_playing = params
121                    .and_then(|p| p.get("now_playing"))
122                    .and_then(|v| v.as_bool())
123                    .unwrap_or(false);
124                commands::player_queue_add(uri, now_playing).await
125            }
126
127            // ============================================================
128            // Search
129            // ============================================================
130            "search" => {
131                let query = params
132                    .and_then(|p| p.get("query"))
133                    .and_then(|v| v.as_str())
134                    .unwrap_or("");
135                let types: Vec<String> = params
136                    .and_then(|p| p.get("types"))
137                    .and_then(|v| v.as_array())
138                    .map(|arr| {
139                        arr.iter()
140                            .filter_map(|v| v.as_str().map(String::from))
141                            .collect()
142                    })
143                    .unwrap_or_default();
144                let limit = params
145                    .and_then(|p| p.get("limit"))
146                    .and_then(|v| v.as_u64())
147                    .unwrap_or(20) as u8;
148                let pins_only = params
149                    .and_then(|p| p.get("pins_only"))
150                    .and_then(|v| v.as_bool())
151                    .unwrap_or(false);
152                let exact = params
153                    .and_then(|p| p.get("exact"))
154                    .and_then(|v| v.as_bool())
155                    .unwrap_or(false);
156                let play = params
157                    .and_then(|p| p.get("play"))
158                    .and_then(|v| v.as_bool())
159                    .unwrap_or(false);
160                let sort = params
161                    .and_then(|p| p.get("sort"))
162                    .and_then(|v| v.as_bool())
163                    .unwrap_or(false);
164
165                let options = SearchOptions {
166                    limit,
167                    pins_only,
168                    exact,
169                    filters: SearchFilters {
170                        artist: params
171                            .and_then(|p| p.get("artist"))
172                            .and_then(|v| v.as_str())
173                            .map(String::from),
174                        album: params
175                            .and_then(|p| p.get("album"))
176                            .and_then(|v| v.as_str())
177                            .map(String::from),
178                        track: params
179                            .and_then(|p| p.get("track"))
180                            .and_then(|v| v.as_str())
181                            .map(String::from),
182                        year: params
183                            .and_then(|p| p.get("year"))
184                            .and_then(|v| v.as_str())
185                            .map(String::from),
186                        genre: params
187                            .and_then(|p| p.get("genre"))
188                            .and_then(|v| v.as_str())
189                            .map(String::from),
190                        isrc: params
191                            .and_then(|p| p.get("isrc"))
192                            .and_then(|v| v.as_str())
193                            .map(String::from),
194                        upc: params
195                            .and_then(|p| p.get("upc"))
196                            .and_then(|v| v.as_str())
197                            .map(String::from),
198                        new: params
199                            .and_then(|p| p.get("new"))
200                            .and_then(|v| v.as_bool())
201                            .unwrap_or(false),
202                        hipster: params
203                            .and_then(|p| p.get("hipster"))
204                            .and_then(|v| v.as_bool())
205                            .unwrap_or(false),
206                    },
207                    play,
208                    sort,
209                };
210
211                commands::search_command(query, &types, options).await
212            }
213
214            // ============================================================
215            // Pin
216            // ============================================================
217            "pin.add" => {
218                let resource_type = params
219                    .and_then(|p| p.get("type"))
220                    .and_then(|v| v.as_str())
221                    .unwrap_or("track");
222                let url_or_id = params
223                    .and_then(|p| p.get("id"))
224                    .and_then(|v| v.as_str())
225                    .unwrap_or("");
226                let alias = params
227                    .and_then(|p| p.get("alias"))
228                    .and_then(|v| v.as_str())
229                    .unwrap_or("");
230                let tags = params.and_then(|p| p.get("tags")).and_then(|v| v.as_str());
231                commands::pin_add(resource_type, url_or_id, alias, tags).await
232            }
233            "pin.remove" => {
234                let alias_or_id = params
235                    .and_then(|p| p.get("id"))
236                    .and_then(|v| v.as_str())
237                    .unwrap_or("");
238                commands::pin_remove(alias_or_id).await
239            }
240            "pin.list" => {
241                let resource_type = params.and_then(|p| p.get("type")).and_then(|v| v.as_str());
242                commands::pin_list(resource_type).await
243            }
244
245            // ============================================================
246            // Playlist
247            // ============================================================
248            "playlist.list" => {
249                let limit = params
250                    .and_then(|p| p.get("limit"))
251                    .and_then(|v| v.as_u64())
252                    .unwrap_or(20) as u8;
253                let offset = params
254                    .and_then(|p| p.get("offset"))
255                    .and_then(|v| v.as_u64())
256                    .unwrap_or(0) as u32;
257                commands::playlist_list(limit, offset).await
258            }
259            "playlist.get" => {
260                let id = params
261                    .and_then(|p| p.get("id"))
262                    .and_then(|v| v.as_str())
263                    .unwrap_or("");
264                commands::playlist_get(id).await
265            }
266            "playlist.create" => {
267                let name = params
268                    .and_then(|p| p.get("name"))
269                    .and_then(|v| v.as_str())
270                    .unwrap_or("New Playlist");
271                let description = params
272                    .and_then(|p| p.get("description"))
273                    .and_then(|v| v.as_str());
274                let public = params
275                    .and_then(|p| p.get("public"))
276                    .and_then(|v| v.as_bool())
277                    .unwrap_or(false);
278                commands::playlist_create(name, description, public).await
279            }
280            "playlist.add" => {
281                let id = params
282                    .and_then(|p| p.get("id"))
283                    .and_then(|v| v.as_str())
284                    .unwrap_or("");
285                let uris: Vec<String> = params
286                    .and_then(|p| p.get("uris"))
287                    .and_then(|v| v.as_array())
288                    .map(|arr| {
289                        arr.iter()
290                            .filter_map(|v| v.as_str().map(String::from))
291                            .collect()
292                    })
293                    .unwrap_or_default();
294                let now_playing = params
295                    .and_then(|p| p.get("now_playing"))
296                    .and_then(|v| v.as_bool())
297                    .unwrap_or(false);
298                let position = params
299                    .and_then(|p| p.get("position"))
300                    .and_then(|v| v.as_u64())
301                    .map(|p| p as u32);
302                let dry_run = params
303                    .and_then(|p| p.get("dry_run"))
304                    .and_then(|v| v.as_bool())
305                    .unwrap_or(false);
306                commands::playlist_add(id, &uris, now_playing, position, dry_run).await
307            }
308            "playlist.remove" => {
309                let id = params
310                    .and_then(|p| p.get("id"))
311                    .and_then(|v| v.as_str())
312                    .unwrap_or("");
313                let uris: Vec<String> = params
314                    .and_then(|p| p.get("uris"))
315                    .and_then(|v| v.as_array())
316                    .map(|arr| {
317                        arr.iter()
318                            .filter_map(|v| v.as_str().map(String::from))
319                            .collect()
320                    })
321                    .unwrap_or_default();
322                let dry_run = params
323                    .and_then(|p| p.get("dry_run"))
324                    .and_then(|v| v.as_bool())
325                    .unwrap_or(false);
326                commands::playlist_remove(id, &uris, dry_run).await
327            }
328            "playlist.edit" => {
329                let id = params
330                    .and_then(|p| p.get("id"))
331                    .and_then(|v| v.as_str())
332                    .unwrap_or("");
333                let name = params.and_then(|p| p.get("name")).and_then(|v| v.as_str());
334                let description = params
335                    .and_then(|p| p.get("description"))
336                    .and_then(|v| v.as_str());
337                let public = params
338                    .and_then(|p| p.get("public"))
339                    .and_then(|v| v.as_bool());
340                commands::playlist_edit(id, name, description, public).await
341            }
342            "playlist.reorder" => {
343                let id = params
344                    .and_then(|p| p.get("id"))
345                    .and_then(|v| v.as_str())
346                    .unwrap_or("");
347                let from = params
348                    .and_then(|p| p.get("from"))
349                    .and_then(|v| v.as_u64())
350                    .unwrap_or(0) as u32;
351                let to = params
352                    .and_then(|p| p.get("to"))
353                    .and_then(|v| v.as_u64())
354                    .unwrap_or(0) as u32;
355                let count = params
356                    .and_then(|p| p.get("count"))
357                    .and_then(|v| v.as_u64())
358                    .unwrap_or(1) as u32;
359                commands::playlist_reorder(id, from, to, count).await
360            }
361            "playlist.follow" => {
362                let id = params
363                    .and_then(|p| p.get("id"))
364                    .and_then(|v| v.as_str())
365                    .unwrap_or("");
366                let public = params
367                    .and_then(|p| p.get("public"))
368                    .and_then(|v| v.as_bool())
369                    .unwrap_or(true);
370                commands::playlist_follow(id, public).await
371            }
372            "playlist.unfollow" => {
373                let id = params
374                    .and_then(|p| p.get("id"))
375                    .and_then(|v| v.as_str())
376                    .unwrap_or("");
377                commands::playlist_unfollow(id).await
378            }
379            "playlist.duplicate" => {
380                let id = params
381                    .and_then(|p| p.get("id"))
382                    .and_then(|v| v.as_str())
383                    .unwrap_or("");
384                let name = params.and_then(|p| p.get("name")).and_then(|v| v.as_str());
385                commands::playlist_duplicate(id, name).await
386            }
387            "playlist.cover" => {
388                let id = params
389                    .and_then(|p| p.get("id"))
390                    .and_then(|v| v.as_str())
391                    .unwrap_or("");
392                commands::playlist_cover(id).await
393            }
394            "playlist.user" => {
395                let user_id = params
396                    .and_then(|p| p.get("user_id"))
397                    .and_then(|v| v.as_str())
398                    .unwrap_or("");
399                commands::playlist_user(user_id).await
400            }
401
402            // ============================================================
403            // Library
404            // ============================================================
405            "library.list" => {
406                let limit = params
407                    .and_then(|p| p.get("limit"))
408                    .and_then(|v| v.as_u64())
409                    .unwrap_or(20) as u8;
410                let offset = params
411                    .and_then(|p| p.get("offset"))
412                    .and_then(|v| v.as_u64())
413                    .unwrap_or(0) as u32;
414                commands::library_list(limit, offset).await
415            }
416            "library.save" => {
417                let ids: Vec<String> = params
418                    .and_then(|p| p.get("ids"))
419                    .and_then(|v| v.as_array())
420                    .map(|arr| {
421                        arr.iter()
422                            .filter_map(|v| v.as_str().map(String::from))
423                            .collect()
424                    })
425                    .unwrap_or_default();
426                let now_playing = params
427                    .and_then(|p| p.get("now_playing"))
428                    .and_then(|v| v.as_bool())
429                    .unwrap_or(false);
430                let dry_run = params
431                    .and_then(|p| p.get("dry_run"))
432                    .and_then(|v| v.as_bool())
433                    .unwrap_or(false);
434                commands::library_save(&ids, now_playing, dry_run).await
435            }
436            "library.remove" => {
437                let ids: Vec<String> = params
438                    .and_then(|p| p.get("ids"))
439                    .and_then(|v| v.as_array())
440                    .map(|arr| {
441                        arr.iter()
442                            .filter_map(|v| v.as_str().map(String::from))
443                            .collect()
444                    })
445                    .unwrap_or_default();
446                let dry_run = params
447                    .and_then(|p| p.get("dry_run"))
448                    .and_then(|v| v.as_bool())
449                    .unwrap_or(false);
450                commands::library_remove(&ids, dry_run).await
451            }
452            "library.check" => {
453                let ids: Vec<String> = params
454                    .and_then(|p| p.get("ids"))
455                    .and_then(|v| v.as_array())
456                    .map(|arr| {
457                        arr.iter()
458                            .filter_map(|v| v.as_str().map(String::from))
459                            .collect()
460                    })
461                    .unwrap_or_default();
462                commands::library_check(&ids).await
463            }
464
465            // ============================================================
466            // Info
467            // ============================================================
468            "info.track" => {
469                let id = params.and_then(|p| p.get("id")).and_then(|v| v.as_str());
470                let id_only = params
471                    .and_then(|p| p.get("id_only"))
472                    .and_then(|v| v.as_bool())
473                    .unwrap_or(false);
474                commands::info_track(id, id_only).await
475            }
476            "info.album" => {
477                let id = params.and_then(|p| p.get("id")).and_then(|v| v.as_str());
478                let id_only = params
479                    .and_then(|p| p.get("id_only"))
480                    .and_then(|v| v.as_bool())
481                    .unwrap_or(false);
482                commands::info_album(id, id_only).await
483            }
484            "info.artist" => {
485                let id = params.and_then(|p| p.get("id")).and_then(|v| v.as_str());
486                let id_only = params
487                    .and_then(|p| p.get("id_only"))
488                    .and_then(|v| v.as_bool())
489                    .unwrap_or(false);
490                let view = params
491                    .and_then(|p| p.get("view"))
492                    .and_then(|v| v.as_str())
493                    .unwrap_or("details");
494                let market = params
495                    .and_then(|p| p.get("market"))
496                    .and_then(|v| v.as_str())
497                    .map(String::from)
498                    .unwrap_or_default();
499                let limit = params
500                    .and_then(|p| p.get("limit"))
501                    .and_then(|v| v.as_u64())
502                    .unwrap_or(20) as u8;
503                let offset = params
504                    .and_then(|p| p.get("offset"))
505                    .and_then(|v| v.as_u64())
506                    .unwrap_or(0) as u32;
507
508                let artist_view = match view {
509                    "top_tracks" => ArtistView::TopTracks,
510                    "albums" => ArtistView::Albums,
511                    "related" => ArtistView::Related,
512                    _ => ArtistView::Details,
513                };
514
515                let query = ArtistQuery::new()
516                    .with_id(id.map(String::from))
517                    .id_only(id_only)
518                    .view(artist_view)
519                    .market(market)
520                    .paginate(limit, offset);
521                commands::info_artist(query).await
522            }
523
524            // ============================================================
525            // User
526            // ============================================================
527            "user.profile" => commands::user_profile().await,
528            "user.top" => {
529                let item_type = params
530                    .and_then(|p| p.get("type"))
531                    .and_then(|v| v.as_str())
532                    .unwrap_or("tracks");
533                let range = params
534                    .and_then(|p| p.get("range"))
535                    .and_then(|v| v.as_str())
536                    .unwrap_or("medium");
537                let limit = params
538                    .and_then(|p| p.get("limit"))
539                    .and_then(|v| v.as_u64())
540                    .unwrap_or(20) as u8;
541                commands::user_top(item_type, range, limit).await
542            }
543            "user.get" => {
544                let user_id = params
545                    .and_then(|p| p.get("id"))
546                    .and_then(|v| v.as_str())
547                    .unwrap_or("");
548                commands::user_get(user_id).await
549            }
550
551            // ============================================================
552            // Show (Podcasts)
553            // ============================================================
554            "show.get" => {
555                let id = params
556                    .and_then(|p| p.get("id"))
557                    .and_then(|v| v.as_str())
558                    .unwrap_or("");
559                commands::show_get(id).await
560            }
561            "show.episodes" => {
562                let id = params
563                    .and_then(|p| p.get("id"))
564                    .and_then(|v| v.as_str())
565                    .unwrap_or("");
566                let limit = params
567                    .and_then(|p| p.get("limit"))
568                    .and_then(|v| v.as_u64())
569                    .unwrap_or(20) as u8;
570                let offset = params
571                    .and_then(|p| p.get("offset"))
572                    .and_then(|v| v.as_u64())
573                    .unwrap_or(0) as u32;
574                commands::show_episodes(id, limit, offset).await
575            }
576            "show.list" => {
577                let limit = params
578                    .and_then(|p| p.get("limit"))
579                    .and_then(|v| v.as_u64())
580                    .unwrap_or(20) as u8;
581                let offset = params
582                    .and_then(|p| p.get("offset"))
583                    .and_then(|v| v.as_u64())
584                    .unwrap_or(0) as u32;
585                commands::show_list(limit, offset).await
586            }
587            "show.save" => {
588                let ids: Vec<String> = params
589                    .and_then(|p| p.get("ids"))
590                    .and_then(|v| v.as_array())
591                    .map(|arr| {
592                        arr.iter()
593                            .filter_map(|v| v.as_str().map(String::from))
594                            .collect()
595                    })
596                    .unwrap_or_default();
597                commands::show_save(&ids).await
598            }
599            "show.remove" => {
600                let ids: Vec<String> = params
601                    .and_then(|p| p.get("ids"))
602                    .and_then(|v| v.as_array())
603                    .map(|arr| {
604                        arr.iter()
605                            .filter_map(|v| v.as_str().map(String::from))
606                            .collect()
607                    })
608                    .unwrap_or_default();
609                commands::show_remove(&ids).await
610            }
611            "show.check" => {
612                let ids: Vec<String> = params
613                    .and_then(|p| p.get("ids"))
614                    .and_then(|v| v.as_array())
615                    .map(|arr| {
616                        arr.iter()
617                            .filter_map(|v| v.as_str().map(String::from))
618                            .collect()
619                    })
620                    .unwrap_or_default();
621                commands::show_check(&ids).await
622            }
623
624            // ============================================================
625            // Episode
626            // ============================================================
627            "episode.get" => {
628                let id = params
629                    .and_then(|p| p.get("id"))
630                    .and_then(|v| v.as_str())
631                    .unwrap_or("");
632                commands::episode_get(id).await
633            }
634            "episode.list" => {
635                let limit = params
636                    .and_then(|p| p.get("limit"))
637                    .and_then(|v| v.as_u64())
638                    .unwrap_or(20) as u8;
639                let offset = params
640                    .and_then(|p| p.get("offset"))
641                    .and_then(|v| v.as_u64())
642                    .unwrap_or(0) as u32;
643                commands::episode_list(limit, offset).await
644            }
645            "episode.save" => {
646                let ids: Vec<String> = params
647                    .and_then(|p| p.get("ids"))
648                    .and_then(|v| v.as_array())
649                    .map(|arr| {
650                        arr.iter()
651                            .filter_map(|v| v.as_str().map(String::from))
652                            .collect()
653                    })
654                    .unwrap_or_default();
655                commands::episode_save(&ids).await
656            }
657            "episode.remove" => {
658                let ids: Vec<String> = params
659                    .and_then(|p| p.get("ids"))
660                    .and_then(|v| v.as_array())
661                    .map(|arr| {
662                        arr.iter()
663                            .filter_map(|v| v.as_str().map(String::from))
664                            .collect()
665                    })
666                    .unwrap_or_default();
667                commands::episode_remove(&ids).await
668            }
669            "episode.check" => {
670                let ids: Vec<String> = params
671                    .and_then(|p| p.get("ids"))
672                    .and_then(|v| v.as_array())
673                    .map(|arr| {
674                        arr.iter()
675                            .filter_map(|v| v.as_str().map(String::from))
676                            .collect()
677                    })
678                    .unwrap_or_default();
679                commands::episode_check(&ids).await
680            }
681
682            // ============================================================
683            // Audiobook
684            // ============================================================
685            "audiobook.get" => {
686                let id = params
687                    .and_then(|p| p.get("id"))
688                    .and_then(|v| v.as_str())
689                    .unwrap_or("");
690                commands::audiobook_get(id).await
691            }
692            "audiobook.chapters" => {
693                let id = params
694                    .and_then(|p| p.get("id"))
695                    .and_then(|v| v.as_str())
696                    .unwrap_or("");
697                let limit = params
698                    .and_then(|p| p.get("limit"))
699                    .and_then(|v| v.as_u64())
700                    .unwrap_or(20) as u8;
701                let offset = params
702                    .and_then(|p| p.get("offset"))
703                    .and_then(|v| v.as_u64())
704                    .unwrap_or(0) as u32;
705                commands::audiobook_chapters(id, limit, offset).await
706            }
707            "audiobook.list" => {
708                let limit = params
709                    .and_then(|p| p.get("limit"))
710                    .and_then(|v| v.as_u64())
711                    .unwrap_or(20) as u8;
712                let offset = params
713                    .and_then(|p| p.get("offset"))
714                    .and_then(|v| v.as_u64())
715                    .unwrap_or(0) as u32;
716                commands::audiobook_list(limit, offset).await
717            }
718            "audiobook.save" => {
719                let ids: Vec<String> = params
720                    .and_then(|p| p.get("ids"))
721                    .and_then(|v| v.as_array())
722                    .map(|arr| {
723                        arr.iter()
724                            .filter_map(|v| v.as_str().map(String::from))
725                            .collect()
726                    })
727                    .unwrap_or_default();
728                commands::audiobook_save(&ids).await
729            }
730            "audiobook.remove" => {
731                let ids: Vec<String> = params
732                    .and_then(|p| p.get("ids"))
733                    .and_then(|v| v.as_array())
734                    .map(|arr| {
735                        arr.iter()
736                            .filter_map(|v| v.as_str().map(String::from))
737                            .collect()
738                    })
739                    .unwrap_or_default();
740                commands::audiobook_remove(&ids).await
741            }
742            "audiobook.check" => {
743                let ids: Vec<String> = params
744                    .and_then(|p| p.get("ids"))
745                    .and_then(|v| v.as_array())
746                    .map(|arr| {
747                        arr.iter()
748                            .filter_map(|v| v.as_str().map(String::from))
749                            .collect()
750                    })
751                    .unwrap_or_default();
752                commands::audiobook_check(&ids).await
753            }
754
755            // ============================================================
756            // Album
757            // ============================================================
758            "album.list" => {
759                let limit = params
760                    .and_then(|p| p.get("limit"))
761                    .and_then(|v| v.as_u64())
762                    .unwrap_or(20) as u8;
763                let offset = params
764                    .and_then(|p| p.get("offset"))
765                    .and_then(|v| v.as_u64())
766                    .unwrap_or(0) as u32;
767                commands::album_list(limit, offset).await
768            }
769            "album.tracks" => {
770                let id = params
771                    .and_then(|p| p.get("id"))
772                    .and_then(|v| v.as_str())
773                    .unwrap_or("");
774                let limit = params
775                    .and_then(|p| p.get("limit"))
776                    .and_then(|v| v.as_u64())
777                    .unwrap_or(20) as u8;
778                let offset = params
779                    .and_then(|p| p.get("offset"))
780                    .and_then(|v| v.as_u64())
781                    .unwrap_or(0) as u32;
782                commands::album_tracks(id, limit, offset).await
783            }
784            "album.save" => {
785                let ids: Vec<String> = params
786                    .and_then(|p| p.get("ids"))
787                    .and_then(|v| v.as_array())
788                    .map(|arr| {
789                        arr.iter()
790                            .filter_map(|v| v.as_str().map(String::from))
791                            .collect()
792                    })
793                    .unwrap_or_default();
794                commands::album_save(&ids).await
795            }
796            "album.remove" => {
797                let ids: Vec<String> = params
798                    .and_then(|p| p.get("ids"))
799                    .and_then(|v| v.as_array())
800                    .map(|arr| {
801                        arr.iter()
802                            .filter_map(|v| v.as_str().map(String::from))
803                            .collect()
804                    })
805                    .unwrap_or_default();
806                commands::album_remove(&ids).await
807            }
808            "album.check" => {
809                let ids: Vec<String> = params
810                    .and_then(|p| p.get("ids"))
811                    .and_then(|v| v.as_array())
812                    .map(|arr| {
813                        arr.iter()
814                            .filter_map(|v| v.as_str().map(String::from))
815                            .collect()
816                    })
817                    .unwrap_or_default();
818                commands::album_check(&ids).await
819            }
820            "album.newReleases" => {
821                let limit = params
822                    .and_then(|p| p.get("limit"))
823                    .and_then(|v| v.as_u64())
824                    .unwrap_or(20) as u8;
825                let offset = params
826                    .and_then(|p| p.get("offset"))
827                    .and_then(|v| v.as_u64())
828                    .unwrap_or(0) as u32;
829                commands::album_new_releases(limit, offset).await
830            }
831
832            // ============================================================
833            // Chapter
834            // ============================================================
835            "chapter.get" => {
836                let id = params
837                    .and_then(|p| p.get("id"))
838                    .and_then(|v| v.as_str())
839                    .unwrap_or("");
840                commands::chapter_get(id).await
841            }
842
843            // ============================================================
844            // Category
845            // ============================================================
846            "category.list" => {
847                let limit = params
848                    .and_then(|p| p.get("limit"))
849                    .and_then(|v| v.as_u64())
850                    .unwrap_or(20) as u8;
851                let offset = params
852                    .and_then(|p| p.get("offset"))
853                    .and_then(|v| v.as_u64())
854                    .unwrap_or(0) as u32;
855                commands::category_list(limit, offset).await
856            }
857            "category.get" => {
858                let id = params
859                    .and_then(|p| p.get("id"))
860                    .and_then(|v| v.as_str())
861                    .unwrap_or("");
862                commands::category_get(id).await
863            }
864            "category.playlists" => {
865                let id = params
866                    .and_then(|p| p.get("id"))
867                    .and_then(|v| v.as_str())
868                    .unwrap_or("");
869                let limit = params
870                    .and_then(|p| p.get("limit"))
871                    .and_then(|v| v.as_u64())
872                    .unwrap_or(20) as u8;
873                let offset = params
874                    .and_then(|p| p.get("offset"))
875                    .and_then(|v| v.as_u64())
876                    .unwrap_or(0) as u32;
877                commands::category_playlists(id, limit, offset).await
878            }
879
880            // ============================================================
881            // Follow
882            // ============================================================
883            "follow.artist" => {
884                let ids: Vec<String> = params
885                    .and_then(|p| p.get("ids"))
886                    .and_then(|v| v.as_array())
887                    .map(|arr| {
888                        arr.iter()
889                            .filter_map(|v| v.as_str().map(String::from))
890                            .collect()
891                    })
892                    .unwrap_or_default();
893                let dry_run = params
894                    .and_then(|p| p.get("dry_run"))
895                    .and_then(|v| v.as_bool())
896                    .unwrap_or(false);
897                commands::follow_artist(&ids, dry_run).await
898            }
899            "follow.user" => {
900                let ids: Vec<String> = params
901                    .and_then(|p| p.get("ids"))
902                    .and_then(|v| v.as_array())
903                    .map(|arr| {
904                        arr.iter()
905                            .filter_map(|v| v.as_str().map(String::from))
906                            .collect()
907                    })
908                    .unwrap_or_default();
909                let dry_run = params
910                    .and_then(|p| p.get("dry_run"))
911                    .and_then(|v| v.as_bool())
912                    .unwrap_or(false);
913                commands::follow_user(&ids, dry_run).await
914            }
915            "follow.unfollowArtist" => {
916                let ids: Vec<String> = params
917                    .and_then(|p| p.get("ids"))
918                    .and_then(|v| v.as_array())
919                    .map(|arr| {
920                        arr.iter()
921                            .filter_map(|v| v.as_str().map(String::from))
922                            .collect()
923                    })
924                    .unwrap_or_default();
925                let dry_run = params
926                    .and_then(|p| p.get("dry_run"))
927                    .and_then(|v| v.as_bool())
928                    .unwrap_or(false);
929                commands::unfollow_artist(&ids, dry_run).await
930            }
931            "follow.unfollowUser" => {
932                let ids: Vec<String> = params
933                    .and_then(|p| p.get("ids"))
934                    .and_then(|v| v.as_array())
935                    .map(|arr| {
936                        arr.iter()
937                            .filter_map(|v| v.as_str().map(String::from))
938                            .collect()
939                    })
940                    .unwrap_or_default();
941                let dry_run = params
942                    .and_then(|p| p.get("dry_run"))
943                    .and_then(|v| v.as_bool())
944                    .unwrap_or(false);
945                commands::unfollow_user(&ids, dry_run).await
946            }
947            "follow.list" => {
948                let limit = params
949                    .and_then(|p| p.get("limit"))
950                    .and_then(|v| v.as_u64())
951                    .unwrap_or(20) as u8;
952                commands::follow_list(limit).await
953            }
954            "follow.checkArtist" => {
955                let ids: Vec<String> = params
956                    .and_then(|p| p.get("ids"))
957                    .and_then(|v| v.as_array())
958                    .map(|arr| {
959                        arr.iter()
960                            .filter_map(|v| v.as_str().map(String::from))
961                            .collect()
962                    })
963                    .unwrap_or_default();
964                commands::follow_check_artist(&ids).await
965            }
966            "follow.checkUser" => {
967                let ids: Vec<String> = params
968                    .and_then(|p| p.get("ids"))
969                    .and_then(|v| v.as_array())
970                    .map(|arr| {
971                        arr.iter()
972                            .filter_map(|v| v.as_str().map(String::from))
973                            .collect()
974                    })
975                    .unwrap_or_default();
976                commands::follow_check_user(&ids).await
977            }
978
979            // ============================================================
980            // Markets
981            // ============================================================
982            "markets.list" => commands::markets_list().await,
983
984            // ============================================================
985            // Unknown method
986            // ============================================================
987            _ => Response::err(
988                404, // Use HTTP-style code; JSON-RPC error code added in from_response
989                format!("Method not found: {}", request.method),
990                ErrorKind::Validation,
991            ),
992        }
993    }
994}
995
996impl Default for Dispatcher {
997    fn default() -> Self {
998        Self::new()
999    }
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004    use super::*;
1005    use crate::rpc::protocol::RpcRequest;
1006
1007    fn make_request(method: &str, params: Option<serde_json::Value>) -> RpcRequest {
1008        RpcRequest {
1009            jsonrpc: "2.0".to_string(),
1010            method: method.to_string(),
1011            params,
1012            id: Some(serde_json::json!(1)),
1013        }
1014    }
1015
1016    #[tokio::test]
1017    async fn test_ping() {
1018        let dispatcher = Dispatcher::new();
1019        let req = make_request("ping", None);
1020        let resp = dispatcher.dispatch(&req).await;
1021        assert_eq!(resp.message, "pong");
1022    }
1023
1024    #[tokio::test]
1025    async fn test_version() {
1026        let dispatcher = Dispatcher::new();
1027        let req = make_request("version", None);
1028        let resp = dispatcher.dispatch(&req).await;
1029        assert!(resp.payload.is_some());
1030        let payload = resp.payload.unwrap();
1031        assert!(payload.get("version").is_some());
1032        assert!(payload.get("name").is_some());
1033    }
1034
1035    #[tokio::test]
1036    async fn test_unknown_method() {
1037        let dispatcher = Dispatcher::new();
1038        let req = make_request("unknown.method", None);
1039        let resp = dispatcher.dispatch(&req).await;
1040        assert_eq!(resp.code, 404);
1041        assert!(resp.message.contains("Method not found"));
1042    }
1043
1044    #[tokio::test]
1045    async fn test_player_transfer_missing_device() {
1046        let dispatcher = Dispatcher::new();
1047        let req = make_request("player.transfer", None);
1048        let resp = dispatcher.dispatch(&req).await;
1049        assert_eq!(resp.code, 400);
1050        assert!(resp.message.contains("device"));
1051    }
1052
1053    #[test]
1054    fn test_dispatcher_default() {
1055        let _dispatcher = Dispatcher;
1056    }
1057}