Rust音频元数据处理库moosicbox_lofty的使用,支持高效读取和编辑MP3、FLAC等格式的ID3标签

Rust音频元数据处理库moosicbox_lofty的使用

moosicbox_lofty是一个用于解析、转换和写入各种音频格式元数据的Rust库,支持高效读取和编辑MP3、FLAC等格式的ID3标签。

特性

  • 支持多种音频格式的元数据处理
  • 提供读取、写入和删除元数据的功能
  • 高性能的元数据处理能力

安装

在Cargo.toml中添加依赖:

moosicbox_lofty = "0.1.0"

示例代码

读取音频标签

use moosicbox_lofty::read_from_path;

fn main() {
    // 读取音频文件元数据
    let tagged_file = read_from_path("path/to/audio.mp3").unwrap();
    
    // 获取主标签
    if let Some(tag) = tagged_file.primary_tag() {
        println!("标题: {:?}", tag.title());
        println!("艺术家: {:?}", tag.artist());
        println!("专辑: {:?}", tag.album());
    }
    
    // 获取所有标签
    for tag in tagged_file.tags() {
        println!("标签类型: {:?}", tag.tag_type());
        println!("标题: {:?}", tag.title());
        println!("艺术家: {:?}", tag.artist());
    }
}

写入音频标签

use moosicbox_lofty::{Tag, TagType, ItemKey};

fn main() {
    // 创建一个新标签
    let mut tag = Tag::new(TangType::Id3v2);
    
    // 设置基本元数据
    tag.insert_text(ItemKey::TrackTitle, "歌曲标题");
    tag.insert_text(ItemKey::TrackArtist, "艺术家");
    tag.insert_text(ItemKey::AlbumTitle, "专辑名称");
    
    // 设置自定义字段
    tag.insert_text(ItemKey::Custom("MY_CUSTOM_FIELD".to_string()), "自定义值");
    
    // 保存到文件
    tag.save_to_path("path/to/audio.mp3").unwrap();
}

删除音频标签

use moosicbox_lofty::{read_from_path, TagType};

fn main() {
    // 读取文件
    let mut tagged_file = read_from_path("path/to/audio.mp3").unwrap();
    
    // 删除特定类型的标签
    tagged_file.remove(TagType::Id3v1);
    
    // 保存更改
    tagged_file.save_to_path("path/to/audio.mp3").unwrap();
}

支持的格式

moosicbox_lofty支持多种音频格式的元数据处理,包括但不限于:

  • MP3 (ID3v1, ID3v2)
  • FLAC
  • OGG
  • WAV
  • AIFF
  • MP4
  • WMA

许可证

moosicbox_lofty采用双重许可:

  • Apache License, Version 2.0
  • MIT license

您可以根据需要选择其中一种许可证。

完整示例代码

下面是一个完整的音频元数据处理示例,包含读取、修改和保存元数据的完整流程:

use moosicbox_lofty::{read_from_path, Tag, TagType, ItemKey};

fn main() {
    // 1. 读取现有音频文件元数据
    let audio_path = "test.mp3";
    let mut tagged_file = read_from_path(audio_path).unwrap();
    
    println!("=== 原始标签信息 ===");
    if let Some(tag) = tagged_file.primary_tag() {
        println!("标题: {:?}", tag.title());
        println!("艺术家: {:?}", tag.artist());
        println!("专辑: {:?}", tag.album());
    }
    
    // 2. 创建新标签并添加元数据
    let mut new_tag = Tag::new(TagType::Id3v2);
    new_tag.insert_text(ItemKey::TrackTitle, "新歌曲标题");
    new_tag.insert_text(ItemKey::TrackArtist, "新艺术家");
    new_tag.insert_text(ItemKey::AlbumTitle, "新专辑");
    new_tag.insert_text(ItemKey::TrackNumber, "1");
    
    // 3. 替换原有标签
    tagged_file.insert_tag(new_tag);
    
    // 4. 删除不需要的旧标签
    tagged_file.remove(TagType::Id3v1);
    
    // 5. 保存修改后的文件
    tagged_file.save_to_path(audio_path).unwrap();
    println!("元数据已成功更新并保存");
    
    // 6. 验证修改结果
    let updated_file = read_from_path(audio_path).unwrap();
    println!("\n=== 更新后的标签信息 ===");
    if let Some(tag) = updated_file.primary_tag() {
        println!("标题: {}", tag.title().unwrap_or("无"));
        println!("艺术家: {}", tag.artist().unwrap_or("无"));
        println!("专辑: {}", tag.album().unwrap_or("无"));
    }
}

这个完整示例展示了:

  1. 如何读取现有音频文件的元数据
  2. 如何创建新的标签并添加各种元数据
  3. 如何替换和删除标签
  4. 如何保存修改后的文件
  5. 如何验证修改结果

