def scan_music_folder(music_folder): bytes_to_minutes = 8 / (1024 * 128 * 60) os.chdir(music_folder) exclude_prefixes = tuple(open('not_cd_folders.txt').read().split('\n')[1:]) # first one is "_Copied" - this is OK def is_included(walk_tuple): folder_name = walk_tuple[0] return not folder_name[len(music_folder) + 1:].startswith(exclude_prefixes) albums = {} for folder, folder_list, file_list in filter(is_included, os.walk(music_folder)): show_progress = len(albums) % 30 == 0 if show_progress: print(folder) for file in filter(is_media_file, file_list): filename = os.path.join(folder, file) try: tags = phrydy.MediaFile(filename) except Exception: print(f'No media info for {file_list[0]}') continue # use album artist (if available) so we can compare 'Various Artist' albums artist = tags.albumartist or tags.artist album_name = tags.album # some buggy mp3s - assume 128kbps duration = tags.length / 60 if tags.length else os.path.getsize(filename) * bytes_to_minutes key = (folder, artist, album_name) # albums is a dict of dicts: each subdict stores (file, duration) as (key, value) pairs albums.setdefault(key, {})[file] = duration # remove albums with only one track return {key: file_list for key, file_list in albums.items() if len(file_list) > 1}
def copy_album(album, files, existing_folder=None): """Copy a given album to the copy folder.""" bad_chars = str.maketrans({char: None for char in '*?/\\<>:|"'}) # can't use these in filenames def remove_bad_chars(filename: str): return filename.translate(bad_chars) folder, artist, title = album if title: no_artist = artist in (None, '', 'Various', 'Various Artists') album_filename = remove_bad_chars(title if no_artist else f'{artist} - {title}') else: album_filename = os.path.basename(folder) album_filename = album_filename[:60].strip() # shorten path names (Windows limit: 260 chars) if existing_folder is None: # making a new folder copied_name = datetime.strftime(datetime.now(), '%Y-%m-%d ') + album_filename os.mkdir(copied_name) n = 0 else: # copying into an existing folder copied_name = f'{existing_folder}; {album_filename}' os.rename(existing_folder, copied_name) n = len(os.listdir(copied_name)) os.chdir(copied_name) for j, f in enumerate(files.keys(), start=1): media_info = phrydy.MediaFile(os.path.join(folder, f)) name, ext = os.path.splitext(f) try: copy_filename = remove_bad_chars(f'{int(media_info.track) + n:02d} {media_info.title}{ext}') except (ValueError, TypeError): # e.g. couldn't get track name or number copy_filename = f'{j + 1 + n:02d} {f}' # fall back to original name copy2(os.path.join(folder, f), copy_filename) os.chdir('..') return copied_name
def check_radio_files(lastfm_user): """Find and remove recently-played tracks from the Radio folder. Fix missing titles in tags.""" # get recently played tracks (as reported by Last.fm) played_tracks = lastfm_user.get_recent_tracks(limit=400) scrobbled_titles = [ f'{track.track.artist.name} - {track.track.title}'.lower() for track in played_tracks ] scrobbled_radio = [] os.chdir(os.path.join(user_profile, 'Radio')) radio_files = os.listdir() # loop over radio files - first check if they've been scrobbled, then try to correct tags where titles aren't set checking_scrobbles = True file_count = 0 min_date = None total_hours = 0 for file in sorted(radio_files): try: file_date = datetime.strptime(file[:10], '%Y-%m-%d') except ValueError: continue # not a date-based filename file_count += 1 min_date = min_date or file_date # set to first one weeks = (file_date - min_date).days // 7 tags = phrydy.MediaFile(file) tags_changed = False total_hours += tags.length / 3600 if checking_scrobbles: track_title = media.artist_title(tags) if track_title.lower() in scrobbled_titles: print(f'Found: {track_title}') scrobbled_radio.append(file) else: print(f'Not found: {track_title}') checking_scrobbles = False # stop here - don't keep searching if tags.title in ('', 'Untitled Episode', None): print(f'Set {file} title to {file[11:-4]}') tags.title = file[ 11: -4] # the bit between the date and the extension (assumes 3-char ext) tags_changed = True if not tags.albumartist: print(f'Set {file} album artist to {tags.artist}') tags.albumartist = tags.artist tags_changed = True if tags_changed and not test_mode: tags.save() toast = '' print('\nTo delete:') for file in scrobbled_radio[: -1]: # don't delete the last one - we might not have finished it toast += f'🗑️ {os.path.splitext(file)[0]}\n' print(file) if not test_mode and os.path.exists(file): send2trash(file) toast += f'📻 {file_count} files; {weeks} weeks; {total_hours:.0f} hours\n' return toast
def album_artist(files): """Replace a blank album artist field with something populated from the artist field.""" if all(phrydy.MediaFile(file).albumartist for file in files): return # nothing to do artists = [phrydy.MediaFile(file).artist for file in files] unique_artists = set(artists) if len(unique_artists ) == 1: # only one for all of them - apply this to album artist = artists[0] print( f'Setting album artist to {artist} for {len(files)} files starting with {files[0]}' ) for file in files: tags = phrydy.MediaFile(file) if not tags.albumartist: tags.albumartist = artist tags.save() else: print(unique_artists)
def get_album_info(media_files): for file in media_files: try: tags = phrydy.MediaFile(file) break except phrydy.mediafile.FileTypeError: print(f'No media info for {file}') else: # no media info for any (or empty list) return None, None # use album artist (if available) so we can compare 'Various Artist' albums artist = tags.albumartist or tags.artist title = str('' or tags.album) # use empty string if album is None return artist, title
def apostrophes(files): """Look for Opus files tagged with bad apostrophes (’), and replace them with normal ones (').""" for file in files: basename, ext = os.path.splitext(file) if ext.lower() != '.opus': continue tags = phrydy.MediaFile(file) for tag_name in tags.readable_fields(): if tag_name == 'art': continue tag_text = getattr(tags, tag_name) if not isinstance(tag_text, str) or '’' not in tag_text: continue setattr(tags, tag_name, tag_text.replace('’', "'")) tags.save() print(file, tag_name, tag_text)
def artist_title(file, separator=' - '): """Return {artist} - {title} string for a given file, converted to lowercase for easy comparison. Pass file as a filename or a MediaFile object from phrydy.""" media_info = phrydy.MediaFile(file) if isinstance(file, str) else file return f'{media_info.artist}{separator}{media_info.title}'.lower()
def artist_title(filename): """Return {artist} - {title} string for a given file.""" media_info = phrydy.MediaFile(filename) return f'{media_info.artist} - {media_info.title}'.lower()