From a883827aaf407b35cbdd3152e057c842622aadfe Mon Sep 17 00:00:00 2001 From: Exafto Date: Fri, 26 Dec 2025 23:55:11 +0200 Subject: [PATCH 1/5] Add harmonicmix plugin for DJ mixing suggestions --- beetsplug/harmonicmix.py | 153 +++++++++++++++++++++++++++++++++++ docs/changelog.rst | 1 + docs/plugins/harmonicmix.rst | 33 ++++++++ test/test_harmonicmix.py | 56 +++++++++++++ 4 files changed, 243 insertions(+) create mode 100644 beetsplug/harmonicmix.py create mode 100644 docs/plugins/harmonicmix.rst create mode 100644 test/test_harmonicmix.py diff --git a/beetsplug/harmonicmix.py b/beetsplug/harmonicmix.py new file mode 100644 index 0000000000..4df061c684 --- /dev/null +++ b/beetsplug/harmonicmix.py @@ -0,0 +1,153 @@ +# This file is part of beets. +# Copyright 2025, Your Name. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Finds songs that are harmonically compatible with a chosen track. +The results also have a matching BPM (+/- 8% from the source track). +""" + +from beets import ui +from beets.plugins import BeetsPlugin + + +class HarmonicLogic: + """Pure music theory logic. + Separated from the plugin for test coverage. + """ + + # Mapping normalized to standard musical notation + # Also covers enharmonics (i.e. F#=Gb and so on) + CIRCLE_OF_FIFTHS = { + # Major Keys + "C": ["C", "B#", "G", "F", "E#", "Am"], + "G": ["G", "D", "C", "B#", "Em", "Fbm"], + "D": ["D", "A", "G", "Bm", "Cbm"], + "A": ["A", "E", "Fb", "D", "F#m", "Gbm"], + "E": ["E", "Fb", "B", "Cb", "A", "C#m", "Dbm"], + "B": ["B", "Cb", "F#", "Gb", "E", "Fb", "G#m", "Abm"], + "Gb": ["Gb", "F#", "Db", "C#", "Cb", "B", "Ebm", "D#m"], + "F#": ["F#", "Gb", "C#", "Db", "B", "Cb", "D#m", "Ebm"], + "Db": ["Db", "C#", "Ab", "G#", "Gb", "F#", "Bbm", "A#m"], + "C#": ["C#", "Db", "G#", "Ab", "F#", "Gb", "A#m", "Bbm"], + "Ab": ["Ab", "G#", "Eb", "D#", "Db", "C#", "Fm", "E#m"], + "Eb": ["Eb", "D#", "Bb", "A#", "Ab", "G#", "Cm", "B#m"], + "Bb": ["Bb", "A#", "F", "E#", "Eb", "D#", "Gm"], + "F": ["F", "E#", "C", "B#", "Bb", "A#", "Dm"], + # Major Enharmonics + "B#": ["C", "B#", "G", "F", "E#", "Am"], + "E#": ["F", "E#", "C", "B#", "Bb", "A#", "Dm"], + "Cb": ["B", "Cb", "F#", "Gb", "E", "Fb", "G#m", "Abm"], + "Fb": ["E", "Fb", "B", "Cb", "A", "C#m", "Dbm"], + # Minor Keys + "Am": ["Am", "Em", "Fbm", "Dm", "C", "B#"], + "Em": ["Em", "Fbm", "Bm", "Cbm", "Am", "G"], + "Bm": ["Bm", "Cbm", "F#m", "Gbm", "Em", "Fbm", "D"], + "F#m": ["F#m", "Gbm", "C#m", "Dbm", "Bm", "Cbm", "A"], + "C#m": ["C#m", "Dbm", "G#m", "Abm", "F#m", "Gbm", "E", "Fb"], + "G#m": ["G#m", "Abm", "D#m", "Ebm", "C#m", "Dbm", "B", "Cb"], + "Ebm": ["Ebm", "D#m", "Bbm", "A#m", "G#m", "Abm", "Gb", "F#"], + "D#m": ["D#m", "Ebm", "A#m", "Bbm", "G#m", "Abm", "F#", "Gb"], + "Bbm": ["Bbm", "A#m", "Fm", "E#m", "Ebm", "D#m", "Db", "C#"], + "Fm": ["Fm", "E#m", "Cm", "B#m", "Bbm", "A#m", "Ab", "G#"], + "Cm": ["Cm", "B#m", "Gm", "Fm", "E#m", "Eb", "D#"], + "Gm": ["Gm", "Dm", "Cm", "B#m", "Bb", "A#"], + "Dm": ["Dm", "Am", "Gm", "F", "E#"], + # Minor Enharmonics + "E#m": ["Fm", "E#m", "Cm", "B#m", "Bbm", "A#m", "Ab", "G#"], + "B#m": ["Cm", "B#m", "Gm", "Fm", "E#m", "Eb", "D#"], + "Cbm": ["Bm", "Cbm", "F#m", "Gbm", "Em", "Fbm", "D"], + "Fbm": ["Em", "Fbm", "Bm", "Cbm", "Am", "G"], + } + + @staticmethod + def get_compatible_keys(key): + """Returns a list of compatible keys for a given input key.""" + # We assume the DB uses standard short notation, i.e. C instead of C major + if not key: + return [] + + # Strip whitespace + key = key.strip() + return HarmonicLogic.CIRCLE_OF_FIFTHS.get(key, []) + + @staticmethod + def get_bpm_range(bpm, range_percent=0.08): + """Returns a tuple (min_bpm, max_bpm)""" + if not bpm: + return (0, 0) + return (bpm * (1 - range_percent), bpm * (1 + range_percent)) + + +class HarmonicMixPlugin(BeetsPlugin): + """The Beets plugin wrapper.""" + + def __init__(self): + super().__init__() + + def commands(self): + cmd = ui.Subcommand("mix", help="find harmonically compatible songs") + cmd.func = self.command + return [cmd] + + def command(self, lib, opts, args): + query = args + items = lib.items(query) + + if not items: + self._log.warning("Song not found!") + return + + source_song = items[0] + # Use .get() to avoid crashing if tags are missing + source_key = source_song.get("key") + source_bpm = source_song.get("bpm") + + if not source_key: + self._log.warning(f"No key found for {source_song.title}") + return + + self._log.info( + f"Source: {source_song.title} | Key: {source_key} | BPM: {source_bpm}" + ) + + # 1. Get Logic from Helper Class + compatible_keys = HarmonicLogic.get_compatible_keys(source_key) + + # 2. BPM Range + # Only use BPM logic if the song actually has a BPM + bpm_query_part = "" + if source_bpm: + min_b, max_b = HarmonicLogic.get_bpm_range(source_bpm) + # Create a range query string for Beets + bpm_query_part = f" bpm:{min_b}..{max_b}" + + # 3. Construct Query + # We want: (Key:A OR Key:B...) AND BPM range + # Beets query syntax for OR is a bit tricky, so we filter manually + # for safety and simplicity, using only the BPM filter in the query. + candidates = lib.items(bpm_query_part.strip()) + + found_count = 0 + for song in candidates: + # skip source + if song.id == source_song.id: + continue + + if song.get("key") in compatible_keys: + self._log.info( + f"MATCH: {song.title} ({song.get('key')}, {song.get('bpm')} BPM)" + ) + found_count += 1 + + if found_count == 0: + self._log.info("No mixable songs found.") diff --git a/docs/changelog.rst b/docs/changelog.rst index a471b4c564..d69179ea10 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -36,6 +36,7 @@ New features: resolve differences in metadata source styles. - :doc:`plugins/spotify`: Added support for multi-artist albums and tracks, saving all contributing artists to the respective fields. +- :doc:`/plugins/harmonicmix`: Suggests harmonically compatible tracks for DJ mixing based on key and BPM. Bug fixes: diff --git a/docs/plugins/harmonicmix.rst b/docs/plugins/harmonicmix.rst new file mode 100644 index 0000000000..564fefbe05 --- /dev/null +++ b/docs/plugins/harmonicmix.rst @@ -0,0 +1,33 @@ +Harmonic Mix Plugin +=================== + +The ``harmonicmix`` plugin is designed to help DJs and playlist curators find songs that mix well together based on the Circle of Fifths. It checks for: + +1. **Harmonic Compatibility:** Finds songs in the same key, the dominant, subdominant, or relative major/minor. It also handles common enharmonics (e.g., F# = Gb). +2. **BPM Matching:** Filters results to show tracks within a mixable BPM range (+/- 8% of the source track). + +Configuration +------------- + +To use the plugin, enable it in your configuration file (``config.yaml``):: + + plugins: harmonicmix + +Usage +----- + +To find songs compatible with a specific track, use the ``mix`` command followed by a query:: + + $ beet mix "Billie Jean" + +The plugin will list tracks that match the harmonic criteria. For example:: + + Source: Billie Jean | Key: F#m | BPM: 117 + ---------------------------------------- + MATCH: Stayin' Alive (F#m, 104 BPM) + MATCH: Another One Bites the Dust (Em, 110 BPM) + +Note that if the source song does not have a ``key`` tag, the plugin cannot find matches. In addition, if a song does not have a ``bpm`` tag, then the matching process only considers the key. +Tags must be in the format: "C" instead of "C major". + +This plugin could also be paired with the ``xtractor`` or ``keyfinder`` plugins for better results. \ No newline at end of file diff --git a/test/test_harmonicmix.py b/test/test_harmonicmix.py new file mode 100644 index 0000000000..49dce697a6 --- /dev/null +++ b/test/test_harmonicmix.py @@ -0,0 +1,56 @@ +# This file is part of beets. +# Copyright 2025, Angelos Exaftopoulos. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Tests for harmonicmix plugin. Test only cover the logic class""" + +from beetsplug.harmonicmix import HarmonicLogic + + +def test_standard_compatibility(): + """Verify standard Circle of Fifths relationships.""" + # Case: C Major should contain G (Dominant), F (Subdominant), Am (Relative Minor) + keys = HarmonicLogic.get_compatible_keys("C") + assert "G" in keys + assert "F" in keys + assert "Am" in keys + + # Case: Am should match Em + keys_am = HarmonicLogic.get_compatible_keys("Am") + assert "Em" in keys_am + + +def test_custom_enharmonics(): + """Verify advanced enharmonics such as E#==Fb""" + # Case: Fbm should be compatible with Am (because Fbm == Em) + keys_am = HarmonicLogic.get_compatible_keys("Am") + assert "Fbm" in keys_am + + # Case: E# should be treated like F + # F is compatible with C, so E# should be in C's list + keys_c = HarmonicLogic.get_compatible_keys("C") + assert "E#" in keys_c + + +def test_bpm_range_calculation(): + """Verify the BPM range logic (+/- 8%).""" + # 100 BPM -> Range should be 92 to 108 + min_b, max_b = HarmonicLogic.get_bpm_range(100, 0.08) + assert min_b == 92.0 + assert max_b == 108.0 + + +def test_empty_input(): + """Ensure it doesn't crash on empty keys.""" + assert HarmonicLogic.get_compatible_keys(None) == [] + assert HarmonicLogic.get_compatible_keys("") == [] From 08e434ff0c701cc31f2f7d8791f7843a07a1c21d Mon Sep 17 00:00:00 2001 From: Exafto Date: Sat, 27 Dec 2025 00:26:45 +0200 Subject: [PATCH 2/5] Update copyright --- beetsplug/harmonicmix.py | 85 +++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/beetsplug/harmonicmix.py b/beetsplug/harmonicmix.py index 4df061c684..1dff2a9d53 100644 --- a/beetsplug/harmonicmix.py +++ b/beetsplug/harmonicmix.py @@ -1,5 +1,5 @@ # This file is part of beets. -# Copyright 2025, Your Name. +# Copyright 2025, Angelos Exaftopoulos. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -16,8 +16,8 @@ The results also have a matching BPM (+/- 8% from the source track). """ -from beets import ui from beets.plugins import BeetsPlugin +from beets import ui class HarmonicLogic: @@ -29,44 +29,47 @@ class HarmonicLogic: # Also covers enharmonics (i.e. F#=Gb and so on) CIRCLE_OF_FIFTHS = { # Major Keys - "C": ["C", "B#", "G", "F", "E#", "Am"], - "G": ["G", "D", "C", "B#", "Em", "Fbm"], - "D": ["D", "A", "G", "Bm", "Cbm"], - "A": ["A", "E", "Fb", "D", "F#m", "Gbm"], - "E": ["E", "Fb", "B", "Cb", "A", "C#m", "Dbm"], - "B": ["B", "Cb", "F#", "Gb", "E", "Fb", "G#m", "Abm"], - "Gb": ["Gb", "F#", "Db", "C#", "Cb", "B", "Ebm", "D#m"], - "F#": ["F#", "Gb", "C#", "Db", "B", "Cb", "D#m", "Ebm"], - "Db": ["Db", "C#", "Ab", "G#", "Gb", "F#", "Bbm", "A#m"], - "C#": ["C#", "Db", "G#", "Ab", "F#", "Gb", "A#m", "Bbm"], - "Ab": ["Ab", "G#", "Eb", "D#", "Db", "C#", "Fm", "E#m"], - "Eb": ["Eb", "D#", "Bb", "A#", "Ab", "G#", "Cm", "B#m"], - "Bb": ["Bb", "A#", "F", "E#", "Eb", "D#", "Gm"], - "F": ["F", "E#", "C", "B#", "Bb", "A#", "Dm"], + 'C': ['C', 'B#', 'G', 'F', 'E#', 'Am'], + 'G': ['G', 'D', 'C', 'B#', 'Em', 'Fbm'], + 'D': ['D', 'A', 'G', 'Bm', 'Cbm'], + 'A': ['A', 'E', 'Fb', 'D', 'F#m', 'Gbm'], + 'E': ['E', 'Fb', 'B', 'Cb', 'A', 'C#m', 'Dbm'], + 'B': ['B', 'Cb', 'F#', 'Gb', 'E', 'Fb', 'G#m', 'Abm'], + 'Gb': ['Gb', 'F#', 'Db', 'C#', 'Cb', 'B', 'Ebm', 'D#m'], + 'F#': ['F#', 'Gb', 'C#', 'Db', 'B', 'Cb', 'D#m', 'Ebm'], + 'Db': ['Db', 'C#', 'Ab', 'G#', 'Gb', 'F#', 'Bbm', 'A#m'], + 'C#': ['C#', 'Db', 'G#', 'Ab', 'F#', 'Gb', 'A#m', 'Bbm'], + 'Ab': ['Ab', 'G#', 'Eb', 'D#', 'Db', 'C#', 'Fm', 'E#m'], + 'Eb': ['Eb', 'D#', 'Bb', 'A#', 'Ab', 'G#', 'Cm', 'B#m'], + 'Bb': ['Bb', 'A#', 'F', 'E#', 'Eb', 'D#', 'Gm'], + 'F': ['F', 'E#', 'C', 'B#', 'Bb', 'A#', 'Dm'], + # Major Enharmonics - "B#": ["C", "B#", "G", "F", "E#", "Am"], - "E#": ["F", "E#", "C", "B#", "Bb", "A#", "Dm"], - "Cb": ["B", "Cb", "F#", "Gb", "E", "Fb", "G#m", "Abm"], - "Fb": ["E", "Fb", "B", "Cb", "A", "C#m", "Dbm"], + 'B#': ['C', 'B#', 'G', 'F', 'E#', 'Am'], + 'E#': ['F', 'E#', 'C', 'B#', 'Bb', 'A#', 'Dm'], + 'Cb': ['B', 'Cb', 'F#', 'Gb', 'E', 'Fb', 'G#m', 'Abm'], + 'Fb': ['E', 'Fb', 'B', 'Cb', 'A', 'C#m', 'Dbm'], + # Minor Keys - "Am": ["Am", "Em", "Fbm", "Dm", "C", "B#"], - "Em": ["Em", "Fbm", "Bm", "Cbm", "Am", "G"], - "Bm": ["Bm", "Cbm", "F#m", "Gbm", "Em", "Fbm", "D"], - "F#m": ["F#m", "Gbm", "C#m", "Dbm", "Bm", "Cbm", "A"], - "C#m": ["C#m", "Dbm", "G#m", "Abm", "F#m", "Gbm", "E", "Fb"], - "G#m": ["G#m", "Abm", "D#m", "Ebm", "C#m", "Dbm", "B", "Cb"], - "Ebm": ["Ebm", "D#m", "Bbm", "A#m", "G#m", "Abm", "Gb", "F#"], - "D#m": ["D#m", "Ebm", "A#m", "Bbm", "G#m", "Abm", "F#", "Gb"], - "Bbm": ["Bbm", "A#m", "Fm", "E#m", "Ebm", "D#m", "Db", "C#"], - "Fm": ["Fm", "E#m", "Cm", "B#m", "Bbm", "A#m", "Ab", "G#"], - "Cm": ["Cm", "B#m", "Gm", "Fm", "E#m", "Eb", "D#"], - "Gm": ["Gm", "Dm", "Cm", "B#m", "Bb", "A#"], - "Dm": ["Dm", "Am", "Gm", "F", "E#"], + 'Am': ['Am', 'Em', 'Fbm', 'Dm', 'C', 'B#'], + 'Em': ['Em', 'Fbm', 'Bm', 'Cbm', 'Am', 'G'], + 'Bm': ['Bm', 'Cbm', 'F#m', 'Gbm', 'Em', 'Fbm', 'D'], + 'F#m': ['F#m', 'Gbm', 'C#m', 'Dbm', 'Bm', 'Cbm', 'A'], + 'C#m': ['C#m', 'Dbm', 'G#m', 'Abm', 'F#m', 'Gbm', 'E', 'Fb'], + 'G#m': ['G#m', 'Abm', 'D#m', 'Ebm', 'C#m', 'Dbm', 'B', 'Cb'], + 'Ebm': ['Ebm', 'D#m', 'Bbm', 'A#m', 'G#m', 'Abm', 'Gb', 'F#'], + 'D#m': ['D#m', 'Ebm', 'A#m', 'Bbm', 'G#m', 'Abm', 'F#', 'Gb'], + 'Bbm': ['Bbm', 'A#m', 'Fm', 'E#m', 'Ebm', 'D#m', 'Db', 'C#'], + 'Fm': ['Fm', 'E#m', 'Cm', 'B#m', 'Bbm', 'A#m', 'Ab', 'G#'], + 'Cm': ['Cm', 'B#m', 'Gm', 'Fm', 'E#m', 'Eb', 'D#'], + 'Gm': ['Gm', 'Dm', 'Cm', 'B#m', 'Bb', 'A#'], + 'Dm': ['Dm', 'Am', 'Gm', 'F', 'E#'], + # Minor Enharmonics - "E#m": ["Fm", "E#m", "Cm", "B#m", "Bbm", "A#m", "Ab", "G#"], - "B#m": ["Cm", "B#m", "Gm", "Fm", "E#m", "Eb", "D#"], - "Cbm": ["Bm", "Cbm", "F#m", "Gbm", "Em", "Fbm", "D"], - "Fbm": ["Em", "Fbm", "Bm", "Cbm", "Am", "G"], + 'E#m': ['Fm', 'E#m', 'Cm', 'B#m', 'Bbm', 'A#m', 'Ab', 'G#'], + 'B#m': ['Cm', 'B#m', 'Gm', 'Fm', 'E#m', 'Eb', 'D#'], + 'Cbm': ['Bm', 'Cbm', 'F#m', 'Gbm', 'Em', 'Fbm', 'D'], + 'Fbm': ['Em', 'Fbm', 'Bm', 'Cbm', 'Am', 'G'], } @staticmethod @@ -95,7 +98,7 @@ def __init__(self): super().__init__() def commands(self): - cmd = ui.Subcommand("mix", help="find harmonically compatible songs") + cmd = ui.Subcommand('mix', help='find harmonically compatible songs') cmd.func = self.command return [cmd] @@ -109,8 +112,8 @@ def command(self, lib, opts, args): source_song = items[0] # Use .get() to avoid crashing if tags are missing - source_key = source_song.get("key") - source_bpm = source_song.get("bpm") + source_key = source_song.get('key') + source_bpm = source_song.get('bpm') if not source_key: self._log.warning(f"No key found for {source_song.title}") @@ -143,7 +146,7 @@ def command(self, lib, opts, args): if song.id == source_song.id: continue - if song.get("key") in compatible_keys: + if song.get('key') in compatible_keys: self._log.info( f"MATCH: {song.title} ({song.get('key')}, {song.get('bpm')} BPM)" ) From e1dbe8940552e52734004b11c3afd3d0f266b75d Mon Sep 17 00:00:00 2001 From: Exafto Date: Sat, 27 Dec 2025 00:59:16 +0200 Subject: [PATCH 3/5] Fix linting, formatting, and extra tests --- beetsplug/harmonicmix.py | 97 +++++++++++++++++++++------------------- test/test_harmonicmix.py | 48 ++++++++++++++------ 2 files changed, 86 insertions(+), 59 deletions(-) diff --git a/beetsplug/harmonicmix.py b/beetsplug/harmonicmix.py index 1dff2a9d53..8ece96d4d7 100644 --- a/beetsplug/harmonicmix.py +++ b/beetsplug/harmonicmix.py @@ -16,8 +16,8 @@ The results also have a matching BPM (+/- 8% from the source track). """ -from beets.plugins import BeetsPlugin from beets import ui +from beets.plugins import BeetsPlugin class HarmonicLogic: @@ -29,47 +29,44 @@ class HarmonicLogic: # Also covers enharmonics (i.e. F#=Gb and so on) CIRCLE_OF_FIFTHS = { # Major Keys - 'C': ['C', 'B#', 'G', 'F', 'E#', 'Am'], - 'G': ['G', 'D', 'C', 'B#', 'Em', 'Fbm'], - 'D': ['D', 'A', 'G', 'Bm', 'Cbm'], - 'A': ['A', 'E', 'Fb', 'D', 'F#m', 'Gbm'], - 'E': ['E', 'Fb', 'B', 'Cb', 'A', 'C#m', 'Dbm'], - 'B': ['B', 'Cb', 'F#', 'Gb', 'E', 'Fb', 'G#m', 'Abm'], - 'Gb': ['Gb', 'F#', 'Db', 'C#', 'Cb', 'B', 'Ebm', 'D#m'], - 'F#': ['F#', 'Gb', 'C#', 'Db', 'B', 'Cb', 'D#m', 'Ebm'], - 'Db': ['Db', 'C#', 'Ab', 'G#', 'Gb', 'F#', 'Bbm', 'A#m'], - 'C#': ['C#', 'Db', 'G#', 'Ab', 'F#', 'Gb', 'A#m', 'Bbm'], - 'Ab': ['Ab', 'G#', 'Eb', 'D#', 'Db', 'C#', 'Fm', 'E#m'], - 'Eb': ['Eb', 'D#', 'Bb', 'A#', 'Ab', 'G#', 'Cm', 'B#m'], - 'Bb': ['Bb', 'A#', 'F', 'E#', 'Eb', 'D#', 'Gm'], - 'F': ['F', 'E#', 'C', 'B#', 'Bb', 'A#', 'Dm'], - + "C": ["C", "B#", "G", "F", "E#", "Am"], + "G": ["G", "D", "C", "B#", "Em", "Fbm"], + "D": ["D", "A", "G", "Bm", "Cbm"], + "A": ["A", "E", "Fb", "D", "F#m", "Gbm"], + "E": ["E", "Fb", "B", "Cb", "A", "C#m", "Dbm"], + "B": ["B", "Cb", "F#", "Gb", "E", "Fb", "G#m", "Abm"], + "Gb": ["Gb", "F#", "Db", "C#", "Cb", "B", "Ebm", "D#m"], + "F#": ["F#", "Gb", "C#", "Db", "B", "Cb", "D#m", "Ebm"], + "Db": ["Db", "C#", "Ab", "G#", "Gb", "F#", "Bbm", "A#m"], + "C#": ["C#", "Db", "G#", "Ab", "F#", "Gb", "A#m", "Bbm"], + "Ab": ["Ab", "G#", "Eb", "D#", "Db", "C#", "Fm", "E#m"], + "Eb": ["Eb", "D#", "Bb", "A#", "Ab", "G#", "Cm", "B#m"], + "Bb": ["Bb", "A#", "F", "E#", "Eb", "D#", "Gm"], + "F": ["F", "E#", "C", "B#", "Bb", "A#", "Dm"], # Major Enharmonics - 'B#': ['C', 'B#', 'G', 'F', 'E#', 'Am'], - 'E#': ['F', 'E#', 'C', 'B#', 'Bb', 'A#', 'Dm'], - 'Cb': ['B', 'Cb', 'F#', 'Gb', 'E', 'Fb', 'G#m', 'Abm'], - 'Fb': ['E', 'Fb', 'B', 'Cb', 'A', 'C#m', 'Dbm'], - + "B#": ["C", "B#", "G", "F", "E#", "Am"], + "E#": ["F", "E#", "C", "B#", "Bb", "A#", "Dm"], + "Cb": ["B", "Cb", "F#", "Gb", "E", "Fb", "G#m", "Abm"], + "Fb": ["E", "Fb", "B", "Cb", "A", "C#m", "Dbm"], # Minor Keys - 'Am': ['Am', 'Em', 'Fbm', 'Dm', 'C', 'B#'], - 'Em': ['Em', 'Fbm', 'Bm', 'Cbm', 'Am', 'G'], - 'Bm': ['Bm', 'Cbm', 'F#m', 'Gbm', 'Em', 'Fbm', 'D'], - 'F#m': ['F#m', 'Gbm', 'C#m', 'Dbm', 'Bm', 'Cbm', 'A'], - 'C#m': ['C#m', 'Dbm', 'G#m', 'Abm', 'F#m', 'Gbm', 'E', 'Fb'], - 'G#m': ['G#m', 'Abm', 'D#m', 'Ebm', 'C#m', 'Dbm', 'B', 'Cb'], - 'Ebm': ['Ebm', 'D#m', 'Bbm', 'A#m', 'G#m', 'Abm', 'Gb', 'F#'], - 'D#m': ['D#m', 'Ebm', 'A#m', 'Bbm', 'G#m', 'Abm', 'F#', 'Gb'], - 'Bbm': ['Bbm', 'A#m', 'Fm', 'E#m', 'Ebm', 'D#m', 'Db', 'C#'], - 'Fm': ['Fm', 'E#m', 'Cm', 'B#m', 'Bbm', 'A#m', 'Ab', 'G#'], - 'Cm': ['Cm', 'B#m', 'Gm', 'Fm', 'E#m', 'Eb', 'D#'], - 'Gm': ['Gm', 'Dm', 'Cm', 'B#m', 'Bb', 'A#'], - 'Dm': ['Dm', 'Am', 'Gm', 'F', 'E#'], - + "Am": ["Am", "Em", "Fbm", "Dm", "C", "B#"], + "Em": ["Em", "Fbm", "Bm", "Cbm", "Am", "G"], + "Bm": ["Bm", "Cbm", "F#m", "Gbm", "Em", "Fbm", "D"], + "F#m": ["F#m", "Gbm", "C#m", "Dbm", "Bm", "Cbm", "A"], + "C#m": ["C#m", "Dbm", "G#m", "Abm", "F#m", "Gbm", "E", "Fb"], + "G#m": ["G#m", "Abm", "D#m", "Ebm", "C#m", "Dbm", "B", "Cb"], + "Ebm": ["Ebm", "D#m", "Bbm", "A#m", "G#m", "Abm", "Gb", "F#"], + "D#m": ["D#m", "Ebm", "A#m", "Bbm", "G#m", "Abm", "F#", "Gb"], + "Bbm": ["Bbm", "A#m", "Fm", "E#m", "Ebm", "D#m", "Db", "C#"], + "Fm": ["Fm", "E#m", "Cm", "B#m", "Bbm", "A#m", "Ab", "G#"], + "Cm": ["Cm", "B#m", "Gm", "Fm", "E#m", "Eb", "D#"], + "Gm": ["Gm", "Dm", "Cm", "B#m", "Bb", "A#"], + "Dm": ["Dm", "Am", "Gm", "F", "E#"], # Minor Enharmonics - 'E#m': ['Fm', 'E#m', 'Cm', 'B#m', 'Bbm', 'A#m', 'Ab', 'G#'], - 'B#m': ['Cm', 'B#m', 'Gm', 'Fm', 'E#m', 'Eb', 'D#'], - 'Cbm': ['Bm', 'Cbm', 'F#m', 'Gbm', 'Em', 'Fbm', 'D'], - 'Fbm': ['Em', 'Fbm', 'Bm', 'Cbm', 'Am', 'G'], + "E#m": ["Fm", "E#m", "Cm", "B#m", "Bbm", "A#m", "Ab", "G#"], + "B#m": ["Cm", "B#m", "Gm", "Fm", "E#m", "Eb", "D#"], + "Cbm": ["Bm", "Cbm", "F#m", "Gbm", "Em", "Fbm", "D"], + "Fbm": ["Em", "Fbm", "Bm", "Cbm", "Am", "G"], } @staticmethod @@ -86,8 +83,14 @@ def get_compatible_keys(key): @staticmethod def get_bpm_range(bpm, range_percent=0.08): """Returns a tuple (min_bpm, max_bpm)""" - if not bpm: - return (0, 0) + try: + bpm = float(bpm) + except (ValueError, TypeError): + return 0, 0 + + if bpm <= 0: + return 0, 0 + return (bpm * (1 - range_percent), bpm * (1 + range_percent)) @@ -98,7 +101,7 @@ def __init__(self): super().__init__() def commands(self): - cmd = ui.Subcommand('mix', help='find harmonically compatible songs') + cmd = ui.Subcommand("mix", help="find harmonically compatible songs") cmd.func = self.command return [cmd] @@ -112,8 +115,8 @@ def command(self, lib, opts, args): source_song = items[0] # Use .get() to avoid crashing if tags are missing - source_key = source_song.get('key') - source_bpm = source_song.get('bpm') + source_key = source_song.get("key") + source_bpm = source_song.get("bpm") if not source_key: self._log.warning(f"No key found for {source_song.title}") @@ -132,7 +135,9 @@ def command(self, lib, opts, args): if source_bpm: min_b, max_b = HarmonicLogic.get_bpm_range(source_bpm) # Create a range query string for Beets - bpm_query_part = f" bpm:{min_b}..{max_b}" + # Rounding to integer for cleaner query, as suggested by review + if min_b > 0 and max_b > 0: + bpm_query_part = f" bpm:{int(min_b)}..{int(max_b)}" # 3. Construct Query # We want: (Key:A OR Key:B...) AND BPM range @@ -146,7 +151,7 @@ def command(self, lib, opts, args): if song.id == source_song.id: continue - if song.get('key') in compatible_keys: + if song.get("key") in compatible_keys: self._log.info( f"MATCH: {song.title} ({song.get('key')}, {song.get('bpm')} BPM)" ) diff --git a/test/test_harmonicmix.py b/test/test_harmonicmix.py index 49dce697a6..47487ada69 100644 --- a/test/test_harmonicmix.py +++ b/test/test_harmonicmix.py @@ -30,16 +30,29 @@ def test_standard_compatibility(): assert "Em" in keys_am -def test_custom_enharmonics(): - """Verify advanced enharmonics such as E#==Fb""" - # Case: Fbm should be compatible with Am (because Fbm == Em) - keys_am = HarmonicLogic.get_compatible_keys("Am") - assert "Fbm" in keys_am +def test_enharmonic_compatibility(): + """Verify that enharmonics are handled.""" + # Db should behave like C# + keys_db = HarmonicLogic.get_compatible_keys("Db") + keys_cs = HarmonicLogic.get_compatible_keys("C#") + + # They should share neighbors + assert "Ab" in keys_db # Dominant of Db + assert "G#" in keys_cs # Dominant of C# (G#=Ab) + - # Case: E# should be treated like F - # F is compatible with C, so E# should be in C's list - keys_c = HarmonicLogic.get_compatible_keys("C") - assert "E#" in keys_c +def test_whitespace_handling(): + """Verify that whitespace is stripped from keys.""" + keys_plain = HarmonicLogic.get_compatible_keys("C") + keys_spaced = HarmonicLogic.get_compatible_keys(" C ") + assert keys_plain == keys_spaced + + +def test_unknown_keys(): + """Verify that unknown keys return an empty list.""" + assert HarmonicLogic.get_compatible_keys("H#") == [] + assert HarmonicLogic.get_compatible_keys("NotAKey") == [] + assert HarmonicLogic.get_compatible_keys(None) == [] def test_bpm_range_calculation(): @@ -50,7 +63,16 @@ def test_bpm_range_calculation(): assert max_b == 108.0 -def test_empty_input(): - """Ensure it doesn't crash on empty keys.""" - assert HarmonicLogic.get_compatible_keys(None) == [] - assert HarmonicLogic.get_compatible_keys("") == [] +def test_bpm_range_edge_cases(): + """Verify BPM edge cases (None, Zero, String input).""" + # None or Zero should return (0, 0) + assert HarmonicLogic.get_bpm_range(None) == (0, 0) + assert HarmonicLogic.get_bpm_range(0) == (0, 0) + + # Strings should be converted safely + min_b, max_b = HarmonicLogic.get_bpm_range("100") + assert min_b == 92.0 + assert max_b == 108.0 + + # Invalid strings should return (0, 0) + assert HarmonicLogic.get_bpm_range("fast") == (0, 0) From 81ae6f71dea8a648f22620a6ead9968038ecfba9 Mon Sep 17 00:00:00 2001 From: Exafto Date: Sat, 27 Dec 2025 01:12:41 +0200 Subject: [PATCH 4/5] Fix documentation formatting --- docs/changelog.rst | 333 +++++++++++++++++++++-------------- docs/plugins/harmonicmix.rst | 47 +++-- 2 files changed, 232 insertions(+), 148 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d69179ea10..ad420f0f7c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,11 +1,13 @@ -Changelog -========= +########### + Changelog +########### Changelog goes here! Please add your entry to the bottom of one of the lists below! -Unreleased ----------- +************ + Unreleased +************ Beets now requires Python 3.10 or later since support for EOL Python 3.9 has been dropped. @@ -36,7 +38,8 @@ New features: resolve differences in metadata source styles. - :doc:`plugins/spotify`: Added support for multi-artist albums and tracks, saving all contributing artists to the respective fields. -- :doc:`/plugins/harmonicmix`: Suggests harmonically compatible tracks for DJ mixing based on key and BPM. +- :doc:`/plugins/harmonicmix`: Suggests harmonically compatible tracks for DJ + mixing based on key and BPM. Bug fixes: @@ -95,8 +98,9 @@ Other changes: - Finally removed gmusic plugin and all related code/docs as the Google Play Music service was shut down in 2020. -2.5.1 (October 14, 2025) ------------------------- +************************** + 2.5.1 (October 14, 2025) +************************** New features: @@ -129,8 +133,9 @@ Other changes: automatically. Applied it to :doc:`plugins/deezer`, :doc:`plugins/discogs`, :doc:`plugins/musicbrainz` and :doc:`plugins/spotify` plugins documentation. -2.5.0 (October 11, 2025) ------------------------- +************************** + 2.5.0 (October 11, 2025) +************************** New features: @@ -213,8 +218,9 @@ For developers and plugin authors: - Metadata source plugins are now registered globally when instantiated, which makes their handling slightly more efficient. -2.4.0 (September 13, 2025) --------------------------- +**************************** + 2.4.0 (September 13, 2025) +**************************** New features: @@ -357,8 +363,9 @@ Other changes: - UI: Use ``text_diff_added`` and ``text_diff_removed`` colors in **all** diff comparisons, including case differences. -2.3.1 (May 14, 2025) --------------------- +********************** + 2.3.1 (May 14, 2025) +********************** Bug fixes: @@ -371,8 +378,9 @@ For packagers: - Force ``poetry`` version below 2 to avoid it mangling file modification times in ``sdist`` package. :bug:`5770` -2.3.0 (May 07, 2025) --------------------- +********************** + 2.3.0 (May 07, 2025) +********************** Beets now requires Python 3.9 or later since support for EOL Python 3.8 has been dropped. @@ -461,8 +469,9 @@ Other changes: to the database. - Database models are now serializable with pickle. -2.2.0 (December 02, 2024) -------------------------- +*************************** + 2.2.0 (December 02, 2024) +*************************** New features: @@ -487,8 +496,9 @@ Other changes: .. _contribute: https://github.com/beetbox/beets/contribute -2.1.0 (November 22, 2024) -------------------------- +*************************** + 2.1.0 (November 22, 2024) +*************************** New features: @@ -577,8 +587,9 @@ Other changes: calculate the bpm. Previously this import was being done immediately, so every ``beet`` invocation was being delayed by a couple of seconds. :bug:`5185` -2.0.0 (May 30, 2024) --------------------- +********************** + 2.0.0 (May 30, 2024) +********************** With this release, beets now requires Python 3.7 or later (it removes support for Python 3.6). @@ -889,8 +900,9 @@ Other changes: - :doc:`/faq`: :ref:`src`: Removed some long lines. - Refactor the test cases to avoid test smells. -1.6.0 (November 27, 2021) -------------------------- +*************************** + 1.6.0 (November 27, 2021) +*************************** This release is our first experiment with time-based releases! We are aiming to publish a new release of beets every 3 months. We therefore have a healthy but @@ -970,8 +982,9 @@ Here are some notes for packagers: Thus, the optional dependency on ``gmusicapi`` does not exist anymore. :bug:`4089` -1.5.0 (August 19, 2021) ------------------------ +************************* + 1.5.0 (August 19, 2021) +************************* This long overdue release of beets includes far too many exciting and useful features than could ever be satisfactorily enumerated. As a technical detail, it @@ -1361,8 +1374,9 @@ For packagers: .. _works: https://musicbrainz.org/doc/Work -1.4.9 (May 30, 2019) --------------------- +********************** + 1.4.9 (May 30, 2019) +********************** This small update is part of our attempt to release new versions more often! There are a few important fixes, and we're clearing the deck for a change to @@ -1392,8 +1406,9 @@ Here's a note for packagers: .. _no_color: https://no-color.org -1.4.8 (May 16, 2019) --------------------- +********************** + 1.4.8 (May 16, 2019) +********************** This release is far too long in coming, but it's a good one. There is the usual torrent of new features and a ridiculously long line of fixes, but there are @@ -1620,8 +1635,9 @@ And some messages for packagers: - The optional :pypi:`python-itunes` dependency has been removed. - Python versions 3.7 and 3.8 are now supported. -1.4.7 (May 29, 2018) --------------------- +********************** + 1.4.7 (May 29, 2018) +********************** This new release includes lots of new features in the importer and the metadata source backends that it uses. We've changed how the beets importer handles @@ -1744,8 +1760,9 @@ There are a couple of changes for developers: all cases, most notably when using the ``mbsync`` plugin. This was a regression since version 1.4.1. :bug:`2921` -1.4.6 (December 21, 2017) -------------------------- +*************************** + 1.4.6 (December 21, 2017) +*************************** The highlight of this release is "album merging," an oft-requested option in the importer to add new tracks to an existing album you already have in your @@ -1849,8 +1866,9 @@ There are some changes for developers: describing the file operation instead of multiple Boolean flags. There is a new numerated type describing how to move, copy, or link files. :bug:`2682` -1.4.5 (June 20, 2017) ---------------------- +*********************** + 1.4.5 (June 20, 2017) +*********************** Version 1.4.5 adds some oft-requested features. When you're importing files, you can now manually set fields on the new music. Date queries have gotten much more @@ -1891,8 +1909,9 @@ There are also some bug fixes: - More informative error messages are displayed when the file format is not recognized. :bug:`2599` -1.4.4 (June 10, 2017) ---------------------- +*********************** + 1.4.4 (June 10, 2017) +*********************** This release built up a longer-than-normal list of nifty new features. We now support DSF audio files and the importer can hard-link your files, for example. @@ -2011,8 +2030,9 @@ We removed backends from two metadata plugins because of bitrot: .. _python-itunes: https://github.com/ocelma/python-itunes -1.4.3 (January 9, 2017) ------------------------ +************************* + 1.4.3 (January 9, 2017) +************************* Happy new year! This new version includes a cornucopia of new features from contributors, including new tags related to classical music and a new @@ -2077,8 +2097,9 @@ For plugin developers: when providing new importer prompt choices (see to consider. For example, you might provide an alternative strategy for picking between the available alternatives or for looking up a release on MusicBrainz. -1.4.2 (December 16, 2016) -------------------------- +*************************** + 1.4.2 (December 16, 2016) +*************************** This is just a little bug fix release. With 1.4.2, we're also confident enough to recommend that anyone who's interested give Python 3 a try: bugs may still @@ -2101,8 +2122,9 @@ Also, we've removed some special handling for logging in the :doc:`/plugins/discogs` that we believe was unnecessary. If spurious log messages appear in this version, please let us know by filing a bug. -1.4.1 (November 25, 2016) -------------------------- +*************************** + 1.4.1 (November 25, 2016) +*************************** Version 1.4 has **alpha-level** Python 3 support. Thanks to the heroic efforts of :user:`jrobeson`, beets should run both under Python 2.7, as before, and now @@ -2196,8 +2218,9 @@ you typed ``beet version``. This has been corrected. .. _six: https://pypi.org/project/six/ -1.3.19 (June 25, 2016) ----------------------- +************************ + 1.3.19 (June 25, 2016) +************************ This is primarily a bug fix release: it cleans up a couple of regressions that appeared in the last version. But it also features the triumphant return of the @@ -2247,8 +2270,9 @@ And other fixes: versions, the plugin would use a ``.jpg`` extension for all images. :bug:`2053` -1.3.18 (May 31, 2016) ---------------------- +*********************** + 1.3.18 (May 31, 2016) +*********************** This update adds a new :doc:`/plugins/hook` that lets you integrate beets with command-line tools and an :doc:`/plugins/export` that can dump data from the @@ -2328,8 +2352,9 @@ Fixes: - :doc:`/plugins/acousticbrainz`: AcousticBrainz lookups are now done over HTTPS. Thanks to :user:`Freso`. :bug:`2007` -1.3.17 (February 7, 2016) -------------------------- +*************************** + 1.3.17 (February 7, 2016) +*************************** This release introduces one new plugin to fetch audio information from the AcousticBrainz_ project and another plugin to make it easier to submit your @@ -2407,8 +2432,9 @@ Fixes: .. _beets.io: https://beets.io/ -1.3.16 (December 28, 2015) --------------------------- +**************************** + 1.3.16 (December 28, 2015) +**************************** The big news in this release is a new :doc:`interactive editor plugin `. It's really nifty: you can now change your music's metadata by @@ -2520,8 +2546,9 @@ Fixes: .. _emby: https://emby.media -1.3.15 (October 17, 2015) -------------------------- +*************************** + 1.3.15 (October 17, 2015) +*************************** This release adds a new plugin for checking file quality and a new source for lyrics. The larger features are: @@ -2583,8 +2610,9 @@ This release has plenty of fixes: - Fixed unit of file size to powers of two (MiB, GiB, etc.) instead of powers of ten (MB, GB, etc.). :bug:`1623` -1.3.14 (August 2, 2015) ------------------------ +************************* + 1.3.14 (August 2, 2015) +************************* This is mainly a bugfix release, but we also have a nifty new plugin for ipfs_ and a bunch of new configuration options. @@ -2679,8 +2707,9 @@ Fixes: .. _python bug: https://bugs.python.org/issue16512 -1.3.13 (April 24, 2015) ------------------------ +************************* + 1.3.13 (April 24, 2015) +************************* This is a tiny bug-fix release. It copes with a dependency upgrade that broke beets. There are just two fixes: @@ -2691,8 +2720,9 @@ beets. There are just two fixes: album art is no longer embedded on import in order to leave files untouched---in effect, ``auto`` is implicitly disabled. :bug:`1427` -1.3.12 (April 18, 2015) ------------------------ +************************* + 1.3.12 (April 18, 2015) +************************* This little update makes queries more powerful, sorts music more intelligently, and removes a performance bottleneck. There's an experimental new plugin for @@ -2750,8 +2780,9 @@ Little fixes and improvements: .. _jellyfish: https://github.com/sunlightlabs/jellyfish -1.3.11 (April 5, 2015) ----------------------- +************************ + 1.3.11 (April 5, 2015) +************************ In this release, we refactored the logging system to be more flexible and more useful. There are more granular levels of verbosity, the output from plugins @@ -2941,8 +2972,9 @@ For developers: .. _bs1770gain: http://bs1770gain.sourceforge.net -1.3.10 (January 5, 2015) ------------------------- +************************** + 1.3.10 (January 5, 2015) +************************** This version adds a healthy helping of new features and fixes a critical MPEG-4--related bug. There are more lyrics sources, there new plugins for @@ -3025,8 +3057,9 @@ As usual, there are loads of little fixes and improvements: .. _plex: https://plex.tv/ -1.3.9 (November 17, 2014) -------------------------- +*************************** + 1.3.9 (November 17, 2014) +*************************** This release adds two new standard plugins to beets: one for synchronizing Last.fm listening data and one for integrating with Linux desktops. And at long @@ -3132,8 +3165,9 @@ And countless little improvements and fixes: - Importing an archive will no longer leave temporary files behind in ``/tmp``. Thanks to :user:`multikatt`. :bug:`1067`, :bug:`1091` -1.3.8 (September 17, 2014) --------------------------- +**************************** + 1.3.8 (September 17, 2014) +**************************** This release has two big new chunks of functionality. Queries now support **sorting** and user-defined fields can now have **types**. @@ -3209,8 +3243,9 @@ Still more fixes and little improvements: .. _discogs_client: https://github.com/discogs/discogs_client -1.3.7 (August 22, 2014) ------------------------ +************************* + 1.3.7 (August 22, 2014) +************************* This release of beets fixes all the bugs, and you can be confident that you will never again find any bugs in beets, ever. It also adds support for plain old @@ -3309,8 +3344,9 @@ And the multitude of little improvements and fixes: - :doc:`/plugins/mbsync`: Track alignment now works with albums that have multiple copies of the same recording. Thanks to Rui Gonçalves. -1.3.6 (May 10, 2014) --------------------- +********************** + 1.3.6 (May 10, 2014) +********************** This is primarily a bugfix release, but it also brings two new plugins: one for playing music in desktop players and another for organizing your directories @@ -3352,8 +3388,9 @@ And those all-important bug fixes: were embedded into the source files. - New plugin event: ``before_item_moved``. Thanks to Robert Speicher. -1.3.5 (April 15, 2014) ----------------------- +************************ + 1.3.5 (April 15, 2014) +************************ This is a short-term release that adds some great new stuff to beets. There's support for tracking and calculating musical keys, the ReplayGain plugin was @@ -3414,8 +3451,9 @@ There are also many bug fixes and little enhancements: .. _enum34: https://pypi.python.org/pypi/enum34 -1.3.4 (April 5, 2014) ---------------------- +*********************** + 1.3.4 (April 5, 2014) +*********************** This release brings a hodgepodge of medium-sized conveniences to beets. A new :ref:`config-cmd` command manages your configuration, we now have :ref:`bash @@ -3492,8 +3530,9 @@ Fixes: .. _requests: https://requests.readthedocs.io/en/master/ -1.3.3 (February 26, 2014) -------------------------- +*************************** + 1.3.3 (February 26, 2014) +*************************** Version 1.3.3 brings a bunch changes to how item and album fields work internally. Along with laying the groundwork for some great things in the @@ -3589,8 +3628,9 @@ Other little fixes: - Album art in files with Vorbis Comments is now marked with the "front cover" type. Thanks to Jason Lefley. -1.3.2 (December 22, 2013) -------------------------- +*************************** + 1.3.2 (December 22, 2013) +*************************** This update brings new plugins for fetching acoustic metrics and listening statistics, many more options for the duplicate detection plugin, and flexible @@ -3664,8 +3704,9 @@ As usual, there are also innumerable little fixes and improvements: .. _mpd: https://www.musicpd.org/ -1.3.1 (October 12, 2013) ------------------------- +************************** + 1.3.1 (October 12, 2013) +************************** This release boasts a host of new little features, many of them contributed by beets' amazing and prolific community. It adds support for Opus_ files, @@ -3730,8 +3771,9 @@ And some fixes: .. _opus: https://www.opus-codec.org/ -1.3.0 (September 11, 2013) --------------------------- +**************************** + 1.3.0 (September 11, 2013) +**************************** Albums and items now have **flexible attributes**. This means that, when you want to store information about your music in the beets database, you're no @@ -3772,8 +3814,9 @@ There's more detail than you could ever need `on the beets blog`_. .. _on the beets blog: https://beets.io/blog/flexattr.html -1.2.2 (August 27, 2013) ------------------------ +************************* + 1.2.2 (August 27, 2013) +************************* This is a bugfix release. We're in the midst of preparing for a large change in beets 1.3, so 1.2.2 resolves some issues that came up over the last few weeks. @@ -3796,8 +3839,9 @@ The improvements in this release are: situation could only arise when importing music from the library directory and when the two albums are imported close in time. -1.2.1 (June 22, 2013) ---------------------- +*********************** + 1.2.1 (June 22, 2013) +*********************** This release introduces a major internal change in the way that similarity scores are handled. It means that the importer interface can now show you @@ -3846,8 +3890,9 @@ And some little enhancements and bug fixes: - :doc:`/plugins/random`: Fix compatibility with Python 2.6. Thanks to Matthias Drochner. -1.2.0 (June 5, 2013) --------------------- +********************** + 1.2.0 (June 5, 2013) +********************** There's a *lot* of new stuff in this release: new data sources for the autotagger, new plugins to look for problems in your library, tracking the date @@ -3960,8 +4005,9 @@ And a batch of fixes: .. _discogs: https://discogs.com/ -1.1.0 (April 29, 2013) ----------------------- +************************ + 1.1.0 (April 29, 2013) +************************ This final release of 1.1 brings a little polish to the betas that introduced the new configuration system. The album art and lyrics plugins also got a little @@ -4007,8 +4053,9 @@ will automatically migrate your configuration to the new system. .. _tomahawk: https://github.com/tomahawk-player/tomahawk -1.1b3 (March 16, 2013) ----------------------- +************************ + 1.1b3 (March 16, 2013) +************************ This third beta of beets 1.1 brings a hodgepodge of little new features (and internal overhauls that will make improvements easier in the future). There are @@ -4076,8 +4123,9 @@ Other stuff: - :doc:`/plugins/chroma`: Catch Acoustid Web service errors when submitting fingerprints. -1.1b2 (February 16, 2013) -------------------------- +*************************** + 1.1b2 (February 16, 2013) +*************************** The second beta of beets 1.1 uses the fancy new configuration infrastructure to add many, many new config options. The import process is more flexible; @@ -4179,8 +4227,9 @@ Other new stuff: .. _itunes sound check: https://support.apple.com/kb/HT2425 -1.1b1 (January 29, 2013) ------------------------- +************************** + 1.1b1 (January 29, 2013) +************************** This release entirely revamps beets' configuration system. The configuration file is now a YAML_ document and is located, along with other support files, in @@ -4213,8 +4262,9 @@ It also adds some new features: - :doc:`/plugins/importfeeds`: Added a new configuration option that controls the base for relative paths used in m3u files. Thanks to Philippe Mongeau. -1.0.0 (January 29, 2013) ------------------------- +************************** + 1.0.0 (January 29, 2013) +************************** After fifteen betas and two release candidates, beets has finally hit one-point-oh. Congratulations to everybody involved. This version of beets will @@ -4228,8 +4278,9 @@ ongoing in the betas of version 1.1. when analyzing non-ASCII filenames. - Silence a spurious warning from version 0.04.12 of the Unidecode module. -1.0rc2 (December 31, 2012) --------------------------- +**************************** + 1.0rc2 (December 31, 2012) +**************************** This second release candidate follows quickly after rc1 and fixes a few small bugs found since that release. There were a couple of regressions and some bugs @@ -4242,8 +4293,9 @@ in a newly added plugin. not available from some sources. - Fix a regression on Windows that caused all relative paths to be "not found". -1.0rc1 (December 17, 2012) --------------------------- +**************************** + 1.0rc1 (December 17, 2012) +**************************** The first release candidate for beets 1.0 includes a deluge of new features contributed by beets users. The vast majority of the credit for this release @@ -4352,8 +4404,9 @@ today on features for version 1.1. .. _tomahawk resolver: https://beets.io/blog/tomahawk-resolver.html -1.0b15 (July 26, 2012) ----------------------- +************************ + 1.0b15 (July 26, 2012) +************************ The fifteenth (!) beta of beets is compendium of small fixes and features, most of which represent long-standing requests. The improvements include matching @@ -4461,8 +4514,9 @@ fetching cover art for your music, enable this plugin after upgrading to beets .. _artist credits: https://wiki.musicbrainz.org/Artist_Credit -1.0b14 (May 12, 2012) ---------------------- +*********************** + 1.0b14 (May 12, 2012) +*********************** The centerpiece of this beets release is the graceful handling of similarly-named albums. It's now possible to import two albums with the same @@ -4547,8 +4601,9 @@ release. .. _pyacoustid: https://github.com/beetbox/pyacoustid -1.0b13 (March 16, 2012) ------------------------ +************************* + 1.0b13 (March 16, 2012) +************************* Beets 1.0b13 consists of a plethora of small but important fixes and refinements. A lyrics plugin is now included with beets; new audio properties @@ -4618,8 +4673,9 @@ to come in the next couple of releases. .. _colorama: https://pypi.python.org/pypi/colorama -1.0b12 (January 16, 2012) -------------------------- +*************************** + 1.0b12 (January 16, 2012) +*************************** This release focuses on making beets' path formatting vastly more powerful. It adds a function syntax for transforming text. Via a new plugin, arbitrary Python @@ -4670,8 +4726,9 @@ filenames that would otherwise conflict. Three new plugins (``inline``, - Removed the ``--path-format`` global flag for ``beet``. - Removed the ``lastid`` plugin, which was deprecated in the previous version. -1.0b11 (December 12, 2011) --------------------------- +**************************** + 1.0b11 (December 12, 2011) +**************************** This version of beets focuses on transitioning the autotagger to the new version of the MusicBrainz database (called NGS). This transition brings with it a @@ -4741,8 +4798,9 @@ release: one for assigning genres and another for ReplayGain analysis. .. _simon chopin: https://github.com/laarmen -1.0b10 (September 22, 2011) ---------------------------- +***************************** + 1.0b10 (September 22, 2011) +***************************** This version of beets focuses on making it easier to manage your metadata *after* you've imported it. A bumper crop of new commands has been added: a @@ -4792,8 +4850,9 @@ plugin. - Fix Unicode encoding of album artist, album type, and label. - Fix crash when "copying" an art file that's already in place. -1.0b9 (July 9, 2011) --------------------- +********************** + 1.0b9 (July 9, 2011) +********************** This release focuses on a large number of small fixes and improvements that turn beets into a well-oiled, music-devouring machine. See the full release notes, @@ -4868,8 +4927,9 @@ below, for a plethora of new features. .. _xargs: https://en.wikipedia.org/wiki/xargs -1.0b8 (April 28, 2011) ----------------------- +************************ + 1.0b8 (April 28, 2011) +************************ This release of beets brings two significant new features. First, beets now has first-class support for "singleton" tracks. Previously, it was only really meant @@ -4919,8 +4979,9 @@ that functionality. - Fix adding individual tracks in BPD. - Fix crash when ``~/.beetsconfig`` does not exist. -1.0b7 (April 5, 2011) ---------------------- +*********************** + 1.0b7 (April 5, 2011) +*********************** Beta 7's focus is on better support for "various artists" releases. These albums can be treated differently via the new ``[paths]`` config section and the @@ -4978,8 +5039,9 @@ new configuration options and the ability to clean up empty directory subtrees. .. _as specified by musicbrainz: https://wiki.musicbrainz.org/ReleaseType -1.0b6 (January 20, 2011) ------------------------- +************************** + 1.0b6 (January 20, 2011) +************************** This version consists primarily of bug fixes and other small improvements. It's in preparation for a more feature-ful release in beta 7. The most important @@ -5020,8 +5082,9 @@ issue involves correct ordering of autotagged albums. .. _upstream bug: https://github.com/quodlibet/mutagen/issues/7 -1.0b5 (September 28, 2010) --------------------------- +**************************** + 1.0b5 (September 28, 2010) +**************************** This version of beets focuses on increasing the accuracy of the autotagger. The main addition is an included plugin that uses acoustic fingerprinting to match @@ -5069,8 +5132,9 @@ are also rolled into this release. .. _!!!: https://musicbrainz.org/artist/f26c72d3-e52c-467b-b651-679c73d8e1a7.html -1.0b4 (August 9, 2010) ----------------------- +************************ + 1.0b4 (August 9, 2010) +************************ This thrilling new release of beets focuses on making the tagger more usable in a variety of ways. First and foremost, it should now be much faster: the tagger @@ -5142,8 +5206,9 @@ Here's the detailed list of changes: - The tagger should now be a little more reluctant to reorder tracks that already have indices. -1.0b3 (July 22, 2010) ---------------------- +*********************** + 1.0b3 (July 22, 2010) +*********************** This release features two major additions to the autotagger's functionality: album art fetching and MusicBrainz ID tags. It also contains some important @@ -5210,8 +5275,9 @@ when accessed through FUSE. Check it out! .. _beetfs: https://github.com/jbaiter/beetfs -1.0b2 (July 7, 2010) --------------------- +********************** + 1.0b2 (July 7, 2010) +********************** This release focuses on high-priority fixes and conspicuously missing features. Highlights include support for two new audio formats (Monkey's Audio and Ogg @@ -5238,7 +5304,8 @@ Vorbis) and an option to log untaggable albums during import. .. _a hand-rolled solution: https://gist.github.com/462717 -1.0b1 (June 17, 2010) ---------------------- +*********************** + 1.0b1 (June 17, 2010) +*********************** Initial release. diff --git a/docs/plugins/harmonicmix.rst b/docs/plugins/harmonicmix.rst index 564fefbe05..cde22d5729 100644 --- a/docs/plugins/harmonicmix.rst +++ b/docs/plugins/harmonicmix.rst @@ -1,33 +1,50 @@ -Harmonic Mix Plugin -=================== +##################### + Harmonic Mix Plugin +##################### -The ``harmonicmix`` plugin is designed to help DJs and playlist curators find songs that mix well together based on the Circle of Fifths. It checks for: +The ``harmonicmix`` plugin is designed to help DJs and playlist curators find +songs that mix well together based on the Circle of Fifths. It checks for: -1. **Harmonic Compatibility:** Finds songs in the same key, the dominant, subdominant, or relative major/minor. It also handles common enharmonics (e.g., F# = Gb). -2. **BPM Matching:** Filters results to show tracks within a mixable BPM range (+/- 8% of the source track). +1. **Harmonic Compatibility:** Finds songs in the same key, the dominant, + subdominant, or relative major/minor. It also handles common enharmonics + (e.g., F# = Gb). +2. **BPM Matching:** Filters results to show tracks within a mixable BPM range + (+/- 8% of the source track). -Configuration -------------- +*************** + Configuration +*************** -To use the plugin, enable it in your configuration file (``config.yaml``):: +To use the plugin, enable it in your configuration file (``config.yaml``): + +:: plugins: harmonicmix -Usage ------ +******* + Usage +******* + +To find songs compatible with a specific track, use the ``mix`` command followed +by a query: -To find songs compatible with a specific track, use the ``mix`` command followed by a query:: +:: $ beet mix "Billie Jean" -The plugin will list tracks that match the harmonic criteria. For example:: +The plugin will list tracks that match the harmonic criteria. For example: + +:: Source: Billie Jean | Key: F#m | BPM: 117 ---------------------------------------- MATCH: Stayin' Alive (F#m, 104 BPM) MATCH: Another One Bites the Dust (Em, 110 BPM) -Note that if the source song does not have a ``key`` tag, the plugin cannot find matches. In addition, if a song does not have a ``bpm`` tag, then the matching process only considers the key. -Tags must be in the format: "C" instead of "C major". +Note that if the source song does not have a ``key`` tag, the plugin cannot find +matches. In addition, if a song does not have a ``bpm`` tag, then the matching +process only considers the key. Tags must be in the format: "C" instead of "C +major". -This plugin could also be paired with the ``xtractor`` or ``keyfinder`` plugins for better results. \ No newline at end of file +This plugin could also be paired with the ``xtractor`` or ``keyfinder`` plugins +for better results. From 66aceabcbbd7c5b32c512d73a1f5794b2a1d915c Mon Sep 17 00:00:00 2001 From: Exafto Date: Sat, 27 Dec 2025 01:40:18 +0200 Subject: [PATCH 5/5] Fix RST header formatting and changelog alignment --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ad420f0f7c..66e0b4554b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -38,7 +38,7 @@ New features: resolve differences in metadata source styles. - :doc:`plugins/spotify`: Added support for multi-artist albums and tracks, saving all contributing artists to the respective fields. -- :doc:`/plugins/harmonicmix`: Suggests harmonically compatible tracks for DJ +- :doc:`plugins/harmonicmix`: Suggests harmonically compatible tracks for DJ mixing based on key and BPM. Bug fixes: