untested
This commit is contained in:
parent
9cd2c9189b
commit
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.mappers.RemoteLibraryResolver import resolve_remote_tracks
|
||||||
from src.StatGenerator import log_stats
|
from src.StatGenerator import log_stats
|
||||||
|
|
||||||
|
import src.backends.mb.mbidFinder as mb
|
||||||
|
import src.mappers.DummyLibGenerator as libgen
|
||||||
|
|
||||||
class Flow:
|
class Flow:
|
||||||
spotify_library : Library = None
|
spotify_library : Library = None
|
||||||
|
|
@ -63,6 +65,10 @@ class Flow:
|
||||||
library_parsed = parser.parse(work_dir / "local" / "local_library.json")
|
library_parsed = parser.parse(work_dir / "local" / "local_library.json")
|
||||||
LibrarySaver(library_parsed).save(work_dir / "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):
|
def fetch_and_save_lyrics(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -111,6 +117,11 @@ class Flow:
|
||||||
self.itunes_library = LibraryLoader().load(work_dir / "itunes_library.json")
|
self.itunes_library = LibraryLoader().load(work_dir / "itunes_library.json")
|
||||||
self.local_library = LibraryLoader().load(work_dir / "local_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):
|
def save_libraries(self):
|
||||||
LibrarySaver(self.spotify_library).save(work_dir / "spotify_library.json")
|
LibrarySaver(self.spotify_library).save(work_dir / "spotify_library.json")
|
||||||
LibrarySaver(self.local_library).save(work_dir / "local_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,
|
"PLay Count": entity.play_count,
|
||||||
"Resolved": int(entity.resolved_percentage * 100),
|
"Resolved": int(entity.resolved_percentage * 100),
|
||||||
"AutoScore": entity.auto_score,
|
"AutoScore": entity.auto_score,
|
||||||
"Added": entity.date_added.strftime("%Y-%m-%d") if entity.date_added 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,
|
# "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,
|
# "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,
|
"LocalId": str(entity.local_id) if entity.local_id else None,
|
||||||
"Path": str(entity.local_path) if entity.local_path 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.load_libraries()
|
||||||
|
|
||||||
|
# flow.gen_fake_libs()
|
||||||
|
# flow.find_mbids()
|
||||||
|
|
||||||
flow.map_local_to_spotify()
|
flow.map_local_to_spotify()
|
||||||
flow.save_libraries()
|
flow.save_libraries()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,8 @@ class ParseItunesXml:
|
||||||
for track in self.library.tracks:
|
for track in self.library.tracks:
|
||||||
track.auto_score = track.play_count
|
track.auto_score = track.play_count
|
||||||
|
|
||||||
|
self.library.auto_score()
|
||||||
|
|
||||||
return self.library
|
return self.library
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,17 @@ def get_artist(tags, fallback=None) -> list :
|
||||||
|
|
||||||
return fallback
|
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):
|
def safe_tag_get(tags, key, index=0):
|
||||||
try:
|
try:
|
||||||
value = tags.get(key, None)
|
value = tags.get(key, None)
|
||||||
|
|
@ -147,6 +158,7 @@ def update_local_library(root_dir, output_dir):
|
||||||
tags = audio.tags
|
tags = audio.tags
|
||||||
|
|
||||||
track['artists'] = get_artist(tags)
|
track['artists'] = get_artist(tags)
|
||||||
|
# track['artists_mbid'] = get_mb_artist(tags)
|
||||||
track['name'] = get_title(tags)
|
track['name'] = get_title(tags)
|
||||||
track['album'] = get_album(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.update_cache()
|
||||||
self.library.name = "Spotify"
|
self.library.name = "Spotify"
|
||||||
self.library.auto_score()
|
|
||||||
|
|
||||||
self.score_tracks()
|
self.score_tracks()
|
||||||
|
self.library.auto_score()
|
||||||
|
|
||||||
return self.library
|
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.Library import *
|
||||||
from src.helpers import *
|
from src.helpers import *
|
||||||
|
import re
|
||||||
|
import string
|
||||||
|
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
||||||
|
|
@ -11,34 +13,49 @@ user_def_artist_map = {
|
||||||
"donda" : "kanye west",
|
"donda" : "kanye west",
|
||||||
}
|
}
|
||||||
|
|
||||||
def normalize(s):
|
user_def_album_map = {
|
||||||
return unicodedata.normalize("NFKC", s).replace("‐", "-")
|
"music" : "music sorry 4 da wait",
|
||||||
|
}
|
||||||
|
|
||||||
def get_score(first: str, second: str) -> float:
|
def get_score(first: str, second: str) -> float:
|
||||||
first_norm = normalize(first).lower()
|
return float(first == second) * 100
|
||||||
second_norm = normalize(second).lower()
|
|
||||||
return float(first_norm == second_norm) * 100
|
def clean_title(title: str) -> str:
|
||||||
return fuzz.token_set_ratio(first, second)
|
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:
|
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)
|
return get_score(first, second)
|
||||||
|
|
||||||
def get_track_score(first: str, second: str) -> float:
|
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:
|
def get_artist_score(first: str, second: str) -> float:
|
||||||
if not first or not second:
|
first = clean_title(first)
|
||||||
return 0.0
|
second = clean_title(second)
|
||||||
|
|
||||||
first_lower = first.lower()
|
if find_mapping(first, second, user_def_artist_map):
|
||||||
second_lower = second.lower()
|
return 100
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
return get_score(first, second)
|
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]:
|
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)
|
track_map[remote_track.id] = (local_track.id, score)
|
||||||
remote_track.local_id = local_track.id
|
remote_track.local_id = local_track.id
|
||||||
|
remote_track.local_path = local_track.local_path
|
||||||
|
|
||||||
process_bar.update(1)
|
process_bar.update(1)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue