Initial commit: Add playlist downloader project with UI and core functionality
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
"""Utility functions for text processing and file handling."""
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def remove_wrong_ed_suffix(text: str) -> str:
|
||||
"""Remove erroneous 'ed' suffixes added to words that shouldn't have them.
|
||||
|
||||
Examples:
|
||||
"Enter Sandmaned" → "Enter Sandman"
|
||||
"Bohemian Rhapsodyed" → "Bohemian Rhapsody"
|
||||
"Don't Stop Me Nowed" → "Don't Stop Me Now"
|
||||
"""
|
||||
if not text or len(text) < 3:
|
||||
return text
|
||||
|
||||
# Words that commonly get corrupted with 'ed'
|
||||
# Pattern: word ends with vowel+consonant, gets 'ed' appended
|
||||
corruptions = {
|
||||
'sandmaned': 'sandman',
|
||||
'nowed': 'now',
|
||||
'suned': 'sun',
|
||||
'dusted': 'dust',
|
||||
'loveed': 'love',
|
||||
'rhapsodyed': 'rhapsody',
|
||||
'queened': 'queen',
|
||||
'championsed': 'champions',
|
||||
'youed': 'you',
|
||||
'symphonyed': 'symphony',
|
||||
'goed': 'go',
|
||||
'zombieed': 'zombie',
|
||||
'beed': 'be',
|
||||
'lifeed': 'life',
|
||||
'youthed': 'youth',
|
||||
}
|
||||
|
||||
result = text
|
||||
for corrupted, correct in corruptions.items():
|
||||
# Case-insensitive replacement while preserving case of the correct word
|
||||
pattern = re.compile(re.escape(corrupted), re.IGNORECASE)
|
||||
|
||||
def replace_preserving_case(match):
|
||||
# If original text was uppercase, return uppercase
|
||||
matched = match.group(0)
|
||||
if matched.isupper():
|
||||
return correct.upper()
|
||||
elif matched[0].isupper():
|
||||
return correct.capitalize()
|
||||
else:
|
||||
return correct
|
||||
|
||||
result = pattern.sub(replace_preserving_case, result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def sanitize_filename(text: str) -> str:
|
||||
"""Remove/replace invalid filename characters."""
|
||||
if not text:
|
||||
return ""
|
||||
text = str(text)
|
||||
# Replace forward/backward slashes with unicode lookalike (for cases like "AC/DC")
|
||||
# ⁄ (U+2044 FRACTION SLASH) looks like "/" but is a valid filename character
|
||||
text = text.replace("/", "⁄").replace("\\", "⁄")
|
||||
# Remove other invalid filename characters
|
||||
text = re.sub(r'[*?"<>|]', '', text)
|
||||
# Remove leading/trailing dots and spaces
|
||||
text = text.strip(". ")
|
||||
# Collapse multiple spaces
|
||||
text = re.sub(r'\s+', ' ', text)
|
||||
return text
|
||||
|
||||
|
||||
def normalize_text(text: str) -> str:
|
||||
"""Normalize text for comparison (lowercase, remove special chars)."""
|
||||
if not text:
|
||||
return ""
|
||||
text = unicodedata.normalize('NFKD', text)
|
||||
text = text.lower()
|
||||
text = text.replace("-", "").replace(" ", "").replace("'", "")
|
||||
return text
|
||||
|
||||
|
||||
def capitalize_artist(artist: str) -> str:
|
||||
"""Capitalize artist name properly."""
|
||||
if not artist or artist.lower() in ["unknown", "unknown artist"]:
|
||||
return artist
|
||||
|
||||
words = artist.split()
|
||||
result = []
|
||||
for word in words:
|
||||
if "-" in word:
|
||||
parts = [p.capitalize() for p in word.split("-")]
|
||||
result.append("-".join(parts))
|
||||
else:
|
||||
result.append(word.capitalize())
|
||||
|
||||
return " ".join(result)
|
||||
|
||||
|
||||
def extract_year(date_string: str) -> Optional[str]:
|
||||
"""Extract year from a date string."""
|
||||
if not date_string:
|
||||
return None
|
||||
|
||||
match = re.search(r"\b(\d{4})\b", str(date_string))
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def clean_title_for_filename(title: str) -> str:
|
||||
"""Remove common suffixes from track titles for cleaner filenames.
|
||||
|
||||
Examples:
|
||||
"There Is a Light That Never Goes Out - 2011 Remaster" → "There Is a Light That Never Goes Out"
|
||||
"Song - Remastered 1" → "Song"
|
||||
"Song - Remastered I" → "Song"
|
||||
"Ain't No Rest For The Wicked - Original Version" → "Ain't No Rest For The Wicked"
|
||||
"Song - Lyrics" → "Song"
|
||||
"Track (Live)" → "Track"
|
||||
"Don't Stop Me Nowed 2011" → "Don't Stop Me Nowed"
|
||||
"""
|
||||
if not title:
|
||||
return ""
|
||||
|
||||
# List of patterns to remove (case-insensitive)
|
||||
patterns_to_remove = [
|
||||
# Remastered with year in both orders
|
||||
r'\s*[-–]\s*(re-mastered|remastered|re-master|remaster)\s+\d{4}', # "- Remastered 2009"
|
||||
|
||||
# Year-based remasters (2011 Remaster, etc.)
|
||||
r'\s*[-–]\s*\d{4}\s+(re-mastered|remastered|re-master|remaster)',
|
||||
r'\s*\(\d{4}\s+(re-mastered|remastered|re-master|remaster)\)',
|
||||
|
||||
# Generic remaster/remastered without year
|
||||
r'\s*[-–]\s*(re-mastered|remastered|re-master|remaster)(\s+\d+)?', # Remastered, Remastered 1, etc.
|
||||
r'\s*[-–]\s*(re-mastered|remastered|re-master|remaster)\s+([ivxlcdm]+)?', # Remastered I, II, etc. (roman numerals)
|
||||
r'\s*[-–]\s*(re-master|remaster)(ed)?\s+(edition|version)',
|
||||
|
||||
# Original/Version suffixes
|
||||
r'\s*[-–]\s*(original\s+)?(version|edit|cut)',
|
||||
r'\s*[-–]\s*original\s+(mix|remix)',
|
||||
|
||||
# Generic versions
|
||||
r'\s*[-–]\s*(lyrics|lyric)',
|
||||
r'\s*[-–]\s*(official\s+)?(audio|video)',
|
||||
r'\s*[-–]\s*(live|acoustic|remix|cover)',
|
||||
r'\s*[-–]\s*(feat\.|featuring).*',
|
||||
|
||||
# Parentheses versions
|
||||
r'\s*\((lyrics|lyric|live|acoustic|remix|cover|original|version|remaster|remastered)\)',
|
||||
r'\s*\(.*remaster.*\)',
|
||||
|
||||
# Simple year at the end (2011, 2020, etc.) - with or without dash/parentheses
|
||||
r'\s*[-–]\s*\d{4}\s*$', # " - 2011" at end
|
||||
r'\s*\(\d{4}\)\s*$', # " (2011)" at end
|
||||
r'\s+\d{4}\s*$', # " 2011" at end
|
||||
]
|
||||
|
||||
cleaned = title
|
||||
for pattern in patterns_to_remove:
|
||||
cleaned = re.sub(pattern, '', cleaned, flags=re.IGNORECASE)
|
||||
|
||||
# Clean up extra spaces and trailing dashes
|
||||
cleaned = re.sub(r'\s+', ' ', cleaned).strip()
|
||||
cleaned = re.sub(r'\s*[-–]\s*$', '', cleaned).strip()
|
||||
|
||||
return cleaned
|
||||
|
||||
|
||||
def clean_track_name(text: str) -> str:
|
||||
"""Remove unwanted parenthetical content from track names (dates, Remaster, Lyrics, Official, etc)."""
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
# Remove common patterns in parentheses
|
||||
patterns = [
|
||||
r'\s*\(\s*\d{4}\s+[Rr]emaster(?:ed)?\s*\)', # (2005 Remaster)
|
||||
r'\s*\(\s*\d{4}\s+[Rr]emaster(?:ed)?\s+[Vv]ersion\s*\)', # (2005 Remastered Version)
|
||||
r'\s*\(\s*[Ll]yrics\s*\)', # (Lyrics)
|
||||
r'\s*\(\s*[Oo]fficial\s+[Vv]ideo\s*\)', # (Official Video)
|
||||
r'\s*\(\s*[Oo]fficial\s+[Aa]udio\s*\)', # (Official Audio)
|
||||
r'\s*\(\s*[Oo]fficial\s*\)', # (Official)
|
||||
r'\s*\(\s*[Ll]ive\s+(?:at\s+)?.*?\)', # (Live) or (Live at Madison Square Garden)
|
||||
r'\s*\(\s*[Ll]ive\s*\)', # (Live)
|
||||
r'\s*\(\s*[Aa]lternate\s+[Vv]ersion\s*\)', # (Alternate Version)
|
||||
r'\s*\(\s*[Aa]lternative\s+[Vv]ersion\s*\)', # (Alternative Version)
|
||||
r'\s*\(\s*[Ff]eature(?:ing)?\s+.*?\)', # (Feature Remix, Feat. Artist, etc)
|
||||
r'\s*\(\s*[Rr]emix\s*\)', # (Remix)
|
||||
r'\s*\(\s*[Dd]eep\s+[Hh]ouse\s*\)', # (Deep House)
|
||||
r'\s*\(\s*[Cc]lean\s*\)', # (Clean)
|
||||
r'\s*\(\s*[Ee]xplicit\s*\)', # (Explicit)
|
||||
r'\s*-\s*[Ll]yrics\s*$', # "- Lyrics" at the end
|
||||
]
|
||||
|
||||
result = text
|
||||
for pattern in patterns:
|
||||
result = re.sub(pattern, '', result, flags=re.IGNORECASE)
|
||||
|
||||
# Clean up multiple spaces
|
||||
result = re.sub(r'\s+', ' ', result).strip()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Database of official artist names with their common variations
|
||||
ARTIST_NAME_CORRECTIONS = {
|
||||
# Format: normalized_key -> official_name
|
||||
"blink182": "Blink-182",
|
||||
"blink 182": "Blink-182",
|
||||
"blink-182": "Blink-182",
|
||||
"linkinpark": "Linkin Park",
|
||||
"linkin park": "Linkin Park",
|
||||
"linkin-park": "Linkin Park",
|
||||
"thebeatles": "The Beatles",
|
||||
"the beatles": "The Beatles",
|
||||
"beatles": "The Beatles",
|
||||
"acdc": "AC/DC",
|
||||
"ac/dc": "AC/DC",
|
||||
"ac dc": "AC/DC",
|
||||
"pinkfloyd": "Pink Floyd",
|
||||
"pink floyd": "Pink Floyd",
|
||||
"pink-floyd": "Pink Floyd",
|
||||
"ledzeppelin": "Led Zeppelin",
|
||||
"led zeppelin": "Led Zeppelin",
|
||||
"led-zeppelin": "Led Zeppelin",
|
||||
"thewho": "The Who",
|
||||
"the who": "The Who",
|
||||
"metallica": "Metallica",
|
||||
"gunsnroses": "Guns N' Roses",
|
||||
"guns n roses": "Guns N' Roses",
|
||||
"guns n' roses": "Guns N' Roses",
|
||||
"guns n roses": "Guns N' Roses",
|
||||
"guns-n-roses": "Guns N' Roses",
|
||||
"redhotchilipeppers": "Red Hot Chili Peppers",
|
||||
"red hot chili peppers": "Red Hot Chili Peppers",
|
||||
"rhcp": "Red Hot Chili Peppers",
|
||||
"greenday": "Green Day",
|
||||
"green day": "Green Day",
|
||||
"green-day": "Green Day",
|
||||
"thepinkfloyd": "Pink Floyd",
|
||||
"rollingstone": "The Rolling Stones",
|
||||
"rolling stone": "The Rolling Stones",
|
||||
"rolling stones": "The Rolling Stones",
|
||||
"therollingstone": "The Rolling Stones",
|
||||
"therollingstones": "The Rolling Stones",
|
||||
"foofighter": "Foo Fighters",
|
||||
"foo fighter": "Foo Fighters",
|
||||
"foo fighters": "Foo Fighters",
|
||||
"thirty second to mars": "Thirty Seconds to Mars",
|
||||
"thirty seconds to mars": "Thirty Seconds to Mars",
|
||||
"30 seconds to mars": "Thirty Seconds to Mars",
|
||||
"30secondstomars": "Thirty Seconds to Mars",
|
||||
"thirtysecondstomars": "Thirty Seconds to Mars",
|
||||
"jared leto": "Thirty Seconds to Mars",
|
||||
}
|
||||
|
||||
|
||||
def normalize_artist_name(artist: str) -> str:
|
||||
"""
|
||||
Normalize artist name to official name using correction database.
|
||||
|
||||
Examples:
|
||||
"blink182" -> "Blink-182"
|
||||
"blink‐182" -> "Blink-182" (Unicode hyphen from MusicBrainz)
|
||||
"linkin park" -> "Linkin Park"
|
||||
"guns n roses" -> "Guns N' Roses"
|
||||
"""
|
||||
if not artist:
|
||||
return artist
|
||||
|
||||
# Normalize all kinds of hyphens/dashes to standard ASCII hyphen
|
||||
artist = artist.replace('\u2010', '-') # Unicode hyphen ‐
|
||||
artist = artist.replace('\u2011', '-') # Non-breaking hyphen ‑
|
||||
artist = artist.replace('\u2012', '-') # Figure dash ‒
|
||||
artist = artist.replace('\u2013', '-') # En dash –
|
||||
artist = artist.replace('\u2014', '-') # Em dash —
|
||||
|
||||
# Create a normalized key for lookup (remove spaces, hyphens, apostrophes)
|
||||
normalized_key = artist.lower().replace(" ", "").replace("-", "").replace("'", "")
|
||||
|
||||
# Check if we have a correction for this artist
|
||||
if normalized_key in ARTIST_NAME_CORRECTIONS:
|
||||
return ARTIST_NAME_CORRECTIONS[normalized_key]
|
||||
|
||||
# If no correction found, return the original artist name
|
||||
return artist
|
||||
Reference in New Issue
Block a user