diff --git a/.gitignore b/.gitignore index a063f2f..d87a43a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ lyrics.txt input __pycache__/ +.mypy_cache/ .vscode/ .idea/ diff --git a/.id3tag_helper.py b/.id3tag_helper.py index 69cc7a7..ef5659b 100755 --- a/.id3tag_helper.py +++ b/.id3tag_helper.py @@ -16,9 +16,9 @@ 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 @@ -31,8 +31,6 @@ USERAGENT = ( LYRICS_ROW = '.main-page>.row>.col-xs-12' -EDITMSG = 'edit' - safename_re = re.compile( r'[^A-Za-z0-9А-ЯЁа-яё \'".,()\[\]&!#$@_~=*+-]' ) @@ -53,6 +51,18 @@ class ParseResult(TypedDict): cover_mime: Optional[str] +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, @@ -63,16 +73,6 @@ parsed = ParseResult( ) -class ParseError(Exception): - - def __init__(self, parsing_obj: str) -> None: - - super().__init__( - f'Unable to parse {parsing_obj}' - ) - self.parsing_obj = parsing_obj - - def main() -> None: global parsed @@ -108,9 +108,11 @@ def main() -> None: print(err) + # pylint: disable=no-member if isinstance(err, ParseError) \ - and err.parsing_obj == EDITMSG: + and err.parsing_obj == ParseError.EDIT: pass + # pylint: enable=no-member else: print( @@ -121,10 +123,10 @@ def main() -> None: manual_info_input(False) - #print(parsed) - tagmp3(file, parsed, copy) + tagmp3(file, copy) +# pylint: disable=redefined-builtin def input(msg: str = '', def_: Any = '') -> str: subprocess.call( @@ -143,6 +145,7 @@ def input(msg: str = '', def_: Any = '') -> str: .removesuffix('\r') except Exception: return def_ +# pylint: enable=redefined-builtin def input_num(msg: str, def_: int = 0) -> int: @@ -189,10 +192,10 @@ 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', 'language': 'ru-RU', 'safesearch': 0, @@ -207,7 +210,7 @@ def search_azurl(title: str) -> str: if link is None: raise ParseError('song URL') - + return str(link.get('href')) @@ -219,7 +222,7 @@ def parse_azlyrics(link: str) -> None: page = session.get(link) soup = BeautifulSoup(page.text, 'html.parser') - + lyrics = soup.select_one( f'{LYRICS_ROW}>div' ':not(.div-share)' @@ -262,7 +265,7 @@ def parse_azlyrics(link: str) -> None: ) if album_re is None: raise ParseError('album name') - + parsed['album'] = album_re[1] parsed['year'] = int(album_re[2]) @@ -279,7 +282,7 @@ def parse_azlyrics(link: str) -> None: parsed['cover_mime'] = req.headers.get( 'Content-Type', 'image/jpeg' ) - + tracklist_elem = soup.select_one('.songlist-panel') if tracklist_elem is not None: @@ -305,7 +308,7 @@ def parse_azlyrics(link: str) -> None: if current_url[0] in track_href: parsed['track_no'] = (i + 1) break - + print('Succesfully parsed') print('Title:', parsed['title']) print('Artist:', parsed['artist']) @@ -314,9 +317,9 @@ def parse_azlyrics(link: str) -> None: print('Correct something?') if input('[y/N] ').lower == 'y': - raise ParseError('edit') - else: - print() + raise ParseError(ParseError.EDIT) + + print() def manual_info_input(overwrite_lyrics: bool = True) -> None: @@ -378,18 +381,19 @@ def manual_info_input(overwrite_lyrics: bool = True) -> None: ) except Exception as err: logging.exception(err) - + print() def tagmp3( file: str, - parsed: ParseResult, copy: bool) -> None: + global parsed + oldpath = Path(file) newpath = oldpath - + if copy: newdir = ( @@ -414,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']) diff --git a/Makefile b/Makefile index a70a933..200af63 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ clean: rm -rf __pycache__/ + rm -rf .mypy_cache/ rm -f lyrics.txt input run: @@ -15,8 +16,8 @@ tags: ./id3tag.sh pyformat: - python3 -m autopep8 --in-place ./*.py + python3 -m autopep8 --in-place .*.py pycheck: - python3 -m mypy ./*.py - python3 -m pylint -j $(nproc) ./*.py + python3 -m mypy .*.py + python3 -m pylint -j4 .*.py diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..9aea9fd --- /dev/null +++ b/pylintrc @@ -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*(# )??$ +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