# Copyright (c) 2004-5 Marek Hnilica. All rights reserved. # High-level tag reading & showing # Distributed under GPL version 2, or (at your option) later import wx from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin import os,string,glob,sys import TimeEditor,ID3,ID3V2,mp3,wma,oggiface,ogg.vorbis from utils import * from OneFileTagEdit import mboxFileTagEdit class mboxTrackList(wx.ListCtrl,ListCtrlAutoWidthMixin): def __init__(self,parent,id,ColouredLines=1,ColourToUse=(238,238,238)): self.ColouredLines=ColouredLines self.ColourToUse=ColourToUse self.ColumnsDragged={} self.SortingColumn=0 self.ReverseDirection=0 self.parent=parent.GetParent().GetParent() self.TagType=self.parent.TagBox.GetValue() wx.ListCtrl.__init__(self, parent, id,style=wx.LC_REPORT|wx.NO_BORDER) ListCtrlAutoWidthMixin.__init__(self) self.MakeColumns() self.CurrentDir='' self.PreviousTagType='' self.PrevDir='' wx.EVT_LIST_ITEM_ACTIVATED(self, self.GetId(), self.ItemActivated) wx.EVT_LIST_ITEM_SELECTED(self, self.GetId(), self.ItemSelected) wx.EVT_LIST_ITEM_DESELECTED(self, self.GetId(), self.ItemDeselected) wx.EVT_LIST_COL_CLICK(self, self.GetId(), self.ColClicked) wx.EVT_RIGHT_UP(self, self.RClick) wx.EVT_LIST_COL_END_DRAG(self,self.GetId(),self.ColDragged) self.NumbersSelection=[] # def MoveItem(self,increment): # item=self.NumbersSelection[len(self.NumbersSelection)-1] # if (item+increment>=0) and (item+increment<=len(self.Tag)): # self.Tag.insert(item+increment,self.Tag[item]) # if increment<0: # del self.Tag[item+1] # else: # del self.Tag[item] # self.DontSortTags=0 # WasSelected=self.NumbersSelection[:] # self.EvaluateTag(self.CurrentDir,self.TagType,Reload=0) # self.UnselectAll(0) # if increment>0: # WasSelected[len(WasSelected)-1]=WasSelected[len(WasSelected)-1]+1 # else: # WasSelected[len(WasSelected)-1]=WasSelected[len(WasSelected)-1]-1 # for i in WasSelected: # self.Select(i,1) # else: # return def ColDragged(self,event): self.ColumnsDragged[event.GetColumn()]=self.GetColumnWidth(event.GetColumn()) def EvaluateTag(self,CurrentDir,TagType,Reload=1): if os.access(CurrentDir,1): SizesDict={} for i in range(self.GetColumnCount()): SizesDict[i]=self.GetColumnWidth(i) self.ClearAll() self.MakeColumns() # to avoid unpleasant columns resizing on background self.SetBestSize(RememberSizes=SizesDict) self.CurrentDir=CurrentDir self.TagType=TagType if self.TagType=='MP3': self.ShowMP3(Reload) if self.TagType=='Ogg': self.ShowOGG(Reload) if self.TagType=='Wma': self.ShowWMA(Reload) if self.TagType=='All': self.ShowAll(Reload) self.PrevDir=self.CurrentDir if not len(self.Tag)==0: self.Select(0) self.NumbersSelection=[0] self.ItemSelected(0) else: self.NumbersSelection=[] self.RestartSelection() self.SetChangedColours() return 0 else: GeneralError(self,text='Cannot read files in directory %s'%CurrentDir,label='Wrong permissions') return 1 def ColClicked(self,event): if not hasattr(self,'Tag'): return if event.GetColumn()==self.SortingColumn and not self.ReverseDirection: self.ReverseDirection=1 else: self.ReverseDirection=0 self.SortingColumn=event.GetColumn() self.EvaluateTag(self.CurrentDir,self.TagType,Reload=0) def SortTags(self): column=self.SortingColumn Operations=0 Tags=[] CopyTag=self.Tag[:] for tag in self.Tag: Tags.append(tag[column]) Operations+=1 Tags.sort() if self.ReverseDirection: Tags.reverse() NewTag=[] for Sorted in Tags: for pos in range(len(CopyTag)): Operations+=1 if Sorted==CopyTag[pos][column]: NewTag.append(CopyTag[pos]) del CopyTag[pos] break self.Tag=NewTag[:] def SetChangedColours(self): OpList=self.parent.RenameTools.PendingCtrl.OperationList if hasattr(self,'Tag'): for Pos in range(len(self.Tag)): for NameInList in OpList: NameToCompare=NameInList[1] if NameInList[0]=='rename': NameToCompare=NameInList[2] if self.Tag[Pos][0]==NameToCompare: item=self.GetItem(Pos) item.SetTextColour(wx.BLUE) self.SetItem(item) if self.ColouredLines: if Pos%2==1: item=self.GetItem(Pos) item.SetBackgroundColour(self.ColourToUse) self.SetItem(item) else: item=self.GetItem(Pos) item.SetBackgroundColour(wx.WHITE) self.SetItem(item) def SetColours(self,UseColour,Colour): if (UseColour!=self.ColouredLines) or (Colour!=self.ColourToUse): self.ColouredLines=UseColour self.ColourToUse=Colour self.SetChangedColours() def RestartSelection(self): try: self.NumbersSelection=[] self.TotalTimeSelected=0 self.TotalTimeChange() except NameError: pass def RClick(self,evt): if not hasattr(self,'SelectAllID'): self.SelectAllID=wx.NewId() self.UnselectAllID=wx.NewId() self.InvertSelectionID=wx.NewId() self.EditTimeID=wx.NewId() self.RenameFileID=wx.NewId() self.ResetSizes=wx.NewId() wx.EVT_MENU(self,self.SelectAllID,self.SelectAll) wx.EVT_MENU(self,self.UnselectAllID,self.UnselectAll) wx.EVT_MENU(self,self.InvertSelectionID,self.InvertSelection) wx.EVT_MENU(self,self.EditTimeID,self.EditTime) wx.EVT_MENU(self,self.RenameFileID,self.RenameFile) wx.EVT_MENU(self,self.ResetSizes,self.ResetColumns) RightClickMenu=wx.Menu() RightClickMenu.Append(self.SelectAllID,'Select all') RightClickMenu.Append(self.InvertSelectionID,'Invert selection') RightClickMenu.Append(self.UnselectAllID,'Unselect all') RightClickMenu.AppendSeparator() RightClickMenu.Append(self.ResetSizes,'Reset columns width') RightClickMenu.AppendSeparator() RightClickMenu.Append(self.RenameFileID,'Rename file') RightClickMenu.Append(self.EditTimeID,'Modify time of track') RightClickMenu.Enable(self.EditTimeID,len(self.NumbersSelection)==1) RightClickMenu.Enable(self.RenameFileID,len(self.NumbersSelection)==1) self.PopupMenu(RightClickMenu,pos=evt.GetPosition()) RightClickMenu.Destroy() def ResetColumns(self,junk): self.ColumnsDragged={} self.EvaluateTag(self.CurrentDir,self.TagType,Reload=0) def RenameFile(self,junk): diag=wx.TextEntryDialog(self, 'Select new filename','Rename file') OriginalName=self.Tag[self.NumbersSelection[0]][0] diag.SetValue(os.path.basename(OriginalName)) if diag.ShowModal() == wx.ID_OK: self.parent.RenameTools.PendingCtrl.AddOperation(['rename',OriginalName,os.path.join(os.path.dirname(OriginalName),diag.GetValue())]) self.parent.RenameTools.PendingCtrl.DoIt(self.parent.RenameTools.DontWaitCheck.GetValue()) self.EvaluateTag(self.CurrentDir,self.TagType,Reload=0) diag.Destroy() def EditTime(self,junk): diag=TimeEditor.TimeEdit(self,self.Tag[self.NumbersSelection[0]][8]) diag.ShowModal() if diag.Closed: ModifiedField=list(self.Tag[self.NumbersSelection[0]]) ModifiedField[8]=diag.NewTime NewTag=[] for i in range(len(self.Tag)): if i == self.NumbersSelection[0]: NewTag.append(ModifiedField) else: NewTag.append(self.Tag[i]) self.Tag=NewTag self.EvaluateTag(self.CurrentDir,self.TagType,Reload=0) diag.Destroy() def SelectAll(self,junk): for i in range(self.GetItemCount()): self.Select(i,1) def UnselectAll(self,junk): for i in range(self.GetItemCount()): self.Select(i,0) def InvertSelection(self,junk): for i in range(self.GetItemCount()): self.Select(i,not self.IsSelected(i)) def TotalTimeChange(self): self.TotalTimeSelected=0 self.TotalTime=0 try: for self.Num in range(len(self.Tag)): self.TotalTime+=self.Tag[self.Num][8] for self.Num in self.NumbersSelection: self.TotalTimeSelected+=self.Tag[self.Num][8] if not len(self.Tag)==0 and not self.parent.RenameTools.PendingCtrl.OperationList: self.parent.SetStatusText(string.zfill(str(len(self.NumbersSelection)),2)+' / '+string.zfill(str(len(self.Tag)),2),1) self.parent.SetStatusText(self.SecTime(self.TotalTimeSelected)+' / '+self.SecTime(self.TotalTime),2) self.parent.AppropriateToolStatus(1) else: for i in range(3): self.parent.SetStatusText('',i) self.parent.AppropriateToolStatus(0) except: pass def ItemSelected(self, event): if not event==0: self.NumbersSelection.append(event.m_itemIndex) self.parent.SetStatusText(self.Tag[event.m_itemIndex][0],0) else: self.NumbersSelection=[0] self.NumbersSelection.sort() self.TotalTimeChange() def ItemDeselected(self,event): self.NumbersSelection.remove(event.m_itemIndex) if not self.NumbersSelection==[]: self.parent.SetStatusText(self.Tag[self.NumbersSelection[len(self.NumbersSelection)-1]][0],0) else: self.parent.SetStatusText('',0) self.NumbersSelection.sort() self.TotalTimeChange() def ItemActivated(self, event): self.currentItem = event.m_itemIndex if EvaluateTagType(self.GetItemText(self.currentItem))=='Wma': WmaMessage(self) else: FileEdit=mboxFileTagEdit(self,wx.ID_ANY) FileEdit.ShowModal() FileEdit.Destroy() def MakeColumns(self): self.InsertColumn(0,'Filename') self.InsertColumn(1,'Title') self.InsertColumn(2,'Artist') self.InsertColumn(3,'Album') self.InsertColumn(4,'Genre') self.InsertColumn(5,'#') self.InsertColumn(6,'Year') self.InsertColumn(7,'Bitrate') self.InsertColumn(8,'Time') column=self.GetColumn(self.SortingColumn) column.SetText(column.GetText().upper()) self.SetColumn(self.SortingColumn,column) def SecTime(self,seconds): minutes=divmod(seconds,60)[0] (hours,minutes)=divmod(minutes,60) minutes=string.zfill(minutes,2) seconds=divmod(seconds,60)[1] seconds=string.zfill(seconds,2) if hours>0: HourString=string.zfill(str(hours),2)+':' else: HourString='' time=HourString+str(minutes)+':'+seconds return time def SetBestSize(self,RememberSizes=0): if not RememberSizes: self.SetColumnWidth(0,-1) # Does exist something like SetMinimalColumnWidth? if self.GetColumnWidth(0)<70: self.SetColumnWidth(0,70) if self.GetColumnWidth(0)>200: self.SetColumnWidth(0,200) self.SetColumnWidth(1,-1) if self.GetColumnWidth(1)<70: self.SetColumnWidth(1,70) if self.GetColumnWidth(1)>250: self.SetColumnWidth(1,250) self.SetColumnWidth(2,-1) if self.GetColumnWidth(2)<70: self.SetColumnWidth(2,70) if self.GetColumnWidth(2)>150: self.SetColumnWidth(2,150) self.SetColumnWidth(3,-1) if self.GetColumnWidth(3)<70: self.SetColumnWidth(3,70) if self.GetColumnWidth(3)>150: self.SetColumnWidth(3,150) self.SetColumnWidth(4,75) self.SetColumnWidth(5,-1) self.SetColumnWidth(6,50) self.SetColumnWidth(7,60) self.SetColumnWidth(8,wx.LIST_AUTOSIZE) for i in self.ColumnsDragged: self.SetColumnWidth(i,self.ColumnsDragged[i]) else: for i in RememberSizes: self.SetColumnWidth(i,RememberSizes[i]) posX,posY=self.GetPosition() X,Y=self.GetSize() self.SetDimensions(posX,posY,X,Y) def FillListWithTags(self): if hasattr(self,'DontSortTags'): del self.DontSortTags else: self.SortTags() for i in range(len(self.Tag)): self.InsertStringItem(i,'') self.SetStringItem(i,0,os.path.basename(self.Tag[i][0])) self.SetStringItem(i,1,self.Tag[i][1]) self.SetStringItem(i,2,self.Tag[i][2]) self.SetStringItem(i,3,self.Tag[i][3]) self.SetStringItem(i,4,self.Tag[i][4]) self.SetStringItem(i,5,self.Tag[i][5]) self.SetStringItem(i,6,self.Tag[i][6]) if not type(self.Tag[i][7])=='str': self.SetStringItem(i,7,str(self.Tag[i][7])) else: self.SetStringItem(i,7,self.Tag[i][7]) try: self.time=int(self.Tag[i][8]) except: pass self.SetStringItem(i,8,self.SecTime(self.time)) self.SetBestSize() def ShowAll(self,Reload): if Reload: self.Tag,Errors=TreateAll(self.CurrentDir) self.OriginalTag=self.Tag[:] if not Errors==[]: GeneralError(self,text=string.join(Errors,'\n')+'\n\nThese files won\'t be included in TrackList',label='Unrecognized files:') self.FillListWithTags() self.PreviousTagType='All' def ShowMP3(self,Reload): if Reload: self.Tag,Errors=TreateMpegDir(self.CurrentDir) self.OriginalTag=self.Tag[:] if not Errors==[]: GeneralError(self,text=string.join(Errors,'\n')+'\n\nThese files won\'t be included in TrackList',label='Unrecognized files:') self.FillListWithTags() self.PreviousTagType='MP3' def ShowWMA(self,Reload): if Reload: self.Tag,Errors=TreateWmaDir(self.CurrentDir) self.OriginalTag=self.Tag[:] if not Errors==[]: GeneralError(self,text=string.join(Errors,'\n')+'\n\nThese files won\'t be included in TrackList',label='Corrupted WMA files:') self.FillListWithTags() self.PreviousTagType='Wma' def ShowOGG(self,Reload): if Reload: self.Tag,Errors=TreateOggDir(self.CurrentDir) self.OriginalTag=self.Tag[:] if not Errors==[]: GeneralError(self,text=string.join(Errors,'\n')+'\n\nThese files won\'t be included in TrackList',label='Corrupted Ogg Vorbis files:') self.PreviousTagType='Ogg' self.FillListWithTags() def RestartToOriginalTag(self): self.Tag=self.OriginalTag[:] self.EvaluateTag(self.CurrentDir,self.TagType,Reload=0) def ReadDir(self,CurrentDir,TagType): if not CurrentDir == '': if CurrentDir==self.PrevDir: if not self.PreviousTagType==TagType: if self.EvaluateTag(CurrentDir,TagType)==0: return 0 else: return 1 else: if self.EvaluateTag(CurrentDir,TagType)==0: return 0 else: return 1 class ProgBar: def __init__(self,totalnumber): self.TotalNumber=totalnumber self.ActualPosition=0 self.ProgressBar=wx.ProgressDialog('Tags detection','Processed 0/%s'%self.TotalNumber,self.TotalNumber,None,wx.PD_APP_MODAL|wx.PD_AUTO_HIDE) def Update(self): self.ActualPosition+=1 self.ProgressBar.Update(self.ActualPosition,'Processed %s/%s'%(self.ActualPosition,self.TotalNumber)) def Destroy(self): self.ProgressBar.Destroy() def CV(v1,v2): if len(v1)>len(v2): return v1 else: return v2 def TreateMpegDir(directory,DiagToUse=None): os.chdir(directory) FileList=glob.glob('*.[Mm][Pp]3') Tags=[] Errors=[] if not FileList==[]: if DiagToUse==None: ProgDiag=ProgBar(len(FileList)) else: ProgDiag=DiagToUse FileList.sort() for filename in FileList: try: id1=ID3.ID3(filename) except: Errors.append(filename) continue id2=ID3V2.ID3v2(filename) id2.COMM=id2.COMM[3:] mpfile=mp3.detect_mp3(filename) if not mpfile==0: if mpfile['vbr']: bitrate=mpfile['vbrrate'] else: bitrate=mpfile['bitrate'] time=mpfile['time'] else: Errors.append(filename) continue if id1.has_key('TITLE'): title=id1['TITLE'] else: title='' if id1.has_key('ARTIST'): artist=id1['ARTIST'] else: artist='' if id1.has_key('ALBUM'): album=id1['ALBUM'] else: album='' if id1.has_key('GENRE'): genre=id1['GENRE'] else: genre='' if id1.has_key('TRACKNUMBER'): track=id1['TRACKNUMBER'] else: track='' if id1.has_key('YEAR'): year=id1['YEAR'] else: year='' if id1.has_key('COMMENT'): comm=id1['COMMENT'] else: comm='' title=CV(id2.TITLE,title) artist=CV(id2.ARTIST,artist) album=CV(id2.ALBUM,album) if genre=='Unknown Genre': genre=CV(id2.GENRE,'') else: genre=CV(id2.GENRE,genre) year=CV(id2.YEAR,year) genre=CV(id2.GENRE,genre) track=CV(id2.TRACK,track) comm=CV(id2.COMM,comm) if id2.TagList.has_key('APIC'): APIC=id2.TagList['APIC'] else: APIC='' Tags.append((os.path.abspath(filename),title,artist,album,genre,track,year,bitrate,time,comm,APIC,os.path.abspath(filename))) ProgDiag.Update() if DiagToUse==None: ProgDiag.Destroy() return Tags,Errors def TreateWmaDir(directory,DiagToUse=None): os.chdir(directory) FileList=glob.glob('*.[Ww][mM][aA]') FileList.sort() TagList=[] ErrorList=[] if not FileList==[]: if DiagToUse==None: ProgDiag=ProgBar(len(FileList)) else: ProgDiag=DiagToUse for filename in FileList: Tag=wma.Tag(filename) if Tag[7]==None: ErrorList.append(os.path.abspath(filename)) continue Tag=(os.path.abspath(filename),Tag[0],Tag[2],Tag[1],Tag[4],Tag[5],Tag[3],Tag[7],0,Tag[6],'',os.path.abspath(filename)) TagList.append(Tag) ProgDiag.Update() if DiagToUse==None: ProgDiag.Destroy() return TagList,ErrorList def TreateOggDir(directory,DiagToUse=None): os.chdir(directory) FileList=glob.glob('*.[Oo][Gg][Gg]') FileList.sort() TagList=[] ErrorList=[] if not FileList==[]: if DiagToUse==None: ProgDiag=ProgBar(len(FileList)) else: ProgDiag=DiagToUse for filename in FileList: try: VorbTag=oggiface.GetTag(filename) except: ErrorList.append(filename) continue if VorbTag.has_key('ALBUM'): Album=VorbTag['ALBUM'] else: Album='' if VorbTag.has_key('COMMENT'): Comm=VorbTag['COMMENT'] else: Comm='' if VorbTag.has_key('ARTIST'): Artist=VorbTag['ARTIST'] else: Artist='' if VorbTag.has_key('TITLE'): Title=VorbTag['TITLE'] else: Title='' if VorbTag.has_key('DATE'): Year=VorbTag['DATE'] else: Year='' if VorbTag.has_key('TRACKNUMBER'): Track=VorbTag['TRACKNUMBER'] else: Track='' if VorbTag.has_key('GENRE'): Genre=VorbTag['GENRE'] else: Genre='' VorbTag=ogg.vorbis.VorbisFile(filename) Bitrate=int(VorbTag.bitrate(0)/1000) Secs=int(VorbTag.time_total(0)) TagList.append((os.path.abspath(filename),Title,Artist,Album,Genre,Track,Year,Bitrate,Secs,Comm,'',os.path.abspath(filename))) ProgDiag.Update() if DiagToUse==None: ProgDiag.Destroy() return TagList,ErrorList def TreateAll(directory): Tags=[] Errors=[] ProgCreated=0 os.chdir(directory) TotalNumber=len(glob.glob('*.[Oo][Gg][Gg]')+glob.glob('*.[Ww][mM][aA]')+glob.glob('*.[Mm][Pp]3')) if TotalNumber>0: ProgDiag=ProgBar(TotalNumber) ProgCreated=1 mpegTags,mpegErrors=TreateMpegDir(directory,ProgDiag) oggTags,oggErrors=TreateOggDir(directory,ProgDiag) wmaTags,wmaErrors=TreateWmaDir(directory,ProgDiag) Tags+=oggTags+wmaTags+mpegTags Errors+=oggErrors+wmaErrors+mpegErrors Tags.sort() # We want to have nicely sorted errors, don't we? ;-) Errors.sort() ProgDiag.Destroy() return Tags,Errors