音乐封面&id3标签编辑器

查看 65|回复 8
作者:cxr666   
今天在https://yym4.com上下载音乐时发现了一个问题:下载的很多歌曲竟然没有专辑封面!!!!因本人有强迫症所以先去网上找了一个可以给mp3加封面的网站,但是还是不行没有用,所以就花一点时间自己用python写了一个音乐封面&id3标签编辑器,应该是吾爱首发,代码如下:
import os
import sys
import tkinter as tk
from tkinter import filedialog, ttk, messagebox
from PIL import Image, ImageTk
import io
import tempfile
from mutagen.id3 import ID3, APIC, TIT2, TPE1, TALB, TCON, COMM, TDRC, ID3NoHeaderError
from mutagen.flac import FLAC, Picture
from mutagen import File
class AudioTagEditor:
    def __init__(self, root):
        self.root = root
        self.root.title("音频标签编辑器")
        self.root.geometry("800x650")
        self.root.resizable(True, True)
        # 设置中文字体支持
        self.style = ttk.Style()
        self.style.configure("TLabel", font=("SimHei", 10))
        self.style.configure("TButton", font=("SimHei", 10))
        self.style.configure("TEntry", font=("SimHei", 10))
        self.style.configure("Text", font=("SimHei", 10))
        # 初始化变量
        self.audio_files = []
        self.current_file = None
        self.cover_path = None
        self.cover_image = None
        # 创建界面
        self.create_widgets()
    def create_widgets(self):
        # 顶部文件选择区域
        file_frame = ttk.Frame(self.root, padding="10")
        file_frame.pack(fill=tk.X)
        ttk.Button(file_frame, text="选择单个文件", command=self.select_single_file).pack(side=tk.LEFT, padx=5)
        ttk.Button(file_frame, text="选择多个文件", command=self.select_multiple_files).pack(side=tk.LEFT, padx=5)
        ttk.Button(file_frame, text="选择文件夹", command=self.select_folder).pack(side=tk.LEFT, padx=5)
        self.file_listbox = tk.Listbox(self.root, height=4, font=("SimHei", 10))
        self.file_listbox.pack(fill=tk.X, padx=10, pady=5)
        self.file_listbox.bind('>', self.on_file_select)
        # 主要内容区域(左右分栏)
        content_frame = ttk.Frame(self.root, padding="10")
        content_frame.pack(fill=tk.BOTH, expand=True)
        # 左侧标签编辑区域
        left_frame = ttk.LabelFrame(content_frame, text="标签信息", padding="10")
        left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 10))
        # 标签输入字段
        fields = [
            ("标题:", "title"),
            ("艺术家:", "artist"),
            ("专辑:", "album"),
            ("流派:", "genre"),
            ("年份:", "year"),
            ("评论:", "comment")
        ]
        self.entries = {}
        row = 0
        for label_text, key in fields:
            ttk.Label(left_frame, text=label_text).grid(row=row, column=0, sticky=tk.W, pady=5)
            if key == "comment":
                # 评论使用多行文本框
                self.entries[key] = tk.Text(left_frame, width=40, height=4)
                self.entries[key].grid(row=row, column=1, sticky=tk.W+tk.E, pady=5)
                # 添加滚动条
                scroll = ttk.Scrollbar(left_frame, command=self.entries[key].yview)
                scroll.grid(row=row, column=2, sticky=tk.N+tk.S)
                self.entries[key]['yscrollcommand'] = scroll.set
            else:
                self.entries[key] = ttk.Entry(left_frame, width=40)
                self.entries[key].grid(row=row, column=1, sticky=tk.W, pady=5)
            row += 1
        # 封面处理区域
        ttk.Label(left_frame, text="专辑封面:").grid(row=row, column=0, sticky=tk.NW, pady=5)
        cover_frame = ttk.Frame(left_frame)
        cover_frame.grid(row=row, column=1, sticky=tk.W, pady=5)
        ttk.Button(cover_frame, text="选择封面", command=self.select_cover).pack(side=tk.TOP, pady=5)
        ttk.Button(cover_frame, text="清除封面", command=self.clear_cover).pack(side=tk.TOP, pady=5)
        row += 1
        # 按钮区域
        button_frame = ttk.Frame(left_frame)
        button_frame.grid(row=row, column=0, columnspan=3, pady=10)
        ttk.Button(button_frame, text="保存当前文件标签", command=self.save_current_tags).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="批量保存所有文件标签", command=self.batch_save_tags).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="从当前文件加载标签", command=self.load_current_tags).pack(side=tk.LEFT, padx=5)
        # 右侧封面预览区域
        right_frame = ttk.LabelFrame(content_frame, text="封面预览", padding="10")
        right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
        self.cover_preview = ttk.Label(right_frame)
        self.cover_preview.pack(fill=tk.BOTH, expand=True)
        # 底部状态栏
        self.status_var = tk.StringVar()
        self.status_var.set("就绪")
        status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
        status_bar.pack(side=tk.BOTTOM, fill=tk.X)
    def select_single_file(self):
        files = filedialog.askopenfilenames(
            filetypes=[("音频文件", "*.mp3;*.flac"), ("所有文件", "*.*")]
        )
        if files:
            self.audio_files = list(files)
            self.update_file_list()
            self.status_var.set(f"已选择 {len(self.audio_files)} 个文件")
    def select_multiple_files(self):
        self.select_single_file()  # 功能相同,只是按钮文字不同
    def select_folder(self):
        folder = filedialog.askdirectory()
        if folder:
            audio_extensions = ('.mp3', '.flac')
            self.audio_files = [
                os.path.join(folder, f)
                for f in os.listdir(folder)
                if f.lower().endswith(audio_extensions)
            ]
            self.update_file_list()
            self.status_var.set(f"已选择文件夹,包含 {len(self.audio_files)} 个音频文件")
    def update_file_list(self):
        self.file_listbox.delete(0, tk.END)
        for file in self.audio_files:
            self.file_listbox.insert(tk.END, os.path.basename(file))
    def on_file_select(self, event):
        selection = self.file_listbox.curselection()
        if selection:
            index = selection[0]
            self.current_file = self.audio_files[index]
            self.load_current_tags()
            self.status_var.set(f"已选择: {os.path.basename(self.current_file)}")
    def select_cover(self):
        cover = filedialog.askopenfilename(
            filetypes=[("图片文件", "*.jpg;*.jpeg;*.png"), ("所有文件", "*.*")]
        )
        if cover:
            self.cover_path = cover
            self.update_cover_preview(cover)
    def update_cover_preview(self, image_path):
        try:
            img = Image.open(image_path)
            img.thumbnail((300, 300))  # 限制预览大小
            self.cover_image = ImageTk.PhotoImage(img)
            self.cover_preview.config(image=self.cover_image)
        except Exception as e:
            messagebox.showerror("错误", f"无法预览图片: {str(e)}")
            self.cover_path = None
    def clear_cover(self):
        self.cover_path = None
        self.cover_preview.config(image="")
        self.cover_image = None
    def load_current_tags(self):
        if not self.current_file:
            return
        # 清空现有输入
        for key in self.entries:
            if key == "comment":
                self.entries[key].delete(1.0, tk.END)
            else:
                self.entries[key].delete(0, tk.END)
        # 清除封面预览
        self.clear_cover()
        try:
            audio = File(self.current_file)
            # 根据文件类型加载标签
            if self.current_file.lower().endswith('.mp3'):
                self._load_mp3_tags(audio)
            elif self.current_file.lower().endswith('.flac'):
                self._load_flac_tags(audio)
        except Exception as e:
            messagebox.showerror("错误", f"加载标签失败: {str(e)}")
    def _load_mp3_tags(self, audio):
        # 尝试获取ID3标签
        try:
            id3 = ID3(self.current_file)
            # 加载文本标签
            if 'TIT2' in id3:
                self.entries['title'].insert(0, id3['TIT2'].text[0])
            if 'TPE1' in id3:
                self.entries['artist'].insert(0, id3['TPE1'].text[0])
            if 'TALB' in id3:
                self.entries['album'].insert(0, id3['TALB'].text[0])
            if 'TCON' in id3:
                self.entries['genre'].insert(0, id3['TCON'].text[0])
            if 'TDRC' in id3:
                self.entries['year'].insert(0, str(id3['TDRC'].text[0]))
            if 'COMM' in id3:
                self.entries['comment'].insert(1.0, id3['COMM'].text[0])
            # 加载封面
            for tag in id3.getall('APIC'):
                try:
                    # 保存临时图片文件用于预览
                    with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as f:
                        f.write(tag.data)
                        temp_path = f.name
                    self.update_cover_preview(temp_path)
                    self.cover_path = temp_path  # 临时文件路径
                except:
                    continue
        except ID3NoHeaderError:
            # 文件没有ID3标签
            pass
    def _load_flac_tags(self, audio):
        # 加载文本标签
        if 'title' in audio:
            self.entries['title'].insert(0, audio['title'][0])
        if 'artist' in audio:
            self.entries['artist'].insert(0, audio['artist'][0])
        if 'album' in audio:
            self.entries['album'].insert(0, audio['album'][0])
        if 'genre' in audio:
            self.entries['genre'].insert(0, audio['genre'][0])
        if 'date' in audio:
            self.entries['year'].insert(0, audio['date'][0])
        if 'comment' in audio:
            self.entries['comment'].insert(1.0, audio['comment'][0])
        # 加载封面
        for picture in audio.pictures:
            try:
                # 保存临时图片文件用于预览
                with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as f:
                    f.write(picture.data)
                    temp_path = f.name
                self.update_cover_preview(temp_path)
                self.cover_path = temp_path  # 临时文件路径
            except:
                continue
    def save_current_tags(self):
        if not self.current_file:
            messagebox.showwarning("警告", "请先选择一个文件")
            return
        # 获取输入的标签信息
        tag_data = {
            'title': self.entries['title'].get(),
            'artist': self.entries['artist'].get(),
            'album': self.entries['album'].get(),
            'genre': self.entries['genre'].get(),
            'year': self.entries['year'].get(),
            'comment': self.entries['comment'].get(1.0, tk.END).strip()
        }
        try:
            if self.current_file.lower().endswith('.mp3'):
                self._save_mp3_tags(self.current_file, tag_data)
            elif self.current_file.lower().endswith('.flac'):
                self._save_flac_tags(self.current_file, tag_data)
            messagebox.showinfo("成功", f"标签已保存到 {os.path.basename(self.current_file)}")
            self.status_var.set(f"已保存: {os.path.basename(self.current_file)}")
        except Exception as e:
            messagebox.showerror("错误", f"保存标签失败: {str(e)}")
    def batch_save_tags(self):
        if not self.audio_files:
            messagebox.showwarning("警告", "请先选择文件")
            return
        # 获取输入的标签信息
        tag_data = {
            'title': self.entries['title'].get(),
            'artist': self.entries['artist'].get(),
            'album': self.entries['album'].get(),
            'genre': self.entries['genre'].get(),
            'year': self.entries['year'].get(),
            'comment': self.entries['comment'].get(1.0, tk.END).strip()
        }
        success_count = 0
        fail_count = 0
        fail_files = []
        for file in self.audio_files:
            try:
                if file.lower().endswith('.mp3'):
                    self._save_mp3_tags(file, tag_data)
                elif file.lower().endswith('.flac'):
                    self._save_flac_tags(file, tag_data)
                success_count += 1
            except:
                fail_count += 1
                fail_files.append(os.path.basename(file))
        msg = f"批量处理完成:\n成功: {success_count} 个文件\n失败: {fail_count} 个文件"
        if fail_files:
            msg += "\n失败文件:\n" + "\n".join(fail_files)
        messagebox.showinfo("批量处理结果", msg)
        self.status_var.set(f"批量处理完成: 成功 {success_count} 个, 失败 {fail_count} 个")
    def _save_mp3_tags(self, file_path, tag_data):
        # 尝试读取现有ID3标签,无标签则创建新ID3v2
        try:
            audio = ID3(file_path)
        except ID3NoHeaderError:
            audio = ID3()  # 新建ID3标签
        # 添加专辑封面(APIC帧)
        if self.cover_path:
            # 清除现有封面
            for tag in audio.getall('APIC'):
                audio.delall('APIC')
            # 确定图片MIME类型
            ext = os.path.splitext(self.cover_path)[1].lower()
            mime = 'image/jpeg' if ext in ('.jpg', '.jpeg') else 'image/png'
            with open(self.cover_path, 'rb') as cover_file:
                cover_data = cover_file.read()
            audio.add(APIC(
                encoding=3,  # 3 = UTF-8
                mime=mime,
                type=3,  # 3 = 封面(Front Cover)
                desc=u'Cover',
                data=cover_data
            ))
        # 添加文本标签(只添加非空字段)
        if tag_data['title']:
            audio['TIT2'] = TIT2(encoding=3, text=tag_data['title'])
        if tag_data['artist']:
            audio['TPE1'] = TPE1(encoding=3, text=tag_data['artist'])
        if tag_data['album']:
            audio['TALB'] = TALB(encoding=3, text=tag_data['album'])
        if tag_data['genre']:
            audio['TCON'] = TCON(encoding=3, text=tag_data['genre'])
        if tag_data['comment']:
            audio['COMM'] = COMM(encoding=3, text=tag_data['comment'])
        if tag_data['year']:
            audio['TDRC'] = TDRC(encoding=3, text=tag_data['year'])
        # 保存标签(ID3v2.3兼容性最佳)
        audio.save(file_path, v2_version=3)
    def _save_flac_tags(self, file_path, tag_data):
        audio = FLAC(file_path)
        # 添加文本标签(只添加非空字段)
        if tag_data['title']:
            audio['title'] = tag_data['title']
        if tag_data['artist']:
            audio['artist'] = tag_data['artist']
        if tag_data['album']:
            audio['album'] = tag_data['album']
        if tag_data['genre']:
            audio['genre'] = tag_data['genre']
        if tag_data['comment']:
            audio['comment'] = tag_data['comment']
        if tag_data['year']:
            audio['date'] = tag_data['year']
        # 添加专辑封面
        if self.cover_path:
            # 清除现有封面
            audio.clear_pictures()
            # 确定图片MIME类型
            ext = os.path.splitext(self.cover_path)[1].lower()
            mime = 'image/jpeg' if ext in ('.jpg', '.jpeg') else 'image/png'
            with open(self.cover_path, 'rb') as cover_file:
                cover_data = cover_file.read()
            picture = Picture()
            picture.data = cover_data
            picture.type = 3  # 3 = 封面(Front Cover)
            picture.mime = mime
            picture.desc = u'Cover'
            audio.add_picture(picture)
        # 保存标签
        audio.save()
