Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Music tracking #272

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b7a9510
fix(backend): user agent now honors MusicBrainz format
IgnisDa Aug 16, 2023
db9d6e2
feat(backend): add enum variants for music
IgnisDa Aug 16, 2023
486b235
fix(frontend): handle music
IgnisDa Aug 16, 2023
8743bd9
feat(backend): allow enabling or disabling music
IgnisDa Aug 16, 2023
6a5cece
feat(backend): configuration for music brainz
IgnisDa Aug 16, 2023
f93900a
fix(backend): remove serde default
IgnisDa Aug 16, 2023
52ab1dc
feat(backend): get music brainz search working
IgnisDa Aug 16, 2023
c9bb300
fix(frontend): precision issues
IgnisDa Aug 16, 2023
67f800e
fix(frontend): get verb for music
IgnisDa Aug 16, 2023
235b96d
chore(backend): get summary for music
IgnisDa Aug 16, 2023
f1409ae
feat(frontend): display music summary
IgnisDa Aug 16, 2023
dc6b4dd
fix(backend): get correct release data
IgnisDa Aug 16, 2023
b3a1baa
refactor(backend): rewrite to use LastFm instead of MusicBrainz
IgnisDa Aug 16, 2023
7b6404f
fix(backend): get enabled features from resolver
IgnisDa Aug 16, 2023
551671e
feat(backend): complete lastfm search response
IgnisDa Aug 16, 2023
7e9c78c
Revert "feat(backend): complete lastfm search response"
IgnisDa Aug 16, 2023
71627f3
Revert "refactor(backend): rewrite to use LastFm instead of MusicBrainz"
IgnisDa Aug 16, 2023
d6c463f
fix(backend): correct casing for configuration params
IgnisDa Aug 16, 2023
571cf80
Merge branch 'main'
IgnisDa Aug 16, 2023
21d02cf
refactor(backend): change structure of output
IgnisDa Aug 16, 2023
805c892
fix(frontend): adapt to new gql schema
IgnisDa Aug 16, 2023
d341a04
feat(backend): remove lot from media search results
IgnisDa Aug 16, 2023
6bbb035
feat(backend): return correct objects for creators
IgnisDa Aug 16, 2023
b5a1b35
refactor(backend): change type names
IgnisDa Aug 16, 2023
797f7a4
chore(frontend): adapt to new gql schema
IgnisDa Aug 16, 2023
696f8ca
refactor(backend): group search details together
IgnisDa Aug 17, 2023
93b5a17
chore(frontend): adapt to new gql schema
IgnisDa Aug 17, 2023
f1a016d
feat(*): get core page limit param from backend
IgnisDa Aug 17, 2023
620cb0a
feat(backend): config param for page limit
IgnisDa Aug 17, 2023
7c07427
feat(backend): use config param for page size
IgnisDa Aug 17, 2023
5e269ea
Merge branch 'main'
IgnisDa Aug 17, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion apps/backend/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,31 @@ pub struct MoviesTmdbConfig {
pub locale: String,
}

impl IsFeatureEnabled for MovieConfig {}

#[derive(Debug, Serialize, Deserialize, Clone, Config)]
pub struct MovieConfig {
/// Settings related to TMDB (movies).
#[setting(nested)]
pub tmdb: MoviesTmdbConfig,
}

impl IsFeatureEnabled for MovieConfig {}
#[derive(Debug, Serialize, Deserialize, Clone, Config)]
#[config(rename_all = "snake_case", env_prefix = "MUSIC_MUSIC_BRAINZ_")]
pub struct MusicBrainzConfig {
/// Used for changing the user agent if your requests are being rate limited.
pub user_agent: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone, Config)]
#[config(rename_all = "snake_case")]
pub struct MusicConfig {
/// Settings related to Music Brainz.
#[setting(nested)]
pub music_brainz: MusicBrainzConfig,
}

impl IsFeatureEnabled for MusicConfig {}

