Add few more fields to song information (#141)

* [meta] Add ignore paths to vscode settings

* [feature] Add few more fields to song information

Fields include lyricist, composer, genre, category
and label.
This commit is contained in:
pmphfm 2021-05-20 22:08:43 -07:00 committed by GitHub
parent 4c25195deb
commit f104355076
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 512 additions and 470 deletions

10
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,10 @@
{
"files.watcherExclude": {
"**/target/**": true,
"**/test-output/**": true
},
"files.exclude": {
"**/target": true,
"**/test-output": true
}
}

836
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -30,7 +30,7 @@ lewton = "0.10.1"
log = "0.4.5" log = "0.4.5"
metaflac = "0.2.3" metaflac = "0.2.3"
mp3-duration = "0.1.9" mp3-duration = "0.1.9"
mp4ameta = "0.7.1" mp4ameta = "0.10.0"
num_cpus = "1.13.0" num_cpus = "1.13.0"
opus_headers = "0.1.2" opus_headers = "0.1.2"
percent-encoding = "2.1" percent-encoding = "2.1"
@ -52,7 +52,7 @@ url = "2.1"
[dependencies.diesel] [dependencies.diesel]
version = "1.4.5" version = "1.4.5"
default_features = false default_features = false
features = ["libsqlite3-sys", "r2d2", "sqlite"] features = ["libsqlite3-sys", "r2d2", "sqlite", "64-column-tables"]
[dependencies.image] [dependencies.image]
version = "0.23.12" version = "0.23.12"

View file

@ -1417,6 +1417,22 @@
"duration": { "duration": {
"type": "integer", "type": "integer",
"example": 571 "example": 571
},
"lyricist": {
"type": "string",
"example": "Timo Tolkki"
},
"composer": {
"type": "string",
"example": "Timo Tolkki"
},
"genre": {
"type": "string",
"example": "Genre"
},
"label": {
"type": "string",
"example": "Noise Records"
} }
} }
}, },
@ -1492,4 +1508,4 @@
"callbacks": {} "callbacks": {}
}, },
"security": [] "security": []
} }

View file

@ -0,0 +1,20 @@
CREATE TEMPORARY TABLE songs_backup(id, path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork, duration);
INSERT INTO songs_backup SELECT id, path, parent, track_number, disc_number, title, artist, album_artist, year, album, artwork, duration FROM songs;
DROP TABLE songs;
CREATE TABLE songs (
id INTEGER PRIMARY KEY NOT NULL,
path TEXT NOT NULL,
parent TEXT NOT NULL,
track_number INTEGER,
disc_number INTEGER,
title TEXT,
artist TEXT,
album_artist TEXT,
year INTEGER,
album TEXT,
artwork TEXT,
duration INTEGER,
UNIQUE(path) ON CONFLICT REPLACE
);
INSERT INTO songs SELECT * FROM songs_backup;
DROP TABLE songs_backup;

View file

@ -0,0 +1,4 @@
ALTER TABLE songs ADD COLUMN lyricist TEXT;
ALTER TABLE songs ADD COLUMN composer TEXT;
ALTER TABLE songs ADD COLUMN genre TEXT;
ALTER TABLE songs ADD COLUMN label TEXT;

View file