if __name__ == "__main__":
    root = tk.Tk()
    app = AudioTagEditor(root)
    root.mainloop()
思路很简单:用Python 处理音频元数据的主流工具(mutagen,支持 MP3、FLAC 等多种格式),来完成配置 ID3v2 标签的各类字段。
安装命令(cmd):pip install mutagen;
打包好的程序:https://wwcq.lanzouu.com/i5pAE38qcj5e 无密码,感谢支持!{:1_893:}

标签, 封面

yun33   

打开显示Traceback (most recent call last):
  File "id3.py", line 3, in
    import tkinter as tk
  File "PyInstaller\loader\pyimod02_importers.py", line 457, in exec_module
  File "tkinter\__init__.py", line 38, in
ImportError: DLL load failed while importing _tkinter: 找不到指定的模块。
SN1t2lO   

那你需要的是musictag
sunson1097   

这个是不是需要一首一首歌来编辑?
cxr666
OP
  


sunson1097 发表于 2025-10-18 10:48
这个是不是需要一首一首歌来编辑?

可以批量 选择添加多个文件即可
cxr666
OP
  


SN1t2lO 发表于 2025-10-18 10:47
那你需要的是musictag

对 我找了这个但是实测不行不知道什么原因,下载后的原音一样
asd787   

感谢分享
FangXunlan   

感谢分享 正需要!!!!
魔力小纸条   

跑不起来啊
您需要登录后才可以回帖 登录 | 立即注册

返回顶部