#[derive(Debug, Serialize, Deserialize, Clone, Config)]
#[config(rename_all = "snake_case", env_prefix = "MANGA_ANILIST_")]
Expand Down Expand Up @@ -443,6 +460,9 @@ pub struct AppConfig {
/// Settings related to movies.
#[setting(nested)]
pub movies: MovieConfig,
/// Settings related to music.
#[setting(nested)]
pub music: MusicConfig,
/// Settings related to podcasts.
#[setting(nested)]
pub podcasts: PodcastConfig,
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/src/migrator/m20230410_create_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ pub enum MetadataSource {
Itunes,
#[sea_orm(string_value = "LI")]
Listennotes,
#[sea_orm(string_value = "MU")]
MusicBrainz,
#[sea_orm(string_value = "OL")]
Openlibrary,
#[sea_orm(string_value = "TM")]
Expand Down Expand Up @@ -106,6 +108,8 @@ pub enum MetadataLot {
Manga,
#[sea_orm(string_value = "MO")]
Movie,
#[sea_orm(string_value = "MU")]
Music,
#[sea_orm(string_value = "SH")]
Show,
#[sea_orm(string_value = "VG")]
Expand Down
39 changes: 34 additions & 5 deletions apps/backend/src/miscellaneous/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ use crate::{
ImportOrExportItemRating, ImportOrExportItemReview, ImportOrExportItemSeen,
MangaSpecifics, MediaCreatorSearchItem, MediaDetails, MediaListItem, MediaSearchItem,
MediaSearchItemResponse, MediaSearchItemWithLot, MediaSpecifics, MetadataCreator,
MetadataImage, MetadataImageUrl, MetadataImages, MovieSpecifics, PodcastSpecifics,
PostReviewInput, ProgressUpdateError, ProgressUpdateErrorVariant, ProgressUpdateInput,
ProgressUpdateResultUnion, SeenOrReviewExtraInformation, SeenPodcastExtraInformation,
SeenShowExtraInformation, ShowSpecifics, UserMediaReminder, UserSummary,
VideoGameSpecifics, Visibility,
MetadataImage, MetadataImageUrl, MetadataImages, MovieSpecifics, MusicSpecifics,
PodcastSpecifics, PostReviewInput, ProgressUpdateError, ProgressUpdateErrorVariant,
ProgressUpdateInput, ProgressUpdateResultUnion, SeenOrReviewExtraInformation,
SeenPodcastExtraInformation, SeenShowExtraInformation, ShowSpecifics,
UserMediaReminder, UserSummary, VideoGameSpecifics, Visibility,
},
IdObject, SearchDetails, SearchInput, SearchResults,
},
Expand All @@ -80,6 +80,7 @@ use crate::{
igdb::IgdbService,
itunes::ITunesService,
listennotes::ListennotesService,
music_brainz::MusicBrainzService,
openlibrary::OpenlibraryService,
tmdb::{TmdbMovieService, TmdbService, TmdbShowService},
},
Expand Down Expand Up @@ -369,6 +370,7 @@ struct GraphqlMediaDetails {
audio_book_specifics: Option<AudioBookSpecifics>,
podcast_specifics: Option<PodcastSpecifics>,
manga_specifics: Option<MangaSpecifics>,
music_specifics: Option<MusicSpecifics>,
anime_specifics: Option<AnimeSpecifics>,
source_url: Option<String>,
}
Expand Down Expand Up @@ -1118,6 +1120,7 @@ pub struct MiscellaneousService {
pub tmdb_shows_service: TmdbShowService,
pub anilist_anime_service: AnilistAnimeService,
pub anilist_manga_service: AnilistMangaService,
pub music_brainz_service: MusicBrainzService,
pub integration_service: IntegrationService,
pub update_metadata: SqliteStorage<UpdateMetadataJob>,
pub recalculate_user_summary: SqliteStorage<RecalculateUserSummaryJob>,
Expand Down Expand Up @@ -1151,6 +1154,8 @@ impl MiscellaneousService {
TmdbMovieService::new(&config.movies.tmdb, config.frontend.page_size).await;
let tmdb_shows_service =
TmdbShowService::new(&config.shows.tmdb, config.frontend.page_size).await;
let music_brainz_service =
MusicBrainzService::new(&config.music.music_brainz, config.frontend.page_size).await;
let audible_service =
AudibleService::new(&config.audio_books.audible, config.frontend.page_size).await;
let igdb_service = IgdbService::new(&config.video_games, config.frontend.page_size).await;
Expand Down Expand Up @@ -1189,6 +1194,7 @@ impl MiscellaneousService {
tmdb_shows_service,
anilist_anime_service,
anilist_manga_service,
music_brainz_service,
integration_service,
update_metadata: update_metadata.clone(),
recalculate_user_summary: recalculate_user_summary.clone(),
Expand Down Expand Up @@ -1346,6 +1352,7 @@ impl MiscellaneousService {
let identifier = &model.identifier;
let source_url = match model.source {
MetadataSource::Custom => None,
MetadataSource::MusicBrainz => todo!(),
MetadataSource::Itunes => Some(format!(
"https://podcasts.apple.com/us/podcast/{slug}/id{identifier}"
)),
Expand Down Expand Up @@ -1402,6 +1409,7 @@ impl MiscellaneousService {
video_game_specifics: None,
audio_book_specifics: None,
podcast_specifics: None,
music_specifics: None,
manga_specifics: None,
anime_specifics: None,
source_url,
Expand All @@ -1413,6 +1421,9 @@ impl MiscellaneousService {
MediaSpecifics::Book(a) => {
resp.book_specifics = Some(a);
}
MediaSpecifics::Music(a) => {
resp.music_specifics = Some(a);
}
MediaSpecifics::Movie(a) => {
resp.movie_specifics = Some(a);
}
Expand Down Expand Up @@ -2469,6 +2480,8 @@ impl MiscellaneousService {

async fn user_preferences(&self, user_id: i32) -> Result<UserPreferences> {
let mut prefs = self.user_by_id(user_id).await?.preferences;
prefs.features_enabled.media.music =
self.config.music.is_enabled() && prefs.features_enabled.media.music;
prefs.features_enabled.media.anime =
self.config.anime.is_enabled() && prefs.features_enabled.media.anime;
prefs.features_enabled.media.audio_book =
Expand Down Expand Up @@ -2626,6 +2639,7 @@ impl MiscellaneousService {
fn get_provider(&self, lot: MetadataLot, source: MetadataSource) -> Result<Provider> {
let err = || Err(Error::new("This source is not supported".to_owned()));
let service: Provider = match source {
MetadataSource::MusicBrainz => Box::new(self.music_brainz_service.clone()),
MetadataSource::Openlibrary => Box::new(self.openlibrary_service.clone()),
MetadataSource::Itunes => Box::new(self.itunes_service.clone()),
MetadataSource::GoogleBooks => Box::new(self.google_books_service.clone()),
Expand Down Expand Up @@ -3239,6 +3253,12 @@ impl MiscellaneousService {
ls.media.movies.runtime += r;
}
}
MediaSpecifics::Music(item) => {
ls.media.music.listened += 1;
if let Some(r) = item.runtime {
ls.media.music.runtime += r;
}
}
MediaSpecifics::Show(item) => {
unique_shows.insert(seen.metadata_id);
for season in item.seasons {
Expand Down Expand Up @@ -3449,6 +3469,7 @@ impl MiscellaneousService {
}))
};
let specifics = match input.lot {
MetadataLot::Music => todo!(),
MetadataLot::AudioBook => match input.audio_book_specifics {
None => return err(),
Some(ref mut s) => MediaSpecifics::AudioBook(s.clone()),
Expand Down Expand Up @@ -3799,6 +3820,9 @@ impl MiscellaneousService {
"movie" => {
preferences.features_enabled.media.movie = value_bool.unwrap()
}
"music" => {
preferences.features_enabled.media.music = value_bool.unwrap()
}
"podcast" => {
preferences.features_enabled.media.podcast = value_bool.unwrap()
}
Expand Down Expand Up @@ -4121,6 +4145,7 @@ impl MiscellaneousService {

async fn media_sources_for_lot(&self, lot: MetadataLot) -> Vec<MetadataSource> {
match lot {
MetadataLot::Music => vec![MetadataSource::MusicBrainz],
MetadataLot::AudioBook => vec![MetadataSource::Audible],
MetadataLot::Book => vec![MetadataSource::Openlibrary, MetadataSource::GoogleBooks],
MetadataLot::Podcast => vec![MetadataSource::Itunes, MetadataSource::Listennotes],
Expand All @@ -4134,6 +4159,10 @@ impl MiscellaneousService {
MetadataSource::iter()
.map(|source| {
let (supported, default) = match source {
MetadataSource::MusicBrainz => (
MusicBrainzService::supported_languages(),
MusicBrainzService::default_language(),
),
MetadataSource::Itunes => (
ITunesService::supported_languages(),
ITunesService::default_language(),
Expand Down
26 changes: 26 additions & 0 deletions apps/backend/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,14 @@ pub mod media {
pub pages: Option<i32>,
}

#[derive(
Debug, Serialize, Deserialize, SimpleObject, Clone, InputObject, Eq, PartialEq, Default,
)]
#[graphql(input_name = "MusicSpecificsInput")]
pub struct MusicSpecifics {
pub runtime: Option<i32>,
}

#[derive(
Debug, Serialize, Deserialize, SimpleObject, Clone, InputObject, Eq, PartialEq, Default,
)]
Expand Down Expand Up @@ -383,6 +391,22 @@ pub mod media {
pub watched: i32,
}

#[derive(
SimpleObject,
Debug,
PartialEq,
Eq,
Clone,
Default,
Serialize,
Deserialize,
FromJsonQueryResult,
)]
pub struct MusicSummary {
pub runtime: i32,
pub listened: i32,
}

#[derive(
SimpleObject,
Debug,
Expand Down Expand Up @@ -465,6 +489,7 @@ pub mod media {
pub struct UserMediaSummary {
pub books: BooksSummary,
pub movies: MoviesSummary,
pub music: MusicSummary,
pub podcasts: PodcastsSummary,
pub shows: ShowsSummary,
pub video_games: VideoGamesSummary,
Expand Down Expand Up @@ -648,6 +673,7 @@ pub mod media {
AudioBook(AudioBookSpecifics),
Book(BookSpecifics),
Movie(MovieSpecifics),
Music(MusicSpecifics),
Podcast(PodcastSpecifics),
Show(ShowSpecifics),
VideoGame(VideoGameSpecifics),
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ pub mod google_books;
pub mod igdb;
pub mod itunes;
pub mod listennotes;
pub mod music_brainz;
pub mod openlibrary;
pub mod tmdb;
110 changes: 110 additions & 0 deletions apps/backend/src/providers/music_brainz.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use surf::{http::headers::USER_AGENT, Client};

use crate::{
config::MusicBrainzConfig,
models::{
media::{MediaDetails, MediaSearchItem},
SearchDetails, SearchResults,
},
traits::{MediaProvider, MediaProviderLanguages},
utils::{convert_date_to_year, get_base_http_client},
};

pub static URL: &str = "https://musicbrainz.org/ws/2/";
pub static IMAGES_URL: &str = "https://coverartarchive.org/";

#[derive(Debug, Clone)]
pub struct MusicBrainzService {
client: Client,
page_limit: i32,
}

impl MediaProviderLanguages for MusicBrainzService {
fn supported_languages() -> Vec<String> {
vec!["us".to_owned()]
}

fn default_language() -> String {
"us".to_owned()
}
}

impl MusicBrainzService {
pub async fn new(config: &MusicBrainzConfig, page_limit: i32) -> Self {
let mut headers = vec![];
if let Some(ref u) = config.user_agent {
headers.push((USER_AGENT, u.clone()));
}
let client = get_base_http_client(URL, headers);
Self { client, page_limit }
}
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct ItemReleaseGroup {
id: String,
title: String,
first_release_date: Option<String>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
struct SearchResponse {
count: i32,
release_groups: Vec<ItemReleaseGroup>,
}

#[async_trait]
impl MediaProvider for MusicBrainzService {
async fn details(&self, identifier: &str) -> Result<MediaDetails> {
todo!();
}

async fn search(
&self,
query: &str,
page: Option<i32>,
) -> Result<SearchResults<MediaSearchItem>> {
let page = page.unwrap_or(1);
let mut rsp = self
.client
.get("release-group")
.query(&serde_json::json!({
"query": format!("release:{}", query),
"limit": self.page_limit,
"offset": (page - 1) * self.page_limit,
"fmt": "json",
}))
.unwrap()
.await
.map_err(|e| anyhow!(e))?;
let search: SearchResponse = rsp.body_json().await.map_err(|e| anyhow!(e))?;
let items = search
.release_groups
.into_iter()
.map(|r| MediaSearchItem {
image: Some(format!("{}/release-group/{}/front", IMAGES_URL, r.id)),
identifier: r.id,
title: r.title,
publish_year: r.first_release_date.and_then(|d| convert_date_to_year(&d)),
})
.collect_vec();
let next_page = if search.count - ((page) * self.page_limit) > 0 {
Some(page + 1)
} else {
None
};
Ok(SearchResults {
details: SearchDetails {
total: search.count,
next_page,
},
items,
})
}
}
2 changes: 2 additions & 0 deletions apps/backend/src/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub struct UserMediaFeaturesEnabledPreferences {
pub book: bool,
pub manga: bool,
pub movie: bool,
pub music: bool,
pub podcast: bool,
pub show: bool,
pub video_game: bool,
Expand All @@ -52,6 +53,7 @@ impl Default for UserMediaFeaturesEnabledPreferences {
book: true,
manga: true,
movie: true,
music: true,
podcast: true,
show: true,
video_game: true,
Expand Down
Loading