Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
Шурупов Илья Викторович
4fd6857287 untested 2026-06-14 23:46:09 +03:00
9 changed files with 168 additions and 22 deletions

11
Flow.py
View file

@ -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
View file

@ -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,
}

View file

@ -26,6 +26,9 @@ def main():
flow.load_libraries()
# flow.gen_fake_libs()
# flow.find_mbids()
flow.map_local_to_spotify()
flow.save_libraries()

View file

@ -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

View file

@ -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)

View 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}")

View file

@ -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

View 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()

View file

@ -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)