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 extract_cover(self): dbg("Extracting cover...") ret = subprocess.getstatusoutput( f'ffmpeg -i "{self.project}" -y -v quiet -an -vcodec copy "{self.cover}"' ) if ret[0] != 0: self.keep_going = False
def checksum(filename) -> str: keep_going = True cmd = [ivonet.APP_FFPROBE, f"{filename}"] process = shell_command(cmd) ret = None while keep_going: try: line = process.stdout.readline() except UnicodeDecodeError: # just skip a line continue dbg(line) if not line: dbg(f"No more input from subprocess") keep_going = False break # print(line) if "checksum == " in line: ret = line.split("== ")[1] process.stdout.close() if process.returncode != 0 and keep_going: raise IOError("Could not retrieve checksum") return ret.strip()
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 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 main(): wx.SystemOptions.SetOption("mac.window-plain-transition", 1) wx.SystemOptions.SetOption("mac.textcontrol-use-spell-checker", 1) app = M4Baker() frame = MainFrame(None, title="M4Baker", size=(1024, 768)) frame.Show(True) dbg("M4Baker initialized.") app.MainLoop()
def __check_process(self, cmd): if self.process and not self.keep_going: self.process.terminate() self.process.stdout.close() self.process.wait() if self.process.returncode != 0 and self.keep_going: # Only throw an exception if the process terminated wrong # but we wanted to keep going self.keep_going = False dbg("Process exitcode: ", self.process.returncode) wx.PostEvent(self.parent, ProcessExceptionEvent(cmd=cmd, project=self.project))
def on_queue(self, event): """Handles the queue event from either the toolbar or the File menu (shortcut).""" if not self.project.verify(): dbg("You slipped between verifies :-) no no no processing allowed.") return self.status("Queueing audiobook...") for aax in self.project.tracks: self.queue_project(aax) self.on_clear(event) event.Skip()
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_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 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 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 on_queue(self, event): """Handles the queue event from either the toolbar or the File menu (shortcut).""" if not self.project.verify(): dbg("You slipped between verifies :-) no no no processing allowed." ) return self.status("Queueing audiobook...") base_dir = None if self.project.name: base_dir, filename = os.path.split(self.project.name) elif self.project.tracks: base_dir = os.path.split(self.project.tracks[0])[0] with wx.FileDialog(self, "Save audiobook...", defaultDir=self.default_save_path, defaultFile=self.project.final_name(), wildcard=ivonet.FILE_WILDCARD_M4B, style=wx.FD_SAVE) as fileDialog: if base_dir: fileDialog.SetDirectory(base_dir) if fileDialog.ShowModal() == wx.ID_CANCEL: return # save the current contents in the file pathname = fileDialog.GetPath() if not pathname.endswith(".m4b"): pathname += ".m4b" self.project.m4b_name = pathname self.queue_project(self.project) self.on_clear(event) event.Skip()
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 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 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(".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 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 on_save(self, event): dbg("on_save event!") save_project(self.parent, self.project) event.Skip()
def remove_from_active_queue(self, book): try: self.active_queue.remove(book) except ValueError as e: dbg(e)
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
def on_tracks_empty(self, event): """Handles the double click event on the tracks panel and will empty the list""" dbg("on_tracks_empty") self.lc_mp3.SetStrings([]) event.Skip()