def _audio_tpub(atuple): audio, atag, advanced, _, _ = atuple if advanced: param = ast.literal_eval(atag) audio.add(TPUB(3, param[1])) else: audio.add(TPUB(3, atag))
def tag_resulting_track(out_file_path, track_info): try: track_to_tag = ID3(out_file_path) except mutagen.id3.error: track_to_tag = ID3() track_to_tag.save(out_file_path) track_to_tag.add(TPE1(encoding=3, text=track_info['track_artist'])) # Artist track_to_tag.add(TIT2(encoding=3, text=track_info['track_title'])) # Title track_to_tag.add(TSRC(encoding=3, text=track_info['ISRC'])) # ISRC track_to_tag.add(TRCK(encoding=3, text=track_info['track_number'])) # Track Number track_to_tag.add(TPOS(encoding=3, text=track_info['disc_number'])) # Disc Number track_to_tag.add(TALB(encoding=3, text=track_info['album_title'])) # Album Title track_to_tag.add(TDRC(encoding=3, text=track_info['album_year'])) # Year track_to_tag.add(TPUB(encoding=3, text=track_info['label'])) # Label track_to_tag.add(TPE2(encoding=3, text=track_info['album_artist'])) # Album artist track_to_tag.add(TCON(encoding=3, text=track_info['genre'])) # Genre track_to_tag.save(out_file_path)
def as_mp3(self): """ Embed metadata to MP3 files. """ music_file = self.music_file #print(music_file) meta_tags = self.meta_tags #print(meta_tags) # EasyID3 is fun to use ;) # For supported easyid3 tags: # https://github.com/quodlibet/mutagen/blob/master/mutagen/easyid3.py # Check out somewhere at end of above linked file ''' mera code adding dummy tags''' try: tags = ID3(music_file) except ID3NoHeaderError: tags = ID3() tags.save(music_file) audiofile = EasyID3(music_file) self._embed_basic_metadata(audiofile, preset=TAG_PRESET) audiofile['media'] = meta_tags['type'] audiofile['author'] = meta_tags['artists'][0]['name'] audiofile['lyricist'] = meta_tags['artists'][0]['name'] audiofile['arranger'] = meta_tags['artists'][0]['name'] audiofile['performer'] = meta_tags['artists'][0]['name'] audiofile['website'] = meta_tags['external_urls']['spotify'] audiofile['length'] = str(meta_tags['duration']) if meta_tags['publisher']: audiofile['encodedby'] = meta_tags['publisher'] if meta_tags['external_ids']['isrc']: audiofile['isrc'] = meta_tags['external_ids']['isrc'] audiofile.save(v2_version=3) # For supported id3 tags: # https://github.com/quodlibet/mutagen/blob/master/mutagen/id3/_frames.py # Each class represents an id3 tag audiofile = ID3(music_file) audiofile['TORY'] = TORY(encoding=3, text=meta_tags['year']) audiofile['TYER'] = TYER(encoding=3, text=meta_tags['year']) audiofile['TPUB'] = TPUB(encoding=3, text=meta_tags['publisher']) audiofile['COMM'] = COMM(encoding=3, text=meta_tags['external_urls']['spotify']) if meta_tags['lyrics']: audiofile['USLT'] = USLT(encoding=3, desc=u'Lyrics', text=meta_tags['lyrics']) try: albumart = urllib.request.urlopen( meta_tags['album']['images'][0]['url']) audiofile['APIC'] = APIC(encoding=3, mime='image/jpeg', type=3, desc=u'Cover', data=albumart.read()) albumart.close() except IndexError: pass audiofile.save(v2_version=3) return True
def to_id3_tags(self, audio_path): """Loads an MP3 file and adds ID3v2.4 tags based on the given discogs entry""" audio = ID3(audio_path, v2_version=4) # Set album art album_art_url = self.get_album_art_url() if album_art_url: r = requests.get(album_art_url) audio.add(APIC( encoding=3, mime='image/jpeg', type=3, desc='Cover', data=r.content )) del r # Set title audio.add(TIT2(encoding=3, text=[self.track.title])) # Set artists audio.add(TPE1(encoding=3, text=self.get_artists())) # Set album audio.add(TALB(encoding=3, text=[self.get_release_title()])) # Set track number audio.add(TRCK(encoding=3, text=[self.track.position])) # Set labels labels = list(self.get_labels()) if len(labels) > 0: audio.add(TPUB(encoding=3, text=labels[0:1])) # Set genres audio.add(TCON(encoding=3, text=self.get_genres())) # Set year audio.add(TYER(encoding=3, text=[self.get_year()])) # for backwards compatibility with v2.3 # The timestamp fields are based on a subset of ISO 8601. When being as # precise as possible the format of a time string is # yyyy-MM-ddTHH:mm:ss (year, "-", month, "-", day, "T", hour (out of # 24), ":", minutes, ":", seconds), but the precision may be reduced by # removing as many time indicators as wanted. Hence valid timestamps # are # yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddTHH, yyyy-MM-ddTHH:mm and # yyyy-MM-ddTHH:mm:ss. All time stamps are UTC. For durations, use # the slash character as described in 8601, and for multiple non- # contiguous dates, use multiple strings, if allowed by the frame # definition. audio.add(TDRL(encoding=3, text=[self.get_year()])) audio.add(TDOR(encoding=3, text=[self.get_year()])) # Set tagging time # aka right now # for completeness sake audio.add(TDTG(encoding=3, text=[datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M')])) # Write to disk audio.save()
def as_mp3(self, path, metadata, cached_albumart=None): """ Embed metadata to MP3 files. """ logger.debug('Writing MP3 metadata to "{path}".'.format(path=path)) # EasyID3 is fun to use ;) # For supported easyid3 tags: # https://github.com/quodlibet/mutagen/blob/master/mutagen/easyid3.py # Check out somewhere at end of above linked file audiofile = EasyID3(path) self._embed_basic_metadata(audiofile, metadata, "mp3", preset=TAG_PRESET) audiofile["media"] = metadata["type"] audiofile["author"] = metadata["artists"][0]["name"] audiofile["lyricist"] = metadata["artists"][0]["name"] audiofile["arranger"] = metadata["artists"][0]["name"] audiofile["performer"] = metadata["artists"][0]["name"] provider = metadata["provider"] audiofile["website"] = metadata["external_urls"][provider] audiofile["length"] = str(metadata["duration"]) if metadata["publisher"]: audiofile["encodedby"] = metadata["publisher"] if metadata["external_ids"]["isrc"]: audiofile["isrc"] = metadata["external_ids"]["isrc"] audiofile.save(v2_version=3) # For supported id3 tags: # https://github.com/quodlibet/mutagen/blob/master/mutagen/id3/_frames.py # Each class in the linked source file represents an id3 tag audiofile = ID3(path) if metadata["year"]: audiofile["TORY"] = TORY(encoding=3, text=metadata["year"]) audiofile["TYER"] = TYER(encoding=3, text=metadata["year"]) if metadata["publisher"]: audiofile["TPUB"] = TPUB(encoding=3, text=metadata["publisher"]) provider = metadata["provider"] audiofile["COMM"] = COMM(encoding=3, text=metadata["external_urls"][provider]) if metadata["lyrics"]: audiofile["USLT"] = USLT(encoding=3, desc=u"Lyrics", text=metadata["lyrics"]) if cached_albumart is None: cached_albumart = urllib.request.urlopen( metadata["album"]["images"][0]["url"]).read() albumart.close() try: audiofile["APIC"] = APIC( encoding=3, mime="image/jpeg", type=3, desc=u"Cover", data=cached_albumart, ) except IndexError: pass audiofile.save(v2_version=3)
def as_mp3(self): """ Embed metadata to MP3 files. """ music_file = self.music_file meta_tags = self.meta_tags # EasyID3 is fun to use ;) # For supported easyid3 tags: # https://github.com/quodlibet/mutagen/blob/master/mutagen/easyid3.py # Check out somewhere at end of above linked file audiofile = EasyID3(music_file) self._embed_basic_metadata(audiofile, preset=TAG_PRESET) audiofile["media"] = meta_tags["type"] audiofile["author"] = meta_tags["artists"][0]["name"] audiofile["lyricist"] = meta_tags["artists"][0]["name"] audiofile["arranger"] = meta_tags["artists"][0]["name"] audiofile["performer"] = meta_tags["artists"][0]["name"] audiofile["website"] = meta_tags["external_urls"][self.provider] audiofile["length"] = str(meta_tags["duration"]) if meta_tags["publisher"]: audiofile["encodedby"] = meta_tags["publisher"] if meta_tags["external_ids"]["isrc"]: audiofile["isrc"] = meta_tags["external_ids"]["isrc"] audiofile.save(v2_version=3) # For supported id3 tags: # https://github.com/quodlibet/mutagen/blob/master/mutagen/id3/_frames.py # Each class represents an id3 tag audiofile = ID3(music_file) if meta_tags["year"]: audiofile["TORY"] = TORY(encoding=3, text=meta_tags["year"]) audiofile["TYER"] = TYER(encoding=3, text=meta_tags["year"]) if meta_tags["publisher"]: audiofile["TPUB"] = TPUB(encoding=3, text=meta_tags["publisher"]) audiofile["COMM"] = COMM( encoding=3, text=meta_tags["external_urls"][self.provider]) if meta_tags["lyrics"]: audiofile["USLT"] = USLT(encoding=3, desc=u"Lyrics", text=meta_tags["lyrics"]) try: albumart = urllib.request.urlopen( meta_tags["album"]["images"][0]["url"]) audiofile["APIC"] = APIC( encoding=3, mime="image/jpeg", type=3, desc=u"Cover", data=albumart.read(), ) albumart.close() except IndexError: pass audiofile.save(v2_version=3) return True
def write_tag(self, tag, value, save=True): text = [normalize_tag_text(value)] if tag in self.id3.keys(): self.id3[tag].text = text elif tag in TRACK_MD_ID3_TAGS: if tag == ID3Tag.TITLE.value: self.id3[tag] = TIT2(text=text) elif tag == ID3Tag.GENRE.value: self.id3[tag] = TCON(text=text) elif tag == ID3Tag.BPM.value: self.id3[tag] = TBPM(text=text) elif tag == ID3Tag.KEY.value: self.id3[tag] = TKEY(text=text) elif tag == ID3Tag.LABEL.value: self.id3[tag] = TPUB(text=text) elif tag == ID3Tag.COMMENT.value or tag == ID3Tag.COMMENT_ENG.value or tag == ID3Tag.COMMENT_XXX.value: self.id3[ID3Tag.COMMENT.value] = COMM(text=text) self.id3[ID3Tag.COMMENT_ENG.value] = COMM(text=text) self.id3[ID3Tag.COMMENT_XXX.value] = COMM(text=text) if save: self.id3.save()
def setID3(baseDIR, filename, artist, title, lyric, albumID, cover_img_path): file_path = os.path.join(baseDIR, filename) audio_file = MP3(file_path, ID3=ID3) encoding=3 # 3 is for utf-8 # add CoverPicture audio_file.tags.add( APIC( encoding=encoding, mime='image/jpg', # image/jpeg or image/png type=3, # 3 is for the cover image desc=u'Cover', data=open(cover_img_path, 'rb').read() ) ) audio_file.tags.add( USLT( encoding=encoding, desc=u'Lyric', text=lyric ) ) audio_file.tags.add( TOPE( encoding=encoding, text=artist ) ) audio_file.tags.add( TPE1( encoding=encoding, text=artist ) ) audio_file.tags.add( TIT1( encoding=encoding, text=title ) ) audio_file.tags.add( TIT2( encoding=encoding, text=title ) ) audio_file.tags.add( TIPL( encoding=encoding, text=[artist] ) ) albumInfo = cc.getAlbumInfoFromMelon(albumID) if not albumInfo == None: audio_file.tags.add( TALB( encoding=encoding, text=[albumInfo['album_name']] ) ) audio_file.tags.add( TPRO( encoding=encoding, text=[albumInfo['copyright']] ) ) audio_file.tags.add( TCON( encoding=encoding, text=[albumInfo['genre']] ) ) audio_file.tags.add( TPUB( encoding=encoding, text=[albumInfo['publisher']] ) ) audio_file.tags.add( TDOR( encoding=encoding, text=[albumInfo['pub_date']] ) ) audio_file.tags.add( TDRL( encoding=encoding, text=[albumInfo['pub_date']] ) ) audio_file.save()
def __init_id3_tags(id3, major=3): """ Attributes: id3 ID3 Tag object major ID3 major version, e.g.: 3 for ID3v2.3 """ from mutagen.id3 import TRCK, TPOS, TXXX, TPUB, TALB, UFID, TPE2, \ TSO2, TMED, TIT2, TPE1, TSRC, IPLS, TORY, TDAT, TYER id3.add(TRCK(encoding=major, text="1/10")) id3.add(TPOS(encoding=major, text="1/1")) id3.add( TXXX(encoding=major, desc="MusicBrainz Release Group Id", text="e00305af-1c72-469b-9a7c-6dc665ca9adc")) id3.add(TXXX(encoding=major, desc="originalyear", text="2011")) id3.add( TXXX(encoding=major, desc="MusicBrainz Album Type", text="album")) id3.add( TXXX(encoding=major, desc="MusicBrainz Album Id", text="e7050302-74e6-42e4-aba0-09efd5d431d8")) id3.add(TPUB(encoding=major, text="J&R Adventures")) id3.add(TXXX(encoding=major, desc="CATALOGNUMBER", text="PRAR931391")) id3.add(TALB(encoding=major, text="Don\'t Explain")) id3.add( TXXX(encoding=major, desc="MusicBrainz Album Status", text="official")) id3.add(TXXX(encoding=major, desc="SCRIPT", text="Latn")) id3.add( TXXX(encoding=major, desc="MusicBrainz Album Release Country", text="US")) id3.add(TXXX(encoding=major, desc="BARCODE", text="804879313915")) id3.add( TXXX(encoding=major, desc="MusicBrainz Album Artist Id", text=[ "3fe817fc-966e-4ece-b00a-76be43e7e73c", "984f8239-8fe1-4683-9c54-10ffb14439e9" ])) id3.add(TPE2(encoding=major, text="Beth Hart & Joe Bonamassa")) id3.add(TSO2(encoding=major, text="Hart, Beth & Bonamassa, Joe")) id3.add(TXXX(encoding=major, desc="ASIN", text="B005NPEUB2")) id3.add(TMED(encoding=major, text="CD")) id3.add( UFID(encoding=major, owner="http://musicbrainz.org", data=b"f151cb94-c909-46a8-ad99-fb77391abfb8")) id3.add(TIT2(encoding=major, text="Sinner's Prayer")) id3.add( TXXX(encoding=major, desc="MusicBrainz Artist Id", text=[ "3fe817fc-966e-4ece-b00a-76be43e7e73c", "984f8239-8fe1-4683-9c54-10ffb14439e9" ])) id3.add(TPE1(encoding=major, text=["Beth Hart & Joe Bonamassa"])) id3.add( TXXX(encoding=major, desc="Artists", text=["Beth Hart", "Joe Bonamassa"])) id3.add(TSRC(encoding=major, text=["NLB931100460", "USMH51100098"])) id3.add( TXXX(encoding=major, desc="MusicBrainz Release Track Id", text="d062f484-253c-374b-85f7-89aab45551c7")) id3.add( IPLS(encoding=major, people=[["engineer", "James McCullagh"], ["engineer", "Jared Kvitka"], ["arranger", "Jeff Bova"], ["producer", "Roy Weisman"], ["piano", "Beth Hart"], ["guitar", "Blondie Chaplin"], ["guitar", "Joe Bonamassa"], ["percussion", "Anton Fig"], ["drums", "Anton Fig"], ["keyboard", "Arlan Schierbaum"], ["bass guitar", "Carmine Rojas"], ["orchestra", "The Bovaland Orchestra"], ["vocals", "Beth Hart"], ["vocals", "Joe Bonamassa"]])), id3.add(TORY(encoding=major, text="2011")) id3.add(TYER(encoding=major, text="2011")) id3.add(TDAT(encoding=major, text="2709"))
def MP3Tagger(self): Caminho = self.lineEdit.text() if Caminho == "": QMessageBox.about(self, "ERRO", "Nenhum caminho especificado.") else: File = glob.glob(Caminho + "/*.mp3") for i in File: # Caminho completo do arquivo com extensão self.label_10.setText(_translate("MainWindow", "")) QtWidgets.QApplication.processEvents() Path = os.path.dirname(i) # Caninho completo da pasta do arquivo Name1 = os.path.basename(i) # Nome do arquivo completo com extensão Name2 = os.path.splitext(Name1) # Nome do arquivo dividido na extensão Name3 = Name2[0].split(" - ") # Nome do arquivo divido no artista e musica Name4 = ''.join(i for i in Name3[0] if not i.isdigit()) # Nome da música sem os números print("Caminho: " + i) print("Name1: " + str(Name1)) print("Name2: " + str(Name2)) print("Name3: " + str(Name3)) print("Name4: " + str(Name4)) self.label_10.setText(_translate("MainWindow", "Renomeando arquivo...")) QtWidgets.QApplication.processEvents() os.rename(i, Path + "\\" + Name3[1] + " -" + Name4 + ".mp3") # Renomeia o arquivo self.label_10.setText(_translate("MainWindow", "Buscando metadados no banco de dados do Spotify...")) QtWidgets.QApplication.processEvents() meta_tags = spotify_tools.generate_metadata(Name3[1] + " -" + Name4) # Gera as tags do mp3 if meta_tags == None: continue else: self.label_6.setText(_translate("MainWindow", str(meta_tags['artists'][0]['name']))) self.label_7.setText(_translate("MainWindow", str(meta_tags['name']))) self.label_8.setText(_translate("MainWindow", str(meta_tags['album']['name']))) self.label_10.setText(_translate("MainWindow", "Aplicando tags...")) ScriptFolder = os.path.dirname(os.path.realpath(sys.argv[0])) IMG = open(ScriptFolder + "\\" + 'cover2.jpg', 'wb') IMG.write(urllib.request.urlopen(meta_tags['album']['images'][0]['url']).read()) IMG.close() time.sleep(1) self.GenericCover3 = QPixmap('cover2.jpg') self.GenericCover4 = self.GenericCover3.scaled(141, 141) self.graphicsView.setPixmap(self.GenericCover4) QtWidgets.QApplication.processEvents() audiofile = MP3(Path + "\\" + Name3[1] + " -" + Name4 + ".mp3") audiofile.tags = None # Exclui qualquer tag antes de aplicar as novas (previne erro) audiofile.add_tags(ID3=EasyID3) audiofile['artist'] = meta_tags['artists'][0]['name'] audiofile['albumartist'] = meta_tags['artists'][0]['name'] audiofile['album'] = meta_tags['album']['name'] audiofile['title'] = meta_tags['name'] audiofile['tracknumber'] = [meta_tags['track_number'], meta_tags['total_tracks']] audiofile['discnumber'] = [meta_tags['disc_number'], 0] audiofile['date'] = meta_tags['release_date'] audiofile['originaldate'] = meta_tags['release_date'] audiofile['media'] = meta_tags['type'] audiofile['author'] = meta_tags['artists'][0]['name'] audiofile['lyricist'] = meta_tags['artists'][0]['name'] audiofile['arranger'] = meta_tags['artists'][0]['name'] audiofile['performer'] = meta_tags['artists'][0]['name'] audiofile['website'] = meta_tags['external_urls']['spotify'] audiofile['length'] = str(meta_tags['duration_ms'] / 1000.0) if meta_tags['publisher']: audiofile['encodedby'] = meta_tags['publisher'] if meta_tags['genre']: audiofile['genre'] = meta_tags['genre'] if meta_tags['copyright']: audiofile['copyright'] = meta_tags['copyright'] if meta_tags['external_ids']['isrc']: audiofile['isrc'] = meta_tags['external_ids']['isrc'] audiofile.save(v2_version=3) # For supported id3 tags: # https://github.com/quodlibet/mutagen/blob/master/mutagen/id3/_frames.py # Each class represents an id3 tag audiofile = ID3(Path + "\\" + Name3[1] + " -" + Name4 + ".mp3") year, *_ = meta_tags['release_date'].split('-') audiofile['TORY'] = TORY(encoding=3, text=year) audiofile['TYER'] = TYER(encoding=3, text=year) audiofile['TPUB'] = TPUB(encoding=3, text=meta_tags['publisher']) audiofile['COMM'] = COMM(encoding=3, text=meta_tags['external_urls']['spotify']) if meta_tags['lyrics']: audiofile['USLT'] = USLT(encoding=3, desc=u'Lyrics', text=meta_tags['lyrics']) try: albumart = urllib.request.urlopen(meta_tags['album']['images'][0]['url']) audiofile['APIC'] = APIC(encoding=3, mime='image/jpeg', type=3, desc=u'Cover', data=albumart.read()) albumart.close() except IndexError: pass audiofile.save(v2_version=3) self.label_10.setText(_translate("MainWindow", "Concluído.")) QtWidgets.QApplication.processEvents() time.sleep(2) # pausa dramática # Some com os textos: self.label_10.setText(_translate("MainWindow", "")) QtWidgets.QApplication.processEvents() QMessageBox.about(self, "Concluído", "Operação concluída.")
def CopyTagsToTranscodedFileMp3(losslessFile, lossyFile): # Because the input flac file is decoded to wav, all metadata is lost. We have to extract this metadata from # the flac file and put it directly into the generated mp3 file. from mutagen.flac import FLAC from mutagen.id3 import ID3 flacFile = FLAC(losslessFile) flacFileTags = flacFile.tags mp3File = ID3(lossyFile) mp3File.delete() for key,value in flacFileTags.items(): if key == 'title': from mutagen.id3 import TIT2 mp3File.add(TIT2(encoding=3, text=value)) elif key == 'album': from mutagen.id3 import TALB mp3File.add(TALB(encoding=3, text=value)) elif key == 'artist': from mutagen.id3 import TPE1 mp3File.add(TPE1(encoding=3, text=value)) elif key == 'tracknumber': from mutagen.id3 import TRCK mp3File.add(TRCK(encoding=3, text=value)) elif key == 'date': from mutagen.id3 import TDRC mp3File.add(TDRC(encoding=3, text=value)) elif key == 'genre': from mutagen.id3 import TCON mp3File.add(TCON(encoding=3, text=value)) elif key == 'discnumber': from mutagen.id3 import TPOS mp3File.add(TPOS(encoding=3, text=value)) elif key == 'composer': from mutagen.id3 import TCOM mp3File.add(TCOM(encoding=3, text=value)) elif key == 'conductor': from mutagen.id3 import TPE3 mp3File.add(TPE3(encoding=3, text=value)) elif key == 'ensemble': from mutagen.id3 import TPE2 mp3File.add(TPE2(encoding=3, text=value)) elif key == 'comment': from mutagen.id3 import COMM mp3File.add(COMM(encoding=3, text=value)) elif key == 'publisher': from mutagen.id3 import TPUB mp3File.add(TPUB(encoding=3, text=value)) elif key == 'opus': from mutagen.id3 import TIT3 mp3File.add(TIT3(encoding=3, text=value)) elif key == 'sourcemedia': from mutagen.id3 import TMED mp3File.add(TMED(encoding=3, text=value)) elif key == 'isrc': from mutagen.id3 import TSRC mp3File.add(TSRC(encoding=3, text=value)) elif key == 'license': from mutagen.id3 import TOWN mp3File.add(TOWN(encoding=3, text=value)) elif key == 'copyright': from mutagen.id3 import WCOP mp3File.add(WCOP(encoding=3, text=value)) elif key == 'encoded-by': from mutagen.id3 import TENC mp3File.add(TENC(encoding=3, text=value)) elif (key == 'part' or key == 'partnumber'): from mutagen.id3 import TIT3 mp3File.add(TIT3(encoding=3, text=value)) elif (key == 'lyricist' or key == 'textwriter'): from mutagen.id3 import TIT3 mp3File.add(TIT3(encoding=3, text=value)) else: from mutagen.id3 import TXXX mp3File.add(TXXX(encoding=3, text=value, desc=key)) mp3File.update_to_v24() mp3File.save() return
def metadata_mp3_mutagen(self, path, media): from mutagen.mp3 import MP3 from mutagen.id3 import ID3, TRCK, TIT2, TPE1, TALB, TCON, TXXX, UFID, TSRC, TPUB, TMED, TRCK, TDRC, APIC try: tags = ID3(path) except Exception: """ kindf of hackish - mutagen does complain if no id3 headers - so just create some """ audio = MP3(path) audio["TIT2"] = TIT2(encoding=3, text=["Empty Title"]) audio.save() tags = ID3(path) # reset tags tags.delete() # user data if INCLUDE_USER and self.user: tags.add( TXXX(encoding=3, desc='open broadcast user', text=u'%s' % self.user.email)) # track-level metadata tags.add(TIT2(encoding=3, text=u'%s' % media.name)) tags.add( UFID(encoding=3, owner='https://openbroadcast.org', data=u'%s' % media.uuid)) tags.add( TXXX(encoding=3, desc='open broadcast API', text=u'https://%s%s' % (self.current_site.domain, media.get_api_url()))) # remove genre tags.add(TCON(encoding=3, text=u'')) tags.add(TMED(encoding=3, text=u'Digital Media')) if media.tracknumber: tags.add(TRCK(encoding=3, text=u'%s' % media.tracknumber)) if media.isrc: tags.add(TSRC(encoding=3, text=u'%s' % media.isrc)) if uuid_by_object(media, 'musicbrainz'): tags.add( UFID(encoding=3, owner='http://musicbrainz.org', data=u'%s' % uuid_by_object(media, 'musicbrainz'))) # release-level metadata if media.release: tags.add(TALB(encoding=3, text=u'%s' % media.release.name)) if media.release.catalognumber: tags.add( TXXX(encoding=3, desc='CATALOGNUMBER', text=u'%s' % media.release.catalognumber)) if media.release.releasedate: tags.add( TDRC(encoding=3, text=u'%s' % media.release.releasedate.year)) if media.release.release_country: tags.add( TXXX(encoding=3, desc='MusicBrainz Album Release Country', text=u'%s' % media.release.release_country.iso2_code)) if media.release.totaltracks and media.tracknumber: tags.add( TRCK(encoding=3, text=u'%s/%s' % (media.tracknumber, media.release.totaltracks))) if media.release.releasedate: tags.add( TDRC(encoding=3, text=u'%s' % media.release.releasedate.year)) if uuid_by_object(media.release, 'musicbrainz'): tags.add( TXXX(encoding=3, desc='MusicBrainz Album Id', text=u'%s' % uuid_by_object(media.release, 'musicbrainz'))) if media.release and media.release.main_image and os.path.exists( media.release.main_image.path): opt = dict(size=(300, 300), crop=True, bw=False, quality=80) try: image = get_thumbnailer( media.release.main_image).get_thumbnail(opt) tags.add( APIC(encoding=3, mime='image/jpeg', type=3, desc=u'Cover', data=open(image.path).read())) except: pass # artist-level metadata if media.artist: tags.add(TPE1(encoding=3, text=u'%s' % media.artist.name)) if uuid_by_object(media.artist, 'musicbrainz'): tags.add( TXXX(encoding=3, desc='MusicBrainz Artist Id', text=u'%s' % uuid_by_object(media.artist, 'musicbrainz'))) # label-level metadata if media.release and media.release.label: tags.add(TPUB(encoding=3, text=u'%s' % media.release.label.name)) tags.save(v1=0) return
def copyTagsToTranscodedFileMp3(losslessFile, lossyFile): # # Copy the tags from the losslessFile (.flac) to the lossyFile. # All previous tags from the lossyFile will be deleted before the # tags from the losslessFile are copied. # from mutagen.flac import FLAC from mutagen.id3 import ID3 # Read all tags from the flac file flacFile = FLAC(losslessFile) flacFileTags = flacFile.tags # Returns a dictionary containing the flac tags # Only mp3 files with ID3 headers can be openend. # So be sure to add some tags during encoding .wav. to mp3 # Mapping from Vorbis comments field recommendations to id3v2_4_0 # For more information about vorbis field recommendations: http://reactor-core.org/ogg-tagging.html # For more information about id3v2_4_0 frames: http://www.id3.org/id3v2.4.0-frames # # Single value tags: # ALBUM -> TALB # ARTIST -> TPE1 # PUBLISHER -> TPUB # COPYRIGHT -> WCOP # DISCNUMBER -> TPOS # ISRC -> TSRC # EAN/UPN # LABEL # LABELNO # LICENSE -> TOWN # OPUS -> TIT3 # SOURCEMEDIA -> TMED # TITLE -> TIT2 # TRACKNUMBER -> TRCK # VERSION # ENCODED-BY -> TENC # ENCODING # Multiple value tags: # COMPOSER -> TCOM # ARRANGER # LYRICIST -> TEXT # AUTHOR -> TEXT # CONDUCTOR -> TPE3 # PERFORMER -> # ENSEMBLE -> TPE2 # PART -> TIT1 # PARTNUMBER -> TIT1 # GENRE -> TCON # DATE -> TDRC # LOCATION # COMMENT -> COMM # Other vorbis tags are mapped to TXXX tags mp3File = ID3(lossyFile) mp3File.delete() for key, value in flacFileTags.items(): if key == 'title': # Map to TIT2 frame from mutagen.id3 import TIT2 mp3File.add(TIT2(encoding=3, text=value)) elif key == 'album': # Map to TALB frame from mutagen.id3 import TALB mp3File.add(TALB(encoding=3, text=value)) elif key == 'artist': # Map to TPE1 frame from mutagen.id3 import TPE1 mp3File.add(TPE1(encoding=3, text=value)) elif key == 'tracknumber': # Map to TRCK frame from mutagen.id3 import TRCK mp3File.add(TRCK(encoding=3, text=value)) elif key == 'date': # Map to TDRC frame from mutagen.id3 import TDRC mp3File.add(TDRC(encoding=3, text=value)) elif key == 'genre': # Map to TCON frame from mutagen.id3 import TCON mp3File.add(TCON(encoding=3, text=value)) elif key == 'discnumber': # Map to TPOS frame from mutagen.id3 import TPOS mp3File.add(TPOS(encoding=3, text=value)) elif key == 'composer': # Map to TCOM frame from mutagen.id3 import TCOM mp3File.add(TCOM(encoding=3, text=value)) elif key == 'conductor': # Map to TPE3 frame from mutagen.id3 import TPE3 mp3File.add(TPE3(encoding=3, text=value)) elif key == 'ensemble': # Map to TPE2 frame from mutagen.id3 import TPE2 mp3File.add(TPE2(encoding=3, text=value)) elif key == 'comment': # Map to COMM frame from mutagen.id3 import COMM mp3File.add(COMM(encoding=3, text=value)) elif key == 'publisher': # Map to TPUB frame from mutagen.id3 import TPUB mp3File.add(TPUB(encoding=3, text=value)) elif key == 'opus': # Map to TIT3 frame from mutagen.id3 import TIT3 mp3File.add(TIT3(encoding=3, text=value)) elif key == 'sourcemedia': # Map to TMED frame from mutagen.id3 import TMED mp3File.add(TMED(encoding=3, text=value)) elif key == 'isrc': # Map to TSRC frame from mutagen.id3 import TSRC mp3File.add(TSRC(encoding=3, text=value)) elif key == 'license': # Map to TOWN frame from mutagen.id3 import TOWN mp3File.add(TOWN(encoding=3, text=value)) elif key == 'copyright': # Map to WCOP frame from mutagen.id3 import WCOP mp3File.add(WCOP(encoding=3, text=value)) elif key == 'encoded-by': # Map to TENC frame from mutagen.id3 import TENC mp3File.add(TENC(encoding=3, text=value)) elif (key == 'part' or key == 'partnumber'): # Map to TIT3 frame from mutagen.id3 import TIT3 mp3File.add(TIT3(encoding=3, text=value)) elif (key == 'lyricist' or key == 'textwriter'): # Map to TEXT frame from mutagen.id3 import TIT3 mp3File.add(TIT3(encoding=3, text=value)) else: #all other tags are mapped to TXXX frames # Map to TXXX frame from mutagen.id3 import TXXX mp3File.add(TXXX(encoding=3, text=value, desc=key)) mp3File.update_to_v24() mp3File.save() return
def run(): parser = argparse.ArgumentParser( prog='odmpy', description='Download/return an Overdrive loan audiobook', epilog= 'Version {version}. [Python {py_major}.{py_minor}.{py_micro}-{platform}] ' 'Source at https://github.com/ping/odmpy/'.format( version=__version__, py_major=sys.version_info.major, py_minor=sys.version_info.minor, py_micro=sys.version_info.micro, platform=sys.platform, )) parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', help='Enable more verbose messages for debugging') parser.add_argument( '-t', '--timeout', dest='timeout', type=int, default=10, help='Timeout (seconds) for network requests. Default 10.') subparsers = parser.add_subparsers( title='Available commands', dest='subparser_name', help='To get more help, use the -h option with the command.') parser_info = subparsers.add_parser( 'info', description='Get information about a loan file.', help='Get information about a loan file') parser_info.add_argument('odm_file', type=str, help='ODM file path') parser_dl = subparsers.add_parser('dl', description='Download from a loan file.', help='Download from a loan file') parser_dl.add_argument('-d', '--downloaddir', dest='download_dir', default='.', help='Download folder path') parser_dl.add_argument('-c', '--chapters', dest='add_chapters', action='store_true', help='Add chapter marks (experimental)') parser_dl.add_argument( '-m', '--merge', dest='merge_output', action='store_true', help='Merge into 1 file (experimental, requires ffmpeg)') parser_dl.add_argument( '--mergeformat', dest='merge_format', choices=['mp3', 'm4b'], default='mp3', help='Merged file format (m4b is slow, experimental, requires ffmpeg)') parser_dl.add_argument( '-k', '--keepcover', dest='always_keep_cover', action='store_true', help='Always generate the cover image file (cover.jpg)') parser_dl.add_argument('-f', '--keepmp3', dest='keep_mp3', action='store_true', help='Keep downloaded mp3 files (after merging)') parser_dl.add_argument('-j', '--writejson', dest='write_json', action='store_true', help='Generate a meta json file (for debugging)') parser_dl.add_argument('odm_file', type=str, help='ODM file path') parser_ret = subparsers.add_parser('ret', description='Return a loan file.', help='Return a loan file.') parser_ret.add_argument('odm_file', type=str, help='ODM file path') args = parser.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) try: # test for odm file args.odm_file except AttributeError: parser.print_help() exit(0) xml_doc = xml.etree.ElementTree.parse(args.odm_file) root = xml_doc.getroot() # Return Book if args.subparser_name == 'ret': logger.info('Returning {} ...'.format(args.odm_file)) early_return_url = root.find('EarlyReturnURL').text try: early_return_res = requests.get(early_return_url, headers={'User-Agent': UA_LONG}, timeout=10) early_return_res.raise_for_status() logger.info('Loan returned successfully: {}'.format(args.odm_file)) except HTTPError as he: if he.response.status_code == 403: logger.warning('Loan is probably already returned.') sys.exit() logger.error( 'Unexpected HTTPError while trying to return loan {}'.format( args.odm_file)) logger.error('HTTPError: {}'.format(str(he))) logger.debug(he.response.content) sys.exit(1) except ConnectionError as ce: logger.error('ConnectionError: {}'.format(str(ce))) sys.exit(1) sys.exit() metadata = None for t in root.itertext(): if not t.startswith('<Metadata>'): continue metadata = xml.etree.ElementTree.fromstring( # remove invalid & char re.sub(r'\s&\s', ' & ', t)) break debug_meta = {} title = metadata.find('Title').text cover_url = metadata.find( 'CoverUrl').text if metadata.find('CoverUrl') != None else '' authors = [ unescape_html(c.text) for c in metadata.find('Creators') if 'Author' in c.attrib.get('role', '') ] if not authors: authors = [ unescape_html(c.text) for c in metadata.find('Creators') if 'Editor' in c.attrib.get('role', '') ] if not authors: authors = [unescape_html(c.text) for c in metadata.find('Creators')] publisher = metadata.find('Publisher').text description = metadata.find('Description').text if metadata.find( 'Description') is not None else '' debug_meta['meta'] = { 'title': title, 'coverUrl': cover_url, 'authors': authors, 'publisher': publisher, 'description': description, } # View Book Info if args.subparser_name == 'info': logger.info(u'{:10} {}'.format('Title:', colored.blue(title))) logger.info(u'{:10} {}'.format( 'Creators:', colored.blue(u', '.join([ u'{} ({})'.format(c.text, c.attrib['role']) for c in metadata.find('Creators') ])))) logger.info(u'{:10} {}'.format('Publisher:', metadata.find('Publisher').text)) logger.info(u'{:10} {}'.format( 'Subjects:', u', '.join([c.text for c in metadata.find('Subjects')]))) logger.info(u'{:10} {}'.format( 'Languages:', u', '.join([c.text for c in metadata.find('Languages')]))) logger.info(u'{:10} \n{}'.format('Description:', metadata.find('Description').text)) for formats in root.findall('Formats'): for f in formats: logger.info(u'\n{:10} {}'.format('Format:', f.attrib['name'])) parts = f.find('Parts') for p in parts: logger.info('* {} - {} ({:,.0f}kB)'.format( p.attrib['name'], p.attrib['duration'], math.ceil(1.0 * int(p.attrib['filesize']) / 1024))) sys.exit() # Download Book download_baseurl = '' download_parts = [] for formats in root.findall('Formats'): for f in formats: protocols = f.find('Protocols') for p in protocols: if p.attrib.get('method', '') != 'download': continue download_baseurl = p.attrib['baseurl'] break parts = f.find('Parts') for p in parts: download_parts.append(p.attrib) debug_meta['download_parts'] = download_parts logger.info('Downloading "{}" by "{}" in {} parts...'.format( colored.blue(title, bold=True), colored.blue(', '.join(authors)), len(download_parts))) # declare book folder/file names here together so we can catch problems from too long names book_folder = os.path.join( args.download_dir, u'{} - {}'.format(title.replace(os.sep, '-'), u', '.join(authors).replace(os.sep, '-'))) # for merged mp3 book_filename = os.path.join( book_folder, u'{} - {}.mp3'.format(title.replace(os.sep, '-'), u', '.join(authors).replace(os.sep, '-'))) # for merged m4b book_m4b_filename = os.path.join( book_folder, u'{} - {}.m4b'.format(title.replace(os.sep, '-'), u', '.join(authors).replace(os.sep, '-'))) if not os.path.exists(book_folder): try: os.makedirs(book_folder) except OSError as exc: if exc.errno not in ( 36, 63): # ref http://www.ioplex.com/~miallen/errcmpp.html raise # Ref OSError: [Errno 36] File name too long https://github.com/ping/odmpy/issues/5 # create book folder, file with just the title book_folder = os.path.join( args.download_dir, u'{}'.format(title.replace(os.sep, '-'))) os.makedirs(book_folder) book_filename = os.path.join( book_folder, u'{}.mp3'.format(title.replace(os.sep, '-'))) book_m4b_filename = os.path.join( book_folder, u'{}.m4b'.format(title.replace(os.sep, '-'))) cover_filename = os.path.join(book_folder, 'cover.jpg') debug_filename = os.path.join(book_folder, 'debug.json') if not os.path.isfile(cover_filename) and cover_url: cover_res = requests.get(cover_url, headers={'User-Agent': UA}) cover_res.raise_for_status() with open(cover_filename, 'wb') as outfile: outfile.write(cover_res.content) acquisition_url = root.find('License').find('AcquisitionUrl').text media_id = root.attrib['id'] client_id = str(uuid.uuid1()).upper() raw_hash = '{client_id}|{omc}|{os}|ELOSNOC*AIDEM*EVIRDREVO'.format( client_id=client_id, omc=OMC, os=OS) m = hashlib.sha1(raw_hash.encode('utf-16-le')) license_hash = base64.b64encode(m.digest()) # Extract license # License file is downloadable only once per odm # so we keep it in case downloads fail _, odm_filename = os.path.split(args.odm_file) license_file = os.path.join(args.download_dir, odm_filename.replace('.odm', '.license')) if os.path.isfile(license_file): logger.warning( 'Already downloaded license file: {}'.format(license_file)) else: # download license file params = OrderedDict([('MediaID', media_id), ('ClientID', client_id), ('OMC', OMC), ('OS', OS), ('Hash', license_hash)]) license_res = requests.get(acquisition_url, params=params, headers={'User-Agent': UA}, timeout=args.timeout, stream=True) try: license_res.raise_for_status() with open(license_file, 'wb') as outfile: for chunk in license_res.iter_content(1024): outfile.write(chunk) logger.debug('Saved license file {}'.format(license_file)) except HTTPError as he: if he.response.status_code == 404: # odm file has expired logger.error('The loan file "{}" has expired.' 'Please download again.'.format(args.odm_file)) else: logger.error(he.response.content) sys.exit(1) except ConnectionError as ce: logger.error('ConnectionError: {}'.format(str(ce))) sys.exit(1) license_xml_doc = xml.etree.ElementTree.parse(license_file) license_root = license_xml_doc.getroot() ns = '{http://license.overdrive.com/2008/03/License.xsd}' license_client = license_root.find('{}SignedInfo'.format(ns)).find( '{}ClientID'.format(ns)) license_client_id = license_client.text lic_file_contents = '' with open(license_file, 'r') as lic_file: lic_file_contents = lic_file.read() cover_bytes = None if os.path.isfile(cover_filename): with open(cover_filename, 'rb') as f: cover_bytes = f.read() track_count = 0 file_tracks = [] keep_cover = args.always_keep_cover audio_lengths_ms = [] for p in download_parts: part_number = int(p['number']) part_filename = os.path.join( book_folder, u'{}.mp3'.format( slugify(u'{} - Part {:02d}'.format(title, part_number), allow_unicode=True))) part_tmp_filename = u'{}.part'.format(part_filename) part_file_size = int(p['filesize']) part_url_filename = p['filename'] part_download_url = '{}/{}'.format(download_baseurl, part_url_filename) part_markers = [] if os.path.isfile(part_filename): logger.warning('Already saved {}'.format( colored.magenta(part_filename))) else: try: part_download_res = requests.get(part_download_url, headers={ 'User-Agent': UA, 'ClientID': license_client_id, 'License': lic_file_contents }, timeout=args.timeout, stream=True) part_download_res.raise_for_status() chunk_size = 1024 * 1024 expected_chunk_count = math.ceil(1.0 * part_file_size / chunk_size) with open(part_tmp_filename, 'wb') as outfile: for chunk in progress.bar( part_download_res.iter_content( chunk_size=chunk_size), label='Part {}'.format(part_number), expected_size=expected_chunk_count): if chunk: outfile.write(chunk) # try to remux file to remove mp3 lame tag errors cmd = [ 'ffmpeg', '-y', '-nostdin', '-hide_banner', '-loglevel', 'info' if logger.level == logging.DEBUG else 'error', '-i', part_tmp_filename, '-c:a', 'copy', '-c:v', 'copy', part_filename ] try: exit_code = subprocess.call(cmd) if exit_code: logger.warning( 'ffmpeg exited with the code: {0!s}'.format( exit_code)) logger.warning('Command: {0!s}'.format(' '.join(cmd))) os.rename(part_tmp_filename, part_filename) else: os.remove(part_tmp_filename) except Exception as ffmpeg_ex: logger.warning('Error executing ffmpeg: {}'.format( str(ffmpeg_ex))) os.rename(part_tmp_filename, part_filename) except HTTPError as he: logger.error('HTTPError: {}'.format(str(he))) logger.debug(he.response.content) sys.exit(1) except ConnectionError as ce: logger.error('ConnectionError: {}'.format(str(ce))) sys.exit(1) try: # Fill id3 info for mp3 part mutagen_audio = MP3(part_filename, ID3=ID3) if not mutagen_audio.tags: mutagen_audio.tags = ID3() if 'TIT2' not in mutagen_audio.tags: mutagen_audio.tags.add( TIT2(encoding=3, text=u'{}'.format(title))) if 'TIT3' not in mutagen_audio.tags: mutagen_audio.tags.add( TIT3(encoding=3, text=u'{}'.format(description))) if 'TALB' not in mutagen_audio.tags: mutagen_audio.tags.add( TALB(encoding=3, text=u'{}'.format(title))) if 'TPE1' not in mutagen_audio.tags: mutagen_audio.tags.add( TPE1(encoding=3, text=u'{}'.format(authors[0]))) if 'TPE2' not in mutagen_audio.tags: mutagen_audio.tags.add( TPE2(encoding=3, text=u'{}'.format(authors[0]))) if 'TRCK' not in mutagen_audio.tags: mutagen_audio.tags.add( TRCK(encoding=3, text=u'{}'.format(part_number))) if 'TPUB' not in mutagen_audio.tags: mutagen_audio.tags.add( TPUB(encoding=3, text=u'{}'.format(publisher))) if 'COMM' not in mutagen_audio.tags: mutagen_audio.tags.add( COMM(encoding=3, desc=u'Description', text=u'{}'.format(description))) if cover_bytes: mutagen_audio.tags.add( APIC(encoding=3, mime=u'image/jpeg', type=3, desc=u'Cover', data=cover_bytes)) mutagen_audio.save() audio_lengths_ms.append( int(round(mutagen_audio.info.length * 1000))) # Extract OD chapter info from mp3s for use in merged file if 'TXXX:OverDrive MediaMarkers' in mutagen_audio.tags \ and mutagen_audio.tags['TXXX:OverDrive MediaMarkers'].text: marker_text = mutagen_audio.tags[ 'TXXX:OverDrive MediaMarkers'].text[0] try: tree = xml.etree.ElementTree.fromstring(marker_text) except UnicodeEncodeError: tree = xml.etree.ElementTree.fromstring( marker_text.encode('ascii', 'ignore').decode('ascii')) for m in tree.iter('Marker'): marker_name = m.find('Name').text.strip() marker_timestamp = m.find('Time').text timestamp = None ts_mark = 0 # 2 timestamp formats found for r in ('%M:%S.%f', '%H:%M:%S.%f'): try: timestamp = time.strptime(marker_timestamp, r) ts = datetime.timedelta(hours=timestamp.tm_hour, minutes=timestamp.tm_min, seconds=timestamp.tm_sec) ts_mark = int(1000 * ts.total_seconds()) break except ValueError: pass if not timestamp: # some invalid timestamp string, e.g. 60:15.00 mobj = re.match(MARKER_TIMESTAMP_HHMMSS, marker_timestamp) if mobj: ts_mark = int(mobj.group('hr')) * 60 * 60 * 1000 + \ int(mobj.group('min')) * 60 * 1000 + \ int(mobj.group('sec')) * 1000 + \ int(mobj.group('ms')) else: mobj = re.match(MARKER_TIMESTAMP_MMSS, marker_timestamp) if mobj: ts_mark = int(mobj.group('min')) * 60 * 1000 + \ int(mobj.group('sec')) * 1000 + \ int(mobj.group('ms')) else: raise ValueError( 'Invalid marker timestamp: {}'.format( marker_timestamp)) track_count += 1 part_markers.append((u'ch{:02d}'.format(track_count), marker_name, ts_mark)) if args.add_chapters and not args.merge_output: # set the chapter marks generated_markers = [] for j, file_marker in enumerate(part_markers): generated_markers.append({ 'id': file_marker[0], 'text': file_marker[1], 'start_time': int(file_marker[2]), 'end_time': int( round(mutagen_audio.info.length * 1000) if j == (len(part_markers) - 1) else part_markers[j + 1][2]), }) mutagen_audio.tags.add( CTOC(element_id=u'toc', flags=CTOCFlags.TOP_LEVEL | CTOCFlags.ORDERED, child_element_ids=[ m['id'].encode('ascii') for m in generated_markers ], sub_frames=[TIT2(text=[u'Table of Contents'])])) for i, m in enumerate(generated_markers): mutagen_audio.tags.add( CHAP(element_id=m['id'].encode('ascii'), start_time=m['start_time'], end_time=m['end_time'], sub_frames=[TIT2(text=[u'{}'.format(m['text'])]) ])) start_time = datetime.timedelta( milliseconds=m['start_time']) end_time = datetime.timedelta(milliseconds=m['end_time']) logger.debug( u'Added chap tag => {}: {}-{} "{}" to "{}"'.format( colored.cyan(m['id']), start_time, end_time, colored.cyan(m['text']), colored.blue(part_filename))) if len(generated_markers) == 1: # Weird player problem on voice where title is shown instead of chapter title mutagen_audio.tags.add( TIT2(encoding=3, text=u'{}'.format(title))) mutagen_audio.save() except Exception as e: logger.warning('Error saving ID3: {}'.format( colored.red(str(e), bold=True))) keep_cover = True logger.info('Saved "{}"'.format(colored.magenta(part_filename))) file_tracks.append({ 'file': part_filename, 'markers': part_markers, }) # end loop: for p in download_parts: debug_meta['audio_lengths_ms'] = audio_lengths_ms debug_meta['file_tracks'] = file_tracks if args.merge_output: if os.path.isfile(book_filename if args.merge_format == 'mp3' else book_m4b_filename): logger.warning('Already saved "{}"'.format( colored.magenta(book_filename if args.merge_format == 'mp3' else book_m4b_filename))) sys.exit(0) logger.info('Generating "{}"...'.format( colored.magenta(book_filename if args.merge_format == 'mp3' else book_m4b_filename))) # We can't directly generate a m4b here even if specified because eyed3 doesn't support m4b/mp4 temp_book_filename = '{}.part'.format(book_filename) cmd = [ 'ffmpeg', '-y', '-nostdin', '-hide_banner', '-loglevel', 'info' if logger.level == logging.DEBUG else 'error', '-stats', '-i', 'concat:{}'.format('|'.join([ft['file'] for ft in file_tracks])), '-acodec', 'copy', '-b:a', '64k', # explicitly set audio bitrate '-f', 'mp3', temp_book_filename ] exit_code = subprocess.call(cmd) if exit_code: logger.error( 'ffmpeg exited with the code: {0!s}'.format(exit_code)) logger.error('Command: {0!s}'.format(' '.join(cmd))) exit(exit_code) os.rename(temp_book_filename, book_filename) mutagen_audio = MP3(book_filename, ID3=ID3) if not mutagen_audio.tags: mutagen_audio.tags = ID3() # Overwrite title since it gets picked up from the first track by default mutagen_audio.tags.add(TIT2(encoding=3, text=u'{}'.format(title))) if 'TALB' not in mutagen_audio.tags: mutagen_audio.tags.add(TALB(encoding=3, text=u'{}'.format(title))) if 'TPE1' not in mutagen_audio.tags: mutagen_audio.tags.add( TPE1(encoding=3, text=u'{}'.format(authors[0]))) if 'TPE2' not in mutagen_audio.tags: mutagen_audio.tags.add( TPE2(encoding=3, text=u'{}'.format(authors[0]))) if 'TPUB' not in mutagen_audio.tags: mutagen_audio.tags.add( TPUB(encoding=3, text=u'{}'.format(publisher))) if 'COMM' not in mutagen_audio.tags: mutagen_audio.tags.add( COMM(encoding=3, desc=u'Description', text=u'{}'.format(description))) mutagen_audio.save() if args.add_chapters: merged_markers = [] for i, f in enumerate(file_tracks): prev_tracks_len_ms = 0 if i == 0 else reduce( lambda x, y: x + y, audio_lengths_ms[0:i]) this_track_endtime_ms = int( reduce(lambda x, y: x + y, audio_lengths_ms[0:i + 1])) file_markers = f['markers'] for j, file_marker in enumerate(file_markers): merged_markers.append({ 'id': file_marker[0], 'text': u'{}'.format(file_marker[1]), 'start_time': int(file_marker[2]) + prev_tracks_len_ms, 'end_time': int(this_track_endtime_ms if j == ( len(file_markers) - 1) else file_markers[j + 1][2] + prev_tracks_len_ms), }) debug_meta['merged_markers'] = merged_markers mutagen_audio.tags.add( CTOC(element_id=u'toc', flags=CTOCFlags.TOP_LEVEL | CTOCFlags.ORDERED, child_element_ids=[ m['id'].encode('ascii') for m in merged_markers ], sub_frames=[TIT2(text=[u"Table of Contents"])])) for i, m in enumerate(merged_markers): mutagen_audio.tags.add( CHAP(element_id=m['id'].encode('ascii'), start_time=m['start_time'], end_time=m['end_time'], sub_frames=[TIT2(text=[u'{}'.format(m['text'])])])) start_time = datetime.timedelta(milliseconds=m['start_time']) end_time = datetime.timedelta(milliseconds=m['end_time']) logger.debug( u'Added chap tag => {}: {}-{} "{}" to "{}"'.format( colored.cyan(m['id']), start_time, end_time, colored.cyan(m['text']), colored.blue(book_filename))) mutagen_audio.save() if args.merge_format == 'mp3': logger.info('Merged files into "{}"'.format( colored.magenta(book_filename if args.merge_format == 'mp3' else book_m4b_filename))) if args.merge_format == 'm4b': temp_book_m4b_filename = '{}.part'.format(book_m4b_filename) cmd = [ 'ffmpeg', '-y', '-nostdin', '-hide_banner', '-loglevel', 'info' if logger.level == logging.DEBUG else 'error', '-stats', '-i', book_filename, ] if os.path.isfile(cover_filename): cmd.extend(['-i', cover_filename]) cmd.extend([ '-map', '0:a', '-c:a', 'aac', '-b:a', '64k', # explicitly set audio bitrate ]) if os.path.isfile(cover_filename): cmd.extend([ '-map', '1:v', '-c:v', 'copy', '-disposition:v:0', 'attached_pic', ]) cmd.extend(['-f', 'mp4', temp_book_m4b_filename]) exit_code = subprocess.call(cmd) if exit_code: logger.error( 'ffmpeg exited with the code: {0!s}'.format(exit_code)) logger.error('Command: {0!s}'.format(' '.join(cmd))) exit(exit_code) os.rename(temp_book_m4b_filename, book_m4b_filename) logger.info('Merged files into "{}"'.format( colored.magenta(book_m4b_filename))) try: os.remove(book_filename) except Exception as e: logger.warning('Error deleting "{}": {}'.format( book_filename, str(e))) if not args.keep_mp3: for f in file_tracks: try: os.remove(f['file']) except Exception as e: logger.warning('Error deleting "{}": {}'.format( f['file'], str(e))) if not keep_cover: try: os.remove(cover_filename) except Exception as e: logger.warning('Error deleting "{}": {}'.format( cover_filename, str(e))) if args.write_json: with open(debug_filename, 'w') as outfile: json.dump(debug_meta, outfile, indent=2)
def update_id3(self, path: str, track: beatport.Track): #AIFF Check aiff = None if path.endswith('.aiff') or path.endswith('.aif'): aiff = AIFF(path) f = aiff.tags else: f = ID3() f.load(path, v2_version=3, translate=True) #Update tags if UpdatableTags.title in self.config.update_tags and self.config.overwrite: f.setall('TIT2', [TIT2(text=track.title)]) if UpdatableTags.artist in self.config.update_tags and self.config.overwrite: f.setall('TPE1', [ TPE1(text=self.config.artist_separator.join( [a.name for a in track.artists])) ]) if UpdatableTags.album in self.config.update_tags and ( self.config.overwrite or len(f.getall('TALB')) == 0): f.setall('TALB', [TALB(text=track.album.name)]) if UpdatableTags.label in self.config.update_tags and ( self.config.overwrite or len(f.getall('TPUB')) == 0): f.setall('TPUB', [TPUB(text=track.label.name)]) if UpdatableTags.bpm in self.config.update_tags and ( self.config.overwrite or len(f.getall('TBPM')) == 0): f.setall('TBPM', [TBPM(text=str(track.bpm))]) if UpdatableTags.genre in self.config.update_tags and ( self.config.overwrite or len(f.getall('TCON')) == 0): f.setall('TCON', [TCON(text=', '.join([g.name for g in track.genres]))]) #Dates if UpdatableTags.date in self.config.update_tags: #ID3 v2.3 if self.config.id3v23 and (self.config.overwrite or (len(f.getall('TYER')) == 0 and len(f.getall('TDAT')) == 0)): date = track.release_date.strftime('%d%m') f.setall('TDRC', []) f.setall('TDAT', [TDAT(text=date)]) f.setall('TYER', [TYER(text=str(track.release_date.year))]) #ID3 v2.4 if not self.config.id3v23 and (self.config.overwrite or len(f.getall('TDRC')) == 0): date = track.release_date.strftime('%Y-%m-%d') f.setall('TDAT', []) f.setall('TYER', []) f.setall('TDRC', [TDRC(text=date)]) if UpdatableTags.key in self.config.update_tags and ( self.config.overwrite or len(f.getall('TKEY')) == 0): f.setall('TKEY', [TKEY(text=track.id3key())]) if UpdatableTags.publishdate in self.config.update_tags and ( self.config.overwrite or len(f.getall('TDRL')) == 0): # f.setall('TORY', [TORY(text=str(track.publish_date.year))]) if not self.config.id3v23: date = track.publish_date.strftime('%Y-%m-%d') f.setall('TDRL', [TDRL(text=date)]) #Other keys if UpdatableTags.other in self.config.update_tags: f.add(TXXX(desc='WWWAUDIOFILE', text=track.url())) f.add(TXXX(desc='WWWPUBLISHER', text=track.label.url('label'))) #Redownlaod cover if self.config.replace_art: try: url = track.art(self.config.art_resolution) r = requests.get(url) data = APIC(encoding=3, mime='image/jpeg', type=3, desc=u'Cover', data=r.content) f.delall('APIC') f['APIC:cover.jpg'] = data except Exception: logging.warning('Error downloading cover for file: ' + path) if aiff == None: if self.config.id3v23: f.save(path, v2_version=3, v1=0) else: f.save(path, v2_version=4, v1=0) else: aiff.save()
from segevmusic.applemusic import AMSong from mutagen.id3 import ID3, TXXX, TIT2, TPE1, TALB, TPE2, TCON, TPUB, TSRC, APIC, TCOP, TDRC, TRCK, TPOS from os import replace from os.path import realpath, join from typing import List TAGS = { "song_name": lambda amsong: TIT2(text=amsong.name), "album_name": lambda amsong: TALB(text=amsong.album_name), "isrc": lambda amsong: TSRC(text=amsong.isrc), "record_label": lambda amsong: TPUB(text=amsong.album.record_label) if amsong.album.record_label else None, "copyright": lambda amsong: TCOP(text=amsong.album.copyright), "genre": lambda amsong: TCON(text=amsong.genres[0]), "album_artist": lambda amsong: TPE2(text=amsong.album.artist_name), "song_artist": lambda amsong: TPE1(text=amsong.artist_name), "itunes_advisory": lambda amsong: TXXX(desc="ITUNESADVISORY", text="1") if amsong.is_explicit else None, "release_date": lambda amsong: TDRC(text=amsong.release_date), "artwork": lambda amsong: APIC(mime='image/jpeg',
def save_mp3_metadata(self, file_path, data): """ Saves the given metadata for an MP3 file. Metadata.save_mp3_metadata(str, dict) """ audio = ID3(file_path) # Writing MP3 tags needs ID3 mp3_audio, format = self.get_mutagen_parser(file_path) if data.get("title"): audio.add(TIT2(encoding=3, text=unicode(data['title']))) if data.get("artist"): audio.add(TPE1(encoding=3, text=unicode(data['artist']))) if data.get("album"): audio.add(TALB(encoding=3, text=unicode(data['album']))) if data.get("genre"): audio.add(TCON(encoding=3, text=unicode(data['genre']))) if data.get("year"): audio.add(TDRC(encoding=3, text=unicode(data['year']))) if data.get("album_artist"): audio.add(TPE2(encoding=3, text=unicode(data['album_artist']))) if data.get("track_number", False) and data.get("total_tracks", False): audio.add( TRCK(encoding=3, text=unicode(data['track_number'] + '/' + data['total_tracks']))) elif data.get("track_number"): total_tracks = self.get_total_tracks(mp3_audio, format) if total_tracks == None: audio.add(TRCK(encoding=3, text=unicode(data['track_number']))) else: audio.add( TRCK(encoding=3, text=unicode(data['track_number'] + '/' + total_tracks))) elif data.get("total_tracks"): t_no = self.get_track_number(mp3_audio, format) if t_no == None: audio.add( TRCK(encoding=3, text=unicode(" /" + data["total_tracks"]))) else: audio.add( TRCK(encoding=3, text=unicode(t_no + '/' + data["total_tracks"]))) if data.get("disc_number", False) and data.get("total_discs", False): audio.add( TPOS(encoding=3, text=unicode(data['disc_number'] + '/' + data['total_discs']))) elif data.get("disc_number"): total_discs = self.get_total_discs(mp3_audio, format) if total_discs == None: audio.add(TPOS(encoding=3, text=unicode(data['disc_number']))) else: audio.add( TPOS(encoding=3, text=unicode(data['disc_number'] + '/' + total_discs))) elif data.get("total_discs"): d_no = self.get_disc_number(mp3_audio, format) if d_no == None: audio.add( TPOS(encoding=3, text=unicode(" /" + data["total_discs"]))) else: audio.add( TPOS(encoding=3, text=unicode(d_no + '/' + data["total_discs"]))) if data.get("composer"): audio.add(TCOM(encoding=3, text=unicode(data['composer']))) if data.get("publisher"): audio.add(TPUB(encoding=3, text=unicode(data['publisher']))) if data.get("comments"): audio.add( COMM(encoding=3, lang="eng", desc="", text=unicode(data['comments']))) audio.save()
def as_mp3(self): """ Embed metadata to MP3 files. """ music_file = self.music_file meta_tags = self.meta_tags # EasyID3 is fun to use ;) # For supported easyid3 tags: # https://github.com/quodlibet/mutagen/blob/master/mutagen/easyid3.py # Check out somewhere at end of above linked file audiofile = EasyID3(music_file) audiofile['artist'] = meta_tags['artists'][0]['name'] audiofile['albumartist'] = meta_tags['artists'][0]['name'] audiofile['album'] = meta_tags['album']['name'] audiofile['title'] = meta_tags['name'] audiofile['tracknumber'] = [ meta_tags['track_number'], meta_tags['total_tracks'] ] audiofile['discnumber'] = [meta_tags['disc_number'], 0] audiofile['date'] = meta_tags['release_date'] audiofile['originaldate'] = meta_tags['release_date'] audiofile['media'] = meta_tags['type'] audiofile['author'] = meta_tags['artists'][0]['name'] audiofile['lyricist'] = meta_tags['artists'][0]['name'] audiofile['arranger'] = meta_tags['artists'][0]['name'] audiofile['performer'] = meta_tags['artists'][0]['name'] audiofile['website'] = meta_tags['external_urls']['spotify'] audiofile['length'] = str(meta_tags['duration']) if meta_tags['publisher']: audiofile['encodedby'] = meta_tags['publisher'] if meta_tags['genre']: audiofile['genre'] = meta_tags['genre'] if meta_tags['copyright']: audiofile['copyright'] = meta_tags['copyright'] if meta_tags['external_ids']['isrc']: audiofile['isrc'] = meta_tags['external_ids']['isrc'] audiofile.save(v2_version=3) # For supported id3 tags: # https://github.com/quodlibet/mutagen/blob/master/mutagen/id3/_frames.py # Each class represents an id3 tag audiofile = ID3(music_file) audiofile['TORY'] = TORY(encoding=3, text=meta_tags['year']) audiofile['TYER'] = TYER(encoding=3, text=meta_tags['year']) audiofile['TPUB'] = TPUB(encoding=3, text=meta_tags['publisher']) audiofile['COMM'] = COMM(encoding=3, text=meta_tags['external_urls']['spotify']) if meta_tags['lyrics']: audiofile['USLT'] = USLT(encoding=3, desc=u'Lyrics', text=meta_tags['lyrics']) try: albumart = urllib.request.urlopen( meta_tags['album']['images'][0]['url']) audiofile['APIC'] = APIC(encoding=3, mime='image/jpeg', type=3, desc=u'Cover', data=albumart.read()) albumart.close() except IndexError: pass audiofile.save(v2_version=3) return True
fd.tags[u'ARTIST'] = u"Waatea" fd.tags[u'ORGANIZATION'] = u"*** NEWS ***" fd.tags[u'LABEL'] = u"*** NEWS *** Updated-%s" % ( datetime.now().strftime('%H:%M-%d-%m-%Y')) fd.tags[u'PUBLISHER'] = u"*** NEWS ***" fd.tags[u'UFID'] = u"1840-WAATEA-NEWS-%02d%s-MP3" % (hour, ampm.upper()) fd.tags[u'OWNER'] = u"admin" fd.tags[u'LENGTH'] = u"%d:%02d.%d" % (min, sec, hund) fd.tags[u'TLEN'] = u"%d:%02d.%d" % (min, sec, hund) retval = fd.save() print(retval) print(fd.tags) from mutagen.id3 import ID3, TPUB audio = ID3(f_name) audio.add(TPUB(encoding=3, text=u"*** NEWS ***")) audio.save() f_name_abs = os.path.join(file_path, f_name) os.rename(f_name, f_name_abs) cmd = 'sudo chown www-data {0}'.format(f_name_abs) p = subprocess.Popen(cmd.split(' ')) p.communicate() cmd = 'sudo chgrp www-data {0}'.format(f_name_abs) p = subprocess.Popen(cmd.split(' ')) p.communicate() td = (datetime.now() - start_time) print('elapsed time = %s' % (td.seconds))