diff --git a/Flow.py b/Flow.py index 9cd7a04..fd10266 100644 --- a/Flow.py +++ b/Flow.py @@ -19,6 +19,8 @@ from src.backends.itunes.ParseItunesXml import ParseItunesXml from src.mappers.RemoteLibraryResolver import resolve_remote_tracks from src.StatGenerator import log_stats +import src.backends.mb.mbidFinder as mb +import src.mappers.DummyLibGenerator as libgen class Flow: spotify_library : Library = None @@ -63,6 +65,10 @@ class Flow: library_parsed = parser.parse(work_dir / "local" / "local_library.json") LibrarySaver(library_parsed).save(work_dir / "local_library.json") + def find_mbids(self): + mb.find_mbids(self.spotify_library) + mb.find_mbids(self.itunes_library) + self.save_libraries() def fetch_and_save_lyrics(self): """ @@ -111,6 +117,11 @@ class Flow: self.itunes_library = LibraryLoader().load(work_dir / "itunes_library.json") self.local_library = LibraryLoader().load(work_dir / "local_library.json") + def gen_fake_libs(self): + libgen.generate_dummy_library(self.spotify_library, work_dir / "spotify" / "dummylib") + libgen.generate_dummy_library(self.itunes_library, work_dir / "itunes" / "dummylib") + + def save_libraries(self): LibrarySaver(self.spotify_library).save(work_dir / "spotify_library.json") LibrarySaver(self.local_library).save(work_dir / "local_library.json") diff --git a/Gui.py b/Gui.py index 3c0ecc8..12d1144 100644 --- a/Gui.py +++ b/Gui.py @@ -16,9 +16,9 @@ def entity_to_row(entity: Entity): "PLay Count": entity.play_count, "Resolved": int(entity.resolved_percentage * 100), "AutoScore": entity.auto_score, - "Added": entity.date_added.strftime("%Y-%m-%d") if entity.date_added else None, - "Played": entity.date_last_play.strftime("%Y-%m-%d") if entity.date_last_play else None, - "Released": entity.date_released.strftime("%Y-%m-%d") if entity.date_released else None, + # "Added": entity.date_added.strftime("%Y-%m-%d") if entity.date_added else None, + # "Played": entity.date_last_play.strftime("%Y-%m-%d") if entity.date_last_play else None, + # "Released": entity.date_released.strftime("%Y-%m-%d") if entity.date_released else None, "LocalId": str(entity.local_id) if entity.local_id else None, "Path": str(entity.local_path) if entity.local_path else None, } diff --git a/main.py b/main.py index 26588d1..05a781d 100644 --- a/main.py +++ b/main.py @@ -26,6 +26,9 @@ def main(): flow.load_libraries() + # flow.gen_fake_libs() + # flow.find_mbids() + flow.map_local_to_spotify() flow.save_libraries() diff --git a/src/backends/itunes/ParseItunesXml.py b/src/backends/itunes/ParseItunesXml.py index 71eb2d7..4b0f417 100755 --- a/src/backends/itunes/ParseItunesXml.py +++ b/src/backends/itunes/ParseItunesXml.py @@ -187,6 +187,8 @@ class ParseItunesXml: for track in self.library.tracks: track.auto_score = track.play_count + self.library.auto_score() + return self.library diff --git a/src/backends/local/LocalLibraryIndexer.py b/src/backends/local/LocalLibraryIndexer.py index 6f132d4..08629f7 100644 --- a/src/backends/local/LocalLibraryIndexer.py +++ b/src/backends/local/LocalLibraryIndexer.py @@ -64,6 +64,17 @@ def get_artist(tags, fallback=None) -> list : return fallback +def get_mb_artist(tags, fallback=None) -> list : + artist_fields = ["TXXX:MusicBrainz Artist Id", "----:com.apple.iTunes:MusicBrainz Artist Id", "MUSICBRAINZ_ARTISTID"] + for field in artist_fields: + if field in tags and tags[field]: + values = tags.get(field) + if not isinstance(values, list): + return values.text + return values + + return fallback + def safe_tag_get(tags, key, index=0): try: value = tags.get(key, None) @@ -147,6 +158,7 @@ def update_local_library(root_dir, output_dir): tags = audio.tags track['artists'] = get_artist(tags) + # track['artists_mbid'] = get_mb_artist(tags) track['name'] = get_title(tags) track['album'] = get_album(tags) diff --git a/src/backends/mb/mbidFinder.py b/src/backends/mb/mbidFinder.py new file mode 100644 index 0000000..6ffcd09 --- /dev/null +++ b/src/backends/mb/mbidFinder.py @@ -0,0 +1,54 @@ +from src.Library import * + +import musicbrainzngs as mb +from tqdm import tqdm + +mb.set_useragent( + "resolve_spotify", + "1.0", + "ilusha.main@gmail.com" +) + +def find_mbid_by_tags(track: Track) -> str | None: + if not track.title or not track.artists: + return None + + artist_names = [a.title for a in track.artists if a.title] + if not artist_names: + return None + + query_parts = [ + f'recording:"{track.title}"', + f'artist:"{artist_names[0]}"' + ] + + query = " AND ".join(query_parts) + + try: + result = mb.search_recordings(query=query, limit=5) + except Exception: + return None + + recordings = result.get("recording-list", []) + if not recordings: + return None + + # Pick best-scored result + best = max( + recordings, + key=lambda r: int(r.get("score", 0)) + ) + + return best.get("id") + + +def find_mbids(library: Library): + tracks = library.tracks + + for track in tqdm(tracks, desc="Resolving MBIDs of " + library.name + " library", unit="track"): + if track.mbid: + continue + + track.mbid = find_mbid_by_tags(track) + if not track.mbid: + print(f"not found for track {track.title} {track.artists[0].title}") diff --git a/src/backends/spotify/ParseSpotify.py b/src/backends/spotify/ParseSpotify.py index cfca39d..fa5d80c 100644 --- a/src/backends/spotify/ParseSpotify.py +++ b/src/backends/spotify/ParseSpotify.py @@ -31,9 +31,9 @@ class Parser: self.library.update_cache() self.library.name = "Spotify" - self.library.auto_score() self.score_tracks() + self.library.auto_score() return self.library diff --git a/src/mappers/DummyLibGenerator.py b/src/mappers/DummyLibGenerator.py new file mode 100644 index 0000000..7aaa544 --- /dev/null +++ b/src/mappers/DummyLibGenerator.py @@ -0,0 +1,46 @@ +from mutagen.easyid3 import EasyID3 +from pydub import AudioSegment +from mutagen.easyid3 import EasyID3 + +from src.Library import * + +def safe_name(s: str) -> str: + return ( + s.replace("/", "_") + .replace("\\", "_") + .replace(":", "_") + .replace("*", "_") + .replace("?", "_") + .replace('"', "_") + .replace("<", "_") + .replace(">", "_") + .replace("|", "_") + .strip() + ) + +def generate_dummy_library(library: Library, root_path: Path): + root_path.mkdir(parents=True, exist_ok=True) + + for artist in library.artists: + artist_path = root_path / safe_name(artist.title or f"artist_{artist.id}") + artist_path.mkdir(exist_ok=True) + + for album in artist.albums: + album_path = artist_path / safe_name(album.title or f"album_{album.id}") + album_path.mkdir(exist_ok=True) + + for track in album.tracks: + track_title = track.title or f"track_{track.id}" + track_file = album_path / f"{safe_name(track_title)}.mp3" + + # duration_ms = max(track.duration_ms or 100, 100) + duration_ms = 10 + silence = AudioSegment.silent(duration=duration_ms) + silence.export(track_file, format="mp3") + + audio = EasyID3(track_file) + audio["title"] = track.title or "" + audio["artist"] = ", ".join([a.title or "" for a in track.artists]) + audio["album"] = album.title or "" + audio["tracknumber"] = str(album.tracks.index(track) + 1) + audio.save() diff --git a/src/mappers/FuzzyMapper.py b/src/mappers/FuzzyMapper.py index c6413d0..1dbbf75 100644 --- a/src/mappers/FuzzyMapper.py +++ b/src/mappers/FuzzyMapper.py @@ -4,6 +4,8 @@ from tqdm import tqdm from src.Library import * from src.helpers import * +import re +import string import unicodedata @@ -11,34 +13,49 @@ user_def_artist_map = { "donda" : "kanye west", } -def normalize(s): - return unicodedata.normalize("NFKC", s).replace("‐", "-") +user_def_album_map = { + "music" : "music sorry 4 da wait", +} def get_score(first: str, second: str) -> float: - first_norm = normalize(first).lower() - second_norm = normalize(second).lower() - return float(first_norm == second_norm) * 100 - return fuzz.token_set_ratio(first, second) + return float(first == second) * 100 + +def clean_title(title: str) -> str: + title = unicodedata.normalize("NFKC", title).replace("‐", "-") + title = unicodedata.normalize("NFKD", title) + title = re.sub(r"\([^)]*\)", "", title) + title = "".join( + c for c in title + if not unicodedata.category(c).startswith("P") + ) + title = title.translate(str.maketrans("", "", string.punctuation)) + title = title.lower() + return " ".join(title.split()) + +def find_mapping(first, second, mapping): + for key, value in mapping.items(): + if (first == key and second == value) or (first == value and second == key): + return True + return False def get_album_score(first: str, second: str) -> float: + first = clean_title(first) + second = clean_title(second) + + if find_mapping(first, second, user_def_album_map): + return 100 + return get_score(first, second) def get_track_score(first: str, second: str) -> float: - return get_score(first, second) + return get_score(clean_title(first), clean_title(second)) def get_artist_score(first: str, second: str) -> float: - if not first or not second: - return 0.0 + first = clean_title(first) + second = clean_title(second) - first_lower = first.lower() - second_lower = second.lower() - - for key, value in user_def_artist_map.items(): - key_lower = key.lower() - value_lower = value.lower() - if (first_lower == key_lower and second_lower == value_lower) or \ - (first_lower == value_lower and second_lower == key_lower): - return 100.0 + if find_mapping(first, second, user_def_artist_map): + return 100 return get_score(first, second) @@ -97,6 +114,7 @@ def generate_map(local_lib : Library, remote_lib : Library, output_dir : Path): if remote_track.id not in artist_map or score > track_map[remote_track.id][1]: track_map[remote_track.id] = (local_track.id, score) remote_track.local_id = local_track.id + remote_track.local_path = local_track.local_path process_bar.update(1)