Serial-ATA / lofty-rs

Audio metadata library
Apache License 2.0
176 stars 34 forks source link

Can't read lyrics tag when description is set #383

Closed g-fb closed 2 months ago

g-fb commented 2 months ago

Reproducer

I tried this code:

use lofty::prelude::ItemKey;
use lofty::probe::Probe;
use lofty::file::TaggedFileExt;

fn main() {
    let tagged_file = Probe::open("/path/to/file.mp3")
        .expect("ERROR: Failed to open file")
        .read();
    let tagged_file = tagged_file.unwrap();
    let tags = tagged_file.tags();
    for tag in tags {
        let lyrics = tag.get_string(&ItemKey::Lyrics).unwrap_or_default();
        println!("{lyrics}");
    }
}

Summary

Can't read lyrics tag when description is set.

Expected behavior

println!("{lyrics}"); prints "the lyrics"

Assets

No response

Serial-ATA commented 2 months ago

In the Id3v2Tag -> Tag conversion, frames with descriptions are intentionally left behind, as Tag has no way of retaining that information:

https://github.com/Serial-ATA/lofty-rs/blob/f17bac49b3f3e7757784c36de8220d2581a19d7a/lofty/src/id3/v2/tag.rs#L1173-L1188

When dealing with tags using anything outside of basic text storage, it's best to use the concrete tags (Id3v2Tag) in this case.

Hopefully in the future there will be a way to preserve more of this information in a generic way, but right now Tag is pretty limited.

g-fb commented 2 months ago

So I should be able to get the lyrics with a description if I use Id3v2Tag?

If yes, then I don't understand how. I tried like this:

let mut tagged_file = lofty::read_from_path(file.path.clone()).unwrap();
let tag = tagged_file.tag_mut(TagType::Id3v2);
match tag {
    Some(tag) => {
        let tag: Id3v2Tag = <Tag as Clone>::clone(&(*tag)).into();
        const LYRICS_ID: FrameId<'_> = FrameId::Valid(Cow::Borrowed("USLT"));
        let lyrics = tag.get_text(&LYRICS_ID);
        println!("file: {}\n{lyrics:#?}", file.path.clone());
    }
    None => ()
}

But I still can't get the ones with a description.

PS: I suck at rust.

Ferry-200 commented 2 months ago

The lyric is stored in ID3V2's USLT frame. I think you should try this code.

let mp3_file = match MpegFile::read_from(&mut file, ParseOptions::new()) {
        Ok(value) => value,
        Err(_) => return None,
    };

let id3v2 = mp3_file.id3v2()?;
let frame = id3v2.get(&FrameId::Valid(Cow::Borrowed("USLT")))?;
    match frame.content() {
        lofty::id3::v2::FrameValue::UnsynchronizedText(lyric_frame) => {
            return Some(lyric_frame.content.clone());
        }
        _ => return None,
    }

I find that these two ways are the same, so please ignore it. 🥲

Serial-ATA commented 2 months ago

But I still can't get the ones with a description.

So a few things:

The solution by @Ferry-200 is correct. If you read an MpegFile directly, you get access to the Id3v2Tag. You're currently using read_from_path which will give you a TaggedFile, and thus you only get access to Tag.

g-fb commented 2 months ago

I got it working.

Thank you both for the help.

Here's the working code

fn get_id3v2_lyrics(path: String) -> Result<String, String> {

    let mp3_file = match MpegFile::read_from(&mut File::open(path.clone()).unwrap(), ParseOptions::new()) {
        Ok(value) => value,
        _ => return Err(format!("Can't read mpeg file: {path}")),
    };

    let id3v2 = match mp3_file.id3v2() {
        Some(value) => value,
        _ => return Err(format!("mpeg file doesn't have an id3v2 tag: {path}")),
    };

    let frame = match Id3v2Tag::get(&id3v2, &FrameId::Valid(Cow::Borrowed("USLT"))) {
        Some(value) => value,
        _ => return Err(format!("mpeg file doesn't have lyrics tag: {path}")),
    };

    let lyrics = match frame.content() {
        FrameValue::UnsynchronizedText(value) => value,
        _ => return Err(format!("Can't get lyrics for mpeg file: {path}")),
    };

    Ok(lyrics.content.clone())

}
Serial-ATA commented 2 months ago

Nice, I'll keep this pinned to remind me to make descriptions and languages available in TagItem somehow.