Adds multi-value fields (single row)

This commit is contained in:
Antoine Gersant 2024-07-15 01:29:09 -07:00
parent 5a785a2e16
commit 9d8d543494
15 changed files with 391 additions and 327 deletions

View file

@ -1,7 +1,6 @@
use id3::TagLike; use id3::TagLike;
use lewton::inside_ogg::OggStreamReader; use lewton::inside_ogg::OggStreamReader;
use log::error; use log::error;
use regex::Regex;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -28,69 +27,31 @@ pub enum Error {
VorbisCommentNotFoundInFlacFile, VorbisCommentNotFoundInFlacFile,
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct SongTags { pub struct SongMetadata {
pub disc_number: Option<u32>, pub disc_number: Option<u32>,
pub track_number: Option<u32>, pub track_number: Option<u32>,
pub title: Option<String>, pub title: Option<String>,
pub duration: Option<u32>, pub duration: Option<u32>,
pub artist: Option<String>, pub artists: Vec<String>,
pub album_artist: Option<String>, pub album_artists: Vec<String>,
pub album: Option<String>, pub album: Option<String>,
pub year: Option<i32>, pub year: Option<i32>,
pub has_artwork: bool, pub has_artwork: bool,
pub lyricist: Option<String>, pub lyricists: Vec<String>,
pub composer: Option<String>, pub composers: Vec<String>,
pub genre: Option<String>, pub genres: Vec<String>,
pub label: Option<String>, pub labels: Vec<String>,
} }
impl From<id3::Tag> for SongTags { pub fn read(path: &Path) -> Option<SongMetadata> {
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,
}
}
}
pub fn read(path: &Path) -> Option<SongTags> {
let data = match utils::get_audio_format(path) { let data = match utils::get_audio_format(path) {
Some(AudioFormat::AIFF) => read_aiff(path), Some(AudioFormat::AIFF) => read_id3(path),
Some(AudioFormat::FLAC) => read_flac(path), Some(AudioFormat::FLAC) => read_flac(path),
Some(AudioFormat::MP3) => read_mp3(path), Some(AudioFormat::MP3) => read_mp3(path),
Some(AudioFormat::OGG) => read_vorbis(path), Some(AudioFormat::OGG) => read_vorbis(path),
Some(AudioFormat::OPUS) => read_opus(path), Some(AudioFormat::OPUS) => read_opus(path),
Some(AudioFormat::WAVE) => read_wave(path), Some(AudioFormat::WAVE) => read_id3(path),
Some(AudioFormat::APE) | Some(AudioFormat::MPC) => read_ape(path), Some(AudioFormat::APE) | Some(AudioFormat::MPC) => read_ape(path),
Some(AudioFormat::MP4) | Some(AudioFormat::M4B) => read_mp4(path), Some(AudioFormat::MP4) | Some(AudioFormat::M4B) => read_mp4(path),
None => return None, None => return None,
@ -104,23 +65,20 @@ pub fn read(path: &Path) -> Option<SongTags> {
} }
} }
trait FrameContent { trait ID3Ext {
/// Returns the value stored, if any, in the Frame. fn get_text_values(&self, frame_name: &str) -> Vec<String>;
/// Say "TCOM" returns composer field.
fn get_text(&self, key: &str) -> Option<String>;
} }
impl FrameContent for id3::Tag { impl ID3Ext for id3::Tag {
fn get_text(&self, key: &str) -> Option<String> { fn get_text_values(&self, frame_name: &str) -> Vec<String> {
let frame = self.get(key)?; self.get(frame_name)
match frame.content() { .and_then(|f| f.content().text_values())
id3::Content::Text(value) => Some(value.to_string()), .map(|i| i.map(str::to_string).collect())
_ => None, .unwrap_or_default()
}
} }
} }
fn read_mp3(path: &Path) -> Result<SongTags, Error> { fn read_id3(path: &Path) -> Result<SongMetadata, Error> {
let tag = id3::Tag::read_from_path(path).or_else(|error| { let tag = id3::Tag::read_from_path(path).or_else(|error| {
if let Some(tag) = error.partial_tag { if let Some(tag) = error.partial_tag {
Ok(tag) Ok(tag)
@ -129,83 +87,102 @@ fn read_mp3(path: &Path) -> Result<SongTags, Error> {
} }
})?; })?;
let artists = tag.get_text_values("TPE1");
let album_artists = tag.get_text_values("TPE2");
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 lyricists = tag.get_text_values("TEXT");
let composers = tag.get_text_values("TCOM");
let genres = tag.get_text_values("TCON");
let labels = tag.get_text_values("TPUB");
Ok(SongMetadata {
disc_number,
track_number,
title,
duration,
artists,
album_artists,
album,
year,
has_artwork,
lyricists,
composers,
genres,
labels,
})
}
fn read_mp3(path: &Path) -> Result<SongMetadata, Error> {
let mut metadata = read_id3(path)?;
let duration = { let duration = {
mp3_duration::from_path(path) mp3_duration::from_path(path)
.map(|d| d.as_secs() as u32) .map(|d| d.as_secs() as u32)
.ok() .ok()
}; };
metadata.duration = duration;
let mut song_tags: SongTags = tag.into(); Ok(metadata)
song_tags.duration = duration; // Use duration from mp3_duration instead of from tags.
Ok(song_tags)
} }
fn read_aiff(path: &Path) -> Result<SongTags, Error> { mod ape_ext {
let tag = id3::Tag::read_from_path(path).or_else(|error| { pub fn read_string(item: &ape::Item) -> Option<String> {
if let Some(tag) = error.partial_tag { match item.value {
Ok(tag) ape::ItemValue::Text(ref s) => Some(s.clone()),
} else { _ => None,
Err(error)
} }
})?; }
Ok(tag.into())
}
fn read_wave(path: &Path) -> Result<SongTags, Error> { pub fn read_strings(items: Vec<&ape::Item>) -> Vec<String> {
let tag = id3::Tag::read_from_path(path).or_else(|error| { items.iter().filter_map(|i| read_string(i)).collect()
if let Some(tag) = error.partial_tag { }
Ok(tag)
} else { pub fn read_i32(item: &ape::Item) -> Option<i32> {
Err(error) match item.value {
ape::ItemValue::Text(ref s) => s.parse::<i32>().ok(),
_ => None,
} }
})?;
Ok(tag.into())
}
fn read_ape_string(item: &ape::Item) -> Option<String> {
match item.value {
ape::ItemValue::Text(ref s) => Some(s.clone()),
_ => None,
} }
}
fn read_ape_i32(item: &ape::Item) -> Option<i32> { pub fn read_x_of_y(item: &ape::Item) -> Option<u32> {
match item.value { match item.value {
ape::ItemValue::Text(ref s) => s.parse::<i32>().ok(), ape::ItemValue::Text(ref s) => {
_ => None, let format = regex::Regex::new(r#"^\d+"#).unwrap();
} if let Some(m) = format.find(s) {
} s[m.start()..m.end()].parse().ok()
} else {
fn read_ape_x_of_y(item: &ape::Item) -> Option<u32> { None
match item.value { }
ape::ItemValue::Text(ref s) => {
let format = Regex::new(r#"^\d+"#).unwrap();
if let Some(m) = format.find(s) {
s[m.start()..m.end()].parse().ok()
} else {
None
} }
_ => None,
} }
_ => None,
} }
} }
fn read_ape(path: &Path) -> Result<SongTags, Error> { fn read_ape(path: &Path) -> Result<SongMetadata, Error> {
let tag = ape::read_from_path(path)?; let tag = ape::read_from_path(path)?;
let artist = tag.item("Artist").and_then(read_ape_string); let artists = ape_ext::read_strings(tag.items("Artist"));
let album = tag.item("Album").and_then(read_ape_string); let album = tag.item("Album").and_then(ape_ext::read_string);
let album_artist = tag.item("Album artist").and_then(read_ape_string); let album_artists = ape_ext::read_strings(tag.items("Album artist"));
let title = tag.item("Title").and_then(read_ape_string); let title = tag.item("Title").and_then(ape_ext::read_string);
let year = tag.item("Year").and_then(read_ape_i32); let year = tag.item("Year").and_then(ape_ext::read_i32);
let disc_number = tag.item("Disc").and_then(read_ape_x_of_y); let disc_number = tag.item("Disc").and_then(ape_ext::read_x_of_y);
let track_number = tag.item("Track").and_then(read_ape_x_of_y); let track_number = tag.item("Track").and_then(ape_ext::read_x_of_y);
let lyricist = tag.item("LYRICIST").and_then(read_ape_string); let lyricists = ape_ext::read_strings(tag.items("LYRICIST"));
let composer = tag.item("COMPOSER").and_then(read_ape_string); let composers = ape_ext::read_strings(tag.items("COMPOSER"));
let genre = tag.item("GENRE").and_then(read_ape_string); let genres = ape_ext::read_strings(tag.items("GENRE"));
let label = tag.item("PUBLISHER").and_then(read_ape_string); let labels = ape_ext::read_strings(tag.items("PUBLISHER"));
Ok(SongTags { Ok(SongMetadata {
artist, artists,
album_artist, album_artists,
album, album,
title, title,
duration: None, duration: None,
@ -213,97 +190,67 @@ fn read_ape(path: &Path) -> Result<SongTags, Error> {
track_number, track_number,
year, year,
has_artwork: false, has_artwork: false,
lyricist, lyricists,
composer, composers,
genre, genres,
label, labels,
}) })
} }
fn read_vorbis(path: &Path) -> Result<SongTags, Error> { fn read_vorbis(path: &Path) -> Result<SongMetadata, Error> {
let file = fs::File::open(path).map_err(|e| Error::Io(path.to_owned(), e))?; let file = fs::File::open(path).map_err(|e| Error::Io(path.to_owned(), e))?;
let source = OggStreamReader::new(file)?; let source = OggStreamReader::new(file)?;
let mut tags = SongTags { let mut metadata = SongMetadata::default();
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,
};
for (key, value) in source.comment_hdr.comment_list { for (key, value) in source.comment_hdr.comment_list {
utils::match_ignore_case! { utils::match_ignore_case! {
match key { match key {
"TITLE" => tags.title = Some(value), "TITLE" => metadata.title = Some(value),
"ALBUM" => tags.album = Some(value), "ALBUM" => metadata.album = Some(value),
"ARTIST" => tags.artist = Some(value), "ARTIST" => metadata.artists.push(value),
"ALBUMARTIST" => tags.album_artist = Some(value), "ALBUMARTIST" => metadata.album_artists.push(value),
"TRACKNUMBER" => tags.track_number = value.parse::<u32>().ok(), "TRACKNUMBER" => metadata.track_number = value.parse::<u32>().ok(),
"DISCNUMBER" => tags.disc_number = value.parse::<u32>().ok(), "DISCNUMBER" => metadata.disc_number = value.parse::<u32>().ok(),
"DATE" => tags.year = value.parse::<i32>().ok(), "DATE" => metadata.year = value.parse::<i32>().ok(),
"LYRICIST" => tags.lyricist = Some(value), "LYRICIST" => metadata.lyricists.push(value),
"COMPOSER" => tags.composer = Some(value), "COMPOSER" => metadata.composers.push(value),
"GENRE" => tags.genre = Some(value), "GENRE" => metadata.genres.push(value),
"PUBLISHER" => tags.label = Some(value), "PUBLISHER" => metadata.labels.push(value),
_ => (), _ => (),
} }
} }
} }
Ok(tags) Ok(metadata)
} }
fn read_opus(path: &Path) -> Result<SongTags, Error> { fn read_opus(path: &Path) -> Result<SongMetadata, Error> {
let headers = opus_headers::parse_from_path(path)?; let headers = opus_headers::parse_from_path(path)?;
let mut tags = SongTags { let mut metadata = SongMetadata::default();
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,
};
for (key, value) in headers.comments.user_comments { for (key, value) in headers.comments.user_comments {
utils::match_ignore_case! { utils::match_ignore_case! {
match key { match key {
"TITLE" => tags.title = Some(value), "TITLE" => metadata.title = Some(value),
"ALBUM" => tags.album = Some(value), "ALBUM" => metadata.album = Some(value),
"ARTIST" => tags.artist = Some(value), "ARTIST" => metadata.artists.push(value),
"ALBUMARTIST" => tags.album_artist = Some(value), "ALBUMARTIST" => metadata.album_artists.push(value),
"TRACKNUMBER" => tags.track_number = value.parse::<u32>().ok(), "TRACKNUMBER" => metadata.track_number = value.parse::<u32>().ok(),
"DISCNUMBER" => tags.disc_number = value.parse::<u32>().ok(), "DISCNUMBER" => metadata.disc_number = value.parse::<u32>().ok(),
"DATE" => tags.year = value.parse::<i32>().ok(), "DATE" => metadata.year = value.parse::<i32>().ok(),
"LYRICIST" => tags.lyricist = Some(value), "LYRICIST" => metadata.lyricists.push(value),
"COMPOSER" => tags.composer = Some(value), "COMPOSER" => metadata.composers.push(value),
"GENRE" => tags.genre = Some(value), "GENRE" => metadata.genres.push(value),
"PUBLISHER" => tags.label = Some(value), "PUBLISHER" => metadata.labels.push(value),
_ => (), _ => (),
} }
} }
} }
Ok(tags) Ok(metadata)
} }
fn read_flac(path: &Path) -> Result<SongTags, Error> { fn read_flac(path: &Path) -> Result<SongMetadata, Error> {
let tag = metaflac::Tag::read_from_path(path)?; let tag = metaflac::Tag::read_from_path(path)?;
let vorbis = tag let vorbis = tag
.vorbis_comments() .vorbis_comments()
@ -319,9 +266,11 @@ fn read_flac(path: &Path) -> Result<SongTags, Error> {
}; };
let has_artwork = tag.pictures().count() > 0; let has_artwork = tag.pictures().count() > 0;
Ok(SongTags { let multivalue = |o: Option<&Vec<String>>| o.cloned().unwrap_or_default();
artist: vorbis.artist().map(|v| v[0].clone()),
album_artist: vorbis.album_artist().map(|v| v[0].clone()), Ok(SongMetadata {
artists: multivalue(vorbis.artist()),
album_artists: multivalue(vorbis.album_artist()),
album: vorbis.album().map(|v| v[0].clone()), album: vorbis.album().map(|v| v[0].clone()),
title: vorbis.title().map(|v| v[0].clone()), title: vorbis.title().map(|v| v[0].clone()),
duration, duration,
@ -329,20 +278,20 @@ fn read_flac(path: &Path) -> Result<SongTags, Error> {
track_number: vorbis.track(), track_number: vorbis.track(),
year, year,
has_artwork, has_artwork,
lyricist: vorbis.get("LYRICIST").map(|v| v[0].clone()), lyricists: multivalue(vorbis.get("LYRICIST")),
composer: vorbis.get("COMPOSER").map(|v| v[0].clone()), composers: multivalue(vorbis.get("COMPOSER")),
genre: vorbis.get("GENRE").map(|v| v[0].clone()), genres: multivalue(vorbis.get("GENRE")),
label: vorbis.get("PUBLISHER").map(|v| v[0].clone()), labels: multivalue(vorbis.get("PUBLISHER")),
}) })
} }
fn read_mp4(path: &Path) -> Result<SongTags, Error> { fn read_mp4(path: &Path) -> Result<SongMetadata, Error> {
let mut tag = mp4ameta::Tag::read_from_path(path)?; let mut tag = mp4ameta::Tag::read_from_path(path)?;
let label_ident = mp4ameta::FreeformIdent::new("com.apple.iTunes", "Label"); let label_ident = mp4ameta::FreeformIdent::new("com.apple.iTunes", "Label");
Ok(SongTags { Ok(SongMetadata {
artist: tag.take_artist(), artists: tag.take_artists().collect(),
album_artist: tag.take_album_artist(), album_artists: tag.take_album_artists().collect(),
album: tag.take_album(), album: tag.take_album(),
title: tag.take_title(), title: tag.take_title(),
duration: tag.duration().map(|v| v.as_secs() as u32), duration: tag.duration().map(|v| v.as_secs() as u32),
@ -350,39 +299,39 @@ fn read_mp4(path: &Path) -> Result<SongTags, Error> {
track_number: tag.track_number().map(|d| d as u32), track_number: tag.track_number().map(|d| d as u32),
year: tag.year().and_then(|v| v.parse::<i32>().ok()), year: tag.year().and_then(|v| v.parse::<i32>().ok()),
has_artwork: tag.artwork().is_some(), has_artwork: tag.artwork().is_some(),
lyricist: tag.take_lyricist(), lyricists: tag.take_lyricists().collect(),
composer: tag.take_composer(), composers: tag.take_composers().collect(),
genre: tag.take_genre(), genres: tag.take_genres().collect(),
label: tag.take_strings_of(&label_ident).next(), labels: tag.take_strings_of(&label_ident).collect(),
}) })
} }
#[test] #[test]
fn reads_file_metadata() { fn reads_file_metadata() {
let sample_tags = SongTags { let sample_tags = SongMetadata {
disc_number: Some(3), disc_number: Some(3),
track_number: Some(1), track_number: Some(1),
title: Some("TEST TITLE".into()), title: Some("TEST TITLE".into()),
artist: Some("TEST ARTIST".into()), artists: vec!["TEST ARTIST".into()],
album_artist: Some("TEST ALBUM ARTIST".into()), album_artists: vec!["TEST ALBUM ARTIST".into()],
album: Some("TEST ALBUM".into()), album: Some("TEST ALBUM".into()),
duration: None, duration: None,
year: Some(2016), year: Some(2016),
has_artwork: false, has_artwork: false,
lyricist: Some("TEST LYRICIST".into()), lyricists: vec!["TEST LYRICIST".into()],
composer: Some("TEST COMPOSER".into()), composers: vec!["TEST COMPOSER".into()],
genre: Some("TEST GENRE".into()), genres: vec!["TEST GENRE".into()],
label: Some("TEST LABEL".into()), labels: vec!["TEST LABEL".into()],
}; };
let flac_sample_tag = SongTags { let flac_sample_tag = SongMetadata {
duration: Some(0), duration: Some(0),
..sample_tags.clone() ..sample_tags.clone()
}; };
let mp3_sample_tag = SongTags { let mp3_sample_tag = SongMetadata {
duration: Some(0), duration: Some(0),
..sample_tags.clone() ..sample_tags.clone()
}; };
let m4a_sample_tag = SongTags { let m4a_sample_tag = SongMetadata {
duration: Some(0), duration: Some(0),
..sample_tags.clone() ..sample_tags.clone()
}; };

View file

@ -167,8 +167,8 @@ impl Index {
WHERE ( path LIKE $1 WHERE ( path LIKE $1
OR title LIKE $1 OR title LIKE $1
OR album LIKE $1 OR album LIKE $1
OR artist LIKE $1 OR artists LIKE $1
OR album_artist LIKE $1 OR album_artists LIKE $1
) )
AND parent NOT LIKE $1 AND parent NOT LIKE $1
"#, "#,

View file

@ -203,8 +203,8 @@ async fn can_get_a_song() {
assert_eq!(song.track_number, Some(5)); assert_eq!(song.track_number, Some(5));
assert_eq!(song.disc_number, None); assert_eq!(song.disc_number, None);
assert_eq!(song.title, Some("シャーベット (Sherbet)".to_owned())); assert_eq!(song.title, Some("シャーベット (Sherbet)".to_owned()));
assert_eq!(song.artist, Some("Tobokegao".to_owned())); assert_eq!(song.artists, MultiString(vec!["Tobokegao".to_owned()]));
assert_eq!(song.album_artist, None); assert_eq!(song.album_artists, MultiString(vec![]));
assert_eq!(song.album, Some("Picnic".to_owned())); assert_eq!(song.album, Some("Picnic".to_owned()));
assert_eq!(song.year, Some(2016)); assert_eq!(song.year, Some(2016));
assert_eq!( assert_eq!(

View file

@ -1,34 +1,34 @@
use serde::{Deserialize, Serialize};
use std::path::Path; use std::path::Path;
use crate::app::vfs::VFS; use crate::app::vfs::VFS;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq)]
pub struct MultiString(pub Vec<String>);
#[derive(Debug, PartialEq, Eq)]
pub enum CollectionFile { pub enum CollectionFile {
Directory(Directory), Directory(Directory),
Song(Song), Song(Song),
} }
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq)]
pub struct Song { pub struct Song {
#[serde(skip_serializing, skip_deserializing)]
pub id: i64, pub id: i64,
pub path: String, pub path: String,
#[serde(skip_serializing, skip_deserializing)]
pub parent: String, pub parent: String,
pub track_number: Option<i64>, pub track_number: Option<i64>,
pub disc_number: Option<i64>, pub disc_number: Option<i64>,
pub title: Option<String>, pub title: Option<String>,
pub artist: Option<String>, pub artists: MultiString,
pub album_artist: Option<String>, pub album_artists: MultiString,
pub year: Option<i64>, pub year: Option<i64>,
pub album: Option<String>, pub album: Option<String>,
pub artwork: Option<String>, pub artwork: Option<String>,
pub duration: Option<i64>, pub duration: Option<i64>,
pub lyricist: Option<String>, pub lyricists: MultiString,
pub composer: Option<String>, pub composers: MultiString,
pub genre: Option<String>, pub genres: MultiString,
pub label: Option<String>, pub labels: MultiString,
} }
impl Song { impl Song {
@ -47,14 +47,12 @@ impl Song {
} }
} }
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Eq)]
pub struct Directory { pub struct Directory {
#[serde(skip_serializing, skip_deserializing)]
pub id: i64, pub id: i64,
pub path: String, pub path: String,
#[serde(skip_serializing, skip_deserializing)]
pub parent: Option<String>, pub parent: Option<String>,
pub artist: Option<String>, pub artists: MultiString,
pub year: Option<i64>, pub year: Option<i64>,
pub album: Option<String>, pub album: Option<String>,
pub artwork: Option<String>, pub artwork: Option<String>,

View file

@ -1,6 +1,8 @@
use log::error; use log::error;
use regex::Regex; use regex::Regex;
use crate::app::index::MultiString;
use super::*; use super::*;
pub struct Collector { pub struct Collector {
@ -31,7 +33,7 @@ impl Collector {
fn collect_directory(&self, directory: traverser::Directory) { fn collect_directory(&self, directory: traverser::Directory) {
let mut directory_album = None; let mut directory_album = None;
let mut directory_year = None; let mut directory_year = None;
let mut directory_artist = None; let mut directory_artists = None;
let mut inconsistent_directory_album = false; let mut inconsistent_directory_album = false;
let mut inconsistent_directory_year = false; let mut inconsistent_directory_year = false;
let mut inconsistent_directory_artist = false; let mut inconsistent_directory_artist = false;
@ -56,14 +58,13 @@ impl Collector {
directory_album = tags.album.as_ref().cloned(); directory_album = tags.album.as_ref().cloned();
} }
if tags.album_artist.is_some() { if !tags.album_artists.is_empty() {
inconsistent_directory_artist |= inconsistent_directory_artist |=
directory_artist.is_some() && directory_artist != tags.album_artist; directory_artists.as_ref() != Some(&tags.album_artists);
directory_artist = tags.album_artist.as_ref().cloned(); directory_artists = Some(tags.album_artists.clone());
} else if tags.artist.is_some() { } else if !tags.artists.is_empty() {
inconsistent_directory_artist |= inconsistent_directory_artist |= directory_artists.as_ref() != Some(&tags.artists);
directory_artist.is_some() && directory_artist != tags.artist; directory_artists = Some(tags.artists.clone());
directory_artist = tags.artist.as_ref().cloned();
} }
let artwork_path = if tags.has_artwork { let artwork_path = if tags.has_artwork {
@ -79,15 +80,15 @@ impl Collector {
track_number: tags.track_number.map(|n| n as i32), track_number: tags.track_number.map(|n| n as i32),
title: tags.title, title: tags.title,
duration: tags.duration.map(|n| n as i32), duration: tags.duration.map(|n| n as i32),
artist: tags.artist, artists: MultiString(tags.artists),
album_artist: tags.album_artist, album_artists: MultiString(tags.album_artists),
album: tags.album, album: tags.album,
year: tags.year, year: tags.year,
artwork: artwork_path, artwork: artwork_path,
lyricist: tags.lyricist, lyricists: MultiString(tags.lyricists),
composer: tags.composer, composers: MultiString(tags.composers),
genre: tags.genre, genres: MultiString(tags.genres),
label: tags.label, labels: MultiString(tags.labels),
})) { })) {
error!("Error while sending song from collector: {}", e); error!("Error while sending song from collector: {}", e);
} }
@ -100,7 +101,7 @@ impl Collector {
directory_album = None; directory_album = None;
} }
if inconsistent_directory_artist { if inconsistent_directory_artist {
directory_artist = None; directory_artists = None;
} }
if let Err(e) = self if let Err(e) = self
@ -110,7 +111,7 @@ impl Collector {
parent: directory_parent_string, parent: directory_parent_string,
artwork: directory_artwork, artwork: directory_artwork,
album: directory_album, album: directory_album,
artist: directory_artist, artists: MultiString(directory_artists.unwrap_or_default()),
year: directory_year, year: directory_year,
date_added: directory.created, date_added: directory.created,
})) { })) {

View file

@ -1,8 +1,14 @@
use std::borrow::Cow;
use log::error; use log::error;
use sqlx::{QueryBuilder, Sqlite}; use sqlx::{
encode::IsNull,
sqlite::{SqliteArgumentValue, SqliteTypeInfo},
QueryBuilder, Sqlite,
};
use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::mpsc::UnboundedReceiver;
use crate::db::DB; use crate::{app::index::MultiString, db::DB};
const INDEX_BUILDING_INSERT_BUFFER_SIZE: usize = 1000; // Insertions in each transaction const INDEX_BUILDING_INSERT_BUFFER_SIZE: usize = 1000; // Insertions in each transaction
@ -12,22 +18,22 @@ pub struct Song {
pub track_number: Option<i32>, pub track_number: Option<i32>,
pub disc_number: Option<i32>, pub disc_number: Option<i32>,
pub title: Option<String>, pub title: Option<String>,
pub artist: Option<String>, pub artists: MultiString,
pub album_artist: Option<String>, pub album_artists: MultiString,
pub year: Option<i32>, pub year: Option<i32>,
pub album: Option<String>, pub album: Option<String>,
pub artwork: Option<String>, pub artwork: Option<String>,
pub duration: Option<i32>, pub duration: Option<i32>,
pub lyricist: Option<String>, pub lyricists: MultiString,
pub composer: Option<String>, pub composers: MultiString,
pub genre: Option<String>, pub genres: MultiString,
pub label: Option<String>, pub labels: MultiString,
} }
pub struct Directory { pub struct Directory {
pub path: String, pub path: String,
pub parent: Option<String>, pub parent: Option<String>,
pub artist: Option<String>, pub artists: MultiString,
pub year: Option<i32>, pub year: Option<i32>,
pub album: Option<String>, pub album: Option<String>,
pub artwork: Option<String>, pub artwork: Option<String>,
@ -46,6 +52,39 @@ pub struct Inserter {
db: DB, db: DB,
} }
static MULTI_STRING_SEPARATOR: &str = "\u{000C}";
impl<'q> sqlx::Encode<'q, Sqlite> for MultiString {
fn encode_by_ref(&self, args: &mut Vec<SqliteArgumentValue<'q>>) -> IsNull {
if self.0.is_empty() {
IsNull::Yes
} else {
let joined = self.0.join(MULTI_STRING_SEPARATOR);
args.push(SqliteArgumentValue::Text(Cow::Owned(joined)));
IsNull::No
}
}
}
impl From<Option<String>> for MultiString {
fn from(value: Option<String>) -> Self {
match value {
None => MultiString(Vec::new()),
Some(s) => MultiString(
s.split(MULTI_STRING_SEPARATOR)
.map(|s| s.to_string())
.collect(),
),
}
}
}
impl sqlx::Type<Sqlite> for MultiString {
fn type_info() -> SqliteTypeInfo {
<&str as sqlx::Type<Sqlite>>::type_info()
}
}
impl Inserter { impl Inserter {
pub fn new(db: DB, receiver: UnboundedReceiver<Item>) -> Self { pub fn new(db: DB, receiver: UnboundedReceiver<Item>) -> Self {
let new_directories = Vec::with_capacity(INDEX_BUILDING_INSERT_BUFFER_SIZE); let new_directories = Vec::with_capacity(INDEX_BUILDING_INSERT_BUFFER_SIZE);
@ -90,12 +129,12 @@ impl Inserter {
}; };
let result = QueryBuilder::<Sqlite>::new( let result = QueryBuilder::<Sqlite>::new(
"INSERT INTO directories(path, parent, artist, year, album, artwork, date_added) ", "INSERT INTO directories(path, parent, artists, year, album, artwork, date_added) ",
) )
.push_values(&self.new_directories, |mut b, directory| { .push_values(&self.new_directories, |mut b, directory| {
b.push_bind(&directory.path) b.push_bind(&directory.path)
.push_bind(&directory.parent) .push_bind(&directory.parent)
.push_bind(&directory.artist) .push_bind(&directory.artists)
.push_bind(directory.year) .push_bind(directory.year)
.push_bind(&directory.album) .push_bind(&directory.album)
.push_bind(&directory.artwork) .push_bind(&directory.artwork)
@ -117,23 +156,23 @@ impl Inserter {
return; return;
}; };
let result = QueryBuilder::<Sqlite>::new("INSERT INTO songs(path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork, duration, lyricist, composer, genre, label) ") let result = QueryBuilder::<Sqlite>::new("INSERT INTO songs(path, parent, track_number, disc_number, title, artists, album_artists, year, album, artwork, duration, lyricists, composers, genres, labels) ")
.push_values(&self.new_songs, |mut b, song| { .push_values(&self.new_songs, |mut b, song| {
b.push_bind(&song.path) b.push_bind(&song.path)
.push_bind(&song.parent) .push_bind(&song.parent)
.push_bind(song.track_number) .push_bind(song.track_number)
.push_bind(song.disc_number) .push_bind(song.disc_number)
.push_bind(&song.title) .push_bind(&song.title)
.push_bind(&song.artist) .push_bind(&song.artists)
.push_bind(&song.album_artist) .push_bind(&song.album_artists)
.push_bind(song.year) .push_bind(song.year)
.push_bind(&song.album) .push_bind(&song.album)
.push_bind(&song.artwork) .push_bind(&song.artwork)
.push_bind(song.duration) .push_bind(song.duration)
.push_bind(&song.lyricist) .push_bind(&song.lyricists)
.push_bind(&song.composer) .push_bind(&song.composers)
.push_bind(&song.genre) .push_bind(&song.genres)
.push_bind(&song.label); .push_bind(&song.labels);
}) })
.build() .build()
.execute(connection.as_mut()) .execute(connection.as_mut())

View file

@ -9,12 +9,12 @@ use std::sync::Arc;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use crate::app::index::metadata::{self, SongTags}; use crate::app::index::metadata::{self, SongMetadata};
#[derive(Debug)] #[derive(Debug)]
pub struct Song { pub struct Song {
pub path: PathBuf, pub path: PathBuf,
pub metadata: SongTags, pub metadata: SongMetadata,
} }
#[derive(Debug)] #[derive(Debug)]

View file

@ -86,7 +86,7 @@ impl Manager {
async fn scrobble_from_path(&self, track: &Path) -> Result<Scrobble, Error> { async fn scrobble_from_path(&self, track: &Path) -> Result<Scrobble, Error> {
let song = self.index.get_song(track).await?; let song = self.index.get_song(track).await?;
Ok(Scrobble::new( Ok(Scrobble::new(
song.artist.as_deref().unwrap_or(""), song.artists.0.first().map(|s| s.as_str()).unwrap_or(""),
song.title.as_deref().unwrap_or(""), song.title.as_deref().unwrap_or(""),
song.album.as_deref().unwrap_or(""), song.album.as_deref().unwrap_or(""),
)) ))

View file

@ -49,7 +49,7 @@ CREATE TABLE directories (
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
path TEXT NOT NULL, path TEXT NOT NULL,
parent TEXT, parent TEXT,
artist TEXT, artists TEXT,
year INTEGER, year INTEGER,
album TEXT, album TEXT,
artwork TEXT, artwork TEXT,
@ -64,16 +64,16 @@ CREATE TABLE songs (
track_number INTEGER, track_number INTEGER,
disc_number INTEGER, disc_number INTEGER,
title TEXT, title TEXT,
artist TEXT, artists TEXT,
album_artist TEXT, album_artists TEXT,
year INTEGER, year INTEGER,
album TEXT, album TEXT,
artwork TEXT, artwork TEXT,
duration INTEGER, duration INTEGER,
lyricist TEXT, lyricists TEXT,
composer TEXT, composers TEXT,
genre TEXT, genres TEXT,
label TEXT, labels TEXT,
UNIQUE(path) ON CONFLICT REPLACE UNIQUE(path) ON CONFLICT REPLACE
); );

View file

@ -255,70 +255,70 @@ async fn post_trigger_index(
async fn get_browse_root( async fn get_browse_root(
_auth: Auth, _auth: Auth,
State(index): State<index::Index>, State(index): State<index::Index>,
) -> Result<Json<Vec<index::CollectionFile>>, APIError> { ) -> Result<Json<Vec<dto::CollectionFile>>, APIError> {
let result = index.browse(std::path::Path::new("")).await?; let result = index.browse(std::path::Path::new("")).await?;
Ok(Json(result)) Ok(Json(result.into_iter().map(|f| f.into()).collect()))
} }
async fn get_browse( async fn get_browse(
_auth: Auth, _auth: Auth,
State(index): State<index::Index>, State(index): State<index::Index>,
Path(path): Path<String>, Path(path): Path<String>,
) -> Result<Json<Vec<index::CollectionFile>>, APIError> { ) -> Result<Json<Vec<dto::CollectionFile>>, APIError> {
let path = percent_decode_str(&path).decode_utf8_lossy(); let path = percent_decode_str(&path).decode_utf8_lossy();
let result = index.browse(std::path::Path::new(path.as_ref())).await?; let result = index.browse(std::path::Path::new(path.as_ref())).await?;
Ok(Json(result)) Ok(Json(result.into_iter().map(|f| f.into()).collect()))
} }
async fn get_flatten_root( async fn get_flatten_root(
_auth: Auth, _auth: Auth,
State(index): State<index::Index>, State(index): State<index::Index>,
) -> Result<Json<Vec<index::Song>>, APIError> { ) -> Result<Json<Vec<dto::Song>>, APIError> {
let songs = index.flatten(std::path::Path::new("")).await?; let songs = index.flatten(std::path::Path::new("")).await?;
Ok(Json(songs)) Ok(Json(songs.into_iter().map(|f| f.into()).collect()))
} }
async fn get_flatten( async fn get_flatten(
_auth: Auth, _auth: Auth,
State(index): State<index::Index>, State(index): State<index::Index>,
Path(path): Path<String>, Path(path): Path<String>,
) -> Result<Json<Vec<index::Song>>, APIError> { ) -> Result<Json<Vec<dto::Song>>, APIError> {
let path = percent_decode_str(&path).decode_utf8_lossy(); let path = percent_decode_str(&path).decode_utf8_lossy();
let songs = index.flatten(std::path::Path::new(path.as_ref())).await?; let songs = index.flatten(std::path::Path::new(path.as_ref())).await?;
Ok(Json(songs)) Ok(Json(songs.into_iter().map(|f| f.into()).collect()))
} }
async fn get_random( async fn get_random(
_auth: Auth, _auth: Auth,
State(index): State<index::Index>, State(index): State<index::Index>,
) -> Result<Json<Vec<index::Directory>>, APIError> { ) -> Result<Json<Vec<dto::Directory>>, APIError> {
let result = index.get_random_albums(20).await?; let result = index.get_random_albums(20).await?;
Ok(Json(result)) Ok(Json(result.into_iter().map(|f| f.into()).collect()))
} }
async fn get_recent( async fn get_recent(
_auth: Auth, _auth: Auth,
State(index): State<index::Index>, State(index): State<index::Index>,
) -> Result<Json<Vec<index::Directory>>, APIError> { ) -> Result<Json<Vec<dto::Directory>>, APIError> {
let result = index.get_recent_albums(20).await?; let result = index.get_recent_albums(20).await?;
Ok(Json(result)) Ok(Json(result.into_iter().map(|f| f.into()).collect()))
} }
async fn get_search_root( async fn get_search_root(
_auth: Auth, _auth: Auth,
State(index): State<index::Index>, State(index): State<index::Index>,
) -> Result<Json<Vec<index::CollectionFile>>, APIError> { ) -> Result<Json<Vec<dto::CollectionFile>>, APIError> {
let result = index.search("").await?; let result = index.search("").await?;
Ok(Json(result)) Ok(Json(result.into_iter().map(|f| f.into()).collect()))
} }
async fn get_search( async fn get_search(
_auth: Auth, _auth: Auth,
State(index): State<index::Index>, State(index): State<index::Index>,
Path(query): Path<String>, Path(query): Path<String>,
) -> Result<Json<Vec<index::CollectionFile>>, APIError> { ) -> Result<Json<Vec<dto::CollectionFile>>, APIError> {
let result = index.search(&query).await?; let result = index.search(&query).await?;
Ok(Json(result)) Ok(Json(result.into_iter().map(|f| f.into()).collect()))
} }
async fn get_playlists( async fn get_playlists(
@ -350,11 +350,11 @@ async fn get_playlist(
auth: Auth, auth: Auth,
State(playlist_manager): State<playlist::Manager>, State(playlist_manager): State<playlist::Manager>,
Path(name): Path<String>, Path(name): Path<String>,
) -> Result<Json<Vec<index::Song>>, APIError> { ) -> Result<Json<Vec<dto::Song>>, APIError> {
let songs = playlist_manager let songs = playlist_manager
.read_playlist(&name, auth.get_username()) .read_playlist(&name, auth.get_username())
.await?; .await?;
Ok(Json(songs)) Ok(Json(songs.into_iter().map(|f| f.into()).collect()))
} }
async fn delete_playlist( async fn delete_playlist(

View file

@ -1,9 +1,9 @@
use serde::{Deserialize, Serialize}; 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; use std::convert::From;
pub const API_MAJOR_VERSION: i32 = 7; pub const API_MAJOR_VERSION: i32 = 8;
pub const API_MINOR_VERSION: i32 = 0; pub const API_MINOR_VERSION: i32 = 0;
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] #[derive(PartialEq, Eq, Debug, Serialize, Deserialize)]
@ -231,5 +231,99 @@ impl From<settings::Settings> for Settings {
} }
} }
// TODO: Preferences, CollectionFile, Song and Directory should have dto types #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CollectionFile {
Directory(Directory),
Song(Song),
}
impl From<index::CollectionFile> for CollectionFile {
fn from(f: index::CollectionFile) -> Self {
match f {
index::CollectionFile::Directory(d) => Self::Directory(d.into()),
index::CollectionFile::Song(s) => Self::Song(s.into()),
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Song {
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub track_number: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disc_number: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub artists: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub album_artists: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub year: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub album: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub artwork: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duration: Option<i64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub lyricists: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub composers: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub genres: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub labels: Vec<String>,
}
impl From<index::Song> for Song {
fn from(s: index::Song) -> Self {
Self {
path: s.path,
track_number: s.track_number,
disc_number: s.disc_number,
title: s.title,
artists: s.artists.0,
album_artists: s.album_artists.0,
year: s.year,
album: s.album,
artwork: s.artwork,
duration: s.duration,
lyricists: s.lyricists.0,
composers: s.composers.0,
genres: s.genres.0,
labels: s.labels.0,
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Directory {
pub path: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub artists: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub year: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub album: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub artwork: Option<String>,
pub date_added: i64,
}
impl From<index::Directory> for Directory {
fn from(d: index::Directory) -> Self {
Self {
path: d.path,
artists: d.artists.0,
year: d.year,
album: d.album,
artwork: d.artwork,
date_added: d.date_added,
}
}
}
// TODO: Preferences, CollectionFile should have dto types
// TODO Song dto type should skip `None` values when serializing, to lower payload sizes by a lot // TODO Song dto type should skip `None` values when serializing, to lower payload sizes by a lot

View file

@ -22,7 +22,6 @@ mod swagger;
mod user; mod user;
mod web; mod web;
use crate::app::index;
use crate::server::dto; use crate::server::dto;
use crate::server::test::constants::*; use crate::server::test::constants::*;
@ -119,7 +118,7 @@ pub trait TestService {
loop { loop {
let browse_request = protocol::browse(Path::new("")); let browse_request = protocol::browse(Path::new(""));
let response = self let response = self
.fetch_json::<(), Vec<index::CollectionFile>>(&browse_request) .fetch_json::<(), Vec<dto::CollectionFile>>(&browse_request)
.await; .await;
let entries = response.body(); let entries = response.body();
if !entries.is_empty() { if !entries.is_empty() {
@ -130,9 +129,7 @@ pub trait TestService {
loop { loop {
let flatten_request = protocol::flatten(Path::new("")); let flatten_request = protocol::flatten(Path::new(""));
let response = self let response = self.fetch_json::<_, Vec<dto::Song>>(&flatten_request).await;
.fetch_json::<_, Vec<index::Song>>(&flatten_request)
.await;
let entries = response.body(); let entries = response.body();
if !entries.is_empty() { if !entries.is_empty() {
break; break;

View file

@ -1,6 +1,5 @@
use http::StatusCode; use http::StatusCode;
use crate::app::index;
use crate::server::dto; use crate::server::dto;
use crate::server::test::{protocol, ServiceType, TestService}; use crate::server::test::{protocol, ServiceType, TestService};
use crate::test_name; use crate::test_name;
@ -50,17 +49,13 @@ async fn trigger_index_golden_path() {
let request = protocol::random(); let request = protocol::random();
let response = service let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await;
.fetch_json::<_, Vec<index::Directory>>(&request)
.await;
let entries = response.body(); let entries = response.body();
assert_eq!(entries.len(), 0); assert_eq!(entries.len(), 0);
service.index().await; service.index().await;
let response = service let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await;
.fetch_json::<_, Vec<index::Directory>>(&request)
.await;
let entries = response.body(); let entries = response.body();
assert_eq!(entries.len(), 3); assert_eq!(entries.len(), 3);
} }

View file

@ -1,7 +1,7 @@
use http::StatusCode; use http::StatusCode;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::app::index; use crate::server::dto;
use crate::server::test::{add_trailing_slash, constants::*, protocol, ServiceType, TestService}; use crate::server::test::{add_trailing_slash, constants::*, protocol, ServiceType, TestService};
use crate::test_name; use crate::test_name;
@ -23,7 +23,7 @@ async fn browse_root() {
let request = protocol::browse(&PathBuf::new()); let request = protocol::browse(&PathBuf::new());
let response = service let response = service
.fetch_json::<_, Vec<index::CollectionFile>>(&request) .fetch_json::<_, Vec<dto::CollectionFile>>(&request)
.await; .await;
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
let entries = response.body(); let entries = response.body();
@ -41,7 +41,7 @@ async fn browse_directory() {
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect(); let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect();
let request = protocol::browse(&path); let request = protocol::browse(&path);
let response = service let response = service
.fetch_json::<_, Vec<index::CollectionFile>>(&request) .fetch_json::<_, Vec<dto::CollectionFile>>(&request)
.await; .await;
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
let entries = response.body(); let entries = response.body();
@ -77,7 +77,7 @@ async fn flatten_root() {
service.login().await; service.login().await;
let request = protocol::flatten(&PathBuf::new()); let request = protocol::flatten(&PathBuf::new());
let response = service.fetch_json::<_, Vec<index::Song>>(&request).await; let response = service.fetch_json::<_, Vec<dto::Song>>(&request).await;
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
let entries = response.body(); let entries = response.body();
assert_eq!(entries.len(), 13); assert_eq!(entries.len(), 13);
@ -92,7 +92,7 @@ async fn flatten_directory() {
service.login().await; service.login().await;
let request = protocol::flatten(Path::new(TEST_MOUNT_NAME)); let request = protocol::flatten(Path::new(TEST_MOUNT_NAME));
let response = service.fetch_json::<_, Vec<index::Song>>(&request).await; let response = service.fetch_json::<_, Vec<dto::Song>>(&request).await;
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
let entries = response.body(); let entries = response.body();
assert_eq!(entries.len(), 13); assert_eq!(entries.len(), 13);
@ -127,9 +127,7 @@ async fn random_golden_path() {
service.login().await; service.login().await;
let request = protocol::random(); let request = protocol::random();
let response = service let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await;
.fetch_json::<_, Vec<index::Directory>>(&request)
.await;
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
let entries = response.body(); let entries = response.body();
assert_eq!(entries.len(), 3); assert_eq!(entries.len(), 3);
@ -145,9 +143,7 @@ async fn random_with_trailing_slash() {
let mut request = protocol::random(); let mut request = protocol::random();
add_trailing_slash(&mut request); add_trailing_slash(&mut request);
let response = service let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await;
.fetch_json::<_, Vec<index::Directory>>(&request)
.await;
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
let entries = response.body(); let entries = response.body();
assert_eq!(entries.len(), 3); assert_eq!(entries.len(), 3);
@ -170,9 +166,7 @@ async fn recent_golden_path() {
service.login().await; service.login().await;
let request = protocol::recent(); let request = protocol::recent();
let response = service let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await;
.fetch_json::<_, Vec<index::Directory>>(&request)
.await;
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
let entries = response.body(); let entries = response.body();
assert_eq!(entries.len(), 3); assert_eq!(entries.len(), 3);
@ -188,9 +182,7 @@ async fn recent_with_trailing_slash() {
let mut request = protocol::recent(); let mut request = protocol::recent();
add_trailing_slash(&mut request); add_trailing_slash(&mut request);
let response = service let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await;
.fetch_json::<_, Vec<index::Directory>>(&request)
.await;
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
let entries = response.body(); let entries = response.body();
assert_eq!(entries.len(), 3); assert_eq!(entries.len(), 3);
@ -212,7 +204,7 @@ async fn search_without_query() {
let request = protocol::search(""); let request = protocol::search("");
let response = service let response = service
.fetch_json::<_, Vec<index::CollectionFile>>(&request) .fetch_json::<_, Vec<dto::CollectionFile>>(&request)
.await; .await;
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
} }
@ -227,12 +219,12 @@ async fn search_with_query() {
let request = protocol::search("door"); let request = protocol::search("door");
let response = service let response = service
.fetch_json::<_, Vec<index::CollectionFile>>(&request) .fetch_json::<_, Vec<dto::CollectionFile>>(&request)
.await; .await;
let results = response.body(); let results = response.body();
assert_eq!(results.len(), 1); assert_eq!(results.len(), 1);
match results[0] { match results[0] {
index::CollectionFile::Song(ref s) => { dto::CollectionFile::Song(ref s) => {
assert_eq!(s.title, Some("Beyond The Door".into())) assert_eq!(s.title, Some("Beyond The Door".into()))
} }
_ => panic!(), _ => panic!(),

View file

@ -1,6 +1,5 @@
use http::StatusCode; use http::StatusCode;
use crate::app::index;
use crate::server::dto; use crate::server::dto;
use crate::server::test::{constants::*, protocol, ServiceType, TestService}; use crate::server::test::{constants::*, protocol, ServiceType, TestService};
use crate::test_name; use crate::test_name;
@ -83,7 +82,7 @@ async fn get_playlist_golden_path() {
} }
let request = protocol::read_playlist(TEST_PLAYLIST_NAME); let request = protocol::read_playlist(TEST_PLAYLIST_NAME);
let response = service.fetch_json::<_, Vec<index::Song>>(&request).await; let response = service.fetch_json::<_, Vec<dto::Song>>(&request).await;
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
} }