Compare commits
7 commits
e8b1330809
...
b6761c2824
Author | SHA1 | Date | |
---|---|---|---|
b6761c2824 | |||
74ccfec742 | |||
426bd118aa | |||
0930d20224 | |||
9687313cb4 | |||
450421ce0d | |||
1728effd97 |
6 changed files with 392 additions and 137 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,5 +1,10 @@
|
|||
files/
|
||||
convert/
|
||||
tagged/
|
||||
.vscode/
|
||||
lyrics.txt
|
||||
input
|
||||
|
||||
__pycache__/
|
||||
.mypy_cache/
|
||||
.vscode/
|
||||
.idea/
|
||||
|
|
|
@ -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()
|
||||
|
||||
try:
|
||||
url = search_azurl(title)
|
||||
print(url)
|
||||
parsed = parse_azlyrics(url)
|
||||
parse_azlyrics(url)
|
||||
|
||||
#print(parsed)
|
||||
tagmp3(file, parsed, copy)
|
||||
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_
|
||||
|
||||
|
@ -138,19 +195,17 @@ def search_azurl(title: str) -> str:
|
|||
|
||||
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:
|
||||
|
@ -159,16 +214,9 @@ def search_azurl(title: str) -> str:
|
|||
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...')
|
||||
|
||||
|
@ -183,73 +231,44 @@ 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:
|
||||
raise ParseError('album name')
|
||||
|
||||
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: ')
|
||||
raise ParseError('album name')
|
||||
|
||||
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())
|
||||
parsed['album'] = album_re[1]
|
||||
parsed['year'] = int(album_re[2])
|
||||
|
||||
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:
|
||||
|
@ -259,8 +278,8 @@ def parse_azlyrics(link: str) -> ParseResult:
|
|||
cover_url = BASEURL + cover_url
|
||||
|
||||
req = session.get(cover_url)
|
||||
result['cover'] = req.content
|
||||
result['cover_mime'] = req.headers.get(
|
||||
parsed['cover'] = req.content
|
||||
parsed['cover_mime'] = req.headers.get(
|
||||
'Content-Type', 'image/jpeg'
|
||||
)
|
||||
|
||||
|
@ -270,14 +289,14 @@ def parse_azlyrics(link: str) -> ParseResult:
|
|||
tracklist = tracklist_elem.select(
|
||||
'.listalbum-item'
|
||||
)
|
||||
result['tracks'] = len(tracklist)
|
||||
parsed['tracks'] = len(tracklist)
|
||||
|
||||
current_url = re.search(
|
||||
r'/(lyrics/.+?\.html)',
|
||||
link,
|
||||
)
|
||||
|
||||
result['track_no'] = 0
|
||||
parsed['track_no'] = 0
|
||||
if current_url is not None:
|
||||
for i, track in enumerate(tracklist):
|
||||
|
||||
|
@ -287,23 +306,32 @@ def parse_azlyrics(link: str) -> ParseResult:
|
|||
|
||||
track_href = str(track_url.get('href'))
|
||||
if current_url[0] in track_href:
|
||||
result['track_no'] = (i + 1)
|
||||
parsed['track_no'] = (i + 1)
|
||||
break
|
||||
|
||||
return result
|
||||
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?')
|
||||
|
||||
if input('[y/N] ').lower == 'y':
|
||||
raise ParseError(ParseError.EDIT)
|
||||
|
||||
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,6 +344,8 @@ def manual_info_input() -> ParseResult:
|
|||
|
||||
try:
|
||||
lyrics_file = Path('.') / 'lyrics.txt'
|
||||
|
||||
if overwrite_lyrics or not lyrics_file.exists():
|
||||
with lyrics_file.open('wt') as f:
|
||||
f.write('\n')
|
||||
|
||||
|
@ -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,23 +373,24 @@ 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
|
||||
|
||||
|
|
26
Makefile
Normal file
26
Makefile
Normal 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
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
debug=1
|
||||
debug=0
|
||||
watching=1
|
||||
links=()
|
||||
success=0
|
||||
|
|
|
@ -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
191
pylintrc
Normal 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
|
Reference in a new issue