This repository has been archived on 2024-04-27. You can view files and clone it, but cannot push or open issues or pull requests.
autoytdlp/.id3tag_helper.py

403 lines
9.7 KiB
Python
Executable file

#!/usr/bin/env python3
import os
import sys
import shutil
from pathlib import Path
import logging
import mimetypes
import subprocess
from typing import TypedDict
from typing import Optional
import re
import requests
from bs4 import BeautifulSoup
from mutagen.id3 import ID3
from mutagen.id3 import TPE1, TIT2, TALB
from mutagen.id3 import TYER, TRCK
from mutagen.id3 import USLT, APIC
BASEURL = 'https://www.azlyrics.com'
USERAGENT = (
'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) '
'Gecko/20100101 Firefox/110.0'
)
LYRICS_ROW = '.main-page>.row>.col-xs-12'
session = requests.Session()
session.headers['User-Agent'] = USERAGENT
class ParseResult(TypedDict):
title: str
artist: str
album: str
year: int
track_no: int
tracks: int
lyrics: str
cover: Optional[bytes]
cover_mime: Optional[str]
class ParseError(Exception):
def __init__(self, parsing_obj: str) -> None:
super().__init__(
f'Unable to parse {parsing_obj}'
)
def main() -> None:
copy = int(sys.argv[1]) == 1
file = sys.argv[2]
title = conv_title(file)
print(
'Enter new title to correct it, '
'or press Enter to continue',
'"!--" without quotes means that '
'you want to enter info and lyrics manually',
sep='\n',
)
print('Title:', title)
correct = input().strip()
parsed: Optional[ParseResult] = None
if correct == '!--':
parsed = manual_info_input()
else:
if correct != '':
title = correct.lower()
url = search_azurl(title)
print(url)
parsed = parse_azlyrics(url)
#print(parsed)
tagmp3(file, parsed, copy)
def input_num(msg: str, def_: int = 0) -> int:
try:
return int(input(msg))
except ValueError:
return def_
def conv_title(file: str) -> str:
# Remove file path
title = file \
.replace('./convert/', '') \
.replace('./files/', '')
# Remove a YT ID and an extension
title = re.sub(
r'-{3}[\w_-]*\.[\w_-]*',
'', title,
)
# Remove "(Official Audio)"
title = re.sub(
r'\(.*\)',
'', title,
)
# underscore -> space
title = title \
.replace('_', ' ') \
.strip() \
.lower()
return title
def search_azurl(title: str) -> str:
print('Searching...')
page = session.get(
'https://searx.dc09.ru/search',
params={
'q': f'{title} site:azlyrics.com',
'category_general': 1,
'language': 'ru-RU',
'time_range': '',
'safesearch': 0,
'theme': 'simple',
},
)
soup = BeautifulSoup(page.text, 'html.parser')
link = soup.select_one(
'div#urls>article>h3>a[href*="azlyrics.com/lyrics/"]'
)
if link is None:
raise ParseError('song URL')
return str(link.get('href'))
def parse_azlyrics(link: str) -> ParseResult:
result = ParseResult(
title='', artist='',
album='', year=0,
track_no=0, tracks=0,
lyrics='',
cover=None,
cover_mime=None,
)
print('Please wait...')
page = session.get(link)
soup = BeautifulSoup(page.text, 'html.parser')
lyrics = soup.select_one(
f'{LYRICS_ROW}>div'
':not(.div-share)'
':not(.lyricsh)'
':not(.ringtone)'
)
if lyrics is None:
raise ParseError('song lyrics')
result['lyrics'] = lyrics.get_text().strip()
artist_elem = soup.select_one(f'{LYRICS_ROW}>.lyricsh>h2')
if artist_elem is None:
print('Unable to parse artist name')
result['artist'] = input('Enter the artist name: ')
else:
result['artist'] = artist_elem.get_text() \
.removesuffix(' Lyrics') \
.strip()
title_elem = soup.select_one(f'{LYRICS_ROW}>b')
if title_elem is None:
print('Unable to parse song title')
result['title'] = input('Enter the title: ')
else:
result['title'] = title_elem.get_text().strip('" ')
album_blocks = soup.select('.songinalbum_title')
album = None
if len(album_blocks) > 1:
album = album_blocks[-2]
elif len(album_blocks) > 0:
album = album_blocks[0]
if album is None:
album_re = None
else:
album_re = re.search(
r'album:\s*"(.+?)"\s*\((\d+)\)',
album.get_text()
)
if album_re is None:
print('Unable to parse album name')
result['album'] = input('Enter the album name: ')
result['year'] = input_num('Enter the release year: ')
result['track_no'] = input_num('This is the track #')
result['tracks'] = input_num('Number of tracks in the album: ')
cover = input('Insert an album cover? [Y/n] ')
if cover.lower() not in ('n','н'):
try:
print(
'Download the cover and enter its path:',
'(relative path is not recommended)',
sep='\n',
)
cover_file = Path(input().strip())
with cover_file.open('rb') as f:
result['cover'] = f.read()
result['cover_mime'] = (
mimetypes.guess_type(cover_file)[0]
or 'image/jpeg'
)
except Exception as err:
logging.exception(err)
else:
result['album'] = album_re[1]
result['year'] = int(album_re[2])
assert album is not None
cover = album.select_one('img.album-image')
if cover is not None:
cover_url = str(cover.get('src'))
if cover_url.startswith('/'):
cover_url = BASEURL + cover_url
req = session.get(cover_url)
result['cover'] = req.content
result['cover_mime'] = req.headers.get(
'Content-Type', 'image/jpeg'
)
tracklist_elem = soup.select_one('.songlist-panel')
if tracklist_elem is not None:
tracklist = tracklist_elem.select(
'.listalbum-item'
)
result['tracks'] = len(tracklist)
current_url = re.search(
r'/(lyrics/.+?\.html)',
link,
)
result['track_no'] = 0
if current_url is not None:
for i, track in enumerate(tracklist):
track_url = track.select_one('a')
if track_url is None:
continue
track_href = str(track_url.get('href'))
if current_url[0] in track_href:
result['track_no'] = (i + 1)
break
return result
def manual_info_input() -> ParseResult:
result = ParseResult(
title=input('Song title: '),
artist=input('Artist name: '),
album=input('Album name: '),
year=input_num('Release year: '),
track_no=input_num('Track #'),
tracks=input_num('Tracks in album: '),
lyrics='', cover=None, cover_mime=None,
)
editor = os.getenv('EDITOR', 'nano')
print('Now, paste the lyrics into a text editor')
print(f'Default editor: {editor}')
print('Enter another or press Enter to continue')
other_editor = input().strip()
if other_editor != '':
editor = other_editor
try:
lyrics_file = Path('.') / 'lyrics.txt'
with lyrics_file.open('wt') as f:
f.write('\n')
subprocess.call([
editor,
lyrics_file,
])
print('Reading file...')
with open('lyrics.txt', 'rt', encoding='utf-8') as f:
result['lyrics'] = f.read().strip()
print('Done')
except OSError as err:
logging.exception(err)
cover = input('Insert an album cover? [Y/n] ')
if cover.lower() not in ('n','н'):
try:
print(
'Download the cover and enter its path:',
'(relative path is not recommended)',
sep='\n',
)
cover_file = Path(input().strip())
with cover_file.open('rb') as f:
result['cover'] = f.read()
result['cover_mime'] = (
mimetypes.guess_type(cover_file)[0]
or 'image/jpeg'
)
except Exception as err:
logging.exception(err)
return result
def tagmp3(
file: str,
parsed: ParseResult,
copy: bool) -> None:
oldpath = Path(file)
newpath = oldpath
if copy:
newdir = (
Path('./tagged') /
parsed['artist'] /
parsed['album']
)
os.makedirs(newdir, exist_ok=True)
newpath = newdir / (
f"{parsed['track_no']}. " +
f"{parsed['title']}.mp3"
)
shutil.copy(oldpath, newpath)
if parsed['cover'] is not None:
ext = mimetypes.guess_extension(
parsed['cover_mime'] or ''
) or '.jpg'
cover = newdir / f'cover{ext}'
with cover.open('wb') as f:
f.write(parsed['cover'])
id3 = ID3(str(newpath))
id3['TPE1'] = TPE1(text=parsed['artist'])
id3['TIT2'] = TIT2(text=parsed['title'])
id3['TALB'] = TALB(text=parsed['album'])
id3['TYER'] = TYER(text=f"{parsed['year']}")
id3['TRCK'] = TRCK(
text=(
f"{parsed['track_no']}/"
f"{parsed['tracks']}"
)
)
id3['USLT'] = USLT(text=parsed['lyrics'])
if parsed['cover'] is not None:
id3['APIC'] = APIC(
data=parsed['cover'],
mime=parsed['cover_mime'],
)
id3.save()
if __name__ == '__main__':
main()