Files
playlist_dl/src/utils/utils.py
T

289 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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"
"blink182" -> "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