def merge_mp3_files(self, merged): if len(self.project.tracks) == 1: self.parent.update(25) shutil.copyfile(self.project.tracks[0], merged) self.parent.update(100) return cmd = [ivonet.APP_MP3_BINDER, '-out', merged] [cmd.append(mp3) for mp3 in self.project.tracks] self.subprocess(cmd) log(f"Merging mp3 files for: {self.project.title}") total = len(self.project.tracks) count = 0 while self.keep_going: try: line = self.process.stdout.readline() except UnicodeDecodeError: # just skip a line continue dbg(line) if not line: dbg(f"Finished Merge for: {self.project.title}") break if "Processing:" in line: count += 1 self.parent.update(int((count * 100) / total)) self.__check_process(cmd)
def create_chapters(self, chapter_file, m4b): cmd = [ivonet.APP_MP4_CHAPS, ] if self.project.chapter_method == CHAPTER_LIST[0]: with open(chapter_file, "w") as fo: chapters = self.project.chapter_file() dbg(chapters) fo.write(chapters) self.parent.update(10) cmd.append("-i") else: fixed = int(self.project.chapter_method.split()[1].strip()) * 60 cmd.append("-e") cmd.append(str(fixed)) cmd.append(m4b) self.subprocess(cmd) log(f"Adding chapter information to: {self.project.title}") while self.keep_going: try: line = self.process.stdout.readline() except UnicodeDecodeError: # just skip a line should not effect the progress continue if not line: dbg(f"Chapter information done: {self.project.title}") break dbg(line) if "QuickTime" in line: self.parent.update(50) self.__check_process(cmd) self.parent.update(100)
def add_cover_art(self, cover, m4b): """add_cover_art(cover_name, audiobook_file) -> audiobook file with cover art Saved the CoverArt from the project to disc and adds it to the audiobook. """ img = wx.Image(BytesIO(self.project.cover_art), wx.BITMAP_TYPE_ANY) img.SaveFile(cover, wx.BITMAP_TYPE_PNG) self.parent.update(10) cmd = [ivonet.APP_MP4_ART, "--add", cover, m4b] self.subprocess(cmd) log(f"Adding Cover Art to: {self.project.title}") while self.keep_going: try: line = self.process.stdout.readline() except UnicodeDecodeError: # just skip a line. continue if not line: dbg(f"Finished Adding CoverArt to: {self.project.title}") break dbg(line) if "adding" in line: self.parent.update(50) self.__check_process(cmd) self.parent.update(100)
def on_file_history(self, event): """Handler for the event on file history selection in the file menu""" file_num = event.GetId() - wx.ID_FILE1 path = self.GetMenuBar().file_history.GetHistoryFile(file_num) log(f"You selected {path}") wx.PostEvent(self, ProjectHistoryEvent(path=path)) self.project_open(path)
def on_disc(self, event): """Handler for the disc and disc_total field events as they are linked""" if not self.check_disc(): log("Corrected disk total as it can not be smaller than the disk.") self.project.disc = self.sc_disc.GetValue() self.project.disc_total = self.sc_disk_total.GetValue() event.Skip()
def on_error(self, event: ProcessExceptionEvent): log("Processing stopped because an error occurred:", event.project.title) dbg("Processing error", str(event.cmd)) self.filename.SetForegroundColour(wx.RED) self.Refresh() self.stop()
def set_disc_total(self, value): """Sets the disc total. It will always set it and that will trigger the on_disc handler to check if all is well... """ self.target.sc_disk_total.SetValue(int(value)) if not self.target.check_disc(): log("Corrected disk total as it can not be smaller than the disk.")
def project_open(self, path): """Open a saved project""" try: with open(path, 'rb') as fi: self.project = pickle.load(fi) self.project.refresh_after_pickle() self.project.name = path self.reset_metadata(self.project) except FileNotFoundError: log(f"File: {path} could not be opened.")
def set_genre(self, value): """Sets the genre. It assumes that if the field has already been set either by a previous event or manually this event can be ignored. In this case we need a 'dirty' flag to do this as the field is a drop down and is never empty This event is less important then manual or previous set values. """ if self.target.genre_pristine: if value in GENRES: self.target.genre_pristine = False self.target.cb_genre.SetValue(value) else: if not self.genre_logged: log(f"Genre {value} from the metadata is not a known genre.") self.genre_logged = True
def on_open_project(self, event): """Handles the open project event.""" self.status("Open Project") with wx.FileDialog(self, message="Choose a file...", defaultDir=os.getcwd(), defaultFile="", wildcard=ivonet.FILE_WILDCARD_PROJECT, style=wx.FD_OPEN | wx.FD_CHANGE_DIR | wx.FD_FILE_MUST_EXIST | wx.FD_PREVIEW) as open_dlg: if open_dlg.ShowModal() == wx.ID_OK: path = open_dlg.GetPath() log(f"Opening file: {path}") self.project_open(path) wx.PostEvent(self, ProjectHistoryEvent(path=path)) event.Skip()
def convert_2_m4a(self, activation_bytes): cmd = [ ivonet.APP_FFMPEG, "-activation_bytes", activation_bytes, '-i', self.project, "-stats", "-y", "-vn", "-c:a", "copy", "-acodec", "aac", "-movflags", "use_metadata_tags", "-map_metadata", "0", "-map_metadata:s:a", "0:s:a", self.m4a, ] dbg(cmd) self.subprocess(cmd) log(f"Conversion has started for: {self.project}") while self.keep_going: try: line = self.process.stdout.readline() except UnicodeDecodeError: # just skip a line continue if not line: dbg(f"Conversion finished for: {self.project}") break dbg(line) duration = self.DURATION.match(line) if duration: self.total_duration = time_seconds(duration.groups()) continue elapsed = self.TIME_ELAPSED.match(line) if elapsed: self.progress = self.calc_percentage_done(elapsed.groups()) self.parent.update(self.progress) self.__check_process(cmd)
def add_metadata(self, m4a): cmd = [ ivonet.APP_ATOMIC_PARSLEY, m4a, "--title", f"{self.project.title}", "--grouping", f"{self.project.grouping}", "--sortOrder", 'album', f"{self.project.grouping}", "--album", f"{self.project.title}", "--artist", f"{self.project.artist}", "--genre", f"{self.project.genre}", "--tracknum", f"{self.project.disc}/{self.project.disc_total}", "--disk", f"{self.project.disc}/{self.project.disc_total}", "--comment", f"""{self.project.get_comment()}""", "--year", f"{self.project.year}", "--encodingTool", f"{ivonet.TXT_APP_NAME} ({ivonet.TXT_APP_TINY_URL})", "--stik", "Audiobook", "--overWrite" ] self.subprocess(cmd) log(f"Adding metadata to: {self.project.title}") while self.keep_going: try: line = self.process.stdout.readline() except UnicodeDecodeError: # just skip a line continue if not line: dbg(f"Finished Adding metadata to {self.project.title}") break dbg(line) if "Progress:" in line: ret = line.split("%") if len(ret) > 1: try: percentage = int(ret[0].split()[-1]) self.parent.update(percentage) dbg(percentage) except (IndexError, ValueError): # Just ignore... probably bad line pass self.parent.update(100) self.__check_process(cmd)
def OnDropFiles(self, x, y, filenames): dbg("MP3 Files dropped", filenames) for name in filenames: if name.lower().endswith(ivonet.FILE_EXTENSION): log("Recognized project file. Opening...") self.target.project_open(name) return True if name.lower().endswith( ".aax" ) and name not in self.target.lc_audiofiles.GetStrings(): self.target.append_track(name) else: log(f"Dropped file '{name}' is not an mp3 file or not unique in the list." ) return False self.genre_logged = False self.disc_correction_logged = False return True
def add_metadata(self, metadata): """AtomicParsley "${AUDIOBOOK}.m4a" --title "${TITLE}" --grouping "${GROUPING}" --sortOrder album "${GROUPING}" --album "${ALBUM}" --artist "${AUTHOR}" --genre "${GENRE}" --tracknum "${TRACK}" --disk "${TRACK}" --comment "${COMMENT}" --year "${YEAR}" --stik Audiobook --overWrite""" tags = metadata["format"]["tags"] cmd = [ ivonet.APP_ATOMIC_PARSLEY, self.m4a, "--title", f"{tags['title']}", "--album", f"{tags['album']}", "--artist", f"{tags['artist']}", "--genre", f"{tags['genre']}", "--comment", f"""{tags['comment']}""", "--year", f"{tags['date']}", "--encodingTool", f"{ivonet.TXT_APP_NAME} ({ivonet.TXT_APP_TINY_URL})", "--stik", "Audiobook", "--overWrite" ] dbg(cmd) self.subprocess(cmd) log(f"Adding metadata to: {self.project}") while self.keep_going: try: line = self.process.stdout.readline() except UnicodeDecodeError: # just skip a line continue if not line: dbg(f"Finished Adding metadata to {self.project}") break dbg(line) if "Progress:" in line: ret = line.split("%") if len(ret) > 1: try: percentage = int(ret[0].split()[-1]) self.parent.update(percentage) dbg(percentage) except (IndexError, ValueError): # Just ignore... probably bad line pass self.parent.update(100) self.__check_process(cmd)
def add_cover_art(self): self.parent.update(10) cmd = [ivonet.APP_MP4_ART, "--add", self.cover, self.m4b] self.subprocess(cmd) log(f"Adding Cover Art to: {self.project}") while self.keep_going: try: line = self.process.stdout.readline() except UnicodeDecodeError: # just skip a line. continue if not line: dbg(f"Finished Adding CoverArt to: {self.project}") break dbg(line) if "adding" in line: self.parent.update(50) self.__check_process(cmd) self.parent.update(100)
def convert_2_m4a(self, merged, m4a): cmd = [ivonet.APP_FFMPEG, '-i', merged, "-stats", "-threads", "4", "-vn", "-y", "-acodec", "aac", "-strict", "-2", "-map_metadata", "0", "-map_metadata:s:a", "0:s:a", "-ac", "1", m4a, ] self.subprocess(cmd) log(f"Conversion has started for: {self.project.title}") while self.keep_going: try: line = self.process.stdout.readline() except UnicodeDecodeError: # just skip a line continue if not line: dbg(f"Conversion finished for: {self.project.title}") break dbg(line) duration = self.DURATION.match(line) if duration: self.total_duration = time_seconds(duration.groups()) continue elapsed = self.TIME_ELAPSED.match(line) if elapsed: self.progress = self.calc_percentage_done(elapsed.groups()) self.parent.update(self.progress) self.__check_process(cmd)
def set_cover_art(self, image): """handles the 'cover_art.force' and 'track.cover_art' events. gets an image file object or file location as input. """ self.project.cover_art = image try: img = wx.Image(BytesIO(image), wx.BITMAP_TYPE_ANY) width = img.GetWidth() height = img.GetHeight() except AssertionError as e: dbg(e) log("CoverArt found but unknown format") return pnl_width, pnl_height = self.cover_art_panel.GetSize() if width > height: new_width = pnl_width new_height = pnl_width * height / width else: new_height = pnl_height new_width = pnl_height * width / height self.cover_art.SetBitmap(wx.Bitmap(img.Scale(new_width, new_height))) self.cover_art.Center() self.cover_art.Refresh()
def activation_bytes(self, rt_file, checksum): cmd = [ivonet.APP_RCRACK, rt_file, "-h", checksum] dbg(cmd) self.subprocess(cmd) log(f"RainbowCrack: {self.project}") ret = None while self.keep_going: try: line = self.process.stdout.readline() except UnicodeDecodeError: continue dbg(line) if not line: dbg(f"Finished Merge for: {self.project}") break if "hex:" in line: ret = line.split("hex:")[1] if not ret or "notfound" in ret: ret = None self.__check_process(cmd) self.parent.update(100) return ret.strip()
def save_project(window, project): filename = project.title or "Untitled" if project.disc_total > 1: filename += f".Part {project.disc}" filename += ivonet.FILE_EXTENSION base_dir = None if project.name: base_dir, filename = os.path.split(project.name) elif project.tracks: base_dir = os.path.split(project.tracks[0])[0] default_dir = os.environ["HOME"] or os.getcwd() with wx.FileDialog(window, message="Save file as ...", defaultDir=default_dir, defaultFile=f"{filename}", wildcard=ivonet.FILE_WILDCARD_PROJECT, style=wx.FD_SAVE ) as save_dlg: save_dlg.SetFilterIndex(0) if base_dir: save_dlg.SetDirectory(base_dir) if save_dlg.ShowModal() == wx.ID_OK: path = save_dlg.GetPath() if not path.endswith(ivonet.FILE_EXTENSION): path += ivonet.FILE_EXTENSION with open(path, 'wb') as fo: dbg("Saving project file.." + str(project)) project.name = path pickle.dump(project, fo) wx.PostEvent(window, ProjectHistoryEvent(path=path)) log(f'Saved to: {path}')
def OnDropFiles(self, x, y, filenames): dbg("MP3 Files dropped", filenames) for name in filenames: if name.lower().endswith(ivonet.FILE_EXTENSION): log("Recognized project file. Opening...") self.target.project_open(name) return True if name.lower().endswith(".mp3") and name not in self.target.lc_mp3.GetStrings(): self.target.append_track(name) tag = TinyTag.get(name, image=True, ignore_errors=True) if tag.get_image(): self.target.set_cover_art(tag.get_image()) for item, mapping in methods: dbg("method: ", item) value = getattr(tag, item) if value: dbg("Value:", value) getattr(self, f"set_{mapping}")(value.strip()) else: log(f"Dropped file '{name}' is not an mp3 file or not unique in the list.") self.genre_logged = False self.disc_correction_logged = False return True
def update_check(self, event): response = requests.get(ivonet.UPDATE_URL) if response.ok and ivonet.VERSION != response.text: log(f"Update {response.text} is available for download.") log("For update goto: https://m4baker.ivonet.nl ")
def run(self): self.running = True log(f"Creating: {self.m4b}") self.parent.stage = 1 checksum = ffprobe.checksum(self.project) log(f"Checksum for [{self.project}] is [{checksum}]") if not checksum: wx.PostEvent( self.parent, ProcessExceptionEvent(msg="No checksum retrieved.", project=self.project)) return self.parent.stage = 2 activation_bytes = self.get_activation_bytes(checksum) log(f"Activation bytes for [{self.project}] are [{activation_bytes}]") if not activation_bytes: wx.PostEvent( self.parent, ProcessExceptionEvent(msg="No activation bites found.", project=self.project)) return if not self.keep_going: self.running = False return self.parent.stage = 3 metadata = ffprobe.metadata(self.project) if not self.keep_going: self.running = False return self.parent.stage = 4 self.convert_2_m4a(activation_bytes) if not self.keep_going: self.running = False return self.parent.stage = 5 self.add_metadata(metadata) if not self.keep_going: self.running = False return self.parent.stage = 7 self.extract_cover() if not self.keep_going: self.running = False return self.parent.stage = 8 self.add_cover_art() self.cleanup() if self.keep_going: self.parent.update(100) wx.PostEvent(self.parent, ProcessDoneEvent()) self.running = False self.keep_going = False log(f"Created: {self.m4b}")
def run(self): self.running = True with tempfile.TemporaryDirectory() as project_tmpdir: dbg("Temp dir:", project_tmpdir) log(f"Creating: {self.project.m4b_name}") # bind all mp3 into one file (mp3binder) self.parent.stage = 0 merged = os.path.join(project_tmpdir, "merged.mp3") self.merge_mp3_files(merged) self.process = None if not self.keep_going: self.running = False return # Convert ffmpeg self.parent.stage = 1 m4a = os.path.join(project_tmpdir, "converted.m4a") self.convert_2_m4a(merged, m4a) self.process = None if not self.keep_going: self.running = False return # Add metadata tags self.parent.stage = 2 self.add_metadata(m4a) self.process = None if not self.keep_going: self.running = False return # Add chapters self.parent.stage = 3 m4b = os.path.join(project_tmpdir, "converted.m4b") chapter_file = os.path.join(project_tmpdir, "converted.chapters.txt") self.create_chapters(chapter_file, m4b) self.process = None if not self.keep_going: self.running = False return # Add CoverArt self.parent.stage = 4 cover = os.path.join(project_tmpdir, "cover.png") self.add_cover_art(cover, m4b) self.process = None if not self.keep_going: self.running = False return self.parent.stage = 5 self.parent.update(25) log(f"Moving completed audiobook [{self.project.title}] to final destination.") try: basename, ext = os.path.splitext(unique_name(self.project.m4b_name)) shutil.move(m4b, unique_name(self.project.m4b_name)) self.parent.update(50) shutil.move(cover, unique_name(basename + ".png")) if os.path.isfile(chapter_file): shutil.move(chapter_file, unique_name(basename + ".chapters.txt")) except InputError as e: log(e.message) self.keep_going = False if self.keep_going: self.parent.update(100) wx.PostEvent(self.parent, ProcessDoneEvent()) log(f"Created: {self.project.m4b_name}") else: log("Something went wrong and the conversion could not complete successfully.") log("Please try again and if the problem persists you can PM me on twitter") log("@ivonet with a description of when and where it goes wrong.") log("Thanks for the help.") self.running = False self.keep_going = False