diff --git a/Cargo.toml b/Cargo.toml index c5dfd0d6..730e5c90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ url = "2.3" [dependencies.diesel] version = "2.0.2" default_features = false -features = ["libsqlite3-sys", "r2d2", "sqlite"] +features = ["libsqlite3-sys", "r2d2", "sqlite", "returning_clauses_for_sqlite_3_35"] [dependencies.image] version = "0.24.4" diff --git a/migrations/2022-11-12-143833_multiple_artists/down.sql b/migrations/2022-11-12-143833_multiple_artists/down.sql new file mode 100644 index 00000000..b86c5016 --- /dev/null +++ b/migrations/2022-11-12-143833_multiple_artists/down.sql @@ -0,0 +1,61 @@ +-- songs +CREATE TEMPORARY TABLE songs_backup(id, path, parent, track_number, disc_number, title, year, album, artwork, duration, lyricist, composer, genre, label); +INSERT INTO songs_backup SELECT * FROM songs; +DROP TABLE songs; +CREATE TABLE songs ( + id INTEGER PRIMARY KEY NOT NULL, + path TEXT NOT NULL, + parent TEXT NOT NULL, + track_number INTEGER, + disc_number INTEGER, + title TEXT, + artist TEXT, + album_artist TEXT, + year INTEGER, + album TEXT, + artwork TEXT, + duration INTEGER, + lyricist TEXT, + composer TEXT, + genre TEXT, + label TEXT, + UNIQUE(path) +); +INSERT INTO songs + SELECT s.id, s.path, s.parent, s.track_number, s.disc_number, s.title, s.year, s.album, s.artwork, s.duration, s.lyricist, s.composer, s.genre, s.label, a.name AS artist, aa.name AS album_artist + FROM songs_backup s + INNER JOIN song_artists sa ON sa.song = s.id + INNER JOIN artists a ON a.id = sa.artist + INNER JOIN song_album_artists saa ON saa.song = s.id + INNER JOIN artists aa ON aa.id = saa.artist + GROUP BY s.id; +DROP TABLE songs_backup; +DROP TABLE song_artists; +DROP TABLE song_album_artists; + +-- directories +CREATE TEMPORARY TABLE directories_backup(id, path, parent, year, album, artwork, date_added); +INSERT INTO directories_backup SELECT * FROM directories; +DROP TABLE directories; +CREATE TABLE directories ( + id INTEGER PRIMARY KEY NOT NULL, + path TEXT NOT NULL, + parent TEXT, + artist TEXT, + year TEXT, + album TEXT, + artwork TEXT, + date_added INTEGER NOT NULL, + UNIQUE(path) +); +INSERT INTO directories + SELECT d.id, d.path, d.parent, d.year, d.album, d.artwork, d.date_added, a.name AS artist + FROM directories_backup d + INNER JOIN directory_artists da ON da.directory = d.id + INNER JOIN artists a ON a.id = da.artist + GROUP BY d.id; +DROP TABLE directories_backup; +DROP TABLE directory_artists; + +-- artists +DROP TABLE artists; diff --git a/migrations/2022-11-12-143833_multiple_artists/up.sql b/migrations/2022-11-12-143833_multiple_artists/up.sql new file mode 100644 index 00000000..6f527a03 --- /dev/null +++ b/migrations/2022-11-12-143833_multiple_artists/up.sql @@ -0,0 +1,93 @@ +CREATE TABLE artists ( + id INTEGER PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + UNIQUE(name) +); + +-- songs +CREATE TEMPORARY TABLE songs_backup(id, path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork, duration, lyricist, composer, genre, label); +INSERT INTO songs_backup SELECT * FROM songs; +DROP TABLE songs; +CREATE TABLE songs ( + id INTEGER PRIMARY KEY NOT NULL, + path TEXT NOT NULL, + parent TEXT NOT NULL, + track_number INTEGER, + disc_number INTEGER, + title TEXT, + year INTEGER, + album TEXT, + artwork TEXT, + duration INTEGER, + lyricist TEXT, + composer TEXT, + genre TEXT, + label TEXT, + UNIQUE(path) +); +INSERT INTO songs SELECT id, path, parent, track_number, disc_number, title, year, album, artwork, duration, lyricist, composer, genre, label FROM songs_backup; + +CREATE TABLE song_artists ( + song INTEGER NOT NULL, + artist INTEGER NOT NULL, + PRIMARY KEY (song, artist), + FOREIGN KEY(song) REFERENCES songs(id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY(artist) REFERENCES artists(id) ON DELETE CASCADE ON UPDATE CASCADE, + UNIQUE(song, artist) ON CONFLICT IGNORE +); + +CREATE TABLE song_album_artists ( + song INTEGER NOT NULL, + artist INTEGER NOT NULL, + PRIMARY KEY (song, artist), + FOREIGN KEY(song) REFERENCES songs(id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY(artist) REFERENCES artists(id) ON DELETE CASCADE ON UPDATE CASCADE, + UNIQUE(song, artist) ON CONFLICT IGNORE +); + +INSERT OR IGNORE INTO artists SELECT NULL, s.artist FROM songs_backup s; +INSERT INTO song_artists + SELECT s.id as song, a.id as artist + FROM songs_backup s, artists a + WHERE s.artist == a.name; + +INSERT OR IGNORE INTO artists SELECT NULL, s.album_artist AS name FROM songs_backup s; +INSERT INTO song_album_artists + SELECT s.id as song, a.id as album_artist + FROM songs_backup s, artists a + WHERE s.artist == a.name; + +DROP TABLE songs_backup; + +-- directories +CREATE TEMPORARY TABLE directories_backup(id, path, parent, artist, year, album, artwork, date_added); +INSERT INTO directories_backup SELECT * FROM directories; +DROP TABLE directories; +CREATE TABLE directories ( + id INTEGER PRIMARY KEY NOT NULL, + path TEXT NOT NULL, + parent TEXT, + year TEXT, + album TEXT, + artwork TEXT, + date_added INTEGER NOT NULL, + UNIQUE(path) +); +INSERT INTO directories SELECT id, path, parent, year, album, artwork, date_added FROM directories_backup; + +CREATE TABLE directory_artists ( + directory INTEGER NOT NULL, + artist INTEGER NOT NULL, + PRIMARY KEY (directory, artist), + FOREIGN KEY(directory) REFERENCES directories(id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY(artist) REFERENCES artists(id) ON DELETE CASCADE ON UPDATE CASCADE, + UNIQUE(directory, artist) ON CONFLICT IGNORE +); + +INSERT OR IGNORE INTO artists SELECT NULL, d.artist AS name FROM directories_backup d; +INSERT INTO directory_artists + SELECT d.id as directory, a.id as artist + FROM directories_backup d, artists a + WHERE d.artist == a.name; + +DROP TABLE directories_backup; diff --git a/src/app/artists.rs b/src/app/artists.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/app/index/metadata.rs b/src/app/index/metadata.rs index ca3c273a..7a305128 100644 --- a/src/app/index/metadata.rs +++ b/src/app/index/metadata.rs @@ -28,14 +28,14 @@ pub enum Error { VorbisCommentNotFoundInFlacFile, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct SongTags { pub disc_number: Option, pub track_number: Option, pub title: Option, pub duration: Option, - pub artist: Option, - pub album_artist: Option, + pub artists: Vec, + pub album_artists: Vec, pub album: Option, pub year: Option, pub has_artwork: bool, @@ -47,38 +47,29 @@ pub struct SongTags { impl From for SongTags { fn from(tag: id3::Tag) -> Self { - let artist = tag.artist().map(|s| s.to_string()); - let album_artist = tag.album_artist().map(|s| s.to_string()); - let album = tag.album().map(|s| s.to_string()); - let title = tag.title().map(|s| s.to_string()); - let duration = tag.duration(); - let disc_number = tag.disc(); - let track_number = tag.track(); - let year = tag - .year() - .or_else(|| tag.date_released().map(|d| d.year)) - .or_else(|| tag.original_date_released().map(|d| d.year)) - .or_else(|| tag.date_recorded().map(|d| d.year)); - let has_artwork = tag.pictures().count() > 0; - let lyricist = tag.get_text("TEXT"); - let composer = tag.get_text("TCOM"); - let genre = tag.genre().map(|s| s.to_string()); - let label = tag.get_text("TPUB"); - SongTags { - disc_number, - track_number, - title, - duration, - artist, - album_artist, - album, - year, - has_artwork, - lyricist, - composer, - genre, - label, + disc_number: tag.disc(), + track_number: tag.track(), + title: tag.title().map(|s| s.to_string()), + duration: tag.duration(), + artists: tag + .artists() + .map_or(Vec::new(), |v| v.iter().map(|s| s.to_string()).collect()), + album_artists: tag + .text_values_for_frame_id("TPE2") + .map_or(Vec::new(), |v| v.iter().map(|s| s.to_string()).collect()), + album: tag.album().map(|s| s.to_string()), + year: tag + .year() + .map(|y| y as i32) + .or_else(|| tag.date_released().map(|d| d.year)) + .or_else(|| tag.original_date_released().map(|d| d.year)) + .or_else(|| tag.date_recorded().map(|d| d.year)), + has_artwork: tag.pictures().count() > 0, + lyricist: tag.get_text("TEXT"), + composer: tag.get_text("TCOM"), + genre: tag.genre().map(|s| s.to_string()), + label: tag.get_text("TPUB"), } } } @@ -193,61 +184,42 @@ fn read_ape_x_of_y(item: &ape::Item) -> Option { fn read_ape(path: &Path) -> Result { let tag = ape::read_from_path(path)?; - let artist = tag.item("Artist").and_then(read_ape_string); - let album = tag.item("Album").and_then(read_ape_string); - let album_artist = tag.item("Album artist").and_then(read_ape_string); - let title = tag.item("Title").and_then(read_ape_string); - let year = tag.item("Year").and_then(read_ape_i32); - let disc_number = tag.item("Disc").and_then(read_ape_x_of_y); - let track_number = tag.item("Track").and_then(read_ape_x_of_y); - let lyricist = tag.item("LYRICIST").and_then(read_ape_string); - let composer = tag.item("COMPOSER").and_then(read_ape_string); - let genre = tag.item("GENRE").and_then(read_ape_string); - let label = tag.item("PUBLISHER").and_then(read_ape_string); - Ok(SongTags { - artist, - album_artist, - album, - title, - duration: None, - disc_number, - track_number, - year, - has_artwork: false, - lyricist, - composer, - genre, - label, - }) + + let mut tags = SongTags::default(); + for item in tag.iter() { + let key = item.key.as_str(); + utils::match_ignore_case! { + match key { + "TITLE" => tags.title = read_ape_string(item), + "ALBUM" => tags.album = read_ape_string(item), + "ARTIST" => tags.artists.extend(read_ape_string(item)), + "ALBUM ARTIST" => tags.album_artists.extend(read_ape_string(item)), + "TRACK" => tags.track_number = read_ape_x_of_y(item), + "DISC" => tags.disc_number = read_ape_x_of_y(item), + "YEAR" => tags.year = read_ape_i32(item), + "LYRICIST" => tags.lyricist = read_ape_string(item), + "COMPOSER" => tags.composer = read_ape_string(item), + "GENRE" => tags.genre = read_ape_string(item), + "PUBLISHER" => tags.label = read_ape_string(item), + _ => (), + } + } + } + Ok(tags) } fn read_vorbis(path: &Path) -> Result { let file = fs::File::open(path).map_err(|e| Error::Io(path.to_owned(), e))?; let source = OggStreamReader::new(file)?; - let mut tags = SongTags { - artist: None, - album_artist: None, - album: None, - title: None, - duration: None, - disc_number: None, - track_number: None, - year: None, - has_artwork: false, - lyricist: None, - composer: None, - genre: None, - label: None, - }; - + let mut tags = SongTags::default(); for (key, value) in source.comment_hdr.comment_list { utils::match_ignore_case! { match key { "TITLE" => tags.title = Some(value), "ALBUM" => tags.album = Some(value), - "ARTIST" => tags.artist = Some(value), - "ALBUMARTIST" => tags.album_artist = Some(value), + "ARTIST" => tags.artists.push(value), + "ALBUMARTIST" => tags.album_artists.push(value), "TRACKNUMBER" => tags.track_number = value.parse::().ok(), "DISCNUMBER" => tags.disc_number = value.parse::().ok(), "DATE" => tags.year = value.parse::().ok(), @@ -266,29 +238,14 @@ fn read_vorbis(path: &Path) -> Result { fn read_opus(path: &Path) -> Result { let headers = opus_headers::parse_from_path(path)?; - let mut tags = SongTags { - artist: None, - album_artist: None, - album: None, - title: None, - duration: None, - disc_number: None, - track_number: None, - year: None, - has_artwork: false, - lyricist: None, - composer: None, - genre: None, - label: None, - }; - + let mut tags = SongTags::default(); for (key, value) in headers.comments.user_comments { utils::match_ignore_case! { match key { "TITLE" => tags.title = Some(value), "ALBUM" => tags.album = Some(value), - "ARTIST" => tags.artist = Some(value), - "ALBUMARTIST" => tags.album_artist = Some(value), + "ARTIST" => tags.artists.push(value), + "ALBUMARTIST" => tags.album_artists.push(value), "TRACKNUMBER" => tags.track_number = value.parse::().ok(), "DISCNUMBER" => tags.disc_number = value.parse::().ok(), "DATE" => tags.year = value.parse::().ok(), @@ -312,24 +269,21 @@ fn read_flac(path: &Path) -> Result { let disc_number = vorbis .get("DISCNUMBER") .and_then(|d| d[0].parse::().ok()); - let year = vorbis.get("DATE").and_then(|d| d[0].parse::().ok()); let mut streaminfo = tag.get_blocks(metaflac::BlockType::StreamInfo); let duration = match streaminfo.next() { Some(metaflac::Block::StreamInfo(s)) => Some(s.total_samples as u32 / s.sample_rate), _ => None, }; - let has_artwork = tag.pictures().count() > 0; - Ok(SongTags { - artist: vorbis.artist().map(|v| v[0].clone()), - album_artist: vorbis.album_artist().map(|v| v[0].clone()), + artists: vorbis.artist().map_or(Vec::new(), Vec::clone), + album_artists: vorbis.album_artist().map_or(Vec::new(), Vec::clone), album: vorbis.album().map(|v| v[0].clone()), title: vorbis.title().map(|v| v[0].clone()), duration, disc_number, track_number: vorbis.track(), - year, - has_artwork, + year: vorbis.get("DATE").and_then(|d| d[0].parse::().ok()), + has_artwork: tag.pictures().count() > 0, lyricist: vorbis.get("LYRICIST").map(|v| v[0].clone()), composer: vorbis.get("COMPOSER").map(|v| v[0].clone()), genre: vorbis.get("GENRE").map(|v| v[0].clone()), @@ -342,8 +296,8 @@ fn read_mp4(path: &Path) -> Result { let label_ident = mp4ameta::FreeformIdent::new("com.apple.iTunes", "Label"); Ok(SongTags { - artist: tag.take_artist(), - album_artist: tag.take_album_artist(), + artists: tag.take_artists().collect(), + album_artists: tag.take_album_artists().collect(), album: tag.take_album(), title: tag.take_title(), duration: tag.duration().map(|v| v.as_secs() as u32), @@ -364,8 +318,8 @@ fn reads_file_metadata() { disc_number: Some(3), track_number: Some(1), title: Some("TEST TITLE".into()), - artist: Some("TEST ARTIST".into()), - album_artist: Some("TEST ALBUM ARTIST".into()), + artists: vec!["TEST ARTIST".into()], + album_artists: vec!["TEST ALBUM ARTIST".into()], album: Some("TEST ALBUM".into()), duration: None, year: Some(2016), diff --git a/src/app/index/query.rs b/src/app/index/query.rs index 54946359..c984ba74 100644 --- a/src/app/index/query.rs +++ b/src/app/index/query.rs @@ -1,10 +1,11 @@ use diesel::dsl::sql; -use diesel::prelude::*; use diesel::sql_types; +use diesel::{alias, prelude::*}; use std::path::{Path, PathBuf}; use super::*; -use crate::db::{self, directories, songs}; +use crate::db::{self, artists, directories, song_album_artists, song_artists, songs}; +use crate::service::dto; #[derive(thiserror::Error, Debug)] pub enum QueryError { @@ -24,7 +25,7 @@ sql_function!( ); impl Index { - pub fn browse

(&self, virtual_path: P) -> Result, QueryError> + pub fn browse

(&self, virtual_path: P) -> Result, QueryError> where P: AsRef, { @@ -37,10 +38,16 @@ impl Index { let real_directories: Vec = directories::table .filter(directories::parent.is_null()) .load(&mut connection)?; + let virtual_directories = real_directories .into_iter() - .filter_map(|d| d.virtualize(&vfs)); - output.extend(virtual_directories.map(CollectionFile::Directory)); + .filter_map(|d| d.virtualize(&vfs)) + .map(|d| d.fetch_artists(&mut connection)) + .map(|d| d.map(dto::CollectionFile::Directory)); + + for d in virtual_directories { + output.push(d?); + } } else { // Browse sub-directory let real_path = vfs.virtual_to_real(virtual_path)?; @@ -50,23 +57,38 @@ impl Index { .filter(directories::parent.eq(&real_path_string)) .order(sql::("path COLLATE NOCASE ASC")) .load(&mut connection)?; - let virtual_directories = real_directories - .into_iter() - .filter_map(|d| d.virtualize(&vfs)); - output.extend(virtual_directories.map(CollectionFile::Directory)); let real_songs: Vec = songs::table .filter(songs::parent.eq(&real_path_string)) .order(sql::("path COLLATE NOCASE ASC")) .load(&mut connection)?; - let virtual_songs = real_songs.into_iter().filter_map(|s| s.virtualize(&vfs)); - output.extend(virtual_songs.map(CollectionFile::Song)); + + // Preallocate capacity + output.reserve(real_directories.len() + real_songs.len()); + + let virtual_directories = real_directories + .into_iter() + .filter_map(|d| d.virtualize(&vfs)) + .map(|s| s.fetch_artists(&mut connection)) + .map(|s| s.map(dto::CollectionFile::Directory)); + for d in virtual_directories { + output.push(d?); + } + + let virtual_songs = real_songs + .into_iter() + .filter_map(|s| s.virtualize(&vfs)) + .map(|s| s.fetch_artists(&mut connection)) + .map(|s| s.map(dto::CollectionFile::Song)); + for d in virtual_songs { + output.push(d?); + } } Ok(output) } - pub fn flatten

(&self, virtual_path: P) -> Result, QueryError> + pub fn flatten

(&self, virtual_path: P) -> Result, QueryError> where P: AsRef, { @@ -89,11 +111,15 @@ impl Index { songs.order(path).load(&mut connection)? }; - let virtual_songs = real_songs.into_iter().filter_map(|s| s.virtualize(&vfs)); - Ok(virtual_songs.collect::>()) + let virtual_songs = real_songs + .into_iter() + .filter_map(|s| s.virtualize(&vfs)) + .map(|s| s.fetch_artists(&mut connection)); + + Ok(virtual_songs.collect::>()?) } - pub fn get_random_albums(&self, count: i64) -> Result, QueryError> { + pub fn get_random_albums(&self, count: i64) -> Result, QueryError> { use self::directories::dsl::*; let vfs = self.vfs_manager.get_vfs()?; let mut connection = self.db.connect()?; @@ -102,13 +128,16 @@ impl Index { .limit(count) .order(random()) .load(&mut connection)?; + let virtual_directories = real_directories .into_iter() - .filter_map(|d| d.virtualize(&vfs)); - Ok(virtual_directories.collect::>()) + .filter_map(|d| d.virtualize(&vfs)) + .map(|d| d.fetch_artists(&mut connection)); + + Ok(virtual_directories.collect::>()?) } - pub fn get_recent_albums(&self, count: i64) -> Result, QueryError> { + pub fn get_recent_albums(&self, count: i64) -> Result, QueryError> { use self::directories::dsl::*; let vfs = self.vfs_manager.get_vfs()?; let mut connection = self.db.connect()?; @@ -117,56 +146,80 @@ impl Index { .order(date_added.desc()) .limit(count) .load(&mut connection)?; + let virtual_directories = real_directories .into_iter() - .filter_map(|d| d.virtualize(&vfs)); - Ok(virtual_directories.collect::>()) + .filter_map(|d| d.virtualize(&vfs)) + .map(|d| d.fetch_artists(&mut connection)); + + Ok(virtual_directories.collect::>()?) } - pub fn search(&self, query: &str) -> Result, QueryError> { + pub fn search(&self, query: &str) -> Result, QueryError> { let vfs = self.vfs_manager.get_vfs()?; let mut connection = self.db.connect()?; let like_test = format!("%{}%", query); let mut output = Vec::new(); // Find dirs with matching path and parent not matching - { + let real_directories: Vec = { use self::directories::dsl::*; - let real_directories: Vec = directories + directories .filter(path.like(&like_test)) .filter(parent.not_like(&like_test)) - .load(&mut connection)?; - - let virtual_directories = real_directories - .into_iter() - .filter_map(|d| d.virtualize(&vfs)); - - output.extend(virtual_directories.map(CollectionFile::Directory)); - } + .load(&mut connection)? + }; // Find songs with matching title/album/artist and non-matching parent - { + let real_songs: Vec = { use self::songs::dsl::*; - let real_songs: Vec = songs + + let album_artists = alias!(artists as album_artists); + songs + .select(songs::all_columns()) + .left_join(song_artists::table) + .left_join(artists::table.on(song_artists::artist.eq(artists::id))) + .left_join(song_album_artists::table) + .left_join( + album_artists + .on(song_album_artists::artist.eq(album_artists.field(artists::id))), + ) .filter( path.like(&like_test) .or(title.like(&like_test)) .or(album.like(&like_test)) - .or(artist.like(&like_test)) - .or(album_artist.like(&like_test)), + .or(artists::name.like(&like_test)) + .or(album_artists.field(artists::name).like(&like_test)), ) .filter(parent.not_like(&like_test)) - .load(&mut connection)?; + .load(&mut connection)? + }; - let virtual_songs = real_songs.into_iter().filter_map(|d| d.virtualize(&vfs)); + // Preallocate capacity + output.reserve(real_directories.len() + real_songs.len()); - output.extend(virtual_songs.map(CollectionFile::Song)); + let virtual_directories = real_directories + .into_iter() + .filter_map(|d| d.virtualize(&vfs)) + .map(|d| d.fetch_artists(&mut connection)) + .map(|d| d.map(dto::CollectionFile::Directory)); + for d in virtual_directories { + output.push(d?); + } + + let virtual_songs = real_songs + .into_iter() + .filter_map(|d| d.virtualize(&vfs)) + .map(|s| s.fetch_artists(&mut connection)) + .map(|s| s.map(dto::CollectionFile::Song)); + for s in virtual_songs { + output.push(s?); } Ok(output) } - pub fn get_song(&self, virtual_path: &Path) -> Result { + pub fn get_song(&self, virtual_path: &Path) -> Result { let vfs = self.vfs_manager.get_vfs()?; let mut connection = self.db.connect()?; @@ -178,9 +231,11 @@ impl Index { .filter(path.eq(real_path_string)) .get_result(&mut connection)?; - match real_song.virtualize(&vfs) { - Some(s) => Ok(s), - None => Err(QueryError::SongNotFound(real_path)), - } + let virtual_song = match real_song.virtualize(&vfs) { + Some(s) => s, + None => return Err(QueryError::SongNotFound(real_path)), + }; + + Ok(virtual_song.fetch_artists(&mut connection)?) } } diff --git a/src/app/index/test.rs b/src/app/index/test.rs index 7ae7e875..884c46c7 100644 --- a/src/app/index/test.rs +++ b/src/app/index/test.rs @@ -4,7 +4,8 @@ use std::path::{Path, PathBuf}; use super::*; use crate::app::test; -use crate::db::{directories, songs}; +use crate::db::{artists, directories, songs}; +use crate::service::dto; use crate::test_name; const TEST_MOUNT_NAME: &str = "root"; @@ -50,8 +51,13 @@ fn update_removes_missing_content() { let mut connection = ctx.db.connect().unwrap(); let all_directories: Vec = directories::table.load(&mut connection).unwrap(); let all_songs: Vec = songs::table.load(&mut connection).unwrap(); + let all_artists: Vec = artists::table + .select(artists::name) + .get_results(&mut connection) + .unwrap(); assert_eq!(all_directories.len(), 6); assert_eq!(all_songs.len(), 13); + assert_eq!(all_artists.len(), 2); } let khemmis_directory = test_collection_dir.join("Khemmis"); @@ -61,8 +67,13 @@ fn update_removes_missing_content() { let mut connection = ctx.db.connect().unwrap(); let all_directories: Vec = directories::table.load(&mut connection).unwrap(); let all_songs: Vec = songs::table.load(&mut connection).unwrap(); + let all_artists: Vec = artists::table + .select(artists::name) + .get_results(&mut connection) + .unwrap(); assert_eq!(all_directories.len(), 4); assert_eq!(all_songs.len(), 8); + assert_eq!(all_artists.len(), 1); } } @@ -77,7 +88,7 @@ fn can_browse_top_level() { let files = ctx.index.browse(Path::new("")).unwrap(); assert_eq!(files.len(), 1); match files[0] { - CollectionFile::Directory(ref d) => assert_eq!(d.path, root_path.to_str().unwrap()), + dto::CollectionFile::Directory(ref d) => assert_eq!(d.path, root_path.to_str().unwrap()), _ => panic!("Expected directory"), } } @@ -96,12 +107,14 @@ fn can_browse_directory() { assert_eq!(files.len(), 2); match files[0] { - CollectionFile::Directory(ref d) => assert_eq!(d.path, khemmis_path.to_str().unwrap()), + dto::CollectionFile::Directory(ref d) => assert_eq!(d.path, khemmis_path.to_str().unwrap()), _ => panic!("Expected directory"), } match files[1] { - CollectionFile::Directory(ref d) => assert_eq!(d.path, tobokegao_path.to_str().unwrap()), + dto::CollectionFile::Directory(ref d) => { + assert_eq!(d.path, tobokegao_path.to_str().unwrap()) + } _ => panic!("Expected directory"), } } @@ -177,8 +190,8 @@ fn can_get_a_song() { assert_eq!(song.track_number, Some(5)); assert_eq!(song.disc_number, None); assert_eq!(song.title, Some("シャーベット (Sherbet)".to_owned())); - assert_eq!(song.artist, Some("Tobokegao".to_owned())); - assert_eq!(song.album_artist, None); + assert_eq!(song.artists, vec!("Tobokegao".to_owned())); + assert_eq!(song.album_artists, Vec::::new()); assert_eq!(song.album, Some("Picnic".to_owned())); assert_eq!(song.year, Some(2016)); assert_eq!( diff --git a/src/app/index/types.rs b/src/app/index/types.rs index 427cf90e..48ea700f 100644 --- a/src/app/index/types.rs +++ b/src/app/index/types.rs @@ -1,30 +1,22 @@ -use serde::{Deserialize, Serialize}; use std::path::Path; -use crate::app::vfs::VFS; -use crate::db::songs; +use diesel::prelude::*; +use diesel::r2d2::{ConnectionManager, PooledConnection}; -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum CollectionFile { - Directory(Directory), - Song(Song), -} +use crate::app::vfs::VFS; +use crate::db::{artists, directory_artists, song_album_artists, song_artists, songs}; +use crate::service::dto; -#[derive(Debug, PartialEq, Eq, Queryable, QueryableByName, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Queryable, QueryableByName)] #[diesel(table_name = songs)] pub struct Song { - #[serde(skip_serializing, skip_deserializing)] - id: i32, + pub id: i32, pub path: String, - #[serde(skip_serializing, skip_deserializing)] pub parent: String, pub track_number: Option, pub disc_number: Option, pub title: Option, - pub artist: Option, - pub album_artist: Option, pub year: Option, - pub album: Option, pub artwork: Option, pub duration: Option, pub lyricist: Option, @@ -47,16 +39,30 @@ impl Song { } Some(self) } + + pub fn fetch_artists( + self, + connection: &mut PooledConnection>, + ) -> Result { + let artists: Vec = song_artists::table + .filter(song_artists::song.eq(self.id)) + .inner_join(artists::table) + .select(artists::name) + .load(connection)?; + let album_artists: Vec = song_album_artists::table + .filter(song_album_artists::song.eq(self.id)) + .inner_join(artists::table) + .select(artists::name) + .load(connection)?; + Ok(dto::Song::new(self, artists, album_artists)) + } } -#[derive(Debug, PartialEq, Eq, Queryable, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Queryable)] pub struct Directory { - #[serde(skip_serializing, skip_deserializing)] - id: i32, + pub id: i32, pub path: String, - #[serde(skip_serializing, skip_deserializing)] pub parent: Option, - pub artist: Option, pub year: Option, pub album: Option, pub artwork: Option, @@ -77,4 +83,16 @@ impl Directory { } Some(self) } + + pub fn fetch_artists( + self, + connection: &mut PooledConnection>, + ) -> Result { + let artists: Vec = directory_artists::table + .filter(directory_artists::directory.eq(self.id)) + .inner_join(artists::table) + .select(artists::name) + .load(connection)?; + Ok(dto::Directory::new(self, artists)) + } } diff --git a/src/app/index/update/cleaner.rs b/src/app/index/update/cleaner.rs index bc3bf69d..97355ab5 100644 --- a/src/app/index/update/cleaner.rs +++ b/src/app/index/update/cleaner.rs @@ -3,7 +3,9 @@ use rayon::prelude::*; use std::path::Path; use crate::app::vfs; -use crate::db::{self, directories, songs, DB}; +use crate::db::{ + self, artists, directories, directory_artists, song_album_artists, song_artists, songs, DB, +}; const INDEX_BUILDING_CLEAN_BUFFER_SIZE: usize = 500; // Deletions in each transaction @@ -24,6 +26,12 @@ pub struct Cleaner { vfs_manager: vfs::Manager, } +#[derive(Identifiable, Queryable, Selectable)] +#[diesel(table_name = artists)] +struct Artist { + id: i32, +} + impl Cleaner { pub fn new(db: DB, vfs_manager: vfs::Manager) -> Self { Self { db, vfs_manager } @@ -80,6 +88,19 @@ impl Cleaner { } } + { + use crate::db::artists::dsl::*; + + let mut connection = self.db.connect()?; + diesel::delete( + artists + .filter(id.ne_all(song_artists::table.select(song_artists::artist))) + .filter(id.ne_all(song_album_artists::table.select(song_album_artists::artist))) + .filter(id.ne_all(directory_artists::table.select(directory_artists::artist))), + ) + .execute(&mut connection)?; + } + Ok(()) } } diff --git a/src/app/index/update/collector.rs b/src/app/index/update/collector.rs index c7900864..3452605b 100644 --- a/src/app/index/update/collector.rs +++ b/src/app/index/update/collector.rs @@ -32,10 +32,10 @@ impl Collector { fn collect_directory(&self, directory: traverser::Directory) { let mut directory_album = None; let mut directory_year = None; - let mut directory_artist = None; + let mut directory_artists = Vec::new(); let mut inconsistent_directory_album = false; let mut inconsistent_directory_year = false; - let mut inconsistent_directory_artist = false; + let mut inconsistent_directory_artists = false; let directory_artwork = self.get_artwork(&directory); let directory_path_string = directory.path.to_string_lossy().to_string(); @@ -57,14 +57,14 @@ impl Collector { directory_album = tags.album.as_ref().cloned(); } - if tags.album_artist.is_some() { - inconsistent_directory_artist |= - directory_artist.is_some() && directory_artist != tags.album_artist; - directory_artist = tags.album_artist.as_ref().cloned(); - } else if tags.artist.is_some() { - inconsistent_directory_artist |= - directory_artist.is_some() && directory_artist != tags.artist; - directory_artist = tags.artist.as_ref().cloned(); + if tags.album_artists.is_empty() { + inconsistent_directory_artists |= + directory_artists.is_empty() && directory_artists != tags.album_artists; + directory_artists = tags.album_artists.clone(); + } else if tags.artists.is_empty() { + inconsistent_directory_artists |= + directory_artists.is_empty() && directory_artists != tags.artists; + directory_artists = tags.artists.clone(); } let artwork_path = if tags.has_artwork { @@ -73,22 +73,11 @@ impl Collector { directory_artwork.as_ref().cloned() }; - if let Err(e) = self.sender.send(inserter::Item::Song(inserter::Song { + if let Err(e) = self.sender.send(inserter::Item::Song(inserter::InsertSong { path: path_string, parent: directory_path_string.clone(), - disc_number: tags.disc_number.map(|n| n as i32), - track_number: tags.track_number.map(|n| n as i32), - title: tags.title, - duration: tags.duration.map(|n| n as i32), - artist: tags.artist, - album_artist: tags.album_artist, - album: tags.album, - year: tags.year, artwork: artwork_path, - lyricist: tags.lyricist, - composer: tags.composer, - genre: tags.genre, - label: tags.label, + tags, })) { error!("Error while sending song from collector: {}", e); } @@ -100,18 +89,18 @@ impl Collector { if inconsistent_directory_album { directory_album = None; } - if inconsistent_directory_artist { - directory_artist = None; + if inconsistent_directory_artists { + directory_artists = Vec::new(); } if let Err(e) = self .sender - .send(inserter::Item::Directory(inserter::Directory { + .send(inserter::Item::Directory(inserter::InsertDirectory { path: directory_path_string, parent: directory_parent_string, artwork: directory_artwork, album: directory_album, - artist: directory_artist, + artists: directory_artists, year: directory_year, date_added: directory.created, })) { diff --git a/src/app/index/update/inserter.rs b/src/app/index/update/inserter.rs index 4c73e441..f234d569 100644 --- a/src/app/index/update/inserter.rs +++ b/src/app/index/update/inserter.rs @@ -2,11 +2,34 @@ use crossbeam_channel::Receiver; use diesel::prelude::*; use log::error; -use crate::db::{directories, songs, DB}; +use crate::app::index::metadata::SongTags; +use crate::app::index::QueryError; +use crate::db::{ + albums, artists, directories, directory_artists, song_album_artists, song_artists, songs, DB, +}; const INDEX_BUILDING_INSERT_BUFFER_SIZE: usize = 1000; // Insertions in each transaction -#[derive(Debug, Insertable)] +#[derive(Debug, Insertable, AsChangeset)] +#[diesel(table_name = artists)] +pub struct Artist { + pub name: String, +} + +#[derive(Debug, Insertable, AsChangeset)] +#[diesel(table_name = albums)] +pub struct Album { + pub name: String, +} + +pub struct InsertSong { + pub path: String, + pub parent: String, + pub artwork: Option, + pub tags: SongTags, +} + +#[derive(Debug, Insertable, AsChangeset)] #[diesel(table_name = songs)] pub struct Song { pub path: String, @@ -14,10 +37,8 @@ pub struct Song { pub track_number: Option, pub disc_number: Option, pub title: Option, - pub artist: Option, - pub album_artist: Option, pub year: Option, - pub album: Option, + pub album: Option, pub artwork: Option, pub duration: Option, pub lyricist: Option, @@ -27,26 +48,56 @@ pub struct Song { } #[derive(Debug, Insertable)] +#[diesel(table_name = song_artists)] +pub struct SongArtist { + song: i32, + artist: i32, +} + +#[derive(Debug, Insertable)] +#[diesel(table_name = song_album_artists)] +pub struct SongAlbumArtist { + song: i32, + artist: i32, +} + +pub struct InsertDirectory { + pub path: String, + pub parent: Option, + pub artists: Vec, + pub year: Option, + pub album: Option, + pub artwork: Option, + pub date_added: i32, +} + +#[derive(Debug, Insertable, AsChangeset)] #[diesel(table_name = directories)] pub struct Directory { pub path: String, pub parent: Option, - pub artist: Option, pub year: Option, pub album: Option, pub artwork: Option, pub date_added: i32, } +#[derive(Debug, Insertable)] +#[diesel(table_name = directory_artists)] +pub struct DirectoryArtist { + directory: i32, + artist: i32, +} + pub enum Item { - Directory(Directory), - Song(Song), + Directory(InsertDirectory), + Song(InsertSong), } pub struct Inserter { receiver: Receiver, - new_directories: Vec, - new_songs: Vec, + new_directories: Vec, + new_songs: Vec, db: DB, } @@ -86,29 +137,150 @@ impl Inserter { } fn flush_directories(&mut self) { - let res = self.db.connect().ok().and_then(|mut connection| { - diesel::insert_into(directories::table) - .values(&self.new_directories) - .execute(&mut *connection) // TODO https://github.com/diesel-rs/diesel/issues/1822 - .ok() - }); - if res.is_none() { - error!("Could not insert new directories in database"); + let res = self + .db + .connect() + .map_err(QueryError::from) + .and_then(|mut connection| { + connection.transaction(|connection| { + for d in self.new_directories.drain(..) { + let dir = Directory { + path: d.path, + parent: d.parent, + artwork: d.artwork, + album: d.album, + year: d.year, + date_added: d.date_added, + }; + let dir_id: i32 = diesel::insert_into(directories::table) + .values(&dir) + .on_conflict(directories::path) + .do_update() + .set(&dir) + .returning(directories::id) + .get_result(connection)?; + + for a in d.artists { + let artist = Artist { name: a }; + let artist_id: i32 = diesel::insert_into(artists::table) + .values(&artist) + .on_conflict(artists::name) + .do_update() + .set(&artist) + .returning(artists::id) + .get_result(connection)?; + + let dir_artist = DirectoryArtist { + directory: dir_id, + artist: artist_id, + }; + diesel::insert_into(directory_artists::table) + .values(dir_artist) + .execute(connection)?; + } + } + + Ok(()) + }) + }); + + if let Err(e) = res { + error!("Could not insert new directories in database: {e}"); } - self.new_directories.clear(); } fn flush_songs(&mut self) { - let res = self.db.connect().ok().and_then(|mut connection| { - diesel::insert_into(songs::table) - .values(&self.new_songs) - .execute(&mut *connection) // TODO https://github.com/diesel-rs/diesel/issues/1822 - .ok() - }); - if res.is_none() { - error!("Could not insert new songs in database"); + let res = self + .db + .connect() + .map_err(QueryError::from) + .and_then(|mut connection| { + connection.transaction(|connection| { + for s in self.new_songs.drain(..) { + let album_id = match s.tags.album { + Some(album) => { + let album = Album { name: album }; + let id: i32 = diesel::insert_into(albums::table) + .values(&album) + .on_conflict(albums::name) + .do_update() + .set(&album) + .returning(albums::id) + .get_result(connection)?; + Some(id) + } + None => None, + }; + + let song = Song { + path: s.path, + parent: s.parent, + disc_number: s.tags.disc_number.map(|n| n as i32), + track_number: s.tags.track_number.map(|n| n as i32), + title: s.tags.title, + duration: s.tags.duration.map(|n| n as i32), + album: album_id, + year: s.tags.year, + artwork: s.artwork, + lyricist: s.tags.lyricist, + composer: s.tags.composer, + genre: s.tags.genre, + label: s.tags.label, + }; + let song_id: i32 = diesel::insert_into(songs::table) + .values(&song) + .on_conflict(songs::path) + .do_update() + .set(&song) + .returning(songs::id) + .get_result(connection)?; + + for a in s.tags.artists { + let artist = Artist { name: a }; + let artist_id: i32 = diesel::insert_into(artists::table) + .values(&artist) + .on_conflict(artists::name) + .do_update() + .set(&artist) + .returning(artists::id) + .get_result(connection)?; + + let song_artist = SongArtist { + song: song_id, + artist: artist_id, + }; + diesel::insert_into(song_artists::table) + .values(song_artist) + .execute(connection)?; + } + + for a in s.tags.album_artists { + let artist = Artist { name: a }; + let artist_id: i32 = diesel::insert_into(artists::table) + .values(&artist) + .on_conflict(artists::name) + .do_update() + .set(&artist) + .returning(artists::id) + .get_result(connection)?; + + let song_album_artist = SongAlbumArtist { + song: song_id, + artist: artist_id, + }; + diesel::insert_into(song_album_artists::table) + .values(song_album_artist) + .execute(connection)?; + } + } + + Ok(()) + }) + }); + + if let Err(e) = res { + error!("Could not insert new songs in database: {e}"); } - self.new_songs.clear(); } } diff --git a/src/app/lastfm.rs b/src/app/lastfm.rs index 74f060f4..f8290f1a 100644 --- a/src/app/lastfm.rs +++ b/src/app/lastfm.rs @@ -84,7 +84,7 @@ impl Manager { fn scrobble_from_path(&self, track: &Path) -> Result { let song = self.index.get_song(track)?; Ok(Scrobble::new( - song.artist.as_deref().unwrap_or(""), + song.artists.first().map_or("", |s| s.as_str()), song.title.as_deref().unwrap_or(""), song.album.as_deref().unwrap_or(""), )) diff --git a/src/app/playlist.rs b/src/app/playlist.rs index 80e0fbaf..c5454420 100644 --- a/src/app/playlist.rs +++ b/src/app/playlist.rs @@ -1,12 +1,12 @@ use core::clone::Clone; use diesel::prelude::*; -use diesel::sql_types; use diesel::BelongingToDsl; use std::path::Path; use crate::app::index::Song; use crate::app::vfs; -use crate::db::{self, playlist_songs, playlists, users, DB}; +use crate::db::{self, playlist_songs, playlists, songs, users, DB}; +use crate::service::dto; #[derive(thiserror::Error, Debug)] pub enum Error { @@ -134,56 +134,47 @@ impl Manager { Ok(()) } - pub fn read_playlist(&self, playlist_name: &str, owner: &str) -> Result, Error> { + pub fn read_playlist(&self, playlist_name: &str, owner: &str) -> Result, Error> { let vfs = self.vfs_manager.get_vfs()?; - let songs: Vec; - { - let mut connection = self.db.connect()?; + let mut connection = self.db.connect()?; - // Find owner - let user: User = { - use self::users::dsl::*; - users - .filter(name.eq(owner)) - .select((id,)) - .first(&mut connection) - .optional()? - .ok_or(Error::UserNotFound)? - }; + // Find owner + let user: User = { + use self::users::dsl::*; + users + .filter(name.eq(owner)) + .select((id,)) + .first(&mut connection) + .optional()? + .ok_or(Error::UserNotFound)? + }; - // Find playlist - let playlist: Playlist = { - use self::playlists::dsl::*; - playlists - .select((id, owner)) - .filter(name.eq(playlist_name).and(owner.eq(user.id))) - .get_result(&mut connection) - .optional()? - .ok_or(Error::PlaylistNotFound)? - }; + // Find playlist + let playlist: Playlist = { + use self::playlists::dsl::*; + playlists + .select((id, owner)) + .filter(name.eq(playlist_name).and(owner.eq(user.id))) + .get_result(&mut connection) + .optional()? + .ok_or(Error::PlaylistNotFound)? + }; - // Select songs. Not using Diesel because we need to LEFT JOIN using a custom column - let query = diesel::sql_query( - r#" - SELECT s.id, s.path, s.parent, s.track_number, s.disc_number, s.title, s.artist, s.album_artist, s.year, s.album, s.artwork, s.duration, s.lyricist, s.composer, s.genre, s.label - FROM playlist_songs ps - LEFT JOIN songs s ON ps.path = s.path - WHERE ps.playlist = ? - ORDER BY ps.ordering - "#, - ); - let query = query.bind::(playlist.id); - songs = query.get_results(&mut connection)?; - } + let songs: Vec = { + playlist_songs::table + .filter(playlist_songs::playlist.eq(playlist.id)) + .inner_join(songs::table.on(playlist_songs::path.eq(songs::path))) + .select(songs::all_columns) + .get_results(&mut connection)? + }; // Map real path to virtual paths - let virtual_songs = songs + Ok(songs .into_iter() .filter_map(|s| s.virtualize(&vfs)) - .collect(); - - Ok(virtual_songs) + .map(|s| s.fetch_artists(&mut connection)) + .collect::>()?) } pub fn delete_playlist(&self, playlist_name: &str, owner: &str) -> Result<(), Error> { diff --git a/src/db/schema.rs b/src/db/schema.rs index dd1b512e..23b9513d 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -12,7 +12,6 @@ table! { id -> Integer, path -> Text, parent -> Nullable, - artist -> Nullable, year -> Nullable, album -> Nullable, artwork -> Nullable, @@ -20,6 +19,13 @@ table! { } } +table! { + directory_artists(directory, artist) { + directory -> Integer, + artist -> Integer, + } +} + table! { misc_settings (id) { id -> Integer, @@ -62,8 +68,6 @@ table! { track_number -> Nullable, disc_number -> Nullable, title -> Nullable, - artist -> Nullable, - album_artist -> Nullable, year -> Nullable, album -> Nullable, artwork -> Nullable, @@ -75,6 +79,48 @@ table! { } } +table! { + song_artists(song, artist) { + song -> Integer, + artist -> Integer, + } +} + +table! { + song_album_artists(song, artist) { + song -> Integer, + artist -> Integer, + } +} + +table! { + artists(id) { + id -> Integer, + name -> Text, + } +} + +table! { + artist_albums(artist, album) { + artist -> Integer, + album -> Integer, + } +} + +table! { + album_songs(album, song) { + album -> Integer, + song -> Integer, + } +} + +table! { + albums(id) { + id -> Integer, + name -> Text, + } +} + table! { users (id) { id -> Integer, @@ -88,16 +134,25 @@ table! { } } +joinable!(song_artists -> songs (song)); +joinable!(song_artists -> artists (artist)); +joinable!(song_album_artists -> songs (song)); +joinable!(song_album_artists -> artists (artist)); +joinable!(directory_artists -> artists (artist)); joinable!(playlist_songs -> playlists (playlist)); joinable!(playlists -> users (owner)); allow_tables_to_appear_in_same_query!( + artists, ddns_config, directories, + directory_artists, misc_settings, mount_points, playlist_songs, playlists, songs, + song_artists, + song_album_artists, users, ); diff --git a/src/service.rs b/src/service.rs index a351576e..2885886a 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,4 +1,4 @@ -mod dto; +pub mod dto; mod error; #[cfg(test)] diff --git a/src/service/actix/api.rs b/src/service/actix/api.rs index d3600ee6..55db2305 100644 --- a/src/service/actix/api.rs +++ b/src/service/actix/api.rs @@ -22,7 +22,7 @@ use std::str; use crate::app::{ config, ddns, - index::{self, Index}, + index::Index, lastfm, playlist, settings, thumbnail, user, vfs::{self, MountDir}, }; @@ -449,7 +449,7 @@ async fn login( async fn browse_root( index: Data, _auth: Auth, -) -> Result>, APIError> { +) -> Result>, APIError> { let result = block(move || index.browse(Path::new(""))).await?; Ok(Json(result)) } @@ -459,7 +459,7 @@ async fn browse( index: Data, _auth: Auth, path: web::Path, -) -> Result>, APIError> { +) -> Result>, APIError> { let result = block(move || { let path = percent_decode_str(&path).decode_utf8_lossy(); index.browse(Path::new(path.as_ref())) @@ -469,7 +469,7 @@ async fn browse( } #[get("/flatten")] -async fn flatten_root(index: Data, _auth: Auth) -> Result>, APIError> { +async fn flatten_root(index: Data, _auth: Auth) -> Result>, APIError> { let songs = block(move || index.flatten(Path::new(""))).await?; Ok(Json(songs)) } @@ -479,7 +479,7 @@ async fn flatten( index: Data, _auth: Auth, path: web::Path, -) -> Result>, APIError> { +) -> Result>, APIError> { let songs = block(move || { let path = percent_decode_str(&path).decode_utf8_lossy(); index.flatten(Path::new(path.as_ref())) @@ -489,13 +489,13 @@ async fn flatten( } #[get("/random")] -async fn random(index: Data, _auth: Auth) -> Result>, APIError> { +async fn random(index: Data, _auth: Auth) -> Result>, APIError> { let result = block(move || index.get_random_albums(20)).await?; Ok(Json(result)) } #[get("/recent")] -async fn recent(index: Data, _auth: Auth) -> Result>, APIError> { +async fn recent(index: Data, _auth: Auth) -> Result>, APIError> { let result = block(move || index.get_recent_albums(20)).await?; Ok(Json(result)) } @@ -504,7 +504,7 @@ async fn recent(index: Data, _auth: Auth) -> Result, _auth: Auth, -) -> Result>, APIError> { +) -> Result>, APIError> { let result = block(move || index.search("")).await?; Ok(Json(result)) } @@ -514,7 +514,7 @@ async fn search( index: Data, _auth: Auth, query: web::Path, -) -> Result>, APIError> { +) -> Result>, APIError> { let result = block(move || index.search(&query)).await?; Ok(Json(result)) } @@ -591,7 +591,7 @@ async fn read_playlist( playlist_manager: Data, auth: Auth, name: web::Path, -) -> Result>, APIError> { +) -> Result>, APIError> { let songs = block(move || playlist_manager.read_playlist(&name, &auth.username)).await?; Ok(Json(songs)) } diff --git a/src/service/dto.rs b/src/service/dto.rs index 8bd79a35..52e46d5c 100644 --- a/src/service/dto.rs +++ b/src/service/dto.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::app::{config, ddns, settings, thumbnail, user, vfs}; +use crate::app::{config, ddns, index, settings, thumbnail, user, vfs}; use std::convert::From; pub const API_MAJOR_VERSION: i32 = 7; @@ -231,5 +231,73 @@ impl From for Settings { } } -// TODO: Preferences, CollectionFile, Song and Directory should have dto types +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum CollectionFile { + Song(Song), + Directory(Directory), +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Song { + pub path: String, + pub track_number: Option, + pub disc_number: Option, + pub title: Option, + pub artists: Vec, + pub album_artists: Vec, + pub year: Option, + pub album: Option, + pub artwork: Option, + pub duration: Option, + pub lyricist: Option, + pub composer: Option, + pub genre: Option, + pub label: Option, +} + +impl Song { + pub fn new(song: index::Song, artists: Vec, album_artists: Vec) -> Self { + Self { + path: song.path, + track_number: song.track_number, + disc_number: song.disc_number, + title: song.title, + artists, + album_artists, + year: song.year, + album: song.album, + artwork: song.artwork, + duration: song.duration, + lyricist: song.lyricist, + composer: song.composer, + genre: song.genre, + label: song.label, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct Directory { + pub path: String, + pub artists: Vec, + pub year: Option, + pub album: Option, + pub artwork: Option, + pub date_added: i32, +} + +impl Directory { + pub fn new(dir: index::Directory, artists: Vec) -> Self { + Self { + path: dir.path, + artists, + year: dir.year, + album: dir.album, + artwork: dir.artwork, + date_added: dir.date_added, + } + } +} + +// TODO: Preferences should have a dto type // TODO Song dto type should skip `None` values when serializing, to lower payload sizes by a lot diff --git a/src/service/test.rs b/src/service/test.rs index 05443061..1cbff9fe 100644 --- a/src/service/test.rs +++ b/src/service/test.rs @@ -19,7 +19,6 @@ mod swagger; mod user; mod web; -use crate::app::index; use crate::service::dto; use crate::service::test::constants::*; @@ -91,7 +90,7 @@ pub trait TestService { loop { let browse_request = protocol::browse(Path::new("")); - let response = self.fetch_json::<(), Vec>(&browse_request); + let response = self.fetch_json::<(), Vec>(&browse_request); let entries = response.body(); if !entries.is_empty() { break; @@ -101,7 +100,7 @@ pub trait TestService { loop { let flatten_request = protocol::flatten(Path::new("")); - let response = self.fetch_json::<_, Vec>(&flatten_request); + let response = self.fetch_json::<_, Vec>(&flatten_request); let entries = response.body(); if !entries.is_empty() { break; diff --git a/src/service/test/admin.rs b/src/service/test/admin.rs index f5e679fd..32b4a5a4 100644 --- a/src/service/test/admin.rs +++ b/src/service/test/admin.rs @@ -1,6 +1,5 @@ use http::StatusCode; -use crate::app::index; use crate::service::dto; use crate::service::test::{protocol, ServiceType, TestService}; use crate::test_name; @@ -50,13 +49,13 @@ fn trigger_index_golden_path() { let request = protocol::random(); - let response = service.fetch_json::<_, Vec>(&request); + let response = service.fetch_json::<_, Vec>(&request); let entries = response.body(); assert_eq!(entries.len(), 0); service.index(); - let response = service.fetch_json::<_, Vec>(&request); + let response = service.fetch_json::<_, Vec>(&request); let entries = response.body(); assert_eq!(entries.len(), 3); } diff --git a/src/service/test/collection.rs b/src/service/test/collection.rs index a95aa75b..cf74dd86 100644 --- a/src/service/test/collection.rs +++ b/src/service/test/collection.rs @@ -1,7 +1,7 @@ use http::StatusCode; use std::path::{Path, PathBuf}; -use crate::app::index; +use crate::service::dto; use crate::service::test::{add_trailing_slash, constants::*, protocol, ServiceType, TestService}; use crate::test_name; @@ -22,7 +22,7 @@ fn browse_root() { service.login(); let request = protocol::browse(&PathBuf::new()); - let response = service.fetch_json::<_, Vec>(&request); + let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); assert_eq!(entries.len(), 1); @@ -38,7 +38,7 @@ fn browse_directory() { let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect(); let request = protocol::browse(&path); - let response = service.fetch_json::<_, Vec>(&request); + let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); assert_eq!(entries.len(), 5); @@ -73,7 +73,7 @@ fn flatten_root() { service.login(); let request = protocol::flatten(&PathBuf::new()); - let response = service.fetch_json::<_, Vec>(&request); + let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); assert_eq!(entries.len(), 13); @@ -88,7 +88,7 @@ fn flatten_directory() { service.login(); let request = protocol::flatten(Path::new(TEST_MOUNT_NAME)); - let response = service.fetch_json::<_, Vec>(&request); + let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); assert_eq!(entries.len(), 13); @@ -123,7 +123,7 @@ fn random_golden_path() { service.login(); let request = protocol::random(); - let response = service.fetch_json::<_, Vec>(&request); + let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); assert_eq!(entries.len(), 3); @@ -139,7 +139,7 @@ fn random_with_trailing_slash() { let mut request = protocol::random(); add_trailing_slash(&mut request); - let response = service.fetch_json::<_, Vec>(&request); + let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); assert_eq!(entries.len(), 3); @@ -162,7 +162,7 @@ fn recent_golden_path() { service.login(); let request = protocol::recent(); - let response = service.fetch_json::<_, Vec>(&request); + let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); assert_eq!(entries.len(), 3); @@ -178,7 +178,7 @@ fn recent_with_trailing_slash() { let mut request = protocol::recent(); add_trailing_slash(&mut request); - let response = service.fetch_json::<_, Vec>(&request); + let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); let entries = response.body(); assert_eq!(entries.len(), 3); @@ -199,7 +199,7 @@ fn search_without_query() { service.login(); let request = protocol::search(""); - let response = service.fetch_json::<_, Vec>(&request); + let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); } @@ -212,11 +212,11 @@ fn search_with_query() { service.login(); let request = protocol::search("door"); - let response = service.fetch_json::<_, Vec>(&request); + let response = service.fetch_json::<_, Vec>(&request); let results = response.body(); assert_eq!(results.len(), 1); match results[0] { - index::CollectionFile::Song(ref s) => { + dto::CollectionFile::Song(ref s) => { assert_eq!(s.title, Some("Beyond The Door".into())) } _ => panic!(), diff --git a/src/service/test/playlist.rs b/src/service/test/playlist.rs index 3677800f..610b23e5 100644 --- a/src/service/test/playlist.rs +++ b/src/service/test/playlist.rs @@ -1,6 +1,5 @@ use http::StatusCode; -use crate::app::index; use crate::service::dto; use crate::service::test::{constants::*, protocol, ServiceType, TestService}; use crate::test_name; @@ -81,7 +80,7 @@ fn get_playlist_golden_path() { } let request = protocol::read_playlist(TEST_PLAYLIST_NAME); - let response = service.fetch_json::<_, Vec>(&request); + let response = service.fetch_json::<_, Vec>(&request); assert_eq!(response.status(), StatusCode::OK); }