#!/usr/bin/env python# -*- coding: utf-8 -*-"""
ID3v1 Reader
============
An ID3v1_ tag reader for MP3 files with a graphical frontend.
.. _ID3v1: http://www.id3.org/ID3v1
:Copyright: 2004-2008 Jochen Kupperschmidt
:Date: 18-Nov-2008 (previous release: 29-Sep-2004)
:License: GNU General Public License
"""from__future__importwith_statementfromglobimportiglobfromitertoolsimportislicefromoperatorimportitemgetterimportosimportTkinterastkimporttkFileDialog# ID3v1 stuffFIELDS=(('title',30),('artist',30),('album',30),('year',4),('comment',30),('genre',1))GENRES=(# The following genres (0-79) are defined in ID3v1:'Blues','Classic Rock','Country','Dance','Disco','Funk','Grunge','Hip-Hop','Jazz','Metal','New Age','Oldies','Other','Pop','R&B','Rap','Reggae','Rock','Techno','Industrial','Alternative','Ska','Death Metal','Pranks','Soundtrack','Euro-Techno','Ambient','Trip-Hop','Vocal','Jazz+Funk','Fusion','Trance','Classical','Instrumental','Acid','House','Game','Sound Clip','Gospel','Noise','Alternative Rock','Bass','Soul','Punk','Space','Meditative','Instrumental Pop','Instrumental Rock','Ethnic','Gothic','Darkwave','Techno-Industrial','Electronic','Pop-Folk','Eurodance','Dream','Southern Rock','Comedy','Cult','Gangsta','Top 40','Christian Rap','Pop/Funk','Jungle','Native US','Cabaret','New Wave','Psychadelic','Rave','Showtunes','Trailer','Lo-Fi','Tribal','Acid Punk','Acid Jazz','Polka','Retro','Musical','Rock & Roll','Hard Rock',# The following genres (80-147) are Winamp extensions# (or at least are 80-125):'Folk','Folk-Rock','National Folk','Swing','Fast Fusion','Bebob','Latin','Revival','Celtic','Bluegrass','Avantgarde','Gothic Rock','Progressive Rock','Psychedelic Rock','Symphonic Rock','Slow Rock','Big Band','Chorus','Easy Listening','Acoustic','Humour','Speech','Chanson','Opera','Chamber Music','Sonata','Symphony','Booty Bass','Primus','Porn Groove','Satire','Slow Jam','Club','Tango','Samba','Folklore','Ballad','Power Ballad','Rhytmic Soul','Freestyle','Duet','Punk Rock','Drum Solo','Acapella','Euro-House','Dance Hall','Goa','Drum & Bass','Club-House','Hardcore','Terror','Indie','BritPop','Negerpunk','Polsk Punk','Beat','Christian Gangsta','Heavy Metal','Black Metal','Crossover','Contemporary C','Christian Rock','Merengue','Salsa','Trash Metal','Anime','JPop','SynthPop')defread_tag_data(filename):"""Read the last 128 bytes from the file."""withopen(filename,'rb')asf:f.seek(-128,os.SEEK_END)returnf.read()defparse_tag(data):"""Parse data into an ID3v1 tag."""chars=iter(data)forname,lengthinFIELDS[:-1]:yieldname,''.join(charforcharinislice(chars,length)ifchar!='\x00')# Genre is the last field.try:yield'genre',GENRES[ord(data[-1])]exceptIndexError:passdefload_tag(filename):"""Load a file's tag into a dictionary."""# Create dictionary with empty strings for defaults.d=dict.fromkeys(map(itemgetter(0),FIELDS),'')try:data=read_tag_data(filename)exceptIOError:# Error reading the file.passelse:ifdata.startswith('TAG'):# Update the dictionary with tag key/value pairs.# Skip the data's ``TAG`` prefix.d.update(parse_tag(data[3:]))returnd# Tkinter GUI stuffclassFileBrowser(tk.Frame):"""A path selector and current directory browser."""def__init__(self,*args,**kwargs):tk.Frame.__init__(self,*args,**kwargs)self.path=tk.StringVar()# Begin with the current directory.self.path.set(os.getcwd())tk.Label(self,text='Path:').grid(row=0,column=0)tk.Entry(self,textvariable=self.path,state=tk.DISABLED,disabledforeground='Black') \
.grid(row=0,column=1,sticky=tk.W+tk.E)tk.Button(self,text='Browse',command=self.change_dir) \
.grid(row=0,column=2)self.files=ScrollableListbox(self)self.files.grid(row=1,column=0,columnspan=3,sticky=tk.N+tk.S+tk.W+tk.E)self.columnconfigure(1,weight=1)self.rowconfigure(1,weight=1)self.change_dir(self.path.get())defget_filename(self):"""Return the current path and filename."""returnos.path.join(self.path.get(),self.files.list.get(tk.ACTIVE))defchange_dir(self,dir=None):"""Change directory and update file list."""ifdirisNone:dir=tkFileDialog.askdirectory()ifnotdir:# User might have chosen to 'abort'.returnself.path.set(os.path.abspath(dir))self.files.update(self.path.get())classScrollableListbox(tk.Frame):"""A list box with horizontal and vertical scrollbars."""def__init__(self,*args,**kwargs):tk.Frame.__init__(self,*args,**kwargs)# Create listbox for files with scrollbars.self.list=tk.Listbox(self)sb_x=tk.Scrollbar(self,orient=tk.HORIZONTAL,command=self.list.xview)sb_y=tk.Scrollbar(self,orient=tk.VERTICAL,command=self.list.yview)self.list.config(xscrollcommand=sb_x.set,yscrollcommand=sb_y.set)# Set up layout.self.list.grid(row=0,column=0,sticky=tk.N+tk.S+tk.W+tk.E)sb_x.grid(row=1,column=0,sticky=tk.S+tk.W+tk.E)sb_y.grid(row=0,column=1,rowspan=2,sticky=tk.N+tk.S+tk.E)self.rowconfigure(0,weight=1)self.columnconfigure(0,weight=1)defupdate(self,path,file_mask='*.mp3'):"""Empty and update list with files from ``path``."""self.list.delete(0,tk.END)forfilenameiniglob(os.path.join(path,file_mask)):self.list.insert(tk.END,os.path.basename(filename))classTagDetails(tk.Frame):"""A grid of tag attributes."""field_names=('filename','title','artist','album','year','comment','genre')def__init__(self,*args,**kwargs):tk.Frame.__init__(self,*args,**kwargs)self.tag={}forrow,fieldinenumerate(self.field_names):self.tag[field]=tk.StringVar()tk.Label(self,text=field.capitalize()+':') \
.grid(row=row,column=0,sticky=tk.W)tk.Entry(self,textvariable=self.tag[field],state=tk.DISABLED,disabledforeground='Black') \
.grid(row=row,column=1,sticky=tk.W+tk.E)self.columnconfigure(1,weight=1)defupdate(self,filename):"""Update the shown tag attributes."""tag=load_tag(filename)tag['filename']=os.path.basename(filename)forkey,valueintag.iteritems():self.tag[key].set(value)classGUI(tk.Tk):"""A graphical frontend for the ID3v1 tag reader."""def__init__(self):"""Create and lay out widgets."""tk.Tk.__init__(self)self.title('ID3v1 Reader')self.fb=FileBrowser(self)self.fb.grid(row=0,column=0,sticky=tk.N+tk.S+tk.W+tk.E)self.tag=TagDetails(self)self.tag.grid(row=1,column=0,sticky=tk.W+tk.E)self.rowconfigure(0,weight=1)self.columnconfigure(0,weight=1)# Bind selection changes to trigger tag display updates.self.fb.files.list.bind('<Double-Button-1>',self.update_tag)defupdate_tag(self,event):"""Update shown tag information."""self.tag.update(self.fb.get_filename())if__name__=='__main__':GUI().mainloop()