Compare commits
1 commit
main
...
mbid-finde
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fd6857287 |
9 changed files with 168 additions and 22 deletions
11
Flow.py
11
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")
|
||||
|
|
|
|||
6
Gui.py
6
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,
|
||||
}
|
||||
|
|
|
|||
3
main.py
3
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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
54
src/backends/mb/mbidFinder.py
Normal file
54
src/backends/mb/mbidFinder.py
Normal file
|
|
@ -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}")
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
46
src/mappers/DummyLibGenerator.py
Normal file
46
src/mappers/DummyLibGenerator.py
Normal file
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue