Example #1
0
 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
Example #2
0
 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
Example #3
0
    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)
Example #4
0
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
Example #5
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
Example #6
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()
Example #7
0
 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))
Example #8
0
    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
Example #9
0
 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)
Example #10
0
 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
Example #11
0
 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)
Example #12
0
 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))
Example #13
0
 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))
Example #14
0
    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()
Example #15
0
 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