Adds multi-value fields (single row)
This commit is contained in:
parent
5a785a2e16
commit
9d8d543494
15 changed files with 391 additions and 327 deletions
|
@ -1,7 +1,6 @@
|
|||
use id3::TagLike;
|
||||
use lewton::inside_ogg::OggStreamReader;
|
||||
use log::error;
|
||||
use regex::Regex;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
|
@ -28,69 +27,31 @@ pub enum Error {
|
|||
VorbisCommentNotFoundInFlacFile,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SongTags {
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct SongMetadata {
|
||||
pub disc_number: Option<u32>,
|
||||
pub track_number: Option<u32>,
|
||||
pub title: Option<String>,
|
||||
pub duration: Option<u32>,
|
||||
pub artist: Option<String>,
|
||||
pub album_artist: Option<String>,
|
||||
pub artists: Vec<String>,
|
||||
pub album_artists: Vec<String>,
|
||||
pub album: Option<String>,
|
||||
pub year: Option<i32>,
|
||||
pub has_artwork: bool,
|
||||
pub lyricist: Option<String>,
|
||||
pub composer: Option<String>,
|
||||
pub genre: Option<String>,
|
||||
pub label: Option<String>,
|
||||
pub lyricists: Vec<String>,
|
||||
pub composers: Vec<String>,
|
||||
pub genres: Vec<String>,
|
||||
pub labels: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<id3::Tag> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(path: &Path) -> Option<SongTags> {
|
||||
pub fn read(path: &Path) -> Option<SongMetadata> {
|
||||
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::MP3) => read_mp3(path),
|
||||
Some(AudioFormat::OGG) => read_vorbis(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::MP4) | Some(AudioFormat::M4B) => read_mp4(path),
|
||||
None => return None,
|
||||
|
@ -104,23 +65,20 @@ pub fn read(path: &Path) -> Option<SongTags> {
|
|||
}
|
||||
}
|
||||
|
||||
trait FrameContent {
|
||||
/// Returns the value stored, if any, in the Frame.
|
||||
/// Say "TCOM" returns composer field.
|
||||
fn get_text(&self, key: &str) -> Option<String>;
|
||||
trait ID3Ext {
|
||||
fn get_text_values(&self, frame_name: &str) -> Vec<String>;
|
||||
}
|
||||
|
||||
impl FrameContent for id3::Tag {
|
||||
fn get_text(&self, key: &str) -> Option<String> {
|
||||
let frame = self.get(key)?;
|
||||
match frame.content() {
|
||||
id3::Content::Text(value) => Some(value.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
impl ID3Ext for id3::Tag {
|
||||
fn get_text_values(&self, frame_name: &str) -> Vec<String> {
|
||||
self.get(frame_name)
|
||||
.and_then(|f| f.content().text_values())
|
||||
.map(|i| i.map(str::to_string).collect())
|
||||
.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| {
|
||||
if let Some(tag) = error.partial_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 = {
|
||||
mp3_duration::from_path(path)
|
||||
.map(|d| d.as_secs() as u32)
|
||||
.ok()
|
||||
};
|
||||
|
||||
let mut song_tags: SongTags = tag.into();
|
||||
song_tags.duration = duration; // Use duration from mp3_duration instead of from tags.
|
||||
Ok(song_tags)
|
||||
metadata.duration = duration;
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
fn read_aiff(path: &Path) -> Result<SongTags, Error> {
|
||||
let tag = id3::Tag::read_from_path(path).or_else(|error| {
|
||||
if let Some(tag) = error.partial_tag {
|
||||
Ok(tag)
|
||||
} else {
|
||||
Err(error)
|
||||
mod ape_ext {
|
||||
pub fn read_string(item: &ape::Item) -> Option<String> {
|
||||
match item.value {
|
||||
ape::ItemValue::Text(ref s) => Some(s.clone()),
|
||||
_ => None,
|
||||
}
|
||||
})?;
|
||||
Ok(tag.into())
|
||||
}
|
||||
}
|
||||
|
||||
fn read_wave(path: &Path) -> Result<SongTags, Error> {
|
||||
let tag = id3::Tag::read_from_path(path).or_else(|error| {
|
||||
if let Some(tag) = error.partial_tag {
|
||||
Ok(tag)
|
||||
} else {
|
||||
Err(error)
|
||||
pub fn read_strings(items: Vec<&ape::Item>) -> Vec<String> {
|
||||
items.iter().filter_map(|i| read_string(i)).collect()
|
||||
}
|
||||
|
||||
pub fn read_i32(item: &ape::Item) -> Option<i32> {
|
||||
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> {
|
||||
match item.value {
|
||||
ape::ItemValue::Text(ref s) => s.parse::<i32>().ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_ape_x_of_y(item: &ape::Item) -> Option<u32> {
|
||||
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
|
||||
pub fn read_x_of_y(item: &ape::Item) -> Option<u32> {
|
||||
match item.value {
|
||||
ape::ItemValue::Text(ref s) => {
|
||||
let format = regex::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 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,
|
||||
let artists = ape_ext::read_strings(tag.items("Artist"));
|
||||
let album = tag.item("Album").and_then(ape_ext::read_string);
|
||||
let album_artists = ape_ext::read_strings(tag.items("Album artist"));
|
||||
let title = tag.item("Title").and_then(ape_ext::read_string);
|
||||
let year = tag.item("Year").and_then(ape_ext::read_i32);
|
||||
let disc_number = tag.item("Disc").and_then(ape_ext::read_x_of_y);
|
||||
let track_number = tag.item("Track").and_then(ape_ext::read_x_of_y);
|
||||
let lyricists = ape_ext::read_strings(tag.items("LYRICIST"));
|
||||
let composers = ape_ext::read_strings(tag.items("COMPOSER"));
|
||||
let genres = ape_ext::read_strings(tag.items("GENRE"));
|
||||
let labels = ape_ext::read_strings(tag.items("PUBLISHER"));
|
||||
Ok(SongMetadata {
|
||||
artists,
|
||||
album_artists,
|
||||
album,
|
||||
title,
|
||||
duration: None,
|
||||
|
@ -213,97 +190,67 @@ fn read_ape(path: &Path) -> Result<SongTags, Error> {
|
|||
track_number,
|
||||
year,
|
||||
has_artwork: false,
|
||||
lyricist,
|
||||
composer,
|
||||
genre,
|
||||
label,
|
||||
lyricists,
|
||||
composers,
|
||||
genres,
|
||||
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 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 metadata = SongMetadata::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),
|
||||
"TRACKNUMBER" => tags.track_number = value.parse::<u32>().ok(),
|
||||
"DISCNUMBER" => tags.disc_number = value.parse::<u32>().ok(),
|
||||
"DATE" => tags.year = value.parse::<i32>().ok(),
|
||||
"LYRICIST" => tags.lyricist = Some(value),
|
||||
"COMPOSER" => tags.composer = Some(value),
|
||||
"GENRE" => tags.genre = Some(value),
|
||||
"PUBLISHER" => tags.label = Some(value),
|
||||
"TITLE" => metadata.title = Some(value),
|
||||
"ALBUM" => metadata.album = Some(value),
|
||||
"ARTIST" => metadata.artists.push(value),
|
||||
"ALBUMARTIST" => metadata.album_artists.push(value),
|
||||
"TRACKNUMBER" => metadata.track_number = value.parse::<u32>().ok(),
|
||||
"DISCNUMBER" => metadata.disc_number = value.parse::<u32>().ok(),
|
||||
"DATE" => metadata.year = value.parse::<i32>().ok(),
|
||||
"LYRICIST" => metadata.lyricists.push(value),
|
||||
"COMPOSER" => metadata.composers.push(value),
|
||||
"GENRE" => metadata.genres.push(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 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 metadata = SongMetadata::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),
|
||||
"TRACKNUMBER" => tags.track_number = value.parse::<u32>().ok(),
|
||||
"DISCNUMBER" => tags.disc_number = value.parse::<u32>().ok(),
|
||||
"DATE" => tags.year = value.parse::<i32>().ok(),
|
||||
"LYRICIST" => tags.lyricist = Some(value),
|
||||
"COMPOSER" => tags.composer = Some(value),
|
||||
"GENRE" => tags.genre = Some(value),
|
||||
"PUBLISHER" => tags.label = Some(value),
|
||||
"TITLE" => metadata.title = Some(value),
|
||||
"ALBUM" => metadata.album = Some(value),
|
||||
"ARTIST" => metadata.artists.push(value),
|
||||
"ALBUMARTIST" => metadata.album_artists.push(value),
|
||||
"TRACKNUMBER" => metadata.track_number = value.parse::<u32>().ok(),
|
||||
"DISCNUMBER" => metadata.disc_number = value.parse::<u32>().ok(),
|
||||
"DATE" => metadata.year = value.parse::<i32>().ok(),
|
||||
"LYRICIST" => metadata.lyricists.push(value),
|
||||
"COMPOSER" => metadata.composers.push(value),
|
||||
"GENRE" => metadata.genres.push(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 vorbis = tag
|
||||
.vorbis_comments()
|
||||
|
@ -319,9 +266,11 @@ fn read_flac(path: &Path) -> Result<SongTags, Error> {
|
|||
};
|
||||
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()),
|
||||
let multivalue = |o: Option<&Vec<String>>| o.cloned().unwrap_or_default();
|
||||
|
||||
Ok(SongMetadata {
|
||||
artists: multivalue(vorbis.artist()),
|
||||
album_artists: multivalue(vorbis.album_artist()),
|
||||
album: vorbis.album().map(|v| v[0].clone()),
|
||||
title: vorbis.title().map(|v| v[0].clone()),
|
||||
duration,
|
||||
|
@ -329,20 +278,20 @@ fn read_flac(path: &Path) -> Result<SongTags, Error> {
|
|||
track_number: vorbis.track(),
|
||||
year,
|
||||
has_artwork,
|
||||
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()),
|
||||
label: vorbis.get("PUBLISHER").map(|v| v[0].clone()),
|
||||
lyricists: multivalue(vorbis.get("LYRICIST")),
|
||||
composers: multivalue(vorbis.get("COMPOSER")),
|
||||
genres: multivalue(vorbis.get("GENRE")),
|
||||
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 label_ident = mp4ameta::FreeformIdent::new("com.apple.iTunes", "Label");
|
||||
|
||||
Ok(SongTags {
|
||||
artist: tag.take_artist(),
|
||||
album_artist: tag.take_album_artist(),
|
||||
Ok(SongMetadata {
|
||||
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),
|
||||
|
@ -350,39 +299,39 @@ fn read_mp4(path: &Path) -> Result<SongTags, Error> {
|
|||
track_number: tag.track_number().map(|d| d as u32),
|
||||
year: tag.year().and_then(|v| v.parse::<i32>().ok()),
|
||||
has_artwork: tag.artwork().is_some(),
|
||||
lyricist: tag.take_lyricist(),
|
||||
composer: tag.take_composer(),
|
||||
genre: tag.take_genre(),
|
||||
label: tag.take_strings_of(&label_ident).next(),
|
||||
lyricists: tag.take_lyricists().collect(),
|
||||
composers: tag.take_composers().collect(),
|
||||
genres: tag.take_genres().collect(),
|
||||
labels: tag.take_strings_of(&label_ident).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_file_metadata() {
|
||||
let sample_tags = SongTags {
|
||||
let sample_tags = SongMetadata {
|
||||
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),
|
||||
has_artwork: false,
|
||||
lyricist: Some("TEST LYRICIST".into()),
|
||||
composer: Some("TEST COMPOSER".into()),
|
||||
genre: Some("TEST GENRE".into()),
|
||||
label: Some("TEST LABEL".into()),
|
||||
lyricists: vec!["TEST LYRICIST".into()],
|
||||
composers: vec!["TEST COMPOSER".into()],
|
||||
genres: vec!["TEST GENRE".into()],
|
||||
labels: vec!["TEST LABEL".into()],
|
||||
};
|
||||
let flac_sample_tag = SongTags {
|
||||
let flac_sample_tag = SongMetadata {
|
||||
duration: Some(0),
|
||||
..sample_tags.clone()
|
||||
};
|
||||
let mp3_sample_tag = SongTags {
|
||||
let mp3_sample_tag = SongMetadata {
|
||||
duration: Some(0),
|
||||
..sample_tags.clone()
|
||||
};
|
||||
let m4a_sample_tag = SongTags {
|
||||
let m4a_sample_tag = SongMetadata {
|
||||
duration: Some(0),
|
||||
..sample_tags.clone()
|
||||
};
|
||||
|
|
|
@ -167,8 +167,8 @@ impl Index {
|
|||
WHERE ( path LIKE $1
|
||||
OR title LIKE $1
|
||||
OR album LIKE $1
|
||||
OR artist LIKE $1
|
||||
OR album_artist LIKE $1
|
||||
OR artists LIKE $1
|
||||
OR album_artists LIKE $1
|
||||
)
|
||||
AND parent NOT LIKE $1
|
||||
"#,
|
||||
|
|
|
@ -203,8 +203,8 @@ async 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, MultiString(vec!["Tobokegao".to_owned()]));
|
||||
assert_eq!(song.album_artists, MultiString(vec![]));
|
||||
assert_eq!(song.album, Some("Picnic".to_owned()));
|
||||
assert_eq!(song.year, Some(2016));
|
||||
assert_eq!(
|
||||
|
|
|
@ -1,34 +1,34 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
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 {
|
||||
Directory(Directory),
|
||||
Song(Song),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Song {
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub id: i64,
|
||||
pub path: String,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub parent: String,
|
||||
pub track_number: Option<i64>,
|
||||
pub disc_number: Option<i64>,
|
||||
pub title: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub album_artist: Option<String>,
|
||||
pub artists: MultiString,
|
||||
pub album_artists: MultiString,
|
||||
pub year: Option<i64>,
|
||||
pub album: Option<String>,
|
||||
pub artwork: Option<String>,
|
||||
pub duration: Option<i64>,
|
||||
pub lyricist: Option<String>,
|
||||
pub composer: Option<String>,
|
||||
pub genre: Option<String>,
|
||||
pub label: Option<String>,
|
||||
pub lyricists: MultiString,
|
||||
pub composers: MultiString,
|
||||
pub genres: MultiString,
|
||||
pub labels: MultiString,
|
||||
}
|
||||
|
||||
impl Song {
|
||||
|
@ -47,14 +47,12 @@ impl Song {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Directory {
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub id: i64,
|
||||
pub path: String,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub parent: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub artists: MultiString,
|
||||
pub year: Option<i64>,
|
||||
pub album: Option<String>,
|
||||
pub artwork: Option<String>,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use log::error;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::app::index::MultiString;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct Collector {
|
||||
|
@ -31,7 +33,7 @@ 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 = None;
|
||||
let mut inconsistent_directory_album = false;
|
||||
let mut inconsistent_directory_year = false;
|
||||
let mut inconsistent_directory_artist = false;
|
||||
|
@ -56,14 +58,13 @@ impl Collector {
|
|||
directory_album = tags.album.as_ref().cloned();
|
||||
}
|
||||
|
||||
if tags.album_artist.is_some() {
|
||||
if !tags.album_artists.is_empty() {
|
||||
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();
|
||||
directory_artists.as_ref() != Some(&tags.album_artists);
|
||||
directory_artists = Some(tags.album_artists.clone());
|
||||
} else if !tags.artists.is_empty() {
|
||||
inconsistent_directory_artist |= directory_artists.as_ref() != Some(&tags.artists);
|
||||
directory_artists = Some(tags.artists.clone());
|
||||
}
|
||||
|
||||
let artwork_path = if tags.has_artwork {
|
||||
|
@ -79,15 +80,15 @@ impl Collector {
|
|||
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,
|
||||
artists: MultiString(tags.artists),
|
||||
album_artists: MultiString(tags.album_artists),
|
||||
album: tags.album,
|
||||
year: tags.year,
|
||||
artwork: artwork_path,
|
||||
lyricist: tags.lyricist,
|
||||
composer: tags.composer,
|
||||
genre: tags.genre,
|
||||
label: tags.label,
|
||||
lyricists: MultiString(tags.lyricists),
|
||||
composers: MultiString(tags.composers),
|
||||
genres: MultiString(tags.genres),
|
||||
labels: MultiString(tags.labels),
|
||||
})) {
|
||||
error!("Error while sending song from collector: {}", e);
|
||||
}
|
||||
|
@ -100,7 +101,7 @@ impl Collector {
|
|||
directory_album = None;
|
||||
}
|
||||
if inconsistent_directory_artist {
|
||||
directory_artist = None;
|
||||
directory_artists = None;
|
||||
}
|
||||
|
||||
if let Err(e) = self
|
||||
|
@ -110,7 +111,7 @@ impl Collector {
|
|||
parent: directory_parent_string,
|
||||
artwork: directory_artwork,
|
||||
album: directory_album,
|
||||
artist: directory_artist,
|
||||
artists: MultiString(directory_artists.unwrap_or_default()),
|
||||
year: directory_year,
|
||||
date_added: directory.created,
|
||||
})) {
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use log::error;
|
||||
use sqlx::{QueryBuilder, Sqlite};
|
||||
use sqlx::{
|
||||
encode::IsNull,
|
||||
sqlite::{SqliteArgumentValue, SqliteTypeInfo},
|
||||
QueryBuilder, Sqlite,
|
||||
};
|
||||
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
|
||||
|
||||
|
@ -12,22 +18,22 @@ pub struct Song {
|
|||
pub track_number: Option<i32>,
|
||||
pub disc_number: Option<i32>,
|
||||
pub title: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub album_artist: Option<String>,
|
||||
pub artists: MultiString,
|
||||
pub album_artists: MultiString,
|
||||
pub year: Option<i32>,
|
||||
pub album: Option<String>,
|
||||
pub artwork: Option<String>,
|
||||
pub duration: Option<i32>,
|
||||
pub lyricist: Option<String>,
|
||||
pub composer: Option<String>,
|
||||
pub genre: Option<String>,
|
||||
pub label: Option<String>,
|
||||
pub lyricists: MultiString,
|
||||
pub composers: MultiString,
|
||||
pub genres: MultiString,
|
||||
pub labels: MultiString,
|
||||
}
|
||||
|
||||
pub struct Directory {
|
||||
pub path: String,
|
||||
pub parent: Option<String>,
|
||||
pub artist: Option<String>,
|
||||
pub artists: MultiString,
|
||||
pub year: Option<i32>,
|
||||
pub album: Option<String>,
|
||||
pub artwork: Option<String>,
|
||||
|
@ -46,6 +52,39 @@ pub struct Inserter {
|
|||
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 {
|
||||
pub fn new(db: DB, receiver: UnboundedReceiver<Item>) -> Self {
|
||||
let new_directories = Vec::with_capacity(INDEX_BUILDING_INSERT_BUFFER_SIZE);
|
||||
|
@ -90,12 +129,12 @@ impl Inserter {
|
|||
};
|
||||
|
||||
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| {
|
||||
b.push_bind(&directory.path)
|
||||
.push_bind(&directory.parent)
|
||||
.push_bind(&directory.artist)
|
||||
.push_bind(&directory.artists)
|
||||
.push_bind(directory.year)
|
||||
.push_bind(&directory.album)
|
||||
.push_bind(&directory.artwork)
|
||||
|
@ -117,23 +156,23 @@ impl Inserter {
|
|||
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| {
|
||||
b.push_bind(&song.path)
|
||||
.push_bind(&song.parent)
|
||||
.push_bind(song.track_number)
|
||||
.push_bind(song.disc_number)
|
||||
.push_bind(&song.title)
|
||||
.push_bind(&song.artist)
|
||||
.push_bind(&song.album_artist)
|
||||
.push_bind(&song.artists)
|
||||
.push_bind(&song.album_artists)
|
||||
.push_bind(song.year)
|
||||
.push_bind(&song.album)
|
||||
.push_bind(&song.artwork)
|
||||
.push_bind(song.duration)
|
||||
.push_bind(&song.lyricist)
|
||||
.push_bind(&song.composer)
|
||||
.push_bind(&song.genre)
|
||||
.push_bind(&song.label);
|
||||
.push_bind(&song.lyricists)
|
||||
.push_bind(&song.composers)
|
||||
.push_bind(&song.genres)
|
||||
.push_bind(&song.labels);
|
||||
})
|
||||
.build()
|
||||
.execute(connection.as_mut())
|
||||
|
|
|
@ -9,12 +9,12 @@ use std::sync::Arc;
|
|||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::app::index::metadata::{self, SongTags};
|
||||
use crate::app::index::metadata::{self, SongMetadata};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Song {
|
||||
pub path: PathBuf,
|
||||
pub metadata: SongTags,
|
||||
pub metadata: SongMetadata,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -86,7 +86,7 @@ impl Manager {
|
|||
async fn scrobble_from_path(&self, track: &Path) -> Result<Scrobble, Error> {
|
||||
let song = self.index.get_song(track).await?;
|
||||
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.album.as_deref().unwrap_or(""),
|
||||
))
|
||||
|
|
|
@ -49,7 +49,7 @@ CREATE TABLE directories (
|
|||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
parent TEXT,
|
||||
artist TEXT,
|
||||
artists TEXT,
|
||||
year INTEGER,
|
||||
album TEXT,
|
||||
artwork TEXT,
|
||||
|
@ -64,16 +64,16 @@ CREATE TABLE songs (
|
|||
track_number INTEGER,
|
||||
disc_number INTEGER,
|
||||
title TEXT,
|
||||
artist TEXT,
|
||||
album_artist TEXT,
|
||||
artists TEXT,
|
||||
album_artists TEXT,
|
||||
year INTEGER,
|
||||
album TEXT,
|
||||
artwork TEXT,
|
||||
duration INTEGER,
|
||||
lyricist TEXT,
|
||||
composer TEXT,
|
||||
genre TEXT,
|
||||
label TEXT,
|
||||
lyricists TEXT,
|
||||
composers TEXT,
|
||||
genres TEXT,
|
||||
labels TEXT,
|
||||
UNIQUE(path) ON CONFLICT REPLACE
|
||||
);
|
||||
|
||||
|
|
|
@ -255,70 +255,70 @@ async fn post_trigger_index(
|
|||
async fn get_browse_root(
|
||||
_auth: Auth,
|
||||
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?;
|
||||
Ok(Json(result))
|
||||
Ok(Json(result.into_iter().map(|f| f.into()).collect()))
|
||||
}
|
||||
|
||||
async fn get_browse(
|
||||
_auth: Auth,
|
||||
State(index): State<index::Index>,
|
||||
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 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(
|
||||
_auth: Auth,
|
||||
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?;
|
||||
Ok(Json(songs))
|
||||
Ok(Json(songs.into_iter().map(|f| f.into()).collect()))
|
||||
}
|
||||
|
||||
async fn get_flatten(
|
||||
_auth: Auth,
|
||||
State(index): State<index::Index>,
|
||||
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 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(
|
||||
_auth: Auth,
|
||||
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?;
|
||||
Ok(Json(result))
|
||||
Ok(Json(result.into_iter().map(|f| f.into()).collect()))
|
||||
}
|
||||
|
||||
async fn get_recent(
|
||||
_auth: Auth,
|
||||
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?;
|
||||
Ok(Json(result))
|
||||
Ok(Json(result.into_iter().map(|f| f.into()).collect()))
|
||||
}
|
||||
|
||||
async fn get_search_root(
|
||||
_auth: Auth,
|
||||
State(index): State<index::Index>,
|
||||
) -> Result<Json<Vec<index::CollectionFile>>, APIError> {
|
||||
) -> Result<Json<Vec<dto::CollectionFile>>, APIError> {
|
||||
let result = index.search("").await?;
|
||||
Ok(Json(result))
|
||||
Ok(Json(result.into_iter().map(|f| f.into()).collect()))
|
||||
}
|
||||
|
||||
async fn get_search(
|
||||
_auth: Auth,
|
||||
State(index): State<index::Index>,
|
||||
Path(query): Path<String>,
|
||||
) -> Result<Json<Vec<index::CollectionFile>>, APIError> {
|
||||
) -> Result<Json<Vec<dto::CollectionFile>>, APIError> {
|
||||
let result = index.search(&query).await?;
|
||||
Ok(Json(result))
|
||||
Ok(Json(result.into_iter().map(|f| f.into()).collect()))
|
||||
}
|
||||
|
||||
async fn get_playlists(
|
||||
|
@ -350,11 +350,11 @@ async fn get_playlist(
|
|||
auth: Auth,
|
||||
State(playlist_manager): State<playlist::Manager>,
|
||||
Path(name): Path<String>,
|
||||
) -> Result<Json<Vec<index::Song>>, APIError> {
|
||||
) -> Result<Json<Vec<dto::Song>>, APIError> {
|
||||
let songs = playlist_manager
|
||||
.read_playlist(&name, auth.get_username())
|
||||
.await?;
|
||||
Ok(Json(songs))
|
||||
Ok(Json(songs.into_iter().map(|f| f.into()).collect()))
|
||||
}
|
||||
|
||||
async fn delete_playlist(
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
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;
|
||||
pub const API_MAJOR_VERSION: i32 = 8;
|
||||
pub const API_MINOR_VERSION: i32 = 0;
|
||||
|
||||
#[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
|
||||
|
|
|
@ -22,7 +22,6 @@ mod swagger;
|
|||
mod user;
|
||||
mod web;
|
||||
|
||||
use crate::app::index;
|
||||
use crate::server::dto;
|
||||
use crate::server::test::constants::*;
|
||||
|
||||
|
@ -119,7 +118,7 @@ pub trait TestService {
|
|||
loop {
|
||||
let browse_request = protocol::browse(Path::new(""));
|
||||
let response = self
|
||||
.fetch_json::<(), Vec<index::CollectionFile>>(&browse_request)
|
||||
.fetch_json::<(), Vec<dto::CollectionFile>>(&browse_request)
|
||||
.await;
|
||||
let entries = response.body();
|
||||
if !entries.is_empty() {
|
||||
|
@ -130,9 +129,7 @@ pub trait TestService {
|
|||
|
||||
loop {
|
||||
let flatten_request = protocol::flatten(Path::new(""));
|
||||
let response = self
|
||||
.fetch_json::<_, Vec<index::Song>>(&flatten_request)
|
||||
.await;
|
||||
let response = self.fetch_json::<_, Vec<dto::Song>>(&flatten_request).await;
|
||||
let entries = response.body();
|
||||
if !entries.is_empty() {
|
||||
break;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use http::StatusCode;
|
||||
|
||||
use crate::app::index;
|
||||
use crate::server::dto;
|
||||
use crate::server::test::{protocol, ServiceType, TestService};
|
||||
use crate::test_name;
|
||||
|
@ -50,17 +49,13 @@ async fn trigger_index_golden_path() {
|
|||
|
||||
let request = protocol::random();
|
||||
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::Directory>>(&request)
|
||||
.await;
|
||||
let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await;
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 0);
|
||||
|
||||
service.index().await;
|
||||
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::Directory>>(&request)
|
||||
.await;
|
||||
let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await;
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 3);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use http::StatusCode;
|
||||
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::test_name;
|
||||
|
||||
|
@ -23,7 +23,7 @@ async fn browse_root() {
|
|||
|
||||
let request = protocol::browse(&PathBuf::new());
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::CollectionFile>>(&request)
|
||||
.fetch_json::<_, Vec<dto::CollectionFile>>(&request)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let entries = response.body();
|
||||
|
@ -41,7 +41,7 @@ async fn browse_directory() {
|
|||
let path: PathBuf = [TEST_MOUNT_NAME, "Khemmis", "Hunted"].iter().collect();
|
||||
let request = protocol::browse(&path);
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::CollectionFile>>(&request)
|
||||
.fetch_json::<_, Vec<dto::CollectionFile>>(&request)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let entries = response.body();
|
||||
|
@ -77,7 +77,7 @@ async fn flatten_root() {
|
|||
service.login().await;
|
||||
|
||||
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);
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 13);
|
||||
|
@ -92,7 +92,7 @@ async fn flatten_directory() {
|
|||
service.login().await;
|
||||
|
||||
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);
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 13);
|
||||
|
@ -127,9 +127,7 @@ async fn random_golden_path() {
|
|||
service.login().await;
|
||||
|
||||
let request = protocol::random();
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::Directory>>(&request)
|
||||
.await;
|
||||
let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 3);
|
||||
|
@ -145,9 +143,7 @@ async fn random_with_trailing_slash() {
|
|||
|
||||
let mut request = protocol::random();
|
||||
add_trailing_slash(&mut request);
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::Directory>>(&request)
|
||||
.await;
|
||||
let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 3);
|
||||
|
@ -170,9 +166,7 @@ async fn recent_golden_path() {
|
|||
service.login().await;
|
||||
|
||||
let request = protocol::recent();
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::Directory>>(&request)
|
||||
.await;
|
||||
let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 3);
|
||||
|
@ -188,9 +182,7 @@ async fn recent_with_trailing_slash() {
|
|||
|
||||
let mut request = protocol::recent();
|
||||
add_trailing_slash(&mut request);
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::Directory>>(&request)
|
||||
.await;
|
||||
let response = service.fetch_json::<_, Vec<dto::Directory>>(&request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let entries = response.body();
|
||||
assert_eq!(entries.len(), 3);
|
||||
|
@ -212,7 +204,7 @@ async fn search_without_query() {
|
|||
|
||||
let request = protocol::search("");
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::CollectionFile>>(&request)
|
||||
.fetch_json::<_, Vec<dto::CollectionFile>>(&request)
|
||||
.await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
}
|
||||
|
@ -227,12 +219,12 @@ async fn search_with_query() {
|
|||
|
||||
let request = protocol::search("door");
|
||||
let response = service
|
||||
.fetch_json::<_, Vec<index::CollectionFile>>(&request)
|
||||
.fetch_json::<_, Vec<dto::CollectionFile>>(&request)
|
||||
.await;
|
||||
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!(),
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use http::StatusCode;
|
||||
|
||||
use crate::app::index;
|
||||
use crate::server::dto;
|
||||
use crate::server::test::{constants::*, protocol, ServiceType, TestService};
|
||||
use crate::test_name;
|
||||
|
@ -83,7 +82,7 @@ async fn get_playlist_golden_path() {
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue