moosicbox_library/
api.rs

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}