您可以根据实际需求调整和扩展这个示例代码。


1 回复

moosicbox_lofty - Rust音频元数据处理库

概述

moosicbox_lofty是一个用于处理音频元数据的Rust库,支持高效读取和编辑多种音频格式的ID3标签和其他元数据。它特别适合需要批量处理音频文件元数据的应用程序开发。

支持格式

  • MP3 (ID3v1, ID3v2)
  • FLAC (Vorbis注释)
  • WAV
  • AIFF
  • MP4 (M4A, ALAC)
  • OGG
  • OPUS
  • WMA
  • APE (Monkey’s Audio)

基本用法

首先在Cargo.toml中添加依赖:

[dependencies]
moosicbox_lofty = "0.10"

示例代码

1. 读取音频文件元数据

use moosicbox_lofty::{read_from_path, TaggedFileExt};

fn main() {
    // 读取音频文件
    let tagged_file = read_from_path("song.mp3").unwrap();
    
    // 获取主标签(通常是ID3标签)
    let tag = tagged_file.primary_tag().unwrap();
    
    // 读取元数据
    println!("标题: {:?}", tag.title());
    println!("艺术家: {:?}", tag.artist());
    println!("专辑: {:?}", tag.album());
    println!("年份: {:?}", tag.year());
    println!("音轨号: {:?}", tag.track());
    println!("流派: {:?}", tag.genre());
}

2. 编辑音频文件元数据

use moosicbox_lofty::{read_from_path, TaggedFileExt, Tag};

fn main() {
    // 读取文件
    let mut tagged_file = read_from_path("song.mp3").unwrap();
    
    // 获取或创建主标签
    let tag = match tagged_file.primary_tag_mut() {
        Some(tag) => tag,
        None => {
            tagged_file.insert_tag(Tag::new(tagged_file.primary_tag_type()));
            tagged_file.primary_tag_mut().unwrap()
        }
    };
    
    // 设置元数据
    tag.set_title("新标题");
    tag.set_artist("新艺术家");
    tag.set_album("新专辑");
    tag.set_year(2023);
    tag.set_track(1);
    tag.set_genre("摇滚");
    
    // 保存更改
    tag.save_to_path("song.mp3").unwrap();
}

3. 批量处理音频文件

use std::path::Path;
use moosicbox_lofty::{read_from_path, TaggedFileExt};

fn process_directory(dir: &Path) {
    if dir.is_dir() {
        for entry in dir.read_dir().unwrap() {
            let entry = entry.unwrap();
            let path = entry.path();
            
            if path.is_file() {
                if let Ok(tagged_file) = read_from_path(&path) {
                    if let Some(tag) = tagged_file.primary_tag() {
                        println!("处理文件: {:?}", path);
                        println!("标题: {:?}", tag.title());
                        // 这里可以添加更多的处理逻辑
                    }
                }
            }
        }
    }
}

fn main() {
    process_directory(Path::new("music_directory"));
}

高级功能

1. 处理图片/封面艺术

use moosicbox_lofty::{read_from_path, TaggedFileExt, Picture};

// 添加封面图片
fn add_cover_art(file_path: &str, image_path: &str) {
    let mut tagged_file = read_from_path(file_path).unwrap();
    let tag = tagged_file.primary_tag_mut().unwrap();
    
    let picture = Picture::from_path(image_path).unwrap();
    tag.push_picture(picture);
    
    tag.save_to_path(file_path).unwrap();
}

// 读取封面图片
fn read_cover_art(file_path: &str) {
    let tagged_file = read_from_path(file_path).unwrap();
    let tag = tagged_file.primary_tag().unwrap();
    
    for (i, picture) in tag.pictures().enumerate() {
        println!("图片 {}: {:?}x{:?}, {:?}字节", 
            i, picture.width(), picture.height(), picture.data().len());
        // 可以将图片数据保存到文件
    }
}

2. 处理自定义帧/字段

use moosicbox_lofty::{read_from_path, TaggedFileExt};

fn handle_custom_frames(file_path: &str) {
    let mut tagged_file = read_from_path(file_path).unwrap();
    let tag = tagged_file.primary_tag_mut().unwrap();
    
    // 添加自定义文本帧
    tag.insert_text_frame("TXXX", "自定义字段", "自定义值");
    
    // 读取自定义帧
    if let Some(value) = tag.get_text("TXXX") {
        println!("自定义字段值: {}", value);
    }
    
    tag.save_to_path(file_path).unwrap();
}

性能提示

  1. 对于批量处理,考虑使用并行处理:
use rayon::prelude::*;
use std::path::Path;

