def generate_clip(self, song: Song, start: int, end: int) -> Path: """Create one clip from an existing MP3 file.""" assert song.filepath is not None album: Optional[str] = song.album if album is None: album = song.filepath.parent.name assert album is not None album = Song.clean(album) dest_dir = self.options.clip_destination_dir(album) filename = song.filename if filename is None or filename == '': filename = song.filepath.name assert filename is not None assert filename != '' if not dest_dir.exists(): dest_dir.mkdir(parents=True) dest_path = dest_dir / filename metadata = Metadata(artist=Song.clean(song.artist), title=Song.clean(song.title), album=album) with self.mp3.create(dest_path, metadata=metadata) as output: src = self.mp3.use(song).clip(start, end) src = src.normalize(0) output.append(src) output.generate() return dest_path
def generate_clip(self, song: Song, start: int, end: int) -> Path: """Create one clip from an existing MP3 file.""" album: Optional[str] = song.album if album is None: album = song.fullpath.parent.name assert album is not None album = Song.clean(album) dest_dir = self.options.clip_destination_dir(album) filename = song.filename assert filename is not None assert filename != '' if not dest_dir.exists(): dest_dir.mkdir(parents=True) dest_path = dest_dir / filename metadata = song.as_dict(exclude={'filename', 'ref_id'}) metadata['album'] = album if start > int(song.duration): raise ValueError( f'{start} is beyond the duration of song "{song.title}"') with self.mp3.create(dest_path, metadata=Metadata(**metadata)) as output: src = self.mp3.use(song).clip(start, end) output.append(src) output.generate() return dest_path
def _sort_level(self, parent: str, column: str, reverse: bool) -> None: """ Sort specified directory level and then any children of that level. """ # create tuple of the value of selected column + its ID for # each item at this level of the tree has_children = False pairs: List[Tuple[str, str]] = [] for ref_id in self.tree.get_children(parent): value = Song.clean(self.tree.set(ref_id, column)).lower() pairs.append(( value, ref_id, )) children = self.tree.get_children(ref_id) if children: self._sort_level(ref_id, column, reverse) has_children = True if has_children and column != 'filename': return pairs.sort(reverse=reverse) # rearrange items into sorted positions for index, (_, ref_id) in enumerate(pairs): self.tree.move(ref_id, parent, index)
def main(args: Sequence[str]) -> int: """used for testing game generation without needing to use the GUI""" #pylint: disable=import-outside-toplevel from musicbingo.mp3 import MP3Factory options = Options.parse(args) if options.game_id == '': options.game_id = datetime.date.today().strftime("%y-%m-%d") progress = TextProgress() mp3parser = MP3Factory.create_parser() clips = Directory(None, 0, options.clips()) progress = TextProgress() clips.search(mp3parser, progress) sys.stdout.write('\n') sys.stdout.flush() num_songs = options.columns * options.rows * 2 songs = clips.songs[:num_songs] if len(songs) < num_songs: for subdir in clips.subdirectories: todo = num_songs - len(songs) if todo < 1: break songs += subdir.songs[:todo] print('Selected {0} songs'.format(len(songs))) sys.stdout.flush() if len(songs) == 0: print('Error: failed to find any songs') return 1 if options.title == '': options.title = Song.choose_collection_title(songs) mp3editor = MP3Factory.create_editor(options.mp3_engine) pdf = DocumentFactory.create_generator('pdf') gen = GameGenerator(options, mp3editor, pdf, progress) gen.generate(songs) return 0
def _generate(self, destination: MP3FileWriter, progress: Progress) -> None: """generate output file, combining all input files""" output: Optional[AudioSegment] = None num_files = float(len(destination._files)) for index, mp3file in enumerate(destination._files, 1): progress.pct = 50.0 * index / num_files progress.text = f'Adding {mp3file.filename.name}' if progress.abort: return seg = AudioSegment.from_mp3(str(mp3file.filename)) if mp3file.start is not None: if mp3file.end is not None: seg = seg[mp3file.start:mp3file.end] else: seg = seg[mp3file.start:] elif mp3file.end is not None: seg = seg[:mp3file.end] if mp3file.headroom is not None: seg = seg.normalize(mp3file.headroom) if output is None: output = seg else: output += seg tags = None if destination._metadata is not None: tags = { "artist": Song.clean(destination._metadata.artist), "title": Song.clean(destination._metadata.title) } if destination._metadata.album: tags["album"] = Song.clean(destination._metadata.album) assert output is not None progress.text = f'Encoding MP3 file "{destination.filename.name}"' progress.pct = 50.0 if progress.abort: return dest_dir = destination.filename.parent if not dest_dir.exists(): dest_dir.mkdir(parents=True) output.export(str(destination.filename), format="mp3", bitrate=destination.bitrate, tags=tags) progress.pct = 100.0
def add_song(self, song: Song) -> None: """Add a song to this panel""" self._data[song.ref_id] = song self.tree.insert('', 'end', str(song.ref_id), values=song.pick(self.COLUMNS)) self._duration += int(song.duration) self._num_songs += 1 self._update_footer()
def setUp(self): """called before each test""" self.tmpdir = Path(tempfile.mkdtemp()) self.songs = [] filename = self.fixture_filename("songs.json") with filename.open('r') as src: for index, item in enumerate(json.load(src)): item['filepath'] = filename.parent / item['filename'] metadata = Metadata(**item) self.songs.append(Song(None, index + 1, metadata))
def append_songs(self, output: MP3FileWriter, songs: List[Song]) -> List[Song]: """ Append all of the songs to the specified output. Returns a new song list with the start_time metadata property of each song set to their positon in the output. """ transition = self.mp3_editor.use(Assets.transition()) #transition = transition.normalize(0) if self.options.mode == GameMode.QUIZ: countdown = self.mp3_editor.use(Assets.quiz_countdown()) else: countdown = self.mp3_editor.use(Assets.countdown()) #countdown = countdown.normalize(headroom=0) if self.options.mode == GameMode.QUIZ: start, end = Assets.QUIZ_COUNTDOWN_POSITIONS['1'] output.append(countdown.clip(start, end)) else: output.append(countdown) tracks = [] num_tracks = len(songs) for index, song in enumerate(songs, start=1): if self.progress.abort: return [] if index > 1: output.append(transition) cur_pos = output.duration next_track = self.mp3_editor.use(song) #.normalize(0) if self.options.mode == GameMode.QUIZ: try: start, end = Assets.QUIZ_COUNTDOWN_POSITIONS[str(index)] number = countdown.clip(start, end) except KeyError: break output.append(number) output.append(transition) output.append(next_track) song_with_pos = song.marshall(exclude=["ref_id"]) song_with_pos['start_time'] = cur_pos.format() metadata = Metadata(**song_with_pos) tracks.append(Song(song._parent, song.ref_id, metadata)) self.progress.text = f'Adding track {index}/{num_tracks}' self.progress.pct = 100.0 * float(index) / float(num_tracks) output.append(transition) self.progress.text = 'Generating MP3 file' self.progress.current_phase = 2 output.generate() if self.progress.abort: return tracks self.progress.text = 'MP3 Generated, creating track listing PDF' self.generate_track_listing(tracks) self.progress.text = 'MP3 and Track listing PDF generated' self.progress.pct = 100.0 return tracks
def _check_file(self, cache: Dict[str, dict], filename: Path, index: int, start_pct: float, depth: int) -> Optional[Song]: """Check one file to see if it an MP3 file or a directory. If it is a directory, a new Directory object is created for that directory. If it is an MP3 file, as new Song object is created """ abs_fname = str(filename) fstats = os.stat(abs_fname) if stat.S_ISDIR(fstats.st_mode): subdir = Directory(self, 1000 * (self.ref_id + index), filename, self.parser, self.progress) subdir.search(depth + 1, start_pct) self.subdirectories.append(subdir) return None if not stat.S_ISREG( fstats.st_mode) or not abs_fname.lower().endswith(".mp3"): return None try: mdata = cache[filename.name] mdata['filepath'] = filename try: mdata['song_id'] = mdata['songId'] del mdata['songId'] except KeyError: pass try: del mdata['index'] except KeyError: pass #print('use cache', filename.name) return Song(self, self.ref_id + index + 1, Metadata(**mdata)) except KeyError: pass if fstats.st_size > self.maxFileSize: raise InvalidMP3Exception(f'{filename} is too large') print('parse', filename.name) metadata = self.parser.parse(filename) return Song(self, self.ref_id + index + 1, metadata)
def generate(self, songs: List[Song]) -> List[Path]: """ Generate all clips for all selected Songs Returns list of filenames of new clips """ total_songs = len(songs) clips: List[Path] = [] start = int(Duration(self.options.clip_start)) end = start + 1000 * self.options.clip_duration for index, song in enumerate(songs): self.progress.text = '{} ({:d}/{:d})'.format( Song.clean(song.title), index, total_songs) self.progress.pct = 100.0 * float(index) / float(total_songs) #pylint: disable=broad-except try: clips.append(self.generate_clip(song, start, end)) except InvalidMP3Exception as err: traceback.print_exc() print(r'Error generating clip: {0} - {1}'.format( Song.clean(song.title), str(err))) self.progress.pct = 100.0 self.progress.text = 'Finished generating clips' return clips
def _parse_song(self, parser: MP3Parser, cache: Dict[str, dict], filename: Path, index: int) -> None: """ Create a Song object for an MP3 file and append to songs list. The cache is checked and if that does not contain a match, the file will be parsed. """ song: Optional[Song] = None try: mdata = cache[filename.name] try: mdata['song_id'] = mdata['songId'] del mdata['songId'] except KeyError: pass try: del mdata['index'] except KeyError: pass self.log.debug('Use cache for "%s"', filename.name) song = Song(filename.name, parent=self, ref_id=(self.ref_id + index + 1), **mdata) except KeyError: self.log.debug('"%s": Failed to find "%s" in cache', self.filename, filename.name) if song is None: self.log.info('Parse "%s"', filename.name) metadata = parser.parse(filename).as_dict() song = Song(filename.name, parent=self, ref_id=(self.ref_id + index + 1), **metadata) assert song is not None with self._lock: self.songs.append(song)
def load_previous_game_songs(self, gamedir: Path) -> None: """ Load all the songs from a previous game. The previous_games_songs set is updated with every song in a previous game. This is then used when adding random tracks to a game to attempt to avoid adding duplicates """ filename = gamedir / self.options.games_tracks_filename if not filename.exists(): return with filename.open('r') as gt_file: for index, song in enumerate(json.load(gt_file), 1): song = Song(None, index, Metadata(**song)) self.previous_games_songs.add(hash(song))
def setUp(self): """called before each test""" self.tmpdir = Path(tempfile.mkdtemp()) #self.songs = [] filename = self.fixture_filename("songs.json") self.directory = Directory(None, 1, filename) with filename.open('r') as src: for index, item in enumerate(json.load(src)): #item['filepath'] = filename.parent / item['filename'] filename = item.pop('filename') item['bitrate'] = 256 item['sample_rate'] = 44100 item['sample_width'] = 16 item['channels'] = 2 self.directory.songs.append( Song(filename, parent=self.directory, ref_id=index + 1, **item))
def restore_song(self, song: Song, update: bool = True) -> None: """ Restores a hidden song from this panel. @raises KeyError if song not in this panel """ self._hidden.remove(song.ref_id) parent = '' if song._parent is not None: parent = str(cast(Directory, song._parent).ref_id) if not self.tree.exists(parent): parent = '' songs: List[Union[Song, Directory]] = [] if self.tree.exists(parent): songs = [ self._data[int(rid)] for rid in self.tree.get_children(parent) ] songs.append(song) column, reverse = self._sorting songs.sort(key=lambda s: getattr(s, column), reverse=reverse) index: int = 0 for item in songs: if item.ref_id == song.ref_id: break index += 1 try: self.tree.reattach(str(song.ref_id), parent, index) except tk.TclError as err: print(f'Error: {err}') print( f'ref_id="{song.ref_id}", parent="{parent}", index="{index}"') self.tree.insert(parent, 'end', str(song.ref_id), values=song.pick(self.COLUMNS)) self._duration += int(song.duration) self._num_songs += 1 if update: self._update_footer()
def append_songs(self, output: MP3FileWriter, songs: List[Song]) -> List[Song]: """ Append all of the songs to the specified output. Returns a new song list with the start_time metadata property of each song set to their positon in the output. """ sample_rate = output.metadata.sample_rate transition = self.mp3_editor.use(Assets.transition(sample_rate)) if self.options.mode == GameMode.QUIZ: countdown = self.mp3_editor.use(Assets.quiz_countdown(sample_rate)) else: countdown = self.mp3_editor.use(Assets.countdown(sample_rate)) overlap: Optional[Duration] = None if self.options.crossfade > 0: overlap = Duration(self.options.crossfade) if self.options.mode == GameMode.QUIZ: start, end = Assets.QUIZ_COUNTDOWN_POSITIONS['1'] output.append(countdown.clip(start, end)) else: output.append(countdown) tracks: List[Song] = [] num_tracks = len(songs) for index, song in enumerate(songs, start=1): if self.progress.abort: return tracks if index > 1: output.append(transition, overlap=overlap) cur_pos = output.duration next_track = self.mp3_editor.use(song) if self.options.mode == GameMode.QUIZ: try: start, end = Assets.QUIZ_COUNTDOWN_POSITIONS[str(index)] number = countdown.clip(start, end) except KeyError: break output.append(number) output.append(transition) output.append(next_track, overlap=overlap) song_with_pos = song.marshall(exclude=["filename", "ref_id"]) song_with_pos['start_time'] = cur_pos.format() tracks.append( Song(song.filename, song._parent, song.ref_id, **song_with_pos)) self.progress.text = f'Adding track {index}/{num_tracks}' self.progress.pct = 100.0 * float(index) / float(num_tracks) output.append(transition, overlap=overlap) if self.options.crossfade > 0: # if we need to re-encode the stream anyway, might as well also # do loudness normalisation output.normalize(1) self.progress.text = 'Generating MP3 file' self.progress.current_phase = 2 output.generate() if self.progress.abort: return tracks self.progress.text = 'MP3 Generated, creating track listing PDF' self.generate_track_listing(tracks) self.progress.text = 'MP3 and Track listing PDF generated' self.progress.pct = 100.0 return tracks