@ -25,6 +25,10 @@ pub struct SongTags {
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 composer: Option<String>,
pub genre: Option<String>,
pub label: Option<String>,
} }
impl From<id3::Tag> for SongTags { impl From<id3::Tag> for SongTags {
@ -42,6 +46,10 @@ impl From<id3::Tag> for SongTags {
.or_else(|| tag.date_released().map(|d| d.year)) .or_else(|| tag.date_released().map(|d| d.year))
.or_else(|| tag.date_recorded().map(|d| d.year)); .or_else(|| tag.date_recorded().map(|d| d.year));
let has_artwork = tag.pictures().count() > 0; 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 { SongTags {
artist, artist,
@ -53,6 +61,10 @@ impl From<id3::Tag> for SongTags {
track_number, track_number,
year, year,
has_artwork, has_artwork,
lyricist,
composer,
genre,
label,
} }
} }
} }
@ -80,6 +92,22 @@ 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>;
}
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,
}
}
}
fn read_mp3(path: &Path) -> Result<SongTags> { fn read_mp3(path: &Path) -> Result<SongTags> {
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 {
@ -159,9 +187,14 @@ fn read_ape(path: &Path) -> Result<SongTags> {
let year = tag.item("Year").and_then(read_ape_i32); let year = tag.item("Year").and_then(read_ape_i32);
let disc_number = tag.item("Disc").and_then(read_ape_x_of_y); 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 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 { Ok(SongTags {
artist, //
album_artist, artist, //
album_artist, //
album, album,
title, title,
duration: None, duration: None,
@ -169,6 +202,10 @@ fn read_ape(path: &Path) -> Result<SongTags> {
track_number, track_number,
year, year,
has_artwork: false, has_artwork: false,
lyricist,
composer,
genre,
label,
}) })
} }
@ -186,6 +223,10 @@ fn read_vorbis(path: &Path) -> Result<SongTags> {
track_number: None, track_number: None,
year: None, year: None,
has_artwork: false, 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 {
@ -198,6 +239,10 @@ fn read_vorbis(path: &Path) -> Result<SongTags> {
"TRACKNUMBER" => tags.track_number = value.parse::<u32>().ok(), "TRACKNUMBER" => tags.track_number = value.parse::<u32>().ok(),
"DISCNUMBER" => tags.disc_number = value.parse::<u32>().ok(), "DISCNUMBER" => tags.disc_number = value.parse::<u32>().ok(),
"DATE" => tags.year = value.parse::<i32>().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),
_ => (), _ => (),
} }
} }
@ -219,6 +264,10 @@ fn read_opus(path: &Path) -> Result<SongTags> {
track_number: None, track_number: None,
year: None, year: None,
has_artwork: false, 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 {
@ -231,6 +280,10 @@ fn read_opus(path: &Path) -> Result<SongTags> {
"TRACKNUMBER" => tags.track_number = value.parse::<u32>().ok(), "TRACKNUMBER" => tags.track_number = value.parse::<u32>().ok(),
"DISCNUMBER" => tags.disc_number = value.parse::<u32>().ok(), "DISCNUMBER" => tags.disc_number = value.parse::<u32>().ok(),
"DATE" => tags.year = value.parse::<i32>().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),
_ => (), _ => (),
} }
} }
@ -267,22 +320,31 @@ fn read_flac(path: &Path) -> Result<SongTags> {
track_number: vorbis.track(), track_number: vorbis.track(),
year, year,
has_artwork, 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()),
}) })
} }
fn read_mp4(path: &Path) -> Result<SongTags> { fn read_mp4(path: &Path) -> Result<SongTags> {
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");
Ok(SongTags { Ok(SongTags {
artist: tag.take_artist(), artist: tag.take_artist(),
album_artist: tag.take_album_artist(), album_artist: tag.take_album_artist(),
album: tag.take_album(), album: tag.take_album(),
title: tag.take_title(), title: tag.take_title(),
duration: tag.duration().map(|v| v as u32), duration: tag.duration().map(|v| v.as_secs() as u32),
disc_number: tag.disc_number().map(|d| d as u32), disc_number: tag.disc_number().map(|d| d as u32),
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(),
composer: tag.take_composer(),
genre: tag.take_genre(),
label: tag.take_string(&label_ident).next(),
}) })
} }
@ -298,6 +360,10 @@ fn reads_file_metadata() {
duration: None, duration: None,
year: Some(2016), year: Some(2016),
has_artwork: false, has_artwork: false,
lyricist: Some("TEST LYRICIST".into()),
composer: Some("TEST COMPOSER".into()),
genre: Some("TEST GENRE".into()),
label: Some("TEST LABEL".into()),
}; };
let flac_sample_tag = SongTags { let flac_sample_tag = SongTags {
duration: Some(0), duration: Some(0),

View file

@ -27,6 +27,10 @@ pub struct Song {
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 composer: Option<String>,
pub genre: Option<String>,
pub label: Option<String>,
} }
impl Song { impl Song {

View file

@ -88,6 +88,10 @@ impl Collector {
album: tags.album, album: tags.album,
year: tags.year, year: tags.year,
artwork: artwork_path, artwork: artwork_path,
lyricist: tags.lyricist,
composer: tags.composer,
genre: tags.genre,
label: tags.label,
})) { })) {
error!("Error while sending song from collector: {}", e); error!("Error while sending song from collector: {}", e);
} }

View file

@ -22,6 +22,10 @@ pub struct Song {
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 composer: Option<String>,
pub genre: Option<String>,
pub label: Option<String>,
} }
#[derive(Debug, Insertable)] #[derive(Debug, Insertable)]

View file

@ -164,7 +164,7 @@ impl Manager {
// Select songs. Not using Diesel because we need to LEFT JOIN using a custom column // Select songs. Not using Diesel because we need to LEFT JOIN using a custom column
let query = diesel::sql_query( let query = diesel::sql_query(
r#" r#"
SELECT s.id, s.path, s.parent, s.track_number, s.disc_number, s.title, s.artist, s.album_artist, s.year, s.album, s.artwork, s.duration SELECT s.id, s.path, s.parent, s.track_number, s.disc_number, s.title, s.artist, s.album_artist, s.year, s.album, s.artwork, s.duration, s.lyricist, s.composer, s.genre, s.label
FROM playlist_songs ps FROM playlist_songs ps
LEFT JOIN songs s ON ps.path = s.path LEFT JOIN songs s ON ps.path = s.path
WHERE ps.playlist = ? WHERE ps.playlist = ?

View file

@ -68,6 +68,10 @@ table! {
album -> Nullable<Text>, album -> Nullable<Text>,
artwork -> Nullable<Text>, artwork -> Nullable<Text>,
duration -> Nullable<Integer>, duration -> Nullable<Integer>,
lyricist -> Nullable<Text>,
composer -> Nullable<Text>,
genre -> Nullable<Text>,
label -> Nullable<Text>,
} }
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.