Compare commits

..

No commits in common. "b6761c282421717e3e50961ee3a6b6eff37aa145" and "e8b1330809b2b9354b9cbf37715b085377f8c1b7" have entirely different histories.

6 changed files with 132 additions and 387 deletions

7
.gitignore vendored
View file

@ -1,10 +1,5 @@
files/ files/
convert/ convert/
tagged/ tagged/
lyrics.txt
input
__pycache__/
.mypy_cache/
.vscode/ .vscode/
.idea/ lyrics.txt

View file

@ -11,14 +11,14 @@ import mimetypes
import subprocess import subprocess
from typing import TypedDict from typing import TypedDict
from typing import Optional, Any from typing import Optional
import re import re
import requests import requests
from bs4 import BeautifulSoup # type: ignore from bs4 import BeautifulSoup
from mutagen.id3 import ID3 # type: ignore from mutagen.id3 import ID3
from mutagen.id3 import TPE1, TIT2, TALB from mutagen.id3 import TPE1, TIT2, TALB
from mutagen.id3 import TYER, TRCK from mutagen.id3 import TYER, TRCK
from mutagen.id3 import USLT, APIC from mutagen.id3 import USLT, APIC
@ -53,30 +53,15 @@ class ParseResult(TypedDict):
class ParseError(Exception): class ParseError(Exception):
EDIT = 'edit'
def __init__(self, parsing_obj: str) -> None: def __init__(self, parsing_obj: str) -> None:
super().__init__( super().__init__(
f'Unable to parse {parsing_obj}' f'Unable to parse {parsing_obj}'
) )
self.parsing_obj = parsing_obj
parsed = ParseResult(
title='', artist='',
album='', year=0,
track_no=0, tracks=0,
lyrics='',
cover=None,
cover_mime=None,
)
def main() -> None: def main() -> None:
global parsed
copy = int(sys.argv[1]) == 1 copy = int(sys.argv[1]) == 1
file = sys.argv[2] file = sys.argv[2]
@ -91,67 +76,25 @@ def main() -> None:
print('Title:', title) print('Title:', title)
correct = input().strip() correct = input().strip()
parsed: Optional[ParseResult] = None
if correct == '!--': if correct == '!--':
manual_info_input() parsed = manual_info_input()
else: else:
if correct != '': if correct != '':
title = correct.lower() title = correct.lower()
url = search_azurl(title)
print(url)
parsed = parse_azlyrics(url)
try: #print(parsed)
url = search_azurl(title) tagmp3(file, parsed, copy)
print(url)
parse_azlyrics(url)
except Exception as err:
print(err)
# pylint: disable=no-member
if isinstance(err, ParseError) \
and err.parsing_obj == ParseError.EDIT:
pass
# pylint: enable=no-member
else:
print(
'In most cases, this error means that '
'the script have received some incorrect data, '
'so you should enter song info manually.'
)
manual_info_input(False)
tagmp3(file, copy)
# pylint: disable=redefined-builtin
def input(msg: str = '', def_: Any = '') -> str:
subprocess.call(
(
f'read -e -r -i "{def_}" -p "{msg}" input; '
'echo -n "$input" >./input'
),
shell=True,
executable='bash',
)
try:
with open('./input', 'rt', encoding='utf-8') as f:
return f.read() \
.removesuffix('\n') \
.removesuffix('\r')
except Exception:
return def_
# pylint: enable=redefined-builtin
def input_num(msg: str, def_: int = 0) -> int: def input_num(msg: str, def_: int = 0) -> int:
try: try:
return int(input(msg, def_)) return int(input(msg))
except ValueError: except ValueError:
return def_ return def_
@ -192,37 +135,46 @@ def conv_title(file: str) -> str:
def search_azurl(title: str) -> str: def search_azurl(title: str) -> str:
print('Searching...') print('Searching...')
page = session.get( page = session.get(
'https://searx.dc09.ru/search', 'https://searx.dc09.ru/search',
params={ # type: ignore params={
'q': f'{title} site:azlyrics.com', 'q': f'{title} site:azlyrics.com',
'category_general': 1,
'language': 'ru-RU', 'language': 'ru-RU',
'time_range': '',
'safesearch': 0, 'safesearch': 0,
'theme': 'simple',
}, },
) )
soup = BeautifulSoup(page.text, 'html.parser') soup = BeautifulSoup(page.text, 'html.parser')
link = soup.select_one( link = soup.select_one(
'div#urls>article>h3>a' 'div#urls>article>h3>a[href*="azlyrics.com/lyrics/"]'
'[href*="azlyrics.com/lyrics/"]'
) )
if link is None: if link is None:
raise ParseError('song URL') raise ParseError('song URL')
return str(link.get('href')) return str(link.get('href'))
def parse_azlyrics(link: str) -> None: def parse_azlyrics(link: str) -> ParseResult:
global parsed result = ParseResult(
title='', artist='',
album='', year=0,
track_no=0, tracks=0,
lyrics='',
cover=None,
cover_mime=None,
)
print('Please wait...') print('Please wait...')
page = session.get(link) page = session.get(link)
soup = BeautifulSoup(page.text, 'html.parser') soup = BeautifulSoup(page.text, 'html.parser')
lyrics = soup.select_one( lyrics = soup.select_one(
f'{LYRICS_ROW}>div' f'{LYRICS_ROW}>div'
':not(.div-share)' ':not(.div-share)'
@ -231,107 +183,127 @@ def parse_azlyrics(link: str) -> None:
) )
if lyrics is None: if lyrics is None:
raise ParseError('song lyrics') raise ParseError('song lyrics')
parsed['lyrics'] = lyrics.get_text().strip() result['lyrics'] = lyrics.get_text().strip()
lyrics_file = Path('.') / 'lyrics.txt'
with lyrics_file.open('wt', encoding='utf-8') as f:
f.write(parsed['lyrics'])
title_elem = soup.select_one(f'{LYRICS_ROW}>b')
if title_elem is None:
raise ParseError('song title')
parsed['title'] = title_elem.get_text().strip('" ')
artist_elem = soup.select_one(f'{LYRICS_ROW}>.lyricsh>h2') artist_elem = soup.select_one(f'{LYRICS_ROW}>.lyricsh>h2')
if artist_elem is None: if artist_elem is None:
raise ParseError('artist name') print('Unable to parse artist name')
parsed['artist'] = artist_elem.get_text() \ result['artist'] = input('Enter the artist name: ')
.removesuffix(' Lyrics') \ else:
.strip() 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_blocks = soup.select('.songinalbum_title')
album = None album = None
if len(album_blocks) > 1: if len(album_blocks) > 1:
album = album_blocks[-2] album = album_blocks[-2]
elif len(album_blocks) > 0: elif len(album_blocks) > 0:
album = album_blocks[0] album = album_blocks[0]
if album is None:
album_re = None
else: else:
raise ParseError('album name') album_re = re.search(
r'album:\s*"(.+?)"\s*\((\d+)\)',
album.get_text()
)
album_re = re.search(
r'album:\s*"(.+?)"\s*\((\d+)\)',
album.get_text()
)
if album_re is None: if album_re is None:
raise ParseError('album name') 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: ')
parsed['album'] = album_re[1] cover = input('Insert an album cover? [Y/n] ')
parsed['year'] = int(album_re[2]) 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())
cover = album.select_one('img.album-image') with cover_file.open('rb') as f:
result['cover'] = f.read()
if cover is not None: result['cover_mime'] = (
mimetypes.guess_type(cover_file)[0]
or 'image/jpeg'
)
except Exception as err:
logging.exception(err)
cover_url = str(cover.get('src')) else:
if cover_url.startswith('/'): result['album'] = album_re[1]
cover_url = BASEURL + cover_url result['year'] = int(album_re[2])
req = session.get(cover_url) assert album is not None
parsed['cover'] = req.content cover = album.select_one('img.album-image')
parsed['cover_mime'] = req.headers.get(
'Content-Type', 'image/jpeg'
)
tracklist_elem = soup.select_one('.songlist-panel') if cover is not None:
if tracklist_elem is not None:
tracklist = tracklist_elem.select( cover_url = str(cover.get('src'))
'.listalbum-item' if cover_url.startswith('/'):
) cover_url = BASEURL + cover_url
parsed['tracks'] = len(tracklist)
current_url = re.search( req = session.get(cover_url)
r'/(lyrics/.+?\.html)', result['cover'] = req.content
link, result['cover_mime'] = req.headers.get(
) 'Content-Type', 'image/jpeg'
)
tracklist_elem = soup.select_one('.songlist-panel')
if tracklist_elem is not None:
parsed['track_no'] = 0 tracklist = tracklist_elem.select(
if current_url is not None: '.listalbum-item'
for i, track in enumerate(tracklist): )
result['tracks'] = len(tracklist)
track_url = track.select_one('a') current_url = re.search(
if track_url is None: r'/(lyrics/.+?\.html)',
continue link,
)
track_href = str(track_url.get('href')) result['track_no'] = 0
if current_url[0] in track_href: if current_url is not None:
parsed['track_no'] = (i + 1) for i, track in enumerate(tracklist):
break
print('Succesfully parsed') track_url = track.select_one('a')
print('Title:', parsed['title']) if track_url is None:
print('Artist:', parsed['artist']) continue
print('Album:', parsed['album'])
print('Track:', parsed['track_no'], '/', parsed['tracks'])
print('Correct something?')
if input('[y/N] ').lower == 'y': track_href = str(track_url.get('href'))
raise ParseError(ParseError.EDIT) if current_url[0] in track_href:
result['track_no'] = (i + 1)
break
print() return result
def manual_info_input(overwrite_lyrics: bool = True) -> None: def manual_info_input() -> ParseResult:
global parsed result = ParseResult(
title=input('Song title: '),
parsed['title'] = input('Song title: ', parsed['title']) artist=input('Artist name: '),
parsed['artist'] = input('Artist name: ', parsed['artist']) album=input('Album name: '),
parsed['album'] = input('Album name: ', parsed['album']) year=input_num('Release year: '),
parsed['year'] = input_num('Release year: ', parsed['year']) track_no=input_num('Track #'),
parsed['track_no'] = input_num('Track #', parsed['track_no']) tracks=input_num('Tracks in album: '),
parsed['tracks'] = input_num('Tracks in album: ', parsed['tracks']) lyrics='', cover=None, cover_mime=None,
)
editor = os.getenv('EDITOR', 'nano') editor = os.getenv('EDITOR', 'nano')
print('Now, paste the lyrics into a text editor') print('Now, paste the lyrics into a text editor')
@ -344,10 +316,8 @@ def manual_info_input(overwrite_lyrics: bool = True) -> None:
try: try:
lyrics_file = Path('.') / 'lyrics.txt' lyrics_file = Path('.') / 'lyrics.txt'
with lyrics_file.open('wt') as f:
if overwrite_lyrics or not lyrics_file.exists(): f.write('\n')
with lyrics_file.open('wt') as f:
f.write('\n')
subprocess.call([ subprocess.call([
editor, editor,
@ -356,14 +326,14 @@ def manual_info_input(overwrite_lyrics: bool = True) -> None:
print('Reading file...') print('Reading file...')
with open('lyrics.txt', 'rt', encoding='utf-8') as f: with open('lyrics.txt', 'rt', encoding='utf-8') as f:
parsed['lyrics'] = f.read().strip() result['lyrics'] = f.read().strip()
print('Done') print('Done')
except OSError as err: except OSError as err:
logging.exception(err) logging.exception(err)
cover = input('Insert an album cover? [Y/n] ') cover = input('Insert an album cover? [Y/n] ')
if cover.lower() != 'n': if cover.lower() not in ('n','н'):
try: try:
print( print(
'Download the cover and enter its path:', 'Download the cover and enter its path:',
@ -373,27 +343,26 @@ def manual_info_input(overwrite_lyrics: bool = True) -> None:
cover_file = Path(input().strip()) cover_file = Path(input().strip())
with cover_file.open('rb') as f: with cover_file.open('rb') as f:
parsed['cover'] = f.read() result['cover'] = f.read()
parsed['cover_mime'] = ( result['cover_mime'] = (
mimetypes.guess_type(cover_file)[0] mimetypes.guess_type(cover_file)[0]
or 'image/jpeg' or 'image/jpeg'
) )
except Exception as err: except Exception as err:
logging.exception(err) logging.exception(err)
print() return result
def tagmp3( def tagmp3(
file: str, file: str,
parsed: ParseResult,
copy: bool) -> None: copy: bool) -> None:
global parsed
oldpath = Path(file) oldpath = Path(file)
newpath = oldpath newpath = oldpath
if copy: if copy:
newdir = ( newdir = (
@ -418,7 +387,7 @@ def tagmp3(
cover = newdir / f'cover{ext}' cover = newdir / f'cover{ext}'
with cover.open('wb') as f: with cover.open('wb') as f:
f.write(parsed['cover']) f.write(parsed['cover'])
id3 = ID3(str(newpath)) id3 = ID3(str(newpath))
id3['TPE1'] = TPE1(text=parsed['artist']) id3['TPE1'] = TPE1(text=parsed['artist'])
id3['TIT2'] = TIT2(text=parsed['title']) id3['TIT2'] = TIT2(text=parsed['title'])

View file

@ -1,26 +0,0 @@
clean:
rm -rf __pycache__/
rm -rf .mypy_cache/
rm -f lyrics.txt input
deps:
python3 -m pip install -r requirements.txt
run:
chmod +x ./autoytdlp.sh
./autoytdlp.sh
conv:
chmod +x ./convert.sh
./convert.sh
tags:
chmod +x ./id3tag.sh
./id3tag.sh
pyformat:
python3 -m autopep8 --in-place .*.py
pycheck:
python3 -m mypy .*.py
python3 -m pylint -j4 .*.py

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
debug=0 debug=1
watching=1 watching=1
links=() links=()
success=0 success=0

View file

@ -20,5 +20,3 @@ echo
find "$directory" -type f -name "*.mp3" -exec \ find "$directory" -type f -name "*.mp3" -exec \
python3 ./.id3tag_helper.py "$copy_arg" {} \; python3 ./.id3tag_helper.py "$copy_arg" {} \;
rm -f ./input

191
pylintrc
View file

@ -1,191 +0,0 @@
[MAIN]
analyse-fallback-blocks=no
extension-pkg-allow-list=
extension-pkg-whitelist=
fail-on=
fail-under=10
ignore=CVS
ignore-paths=
ignore-patterns=^\.#
ignored-modules=
jobs=4
limit-inference-results=100
load-plugins=
persistent=yes
py-version=3.10
recursive=no
suggestion-mode=yes
unsafe-load-any-extension=no
[REPORTS]
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
msg-template=
reports=no
score=yes
[MESSAGES CONTROL]
confidence=HIGH,
CONTROL_FLOW,
INFERENCE,
INFERENCE_FAILURE,
UNDEFINED
disable=raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
too-many-branches,
too-many-statements,
too-many-locals,
broad-except,
global-variable-not-assigned
enable=c-extension-no-member
[SIMILARITIES]
ignore-comments=yes
ignore-docstrings=yes
ignore-imports=yes
ignore-signatures=yes
min-similarity-lines=4
[MISCELLANEOUS]
notes=FIXME,
XXX,
TODO
notes-rgx=
[DESIGN]
exclude-too-few-public-methods=
ignored-parents=
max-args=5
max-attributes=7
max-bool-expr=5
max-branches=12
max-locals=15
max-parents=7
max-public-methods=20
max-returns=6
max-statements=50
min-public-methods=2
[STRING]
check-quote-consistency=no
check-str-concat-over-line-jumps=no
[CLASSES]
check-protected-access-in-special-methods=no
defining-attr-methods=__init__,
__new__,
setUp,
__post_init__
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
valid-classmethod-first-arg=cls
valid-metaclass-classmethod-first-arg=cls
[FORMAT]
expected-line-ending-format=
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
indent-after-paren=4
indent-string=' '
max-line-length=100
max-module-lines=1000
single-line-class-stmt=no
single-line-if-stmt=no
[IMPORTS]
allow-any-import-level=
allow-wildcard-with-all=no
deprecated-modules=
ext-import-graph=
import-graph=
int-import-graph=
known-standard-library=
known-third-party=enchant
preferred-modules=
[VARIABLES]
additional-builtins=
allow-global-unused-variables=yes
allowed-redefined-builtins=
callbacks=cb_,
_cb
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
ignored-argument-names=_.*|^ignored_|^unused_
init-import=no
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
[LOGGING]
logging-format-style=old
logging-modules=logging
[EXCEPTIONS]
overgeneral-exceptions=BaseException,
Exception
[BASIC]
argument-naming-style=snake_case
attr-naming-style=snake_case
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
bad-names-rgxs=
class-attribute-naming-style=any
class-const-naming-style=UPPER_CASE
class-naming-style=PascalCase
const-naming-style=any
docstring-min-length=-1
function-naming-style=snake_case
good-names=i,
j,
k,
f,
ex,
Run,
_
good-names-rgxs=
include-naming-hint=no
inlinevar-naming-style=any
method-naming-style=snake_case
module-naming-style=snake_case
name-group=
no-docstring-rgx=^_
property-classes=abc.abstractproperty
variable-naming-style=snake_case
[SPELLING]
max-spelling-suggestions=4
spelling-dict=
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
spelling-ignore-words=
spelling-private-dict-file=
spelling-store-unknown-words=no
[TYPECHECK]
contextmanager-decorators=contextlib.contextmanager
generated-members=
ignore-none=yes
ignore-on-opaque-inference=yes
ignored-checks-for-mixins=no-member,
not-async-context-manager,
not-context-manager,
attribute-defined-outside-init
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
missing-member-hint=yes
missing-member-hint-distance=1
missing-member-max-choices=1
mixin-class-rgx=.*[Mm]ixin
signature-mutators=
[REFACTORING]
max-nested-blocks=5
never-returning-functions=sys.exit,argparse.parse_error