コード例 #1
0
    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
コード例 #2
0
 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
コード例 #3
0
 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
コード例 #4
0
 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
コード例 #5
0
 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)
コード例 #6
0
 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]
コード例 #7
0
ファイル: functions.py プロジェクト: spiritualized/centrifuge
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])
コード例 #8
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)))
コード例 #9
0
ファイル: test_Release.py プロジェクト: spiritualized/metafix
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)
コード例 #10
0
    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
コード例 #11
0
    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)
コード例 #12
0
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)
コード例 #13
0
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
コード例 #14
0
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)
コード例 #15
0
    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
コード例 #16
0
 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
コード例 #17
0
    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)