fn process_files_parallel(dir: &Path) {
    let files: Vec<_> = dir.read_dir().unwrap()
        .filter_map(|e| e.ok())
        .map(|e| e.path())
        .collect();
    
    files.par_iter().for_each(|path| {
        if let Ok(tagged_file) = read_from_path(path) {
            // 处理每个文件
        }
    });
}
  1. 对于只读操作,使用read_from而不是read_from_path可以避免文件锁定问题。

注意事项

  1. 修改文件前最好先备份,以防意外损坏
  2. 某些音频格式对元数据的支持有限
  3. 处理大量文件时注意内存使用

moosicbox_lofty提供了强大而灵活的音频元数据处理能力,适合从简单的元数据读写到复杂的音频文件管理应用。

完整示例代码

以下是一个完整的音频元数据管理工具示例,结合了读取、编辑和批量处理功能:

use moosicbox_lofty::{read_from_path, TaggedFileExt, Tag, Picture};
use std::path::{Path, PathBuf};
use std::io::{self, Write};
use rayon::prelude::*;

fn main() {
    println!("音频元数据管理工具");
    
    loop {
        println!("\n请选择操作:");
        println!("1. 查看音频文件元数据");
        println!("2. 编辑音频文件元数据");
        println!("3. 批量处理目录中的音频文件");
        println!("4. 添加/查看封面图片");
        println!("5. 退出");
        
        print!("> ");
        io::stdout().flush().unwrap();
        
        let mut choice = String::new();
        io::stdin().read_line(&mut choice).unwrap();
        
        match choice.trim() {
            "1" => view_metadata(),
            "2" => edit_metadata(),
            "3" => batch_process(),
            "4" => handle_cover_art(),
            "5" => break,
            _ => println!("无效选择"),
        }
    }
}

fn view_metadata() {
    let path = input_path("请输入音频文件路径: ");
    
    match read_from_path(&path) {
        Ok(tagged_file) => {
            if let Some(tag) = tagged_file.primary_tag() {
                println!("\n=== 元数据 ===");
                println!("标题: {:?}", tag.title());
                println!("艺术家: {:?}", tag.artist());
                println!("专辑: {:?}", tag.album());
                println!("年份: {:?}", tag.year());
                println!("音轨号: {:?}", tag.track());
                println!("流派: {:?}", tag.genre());
                
                // 显示自定义帧
                println!("\n自定义帧:");
                for frame in tag.frames() {
                    println!("{}: {:?}", frame.id(), frame.content());
                }
            } else {
                println!("文件没有元数据标签");
            }
        }
        Err(e) => println!("读取文件失败: {}", e),
    }
}

fn edit_metadata() {
    let path = input_path("请输入音频文件路径: ");
    
    let mut tagged_file = match read_from_path(&path) {
        Ok(f) => f,
        Err(e) => {
            println!("读取文件失败: {}", e);
            return;
        }
    };
    
    let tag = match tagged_file.primary_tag_mut() {
        Some(t) => t,
        None => {
            println!("创建新标签");
            tagged_file.insert_tag(Tag::new(tagged_file.primary_tag_type()));
            tagged_file.primary_tag_mut().unwrap()
        }
    };
    
    println!("\n当前元数据:");
    println!("1. 标题: {:?}", tag.title());
    println!("2. 艺术家: {:?}", tag.artist());
    println!("3. 专辑: {:?}", tag.album());
    println!("4. 年份: {:?}", tag.year());
    println!("5. 音轨号: {:?}", tag.track());
    println!("6. 流派: {:?}", tag.genre());
    
    loop {
        print!("\n选择要编辑的字段(1-6)或输入's'保存并退出,'q'不保存退出: ");
        io::stdout().flush().unwrap();
        
        let mut choice = String::new();
        io::stdin().read_line(&mut choice).unwrap();
        let choice = choice.trim();
        
        match choice {
            "1" => {
                let title = input_string("输入新标题: ");
                tag.set_title(title);
            }
            "2" => {
                let artist = input_string("输入新艺术家: ");
                tag.set_artist(artist);
            }
            "3" => {
                let album = input_string("输入新专辑: ");
                tag.set_album(album);
            }
            "4" => {
                if let Ok(year) = input_string("输入新年份: ").parse::<u32>() {
                    tag.set_year(year);
                } else {
                    println!("无效的年份");
                }
            }
            "5" => {
                if let Ok(track) = input_string("输入新音轨号: ").parse::<u32>() {
                    tag.set_track(track);
                } else {
                    println!("无效的音轨号");
                }
            }
            "6" => {
                let genre = input_string("输入新流派: ");
                tag.set_genre(genre);
            }
            "s" => {
                if let Err(e) = tag.save_to_path(&path) {
                    println!("保存失败: {}", e);
                } else {
                    println!("保存成功");
                }
                break;
            }
            "q" => break,
            _ => println!("无效选择"),
        }
    }
}

