Compare commits

...

7 commits

Author SHA1 Message Date
b6761c2824 make deps 2023-02-08 15:01:47 +04:00
74ccfec742 PEP8, MyPy, Pylint; Makefile glob bugfix 2023-02-08 15:00:20 +04:00
426bd118aa Makefile 2023-02-08 14:43:55 +04:00
0930d20224 DRY principle 2023-02-08 14:40:20 +04:00
9687313cb4 Manual info correction, input() using readline 2023-02-08 14:07:51 +04:00
450421ce0d ParseError -> manual input 2023-02-07 20:52:08 +04:00
1728effd97 Disabled debug option 2023-02-07 20:09:05 +04:00
6 changed files with 392 additions and 137 deletions

7
.gitignore vendored
View file

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

View file

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

26
Makefile Normal file
View file

@ -0,0 +1,26 @@
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
debug=1
debug=0
watching=1
links=()
success=0

View file

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

191
pylintrc Normal file
View file

@ -0,0 +1,191 @@
[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