Initial commit
*Rupert* ("Ripper") is a tool to rip CD (and eventually DVD) media (almost) automatically. It converts CDDA to WAV using `cdparanoia`, encodes the output with `flac`, and adds metadata tags from MusicBrainz using *mutagen*. The console user interface is provided by *rich*.pull/3/head
commit
a997be4515
|
@ -0,0 +1,2 @@
|
||||||
|
/.venv
|
||||||
|
/dist
|
|
@ -0,0 +1,8 @@
|
||||||
|
[MASTER]
|
||||||
|
extension-pkg-whitelist = pydantic
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
disable =
|
||||||
|
invalid-name,
|
||||||
|
no-else-return,
|
||||||
|
raise-missing-from,
|
|
@ -0,0 +1,114 @@
|
||||||
|
# The default ``config.py``
|
||||||
|
# flake8: noqa
|
||||||
|
|
||||||
|
|
||||||
|
def set_prefs(prefs):
|
||||||
|
"""This function is called before opening the project"""
|
||||||
|
|
||||||
|
# Specify which files and folders to ignore in the project.
|
||||||
|
# Changes to ignored resources are not added to the history and
|
||||||
|
# VCSs. Also they are not returned in `Project.get_files()`.
|
||||||
|
# Note that ``?`` and ``*`` match all characters but slashes.
|
||||||
|
# '*.pyc': matches 'test.pyc' and 'pkg/test.pyc'
|
||||||
|
# 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc'
|
||||||
|
# '.svn': matches 'pkg/.svn' and all of its children
|
||||||
|
# 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o'
|
||||||
|
# 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o'
|
||||||
|
prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject',
|
||||||
|
'.hg', '.svn', '_svn', '.git', '.tox']
|
||||||
|
|
||||||
|
# Specifies which files should be considered python files. It is
|
||||||
|
# useful when you have scripts inside your project. Only files
|
||||||
|
# ending with ``.py`` are considered to be python files by
|
||||||
|
# default.
|
||||||
|
# prefs['python_files'] = ['*.py']
|
||||||
|
|
||||||
|
# Custom source folders: By default rope searches the project
|
||||||
|
# for finding source folders (folders that should be searched
|
||||||
|
# for finding modules). You can add paths to that list. Note
|
||||||
|
# that rope guesses project source folders correctly most of the
|
||||||
|
# time; use this if you have any problems.
|
||||||
|
# The folders should be relative to project root and use '/' for
|
||||||
|
# separating folders regardless of the platform rope is running on.
|
||||||
|
# 'src/my_source_folder' for instance.
|
||||||
|
# prefs.add('source_folders', 'src')
|
||||||
|
|
||||||
|
# You can extend python path for looking up modules
|
||||||
|
# prefs.add('python_path', '~/python/')
|
||||||
|
|
||||||
|
# Should rope save object information or not.
|
||||||
|
prefs['save_objectdb'] = True
|
||||||
|
prefs['compress_objectdb'] = False
|
||||||
|
|
||||||
|
# If `True`, rope analyzes each module when it is being saved.
|
||||||
|
prefs['automatic_soa'] = True
|
||||||
|
# The depth of calls to follow in static object analysis
|
||||||
|
prefs['soa_followed_calls'] = 0
|
||||||
|
|
||||||
|
# If `False` when running modules or unit tests "dynamic object
|
||||||
|
# analysis" is turned off. This makes them much faster.
|
||||||
|
prefs['perform_doa'] = True
|
||||||
|
|
||||||
|
# Rope can check the validity of its object DB when running.
|
||||||
|
prefs['validate_objectdb'] = True
|
||||||
|
|
||||||
|
# How many undos to hold?
|
||||||
|
prefs['max_history_items'] = 32
|
||||||
|
|
||||||
|
# Shows whether to save history across sessions.
|
||||||
|
prefs['save_history'] = True
|
||||||
|
prefs['compress_history'] = False
|
||||||
|
|
||||||
|
# Set the number spaces used for indenting. According to
|
||||||
|
# :PEP:`8`, it is best to use 4 spaces. Since most of rope's
|
||||||
|
# unit-tests use 4 spaces it is more reliable, too.
|
||||||
|
prefs['indent_size'] = 4
|
||||||
|
|
||||||
|
# Builtin and c-extension modules that are allowed to be imported
|
||||||
|
# and inspected by rope.
|
||||||
|
prefs['extension_modules'] = []
|
||||||
|
|
||||||
|
# Add all standard c-extensions to extension_modules list.
|
||||||
|
prefs['import_dynload_stdmods'] = True
|
||||||
|
|
||||||
|
# If `True` modules with syntax errors are considered to be empty.
|
||||||
|
# The default value is `False`; When `False` syntax errors raise
|
||||||
|
# `rope.base.exceptions.ModuleSyntaxError` exception.
|
||||||
|
prefs['ignore_syntax_errors'] = False
|
||||||
|
|
||||||
|
# If `True`, rope ignores unresolvable imports. Otherwise, they
|
||||||
|
# appear in the importing namespace.
|
||||||
|
prefs['ignore_bad_imports'] = False
|
||||||
|
|
||||||
|
# If `True`, rope will insert new module imports as
|
||||||
|
# `from <package> import <module>` by default.
|
||||||
|
prefs['prefer_module_from_imports'] = False
|
||||||
|
|
||||||
|
# If `True`, rope will transform a comma list of imports into
|
||||||
|
# multiple separate import statements when organizing
|
||||||
|
# imports.
|
||||||
|
prefs['split_imports'] = False
|
||||||
|
|
||||||
|
# If `True`, rope will remove all top-level import statements and
|
||||||
|
# reinsert them at the top of the module when making changes.
|
||||||
|
prefs['pull_imports_to_top'] = True
|
||||||
|
|
||||||
|
# If `True`, rope will sort imports alphabetically by module name instead
|
||||||
|
# of alphabetically by import statement, with from imports after normal
|
||||||
|
# imports.
|
||||||
|
prefs['sort_imports_alphabetically'] = False
|
||||||
|
|
||||||
|
# Location of implementation of
|
||||||
|
# rope.base.oi.type_hinting.interfaces.ITypeHintingFactory In general
|
||||||
|
# case, you don't have to change this value, unless you're an rope expert.
|
||||||
|
# Change this value to inject you own implementations of interfaces
|
||||||
|
# listed in module rope.base.oi.type_hinting.providers.interfaces
|
||||||
|
# For example, you can add you own providers for Django Models, or disable
|
||||||
|
# the search type-hinting in a class hierarchy, etc.
|
||||||
|
prefs['type_hinting_factory'] = (
|
||||||
|
'rope.base.oi.type_hinting.factory.default_type_hinting_factory')
|
||||||
|
|
||||||
|
|
||||||
|
def project_opened(project):
|
||||||
|
"""This function is called after opening the project"""
|
||||||
|
# Do whatever you like here!
|
Binary file not shown.
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"python.pythonPath": ".venv/bin/python",
|
||||||
|
"python.linting.mypyEnabled": true,
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
[mypy]
|
||||||
|
allow_redefinition = False
|
||||||
|
allow_untyped_globals = False
|
||||||
|
check_untyped_defs = True
|
||||||
|
disallow_any_generics = True
|
||||||
|
disallow_incomplete_defs = True
|
||||||
|
disallow_subclassing_any = True
|
||||||
|
disallow_untyped_calls = True
|
||||||
|
disallow_untyped_decorators = True
|
||||||
|
disallow_untyped_defs = True
|
||||||
|
disallow_untyped_defs = True
|
||||||
|
ignore_missing_imports = True
|
||||||
|
implicit_reexport = False
|
||||||
|
local_partial_types = False
|
||||||
|
namespace_packages = True
|
||||||
|
no_implicit_optional = True
|
||||||
|
strict_equality = True
|
||||||
|
strict_optional = True
|
||||||
|
warn_no_return = True
|
||||||
|
warn_redundant_cass = True
|
||||||
|
warn_return_any = True
|
||||||
|
warn_return_any = True
|
||||||
|
warn_unreachable = True
|
||||||
|
warn_unused_configs = True
|
||||||
|
warn_unused_ignores = True
|
|
@ -0,0 +1,509 @@
|
||||||
|
[[package]]
|
||||||
|
name = "astroid"
|
||||||
|
version = "2.4.2"
|
||||||
|
description = "An abstract syntax tree for Python with inference support."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
lazy-object-proxy = ">=1.4.0,<1.5.0"
|
||||||
|
six = ">=1.12,<2.0"
|
||||||
|
typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""}
|
||||||
|
wrapt = ">=1.11,<2.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "7.1.2"
|
||||||
|
description = "Composable command line interface toolkit"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.4"
|
||||||
|
description = "Cross-platform colored terminal text."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "commonmark"
|
||||||
|
version = "0.9.1"
|
||||||
|
description = "Python parser for the CommonMark Markdown spec"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flake8"
|
||||||
|
version = "3.8.4"
|
||||||
|
description = "the modular source code checker: pep8 pyflakes and co"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||||
|
mccabe = ">=0.6.0,<0.7.0"
|
||||||
|
pycodestyle = ">=2.6.0a1,<2.7.0"
|
||||||
|
pyflakes = ">=2.2.0,<2.3.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "importlib-metadata"
|
||||||
|
version = "3.3.0"
|
||||||
|
description = "Read metadata from Python packages"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
|
||||||
|
zipp = ">=0.5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
|
||||||
|
testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "isort"
|
||||||
|
version = "5.7.0"
|
||||||
|
description = "A Python utility / library to sort Python imports."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6,<4.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
|
||||||
|
requirements_deprecated_finder = ["pipreqs", "pip-api"]
|
||||||
|
colors = ["colorama (>=0.4.3,<0.5.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy-object-proxy"
|
||||||
|
version = "1.4.3"
|
||||||
|
description = "A fast and thorough lazy object proxy."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mccabe"
|
||||||
|
version = "0.6.1"
|
||||||
|
description = "McCabe checker, plugin for flake8"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "musicbrainzngs"
|
||||||
|
version = "0.7.1"
|
||||||
|
description = "Python bindings for the MusicBrainz NGS and the Cover Art Archive webservices"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mutagen"
|
||||||
|
version = "1.45.1"
|
||||||
|
description = "read and write audio tags for many formats"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5, <4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy"
|
||||||
|
version = "0.790"
|
||||||
|
description = "Optional static typing for Python"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
mypy-extensions = ">=0.4.3,<0.5.0"
|
||||||
|
typed-ast = ">=1.4.0,<1.5.0"
|
||||||
|
typing-extensions = ">=3.7.4"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dmypy = ["psutil (>=4.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "0.4.3"
|
||||||
|
description = "Experimental type system extensions for programs checked with the mypy typechecker."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycodestyle"
|
||||||
|
version = "2.6.0"
|
||||||
|
description = "Python style guide checker"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic"
|
||||||
|
version = "1.7.3"
|
||||||
|
description = "Data validation and settings management using python 3.6 type hinting"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dotenv = ["python-dotenv (>=0.10.4)"]
|
||||||
|
email = ["email-validator (>=1.0.3)"]
|
||||||
|
typing_extensions = ["typing-extensions (>=3.7.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyflakes"
|
||||||
|
version = "2.2.0"
|
||||||
|
description = "passive checker of Python programs"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.7.3"
|
||||||
|
description = "Pygments is a syntax highlighting package written in Python."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pylint"
|
||||||
|
version = "2.6.0"
|
||||||
|
description = "python code static checker"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5.*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
astroid = ">=2.4.0,<=2.5"
|
||||||
|
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
|
isort = ">=4.2.5,<6"
|
||||||
|
mccabe = ">=0.6,<0.7"
|
||||||
|
toml = ">=0.7.1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-libdiscid"
|
||||||
|
version = "1.1"
|
||||||
|
description = "Python bindings for libdiscid"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyudev"
|
||||||
|
version = "0.22.0"
|
||||||
|
description = "A libudev binding"
|
||||||
|
category = "main"
|
||||||
|
optional = true
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
six = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rich"
|
||||||
|
version = "9.6.2"
|
||||||
|
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6,<4.0"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = ">=0.4.0,<0.5.0"
|
||||||
|
commonmark = ">=0.9.0,<0.10.0"
|
||||||
|
pygments = ">=2.6.0,<3.0.0"
|
||||||
|
typing-extensions = ">=3.7.4,<4.0.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rope"
|
||||||
|
version = "0.18.0"
|
||||||
|
description = "a python refactoring library..."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["pytest"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.15.0"
|
||||||
|
description = "Python 2 and 3 compatibility utilities"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.10.2"
|
||||||
|
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typed-ast"
|
||||||
|
version = "1.4.2"
|
||||||
|
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typer"
|
||||||
|
version = "0.3.2"
|
||||||
|
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
click = ">=7.1.1,<7.2.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
test = ["pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.782)", "black (>=19.10b0,<20.0b0)", "isort (>=5.0.6,<6.0.0)", "shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)"]
|
||||||
|
all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)"]
|
||||||
|
dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)"]
|
||||||
|
doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=5.4.0,<6.0.0)", "markdown-include (>=0.5.1,<0.6.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "3.7.4.3"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.5+"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wrapt"
|
||||||
|
version = "1.12.1"
|
||||||
|
description = "Module for decorators, wrappers and monkey patching."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zipp"
|
||||||
|
version = "3.4.0"
|
||||||
|
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
|
||||||
|
testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
|
||||||
|
|
||||||
|
[extras]
|
||||||
|
udev = ["pyudev"]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "1.1"
|
||||||
|
python-versions = "^3.7"
|
||||||
|
content-hash = "f3ded121ca6cca186d5e649e2a1f73136e28ed4d57cae8118fb7d97f5c236c80"
|
||||||
|
|
||||||
|
[metadata.files]
|
||||||
|
astroid = [
|
||||||
|
{file = "astroid-2.4.2-py3-none-any.whl", hash = "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"},
|
||||||
|
{file = "astroid-2.4.2.tar.gz", hash = "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703"},
|
||||||
|
]
|
||||||
|
click = [
|
||||||
|
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
|
||||||
|
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
|
||||||
|
]
|
||||||
|
colorama = [
|
||||||
|
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
|
||||||
|
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
|
||||||
|
]
|
||||||
|
commonmark = [
|
||||||
|
{file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
|
||||||
|
{file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
|
||||||
|
]
|
||||||
|
flake8 = [
|
||||||
|
{file = "flake8-3.8.4-py2.py3-none-any.whl", hash = "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839"},
|
||||||
|
{file = "flake8-3.8.4.tar.gz", hash = "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"},
|
||||||
|
]
|
||||||
|
importlib-metadata = [
|
||||||
|
{file = "importlib_metadata-3.3.0-py3-none-any.whl", hash = "sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"},
|
||||||
|
{file = "importlib_metadata-3.3.0.tar.gz", hash = "sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed"},
|
||||||
|
]
|
||||||
|
isort = [
|
||||||
|
{file = "isort-5.7.0-py3-none-any.whl", hash = "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc"},
|
||||||
|
{file = "isort-5.7.0.tar.gz", hash = "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e"},
|
||||||
|
]
|
||||||
|
lazy-object-proxy = [
|
||||||
|
{file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-win32.whl", hash = "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp34-cp34m-win32.whl", hash = "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp34-cp34m-win_amd64.whl", hash = "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp38-cp38-win32.whl", hash = "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd"},
|
||||||
|
{file = "lazy_object_proxy-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239"},
|
||||||
|
]
|
||||||
|
mccabe = [
|
||||||
|
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
|
||||||
|
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
|
||||||
|
]
|
||||||
|
musicbrainzngs = [
|
||||||
|
{file = "musicbrainzngs-0.7.1-py2.py3-none-any.whl", hash = "sha256:e841a8f975104c0a72290b09f59326050194081a5ae62ee512f41915090e1a10"},
|
||||||
|
{file = "musicbrainzngs-0.7.1.tar.gz", hash = "sha256:ab1c0100fd0b305852e65f2ed4113c6de12e68afd55186987b8ed97e0f98e627"},
|
||||||
|
]
|
||||||
|
mutagen = [
|
||||||
|
{file = "mutagen-1.45.1-py3-none-any.whl", hash = "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed"},
|
||||||
|
{file = "mutagen-1.45.1.tar.gz", hash = "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1"},
|
||||||
|
]
|
||||||
|
mypy = [
|
||||||
|
{file = "mypy-0.790-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669"},
|
||||||
|
{file = "mypy-0.790-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802"},
|
||||||
|
{file = "mypy-0.790-cp35-cp35m-win_amd64.whl", hash = "sha256:e86bdace26c5fe9cf8cb735e7cedfe7850ad92b327ac5d797c656717d2ca66de"},
|
||||||
|
{file = "mypy-0.790-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e97e9c13d67fbe524be17e4d8025d51a7dca38f90de2e462243ab8ed8a9178d1"},
|
||||||
|
{file = "mypy-0.790-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0d34d6b122597d48a36d6c59e35341f410d4abfa771d96d04ae2c468dd201abc"},
|
||||||
|
{file = "mypy-0.790-cp36-cp36m-win_amd64.whl", hash = "sha256:72060bf64f290fb629bd4a67c707a66fd88ca26e413a91384b18db3876e57ed7"},
|
||||||
|
{file = "mypy-0.790-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:eea260feb1830a627fb526d22fbb426b750d9f5a47b624e8d5e7e004359b219c"},
|
||||||
|
{file = "mypy-0.790-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c614194e01c85bb2e551c421397e49afb2872c88b5830e3554f0519f9fb1c178"},
|
||||||
|
{file = "mypy-0.790-cp37-cp37m-win_amd64.whl", hash = "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324"},
|
||||||
|
{file = "mypy-0.790-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4e7bf7f1214826cf7333627cb2547c0db7e3078723227820d0a2490f117a01"},
|
||||||
|
{file = "mypy-0.790-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:af4e9ff1834e565f1baa74ccf7ae2564ae38c8df2a85b057af1dbbc958eb6666"},
|
||||||
|
{file = "mypy-0.790-cp38-cp38-win_amd64.whl", hash = "sha256:da56dedcd7cd502ccd3c5dddc656cb36113dd793ad466e894574125945653cea"},
|
||||||
|
{file = "mypy-0.790-py3-none-any.whl", hash = "sha256:2842d4fbd1b12ab422346376aad03ff5d0805b706102e475e962370f874a5122"},
|
||||||
|
{file = "mypy-0.790.tar.gz", hash = "sha256:2b21ba45ad9ef2e2eb88ce4aeadd0112d0f5026418324176fd494a6824b74975"},
|
||||||
|
]
|
||||||
|
mypy-extensions = [
|
||||||
|
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
||||||
|
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||||
|
]
|
||||||
|
pycodestyle = [
|
||||||
|
{file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"},
|
||||||
|
{file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"},
|
||||||
|
]
|
||||||
|
pydantic = [
|
||||||
|
{file = "pydantic-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd"},
|
||||||
|
{file = "pydantic-1.7.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a4143c8d0c456a093387b96e0f5ee941a950992904d88bc816b4f0e72c9a0009"},
|
||||||
|
{file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:d8df4b9090b595511906fa48deda47af04e7d092318bfb291f4d45dfb6bb2127"},
|
||||||
|
{file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:514b473d264671a5c672dfb28bdfe1bf1afd390f6b206aa2ec9fed7fc592c48e"},
|
||||||
|
{file = "pydantic-1.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:dba5c1f0a3aeea5083e75db9660935da90216f8a81b6d68e67f54e135ed5eb23"},
|
||||||
|
{file = "pydantic-1.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59e45f3b694b05a69032a0d603c32d453a23f0de80844fb14d55ab0c6c78ff2f"},
|
||||||
|
{file = "pydantic-1.7.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b24e8a572e4b4c18f614004dda8c9f2c07328cb5b6e314d6e1bbd536cb1a6c1"},
|
||||||
|
{file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:b2b054d095b6431cdda2f852a6d2f0fdec77686b305c57961b4c5dd6d863bf3c"},
|
||||||
|
{file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:025bf13ce27990acc059d0c5be46f416fc9b293f45363b3d19855165fee1874f"},
|
||||||
|
{file = "pydantic-1.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6e3874aa7e8babd37b40c4504e3a94cc2023696ced5a0500949f3347664ff8e2"},
|
||||||
|
{file = "pydantic-1.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e682f6442ebe4e50cb5e1cfde7dda6766fb586631c3e5569f6aa1951fd1a76ef"},
|
||||||
|
{file = "pydantic-1.7.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:185e18134bec5ef43351149fe34fda4758e53d05bb8ea4d5928f0720997b79ef"},
|
||||||
|
{file = "pydantic-1.7.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:f5b06f5099e163295b8ff5b1b71132ecf5866cc6e7f586d78d7d3fd6e8084608"},
|
||||||
|
{file = "pydantic-1.7.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:24ca47365be2a5a3cc3f4a26dcc755bcdc9f0036f55dcedbd55663662ba145ec"},
|
||||||
|
{file = "pydantic-1.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:d1fe3f0df8ac0f3a9792666c69a7cd70530f329036426d06b4f899c025aca74e"},
|
||||||
|
{file = "pydantic-1.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f6864844b039805add62ebe8a8c676286340ba0c6d043ae5dea24114b82a319e"},
|
||||||
|
{file = "pydantic-1.7.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ecb54491f98544c12c66ff3d15e701612fc388161fd455242447083350904730"},
|
||||||
|
{file = "pydantic-1.7.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:ffd180ebd5dd2a9ac0da4e8b995c9c99e7c74c31f985ba090ee01d681b1c4b95"},
|
||||||
|
{file = "pydantic-1.7.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8d72e814c7821125b16f1553124d12faba88e85405b0864328899aceaad7282b"},
|
||||||
|
{file = "pydantic-1.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:475f2fa134cf272d6631072554f845d0630907fce053926ff634cc6bc45bf1af"},
|
||||||
|
{file = "pydantic-1.7.3-py3-none-any.whl", hash = "sha256:38be427ea01a78206bcaf9a56f835784afcba9e5b88fbdce33bbbfbcd7841229"},
|
||||||
|
{file = "pydantic-1.7.3.tar.gz", hash = "sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9"},
|
||||||
|
]
|
||||||
|
pyflakes = [
|
||||||
|
{file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"},
|
||||||
|
{file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"},
|
||||||
|
]
|
||||||
|
pygments = [
|
||||||
|
{file = "Pygments-2.7.3-py3-none-any.whl", hash = "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"},
|
||||||
|
{file = "Pygments-2.7.3.tar.gz", hash = "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716"},
|
||||||
|
]
|
||||||
|
pylint = [
|
||||||
|
{file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"},
|
||||||
|
{file = "pylint-2.6.0.tar.gz", hash = "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210"},
|
||||||
|
]
|
||||||
|
python-libdiscid = [
|
||||||
|
{file = "python-libdiscid-1.1.tar.gz", hash = "sha256:c342531ca6cf0c0ed7890515d135e6d16199d81040189c8653002327f7e7229a"},
|
||||||
|
]
|
||||||
|
pyudev = [
|
||||||
|
{file = "pyudev-0.22.0.tar.gz", hash = "sha256:69bb1beb7ac52855b6d1b9fe909eefb0017f38d917cba9939602c6880035b276"},
|
||||||
|
]
|
||||||
|
rich = [
|
||||||
|
{file = "rich-9.6.2-py3-none-any.whl", hash = "sha256:e0efd2ba715dcfb78e57986e15c6d70a3beb98a7015471ca9dd511571a8a9882"},
|
||||||
|
{file = "rich-9.6.2.tar.gz", hash = "sha256:b6a7f9ef1a35c248498952d3454fb4f88de415dd989f97c3e5c5e2235d66e3a5"},
|
||||||
|
]
|
||||||
|
rope = [
|
||||||
|
{file = "rope-0.18.0.tar.gz", hash = "sha256:786b5c38c530d4846aa68a42604f61b4e69a493390e3ca11b88df0fbfdc3ed04"},
|
||||||
|
]
|
||||||
|
six = [
|
||||||
|
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
|
||||||
|
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
|
||||||
|
]
|
||||||
|
toml = [
|
||||||
|
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||||
|
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||||
|
]
|
||||||
|
typed-ast = [
|
||||||
|
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"},
|
||||||
|
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"},
|
||||||
|
{file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"},
|
||||||
|
{file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"},
|
||||||
|
{file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"},
|
||||||
|
{file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"},
|
||||||
|
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"},
|
||||||
|
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"},
|
||||||
|
{file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"},
|
||||||
|
{file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"},
|
||||||
|
{file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"},
|
||||||
|
{file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"},
|
||||||
|
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"},
|
||||||
|
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"},
|
||||||
|
{file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"},
|
||||||
|
{file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"},
|
||||||
|
{file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"},
|
||||||
|
{file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"},
|
||||||
|
{file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"},
|
||||||
|
{file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"},
|
||||||
|
{file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"},
|
||||||
|
{file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"},
|
||||||
|
{file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"},
|
||||||
|
{file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"},
|
||||||
|
{file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"},
|
||||||
|
{file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"},
|
||||||
|
{file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"},
|
||||||
|
{file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"},
|
||||||
|
{file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"},
|
||||||
|
{file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"},
|
||||||
|
]
|
||||||
|
typer = [
|
||||||
|
{file = "typer-0.3.2-py3-none-any.whl", hash = "sha256:ba58b920ce851b12a2d790143009fa00ac1d05b3ff3257061ff69dbdfc3d161b"},
|
||||||
|
{file = "typer-0.3.2.tar.gz", hash = "sha256:5455d750122cff96745b0dec87368f56d023725a7ebc9d2e54dd23dc86816303"},
|
||||||
|
]
|
||||||
|
typing-extensions = [
|
||||||
|
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
|
||||||
|
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
|
||||||
|
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
|
||||||
|
]
|
||||||
|
wrapt = [
|
||||||
|
{file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"},
|
||||||
|
]
|
||||||
|
zipp = [
|
||||||
|
{file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"},
|
||||||
|
{file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"},
|
||||||
|
]
|
|
@ -0,0 +1,42 @@
|
||||||
|
[tool.poetry]
|
||||||
|
name = "rupert"
|
||||||
|
version = "0.0.0"
|
||||||
|
description = ""
|
||||||
|
authors = ["Dustin C. Hatch <dustin@hatch.name>"]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.7"
|
||||||
|
pyudev = { version = "^0.22.0", optional = true }
|
||||||
|
rich = "^9.6.2"
|
||||||
|
typer = "^0.3.2"
|
||||||
|
pydantic = "^1.7.3"
|
||||||
|
python-libdiscid = "^1.1"
|
||||||
|
musicbrainzngs = "^0.7.1"
|
||||||
|
mutagen = "^1.45.1"
|
||||||
|
|
||||||
|
[tool.poetry.dev-dependencies]
|
||||||
|
pylint = "^2.6.0"
|
||||||
|
mypy = "^0.790"
|
||||||
|
flake8 = "^3.8.4"
|
||||||
|
rope = "^0.18.0"
|
||||||
|
|
||||||
|
[tool.poetry.scripts]
|
||||||
|
ripper = "rupert.main:main"
|
||||||
|
|
||||||
|
[tool.poetry.extras]
|
||||||
|
udev = ["pyudev"]
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 79
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
ensure_newline_before_comments = true
|
||||||
|
force_grid_wrap = 0
|
||||||
|
include_trailing_comma = true
|
||||||
|
line_length = 79
|
||||||
|
lines_after_imports = 2
|
||||||
|
multi_line_output = 3
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry>=0.12"]
|
||||||
|
build-backend = "poetry.masonry.api"
|
|
@ -0,0 +1,83 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from libdiscid.compat import discid
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
import discid
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyudev
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
pyudev = None
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
OFFSETS = {'ASUS_BW-12B1ST_a': 6, 'ATAPI_iHES212_3': 702}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Disc:
|
||||||
|
disc_id: str
|
||||||
|
toc_string: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DiscDrive:
|
||||||
|
device_node: Path
|
||||||
|
model: str
|
||||||
|
offset: Optional[int]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_path(cls, path: Optional[Path]) -> DiscDrive:
|
||||||
|
if pyudev is not None:
|
||||||
|
return cls._from_udev(path)
|
||||||
|
elif path is not None:
|
||||||
|
return cls._from_sysfs(path)
|
||||||
|
else:
|
||||||
|
raise FileNotFoundError(
|
||||||
|
'No CD-ROM device specifed and missing udev support'
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_udev(cls, path: Optional[Path]) -> DiscDrive:
|
||||||
|
udev = pyudev.Context()
|
||||||
|
if path:
|
||||||
|
path_str = path.as_posix()
|
||||||
|
try:
|
||||||
|
dev = pyudev.Device.from_device_file(udev, path_str)
|
||||||
|
except pyudev.DeviceNotFoundError:
|
||||||
|
raise FileNotFoundError(path_str)
|
||||||
|
else:
|
||||||
|
block_devices = udev.list_devices(subsystem='block')
|
||||||
|
try:
|
||||||
|
dev = next(iter(block_devices.match_property('ID_CDROM', 1)))
|
||||||
|
except StopIteration:
|
||||||
|
raise FileNotFoundError('No CD-ROM device found')
|
||||||
|
model = dev.properties.get('ID_MODEL')
|
||||||
|
return cls(
|
||||||
|
device_node=Path(dev.device_node),
|
||||||
|
model=model,
|
||||||
|
offset=OFFSETS.get(model),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_sysfs(cls, path: Path) -> DiscDrive:
|
||||||
|
sysfs_path = Path('/sys/block') / path.name
|
||||||
|
with (sysfs_path / 'device/model').open() as f:
|
||||||
|
model = '_'.join(f.read().strip().split())
|
||||||
|
with (sysfs_path / 'device/vendor').open() as f:
|
||||||
|
vendor = '_'.join(f.read().strip().split())
|
||||||
|
model = f'{vendor}_{model}'
|
||||||
|
return cls(device_node=path, model=model, offset=OFFSETS.get(model))
|
||||||
|
|
||||||
|
def get_disc(self) -> Disc:
|
||||||
|
disc = discid.read(str(self.device_node))
|
||||||
|
return Disc(disc_id=disc.id, toc_string=disc.toc_string)
|
|
@ -0,0 +1,249 @@
|
||||||
|
# Copyright 2016 FireMon. All rights reserved.
|
||||||
|
#
|
||||||
|
# This file is a part of the FireMon codebase. The contents of this
|
||||||
|
# file are confidential and cannot be distributed without prior
|
||||||
|
# written authorization.
|
||||||
|
#
|
||||||
|
# Warning: This computer program is protected by copyright law and
|
||||||
|
# international treaties. Unauthorized reproduction or distribution of
|
||||||
|
# this program, or any portion of it, may result in severe civil and
|
||||||
|
# criminal penalties, and will be prosecuted to the maximum extent
|
||||||
|
# possible under the law.
|
||||||
|
'''\
|
||||||
|
This module provides Python bindings for the Linux inotify system, which
|
||||||
|
is a means for receiving events about file access and modification.
|
||||||
|
|
||||||
|
All inotify operations are handled by the :py:class:`Inotify` class.
|
||||||
|
When an instance is created, the inotify system is initialized and an
|
||||||
|
inotify file descriptor is assigned.
|
||||||
|
|
||||||
|
To begin watching files, use the :py:meth:`Inotify.add_watch` method.
|
||||||
|
Messages can then be obtained by iterating over the results of the
|
||||||
|
:py:meth:`Inotify.read` method.
|
||||||
|
|
||||||
|
>>> inot = Inotify()
|
||||||
|
>>> inot.add_watch('/tmp', IN_CREATE | IN_MOVE)
|
||||||
|
>>> for event in inot.read():
|
||||||
|
... print(event)
|
||||||
|
|
||||||
|
The :py:meth:`Inotify.read` method will block until an event is
|
||||||
|
received. To avoid blocking, use an I/O multiplexing mechanism such as
|
||||||
|
:py:func:`~select.select` or :py:class:`~select.epoll`.
|
||||||
|
:py:class:`Inotify` instances can be passed directly to these
|
||||||
|
mechanisms, or the underlying file descriptor can be obtained by calling
|
||||||
|
the :py:meth:`Inotify.fileno` method.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import collections
|
||||||
|
import ctypes.util
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
_libc = ctypes.CDLL(ctypes.util.find_library('c'))
|
||||||
|
|
||||||
|
_errno = _libc.__errno_location
|
||||||
|
_errno.restype = ctypes.POINTER(ctypes.c_int)
|
||||||
|
|
||||||
|
_libc.inotify_add_watch.argtypes = (ctypes.c_int, ctypes.c_char_p,
|
||||||
|
ctypes.c_uint32)
|
||||||
|
_libc.inotify_rm_watch.argtypes = (ctypes.c_int, ctypes.c_int)
|
||||||
|
|
||||||
|
IN_NONBLOCK = 0x800
|
||||||
|
IN_CLOEXEC = 0x80000
|
||||||
|
|
||||||
|
#: File was accessed.
|
||||||
|
IN_ACCESS = 0x1
|
||||||
|
#: File was modified.
|
||||||
|
IN_MODIFY = 0x2
|
||||||
|
#: Metadata changed.
|
||||||
|
IN_ATTRIB = 0x4
|
||||||
|
#: Writtable file was closed.
|
||||||
|
IN_CLOSE_WRITE = 0x8
|
||||||
|
#: Unwrittable file closed.
|
||||||
|
IN_CLOSE_NOWRITE = 0x10
|
||||||
|
#: Close.
|
||||||
|
IN_CLOSE = IN_CLOSE_WRITE | IN_CLOSE_NOWRITE
|
||||||
|
#: File was opened.
|
||||||
|
IN_OPEN = 0x20
|
||||||
|
#: File was moved from X.
|
||||||
|
IN_MOVED_FROM = 0x40
|
||||||
|
#: File was moved to Y.
|
||||||
|
IN_MOVED_TO = 0x80
|
||||||
|
#: Moves.
|
||||||
|
IN_MOVE = IN_MOVED_FROM | IN_MOVED_TO
|
||||||
|
#: Subfile was created.
|
||||||
|
IN_CREATE = 0x100
|
||||||
|
#: Subfile was deleted.
|
||||||
|
IN_DELETE = 0x200
|
||||||
|
#: Self was deleted.
|
||||||
|
IN_DELETE_SELF = 0x400
|
||||||
|
#: Self was moved.
|
||||||
|
IN_MOVE_SELF = 0x800
|
||||||
|
|
||||||
|
#: Backing fs was unmounted.
|
||||||
|
IN_UNMOUNT = 0x2000
|
||||||
|
#: Event queued overflowed.
|
||||||
|
IN_Q_OVERFLOW = 0x4000
|
||||||
|
#: File was ignored.
|
||||||
|
IN_IGNORED = 0x8000
|
||||||
|
|
||||||
|
#: Only watch the path if it is a directory.
|
||||||
|
IN_ONLYDIR = 0x1000000
|
||||||
|
#: DO not follow a sym link.
|
||||||
|
IN_DONT_FOLLOW = 0x2000000
|
||||||
|
#: Exclude events on unlinked objects.
|
||||||
|
IN_EXCL_UNLINK = 0x4000000
|
||||||
|
#: Add the mask of an already existing watch.
|
||||||
|
IN_MASK_ADD = 0x20000000
|
||||||
|
#: Event occurred against dir.
|
||||||
|
IN_ISDIR = 0x40000000
|
||||||
|
#: Only send event once.
|
||||||
|
IN_ONESHOT = 0x80000000
|
||||||
|
|
||||||
|
#: All events which a program can wait on.
|
||||||
|
IN_ALL_EVENTS = (IN_ACCESS | IN_MODIFY | IN_ATTRIB | IN_CLOSE_WRITE |
|
||||||
|
IN_CLOSE_NOWRITE | IN_OPEN | IN_MOVED_FROM | IN_MOVED_TO |
|
||||||
|
IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF)
|
||||||
|
|
||||||
|
|
||||||
|
class InotifyError(OSError):
|
||||||
|
'''Raised when an error is returned by inotify'''
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_c_err(cls):
|
||||||
|
errno = _errno().contents.value
|
||||||
|
return cls(errno, os.strerror(errno))
|
||||||
|
|
||||||
|
|
||||||
|
class Inotify(object):
|
||||||
|
'''Wrapper class for Linux inotify capabilities'''
|
||||||
|
|
||||||
|
STRUCT_FMT = '@iIII'
|
||||||
|
STRUCT_SIZE = struct.calcsize(STRUCT_FMT)
|
||||||
|
BUFSIZE = STRUCT_SIZE + 256
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
fd = _libc.inotify_init()
|
||||||
|
if fd == -1:
|
||||||
|
raise InotifyError.from_c_err()
|
||||||
|
self.__fd = fd
|
||||||
|
self.__watches = {}
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, tb):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def fileno(self):
|
||||||
|
'''Return the underlying inotify file descriptor'''
|
||||||
|
|
||||||
|
return self.__fd
|
||||||
|
|
||||||
|
def add_watch(self, pathname, mask):
|
||||||
|
'''Add a new watch
|
||||||
|
|
||||||
|
:param pathname: The path to the file or directory to watch
|
||||||
|
:param mask: The events to watch, as a bit field
|
||||||
|
:returns: The new watch descriptor
|
||||||
|
:raises: :py:exc:`InotifyError`
|
||||||
|
'''
|
||||||
|
|
||||||
|
wd = _libc.inotify_add_watch(self.__fd, pathname, mask)
|
||||||
|
if wd == -1:
|
||||||
|
raise InotifyError.from_c_err()
|
||||||
|
self.__watches[wd] = pathname
|
||||||
|
return wd
|
||||||
|
|
||||||
|
def rm_watch(self, wd):
|
||||||
|
'''Remove an existing watch
|
||||||
|
|
||||||
|
:param wd: The watch descriptor to remove
|
||||||
|
:raises: :py:exc:`InotifyError`
|
||||||
|
'''
|
||||||
|
|
||||||
|
ret = _libc.inotify_rm_watch(self.__fd, wd)
|
||||||
|
if ret == -1:
|
||||||
|
raise InotifyError.from_c_err()
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
'''Iterate over received events
|
||||||
|
|
||||||
|
:returns: An iterator that yields :py:class:`Event` objects
|
||||||
|
|
||||||
|
This method returns an iterator for all of the events received
|
||||||
|
in a single batch. Iterating over the returned value will block
|
||||||
|
until an event is received
|
||||||
|
'''
|
||||||
|
|
||||||
|
buf = memoryview(os.read(self.__fd, self.BUFSIZE))
|
||||||
|
nread = len(buf)
|
||||||
|
pos = 0
|
||||||
|
while pos < nread:
|
||||||
|
packed = buf[pos:pos + self.STRUCT_SIZE]
|
||||||
|
pos += self.STRUCT_SIZE
|
||||||
|
wd, mask, cookie, sz = self._unpack(packed)
|
||||||
|
if sz:
|
||||||
|
name = buf[pos:pos + sz].tobytes()
|
||||||
|
name = name[:name.index(b'\x00')]
|
||||||
|
pos += sz
|
||||||
|
else:
|
||||||
|
name = None
|
||||||
|
pathname = self.__watches[wd]
|
||||||
|
yield Event(wd, mask, cookie, name, pathname)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
'''Close all watch descriptors and the inotify descriptor'''
|
||||||
|
|
||||||
|
for wd in self.__watches:
|
||||||
|
try:
|
||||||
|
self.rm_watch(wd)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
os.close(self.__fd)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _unpack(cls, buf):
|
||||||
|
return struct.unpack(cls.STRUCT_FMT, buf[:cls.STRUCT_SIZE])
|
||||||
|
|
||||||
|
|
||||||
|
Event = collections.namedtuple('Event', (
|
||||||
|
'wd',
|
||||||
|
'mask',
|
||||||
|
'cookie',
|
||||||
|
'name',
|
||||||
|
'pathname',
|
||||||
|
))
|
||||||
|
'''A tuple containing information about a single event
|
||||||
|
|
||||||
|
Each tuple contains the following items:
|
||||||
|
|
||||||
|
.. py:attribute:: wd
|
||||||
|
|
||||||
|
identifies the watch for which this event occurs. It is one of the
|
||||||
|
watch descriptors returned by a previous call to
|
||||||
|
:py:meth:`Inotify.add_watch`
|
||||||
|
|
||||||
|
.. py:attribute:: mask
|
||||||
|
|
||||||
|
contains bits that describe the event that occurred
|
||||||
|
|
||||||
|
.. py:attribute:: cookie
|
||||||
|
|
||||||
|
A unique integer that connects related events. Currently this is used
|
||||||
|
only for rename events, and allows the resulting pair of
|
||||||
|
:py:data:`IN_MOVED_FROM` and :py:data`IN_MOVED_TO` events to be
|
||||||
|
connected by the application. For all other event types, cookie is set
|
||||||
|
to ``0``.
|
||||||
|
|
||||||
|
.. py:attribute:: name
|
||||||
|
|
||||||
|
The ``name`` field is present only when an event is returned for a
|
||||||
|
file inside a watched directory; it identifies the filename within to
|
||||||
|
the watched directory.
|
||||||
|
|
||||||
|
.. py:attribute:: pathname
|
||||||
|
|
||||||
|
The path of the watched file or directory that emitted the event
|
||||||
|
'''
|
|
@ -0,0 +1,217 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, Iterable, List, Optional
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from rich.console import Console, ConsoleOptions, RenderResult
|
||||||
|
from rich.live import Live
|
||||||
|
from rich.logging import RichHandler
|
||||||
|
from rich.measure import Measurement
|
||||||
|
from rich.prompt import Prompt
|
||||||
|
from rich.spinner import Spinner
|
||||||
|
from rich.table import Table
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
from .disc import DiscDrive
|
||||||
|
from .musicbrainz import Release, get_release_by_id, get_releases_from_disc
|
||||||
|
from .ripper import Ripper, TrackStatus
|
||||||
|
|
||||||
|
|
||||||
|
RELEASE_INFO_TMPL = (
|
||||||
|
'[bright_white][b]{artist} - [i]{title}[/i][/b][/bright_white] '
|
||||||
|
'([magenta]{year}[/magenta]): [gray]{more_info}[/gray]'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Status:
|
||||||
|
def __init__(self, status: TrackStatus) -> None:
|
||||||
|
self.running = True
|
||||||
|
self.set_status(status)
|
||||||
|
|
||||||
|
def __rich_console__(
|
||||||
|
self,
|
||||||
|
console: Console,
|
||||||
|
options: ConsoleOptions, # pylint: disable=unused-argument
|
||||||
|
) -> RenderResult:
|
||||||
|
time = console.get_time()
|
||||||
|
if self.spinner.start_time is None:
|
||||||
|
self.spinner.start_time = time
|
||||||
|
yield self.render(time - self.spinner.start_time)
|
||||||
|
|
||||||
|
def __rich_measure__(
|
||||||
|
self, console: Console, max_width: int
|
||||||
|
) -> Measurement:
|
||||||
|
text = self.render(0)
|
||||||
|
return Measurement.get(console, text, max_width)
|
||||||
|
|
||||||
|
def set_status(self, status: TrackStatus) -> None:
|
||||||
|
if status is TrackStatus.done:
|
||||||
|
self.running = False
|
||||||
|
return
|
||||||
|
elif status is TrackStatus.encoding:
|
||||||
|
spinner = 'arrow'
|
||||||
|
else:
|
||||||
|
spinner = 'arc'
|
||||||
|
text = f'{status.value} ...'
|
||||||
|
self.spinner = Spinner(spinner, text, style='status.spinner')
|
||||||
|
|
||||||
|
def render(self, time: float) -> Text:
|
||||||
|
if self.running:
|
||||||
|
return self.spinner.render(time)
|
||||||
|
else:
|
||||||
|
return Text('Done!')
|
||||||
|
|
||||||
|
|
||||||
|
def format_release(release: Release) -> str:
|
||||||
|
more_info = [
|
||||||
|
'[bright_blue]{} disc(s)[/bright_blue]'.format(
|
||||||
|
len(release.medium_list)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
if release.packaging:
|
||||||
|
more_info.append(f'[bright_red]{release.packaging}[/bright_red]')
|
||||||
|
if release.country:
|
||||||
|
more_info.append(f'[bright_green]{release.country}[/bright_green]')
|
||||||
|
if release.label_info:
|
||||||
|
for label_info in release.label_info:
|
||||||
|
if label_info.catalog_number:
|
||||||
|
more_info.append(
|
||||||
|
f'[bright_cyan]{label_info.catalog_number}[/bright_cyan]'
|
||||||
|
)
|
||||||
|
break
|
||||||
|
return RELEASE_INFO_TMPL.format(
|
||||||
|
artist=release.artist_credit_phrase,
|
||||||
|
title=release.title,
|
||||||
|
year=release.date.year,
|
||||||
|
more_info=', '.join(more_info),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_menu(console: Console, choices: Iterable[Any]) -> int:
|
||||||
|
max_ = 0
|
||||||
|
for idx, choice in enumerate(choices):
|
||||||
|
console.print(f'[bright_yellow]{idx + 1})[/bright_yellow]: {choice}')
|
||||||
|
max_ += 1
|
||||||
|
while 1:
|
||||||
|
choice = Prompt.ask('Selection')
|
||||||
|
try:
|
||||||
|
i = int(choice)
|
||||||
|
except ValueError:
|
||||||
|
console.print(f'[red]Invalid input: {choice}[/red]')
|
||||||
|
continue
|
||||||
|
if i < 1 or i > max_:
|
||||||
|
console.print(f'[red]Invalid selection: {i}[/red]')
|
||||||
|
continue
|
||||||
|
return i - 1
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_release(console: Console, drive: DiscDrive) -> Release:
|
||||||
|
releases = get_releases_from_disc(drive.get_disc()).release_list
|
||||||
|
if not releases:
|
||||||
|
console.print('[red]Could not find a matching MusicBrainz release')
|
||||||
|
raise SystemExit(1)
|
||||||
|
if len(releases) == 1:
|
||||||
|
return releases[0]
|
||||||
|
console.print(
|
||||||
|
'Multiple matching releases found. '
|
||||||
|
'Please select the correct release'
|
||||||
|
)
|
||||||
|
choice = prompt_menu(console, (format_release(r) for r in releases))
|
||||||
|
return releases[choice]
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_select_disc(console: Console, num_discs: int) -> int:
|
||||||
|
console.print(
|
||||||
|
'Found part of a multi-disc album. Please select the disc number'
|
||||||
|
)
|
||||||
|
return prompt_menu(console, (f'Disc {x}' for x in range(1, num_discs + 1)))
|
||||||
|
|
||||||
|
|
||||||
|
def run(
|
||||||
|
tracks: Optional[List[str]] = typer.Option(
|
||||||
|
None,
|
||||||
|
'-t',
|
||||||
|
'--tracks',
|
||||||
|
metavar='TRACKS',
|
||||||
|
help='Select tracks/track sequences (e.g. 1, 1-, 5-9)',
|
||||||
|
),
|
||||||
|
use_libcdio: bool = typer.Option(
|
||||||
|
False, help='Use cd-paranoia from libcdio instead of cdparanoia'
|
||||||
|
),
|
||||||
|
device: Optional[Path] = typer.Option(
|
||||||
|
None,
|
||||||
|
'--device',
|
||||||
|
'-d',
|
||||||
|
metavar='PATH',
|
||||||
|
help='Path to the CD-ROM device',
|
||||||
|
),
|
||||||
|
mbid: Optional[str] = typer.Option(None, help='MusicBrainz release ID'),
|
||||||
|
verbose: int = typer.Option(
|
||||||
|
0,
|
||||||
|
'--verbose',
|
||||||
|
'-v',
|
||||||
|
count=True,
|
||||||
|
help='Increase log level (can be repated)',
|
||||||
|
),
|
||||||
|
):
|
||||||
|
console = Console(highlight=False)
|
||||||
|
if verbose < 1:
|
||||||
|
level = logging.WARNING
|
||||||
|
elif verbose < 2:
|
||||||
|
level = logging.INFO
|
||||||
|
else:
|
||||||
|
level = logging.DEBUG
|
||||||
|
logging.basicConfig(
|
||||||
|
level=level,
|
||||||
|
format='%(threadName)s [%(name)s] %(message)s',
|
||||||
|
datefmt='[%X]',
|
||||||
|
handlers=[RichHandler(console=console, show_path=False)],
|
||||||
|
)
|
||||||
|
logging.getLogger('musicbrainzngs').setLevel(logging.ERROR)
|
||||||
|
try:
|
||||||
|
dev = DiscDrive.from_path(device)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
console.print(f'[bold red]File not found: {e}')
|
||||||
|
raise SystemExit(os.EX_OSFILE)
|
||||||
|
|
||||||
|
if mbid is not None:
|
||||||
|
release = get_release_by_id(mbid)
|
||||||
|
else:
|
||||||
|
release = prompt_release(console, dev)
|
||||||
|
console.print(f'Ripping {format_release(release)}')
|
||||||
|
|
||||||
|
num_discs = len(release.medium_list)
|
||||||
|
if len(release.medium_list) > 1:
|
||||||
|
discno = prompt_select_disc(console, num_discs)
|
||||||
|
else:
|
||||||
|
discno = 0
|
||||||
|
|
||||||
|
table = Table()
|
||||||
|
table.add_column("Track", justify='right')
|
||||||
|
table.add_column("Status", min_width=14)
|
||||||
|
|
||||||
|
with Live(table, console=console, refresh_per_second=12):
|
||||||
|
ripper = Ripper(dev, release, discno, tracks or None, use_libcdio)
|
||||||
|
trackdict: Dict[int, Status] = {}
|
||||||
|
for track, status in ripper.rip():
|
||||||
|
if track is None:
|
||||||
|
assert not isinstance(status, TrackStatus)
|
||||||
|
console.print(
|
||||||
|
status[0], style='bold red' if status[1] else None
|
||||||
|
)
|
||||||
|
elif track in trackdict:
|
||||||
|
assert isinstance(status, TrackStatus)
|
||||||
|
trackdict[track].set_status(status)
|
||||||
|
else:
|
||||||
|
assert isinstance(status, TrackStatus)
|
||||||
|
trackdict[track] = s = Status(status)
|
||||||
|
table.add_row(str(track), s)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
typer.run(run)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
|
@ -0,0 +1,77 @@
|
||||||
|
import datetime
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import musicbrainzngs
|
||||||
|
import pydantic
|
||||||
|
|
||||||
|
from .disc import Disc
|
||||||
|
|
||||||
|
|
||||||
|
musicbrainzngs.set_useragent('DCPlayer', '0.0.1', 'https://dcplayer.audio/')
|
||||||
|
|
||||||
|
|
||||||
|
class Artist(pydantic.BaseModel):
|
||||||
|
id: str
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class ArtistCredit(pydantic.BaseModel):
|
||||||
|
artist: Artist
|
||||||
|
|
||||||
|
|
||||||
|
class Recording(pydantic.BaseModel):
|
||||||
|
id: str
|
||||||
|
title: str
|
||||||
|
|
||||||
|
|
||||||
|
class Track(pydantic.BaseModel):
|
||||||
|
id: str
|
||||||
|
recording: Recording
|
||||||
|
|
||||||
|
|
||||||
|
class Medium(pydantic.BaseModel):
|
||||||
|
track_list: List[Track] = pydantic.Field(alias='track-list')
|
||||||
|
|
||||||
|
|
||||||
|
class LabelInfo(pydantic.BaseModel):
|
||||||
|
catalog_number: Optional[str] = pydantic.Field(
|
||||||
|
None, alias='catalog-number'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Release(pydantic.BaseModel):
|
||||||
|
id: str
|
||||||
|
title: str
|
||||||
|
artist_credit: List[ArtistCredit] = pydantic.Field(alias='artist-credit')
|
||||||
|
artist_credit_phrase: str = pydantic.Field(alias='artist-credit-phrase')
|
||||||
|
medium_list: List[Medium] = pydantic.Field(alias='medium-list')
|
||||||
|
date: datetime.date
|
||||||
|
packaging: Optional[str] = None
|
||||||
|
country: Optional[str] = None
|
||||||
|
label_info: Optional[List[LabelInfo]] = pydantic.Field(
|
||||||
|
None, alias='label-info-list'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseResponse(pydantic.BaseModel):
|
||||||
|
release_list: List[Release] = pydantic.Field(alias='release-list')
|
||||||
|
release_count: int = pydantic.Field(alias='release-count')
|
||||||
|
|
||||||
|
|
||||||
|
def get_releases_from_disc(disc: Disc) -> ReleaseResponse:
|
||||||
|
res = musicbrainzngs.get_releases_by_discid(
|
||||||
|
disc.disc_id,
|
||||||
|
toc=disc.toc_string,
|
||||||
|
includes=['artists', 'recordings', 'labels'],
|
||||||
|
)
|
||||||
|
if 'disc' in res:
|
||||||
|
return ReleaseResponse.parse_obj(res['disc'])
|
||||||
|
else:
|
||||||
|
return ReleaseResponse.parse_obj(res)
|
||||||
|
|
||||||
|
|
||||||
|
def get_release_by_id(mbid: str) -> Release:
|
||||||
|
res = musicbrainzngs.get_release_by_id(
|
||||||
|
mbid, includes=['artists', 'recordings', 'labels']
|
||||||
|
)
|
||||||
|
return Release.parse_obj(res['release'])
|
|
@ -0,0 +1,343 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import enum
|
||||||
|
import glob
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import queue
|
||||||
|
import select
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from types import TracebackType
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Callable,
|
||||||
|
Iterable,
|
||||||
|
Optional,
|
||||||
|
Tuple,
|
||||||
|
Type,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
|
import mutagen
|
||||||
|
|
||||||
|
from . import inotify
|
||||||
|
from .disc import DiscDrive
|
||||||
|
from .musicbrainz import Release
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TrackList = Iterable[Union[str, int]]
|
||||||
|
|
||||||
|
|
||||||
|
class TrackStatus(enum.Enum):
|
||||||
|
ripping = 'Ripping'
|
||||||
|
encoding = 'Encoding'
|
||||||
|
done = 'Done'
|
||||||
|
|
||||||
|
|
||||||
|
StatusCallback = Callable[[str, TrackStatus], None]
|
||||||
|
CompleteCallback = Callable[[str, bool], None]
|
||||||
|
StatusMessage = Tuple[Optional[int], Union[Tuple[str, bool], TrackStatus]]
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
ProcessQueue = queue.Queue[ # pylint: disable=unsubscriptable-object
|
||||||
|
Optional[str]
|
||||||
|
]
|
||||||
|
StatusQueue = queue.Queue[ # pylint: disable=unsubscriptable-object
|
||||||
|
StatusMessage
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
ProcessQueue = queue.Queue
|
||||||
|
StatusQueue = queue.Queue
|
||||||
|
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
FILENAME_SAFE_MAP = {
|
||||||
|
'’': "'",
|
||||||
|
':': ' - ',
|
||||||
|
'/': '-',
|
||||||
|
}
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
|
class RipThread(threading.Thread):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
device: DiscDrive,
|
||||||
|
tracks: Optional[TrackList] = None,
|
||||||
|
use_libcdio: bool = False,
|
||||||
|
):
|
||||||
|
super().__init__(name='RipThread')
|
||||||
|
if not tracks:
|
||||||
|
tracks = ('1-',)
|
||||||
|
self.tracks = tracks
|
||||||
|
self.device = device
|
||||||
|
self.use_libcdio = bool(use_libcdio)
|
||||||
|
self.on_complete: Optional[CompleteCallback] = None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
log.info('Starting rip from device %s', self.device.device_node)
|
||||||
|
log.debug(
|
||||||
|
'Using offset %d for %s', self.device.offset, self.device.model
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.use_libcdio:
|
||||||
|
cmd = ['cd-paranoia']
|
||||||
|
else:
|
||||||
|
cmd = ['cdparanoia']
|
||||||
|
cmd += (
|
||||||
|
'--batch',
|
||||||
|
'--quiet',
|
||||||
|
'--force-read-speed',
|
||||||
|
'1',
|
||||||
|
'--output-wav',
|
||||||
|
'--sample-offset',
|
||||||
|
str(self.device.offset),
|
||||||
|
'--force-cdrom-device',
|
||||||
|
str(self.device.device_node),
|
||||||
|
'--verbose',
|
||||||
|
)
|
||||||
|
cmd += (str(t) for t in self.tracks)
|
||||||
|
log.debug('Running command: %s', cmd)
|
||||||
|
with open('rupert-rip.out', 'wb') as f:
|
||||||
|
p = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
stdout=f,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
self._complete(
|
||||||
|
f'{cmd[0]} exited with status {p.returncode}', p.returncode != 0
|
||||||
|
)
|
||||||
|
|
||||||
|
def _complete(self, message: str, is_err: bool) -> None:
|
||||||
|
if self.on_complete is not None:
|
||||||
|
self.on_complete(message, is_err)
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessThread(threading.Thread):
|
||||||
|
|
||||||
|
encoding = sys.getfilesystemencoding()
|
||||||
|
|
||||||
|
def __init__(self, q: ProcessQueue) -> None:
|
||||||
|
super().__init__(name='ProcessThread')
|
||||||
|
self.q = q
|
||||||
|
self.quitpipe = None
|
||||||
|
self.on_status: Optional[StatusCallback] = None
|
||||||
|
|
||||||
|
def handle_event(self, evt):
|
||||||
|
filename = evt.name.decode(self.encoding)
|
||||||
|
if filename.endswith('.wav'):
|
||||||
|
if evt.mask & inotify.IN_CREATE:
|
||||||
|
log.debug('Started ripping %s', filename)
|
||||||
|
self._status(filename, TrackStatus.ripping)
|
||||||
|
if evt.mask & inotify.IN_CLOSE_WRITE:
|
||||||
|
log.debug('Finished ripping %s', filename)
|
||||||
|
self._status(filename, TrackStatus.encoding)
|
||||||
|
self.q.put(filename)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.quitpipe = os.pipe()
|
||||||
|
try:
|
||||||
|
with inotify.Inotify() as inot:
|
||||||
|
inot.add_watch(
|
||||||
|
b'.', inotify.IN_CLOSE_WRITE | inotify.IN_CREATE
|
||||||
|
)
|
||||||
|
while True:
|
||||||
|
ready = select.select((self.quitpipe[0], inot), (), ())[0]
|
||||||
|
if inot in ready:
|
||||||
|
for evt in inot.read():
|
||||||
|
if not evt.name:
|
||||||
|
continue
|
||||||
|
self.handle_event(evt)
|
||||||
|
if self.quitpipe[0] in ready:
|
||||||
|
log.debug('Shutting down')
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
os.close(self.quitpipe[0])
|
||||||
|
self.q.put(None)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
log.debug('Stopping process thread')
|
||||||
|
if self.quitpipe is not None:
|
||||||
|
os.close(self.quitpipe[1])
|
||||||
|
|
||||||
|
def _status(self, filename: str, status: TrackStatus) -> None:
|
||||||
|
if self.on_status is not None:
|
||||||
|
self.on_status(filename, status)
|
||||||
|
|
||||||
|
|
||||||
|
class EncodeThread(threading.Thread):
|
||||||
|
def __init__(self, release: Release, discno: int, q: ProcessQueue) -> None:
|
||||||
|
super().__init__(name='EncodeThread')
|
||||||
|
self.release = release
|
||||||
|
self.discno = discno
|
||||||
|
self.q = q
|
||||||
|
self.on_status: Optional[StatusCallback] = None
|
||||||
|
self.on_complete: Optional[CompleteCallback] = None
|
||||||
|
|
||||||
|
def encode(self, filename: str) -> None:
|
||||||
|
log.info('Encoding %s to flac', filename)
|
||||||
|
cmd = ['flac', '--silent', filename]
|
||||||
|
log.debug('Running command %s', cmd)
|
||||||
|
p = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
assert p.stdout
|
||||||
|
codec = sys.getfilesystemencoding()
|
||||||
|
while 1:
|
||||||
|
d = p.stdout.readline().decode(codec, 'replace')
|
||||||
|
if not d:
|
||||||
|
break
|
||||||
|
log.debug(d.rstrip())
|
||||||
|
p.wait()
|
||||||
|
log.info('flac exited with status %d', p.returncode)
|
||||||
|
|
||||||
|
def tag(self, filename: str) -> None:
|
||||||
|
basename = os.path.splitext(filename)[0]
|
||||||
|
filename = basename + '.flac'
|
||||||
|
|
||||||
|
log.info('Adding tags to %s', filename)
|
||||||
|
trackno = int(filename[5:7])
|
||||||
|
artist = self.release.artist_credit[0].artist
|
||||||
|
album = self.release.title
|
||||||
|
medium = self.release.medium_list[self.discno]
|
||||||
|
track = medium.track_list[trackno - 1]
|
||||||
|
tags = mutagen.File(filename, easy=True)
|
||||||
|
tags['tracknumber'] = str(trackno)
|
||||||
|
tags['artist'] = tags['albumartist'] = artist.name
|
||||||
|
tags['album'] = album
|
||||||
|
tags['title'] = track.recording.title
|
||||||
|
tags['date'] = str(self.release.date.year)
|
||||||
|
if len(self.release.medium_list) > 1:
|
||||||
|
tags['discnumber'] = str(self.discno + 1)
|
||||||
|
tags['musicbrainz_albumid'] = self.release.id
|
||||||
|
tags['musicbrainz_artistid'] = artist.id
|
||||||
|
tags['musicbrainz_releasetrackid'] = track.id
|
||||||
|
tags.save()
|
||||||
|
|
||||||
|
newname = '{track:02} {artist} - {title}.flac'.format(
|
||||||
|
track=trackno, artist=artist.name, title=track.recording.title
|
||||||
|
)
|
||||||
|
log.info('Renaming "%s" to "%s"', filename, newname)
|
||||||
|
os.rename(filename, safe_name(newname))
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
while 1:
|
||||||
|
filename = self.q.get()
|
||||||
|
if filename is None:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
self.encode(filename)
|
||||||
|
self.tag(filename)
|
||||||
|
except Exception as e: # pylint: disable=broad-except
|
||||||
|
log.error('Error encoding/tagging %s: %s', filename, e)
|
||||||
|
finally:
|
||||||
|
os.unlink(filename)
|
||||||
|
self._status(filename, TrackStatus.done)
|
||||||
|
self._complete()
|
||||||
|
|
||||||
|
def _complete(self) -> None:
|
||||||
|
if self.on_complete is not None:
|
||||||
|
self.on_complete('Finished encoding files', False)
|
||||||
|
|
||||||
|
def _status(self, filename: str, status: TrackStatus) -> None:
|
||||||
|
if self.on_status is not None:
|
||||||
|
self.on_status(filename, status)
|
||||||
|
|
||||||
|
|
||||||
|
class Ripper:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
device: DiscDrive,
|
||||||
|
release: Release,
|
||||||
|
discno: int,
|
||||||
|
tracks: Optional[TrackList] = None,
|
||||||
|
use_libcdio: bool = False,
|
||||||
|
) -> None:
|
||||||
|
self.device = device
|
||||||
|
self.release = release
|
||||||
|
self.discno = discno
|
||||||
|
self.tracks = tracks
|
||||||
|
self.use_libcdio = use_libcdio
|
||||||
|
self._status_queue: StatusQueue = queue.Queue()
|
||||||
|
q: ProcessQueue = queue.Queue()
|
||||||
|
self._rip_thread = RipThread(
|
||||||
|
self.device, self.tracks, self.use_libcdio
|
||||||
|
)
|
||||||
|
self._rip_thread.on_complete = self.on_complete
|
||||||
|
self._process_thread = ProcessThread(q)
|
||||||
|
self._process_thread.on_status = self.on_status
|
||||||
|
self._encode_thread = EncodeThread(self.release, self.discno, q)
|
||||||
|
self._encode_thread.on_status = self.on_status
|
||||||
|
self._encode_thread.on_complete = self.on_complete
|
||||||
|
|
||||||
|
def __enter__(self,) -> Ripper:
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(
|
||||||
|
self,
|
||||||
|
exc_type: Type[Exception],
|
||||||
|
exc_value: Exception,
|
||||||
|
tb: TracebackType,
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
def rip(self) -> Iterable[StatusMessage]:
|
||||||
|
start = time.monotonic()
|
||||||
|
dirname = safe_name(
|
||||||
|
f'{self.release.artist_credit_phrase} - {self.release.title}'
|
||||||
|
)
|
||||||
|
if not os.path.isdir(dirname):
|
||||||
|
log.info('Creating directory: %s', dirname)
|
||||||
|
os.mkdir(dirname)
|
||||||
|
os.chdir(dirname)
|
||||||
|
if len(self.release.medium_list) > 1:
|
||||||
|
subdirname = f'Disc {self.discno + 1}'
|
||||||
|
if not os.path.isdir(subdirname):
|
||||||
|
log.info('Creating directory: %s', subdirname)
|
||||||
|
os.mkdir(subdirname)
|
||||||
|
os.chdir(subdirname)
|
||||||
|
|
||||||
|
for filename in glob.glob('track*.cdda.wav'):
|
||||||
|
os.unlink(filename)
|
||||||
|
|
||||||
|
self._process_thread.start()
|
||||||
|
self._encode_thread.start()
|
||||||
|
self._rip_thread.start()
|
||||||
|
while 1:
|
||||||
|
track, status = self._status_queue.get()
|
||||||
|
yield track, status
|
||||||
|
if track is None:
|
||||||
|
break
|
||||||
|
self._process_thread.stop()
|
||||||
|
while 1:
|
||||||
|
track, status = self._status_queue.get()
|
||||||
|
yield track, status
|
||||||
|
if track is None:
|
||||||
|
break
|
||||||
|
end = time.monotonic()
|
||||||
|
log.info('Ripping/encoding took %d seconds', end - start)
|
||||||
|
|
||||||
|
def on_status(self, filename: str, status: TrackStatus) -> None:
|
||||||
|
log.debug('Filename: %s, Status: %s', filename, status.name)
|
||||||
|
if filename.startswith('track') and filename.endswith('.cdda.wav'):
|
||||||
|
track = int(filename[5:-9])
|
||||||
|
self._status_queue.put((track, status))
|
||||||
|
|
||||||
|
def on_complete(self, message: str, is_err: bool) -> None:
|
||||||
|
self._status_queue.put((None, (message, is_err)))
|
||||||
|
|
||||||
|
|
||||||
|
def safe_name(name: str) -> str:
|
||||||
|
for k, v in FILENAME_SAFE_MAP.items():
|
||||||
|
name = name.replace(k, v)
|
||||||
|
return name
|
Loading…
Reference in New Issue