fn batch_process() {
    let dir_path = input_path("请输入目录路径: ");
    let dir = Path::new(&dir_path);
    
    if !dir.is_dir() {
        println!("路径不是目录");
        return;
    }
    
    let files: Vec<PathBuf> = dir.read_dir()
        .unwrap()
        .filter_map(|e| e.ok())
        .map(|e| e.path())
        .collect();
    
    println!("\n找到 {} 个文件", files.len());
    
    // 并行处理
    files.par_iter().for_each(|path| {
        if let Ok(tagged_file) = read_from_path(path) {
            if let Some(tag) = tagged_file.primary_tag() {
                println!("处理文件: {:?}", path.file_name().unwrap());
                println!("  标题: {:?}", tag.title());
                println!("  艺术家: {:?}", tag.artist());
                // 这里可以添加批量处理逻辑
            }
        }
    });
}

fn handle_cover_art() {
    println!("\n封面图片操作:");
    println!("1. 添加封面图片");
    println!("2. 查看封面图片信息");
    println!("3. 提取封面图片");
    
    let mut choice = String::new();
    io::stdin().read_line(&mut choice).unwrap();
    
    match choice.trim() {
        "1" => {
            let audio_path = input_path("请输入音频文件路径: ");
            let image_path = input_path("请输入图片文件路径: ");
            
            let mut tagged_file = match read_from_path(&audio_path) {
                Ok(f) => f,
                Err(e) => {
                    println!("读取音频文件失败: {}", e);
                    return;
                }
            };
            
            let tag = match tagged_file.primary_tag_mut() {
                Some(t) => t,
                None => {
                    println!("创建新标签");
                    tagged_file.insert_tag(Tag::new(tagged_file.primary_tag_type()));
                    tagged_file.primary_tag_mut().unwrap()
                }
            };
            
            match Picture::from_path(&image_path) {
                Ok(picture) => {
                    tag.push_picture(picture);
                    if let Err(e) = tag.save_to_path(&audio_path) {
                        println!("保存失败: {}", e);
                    } else {
                        println!("封面图片添加成功");
                    }
                }
                Err(e) => println!("读取图片失败: {}", e),
            }
        }
        "2" => {
            let audio_path = input_path("请输入音频文件路径: ");
            
            let tagged_file = match read_from_path(&audio_path) {
                Ok(f) => f,
                Err(e) => {
                    println!("读取文件失败: {}", e);
                    return;
                }
            };
            
            if let Some(tag) = tagged_file.primary_tag() {
                for (i, picture) in tag.pictures().enumerate() {
                    println!("图片 {}:", i + 1);
                    println!("  类型: {:?}", picture.pic_type());
                    println!("  尺寸: {}x{}", picture.width(), picture.height());
                    println!("  大小: {} 字节", picture.data().len());
                    println!("  MIME类型: {:?}", picture.mime_type());
                }
            } else {
                println!("文件没有元数据标签");
            }
        }
        "3" => {
            let audio_path = input_path("请输入音频文件路径: ");
            let output_dir = input_path("请输入输出目录: ");
            
            let tagged_file = match read_from_path(&audio_path) {
                Ok(f) => f,
                Err(e) => {
                    println!("读取文件失败: {}", e);
                    return;
                }
            };
            
            if let Some(tag) = tagged_file.primary_tag() {
                for (i, picture) in tag.pictures().enumerate() {
                    let output_path = Path::new(&output_dir)
                        .join(format!("cover_{}.{}", i + 1, picture.extension()));
                    
                    if let Err(e) = std::fs::write(&output_path, picture.data()) {
                        println!("保存图片 {} 失败: {}", i + 1, e);
                    } else {
                        println!("图片 {} 保存到: {:?}", i + 1, output_path);
                    }
                }
            } else {
                println!("文件没有元数据标签");
            }
        }
        _ => println!("无效选择"),
    }
}

fn input_path(prompt: &str) -> String {
    print!("{}", prompt);
    io::stdout().flush().unwrap();
    let mut input = String::new();
    io::stdin().read_line(&mut input).unwrap();
    input.trim().to_string()
}

fn input_string(prompt: &str) -> String {
    print!("{}", prompt);
    io::stdout().flush().unwrap();
    let mut input = String::new();
    io::stdin().read_line(&mut input).unwrap();
    input.trim().to_string()
}

这个完整示例提供了以下功能:

  1. 查看音频文件元数据
  2. 编辑音频文件元数据
  3. 批量处理目录中的音频文件
  4. 添加/查看/提取封面图片

使用方法:

  1. 将代码保存为main.rs
  2. 在Cargo.toml中添加moosicbox_lofty和rayon依赖
  3. 运行程序并按提示操作
回到顶部