# -*- coding: cp949 -*- import re, urllib2 import eyed3 import codeshift import util class maniadb_tag_info: ''' Maniadb 태그 정보 기록을 위한 클래스 update_mp3_tag 일반함수의 내부에서 사용된다. 유저 코드에서 직접적으로 선언되어 사용되는 일은 없다. ''' def get_info(self, song_info, idx, album_info = None): ''' <파싱된 콘텐츠>에서 MP3 태그 정보에 기록할 정보를 재차 추출한다. 파싱된 콘텐츠란 'parse_search_v3', 'parse_album_v3'와 같은 모듈에 의해 OpenAPI를 통해 전달 받은 XML문서의 구조를 파이썬의 딕셔너리와 리스트로 변환시킨 것이다. 이 클래스가 멤버 변수로 기록하는 내용은 다음과 같다. 제목 변수명 출처 ============================================================= 앨범 제목 album_title album_info, song_info (album_info 가 없으면) 아티스트 이름 artist_name song_info 커버 이미지 데이터 cover_bin http response 커버 이미지 MIME type cover_type http response 커버 이미지 URL cover_url song_info 디스크 번호 disc_num album_info 레이블 publisher album_info 발매일 release_date song_info 곡명 song_title song_info 트랙 번호 song_track album_info 전체 디스크 장수 total_disc album_info 디스크의 전체 트랙 total_track album_info Args: song_info: 곡 정보 검색 결과 (파싱된 콘텐츠) idx: 곡 정보 검색 결과 중 한 아이템의 인덱스. album_info: 앨범 세부 정보 (파싱된 콘텐츠) Returns: 모듈의 내부 변수에 지정된 값이 저장된다. Raises: 일반 예외: 디스크/트랙 번호를 찾을 수 없을 경우에 발생한다. Maniadb의 모든 데이터를 100% 신뢰할 수는 없다. 가끔씩 같은 곡의 ID가 song_info와 album_info에서 다르게 나올 때가 있다. ''' # choose song item song_item = song_info[u'item'][idx] # song tags if album_info != None: self.album_title = album_info[u'shorttitle'] else: self.album_title = song_item[u'album'][u'title'] self.artist_name = song_item[u'artist'][u'name'] self.song_title = song_item[u'title'] self.release_date = song_item[u'album'][u'release'] # fix: some song title may have a html anchor tag. Get rid of it. detect_link = re.match(r'(.+)]+>(.*)(.*)', self.song_title) if detect_link: self.song_title = ''.join(detect_link.groups()) # get url self.cover_url = song_info[u'image'] # get binary image and image type from url resp = urllib2.urlopen(self.cover_url) self.cover_type = resp.info()[u'Content-Type'] self.cover_bin = resp.read() # optional data if album_info != None: # search for disc number, track number disc_num, track_num = self.__search_song_id(song_item[u'id'], album_info[u'disc']) if disc_num == None or track_num == None: msg = u'Song ID \''+song_item[u'id']+u'\' not found. ' msg += u'Check \'album_result.xml\' and \'search_result.xml\' for reason' raise Exception(msg) self.song_track = unicode(track_num) # song track number self.disc_num = unicode(disc_num) # disc number self.total_disc = unicode(len(album_info[u'disc'])) self.total_track = unicode(len(album_info[u'disc'][disc_num-1][u'song'])) self.publisher = album_info[u'product'][0][u'release'] # label # Just for dumping def dump(self): '''Dump all data''' print u'album_title: %s' % self.album_title print u'artist_name: %s' % self.artist_name print u'cover_bin size: %d' % len(self.cover_bin) print u'cover_url: %s' % self.cover_url print u'disc_num: %s' % self.disc_num print u'release_date: %s' % self.release_date print u'publisher: %s' % self.publisher print u'song_title: %s' % self.song_title print u'song_track: %s' % self.song_track print u'total_disc: %s' % self.total_disc print u'total_track: %s' % self.total_track # save image to file def save_cover_as(self, file_name_no_ext): '''Save cover image to a file. Not used normally.''' if self.cover_bin == '': raise Exception(u'No cover file contents') if self.cover_type == '': raise Exception(u'No mime type') ext = self.cover_type.split(u'/')[1] file_name = file_name_no_ext + u'.' + ext with open(file_name, u'wb') as f: f.write(self.cover_bin) # find song_id in disc list def __search_song_id(self, song_id, discs): #print 'song_id:', repr(song_id) disc_num = 0 track_num = 0 # disc[u'no']는 간혹 숫자 대신 영문자 'A', 'B'등이 들어갈 수 있다. # 이는 태그 기록 시점에서 문제가 될 수 있으므로 가급적 피한다. # 디스크 번호와 마찬가지로 트랙 번호도 직접 integer 변수로 처리. for disc in discs: songs = disc[u'song'] #disc_num = disc[u'no'] disc_num += 1 track_num = 0 for song in songs: track_num += 1 #print u'song[u\'id\']:', repr(song[u'id']) if song[u'id'] == song_id: return disc_num, track_num return None, None album_title = u'' artist_name = u'' cover_bin = u'' cover_type = u'' cover_url = u'' disc_num = u'' publisher = u'' release_date = u'' song_title = u'' song_track = u'' total_disc = u'' total_track = u'' ### END OF CLASS maniadb_tag_info ### # Update MP3 Tag from information def update_mp3_tag(file_name, song_info, idx, album_info = None): ''' mp3 태그를 새롭게 갱신한다. 기존의 태그 정보는 완전 삭제된다. mp3 태그 수정을 위해 eyed3 라이브러리를 사용하였다. 태그 수정은 일괄적으로 ID3V2.3을 이용하도록 되어 있다. Args: file_name: 태그를 수정할 파일 이름 song_info: 곡 정보 검색 결과 (파싱된 콘텐츠) idx: 곡 정보 검색 결과 중 한 아이템의 인덱스. album_info: 앨범 세부 정보 (파싱된 콘텐츠) Returns: 없음. Raises: 정의하지 않음. ''' # Maniadb tag information class tag_info = maniadb_tag_info() # Get information from information object tag_info.get_info(song_info, idx, album_info) # DUMP for debugging tag_info.dump() # Remove current tag eyed3.id3.tag.Tag.remove(file_name) # Load file audio_file = eyed3.load(file_name) # Input new tag audio_file.tag = eyed3.id3.Tag() audio_file.tag.header.version = eyed3.id3.ID3_V2_3 # ID3V2.3 audio_file.tag.album = tag_info.album_title # Album title audio_file.tag.artist = tag_info.artist_name # Artist Name audio_file.tag.disc_num = (tag_info.disc_num, tag_info.total_disc) # Disc number audio_file.tag.publisher = tag_info.publisher # Publisher audio_file.tag.title = tag_info.song_title # Song title audio_file.tag.track_num = (tag_info.song_track, tag_info.total_track) # Current track number, total track number # Image imagetype = eyed3.id3.frames.ImageFrame.stringToPicType('FRONT_COVER') audio_file.tag.images.set(imagetype, tag_info.cover_bin, tag_info.cover_type) # optional, TYER, release date reldate = tag_info.release_date if reldate != '': try: year = int(reldate[0:4]) month = int(reldate[4:6]) day = int(reldate[6:8]) if month == 0: month = None if day == 0: day = None audio_file.tag.release_date = eyed3.core.Date(year, month, day) # Release date audio_file.tag.setTextFrame(u'TYER', unicode(year)) # Year of release. Default comlumn in Mp3Tag. except ValueError: print u'\'release_date\' seems to be wrong. value:', reldate print u'Skip writing release date tag' except IndexError: print u'\'release_date\' does not have \'yyyymmdd\' format:', reldate print u'Skip writing release date tag' # Save audio_file.tag.save(file_name) # MP3 File information. def mp3info(file_name): ''' MP3 파일과 태그의 정보를 검색한다. Args: file_name: 검색할 파일명. Returns: 정보를 담은 파이썬 딕셔너리 객체. 딕셔너리 객체는 다음과 같은 정보를 가지고 있다. 키 값 ======================================== tag_ver 태그 체계의 버전. 튜플로 표시된다. e.g. (2, 3, 0) path 파일의 절대 경로. song 노래 제목. artist 아티스트 이름. album 앨범 제목. release_date 출시일 (발매일). time 재생시간 hh:mm:ss 혹은 mm:ss 로 표시된다. vbr True/False 값으로 VBR (Variable Bitrate) 여부를 표시. bit_rate Bitrate를 표시. 단위는 kbps. size_bytes 파일 크기를 표시. 단위는 바이트. disc_num 디스크 번호. (현재 디스크, 전체 디스크 수) 튜플로 표시. track_num 트랙 번호. (현재 트랙, 전체 트랙) 튜플로 표시. publisher 앨범을 출시한 레이블 이름. Raises: 정의하지 않음. ''' audio_file = eyed3.load(file_name) tag = audio_file.tag info = audio_file.info tag_info = { u'tag_ver': '', u'path': '', u'song': '', u'artist': '', u'album': '', u'release_date': '', u'time': '', u'vbr': '', u'bit_rate': 0, u'size_bytes': 0, u'disc_num': None, u'track_num': None, u'publisher': '', } ### Some information available even though tag doesn't exist # path tag_info[u'path'] = audio_file.path # time tag_info[u'time'] = sec2hms(info.time_secs) # vbr tag_info[u'vbr'] = info.bit_rate[0] # bitrate tag_info[u'bit_rate'] = info.bit_rate[1] # size_bytes tag_info[u'size_bytes'] = info.size_bytes if tag is None: return tag_info ### Tag information # tag version if tag.header.version is not None: tag_info[u'tag_ver'] = tag.header.version # song title if tag.title is not None : tag_info[u'song'] = tag.title # artist if tag.artist is not None : tag_info[u'artist'] = tag.artist # album title if tag.album is not None : tag_info[u'album'] = tag.album # release date if tag.release_date is not None: tag_info[u'release_date'] = tag.release_date # disc number if tag.disc_num is not None: tag_info[u'disc_num'] = tag.disc_num # track number if tag.track_num is not None: tag_info[u'track_num'] = tag.track_num # publisher if tag.publisher is not None: tag_info[u'publisher'] = tag.publisher return tag_info def sec2hms(time_sec): ''' 초를 hh:mm:ss 혹은 mm:ss 형식의 텍스트로 변경 ''' sec = time_sec hour = sec/3600 sec = sec%3600 min = sec/60 sec = sec%60 if hour > 0: return u'%02d:%02d:%02d' % (hour, min, sec) else: return u'%02d:%02d' % (min, sec)