Lofty

repository·main·Indexed 43 days ago

https://github.com/serial-ata/lofty-rs

A Rust-based toolkit for parsing, converting, and writing metadata across a diverse range of audio file formats and metadata standards.

Tokens
23.4K
Snippets
86
Records
99
Agent score
56%

What's inside Lofty

  1. Overview of Lofty

    main
    Lofty is a Rust library designed to parse, convert, and write metadata to various audio formats. It supports a wide range of file formats and metadata standards, allowing developers to manipulate audio tags programmatically.
  2. Understand the benchmark categories in Lofty

    main

    Lofty provides two distinct categories of benchmarks to measure performance:

    1. File parsing: Measures the time taken to parse various supported file formats using representative assets. These assets use the format's "primary tag" type and include fields such as Title, Artist, Album, Date, Track number, Genre, Picture (Front cover), and Encoder.
    2. Tag creation: Measures the time taken to create tags and dump them to a writer. Note that this category does not include the time required to write the tags to a physical file.
  3. Implement SplitTag and MergeTag for Tag conversion

    main

    Converting a concrete tag to a generic Tag and back requires implementing SplitTag and MergeTag.

    1. SplitTag: Takes the concrete tag and extracts all items that can be expressed as generic ItemKeys into a Tag. The remaining non-generic items are stored in a Remainder type (typically a tuple struct wrapper around the original tag).
    2. MergeTag: Takes the Remainder and a generic Tag and merges them back into the concrete tag format.

    Once these are implemented, you can provide From<FooTag> for Tag and From<Tag> for FooTag implementations using the split/merge logic.

    // Example Remainder pattern
    #[derive(Debug, Clone, Default)]
    pub struct SplitTagRemainder(FooTag);
    
    impl SplitTag for FooTag {
        type Remainder = SplitTagRemainder;
    
        fn split_tag(mut self) -> (Self::Remainder, Tag) {
            let mut tag = Tag::new(TagType::Foo);
            // ... logic to move items into generic Tag
            (SplitTagRemainder(self), tag)
        }
    }
    
    impl MergeTag for SplitTagRemainder {
        type Merged = FooTag;
    
        fn merge_tag(self, mut tag: Tag) -> Self::Merged {
            let Self(mut merged) = self;
            // ... logic to move items from Tag back to concrete struct
            merged
        }
    }
  4. Register a new FileType variant

    main

    To make Lofty aware of your new format, you must update the FileType enum in src/file/file_type.rs with three specific pieces of information:

    1. Add the variant: Add your format to the FileType enum.
    2. Specify the primary tag type: Update primary_tag_type() to return the preferred TagType for your format.
    3. Map the extension: Update from_ext() to map your file extension (e.g., "foo") to your new variant.
    4. Identify the magic signature: Update quick_type_guess() to check the file buffer for your format's magic bytes.

    Finally, add the name of your format to the LOFTY_FILE_TYPES constant in lofty_attr/src/internal.rs to enable attribute macro support.

    // 1. Add variant
    pub enum FileType {
        Foo,
        // ...
    }
    
    // 2. Primary tag type
    pub fn primary_tag_type(&self) -> TagType {
        match self {
            FileType::Foo => TagType::Id3v2,
            // ...
        }
    }
    
    // 3. Extension mapping
    pub fn from_ext<E>(ext: E) -> Option<Self> {
        match ext.as_str() {
            "foo" => Some(Self::Foo),
            // ...
        }
    }
    
    // 4. Magic signature
    fn quick_type_guess(buf: &[u8]) -> Option<Self> {
        match buf[0] {
            0x0F if buf.starts_with(0x0F00) => Some(Self::Foo),
            // ...
        }
    }
    
    // 5. Register in lofty_attr
    const LOFTY_FILE_TYPES: [&str; N] = [
        "Foo",
        // ...
    ];
  5. Map concrete keys to generic ItemKeys

    main

    To allow conversion between your concrete tag and the generic Tag type, you must define how your format's string keys map to Lofty's ItemKey enum. Use the gen_map! macro to create a mapping, then register it using the gen_item_keys! macro in src/tag/item.rs.

    // 1. Define the mapping
    gen_map!(
        FOO_MAP;
    
        "TITLE" => TrackTitle,
        "ARIST" => TrackArtist,
        "ALBUM" => AlbumTitle,
    );
    
    // 2. Register the map in gen_item_keys!
    gen_item_keys!(
        MAPS => [
            // ...
            [TagType::Foo, FOO_MAP]
        ];
        // ...
    );
  6. Define a Tag struct with metadata

    main

    Define your concrete tag struct in src/your_tag/tag/mod.rs. Use the #[tag] attribute macro from lofty_attr to specify the tag's description and supported FileTypes. This macro automatically generates SUPPORTED_FORMATS and READ_ONLY_FORMATS constants and user-facing documentation.

    To specify that a format is read-only (meaning the tag can be read or removed, but not written as a new tag), use the read_only(FileType) syntax within supported_formats.

    use lofty_attr::tag;
    
    // Standard support
    #[tag(description = "A Foo tag", supported_formats(Foo))]
    pub struct FooTag {
        items: Vec<(String, String)>
    }
    
    // Support with read-only constraints
    #[tag(description = "A Foo tag", supported_formats(Foo, read_only(Mpeg)))]
    pub struct FooTag {
        items: Vec<(String, String)>
    }
  7. How to structure a new tag format directory

    main

    When adding a new tag format, the directory structure depends on whether the tag is specific to one audio format or generic across multiple formats.

    1. Format-specific tags: If the tag is only used by a specific audio format (e.g., the 'Foo' format), place the definition in a subdirectory of that format's directory.
    2. Generic tags: If the tag is supported by multiple audio formats (e.g., ID3), define it in its own folder directly inside src/.

    Every tag implementation requires the following files:

    • mod.rs: Contains the tag struct definition and module exports.
    • read.rs: Contains the logic for reading the tag from a file.
    • write.rs: Contains the logic for writing the tag to supported formats.
    src/
    └── foo/
        └── tag/
            ├── mod.rs
            ├── read.rs
            └── write.rs
  8. Testing a new file format

    main

    When adding a new format, implement the following testing layers:

    1. Unit Tests

    Mandatory unit tests for property reading must be stored in src/properties.rs.

    2. Integration Tests

    Create a comprehensive test file in tests/files/{format}.rs. Use a sample file containing all supported tags located at tests/files/assets/minimal/full_test.{ext}.

    Recommended test suite:

    • read(): Verify all expected information is present using crate::verify_artist().
    • write(): Verify tag modification using crate::set_artist().
    • remove_{tag}(): Verify tag removal using crate::remove_tag_test() for each supported tag type (e.g., remove_ape(), remove_id3v2()).

    3. Fuzz Tests

    Define fuzz targets in fuzz/fuzz_targets/{format}file_read_from to test the robustness of the read_from function.

    // Example Fuzz Target
    #![no_main]
    
    use std::io::Cursor;
    use libfuzzer_sys::fuzz_target;
    use lofty::config::ParseOptions;
    use lofty::file::AudioFile;
    
    fuzz_target!(|data: Vec<u8>| {
        let _ = lofty::foo::FooFile::read_from(
            &mut Cursor::new(data),
            ParseOptions::new().read_properties(false),
        );
    });
  9. Test a new Tag format

    main

    When adding a new tag, follow these testing requirements:

    1. Assets

    Place binary test assets in tests/tags/assets/. Name them test.{ext} where {ext} is the tag name (e.g., test.foo). Assets should contain a representative set of tags (Title, Artist, Album, etc.).

    2. Unit Tests

    Implement at least four unit tests in your tag's read module:

    • parse_{tag}: Verifies reading the asset matches a manually constructed tag.
    • {tag}_re_read: Verifies a round-trip (Read $\rightarrow$ Write $\rightarrow$ Read) using TagExt::dump_to().
    • {tag}_to_tag: Verifies conversion to a generic Tag using verify_tag().
    • tag_to_{tag}: Verifies conversion from a generic Tag back to the concrete type using create_tag().

    3. Integration Tests

    If necessary, place integration tests in tests/tags/.

  10. How to submit a feature request

    main

    Feature requests should be categorized into one of two types based on their scope:

    Miscellaneous Feature Requests

    Use the [MISC] Feature Request template for features that:

    • Have no public API.
    • Are not fully fleshed out.
    • Do not have critical implementation details.

    API Feature Requests

    Use the [API] Feature Request template if you have a specific design in mind for how the feature should be used. This template is for proposing an API design, not an implementation. Provide a code sketch of the intended interface.

    pub struct MyTag {
        some_field: ty,
        some_other_field: ty,
        // ... omit other fields if extending an existing type
    }
    
    impl MyTag {
        pub fn foo(&self) -> String;
    }
  11. Implement the Accessor trait for a Tag

    main

    The Accessor trait provides the primary interface for interacting with tag items (like title, artist, or album). You must implement the getters, setters, and removers for the specific items your tag supports.

    Implementation Note: If your tag format allows keys to appear multiple times, implement both insert (which replaces existing occurrences) and push (which appends to the list).

    impl Accessor for FooTag {
        fn title(&self) -> Option<Cow<'_, str>> { /* implementation */ }
        fn set_title(&mut self, value: String) { /* implementation */ }
        fn remove_title(&mut self) { /* implementation */ }
    
        // ... repeat for artist, album, etc.
    }