def __normalize_release_artists(release: Release) -> List[str]: """normalize release artists""" release_artists = release.validate_release_artists() if not release_artists: release_artists = release.extract_release_artist() return release_artists
def __fix_missing_total_discs(release: Release) -> None: # fix missing total disc numbers if not release.validate_total_discs(): total_discs = release.get_total_discs() if total_discs: for track in release.tracks.values(): track.total_discs = total_discs
def __disc_number_best_guess(release: Release) -> None: """if disc number is missing and there appears to only be one disc, set to """ validated_disc_numbers = release.get_total_tracks() if not release.validate_total_discs() and len( validated_disc_numbers) == 1: for track in release.tracks.values(): track.disc_number = 1
def __extract_track_disc_numbers_from_filenames(release: Release) -> None: """extract missing track and disc numbers from filenames""" validated_track_numbers = release.validate_track_numbers() validated_disc_numbers = release.validate_disc_numbers() for path in release.tracks: track_num, disc_num = extract_track_disc(os.path.split(path)[-1]) if (not release.tracks[path].track_number and track_num) or validated_track_numbers: release.tracks[path].track_number = track_num if validated_track_numbers and disc_num: release.tracks[path].disc_number = disc_num if (not release.tracks[path].disc_number and disc_num) or validated_disc_numbers: release.tracks[path].disc_number = disc_num
def __extract_year_from_folder_name(release: Release, folder_name: str) -> None: """extract missing year from folder name""" extracted_year = extract_release_year(folder_name) if not release.validate_release_date() and extracted_year: for track in release.tracks.values(): track.date = str(extracted_year)
def __fix_missing_total_tracks(release: Release) -> None: # fix missing total track numbers validated_disc_numbers = release.get_total_tracks() for track in release.tracks.values(): disc_number = track.disc_number if track.disc_number else 1 if validated_disc_numbers.get(disc_number): track.total_tracks = validated_disc_numbers[disc_number]
def rename_files(release: Release, parent_path: str, dry_run=False) -> None: renames = [] # rename files for filename in release.tracks: correct_filename = release.tracks[filename].get_filename(release.is_va()) if not correct_filename: return dest_path = os.path.join(parent_path, correct_filename) # if the track passes validation, and the filename differs from the calculated correct filename if filename != correct_filename and ( # and the destination doesn't exist, or it's a case-insensitive match of the source not os.path.exists(dest_path) or os.path.normcase(filename) == os.path.normcase(correct_filename)): renames.append([filename, correct_filename]) for rename in renames: if not dry_run: dest_path = os.path.join(parent_path, rename[1]) if os.name == "nt" and len(dest_path) > 260: fn, ext = os.path.splitext(dest_path) dest_path = dest_path[0:260 - len(ext)] + ext if os.path.exists(dest_path) and os.path.normcase(rename[0]) != os.path.normcase(rename[1]): logging.getLogger(__name__).error("File already exists, could not rename: {0}".format(dest_path)) continue while True: try: os.rename(os.path.join(parent_path, rename[0]), dest_path) break except PermissionError: logging.getLogger(__name__).error("PermissionError: could not rename '{0}' -> '{1}', retrying..." .format(os.path.join(parent_path, rename[0]), dest_path)) time.sleep(1) # clean up empty directories curr_parent_path = os.path.join(parent_path, os.path.split(rename[0])[0]) while not os.listdir(curr_parent_path): os.rmdir(curr_parent_path) curr_parent_path = os.path.split(curr_parent_path)[0] release.tracks[rename[1]] = release.tracks.pop(rename[0])
def validate_folder_name(release: Release, violations: List[Violation], folder_name: str, skip_comparison: bool, group_by_category: bool = False, codec_short: bool = True) -> None: if not release.can_validate_folder_name(): violations.append( Violation(ViolationType.FOLDER_NAME, "Cannot validate folder name")) return valid_folder_name = release.get_folder_name( codec_short=codec_short, group_by_category=group_by_category) if valid_folder_name != folder_name and not skip_comparison: violations.append( Violation( ViolationType.FOLDER_NAME, "Invalid folder name '{folder_name}' should be '{valid_folder_name}'" .format(folder_name=folder_name, valid_folder_name=valid_folder_name)))
def create_test_release(artists=None, release_artists=None, release_title="Mezzanine", date="1998-04-17", track_titles=None, genres=None) -> Release: if not artists: artists = ["Massive Attack"] if not release_artists: release_artists = ["Massive Attack"] if not track_titles: track_titles = [ "Angel", "Risingson", "Teardrop", "Inertia Creeps", "Exchange", "Dissolved Girl", "Man Next Door", "Black Milk", "Mezzanine", "Group Four", "(Exchange)" ] if not genres: genres = [ 'Trip-hop', 'Electronic', 'Chillout', 'Electronica', 'Downtempo', '90s', 'Alternative', 'Ambient', 'British', 'Dark', 'Bristol Sound', 'Atmospheric', 'Hypnotic', 'UK', 'Alternative Dance', 'Bristol', 'Chill', 'Dub', 'Experimental', 'Indie', 'Leftfield', 'Lounge', 'Nocturnal', '1990s', 'Bass', 'Electro', 'Intense', 'Relax', 'Sophisticated' ] tracks = OrderedDict() base_track = Track(artists=artists, release_artists=release_artists, date=date, release_title=release_title, track_number=1, total_tracks=len(track_titles), disc_number=1, total_discs=1, genres=genres, stream_info=StreamInfo(tag_type=TagType.ID3, mp3_method=Mp3Method.CBR, length=100.123, bitrate=128000, xing=Xing())) for i in range(1, len(track_titles) + 1): curr_track = copy.deepcopy(base_track) curr_track.track_title = track_titles[i - 1] curr_track.track_number = i tracks["{0} - {1}.mp3".format(str(i).zfill(2), curr_track.track_title)] = curr_track return Release(tracks)
def __fix_release_title(release: Release) -> None: """fix release title""" release_title = release.validate_release_title() if not release_title: return for category_stub in ["CD", "CDS", "CDM"]: if release_title.endswith(" {0}".format(category_stub)): release_title = release_title[:-(len(category_stub) + 1)] bracket_pairs = [["[", "]"], ["(", ")"], ["{", "}"]] # Remove "[Source]" from release title for source in ReleaseSource: for brackets in bracket_pairs: curr_source = "{0}{1}{2}".format(brackets[0], source.value, brackets[1]) pattern = "(?i)( )?" + re.escape(curr_source) if re.search(pattern, release_title): release_title = re.sub(pattern, "", release_title) # Remove trailing ' Source' from release title for source in [x for x in ReleaseSource]: pattern = "(?i)( \-)? " + source.value + "$" if re.search(pattern, release_title): release_title = re.sub(pattern, "", release_title) # Remove "[Category]" from release title for category in ReleaseCategory: for brackets in bracket_pairs: curr_category = "{0}{1}{2}".format(brackets[0], category.value, brackets[1]) pattern = "(?i)( )?" + re.escape(curr_category) if re.search(pattern, release_title): release_title = re.sub(pattern, "", release_title) # Remove trailing ' category' from release title for category in [ x for x in ReleaseCategory if x is not ReleaseCategory.ALBUM ]: pattern = "(?i)( \-)? " + category.value + "$" if re.search(pattern, release_title): release_title = re.sub(pattern, "", release_title) # Overwrite the release's 'release title' tags for track in release.tracks.values(): if track.release_title != release_title: track.release_title = release_title
def __lastfm_fixes(self, release: Release, release_artists: List[str]) -> None: # lastfm fixes release_title = release.validate_release_title() if not self.lastfm or not len(release_artists) or not release_title: return # extract (edition info) from release titles release_title, release_edition = split_release_title( normalize_release_title(release_title)) flattened_artist = flatten_artists(release_artists) lastfm_release = None while True: try: lastfm_release = self.lastfm.get_release( flattened_artist, release_title) break except LastfmCache.ReleaseNotFoundError as e: logging.getLogger(__name__).error(e) break except LastfmCache.UpgradeRequiredError: logging.getLogger(__name__).error(upgrade_message) exit(1) except LastfmCache.ConnectionError: logging.getLogger(__name__).error( "Connection error while retrieving release, retrying...") time.sleep(1) except LastfmCache.LastfmCacheError: logging.getLogger(__name__).error( "Server error while retrieving release, retrying...") time.sleep(1) self.__lastfm_release_fixes(release, lastfm_release, release_artists, release_title, release_edition) # fix track artists using lastfm self.__lastfm_fix_track_artists(release) # fix release artists using lastfm self.__lastfm_fix_release_artists(release)
def validate_releases(validator: ReleaseValidator, release_dirs: List[str], args: argparse.Namespace) -> None: """Validate releases found in the scan directory""" # assemble_discs(release_dirs, False) for curr_dir in release_dirs: audio, non_audio, unreadable = load_directory(curr_dir) release = Release(audio, guess_category_from_path(curr_dir), guess_source_from_path(curr_dir)) codec_short = not args.full_codec_names violations = validator.validate(release) validate_folder_name(release, violations, os.path.split(curr_dir)[1], False, codec_short) add_unreadable_files(violations, unreadable) print("{0} violations: {1}".format(format_violations_str(violations), curr_dir)) if args.show_violations: print_list(violations)
def move_rename_folder(release: Release, unique_releases: Set[Tuple], curr_dir: str, dest_folder: str, duplicate_folder: str, args: argparse.Namespace) -> str: """Rename a release folder, and move to a destination folder""" # if a dry run,or the folder name cannot be validated, do nothing if args.dry_run or not release.can_validate_folder_name(): return curr_dir moved_dir = curr_dir # rename the release folder codec_short = not args.full_codec_names fixed_dir = os.path.join( os.path.split(curr_dir)[0], release.get_folder_name(codec_short=codec_short, group_by_category=args.group_by_category)) if curr_dir != fixed_dir: if not os.path.exists(fixed_dir) or os.path.normcase( curr_dir) == os.path.normcase(fixed_dir): while True: try: os.rename(curr_dir, fixed_dir) break except PermissionError: logging.getLogger(__name__).error( "PermissionError: could not rename directory to {0}". format(fixed_dir)) time.sleep(1) moved_dir = fixed_dir else: logging.getLogger(__name__).error( "Release folder already exists: {0}".format(fixed_dir)) # move the release folder to a destination moved_duplicate = False if dest_folder and release.num_violations == 0: artist_folder = flatten_artists(release.validate_release_artists()) \ if args.group_by_artist and not release.is_va() else "" category_folder = str( release.category.value) if args.group_by_category else "" curr_dest_parent_folder = os.path.join(dest_folder, category_folder, artist_folder) curr_dest_folder = os.path.join( curr_dest_parent_folder, release.get_folder_name(codec_short=codec_short, group_by_category=args.group_by_category)) if os.path.normcase(moved_dir) != os.path.normcase(curr_dest_folder): if not os.path.exists(curr_dest_parent_folder): os.makedirs(curr_dest_parent_folder, exist_ok=True) if not os.path.exists(curr_dest_folder): os.rename(moved_dir, curr_dest_folder) moved_dir = curr_dest_folder # clean up empty directories curr_src_parent_folder = os.path.split(fixed_dir)[0] while not os.listdir(curr_src_parent_folder): os.rmdir(curr_src_parent_folder) curr_src_parent_folder = os.path.split( curr_src_parent_folder)[0] else: if duplicate_folder: release_folder_name = release.get_folder_name( codec_short=codec_short, group_by_category=args.group_by_category) moved_dir = move_duplicate(duplicate_folder, moved_dir, release_folder_name) moved_duplicate = True else: logging.getLogger(__name__).error( "Destination folder already exists: {0}".format( fixed_dir)) # deduplicate versions of the same release unique_release = UniqueRelease( release.validate_release_artists(), release.validate_release_date().split("-")[0], release.validate_release_title(), release.validate_codec(), release.get_codec_rank(), moved_dir) if duplicate_folder and release.num_violations == 0 and not moved_duplicate: if unique_release in unique_releases: existing = [x for x in unique_releases if x == unique_release][0] if unique_release > existing: # move the existing one release_folder_name = os.path.split(existing.path)[1] moved_dir = move_duplicate(duplicate_folder, existing.path, release_folder_name) unique_releases.remove(unique_release) unique_releases.add(unique_release) else: # move the current one release_folder_name = release.get_folder_name( codec_short=codec_short, group_by_category=args.group_by_category) moved_dir = move_duplicate(duplicate_folder, moved_dir, release_folder_name) else: unique_releases.add(unique_release) return moved_dir
def fix_releases(validator: ReleaseValidator, release_dirs: Iterator[str], args: argparse.Namespace, dest_folder: str, invalid_folder: str, duplicate_folder: str) -> None: """Fix releases found in the scan directory""" unique_releases = set() for curr_dir in release_dirs: if not can_lock_path(curr_dir): logging.getLogger(__name__).error( "Could not lock directory {0}".format(curr_dir)) continue audio, non_audio, unreadable = load_directory(curr_dir) release = Release(audio, guess_category_from_path(curr_dir), guess_source_from_path(curr_dir)) fixed = validator.fix(release, os.path.split(curr_dir)[1]) if not args.dry_run: for x in fixed.tracks: if fixed.tracks[x] != release.tracks[x] or fixed.tracks[ x].always_write: cleartag.write_tags(os.path.join(curr_dir, x), fixed.tracks[x]) # rename files rename_files(fixed, curr_dir, args.dry_run) new_tracks = OrderedDict() for x in fixed.tracks: correct_filename = fixed.tracks[x].get_filename(fixed.is_va()) if correct_filename: new_tracks[correct_filename] = fixed.tracks[x] else: new_tracks[x] = fixed.tracks[x] fixed.tracks = new_tracks # calculate violations before and after fixing codec_short = not args.full_codec_names old_violations = validator.validate(release) validate_folder_name(release, old_violations, os.path.split(curr_dir)[1], False, args.group_by_category, codec_short) add_unreadable_files(old_violations, unreadable) violations = validator.validate(fixed) validate_folder_name(fixed, violations, os.path.split(curr_dir)[1], True, args.group_by_category, codec_short) add_unreadable_files(violations, unreadable) if len(violations) == 0: moved_dir = move_rename_folder(fixed, unique_releases, curr_dir, dest_folder, duplicate_folder, args) else: moved_dir = move_invalid_folder(curr_dir, invalid_folder, violations, args.move_invalid) enforce_max_path(moved_dir) print("{0} violations: {1}".format( format_violations_str(old_violations, violations), moved_dir)) if args.show_violations: if old_violations: print("Before") print_list(old_violations) if violations: print("After:") print_list(violations)
def __lastfm_release_fixes(self, release: Release, lastfm_release: LastfmRelease, release_artists: List[str], release_title: str, release_edition: str) -> None: """lastfm release fixes""" if release_artists: for track in release.tracks.values(): track.release_artists = release_artists if not lastfm_release: return # release title if lastfm_release.release_name != release_title and \ ReleaseValidator.__lastfm_can_fix_release_title(release_title, lastfm_release.release_name): release_title_full = lastfm_release.release_name if release_edition: release_title_full = "{0} {1}".format( lastfm_release.release_name, release_edition) for track in release.tracks.values(): track.release_title = release_title_full # dates if lastfm_release.release_date: for track in release.tracks.values(): if lastfm_release.release_date and lastfm_release.release_date != track.date: track.date = lastfm_release.release_date # tags/genres (only fail if 0-1 genres - i.e. lastfm tags have never been applied) release_genres = release.validate_genres() lastfm_tags = self.__get_lastfm_tags(release_title, release_artists) if len(release_genres) < 2 <= len(lastfm_tags): for track in release.tracks.values(): track.genres = lastfm_tags # fill missing track numbers from lastfm for track in release.tracks.values(): if track.track_number: continue track_num_matches = [ int(x) for x in lastfm_release.tracks if normalize_track_title(lastfm_release.tracks[x].track_name). lower() == normalize_track_title(track.track_title).lower() ] if track_num_matches and len(track_num_matches) == 1 and not \ [x.track_number for x in release.tracks.values() if x.track_number == track_num_matches[0]]: track.track_number = track_num_matches[0] # match and validate track titles (intersection only) track_numbers_validated = not release.validate_track_numbers() for track in release.tracks.values(): if track.track_number in lastfm_release.tracks: lastfm_title = normalize_track_title( lastfm_release.tracks[track.track_number].track_name) if track.track_title != lastfm_title: # if the track title is missing, or if it is lowercase and there is a case insensitive match if (not track.track_title and track_numbers_validated) or \ (track.track_title.islower() and track.track_title.lower() == lastfm_title.lower()): track.track_title = lastfm_title # case insensitive match, tag version has no capital letters elif track.track_title.lower() == lastfm_title.lower() \ and track.track_title.lower() == track.track_title: track.track_title = lastfm_title
def __normalize_release_title(release: Release) -> None: """normalize release title""" release_title = release.validate_release_title() for track in release.tracks.values(): if track.release_title != release_title: track.release_title = release_title
def validate(self, release: Release) -> List[Violation]: violations = OrderedSet() # leading/trailing whitespace for filename, track in release.tracks.items(): if track.artists != track.strip_whitespace_artists(): violations.add( Violation( ViolationType.ARTIST_WHITESPACE, "File '{0}' has leading/trailing whitespace in its Artist(s)" .format(filename))) for filename, track in release.tracks.items(): if track.release_artists != track.strip_whitespace_release_artists( ): violations.add( Violation( ViolationType.RELEASE_ARTIST_WHITESPACE, "File '{0}' has leading/trailing whitespace in its Album/Release Artist(s)" .format(filename))) for filename, track in release.tracks.items(): if track.date != track.strip_whitespace_date(): violations.add( Violation( ViolationType.DATE_WHITESPACE, "File '{0}' has leading/trailing whitespace in its Year/Date" .format(filename))) for filename, track in release.tracks.items(): if track.release_title != track.strip_whitespace_release_title(): violations.add( Violation( ViolationType.RELEASE_TITLE_WHITESPACE, "File '{0}' has leading/trailing whitespace in its Album/Release Title" .format(filename))) for filename, track in release.tracks.items(): if track.track_title != track.strip_whitespace_track_title(): violations.add( Violation( ViolationType.TRACK_TITLE_WHITESPACE, "File '{0}' has leading/trailing whitespace in its Track Title" .format(filename))) for filename, track in release.tracks.items(): if track.genres != track.strip_whitespace_genres(): violations.add( Violation( ViolationType.GENRE_WHITESPACE, "File '{0}' has leading/trailing whitespace in its Genre(s)" .format(filename))) # release date if not release.validate_release_date(): violations.add( Violation( ViolationType.DATE_INCONSISTENT, "Release contains blank or inconsistent 'Date' tags")) # artists if release.blank_artists(): violations.add( Violation( ViolationType.ARTIST_BLANK, "Release contains {0} tracks with missing 'Artist' tags". format(release.blank_artists()))) # track titles if release.blank_track_titles(): violations.add( Violation( ViolationType.TRACK_TITLE_BLANK, "Release contains {0} tracks with missing 'Track Title' tags" .format(release.blank_track_titles()))) # release artist release_artists = release.validate_release_artists() if not release_artists: violations.add( Violation( ViolationType.RELEASE_ARTIST_INCONSISTENT, "Release contains blank or inconsistent 'Album/Release Artist' tags" )) # if the lastfmcache is present, validate the release artist validated_release_artists = release_artists if self.lastfm and len(release_artists) == 1: validated_release_artists = [] for artist in release_artists: try: validated_release_artist = self.lastfm.get_artist( artist.strip()).artist_name if validated_release_artist != artist: violations.add( Violation( ViolationType.RELEASE_ARTIST_SPELLING, "Incorrectly spelled Album/Release Artist '{0}' (should be '{1}')" .format(artist, validated_release_artist))) validated_release_artists.append(validated_release_artist) except LastfmCache.ArtistNotFoundError: violations.add( Violation( ViolationType.ARTIST_LOOKUP, "Lookup failed of release artist '{release_artist}'" .format(release_artist=artist.strip()))) # release title release_title = release.validate_release_title() if not release_title: violations.add( Violation( ViolationType.RELEASE_TITLE_INCONSISTENT, "Release contains blank or inconsistent 'Album/Release Title' tags" )) bracket_pairs = [["[", "]"], ["(", ")"], ["{", "}"]] if release_title: # check if "[Source]" is contained in the release title for source in ReleaseSource: for brackets in bracket_pairs: curr_source = "{0}{1}{2}".format(brackets[0], source.value, brackets[1]) if curr_source.lower() in release_title.lower(): violations.add( Violation( ViolationType.RELEASE_TITLE_SOURCE, "Release title contains source {0}".format( curr_source))) # check if the release title ends with a space and a source name, without brackets for source in [x for x in ReleaseSource]: if release_title.lower().endswith(" {0}".format( source.value.lower())): violations.add( Violation( ViolationType.RELEASE_TITLE_SOURCE, "Release title ends with source {0}".format( source.value))) # check if "[Category]" is contained in the release title for category in ReleaseCategory: for brackets in bracket_pairs: curr_category = "{0}{1}{2}".format(brackets[0], category.value, brackets[1]) if curr_category.lower() in release_title.lower(): violations.add( Violation( ViolationType.RELEASE_TITLE_CATEGORY, "Release title contains category {0}".format( curr_category))) # check if the release title ends with a space and a category name, without brackets (except Album) for category in [ x for x in ReleaseCategory if x is not ReleaseCategory.ALBUM ]: if release_title.lower().endswith(" {0}".format( category.value.lower())): violations.add( Violation( ViolationType.RELEASE_TITLE_CATEGORY, "Release title ends with category {0}".format( category.value))) # lastfm artist validations if self.lastfm and release_title and len(validated_release_artists): # extract (edition info) from release titles release_title, _ = split_release_title( normalize_release_title(release_title)) flattened_artist = flatten_artists(validated_release_artists) lastfm_release = None try: lastfm_release = self.lastfm.get_release( flattened_artist, release_title) except LastfmCache.ReleaseNotFoundError as e: logging.getLogger(__name__).error(e) if lastfm_release: # release title if lastfm_release.release_name != release_title and \ ReleaseValidator.__lastfm_can_fix_release_title(release_title, lastfm_release.release_name): violations.add( Violation( ViolationType.RELEASE_TITLE_SPELLING, "Incorrectly spelled Album/Release name '{0}' (should be '{1}')" .format(release_title, lastfm_release.release_name))) # dates if lastfm_release.release_date: date = next(iter(release.tracks.values())).date if lastfm_release.release_date != date and \ (not date or len(lastfm_release.release_date) >= len(date)): violations.add( Violation( ViolationType.DATE_INCORRECT, "Incorrect Release Date '{0}' (should be '{1}')" .format(date, lastfm_release.release_date))) # tags/genres (only fail if 0-1 genres - i.e. lastfm tags have never been applied) release_genres = release.validate_genres() lastfm_tags = self.__get_lastfm_tags( release_title, validated_release_artists) if len(release_genres) < 2 <= len(lastfm_tags): violations.add( Violation( ViolationType.BAD_GENRES, "Bad release genres: [{0}] (should be [{1}])". format(", ".join(release_genres), ", ".join(lastfm_tags)))) # match and validate track titles (intersection only) if self.lastfm_track_title_validation: for track in release.tracks.values(): if track.track_number in lastfm_release.tracks: lastfm_title = normalize_track_title( lastfm_release.tracks[ track.track_number].track_name) if not track.track_title or track.track_title.lower( ) != lastfm_title.lower(): violations.add( Violation( ViolationType.INCORRECT_TRACK_TITLE, "Incorrect track title '{0}' should be: '{1}'" .format(track.track_title, lastfm_title))) # track artists for track in release.tracks.values(): for artist in track.artists: while True: try: validated_artist = self.lastfm.get_artist( normalize_artist_name(artist)).artist_name if validated_artist != artist: violations.add( Violation( ViolationType.TRACK_ARTIST_SPELLING, "Incorrectly spelled Track Artist '{0}' (should be '{1}')" .format(artist, validated_artist))) break except LastfmCache.ArtistNotFoundError: # as e: # violations.add(str(e)) break except LastfmCache.LastfmCacheError: time.sleep(1) # release artists for track in release.tracks.values(): for artist in track.release_artists: while True: try: validated_artist = self.lastfm.get_artist( normalize_artist_name(artist)).artist_name if validated_artist != artist: violations.add( Violation( ViolationType.RELEASE_ARTIST_SPELLING, "Incorrectly spelled Release Artist '{0}' (should be '{1}')" .format(artist, validated_artist))) break except LastfmCache.ArtistNotFoundError: # as e: # violations.add(str(e)) break except LastfmCache.LastfmCacheError: time.sleep(1) validated_track_numbers = release.validate_track_numbers() if validated_track_numbers: flattened_track_nums = [] for disc in validated_track_numbers: flattened_track_nums.append( "\nDisc " + str(disc) + ": " + ",".join(str(i) for i in validated_track_numbers[disc])) violations.add( Violation( ViolationType.MISSING_TRACKS, "Release does not have a full set of tracks:{0}".format( "".join(flattened_track_nums)))) validated_total_tracks = release.validate_total_tracks() for disc in validated_total_tracks: violations.add( Violation( ViolationType.TOTAL_TRACKS_INCONSISTENT, "Release disc {0} has blank, inconsistent or incorrect 'Total Tracks' tags" .format(disc))) # disc number validated_disc_numbers = release.validate_disc_numbers() if validated_disc_numbers: violations.add( Violation( ViolationType.MISSING_DISCS, "Release does not have a full set of discs: {0}".format( ", ".join(str(i) for i in validated_disc_numbers)))) # total discs if not release.validate_total_discs(): violations.add( Violation(ViolationType.TOTAL_DISCS_INCONSISTENT, "Release has incorrect 'Total Discs' tags")) # file type if len(release.get_tag_types()) != 1: violations.add( Violation( ViolationType.TAG_TYPES_INCONSISTENT, "Release has inconsistent tag types: {0}".format(", ".join( [str(x) for x in release.get_tag_types()])))) # bitrate - CBR/VBR/Vx/APS/APE if len(release.get_codecs()) != 1: violations.add( Violation( ViolationType.CODECS_INCONSISTENT, "Release has inconsistent codecs: [{0}]".format(", ".join( release.get_codecs())))) if len(unique([int(x / 1000) for x in release.get_cbr_bitrates()])) > 1: violations.add( Violation( ViolationType.CBR_INCONSISTENT, "Release has inconsistent CBR bitrates: {0}".format( ", ".join([str(x) for x in release.get_cbr_bitrates()])))) # track titles for filename in release.tracks: correct_filename = release.tracks[filename].get_filename( release.is_va()) if correct_filename and filename != correct_filename: violations.add( Violation( ViolationType.FILENAME, "Invalid filename: {0} - should be '{1}'".format( filename, correct_filename))) # forbidden comment substrings for track in release.tracks.values(): if not track.comment: continue for substr in self.forbidden_comment_substrings: if substr in track.comment.lower(): violations.add( Violation( ViolationType.COMMENT_SUBSTRING, "Invalid comment: contains forbidden substring '{0}'" .format(substr))) release.num_violations = len(violations) return list(violations)