1#![allow(clippy::module_name_repetitions)]
2
3use actix_web::{
4 Result, Scope,
5 dev::{ServiceFactory, ServiceRequest},
6 error::{ErrorBadRequest, ErrorInternalServerError, ErrorNotFound},
7 route,
8 web::{self, Json},
9};
10use moosicbox_music_api_models::{AlbumsRequest, search::api::ApiSearchResultsResponse};
11use moosicbox_music_models::{AlbumSort, api::ApiAlbum, id::parse_integer_ranges_to_ids};
12use moosicbox_paging::{Page, PagingRequest};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use strum_macros::{AsRefStr, EnumString};
16use switchy_database::profiles::LibraryDatabase;
17
18use crate::{
19 LibraryAddFavoriteAlbumError, LibraryAddFavoriteArtistError, LibraryAddFavoriteTrackError,
20 LibraryAlbumError, LibraryAlbumOrder, LibraryAlbumOrderDirection, LibraryAlbumTracksError,
21 LibraryAlbumType, LibraryArtist, LibraryArtistAlbumsError, LibraryArtistError,
22 LibraryArtistOrder, LibraryArtistOrderDirection, LibraryAudioQuality,
23 LibraryFavoriteAlbumsError, LibraryFavoriteArtistsError, LibraryFavoriteTracksError,
24 LibraryRemoveFavoriteAlbumError, LibraryRemoveFavoriteArtistError,
25 LibraryRemoveFavoriteTrackError, LibraryTrack, LibraryTrackError, LibraryTrackFileUrlError,
26 LibraryTrackOrder, LibraryTrackOrderDirection, ReindexError, SearchType, add_favorite_album,
27 add_favorite_artist, add_favorite_track, album, album_tracks, artist, artist_albums,
28 favorite_albums, favorite_artists, favorite_tracks, reindex_global_search_index,
29 remove_favorite_album, remove_favorite_artist, remove_favorite_track, search, track,
30 track_file_url,
31};
32
33pub fn bind_services<
34 T: ServiceFactory<ServiceRequest, Config = (), Error = actix_web::Error, InitError = ()>,
35>(
36 scope: Scope<T>,
37) -> Scope<T> {
38 scope
39 .service(track_file_url_endpoint)
40 .service(favorite_artists_endpoint)
41 .service(add_favorite_artist_endpoint)
42 .service(remove_favorite_artist_endpoint)
43 .service(favorite_albums_endpoint)
44 .service(add_favorite_album_endpoint)
45 .service(remove_favorite_album_endpoint)
46 .service(favorite_tracks_endpoint)
47 .service(add_favorite_track_endpoint)
48 .service(remove_favorite_track_endpoint)
49 .service(artist_albums_endpoint)
50 .service(album_tracks_endpoint)
51 .service(album_endpoint)
52 .service(artist_endpoint)
53 .service(track_endpoint)
54 .service(search_endpoint)
55 .service(reindex_endpoint)
56}
57
58#[cfg(feature = "openapi")]
59#[derive(utoipa::OpenApi)]
60#[openapi(
61 tags((name = "Library")),
62 paths(
63 track_file_url_endpoint,
64 favorite_artists_endpoint,
65 add_favorite_artist_endpoint,
66 remove_favorite_artist_endpoint,
67 favorite_albums_endpoint,
68 add_favorite_album_endpoint,
69 remove_favorite_album_endpoint,
70 add_favorite_track_endpoint,
71 remove_favorite_track_endpoint,
72 favorite_tracks_endpoint,
73 artist_albums_endpoint,
74 album_tracks_endpoint,
75 album_endpoint,
76 artist_endpoint,
77 track_endpoint,
78 search_endpoint,
79 reindex_endpoint,
80 ),
81 components(schemas(
82 LibraryTrackQuery,
83 AlbumType,
84 ApiArtist,
85 ApiAlbum,
86 ApiTrack,
87 ApiLibraryArtist,
88 ApiLibraryAlbum,
89 ApiLibraryTrack,
90 ApiSearchResultsResponse,
91 moosicbox_music_api_models::search::api::ApiGlobalSearchResult,
92 moosicbox_music_api_models::search::api::ApiGlobalArtistSearchResult,
93 moosicbox_music_api_models::search::api::ApiGlobalAlbumSearchResult,
94 moosicbox_music_api_models::search::api::ApiGlobalTrackSearchResult,
95 LibraryArtistOrder,
96 LibraryArtistOrderDirection,
97 LibraryAlbumOrder,
98 LibraryAlbumOrderDirection,
99 LibraryTrackOrder,
100 LibraryTrackOrderDirection,
101 SearchType,
102 LibraryAudioQuality,
103 ))
104)]
105pub struct Api;
106
107#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
108#[serde(rename_all = "camelCase")]
109#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
110pub struct ApiLibraryAlbum {
111 pub id: u64,
112 pub artist: String,
113 pub artist_id: u64,
114 pub contains_cover: bool,
115 pub explicit: bool,
116 pub date_released: Option<String>,
117 pub title: String,
118}
119
120#[derive(Debug, Serialize, Deserialize, Clone)]
121#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
122#[serde(tag = "type")]
123#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
124pub enum ApiTrack {
125 Library(ApiLibraryTrack),
126}
127
128impl From<LibraryTrack> for ApiTrack {
129 fn from(value: LibraryTrack) -> Self {
130 Self::Library(ApiLibraryTrack {
131 id: value.id,
132 number: value.number,
133 album: value.album,
134 album_id: value.album_id,
135 artist: value.artist,
136 artist_id: value.artist_id,
137 contains_cover: value.artwork.is_some(),
138 duration: value.duration,
139 explicit: false,
140 title: value.title,
141 })
142 }
143}
144
145#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
146#[serde(rename_all = "camelCase")]
147#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
148pub struct ApiLibraryTrack {
149 pub id: u64,
150 pub number: u32,
151 pub album: String,
152 pub album_id: u64,
153 pub artist: String,
154 pub artist_id: u64,
155 pub contains_cover: bool,
156 pub duration: f64,
157 pub explicit: bool,
158 pub title: String,
159}
160
161#[derive(Debug, Serialize, Deserialize, Clone)]
162#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
163#[serde(tag = "type")]
164#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
165pub enum ApiArtist {
166 Library(ApiLibraryArtist),
167}
168
169impl From<LibraryArtist> for ApiArtist {
170 fn from(value: LibraryArtist) -> Self {
171 Self::Library(ApiLibraryArtist {
172 id: value.id,
173 contains_cover: value.cover.is_some(),
174 title: value.title,
175 })
176 }
177}
178
179#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
180#[serde(rename_all = "camelCase")]
181#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
182pub struct ApiLibraryArtist {
183 pub id: u64,
184 pub contains_cover: bool,
185 pub title: String,
186}
187
188impl From<LibraryTrackFileUrlError> for actix_web::Error {
189 fn from(e: LibraryTrackFileUrlError) -> Self {
190 match e {
191 LibraryTrackFileUrlError::NoFile => ErrorNotFound("Track file not found"),
192 LibraryTrackFileUrlError::LibraryTrack(_) => ErrorInternalServerError(e.to_string()),
193 }
194 }
195}
196
197#[derive(Deserialize)]
198#[serde(rename_all = "camelCase")]
199pub struct LibraryTrackFileUrlQuery {
200 track_id: u64,
201 audio_quality: LibraryAudioQuality,
202}
203
204#[cfg_attr(
205 feature = "openapi", utoipa::path(
206 tags = ["Library"],
207 get,
208 path = "/track/url",
209 description = "Get track stream URL for the audio",
210 params(
211 ("moosicbox-profile" = String, Header, description = "MoosicBox profile"),
212 ("trackId" = u64, Query, description = "The track ID"),
213 ("audioQuality" = LibraryAudioQuality, Query, description = "Page offset"),
214 ),
215 responses(
216 (
217 status = 200,
218 description = "The track URL",
219 body = Value,
220 )
221 )
222 )
223)]
224#[route("/track/url", method = "GET")]
225pub async fn track_file_url_endpoint(
226 query: web::Query<LibraryTrackFileUrlQuery>,
227 db: LibraryDatabase,
228) -> Result<Json<Value>> {
229 Ok(Json(serde_json::json!({
230 "urls": track_file_url(
231 &db,
232 query.audio_quality,
233 &query.track_id.into(),
234
235 )
236 .await?,
237 })))
238}
239
240impl From<LibraryFavoriteAlbumsError> for actix_web::Error {
241 fn from(err: LibraryFavoriteAlbumsError) -> Self {
242 log::error!("{err:?}");
243 ErrorInternalServerError(err.to_string())
244 }
245}
246
247#[derive(Deserialize)]
248#[serde(rename_all = "camelCase")]
249pub struct LibraryFavoriteAlbumsQuery {
250 offset: Option<u32>,
251 limit: Option<u32>,
252 order: Option<LibraryAlbumOrder>,
253 order_direction: Option<LibraryAlbumOrderDirection>,
254}
255
256#[cfg_attr(
257 feature = "openapi", utoipa::path(
258 tags = ["Library"],
259 get,
260 path = "/favorites/albums",
261 description = "List favorite albums",
262 params(
263 ("moosicbox-profile" = String, Header, description = "MoosicBox profile"),
264 ("offset" = Option<u32>, Query, description = "Page offset"),
265 ("limit" = Option<u32>, Query, description = "Page limit"),
266 ("order" = Option<LibraryAlbumOrder>, Query, description = "Sort order"),
267 ("orderDirection" = Option<LibraryAlbumOrderDirection>, Query, description = "Sort order direction"),
268 ),
269 responses(
270 (
271 status = 200,
272 description = "Page of album metadata",
273 body = Value,
274 )
275 )
276 )
277)]
278#[route("/favorites/albums", method = "GET")]
279pub async fn favorite_albums_endpoint(
280 query: web::Query<LibraryFavoriteAlbumsQuery>,
281 db: LibraryDatabase,
282) -> Result<Json<Page<ApiAlbum>>> {
283 Ok(Json(
284 favorite_albums(
285 &db,
286 &AlbumsRequest {
287 sort: match (query.order, query.order_direction) {
288 (None, None) => None,
289 (None, Some(direction)) => Some(match direction {
290 LibraryAlbumOrderDirection::Asc => AlbumSort::ReleaseDateAsc,
291 LibraryAlbumOrderDirection::Desc => AlbumSort::ReleaseDateDesc,
292 }),
293 (Some(order), None) => Some(match order {
294 LibraryAlbumOrder::Date => AlbumSort::ReleaseDateDesc,
295 }),
296 (Some(order), Some(direction)) => Some(match (order, direction) {
297 (LibraryAlbumOrder::Date, LibraryAlbumOrderDirection::Asc) => {
298 AlbumSort::ReleaseDateAsc
299 }
300 (LibraryAlbumOrder::Date, LibraryAlbumOrderDirection::Desc) => {
301 AlbumSort::ReleaseDateDesc
302 }
303 }),
304 },
305 page: if query.offset.is_some() || query.limit.is_some() {
306 Some(PagingRequest {
307 offset: query.offset.unwrap_or(0),
308 limit: query.limit.unwrap_or(10),
309 })
310 } else {
311 None
312 },
313 ..Default::default()
314 },
315 )
316 .await?
317 .ok_try_into_map_err(|e| LibraryFavoriteAlbumsError::RequestFailed(format!("{e:?}")))?
318 .into(),
319 ))
320}
321
322impl From<LibraryFavoriteArtistsError> for actix_web::Error {
323 fn from(err: LibraryFavoriteArtistsError) -> Self {
324 log::error!("{err:?}");
325 ErrorInternalServerError(err.to_string())
326 }
327}
328
329#[derive(Deserialize)]
330#[serde(rename_all = "camelCase")]
331pub struct LibraryFavoriteArtistsQuery {
332 offset: Option<u32>,
333 limit: Option<u32>,
334 order: Option<LibraryArtistOrder>,
335 order_direction: Option<LibraryArtistOrderDirection>,
336}
337
338#[cfg_attr(
339 feature = "openapi", utoipa::path(
340 tags = ["Library"],
341 get,
342 path = "/favorites/artists",
343 description = "List favorite artists",
344 params(
345 ("moosicbox-profile" = String, Header, description = "MoosicBox profile"),
346 ("offset" = Option<u32>, Query, description = "Page offset"),
347 ("limit" = Option<u32>, Query, description = "Page limit"),
348 ("order" = Option<LibraryArtistOrder>, Query, description = "Sort order"),
349 ("orderDirection" = Option<LibraryArtistOrderDirection>, Query, description = "Sort order direction"),
350 ),
351 responses(
352 (
353 status = 200,
354 description = "Page of artist metadata",
355 body = Value,
356 )
357 )
358 )
359)]
360#[route("/favorites/artists", method = "GET")]
361pub async fn favorite_artists_endpoint(
362 query: web::Query<LibraryFavoriteArtistsQuery>,
363 db: LibraryDatabase,
364) -> Result<Json<Page<ApiArtist>>> {
365 let artist: Page<LibraryArtist> = favorite_artists(
366 &db,
367 query.offset,
368 query.limit,
369 query.order,
370 query.order_direction,
371 )
372 .await?
373 .into();
374
375 Ok(Json(artist.into()))
376}
377
378impl From<LibraryAddFavoriteArtistError> for actix_web::Error {
379 fn from(err: LibraryAddFavoriteArtistError) -> Self {
380 log::error!("{err:?}");
381 ErrorInternalServerError(err.to_string())
382 }
383}
384
385#[derive(Deserialize)]
386#[serde(rename_all = "camelCase")]
387pub struct LibraryAddFavoriteArtistsQuery {
388 artist_id: u64,
389}
390
391#[cfg_attr(
392 feature = "openapi", utoipa::path(
393 tags = ["Library"],
394 post,
395 path = "/favorites/artists",
396 description = "Add favorite artist",
397 params(
398 ("moosicbox-profile" = String, Header, description = "MoosicBox profile"),
399 ("artistId" = u64, Query, description = "The artist ID"),
400 ),
401 responses(
402 (
403 status = 200,
404 description = "Success message",
405 body = Value,
406 )
407 )
408 )
409)]
410#[route("/favorites/artists", method = "POST")]
411pub async fn add_favorite_artist_endpoint(
412 query: web::Query<LibraryAddFavoriteArtistsQuery>,
413 db: LibraryDatabase,
414) -> Result<Json<Value>> {
415 add_favorite_artist(&db, &query.artist_id.into())?;
416
417 Ok(Json(serde_json::json!({
418 "success": true
419 })))
420}
421
422impl From<LibraryRemoveFavoriteArtistError> for actix_web::Error {
423 fn from(err: LibraryRemoveFavoriteArtistError) -> Self {
424 log::error!("{err:?}");
425 ErrorInternalServerError(err.to_string())
426 }
427}
428
429#[derive(Deserialize)]
430#[serde(rename_all = "camelCase")]
431pub struct LibraryRemoveFavoriteArtistsQuery {
432 artist_id: u64,
433}
434
435#[cfg_attr(
436 feature = "openapi", utoipa::path(
437 tags = ["Library"],
438 delete,
439 path = "/favorites/artists",
440 description = "Delete favorite artist",
441 params(
442 ("moosicbox-profile" = String, Header, description = "MoosicBox profile"),
443 ("artistId" = u64, Query, description = "The artist ID"),
444 ),
445 responses(
446 (
447 status = 200,
448 description = "Success message",
449 body = Value,
450 )
451 )
452 )
453)]
454#[route("/favorites/artists", method = "DELETE")]
455pub async fn remove_favorite_artist_endpoint(
456 query: web::Query<LibraryRemoveFavoriteArtistsQuery>,
457 db: LibraryDatabase,
458) -> Result<Json<Value>> {
459 remove_favorite_artist(&db, &query.artist_id.into())?;
460
461 Ok(Json(serde_json::json!({
462 "success": true
463 })))
464}
465
466impl From<LibraryAddFavoriteAlbumError> for actix_web::Error {
467 fn from(err: LibraryAddFavoriteAlbumError) -> Self {
468 log::error!("{err:?}");
469 ErrorInternalServerError(err.to_string())
470 }
471}
472
473#[derive(Deserialize)]
474#[serde(rename_all = "camelCase")]
475pub struct LibraryAddFavoriteAlbumsQuery {
476 album_id: u64,
477}
478
479#[cfg_attr(
480 feature = "openapi", utoipa::path(
481 tags = ["Library"],
482 post,
483 path = "/favorites/albums",
484 description = "Add favorite album",
485 params(
486 ("moosicbox-profile" = String, Header, description = "MoosicBox profile"),
487 ("albumId" = u64, Query, description = "The album ID"),
488 ),
489 responses(
490 (
491 status = 200,
492 description = "Success message",
493 body = Value,
494 )
495 )
496 )
497)]
498#[route("/favorites/albums", method = "POST")]
499pub async fn add_favorite_album_endpoint(
500 query: web::Query<LibraryAddFavoriteAlbumsQuery>,
501 db: LibraryDatabase,
502) -> Result<Json<Value>> {
503 add_favorite_album(&db, &query.album_id.into())?;
504
505 Ok(Json(serde_json::json!({
506 "success": true
507 })))
508}
509
510impl From<LibraryRemoveFavoriteAlbumError> for actix_web::Error {
511 fn from(err: LibraryRemoveFavoriteAlbumError) -> Self {
512 log::error!("{err:?}");
513 ErrorInternalServerError(err.to_string())
514 }
515}
516
517#[derive(Deserialize)]
518#[serde(rename_all = "camelCase")]
519pub struct LibraryRemoveFavoriteAlbumsQuery {
520 album_id: u64,
521}
522
523#[cfg_attr(
524 feature = "openapi", utoipa::path(
525 tags = ["Library"],
526 delete,
527 path = "/favorites/albums",
528 description = "Delete favorite album",
529 params(
530 ("moosicbox-profile" = String, Header, description = "MoosicBox profile"),
531 ("albumId" = u64, Query, description = "The album ID"),
532 ),
533 responses(
534 (
535 status = 200,
536 description = "Success message",
537 body = Value,
538 )
539 )
540 )
541)]
542#[route("/favorites/albums", method = "DELETE")]
543pub async fn remove_favorite_album_endpoint(
544 query: web::Query<LibraryRemoveFavoriteAlbumsQuery>,
545 db: LibraryDatabase,
546) -> Result<Json<Value>> {
547 remove_favorite_album(&db, &query.album_id.into())?;
548
549 Ok(Json(serde_json::json!({
550 "success": true
551 })))
552}
553
554impl From<LibraryAddFavoriteTrackError> for actix_web::Error {
555 fn from(err: LibraryAddFavoriteTrackError) -> Self {
556 log::error!("{err:?}");
557 ErrorInternalServerError(err.to_string())
558 }
559}
560
561#[derive(Deserialize)]
562#[serde(rename_all = "camelCase")]
563pub struct LibraryAddFavoriteTracksQuery {
564 track_id: u64,
565}
566
567#[cfg_attr(
568 feature = "openapi", utoipa::path(
569 tags = ["Library"],
570 post,
571 path = "/favorites/tracks",
572 description = "Add favorite track",
573 params(
574 ("moosicbox-profile" = String, Header, description = "MoosicBox profile"),
575 ("trackId" = u64, Query, description = "The track ID"),
576 ),
577 responses(
578 (
579 status = 200,
580 description = "Success message",
581 body = Value,
582 )
583 )
584 )
585)]
586#[route("/favorites/tracks", method = "POST")]
587pub async fn add_favorite_track_endpoint(
588 query: web::Query<LibraryAddFavoriteTracksQuery>,
589 db: LibraryDatabase,
590) -> Result<Json<Value>> {
591 add_favorite_track(&db, &query.track_id.into())?;
592
593 Ok(Json(serde_json::json!({
594 "success": true
595 })))
596}
597
598impl From<LibraryRemoveFavoriteTrackError> for actix_web::Error {
599 fn from(err: LibraryRemoveFavoriteTrackError) -> Self {
600 log::error!("{err:?}");
601 ErrorInternalServerError(err.to_string())
602 }
603}
604
605#[derive(Deserialize)]
606#[serde(rename_all = "camelCase")]
607pub struct LibraryRemoveFavoriteTracksQuery {
608 track_id: u64,
609}
610
611#[cfg_attr(
612 feature = "openapi", utoipa::path(
613 tags = ["Library"],
614 delete,
615 path = "/favorites/tracks",
616 description = "Delete favorite track",
617 params(
618 ("moosicbox-profile" = String, Header, description = "MoosicBox profile"),
619 ("trackId" = u64, Query, description = "The track ID"),
620 ),
621 responses(
622 (
623 status = 200,
624 description = "Success message",
625 body = Value,
626 )
627 )
628 )
629)]
630#[route("/favorites/tracks", method = "DELETE")]
631pub async fn remove_favorite_track_endpoint(
632 query: web::Query<LibraryRemoveFavoriteTracksQuery>,
633 db: LibraryDatabase,
634) -> Result<Json<Value>> {
635 remove_favorite_track(&db, &query.track_id.into())?;
636
637 Ok(Json(serde_json::json!({
638 "success": true
639 })))
640}
641
642impl From<LibraryFavoriteTracksError> for actix_web::Error {
643 fn from(err: LibraryFavoriteTracksError) -> Self {
644 log::error!("{err:?}");
645 ErrorInternalServerError(err.to_string())
646 }
647}
648
649#[derive(Deserialize)]
650#[serde(rename_all = "camelCase")]
651pub struct LibraryFavoriteTracksQuery {
652 track_ids: Option<String>,
653 offset: Option<u32>,
654 limit: Option<u32>,
655 order: Option<LibraryTrackOrder>,
656 order_direction: Option<LibraryTrackOrderDirection>,
657}
658
659#[cfg_attr(
660 feature = "openapi", utoipa::path(
661 tags = ["Library"],
662 get,
663 path = "/favorites/tracks",
664 description = "List favorite tracks",
665 params(
666 ("moosicbox-profile" = String, Header, description = "MoosicBox profile"),
667 ("trackIds" = Option<String>, Query, description = "A comma-separated list of track IDs"),
668 ("offset" = Option<u32>, Query, description = "Page offset"),
669 ("limit" = Option<u32>, Query, description = "Page limit"),
670 ("order" = Option<LibraryTrackOrder>, Query, description = "Sort order"),
671 ("orderDirection" = Option<LibraryTrackOrderDirection>, Query, description = "Sort order direction"),
672 ),
673 responses(
674 (
675 status = 200,
676 description = "List of artist album metadata",
677 body = Value,
678 )
679 )
680 )
681)]
682#[route("/favorites/tracks", method = "GET")]
683pub async fn favorite_tracks_endpoint(
684 query: web::Query<LibraryFavoriteTracksQuery>,
685 db: LibraryDatabase,
686) -> Result<Json<Page<ApiTrack>>> {
687 let track_ids = query
688 .track_ids
689 .as_ref()
690 .map(|ids| parse_integer_ranges_to_ids(ids.as_str()))
691 .transpose()
692 .map_err(|e| ErrorBadRequest(format!("Invalid track id values: {e:?}")))?;
693
694 let tracks: Page<LibraryTrack> = favorite_tracks(
695 &db,
696 track_ids.as_deref(),
697 query.offset,
698 query.limit,
699 query.order,
700 query.order_direction,
701 )
702 .await?
703 .into();
704
705 Ok(Json(tracks.into()))
706}
707
708impl From<LibraryArtistAlbumsError> for actix_web::Error {
709 fn from(err: LibraryArtistAlbumsError) -> Self {
710 log::error!("{err:?}");
711 ErrorInternalServerError(err.to_string())
712 }
713}
714
715#[derive(Deserialize)]
716#[serde(rename_all = "camelCase")]
717pub struct LibraryArtistAlbumsQuery {
718 artist_id: u64,
719 offset: Option<u32>,
720 limit: Option<u32>,
721 album_type: Option<AlbumType>,
722}
723
724#[derive(Debug, Serialize, Deserialize, EnumString, AsRefStr, Copy, Clone)]
725#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
726#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
727#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
728pub enum AlbumType {
729 Lp,
730 EpsAndSingles,
731 Compilations,
732}
733
734impl From<AlbumType> for LibraryAlbumType {
735 fn from(value: AlbumType) -> Self {
736 match value {
737 AlbumType::Lp => Self::Lp,
738 AlbumType::EpsAndSingles => Self::EpsAndSingles,
739 AlbumType::Compilations => Self::Compilations,
740 }
741 }
742}
743
744#[cfg_attr(
745 feature = "openapi", utoipa::path(
746 tags = ["Library"],
747 get,
748 path = "/artists/albums",
749 description = "Get the list of artist album metadata for an artistId",
750 params(
751 ("moosicbox-profile" = String, Header, description = "MoosicBox profile"),
752 ("artistId" = u64, Query, description = "The artist ID"),
753 ("offset" = Option<u32>, Query, description = "Page offset"),
754 ("limit" = Option<u32>, Query, description = "Page limit"),
755 ("albumType" = Option<AlbumType>, Query, description = "Filter to this album type"),
756 ),
757 responses(
758 (
759 status = 200,
760 description = "List of artist album metadata",
761 body = Vec<ApiAlbum>,
762 )
763 )
764 )
765)]
766#[route("/artists/albums", method = "GET")]
767pub async fn artist_albums_endpoint(
768 query: web::Query<LibraryArtistAlbumsQuery>,
769 db: LibraryDatabase,
770) -> Result<Json<Page<ApiAlbum>>> {
771 Ok(Json(
772 artist_albums(
773 &db,
774 &query.artist_id.into(),
775 query.offset,
776 query.limit,
777 query.album_type.map(Into::into),
778 )
779 .await?
780 .map(Into::into)
781 .into(),
782 ))
783}
784
785impl From<LibraryAlbumTracksError> for actix_web::Error {
786 fn from(err: LibraryAlbumTracksError) -> Self {
787 log::error!("{err:?}");
788 ErrorInternalServerError(err.to_string())
789 }
790}
791
792#[derive(Deserialize)]
793#[serde(rename_all = "camelCase")]
794pub struct LibraryAlbumTracksQuery {
795 album_id: u64,
796 offset: Option<u32>,
797 limit: Option<u32>,
798}
799
800#[cfg_attr(
801 feature = "openapi", utoipa::path(
802 tags = ["Library"],
803 get,
804 path = "/albums/tracks",
805 description = "Get the list of album track metadata for an albumId",
806 params(
807 ("moosicbox-profile" = String, Header, description = "MoosicBox profile"),
808 ("albumId" = u64, Query, description = "The album ID"),
809 ("offset" = Option<u32>, Query, description = "Page offset"),
810 ("limit" = Option<u32>, Query, description = "Page limit"),
811 ),
812 responses(
813 (
814 status = 200,
815 description = "List of album track metadata",
816 body = Vec<ApiTrack>,
817 )
818 )
819 )
820)]
821#[route("/albums/tracks", method = "GET")]
822pub async fn album_tracks_endpoint(
823 query: web::Query<LibraryAlbumTracksQuery>,
824 db: LibraryDatabase,
825) -> Result<Json<Page<ApiTrack>>> {
826 let tracks: Page<LibraryTrack> =
827 album_tracks(&db, &query.album_id.into(), query.offset, query.limit)
828 .await?
829 .into();
830
831 Ok(Json(tracks.into()))
832}
833
834impl From<LibraryAlbumError> for actix_web::Error {
835 fn from(err: LibraryAlbumError) -> Self {
836 log::error!("{err:?}");
837 match err {
838 LibraryAlbumError::DatabaseFetch(_) => ErrorInternalServerError(err.to_string()),
839 }
840 }
841}
842
843#[derive(Deserialize)]
844#[serde(rename_all = "camelCase")]
845pub struct LibraryAlbumQuery {
846 album_id: u64,
847}
848
849#[cfg_attr(
850 feature = "openapi", utoipa::path(
851 tags = ["Library"],
852 get,
853 path = "/albums",
854 description = "Get the album metadata for an albumId",
855 params(
856 ("moosicbox-profile" = String, Header, description = "MoosicBox profile"),
857 ("albumId" = u64, Query, description = "The album ID"),
858 ),
859 responses(
860 (
861 status = 200,
862 description = "Album metadata information",
863 body = ApiAlbum,
864 )
865 )
866 )
867)]
868#[route("/albums", method = "GET")]
869pub async fn album_endpoint(
870 query: web::Query<LibraryAlbumQuery>,
871 db: LibraryDatabase,
872) -> Result<Json<ApiAlbum>> {
873 let album = album(&db, &query.album_id.into())
874 .await?
875 .ok_or_else(|| ErrorNotFound("Album not found"))?;
876
877 Ok(Json(album.into()))
878}
879
880impl From<LibraryArtistError> for actix_web::Error {
881 fn from(err: LibraryArtistError) -> Self {
882 log::error!("{err:?}");
883 ErrorInternalServerError(err.to_string())
884 }
885}
886
887#[derive(Deserialize)]
888#[serde(rename_all = "camelCase")]
889pub struct LibraryArtistQuery {
890 artist_id: u64,
891}
892
893#[cfg_attr(
894 feature = "openapi", utoipa::path(
895 tags = ["Library"],
896 get,
897 path = "/artists",
898 description = "Get the artist metadata for an artistId",
899 params(
900 ("moosicbox-profile" = String, Header, description = "MoosicBox profile"),
901 ("artistId" = u64, Query, description = "The artist ID"),
902 ),
903 responses(
904 (
905 status = 200,
906 description = "Artist metadata information",
907 body = ApiArtist,
908 )
909 )
910 )
911)]
912#[route("/artists", method = "GET")]
913pub async fn artist_endpoint(
914 query: web::Query<LibraryArtistQuery>,
915 db: LibraryDatabase,
916) -> Result<Json<ApiArtist>> {
917 let artist = artist(&db, &query.artist_id.into()).await?;
918
919 Ok(Json(artist.into()))
920}
921
922impl From<LibraryTrackError> for actix_web::Error {
923 fn from(err: LibraryTrackError) -> Self {
924 log::error!("{err:?}");
925 ErrorInternalServerError(err.to_string())
926 }
927}
928
929#[derive(Deserialize)]
930#[serde(rename_all = "camelCase")]
931#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
932pub struct LibraryTrackQuery {
933 track_id: u64,
934}
935
936#[cfg_attr(
937 feature = "openapi", utoipa::path(
938 tags = ["Library"],
939 get,
940 path = "/tracks",
941 description = "Get the track metadata for a trackId",
942 params(
943 ("moosicbox-profile" = String, Header, description = "MoosicBox profile"),
944 ("trackId" = u64, Query, description = "The track ID"),
945 ),
946 responses(
947 (
948 status = 200,
949 description = "Track metadata information",
950 body = ApiTrack,
951 )
952 )
953 )
954)]
955#[route("/tracks", method = "GET")]
956pub async fn track_endpoint(
957 query: web::Query<LibraryTrackQuery>,
958 db: LibraryDatabase,
959) -> Result<Json<ApiTrack>> {
960 let track = track(&db, &query.track_id.into())
961 .await?
962 .ok_or_else(|| ErrorNotFound("Track not found"))?;
963
964 Ok(Json(track.into()))
965}
966
967#[derive(Deserialize)]
968#[serde(rename_all = "camelCase")]
969pub struct LibrarySearchQuery {
970 query: String,
971 offset: Option<u32>,
972 limit: Option<u32>,
973 types: Option<Vec<SearchType>>,
974}
975
976#[cfg_attr(
977 feature = "openapi", utoipa::path(
978 tags = ["Library"],
979 get,
980 path = "/search",
981 description = "Search the library for artists/albums/tracks that fuzzy match the query",
982 params(
983 ("moosicbox-profile" = String, Header, description = "MoosicBox profile"),
984 ("query" = String, Query, description = "The search query"),
985 ("offset" = Option<u32>, Query, description = "Page offset"),
986 ("limit" = Option<u32>, Query, description = "Page limit"),
987 ("types" = Option<Vec<SearchType>>, Query, description = "List of types to filter the search by"),
988 ),
989 responses(
990 (
991 status = 200,
992 description = "A page of matches for the given search query",
993 body = ApiSearchResultsResponse,
994 )
995 )
996 )
997)]
998#[route("/search", method = "GET")]
999pub async fn search_endpoint(
1000 query: web::Query<LibrarySearchQuery>,
1001) -> Result<Json<ApiSearchResultsResponse>> {
1002 let results = search(
1003 &query.query,
1004 query.offset,
1005 query.limit,
1006 query
1007 .types
1008 .clone()
1009 .map(|x| x.into_iter().map(Into::into).collect::<Vec<_>>())
1010 .as_deref(),
1011 )
1012 .map_err(ErrorInternalServerError)?;
1013
1014 Ok(Json(results))
1015}
1016
1017impl From<ReindexError> for actix_web::Error {
1018 fn from(err: ReindexError) -> Self {
1019 log::error!("{err:?}");
1020 match err {
1021 ReindexError::DatabaseFetch(_)
1022 | ReindexError::RecreateIndex(_)
1023 | ReindexError::PopulateIndex(_)
1024 | ReindexError::GetAlbums(_) => ErrorInternalServerError(err.to_string()),
1025 }
1026 }
1027}
1028
1029#[derive(Deserialize)]
1030#[serde(rename_all = "camelCase")]
1031pub struct ReindexQuery {}
1032
1033#[cfg_attr(
1034 feature = "openapi", utoipa::path(
1035 tags = ["Library"],
1036 post,
1037 path = "/reindex",
1038 description = "Reindex the search database with the complete library",
1039 params(
1040 ("moosicbox-profile" = String, Header, description = "MoosicBox profile"),
1041 ),
1042 responses(
1043 (
1044 status = 200,
1045 description = "Success message",
1046 body = Value,
1047 )
1048 )
1049 )
1050)]
1051#[route("/reindex", method = "POST")]
1052pub async fn reindex_endpoint(
1053 _query: web::Query<ReindexQuery>,
1054 db: LibraryDatabase,
1055) -> Result<Json<Value>> {
1056 reindex_global_search_index(&db).await?;
1057
1058 Ok(Json(serde_json::json!({"success": true})))
1059}