pax_global_header00006660000000000000000000000064143201601150014503gustar00rootroot0000000000000052 comment=6ae155405770afbc1446432e71782d761218baa4 eyeD3-0.9.7/000077500000000000000000000000001432016011500124715ustar00rootroot00000000000000eyeD3-0.9.7/.cookiecutter.yml000066400000000000000000000023171432016011500157750ustar00rootroot00000000000000 default_context: add_docs: "yes" bitbucket_repo: "eyed3" bitbucket_url: "https://bitbucket.org/bitbucket-user/eyed3" bitbucket_username: "bitbucket-user" default_locale: "en_US" email: "travis@pobox.com" full_name: "Travis Shirk" gettext_domain: "None" github_repo: "eyeD3" github_url: "https://github.com/nicfit/eyeD3" github_username: "nicfit" intended_audience: "Developers" license: "GNU GPL v3.0" project_long_description: "" project_name: "eyeD3" project_short_description: "Python audio data toolkit (ID3 and MP3)" project_slug: "eyed3" py26: "no" py27: "no" py33: "no" py34: "no" py35: "no" py36: "yes" py37: "yes" py38: "yes" py39: "yes" py_module: "eyed3" pyapp_type: "normal" pypi_repo_name: "eyeD3" pypi_username: "nicfit" pypy: "no" pypy3: "no" release_date: "today" requirements_yaml: "yes" src_dir: "." use_bitbucket: "no" use_github: "yes" use_make: "yes" use_paver: "no" use_pypi_deployment_with_travis: "no" use_pytest: "yes" use_rtd: "no" use_travis_ci: "yes" version: "0.8.8" web: "http://eyed3.nicfit.net/" year: "2002-2019" eyeD3-0.9.7/.editorconfig000066400000000000000000000004441432016011500151500ustar00rootroot00000000000000# http://editorconfig.org root = true [*] indent_style = space indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true charset = utf-8 end_of_line = lf [*.bat] indent_style = tab end_of_line = crlf [LICENSE] insert_final_newline = false [Makefile] indent_style = tab eyeD3-0.9.7/.gitchangelog.rc000066400000000000000000000145211432016011500155330ustar00rootroot00000000000000## ## Format ## ## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...] ## ## Description ## ## ACTION is one of 'chg', 'fix', 'new' ## ## Is WHAT the change is about. ## ## 'chg' is for refactor, small improvement, cosmetic changes... ## 'fix' is for bug fixes ## 'new' is for new features, big improvement ## ## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc' ## ## Is WHO is concerned by the change. ## ## 'dev' is for developpers (API changes, refactors...) ## 'usr' is for final users (UI changes) ## 'pkg' is for packagers (packaging changes) ## 'test' is for testers (test only related changes) ## 'doc' is for doc guys (doc only changes) ## ## COMMIT_MSG is ... well ... the commit message itself. ## ## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic' ## ## They are preceded with a '!' or a '@' (prefer the former, as the ## latter is wrongly interpreted in github.) Commonly used tags are: ## ## 'refactor' is obviously for refactoring code only ## 'minor' is for a very meaningless change (a typo, adding a comment) ## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...) ## 'wip' is for partial functionality but complete subfunctionality. ## ## Example: ## ## new: usr: support of bazaar implemented ## chg: re-indentend some lines !cosmetic ## new: dev: updated code to be compatible with last version of killer lib. ## fix: pkg: updated year of licence coverage. ## new: test: added a bunch of test around user usability of feature X. ## fix: typo in spelling my name in comment. !minor ## ## Please note that multi-line commit message are supported, and only the ## first line will be considered as the "summary" of the commit message. So ## tags, and other rules only applies to the summary. The body of the commit ## message will be displayed in the changelog without reformatting. ## ## ``ignore_regexps`` is a line of regexps ## ## Any commit having its full commit message matching any regexp listed here ## will be ignored and won't be reported in the changelog. ## ignore_regexps = [ r'!minor', r'!cosmetic', r'!refactor', r'!wip', r'!prep', r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:', r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:', r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$', ] ## ``section_regexps`` is a list of 2-tuples associating a string label and a ## list of regexp ## ## Commit messages will be classified in sections thanks to this. Section ## titles are the label, and a commit is classified under this section if any ## of the regexps associated is matching. ## ## Please note that ``section_regexps`` will only classify commits and won't ## make any changes to the contents. So you'll probably want to go check ## ``subject_process`` (or ``body_process``) to do some changes to the subject, ## whenever you are tweaking this variable. ## section_regexps = [ ('New', [ r'^[nN]ew\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', ]), ('Changes', [ r'^[cC]hg\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', ]), ('Fix', [ r'^[fF]ix\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$', ]), ('Other', None ## Match all lines ), ] ## ``body_process`` is a callable ## ## This callable will be given the original body and result will ## be used in the changelog. ## ## Available constructs are: ## ## - any python callable that take one txt argument and return txt argument. ## ## - ReSub(pattern, replacement): will apply regexp substitution. ## ## - Indent(chars=" "): will indent the text with the prefix ## Please remember that template engines gets also to modify the text and ## will usually indent themselves the text if needed. ## ## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns ## ## - noop: do nothing ## ## - ucfirst: ensure the first letter is uppercase. ## (usually used in the ``subject_process`` pipeline) ## ## - final_dot: ensure text finishes with a dot ## (usually used in the ``subject_process`` pipeline) ## ## - strip: remove any spaces before or after the content of the string ## ## Additionally, you can `pipe` the provided filters, for instance: #body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ") #body_process = Wrap(regexp=r'\n(?=\w+\s*:)') #body_process = noop body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip ## ``subject_process`` is a callable ## ## This callable will be given the original subject and result will ## be used in the changelog. ## ## Available constructs are those listed in ``body_process`` doc. subject_process = (strip | ReSub(r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n@]*)(@[a-z]+\s+)*$', r'\4') | ucfirst | final_dot) ## ``tag_filter_regexp`` is a regexp ## ## Tags that will be used for the changelog must match this regexp. ## tag_filter_regexp = r'^v[0-9]+\.[0-9]+(\.[0-9]+)?$' ## ``unreleased_version_label`` is a string ## ## This label will be used as the changelog Title of the last set of changes ## between last valid tag and HEAD if any. unreleased_version_label = "%%version%% (unreleased)" ## ``output_engine`` is a callable ## ## This will change the output format of the generated changelog file ## ## Available choices are: ## ## - rest_py ## ## Legacy pure python engine, outputs ReSTructured text. ## This is the default. ## ## - mustache() ## ## Template name could be any of the available templates in ## ``templates/mustache/*.tpl``. ## Requires python package ``pystache``. ## Examples: ## - mustache("markdown") ## - mustache("restructuredtext") ## ## - makotemplate() ## ## Template name could be any of the available templates in ## ``templates/mako/*.tpl``. ## Requires python package ``mako``. ## Examples: ## - makotemplate("restructuredtext") ## output_engine = rest_py #output_engine = mustache("restructuredtext") #output_engine = mustache("markdown") #output_engine = makotemplate("restructuredtext") ## ``include_merge`` is a boolean ## ## This option tells git-log whether to include merge commits in the log. ## The default is to include them. include_merge = True eyeD3-0.9.7/.github/000077500000000000000000000000001432016011500140315ustar00rootroot00000000000000eyeD3-0.9.7/.github/workflows/000077500000000000000000000000001432016011500160665ustar00rootroot00000000000000eyeD3-0.9.7/.github/workflows/codeql-analysis.yml000066400000000000000000000046161432016011500217100ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ 0.9.x ] pull_request: # The branches below must be a subset of the branches above branches: [ 0.9.x ] schedule: - cron: '35 18 * * 5' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'python' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] # Learn more: # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed steps: - name: Checkout repository uses: actions/checkout@v2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v1 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 eyeD3-0.9.7/.github/workflows/main.yml000066400000000000000000000024561432016011500175440ustar00rootroot00000000000000name: Main eyeD3 workflow on: push: branches: - master - 0.9.x pull_request: branches: - master - 0.9.x jobs: build: runs-on: ubuntu-latest strategy: matrix: os: [Ubuntu, MacOS, Windows] python-version: ["3.10", "3.9", "3.8"] steps: - uses: actions/checkout@v2 # Python - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} # Test data - name: Test data cache id: cache-test-data uses: actions/cache@v2 with: path: ~/eyeD3-test-data key: ${{ runner.os }}-test-data - name: Install test data # If the directory exists it is not re-downloaded, but symlinks are # restored, and that's needed after a cache is restored. run: make TEST_DATA_DIR=~ test-data - name: Install dependencies run: | python -m pip install --upgrade pip pip install .[test] .[display-plugin] .[yaml-plugin] # Run tests - name: Tests run: | make lint make coverage make check-manifest # XXX: What to do with coverage? Was using coverall, meh. Github pages? # XXX: Removed pypy3 because of some coverage module not found error... eyeD3-0.9.7/.gitignore000066400000000000000000000021511432016011500144600ustar00rootroot00000000000000test*.id3 tests/data tests/eyeD3-test-data* .idea/ .cookiecutter.md5 _misc/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ tags var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml htmlcov *,cover .hypothesis/ # Translations *.mo # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject eyeD3-0.9.7/AUTHORS.rst000066400000000000000000000025441432016011500143550ustar00rootroot00000000000000Authors ------- eyeD3 is written and maintained by: * Travis Shirk and has been contributed to by (ordered by date of first contribution): * Ryan Finnie * Henning Kiel * Knight Walker * Todd Zullinger * Aaron VonderHaar * Alexander Thomas * Michael Schout * Renaud Saint-Gratien * David Grant * Gergan Penkov * Stephen Fairchild * Ville Skyttä * Ben Isaacs * Neil Schemenauer * Otávio Pontes * Nathaniel Clark * Hans Meine * Hans Petter Jansson * Sebastian Patschorke * Bouke Versteegh * gaetano-guerriero * mafro * Gabriel Diego Teixeira * Chris Newton * Thomas Klausner * Tim Gates * chamatht * grun * guiweber * zhu eyeD3-0.9.7/CONTRIBUTING.rst000066400000000000000000000064041432016011500151360ustar00rootroot00000000000000============ Contributing ============ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. You can contribute in many ways: Types of Contributions ---------------------- Report Bugs ~~~~~~~~~~~ Report bugs at https://github.com/nicfit/eyeD3/issues. If you are reporting a bug, please include: * Your operating system name and version. * Any details about your local setup that might be helpful in troubleshooting. * Detailed steps to reproduce the bug. Fix Bugs ~~~~~~~~ Look through the GitHub issues for bugs. Anything tagged with "bug" is open to whoever wants to implement it. Implement Features ~~~~~~~~~~~~~~~~~~ Look through the GitHub issues for features. Anything tagged with "feature" is open to whoever wants to implement it. Write Documentation ~~~~~~~~~~~~~~~~~~~ eyeD3 could always use more documentation, whether as part of the official eyeD3 docs, in docstrings, or even on the web in blog posts, articles, and such. Submit Feedback ~~~~~~~~~~~~~~~ The best way to send feedback is to file an issue at https://github.com/nicfit/eyeD3/issues. If you are proposing a feature: * Explain in detail how it would work. * Keep the scope as narrow as possible, to make it easier to implement. * Remember that this is a volunteer-driven project, and that contributions are welcome :) Get Started! ------------ Ready to contribute? Here's how to set up eyeD3 for local development. 1. Fork the `eyeD3` repo on GitHub. 2. Clone your fork locally:: $ git clone git@github.com:your_name_here/eyeD3.git 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: $ mkvirtualenv eyed3 $ cd eyed3/ $ python setup.py develop 4. Install all dependencies for development and extra packages. For full test coverage you'll also need some test data.:: $ for reqs in $(ls requirements/*.txt); do pip install -U -r $reqs ; done $ make test-data 5. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature Now you can make your changes locally. 6. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox: .. code-block:: bash $ make lint $ make test $ make test-all # Optional, requires multiple versions of Python To get flake8 and tox, just pip install them into your virtualenv. 7. Commit your changes and push your branch to GitHub.:: $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature 8. Submit a pull request through the GitHub website. Pull Request Guidelines ----------------------- Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. 3. The pull request should work for Python 2.7, and 3.3, 3.4, 3.5, and for PyPy. Check https://travis-ci.org/nicfit/eyeD3/pulls and make sure that the tests pass for all supported Python versions. eyeD3-0.9.7/HISTORY.rst000066400000000000000000000777201432016011500144010ustar00rootroot00000000000000Release History =============== .. :changelog: v0.9.7 (2022-10-07) : Sunshine ---------------------------------- New ~~~ - [fixup-plugin] Added --no-dir-rename. - Add remove-all-unknown to classic plugin. - Track/disc number support for JSON/YAML plugins (and tests) - CountAndTotalTuple named tuple for count/total values. - EyeD3 __all__ includes core.AudioFile. - AudioFile.__str__ returns path. - Strict render Frame flag to continue on render errors. Applied to RelVolAdjFrameV24, etc - Track unsupported frames and add `--remove-all-unknown` classic plugin option (#586) Changes ~~~~~~~ - Removed display-plugin due to Grako EOL (#585) - [stats] Print bytes as text and filter private members. - Remove Python 3.6 support, EOL. (#560) Fix ~~~ - Doc grammar fixed (#537) <74979584+piatrashkakanstantinass@users.noreply.github.com> - Fix problem when frame file format invalid, and input.read() returns no more data at all - this had the potential to loop endlessly. (#539) Other ~~~~~ - Create codeql-analysis.yml. - Documentation updates. - Also updated deps, and removed Python 3.7 from official testing. - Docs: Fix a few typos (#573) - Spelling fixes (#559) - Typo fixes (#558) v0.9.6 (2020-12-28) : True Blue ---------------------------------- New ~~~ - Id3.Tag(version=) keyword argument. - Expose TextFrame ctor kwargs to Apple frames. fixes #407 - Added --about CLI argument for extra version/program info. Fix ~~~ - Preserve linked file info in Tag.clear(). fixes #442 - Handle v1 .id3/.tag files. - Improved `art` plugin behavior when missing dependencies. - [art plugin] Improved error for missing dependencies. - TYER conversion (and restored non v2.2 breakage, for now) - ID3 v2.2, date getters return values again. - Passed filtered files list or handleDirectory, and skip non-existant symlinks - Fixed installation supported Python text. fixes #405 - Implement v1.0/v1.1 tag conversion rules. Other ~~~~~ - Poetry build system (#500) v0.9.5 (2020-03-28) : I Knew Her, She Knew Me ---------------------------------------------- Fix ~~~ - `eyeD3 --genre ""` to clear genre frame restored. - Genre id->name mapping for non-standard genres and custom maps. v0.9.4 (2020-03-21) : The Devil Made Me Do It ----------------------------------------------- New ~~~ - Relative volume adjustments (RVA2 and RVAD) (#399) - Tag properties copyright and encoded_by - Support GRP1 (Apple) frames. Changes ~~~~~~~ - Genre serialization not ID3 v2.3 format by default, and other genre cleanup (#402) fixes #382 Fix ~~~ - Date correctness between ID3 versions (#396) - PopularityFrame email encoding bug. - Plugins more featured in docs v0.9.3 (2020-03-01) : It Dawned On Me -------------------------------------- Changes ~~~~~~~ - Track/disc numbers can be set with integer strings. - Disc number getter and setter hooks v0.9.2 (2020-02-10) : Into The Future -------------------------------------- Fix ~~~ - Removed setting of PYTHONIOENCODING, it breaks MacOS. Fixes #388 v0.9.1 (2020-02-09) : Dead and Gone ------------------------------------ Fix ~~~ - Docs and pep8. Other ~~~~~ - Experiment with setting utf-8 writer for stdout and stderr. v0.9 (2020-01-01) : Favorite Thing ----------------------------------- Major Changes ~~~~~~~~~~~~~ - Dropped support for Python versions 2.7, 3.4, and 3.5. - File scanning is no longer recursive by default; use `-r / --recursive`. - Default log-level changed from WARNING to ERROR. New ~~~ - Mime-type detection uses filetype.py (libmagic no longer required) - setFileScannerOpts function accepts `default_recursive` option. - A new `jsontag` plugin for converting tags to JSON. - A new `extract` plugin for extracting tags from media. - A new `yamltag` plugin for converting tags to YAML. - A new `mimetypes` plugin for listing file mime-types / measuring performance - Original artist support (TOPE frame, --orig-artist) - Added support for Python 3.8 and pypy3. Changes ~~~~~~~ - Log warning when ID3 v1.x text truncation occurs. Fixes #299. - Accept (invalid) date strings for the form YYYYMMDD. Fixes #379 - Adjust replay gain correctly for lame >= 3.95.1 headers. - Added -r/--recursive argument. eyeD3 is no longer recursive by default (#378) - Regenerated grako parser. - New ValueError for _setNum when unknown type/values are passed. - Moved src/* to top-level repo directory. Fix ~~~ - PRIV data type checking, fixed examples, etc. - Use tox for `make test` - ID3 v2.3 to v2.4 date conversion. - Match mp3 mime-types against all possible mime-types. Specifically, application/x-font-gdos. Fixes #338 - Fix simple typo: titel -> title. - Fixed: load the right config file in arguments. - Fix issue tracker link. Fixes #333. - Fixed art plugin when `pylast` is not installed. - Unbound variable for track num/total. Fixes #327. - Fixed MP3 header search to not false match on BOMs. - Honor APIC text encoding when description is "". #200. - Fixed bug with improper types when re-rendering unique file ID. (#324) - UFID fixes, update (#325) Other ~~~~~ - Deprecation of eyed3.utils.guessMimeType - Removed ipdb from dev requirements v0.8.12 (2019-12-27) --------------------- Changes ~~~~~~~ - Accept (invalid) date strings for the form YYYYMMDD. Fixes #379 Other ~~~~~ - Test with py38 v0.8.11 (2019-11-09) ------------------------ Fix ~~~ - ID3 v2.3 to v2.4 date conversion. - Match mp3 mime-types against all possible mime-types. Specifically, application/x-font-gdos. Fixes #338 v0.8.10 (2019-03-07) : Apples ------------------------------ New ~~~ - Log warning when ID3 v1.x text truncation occurs. Fixes #299. Fix ~~~ - Honor APIC text encoding when description is "". #200. - Fixed bug with improper types when re-rendering unique file ID. (#324) v0.8.9 (2019-01-12) : Descent Into... -------------------------------------- Changes ~~~~~~~ - Fixup plugin: -t changed to --type. - Pin pathlib to latest version 1.0.1 (#304) Fix ~~~ - Force no-color output when stdout is not a terminal (#297) - Requirements.txt: pathlib is only needed for older python versions (#284) - Art plugin: Pin pylast to 2.x to preserve Python2 support. v0.8.8 (2018-11-28) : In Ruins ------------------------------ New ~~~ - Follow symlink directories. Fixes #224 Changes ~~~~~~~ - Eyed3.core.AudioInfo `time_secs` is now a float and non-lossy. Fixes #210 - Removed Python 3.3 support. Fix ~~~ - Better type handling during TLEN [fixup plugin]. - Don't tweak logging by default, only thru `main`. Fixes #243 Other ~~~~~ - Added a separate example for Windows (--add-image ) [Addresses the issue #219] (#220) v0.8.7 (2018-06-11) : Aeon --------------------------- Fix ~~~ - Only use os.fwalk where supported. v0.8.6 (2018-05-27) : Robot Man -------------------------------- New ~~~ - Art plugin can now download album covers from last.fm. Changes ~~~~~~~ - Use os.fwalk for its better performance (esp. >= py37) Fixes #166 - TagTemplate `path_friendly` is now a string, namely the delimiter to use. Fix ~~~ - Classic plugin: --write-image will work with --quiet. Fixes #188 - Multiple fixes for display plugin %images% replacements. Fixes #176 - Allow --remove-* options to work when there are no tags. Fixes #183 v0.8.5 (2018-03-27) : 30$ Bag ----------------------------- New ~~~ - Mp3AudioFile.initTag now returns the new tag. - Eyed3.core.EP_MAX_SIZE_HINT. - Added docs for install devel dependencies and test data. Changes ~~~~~~~ - Similarly to TextFrame, fallback to latin1 for invalid encodings. - Removed paver as a dep. - Removed fabfile and mkenv. - Clean pytest_cache. - Nicfit.py cc update. Fix ~~~ - Handle missing `fcntl` on Windows. Fixes #135. - In addition to None, "" will now clear dates. - Update index.rst to reflect the code is in a Git repo, not Mercurial (#164) Other ~~~~~ - Update pytest from 3.2.2 to 3.5.0 (#175) - Update twine from 1.9.1 to 1.11.0 (#173) - Update sphinx from 1.6.5 to 1.7.2 (#174) - Update sphinxcontrib-paverutils from 1.16.0 to 1.17.0 (#172) - Update pytest-runner from 3.0 to 4.2 (#171) - Update nicfit.py from 0.7 to 0.8 (#161) - Update ipdb from 0.10.3 to 0.11 (#159) - Update factory-boy from 2.9.2 to 2.10.0 (#150) - Update pyaml from 17.10.0 to 17.12.1 (#138) - Update python-magic to 0.4.15 (#130) - Update pip-tools from 1.10.1 to 1.11.0 (#129) - Update check-manifest from 0.35 to 0.36 (#125) v0.8.4 (2017-11-17) : The Cold Vein ------------------------------------- New ~~~ - Composer (TCOM) support (#123) - Check for version incompatibilities during version changes. Changes ~~~~~~~ - More forgiving of invalid text encoding identifiers (fixes #101) - More forgiving of bad Unicode in text frames (fixes #105) - EyeD3 cmd line helper turned not session-scoped fixture. - Only warn about missing grako when the plugin is used. Fixes #115. Fix ~~~ - Fix python3 setup when system encoding is not utf-8 (#120) - Fix bad frames detection in stats plugin for python3 (#113) - Script exits with 0 status when called with --version/--help (#109) - Help pymagic with poorly encoded filenames. - [display plugin] Handle comments. - [display plugin] Handle internal exception types. Fixes #118. - IOError (nor OSError) have a message attr. Other ~~~~~ - Set theme jekyll-theme-slate. - Update pytest to 3.2.5 (#122) - Update pytest-runner to 3.0 (#108) - Update sphinx to 1.6.5 (#106) - Update flake8 to 3.5.0 (#107) v0.8.3 (2017-10-22) : So Alone ------------------------------- Fix ~~~ - Reload and process after tag removals, fixes #102. (PR #103) - Display incorrectly encoded strings (usually filenames) Other ~~~~~ - Make the classic output span the actual width of the tty so you can see the actual path with a long file name. (#92) v0.8.2 (2017-09-23) : Standing At the Station ---------------------------------------------- New ~~~ - Pypy and pypy3 support. Changes ~~~~~~~ - 'nose' is no longer used/required for testing. Fix ~~~ - Fix for Unicode paths when using Python2. Fixes #56. v0.8.1 (2017-08-26) : I Can't Talk To You ------------------------------------------ New ~~~ - ``make pkg-test-data`` target. - Sample mime-type tests. Fix ~~~ - Added ``python-magic`` as a dependency for reliable mime-type detection. Fixes #61 - Add pathlib to requirements. Fixes #43. - [doc] Fixed github URL. v0.8 (2017-05-13) : I Don't Know My Name ----------------------------------------- .. warning:: This release is **NOT** API compatible with 0.7.x. The majority of the command line interface has been preserved although many options have either changed or been removed. Additionally, support for Python 2.6 has been dropped. New ~~~ - Python 3 support (version 2.7 and >= 3.3 supported) - The Display plugin (-P/--plugin display) enables complete control over tag output. Requires ``grako``. If using pip, ``pip install eyeD3[display]``. Contributed by Sebastian Patschorke. - Genre.parse(id3_std=False) (and --non-std-genres) to disable genre # mapping. - eyed3.load accept pathlib.Path arguments. - eyed3.core.AudioFile accept pathlib.Path arguments. - eyed3.utils.walk accept pathlib.Path arguments. - New manual page. Contributed by Gaetano Guerriero - ``make test-data`` Changes ~~~~~~~~ - Project home from to GitHub: https://github.com/nicfit/eyeD3 Fix ~~~ - Lang fixes, and no longer coerce invalids to eng. Other ~~~~~ - Moved to pytest, although unittest not yet purged. 0.7.11 - 03.12.2017 (Evergreen) ------------------------------------ New Features: * Repo and issue tracker moved to GitHub: https://github.com/nicfit/eyeD3 Bug Fixes: * 'NoneType' object has no attribute 'year' * Multiple date related fixes. * Allow superfluous --no-tagging-ttme-frame option for backward compatibility. * The --version option now prints a short, version-only, message. * Allow --year option for backward compatibility. Converts to --release-year. * Fixes for --user-text-frame with multiple colons and similar fixes. * ID3 v1.1 encoding fixes. .. _release-0.7.10: 0.7.10 - 12.10.2016 (Hollow) --------------------------------- Bug Fixes: * Missing import * Fix the rendering of default constructed id3.TagHeader * Fixed Tag.frameiter 0.7.9 - 11.27.2015 (Collapse/Failure) -------------------------------------- New Features: * process files and directories in a sorted fashion. * display the ellipsis file name and path, and the file size right justified in printHeader. * stating to be unable to find a valid mp3 frame without a hint, where this happened is rather unfortunate. I noticed this from using eyed3.load() calls. * [fixup plugin] - Better compilation support. Bug Fixes: * Fixed missing 'math' import. * Replaced invalid Unicode. * Disabled ANSI codes on Windows * More friendly logging (as a module) 0.7.8 - 05.25.2015 (Chartsengrafs) --------------------------------------- New Features: * [pymod plugin] -- A more procedural plugin interface with modules. * [art plugin] -- Extract tag art to image files, or add images to tags. * eyed3.utils.art - High level tag art API * eyed3.id3.frames.ImageFrame.makeFileName produces the file extension .jpg instead of .jpeg for JPEG mime-types. * Added eyed3.utils.makeUniqueFileName for better reuse. * [statistics plugin] -- Less score deduction for lower bit rates. * Split example plugins module into discrete plugin modules. * [fixup plugin] -- Added --fix-case for applying ``title()`` to names * [fixup plugin] -- Detects and optionally removes files determined to be cruft. * eyed3.id3.Tag -- Added ``frameiter`` method for iterating over tag frames. * Added optional ``preserve_file_time`` argument to eyed3.id3.Tag.remove. * Removed python-magic dependency, it not longer offers any value (AFAICT). Bug Fixes: * ashing on --remove-frame PRIV * rse lameinfo even if crc16 is not correct * po in docs/installation.rst * Request to update the GPL License in source files * Fixes to eyed3.id3.tag.TagTemplate when expanding empty dates. * eyed3.plugins.Plugin.handleDone return code is not actually used. * [classic plugin] -- Fixed ID3v1 --verbose bug. * [fixup plugin] -- Better date handling, album type, and many bug fixes. 0.7.5 - 09.06.2014 (Nerve Endings) --------------------------------------- New Features: * Support for album artist info. By Cyril Roelandt * [fixup plugin] -- Custom patterns for file/directory renaming. By Matt Black * API providing simple prompts for plugins to use. * API and TXXX frame mappings for album type (e.g. various, album, demo, etc.) and artist origin (i.e. where the artist/band is from). * Lower cases ANSI codes and other console fixes. * Added the ability to set (remove) tag padding. See `eyeD3 --max-padding` option. By Hans Meine. * Tag class contains read_only attribute than can be set to ``True`` to disable the ``save`` method. * [classic plugin] -- Added ``--track-offset`` for incrementing/decrementing the track number. * [fixup plugin] -- Check for and fix cover art files. Bug Fixes: * Build from pypi when ``paver`` is not available. * Disable ANSI color codes when TERM == "dumb" * Locking around libmagic. * Work around for zero-padded utf16 strings. * Safer tempfile usage. * Better default v1.x genre. 0.7.3 - 07.12.2013 (Harder They Fall) ------------------------------------------ Bug fixes: * Allow setup.py to run with having ``paver`` installed. * [statistics plugin] Don't crash when 0 files are processed. 0.7.2 - 07.06.2013 (Nevertheless) ------------------------------------------ New Features: * Python 2.6 is now supported if ``argparse`` and ``ordereddict`` dependencies are installed. Thanks to Bouke Versteegh for much of this. * More support and bug fixes for `ID3 chapters and table-of-contents`_. * [classic plugin] ``-d/-D`` options for setting tag disc number and disc set total. * Frames are always written in sorted order, so if a tag is rewritten with no values changed the file's checksum remains the same. * Documentation and examples are now included in source distribution. * [classic plugin] Removed ``-p`` for setting publisher since using it when ``-P`` is intended is destructive. * [classic plugin] Supports ``--no-color`` to disable color output. Note, this happens automatically if the output streams is not a TTY. * ``Tag.save`` supports preserving the file modification time; and option added to classic plugin. * [statistics plgin] Added rules for "lint-like" checking of a collection. The rules are not yet configurable. * ERROR is now the default log level. Bug fixes: * Various fixes for PRIV frames, error handling, etc. from Bouke Versteegh * Convert '/' to '-' in TagTemplate names (i.e. --rename) * Drop TSIZ frames when converting to ID3 v2.4 * ID3 tag padding size now set correctly. * Fixes for Unicode paths. * License clarification in pkg-info. * The ``-b`` setup.py argument is now properly supported. * Magic module `hasattr` fix. * More robust handling of bogus play count values. * More robust handling of bogus date values. * Proper unicode handling of APIC descriptions. * Proper use of argparse.ArgumentTypeError * Allow TCMP frames when parsing. * Accept more invalid frame types (iTunes) * Documentation fixes. * Fix for bash completion script. * Fix for certain mp3 bit rate and play time computations. .. _ID3 chapters and table-of-contents: http://www.id3.org/id3v2-chapters-1.0 0.7.1 - 11.25.2012 (Feel It) ------------------------------ New Features: * Support for `ID3 chapters and table-of-contents`_ frames (i.e.CHAP and CTOC). * A new plugin for toggling the state of iTunes podcast files. In other words, PCST and WFED support. Additionally, the Apple "extensions" frames TKWD, TDES, and TGID are supported. Run ``eyeD3 -P itunes-podcast --help`` for more info. * Native frame type for POPM (Popularity meter). See the :func:`eyed3.id3.tag.Tag.popularities` accessor method. * Plugins can deal with traversed directories instead of only file-by-file. Also, :class:`eyed3.plugins.LoaderPlugin` can optionally cache the loaded audio file objects for each callback to ``handleDirectory``. * [classic plugin] New --remove-frame option. * [statistics plugin] More accurate values and easier to extend. Bug fixes: * Fixed a very old bug where certain values of 0 would be written to the tag as '' instead of '\x00'. * Don't crash on malformed (invalid) UFID frames. * Handle timestamps that are terminated with 'Z' to show the time is UTC. * Conversions between ID3 v2.3 and v2.4 date frames fixed. * [classic plugin] Use the system text encoding (locale) when converting lyrics files to Unicode. 0.7.0 - 11.15.2012 (Be Quiet and Drive) ---------------------------------------- .. warning:: This release is **NOT** API compatible with 0.6.x. The majority of the command line interface has been preserved although many options have either changed or been removed. .. New Features: * Command line script ``eyeD3`` now supports plugins. The default plugin is the classic interface for tag reading and editing. * Plugins for writing NFO files, displaying lame/xing headers, jabber tunes, and library statistics. * Module name is now ``eyed3`` (all lower case) to be more standards conforming. * New ``eyed3.id3.Tag`` interface based on properties. * Improved ID3 date frame support and 2.3<->2.4 conversion, and better conversions, in general. * Native support for many more ID3 frame types. * Python Package Index friendly, and installable with 'pip'. * Improved mime-type detection. * Improved unicode support. * Support for config files to contain common options for the command-line tool. 0.6.18 - 11.25.2011 (Nobunny loves you) ----------------------------------------------- New features: * Support for disc number frames (TPOS). Thanks to Nathaniel Clark * Added %Y (year) and %G (genre) substitution variables for file renames. Thanks to Otávio Pontes * Improved XML (--jep-118) escaping and a new option (--rfc822) to output in RFC 822 format. Thanks to Neil Schemenauer * --rename will NOT clobber existing files. * New option --itunes to write only iTunes accepted genres. Thanks to Ben Isaacs * If available the 'magic' module will be used to determine mimetypes when the filename is not enough. Thanks to Ville Skyttä * --set-encoding can be used along with a version conversion arg to apply a new encoding to the new tag. * Increased performance for mp3 header search when malformed GEOB frames are encountered. Thanks to Stephen Fairchild * Less crashing when invalid user text frames are encountered. * Less crashing when invalid BPM values (empty/non-numeric) are encountered. 0.6.17 - 02.01.2009 (The Point of No Return) ----------------------------------------------- Bug fixes: * Workaround invalid utf16 * Show all genres during --list-genres * Workaround invalid PLCT frames. * Show all tracks during --nfo output. New features: * Support for URL frames (W??? and WXXX) * Program exit code for the 'eyeD3' command line tool 0.6.16 - 06.09.2008 (Gimme Danger) ----------------------------------------------- Bug fixes: * Typo fix of sysnc/unsync data. Thanks to Gergan Penkov * Infinite loop fix when dealing with malformed APIC frames. * Tag.removeUserTextFrame helper. Thanks to David Grant 0.6.15 - 03.02.2008 (Doin' The Cockroach) ----------------------------------------------- Bug fixes: * ID3 v1 comment encoding (latin1) bug fix (Renaud Saint-Gratien ) * APIC picture type fix (Michael Schout ) * Fixed console Unicode encoding for display. * Fixed frame de-unsnychronization bugs. * Round float BPMs to int (per the spec) 0.6.14 - 05.08.2007 (Breakthrough) ----------------------------------------------- Bugs fixes: - Fixed a nasty corruption of the first mp3 header when writing to files that do not already contain a tag. - Fixed a bug that would duplicate TYER frames when setting new values. - Fixed the reading/validation of some odd (i.e.,rare) mp3 headers New Features: - Encoding info extracted from Lame mp3 headers [Todd Zullinger] - Genre names will now support '|' to allow for genres like "Rock|Punk|Pop-Punk" and '!' for "Oi!" 0.6.13 - 04.30.2007 (Undercovers On) ----------------------------------------------- - Numerous write fixes, especially for v2.4 tags. Thanks to Alexander Thomas for finding these. - Add --no-zero-padding option to allow disabling of zero padding track numbers - Add --nfo option to output NFO format files about music directories. - Time computation fixes when MP3 frames headers were mistakingly found. 0.6.12 - 02.18.2007 (Rid Of Me) ----------------------------------------------- - Handle Mac style line ending in lyrics and display with the proper output encoding. [Todd Zullinger] - TDTG support and other date frame fixes. [Todd Zullinger] - Output encoding bug fixes. [Todd Zullinger] 0.6.11 - 11.05.2006 (Disintegration) ----------------------------------------------- - Support for GEOB (General encapsulated object) frames from Aaron VonderHaar - Decreased memory consumption during tag rewrites/removals. - Allow the "reserved" mpeg version bits when not in strict mode. - Solaris packages available via Blastwave - http://www.blastwave.org/packages.php/pyeyed3 0.6.10 - 03.19.2006 (Teh Mesk release) ----------------------------------------------- - Unsynchronized lyrics (USLT) frame support [Todd Zullinger ] - UTF16 bug fixes - More forgiving of invalid User URL frames (WXXX) - RPM spec file fixes [Knight Walker ] - More details in --verbose display 0.6.9 - 01.08.2005 (The Broken Social Scene Release) ------------------------------------------------------- - eyeD3 (the CLI) processes directories more efficiently - A specific file system encoding can be specified for file renaming, see --fs-encoding (Andrew de Quincey) - Faster mp3 header search for empty and/or corrupt mp3 files - Extended header fixes - Bug fix for saving files with no current tag - What would a release be without unicode fixes, this time it's unicode filename output and JEP 0118 output. 0.6.8 - 08.29.2005 (The Anal Cunt Release) ----------------------------------------------- - Frame header size bug. A _serious_ bug since writes MAY be affected (note: I've had no problems reported so far). 0.6.7 - 08.28.2005 (The Autopsy Release) -------------------------------------------- - Beats per minute (TPBM) interface - Publisher/label (TPUB) interface - When not in strict mode exceptions for invalid tags are quelled more often - Support for iTunes ID3 spec violations regarding multiple APIC frames - Bug fix where lang in CommentFrame was unicode where it MUST be ascii - Bug fixed for v2.2 frame header sizes - Bug fixed for v2.2 PIC frames - File rename bug fixes - Added -c option as an alias for --comment - -i/--write-images now takes a destination path arg. Due to optparse non-support for optional arguments the path MUST be specified. This option no longer clobbers existing files. 0.6.6 - 05.15.2005 (The Electric Wizard Release) --------------------------------------------------- - APIC frames can now be removed. - An interface for TBPM (beats per minute) frames. - Utf-16 bug fixes and better unicode display/output - RPM spec file fixes 0.6.5 - 04.16.2005 ----------------------------------------------- - Read-only support for ID3 v2.2 - TPOS frame support (disc number in set). - Bug fixes 0.6.4 - 02.05.2005 ----------------------------------------------- - Native support for play count (PCNT), and unique file id (UFID) frames. - More relaxed genre processing. - Sync-safe bug fixed when the tag header requests sync-safety and not the frames themselves. - configure should successfly detect python release candidates and betas. 0.6.3 - 11.23.2004 ----------------------------------------------- - Much better unicode support when writing to the tag. - Added Tag.setEncoding (--set-encoding) and --force-update - Handle MP3 frames that violate spec when in non-strict mode. (Henning Kiel ) - Fix for Debian bug report #270964 - Various bug fixes. 0.6.2 - 8.29.2004 (Happy Birthday Mom!) ----------------------------------------------- - TagFile.rename and Tag.tagToString (eyeD3 --rename=PATTERN). The latter supports substitution of tag values: %A is artist, %t is title, %a is album, %n is track number, and %N is track total. - eyeD3 man page. - User text frame (TXXX) API and --set-user-text-frame. - Python 2.2/Optik compatibility works now. - ebuild for Gentoo (http://eyed3.nicfit.net/releases/gentoo/) 0.6.1 - 5/14/2004 (Oz/2 Ohh my!) --------------------------------- - Unicode support - UTF-8, UTF-16, and UTF-16BE - Adding images (APIC frames) is supported (--add-image, Tag.addImage(), etc.) - Added a --relaxed option to be much more forgiving about tags that violate the spec. Quite useful for removing such tags. - Added Tag.setTextFrame (--set-text-frame=FID:TEXT) - Added --remove-comments. - Now requires Python 2.3. Sorry, but I like cutting-edge python features. - Better handling and conversion (2.3 <=> 2.4) of the multiple date frames. - Output format per JEP 0118: User Tune, excluding xsd:duration format for (http://www.jabber.org/jeps/jep-0118.html) - Lot's of bug fixes. - Added a mailing list. Subscribe by sending a message to eyed3-devel-subscribe@nicfit.net 0.5.1 - 7/17/2003 (It's Too Damn Hot to Paint Release) ----------------------------------------------------------- - Temporary files created during ID3 saving are now properly cleaned up. - Fixed a "bug" when date frames are present but contain empty strings. - Added a --no-color option to the eyeD3 driver. - Workaround invalid tag sizes by implyied padding. - Updated README 0.5.0 - 6/7/2003 (The Long Time Coming Release) ------------------------------------------------- - ID3 v2.x saving. - The eyeD3 driver/sample program is much more complete, allowing for most common tag operations such as tag display, editing, removal, etc. Optik is required to use this program. See the README. - Complete access to all artist and title frames (i.e. TPE* and TIT*) - Full v2.4 date support (i.e. TDRC). - Case insensitive genres and compression fixes. (Gary Shao) - ExtendedHeader support, including CRC checksums. - Frame groups now supported. - Syncsafe integer conversion bug fixes. - Bug fixes related to data length indicator bytes. - Genre and lot's of other bug fixes. 0.4.0 - 11/11/2002 (The Anniversary Release) --------------------------------------------- - Added the ability to save tags in ID v1.x format, including when the linked file was IDv2. Original backups are created by default for the time being... - Added deleting of v1 and v2 frames from the file. - Zlib frame data decompression is now working. - bin/eyeD3 now displays user text frames, mp3 copyright and originality, URLs, all comments, and images. Using the --write-images arg will write each APIC image data to disk. - Added eyeD3.isMp3File(), Tag.clear(), Tag.getImages(), Tag.getURLs(), Tag.getCDID(), FrameSet.removeFrame(), Tag.save(), ImageFrame.writeFile(), etc... - Modified bin/eyeD3 to grok non Mp3 files. This allows testing with files containing only tag data and lays some groundwork for future OGG support. - Fixed ImageFrame mime type problem. - Fixed picture type scoping problems. 0.3.1 - 10/24/2002 ------------------- - RPM packages added. - Fixed a bug related to ID3 v1.1 track numbers. (Aubin Paul) - Mp3AudioFile matchs ``*.mp3`` and ``*.MP3``. (Aubin Paul) 0.3.0 - 10/21/2002 ------------------ - Added a higher level class called Mp3AudioFile. - MP3 frame (including Xing) decoding for obtaining bit rate, play time, etc. - Added APIC frame support (eyeD3.frames.Image). - BUG FIX: Tag unsynchronization and deunsynchronization now works correctly and is ID3 v2.4 compliant. - Tags can be linked with file names or file objects. - More tag structure abstractions (TagHeader, Frame, FrameSet, etc.). - BUG FIX: GenreExceptions were not being caught in eyeD3 driver. 0.2.0 - 8/15/2002 ---------------------- - ID3_Tag was renamed to Tag. - Added Genre and GenreMap (eyeD3.genres is defined as the latter type) - Added support of ID3 v1 and v2 comments. - The ID3v2Frame file was renamed ID3v2 and refactoring work has started with the addition of TagHeader. 0.1.0 - 7/31/2002 ---------------------- - Initial release. eyeD3-0.9.7/LICENSE000066400000000000000000001045141432016011500135030ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . eyeD3-0.9.7/MANIFEST.in000066400000000000000000000010231432016011500142230ustar00rootroot00000000000000include AUTHORS.rst include CONTRIBUTING.rst include HISTORY.rst include LICENSE include README.rst include Makefile include tox.ini graft docs prune docs/_build recursive-include test *.py exclude .cookiecutter.yml exclude .gitchangelog.rc global-exclude __pycache__ global-exclude .editorconfig global-exclude *.py[co] recursive-include requirements *.txt recursive-include eyed3 *.py include eyed3/plugins/DisplayPattern.ebnf recursive-include examples * exclude fabfile.py exclude mkenv.sh exclude pavement.py prune etc eyeD3-0.9.7/Makefile000066400000000000000000000226711432016011500141410ustar00rootroot00000000000000## User settings PYTEST_ARGS ?= ./tests PYPI_REPO ?= pypi BUMP ?= prerelease TEST_DATA_DIR ?= $(shell pwd)/tests CC_MERGE ?= yes CC_OPTS ?= --no-input ifdef TERM BOLD_COLOR = $(shell tput bold) HELP_COLOR = $(shell tput setaf 6) HEADER_COLOR = $(BOLD_COLOR)$(shell tput setaf 2) NO_COLOR = $(shell tput sgr0) endif ## Defaults help: ## List all commands @printf "\n$(BOLD_COLOR)***** eyeD3 Makefile help *****$(NO_COLOR)\n" @# This code borrowed from https://github.com/jedie/poetry-publish/blob/master/Makefile @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9 -]+:.*?## / {printf "$(HELP_COLOR)%-20s$(NO_COLOR) %s\n", $$1, $$2}' $(MAKEFILE_LIST) @echo "" @printf "$(BOLD_COLOR)Options:$(NO_COLOR)\n" @printf "$(HELP_COLOR)%-20s$(NO_COLOR) %s\n" PYTEST_ARGS "If defined PDB options are added when 'pytest' is invoked" @printf "$(HELP_COLOR)%-20s$(NO_COLOR) %s\n" PYPI_REPO "The package index to publish, 'pypi' by default." @printf "$(HELP_COLOR)%-20s$(NO_COLOR) %s\n" BROWSER "HTML viewer used by docs-view/coverage-view" @printf "$(HELP_COLOR)%-20s$(NO_COLOR) %s\n" CC_MERGE "Set to no to disable cookiecutter merging." @printf "$(HELP_COLOR)%-20s$(NO_COLOR) %s\n" CC_OPTS "OVerrided the default options (--no-input) with your own." @echo "" all: build test ## Build and test ## Config PROJECT_NAME = $(shell python setup.py --name 2> /dev/null) VERSION = $(shell python setup.py --version 2> /dev/null) SRC_DIRS = ./eyed3 ABOUT_PY = eyed3/__regarding__.py GITHUB_USER = nicfit GITHUB_REPO = eyeD3 RELEASE_NAME = $(shell sed -n "s/^release_name = \"\(.*\)\"/\1/p" pyproject.toml) RELEASE_TAG = v$(VERSION) CHANGELOG = HISTORY.rst CHANGELOG_HEADER = v${VERSION} ($(shell date --iso-8601))$(if ${RELEASE_NAME}, : ${RELEASE_NAME},) TEST_DATA = eyeD3-test-data TEST_DATA_FILE = ${TEST_DATA}.tgz ## Build .PHONY: build build: $(ABOUT_PY) setup.py ## Build the project setup.py: pyproject.toml poetry.lock poetry2setup >| setup.py $(ABOUT_PY): pyproject.toml regarding -o $@ # Note, this clean rule is NOT to be called as part of `clean` clean-autogen: -rm $(ABOUT_PY) setup.py ## Clean clean: clean-test clean-dist clean-local clean-docs # Clean the project rm -rf ./build rm -rf eye{d,D}3.egg-info rm -fr .eggs/ find . -name '*.egg' -exec rm -f {} + find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + find . -name '__pycache__' -exec rm -fr {} + clean-local: -rm tags -rm all.id3 example.id3 find . -name '*.rej' -exec rm -f '{}' \; find . -name '*.orig' -exec rm -f '{}' \; find . -type f -name '*~' | xargs -r rm ## Test .PHONY: test test: ## Run tests with default python pytest $(PYTEST_ARGS) test-all: ## Run tests with all supported versions of Python tox --parallel=all $(PYTEST_ARGS) test-data: # Move these to eyed3.nicfit.net test -f ${TEST_DATA_DIR}/${TEST_DATA_FILE} || \ wget --quiet "http://eyed3.nicfit.net/releases/${TEST_DATA_FILE}" \ -O ${TEST_DATA_DIR}/${TEST_DATA_FILE} tar xzf ${TEST_DATA_DIR}/${TEST_DATA_FILE} -C ${TEST_DATA_DIR} cd tests && rm -f ./data && ln -s ${TEST_DATA_DIR}/${TEST_DATA} ./data clean-test: rm -fr .tox/ rm -f .coverage find . -name '.pytest_cache' -type d -exec rm -rf {} + -rm .testmondata -rm examples/*.id3 clean-test-data: -rm tests/data -rm tests/${TEST_DATA_FILE} pkg-test-data: test -d build || mkdir build tar czf ./build/${TEST_DATA_FILE} -h --exclude-vcs -C ./tests \ ./eyeD3-test-data publish-test-data: pkg-test-data scp ./build/${TEST_DATA_FILE} eyed3.nicfit.net:./data1/eyeD3-releases/ coverage: coverage run --source $(SRC_DIRS) -m pytest $(PYTEST_ARGS) coverage report coverage html coverage-view: @if [ ! -f build/tests/coverage/index.html ]; then \ ${MAKE} coverage; \ fi @${BROWSER} build/tests/coverage/index.html ## Documentation .PHONY: docs docs: ## Generate project documentation with Sphinx rm -f docs/eyed3.rst rm -f docs/modules.rst sphinx-apidoc --force -H "$(shell echo $(PROJECT_NAME) | tr '[:upper:]' '[:lower:]') module" -V $(VERSION) -o docs/ ${SRC_DIRS} $(MAKE) -C docs clean etc/mycog.py $(MAKE) -C docs html -rm example.id3 docs-dist: docs test -d dist || mkdir dist cd docs/_build && \ tar czvf ../../dist/${PROJECT_NAME}-${VERSION}_docs.tar.gz html docs-view: docs $(BROWSER) docs/_build/html/index.html clean-docs: $(MAKE) -C docs clean -rm README.html lint: ## Check coding style flake8 $(SRC_DIRS) ## Distribute .PHONY: dist dist: clean sdist bdist docs-dist ## Create source and binary distribution files @# The cd dist keeps the dist/ prefix out of the md5sum files @cd dist && \ for f in $$(ls); do \ md5sum $${f} > $${f}.md5; \ done @ls dist sdist: build poetry build --format sdist bdist: build poetry build --format wheel clean-dist: ## Clean distribution artifacts (included in `clean`) rm -rf dist check-manifest: check-manifest _check-version-tag: @if git tag -l | grep -E '^$(shell echo ${RELEASE_TAG} | sed 's|\.|.|g')$$' > /dev/null; then \ echo "Version tag '${RELEASE_TAG}' already exists!"; \ false; \ fi authors: @git authors --list | while read auth ; do \ email=`echo "$$auth" | awk 'match($$0, /.*<(.*)>/, m) {print m[1]}'`;\ echo "Checking $$email...";\ if echo "$$email" | grep -v 'users.noreply.github.com'\ | grep -v 'github-bot@pyup.io' \ > /dev/null ; then \ grep "$$email" AUTHORS.rst > /dev/null || echo " * $$auth" >> AUTHORS.rst;\ fi;\ done ## Install install: ## Install project and dependencies poetry install --only main install-dev: ## Install project, dependencies, and developer tools poetry install --all-extras ## Release release: pre-release clean install-dev \ _freeze-release dist _tag-release \ upload-release pre-release: clean-autogen build _check-version-tag \ check-manifest authors changelog test-all @# Keep docs off pre-release target list, else it is pruned during 'release' but @# after a clean. @$(MAKE) docs @test -n "${GITHUB_USER}" || (echo "GITHUB_USER not set, needed for github" && false) @test -n "${GITHUB_TOKEN}" || (echo "GITHUB_TOKEN not set, needed for github" && false) @github-release --version # Just a exe existence check @git status -s -b bump-release: requirements @# TODO: is not a pre-release, clear release_name poetry version $(BUMP) .PHONY: requirements requirements: poetry show --outdated poetry update --lock poetry export -f requirements.txt --without-hashes\ --output requirements/requirements.txt poetry export -f requirements.txt --without-hashes\ --output requirements/test-requirements.txt -E test poetry export -f requirements.txt --without-hashes --output requirements/dev-requirements.txt --with dev poetry export -f requirements.txt --without-hashes\ --output requirements/extra-requirements.txt \ -E art-plugin -E yaml-plugin $(MAKE) build upload-release: _pypi-release _github-release _web-release _pypi-release: poetry publish -r ${PYPI_REPO} _github-release: name="${RELEASE_TAG}"; \ if test -n "${RELEASE_NAME}"; then \ name="${RELEASE_TAG} (${RELEASE_NAME})"; \ fi; \ prerelease=""; \ if echo "${RELEASE_TAG}" | grep '[^v0-9\.]'; then \ prerelease="--pre-release"; \ fi; \ echo "NAME: $$name"; \ echo "PRERELEASE: $$prerelease"; \ github-release --verbose release --user "${GITHUB_USER}" \ --repo ${GITHUB_REPO} --tag ${RELEASE_TAG} \ --name "$${name}" $${prerelease} for file in $$(find dist -type f -exec basename {} \;) ; do \ echo "Uploading: $$file"; \ github-release upload --user "${GITHUB_USER}" --repo ${GITHUB_REPO} \ --tag ${RELEASE_TAG} --name $${file} --file dist/$${file}; \ done _web-release: for f in `find dist -type f`; do \ scp $$f eyed3.nicfit.net:./data1/eyeD3-releases/`basename $$f`; \ done _freeze-release: @(git diff --quiet && git diff --quiet --staged) || \ (printf "\n!!! Working repo has uncommitted/un-staged changes. !!!\n" && \ printf "\nCommit and try again.\n" && false) _tag-release: git tag -a $(RELEASE_TAG) -m "Release $(RELEASE_TAG)" git push --tags origin changelog: @last=`git tag -l --sort=version:refname | grep '^v[0-9]' | tail -n1`;\ if ! grep "${CHANGELOG_HEADER}" ${CHANGELOG} > /dev/null; then \ rm -f ${CHANGELOG}.new; \ if test -n "$$last"; then \ gitchangelog --author-format=email \ --omit-author="travis@pobox.com" $${last}..HEAD |\ sed "s|^%%version%% .*|${CHANGELOG_HEADER}|" |\ sed '/^.. :changelog:/ r/dev/stdin' ${CHANGELOG} \ > ${CHANGELOG}.new; \ else \ cat ${CHANGELOG} |\ sed "s/^%%version%% .*/${CHANGELOG_HEADER}/" \ > ${CHANGELOG}.new;\ fi; \ mv ${CHANGELOG}.new ${CHANGELOG}; \ fi ## MISC README.html: README.rst rst2html5.py README.rst >| README.html if test -n "${BROWSER}"; then \ ${BROWSER} README.html;\ fi GIT_COMMIT_HOOK = .git/hooks/commit-msg cookiecutter: tmp_d=`mktemp -d`; cc_d=$$tmp_d/eyeD3; \ if test "${CC_MERGE}" = "no"; then \ nicfit cookiecutter ${CC_OPTS} "$${tmp_d}"; \ git -C "$$cc_d" diff; \ git -C "$$cc_d" status -s -b; \ else \ nicfit cookiecutter --merge ${CC_OPTS} "$${tmp_d}" \ --extra-merge ${GIT_COMMIT_HOOK} ${GIT_COMMIT_HOOK};\ fi; \ rm -rf $$tmp_d ## Runtime environment venv: source /usr/bin/virtualenvwrapper.sh && \ mkvirtualenv eyeD3 && \ pip install -U pip && \ poetry install --no-dev clean-venv: source /usr/bin/virtualenvwrapper.sh && \ rmvirtualenv eyeD3 eyeD3-0.9.7/README.rst000066400000000000000000000072641432016011500141710ustar00rootroot00000000000000Status ------ .. image:: https://img.shields.io/pypi/v/eyeD3.svg :target: https://pypi.python.org/pypi/eyeD3/ :alt: Latest Version .. image:: https://img.shields.io/pypi/status/eyeD3.svg :target: https://pypi.python.org/pypi/eyeD3/ :alt: Project Status .. image:: https://travis-ci.org/nicfit/eyeD3.svg?branch=master :target: https://travis-ci.org/nicfit/eyeD3 :alt: Build Status .. image:: https://img.shields.io/pypi/l/eyeD3.svg :target: https://pypi.python.org/pypi/eyeD3/ :alt: License .. image:: https://img.shields.io/pypi/pyversions/eyeD3.svg :target: https://pypi.python.org/pypi/eyeD3/ :alt: Supported Python versions .. image:: https://coveralls.io/repos/nicfit/eyeD3/badge.svg :target: https://coveralls.io/r/nicfit/eyeD3 :alt: Coverage Status About ----- eyeD3_ is a Python tool for working with audio files, specifically MP3 files containing ID3_ metadata (i.e. song info). It provides a command-line tool (``eyeD3``) and a Python library (``import eyed3``) that can be used to write your own applications or plugins that are callable from the command-line tool. For example, to set some song information in an mp3 file called ``song.mp3``:: $ eyeD3 -a Integrity -A "Humanity Is The Devil" -t "Hollow" -n 2 song.mp3 With this command we've set the artist (``-a/--artist``), album (``-A/--album``), title (``-t/--title``), and track number (``-n/--track-num``) properties in the ID3 tag of the file. This is the standard interface that eyeD3 has always had in the past, therefore it is also the default plugin when no other is specified. The results of this command can be seen by running the ``eyeD3`` with no options. :: $ eyeD3 song.mp3 song.mp3 [ 3.06 MB ] ------------------------------------------------------------------------- ID3 v2.4: title: Hollow artist: Integrity album: Humanity Is The Devil album artist: None track: 2 ------------------------------------------------------------------------- The same can be accomplished using Python. :: import eyed3 audiofile = eyed3.load("song.mp3") audiofile.tag.artist = "Token Entry" audiofile.tag.album = "Free For All Comp LP" audiofile.tag.album_artist = "Various Artists" audiofile.tag.title = "The Edge" audiofile.tag.track_num = 3 audiofile.tag.save() eyeD3_ is written and maintained by `Travis Shirk`_ and is licensed under version 3 of the GPL_. Features -------- * Python package (`import eyed3`) for writing applications and plugins. * `eyeD3` : Command-line tool driver script that supports plugins. * Easy ID3 editing/viewing of audio metadata from the command-line. * Plugins for: Tag to string formatting (display), album fixing (fixup), cover art downloading (art), collection stats (stats), and json/yaml/jabber/nfo output formats, and more included. * Support for ID3 versions 1.x, 2.2 (read-only), 2.3, and 2.4. * Support for the MP3 audio format exposing details such as play time, bit rate, sampling frequency, etc. * Abstract design allowing future support for different audio formats and metadata containers. Get Started ----------- Python >= 3.7 is required. For `installation instructions`_ or more complete `documentation`_ see http://eyeD3.nicfit.net/ Please post feedback and/or defects on the `issue tracker`_, or `mailing list`_. .. _eyeD3: http://eyeD3.nicfit.net/ .. _Travis Shirk: travis@pobox.com .. _issue tracker: https://github.com/nicfit/eyeD3/issues .. _mailing list: https://groups.google.com/forum/?fromgroups#!forum/eyed3-users .. _installation instructions: http://eyeD3.nicfit.net/index.html#installation .. _documentation: http://eyeD3.nicfit.net/index.html#documentation .. _GPL: http://www.gnu.org/licenses/gpl-2.0.html .. _ID3: http://id3.org/ eyeD3-0.9.7/docs/000077500000000000000000000000001432016011500134215ustar00rootroot00000000000000eyeD3-0.9.7/docs/Makefile000066400000000000000000000151461432016011500150700ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/eyed3.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/eyed3.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/eyed3" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/eyed3" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." eyeD3-0.9.7/docs/_config.yml000066400000000000000000000000311432016011500155420ustar00rootroot00000000000000theme: jekyll-theme-slateeyeD3-0.9.7/docs/_static/000077500000000000000000000000001432016011500150475ustar00rootroot00000000000000eyeD3-0.9.7/docs/_static/.keep000066400000000000000000000000001432016011500157620ustar00rootroot00000000000000eyeD3-0.9.7/docs/_static/rtd.css000066400000000000000000000356341432016011500163650ustar00rootroot00000000000000/* * rtd.css * ~~~~~~~~~~~~~~~ * * Sphinx stylesheet -- sphinxdoc theme. Originally created by * Armin Ronacher for Werkzeug. * * Customized for ReadTheDocs by Eric Pierce & Eric Holscher * * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ /* RTD colors * light blue: #e8ecef * medium blue: #8ca1af * dark blue: #465158 * dark grey: #444444 * * white hover: #d1d9df; * medium blue hover: #697983; * green highlight: #8ecc4c * light blue (project bar): #e8ecef */ @import url("basic.css"); /* PAGE LAYOUT -------------------------------------------------------------- */ body { font: 100%/1.5 "ff-meta-web-pro-1","ff-meta-web-pro-2",Arial,"Helvetica Neue",sans-serif; text-align: center; color: black; background-color: #465158; padding: 0; margin: 0; } div.document { text-align: left; background-color: #e8ecef; } div.bodywrapper { background-color: #ffffff; border-left: 1px solid #ccc; border-bottom: 1px solid #ccc; margin: 0 0 0 16em; } div.body { margin: 0; padding: 0.5em 1.3em; max-width: 55em; min-width: 20em; } div.related { font-size: 1em; background-color: #465158; } div.documentwrapper { float: left; width: 100%; background-color: #e8ecef; } /* HEADINGS --------------------------------------------------------------- */ h1 { margin: 0; padding: 0.7em 0 0.3em 0; font-size: 1.5em; line-height: 1.15; color: #111; clear: both; } h2 { margin: 2em 0 0.2em 0; font-size: 1.35em; padding: 0; color: #465158; } h3 { margin: 1em 0 -0.3em 0; font-size: 1.2em; color: #6c818f; } div.body h1 a, div.body h2 a, div.body h3 a, div.body h4 a, div.body h5 a, div.body h6 a { color: black; } h1 a.anchor, h2 a.anchor, h3 a.anchor, h4 a.anchor, h5 a.anchor, h6 a.anchor { display: none; margin: 0 0 0 0.3em; padding: 0 0.2em 0 0.2em; color: #aaa !important; } h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, h5:hover a.anchor, h6:hover a.anchor { display: inline; } h1 a.anchor:hover, h2 a.anchor:hover, h3 a.anchor:hover, h4 a.anchor:hover, h5 a.anchor:hover, h6 a.anchor:hover { color: #777; background-color: #eee; } /* LINKS ------------------------------------------------------------------ */ /* Normal links get a pseudo-underline */ a { color: #444; text-decoration: none; border-bottom: 1px solid #ccc; } /* Links in sidebar, TOC, index trees and tables have no underline */ .sphinxsidebar a, .toctree-wrapper a, .indextable a, #indices-and-tables a { color: #444; text-decoration: none; border-bottom: none; } /* Most links get an underline-effect when hovered */ a:hover, div.toctree-wrapper a:hover, .indextable a:hover, #indices-and-tables a:hover { color: #111; text-decoration: none; border-bottom: 1px solid #111; } /* Footer links */ div.footer a { color: #86989B; text-decoration: none; border: none; } div.footer a:hover { color: #a6b8bb; text-decoration: underline; border: none; } /* Permalink anchor (subtle grey with a red hover) */ div.body a.headerlink { color: #ccc; font-size: 1em; margin-left: 6px; padding: 0 4px 0 4px; text-decoration: none; border: none; } div.body a.headerlink:hover { color: #c60f0f; border: none; } /* NAVIGATION BAR --------------------------------------------------------- */ div.related ul { height: 2.5em; } div.related ul li { margin: 0; padding: 0.65em 0; float: left; display: block; color: white; /* For the >> separators */ font-size: 0.8em; } div.related ul li.right { float: right; margin-right: 5px; color: transparent; /* Hide the | separators */ } /* "Breadcrumb" links in nav bar */ div.related ul li a { order: none; background-color: inherit; font-weight: bold; margin: 6px 0 6px 4px; line-height: 1.75em; color: #ffffff; padding: 0.4em 0.8em; border: none; border-radius: 3px; } /* previous / next / modules / index links look more like buttons */ div.related ul li.right a { margin: 0.375em 0; background-color: #697983; text-shadow: 0 1px rgba(0, 0, 0, 0.5); border-radius: 3px; -webkit-border-radius: 3px; -moz-border-radius: 3px; } /* All navbar links light up as buttons when hovered */ div.related ul li a:hover { background-color: #8ca1af; color: #ffffff; text-decoration: none; border-radius: 3px; -webkit-border-radius: 3px; -moz-border-radius: 3px; } /* Take extra precautions for tt within links */ a tt, div.related ul li a tt { background: inherit !important; color: inherit !important; } /* SIDEBAR ---------------------------------------------------------------- */ div.sphinxsidebarwrapper { padding: 0; } div.sphinxsidebar { margin: 0; margin-left: -100%; float: left; top: 3em; left: 0; padding: 0 1em; width: 14em; font-size: 1em; text-align: left; background-color: #e8ecef; } div.sphinxsidebar img { max-width: 12em; } div.sphinxsidebar h3, div.sphinxsidebar h4 { margin: 1.2em 0 0.3em 0; font-size: 1em; padding: 0; color: #222222; font-family: "ff-meta-web-pro-1", "ff-meta-web-pro-2", "Arial", "Helvetica Neue", sans-serif; } div.sphinxsidebar h3 a { color: #444444; } div.sphinxsidebar ul, div.sphinxsidebar p { margin-top: 0; padding-left: 0; line-height: 130%; background-color: #e8ecef; } /* No bullets for nested lists, but a little extra indentation */ div.sphinxsidebar ul ul { list-style-type: none; margin-left: 1.5em; padding: 0; } /* A little top/bottom padding to prevent adjacent links' borders * from overlapping each other */ div.sphinxsidebar ul li { padding: 1px 0; } /* A little left-padding to make these align with the ULs */ div.sphinxsidebar p.topless { padding-left: 0 0 0 1em; } /* Make these into hidden one-liners */ div.sphinxsidebar ul li, div.sphinxsidebar p.topless { white-space: nowrap; overflow: hidden; } /* ...which become visible when hovered */ div.sphinxsidebar ul li:hover, div.sphinxsidebar p.topless:hover { overflow: visible; } /* Search text box and "Go" button */ #searchbox { margin-top: 2em; margin-bottom: 1em; background: #ddd; padding: 0.5em; border-radius: 6px; -moz-border-radius: 6px; -webkit-border-radius: 6px; } #searchbox h3 { margin-top: 0; } /* Make search box and button abut and have a border */ input, div.sphinxsidebar input { border: 1px solid #999; float: left; } /* Search textbox */ input[type="text"] { margin: 0; padding: 0 3px; height: 20px; width: 144px; border-top-left-radius: 3px; border-bottom-left-radius: 3px; -moz-border-radius-topleft: 3px; -moz-border-radius-bottomleft: 3px; -webkit-border-top-left-radius: 3px; -webkit-border-bottom-left-radius: 3px; } /* Search button */ input[type="submit"] { margin: 0 0 0 -1px; /* -1px prevents a double-border with textbox */ height: 22px; color: #444; background-color: #e8ecef; padding: 1px 4px; font-weight: bold; border-top-right-radius: 3px; border-bottom-right-radius: 3px; -moz-border-radius-topright: 3px; -moz-border-radius-bottomright: 3px; -webkit-border-top-right-radius: 3px; -webkit-border-bottom-right-radius: 3px; } input[type="submit"]:hover { color: #ffffff; background-color: #8ecc4c; } div.sphinxsidebar p.searchtip { clear: both; padding: 0.5em 0 0 0; background: #ddd; color: #666; font-size: 0.9em; } /* Sidebar links are unusual */ div.sphinxsidebar li a, div.sphinxsidebar p a { background: #e8ecef; /* In case links overlap main content */ border-radius: 3px; -moz-border-radius: 3px; -webkit-border-radius: 3px; border: 1px solid transparent; /* To prevent things jumping around on hover */ padding: 0 5px 0 5px; } div.sphinxsidebar li a:hover, div.sphinxsidebar p a:hover { color: #111; text-decoration: none; border: 1px solid #888; } /* Tweak any link appearing in a heading */ div.sphinxsidebar h3 a { } /* OTHER STUFF ------------------------------------------------------------ */ cite, code, tt { font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; font-size: 0.95em; letter-spacing: 0.01em; } tt { background-color: #f2f2f2; color: #444; } tt.descname, tt.descclassname, tt.xref { border: 0; } hr { border: 1px solid #abc; margin: 2em; } pre, #_fontwidthtest { font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; margin: 1em 2em; font-size: 0.95em; letter-spacing: 0.015em; line-height: 120%; padding: 0.5em; border: 1px solid #ccc; background-color: #eee; border-radius: 6px; -moz-border-radius: 6px; -webkit-border-radius: 6px; } pre a { color: inherit; text-decoration: underline; } td.linenos pre { padding: 0.5em 0; } div.quotebar { background-color: #f8f8f8; max-width: 250px; float: right; padding: 2px 7px; border: 1px solid #ccc; } div.topic { background-color: #f8f8f8; } table { border-collapse: collapse; margin: 0 -0.5em 0 -0.5em; } table td, table th { padding: 0.2em 0.5em 0.2em 0.5em; } /* ADMONITIONS AND WARNINGS ------------------------------------------------- */ /* Shared by admonitions and warnings */ div.admonition, div.warning { font-size: 0.9em; margin: 2em; padding: 0; /* border-radius: 6px; -moz-border-radius: 6px; -webkit-border-radius: 6px; */ } div.admonition p, div.warning p { margin: 0.5em 1em 0.5em 1em; padding: 0; } div.admonition pre, div.warning pre { margin: 0.4em 1em 0.4em 1em; } div.admonition p.admonition-title, div.warning p.admonition-title { margin: 0; padding: 0.1em 0 0.1em 0.5em; color: white; font-weight: bold; font-size: 1.1em; text-shadow: 0 1px rgba(0, 0, 0, 0.5); } div.admonition ul, div.admonition ol, div.warning ul, div.warning ol { margin: 0.1em 0.5em 0.5em 3em; padding: 0; } /* Admonitions only */ div.admonition { border: 1px solid #609060; background-color: #e9ffe9; } div.admonition p.admonition-title { background-color: #70A070; border-bottom: 1px solid #609060; } /* Warnings only */ div.warning { border: 1px solid #900000; background-color: #ffe9e9; } div.warning p.admonition-title { background-color: #b04040; border-bottom: 1px solid #900000; } div.versioninfo { margin: 1em 0 0 0; border: 1px solid #ccc; background-color: #DDEAF0; padding: 8px; line-height: 1.3em; font-size: 0.9em; } .viewcode-back { font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif; } div.viewcode-block:target { background-color: #f4debf; border-top: 1px solid #ac9; border-bottom: 1px solid #ac9; } dl { margin: 1em 0 2.5em 0; } /* Highlight target when you click an internal link */ dt:target { background: #ffe080; } /* Don't highlight whole divs */ div.highlight { background: transparent; } /* But do highlight spans (so search results can be highlighted) */ span.highlight { background: #ffe080; } div.footer { background-color: #465158; color: #eeeeee; padding: 0 2em 2em 2em; clear: both; font-size: 0.8em; text-align: center; } p { margin: 0.8em 0 0.5em 0; } .section p img { margin: 1em 2em; } /* MOBILE LAYOUT -------------------------------------------------------------- */ @media screen and (max-width: 600px) { h1, h2, h3, h4, h5 { position: relative; } ul { padding-left: 1.75em; } div.bodywrapper a.headerlink, #indices-and-tables h1 a { color: #e6e6e6; font-size: 80%; float: right; line-height: 1.8; position: absolute; right: -0.7em; visibility: inherit; } div.bodywrapper h1 a.headerlink, #indices-and-tables h1 a { line-height: 1.5; } pre { font-size: 0.7em; overflow: auto; word-wrap: break-word; white-space: pre-wrap; } div.related ul { height: 2.5em; padding: 0; text-align: left; } div.related ul li { clear: both; color: #465158; padding: 0.2em 0; } div.related ul li:last-child { border-bottom: 1px dotted #8ca1af; padding-bottom: 0.4em; margin-bottom: 1em; width: 100%; } div.related ul li a { color: #465158; padding-right: 0; } div.related ul li a:hover { background: inherit; color: inherit; } div.related ul li.right { clear: none; padding: 0.65em 0; margin-bottom: 0.5em; } div.related ul li.right a { color: #fff; padding-right: 0.8em; } div.related ul li.right a:hover { background-color: #8ca1af; } div.body { clear: both; min-width: 0; word-wrap: break-word; } div.bodywrapper { margin: 0 0 0 0; } div.sphinxsidebar { float: none; margin: 0; width: auto; } div.sphinxsidebar input[type="text"] { height: 2em; line-height: 2em; width: 70%; } div.sphinxsidebar input[type="submit"] { height: 2em; margin-left: 0.5em; width: 20%; } div.sphinxsidebar p.searchtip { background: inherit; margin-bottom: 1em; } div.sphinxsidebar ul li, div.sphinxsidebar p.topless { white-space: normal; } .bodywrapper img { display: block; margin-left: auto; margin-right: auto; max-width: 100%; } div.documentwrapper { float: none; } div.admonition, div.warning, pre, blockquote { margin-left: 0em; margin-right: 0em; } .body p img { margin: 0; } #searchbox { background: transparent; } .related:not(:first-child) li { display: none; } .related:not(:first-child) li.right { display: block; } div.footer { padding: 1em; } .rtd_doc_footer .badge { float: none; margin: 1em auto; position: static; } .rtd_doc_footer .badge.revsys-inline { margin-right: auto; margin-bottom: 2em; } table.indextable { display: block; width: auto; } .indextable tr { display: block; } .indextable td { display: block; padding: 0; width: auto !important; } .indextable td dt { margin: 1em 0; } ul.search { margin-left: 0.25em; } ul.search li div.context { font-size: 90%; line-height: 1.1; margin-bottom: 1; margin-left: 0; } } eyeD3-0.9.7/docs/_templates/000077500000000000000000000000001432016011500155565ustar00rootroot00000000000000eyeD3-0.9.7/docs/_templates/.keepme000066400000000000000000000000001432016011500170130ustar00rootroot00000000000000eyeD3-0.9.7/docs/authors.rst000066400000000000000000000000341432016011500156350ustar00rootroot00000000000000.. include:: ../AUTHORS.rst eyeD3-0.9.7/docs/cli.rst000066400000000000000000000106121432016011500147220ustar00rootroot00000000000000 'eyeD3' Command Line Tool ========================== The ``eyeD3`` command line interface is based on plugins. The main driver knows how to traverse file systems and load audio files for hand-off to the plugin to do something interesting. With no plugin selected a simplified usage is: .. code-block:: sh $ eyeD3 --help usage: eyeD3 [-h] [--version] [--exclude PATTERN] [--plugins] [--plugin NAME] [PATH [PATH ...]] positional arguments: PATH Files or directory paths optional arguments: -h, --help show this help message and exit --version Display version information and exit --exclude PATTERN A regular expression for path exclusion. May be specified multiple times. --plugins List all available plugins --plugin NAME Specify which plugin to use. The ``PATH`` argument(s) along with optional usage of ``--exclude`` are used to tell ``eyeD3`` what files or directories to process. Directories are searched recursively and every file encountered is passed to the plugin until no more files are found. To list the available plugins use the ``--plugins`` option and to select a plugin pass its name using ``--plugin=``. .. {{{cog cli_example("examples/cli_examples.sh", "PLUGINS_LIST", lang="bash") }}} .. {{{end}}} If no ``--plugin=`` option is provided the *default* plugin is selected. Currently this is set to be the command line tag viewer/editor that has been the primary interface in all versions of eyeD3 prior to 0.7.x. Plugins -------- .. toctree:: :maxdepth: 1 plugins .. _config-files: Configuration Files ------------------- Command line options can be read from a configuration file using the ``-C/--config`` option. It expects a path to an `Ini `_ file contain sections with option values. A sample config file, for example: .. literalinclude:: ../examples/config.ini :language: ini If the file ``${HOME}/.eyeD3/config.ini`` exists it is loaded each time eyeD3 is run and the values take effect. This can be disabled with ``--no-config``. Custom Plugins -------------- Plugins are any class found in the plugin search path (see 'plugin_path' in :ref:`config-files`) that inherits from :class:`eyed3.plugins.Plugin`. The interface is simple, the basic attributes of the plugin (name, description, etc.) are set using menber variables and for each file ``eyeD3`` traverses (using the given path(s) and optional ``--exclude`` options) the method ``handleFile`` will be called. The return value of this call is ignored, but if you wish to halt processing of files a ``StopIteration`` exception can be raised. Here the plugin should do whatever interesting it thinks it would like to do with the passed files. When all input files are processed the method ``handleDone`` is called and the program exits. Below is an 'echo' plugin that prints each filename/path and the file's mime-type. .. literalinclude:: ../examples/plugins/echo.py Many plugins might prefer to deal with only file types ``eyeD3`` natively supports, namely mp3 audio files. To automatically load :class:`eyed3.core.AudioFile` objects using :func:`eyed3.core.load` inherit from the :class:`eyed3.plugins.LoaderPlugin` class. In this model the member ``self.audio_file`` is initialized to the parsed mp3/id3 objects. If the file is not a supported audio file type the value is set to ``None``. In the next example the ``LoaderPlugin`` is used to set the ``audio_file`` member variable which contains the info and tag objects. .. literalinclude:: ../examples/plugins/echo2.py .. seealso:: :ref:`config-files`, :class:`eyed3.plugins.Plugin`, :class:`eyed3.plugins.classic.ClassicPlugin`, :class:`eyed3.mp3.Mp3AudioInfo`, :class:`eyed3.id3.tag.Tag` Documenting Plugins ^^^^^^^^^^^^^^^^^^^^ Plugin docs are generated. Start each plugin with the following template; **but replace the square brackets with curly.*** :: Example Plugin =============== .. [[[cog .. cog.out(cog_pluginHelp("example-plugin")) .. ]]] .. [[[end]]] The documentation build process will run `eyeD3 --plugin example-plugin` and generate docs from the command line options and plugin metadata such as the description. The plugin index in `cli.rst` should also me updated to include the new plugin. eyeD3-0.9.7/docs/compliance.rst000066400000000000000000000067221432016011500162740ustar00rootroot00000000000000########## Compliance ########## ID3 === Unsupported Features -------------------- * ID3 frame encryption * Writing of sync-safe data (i.e. unsynchronized) because it is 2012. Reading of unsyncronized tags (v2.3) and frames (v2.4) **is** supported. Dates ----- One of the major differences between 2.3 and 2.4 is dates. ID3 v2.3 Date Frames ~~~~~~~~~~~~~~~~~~~~ - TDAT date (recording date of form DDMM, always 4 bytes) - TYER year (recording year of form YYYY, always 4 bytes) - TIME time (recording time of form HHMM, always 4 bytes) - TORY orig release year - TRDA recording date (more freeform replacement for TDAT, TYER, TIME. e.g., "4th-7th June, 12th June" in combination with TYER) - TDLY playlist delay (also defined in ID3 v2.4) ID3 v2.4 Date Frames ~~~~~~~~~~~~~~~~~~~~ All v2.4 dates follow ISO 8601 formats. - TDEN encoding datetime - TDOR orig release date - TDRC recording date - TDRL release date - TDTG tagging time - TDLY playlist delay (also defined in ID3 v2.3) From the ID3 specs:: yyyy-MM-ddTHH:mm:ss (year, "-", month, "-", day, "T", hour (out of 24), ":", minutes, ":", seconds), but the precision may be reduced by removing as many time indicators as wanted. Hence valid timestamps are yyyy, yyyy-MM, yyyy-MM-dd, yyyy-MM-ddTHH, yyyy-MM-ddTHH:mm and yyyy-MM-ddTHH:mm:ss. All time stamps are UTC. For durations, use the slash character as described in 8601, and for multiple non- contiguous dates, use multiple strings, if allowed by the frame definition. The ISO 8601 'W' delimiter for numeric weeks is NOT supported. Times that contain a 'Z' at the end to signal the time is UTC is supported. Common Date Frame Extensions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ MusicBrainz uses *XDOR* in v2.3 tags as the **full** original release date, whereas *TORY* (v2.3) only represents the release year. Version 2.4 does not use/need this extension since *TDOR* is available. v2.4 <-> 2.3 mappings ~~~~~~~~~~~~~~~~~~~~~ When converting to/from v2.3 and v2.4 it is neceswsary to convert date frames. The following is the mappings eyeD3 uses when converting. Version 2.3 --> version 2.4 * TYER, TDAT, TIME --> TDRC * TORY --> TDOR * TRDA --> none * XDOR --> TDOR If both *TORY* and *XDOR* exist, XDOR is preferred. Version 2.4 --> version 2.3 * TDRC --> TYER, TDAT, TIME * TDOR --> TORY * TDRL --> TORY * TDEN --> none * TDTG --> none Non Standard Frame Support -------------------------- NCON ~~~~ A MusicMatch extension of unknown binary format. Frames of this type are parsed as raw ``Frame`` objects, therefore the data is not parsed. The frames are preserved and can be deleted and written (as is). TCMP ~~~~ An iTunes extension to signify that a track is part of a compilation. This frame is handled by ``TextFrame`` and the data is either a '1' if part of a compilation or '0' (or empty) if not. XSOA, XSOP, XSOT ~~~~~~~~~~~~~~~~ These are alternative sort-order strings for album, performer, and title, respectively. They are often added to ID3v2.3 tags while v2.4 does not require them since TSOA, TSOP, and TSOT are native frames. These frames are preserved but are not written when using v2.3. If the tag is converted to v2.4 then the corresponding native frame is used. XDOR ~~~~ A MusicBrainz extension for the **full** original release date, since TORY only contains the year of original release. In ID3 v2.4 this frame became TDOR. PCST, WFED, TKWD, TDES, TGID ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Apple extensions for podcasts. eyeD3-0.9.7/docs/conf.py000066400000000000000000000207421432016011500147250ustar00rootroot00000000000000#!/usr/bin/env python # # eyeD3 documentation build configuration file, created by # sphinx-quickstart on Tue Jul 9 22:26:36 2013. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys from datetime import datetime # If extensions (or modules to document with autodoc) are in another # directory, add these directories to sys.path here. If the directory is # relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # Get the project root dir, which is the parent dir of this cwd = os.getcwd() project_root = os.path.join("./", os.path.dirname(cwd)) # Insert the project root dir as the first element in the PYTHONPATH. # This lets us ensure that the source package is imported, and that its # version is used. sys.path.insert(0, project_root) from eyed3.__regarding__ import ( project_name, version, years, author ) # -- General configuration --------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinx.ext.extlinks', "sphinx_issues"] issues_github_path = "nicfit/eyeD3" # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = project_name copyright = '{years}, {author}'.format(years=years, author=author) # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout # the built documents. # release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to # some non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built # documents. #keep_warnings = False # -- Options for HTML output ------------------------------------------- html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a # theme further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as # html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the # top of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon # of the docs. This file should be a Windows icon file (.ico) being # 16x16 or 32x32 pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) # here, relative to this directory. They are copied after the builtin # static files, so a file named "default.css" will overwrite the builtin # "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names # to template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. # Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. # Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages # will contain a tag referring to it. The value of this option # must be the base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'eyed3doc' # -- Options for LaTeX output ------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', 'eyed3.tex', u'eyeD3 Documentation', u'Travis Shirk', 'manual'), ] # The name of an image file (relative to this directory) to place at # the top of the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings # are parts, not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output ------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'eyed3', u'eyeD3 Documentation', [u'Travis Shirk'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ---------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'eyed3', u'eyeD3 Documentation', u'Travis Shirk', 'eyed3', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'http://docs.python.org/': None} autodoc_member_order = "bysource" autoclass_content = "both" eyeD3-0.9.7/docs/contributing.rst000066400000000000000000000000411432016011500166550ustar00rootroot00000000000000.. include:: ../CONTRIBUTING.rst eyeD3-0.9.7/docs/eyeD3.1000066400000000000000000000406451432016011500144650ustar00rootroot00000000000000.TH EYED3 "1" "Sept. 12, 2016" "eyeD3 0.7.9" "" .SH "NAME" .B eyeD3 \- displays and manipulates id3-tags on mp3 files .SH "SYNOPSIS" .B eyeD3 .RI [ options ] .RI PATH .RI [ PATH... ] .SH "DESCRIPTION" .B eyeD3 Manipulates ID3 tags in mp3 files and is able to read/write and convert between ID3 v1.0, v1.1, v2.3 and v2.4 tags. High-level access is provided to most frames, including APIC (i.e., images) frames. The \fBeyeD3\fR command line interface is based on plugins that operates on files. To list the available plugins use the \fB--plugins\fR option and to select a plugin pass its name using \fB--plugin=\fRNAME. The \fBPATH\fR argument(s) along with optional usage of \fB--exclude\fR are used to tell \fBeyeD3\fR what files or directories to process. Directories are searched recursively and every file encountered is passed to the selected plugin until no more files are found. .SH "BASE OPTIONS" .TP \fB-h, \fB--help\fR Show a brief help string and exit. .TP \fB--version\fR Display version information and exit .TP \fB--exclude=\fRPATTERN A regular expression for path exclusion. May be specified multiple times. .TP \fB-L, \fB--plugins\fR List all available plugins. .TP \fB-P \fRNAME, \fB--plugin=\fRNAME Specify which plugin to use. The default is \'classic' .TP \fB-C \fRFILE, \fB--config=\fRFILE Supply a configuration file. The default is \'~/.eyeD3/config.ini', although even that is optional. .TP \fB--no-config\fR Do not load the default user config \'~/.eyeD3/config.ini'. The -c/--config options are still honored if present. .TP \fB--backup\fR Plugins should honor this option such that a backup is made of any file modified. The backup is made in same directory with a \'.orig' extension added. .TP \fB-Q, \fB--quiet\fR A hint to plugins to output less. .TP \fB--fs-encoding=\fRENCODING Use the specified file system encoding for filenames. Default is detected from the current locale, overriding is useful for example when reading from mounted file systems. .TP \fB--no-color\fR Suppress color codes in console output. This will happen automatically if the output is not a TTY (e.g. when redirecting to a file) .SH "DEBUGGING" .TP \fB-l \fRLEVEL[:LOGGER], \fB--log-level=\fRLEVEL[:LOGGER] Set a log level. This option may be specified multiple times. If a logger name is specified than the level applies only to that logger, otherwise the level is set on the top-level logger. Acceptable levels are \'debug', \'verbose', \'info', \'warning', \'error', \'critical'. .TP \fB--profiler\fR Run using python profiler. .TP \fB--debugger\fR Drop into python debugger when errors occur. .SH `CLASSIC` PLUGIN This plugin is the classic eyeD3 interface for viewing and editing tags. All \fBPATH\fR arguments are parsed and displayed. Directory paths are searched recursively. Any editing options (\fB--artist\fR, \fB--title\fR) are applied to each file read. All date options (\fB--release-year\fR excepted) follow ISO 8601 format. This is \'yyyy-mm-ddThh:mm:ss'. The year is required, and each component thereafter is optional. For example, "2012-03" is valid, "2012--12" is not. .TP \fB-a \fRSTRING, \fB--artist=\fRSTRING Set the artist name. .TP \fB-A \fRSTRING, \fB--album=\fRSTRING Set the album name. .TP \fB-b \fRSTRING, \fB--album-artist=\fRSTRING Set the album artist name. "Various Artists", for example. Another example is collaborations when the track artist might be "Eminem featuring Proof" the album artist would be "Eminem". .TP \fB-t \fRSTRING, \fB--title=\fRSTRING Set the track title. .TP \fB-n \fRNUM, \fB--track=\fRNUM Set the track number. Use 0 to clear. .TP \fB-N \fRNUM, \fB--track-total=\fRNUM Set total number of tracks. Use 0 to clear. .TP \fB--track-offset=\fRN Increment/decrement the track number by [-]N. This option is applied after \fB--track\fR=N is set. .TP \fB-d \fRNUM, \fB--disc-num=\fRNUM Set the disc number. Use 0 to clear. .TP \fB-G \fRGENRE, \fB--genre=\fRGENRE Set the genre. If the argument is a standard ID3 genre name or number both will be set. Otherwise, any string can be used. Use \fB--plugin\fR=genres for a list of standard ID3 genre names/ids. .TP \fB-Y \fRYEAR, \fB--release-year=\fRYEAR Set the year the track was released. Use the date options for more precise values or dates other than release. .TP \fB-c \fRSTRING, \fB--comment=\fRSTRING Set a comment. In ID3 tags this is the comment with an empty description. See \fB--add-comment\fR to add multiple comment frames. .TP \fB--rename=\fRPATTERN Rename file (the extension is not affected) based on data in the tag using substitution variables: $album, $album_artist, $artist, $best_date, $best_date:prefer_recording, $best_date:prefer_recording:year, $best_date:prefer_release, $best_date:prefer_release:year, $best_date:year, $disc:num, $disc:total, $file, $file:ext, $original_release_date, $original_release_date:year, $recording_date, $recording_date:year, $release_date, $release_date:year, $title, $track:num, $track:total. . .SS ID3 options \fB-1, \fB--v1\fR Only read and write ID3 v1.x tags. By default, v1.x tags are only read or written if there is not a v2 tag in the file. .TP \fB-2, \fB--v2\fR Only read/write ID3 v2.x tags. This is the default unless the file only contains a v1 tag. .TP \fB--to-v1.1\fR Convert the file's tag to ID3 v1.1 (Or 1.0 if there is no track number). .TP \fB--to-v2.3\fR Convert the file's tag to ID3 v2.3. .TP \fB--to-v2.4\fR Convert the file's tag to ID3 v2.4 .TP \fB--release-date=\fRDATE Set the date the track/album was released .TP \fB--orig-release-date=\fRDATE Set the original date the track/album was released. .TP \fB--recording-date=\fRDATE Set the date the track/album was recorded .TP \fB--encoding-date=\fRDATE Set the date the file was encoded .TP \fB--tagging-date=\fRDATE Set the date the file was tagged .TP \fB--publisher=\fRSTRING Set the publisher/label name .TP \fB--play-count=\fR[+]N Set the number of times played counter. If the argument value begins with \'+' the tag's play count is incremented by N, otherwise the value is set to exactly N. .TP \fB--bpm=\fRN Set the beats per minute value. .TP \fB--unique-file-id=\fROWNER_ID:ID Add a unique file ID frame. If the ID arg is empty the frame is removed. An OWNER_ID is required. The ID may be no more than 64 bytes. .TP \fB--add-comment=\fRCOMMENT[:DESCRIPTION[:LANG]] Add or replace a comment. There may be more than one comment in a tag, as long as the DESCRIPTION and LANG values are unique. The default DESCRIPTION is \'' and the default language code is \'eng'. .TP \fB--remove-comment=\fRDESCRIPTION[:LANG] Remove comment matching DESCRIPTION and LANG. The default language code is \'eng'. .TP \fB--remove-all-comments Remove all comments from the tag. .TP \fB--add-lyrics=\fRLYRICS_FILE[:DESCRIPTION[:LANG]] Add or replace a lyrics. There may be more than one set of lyrics in a tag, as long as the DESCRIPTION and LANG values are unique. The default DESCRIPTION is '' and the default language code is 'eng'. .TP \fB--remove-lyrics=\fRDESCRIPTION[:LANG] Remove lyrics matching DESCRIPTION and LANG. The default language code is 'eng'. .TP \fB--remove-all-lyrics Remove all lyrics from the tag. .TP \fB--text-frame=\fRFID:TEXT Set the value of a text frame. To remove the frame, specify an empty value. For example, --text-frame="TDRC:" .TP \fB--user-text-frame=\fRDESC:TEXT Set the value of a user text frame (i.e., TXXX). To remove the frame, specify an empty value. e.g., --user-text-frame="SomeDesc:" .TP \fB--user-url-frame=\fRDESCRIPTION:URL Set the value of a user URL frame (i.e., WXXX). To remove the frame, specify an empty value. e.g., --user-url-frame="SomeDesc:" .TP \fB--add-image=\fRIMG_PATH:TYPE[:DESCRIPTION] Add or replace an image. There may be more than one image in a tag, as long as the DESCRIPTION values are unique. The default DESCRIPTION is \''. If PATH begins with \'http[s]://' then it is interpreted as a URL instead of a file containing image data. The TYPE must be one of the following: OTHER, ICON, OTHER_ICON, FRONT_COVER, BACK_COVER, LEAFLET, MEDIA, LEAD_ARTIST, ARTIST, CONDUCTOR, BAND, COMPOSER, LYRICIST, RECORDING_LOCATION, DURING_RECORDING, DURING_PERFORMANCE, VIDEO, BRIGHT_COLORED_FISH, ILLUSTRATION, BAND_LOGO, PUBLISHER_LOGO. .TP \fB--remove-image=\fRDESCRIPTION Remove image matching DESCRIPTION. .TP \fB--remove-all-images Remove all images from the tag .TP \fB--write-images=\fRDIR Causes all attached images (APIC frames) to be written to the specified directory. .TP \fB--add-object=\fROBJ_PATH:MIME-TYPE[:DESCRIPTION[:FILENAME]] Add or replace an object. There may be more than one object in a tag, as long as the DESCRIPTION values are unique. The default DESCRIPTION is \''. .TP \fB--remove-object=\fRDESCRIPTION Remove object matching DESCRIPTION. .TP \fB--remove-all-objects Remove all objects from the tag .TP \fB--add-popularity=\fREMAIL:RATING[:PLAY_COUNT] Adds a pupularity metric. There may be multiples popularity values, but each must have a unique email address component. The rating is a number between 0 (worst) and 255 (best). The play count is optional, and defaults to 0, since there is already a dedicated play count frame. .TP \fB--remove-popularity=\fREMAIL Removes the popularity frame with the specified email key. .TP \fB--remove-v1 Remove ID3 v1.x tag. .TP \fB--remove-v2 Remove ID3 v2.x tag. .TP \fB--remove-all Remove ID3 v1.x and v2.x tags. .TP \fB--remove-frame=\fRFID Remove all frames with the given ID. This option may be specified multiple times. .TP \fB--max-padding=\fNUM_BYTES Shrink file if tag padding (unused space) exceeds the given number of bytes. (Useful e.g. after removal of large cover art.) Default is 64 KiB, file will be rewritten with default padding (1 KiB) or max padding, whichever is smaller. .TP \fB--encoding=\fRlatin1|utf8|utf16|utf16-be Set the encoding that is used for all text frames. This option is only applied if the tag is updated as the result of an edit option (e.g. --artist, --title, etc.) or --force-update is specified. . .SS Misc options \fB--force-update\fR Rewrite the tag despite there being no edit options. .TP \fB--preserve-file-times\fR When writing, do not update file modification times. . .SH OTHER PLUGINS Execution of a plugin other than \'classic' is possible through the \fB--plugin\fR option. . .SS art This plugin manages art files and tags for albums, artists, etc... Options --update-files and --update-tags are mutually exclusive. .TP \fB--update-files\fR Write art files from tag images. .TP \fB--update-tags\fR Write tag image from art files. .SS fixup Performs various checks and fixes to directories of audio files. Operates on directories at a time, fixing each as a unit (album, compilation, live set, etc.). All of these should have common dates, for example but other characteristics may vary. The --type should be used whenever possible, \'lp' is the default. The following tests and fixes always apply: .RS 2 .IP 1. 4 Every file will be given an ID3 tag if one is missing. .sp -1 .IP 2. 4 Set ID3 v2.4. .sp -1 .IP 3. 4 Set a consistent album name for all files in the directory. .sp -1 .IP 4. 4 Set a consistent artist name for all files, unless the type is \'various' in which case the artist may vary (but must exist). .sp -1 .IP 5. 4 Ensure each file has a title. .sp -1 .IP 6. 4 Ensure each file has a track # and track total. .sp -1 .IP 7. 4 Ensure all files have a release and original release date, unless the type is \'live' in which case the recording date is set. .sp -1 .IP 8. 4 All ID3 frames of the following types are removed: USER, PRIV .sp -1 .IP 9. 4 All ID3 files have TLEN (track length in ms) set (or updated). .sp -1 .IP 10. 4 The album/dir type is set in the tag. Types of \'lp' and \'various' do not have this field set since the latter is the default and the former can be determined during sync. In ID3 terms the value is in TXXX (description: \'eyeD3#album_type'). .sp -1 .IP 11. 4 Files are renamed as follows: .sp -1 .RS 4 .IP \(bu 3 Type \'various': ${track:num} - ${artist} - ${title} .sp -1 .IP \(bu 3 Type \'single': ${artist} - ${title} .sp -1 .IP \(bu 3 All other types: ${artist} - ${track:num} - ${title} .sp -1 .IP \(bu 3 A rename template can be supplied in --file-rename-pattern .RE .sp -1 .IP 12. 4 Directories are renamed as follows: .sp -1 .RS 4 .IP \(bu 3 Type \'live': ${best_date:prefer_recording} - ${album} .sp -1 .IP \(bu 3 All other types: ${best_date:prefer_release} - ${album} .sp -1 .IP \(bu 3 A rename template can be supplied in --dir-rename-pattern .RE .RE Album types .RS 2 .IP \(bu 2 \'lp': A traditinal "album" of songs from a single artist. No extra info is written to the tag since this is the default. .sp -1 .IP \(bu 2 \'ep': A short collection of songs from a single artist. The string "ep" is written to the tag's \'eyeD3#album_type' field. .sp -1 .IP \(bu 2 \'various': A collection of songs from different artists. The string "various" is written to the tag's \'eyeD3#album_type' field. .sp -1 .IP \(bu 2 \'live': A collection of live recordings from a single artist. The string "live" is written to the tag's \'eyeD3#album_type' field. .sp -1 .IP \(bu 2 \'compilation': A collection of songs from various recordings by a single artist. The string "compilation'"is written to the tag's \'eyeD3#album_type' field. Compilation dates, unlike other types, may differ. .sp -1 .IP \(bu 2 \'demo': A demo recording by a single artist. The string "demo" is written to the tag's \'eyeD3#album_type' field. .sp -1 .IP \(bu 2 \'single': A track that should no be associated with an album (even if it has album metadata). The string "single" is written to the tag's \'eyeD3#album_type' field. .RE .TP \fB-t\fR TYPE, \fB--type\fR TYPE One of the album types. How to treat each directory. The default is "lp" although you may be prompted for an alternate choice if the files look like another type. .TP \fB--fix-case\fR Fix casing on each string field by capitalizing each word. .TP \fB-n\fR, \fB--dry-run\fR Only print the operations that would take place, but do not execute them. .TP \fB--no-prompt\fR Exit if prompted. .TP \fB--dotted-dates\fR Separate date with \'.' instead of \'-' when naming directories. .TP \fB--file-rename-pattern\fR FILE_RENAME_PATTERN Rename file (the extension is not affected) based on data in the tag using substitution variables: $album, $album_artist, $artist, $best_date, $best_date:prefer_recording, $best_date:prefer_recording:year, $best_date:prefer_release, $best_date:prefer_release:year, $best_date:year, $disc:num, $disc:total, $file, $file:ext, $original_release_date, $original_release_date:year, $recording_date, $recording_date:year, $release_date, $release_date:year, $title, $track:num, $track:total .TP \fB--dir-rename-pattern\fR DIR_RENAME_PATTERN Rename directory based on data in the tag using substitution variables. Available substitution are the same as in --file-rename-pattern .SS genres Display the full list of standard ID3 genres. ID3 v1 defined a list of genres and mapped them to to numeric values so they can be stored as a single byte. It is \fIrecommended\fR that these genres are used although most newer software (including eyeD3) does not care. .SS itunes-podcast Adds (or removes) the tags necessary for Apple iTunes to identify the file as a podcast. .TP \fB--add\fR Add the podcast frames. .TP \fB--remove\fR Remove the podcast frames. .SS lameinfo Outputs lame header (if one exists) for file. The \'lame' (or xing) header provides extra information about the mp3 that is useful to players and encoders but not officially part of the mp3 specification. Variable bit rate mp3s, for example, use this header. For more details see . \'xing' is an alias for this plugin. .SS nfo Create NFO files for each directory scanned. Each directory scanned is treated as an album and a NFO () file is written to standard out. NFO files are often found in music archives. .SS stats Computes statistics for all audio files scanned. .TP \fB--verbose\fR Show details for each file with rule violations. .SS xep-118 Outputs all tags in XEP-118 XML format. See: .SH CONFIGURATION FILE Command line options can be read from a configuration file using the -C/--config option. It expects a path to an Ini () file contain sections with option values. For a sample config file see /usr/share/doc/eyeD3/examples/config.ini. If the file \'${HOME}/.eyeD3/config.ini' exists it is loaded each time eyeD3 is run and the values take effect. This can be disabled with \'--no-config'. .SH SEE ALSO http://eyed3.nicfit.net/ .SH AUTHOR eyeD3 was written by Travis Shirk . eyeD3-0.9.7/docs/eyed3.id3.rst000066400000000000000000000013101432016011500156350ustar00rootroot00000000000000eyed3.id3 package ================= Submodules ---------- eyed3.id3.apple module ---------------------- .. automodule:: eyed3.id3.apple :members: :undoc-members: :show-inheritance: eyed3.id3.frames module ----------------------- .. automodule:: eyed3.id3.frames :members: :undoc-members: :show-inheritance: eyed3.id3.headers module ------------------------ .. automodule:: eyed3.id3.headers :members: :undoc-members: :show-inheritance: eyed3.id3.tag module -------------------- .. automodule:: eyed3.id3.tag :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: eyed3.id3 :members: :undoc-members: :show-inheritance: eyeD3-0.9.7/docs/eyed3.mp3.rst000066400000000000000000000004711432016011500156640ustar00rootroot00000000000000eyed3.mp3 package ================= Submodules ---------- eyed3.mp3.headers module ------------------------ .. automodule:: eyed3.mp3.headers :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: eyed3.mp3 :members: :undoc-members: :show-inheritance: eyeD3-0.9.7/docs/eyed3.plugins.rst000066400000000000000000000045661432016011500166570ustar00rootroot00000000000000eyed3.plugins package ===================== Submodules ---------- eyed3.plugins.art module ------------------------ .. automodule:: eyed3.plugins.art :members: :undoc-members: :show-inheritance: eyed3.plugins.classic module ---------------------------- .. automodule:: eyed3.plugins.classic :members: :undoc-members: :show-inheritance: eyed3.plugins.extract module ---------------------------- .. automodule:: eyed3.plugins.extract :members: :undoc-members: :show-inheritance: eyed3.plugins.fixup module -------------------------- .. automodule:: eyed3.plugins.fixup :members: :undoc-members: :show-inheritance: eyed3.plugins.genres module --------------------------- .. automodule:: eyed3.plugins.genres :members: :undoc-members: :show-inheritance: eyed3.plugins.itunes module --------------------------- .. automodule:: eyed3.plugins.itunes :members: :undoc-members: :show-inheritance: eyed3.plugins.jsontag module ---------------------------- .. automodule:: eyed3.plugins.jsontag :members: :undoc-members: :show-inheritance: eyed3.plugins.lameinfo module ----------------------------- .. automodule:: eyed3.plugins.lameinfo :members: :undoc-members: :show-inheritance: eyed3.plugins.lastfm module --------------------------- .. automodule:: eyed3.plugins.lastfm :members: :undoc-members: :show-inheritance: eyed3.plugins.mimetype module ----------------------------- .. automodule:: eyed3.plugins.mimetype :members: :undoc-members: :show-inheritance: eyed3.plugins.nfo module ------------------------ .. automodule:: eyed3.plugins.nfo :members: :undoc-members: :show-inheritance: eyed3.plugins.pymod module -------------------------- .. automodule:: eyed3.plugins.pymod :members: :undoc-members: :show-inheritance: eyed3.plugins.stats module -------------------------- .. automodule:: eyed3.plugins.stats :members: :undoc-members: :show-inheritance: eyed3.plugins.xep\_118 module ----------------------------- .. automodule:: eyed3.plugins.xep_118 :members: :undoc-members: :show-inheritance: eyed3.plugins.yamltag module ---------------------------- .. automodule:: eyed3.plugins.yamltag :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: eyed3.plugins :members: :undoc-members: :show-inheritance: eyeD3-0.9.7/docs/eyed3.rst000066400000000000000000000012041432016011500151610ustar00rootroot00000000000000eyed3 package ============= Subpackages ----------- .. toctree:: :maxdepth: 4 eyed3.id3 eyed3.mp3 eyed3.plugins eyed3.utils Submodules ---------- eyed3.core module ----------------- .. automodule:: eyed3.core :members: :undoc-members: :show-inheritance: eyed3.main module ----------------- .. automodule:: eyed3.main :members: :undoc-members: :show-inheritance: eyed3.mimetype module --------------------- .. automodule:: eyed3.mimetype :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: eyed3 :members: :undoc-members: :show-inheritance: eyeD3-0.9.7/docs/eyed3.utils.rst000066400000000000000000000015651432016011500163320ustar00rootroot00000000000000eyed3.utils package =================== Submodules ---------- eyed3.utils.art module ---------------------- .. automodule:: eyed3.utils.art :members: :undoc-members: :show-inheritance: eyed3.utils.binfuncs module --------------------------- .. automodule:: eyed3.utils.binfuncs :members: :undoc-members: :show-inheritance: eyed3.utils.console module -------------------------- .. automodule:: eyed3.utils.console :members: :undoc-members: :show-inheritance: eyed3.utils.log module ---------------------- .. automodule:: eyed3.utils.log :members: :undoc-members: :show-inheritance: eyed3.utils.prompt module ------------------------- .. automodule:: eyed3.utils.prompt :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: eyed3.utils :members: :undoc-members: :show-inheritance: eyeD3-0.9.7/docs/history.rst000066400000000000000000000000341432016011500156510ustar00rootroot00000000000000.. include:: ../HISTORY.rst eyeD3-0.9.7/docs/index.rst000066400000000000000000000032331432016011500152630ustar00rootroot00000000000000================ Welcome to eyeD3 ================ .. include:: ../README.rst Installation ============ Stable releases of eyeD3 are best installed via ``pip`` or ``easy_install``; or you may download TGZ or ZIP source archives from a couple of official locations. Detailed instructions and links may be found on the :doc:`installation` page. Otherwise, if you want to live on the edge, you can pull down the source code from the Git repository at `GitHub`_. The :doc:`installation` page has details for how to access the source code. .. _GitHub: https://github.com/nicfit/eyeD3 .. toctree:: :hidden: installation .. _documentation-index: Documentation ============= .. toctree:: :maxdepth: 2 cli plugins modules compliance contributing authors .. toctree:: :hidden: history ChangeLog ========= Changes made to eyeD3 and the project's release history can be found in the :doc:`history`. .. _references-index: References ========== - ID3 `v1.x Specification `_ - ID3 v2.4 `Structure `_ and `Frames `_ - ID3 `v2.3 Specification `_ - ID3 `v2.2 Specification `_ - ISO `8601 Date and Time `_ - ISO `639-2 Language Codes `_ - MusicBrainz Tag `Mappings `_ - MP3 `Headers `_ Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` eyeD3-0.9.7/docs/installation.rst000066400000000000000000000043201432016011500166530ustar00rootroot00000000000000============ Installation ============ Easy Installation ================= Install using 'pip' ------------------- *pip* is a tool for installing Python packages from `Python Package Index`_ and is a replacement for *easy_install*. It will install the package using the first 'python' in your path so it is especially useful when used along with `virtualenv`_, otherwise root access may be required. .. code-block:: sh $ pip install eyeD3 # Optional: To install the ultra powerful Display plugin (-P display) $ pip install eyeD3[display-plugin] .. _virtualenv: http://www.virtualenv.org/ .. _Python Package Index: http://pypi.python.org/pypi/eyeD3 Note that on Windows, you also need to install the libmagic binaries. .. code-block:: sh $ pip install python-magic-bin Dependencies ============ eyeD3 |version| has been tested with Python >=3.7. See version 0.8.x for Python 2.7,>=3.3 and version 0.7.x for Python 2.6 support. The primary interface for building and installing is `Setuptools`_. For example, ``python setup.py install``. .. _setuptools: http://pypi.python.org/pypi/setuptools Development Dependencies ------------------------ If you are interested in doing development work on eyeD3 (or even just running the test suite), you may also need to install some additional packages: $ pip install -r requirements/test.txt $ pip install -r requirements/dev.txt Download Source Archive ======================= Source packages are available from the `release archive`_ in tar.gz and zip formats. After un-archiving the distribution file you can install in the common manner: .. code-block:: sh $ tar xzf eyeD3-X.Y.Z.tar.gz $ cd eyeD3-X.Y.Z # This may require root access $ python setup.py install Or you can run from the archive directory directly: .. code-block:: sh $ tar xzf eyeD3-X.Y.Z.tar.gz $ cd eyeD3-X.Y.Z $ python setup.py build $ export PYTHONPATH=`pwd`/build/lib $ export PATH=${PATH}:`pwd`/bin .. _release archive: http://eyed3.nicfit.net/releases/ Checking Out the Source Code ============================ .. code-block:: sh $ git clone https://github.com/nicfit/eyeD3.git .. note:: When submitting patches please base them on the 'master' branch. eyeD3-0.9.7/docs/make.bat000066400000000000000000000144711432016011500150350ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\eyed3.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\eyed3.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end eyeD3-0.9.7/docs/modules.rst000066400000000000000000000001021432016011500156140ustar00rootroot00000000000000eyed3 module ============ .. toctree:: :maxdepth: 4 eyed3 eyeD3-0.9.7/docs/plugins.rst000066400000000000000000000006071432016011500156370ustar00rootroot00000000000000Plugins -------- .. toctree:: :maxdepth: 1 plugins/art_plugin plugins/classic_plugin plugins/extract_plugin plugins/fixup_plugin plugins/itunes_plugin plugins/json_plugin plugins/genres_plugin plugins/lameinfo_plugin plugins/mimetypes_plugin plugins/nfo_plugin plugins/pymod_plugin plugins/stats_plugin plugins/xep118_plugin plugins/yaml_plugin eyeD3-0.9.7/docs/plugins/000077500000000000000000000000001432016011500151025ustar00rootroot00000000000000eyeD3-0.9.7/docs/plugins/art_plugin.rst000066400000000000000000000007221432016011500200010ustar00rootroot00000000000000art(work) plugin ================= .. {{{cog .. cog.out(cog_pluginHelp("art")) .. }}} *Art for albums, artists, etc.* Names ----- art Description ----------- Options ------- .. code-block:: text -F, --update-files Write art files from tag images. -T, --update-tags Write tag image from art files. -D, --download Attempt to download album art if missing. -v, --verbose Show detailed information for all art found. .. {{{end}}} eyeD3-0.9.7/docs/plugins/classic_plugin.rst000066400000000000000000000433451432016011500206440ustar00rootroot00000000000000classic - Tag Viewer/Editor ============================ .. {{{cog .. cog.out(cog_pluginHelp("classic")) .. }}} *Classic eyeD3 interface for viewing and editing tags.* Names ----- classic Description ----------- All PATH arguments are parsed and displayed. Directory paths are searched recursively. Any editing options (--artist, --title) are applied to each file read. All date options (-Y, --release-year excepted) follow ISO 8601 format. This is ``yyyy-mm-ddThh:mm:ss``. The year is required, and each component thereafter is optional. For example, 2012-03 is valid, 2012--12 is not. Options ------- .. code-block:: text --release-year excepted) follow ISO 8601 format. This is ``yyyy-mm-ddThh:mm:ss``. The year is required, and each component thereafter is optional. For example, 2012-03 is valid, 2012--12 is not. -a STRING, --artist STRING Set the artist name. -A STRING, --album STRING Set the album name. -b STRING, --album-artist STRING Set the album artist name. 'Various Artists', for example. Another example is collaborations when the track artist might be 'Eminem featuring Proof' the album artist would be 'Eminem'. -t STRING, --title STRING Set the track title. -n NUM, --track NUM Set the track number. Use 0 to clear. -N NUM, --track-total NUM Set total number of tracks. Use 0 to clear. --track-offset N Increment/decrement the track number by [-]N. This option is applied after --track=N is set. --composer STRING Set the composer's name. --orig-artist STRING Set the orignal artist's name. For example, a cover song can include the orignal author of the track. -d NUM, --disc-num NUM Set the disc number. Use 0 to clear. -D NUM, --disc-total NUM Set total number of discs in set. Use 0 to clear. -G GENRE, --genre GENRE Set the genre. If the argument is a standard ID3 genre name or number both will be set. Otherwise, any string can be used. Run 'eyeD3 --plugin=genres' for a list of standard ID3 genre names/ids. --non-std-genres Disables certain ID3 genre standards, such as the mapping of numeric value to genre names. For example, genre=1 is taken literally, not mapped to 'Classic Rock'. -Y YEAR, --release-year YEAR Set the year the track was released. Use the date options for more precise values or dates other than release. -c STRING, --comment STRING Set a comment. In ID3 tags this is the comment with an empty description. See --add-comment to add multiple comment frames. --artist-city STRING The artist's city of origin. Stored as a user text frame `eyeD3#artist_origin` --artist-state STRING The artist's state of origin. Stored as a user text frame `eyeD3#artist_origin` --artist-country STRING The artist's country of origin. Stored as a user text frame `eyeD3#artist_origin` --rename PATTERN Rename file (the extension is not affected) based on data in the tag using substitution variables: $album, $album_artist, $artist, $best_date, $best_date:prefer_recording, $best_date:prefer_recording:year, $best_date:prefer_release, $best_date:prefer_release:year, $best_date:year, $disc:num, $disc:total, $file, $file:ext, $original_release_date, $original_release_date:year, $recording_date, $recording_date:year, $release_date, $release_date:year, $title, $track:num, $track:total ID3 options: -1, --v1 Only read and write ID3 v1.x tags. By default, v1.x tags are only read or written if there is not a v2 tag in the file. -2, --v2 Only read/write ID3 v2.x tags. This is the default unless the file only contains a v1 tag. --to-v1.1 Convert the file's tag to ID3 v1.1 (Or 1.0 if there is no track number) --to-v2.3 Convert the file's tag to ID3 v2.3 --to-v2.4 Convert the file's tag to ID3 v2.4 --release-date DATE Set the date the track/album was released --orig-release-date DATE Set the original date the track/album was released --recording-date DATE Set the date the track/album was recorded --encoding-date DATE Set the date the file was encoded --tagging-date DATE Set the date the file was tagged --publisher STRING Set the publisher/label name --play-count <+>N Set the number of times played counter. If the argument value begins with '+' the tag's play count is incremented by N, otherwise the value is set to exactly N. --bpm N Set the beats per minute value. --unique-file-id OWNER_ID:ID Add a unique file ID frame. If the ID arg is empty the frame is removed. An OWNER_ID is required. The ID may be no more than 64 bytes. --add-comment COMMENT[:DESCRIPTION[:LANG]] Add or replace a comment. There may be more than one comment in a tag, as long as the DESCRIPTION and LANG values are unique. The default DESCRIPTION is '' and the default language code is 'eng'. --remove-comment DESCRIPTION[:LANG] Remove comment matching DESCRIPTION and LANG. The default language code is 'eng'. --remove-all-comments Remove all comments from the tag. --remove-all-unknown Remove all unknown frames from the tag. --add-lyrics LYRICS_FILE[:DESCRIPTION[:LANG]] Add or replace a lyrics. There may be more than one set of lyrics in a tag, as long as the DESCRIPTION and LANG values are unique. The default DESCRIPTION is '' and the default language code is 'eng'. --remove-lyrics DESCRIPTION[:LANG] Remove lyrics matching DESCRIPTION and LANG. The default language code is 'eng'. --remove-all-lyrics Remove all lyrics from the tag. --text-frame FID:TEXT Set the value of a text frame. To remove the frame, specify an empty value. For example, --text- frame='TDRC:' --user-text-frame DESC:TEXT Set the value of a user text frame (i.e., TXXX). To remove the frame, specify an empty value. e.g., --user-text-frame='SomeDesc:' --url-frame FID:URL Set the value of a URL frame. To remove the frame, specify an empty value. e.g., --url- frame='WCOM:' --user-url-frame DESCRIPTION:URL Set the value of a user URL frame (i.e., WXXX). To remove the frame, specify an empty value. e.g., --user-url-frame='SomeDesc:' --add-image IMG_PATH:TYPE[:DESCRIPTION] Add or replace an image. There may be more than one image in a tag, as long as the DESCRIPTION values are unique. The default DESCRIPTION is ''. If PATH begins with 'http[s]://' then it is interpreted as a URL instead of a file containing image data. The TYPE must be one of the following: OTHER, ICON, OTHER_ICON, FRONT_COVER, BACK_COVER, LEAFLET, MEDIA, LEAD_ARTIST, ARTIST, CONDUCTOR, BAND, COMPOSER, LYRICIST, RECORDING_LOCATION, DURING_RECORDING, DURING_PERFORMANCE, VIDEO, BRIGHT_COLORED_FISH, ILLUSTRATION, BAND_LOGO, PUBLISHER_LOGO. --remove-image DESCRIPTION Remove image matching DESCRIPTION. --remove-all-images Remove all images from the tag --write-images DIR Causes all attached images (APIC frames) to be written to the specified directory. --add-object OBJ_PATH:MIME-TYPE[:DESCRIPTION[:FILENAME]] Add or replace an object. There may be more than one object in a tag, as long as the DESCRIPTION values are unique. The default DESCRIPTION is ''. --remove-object DESCRIPTION Remove object matching DESCRIPTION. --write-objects DIR Causes all attached objects (GEOB frames) to be written to the specified directory. --remove-all-objects Remove all objects from the tag --add-popularity EMAIL:RATING[:PLAY_COUNT] Adds a pupularity metric. There may be multiples popularity values, but each must have a unique email address component. The rating is a number between 0 (worst) and 255 (best). The play count is optional, and defaults to 0, since there is already a dedicated play count frame. --remove-popularity EMAIL Removes the popularity frame with the specified email key. --remove-v1 Remove ID3 v1.x tag. --remove-v2 Remove ID3 v2.x tag. --remove-all Remove ID3 v1.x and v2.x tags. --remove-frame FID Remove all frames with the given ID. This option may be specified multiple times. --max-padding NUM_BYTES Shrink file if tag padding (unused space) exceeds the given number of bytes. (Useful e.g. after removal of large cover art.) Default is 64 KiB, file will be rewritten with default padding (1 KiB) or max padding, whichever is smaller. --no-max-padding Disable --max-padding altogether. --encoding latin1|utf8|utf16|utf16-be Set the encoding that is used for all text frames. This option is only applied if the tag is updated as the result of an edit option (e.g. --artist, --title, etc.) or --force-update is specified. Misc options: --force-update Rewrite the tag despite there being no edit options. -v, --verbose Show all available tag data --preserve-file-times When writing, do not update file modification times. .. {{{end}}} Examples -------- eyeD3 can do more than edit exiting tags, it can also create new tags from nothing. For these examples we'll make a dummy file to work with. .. {{{cog cli_example("examples/cli_examples.sh", "SETUP", lang="bash") }}} .. code-block:: bash $ rm -f example.id3 $ touch example.id3 $ ls -s example.id3 0 example.id3 .. {{{end}}} Now let's set some common attributes like artist and title. .. {{{cog cli_example("examples/cli_examples.sh", "ART_TIT_SET", lang="bash") }}} .. {{{end}}} Most options have a shorter name that can be used to save typing. Let's add the album name (``-A``), the genre (``-G``), and the year (``-Y``) the record was released. .. {{{cog cli_example("examples/cli_examples.sh", "ALB_YR_G_SET", lang="bash") }}} .. {{{end}}} Notice how the genre displayed as "Hardcore (id 129)" in the above tag listing. This happens because the genre is a recognized value as defined by the ID3 v1 standard. eyeD3 used to be very strict about genres, but no longer. You can store any value you'd like. For a list of recognized genres and their respective IDs see the `genres plugin `_. .. {{{cog cli_example("examples/cli_examples.sh", "NONSTD_GENRE_SET", lang="bash") }}} .. {{{end}}} By default writes ID3 v2.4 tags. This is the latest standard and supports UTF-8 which is a very nice thing. Some players are not caught up with the latest standards (iTunes, pfft) so it may be necessary to convert amongst the various versions. In some cases this can be a lossy operation if a certain data field is not supported, but eyeD3 does its best to convert when the data whenever possible. .. {{{cog cli_example("examples/cli_examples.sh", "CONVERT1", lang="bash") }}} .. code-block:: bash # Convert the current v2.4 frame to v2.3 $ eyeD3 --to-v2.3 example.id3 -Q .../home/travis/devel/eyeD3/example.id3[ 0.00 Bytes ] ------------------------- ID3 v2.4: 0 frames Writing ID3 version v2.3 ------------------------- # Convert back $ eyeD3 --to-v2.4 example.id3 -Q .../home/travis/devel/eyeD3/example.id3[ 266.00 Bytes ] ------------------------- ID3 v2.3: 0 frames Writing ID3 version v2.4 ------------------------- # Convert to v1, this will lose all the more advanced data members ID3 v2 offers $ eyeD3 --to-v1.1 example.id3 -Q .../home/travis/devel/eyeD3/example.id3[ 266.00 Bytes ] ------------------------- ID3 v2.4: 0 frames Writing ID3 version v1.1 ------------------------- .. {{{end}}} The last conversion above converted to v1.1, or so the output says. The final listing shows that the tag is version 2.4. This is because tags can contain both versions at once and eyeD3 will always show/load v2 tags first. To select the version 1 tag use the ``-1`` option. Also note how the the non-standard genre was lost by the conversion, thankfully it is still in the v2 tag. .. {{{cog cli_example("examples/cli_examples.sh", "DISPLAY_V1", lang="bash") }}} .. code-block:: bash $ eyeD3 -1 example.id3 .../home/travis/devel/eyeD3/example.id3[ 394.00 Bytes ] ------------------------- ID3 v1.0: title: artist: album: track: genre: Other (id 12) ------------------------- .. {{{end}}} The ``-1`` and ``-2`` options also determine which tag will be edited, or even which tag will be converted when one of the conversion options is passed. .. {{{cog cli_example("examples/cli_examples.sh", "SET_WITH_VERSIONS", lang="bash") }}} .. code-block:: bash # Set an artist value in the ID3 v1 tag $ eyeD3 -1 example.id3 -a id3v1 .../home/travis/devel/eyeD3/example.id3[ 394.00 Bytes ] ------------------------- Setting artist: id3v1 ID3 v1.0: title: artist: id3v1 album: track: genre: Other (id 12) Writing ID3 version v1.0 ------------------------- # The file now has a v1 and v2 tag, change the v2 artist $ eyeD3 -2 example.id3 -a id3v2 .../home/travis/devel/eyeD3/example.id3[ 394.00 Bytes ] ------------------------- Setting artist: id3v2 ID3 v2.4: title: artist: id3v2 album: track: Writing ID3 version v2.4 ------------------------- # Take all the values from v2.4 tag (the default) and set them in the v1 tag. $ eyeD3 -2 --to-v1.1 example.id3 .../home/travis/devel/eyeD3/example.id3[ 394.00 Bytes ] ------------------------- ID3 v2.4: title: artist: id3v2 album: track: Writing ID3 version v1.1 ------------------------- # Take all the values from v1 tag and convert to ID3 v2.3 $ eyeD3 -1 --to-v2.3 example.id3 .../home/travis/devel/eyeD3/example.id3[ 394.00 Bytes ] ------------------------- ID3 v1.0: title: artist: id3v2 album: track: genre: Other (id 12) Writing ID3 version v2.3 ------------------------- .. {{{end}}} At this point the tag is all messed up with by these experiments, you can always remove the tags to start again. .. {{{cog cli_example("examples/cli_examples.sh", "REMOVE_ALL_TAGS", lang="bash") }}} .. code-block:: bash $ eyeD3 --remove-all example.id3 .../home/travis/devel/eyeD3/example.id3[ 394.00 Bytes ] ------------------------- Removing ID3 v1.x and/or v2.x tag: SUCCESS No ID3 v1.x/v2.x tag found! .. {{{end}}} Complex Options --------------- Some of the command line options contain multiple pieces of information in a single value. Take for example the ``--add-image`` option:: --add-image IMG_PATH:TYPE[:DESCRIPTION] This option has 3 pieced of information where one (DESCRIPTION) is optional (denoted by the square brackets). Each individual value is seprated by a ':' like so: .. code-block:: bash $ eyeD3 --add-image cover.png:FRONT_COVER This will load the image data from ``cover.png`` and store it in the tag with the type value for FRONT_COVER images. The list of valid image types are listed in the ``--help`` usage information which also states that the IMG_PATH value may be a URL so that the image data does not have to be stored in the the tag itself. Let's try that now. .. code-block:: bash $ eyeD3 --add-image http://example.com/cover.jpg:FRONT_COVER eyeD3: error: argument --add-image: invalid ImageArg value: 'http://example.com/cover.jpg:FRONT_COVER' The problem is the ``':'`` character in the the URL, it confuses the format description of the option value. To solve this escape all delimeter characters in option values with ``'\\'`` (for linux and macOS), single ``'\'`` for Windows). Linux/MacOS: .. {{{cog cli_example("examples/cli_examples.sh", "IMG_URL", lang="bash") }}} .. code-block:: bash $ eyeD3 --add-image http\\://example.com/cover.jpg:FRONT_COVER example.id3 .../home/travis/devel/eyeD3/example.id3[ 0.00 Bytes ] ------------------------- Adding image http://example.com/cover.jpg ID3 v2.4: title: artist: album: track: FRONT_COVER Image: [Type: -->] [URL: b'http://example.com/cover.jpg'] Description: Writing ID3 version v2.4 ------------------------- .. {{{end}}} Windows: .. {{{cog cli_example("examples/cli_examples.sh", "IMG_URL", lang="bash") }}} .. code-block:: bash $ eyeD3 --add-image http\\://example.com/cover.jpg:FRONT_COVER example.id3 .../home/travis/devel/eyeD3/example.id3[ 311.00 Bytes ] ------------------------- Adding image http://example.com/cover.jpg ID3 v2.4: title: artist: album: track: FRONT_COVER Image: [Type: -->] [URL: b'http://example.com/cover.jpg'] Description: Writing ID3 version v2.4 ------------------------- .. {{{end}}} eyeD3-0.9.7/docs/plugins/extract_plugin.rst000066400000000000000000000007121432016011500206640ustar00rootroot00000000000000Extract Plugin =============== .. {{{cog .. cog.out(cog_pluginHelp("extract")) .. }}} *Extract tags from audio files.* Names ----- extract Description ----------- Options ------- .. code-block:: text -o OUTPUT_FILE, --output-file OUTPUT_FILE The the tag is written to this file in native format. -H, --hex Output hexadecimal format. --strip-padding Exclude tag padding, if any. .. {{{end}}} eyeD3-0.9.7/docs/plugins/fixup_plugin.rst000066400000000000000000000150231432016011500203460ustar00rootroot00000000000000fixup - Music directory fixer ============================= .. {{{cog .. cog.out(cog_pluginHelp("fixup")) .. }}} *Performs various checks and fixes to directories of audio files.* Names ----- fixup Description ----------- Operates on directories at a time, fixing each as a unit (album, compilation, live set, etc.). All of these should have common dates, for example but other characteristics may vary. The ``--type`` should be used whenever possible, ``lp`` is the default. The following test and fixes always apply: 1. Every file will be given an ID3 tag if one is missing. 2. Set ID3 v2.4. 3. Set a consistent album name for all files in the directory. 4. Set a consistent artist name for all files, unless the type is ``various`` in which case the artist may vary (but must exist). 5. Ensure each file has a title. 6. Ensure each file has a track # and track total. 7. Ensure all files have a release and original release date, unless the type is ``live`` in which case the recording date is set. 8. All ID3 frames of the following types are removed: USER, PRIV 9. All ID3 files have TLEN (track length in ms) set (or updated). 10. The album/dir type is set in the tag. Types of ``lp`` and ``various`` do not have this field set since the latter is the default and the former can be determined during sync. In ID3 terms the value is in TXXX (description: ``eyeD3#album_type``). 11. Files are renamed as follows: - Type ``various``: ${track:num} - ${artist} - ${title} - Type ``single``: ${artist} - ${title} - All other types: ${artist} - ${track:num} - ${title} - A rename template can be supplied in --file-rename-pattern 12. Directories are renamed as follows: - Type ``live``: ${best_date:prefer_recording} - ${album} - All other types: ${best_date:prefer_release} - ${album} - A rename template can be supplied in --dir-rename-pattern Album types: - ``lp``: A traditinal "album" of songs from a single artist. No extra info is written to the tag since this is the default. - ``ep``: A short collection of songs from a single artist. The string 'ep' is written to the tag's ``eyeD3#album_type`` field. - ``various``: A collection of songs from different artists. The string 'various' is written to the tag's ``eyeD3#album_type`` field. - ``live``: A collection of live recordings from a single artist. The string 'live' is written to the tag's ``eyeD3#album_type`` field. - ``compilation``: A collection of songs from various recordings by a single artist. The string 'compilation' is written to the tag's ``eyeD3#album_type`` field. Compilation dates, unlike other types, may differ. - ``demo``: A demo recording by a single artist. The string 'demo' is written to the tag's ``eyeD3#album_type`` field. - ``single``: A track that should no be associated with an album (even if it has album metadata). The string 'single' is written to the tag's ``eyeD3#album_type`` field. Options ------- .. code-block:: text - Type ``various``: ${track:num} - ${artist} - ${title} - Type ``single``: ${artist} - ${title} - All other types: ${artist} - ${track:num} - ${title} - A rename template can be supplied in --file-rename-pattern 12. Directories are renamed as follows: - Type ``live``: ${best_date:prefer_recording} - ${album} - All other types: ${best_date:prefer_release} - ${album} - A rename template can be supplied in --dir-rename-pattern Album types: - ``lp``: A traditinal "album" of songs from a single artist. No extra info is written to the tag since this is the default. - ``ep``: A short collection of songs from a single artist. The string 'ep' is written to the tag's ``eyeD3#album_type`` field. - ``various``: A collection of songs from different artists. The string 'various' is written to the tag's ``eyeD3#album_type`` field. - ``live``: A collection of live recordings from a single artist. The string 'live' is written to the tag's ``eyeD3#album_type`` field. - ``compilation``: A collection of songs from various recordings by a single artist. The string 'compilation' is written to the tag's ``eyeD3#album_type`` field. Compilation dates, unlike other types, may differ. - ``demo``: A demo recording by a single artist. The string 'demo' is written to the tag's ``eyeD3#album_type`` field. - ``single``: A track that should no be associated with an album (even if it has album metadata). The string 'single' is written to the tag's ``eyeD3#album_type`` field. --type {lp,ep,compilation,live,various,demo,single} How to treat each directory. The default is 'lp', although you may be prompted for an alternate choice if the files look like another type. --fix-case Fix casing on each string field by capitalizing each word. -n, --dry-run Only print the operations that would take place, but do not execute them. --no-prompt Exit if prompted. --dotted-dates Separate date with '.' instead of '-' when naming directories. --file-rename-pattern FILE_RENAME_PATTERN Rename file (the extension is not affected) based on data in the tag using substitution variables: $album, $album_artist, $artist, $best_date, $best_date:prefer_recording, $best_date:prefer_recording:year, $best_date:prefer_release, $best_date:prefer_release:year, $best_date:year, $disc:num, $disc:total, $file, $file:ext, $original_release_date, $original_release_date:year, $recording_date, $recording_date:year, $release_date, $release_date:year, $title, $track:num, $track:total --dir-rename-pattern DIR_RENAME_PATTERN Rename directory based on data in the tag using substitution variables: $album, $album_artist, $artist, $best_date, $best_date:prefer_recording, $best_date:prefer_recording:year, $best_date:prefer_release, $best_date:prefer_release:year, $best_date:year, $disc:num, $disc:total, $file, $file:ext, $original_release_date, $original_release_date:year, $recording_date, $recording_date:year, $release_date, $release_date:year, $title, $track:num, $track:total --no-dir-rename Do not rename the directory. .. {{{end}}} eyeD3-0.9.7/docs/plugins/genres_plugin.rst000066400000000000000000000137471432016011500205110ustar00rootroot00000000000000genres - ID3 Genre List ======================= .. {{{cog .. cog.out(cog_pluginHelp("genres")) .. }}} *Display the full list of standard ID3 genres.* Names ----- genres Description ----------- ID3 v1 defined a list of genres and mapped them to to numeric values so they can be stored as a single byte. It is *recommended* that these genres are used although most newer software (including eyeD3) does not care. Options ------- .. code-block:: text -1, --single-column List on genre per line. .. {{{end}}} Example ------- .. {{{cog cli_example("examples/cli_examples.sh", "GENRES_PLUGIN1", lang="bash") }}} .. code-block:: bash $ eyeD3 --plugin=genres 0: Blues 96: Big Band 1: Classic Rock 97: Chorus 2: Country 98: Easy Listening 3: Dance 99: Acoustic 4: Disco 100: Humour 5: Funk 101: Speech 6: Grunge 102: Chanson 7: Hip-Hop 103: Opera 8: Jazz 104: Chamber Music 9: Metal 105: Sonata 10: New Age 106: Symphony 11: Oldies 107: Booty Bass 12: Other 108: Primus 13: Pop 109: Porn Groove 14: R&B 110: Satire 15: Rap 111: Slow Jam 16: Reggae 112: Club 17: Rock 113: Tango 18: Techno 114: Samba 19: Industrial 115: Folklore 20: Alternative 116: Ballad 21: Ska 117: Power Ballad 22: Death Metal 118: Rhythmic Soul 23: Pranks 119: Freestyle 24: Soundtrack 120: Duet 25: Euro-Techno 121: Punk Rock 26: Ambient 122: Drum Solo 27: Trip-Hop 123: A Cappella 28: Vocal 124: Euro-House 29: Jazz+Funk 125: Dance Hall 30: Fusion 126: Goa 31: Trance 127: Drum & Bass 32: Classical 128: Club-House 33: Instrumental 129: Hardcore 34: Acid 130: Terror 35: House 131: Indie 36: Game 132: BritPop 37: Sound Clip 133: Negerpunk 38: Gospel 134: Polsk Punk 39: Noise 135: Beat 40: AlternRock 136: Christian Gangsta Rap 41: Bass 137: Heavy Metal 42: Soul 138: Black Metal 43: Punk 139: Crossover 44: Space 140: Contemporary Christian 45: Meditative 141: Christian Rock 46: Instrumental Pop 142: Merengue 47: Instrumental Rock 143: Salsa 48: Ethnic 144: Thrash Metal 49: Gothic 145: Anime 50: Darkwave 146: JPop 51: Techno-Industrial 147: Synthpop 52: Electronic 148: Abstract 53: Pop-Folk 149: Art Rock 54: Eurodance 150: Baroque 55: Dream 151: Bhangra 56: Southern Rock 152: Big Beat 57: Comedy 153: Breakbeat 58: Cult 154: Chillout 59: Gangsta Rap 155: Downtempo 60: Top 40 156: Dub 61: Christian Rap 157: EBM 62: Pop / Funk 158: Eclectic 63: Jungle 159: Electro 64: Native American 160: Electroclash 65: Cabaret 161: Emo 66: New Wave 162: Experimental 67: Psychedelic 163: Garage 68: Rave 164: Global 69: Showtunes 165: IDM 70: Trailer 166: Illbient 71: Lo-Fi 167: Industro-Goth 72: Tribal 168: Jam Band 73: Acid Punk 169: Krautrock 74: Acid Jazz 170: Leftfield 75: Polka 171: Lounge 76: Retro 172: Math Rock 77: Musical 173: New Romantic 78: Rock & Roll 174: Nu-Breakz 79: Hard Rock 175: Post-Punk 80: Folk 176: Post-Rock 81: Folk-Rock 177: Psytrance 82: National Folk 178: Shoegaze 83: Swing 179: Space Rock 84: Fast Fusion 180: Trop Rock 85: Bebob 181: World Music 86: Latin 182: Neoclassical 87: Revival 183: Audiobook 88: Celtic 184: Audio Theatre 89: Bluegrass 185: Neue Deutsche Welle 90: Avantgarde 186: Podcast 91: Gothic Rock 187: Indie Rock 92: Progressive Rock 188: G-Funk 93: Psychedelic Rock 189: Dubstep 94: Symphonic Rock 190: Garage Rock 95: Slow Rock 191: Psybient .. {{{end}}} eyeD3-0.9.7/docs/plugins/itunes_plugin.rst000066400000000000000000000020101432016011500205120ustar00rootroot00000000000000itunes-podcast - Convert files so iTunes recognizes them as podcasts ==================================================================== .. {{{cog .. cog.out(cog_pluginHelp("itunes-podcast")) .. }}} *Adds (or removes) the tags necessary for Apple iTunes to identify the file as a podcast.* Names ----- itunes-podcast Description ----------- Options ------- .. code-block:: text --add Add the podcast frames. --remove Remove the podcast frames. .. {{{end}}} Example ------- .. {{{cog cli_example("examples/cli_examples.sh", "ITUNES_PODCAST_PLUGIN", lang="bash") }}} .. code-block:: bash $ eyeD3 -P itunes-podcast example.id3 /home/travis/devel/eyeD3/example.id3 iTunes podcast? :-( $ eyeD3 -P itunes-podcast example.id3 --add /home/travis/devel/eyeD3/example.id3 iTunes podcast? :-( Adding... iTunes podcast? :-) $ eyeD3 -P itunes-podcast example.id3 --remove /home/travis/devel/eyeD3/example.id3 iTunes podcast? :-) Removing... iTunes podcast? :-( .. {{{end}}} eyeD3-0.9.7/docs/plugins/json_plugin.rst000066400000000000000000000005151432016011500201640ustar00rootroot00000000000000JSON Plugin =============== .. {{{cog .. cog.out(cog_pluginHelp("json")) .. }}} *Outputs all tags as JSON.* Names ----- json Description ----------- Options ------- .. code-block:: text -c, --compact Output in compact form, wound new lines or indentation. -s, --sort Output JSON in sorted by key. .. {{{end}}} eyeD3-0.9.7/docs/plugins/lameinfo_plugin.rst000066400000000000000000000027701432016011500210120ustar00rootroot00000000000000lameinfo (xing) - Lame (Xing) Header Information ================================================ .. {{{cog .. cog.out(cog_pluginHelp("lameinfo")) .. }}} *Outputs lame header (if one exists) for file.* Names ----- lameinfo (aliases: xing) Description ----------- The 'lame' (or xing) header provides extra information about the mp3 that is useful to players and encoders but not officially part of the mp3 specification. Variable bit rate mp3s, for example, use this header. For more details see `here `_ Options ------- .. code-block:: text No extra options supported .. {{{end}}} Example ------- .. {{{cog cli_example("examples/cli_examples.sh", "LAME_PLUGIN", lang="bash") }}} .. code-block:: bash $ eyeD3 -P lameinfo tests/data/notag-vbr.mp3 .../home/travis/devel/eyeD3/tests/data/notag-vbr.mp3[ 5.98 MB ] ------------------------- Encoder Version : LAME3.91 LAME Tag Revision : 0 VBR Method : Variable Bitrate method2 (mtrh) Lowpass Filter : 19500 Encoding Flags : --nspsytune ATH Type : 3 Bitrate (Minimum) : 0 Encoder Delay : 576 samples Encoder Padding : 1848 samples Noise Shaping : 1 Stereo Mode : Joint Unwise Settings : False Sample Frequency : 44.1 kHz MP3 Gain : 0 (+0.0 dB) Preset : Unknown Surround Info : None Music Length : 5.98 MB Music CRC-16 : 675C LAME Tag CRC-16 : 5B62 .. {{{end}}} eyeD3-0.9.7/docs/plugins/mimetypes_plugin.rst000066400000000000000000000004701432016011500212270ustar00rootroot00000000000000Mime-types Plugin ================== .. {{{cog .. cog.out(cog_pluginHelp("mimetypes")) .. }}} *eyeD3 plugin* Names ----- mimetypes Description ----------- Options ------- .. code-block:: text --status Print dot status. --parse-files Parse each file. --hide-notfound .. {{{end}}} eyeD3-0.9.7/docs/plugins/nfo_plugin.rst000066400000000000000000000026651432016011500200050ustar00rootroot00000000000000nfo - (I)NFO File Generator =========================== .. {{{cog .. cog.out(cog_pluginHelp("nfo")) .. }}} *Create NFO files for each directory scanned.* Names ----- nfo Description ----------- Each directory scanned is treated as an album and a `NFO `_ file is written to standard out. NFO files are often found in music archives. Options ------- .. code-block:: text No extra options supported .. {{{end}}} Example ------- .. code-block:: bash $ eyeD3 -P nfo ~/music/Nine\ Inch\ Nails/1992\ -\ Broken/ :: Artist : Nine Inch Nails Album : Broken Released : 1992 Genre : Noise Source : Encoder : LAME3.95 Codec : mp3 Bitrate : ~167 K/s @ 44100 Hz, Joint stereo Tag : ID3 v2.3 Ripped By: Track Listing ------------- 1. Pinion (01:02) 2. Wish (03:46) 3. Last (04:44) 4. Help Me I am in Hell (01:56) 5. Happiness in Slavery (05:21) 6. Gave Up (04:08) 7. Physical (You're So) (05:29) 8. Suck (05:07) Total play time : 31:33 Total size : 37.74 MB ============================================================================== .NFO file created with eyeD3 0.7.0 on Tue Oct 23 23:44:27 2012 For more information about eyeD3 go to http://eyeD3.nicfit.net/ ============================================================================== eyeD3-0.9.7/docs/plugins/pymod_plugin.rst000066400000000000000000000021551432016011500203450ustar00rootroot00000000000000pymod - Use simple python modules as eyeD3 plugins ================================================== .. {{{cog .. cog.out(cog_pluginHelp("pymod")) .. }}} *Imports a Python module file and calls its functions for the the various plugin events.* Names ----- pymod Description ----------- If no module if provided a file named eyeD3mod.py in the current working directory is imported. If any of the following methods exist they still be invoked: def audioFile(audio_file): """Invoked for every audio file that is encountered. The ``audio_file`` is of type ``eyed3.core.AudioFile``; currently this is the concrete type ``eyed3.mp3.Mp3AudioFile``.""" pass def audioDir(d, audio_files, images): """This function is invoked for any directory (``d``) that contains audio (``audio_files``) or image (``images``) media.""" pass def done(): """This method is invoke before successful exit.""" pass Options ------- .. code-block:: text -m MODULE, --module MODULE The Python module module to invoke. The default is ./eyeD3mod.py .. {{{end}}} Example ------- TODO eyeD3-0.9.7/docs/plugins/stats_plugin.rst000066400000000000000000000005271432016011500203540ustar00rootroot00000000000000stats - Music Collection Statistics =================================== .. {{{cog .. cog.out(cog_pluginHelp("stats")) .. }}} *Computes statistics for all audio files scanned.* Names ----- stats Description ----------- Options ------- .. code-block:: text --verbose Show details for each file with rule violations. .. {{{end}}} eyeD3-0.9.7/docs/plugins/xep118_plugin.rst000066400000000000000000000006011432016011500202350ustar00rootroot00000000000000xep-118 - Jabber (XMPP) Tune Format =================================== .. {{{cog .. cog.out(cog_pluginHelp("xep-118")) .. }}} *Outputs all tags in XEP-118 XML format. (see: http://xmpp.org/extensions/xep-0118.html)* Names ----- xep-118 Description ----------- Options ------- .. code-block:: text --no-pretty-print Output without new lines or indentation. .. {{{end}}} eyeD3-0.9.7/docs/plugins/yaml_plugin.rst000066400000000000000000000003551432016011500201570ustar00rootroot00000000000000YAML Plugin =============== .. {{{cog .. cog.out(cog_pluginHelp("yaml")) .. }}} *Outputs all tags as YAML.* Names ----- yaml Description ----------- Options ------- .. code-block:: text No extra options supported .. {{{end}}} eyeD3-0.9.7/docs/readme.rst000066400000000000000000000000451432016011500154070ustar00rootroot00000000000000:orphan: .. include:: ../README.rst eyeD3-0.9.7/docs/usage.rst000066400000000000000000000001211432016011500152510ustar00rootroot00000000000000:orphan: ======== Usage ======== To use eyeD3 in a project:: import eyed3 eyeD3-0.9.7/etc/000077500000000000000000000000001432016011500132445ustar00rootroot00000000000000eyeD3-0.9.7/etc/eyeD3.bash000066400000000000000000000015421432016011500150560ustar00rootroot00000000000000# bash-completion for 'eyeD3' unset __EYED3_COMPLETION_LONG_OPT function _eyeD3_completion() { [[ -e `which eyeD3 2> /dev/null` ]] || return 0 # Variables to hold the current word and possible matches local cur="${COMP_WORDS[COMP_CWORD]}" local opts=() case "${cur}" in -*) if [[ -z "${__EYED3_COMPLETION_LONG_OPT}" ]]; then export __EYED3_COMPLETION_LONG_OPT=$( eyeD3 --help | egrep -o " \-[A-Za-z0-9_\.\-]+\=?" | sort -u) fi opts="${__EYED3_COMPLETION_LONG_OPT}" ;; *) ;; esac # Set possible completions COMPREPLY=($(compgen -W "${opts}" -- ${cur})) } complete -o default -o nospace -F _eyeD3_completion eyeD3 alias eyeD3-fixup='eyeD3 -P fixup' alias eyeD3-stats='eyeD3 -P stats' alias eyeD3-art='eyeD3 -P art' eyeD3-0.9.7/etc/mycog.py000077500000000000000000000164561432016011500147530ustar00rootroot00000000000000#!/usr/bin/env python import sys import cogapp from pathlib import Path from paver.easy import sh options = { 'cog': {'beginspec': '{{{cog', 'endoutput': '{{{end}}}', 'endspec': '}}}', 'includedir': str(Path.cwd())}, 'dry_run': None, 'sphinx': {'builddir': '_build', 'builder': 'html', 'docroot': 'docs', 'template_args': {}}} _default_include_marker = dict( py="# " ) def _cogsh(cog): """The sh command used within cog. Runs the command (unless it's a dry run) and inserts the output into the cog output if insert_output is True.""" def shfunc(command, insert_output=True): output = sh(command, capture=insert_output) if insert_output: cog.cogmodule.out(output) return shfunc class Includer(object): """Looks up SectionedFiles relative to the basedir. When called with a filename and an optional section, the Includer will: 1. look up that file relative to the basedir in a cache 2. load it as a SectionedFile if it's not in the cache 3. return the whole file if section is None 4. return just the section desired if a section is requested If a cog object is provided at initialization, the text will be output (via cog's out) rather than returned as a string. You can pass in include_markers which is a dictionary that maps file extensions to the single line comment character for that file type. If there is an include marker available, then output like: # section 'sectionname' from 'file.py' There are some default include markers. If you don't pass in anything, no include markers will be displayed. If you pass in an empty dictionary, the default ones will be displayed. """ def __init__(self, basedir, cog=None, include_markers=None): self.include_markers = {} if include_markers is not None: self.include_markers.update(_default_include_marker) if include_markers: self.include_markers.update(include_markers) self.basedir = Path(basedir) self.cog = cog self.files = {} def __call__(self, fn, section=None): from paver.doctools import SectionedFile f = self.files.get(fn) if f is None: f = SectionedFile(self.basedir / fn) self.files[fn] = f ext = Path(fn).suffix.replace(".", "") marker = self.include_markers.get(ext) if section is None: if marker: value = marker + "file '" + fn + "'\n" + f.all else: value = f.all else: if marker: value = marker + "section '" + section + "' in file '" + fn \ + "'\n" + f[section] else: value = f[section] if self.cog: self.cog.cogmodule.out(value) else: return value def cog_pluginHelp(name): from string import Template import argparse import eyed3.plugins substs = {} template = Template(''' *$summary* Names ----- $name $altnames Description ----------- $description Options ------- .. code-block:: text $options ''') plugin = eyed3.plugins.load(name) substs["name"] = plugin.NAMES[0] if len(plugin.NAMES) > 1: substs["altnames"] = "(aliases: %s)" % ", ".join(plugin.NAMES[1:]) else: substs["altnames"] = "" substs["summary"] = plugin.SUMMARY substs["description"] = plugin.DESCRIPTION if plugin.DESCRIPTION else "" arg_parser = argparse.ArgumentParser() _ = plugin(arg_parser) # noqa buffer = "" found_opts = False for line in arg_parser.format_help().splitlines(True): if not found_opts: if line.lstrip().startswith('-') and not line.lstrip().startswith("-h"): buffer += (" " * 2) + line found_opts = True else: if buffer == '\n': buffer += line else: buffer += (" " * 2) + line if buffer.strip(): substs["options"] = buffer else: substs["options"] = " No extra options supported" return template.substitute(substs) setattr(__builtins__, "cog_pluginHelp", cog_pluginHelp) class CliExample(Includer): def __call__(self, fn, section=None, lang="bash"): # Resetting self.cog to get a string back from Includer.__call__ cog = self.cog self.cog = None raw = Includer.__call__(self, fn, section=section) self.cog = cog self.cog.cogmodule.out("\n.. code-block:: %s\n\n" % lang) for line in raw.splitlines(True): if line.strip() == "": self.cog.cogmodule.out(line) else: cmd = line.strip() cmd_line = "" if not cmd.startswith('#'): cmd_line = "$ %s\n" % cmd else: cmd_line = cmd + '\n' cmd_line = (' ' * 2) + cmd_line self.cog.cogmodule.out(cmd_line) if cmd.startswith("eyeD3 "): cmd += " --no-color --no-config " output = sh(cmd, capture=True) if output: self.cog.cogmodule.out("\n") for ol in output.splitlines(True): self.cog.cogmodule.out(' ' * 2 + ol) if output: self.cog.cogmodule.out("\n") # XXX: modified from paver.doctools._runcog to add includers def _runcog(options, uncog=False): """Common function for the cog and runcog tasks.""" #options.order('cog', 'sphinx', add_rest=True) cog = cogapp.Cog() if uncog: cog.options.bNoGenerate = True cog.options.bReplace = True cog.options.bDeleteCode = options["cog"].get("delete_code", False) includedir = options["cog"].get('includedir', None) if includedir: markers = options["cog"].get("include_markers") include = Includer( includedir, cog=cog, include_markers=options["cog"].get("include_markers")) # load cog's namespace with our convenience functions. cog.options.defines['include'] = include cog.options.defines['sh'] = _cogsh(cog) cli_includer = CliExample(includedir, cog=cog, include_markers=markers) cog.options.defines["cli_example"] = cli_includer cog.options.defines.update(options["sphinx"].get("defines", {})) cog.options.sBeginSpec = options["cog"].get('beginspec', r'{{{cog') cog.options.sEndSpec = options["cog"].get('endspec', r'}}}') cog.options.sEndOutput = options["cog"].get('endoutput', r'{{{end}}}') basedir = options["sphinx"].get('basedir', None) if basedir is None: basedir = Path(options["sphinx"].get('docroot', "docs")) / \ options["sphinx"].get('sourcedir', "") basedir = Path(basedir) pattern = options["sphinx"].get("pattern", "**/*.rst") if pattern: files = basedir.glob(pattern) else: # FIXME: This cannot happen since pattern is never None files = basedir.glob("**/*") for f in sorted(files): cog.processOneFile(str(f)) def main(): sys.path.append("./") try: _runcog(options) finally: sys.path.remove("./") if __name__ == "__main__": sys.exit(main() or 0) eyeD3-0.9.7/examples/000077500000000000000000000000001432016011500143075ustar00rootroot00000000000000eyeD3-0.9.7/examples/chapters.py000077500000000000000000000046551432016011500165070ustar00rootroot00000000000000#!/usr/bin/env python import sys from eyed3.id3.tag import Tag def printChapter(chapter): # The element ID is the unique key for this chapter print("== Chapter '%s'" % chapter.element_id) # TIT2 sub frame print("-- Title:", chapter.title) # TIT3 sub frame print("-- subtitle:", chapter.subtitle) # WXXX sub frame print("-- url:", chapter.user_url) # Start and end time - tuple print("-- Start time: %d; End time: %d" % chapter.times) # Start and end offset - tuple. None is used to set to "no offset" print("-- Start offset: %s; End offset: %s" % tuple((str(o) for o in chapter.offsets))) print("-- Sub frames:", str(list(chapter.sub_frames.keys()))) tag = Tag() if len(sys.argv) > 1: tag.parse(sys.argv[1]) for toc in tag.table_of_contents: print("=== Table of contents:", toc.element_id) print("--- description:", toc.description) print("--- toplevel:", toc.toplevel) print("--- ordered:", toc.ordered) print("--- child_ids:", toc.child_ids) tag.chapters.set("a brand new chapter", (16234, 21546)) tag.chapters.set("another brand new chapter", (21567, 30000), (654221, 765543)) tag.chapters.set("final chapter", (40000, 50000)) tag.chapters.set("oops", (21567, 30000), (654221, 765543)) tag.chapters.remove("oops") chapter_frame = tag.chapters.get("final chapter") chapter_frame.element_id = b"Final Chapter" chapter_frame.offsets = (800000, None) chapter_frame.user_url = "http://example.com/foo" chapter_frame.user_url = "http://example.com/chapter#final" chapter_frame.user_url = None print("-" * 80) for chap in tag.chapters: print(chap) printChapter(chap) print("-" * 80) # Given a list of chapter IDs from the table of contents access each chapter print("+" * 80) for toc in tag.table_of_contents: print("toc:", toc.element_id) for chap_id in toc.child_ids: print(chap_id) printChapter(tag.chapters[chap_id]) print("+" * 80) ## Brand new frames tag = Tag() toc = tag.table_of_contents.set("toc", toplevel=True, child_ids=["intro", "chap1", "chap2", "chap3"], description=u"Table of Contents") toc2 = tag.table_of_contents.set("toc2") toc.child_ids.append(toc2.element_id) chap4 = tag.chapters.set("chap4", times=(100, 200)) toc2.child_ids.append(chap4.element_id) try: tag.table_of_contents.set("oops", toplevel=True) except ValueError as ex: print("Expected:", ex) eyeD3-0.9.7/examples/cli_examples.sh000077500000000000000000000035511432016011500173170ustar00rootroot00000000000000#!/bin/bash shopt -s expand_aliases alias eyeD3='eyeD3 --no-color --no-config' # [[[section SETUP]]] rm -f example.id3 touch example.id3 ls -s example.id3 # [[[endsection]]] # [[[section ART_TIT_SET]]] eyeD3 --artist="Token Entry" --title="Entities" example.id3 -Q # [[[endsection]]] # [[[section ALB_YR_G_SET]]] eyeD3 -A "Jaybird" -Y 1987 -G "Hardcore" example.id3 -Q eyeD3 example.id3 # [[[endsection]]] # [[[section NONSTD_GENRE_SET]]] eyeD3 --genre="New York City Hardcore" example.id3 -Q eyeD3 example.id3 # [[[endsection]]] # [[[section CONVERT1]]] # Convert the current v2.4 frame to v2.3 eyeD3 --to-v2.3 example.id3 -Q # Convert back eyeD3 --to-v2.4 example.id3 -Q # Convert to v1, this will lose all the more advanced data members ID3 v2 offers eyeD3 --to-v1.1 example.id3 -Q # [[[endsection]]] # [[[section DISPLAY_V1]]] eyeD3 -1 example.id3 # [[[endsection]]] # [[[section SET_WITH_VERSIONS]]] # Set an artist value in the ID3 v1 tag eyeD3 -1 example.id3 -a id3v1 # The file now has a v1 and v2 tag, change the v2 artist eyeD3 -2 example.id3 -a id3v2 # Take all the values from v2.4 tag (the default) and set them in the v1 tag. eyeD3 -2 --to-v1.1 example.id3 # Take all the values from v1 tag and convert to ID3 v2.3 eyeD3 -1 --to-v2.3 example.id3 # [[[endsection]]] # [[[section IMG_URL]]] eyeD3 --add-image http\\://example.com/cover.jpg:FRONT_COVER example.id3 # [[[endsection]]] # [[[section GENRES_PLUGIN1]]] eyeD3 --plugin=genres # [[[endsection]]] # [[[section LAME_PLUGIN]]] eyeD3 -P lameinfo tests/data/notag-vbr.mp3 # [[[endsection]]] # [[[section PLUGINS_LIST]]] eyeD3 --plugins # [[[endsection]]] # [[[section ITUNES_PODCAST_PLUGIN]]] eyeD3 -P itunes-podcast example.id3 eyeD3 -P itunes-podcast example.id3 --add eyeD3 -P itunes-podcast example.id3 --remove # [[[endsection]]] # [[[section REMOVE_ALL_TAGS]]] eyeD3 --remove-all example.id3 # [[[endsection]]] eyeD3-0.9.7/examples/config.ini000066400000000000000000000006211432016011500162540ustar00rootroot00000000000000# eyeD3 config file. # default: ~/.eyeD3/config.ini # overridde using -c/--config [default] # Default plugin to use. plugin = # General options to always use. These can be plugin specific but SHOULD NOT be. # Any -C/--config and -P/--plugin options are ignored. options = #options = --pdb # Extra directories to load plugins. Separated by ':' plugin_path = ~/.eyeD3 # vim: set filetype=dosini: eyeD3-0.9.7/examples/plugins/000077500000000000000000000000001432016011500157705ustar00rootroot00000000000000eyeD3-0.9.7/examples/plugins/echo.py000066400000000000000000000004611432016011500172610ustar00rootroot00000000000000import eyed3 from eyed3.plugins import Plugin from eyed3.utils import guessMimetype class EchoPlugin(eyed3.plugins.Plugin): NAMES = ["echo"] SUMMARY = u"Displays each filename and mime-type passed to the plugin" def handleFile(self, f): print("%s\t[ %s ]" % (f, guessMimetype(f))) eyeD3-0.9.7/examples/plugins/echo2.py000066400000000000000000000007551432016011500173510ustar00rootroot00000000000000from eyed3.plugins import LoaderPlugin class Echo2Plugin(LoaderPlugin): SUMMARY = u"Displays details about audio files" NAMES = ["echo2"] def handleFile(self, f): super(Echo2Plugin, self).handleFile(f) if not self.audio_file: print("%s: Unsupported type" % f) else: print("Audio info: %s Metadata tag: %s " % ("yes" if self.audio_file.info else "no", "yes" if self.audio_file.tag else "no")) eyeD3-0.9.7/examples/tag_example.py000077500000000000000000000136161432016011500171610ustar00rootroot00000000000000#!/usr/bin/env python from eyed3.id3 import Tag from eyed3.id3 import ID3_V1_0, ID3_V1_1, ID3_V2_3, ID3_V2_4 import logging from eyed3 import log log.setLevel(logging.DEBUG) t = Tag() t.artist = "M.O.P." t.title = "How About Some Hardcore" t.album = "To The Death" t.genre = "Hip-Hop" t.track_num = (3, 5) t.disc_num = (1, 1) t.original_release_date = "1994-04-07" t.release_date = "1994-04-07" t.encoding_date = "2002-03" t.recording_date = 1996 t.tagging_date = "2012-2-5" t.comments.set("Gritty, yo!") t.comments.set("Brownsville, Brooklyn", "Origin") t.user_text_frames.set("****", "Rating") t.artist_url = b"http://allmusic.com/artist/mop-p194909" t.user_url_frames.set(b"http://eyed3.nicfit.net/") t.bpm = 187 t.play_count = 125 t.unique_file_ids.set(b"43e888e067ea107f964916af6259cbe7", "md5sum") t.cd_id = b"\x3c\x33\x4d\x41\x43\x59\x3c\x33" t.privates.set(b"Secrets", b"Billy Danzenie") t.terms_of_use = "Blunted" t.lyrics.set(""" [ Billy Danzenie ] How about some hardcore? (Yeah, we like it raw!) (4x) How about some hardcore? [ VERSE 1: Billy Danzenie ] (Yeah, we like it raw in the streets) For the fellas on the corner posted up 20 deep With your ifth on your hip, ready to flip Whenever you empty your clip, dip, trip your sidekick You got skill, you best manage to chill And do yourself a favor, don`t come nowhere near the Hill With that bullshit, word, money grip, it`ll cost ya Make you reminisce of Frank Nitty `The Enforcer` I move with M.O.P.`s Last Generation Straight up and down, act like you want a confrontation I packs my gat, I gotta stay strapped I bust mines, don`t try to sneak up on me from behind Don`t sleep, I get deep when I creep I see right now I got to show you it ain`t nothin sweet Go get your muthaf**kin hammer And act like you want drama I send a message to your mama `Hello, do you know your one son left? I had license to kill and he had been marked for death He`s up the Hill in the back of the building with two in the dome I left him stiffer than a tombstone` [ Li`l Fame ] How about some hardcore? (Yeah, we like it raw!) (4x) How about some hardcore? [ VERSE 2: Billy Danzenie ] (Yeah, we like it rugged in the ghetto) I used to pack sling shots, but now I`m packin heavy metal A rugged underground freestyler Is Li`l Fame, muthaf**ka, slap, Li`l Mallet When I let off, it`s a burning desire Niggas increase the peace cause when I release it be rapid fire For the cause I drop niggas like drawers Niggas`ll hit the floors from the muthaf**kin .44`s I`m talkin titles when it`s showtime f**k around, I have niggas call the injury help line I bust words in my verse that`ll serve Even on my first nerve I put herbs to curbs I ain`t about givin niggas a chance And I still raise sh*t to make my brother wanna get up and dance Front, I make it a thrill to kill Bringin the ruckus, it`s the neighborhood hoods for the Hill that`s real Me and mics, that`s unlike niggas and dykes So who wanna skate, cause I`m puttin niggas on ice Whatever I drop must be rough, rugged and hard more (Yeah!) [ Billy Danzenie ] How about some hardcore? (Yeah, we like it raw!) (4x) [ VERSE 3: Billy Danzenie ] Yo, here I am (So what up?) Get it on, cocksucker That nigga Bill seem to be a ill black brother I gets dough from the way I flow And before I go You muthaf**kas gonna know That I ain`t nothin to f**k with - duck quick I squeeze when I`m stressed Them teflons`ll tear through your vest I love a bloodbath (niggas know the half) You can feel the wrath (Saratoga/St. Marks Ave.) B-i-l-l-y D-a-n-z-e n-i-e, me, Billy Danzenie (Knock, knock) Who`s there? (Li`l Fame) Li`l Fame who? (Li`l Fame, your nigga) Boom! Ease up off the trigger It`s aight, me and shorty go to gunfights Together we bring the ruckus, right? We trump tight, aight? I earned mine, so I`m entitled to a title (7 f**kin 30) that means I`m homicidal [ Li`l Fame ] How about some hardcore? (Yeah, we like it raw!) (4x) [ VERSE 4: Li`l Fame ] Yo, I scream on niggas like a rollercoaster To them wack muthaf**kas, go hang it up like a poster Niggas get excited, but don`t excite me Don`t invite me, I`m splittin niggas` heads where the white be Try to trash this, this little bastard`ll blast it Only puttin niggas in comas and caskets I ain`t a phoney, I put the `mack` in a -roni I leave you lonely (Yeah, yeah, get on his ass, homie) Up in your anus, I pack steel that`s stainless We came to claim this, and Li`l Fame`ll make you famous I mack hoes, rock shows and stack dough Cause I`m in effect, knockin muthaf**kas like five-o I`m catchin other niggas peepin, shit, I ain`t sleepin I roll deep like a muthaf**kin Puerto-Rican So when I write my competition looks sadly For broke-ass niggas I make it happen like Mariah Carey I got sh*t for niggas that roll bold Li`l Fame is like a orthopedic shoe, I got mad soul I`ma kill em before I duck em Because yo, mother made em, mother had em and muthaf**k em [ Li`l Fame ] Knowmsayin? Li`l Fame up in this muthaf**ka Givin shoutouts to my man D/R Period [Name] Lazy Laz My man Broke As* Moe The whole Saratoga Ave. Youknowmsayin? Representin for Brooklyn Most of all my cousin Prince Leroy, Big Mal, rest in peace [ Billy Danzenie ] Danzenie up in this muthaf**ka I`d like to say what`s up to the whole M.O.P. Brooklyn, period Them niggas that just don`t give a f**k [ O.G. Bu-Bang Bet yo ass, nigga Hey yo, this muthaf**kin Babyface [Name] Aka O.G. Bu-Bang Yo, I wanna say what`s up to the whole muthaf**kin M.O.P. boyyeee """) t.save("example-v2_4.id3", version=ID3_V2_4) t.save("example-v2_3.id3", version=ID3_V2_3) # Loss of the release date month and day. # Loss of the comment with description. t.save("example-v1_1.id3", version=ID3_V1_1) # Loses what v1.1 loses, and the track # t.save("example-v1_0.id3", version=ID3_V1_0) ''' from eyed3.id3.tag import TagTemplate template = "$artist/"\ "$best_release_date:year - $album/"\ "$artist - $track:num - $title.$file:ext" print TagTemplate(template).substitute(t, zeropad=True) ''' eyeD3-0.9.7/eyed3/000077500000000000000000000000001432016011500135025ustar00rootroot00000000000000eyeD3-0.9.7/eyed3/__about__.py000066400000000000000000000017771432016011500157760ustar00rootroot00000000000000from .__regarding__ import * # noqa: F403 __project_name__ = project_name __version__ = version __version_info__ = version_info __release__ = version_info.release __release_name__ = release_name __years__ = years __project_slug__ = "eyed3" __pypi_name__ = "eyeD3" __author__ = author __author_email__ = author_email __url__ = homepage __description__ = description # FIXME: __long_description__ not being used anywhere. __long_description__ = """ eyeD3 is a Python module and command line program for processing ID3 tags. Information about mp3 files (i.e bit rate, sample frequency, play time, etc.) is also provided. The formats supported are ID3 v1.0/v1.1 and v2.3/v2.4. """ __license__ = "GNU GPL v3.0" __github_url__ = "https://github.com/nicfit/eyeD3", __version_txt__ = """%(__project_name__)s %(__version__)s © Copyright %(__years__)s %(__author__)s This program comes with ABSOLUTELY NO WARRANTY! See LICENSE for details. Run with --help/-h for usage information or read the docs at %(__url__)s""" % (locals()) eyeD3-0.9.7/eyed3/__init__.py000066400000000000000000000025141432016011500156150ustar00rootroot00000000000000import sys import codecs import locale from .__about__ import __version__ as version _DEFAULT_ENCODING = "latin1" # The local encoding, used when parsing command line options, console output, # etc. The default is always ``latin1`` if it cannot be determined, it is NOT # the value shown. LOCAL_ENCODING = locale.getpreferredencoding(do_setlocale=True) if not LOCAL_ENCODING or LOCAL_ENCODING == "ANSI_X3.4-1968": # pragma: no cover LOCAL_ENCODING = _DEFAULT_ENCODING # The local file system encoding, the default is ``latin1`` if it cannot be determined. LOCAL_FS_ENCODING = sys.getfilesystemencoding() if not LOCAL_FS_ENCODING: # pragma: no cover LOCAL_FS_ENCODING = _DEFAULT_ENCODING class Error(Exception): """Base exception type for all eyed3 errors.""" def __init__(self, *args): super().__init__(*args) if args: # The base class will do exactly this if len(args) == 1, # but not when > 1. Note, the 2.7 base class will, 3 will not. # Make it so. self.message = args[0] from .utils.log import log # noqa: E402 from .core import load, AudioFile # noqa: E402 del sys del codecs del locale __all__ = ["AudioFile", "load", "log", "version", "LOCAL_ENCODING", "LOCAL_FS_ENCODING", "Error"] eyeD3-0.9.7/eyed3/__regarding__.py000066400000000000000000000013211432016011500166070ustar00rootroot00000000000000""" ~~~~~~~~~~ DO NOT EDIT THIS FILE! Autogenerated by `regarding` ~~~~~~~~~~ https://github.com/nicfit/regarding """ import dataclasses __all__ = ["Version", "project_name", "version", "version_info", "release_name", "author", "author_email", "years", "description", "homepage"] @dataclasses.dataclass class Version: major: int minor: int maint: int release: str release_name: str project_name = "eyeD3" version = "0.9.7" release_name = "Sunshine" author = "Travis Shirk" author_email = "travis@pobox.com" years = "2002-2022" version_info = Version( 0, 9, 7, "final", "Sunshine" ) description = "Python audio data toolkit (ID3 and MP3)" homepage = "https://eyeD3.nicfit.net/" eyeD3-0.9.7/eyed3/core.py000066400000000000000000000324731432016011500150150ustar00rootroot00000000000000"""Basic core types and utilities.""" import os import time import functools import pathlib import dataclasses from collections import namedtuple from typing import Optional from . import LOCAL_FS_ENCODING from .utils.log import getLogger log = getLogger(__name__) # Audio type selector for no audio. AUDIO_NONE = 0 # Audio type selector for MPEG (mp3) audio. AUDIO_MP3 = 1 AUDIO_TYPES = (AUDIO_NONE, AUDIO_MP3) LP_TYPE = "lp" EP_TYPE = "ep" EP_MAX_SIZE_HINT = 6 COMP_TYPE = "compilation" LIVE_TYPE = "live" VARIOUS_TYPE = "various" DEMO_TYPE = "demo" SINGLE_TYPE = "single" ALBUM_TYPE_IDS = [LP_TYPE, EP_TYPE, COMP_TYPE, LIVE_TYPE, VARIOUS_TYPE, DEMO_TYPE, SINGLE_TYPE] VARIOUS_ARTISTS = "Various Artists" # A key that can be used in a TXXX frame to specify the type of collection # (or album) a file belongs. See :class:`eyed3.core.ALBUM_TYPE_IDS`. TXXX_ALBUM_TYPE = "eyeD3#album_type" # A key that can be used in a TXXX frame to specify the origin of an # artist/band. i.e. where they are from. # The format is: citystatecountry TXXX_ARTIST_ORIGIN = "eyeD3#artist_origin" # A 2-tuple for count and a total count. e.g. track 3 of 10, count of total. CountAndTotalTuple = namedtuple("CountAndTotalTuple", "count, total") @dataclasses.dataclass class ArtistOrigin: city: str state: str country: str def __bool__(self): return bool(self.city or self.state or self.country) def id3Encode(self): return "\t".join([(o if o else "") for o in dataclasses.astuple(self)]) @dataclasses.dataclass class AudioInfo: """A base container for common audio details.""" # The number of seconds of audio data (i.e., the playtime) time_secs: float # The number of bytes of audio data. size_bytes: int def __post_init__(self): self.time_secs = int(self.time_secs * 100.0) / 100.0 class Tag: """An abstract interface for audio tag (meta) data (e.g. artist, title, etc.) """ read_only: bool = False def _setArtist(self, val): raise NotImplementedError() # pragma: nocover def _getArtist(self): raise NotImplementedError() # pragma: nocover def _getAlbumArtist(self): raise NotImplementedError() # pragma: nocover def _setAlbumArtist(self, val): raise NotImplementedError() # pragma: nocover def _setAlbum(self, val): raise NotImplementedError() # pragma: nocover def _getAlbum(self): raise NotImplementedError() # pragma: nocover def _setTitle(self, val): raise NotImplementedError() # pragma: nocover def _getTitle(self): raise NotImplementedError() # pragma: nocover def _setTrackNum(self, val): raise NotImplementedError() # pragma: nocover def _getTrackNum(self) -> CountAndTotalTuple: raise NotImplementedError() # pragma: nocover @property def artist(self): return self._getArtist() @artist.setter def artist(self, v): self._setArtist(v) @property def album_artist(self): return self._getAlbumArtist() @album_artist.setter def album_artist(self, v): self._setAlbumArtist(v) @property def album(self): return self._getAlbum() @album.setter def album(self, v): self._setAlbum(v) @property def title(self): return self._getTitle() @title.setter def title(self, v): self._setTitle(v) @property def track_num(self) -> CountAndTotalTuple: """Track number property. Must return a 2-tuple of (track-number, total-number-of-tracks). Either tuple value may be ``None``. """ return self._getTrackNum() @track_num.setter def track_num(self, v): self._setTrackNum(v) def __init__(self, title=None, artist=None, album=None, album_artist=None, track_num=None): self.title = title self.artist = artist self.album = album self.album_artist = album_artist self.track_num = track_num class AudioFile: """Abstract base class for audio file types (AudioInfo + Tag)""" tag: Tag = None def _read(self): """Subclasses MUST override this method and set ``self._info``, ``self._tag`` and ``self.type``. """ raise NotImplementedError() def initTag(self, version=None): raise NotImplementedError() def rename(self, name, fsencoding=LOCAL_FS_ENCODING, preserve_file_time=False): """Rename the file to ``name``. The encoding used for the file name is :attr:`eyed3.LOCAL_FS_ENCODING` unless overridden by ``fsencoding``. Note, if the target file already exists, or the full path contains non-existent directories the operation will fail with :class:`IOError`. File times are not modified when ``preserve_file_time`` is ``True``, ``False`` is the default. """ curr_path = pathlib.Path(self.path) ext = curr_path.suffix new_path = curr_path.parent / "{name}{ext}".format(**locals()) if new_path.exists(): raise IOError(f"File '{new_path}' exists, will not overwrite") elif not new_path.parent.exists(): raise IOError("Target directory '%s' does not exists, will not " "create" % new_path.parent) os.rename(self.path, str(new_path)) if self.tag: self.tag.file_info.name = str(new_path) if preserve_file_time: self.tag.file_info.touch((self.tag.file_info.atime, self.tag.file_info.mtime)) self.path = str(new_path) @property def path(self): """The absolute path of this file.""" return self._path @path.setter def path(self, path): """Set the path""" if isinstance(path, pathlib.Path): path = str(path) self._path = path @property def info(self) -> AudioInfo: """Returns a concrete implementation of :class:`eyed3.core.AudioInfo`""" return self._info @property def tag(self): """Returns a concrete implementation of :class:`eyed3.core.Tag`""" return self._tag @tag.setter def tag(self, t): self._tag = t def __init__(self, path): """Construct with a path and invoke ``_read``. All other members are set to None.""" if isinstance(path, pathlib.Path): path = str(path) self.path = path self.type = None self._info = None self._tag = None self._read() def __str__(self): return str(self.path) @functools.total_ordering class Date: """ A class for representing a date and time (optional). This class differs from ``datetime.datetime`` in that the default values for month, day, hour, minute, and second is ``None`` and not 'January 1, 00:00:00'. This allows for an object that is simply 1987, and not January 1 12AM, for example. But when more resolution is required those vales can be set as well. """ TIME_STAMP_FORMATS = ["%Y", "%Y-%m", "%Y-%m-%d", "%Y-%m-%dT%H", "%Y-%m-%dT%H:%M", "%Y-%m-%dT%H:%M:%S", # The following end with 'Z' signally time is UTC "%Y-%m-%dT%HZ", "%Y-%m-%dT%H:%MZ", "%Y-%m-%dT%H:%M:%SZ", # The following are wrong per the specs, but ... "%Y-%m-%d %H:%M:%S", "%Y-00-00", "%Y%m%d", ] """Valid time stamp formats per ISO 8601 and used by `strptime`.""" def __init__(self, year, month=None, day=None, hour=None, minute=None, second=None): # Validate with datetime from datetime import datetime _ = datetime(year, month if month is not None else 1, day if day is not None else 1, hour if hour is not None else 0, minute if minute is not None else 0, second if second is not None else 0) self._year = year self._month = month self._day = day self._hour = hour self._minute = minute self._second = second # Python's date classes do a lot more date validation than does not # need to be duplicated here. Validate it _ = Date._validateFormat(str(self)) # noqa @property def year(self): return self._year @property def month(self): return self._month @property def day(self): return self._day @property def hour(self): return self._hour @property def minute(self): return self._minute @property def second(self): return self._second def __eq__(self, rhs): if not rhs: return False return (self.year == rhs.year and self.month == rhs.month and self.day == rhs.day and self.hour == rhs.hour and self.minute == rhs.minute and self.second == rhs.second) def __ne__(self, rhs): return not(self == rhs) def __lt__(self, rhs): if not rhs: return False for left, right in ((self.year, rhs.year), (self.month, rhs.month), (self.day, rhs.day), (self.hour, rhs.hour), (self.minute, rhs.minute), (self.second, rhs.second)): left = left if left is not None else -1 right = right if right is not None else -1 if left < right: return True elif left > right: return False return False def __hash__(self): return hash(str(self)) @staticmethod def _validateFormat(s): pdate, fmt = None, None for fmt in Date.TIME_STAMP_FORMATS: try: pdate = time.strptime(s, fmt) break except ValueError: # date string did not match format. continue if pdate is None: raise ValueError(f"Invalid date string: {s}") assert pdate return pdate, fmt @staticmethod def parse(s): """Parses date strings that conform to ISO-8601.""" if not isinstance(s, str): s = s.decode("ascii") s = s.strip('\x00') pdate, fmt = Date._validateFormat(s) # Here is the difference with Python date/datetime objects, some # of the members can be None kwargs = {} if "%m" in fmt: kwargs["month"] = pdate.tm_mon if "%d" in fmt: kwargs["day"] = pdate.tm_mday if "%H" in fmt: kwargs["hour"] = pdate.tm_hour if "%M" in fmt: kwargs["minute"] = pdate.tm_min if "%S" in fmt: kwargs["second"] = pdate.tm_sec return Date(pdate.tm_year, **kwargs) def __str__(self): """Returns date strings that conform to ISO-8601. The returned string will be no larger than 17 characters.""" s = "%d" % self.year if self.month: s += "-%s" % str(self.month).rjust(2, '0') if self.day: s += "-%s" % str(self.day).rjust(2, '0') if self.hour is not None: s += "T%s" % str(self.hour).rjust(2, '0') if self.minute is not None: s += ":%s" % str(self.minute).rjust(2, '0') if self.second is not None: s += ":%s" % str(self.second).rjust(2, '0') return s def parseError(ex) -> None: """A function that is invoked when non-fatal parse, format, etc. errors occur. In most cases the invalid values will be ignored or possibly fixed. This function simply logs the error.""" log.warning(ex) def load(path, tag_version=None) -> Optional[AudioFile]: """Loads the file identified by ``path`` and returns a concrete type of :class:`eyed3.core.AudioFile`. If ``path`` is not a file an ``IOError`` is raised. ``None`` is returned when the file type (i.e. mime-type) is not recognized. The following AudioFile types are supported: * :class:`eyed3.mp3.Mp3AudioFile` - For mp3 audio files. * :class:`eyed3.id3.TagFile` - For raw ID3 data files. If ``tag_version`` is not None (the default) only a specific version of metadata is loaded. This value must be a version constant specific to the eventual format of the metadata. """ from . import mimetype, mp3, id3 if not isinstance(path, pathlib.Path): path = pathlib.Path(path) log.debug(f"Loading file: {path}") if path.exists(): if not path.is_file(): raise IOError(f"not a file: {path}") else: raise IOError(f"file not found: {path}") mtype = mimetype.guessMimetype(path) log.debug(f"File mime-type: {mtype}") if mtype in mp3.MIME_TYPES: return mp3.Mp3AudioFile(path, tag_version) elif mtype == id3.ID3_MIME_TYPE: return id3.TagFile(path, tag_version) else: return None eyeD3-0.9.7/eyed3/id3/000077500000000000000000000000001432016011500141615ustar00rootroot00000000000000eyeD3-0.9.7/eyed3/id3/__init__.py000066400000000000000000000331421432016011500162750ustar00rootroot00000000000000import re import functools from .. import core from .. import Error from ..utils.log import getLogger log = getLogger(__name__) # Version 1, 1.0 or 1.1 ID3_V1 = (1, None, None) # Version 1.0, specifically ID3_V1_0 = (1, 0, 0) # Version 1.1, specifically ID3_V1_1 = (1, 1, 0) # Version 2, 2.2, 2.3 or 2.4 ID3_V2 = (2, None, None) # Version 2.2, specifically ID3_V2_2 = (2, 2, 0) # Version 2.3, specifically ID3_V2_3 = (2, 3, 0) # Version 2.4, specifically ID3_V2_4 = (2, 4, 0) # The default version for eyeD3 tags and save operations. ID3_DEFAULT_VERSION = ID3_V2_4 # Useful for operations where any version will suffice. ID3_ANY_VERSION = (ID3_V1[0] | ID3_V2[0], None, None) # Byte code for latin1 LATIN1_ENCODING = b"\x00" # Byte code for UTF-16 UTF_16_ENCODING = b"\x01" # Byte code for UTF-16 (big endian) UTF_16BE_ENCODING = b"\x02" # Byte code for UTF-8 (Not supported in ID3 versions < 2.4) UTF_8_ENCODING = b"\x03" # Default language code for frames that contain a language portion. DEFAULT_LANG = b"eng" ID3_MIME_TYPE = "application/x-id3" ID3_MIME_TYPE_EXTENSIONS = (".id3", ".tag") def isValidVersion(v, fully_qualified=False): """Check the tuple ``v`` against the list of valid ID3 version constants. If ``fully_qualified`` is ``True`` it is enforced that there are 3 components to the version in ``v``. Returns ``True`` when valid and ``False`` otherwise.""" valid = v in [ID3_V1, ID3_V1_0, ID3_V1_1, ID3_V2, ID3_V2_2, ID3_V2_3, ID3_V2_4, ID3_ANY_VERSION] if not valid: return False if fully_qualified: return None not in (v[0], v[1], v[2]) else: return True def normalizeVersion(v): """If version tuple ``v`` is of the non-specific type (v1 or v2, any, etc.) a fully qualified version is returned.""" if v == ID3_V1: v = ID3_V1_1 elif v == ID3_V2: assert ID3_DEFAULT_VERSION[0] & ID3_V2[0] v = ID3_DEFAULT_VERSION elif v == ID3_ANY_VERSION: v = ID3_DEFAULT_VERSION # Now, correct bogus version as seen in the wild if v[:2] == (2, 2) and v[2] != 0: v = (2, 2, 0) return v # Convert an ID3 version constant to a display string def versionToString(v): """Conversion version tuple ``v`` to a string description.""" if v == ID3_ANY_VERSION: return "v1.x/v2.x" elif v[0] == 1: if v == ID3_V1_0: return "v1.0" elif v == ID3_V1_1: return "v1.1" elif v == ID3_V1: return "v1.x" elif v[0] == 2: if v == ID3_V2_2: return "v2.2" elif v == ID3_V2_3: return "v2.3" elif v == ID3_V2_4: return "v2.4" elif v == ID3_V2: return "v2.x" raise ValueError("Invalid ID3 version constant: %s" % str(v)) class GenreException(Error): """Excpetion type for exceptions related to genres.""" @functools.total_ordering class Genre: """A genre in terms of a ``name`` and and ``id``. Only when ``name`` is a "standard" genre (as defined by ID3 v1) will ``id`` be a value other than ``None``.""" def __init__(self, name=None, id: int=None, genre_map=None): """Constructor takes an optional name and ID. If `id` is provided the `name`, regardless of value, is set to the string the id maps to. Likewise, if `name` is passed and is a standard genre the id is set to the correct value. Any invalid id values cause a `ValueError` to be raised. Genre names that are not in the standard list are still accepted but the `id` value is set to `None`.""" self._id, self._name = None, None self._genre_map = genre_map or genres if not name and id is None: return # An ID always takes precedence if id is not None: try: self.id = id # valid id will set name if name and name != self.name: log.warning(f"Genre ID takes precedence and remapped '{name}' to '{self.name}'") except ValueError: log.warning(f"Invalid numeric genre ID: {id}") if not name: # Gave an invalid ID and no name to fallback on raise self.name = name self.id = None else: # All we have is a name self.name = name assert self.id or self.name @property def id(self): """The Genre's id property. When setting the value is strictly enforced and if the value is not a valid genre code a ``ValueError`` is raised. Otherwise the id is set **and** the ``name`` property is updated to the code's string name. """ return self._id @id.setter def id(self, val): if val is None: self._id = None return val = int(val) if val not in self._genre_map.keys() or not self._genre_map[val]: raise ValueError(f"Unknown genre ID: {val}") name = self._genre_map[val] self._id = val self._name = name @property def name(self): """The Genre's name property. When setting the value the name is looked up in the standard genre map and if found the ``id`` ppropery is set to the numeric valud **and** the name is normalized to the sting found in the map. Non standard genres are set (with a warning log) and the ``id`` is set to ``None``. It is valid to set the value to ``None``. """ return self._name @name.setter def name(self, val): if val is None: self._name = None return if val.lower() in list(self._genre_map.keys()): self._id = self._genre_map[val] # normalize the name self._name = self._genre_map[self._id] else: log.warning(f"Non standard genre name: {val}") self._id = None self._name = val @staticmethod def parse(g_str, id3_std=True): """Parses genre information from `genre_str`. The following formats are supported: 01, 2, 23, 125 - ID3 v1.x style. (01), (2), (129)Hardcore, (9)Metal, Indie - ID3v2 style with and without refinement. Raises GenreException when an invalid string is passed. """ g_str = g_str.strip() if not g_str: return None def strip0Padding(s): if len(s) > 1: return s.lstrip("0") else: return s if id3_std: # ID3 v1 style. # Match 03, 34, 129. if re.compile(r"[0-9][0-9]*$").match(g_str): return Genre(id=int(strip0Padding(g_str))) # ID3 v2 style. # Match (03), (0)Blues, (15) Rap v23_match = re.compile(r"\(([0-9][0-9]*)\)(.*)$").match(g_str) if v23_match: (gid, name) = v23_match.groups() gid = int(strip0Padding(gid)) if gid and name: gid = gid name = name.strip() else: gid = gid name = None return Genre(id=gid, name=name) return Genre(id=None, name=g_str) def __str__(self): s = "" if self.id is not None: s += f"({self.id:d})" if self.name: s += self.name return s def __eq__(self, rhs): if not rhs: return False elif type(rhs) is str: return self.name == rhs else: return self.id == rhs.id and self.name == rhs.name def __lt__(self, rhs): if not rhs: return False elif type(rhs) is str: return self.name == rhs else: return self.name < rhs.name class GenreMap(dict): """Classic genres defined around ID3 v1 but suitable anywhere. This class is used primarily as a way to map numeric genre values to a string name. Genre strings on the other hand are not required to exist in this list. """ GENRE_MIN = 0 GENRE_MAX = None ID3_GENRE_MIN = 0 ID3_GENRE_MAX = 79 WINAMP_GENRE_MIN = 80 WINAMP_GENRE_MAX = 191 GENRE_ID3V1_MAX = 255 def __init__(self, *args): """The optional ``*args`` are passed directly to the ``dict`` constructor.""" global ID3_GENRES super().__init__(*args) # ID3 genres as defined by the v1.1 spec with WinAmp extensions. for i, g in enumerate(ID3_GENRES): self[i] = g self[g.lower() if g else None] = i GenreMap.GENRE_MAX = len(ID3_GENRES) - 1 # Pad up to 255 for i in range(GenreMap.GENRE_MAX + 1, 255 + 1): self[i] = None self[None] = 255 def get(self, key): if type(key) is int: name, gid = self[key], key else: gid = self[key] name = self[gid] return Genre(name, id=gid, genre_map=self) def __getitem__(self, key): if key and type(key) is not int: key = key.lower() return super().__getitem__(key) @property def ids(self): return list(sorted([k for k in self.keys() if type(k) is int and self[k]])) def iter(self): for gid in self.ids: g = self[gid] if g: yield Genre(g, id=gid) class TagFile(core.AudioFile): """ A shim class for dealing with files that contain only ID3 data, no audio. """ def __init__(self, path, version=ID3_ANY_VERSION): self._tag_version = version core.AudioFile.__init__(self, path) assert(self.type == core.AUDIO_NONE) def _read(self): with open(self.path, 'rb') as file_obj: tag = Tag() tag_found = tag.parse(file_obj, self._tag_version) self._tag = tag if tag_found else None self.type = core.AUDIO_NONE def initTag(self, version=ID3_DEFAULT_VERSION): """Add a id3.Tag to the file (removing any existing tag if one exists). """ self.tag = Tag() self.tag.version = version self.tag.file_info = FileInfo(self.path) # ID3 genres, as defined in ID3 v1. The position in the list is the genre's numeric byte value. ID3_GENRES = [ 'Blues', 'Classic Rock', 'Country', 'Dance', 'Disco', 'Funk', 'Grunge', 'Hip-Hop', 'Jazz', 'Metal', 'New Age', 'Oldies', 'Other', 'Pop', 'R&B', 'Rap', 'Reggae', 'Rock', 'Techno', 'Industrial', 'Alternative', 'Ska', 'Death Metal', 'Pranks', 'Soundtrack', 'Euro-Techno', 'Ambient', 'Trip-Hop', 'Vocal', 'Jazz+Funk', 'Fusion', 'Trance', 'Classical', 'Instrumental', 'Acid', 'House', 'Game', 'Sound Clip', 'Gospel', 'Noise', 'AlternRock', 'Bass', 'Soul', 'Punk', 'Space', 'Meditative', 'Instrumental Pop', 'Instrumental Rock', 'Ethnic', 'Gothic', 'Darkwave', 'Techno-Industrial', 'Electronic', 'Pop-Folk', 'Eurodance', 'Dream', 'Southern Rock', 'Comedy', 'Cult', 'Gangsta Rap', 'Top 40', 'Christian Rap', 'Pop / Funk', 'Jungle', 'Native American', 'Cabaret', 'New Wave', 'Psychedelic', 'Rave', 'Showtunes', 'Trailer', 'Lo-Fi', 'Tribal', 'Acid Punk', 'Acid Jazz', 'Polka', 'Retro', 'Musical', 'Rock & Roll', 'Hard Rock', 'Folk', 'Folk-Rock', 'National Folk', 'Swing', 'Fast Fusion', 'Bebob', 'Latin', 'Revival', 'Celtic', 'Bluegrass', 'Avantgarde', 'Gothic Rock', 'Progressive Rock', 'Psychedelic Rock', 'Symphonic Rock', 'Slow Rock', 'Big Band', 'Chorus', 'Easy Listening', 'Acoustic', 'Humour', 'Speech', 'Chanson', 'Opera', 'Chamber Music', 'Sonata', 'Symphony', 'Booty Bass', 'Primus', 'Porn Groove', 'Satire', 'Slow Jam', 'Club', 'Tango', 'Samba', 'Folklore', 'Ballad', 'Power Ballad', 'Rhythmic Soul', 'Freestyle', 'Duet', 'Punk Rock', 'Drum Solo', 'A Cappella', 'Euro-House', 'Dance Hall', 'Goa', 'Drum & Bass', 'Club-House', 'Hardcore', 'Terror', 'Indie', 'BritPop', 'Negerpunk', 'Polsk Punk', 'Beat', 'Christian Gangsta Rap', 'Heavy Metal', 'Black Metal', 'Crossover', 'Contemporary Christian', 'Christian Rock', 'Merengue', 'Salsa', 'Thrash Metal', 'Anime', 'JPop', 'Synthpop', # https://de.wikipedia.org/wiki/Liste_der_ID3v1-Genres 'Abstract', 'Art Rock', 'Baroque', 'Bhangra', 'Big Beat', 'Breakbeat', 'Chillout', 'Downtempo', 'Dub', 'EBM', 'Eclectic', 'Electro', 'Electroclash', 'Emo', 'Experimental', 'Garage', 'Global', 'IDM', 'Illbient', 'Industro-Goth', 'Jam Band', 'Krautrock', 'Leftfield', 'Lounge', 'Math Rock', 'New Romantic', 'Nu-Breakz', 'Post-Punk', 'Post-Rock', 'Psytrance', 'Shoegaze', 'Space Rock', 'Trop Rock', 'World Music', 'Neoclassical', 'Audiobook', 'Audio Theatre', 'Neue Deutsche Welle', 'Podcast', 'Indie Rock', 'G-Funk', 'Dubstep', 'Garage Rock', 'Psybient', ] # A map of standard genre names and IDs per the ID3 v1 genre definition. genres = GenreMap() from . import frames # noqa from .tag import Tag, TagException, TagTemplate, FileInfo # noqa eyeD3-0.9.7/eyed3/id3/apple.py000066400000000000000000000026271432016011500156430ustar00rootroot00000000000000""" Here lies Apple frames, all of which are non-standard. All of these would have been standard user text frames by anyone not being a bastard, on purpose. """ from .frames import Frame, TextFrame PCST_FID = b"PCST" WFED_FID = b"WFED" TKWD_FID = b"TKWD" TDES_FID = b"TDES" TGID_FID = b"TGID" GRP1_FID = b"GRP1" class PCST(Frame): """Indicates a podcast. The 4 bytes of data is undefined, and is typically all 0.""" def __init__(self, _=None): super().__init__(PCST_FID) def render(self): self.data = b"\x00" * 4 return super(PCST, self).render() class TKWD(TextFrame): """Podcast keywords.""" def __init__(self, _=None, **kwargs): super().__init__(TKWD_FID, **kwargs) class TDES(TextFrame): """Podcast description. One encoding byte followed by text per encoding.""" def __init__(self, _=None, **kwargs): super().__init__(TDES_FID, **kwargs) class TGID(TextFrame): """Podcast URL of the audio file. This should be a W frame!""" def __init__(self, _=None, **kwargs): super().__init__(TGID_FID, **kwargs) class WFED(TextFrame): """Another podcast URL, the feed URL it is said.""" def __init__(self, _=None, url=""): super().__init__(WFED_FID, url) class GRP1(TextFrame): """Apple grouping, could be a TIT1 conversion.""" def __init__(self, _=None, **kwargs): super().__init__(GRP1_FID, **kwargs) eyeD3-0.9.7/eyed3/id3/frames.py000066400000000000000000002400741432016011500160170ustar00rootroot00000000000000import dataclasses from io import BytesIO from collections import namedtuple from .. import core from ..utils import requireUnicode, requireBytes from ..utils.binfuncs import ( bin2bytes, bin2dec, bytes2bin, dec2bin, bytes2dec, dec2bytes, signedInt162bytes, bytes2signedInt16, ) from .. import Error from . import ID3_V2, ID3_V2_2, ID3_V2_3, ID3_V2_4 from . import (LATIN1_ENCODING, UTF_8_ENCODING, UTF_16BE_ENCODING, UTF_16_ENCODING, DEFAULT_LANG) from .headers import FrameHeader from ..utils import b from ..utils.log import getLogger log = getLogger(__name__) ISO_8859_1 = "iso-8859-1" class FrameException(Error): pass TITLE_FID = b"TIT2" # noqa SUBTITLE_FID = b"TIT3" # noqa ARTIST_FID = b"TPE1" # noqa ALBUM_ARTIST_FID = b"TPE2" # noqa ORIG_ARTIST_FID = b"TOPE" # noqa COMPOSER_FID = b"TCOM" # noqa ALBUM_FID = b"TALB" # noqa TRACKNUM_FID = b"TRCK" # noqa GENRE_FID = b"TCON" # noqa COMMENT_FID = b"COMM" # noqa USERTEXT_FID = b"TXXX" # noqa OBJECT_FID = b"GEOB" # noqa UNIQUE_FILE_ID_FID = b"UFID" # noqa LYRICS_FID = b"USLT" # noqa DISCNUM_FID = b"TPOS" # noqa IMAGE_FID = b"APIC" # noqa USERURL_FID = b"WXXX" # noqa PLAYCOUNT_FID = b"PCNT" # noqa BPM_FID = b"TBPM" # noqa PUBLISHER_FID = b"TPUB" # noqa CDID_FID = b"MCDI" # noqa PRIVATE_FID = b"PRIV" # noqa TOS_FID = b"USER" # noqa POPULARITY_FID = b"POPM" # noqa ENCODED_BY_FID = b"TENC" # noqa COPYRIGHT_FID = b"TCOP" # noqa URL_COMMERCIAL_FID = b"WCOM" # noqa URL_COPYRIGHT_FID = b"WCOP" # noqa URL_AUDIOFILE_FID = b"WOAF" # noqa URL_ARTIST_FID = b"WOAR" # noqa URL_AUDIOSRC_FID = b"WOAS" # noqa URL_INET_RADIO_FID = b"WORS" # noqa URL_PAYMENT_FID = b"WPAY" # noqa URL_PUBLISHER_FID = b"WPUB" # noqa URL_FIDS = [URL_COMMERCIAL_FID, URL_COPYRIGHT_FID, # noqa URL_AUDIOFILE_FID, URL_ARTIST_FID, URL_AUDIOSRC_FID, URL_INET_RADIO_FID, URL_PAYMENT_FID, URL_PUBLISHER_FID] TOC_FID = b"CTOC" # noqa CHAPTER_FID = b"CHAP" # noqa DEPRECATED_DATE_FIDS = [b"TDAT", b"TYER", b"TIME", b"TORY", b"TRDA", # Nonstandard v2.3 only b"XDOR", ] DATE_FIDS = [b"TDEN", b"TDOR", b"TDRC", b"TDRL", b"TDTG"] class Frame(object): _render_strict = True @requireBytes(1) def __init__(self, id): self.id = id self.header = None self.decompressed_size = 0 self.group_id = None self.encrypt_method = None self.data = None self.data_len = 0 self._encoding = None self._unknown = False @property def header(self): return self._header @header.setter def header(self, h): self._header = h @requireBytes(1) def parse(self, data, frame_header): self.id = frame_header.id self.header = frame_header self.data = self._disassembleFrame(data) def render(self): return self._assembleFrame(self.data) def __lt__(self, other): return self.id < other.id @staticmethod def decompress(data): import zlib log.debug("before decompression: %d bytes" % len(data)) data = zlib.decompress(data, 15) log.debug("after decompression: %d bytes" % len(data)) return data @staticmethod def compress(data): import zlib log.debug("before compression: %d bytes" % len(data)) data = zlib.compress(data) log.debug("after compression: %d bytes" % len(data)) return data @staticmethod def decrypt(data): log.warning("Frame decryption not yet supported, leaving data as is.") return data @staticmethod def encrypt(data): log.warning("Frame encryption not yet supported, leaving data as is.") return data @requireBytes(1) def _disassembleFrame(self, data): assert self.header header = self.header # Format flags in the frame header may add extra data to the # beginning of this data. if header.minor_version <= 3: # 2.3: compression(4), encryption(1), group(1) if header.compressed: self.decompressed_size = bin2dec(bytes2bin(data[:4])) data = data[4:] log.debug("Decompressed Size: %d" % self.decompressed_size) if header.encrypted: self.encrypt_method = bin2dec(bytes2bin(data[0:1])) data = data[1:] log.debug("Encryption Method: %d" % self.encrypt_method) if header.grouped: self.group_id = bin2dec(bytes2bin(data[0:1])) data = data[1:] log.debug("Group ID: %d" % self.group_id) else: # 2.4: group(1), encrypted(1), data_length_indicator (4,7) if header.grouped: self.group_id = bin2dec(bytes2bin(data[0:1])) log.debug("Group ID: %d" % self.group_id) data = data[1:] if header.encrypted: self.encrypt_method = bin2dec(bytes2bin(data[0:1])) data = data[1:] log.debug("Encryption Method: %d" % self.encrypt_method) if header.data_length_indicator: self.data_len = bin2dec(bytes2bin(data[:4], 7)) data = data[4:] log.debug("Data Length: %d" % self.data_len) if header.compressed: self.decompressed_size = self.data_len log.debug("Decompressed Size: %d" % self.decompressed_size) if header.minor_version == 4 and header.unsync: data = deunsyncData(data) if header.encrypted: data = self.decrypt(data) if header.compressed: data = self.decompress(data) return data @requireBytes(1) def _assembleFrame(self, data): assert self.header header = self.header # eyeD3 never writes unsync'd frames header.unsync = False format_data = b"" if header.minor_version == 3: if header.compressed: format_data += bin2bytes(dec2bin(len(data), 32)) if header.encrypted: format_data += bin2bytes(dec2bin(self.encrypt_method, 8)) if header.grouped: format_data += bin2bytes(dec2bin(self.group_id, 8)) else: if header.grouped: format_data += bin2bytes(dec2bin(self.group_id, 8)) if header.encrypted: format_data += bin2bytes(dec2bin(self.encrypt_method, 8)) if header.compressed or header.data_length_indicator: header.data_length_indicator = 1 format_data += bin2bytes(dec2bin(len(data), 32)) if header.compressed: data = self.compress(data) if header.encrypted: data = self.encrypt(data) self.data = format_data + data return header.render(len(self.data)) + self.data @property def text_delim(self): assert self.encoding is not None return b"\x00\x00" if self.encoding in (UTF_16_ENCODING, UTF_16BE_ENCODING) else b"\x00" def _initEncoding(self): assert self.header.version and len(self.header.version) == 3 curr_enc = self.encoding if self.encoding is not None: # Make sure the encoding is valid for this version if self.header.version[:2] < (2, 4): if self.header.version[0] == 1: self.encoding = LATIN1_ENCODING else: if self.encoding > UTF_16_ENCODING: # v2.3 cannot do utf16 BE or utf8 self.encoding = UTF_16_ENCODING else: if self.header.version[:2] < (2, 4): if self.header.version[0] == 2: self.encoding = UTF_16_ENCODING else: self.encoding = LATIN1_ENCODING else: self.encoding = UTF_8_ENCODING log.debug(f"_initEncoding: was={curr_enc} now={self.encoding}") @property def encoding(self): return self._encoding @encoding.setter def encoding(self, enc): if not isinstance(enc, bytes): raise TypeError("encoding argument must be a byte string.") elif not LATIN1_ENCODING <= enc <= UTF_8_ENCODING: log.warning("Unknown encoding value {}".format(enc)) enc = LATIN1_ENCODING self._encoding = enc @property def strict_rendering(self): return self._render_strict @property def unknown(self): return self._unknown class TextFrame(Frame): """Text frames. Data string format: encoding (one byte) + text """ @requireUnicode("text") def __init__(self, id, text=None): super(TextFrame, self).__init__(id) assert(self.id[0:1] == b'T' or self.id in [b"XSOA", b"XSOP", b"XSOT", b"XDOR", b"WFED", b"GRP1"]) self.text = text or "" @property def text(self): return self._text @text.setter @requireUnicode(1) def text(self, txt): self._text = txt def parse(self, data, frame_header): super().parse(data, frame_header) try: self.encoding = self.data[0:1] text_data = self.data[1:] except ValueError as err: log.warning("TextFrame[{fid}] - {err}; using latin1" .format(err=err, fid=self.id)) self.encoding = LATIN1_ENCODING text_data = self.data[:] try: self.text = decodeUnicode(text_data, self.encoding) except UnicodeDecodeError as err: log.warning(f"Error decoding text frame {self.id}: {err}") self.text = "" log.debug("TextFrame text: %s" % self.text) def render(self): self._initEncoding() self.data = (self.encoding + self.text.encode(id3EncodingToString(self.encoding))) assert type(self.data) is bytes return super().render() class UserTextFrame(TextFrame): @requireUnicode("description", "text") def __init__(self, id=USERTEXT_FID, description="", text=""): super(UserTextFrame, self).__init__(id, text=text) self.description = description @property def description(self): return self._description @description.setter @requireUnicode(1) def description(self, txt): self._description = txt def parse(self, data, frame_header): """Data string format: encoding (one byte) + description + b"\x00" + text """ # Calling Frame, not TextFrame implementation here since TextFrame # does not know about description Frame.parse(self, data, frame_header) try: self.encoding = self.data[0:1] (d, t) = splitUnicode(self.data[1:], self.encoding) except ValueError as err: log.warning("UserTextFrame[{fid}] - {err}; using latin1" .format(err=err, fid=self.id)) self.encoding = LATIN1_ENCODING (d, t) = splitUnicode(self.data[:], self.encoding) self.description = decodeUnicode(d, self.encoding) log.debug("UserTextFrame description: %s" % self.description) self.text = decodeUnicode(t, self.encoding) log.debug("UserTextFrame text: %s" % self.text) def render(self): self._initEncoding() data = (self.encoding + self.description.encode(id3EncodingToString(self.encoding)) + self.text_delim + self.text.encode(id3EncodingToString(self.encoding))) self.data = data # Calling Frame, not the base return Frame.render(self) class DateFrame(TextFrame): def __init__(self, id, date=""): assert(id in DATE_FIDS or id in DEPRECATED_DATE_FIDS) super().__init__(id, text=str(date)) self.date = self.text self.encoding = LATIN1_ENCODING def parse(self, data, frame_header): super().parse(data, frame_header) try: if self.text: _ = core.Date.parse(self.text) # noqa except ValueError: # Date is invalid, log it and reset. core.parseError(FrameException(f"Invalid date: {self.text}")) self.text = "" @property def date(self): return core.Date.parse(self.text.encode("latin1")) if self.text else None @date.setter def date(self, date): """Set value with a either an ISO 8601 date string or a eyed3.core.Date object.""" if not date: self.text = "" return try: if type(date) is str: date = core.Date.parse(date) elif type(date) is int: # Date is year date = core.Date(date) elif not isinstance(date, core.Date): raise TypeError("str, int, or eyed3.core.Date type expected") except ValueError: log.warning(f"Invalid date text: {date}") self.text = "" return self.text = str(date) def _initEncoding(self): # Dates are always latin1 since they are always represented in ISO 8601 self.encoding = LATIN1_ENCODING class UrlFrame(Frame): def __init__(self, id, url=""): assert(id in URL_FIDS or id == USERURL_FID) super(UrlFrame, self).__init__(id) self.encoding = LATIN1_ENCODING # Per the specs self.url = url @property def url(self): return self._url @url.setter def url(self, url): if isinstance(url, bytes): url = str(url, ISO_8859_1) else: url.encode(ISO_8859_1) # Likewise, it must encode self._url = url def parse(self, data, frame_header): super().parse(data, frame_header) try: self.url = self.data except UnicodeDecodeError: log.warning("Non ascii url, clearing.") self.url = "" def render(self): self.data = self.url.encode(ISO_8859_1) return super(UrlFrame, self).render() class UserUrlFrame(UrlFrame): """ Data string format: encoding (one byte) + description + b"\x00" + url (iso-8859-1) """ @requireUnicode("description") def __init__(self, id=USERURL_FID, description="", url=""): UrlFrame.__init__(self, id, url=url) assert(self.id == USERURL_FID) self.description = description @property def description(self): return self._description @description.setter @requireUnicode(1) def description(self, desc): self._description = desc def parse(self, data, frame_header): # Calling Frame and NOT UrlFrame to get the basic disassemble behavior # UrlFrame would be confused by the encoding, desc, etc. super().parse(data, frame_header) self.encoding = encoding = self.data[0:1] (d, u) = splitUnicode(self.data[1:], encoding) self.description = decodeUnicode(d, encoding) log.debug("UserUrlFrame description: %s" % self.description) # The URL is ascii, ensure try: self.url = str(u, "ascii").encode("ascii") except UnicodeDecodeError: log.warning("Non ascii url, clearing.") self.url = "" log.debug("UserUrlFrame text: %s" % self.url) def render(self): self._initEncoding() data = (self.encoding + self.description.encode(id3EncodingToString(self.encoding)) + self.text_delim + self.url.encode(ISO_8859_1)) self.data = data # Calling Frame, not the base. return Frame.render(self) class UnknownFrame(Frame): """Unknown Frame. """ def __init__(self, id): super().__init__(id) assert self.id not in ID3_FRAMES and self.id not in NONSTANDARD_ID3_FRAMES self._unknown = True ## # Data string format: #
# Text encoding $xx # MIME type $00 # Picture type $xx # Description $00 (00) # Picture data class ImageFrame(Frame): OTHER = 0x00 # noqa ICON = 0x01 # 32x32 png only. # noqa OTHER_ICON = 0x02 # noqa FRONT_COVER = 0x03 # noqa BACK_COVER = 0x04 # noqa LEAFLET = 0x05 # noqa MEDIA = 0x06 # label side of cd, vinyl, etc. # noqa LEAD_ARTIST = 0x07 # noqa ARTIST = 0x08 # noqa CONDUCTOR = 0x09 # noqa BAND = 0x0A # noqa COMPOSER = 0x0B # noqa LYRICIST = 0x0C # noqa RECORDING_LOCATION = 0x0D # noqa DURING_RECORDING = 0x0E # noqa DURING_PERFORMANCE = 0x0F # noqa VIDEO = 0x10 # noqa BRIGHT_COLORED_FISH = 0x11 # There's always room for porno. # noqa ILLUSTRATION = 0x12 # noqa BAND_LOGO = 0x13 # noqa PUBLISHER_LOGO = 0x14 # noqa MIN_TYPE = OTHER # noqa MAX_TYPE = PUBLISHER_LOGO # noqa URL_MIME_TYPE = b"-->" # noqa URL_MIME_TYPE_STR = "-->" # noqa URL_MIME_TYPE_VALUES = (URL_MIME_TYPE, URL_MIME_TYPE_STR) @requireUnicode("description") def __init__(self, id=IMAGE_FID, description="", image_data=None, image_url=None, picture_type=None, mime_type=None): assert(id == IMAGE_FID) super(ImageFrame, self).__init__(id) self.description = description self.image_data = image_data self.image_url = image_url # XXX: Add this member as `type` and deprecate picture_type?? self.picture_type = picture_type self.mime_type = mime_type @property def description(self): return self._description @description.setter @requireUnicode(1) def description(self, d): self._description = d @property def mime_type(self): return str(self._mime_type, "ascii") @mime_type.setter def mime_type(self, m): m = m or b'' self._mime_type = m if isinstance(m, bytes) else m.encode('ascii') @property def picture_type(self): return self._pic_type @picture_type.setter def picture_type(self, t): if t is not None and (t < ImageFrame.MIN_TYPE or t > ImageFrame.MAX_TYPE): raise ValueError("Invalid picture_type: %d" % t) self._pic_type = t def parse(self, data, frame_header): super().parse(data, frame_header) input = BytesIO(self.data) log.debug("APIC frame data size: %d" % len(self.data)) self.encoding = encoding = input.read(1) # Mime type self._mime_type = b"" if frame_header.minor_version != 2: ch = input.read(1) while ch and ch != b"\x00": self._mime_type += ch ch = input.read(1) else: # v2.2 (OBSOLETE) special case self._mime_type = input.read(3) log.debug("APIC mime type: %s" % self._mime_type) if not self._mime_type: core.parseError(FrameException("APIC frame does not contain a mime " "type")) if (self._mime_type != self.URL_MIME_TYPE and self._mime_type.find(b"/") == -1): self._mime_type = b"image/" + self._mime_type pt = ord(input.read(1)) log.debug("Initial APIC picture type: %d" % pt) if pt < self.MIN_TYPE or pt > self.MAX_TYPE: core.parseError(FrameException("Invalid APIC picture type: %d" % pt)) self.picture_type = self.OTHER else: self.picture_type = pt log.debug("APIC picture type: %d" % self.picture_type) self.desciption = "" # Remaining data is a NULL separated description and image data buffer = input.read() input.close() (desc, img) = splitUnicode(buffer, encoding) log.debug("description len: %d" % len(desc)) log.debug("image len: %d" % len(img)) self.description = decodeUnicode(desc, encoding) log.debug("APIC description: %s" % self.description) if self._mime_type.find(self.URL_MIME_TYPE) != -1: self.image_data = None self.image_url = img log.debug("APIC image URL: %s" % len(self.image_url.decode("ascii"))) else: self.image_data = img self.image_url = None log.debug("APIC image data: %d bytes" % len(self.image_data)) if not self.image_data and not self.image_url: core.parseError(FrameException("APIC frame does not contain image " "data/url")) def render(self): # some code has problems with image descriptions encoded <> latin1 # namely mp3diags: work around the problem by forcing latin1 encoding # for empty descriptions, which is by far the most common case anyway self._initEncoding() if not self.image_data and self.image_url: self._mime_type = self.URL_MIME_TYPE data = (self.encoding + self._mime_type + b"\x00" + bin2bytes(dec2bin(self.picture_type, 8)) + self.description.encode(id3EncodingToString(self.encoding)) + self.text_delim) if self.image_data: data += self.image_data elif self.image_url: data += self.image_url self.data = data return super(ImageFrame, self).render() @staticmethod def picTypeToString(t): if t == ImageFrame.OTHER: return "OTHER" elif t == ImageFrame.ICON: return "ICON" elif t == ImageFrame.OTHER_ICON: return "OTHER_ICON" elif t == ImageFrame.FRONT_COVER: return "FRONT_COVER" elif t == ImageFrame.BACK_COVER: return "BACK_COVER" elif t == ImageFrame.LEAFLET: return "LEAFLET" elif t == ImageFrame.MEDIA: return "MEDIA" elif t == ImageFrame.LEAD_ARTIST: return "LEAD_ARTIST" elif t == ImageFrame.ARTIST: return "ARTIST" elif t == ImageFrame.CONDUCTOR: return "CONDUCTOR" elif t == ImageFrame.BAND: return "BAND" elif t == ImageFrame.COMPOSER: return "COMPOSER" elif t == ImageFrame.LYRICIST: return "LYRICIST" elif t == ImageFrame.RECORDING_LOCATION: return "RECORDING_LOCATION" elif t == ImageFrame.DURING_RECORDING: return "DURING_RECORDING" elif t == ImageFrame.DURING_PERFORMANCE: return "DURING_PERFORMANCE" elif t == ImageFrame.VIDEO: return "VIDEO" elif t == ImageFrame.BRIGHT_COLORED_FISH: return "BRIGHT_COLORED_FISH" elif t == ImageFrame.ILLUSTRATION: return "ILLUSTRATION" elif t == ImageFrame.BAND_LOGO: return "BAND_LOGO" elif t == ImageFrame.PUBLISHER_LOGO: return "PUBLISHER_LOGO" else: raise ValueError("Invalid APIC picture type: %d" % t) @staticmethod def stringToPicType(s): if s == "OTHER": return ImageFrame.OTHER elif s == "ICON": return ImageFrame.ICON elif s == "OTHER_ICON": return ImageFrame.OTHER_ICON elif s == "FRONT_COVER": return ImageFrame.FRONT_COVER elif s == "BACK_COVER": return ImageFrame.BACK_COVER elif s == "LEAFLET": return ImageFrame.LEAFLET elif s == "MEDIA": return ImageFrame.MEDIA elif s == "LEAD_ARTIST": return ImageFrame.LEAD_ARTIST elif s == "ARTIST": return ImageFrame.ARTIST elif s == "CONDUCTOR": return ImageFrame.CONDUCTOR elif s == "BAND": return ImageFrame.BAND elif s == "COMPOSER": return ImageFrame.COMPOSER elif s == "LYRICIST": return ImageFrame.LYRICIST elif s == "RECORDING_LOCATION": return ImageFrame.RECORDING_LOCATION elif s == "DURING_RECORDING": return ImageFrame.DURING_RECORDING elif s == "DURING_PERFORMANCE": return ImageFrame.DURING_PERFORMANCE elif s == "VIDEO": return ImageFrame.VIDEO elif s == "BRIGHT_COLORED_FISH": return ImageFrame.BRIGHT_COLORED_FISH elif s == "ILLUSTRATION": return ImageFrame.ILLUSTRATION elif s == "BAND_LOGO": return ImageFrame.BAND_LOGO elif s == "PUBLISHER_LOGO": return ImageFrame.PUBLISHER_LOGO else: raise ValueError("Invalid APIC picture type: %s" % s) def makeFileName(self, name=None): name = ImageFrame.picTypeToString(self.picture_type) if not name \ else name ext = self.mime_type.split("/")[1] if ext == "jpeg": ext = "jpg" return ".".join([name, ext]) class ObjectFrame(Frame): @requireUnicode("description", "filename") def __init__(self, fid=OBJECT_FID, description="", filename="", object_data=None, mime_type=None): super().__init__(fid) self.description = description self.filename = filename self.mime_type = mime_type self.object_data = object_data @property def description(self): return self._description @description.setter @requireUnicode(1) def description(self, txt): self._description = txt @property def mime_type(self): return str(self._mime_type, "ascii") @mime_type.setter def mime_type(self, m): m = m or b'' self._mime_type = m if isinstance(m, bytes) else m.encode('ascii') @property def filename(self): return self._filename @filename.setter @requireUnicode(1) def filename(self, txt): self._filename = txt def parse(self, data, frame_header): """Parse the frame from ``data`` bytes using details from ``frame_header``. Data string format:
Text encoding $xx MIME type $00 Filename $00 (00) Content description $00 (00) Encapsulated object """ super().parse(data, frame_header) input = BytesIO(self.data) log.debug("GEOB frame data size: " + str(len(self.data))) self.encoding = encoding = input.read(1) # Mime type self._mime_type = b"" if self.header.minor_version != 2: ch = input.read(1) while ch not in (b'', b'\0'): self._mime_type += ch ch = input.read(1) else: # v2.2 (OBSOLETE) special case self._mime_type = input.read(3) log.debug("GEOB mime type: %s" % self._mime_type) if not self._mime_type: core.parseError(FrameException("GEOB frame does not contain a " "mime type")) if self._mime_type.find(b"/") == -1: core.parseError(FrameException("GEOB frame does not contain a " "valid mime type")) self.filename = "" self.description = "" # Remaining data is a NULL separated filename, description and object # data buffer = input.read() input.close() (filename, buffer) = splitUnicode(buffer, encoding) (desc, obj) = splitUnicode(buffer, encoding) self.filename = decodeUnicode(filename, encoding) log.debug("GEOB filename: " + self.filename) self.description = decodeUnicode(desc, encoding) log.debug("GEOB description: " + self.description) self.object_data = obj log.debug("GEOB data: %d bytes " % len(self.object_data)) if not self.object_data: core.parseError(FrameException("GEOB frame does not contain any " "data")) def render(self): self._initEncoding() data = (self.encoding + self._mime_type + b"\x00" + self.filename.encode(id3EncodingToString(self.encoding)) + self.text_delim + self.description.encode(id3EncodingToString(self.encoding)) + self.text_delim + (self.object_data or b"")) self.data = data return super(ObjectFrame, self).render() class PrivateFrame(Frame): """PRIV""" owner_id: bytes owner_data: bytes def __init__(self, id=PRIVATE_FID, owner_id=b"", owner_data=b""): super().__init__(id) assert id == PRIVATE_FID for arg in (owner_id, owner_data): if type(arg) is not bytes: raise ValueError("PRIV owner fields require bytes type") self.owner_id = owner_id self.owner_data = owner_data def parse(self, data, frame_header): super().parse(data, frame_header) try: self.owner_id, self.owner_data = self.data.split(b"\x00", 1) except ValueError: # If data doesn't contain required \x00 # all data is taken to be owner_id self.owner_id = self.data def render(self): self.data = self.owner_id + b"\x00" + self.owner_data return super(PrivateFrame, self).render() class MusicCDIdFrame(Frame): def __init__(self, id=CDID_FID, toc=b""): super(MusicCDIdFrame, self).__init__(id) assert(id == CDID_FID) self.toc = toc @property def toc(self): return self.data @toc.setter def toc(self, toc): self.data = toc def parse(self, data, frame_header): super().parse(data, frame_header) self.toc = self.data class PlayCountFrame(Frame): def __init__(self, id=PLAYCOUNT_FID, count=0): super(PlayCountFrame, self).__init__(id) assert(self.id == PLAYCOUNT_FID) if count is None or count < 0: raise ValueError("Invalid count value: %s" % str(count)) self.count = count def parse(self, data, frame_header): super().parse(data, frame_header) # data of less then 4 bytes is handled with with 'sz' arg if len(self.data) < 4: log.warning("Fixing invalid PCNT frame: less than 32 bits") self.count = bytes2dec(self.data) def render(self): self.data = dec2bytes(self.count, 32) return super(PlayCountFrame, self).render() class PopularityFrame(Frame): """Frame type for 'POPM' frames; popularity. Frame format:
Email to user $00 Rating $xx Counter $xx xx xx xx (xx ...) """ def __init__(self, id=POPULARITY_FID, email=b"", rating=0, count=0): super(PopularityFrame, self).__init__(id) assert(self.id == POPULARITY_FID) self.email = email self.rating = rating if count is None or count < 0: raise ValueError("Invalid count value: %s" % str(count)) self.count = count @property def rating(self): return self._rating @rating.setter def rating(self, rating): if rating < 0 or rating > 255: raise ValueError("Popularity rating must be >= 0 and <=255") self._rating = rating @property def email(self): return self._email @email.setter def email(self, email): # XXX: becoming a pattern? if isinstance(email, str): self._email = email.encode("ascii") elif isinstance(email, bytes): _ = email.decode("ascii") # noqa self._email = email else: raise TypeError("bytes, str, unicode email required") @property def count(self): return self._count @count.setter def count(self, count): if count < 0: raise ValueError("Popularity count must be > 0") self._count = count def parse(self, data, frame_header): super().parse(data, frame_header) data = self.data null_byte = data.find(b'\x00') try: self.email = data[:null_byte] except UnicodeDecodeError: core.parseError(FrameException("Invalid (non-ascii) POPM email " "address. Setting to 'BOGUS'")) self.email = b"BOGUS" data = data[null_byte + 1:] self.rating = bytes2dec(data[0:1]) data = data[1:] if len(self.data) < 4: core.parseError(FrameException( "Invalid POPM play count: less than 32 bits.")) self.count = bytes2dec(data) def render(self): data = (self.email or b"") + b'\x00' data += dec2bytes(self.rating) data += dec2bytes(self.count, 32) self.data = data return super(PopularityFrame, self).render() class UniqueFileIDFrame(Frame): def __init__(self, id=UNIQUE_FILE_ID_FID, owner_id=b"", uniq_id=b""): super().__init__(id) assert(self.id == UNIQUE_FILE_ID_FID) self.owner_id = owner_id self.uniq_id = uniq_id @property def owner_id(self): return self._owner_id @owner_id.setter def owner_id(self, oid): self._owner_id = b(oid) if oid else b"" @property def uniq_id(self): return self._uniq_id @uniq_id.setter def uniq_id(self, uid): self._uniq_id = b(uid) if uid else b"" def parse(self, data, frame_header): """ Data format Owner identifier $00 Identifier up to 64 bytes binary data> """ super().parse(data, frame_header) split_data = self.data.split(b'\x00', 1) if len(split_data) == 2: (self.owner_id, self.uniq_id) = split_data else: self.owner_id, self.uniq_id = b"", b"".join(split_data[0:1]) log.debug("UFID owner_id: %s" % self.owner_id) log.debug("UFID id: %s" % self.uniq_id) if not self.owner_id: dummy_owner_id = "http://www.id3.org/dummy/ufid.html" self.owner_id = dummy_owner_id core.parseError(FrameException("Invalid UFID, owner_id is empty. " "Setting to '%s'" % dummy_owner_id)) elif 0 <= len(self.uniq_id) > 64: core.parseError(FrameException("Invalid UFID, ID is empty or too " "long: %s" % self.uniq_id)) def render(self): assert isinstance(self.owner_id, bytes) assert isinstance(self.uniq_id, bytes) self.data = self.owner_id + b"\x00" + self.uniq_id return super().render() class LanguageCodeMixin(object): @property def lang(self): assert self._lang is not None return self._lang @lang.setter @requireBytes(1) def lang(self, lang): if not lang: self._lang = b"" return lang = lang.strip(b"\00") lang = lang[:3] if lang else DEFAULT_LANG try: if lang != DEFAULT_LANG: lang.decode("ascii") except UnicodeDecodeError: lang = DEFAULT_LANG assert len(lang) <= 3 self._lang = lang def _renderLang(self): lang = self.lang if len(lang) < 3: lang = lang + (b"\x00" * (3 - len(lang))) return lang class DescriptionLangTextFrame(Frame, LanguageCodeMixin): @requireBytes(1, 3) @requireUnicode(2, 4) def __init__(self, id, description, lang, text): super().__init__(id) self.lang = lang self.description = description self.text = text @property def description(self): return self._description @description.setter @requireUnicode(1) def description(self, description): self._description = description @property def text(self): return self._text @text.setter @requireUnicode(1) def text(self, text): self._text = text def parse(self, data, frame_header): super().parse(data, frame_header) self.encoding = self.data[0:1] self.lang = self.data[1:4] log.debug("%s lang: %s" % (self.id, self.lang)) try: (d, t) = splitUnicode(self.data[4:], self.encoding) self.description = decodeUnicode(d, self.encoding) log.debug("%s description: %s" % (self.id, self.description)) self.text = decodeUnicode(t, self.encoding) log.debug("%s text: %s" % (self.id, self.text)) except ValueError: log.warning("Invalid %s frame; no description/text" % self.id) self.description = "" self.text = "" def render(self): lang = self._renderLang() self._initEncoding() data = (self.encoding + lang + self.description.encode(id3EncodingToString(self.encoding)) + self.text_delim + self.text.encode(id3EncodingToString(self.encoding))) self.data = data return super(DescriptionLangTextFrame, self).render() class CommentFrame(DescriptionLangTextFrame): def __init__(self, id=COMMENT_FID, description="", lang=DEFAULT_LANG, text=""): super(CommentFrame, self).__init__(id, description, lang, text) assert(self.id == COMMENT_FID) class LyricsFrame(DescriptionLangTextFrame): def __init__(self, id=LYRICS_FID, description="", lang=DEFAULT_LANG, text=""): super(LyricsFrame, self).__init__(id, description, lang, text) assert(self.id == LYRICS_FID) class TermsOfUseFrame(Frame, LanguageCodeMixin): @requireUnicode("text") def __init__(self, id=b"USER", text="", lang=DEFAULT_LANG): super(TermsOfUseFrame, self).__init__(id) self.lang = lang self.text = text @property def text(self): return self._text @text.setter @requireUnicode(1) def text(self, text): self._text = text def parse(self, data, frame_header): super().parse(data, frame_header) self.encoding = encoding = self.data[0:1] self.lang = self.data[1:4] log.debug("%s lang: %s" % (self.id, self.lang)) self.text = decodeUnicode(self.data[4:], encoding) log.debug("%s text: %s" % (self.id, self.text)) def render(self): lang = self._renderLang() self._initEncoding() self.data = (self.encoding + lang + self.text.encode(id3EncodingToString(self.encoding))) return super(TermsOfUseFrame, self).render() class TocFrame(Frame): """Table of content frame. There may be more than one, but only one may have the top-level flag set. Data format: Element ID: \x00 TOC flags: %000000ab Entry count: %xx Child elem IDs: \x00 (... num entry count) Description: TIT2 frame (optional) """ TOP_LEVEL_FLAG_BIT = 6 ORDERED_FLAG_BIT = 7 @requireBytes(1, 2) def __init__(self, id=TOC_FID, element_id=None, toplevel=True, ordered=True, child_ids=None, description=None): assert id == TOC_FID super().__init__(id) self.element_id = element_id self.toplevel = toplevel self.ordered = ordered self.child_ids = child_ids or [] self.description = description def parse(self, data, frame_header): super().parse(data, frame_header) data = self.data log.debug("CTOC frame data size: %d" % len(data)) null_byte = data.find(b'\x00') self.element_id = data[0:null_byte] data = data[null_byte + 1:] flag_bits = bytes2bin(data[0:1]) self.toplevel = bool(flag_bits[self.TOP_LEVEL_FLAG_BIT]) self.ordered = bool(flag_bits[self.ORDERED_FLAG_BIT]) entry_count = bytes2dec(data[1:2]) data = data[2:] self.child_ids = [] for _ in range(entry_count): null_byte = data.find(b'\x00') self.child_ids.append(data[:null_byte]) data = data[null_byte + 1:] # Any data remaining must be a TIT2 frame self.description = None if data and data[:4] != b"TIT2": log.warning("Invalid toc data, TIT2 frame expected") return elif data: data = BytesIO(data) frame_header = FrameHeader.parse(data, self.header.version) data = data.read() description_frame = TextFrame(TITLE_FID) description_frame.parse(data, frame_header) self.description = description_frame.text def render(self): flags = [0] * 8 if self.toplevel: flags[self.TOP_LEVEL_FLAG_BIT] = 1 if self.ordered: flags[self.ORDERED_FLAG_BIT] = 1 data = (self.element_id + b'\x00' + bin2bytes(flags) + dec2bytes(len(self.child_ids))) for cid in self.child_ids: data += cid + b'\x00' if self.description is not None: desc_frame = TextFrame(TITLE_FID, self.description) desc_frame.header = FrameHeader(TITLE_FID, self.header.version) data += desc_frame.render() self.data = data return super().render() class RelVolAdjFrameV24(Frame): CHANNEL_TYPE_OTHER = 0 CHANNEL_TYPE_MASTER = 1 CHANNEL_TYPE_FRONT_RIGHT = 2 CHANNEL_TYPE_FRONT_LEFT = 3 CHANNEL_TYPE_BACK_RIGHT = 4 CHANNEL_TYPE_BACK_LEFT = 5 CHANNEL_TYPE_FRONT_CENTER = 6 CHANNEL_TYPE_BACK_CENTER = 7 CHANNEL_TYPE_BASS = 8 _render_strict = False @property def identifier(self): return str(self._identifier, "latin1") @identifier.setter def identifier(self, ident): if type(ident) != bytes: ident = ident.encode("latin1") self._identifier = ident @property def channel_type(self): return self._channel_type @channel_type.setter def channel_type(self, t): if 0 <= t <= 8: self._channel_type = t else: raise ValueError(f"Invalid type {t}") @property def adjustment(self): return (self._adjustment or 0) / 512 @adjustment.setter def adjustment(self, adj): self._adjustment = adj * 512 @property def peak(self): return self._peak @peak.setter def peak(self, v): self._peak = v def __init__(self, fid=b"RVA2", identifier=None, channel_type=None, adjustment=None, peak=None): assert fid == b"RVA2" super().__init__(fid) self.identifier = identifier or "" self.channel_type = channel_type or self.CHANNEL_TYPE_OTHER self.adjustment = adjustment or 0 self.peak = peak or 0 def parse(self, data, frame_header): super().parse(data, frame_header) if self.header.version != ID3_V2_4: raise FrameException(f"Invalid frame version: {self.header.version}") data = self.data self.identifier, data = data.split(b"\x00", maxsplit=1) self.channel_type = data[0] self._adjustment = bytes2signedInt16(data[1:3]) if len(data) > 3: bits_per_peak = data[3] if bits_per_peak: self._peak = bytes2dec(data[4:4 + (bits_per_peak // 8)]) log.debug(f"Parsed RVA2: identifier={self.identifier} channel_type={self.channel_type} " f"adjustment={self.adjustment} _adjustment={self._adjustment} peak={self.peak}") def render(self): assert self._channel_type is not None if self.header is None: self.header = FrameHeader(self.id, ID3_V2_4) assert self.header.version == ID3_V2_4 self.data =\ self._identifier + b"\x00" +\ dec2bytes(self._channel_type) +\ signedInt162bytes(self._adjustment or 0) if self._peak: peak_data = b"" num_pk_bits = len(dec2bin(self._peak)) for sz in (8, 16, 32): if num_pk_bits > sz: continue peak_data += dec2bytes(sz, 8) + dec2bytes(self._peak, sz) break if not peak_data: raise ValueError(f"Peak value out of range: {self._peak}") self.data += peak_data return super().render() class RelVolAdjFrameV23(Frame): FRONT_CHANNEL_RIGHT_BIT = 0 FRONT_CHANNEL_LEFT_BIT = 1 BACK_CHANNEL_RIGHT_BIT = 2 BACK_CHANNEL_LEFT_BIT = 3 FRONT_CENTER_CHANNEL_BIT = 4 BASS_CHANNEL_BIT = 5 CHANNEL_DEFN = [("front_right", FRONT_CHANNEL_RIGHT_BIT), ("front_left", FRONT_CHANNEL_LEFT_BIT), ("front_right_peak", None), ("front_left_peak", None), ("back_right", BACK_CHANNEL_RIGHT_BIT), ("back_left", BACK_CHANNEL_LEFT_BIT), ("back_right_peak", None), ("back_left_peak", None), ("front_center", FRONT_CENTER_CHANNEL_BIT), ("front_center_peak", None), ("bass", BASS_CHANNEL_BIT), ("bass_peak", None), ] @dataclasses.dataclass class VolumeAdjustments: master: int = 0 master_peak: int = 0 front_right: int = 0 front_left: int = 0 front_right_peak: int = 0 front_left_peak: int = 0 back_right: int = 0 back_left: int = 0 back_right_peak: int = 0 back_left_peak: int = 0 front_center: int = 0 front_center_peak: int = 0 back_center: int = 0 back_center_peak: int = 0 bass: int = 0 bass_peak: int = 0 other: int = 0 other_peak: int = 0 _channel_map = { RelVolAdjFrameV24.CHANNEL_TYPE_MASTER: "master", RelVolAdjFrameV24.CHANNEL_TYPE_OTHER: "other", RelVolAdjFrameV24.CHANNEL_TYPE_FRONT_RIGHT: "front_right", RelVolAdjFrameV24.CHANNEL_TYPE_FRONT_LEFT: "front_left", RelVolAdjFrameV24.CHANNEL_TYPE_BACK_RIGHT: "back_right", RelVolAdjFrameV24.CHANNEL_TYPE_BACK_LEFT: "back_left", RelVolAdjFrameV24.CHANNEL_TYPE_FRONT_CENTER: "front_center", RelVolAdjFrameV24.CHANNEL_TYPE_BACK_CENTER: "back_center", RelVolAdjFrameV24.CHANNEL_TYPE_BASS: "bass", } @property def has_master_channel(self) -> bool: return bool(self.master or self.master_peak) @property def has_front_channel(self) -> bool: return bool( self.front_right or self.front_left or self.front_right_peak or self.front_left_peak ) @property def has_back_channel(self) -> bool: return bool( self.back_right or self.back_left or self.back_right_peak or self.back_left_peak ) @property def has_front_center_channel(self) -> bool: return bool(self.front_center or self.front_center_peak) @property def has_back_center_channel(self) -> bool: return bool(self.back_center or self.back_center_peak) @property def has_bass_channel(self) -> bool: return bool(self.bass or self.bass_peak) @property def has_other_channel(self) -> bool: return bool(self.other or self.other_peak) def boundsCheck(self): invalids = [] for name, value in dataclasses.asdict(self).items(): if value > 65536 or value < -65536: invalids.append(name) if invalids: raise ValueError(f"Invalid RVAD channel values: {','.join(invalids)}") def setChannelAdj(self, chan_type, value): setattr(self, self._channel_map[chan_type], value) def setChannelPeak(self, chan_type, value): setattr(self, f"{self._channel_map[chan_type]}_peak", value) def __init__(self, fid=b"RVAD"): assert fid == b"RVAD" super().__init__(fid) self.adjustments = None def toV24(self) -> list: """Return a list of RVA2 frames""" converted = [] def append(ch_type, ch_adj, ch_peak): if not ch_adj and not ch_peak: return converted.append( RelVolAdjFrameV24(channel_type=ch_type, adjustment=ch_adj / 512, peak=ch_peak) ) for channel in ["front_right", "front_left", "back_right", "back_left", "front_center", "bass"]: chtype = getattr(RelVolAdjFrameV24, f"CHANNEL_TYPE_{channel.upper()}") adj = getattr(self.adjustments, channel) pk = getattr(self.adjustments, f"{channel}_peak") append(chtype, adj, pk) return converted def parse(self, data, frame_header): super().parse(data, frame_header) if self.header.version not in (ID3_V2_3, ID3_V2_2): raise FrameException("Invalid v2.4 frame: RVAD") data = self.data inc_dec_bit_list = bytes2bin(bytes([data[0]])) inc_dec_bit_list.reverse() bytes_per_vol = data[1] // 8 if bytes_per_vol > 2: raise FrameException("RVAD volume adj out of bounds") self.adjustments = self.VolumeAdjustments() offset = 2 for adj_name, inc_dec_bit in self.CHANNEL_DEFN: if offset >= len(data): break adj_val = bytes2dec(data[offset:offset + bytes_per_vol]) offset += bytes_per_vol if (inc_dec_bit is not None and adj_val and inc_dec_bit_list[inc_dec_bit] == 0): # Decrement adj_val = -adj_val setattr(self.adjustments, adj_name, adj_val) try: log.debug(f"Parsed RVAD frames adjustments: {self.adjustments}") self.adjustments.boundsCheck() except ValueError: # pragma: nocover self.adjustments = None raise def render(self): data = b"" inc_dec_bits = [0] * 8 if self.header is None: self.header = FrameHeader(self.id, ID3_V2_3) assert self.header.version == ID3_V2_3 self.adjustments.boundsCheck() # May raise ValueError # Only the front channel is required inc_dec_bits[self.FRONT_CHANNEL_RIGHT_BIT] = 1 if self.adjustments.front_right > 0 else 0 inc_dec_bits[self.FRONT_CHANNEL_LEFT_BIT] = 1 if self.adjustments.front_left > 0 else 0 data += dec2bytes(abs(self.adjustments.front_right), p=16) data += dec2bytes(abs(self.adjustments.front_left), p=16) data += dec2bytes(abs(self.adjustments.front_right_peak), p=16) data += dec2bytes(abs(self.adjustments.front_left_peak), p=16) # Back channel if True in (self.adjustments.has_bass_channel, self.adjustments.has_front_center_channel, self.adjustments.has_back_channel): inc_dec_bits[self.BACK_CHANNEL_RIGHT_BIT] = 1 if self.adjustments.back_right > 0 else 0 inc_dec_bits[self.BACK_CHANNEL_LEFT_BIT] = 1 if self.adjustments.back_left > 0 else 0 data += dec2bytes(abs(self.adjustments.back_right), p=16) data += dec2bytes(abs(self.adjustments.back_left), p=16) data += dec2bytes(abs(self.adjustments.back_right_peak), p=16) data += dec2bytes(abs(self.adjustments.back_left_peak), p=16) # Center (front) channel if True in (self.adjustments.has_bass_channel, self.adjustments.has_front_center_channel): inc_dec_bits[self.FRONT_CENTER_CHANNEL_BIT] = 1 if self.adjustments.front_center > 0 \ else 0 data += dec2bytes(abs(self.adjustments.front_center), p=16) data += dec2bytes(abs(self.adjustments.front_center_peak), p=16) # Bass channel if self.adjustments.has_bass_channel: inc_dec_bits[self.BASS_CHANNEL_BIT] = 1 if self.adjustments.bass > 0 else 0 data += dec2bytes(abs(self.adjustments.bass), p=16) data += dec2bytes(abs(self.adjustments.bass_peak), p=16) self.data = bin2bytes(reversed(inc_dec_bits)) + b"\x10" + data return super().render() StartEndTuple = namedtuple("StartEndTuple", ["start", "end"]) """A 2-tuple, with names 'start' and 'end'.""" class ChapterFrame(Frame): """Frame type for chapter/section of the audio file. (10 bytes) Element ID $00 Start time $xx xx xx xx End time $xx xx xx xx Start offset $xx xx xx xx End offset $xx xx xx xx """ NO_OFFSET = 4294967295 """No offset value, aka '0xff0xff0xff0xff'""" def __init__(self, id=CHAPTER_FID, element_id=None, times=None, offsets=None, sub_frames=None): assert(id == CHAPTER_FID) super(ChapterFrame, self).__init__(id) self.element_id = element_id self.times = times or StartEndTuple(None, None) self.offsets = offsets or StartEndTuple(None, None) self.sub_frames = sub_frames or FrameSet() def parse(self, data, frame_header): from .headers import TagHeader, ExtendedTagHeader super().parse(data, frame_header) data = self.data log.debug("CTOC frame data size: %d" % len(data)) null_byte = data.find(b'\x00') self.element_id = data[0:null_byte] data = data[null_byte + 1:] start = bytes2dec(data[:4]) data = data[4:] end = bytes2dec(data[:4]) data = data[4:] self.times = StartEndTuple(start, end) start = bytes2dec(data[:4]) data = data[4:] end = bytes2dec(data[:4]) data = data[4:] self.offsets = StartEndTuple(start if start != self.NO_OFFSET else None, end if end != self.NO_OFFSET else None) if data: dummy_tag_header = TagHeader(self.header.version) dummy_tag_header.tag_size = len(data) _ = self.sub_frames.parse(BytesIO(data), dummy_tag_header, # noqa ExtendedTagHeader()) else: self.sub_frames = FrameSet() def render(self): data = self.element_id + b'\x00' for n in self.times + self.offsets: if n is not None: data += dec2bytes(n, 32) else: data += b'\xff\xff\xff\xff' for f in self.sub_frames.getAllFrames(): f.header = FrameHeader(f.id, self.header.version) data += f.render() self.data = data return super(ChapterFrame, self).render() @property def title(self): if TITLE_FID in self.sub_frames: return self.sub_frames[TITLE_FID][0].text return None @title.setter def title(self, title): self.sub_frames.setTextFrame(TITLE_FID, title) @property def subtitle(self): if SUBTITLE_FID in self.sub_frames: return self.sub_frames[SUBTITLE_FID][0].text return None @subtitle.setter def subtitle(self, subtitle): self.sub_frames.setTextFrame(SUBTITLE_FID, subtitle) @property def user_url(self): if USERURL_FID in self.sub_frames: frame = self.sub_frames[USERURL_FID][0] # Not returning frame description, it is always the same since it # allows only 1 URL. return frame.url return None @user_url.setter def user_url(self, url): DESCRIPTION = "chapter url" if url is None: del self.sub_frames[USERURL_FID] else: if USERURL_FID in self.sub_frames: for frame in self.sub_frames[USERURL_FID]: if frame.description == DESCRIPTION: frame.url = url return self.sub_frames[USERURL_FID] = UserUrlFrame(USERURL_FID, DESCRIPTION, url) # XXX: This data structure pretty much sucks, or it is beautiful anarchy class FrameSet(dict): def __init__(self): dict.__init__(self) self._unknown_frame_ids = set() def parse(self, f, tag_header, extended_header): """Read frames starting from the current read position of the file object. Returns the amount of padding which occurs after the tag, but before the audio content. A return value of 0 does not mean error.""" self.clear() self._unknown_frame_ids.clear() padding_size = 0 size_left = tag_header.tag_size - extended_header.size consumed_size = 0 # Handle a tag-level unsync. Some frames may have their own unsync bit # set instead. tag_data = f.read(size_left) # If the tag is 2.3 and the tag header unsync bit is set then all the # frame data is deunsync'd at once, otherwise it will happen on a per-frame basis. if tag_header.unsync and tag_header.version <= ID3_V2_3: log.debug("De-unsynching %d bytes at once (<= 2.3 tag)" % len(tag_data)) og_size = len(tag_data) tag_data = deunsyncData(tag_data) size_left = len(tag_data) log.debug("De-unsynch'd %d bytes at once (<= 2.3 tag) to %d bytes" % (og_size, size_left)) # Adding bytes to simulate the tag header(s) in the buffer. This keeps # f.tell() values matching the file offsets for logging. prepadding = b'\x00' * 10 # Tag header prepadding += b'\x00' * extended_header.size tag_buffer = BytesIO(prepadding + tag_data) tag_buffer.seek(len(prepadding)) frame_count = 0 while size_left > 0: log.debug("size_left: " + str(size_left)) if size_left < (10 + 1): # The size of the smallest frame. log.debug("FrameSet: Implied padding (size_left= 2 and minor <= 4)): raise TagException("ID3 v%d.%d is not supported" % (major, minor)) self.version = (major, minor, rev) # 1 byte (first 4 bits): flags data = f.read(1) if not data: return False (self.unsync, self.extended, self.experimental, self.footer) = (bool(b) for b in bytes2bin(data)[0:4]) log.debug("TagHeader [flags]: unsync(%d) extended(%d) " "experimental(%d) footer(%d)" % (self.unsync, self.extended, self.experimental, self.footer)) # 4 bytes: The size of the extended header (if any), frames, and padding # afer unsynchronization. This is a sync safe integer, so only the # bottom 7 bits of each byte are used. tag_size_bytes = f.read(4) if len(tag_size_bytes) != 4: return False log.debug("TagHeader [size string]: 0x%02x%02x%02x%02x" % (tag_size_bytes[0], tag_size_bytes[1], tag_size_bytes[2], tag_size_bytes[3])) self.tag_size = bin2dec(bytes2bin(tag_size_bytes, 7)) log.debug("TagHeader [size]: %d (0x%x)" % (self.tag_size, self.tag_size)) return True def render(self, tag_len=None): if tag_len is not None: self.tag_size = tag_len if self.unsync: raise NotImplementedError("eyeD3 does not write (only reads) " "unsync'd data") data = b"ID3" data += bytes([self.minor_version]) + bytes([self.rev_version]) data += bin2bytes([int(self.unsync), int(self.extended), int(self.experimental), int(self.footer), 0, 0, 0, 0]) log.debug("Setting tag size to %d" % self.tag_size) data += bin2bytes(bin2synchsafe(dec2bin(self.tag_size, 32))) log.debug("TagHeader rendered %d bytes" % len(data)) return data class ExtendedTagHeader(object): RESTRICT_TAG_SZ_LARGE = 0x00 RESTRICT_TAG_SZ_MED = 0x01 RESTRICT_TAG_SZ_SMALL = 0x02 RESTRICT_TAG_SZ_TINY = 0x03 RESTRICT_TEXT_ENC_NONE = 0x00 RESTRICT_TEXT_ENC_UTF8 = 0x01 RESTRICT_TEXT_LEN_NONE = 0x00 RESTRICT_TEXT_LEN_1024 = 0x01 RESTRICT_TEXT_LEN_128 = 0x02 RESTRICT_TEXT_LEN_30 = 0x03 RESTRICT_IMG_ENC_NONE = 0x00 RESTRICT_IMG_ENC_PNG_JPG = 0x01 RESTRICT_IMG_SZ_NONE = 0x00 RESTRICT_IMG_SZ_256 = 0x01 RESTRICT_IMG_SZ_64 = 0x02 RESTRICT_IMG_SZ_64_EXACT = 0x03 def __init__(self): self.size = 0 self._flags = 0 self.crc = None self._restrictions = 0 @property def update_bit(self): return bool(self._flags & 0x40) @update_bit.setter def update_bit(self, v): if v: self._flags |= 0x40 else: self._flags &= ~0x40 @property def crc_bit(self): return bool(self._flags & 0x20) @crc_bit.setter def crc_bit(self, v): if v: self._flags |= 0x20 else: self._flags &= ~0x20 @property def crc(self): return self._crc @crc.setter def crc(self, v): self.crc_bit = 1 if v else 0 self._crc = v @property def restrictions_bit(self): return bool(self._flags & 0x10) @restrictions_bit.setter def restrictions_bit(self, v): if v: self._flags |= 0x10 else: self._flags &= ~0x10 @property def tag_size_restriction(self): return self._restrictions >> 6 @tag_size_restriction.setter def tag_size_restriction(self, v): assert(v >= 0 and v <= 3) self.restrictions_bit = 1 self._restrictions = (v << 6) | (self._restrictions & 0x3f) @property def tag_size_restriction_description(self): val = self.tag_size_restriction if val == ExtendedTagHeader.RESTRICT_TAG_SZ_LARGE: return "No more than 128 frames and 1 MB total tag size" elif val == ExtendedTagHeader.RESTRICT_TAG_SZ_MED: return "No more than 64 frames and 128 KB total tag size" elif val == ExtendedTagHeader.RESTRICT_TAG_SZ_SMALL: return "No more than 32 frames and 40 KB total tag size" elif val == ExtendedTagHeader.RESTRICT_TAG_SZ_TINY: return "No more than 32 frames and 4 KB total tag size" @property def text_enc_restriction(self): return (self._restrictions & 0x20) >> 5 @text_enc_restriction.setter def text_enc_restriction(self, v): assert(v == 0 or v == 1) self.restrictions_bit = 1 self._restrictions ^= 0x20 @property def text_enc_restriction_description(self): if self.text_enc_restriction: return "Strings are only encoded with ISO-8859-1 or UTF-8" else: return "None" @property def text_length_restriction(self): return (self._restrictions >> 3) & 0x03 @text_length_restriction.setter def text_length_restriction(self, v): assert(v >= 0 and v <= 3) self.restrictions_bit = 1 self._restrictions = (v << 3) | (self._restrictions & 0xe7) @property def text_length_restriction_description(self): val = self.text_length_restriction if val == ExtendedTagHeader.RESTRICT_TEXT_LEN_NONE: return "None" elif val == ExtendedTagHeader.RESTRICT_TEXT_LEN_1024: return "No string is longer than 1024 characters." elif val == ExtendedTagHeader.RESTRICT_TEXT_LEN_128: return "No string is longer than 128 characters." elif val == ExtendedTagHeader.RESTRICT_TEXT_LEN_30: return "No string is longer than 30 characters." @property def image_enc_restriction(self): return (self._restrictions & 0x04) >> 2 @image_enc_restriction.setter def image_enc_restriction(self, v): assert(v == 0 or v == 1) self.restrictions_bit = 1 self._restrictions ^= 0x04 @property def image_enc_restriction_description(self): if self.image_enc_restriction: return "Images are encoded only with PNG [PNG] or JPEG [JFIF]." else: return "None" @property def image_size_restriction(self): return self._restrictions & 0x03 @image_size_restriction.setter def image_size_restriction(self, v): assert(v >= 0 and v <= 3) self.restrictions_bit = 1 self._restrictions = v | (self._restrictions & 0xfc) @property def image_size_restriction_description(self): val = self.image_size_restriction if val == ExtendedTagHeader.RESTRICT_IMG_SZ_NONE: return "None" elif val == ExtendedTagHeader.RESTRICT_IMG_SZ_256: return "All images are 256x256 pixels or smaller." elif val == ExtendedTagHeader.RESTRICT_IMG_SZ_64: return "All images are 64x64 pixels or smaller." elif val == ExtendedTagHeader.RESTRICT_IMG_SZ_64_EXACT: return "All images are exactly 64x64 pixels, unless required "\ "otherwise." def _syncsafeCRC(self): return bytes([ (self.crc >> 28) & 0x7f, (self.crc >> 21) & 0x7f, (self.crc >> 14) & 0x7f, (self.crc >> 7) & 0x7f, (self.crc >> 0) & 0x7f, ]) def render(self, version, frame_data, padding=0): if version[0] != 2: raise ValueError(f"Invalid version: {version} != 2 (expected)") data = b"" if version[1] == 4: # Version 2.4 size = 6 # Extended flags. if self.update_bit: data += b"\x00" if self.crc_bit: data += b"\x05" # XXX: Using the absolute value of the CRC. The spec is unclear # about the type of this data. self.crc = int(math.fabs(binascii.crc32(frame_data + (b"\x00" * padding)))) crc_data = self._syncsafeCRC() if len(crc_data) < 5: # pad if necessary crc_data = (b"\x00" * (5 - len(crc_data))) + crc_data assert(len(crc_data) == 5) data += crc_data if self.restrictions_bit: data += b"\x01" data += bytes([self._restrictions]) log.debug("Rendered extended header data (%d bytes)" % len(data)) # Extended header size. size = bin2bytes(bin2synchsafe(dec2bin(len(data) + 6, 32))) assert(len(size) == 4) data = size + b"\x01" + bin2bytes(dec2bin(self._flags)) + data log.debug("Rendered extended header of size %d" % len(data)) else: # Version 2.3 size = 6 # Note, the 4 size bytes are not included in the size # Extended flags. f = [0] * 16 crc = None if self.crc_bit: f[0] = 1 # XXX: Using the absolute value of the CRC. The spec is unclear # about the type of this value. self.crc = int(math.fabs(binascii.crc32(frame_data + (b"\x00" * padding)))) crc = bin2bytes(dec2bin(self.crc)) assert(len(crc) == 4) size += 4 flags = bin2bytes(f) assert(len(flags) == 2) # Extended header size. size = bin2bytes(dec2bin(size, 32)) assert(len(size) == 4) # Padding size padding_size = bin2bytes(dec2bin(padding, 32)) data = size + flags + padding_size if crc: data += crc return data # Only call this when you *know* there is an extened header. def parse(self, fp, version): '''Parse an ID3 v2 extended header starting at the current position of ``fp`` and per the format defined by ``version``. This method should only be called when the presence of an extended header is known since it moves the file position. If a header is found but malformed an ``eyed3.id3.tag.TagException`` is thrown. The return value is ``None``. ''' from .tag import TagException assert(version[0] == 2) log.debug("Parsing extended header @ 0x%x" % fp.tell()) # First 4 bytes is the size of the extended header. data = fp.read(4) if version[1] == 4: # sync-safe sz = bin2dec(bytes2bin(data, 7)) self.size = sz log.debug("Extended header size (includes the 4 size bytes): %d" % sz) data = fp.read(sz - 4) # Number of flag bytes if data[0] != 1 or (data[1] & 0x8f): # As of 2.4 the first byte is 1 and the second can only have # bits 6, 5, and 4 set. raise TagException("Invalid Extended Header") self._flags = data[1] log.debug("Extended header flags: %x" % self._flags) offset = 2 if self.update_bit: log.debug("Extended header has update bit set") assert(data[offset] == 0) offset += 1 if self.crc_bit: log.debug("Extended header has CRC bit set") assert(data[offset] == 5) offset += 1 crc_data = data[offset:offset + 5] # This is sync-safe. self.crc = bin2dec(bytes2bin(crc_data, 7)) log.debug("Extended header CRC: %d" % self.crc) offset += 5 if self.restrictions_bit: log.debug("Extended header has restrictions bit set") assert(data[offset] == 1) offset += 1 self._restrictions = data[offset] offset += 1 else: # v2.3 is totally different... *sigh* sz = bin2dec(bytes2bin(data)) self.size = sz log.debug("Extended header size (not including 4 size bytes): %d" % sz) tmpFlags = fp.read(2) # Read the padding size, but it'll be computed during the parse. ps = fp.read(4) log.debug("Extended header says there is %d bytes of padding" % bin2dec(bytes2bin(ps))) # Make this look like a v2.4 mask. self._flags = tmpFlags[0] >> 2 if self.crc_bit: log.debug("Extended header has CRC bit set") crc_data = fp.read(4) self.crc = bin2dec(bytes2bin(crc_data)) log.debug("Extended header CRC: %d" % self.crc) class FrameHeader: """A header for each and every ID3 frame in a tag.""" # 2.4 not only added flag bits, but also reordered the previously defined # flags. So these are mapped once the ID3 version is known. Access through # 'self', always TAG_ALTER = None FILE_ALTER = None READ_ONLY = None COMPRESSED = None ENCRYPTED = None GROUPED = None UNSYNC = None DATA_LEN = None # Constructor. @requireBytes(1) def __init__(self, fid, version): self._version = version self._setBitMask() # _setBitMask will throw if the version is no good # Correctly set size of header (v2.2 is smaller) self.size = 10 if self.minor_version != 2 else 6 # The frame header itself... self.id = fid # First 4 bytes, frame ID self._flags = [0] * 16 # 16 bits, represented here as a list self.data_size = 0 # 4 bytes, size of frame data def copyFlags(self, rhs): self.tag_alter = rhs._flags[rhs.TAG_ALTER] self.file_alter = rhs._flags[rhs.FILE_ALTER] self.read_only = rhs._flags[rhs.READ_ONLY] self.compressed = rhs._flags[rhs.COMPRESSED] self.encrypted = rhs._flags[rhs.ENCRYPTED] self.grouped = rhs._flags[rhs.GROUPED] self.unsync = rhs._flags[rhs.UNSYNC] self.data_length_indicator = rhs._flags[rhs.DATA_LEN] @property def major_version(self): return self._version[0] @property def minor_version(self): return self._version[1] @property def version(self): return self._version @property def tag_alter(self): return self._flags[self.TAG_ALTER] @tag_alter.setter def tag_alter(self, b): self._flags[self.TAG_ALTER] = int(bool(b)) @property def file_alter(self): return self._flags[self.FILE_ALTER] @file_alter.setter def file_alter(self, b): self._flags[self.FILE_ALTER] = int(bool(b)) @property def read_only(self): return self._flags[self.READ_ONLY] @read_only.setter def read_only(self, b): self._flags[self.READ_ONLY] = int(bool(b)) @property def compressed(self): return self._flags[self.COMPRESSED] @compressed.setter def compressed(self, b): self._flags[self.COMPRESSED] = int(bool(b)) @property def encrypted(self): return self._flags[self.ENCRYPTED] @encrypted.setter def encrypted(self, b): self._flags[self.ENCRYPTED] = int(bool(b)) @property def grouped(self): return self._flags[self.GROUPED] @grouped.setter def grouped(self, b): self._flags[self.GROUPED] = int(bool(b)) @property def unsync(self): return self._flags[self.UNSYNC] @unsync.setter def unsync(self, b): self._flags[self.UNSYNC] = int(bool(b)) @property def data_length_indicator(self): return self._flags[self.DATA_LEN] @data_length_indicator.setter def data_length_indicator(self, b): self._flags[self.DATA_LEN] = int(bool(b)) def _setBitMask(self): major = self.major_version minor = self.minor_version # 1.x tags are converted to 2.4 frames internally. These frames are # created with frame flags \x00. if (major == 2 and minor in (3, 2)): # v2.2 does not contain flags, but set anyway, as long as the # values remain 0 all is good self.TAG_ALTER = 0 self.FILE_ALTER = 1 self.READ_ONLY = 2 self.COMPRESSED = 8 self.ENCRYPTED = 9 self.GROUPED = 10 # This is not in 2.3 frame header flags, map to unused self.UNSYNC = 14 # This is not in 2.3 frame header flags, map to unused self.DATA_LEN = 4 elif ((major == 2 and minor == 4) or (major == 1 and minor in (0, 1))): self.TAG_ALTER = 1 self.FILE_ALTER = 2 self.READ_ONLY = 3 self.COMPRESSED = 12 self.ENCRYPTED = 13 self.GROUPED = 9 self.UNSYNC = 14 self.DATA_LEN = 15 else: raise ValueError("ID3 v" + str(major) + "." + str(minor) + " is not supported.") def render(self, data_size): data = b'' assert(type(self.id) is bytes) data += self.id self.data_size = data_size if self.minor_version == 3: data += bin2bytes(dec2bin(data_size, 32)) else: data += bin2bytes(bin2synchsafe(dec2bin(data_size, 32))) if self.unsync: raise NotImplementedError("eyeD3 does not write (only reads) " "unsync'd data") data += bin2bytes(self._flags) return data @staticmethod def _parse2_2(f, version): from .frames import map2_2FrameId from .frames import FrameException frame_id_22 = f.read(3) frame_id = map2_2FrameId(frame_id_22) if FrameHeader._isValidFrameId(frame_id): log.debug("FrameHeader [id]: %s (0x%x%x%x)" % (frame_id_22, frame_id_22[0], frame_id_22[1], frame_id_22[2])) frame_header = FrameHeader(frame_id, version) # data_size corresponds to the size of the data segment after # encryption, compression, and unsynchronization. sz = f.read(3) frame_header.data_size = bin2dec(bytes2bin(sz, 8)) log.debug("FrameHeader [data size]: %d (0x%X)" % (frame_header.data_size, frame_header.data_size)) return frame_header elif frame_id == b'\x00\x00\x00': log.debug("FrameHeader: Null frame id found at byte %d" % f.tell()) else: core.parseError(FrameException("FrameHeader: Illegal Frame ID: %s" % frame_id)) return None @staticmethod def parse(f, version): from .frames import FrameException log.debug("FrameHeader [start byte]: %d (0x%X)" % (f.tell(), f.tell())) major_version, minor_version = version[:2] if minor_version == 2: return FrameHeader._parse2_2(f, version) frame_id = f.read(4) if FrameHeader._isValidFrameId(frame_id): log.debug("FrameHeader [id]: %s (0x%x%x%x%x)" % (frame_id, frame_id[0], frame_id[1], frame_id[2], frame_id[3])) frame_header = FrameHeader(frame_id, version) # data_size corresponds to the size of the data segment after # encryption, compression, and unsynchronization. sz = f.read(4) # In ID3 v2.4 this value became a synch-safe integer, meaning only # the low 7 bits are used per byte. if minor_version == 3: frame_header.data_size = bin2dec(bytes2bin(sz, 8)) else: frame_header.data_size = bin2dec(bytes2bin(sz, 7)) log.debug("FrameHeader [data size]: %d (0x%X)" % (frame_header.data_size, frame_header.data_size)) # Frame flags. flags = f.read(2) frame_header._flags = bytes2bin(flags) if log.getEffectiveLevel() <= logging.DEBUG: log.debug("FrameHeader [flags]: ta(%d) fa(%d) ro(%d) co(%d) " "en(%d) gr(%d) un(%d) dl(%d)" % (frame_header.tag_alter, frame_header.file_alter, frame_header.read_only, frame_header.compressed, frame_header.encrypted, frame_header.grouped, frame_header.unsync, frame_header.data_length_indicator)) if (frame_header.minor_version >= 4 and frame_header.compressed and not frame_header.data_length_indicator): core.parseError(FrameException("Invalid frame; compressed with " "no data length indicator")) return frame_header elif frame_id == b'\x00' * 4: log.debug("FrameHeader: Null frame id found at byte %d" % f.tell()) else: core.parseError(FrameException("FrameHeader: Illegal Frame ID: %s" % frame_id)) return None @staticmethod def _isValidFrameId(id): import re return re.compile(b"^[A-Z0-9][A-Z0-9][A-Z0-9][A-Z0-9]$").match(id) eyeD3-0.9.7/eyed3/id3/tag.py000066400000000000000000002165651432016011500153250ustar00rootroot00000000000000import os import codecs import string import shutil import tempfile import textwrap from ..utils import requireUnicode, chunkCopy, datePicker, b from .. import core from ..core import TXXX_ALBUM_TYPE, TXXX_ARTIST_ORIGIN, ALBUM_TYPE_IDS, ArtistOrigin from .. import Error from . import (ID3_ANY_VERSION, ID3_DEFAULT_VERSION, ID3_V1, ID3_V1_0, ID3_V1_1, ID3_V2, ID3_V2_2, ID3_V2_3, ID3_V2_4, versionToString) from . import DEFAULT_LANG from . import Genre from . import frames from .headers import TagHeader, ExtendedTagHeader from ..utils.log import getLogger log = getLogger(__name__) ID3_V1_COMMENT_DESC = "ID3v1.x Comment" ID3_V1_MAX_TEXTLEN = 30 ID3_V1_STRIP_CHARS = string.whitespace.encode("latin1") + b"\x00" DEFAULT_PADDING = 256 class TagException(Error): pass class Tag(core.Tag): def __init__(self, version=ID3_DEFAULT_VERSION, **kwargs): self.file_info = None self.header = None self.extended_header = None self.frame_set = None self._comments = None self._images = None self._lyrics = None self._objects = None self._privates = None self._user_texts = None self._unique_file_ids = None self._user_urls = None self._chapters = None self._tocs = None self._popularities = None self.file_info = None self.clear(version=version) super().__init__(**kwargs) def clear(self, *, version=ID3_DEFAULT_VERSION): """Reset all tag data.""" # ID3 tag header self.header = TagHeader(version=version) # Optional extended header in v2 tags. self.extended_header = ExtendedTagHeader() # Contains the tag's frames. ID3v1 fields are read and converted # the the corresponding v2 frame. self.frame_set = frames.FrameSet() self._comments = CommentsAccessor(self.frame_set) self._images = ImagesAccessor(self.frame_set) self._lyrics = LyricsAccessor(self.frame_set) self._objects = ObjectsAccessor(self.frame_set) self._privates = PrivatesAccessor(self.frame_set) self._user_texts = UserTextsAccessor(self.frame_set) self._unique_file_ids = UniqueFileIdAccessor(self.frame_set) self._user_urls = UserUrlsAccessor(self.frame_set) self._chapters = ChaptersAccessor(self.frame_set) self._tocs = TocAccessor(self.frame_set) self._popularities = PopularitiesAccessor(self.frame_set) def parse(self, fileobj, version=ID3_ANY_VERSION): self.clear() version = version or ID3_ANY_VERSION close_file = False try: filename = fileobj.name except AttributeError: if type(fileobj) is str: filename = fileobj fileobj = open(filename, "rb") close_file = True else: raise ValueError(f"Invalid type: {type(fileobj)}") self.file_info = FileInfo(filename) try: tag_found = False padding = 0 # The & is for supporting the "meta" versions, any, etc. if version[0] & 2: tag_found, padding = self._loadV2Tag(fileobj) if not tag_found and version[0] & 1: tag_found, padding = self._loadV1Tag(fileobj) if tag_found: self.extended_header = None if tag_found and self.isV2(): self.file_info.tag_size = (TagHeader.SIZE + self.header.tag_size) if tag_found: self.file_info.tag_padding_size = padding finally: if close_file: fileobj.close() return tag_found def _loadV2Tag(self, fp): """Returns (tag_found, padding_len)""" fp.seek(0) # Look for a tag and if found load it. if not self.header.parse(fp): return False, 0 # Read the extended header if present. if self.header.extended: self.extended_header.parse(fp, self.header.version) # Header is definitely there so at least one frame *must* follow. padding = self.frame_set.parse(fp, self.header, self.extended_header) log.debug("Tag contains %d bytes of padding." % padding) return True, padding def _loadV1Tag(self, fp): v1_enc = "latin1" # Seek to the end of the file where all v1x tags are written. # v1.x tags are 128 bytes min and max fp.seek(0, 2) if fp.tell() < 128: return False, 0 fp.seek(-128, 2) tag_data = fp.read(128) if tag_data[0:3] != b"TAG": return False, 0 log.debug("Located ID3 v1 tag") # v1.0 is implied until a v1.1 feature is recognized. self.version = ID3_V1_0 title = tag_data[3:33].strip(ID3_V1_STRIP_CHARS) log.debug("Title: %s" % title) if title: self.title = str(title, v1_enc) artist = tag_data[33:63].strip(ID3_V1_STRIP_CHARS) log.debug("Artist: %s" % artist) if artist: self.artist = str(artist, v1_enc) album = tag_data[63:93].strip(ID3_V1_STRIP_CHARS) log.debug("Album: %s" % album) if album: self.album = str(album, v1_enc) year = tag_data[93:97].strip(ID3_V1_STRIP_CHARS) log.debug("Year: %s" % year) try: if year and int(year): # Values here typically mean the year of release self.release_date = int(year) except ValueError: # Bogus year strings. log.warn("ID3v1.x tag contains invalid year: %s" % year) pass # Can't use ID3_V1_STRIP_CHARS here, since the final byte is numeric comment = tag_data[97:127].rstrip(b"\x00") # Track numbers stuffed in the comment field is what makes v1.1 if comment: if (len(comment) >= 2 and # Python the slices (the chars), so this is really # comment[2] and comment[-1] comment[-2:-1] == b"\x00"): log.debug("Track Num found, setting version to v1.1") self.version = ID3_V1_1 track = comment[-1] self.track_num = (track, None) log.debug("Track: " + str(track)) comment = comment[:-2].strip(ID3_V1_STRIP_CHARS) # There may only have been a track # if comment: log.debug(f"Comment: {comment}") self.comments.set(str(comment, v1_enc), ID3_V1_COMMENT_DESC) genre = ord(tag_data[127:128]) log.debug(f"Genre ID: {genre}") try: self.genre = genre except ValueError as ex: log.warning(ex) self.genre = None return True, 0 @property def version(self): return self.header.version @version.setter def version(self, v): # Tag version changes required possible frame conversion std, non = self._checkForConversions(v) converted = [] if non: converted = self._convertFrames(std, non, v) if converted: self.frame_set.clear() for frame in (std + converted): self.frame_set[frame.id] = frame self.header.version = v def isV1(self): """Test ID3 major version for v1.x""" return self.header.major_version == 1 def isV2(self): """Test ID3 major version for v2.x""" return self.header.major_version == 2 @requireUnicode(2) def setTextFrame(self, fid: bytes, txt: str): fid = b(fid, codecs.ascii_encode) if not fid.startswith(b"T") or fid.startswith(b"TX"): raise ValueError("Invalid frame-id for text frame") if not txt and self.frame_set[fid]: del self.frame_set[fid] elif txt: self.frame_set.setTextFrame(fid, txt) # FIXME: is returning data not a Frame. def getTextFrame(self, fid: bytes): fid = b(fid, codecs.ascii_encode) if not fid.startswith(b"T") or fid.startswith(b"TX"): raise ValueError("Invalid frame-id for text frame") f = self.frame_set[fid] return f[0].text if f else None @requireUnicode(1) def _setArtist(self, val): self.setTextFrame(frames.ARTIST_FID, val) def _getArtist(self): return self.getTextFrame(frames.ARTIST_FID) @requireUnicode(1) def _setAlbumArtist(self, val): self.setTextFrame(frames.ALBUM_ARTIST_FID, val) def _getAlbumArtist(self): return self.getTextFrame(frames.ALBUM_ARTIST_FID) @requireUnicode(1) def _setComposer(self, val): self.setTextFrame(frames.COMPOSER_FID, val) def _getComposer(self): return self.getTextFrame(frames.COMPOSER_FID) @property def composer(self): return self._getComposer() @composer.setter def composer(self, v): self._setComposer(v) @requireUnicode(1) def _setAlbum(self, val): self.setTextFrame(frames.ALBUM_FID, val) def _getAlbum(self): return self.getTextFrame(frames.ALBUM_FID) @requireUnicode(1) def _setTitle(self, val): self.setTextFrame(frames.TITLE_FID, val) def _getTitle(self): return self.getTextFrame(frames.TITLE_FID) def _setTrackNum(self, val): self._setNum(frames.TRACKNUM_FID, val) def _getTrackNum(self) -> core.CountAndTotalTuple: return self._splitNum(frames.TRACKNUM_FID) def _setDiscNum(self, val): self._setNum(frames.DISCNUM_FID, val) def _getDiscNum(self) -> core.CountAndTotalTuple: return self._splitNum(frames.DISCNUM_FID) def _splitNum(self, fid) -> core.CountAndTotalTuple: f = self.frame_set[fid] first, second = None, None if f and f[0].text: n = f[0].text.split('/') try: first = int(n[0]) second = int(n[1]) if len(n) == 2 else None except ValueError as ex: log.warning(str(ex)) return core.CountAndTotalTuple(first, second) def _setNum(self, fid, val): if type(val) is str: val = int(val) if type(val) is tuple: if len(val) != 2: raise ValueError("A 2-tuple of int values is required.") else: tn, tt = tuple([int(v) if v is not None else None for v in val]) elif type(val) is int: tn, tt = val, None elif val is None: tn, tt = None, None else: raise TypeError("Invalid value, should int 2-tuple, int, or None: " f"{val} ({val.__class__.__name__})") n = (tn, tt) if n[0] is None and n[1] is None: if self.frame_set[fid]: del self.frame_set[fid] return total_str = "" if n[1] is not None: if 0 <= n[1] <= 9: total_str = "0" + str(n[1]) else: total_str = str(n[1]) t = n[0] if n[0] else 0 track_str = str(t) # Pad with zeros according to how large the total count is. if len(track_str) == 1: track_str = "0" + track_str if len(track_str) < len(total_str): track_str = ("0" * (len(total_str) - len(track_str))) + track_str final_str = "" if track_str and total_str: final_str = "%s/%s" % (track_str, total_str) elif track_str and not total_str: final_str = track_str self.frame_set.setTextFrame(fid, str(final_str)) @property def comments(self): return self._comments def _getBpm(self): from decimal import Decimal, ROUND_HALF_UP, InvalidOperation bpm = None if frames.BPM_FID in self.frame_set: bpm_str = self.frame_set[frames.BPM_FID][0].text or "0" try: # Round floats since the spec says this is an integer. Python3 # changed how 'round' works, hence the using of decimal bpm = int(Decimal(bpm_str).quantize(1, ROUND_HALF_UP)) except (InvalidOperation, ValueError) as ex: log.warning(ex) return bpm def _setBpm(self, bpm): assert(bpm >= 0) self.setTextFrame(frames.BPM_FID, str(bpm)) bpm = property(_getBpm, _setBpm) @property def play_count(self): if frames.PLAYCOUNT_FID in self.frame_set: pc = self.frame_set[frames.PLAYCOUNT_FID][0] return pc.count else: return None @play_count.setter def play_count(self, count): if count is None: del self.frame_set[frames.PLAYCOUNT_FID] return if count < 0: raise ValueError("Invalid play count value: %d" % count) if self.frame_set[frames.PLAYCOUNT_FID]: pc = self.frame_set[frames.PLAYCOUNT_FID][0] pc.count = count else: self.frame_set[frames.PLAYCOUNT_FID] = \ frames.PlayCountFrame(count=count) def _getPublisher(self): if frames.PUBLISHER_FID in self.frame_set: pub = self.frame_set[frames.PUBLISHER_FID] return pub[0].text else: return None @requireUnicode(1) def _setPublisher(self, p): self.setTextFrame(frames.PUBLISHER_FID, p) publisher = property(_getPublisher, _setPublisher) @property def cd_id(self): if frames.CDID_FID in self.frame_set: return self.frame_set[frames.CDID_FID][0].toc else: return None @cd_id.setter def cd_id(self, toc): if len(toc) > 804: raise ValueError("CD identifier table of contents can be no " "greater than 804 bytes") if self.frame_set[frames.CDID_FID]: cdid = self.frame_set[frames.CDID_FID][0] cdid.toc = bytes(toc) else: self.frame_set[frames.CDID_FID] = \ frames.MusicCDIdFrame(toc=toc) @property def unknown_frame_ids(self) -> set: return self.frame_set.unknown_frame_ids @property def images(self): return self._images def _getEncodingDate(self): return self._getDate(b"TDEN") def _setEncodingDate(self, date): self._setDate(b"TDEN", date) encoding_date = property(_getEncodingDate, _setEncodingDate) @property def best_release_date(self): """This method tries its best to return a date of some sort, amongst all the possible date frames. The order of preference for a release date is 1) date of original release 2) date of this versions release 3) the recording date. Or None is returned.""" import warnings warnings.warn("Use Tag.getBestDate() instead", DeprecationWarning, stacklevel=2) return (self.original_release_date or self.release_date or self.recording_date) def getBestDate(self, prefer_recording_date=False): """This method returns a date of some sort, amongst all the possible date frames. The order of preference is: 1) date of original release 2) date of this versions release 3) the recording date. Unless ``prefer_recording_date`` is ``True`` in which case the order is 3, 1, 2. ``None`` will be returned if no dates are available.""" return datePicker(self, prefer_recording_date) def _getReleaseDate(self): if self.version == ID3_V2_3: # v2.3 does NOT have a release date, only TORY, so that is what is returned return self._getV23OriginalReleaseDate() else: return self._getDate(b"TDRL") def _setReleaseDate(self, date): if self.version == ID3_V2_3: # v2.3 does NOT have a release date, only TORY, so that is what is set self._setOriginalReleaseDate(date) else: self._setDate(b"TDRL", date) release_date = property(_getReleaseDate, _setReleaseDate) release_date.__doc__ = textwrap.dedent(""" The date the audio was released. This is NOT the original date the work was released, instead it is more like the pressing or version of the release. Original release date is usually what is intended but many programs use this frame and/or don't distinguish between the two. NOTE: ID3v2.3 only has original release date, so setting release_date is the same as original_release_value; they both set TORY. """) def _getOrigReleaseDate(self): if self.version == ID3_V2_3: return self._getV23OriginalReleaseDate() else: return self._getDate(b"TDOR") or self._getV23OriginalReleaseDate() _getOriginalReleaseDate = _getOrigReleaseDate def _setOrigReleaseDate(self, date): if self.version == ID3_V2_3: self._setDate(b"TORY", date) else: self._setDate(b"TDOR", date) _setOriginalReleaseDate = _setOrigReleaseDate original_release_date = property(_getOrigReleaseDate, _setOrigReleaseDate) original_release_date.__doc__ = textwrap.dedent(""" The date the work was originally released. NOTE: ID3v2.3 only stores year. If the Date object is more precise it is store in `XDOR`, and XDOR is preferred when acessing. The year-only date is stored in the standard `TORY` frame as well. """) def _getRecordingDate(self): if self.version == ID3_V2_3: return self._getV23RecordingDate() else: return self._getDate(b"TDRC") def _setRecordingDate(self, date): if date in (None, ""): for fid in (b"TDRC", b"TYER", b"TDAT", b"TIME"): self._setDate(fid, None) elif self.version == ID3_V2_4: self._setDate(b"TDRC", date) else: if not isinstance(date, core.Date): date = core.Date.parse(date) self._setDate(b"TYER", str(date.year)) if None not in (date.month, date.day): date_str = "%s%s" % (str(date.day).rjust(2, "0"), str(date.month).rjust(2, "0")) self._setDate(b"TDAT", date_str) if None not in (date.hour, date.minute): date_str = "%s%s" % (str(date.hour).rjust(2, "0"), str(date.minute).rjust(2, "0")) self._setDate(b"TIME", date_str) recording_date = property(_getRecordingDate, _setRecordingDate) """The date of the recording. Many applications use this for release date regardless of the fact that this value is rarely known, and release dates are more correct.""" def _getV23RecordingDate(self): # v2.3 TYER (yyyy), TDAT (DDMM), TIME (HHmm) date = None try: date_str = b"" if b"TYER" in self.frame_set: date_str = self.frame_set[b"TYER"][0].text.encode("latin1") date = core.Date.parse(date_str) if b"TDAT" in self.frame_set: text = self.frame_set[b"TDAT"][0].text.encode("latin1") date_str += b"-%s-%s" % (text[2:], text[:2]) date = core.Date.parse(date_str) if b"TIME" in self.frame_set: text = self.frame_set[b"TIME"][0].text.encode("latin1") date_str += b"T%s:%s" % (text[:2], text[2:]) date = core.Date.parse(date_str) except ValueError as ex: log.warning("Invalid v2.3 TYER, TDAT, or TIME frame: %s" % ex) return date def _getV23OriginalReleaseDate(self): date, date_str = None, None try: # XDOR is preferred since it can gave a full date, whereas TORY is year only. for fid in (b"XDOR", b"TORY"): if fid in self.frame_set: date_str = self.frame_set[fid][0].text.encode("latin1") break if date_str: date = core.Date.parse(date_str) except ValueError as ex: log.warning(f"Invalid v2.3 TORY/XDOR frame: {ex}") return date def _getTaggingDate(self): return self._getDate(b"TDTG") def _setTaggingDate(self, date): self._setDate(b"TDTG", date) tagging_date = property(_getTaggingDate, _setTaggingDate) def _setDate(self, fid, date): def removeFrame(frame_id): try: del self.frame_set[frame_id] except KeyError: pass def setFrame(frame_id, date_val): if frame_id in self.frame_set: self.frame_set[frame_id][0].date = date_val else: self.frame_set[frame_id] = frames.DateFrame(frame_id, str(date_val)) assert fid in frames.DATE_FIDS or fid in frames.DEPRECATED_DATE_FIDS if fid == b"XDOR": raise ValueError("Set TORY with a full date (i.e. more than year)") clean_fids = [fid] if fid == b"TORY": clean_fids.append(b"XDOR") if date in (None, ""): for cid in clean_fids: removeFrame(cid) return # Special casing the conversion to DATE objects cuz TDAT and TIME won't if fid not in (b"TDAT", b"TIME"): # Convert to ISO format which is what FrameSet wants. date_type = type(date) if date_type is int: # The integer year date = core.Date(date) elif date_type is str: date = core.Date.parse(date) elif not isinstance(date, core.Date): raise TypeError(f"Invalid type: {date_type}") if fid == b"TORY": setFrame(fid, date.year) if date.month: setFrame(b"XDOR", date) else: removeFrame(b"XDOR") else: setFrame(fid, date) def _getDate(self, fid): if fid in (b"TORY", b"XDOR"): return self._getV23OriginalReleaseDate() if fid in self.frame_set: if fid in (b"TYER", b"TDAT", b"TIME"): if fid == b"TYER": # Contain years only, date conversion can happen return core.Date(int(self.frame_set[fid][0].text)) else: return self.frame_set[fid][0].text else: return self.frame_set[fid][0].date else: return None @property def lyrics(self): return self._lyrics @property def disc_num(self): return self._getDiscNum() @disc_num.setter def disc_num(self, val): self._setDiscNum(val) @property def objects(self): return self._objects @property def privates(self): return self._privates @property def popularities(self): return self._popularities def _getGenre(self, id3_std=True): f = self.frame_set[frames.GENRE_FID] if f and f[0].text: try: return Genre.parse(f[0].text, id3_std=id3_std) except ValueError: # pragma: nocover return None else: return None def _setGenre(self, g, id3_std=True): """Set the genre. Four types are accepted for the ``g`` argument. A Genre object, an acceptable (see Genre.parse) genre string, or an integer genre ID all will set the value. A value of None will remove the genre.""" if g in ("", None): if self.frame_set[frames.GENRE_FID]: del self.frame_set[frames.GENRE_FID] return if isinstance(g, str): g = Genre.parse(g, id3_std=id3_std) elif isinstance(g, int): g = Genre(id=g) elif not isinstance(g, Genre): raise TypeError(f"Invalid genre data type: {type(g)}") assert g self.frame_set.setTextFrame(frames.GENRE_FID, f"{g.name if g.name else g.id}") # genre property genre = property(_getGenre, _setGenre) def _getNonStdGenre(self): return self._getGenre(id3_std=False) def _setNonStdGenre(self, val): self._setGenre(val, id3_std=False) # non-standard genre (unparsed, unmapped) property non_std_genre = property(_getNonStdGenre, _setNonStdGenre) @property def user_text_frames(self): return self._user_texts def _setUrlFrame(self, fid, url): if fid not in frames.URL_FIDS: raise ValueError("Invalid URL frame-id") if self.frame_set[fid]: if not url: del self.frame_set[fid] else: self.frame_set[fid][0].url = url else: self.frame_set[fid] = frames.UrlFrame(fid, url) def _getUrlFrame(self, fid): if fid not in frames.URL_FIDS: raise ValueError("Invalid URL frame-id") f = self.frame_set[fid] return f[0].url if f else None @property def commercial_url(self): return self._getUrlFrame(frames.URL_COMMERCIAL_FID) @commercial_url.setter def commercial_url(self, url): self._setUrlFrame(frames.URL_COMMERCIAL_FID, url) @property def copyright_url(self): return self._getUrlFrame(frames.URL_COPYRIGHT_FID) @copyright_url.setter def copyright_url(self, url): self._setUrlFrame(frames.URL_COPYRIGHT_FID, url) @property def audio_file_url(self): return self._getUrlFrame(frames.URL_AUDIOFILE_FID) @audio_file_url.setter def audio_file_url(self, url): self._setUrlFrame(frames.URL_AUDIOFILE_FID, url) @property def audio_source_url(self): return self._getUrlFrame(frames.URL_AUDIOSRC_FID) @audio_source_url.setter def audio_source_url(self, url): self._setUrlFrame(frames.URL_AUDIOSRC_FID, url) @property def artist_url(self): return self._getUrlFrame(frames.URL_ARTIST_FID) @artist_url.setter def artist_url(self, url): self._setUrlFrame(frames.URL_ARTIST_FID, url) @property def internet_radio_url(self): return self._getUrlFrame(frames.URL_INET_RADIO_FID) @internet_radio_url.setter def internet_radio_url(self, url): self._setUrlFrame(frames.URL_INET_RADIO_FID, url) @property def payment_url(self): return self._getUrlFrame(frames.URL_PAYMENT_FID) @payment_url.setter def payment_url(self, url): self._setUrlFrame(frames.URL_PAYMENT_FID, url) @property def publisher_url(self): return self._getUrlFrame(frames.URL_PUBLISHER_FID) @publisher_url.setter def publisher_url(self, url): self._setUrlFrame(frames.URL_PUBLISHER_FID, url) @property def user_url_frames(self): return self._user_urls @property def unique_file_ids(self): return self._unique_file_ids @property def terms_of_use(self): if self.frame_set[frames.TOS_FID]: return self.frame_set[frames.TOS_FID][0].text @terms_of_use.setter def terms_of_use(self, tos): """Set the terms of use text. To specify a language (other than DEFAULT_LANG) code with the text pass a tuple: (text, lang) Language codes are 3 *bytes* of ascii data. """ if isinstance(tos, tuple): tos, lang = tos else: lang = DEFAULT_LANG if self.frame_set[frames.TOS_FID]: self.frame_set[frames.TOS_FID][0].text = tos self.frame_set[frames.TOS_FID][0].lang = lang else: self.frame_set[frames.TOS_FID] = frames.TermsOfUseFrame(text=tos, lang=lang) def _setCopyright(self, copyrt): self.setTextFrame(frames.COPYRIGHT_FID, copyrt) def _getCopyright(self): if frames.COPYRIGHT_FID in self.frame_set: return self.frame_set[frames.COPYRIGHT_FID][0].text copyright = property(_getCopyright, _setCopyright) def _setEncodedBy(self, enc): self.setTextFrame(frames.ENCODED_BY_FID, enc) def _getEncodedBy(self): if frames.ENCODED_BY_FID in self.frame_set: return self.frame_set[frames.ENCODED_BY_FID][0].text encoded_by = property(_getEncodedBy, _setEncodedBy) def _raiseIfReadonly(self): if self.read_only: raise RuntimeError("Tag is set read only.") def save(self, filename=None, version=None, encoding=None, backup=False, preserve_file_time=False, max_padding=None): """Save the tag. If ``filename`` is not give the value from the ``file_info`` member is used, or a ``TagException`` is raised. The ``version`` argument can be used to select an ID3 version other than the version read. ``Select text encoding with ``encoding`` or use the existing (or default) encoding. If ``backup`` is True the original file is preserved; likewise if ``preserve_file_time`` is True the file´s modification/access times are not updated. """ self._raiseIfReadonly() if not (filename or self.file_info): raise TagException("No file") elif filename: self.file_info = FileInfo(filename) version = version if version else self.version if version == ID3_V2_2: raise NotImplementedError("Unable to write ID3 v2.2") self.version = version if backup and os.path.isfile(self.file_info.name): backup_name = "%s.%s" % (self.file_info.name, "orig") i = 1 while os.path.isfile(backup_name): backup_name = "%s.%s.%d" % (self.file_info.name, "orig", i) i += 1 shutil.copyfile(self.file_info.name, backup_name) if version[0] == 1: self._saveV1Tag(version) elif version[0] == 2: self._saveV2Tag(version, encoding, max_padding) else: assert not "Version bug: %s" % str(version) if preserve_file_time and None not in (self.file_info.atime, self.file_info.mtime): self.file_info.touch((self.file_info.atime, self.file_info.mtime)) else: self.file_info.initStatTimes() def _saveV1Tag(self, version): self._raiseIfReadonly() assert(version[0] == 1) def pack(s, n): assert(type(s) is bytes) if len(s) > n: log.warning(f"ID3 v1.x text value truncated to length {n}") return s.ljust(n, b'\x00')[:n] def encode(s): return s.encode("latin_1", "replace") # Build tag buffer. tag = b"TAG" tag += pack(encode(self.title) if self.title else b"", ID3_V1_MAX_TEXTLEN) tag += pack(encode(self.artist) if self.artist else b"", ID3_V1_MAX_TEXTLEN) tag += pack(encode(self.album) if self.album else b"", ID3_V1_MAX_TEXTLEN) release_date = self.getBestDate() year = str(release_date.year).encode("ascii") if release_date else b"" tag += pack(year, 4) cmt = "" for c in self.comments: if c.description == ID3_V1_COMMENT_DESC: cmt = c.text # We prefer this one over "" break elif c.description == "": cmt = c.text # Keep searching in case we find the description eyeD3 uses. cmt = pack(encode(cmt), ID3_V1_MAX_TEXTLEN) if version != ID3_V1_0: track = self.track_num[0] if track is not None: cmt = cmt[0:28] + b"\x00" + bytes([int(track) & 0xff]) tag += cmt if not self.genre or self.genre.id is None: genre = 12 # Other else: genre = self.genre.id tag += bytes([genre & 0xff]) assert len(tag) == 128 mode = "rb+" if os.path.isfile(self.file_info.name) else "w+b" with open(self.file_info.name, mode) as tag_file: # Write the tag over top an original or append it. try: tag_file.seek(-128, 2) if tag_file.read(3) == b"TAG": tag_file.seek(-128, 2) else: tag_file.seek(0, 2) except IOError: # File is smaller than 128 bytes. tag_file.seek(0, 2) tag_file.write(tag) tag_file.flush() def _checkForConversions(self, target_version): """Check the current frame set against `target_version` for frames requiring conversion. :param: The version the frames need to map to. :returns: A 2-tuple where the first element is a list of frames that are accepted for `target_version`, and the second a list of frames requiring conversion. """ std_frames = [] non_std_frames = [] for f in self.frame_set.getAllFrames(): try: _, fversion, _ = frames.ID3_FRAMES[f.id] if fversion in (target_version, ID3_V2): std_frames.append(f) else: non_std_frames.append(f) except KeyError: # Not a standard frame (ID3_FRAMES) try: _, fversion, _ = frames.NONSTANDARD_ID3_FRAMES[f.id] # but is it one we can handle. if fversion in (target_version, ID3_V2): std_frames.append(f) else: non_std_frames.append(f) except KeyError: # Don't know anything about this pass it on for the error # check there. non_std_frames.append(f) return std_frames, non_std_frames def _render(self, version, curr_tag_size, max_padding_size): converted_frames = [] std_frames, non_std_frames = self._checkForConversions(version) if non_std_frames: converted_frames = self._convertFrames(std_frames, non_std_frames, version) # Render all frames first so the data size is known for the tag header. frame_data = b"" for f in std_frames + converted_frames: frame_header = frames.FrameHeader(f.id, version) if f.header: frame_header.copyFlags(f.header) f.header = frame_header log.debug(f"Rendering frame: {frame_header.id}") try: raw_frame = f.render() except Exception as ex: if not f.strict_rendering: log.warning(f"Ignoring failed render {f.__class__}: {ex}") continue else: raise log.debug(f"Rendered {len(raw_frame)} bytes") frame_data += raw_frame log.debug("Rendered %d total frame bytes" % len(frame_data)) # eyeD3 never writes unsync'd data self.header.unsync = False pending_size = TagHeader.SIZE + len(frame_data) if self.header.extended: # Using dummy data and padding, the actual size of this header # will be the same regardless, it's more about the flag bits tmp_ext_header_data = self.extended_header.render(version, b"\x00", 0) pending_size += len(tmp_ext_header_data) if pending_size > curr_tag_size: # current tag (minus padding) larger than the current (plus padding) padding_size = DEFAULT_PADDING rewrite_required = True else: padding_size = curr_tag_size - pending_size if max_padding_size is not None and padding_size > max_padding_size: padding_size = min(DEFAULT_PADDING, max_padding_size) rewrite_required = True else: rewrite_required = False assert padding_size >= 0 log.debug(f"Using {padding_size} bytes of padding") # Extended header ext_header_data = b"" if self.header.extended: log.debug("Rendering extended header") ext_header_data += self.extended_header.render(self.header.version, frame_data, padding_size) # Render the tag header. total_size = pending_size + padding_size data_size = total_size - TagHeader.SIZE log.debug( f"Rendering {versionToString(version)} tag header with size {data_size}" ) header_data = self.header.render(data_size) # Assemble the entire tag. tag_data = (header_data + ext_header_data + frame_data) assert len(tag_data) == (total_size - padding_size) return rewrite_required, tag_data, b"\x00" * padding_size def _saveV2Tag(self, version, encoding, max_padding): self._raiseIfReadonly() assert(version[0] == 2 and version[1] != 2) log.debug("Rendering tag version: %s" % versionToString(version)) file_exists = os.path.exists(self.file_info.name) if encoding: # Any invalid encoding is going to get coerced to a valid value # when the frame is rendered. for f in self.frame_set.getAllFrames(): f.encoding = frames.stringToEncoding(encoding) curr_tag_size = 0 if file_exists: # We may be converting from 1.x to 2.x so we need to find any # current v2.x tag otherwise we're gonna hork the file. # This also resets all offsets, state, etc. and makes me feel safe. tmp_tag = Tag() if tmp_tag.parse(self.file_info.name, ID3_V2): log.debug("Found current v2.x tag:") curr_tag_size = tmp_tag.file_info.tag_size log.debug("Current tag size: %d" % curr_tag_size) rewrite_required, tag_data, padding = self._render(version, curr_tag_size, max_padding) log.debug("Writing %d bytes of tag data and %d bytes of " "padding" % (len(tag_data), len(padding))) if rewrite_required: # Open tmp file with tempfile.NamedTemporaryFile("wb", delete=False) \ as tmp_file: tmp_file.write(tag_data + padding) # Copy audio data in chunks with open(self.file_info.name, "rb") as tag_file: if curr_tag_size != 0: seek_point = curr_tag_size else: seek_point = 0 log.debug("Seeking to beginning of audio data, " "byte %d (%x)" % (seek_point, seek_point)) tag_file.seek(seek_point) chunkCopy(tag_file, tmp_file) tmp_file.flush() # Move tmp to orig. shutil.copyfile(tmp_file.name, self.file_info.name) os.unlink(tmp_file.name) else: with open(self.file_info.name, "r+b") as tag_file: tag_file.write(tag_data + padding) else: _, tag_data, padding = self._render(version, 0, None) with open(self.file_info.name, "wb") as tag_file: tag_file.write(tag_data + padding) log.debug("Tag write complete. Updating FileInfo state.") self.file_info.tag_size = len(tag_data) + len(padding) def _convertFrames_v1(self, std_frames, convert_list, version) -> list: assert version[0] == 1 converted_frames = [] track_num_frame = None for frame in std_frames: if frame.id == frames.TRACKNUM_FID: # Find track_num so it can be enforced for 1.1 track_num_frame = frame elif frame.id == frames.COMMENT_FID and frame.description == ID3_V1_COMMENT_DESC: # Comments truncated to make room for v1.1 track if version == ID3_V1_1: if len(frame.text) > ID3_V1_MAX_TEXTLEN - 2: trunc_text = frame.text[:ID3_V1_MAX_TEXTLEN - 2] log.info(f"Truncating ID3 v1 comment due to tag conversion: {frame.text}") frame.text = trunc_text # v1.1 must have a track num if track_num_frame is None and version == ID3_V1_1: log.info("ID3 v1.0->v1.1 conversion forces track number, defaulting to 1") std_frames.append(frames.TextFrame(frames.TRACKNUM_FID, "1")) # v1.0 must not elif track_num_frame is not None and version == ID3_V1_0: log.info("ID3 v1.1->v1.0 conversion forces deleting track number") std_frames.remove(track_num_frame) for frame in list(convert_list): # Let date frames thru, the right thing will happen on save if isinstance(frame, frames.DateFrame): converted_frames.append(frame) convert_list.remove(frame) return converted_frames def _convertFrames(self, std_frames, convert_list, version) -> list: """Maps frame incompatibilities between ID3 tag versions. The items in ``std_frames`` need no conversion, but the list/frames may be edited if necessary (e.g. a converted frame replaces a frame in the list). The items in ``convert_list`` are the frames to convert and return. The ``version`` is the target ID3 version.""" from . import versionToString from .frames import DATE_FIDS, DEPRECATED_DATE_FIDS, DateFrame, TextFrame if version[0] == 1: return self._convertFrames_v1(std_frames, convert_list, version) # Only ID3 v2.x onward assert version[0] != 1 converted_frames = [] flist = list(convert_list) # Date frame conversions. date_frames = {} for f in flist: if version == ID3_V2_4: if f.id in DEPRECATED_DATE_FIDS: date_frames[f.id] = f else: if f.id in DATE_FIDS: date_frames[f.id] = f if date_frames: def fidHandled(_fid): # A duplicate text frame (illegal ID3 but oft seen) may exist. The date_frames dict # will have one, but the flist has multiple, hence the loop. for _frame in list(flist): if _frame.id == _fid: flist.remove(_frame) del date_frames[_fid] if version == ID3_V2_4: if b"TORY" in date_frames or b"XDOR" in date_frames: # XDOR -> TDOR (full date) # TORY -> TDOR (year only) date = self._getV23OriginalReleaseDate() if date: converted_frames.append(DateFrame(b"TDOR", date)) for fid in (b"TORY", b"XDOR"): if fid in flist: fidHandled(fid) # TYER, TDAT, TIME -> TDRC if (b"TYER" in date_frames or b"TDAT" in date_frames or b"TIME" in date_frames): date = self._getV23RecordingDate() if date: converted_frames.append(DateFrame(b"TDRC", date)) for fid in [b"TYER", b"TDAT", b"TIME"]: if fid in date_frames: fidHandled(fid) elif version == ID3_V2_3: if b"TDOR" in date_frames: date = date_frames[b"TDOR"].date if date: # TORY is year only converted_frames.append(DateFrame(b"TORY", str(date.year))) if date and date.month: converted_frames.append(DateFrame(b"XDOR", str(date))) fidHandled(b"TDOR") if b"TDRC" in date_frames: date = date_frames[b"TDRC"].date if date: converted_frames.append(DateFrame(b"TYER", str(date.year))) if None not in (date.month, date.day): date_str = "%s%s" %\ (str(date.day).rjust(2, "0"), str(date.month).rjust(2, "0")) converted_frames.append(TextFrame(b"TDAT", date_str)) if None not in (date.hour, date.minute): date_str = "%s%s" %\ (str(date.hour).rjust(2, "0"), str(date.minute).rjust(2, "0")) converted_frames.append(TextFrame(b"TIME", date_str)) fidHandled(b"TDRC") if b"TDRL" in date_frames: # TDRL -> Nothing log.warning("TDRL value dropped.") fidHandled(b"TDRL") # All other date frames have no conversion for fid in date_frames: log.warning(f"{str(fid, 'ascii')} frame being dropped due to conversion to " f"{versionToString(version)}") flist.remove(date_frames[fid]) # Convert sort order frames 2.3 (XSO*) <-> 2.4 (TSO*) prefix = b"X" if version == ID3_V2_4 else b"T" fids = [prefix + suffix for suffix in [b"SOA", b"SOP", b"SOT"]] soframes = [f for f in flist if f.id in fids] for frame in soframes: frame.id = (b"X" if prefix == b"T" else b"T") + frame.id[1:] flist.remove(frame) converted_frames.append(frame) # TSIZ (v2.3) are completely deprecated, remove them if version == ID3_V2_4: flist = [f for f in flist if f.id != b"TSIZ"] # TSST (v2.4) --> TIT3 (2.3) if version == ID3_V2_3 and b"TSST" in [f.id for f in flist]: tsst_frame = [f for f in flist if f.id == b"TSST"][0] flist.remove(tsst_frame) tsst_frame = frames.UserTextFrame( description="Subtitle (converted)", text=tsst_frame.text) converted_frames.append(tsst_frame) # RVAD (v2.3) --> RVA2* (2.4) if version == ID3_V2_4 and b"RVAD" in [f.id for f in flist]: rvad = [f for f in flist if f.id == b"RVAD"][0] for rva2 in rvad.toV24(): converted_frames.append(rva2) flist.remove(rvad) # RVA2* (v2.4) --> RVAD (2.3) elif version == ID3_V2_3 and b"RVA2" in [f.id for f in flist]: adj = frames.RelVolAdjFrameV23.VolumeAdjustments() for rva2 in [f for f in flist if f.id == b"RVA2"]: adj.setChannelAdj(rva2.channel_type, rva2.adjustment * 512) adj.setChannelPeak(rva2.channel_type, rva2.peak) flist.remove(rva2) rvad = frames.RelVolAdjFrameV23() rvad.adjustments = adj converted_frames.append(rvad) # Raise an error for frames that could not be converted. if len(flist) != 0: unconverted = ", ".join([f.id.decode("ascii") for f in flist]) if version[0] != 1: raise TagException("Unable to convert the following frames to " f"version {versionToString(version)}: {unconverted}") # Some frames in converted_frames may replace/edit frames in std_frames. for cframe in converted_frames: for sframe in std_frames: if cframe.id == sframe.id: std_frames.remove(sframe) return converted_frames @staticmethod def remove(filename, version=ID3_ANY_VERSION, preserve_file_time=False): tag = None retval = False if version[0] & ID3_V1[0]: # ID3 v1.x tag = Tag() with open(filename, "r+b") as tag_file: found = tag.parse(tag_file, ID3_V1) if found: tag_file.seek(-128, 2) log.debug("Removing ID3 v1.x Tag") tag_file.truncate() retval |= True if version[0] & ID3_V2[0]: tag = Tag() with open(filename, "rb") as tag_file: found = tag.parse(tag_file, ID3_V2) if found: log.debug("Removing ID3 %s tag" % versionToString(tag.version)) tag_file.seek(tag.file_info.tag_size) # Open tmp file with tempfile.NamedTemporaryFile("wb", delete=False) \ as tmp_file: chunkCopy(tag_file, tmp_file) # Move tmp to orig shutil.copyfile(tmp_file.name, filename) os.unlink(tmp_file.name) retval |= True if preserve_file_time and retval and None not in (tag.file_info.atime, tag.file_info.mtime): tag.file_info.touch((tag.file_info.atime, tag.file_info.mtime)) return retval @property def chapters(self): return self._chapters @property def table_of_contents(self): return self._tocs @property def album_type(self): if TXXX_ALBUM_TYPE in self.user_text_frames: return self.user_text_frames.get(TXXX_ALBUM_TYPE).text else: return None @album_type.setter def album_type(self, t): if not t: self.user_text_frames.remove(TXXX_ALBUM_TYPE) elif t in ALBUM_TYPE_IDS: self.user_text_frames.set(t, TXXX_ALBUM_TYPE) else: raise ValueError("Invalid album_type: %s" % t) @property def artist_origin(self): """Returns None or a `ArtistOrigin` dataclass: (city, state, country) Any may be ``None``. """ if TXXX_ARTIST_ORIGIN not in self.user_text_frames: return None origin = self.user_text_frames.get(TXXX_ARTIST_ORIGIN).text vals = origin.split('\t') vals.extend([None] * (3 - len(vals))) vals = [None if not v else v for v in vals] return ArtistOrigin(*vals) @artist_origin.setter def artist_origin(self, origin: ArtistOrigin): if origin is None or origin == (None, None, None): self.user_text_frames.remove(TXXX_ARTIST_ORIGIN) else: self.user_text_frames.set(origin.id3Encode(), TXXX_ARTIST_ORIGIN) def frameiter(self, fids=None): """A iterator for tag frames. If ``fids`` is passed it must be a list of frame IDs to filter and return.""" fids = fids or [] fids = [(b(f, codecs.ascii_encode) if isinstance(f, str) else f) for f in fids] for f in self.frame_set.getAllFrames(): if not fids or f.id in fids: yield f def _getOrigArtist(self): return self.getTextFrame(frames.ORIG_ARTIST_FID) def _setOrigArtist(self, name): self.setTextFrame(frames.ORIG_ARTIST_FID, name) @property def original_artist(self): return self._getOrigArtist() @original_artist.setter def original_artist(self, name): self._setOrigArtist(name) class FileInfo: """ This class is for storing information about a parsed file. It contains info such as the filename, original tag size, and amount of padding; all of which can make rewriting faster. """ def __init__(self, file_name, tagsz=0, tpadd=0): from .. import LOCAL_FS_ENCODING if type(file_name) is str: self.name = file_name else: try: self.name = str(file_name, LOCAL_FS_ENCODING) except UnicodeDecodeError: # Work around the local encoding not matching that of a mounted # filesystem log.warning("Mismatched file system encoding for file '%s'" % repr(file_name)) self.name = file_name self.tag_size = tagsz or 0 # This includes the padding byte count. self.tag_padding_size = tpadd or 0 self.atime, self.mtime = None, None self.initStatTimes() def initStatTimes(self): try: s = os.stat(self.name) except OSError: self.atime, self.mtime = None, None else: self.atime, self.mtime = s.st_atime, s.st_mtime def touch(self, times): """times is a 2-tuple of (atime, mtime).""" os.utime(self.name, times) self.initStatTimes() class AccessorBase: def __init__(self, fid, fs, match_func=None): self._fid = fid self._fs = fs self._match_func = match_func def __iter__(self): for f in self._fs[self._fid] or []: yield f def __len__(self): return len(self._fs[self._fid] or []) def __getitem__(self, i): frames = self._fs[self._fid] if not frames: raise IndexError("list index out of range") return frames[i] def get(self, *args, **kwargs): for frame in self._fs[self._fid] or []: if self._match_func(frame, *args, **kwargs): return frame return None def remove(self, *args, **kwargs): """Returns the removed item or ``None`` if not found.""" fid_frames = self._fs[self._fid] or [] for frame in fid_frames: if self._match_func(frame, *args, **kwargs): fid_frames.remove(frame) return frame return None class DltAccessor(AccessorBase): """Access matching tag frames by "description" and/or "lang" values.""" def __init__(self, FrameClass, fid, fs): def match_func(frame, description, lang=DEFAULT_LANG): return (frame.description == description and frame.lang == (lang if isinstance(lang, bytes) else lang.encode("ascii"))) super().__init__(fid, fs, match_func) self.FrameClass = FrameClass @requireUnicode(1, 2) def set(self, text, description="", lang=DEFAULT_LANG): lang = lang or DEFAULT_LANG for f in self._fs[self._fid] or []: if f.description == description and f.lang == lang: # Exists, update text f.text = text return f new_frame = self.FrameClass(description=description, lang=lang, text=text) self._fs[self._fid] = new_frame return new_frame @requireUnicode(1) def remove(self, description, lang=DEFAULT_LANG): return super().remove(description, lang=lang or DEFAULT_LANG) @requireUnicode(1) def get(self, description, lang=DEFAULT_LANG): return super().get(description, lang=lang or DEFAULT_LANG) class CommentsAccessor(DltAccessor): def __init__(self, fs): super().__init__(frames.CommentFrame, frames.COMMENT_FID, fs) class LyricsAccessor(DltAccessor): def __init__(self, fs): super().__init__(frames.LyricsFrame, frames.LYRICS_FID, fs) class ImagesAccessor(AccessorBase): def __init__(self, fs): def match_func(frame, description): return frame.description == description super().__init__(frames.IMAGE_FID, fs, match_func) @requireUnicode("description") def set(self, type_, img_data, mime_type, description="", img_url=None): """Add an image of ``type_`` (a type constant from ImageFrame). The ``img_data`` is either bytes or ``None``. In the latter case ``img_url`` MUST be the URL to the image. In this case ``mime_type`` is ignored and "-->" is used to signal this as a link and not data (per the ID3 spec).""" img_url = b(img_url) if img_url else None if not img_data and not img_url: raise ValueError("img_url MUST not be none when no image data") mime_type = mime_type if img_data else frames.ImageFrame.URL_MIME_TYPE mime_type = b(mime_type) images = self._fs[frames.IMAGE_FID] or [] for img in images: if img.description == description: # update if not img_data: img.image_url = img_url img.image_data = None img.mime_type = frames.ImageFrame.URL_MIME_TYPE else: img.image_url = None img.image_data = img_data img.mime_type = mime_type img.picture_type = type_ return img img_frame = frames.ImageFrame(description=description, image_data=img_data, image_url=img_url, mime_type=mime_type, picture_type=type_) self._fs[frames.IMAGE_FID] = img_frame return img_frame @requireUnicode(1) def remove(self, description): return super().remove(description) @requireUnicode(1) def get(self, description): return super().get(description) class ObjectsAccessor(AccessorBase): def __init__(self, fs): def match_func(frame, description): return frame.description == description super().__init__(frames.OBJECT_FID, fs, match_func) @requireUnicode("description", "filename") def set(self, data, mime_type, description="", filename=""): objects = self._fs[frames.OBJECT_FID] or [] for obj in objects: if obj.description == description: # update obj.object_data = data obj.mime_type = mime_type obj.filename = filename return obj obj_frame = frames.ObjectFrame(description=description, filename=filename, object_data=data, mime_type=mime_type) self._fs[frames.OBJECT_FID] = obj_frame return obj_frame @requireUnicode(1) def remove(self, description): return super().remove(description) @requireUnicode(1) def get(self, description): return super().get(description) class PrivatesAccessor(AccessorBase): def __init__(self, fs): def match_func(frame, owner_id): return frame.owner_id == owner_id super().__init__(frames.PRIVATE_FID, fs, match_func) def set(self, data, owner_id): priv_frames = self._fs[frames.PRIVATE_FID] or [] for f in priv_frames: if f.owner_id == owner_id: # update f.owner_data = data return f priv_frame = frames.PrivateFrame(owner_id=owner_id, owner_data=data) self._fs[frames.PRIVATE_FID] = priv_frame return priv_frame def remove(self, owner_id): return super().remove(owner_id) def get(self, owner_id): return super().get(owner_id) class UserTextsAccessor(AccessorBase): def __init__(self, fs): def match_func(frame, description): return frame.description == description super().__init__(frames.USERTEXT_FID, fs, match_func) @requireUnicode(1, "description") def set(self, text, description=""): flist = self._fs[frames.USERTEXT_FID] or [] for utf in flist: if utf.description == description: # update utf.text = text return utf utf = frames.UserTextFrame(description=description, text=text) self._fs[frames.USERTEXT_FID] = utf return utf @requireUnicode(1) def remove(self, description): return super().remove(description) @requireUnicode(1) def get(self, description): return super().get(description) @requireUnicode(1) def __contains__(self, description): return bool(self.get(description)) class UniqueFileIdAccessor(AccessorBase): def __init__(self, fs): def match_func(frame, owner_id): return frame.owner_id == owner_id super().__init__(frames.UNIQUE_FILE_ID_FID, fs, match_func) def set(self, data, owner_id): data, owner_id = b(data), b(owner_id) if len(data) > 64: raise TagException("UFID data must be 64 bytes or less") flist = self._fs[frames.UNIQUE_FILE_ID_FID] or [] for f in flist: if f.owner_id == owner_id: # update f.uniq_id = data return f uniq_id_frame = frames.UniqueFileIDFrame(owner_id=owner_id, uniq_id=data) self._fs[frames.UNIQUE_FILE_ID_FID] = uniq_id_frame return uniq_id_frame def remove(self, owner_id): owner_id = b(owner_id) return super().remove(owner_id) def get(self, owner_id): owner_id = b(owner_id) return super().get(owner_id) class UserUrlsAccessor(AccessorBase): def __init__(self, fs): def match_func(frame, description): return frame.description == description super().__init__(frames.USERURL_FID, fs, match_func) @requireUnicode("description") def set(self, url, description=""): flist = self._fs[frames.USERURL_FID] or [] for uuf in flist: if uuf.description == description: # update uuf.url = url return uuf uuf = frames.UserUrlFrame(description=description, url=url) self._fs[frames.USERURL_FID] = uuf return uuf @requireUnicode(1) def remove(self, description): return super().remove(description) @requireUnicode(1) def get(self, description): return super().get(description) class PopularitiesAccessor(AccessorBase): def __init__(self, fs): def match_func(frame, email): return frame.email == email super().__init__(frames.POPULARITY_FID, fs, match_func) def set(self, email, rating, play_count): flist = self._fs[frames.POPULARITY_FID] or [] for popm in flist: if popm.email == email: # update popm.rating = rating popm.count = play_count return popm popm = frames.PopularityFrame(email=email, rating=rating, count=play_count) self._fs[frames.POPULARITY_FID] = popm return popm def remove(self, email): return super().remove(email) def get(self, email): return super().get(email) class ChaptersAccessor(AccessorBase): def __init__(self, fs): def match_func(frame, element_id): return frame.element_id == element_id super().__init__(frames.CHAPTER_FID, fs, match_func) def set(self, element_id, times, offsets=(None, None), sub_frames=None): flist = self._fs[frames.CHAPTER_FID] or [] for chap in flist: if chap.element_id == element_id: # update chap.times, chap.offsets = times, offsets if sub_frames: chap.sub_frames = sub_frames return chap chap = frames.ChapterFrame(element_id=element_id, times=times, offsets=offsets, sub_frames=sub_frames) self._fs[frames.CHAPTER_FID] = chap return chap def remove(self, element_id): return super().remove(element_id) def get(self, element_id): return super().get(element_id) def __getitem__(self, elem_id): """Overiding the index based __getitem__ for one indexed with chapter element IDs. These are stored in the tag's table of contents frames.""" for chapter in (self._fs[frames.CHAPTER_FID] or []): if chapter.element_id == elem_id: return chapter raise IndexError("chapter '%s' not found" % elem_id) class TocAccessor(AccessorBase): def __init__(self, fs): def match_func(frame, element_id): return frame.element_id == element_id super().__init__(frames.TOC_FID, fs, match_func) def __iter__(self): tocs = list(self._fs[self._fid] or []) for toc_frame in tocs: # Find and put top level at the front of the list if toc_frame.toplevel: tocs.remove(toc_frame) tocs.insert(0, toc_frame) break for toc in tocs: yield toc @requireUnicode("description") def set(self, element_id, toplevel=False, ordered=True, child_ids=None, description=""): flist = self._fs[frames.TOC_FID] or [] # Enforce one top-level if toplevel: for toc in flist: if toc.toplevel: raise ValueError("There may only be one top-level " "table of contents. Toc '%s' is current " "top-level." % toc.element_id) for toc in flist: if toc.element_id == element_id: # update toc.toplevel = toplevel toc.ordered = ordered toc.child_ids = child_ids toc.description = description return toc toc = frames.TocFrame(element_id=element_id, toplevel=toplevel, ordered=ordered, child_ids=child_ids, description=description) self._fs[frames.TOC_FID] = toc return toc def remove(self, element_id): return super().remove(element_id) def get(self, element_id): return super().get(element_id) def __getitem__(self, elem_id): """Overiding the index based __getitem__ for one indexed with table of contents element IDs.""" for toc in (self._fs[frames.TOC_FID] or []): if toc.element_id == elem_id: return toc raise IndexError("toc '%s' not found" % elem_id) class TagTemplate(string.Template): idpattern = r'[_a-z][_a-z0-9:]*' def __init__(self, pattern, path_friendly="-", dotted_dates=False): super().__init__(pattern) if type(path_friendly) is bool and path_friendly: # Previous versions used boolean values, convert old default to new path_friendly = "-" self._path_friendly = path_friendly self._dotted_dates = dotted_dates def substitute(self, tag, zeropad=True): mapping = self._makeMapping(tag, zeropad) # Helper function for .sub() def convert(mo): named = mo.group('named') if named is not None: try: if type(mapping[named]) is tuple: func, args = mapping[named][0], mapping[named][1:] return '%s' % func(tag, named, *args) # We use this idiom instead of str() because the latter # will fail if val is a Unicode containing non-ASCII return '%s' % (mapping[named],) except KeyError: return self.delimiter + named braced = mo.group('braced') if braced is not None: try: if type(mapping[braced]) is tuple: func, args = mapping[braced][0], mapping[braced][1:] return '%s' % func(tag, braced, *args) return '%s' % (mapping[braced],) except KeyError: return self.delimiter + '{' + braced + '}' if mo.group('escaped') is not None: return self.delimiter if mo.group('invalid') is not None: return self.delimiter raise ValueError('Unrecognized named group in pattern', self.pattern) name = self.pattern.sub(convert, self.template) if self._path_friendly: name = name.replace("/", self._path_friendly) return name safe_substitute = substitute def _dates(self, tag, param): if param.startswith("release_"): date = tag.release_date elif param.startswith("recording_"): date = tag.recording_date elif param.startswith("original_release_"): date = tag.original_release_date else: date = tag.getBestDate( prefer_recording_date=":prefer_recording" in param) if date and param.endswith(":year"): dstr = str(date.year) elif date: dstr = str(date) else: dstr = "" if self._dotted_dates: dstr = dstr.replace('-', '.') return dstr @staticmethod def _nums(num_tuple, param, zeropad) -> int: nn, nt = ((str(n) if n else None) for n in num_tuple) if zeropad: if nt: nt = nt.rjust(2, "0") nn = nn.rjust(len(nt) if nt else 2, "0") if param.endswith(":num"): return nn elif param.endswith(":total"): return nt else: raise ValueError("Unknown template param: %s" % param) def _track(self, tag, param, zeropad): return self._nums(tag.track_num, param, zeropad) def _disc(self, tag, param, zeropad): return self._nums(tag.disc_num, param, zeropad) @staticmethod def _file(tag, param): assert(param.startswith("file")) if param.endswith(":ext"): return os.path.splitext(tag.file_info.name)[1][1:] else: return tag.file_info.name def _makeMapping(self, tag, zeropad): return {"artist": tag.artist if tag else None, "album_artist": tag.album_artist if tag else None, "album": tag.album if tag else None, "title": tag.title if tag else None, "track:num": (self._track, zeropad) if tag else None, "track:total": (self._track, zeropad) if tag else None, "release_date": (self._dates,) if tag else None, "release_date:year": (self._dates,) if tag else None, "recording_date": (self._dates,) if tag else None, "recording_date:year": (self._dates,) if tag else None, "original_release_date": (self._dates,) if tag else None, "original_release_date:year": (self._dates,) if tag else None, "best_date": (self._dates,) if tag else None, "best_date:year": (self._dates,) if tag else None, "best_date:prefer_recording": (self._dates,) if tag else None, "best_date:prefer_release": (self._dates,) if tag else None, "best_date:prefer_recording:year": (self._dates,) if tag else None, "best_date:prefer_release:year": (self._dates,) if tag else None, "file": (self._file,) if tag else None, "file:ext": (self._file,) if tag else None, "disc:num": (self._disc, zeropad) if tag else None, "disc:total": (self._disc, zeropad) if tag else None, } eyeD3-0.9.7/eyed3/main.py000066400000000000000000000253571432016011500150140ustar00rootroot00000000000000import os import sys import textwrap import warnings import deprecation from io import StringIO from configparser import ConfigParser from configparser import Error as ConfigParserError import eyed3 import eyed3.utils import eyed3.utils.console import eyed3.plugins import eyed3.__about__ from eyed3.utils.log import initLogging DEFAULT_PLUGIN = "classic" DEFAULT_CONFIG = os.path.expandvars("${HOME}/.config/eyeD3/config.ini") USER_PLUGINS_DIR = os.path.expandvars("${HOME}/.config/eyeD3/plugins") DEFAULT_CONFIG_DEPRECATED = os.path.expandvars("${HOME}/.eyeD3/config.ini") USER_PLUGINS_DIR_DEPRECATED = os.path.expandvars("${HOME}/.eyeD3/plugins") def main(args, config): if "list_plugins" in args and args.list_plugins: _listPlugins(config) return 0 args.plugin.start(args, config) recursive = False if "non_recursive" in args: recursive = not args.non_recursive elif "recursive" in args: recursive = args.recursive # Process paths (files/directories) for p in args.paths: eyed3.utils.walk(args.plugin, p, excludes=args.excludes, fs_encoding=args.fs_encoding, recursive=recursive) retval = args.plugin.handleDone() return retval or 0 def _listPlugins(config): from eyed3.utils.console import Fore, Style def header(name): is_default = name == DEFAULT_PLUGIN return (Style.BRIGHT + (Fore.GREEN if is_default else '') + "* " + name + Style.RESET_ALL) all_plugins = eyed3.plugins.load(reload=True, paths=_getPluginPath(config)) # Create a new dict for sorted display plugin_names = [] for plugin in set(all_plugins.values()): plugin_names.append(plugin.NAMES[0]) print("\nType 'eyeD3 --plugin= --help' for more help\n") plugin_names.sort() for name in plugin_names: plugin = all_plugins[name] alt_names = plugin.NAMES[1:] alt_names = f" ({', '.join(alt_names)})" if alt_names else "" print(f"{header(name)} {alt_names}:") for txt in textwrap.wrap(plugin.SUMMARY, initial_indent=' ' * 2, subsequent_indent=' ' * 2): print(f"{Fore.YELLOW}{txt}{Style.RESET_ALL}") print("") @deprecation.deprecated(deprecated_in="0.9a2", removed_in="1.0", current_version=eyed3.__about__.__version__, details=f"Default eyeD3 config moved to {DEFAULT_CONFIG}") def _deprecatedConfigFileCheck(_): """This here to add deprecation.""" def _loadConfig(args): config_files = [] if args.config: config_files.append(os.path.abspath(args.config)) if args.no_config is False: config_files.append(DEFAULT_CONFIG) config_files.append(DEFAULT_CONFIG_DEPRECATED) if not config_files: return None for config_file in config_files: if os.path.isfile(config_file): _deprecatedConfigFileCheck(config_file) try: config = ConfigParser() config.read(config_file) except ConfigParserError as ex: eyed3.log.warning(f"User config error: {ex}") return None else: return config elif config_file != DEFAULT_CONFIG and config_file != DEFAULT_CONFIG_DEPRECATED: raise IOError(f"User config not found: {config_file}") def _getPluginPath(config): plugin_path = [USER_PLUGINS_DIR] if config and config.has_option("default", "plugin_path"): val = config.get("default", "plugin_path") plugin_path += [os.path.expanduser(os.path.expandvars(d)) for d in val.split(':') if val] return plugin_path def profileMain(args, config): # pragma: no cover """This is the main function for profiling http://code.google.com/appengine/kb/commontasks.html#profiling """ import cProfile import pstats eyed3.log.debug("driver profileMain") prof = cProfile.Profile() prof = prof.runctx("main(args)", globals(), locals()) stream = StringIO() stats = pstats.Stats(prof, stream=stream) stats.sort_stats("time") # Or cumulative stats.print_stats(100) # 80 = how many to print # The rest is optional. stats.print_callees() stats.print_callers() sys.stderr.write("Profile data:\n%s\n" % stream.getvalue()) return 0 def setFileScannerOpts(arg_parser, default_recursive=False, paths_metavar="PATH", paths_help="Files or directory paths"): if default_recursive is False: arg_parser.add_argument("-r", "--recursive", action="store_true", dest="recursive", help="Recurse into subdirectories.") else: arg_parser.add_argument("-R", "--non-recursive", action="store_true", dest="non_recursive", help="Do not recurse into subdirectories.") arg_parser.add_argument("--exclude", action="append", metavar="PATTERN", dest="excludes", help="A regular expression for path exclusion. May be specified " "multiple times.") arg_parser.add_argument("--fs-encoding", action="store", dest="fs_encoding", default=eyed3.LOCAL_FS_ENCODING, metavar="ENCODING", help="Use the specified file system encoding for filenames. " f"Default as it was detected is '{eyed3.LOCAL_FS_ENCODING}' but " "this option is still useful when reading from mounted file " "systems.") arg_parser.add_argument("paths", metavar=paths_metavar, nargs="*", help=paths_help) def makeCmdLineParser(subparser=None): from eyed3.utils import ArgumentParser p = ArgumentParser(prog=eyed3.__about__.__project_name__, add_help=True)\ if not subparser else subparser setFileScannerOpts(p) p.add_argument("-L", "--plugins", action="store_true", default=False, dest="list_plugins", help="List all available plugins") p.add_argument("-P", "--plugin", action="store", dest="plugin", default=None, metavar="NAME", help=f"Specify which plugin to use. The default is '{DEFAULT_PLUGIN}'") p.add_argument("-C", "--config", action="store", dest="config", default=None, metavar="FILE", help="Supply a configuration file. The default is " f"'{DEFAULT_CONFIG}', although even that is optional.") p.add_argument("--backup", action="store_true", dest="backup", help="Plugins should honor this option such that " "a backup is made of any file modified. The backup " "is made in same directory with a '.orig' " "extension added.") p.add_argument("-Q", "--quiet", action="store_true", dest="quiet", default=False, help="A hint to plugins to output less.") p.add_argument("--no-color", action="store_true", dest="no_color", help="Suppress color codes in console output. " "This will happen automatically if the output is " "not a TTY (e.g. when redirecting to a file)") p.add_argument("--no-config", action="store_true", dest="no_config", help=f"Do not load the default user config '{DEFAULT_CONFIG}'. " "The -c/--config options are still honored if present.") return p def parseCommandLine(cmd_line_args=None): cmd_line_args = list(cmd_line_args) if cmd_line_args else list(sys.argv[1:]) # Remove any options not related to plugin/config for first parse. These # determine the parser for the next stage. stage_one_args = [] idx, auto_append = 0, False while idx < len(cmd_line_args): opt = cmd_line_args[idx] if auto_append: stage_one_args.append(opt) auto_append = False if opt in ("-C", "--config", "-P", "--plugin", "--no-config"): stage_one_args.append(opt) if opt != "--no-config": auto_append = True elif (opt.startswith("-C=") or opt.startswith("--config=") or opt.startswith("-P=") or opt.startswith("--plugin=")): stage_one_args.append(opt) idx += 1 parser = makeCmdLineParser() args = parser.parse_args(stage_one_args) config = _loadConfig(args) if args.plugin: # Plugin on the command line takes precedence over config. plugin_name = args.plugin elif config and config.has_option("default", "plugin"): # Get default plugin from config or use DEFAULT_CONFIG plugin_name = config.get("default", "plugin") if not plugin_name: plugin_name = DEFAULT_PLUGIN else: plugin_name = DEFAULT_PLUGIN PluginClass = eyed3.plugins.load(plugin_name, paths=_getPluginPath(config)) if PluginClass is None: eyed3.utils.console.printError("Plugin not found: %s" % plugin_name) parser.exit(1) plugin = PluginClass(parser) if config and config.has_option("default", "options"): cmd_line_args.extend(config.get("default", "options").split()) if config and config.has_option(plugin_name, "options"): cmd_line_args.extend(config.get(plugin_name, "options").split()) # Re-parse the command line including options from the config. args = parser.parse_args(args=cmd_line_args) args.plugin = plugin eyed3.log.debug("command line args: %s", args) eyed3.log.debug("plugin is: %s", plugin) return args, parser, config def _main(): """Entry point""" initLogging() args = None try: args, _, config = parseCommandLine() eyed3.utils.console.AnsiCodes.init(not args.no_color) mainFunc = main if args.debug_profile is False else profileMain retval = mainFunc(args, config) except KeyboardInterrupt: retval = 0 except (StopIteration, IOError) as ex: eyed3.utils.console.printError(str(ex)) retval = 1 except Exception as ex: eyed3.utils.console.printError(f"Uncaught exception: {ex}\n") eyed3.log.exception(ex) retval = 1 if args.debug_pdb: try: with warnings.catch_warnings(): warnings.simplefilter("ignore", PendingDeprecationWarning) # Must delay the import of ipdb as say as possible because # of https://github.com/gotcha/ipdb/issues/48 import ipdb as pdb # noqa except ImportError: import pdb # noqa e, m, tb = sys.exc_info() pdb.post_mortem(tb) sys.exit(retval) if __name__ == "__main__": # pragma: no cover _main() eyeD3-0.9.7/eyed3/mimetype.py000066400000000000000000000063341432016011500157130ustar00rootroot00000000000000import pathlib import filetype from io import BytesIO from .id3 import ID3_MIME_TYPE, ID3_MIME_TYPE_EXTENSIONS from .mp3 import MIME_TYPES as MP3_MIME_TYPES from .utils.log import getLogger from filetype.utils import _NUM_SIGNATURE_BYTES log = getLogger(__name__) def guessMimetype(filename): """Return the mime-type for `filename`.""" path = pathlib.Path(filename) if not isinstance(filename, pathlib.Path) else filename with path.open("rb") as signature: # Since filetype only reads 262 of file many mp3s starting with null bytes will not find # a header, so ignoring null bytes and using the bytes interface... buf = b"" while not buf: data = signature.read(_NUM_SIGNATURE_BYTES) if not data: break data = data.lstrip(b"\x00") if data: data_len = len(data) if data_len >= _NUM_SIGNATURE_BYTES: buf = data[:_NUM_SIGNATURE_BYTES] else: buf = data + signature.read(_NUM_SIGNATURE_BYTES - data_len) # Special casing .id3/.tag because extended filetype with add_type() prepends, meaning # all mp3 would be labeled mimetype id3, while appending would mean each .id3 would be # mime mpeg. if path.suffix in ID3_MIME_TYPE_EXTENSIONS: if Id3Tag().match(buf) or Id3TagExt().match(buf): return Id3TagExt.MIME return filetype.guess_mime(buf) class Mp2x(filetype.Type): """Implements the MP2.x audio type matcher.""" MIME = MP3_MIME_TYPES[0] EXTENSION = "mp3" def __init__(self): super().__init__(mime=self.__class__.MIME, extension=self.__class__.EXTENSION) def match(self, buf): from .mp3.headers import findHeader return (len(buf) > 2 and buf[0] == 0xff and buf[1] in (0xf3, 0xe3) and findHeader(BytesIO(buf), 0)[1]) class Mp3Invalids(filetype.Type): """Implements a MP3 audio type matcher this is odd or/corrupt mp3.""" MIME = MP3_MIME_TYPES[0] EXTENSION = "mp3" def __init__(self): super().__init__(mime=self.__class__.MIME, extension=self.__class__.EXTENSION) def match(self, buf): from .mp3.headers import findHeader header = findHeader(BytesIO(buf), 0)[1] log.debug(f"Mp3Invalid, found: {header}") return bool(header) class Id3Tag(filetype.Type): """Implements a MP3 audio type matcher this is odd or/corrupt mp3.""" MIME = ID3_MIME_TYPE EXTENSION = "id3" def __init__(self): super().__init__(mime=self.__class__.MIME, extension=self.__class__.EXTENSION) def match(self, buf): return buf[:3] in (b"ID3", b"TAG") or len(buf) == 0 class Id3TagExt(Id3Tag): EXTENSION = "tag" class M3u(filetype.Type): """Implements the m3u playlist matcher.""" MIME = "audio/x-mpegurl" EXTENSION = "m3u" def __init__(self): super().__init__(mime=self.__class__.MIME, extension=self.__class__.EXTENSION) def match(self, buf): return len(buf) > 6 and buf.startswith(b"#EXTM3U") # Not using `add_type()`, to append filetype.types.append(Mp2x()) filetype.types.append(M3u()) filetype.types.append(Mp3Invalids()) eyeD3-0.9.7/eyed3/mp3/000077500000000000000000000000001432016011500142015ustar00rootroot00000000000000eyeD3-0.9.7/eyed3/mp3/__init__.py000066400000000000000000000147151432016011500163220ustar00rootroot00000000000000import os import re import stat from .. import Error from .. import id3 from .. import core from ..utils.log import getLogger log = getLogger(__name__) class Mp3Exception(Error): """Used to signal mp3-related errors.""" pass NAME = "mpeg" # Mime-types that are recognized at MP3 MIME_TYPES = ["audio/mpeg", "audio/mp3", "audio/x-mp3", "audio/x-mpeg", "audio/mpeg3", "audio/x-mpeg3", "audio/mpg", "audio/x-mpg", "audio/x-mpegaudio", "audio/mpegapplication/x-tar", ] # Mime-types that have been seen to contain mp3 data. OTHER_MIME_TYPES = ['application/octet-stream', # ??? 'audio/x-hx-aac-adts', # ??? 'audio/x-wav', # RIFF wrapped mp3s ] # Valid file extensions. EXTENSIONS = [".mp3"] class Mp3AudioInfo(core.AudioInfo): def __init__(self, file_obj, start_offset, tag): from . import headers from .headers import timePerFrame log.debug("mp3 header search starting @ %x" % start_offset) self.mp3_header = None self.xing_header = None self.vbri_header = None # If not ``None``, the Lame header. # See :class:`eyed3.mp3.headers.LameHeader` self.lame_tag = None # 2-tuple, (vrb?:boolean, bitrate:int) self.bit_rate = (None, None) header_pos = 0 while self.mp3_header is None: # Find first mp3 header (header_pos, header_int, header_bytes) = headers.findHeader(file_obj, start_offset) if not header_int: try: fname = file_obj.name except AttributeError: fname = 'unknown' raise headers.Mp3Exception( "Unable to find a valid mp3 frame in '%s'" % fname) try: self.mp3_header = headers.Mp3Header(header_int) log.debug("mp3 header %x found at position: 0x%x" % (header_int, header_pos)) except headers.Mp3Exception as ex: log.debug("Invalid mp3 header: %s" % str(ex)) # keep looking... start_offset += 4 file_obj.seek(header_pos) mp3_frame = file_obj.read(self.mp3_header.frame_length) if re.compile(b'Xing|Info').search(mp3_frame): # Check for Xing/Info header information. self.xing_header = headers.XingHeader() if not self.xing_header.decode(mp3_frame): log.debug("Ignoring corrupt Xing header") self.xing_header = None elif mp3_frame.find(b'VBRI') >= 0: # Check for VBRI header information. self.vbri_header = headers.VbriHeader() if not self.vbri_header.decode(mp3_frame): log.debug("Ignoring corrupt VBRI header") self.vbri_header = None # Check for LAME Tag self.lame_tag = headers.LameHeader(mp3_frame) # Set file size size_bytes = os.stat(file_obj.name)[stat.ST_SIZE] # Compute track play time. if self.xing_header and self.xing_header.vbr: tpf = timePerFrame(self.mp3_header, True) time_secs = tpf * self.xing_header.numFrames elif self.vbri_header and self.vbri_header.version == 1: tpf = timePerFrame(self.mp3_header, True) time_secs = tpf * self.vbri_header.num_frames else: tpf = timePerFrame(self.mp3_header, False) length = size_bytes if tag and tag.isV2(): length -= tag.header.SIZE + tag.header.tag_size # Handle the case where there is a v2 tag and a v1 tag. file_obj.seek(-128, 2) if file_obj.read(3) == "TAG": length -= 128 elif tag and tag.isV1(): length -= 128 time_secs = (length / self.mp3_header.frame_length) * tpf # Compute bitrate if (self.xing_header and self.xing_header.vbr and self.xing_header.numFrames): # if xing_header.numFrames == 0, ZeroDivisionError br = int((self.xing_header.numBytes * 8) / (tpf * self.xing_header.numFrames * 1000)) vbr = True else: br = self.mp3_header.bit_rate vbr = False self.bit_rate = (vbr, br) self.sample_freq = self.mp3_header.sample_freq self.mode = self.mp3_header.mode super().__init__(time_secs, size_bytes) ## # Helper to get the bitrate as a string. The prefix '~' is used to denote # variable bit rates. @property def bit_rate_str(self): (vbr, bit_rate) = self.bit_rate return f"{'~' if vbr else ''}{bit_rate} kb/s" class Mp3AudioFile(core.AudioFile): """Audio file container for mp3 files.""" def __init__(self, path, version=id3.ID3_ANY_VERSION): self._tag_version = version super().__init__(path) assert self.type == core.AUDIO_MP3 def _read(self): with open(self.path, "rb") as file_obj: self._tag = id3.Tag() tag_found = self._tag.parse(file_obj, self._tag_version) # Compute offset for starting mp3 data search if tag_found and self._tag.isV1(): mp3_offset = 0 elif tag_found and self._tag.isV2(): mp3_offset = self._tag.header.SIZE + self._tag.header.tag_size else: mp3_offset = 0 self._tag = None try: self._info = Mp3AudioInfo(file_obj, mp3_offset, self._tag) except Mp3Exception as ex: # Only logging a warning here since we can still operate on # the tag. log.warning(ex) self._info = None self.type = core.AUDIO_MP3 def initTag(self, version=id3.ID3_DEFAULT_VERSION): """Add a id3.Tag to the file (removing any existing tag if one exists).""" self.tag = id3.Tag() self.tag.version = version self.tag.file_info = id3.FileInfo(self.path) return self.tag @core.AudioFile.tag.setter def tag(self, t): if t: t.file_info = id3.FileInfo(self.path) if self._tag and self._tag.file_info: t.file_info.tag_size = self._tag.file_info.tag_size t.file_info.tag_padding_size = \ self._tag.file_info.tag_padding_size self._tag = t eyeD3-0.9.7/eyed3/mp3/headers.py000066400000000000000000000775201432016011500162010ustar00rootroot00000000000000import deprecation from math import log10 from . import Mp3Exception from ..utils.binfuncs import bytes2bin, bytes2dec, bin2dec from ..utils.log import getLogger from ..__about__ import __version__ log = getLogger(__name__) def isValidHeader(header): """Determine if ``header`` (an integer, 4 bytes compared) is a valid mp3 frame header.""" # Test for the mp3 frame sync: 11 set bits. sync = (header >> 16) if sync & 0xffe0 != 0xffe0: # ffe0 is 11 sync bits, 12 are not used in order to support identifying # mpeg v2.5 (bits 20,19) return False # All the remaining tests are not entirely required, but do help in # finding false syncs version = (header >> 19) & 0x3 if version == 1: # This is a "reserved" version log.debug("invalid mpeg version") return False layer = (header >> 17) & 0x3 if layer == 0: # This is a "reserved" layer log.debug("invalid mpeg layer") return False bitrate = (header >> 12) & 0xf if bitrate in (0, 0xf): # free and bad bitrate values log.debug("invalid mpeg bitrate") return False sample_rate = (header >> 10) & 0x3 if sample_rate == 0x3: # this is a "reserved" sample rate log.debug("invalid mpeg sample rate") return False return True def findHeader(fp, start_pos=0): """Locate the first mp3 header in file stream ``fp`` starting a offset ``start_pos`` (defaults to 0). Returned is a 3-tuple containing the offset where the header was found, the header as an integer, and the header as 4 bytes. If no header is found header_int will equal 0. """ def isBOM(buffer, pos): """Check for unicode BOM""" try: if pos - 1 >= 0: if buffer[pos - 1] == 254: return True return buffer[pos + 1] == 254 except IndexError: return False def find_sync(_fp, _pos=0): chunk_sz = 8192 # Measured as optimal _fp.seek(_pos) data = _fp.read(chunk_sz) while data: pos = 0 while 0 <= pos < chunk_sz: pos = data.find(b"\xff", pos) if pos == -1: break if not isBOM(data, pos): h = data[pos:pos + 4] if len(h) == 4: return tuple([_pos + pos, h]) pos += 1 _pos += len(data) data = _fp.read(chunk_sz) return None, None sync_pos, header_bytes = find_sync(fp, start_pos) while sync_pos is not None: header = bytes2dec(header_bytes) if isValidHeader(header): return tuple([sync_pos, header, header_bytes]) sync_pos, header_bytes = find_sync(fp, start_pos + sync_pos + 2) return None, None, None def timePerFrame(mp3_header, vbr): """Computes the number of seconds per mp3 frame. It can be used to compute overall playtime and bitrate. The mp3 layer and sample rate from ``mp3_header`` are used to compute the number of seconds (fractional float point value) per mp3 frame. Be sure to set ``vbr`` True when dealing with VBR, otherwise playtimes may be incorrect.""" # https://bitbucket.org/nicfit/eyed3/issue/32/mp3audioinfotime_secs-incorrect-for-mpeg2 if mp3_header.version >= 2.0 and vbr: row = _mp3VersionKey(mp3_header.version) else: row = 0 return (float(SAMPLES_PER_FRAME_TABLE[row][mp3_header.layer]) / float(mp3_header.sample_freq)) @deprecation.deprecated(deprecated_in="0.9a2", removed_in="1.0", current_version=__version__, details="Use timePerFrame instead") def compute_time_per_frame(mp3_header): if mp3_header is not None: return timePerFrame(mp3_header, False) class Mp3Header: """Header container for MP3 frames.""" def __init__(self, header_data=None): self.version = None self.layer = None self.error_protection = None self.bit_rate = None self.sample_freq = None self.padding = None self.private_bit = None self.copyright = None self.original = None self.emphasis = None self.mode = None # This value is left as is: 0<=mode_extension<=3. # See http://www.dv.co.yu/mpgscript/mpeghdr.htm for how to interpret self.mode_extension = None self.frame_length = None if header_data: self.decode(header_data) # This may throw an Mp3Exception if the header is malformed. def decode(self, header): if not isValidHeader(header): raise Mp3Exception("Invalid MPEG header") # MPEG audio version from bits 19 and 20. version = (header >> 19) & 0x3 self.version = [2.5, None, 2.0, 1.0][version] if self.version is None: raise Mp3Exception("Illegal MPEG version") # MPEG layer self.layer = 4 - ((header >> 17) & 0x3) if self.layer == 4: raise Mp3Exception("Illegal MPEG layer") # Decode some simple values. self.error_protection = not (header >> 16) & 0x1 self.padding = (header >> 9) & 0x1 self.private_bit = (header >> 8) & 0x1 self.copyright = (header >> 3) & 0x1 self.original = (header >> 2) & 0x1 # Obtain sampling frequency. sample_bits = (header >> 10) & 0x3 self.sample_freq = \ SAMPLE_FREQ_TABLE[sample_bits][_mp3VersionKey(self.version)] if not self.sample_freq: raise Mp3Exception("Illegal MPEG sampling frequency") # Compute bitrate. bit_rate_row = (header >> 12) & 0xf if int(self.version) == 1 and self.layer == 1: bit_rate_col = 0 elif int(self.version) == 1 and self.layer == 2: bit_rate_col = 1 elif int(self.version) == 1 and self.layer == 3: bit_rate_col = 2 elif int(self.version) == 2 and self.layer == 1: bit_rate_col = 3 elif int(self.version) == 2 and (self.layer == 2 or self.layer == 3): bit_rate_col = 4 else: raise Mp3Exception("Mp3 version %f and layer %d is an invalid " "combination" % (self.version, self.layer)) self.bit_rate = BIT_RATE_TABLE[bit_rate_row][bit_rate_col] if self.bit_rate is None: raise Mp3Exception("Invalid bit rate") # We know know the bit rate specified in this frame, but if the file # is VBR we need to obtain the average from the Xing header. # This is done by the caller since right now all we have is the frame # header. # Emphasis; whatever that means?? emph = header & 0x3 if emph == 0: self.emphasis = EMPHASIS_NONE elif emph == 1: self.emphasis = EMPHASIS_5015 elif emph == 2: self.emphasis = EMPHASIS_CCIT else: raise Mp3Exception("Illegal mp3 emphasis value: %d" % emph) # Channel mode. mode_bits = (header >> 6) & 0x3 if mode_bits == 0: self.mode = MODE_STEREO elif mode_bits == 1: self.mode = MODE_JOINT_STEREO elif mode_bits == 2: self.mode = MODE_DUAL_CHANNEL_STEREO else: self.mode = MODE_MONO self.mode_extension = (header >> 4) & 0x3 # Layer II has restrictions wrt to mode and bit rate. This code # enforces them. if self.layer == 2: m = self.mode br = self.bit_rate if (br in [32, 48, 56, 80] and (m != MODE_MONO)): raise Mp3Exception("Invalid mode/bitrate combination for layer " "II") if (br in [224, 256, 320, 384] and (m == MODE_MONO)): raise Mp3Exception("Invalid mode/bitrate combination for layer " "II") br = self.bit_rate * 1000 sf = self.sample_freq p = self.padding if self.layer == 1: # Layer 1 uses 32 bit slots for padding. p = self.padding * 4 self.frame_length = int((((12 * br) / sf) + p) * 4) else: # Layer 2 and 3 uses 8 bit slots for padding. p = self.padding * 1 self.frame_length = int(((144 * br) / sf) + p) # Dump the state. log.debug("MPEG audio version: " + str(self.version)) log.debug("MPEG audio layer: " + ("I" * self.layer)) log.debug("MPEG sampling frequency: " + str(self.sample_freq)) log.debug("MPEG bit rate: " + str(self.bit_rate)) log.debug("MPEG channel mode: " + self.mode) log.debug("MPEG channel mode extension: " + str(self.mode_extension)) log.debug("MPEG CRC error protection: " + str(self.error_protection)) log.debug("MPEG original: " + str(self.original)) log.debug("MPEG copyright: " + str(self.copyright)) log.debug("MPEG private bit: " + str(self.private_bit)) log.debug("MPEG padding: " + str(self.padding)) log.debug("MPEG emphasis: " + str(self.emphasis)) log.debug("MPEG frame length: " + str(self.frame_length)) class VbriHeader(object): def __init__(self): self.vbr = True self.version = None ## # \brief Decode the VBRI info from \a frame. # http://www.codeproject.com/audio/MPEGAudioInfo.asp#VBRIHeader def decode(self, frame): # The header is 32 bytes after the end of the first MPEG audio header, # therefore 4 + 32 = 36 offset = 36 head = frame[offset:offset + 4] if head != 'VBRI': return False log.debug("VBRI header detected @ %x" % (offset)) offset += 4 self.version = bin2dec(bytes2bin(frame[offset:offset + 2])) offset += 2 self.delay = bin2dec(bytes2bin(frame[offset:offset + 2])) offset += 2 self.quality = bin2dec(bytes2bin(frame[offset:offset + 2])) offset += 2 self.num_bytes = bin2dec(bytes2bin(frame[offset:offset + 4])) offset += 4 self.num_frames = bin2dec(bytes2bin(frame[offset:offset + 4])) offset += 4 return True class XingHeader: """Header class for the Xing header extensions.""" def __init__(self): self.numFrames = int() self.numBytes = int() self.toc = [0] * 100 self.vbrScale = int() # Pass in the first mp3 frame from the file as a byte string. # If an Xing header is present in the file it'll be in the first mp3 # frame. This method returns true if the Xing header is found in the # frame, and false otherwise. def decode(self, frame): # mp3 version version = (frame[1] >> 3) & 0x1 # channel mode. mode = (frame[3] >> 6) & 0x3 # Find the start of the Xing header. if version: # +4 in all of these to skip initial mp3 frame header. if mode != 3: pos = 32 + 4 else: pos = 17 + 4 else: if mode != 3: pos = 17 + 4 else: pos = 9 + 4 head = frame[pos:pos + 4] self.vbr = (head == b'Xing') and True or False if head not in [b'Xing', b'Info']: return False log.debug("%s header detected @ %x" % (head, pos)) pos += 4 # Read Xing flags. headFlags = bin2dec(bytes2bin(frame[pos:pos + 4])) pos += 4 log.debug("%s header flags: 0x%x" % (head, headFlags)) # Read frames header flag and value if present if headFlags & FRAMES_FLAG: self.numFrames = bin2dec(bytes2bin(frame[pos:pos + 4])) pos += 4 log.debug("%s numFrames: %d" % (head, self.numFrames)) # Read bytes header flag and value if present if headFlags & BYTES_FLAG: self.numBytes = bin2dec(bytes2bin(frame[pos:pos + 4])) pos += 4 log.debug("%s numBytes: %d" % (head, self.numBytes)) # Read TOC header flag and value if present if headFlags & TOC_FLAG: self.toc = frame[pos:pos + 100] pos += 100 log.debug("%s TOC (100 bytes): PRESENT" % head) else: log.debug("%s TOC (100 bytes): NOT PRESENT" % head) # Read vbr scale header flag and value if present if headFlags & VBR_SCALE_FLAG and head == b'Xing': self.vbrScale = bin2dec(bytes2bin(frame[pos:pos + 4])) pos += 4 log.debug("%s vbrScale: %d" % (head, self.vbrScale)) return True class LameHeader(dict): """ Mp3 Info tag (AKA LAME Tag) Lame (and some other encoders) write a tag containing various bits of info about the options used at encode time. If available, the following are parsed and stored in the LameHeader dict: * encoder_version: short encoder version [str] * tag_revision: revision number of the tag [int] * vbr_method: VBR method used for encoding [str] * lowpass_filter: lowpass filter frequency in Hz [int] * replaygain: if available, radio and audiofile gain (see below) [dict] * encoding_flags: encoding flags used [list] * nogap: location of gaps when --nogap was used [list] * ath_type: ATH type [int] * bitrate: bitrate and type (Constant, Target, Minimum) [tuple] * encoder_delay: samples added at the start of the mp3 [int] * encoder_padding: samples added at the end of the mp3 [int] * noise_shaping: noise shaping method [int] * stereo_mode: stereo mode used [str] * unwise_settings: whether unwise settings were used [boolean] * sample_freq: source sample frequency [str] * mp3_gain: mp3 gain adjustment (rarely used) [float] * preset: preset used [str] * surround_info: surround information [str] * music_length: length in bytes of original mp3 [int] * music_crc: CRC-16 of the mp3 music data [int] * infotag_crc: CRC-16 of the info tag [int] Prior to ~3.90, Lame simply stored the encoder version in the first frame. If the infotag_crc is invalid, then we try to read this version string. A simple way to tell if the LAME Tag is complete is to check for the infotag_crc key. Replay Gain data is only available since Lame version 3.94b. If set, the replaygain dict has the following structure: .. code-block:: peak_amplitude: peak signal amplitude [float] radio: name: name of the gain adjustment [str] adjustment: gain adjustment [float] originator: originator of the gain adjustment [str] audiofile: [same as radio] Note that as of 3.95.1, Lame uses 89dB as a reference level instead of the 83dB that is specified in the Replay Gain spec. This is not automatically compensated for. You can do something like this if you want: .. code-block:: import eyeD3 af = eyeD3.mp3.Mp3AudioFile('/path/to/some.mp3') lamever = af.lameTag['encoder_version'] name, ver = lamever[:4], lamever[4:] gain = af.lameTag['replaygain']['radio']['adjustment'] if name == 'LAME' and eyeD3.mp3.lamevercmp(ver, '3.95') > 0: gain -= 6 Radio and Audiofile Replay Gain are often referred to as Track and Album gain, respectively. See https://replaygain.hydrogenaudio.org/ for further details on Replay Gain. See http://gabriel.mp3-tech.org/mp3infotag.html for the gory details of the LAME Tag. """ # from the LAME source: # http://lame.cvs.sourceforge.net/*checkout*/lame/lame/libmp3lame/VbrTag.c _crc16_table = [ 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040] ENCODER_FLAGS = { 'NSPSYTUNE': 0x0001, 'NSSAFEJOINT': 0x0002, 'NOGAP_NEXT': 0x0004, 'NOGAP_PREV': 0x0008} PRESETS = { 0: 'Unknown', # 8 to 320 are reserved for ABR bitrates 410: 'V9', 420: 'V8', 430: 'V7', 440: 'V6', 450: 'V5', 460: 'V4', 470: 'V3', 480: 'V2', 490: 'V1', 500: 'V0', 1000: 'r3mix', 1001: 'standard', 1002: 'extreme', 1003: 'insane', 1004: 'standard/fast', 1005: 'extreme/fast', 1006: 'medium', 1007: 'medium/fast'} REPLAYGAIN_NAME = { 0: 'Not set', 1: 'Radio', 2: 'Audiofile'} REPLAYGAIN_ORIGINATOR = { 0: 'Not set', 1: 'Set by artist', 2: 'Set by user', 3: 'Set automatically', 100: 'Set by simple RMS average'} SAMPLE_FREQUENCIES = { 0: '<= 32 kHz', 1: '44.1 kHz', 2: '48 kHz', 3: '> 48 kHz'} STEREO_MODES = { 0: 'Mono', 1: 'Stereo', 2: 'Dual', 3: 'Joint', 4: 'Force', 5: 'Auto', 6: 'Intensity', 7: 'Undefined'} SURROUND_INFO = { 0: 'None', 1: 'DPL encoding', 2: 'DPL2 encoding', 3: 'Ambisonic encoding', 8: 'Reserved'} VBR_METHODS = { 0: 'Unknown', 1: 'Constant Bitrate', 2: 'Average Bitrate', 3: 'Variable Bitrate method1 (old/rh)', 4: 'Variable Bitrate method2 (mtrh)', 5: 'Variable Bitrate method3 (mt)', 6: 'Variable Bitrate method4', 8: 'Constant Bitrate (2 pass)', 9: 'Average Bitrate (2 pass)', 15: 'Reserved'} def __init__(self, frame): """Read the LAME info tag. frame should be the first frame of an mp3. """ super().__init__() self.decode(frame) def _crc16(self, data, val=0): """Compute a CRC-16 checksum on a data stream.""" for c in [bytes([b]) for b in data]: val = self._crc16_table[ord(c) ^ (val & 0xff)] ^ (val >> 8) return val def decode(self, frame): """Decode the LAME info tag.""" try: pos = frame.index(b"LAME") except ValueError: return log.debug(f"Lame info tag found at position {pos}") # check the info tag crc. If it's not valid, no point parsing much more. lamecrc = bin2dec(bytes2bin(frame[190:192])) if self._crc16(frame[:190]) != lamecrc: log.warning("Lame tag CRC check failed") else: log.debug("Lame tag CRC OK") try: # Encoder short VersionString, 9 bytes self['encoder_version'] = str(frame[pos:pos + 9].rstrip(), "latin1") log.debug('Lame Encoder Version: %s' % self['encoder_version']) pos += 9 # Info Tag revision + VBR method, 1 byte self['tag_revision'] = bin2dec(bytes2bin(frame[pos:pos + 1])[:5]) vbr_method = bin2dec(bytes2bin(frame[pos:pos + 1])[5:]) self['vbr_method'] = self.VBR_METHODS.get(vbr_method, 'Unknown') log.debug('Lame info tag version: %s' % self['tag_revision']) log.debug('Lame VBR method: %s' % self['vbr_method']) pos += 1 # Lowpass filter value, 1 byte self['lowpass_filter'] = bin2dec( bytes2bin(frame[pos:pos + 1])) * 100 log.debug('Lame Lowpass filter value: %s Hz' % self['lowpass_filter']) pos += 1 # Replay Gain, 8 bytes total replaygain = {} # Peak signal amplitude, 4 bytes peak = bin2dec(bytes2bin(frame[pos:pos + 4])) << 5 if peak > 0: peak /= float(1 << 28) db = 20 * log10(peak) replaygain['peak_amplitude'] = peak log.debug('Lame Peak signal amplitude: %.8f (%+.1f dB)' % (peak, db)) pos += 4 # Radio and Audiofile Gain, AKA track and album, 2 bytes each for gaintype in ['radio', 'audiofile']: name = bin2dec(bytes2bin(frame[pos:pos + 2])[:3]) orig = bin2dec(bytes2bin(frame[pos:pos + 2])[3:6]) sign = bin2dec(bytes2bin(frame[pos:pos + 2])[6:7]) adj = bin2dec(bytes2bin(frame[pos:pos + 2])[7:]) / 10.0 if sign: adj *= -1 # Lame 3.95.1 and above use 89dB as a reference instead of 83dB as defined by the # Replay Gain spec. This will be compensated for with `adj -= 6` lamever = self['encoder_version'] if lamever[:4] == 'LAME' and lamevercmp(lamever[4:], "3.95") > 0: adj -= 6 if orig: name = self.REPLAYGAIN_NAME.get(name, 'Unknown') orig = self.REPLAYGAIN_ORIGINATOR.get(orig, 'Unknown') replaygain[gaintype] = {'name': name, 'adjustment': adj, 'originator': orig} log.debug('Lame %s Replay Gain: %s dB (%s)' % (name, adj, orig)) pos += 2 if replaygain: self['replaygain'] = replaygain # Encoding flags + ATH Type, 1 byte encflags = bin2dec(bytes2bin(frame[pos:pos + 1])[:4]) (self['encoding_flags'], self['nogap']) = self._parse_encflags(encflags) self['ath_type'] = bin2dec(bytes2bin(frame[pos:pos + 1])[4:]) log.debug('Lame Encoding flags: %s' % ' '.join(self['encoding_flags'])) if self['nogap']: log.debug('Lame No gap: %s' % ' and '.join(self['nogap'])) log.debug('Lame ATH type: %s' % self['ath_type']) pos += 1 # if ABR {specified bitrate} else {minimal bitrate}, 1 byte btype = 'Constant' if 'Average' in self['vbr_method']: btype = 'Target' elif 'Variable' in self['vbr_method']: btype = 'Minimum' # bitrate may be modified below after preset is read self['bitrate'] = (bin2dec(bytes2bin(frame[pos:pos + 1])), btype) log.debug('Lame Bitrate (%s): %s' % (btype, self['bitrate'][0])) pos += 1 # Encoder delays, 3 bytes self['encoder_delay'] = bin2dec(bytes2bin(frame[pos:pos + 3])[:12]) self['encoder_padding'] = bin2dec( bytes2bin(frame[pos:pos + 3])[12:]) log.debug('Lame Encoder delay: %s samples' % self['encoder_delay']) log.debug('Lame Encoder padding: %s samples' % self['encoder_padding']) pos += 3 # Misc, 1 byte sample_freq = bin2dec(bytes2bin(frame[pos:pos + 1])[:2]) unwise_settings = bin2dec(bytes2bin(frame[pos:pos + 1])[2:3]) stereo_mode = bin2dec(bytes2bin(frame[pos:pos + 1])[3:6]) self['noise_shaping'] = bin2dec(bytes2bin(frame[pos:pos + 1])[6:]) self['sample_freq'] = self.SAMPLE_FREQUENCIES.get(sample_freq, 'Unknown') self['unwise_settings'] = bool(unwise_settings) self['stereo_mode'] = self.STEREO_MODES.get(stereo_mode, 'Unknown') log.debug('Lame Source Sample Frequency: %s' % self['sample_freq']) log.debug('Lame Unwise settings used: %s' % self['unwise_settings']) log.debug('Lame Stereo mode: %s' % self['stereo_mode']) log.debug('Lame Noise Shaping: %s' % self['noise_shaping']) pos += 1 # MP3 Gain, 1 byte sign = bytes2bin(frame[pos:pos + 1])[0] gain = bin2dec(bytes2bin(frame[pos:pos + 1])[1:]) if sign: gain *= -1 self['mp3_gain'] = gain db = gain * 1.5 log.debug('Lame MP3 Gain: %s (%+.1f dB)' % (self['mp3_gain'], db)) pos += 1 # Preset and surround info, 2 bytes surround = bin2dec(bytes2bin(frame[pos:pos + 2])[2:5]) preset = bin2dec(bytes2bin(frame[pos:pos + 2])[5:]) if preset in range(8, 321): if self['bitrate'][0] >= 255: # the value from preset is better in this case self['bitrate'] = (preset, btype) log.debug('Lame Bitrate (%s): %s' % (btype, self['bitrate'][0])) if 'Average' in self['vbr_method']: preset = 'ABR %s' % preset else: preset = 'CBR %s' % preset else: preset = self.PRESETS.get(preset, preset) self['surround_info'] = self.SURROUND_INFO.get(surround, surround) self['preset'] = preset log.debug('Lame Surround Info: %s' % self['surround_info']) log.debug('Lame Preset: %s' % self['preset']) pos += 2 # MusicLength, 4 bytes self['music_length'] = bin2dec(bytes2bin(frame[pos:pos + 4])) log.debug('Lame Music Length: %s bytes' % self['music_length']) pos += 4 # MusicCRC, 2 bytes self['music_crc'] = bin2dec(bytes2bin(frame[pos:pos + 2])) log.debug('Lame Music CRC: %04X' % self['music_crc']) pos += 2 # CRC-16 of Info Tag, 2 bytes self['infotag_crc'] = lamecrc # we read this earlier log.debug('Lame Info Tag CRC: %04X' % self['infotag_crc']) pos += 2 except IndexError: log.warning("Truncated LAME info header, values incomplete.") def _parse_encflags(self, flags): """Parse encoder flags. Returns a tuple containing lists of encoder flags and nogap data in human readable format. """ encoder_flags, nogap = [], [] if not flags: return encoder_flags, nogap if flags & self.ENCODER_FLAGS['NSPSYTUNE']: encoder_flags.append('--nspsytune') if flags & self.ENCODER_FLAGS['NSSAFEJOINT']: encoder_flags.append('--nssafejoint') NEXT = self.ENCODER_FLAGS['NOGAP_NEXT'] PREV = self.ENCODER_FLAGS['NOGAP_PREV'] if flags & (NEXT | PREV): encoder_flags.append('--nogap') if flags & PREV: nogap.append('before') if flags & NEXT: nogap.append('after') return encoder_flags, nogap def lamevercmp(x, y): """Compare LAME version strings. alpha and beta versions are considered older. Versions with sub minor parts or end with 'r' are considered newer. :param x: The first version to compare. :param y: The second version to compare. :returns: Return negative if xy. """ def cmp(a, b): # This is Python2's built-in `cmp`, which was removed from Python3 # And depends on bool - bool yielding the integer -1, 0, 1 return (a > b) - (a < b) x = x.ljust(5) y = y.ljust(5) if x[:5] == y[:5]: return 0 ret = cmp(x[:4], y[:4]) if ret: return ret xmaj, xmin = x.split('.')[:2] ymaj, ymin = y.split('.')[:2] minparts = ['.'] # lame 3.96.1 added the use of r in the very short version for post releases if (xmaj == '3' and xmin >= '96') or (ymaj == '3' and ymin >= '96'): minparts.append('r') if x[4] in minparts: return 1 if y[4] in minparts: return -1 if x[4] == ' ': return 1 if y[4] == ' ': return -1 return cmp(x[4], y[4]) # MPEG1 MPEG2 MPEG2.5 SAMPLE_FREQ_TABLE = ((44100, 22050, 11025), (48000, 24000, 12000), (32000, 16000, 8000), (None, None, None)) # V1/L1 V1/L2 V1/L3 V2/L1 V2/L2&L3 BIT_RATE_TABLE = ((0, 0, 0, 0, 0), # noqa (32, 32, 32, 32, 8), # noqa (64, 48, 40, 48, 16), # noqa (96, 56, 48, 56, 24), # noqa (128, 64, 56, 64, 32), # noqa (160, 80, 64, 80, 40), # noqa (192, 96, 80, 96, 48), # noqa (224, 112, 96, 112, 56), # noqa (256, 128, 112, 128, 64), # noqa (288, 160, 128, 144, 80), # noqa (320, 192, 160, 160, 96), # noqa (352, 224, 192, 176, 112), # noqa (384, 256, 224, 192, 128), # noqa (416, 320, 256, 224, 144), # noqa (448, 384, 320, 256, 160), # noqa (None, None, None, None, None)) # Rows 1 and 2 (mpeg 2.x) are only used for those versions *and* VBR. # L1 L2 L3 SAMPLES_PER_FRAME_TABLE = ((None, 384, 1152, 1152), # MPEG 1 (None, 384, 1152, 576), # MPEG 2 (None, 384, 1152, 576), # MPEG 2.5 ) # Emphasis constants EMPHASIS_NONE = "None" EMPHASIS_5015 = "50/15 ms" EMPHASIS_CCIT = "CCIT J.17" # Mode constants MODE_STEREO = "Stereo" MODE_JOINT_STEREO = "Joint stereo" MODE_DUAL_CHANNEL_STEREO = "Dual channel stereo" MODE_MONO = "Mono" # Xing flag bits FRAMES_FLAG = 0x0001 BYTES_FLAG = 0x0002 TOC_FLAG = 0x0004 VBR_SCALE_FLAG = 0x0008 def _mp3VersionKey(version): """Map mp3 version float to a data structure index. 1 -> 0, 2 -> 1, 2.5 -> 2 """ if version == 2.5: key = 2 else: key = int(version - 1) if not 0 <= key <= 2: raise ValueError(f"Invalid mp3 version key value: {key}") return key eyeD3-0.9.7/eyed3/plugins/000077500000000000000000000000001432016011500151635ustar00rootroot00000000000000eyeD3-0.9.7/eyed3/plugins/DisplayPattern.ebnf000066400000000000000000000030111432016011500207550ustar00rootroot00000000000000(* ################################################################################ # Copyright (C) 2016 Sebastian Patschorke # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, see . # ################################################################################ parser generation: $ python -m grako --name DisplayPattern \ -o eyed3/plugins/_display_parser.py \ eyed3/plugins/DisplayPattern.ebnf *) start = pattern $ ; pattern = { text | tag | function }* ; tag = tag:( "%" name:string { "," parameters+:(parameter) }* "%" ); function = function:("$" name:string "(" [ parameters+:(parameter) { "," parameters+:(parameter) }* ] ")" ); parameter = [ {" "}* name:string "=" ] [ value:pattern ] ; text = text:?/(\\\\|\\%|\\\$|\\,|\\\(|\\\)|\\=|\\n|\\t|[^\\%$,()])+/? ; string = ?/([^\\%$,()=])+/? ; eyeD3-0.9.7/eyed3/plugins/__init__.py000066400000000000000000000164251432016011500173040ustar00rootroot00000000000000import os import sys import pathlib from eyed3 import core, utils from eyed3.utils.log import getLogger from eyed3.utils import guessMimetype, formatSize from eyed3.utils.console import printMsg, printError, HEADER_COLOR, boldText, Fore _PLUGINS = {} log = getLogger(__name__) def load(name=None, reload=False, paths=None): """Returns the eyed3.plugins.Plugin *class* identified by ``name``. If ``name`` is ``None`` then the full list of plugins is returned. Once a plugin is loaded its class object is cached, and future calls to this function will returned the cached version. Use ``reload=True`` to refresh the cache.""" global _PLUGINS if len(list(_PLUGINS.keys())) and not reload: # Return from the cache if possible try: return _PLUGINS[name] if name else _PLUGINS except KeyError: # It's not in the cache, look again and refresh cash _PLUGINS = {} else: _PLUGINS = {} def _isValidModule(f, d): """Determine if file `f` is a valid module file name.""" # 1) tis a file # 2) does not start with '_', or '.' # 3) avoid the .pyc dup return bool(os.path.isfile(os.path.join(d, f)) and f[0] not in ('_', '.') and f.endswith(".py")) log.debug(f"Extra plugin paths: {paths}") for d in [os.path.dirname(__file__)] + (paths if paths else []): log.debug(f"Searching '{d}' for plugins") if not os.path.isdir(d): continue if d not in sys.path: sys.path.append(d) try: for f in os.listdir(d): if not _isValidModule(f, d): continue mod_name = os.path.splitext(f)[0] try: mod = __import__(mod_name, globals=globals(), locals=locals()) except ImportError as ex: log.verbose(f"Plugin {(f, d)} requires packages that are not installed: {ex}") continue except Exception: log.exception(f"Bad plugin {(f, d)}") continue for attr in [getattr(mod, a) for a in dir(mod)]: if type(attr) == type and issubclass(attr, Plugin): # This is a eyed3.plugins.Plugin PluginClass = attr if (PluginClass not in list(_PLUGINS.values()) and len(PluginClass.NAMES)): log.debug(f"loading plugin '{mod}' from '{d}{os.path.sep}{f}'") # Setting the main name outside the loop to ensure # there is at least one, otherwise a KeyError is # thrown. main_name = PluginClass.NAMES[0] _PLUGINS[main_name] = PluginClass for alias in PluginClass.NAMES[1:]: # Add alternate names _PLUGINS[alias] = PluginClass # If 'plugin' is found return it immediately if name and name in PluginClass.NAMES: return PluginClass finally: if d in sys.path: sys.path.remove(d) log.debug(f"Plugins loaded: {_PLUGINS}") if name: # If a specific plugin was requested and we've not returned yet... return None return _PLUGINS class Plugin(utils.FileHandler): """Base class for all eyeD3 plugins""" # One line about the plugin SUMMARY = "eyeD3 plugin" # Detailed info about the plugin DESCRIPTION = "" # A list of **at least** one name for invoking the plugin, values [1:] are treated as alias NAMES = [] def __init__(self, arg_parser): self.arg_parser = arg_parser self.arg_group = arg_parser.add_argument_group("Plugin options", f"{self.SUMMARY}\n{self.DESCRIPTION}") def start(self, args, config): """Called after command line parsing but before any paths are processed. The ``self.args`` argument (the parsed command line) and ``self.config`` (the user config, if any) is set here.""" self.args = args self.config = config def handleFile(self, f): pass def handleDone(self): """Called after all file/directory processing; before program exit. The return value is passed to sys.exit (None results in 0).""" pass @staticmethod def _getHardRule(width): return "-" * width @staticmethod def _getFileHeader(path, width): path = pathlib.Path(path) file_size = path.stat().st_size path_str = str(path) size_str = formatSize(file_size) size_len = len(size_str) + 5 if len(path_str) + size_len >= width: path_str = "..." + str(path)[-(75 - size_len):] padding_len = width - len(path_str) - size_len return "{path}{color}{padding}[ {size} ]{reset}"\ .format(path=boldText(path_str, c=HEADER_COLOR()), color=HEADER_COLOR(), padding=" " * padding_len, size=size_str, reset=Fore.RESET) class LoaderPlugin(Plugin): """A base class that provides auto loading of audio files""" def __init__(self, arg_parser, cache_files=False, track_images=False): """Constructor. If ``cache_files`` is True (off by default) then each AudioFile is appended to ``_file_cache`` during ``handleFile`` and the list is cleared by ``handleDirectory``.""" super().__init__(arg_parser) self._num_loaded = 0 self._file_cache = [] if cache_files else None self._dir_images = [] if track_images else None self.audio_file = None def handleFile(self, f, *args, **kwargs): """Loads ``f`` and sets ``self.audio_file`` to an instance of :class:`eyed3.core.AudioFile` or ``None`` if an error occurred or the file is not a recognized type. The ``*args`` and ``**kwargs`` are passed to :func:`eyed3.core.load`. """ try: self.audio_file = core.load(f, *args, **kwargs) except NotImplementedError as ex: # Frame decryption, for instance... printError(str(ex)) return if self.audio_file: self._num_loaded += 1 if self._file_cache is not None: self._file_cache.append(self.audio_file) elif self._dir_images is not None: mt = guessMimetype(f) if mt and mt.startswith("image/"): self._dir_images.append(f) def handleDirectory(self, d, _): """Override to make use of ``self._file_cache``. By default the list is cleared, subclasses should consider doing the same otherwise every AudioFile will be cached.""" if self._file_cache is not None: self._file_cache = [] if self._dir_images is not None: self._dir_images = [] def handleDone(self): """If no audio files were loaded this simply prints 'Nothing to do'.""" if self._num_loaded == 0: printMsg("No audio files found.") eyeD3-0.9.7/eyed3/plugins/art.py000066400000000000000000000233621432016011500163310ustar00rootroot00000000000000import io import os import hashlib from pathlib import Path from eyed3.utils import art from eyed3 import log from eyed3.mimetype import guessMimetype from eyed3.plugins import LoaderPlugin from eyed3.core import VARIOUS_ARTISTS from eyed3.id3.frames import ImageFrame from eyed3.utils import makeUniqueFileName from eyed3.utils.console import printMsg, printWarning, cformat, Fore DESCR_FNAME_PREFIX = "filename: " md5_file_cache = {} def _importMessage(missing): return f"Missing dependencies {missing}. Install with `pip install eyeD3[art-plugin]`" try: import PIL # noqa import requests from eyed3.plugins.lastfm import getAlbumArt _PLUGIN_ACTIVE = True _IMPORT_ERROR = None except ImportError as ex: _PLUGIN_ACTIVE = False _IMPORT_ERROR = ex class ArtFile(object): def __init__(self, file_path): self.art_type = art.matchArtFile(file_path) self.file_path = file_path self.id3_art_type = (art.TO_ID3_ART_TYPES[self.art_type][0] if self.art_type else None) self._img_data = None self._mime_type = None @property def image_data(self): if self._img_data: return self._img_data with open(self.file_path, "rb") as f: self._img_data = f.read() return self._img_data @property def mime_type(self): if self._mime_type: return self._mime_type self._mime_type = guessMimetype(self.file_path) return self._mime_type class ArtPlugin(LoaderPlugin): SUMMARY = "Art for albums, artists, etc." DESCRIPTION = "" NAMES = ["art"] def __init__(self, arg_parser): super(ArtPlugin, self).__init__(arg_parser, cache_files=True, track_images=True) self._retval = 0 g = self.arg_group g.add_argument("-F", "--update-files", action="store_true", help="Write art files from tag images.") g.add_argument("-T", "--update-tags", action="store_true", help="Write tag image from art files.") dl_help = "Attempt to download album art if missing." g.add_argument("-D", "--download", action="store_true", help=dl_help) g.add_argument("-v", "--verbose", action="store_true", help="Show detailed information for all art found.") def start(self, args, config): if not _PLUGIN_ACTIVE: err_msg = _importMessage([_IMPORT_ERROR.name]) log.critical(err_msg) raise RuntimeError(err_msg) if args.update_files and args.update_tags: # Not using add_mutually_exclusive_group from argparse because # the options belong to the plugin opts group (self.arg_group) raise StopIteration("The --update-tags and --update-files options " "are mutually exclusive, use only one at a " "time.") super(ArtPlugin, self).start(args, config) def _verbose(self, s): if self.args.verbose: printMsg(s) def handleDirectory(self, d, _): global md5_file_cache md5_file_cache.clear() if not self._file_cache: log.debug(f"{d}: nothing to do.") return try: all_tags = sorted([f.tag for f in self._file_cache if f.tag], key=lambda x: x.file_info.name) # If not deemed an album, move on. if len(set([t.album for t in all_tags])) > 1: log.debug(f"Skipping directory '{d}', non-album.") return printMsg(cformat("\nChecking: ", Fore.BLUE) + d) # File images dir_art = [] for img_file in self._dir_images: img_base = os.path.basename(img_file) art_file = ArtFile(img_file) try: pil_img = pilImage(img_file) except IOError as ex: printWarning(str(ex)) continue if art_file.art_type: self._verbose( f"file {img_base}: {art_file.art_type}\n\t{pilImageDetails(pil_img)}") dir_art.append(art_file) else: self._verbose(f"file {img_base}: unknown (ignored)") if not dir_art: print(cformat("NONE", Fore.RED)) self._retval += 1 else: print(cformat("OK", Fore.GREEN)) # --download handling if not dir_art and self.args.download: tag = all_tags[0] artists = set([t.artist for t in all_tags]) if len(artists) > 1: artist_query = VARIOUS_ARTISTS else: artist_query = tag.album_artist or tag.artist try: url = getAlbumArt(artist_query, tag.album) print("Downloading album art...") resp = requests.get(url) if resp.status_code != 200: raise ValueError() except ValueError: print("Album art download not found") else: img = pilImage(io.BytesIO(resp.content)) cover = Path(d) / "cover.{}".format(img.format.lower()) assert not cover.exists() img.save(str(cover)) print("Save {cover}".format(cover=cover)) # Tag images for tag in all_tags: file_base = os.path.basename(tag.file_info.name) for img in tag.images: try: pil_img = pilImage(img) pil_img_details = pilImageDetails(pil_img) except OSError as ex: printWarning(str(ex)) continue if img.picture_type in art.FROM_ID3_ART_TYPES: img_type = art.FROM_ID3_ART_TYPES[img.picture_type] self._verbose("tag %s: %s (Description: %s)\n\t%s" % (file_base, img_type, img.description, pil_img_details)) if self.args.update_files: assert(not self.args.update_tags) path = os.path.dirname(tag.file_info.name) if img.description.startswith(DESCR_FNAME_PREFIX): # Use filename from Image description fname = img.description[ len(DESCR_FNAME_PREFIX):].strip() fname = os.path.splitext(fname)[0] else: fname = art.FILENAMES[img_type][0].strip("*") fname = img.makeFileName(name=fname) if (md5File(os.path.join(path, fname)) == md5Data(img.image_data)): printMsg("Skipping writing of %s, file " "exists and is exactly the same." % fname) else: img_file = makeUniqueFileName( os.path.join(path, fname), uniq=img.description) printWarning("Writing %s..." % img_file) with open(img_file, "wb") as fp: fp.write(img.image_data) else: self._verbose( "tag %s: unhandled image type %d (ignored)" % (file_base, img.picture_type) ) # Copy file art to tags. if self.args.update_tags: assert(not self.args.update_files) for tag in all_tags: for art_file in dir_art: art_path = os.path.basename(art_file.file_path) printMsg("Copying %s to tag '%s' image" % (art_path, art_file.id3_art_type)) descr = "filename: %s" % os.path.splitext(art_path)[0] tag.images.set(art_file.id3_art_type, art_file.image_data, art_file.mime_type, description=descr) tag.save() finally: # Cleans up... super(ArtPlugin, self).handleDirectory(d, _) def handleDone(self): return self._retval def pilImage(source): from PIL import Image if isinstance(source, ImageFrame): return Image.open(io.BytesIO(source.image_data)) else: return Image.open(source) def pilImageDetails(img): return "[%dx%d %s md5:%s]" % (img.size[0], img.size[1], img.format.lower(), md5Data(img.tobytes())) if img else "" def md5Data(data): md5 = hashlib.md5() md5.update(data) return md5.hexdigest() def md5File(file_name): """Compute md5 hash for contents of ``file_name``.""" global md5_file_cache if file_name in md5_file_cache: return md5_file_cache[file_name] md5 = hashlib.md5() try: with open(file_name, "rb") as f: md5.update(f.read()) md5_file_cache[file_name] = md5.hexdigest() return md5_file_cache[file_name] except IOError: return None eyeD3-0.9.7/eyed3/plugins/classic.py000066400000000000000000001524361432016011500171710ustar00rootroot00000000000000import os import re import dataclasses from functools import partial from argparse import ArgumentTypeError from eyed3.plugins import LoaderPlugin from eyed3 import core, id3, mp3 from eyed3.utils import makeUniqueFileName, b, formatTime from eyed3.utils.console import ( printMsg, printError, printWarning, boldText, getTtySize, ) from eyed3.id3.frames import ImageFrame from eyed3.mimetype import guessMimetype from eyed3.utils.log import getLogger log = getLogger(__name__) FIELD_DELIM = ':' DEFAULT_MAX_PADDING = 64 * 1024 class ClassicPlugin(LoaderPlugin): SUMMARY = "Classic eyeD3 interface for viewing and editing tags." DESCRIPTION = """ All PATH arguments are parsed and displayed. Directory paths are searched recursively. Any editing options (--artist, --title) are applied to each file read. All date options (-Y, --release-year excepted) follow ISO 8601 format. This is ``yyyy-mm-ddThh:mm:ss``. The year is required, and each component thereafter is optional. For example, 2012-03 is valid, 2012--12 is not. """ NAMES = ["classic"] def __init__(self, arg_parser): super(ClassicPlugin, self).__init__(arg_parser) g = self.arg_group def PositiveIntArg(i): i = int(i) if i < 0: raise ArgumentTypeError("positive number required") return i # Common options g.add_argument("-a", "--artist", dest="artist", metavar="STRING", help=ARGS_HELP["--artist"]) g.add_argument("-A", "--album", dest="album", metavar="STRING", help=ARGS_HELP["--album"]) g.add_argument("-b", "--album-artist", dest="album_artist", metavar="STRING", help=ARGS_HELP["--album-artist"]) g.add_argument("-t", "--title", dest="title", metavar="STRING", help=ARGS_HELP["--title"]) g.add_argument("-n", "--track", type=PositiveIntArg, dest="track", metavar="NUM", help=ARGS_HELP["--track"]) g.add_argument("-N", "--track-total", type=PositiveIntArg, dest="track_total", metavar="NUM", help=ARGS_HELP["--track-total"]) g.add_argument("--track-offset", type=int, dest="track_offset", metavar="N", help=ARGS_HELP["--track-offset"]) g.add_argument("--composer", dest="composer", metavar="STRING", help=ARGS_HELP["--composer"]) g.add_argument("--orig-artist", dest="orig_artist", metavar="STRING", help=ARGS_HELP["--orig-artist"]) g.add_argument("-d", "--disc-num", type=PositiveIntArg, dest="disc_num", metavar="NUM", help=ARGS_HELP["--disc-num"]) g.add_argument("-D", "--disc-total", type=PositiveIntArg, dest="disc_total", metavar="NUM", help=ARGS_HELP["--disc-total"]) g.add_argument("-G", "--genre", dest="genre", metavar="GENRE", help=ARGS_HELP["--genre"]) g.add_argument("--non-std-genres", dest="non_std_genres", action="store_true", help=ARGS_HELP["--non-std-genres"]) g.add_argument("-Y", "--release-year", type=PositiveIntArg, dest="release_year", metavar="YEAR", help=ARGS_HELP["--release-year"]) g.add_argument("-c", "--comment", dest="simple_comment", metavar="STRING", help=ARGS_HELP["--comment"]) g.add_argument("--artist-city", metavar="STRING", help="The artist's city of origin. " f"Stored as a user text frame `{core.TXXX_ARTIST_ORIGIN}`") g.add_argument("--artist-state", metavar="STRING", help="The artist's state of origin. " f"Stored as a user text frame `{core.TXXX_ARTIST_ORIGIN}`") g.add_argument("--artist-country", metavar="STRING", help="The artist's country of origin. " f"Stored as a user text frame `{core.TXXX_ARTIST_ORIGIN}`") g.add_argument("--rename", dest="rename_pattern", metavar="PATTERN", help=ARGS_HELP["--rename"]) gid3 = arg_parser.add_argument_group("ID3 options") def _splitArgs(arg, maxsplit=None): NEW_DELIM = "#DELIM#" arg = re.sub(r"\\%s" % FIELD_DELIM, NEW_DELIM, arg) t = tuple(re.sub(NEW_DELIM, FIELD_DELIM, s) for s in arg.split(FIELD_DELIM)) if maxsplit is not None and maxsplit < 2: raise ValueError("Invalid maxsplit value: {}".format(maxsplit)) elif maxsplit and len(t) > maxsplit: t = t[:maxsplit - 1] + (FIELD_DELIM.join(t[maxsplit - 1:]),) assert len(t) <= maxsplit return t def DescLangArg(arg): """DESCRIPTION[:LANG]""" vals = _splitArgs(arg, 2) desc = vals[0] lang = vals[1] if len(vals) > 1 else id3.DEFAULT_LANG return desc, b(lang)[:3] or id3.DEFAULT_LANG def DescTextArg(arg): """DESCRIPTION:TEXT""" vals = _splitArgs(arg, 2) desc = vals[0].strip() text = FIELD_DELIM.join(vals[1:] if len(vals) > 1 else []) return desc or "", text or "" KeyValueArg = DescTextArg def DescUrlArg(arg): desc, url = DescTextArg(arg) return desc, url.encode("latin1") def FidArg(arg): fid = arg.strip().encode("ascii") if not fid: raise ArgumentTypeError("No frame ID") return fid def TextFrameArg(arg): """FID:TEXT""" vals = _splitArgs(arg, 2) fid = vals[0].strip().encode("ascii") if not fid: raise ArgumentTypeError("No frame ID") text = vals[1] if len(vals) > 1 else "" return fid, text def UrlFrameArg(arg): """FID:TEXT""" fid, url = TextFrameArg(arg) return fid, url.encode("latin1") def DateArg(date_str): return core.Date.parse(date_str) if date_str else "" def CommentArg(arg): """ COMMENT[:DESCRIPTION[:LANG] """ vals = _splitArgs(arg, 3) text = vals[0] if not text: raise ArgumentTypeError("text required") desc = vals[1] if len(vals) > 1 else "" lang = vals[2] if len(vals) > 2 else id3.DEFAULT_LANG return text, desc, b(lang)[:3] def LyricsArg(arg): text, desc, lang = CommentArg(arg) try: with open(text, "r") as fp: data = fp.read() except Exception: # noqa: B901 raise ArgumentTypeError("Unable to read file") return data, desc, lang def PlayCountArg(pc): if not pc: raise ArgumentTypeError("value required") increment = False if pc[0] == "+": pc = int(pc[1:]) increment = True else: pc = int(pc) if pc < 0: raise ArgumentTypeError("out of range") return increment, pc def BpmArg(bpm): bpm = int(float(bpm) + 0.5) if bpm <= 0: raise ArgumentTypeError("out of range") return bpm def DirArg(d): if not d or not os.path.isdir(d): raise ArgumentTypeError("invalid directory: %s" % d) return d def ImageArg(s): """PATH:TYPE[:DESCRIPTION] Returns (path, type_id, mime_type, description) """ args = _splitArgs(s, 3) if len(args) < 2: raise ArgumentTypeError("Format is: PATH:TYPE[:DESCRIPTION]") path, type_str = args[:2] desc = args[2] if len(args) > 2 else "" try: type_id = id3.frames.ImageFrame.stringToPicType(type_str) except Exception: # noqa: B901 raise ArgumentTypeError("invalid pic type: {}".format(type_str)) if not path: raise ArgumentTypeError("path required") elif True in [path.startswith(prefix) for prefix in ["http://", "https://"]]: mt = ImageFrame.URL_MIME_TYPE else: if not os.path.isfile(path): raise ArgumentTypeError("file does not exist") mt = guessMimetype(path) if mt is None: raise ArgumentTypeError("Cannot determine mime-type") return path, type_id, mt, desc def ObjectArg(s): """OBJ_PATH:MIME-TYPE[:DESCRIPTION[:FILENAME]], Returns (path, mime_type, description, filename) """ args = _splitArgs(s, 4) if len(args) < 2: raise ArgumentTypeError("too few parts") path = args[0] if path: mt = args[1] desc = args[2] if len(args) > 2 else "" filename = args[3] \ if len(args) > 3 \ else os.path.basename(path) if not os.path.isfile(path): raise ArgumentTypeError("file does not exist") if not mt: raise ArgumentTypeError("mime-type required") else: raise ArgumentTypeError("path required") return (path, mt, desc, filename) def UniqFileIdArg(arg): owner_id, id = KeyValueArg(arg) if not owner_id: raise ArgumentTypeError("owner_id required") id = id.encode("latin1") # don't want to pass unicode if len(id) > 64: raise ArgumentTypeError("id must be <= 64 bytes") return (owner_id, id) def PopularityArg(arg): """EMAIL:RATING[:PLAY_COUNT] Returns (email, rating, play_count) """ args = _splitArgs(arg, 3) if len(args) < 2: raise ArgumentTypeError("Incorrect number of argument components") email = args[0] rating = int(float(args[1])) if rating < 0 or rating > 255: raise ArgumentTypeError("Rating out-of-range") play_count = 0 if len(args) > 2: play_count = int(args[2]) if play_count < 0: raise ArgumentTypeError("Play count out-of-range") return (email, rating, play_count) # Tag versions gid3.add_argument("-1", "--v1", action="store_const", const=id3.ID3_V1, dest="tag_version", default=id3.ID3_ANY_VERSION, help=ARGS_HELP["--v1"]) gid3.add_argument("-2", "--v2", action="store_const", const=id3.ID3_V2, dest="tag_version", default=id3.ID3_ANY_VERSION, help=ARGS_HELP["--v2"]) gid3.add_argument("--to-v1.1", action="store_const", const=id3.ID3_V1_1, dest="convert_version", help=ARGS_HELP["--to-v1.1"]) gid3.add_argument("--to-v2.3", action="store_const", const=id3.ID3_V2_3, dest="convert_version", help=ARGS_HELP["--to-v2.3"]) gid3.add_argument("--to-v2.4", action="store_const", const=id3.ID3_V2_4, dest="convert_version", help=ARGS_HELP["--to-v2.4"]) # Dates gid3.add_argument("--release-date", type=DateArg, dest="release_date", metavar="DATE", help=ARGS_HELP["--release-date"]) gid3.add_argument("--orig-release-date", type=DateArg, dest="orig_release_date", metavar="DATE", help=ARGS_HELP["--orig-release-date"]) gid3.add_argument("--recording-date", type=DateArg, dest="recording_date", metavar="DATE", help=ARGS_HELP["--recording-date"]) gid3.add_argument("--encoding-date", type=DateArg, dest="encoding_date", metavar="DATE", help=ARGS_HELP["--encoding-date"]) gid3.add_argument("--tagging-date", type=DateArg, dest="tagging_date", metavar="DATE", help=ARGS_HELP["--tagging-date"]) # Misc gid3.add_argument("--publisher", action="store", dest="publisher", metavar="STRING", help=ARGS_HELP["--publisher"]) gid3.add_argument("--play-count", type=PlayCountArg, dest="play_count", metavar="<+>N", default=None, help=ARGS_HELP["--play-count"]) gid3.add_argument("--bpm", type=BpmArg, dest="bpm", metavar="N", default=None, help=ARGS_HELP["--bpm"]) gid3.add_argument("--unique-file-id", action="append", type=UniqFileIdArg, dest="unique_file_ids", metavar="OWNER_ID:ID", default=[], help=ARGS_HELP["--unique-file-id"]) # Comments gid3.add_argument("--add-comment", action="append", dest="comments", metavar="COMMENT[:DESCRIPTION[:LANG]]", default=[], type=CommentArg, help=ARGS_HELP["--add-comment"]) gid3.add_argument("--remove-comment", action="append", type=DescLangArg, dest="remove_comment", default=[], metavar="DESCRIPTION[:LANG]", help=ARGS_HELP["--remove-comment"]) gid3.add_argument("--remove-all-comments", action="store_true", dest="remove_all_comments", help=ARGS_HELP["--remove-all-comments"]) gid3.add_argument("--remove-all-unknown", action="store_true", dest="remove_all_unknown", help=ARGS_HELP["--remove-all-unknown"]) gid3.add_argument("--add-lyrics", action="append", type=LyricsArg, dest="lyrics", default=[], metavar="LYRICS_FILE[:DESCRIPTION[:LANG]]", help=ARGS_HELP["--add-lyrics"]) gid3.add_argument("--remove-lyrics", action="append", type=DescLangArg, dest="remove_lyrics", default=[], metavar="DESCRIPTION[:LANG]", help=ARGS_HELP["--remove-lyrics"]) gid3.add_argument("--remove-all-lyrics", action="store_true", dest="remove_all_lyrics", help=ARGS_HELP["--remove-all-lyrics"]) gid3.add_argument("--text-frame", action="append", type=TextFrameArg, dest="text_frames", metavar="FID:TEXT", default=[], help=ARGS_HELP["--text-frame"]) gid3.add_argument("--user-text-frame", action="append", type=DescTextArg, dest="user_text_frames", metavar="DESC:TEXT", default=[], help=ARGS_HELP["--user-text-frame"]) gid3.add_argument("--url-frame", action="append", type=UrlFrameArg, dest="url_frames", metavar="FID:URL", default=[], help=ARGS_HELP["--url-frame"]) gid3.add_argument("--user-url-frame", action="append", type=DescUrlArg, dest="user_url_frames", metavar="DESCRIPTION:URL", default=[], help=ARGS_HELP["--user-url-frame"]) gid3.add_argument("--add-image", action="append", type=ImageArg, dest="images", metavar="IMG_PATH:TYPE[:DESCRIPTION]", default=[], help=ARGS_HELP["--add-image"]) gid3.add_argument("--remove-image", action="append", dest="remove_image", default=[], metavar="DESCRIPTION", help=ARGS_HELP["--remove-image"]) gid3.add_argument("--remove-all-images", action="store_true", dest="remove_all_images", help=ARGS_HELP["--remove-all-images"]) gid3.add_argument("--write-images", dest="write_images_dir", metavar="DIR", type=DirArg, help=ARGS_HELP["--write-images"]) gid3.add_argument("--add-object", action="append", type=ObjectArg, dest="objects", default=[], metavar="OBJ_PATH:MIME-TYPE[:DESCRIPTION[:FILENAME]]", help=ARGS_HELP["--add-object"]) gid3.add_argument("--remove-object", action="append", dest="remove_object", default=[], metavar="DESCRIPTION", help=ARGS_HELP["--remove-object"]) gid3.add_argument("--write-objects", action="store", dest="write_objects_dir", metavar="DIR", default=None, help=ARGS_HELP["--write-objects"]) gid3.add_argument("--remove-all-objects", action="store_true", dest="remove_all_objects", help=ARGS_HELP["--remove-all-objects"]) gid3.add_argument("--add-popularity", action="append", type=PopularityArg, dest="popularities", default=[], metavar="EMAIL:RATING[:PLAY_COUNT]", help=ARGS_HELP["--add-popularity"]) gid3.add_argument("--remove-popularity", action="append", type=str, dest="remove_popularity", default=[], metavar="EMAIL", help=ARGS_HELP["--remove-popularity"]) gid3.add_argument("--remove-v1", action="store_true", dest="remove_v1", default=False, help=ARGS_HELP["--remove-v1"]) gid3.add_argument("--remove-v2", action="store_true", dest="remove_v2", default=False, help=ARGS_HELP["--remove-v2"]) gid3.add_argument("--remove-all", action="store_true", default=False, dest="remove_all", help=ARGS_HELP["--remove-all"]) gid3.add_argument("--remove-frame", action="append", default=[], dest="remove_fids", metavar="FID", type=FidArg, help=ARGS_HELP["--remove-frame"]) # 'True' means 'apply default max_padding, but only if saving anyhow' gid3.add_argument("--max-padding", type=int, dest="max_padding", default=True, metavar="NUM_BYTES", help=ARGS_HELP["--max-padding"]) gid3.add_argument("--no-max-padding", dest="max_padding", action="store_const", const=None, help=ARGS_HELP["--no-max-padding"]) _encodings = ["latin1", "utf8", "utf16", "utf16-be"] gid3.add_argument("--encoding", dest="text_encoding", default=None, choices=_encodings, metavar='|'.join(_encodings), help=ARGS_HELP["--encoding"]) # Misc options gid4 = arg_parser.add_argument_group("Misc options") gid4.add_argument("--force-update", action="store_true", default=False, dest="force_update", help=ARGS_HELP["--force-update"]) gid4.add_argument("-v", "--verbose", action="store_true", dest="verbose", help=ARGS_HELP["--verbose"]) gid4.add_argument("--preserve-file-times", action="store_true", dest="preserve_file_time", help=ARGS_HELP["--preserve-file-times"]) def handleFile(self, f): parse_version = self.args.tag_version try: super().handleFile(f, tag_version=parse_version) except id3.TagException as tag_ex: printError(str(tag_ex)) return if not self.audio_file: return self.terminal_width = getTtySize()[1] self.printHeader(f) if self.audio_file.tag and self.handleRemoves(self.audio_file.tag): # Reload after removal super(ClassicPlugin, self).handleFile(f, tag_version=parse_version) if not self.audio_file: return new_tag = False if not self.audio_file.tag: self.audio_file.initTag(version=parse_version) new_tag = True try: save_tag = (self.handleEdits(self.audio_file.tag) or self.handlePadding(self.audio_file.tag) or self.args.force_update or self.args.convert_version) except ValueError as ex: printError(str(ex)) return self.printAudioInfo(self.audio_file.info) if not save_tag and new_tag: printError(f"No ID3 {id3.versionToString(self.args.tag_version)} tag found!") return self.printTag(self.audio_file.tag) if self.args.write_images_dir: for img in self.audio_file.tag.images: if img.mime_type not in ImageFrame.URL_MIME_TYPE_VALUES: img_path = "%s%s" % (self.args.write_images_dir, os.path.sep) if not os.path.isdir(img_path): raise IOError("Directory does not exist: %s" % img_path) img_file = makeUniqueFileName( os.path.join(img_path, img.makeFileName())) printWarning("Writing %s..." % img_file) with open(img_file, "wb") as fp: fp.write(img.image_data) if save_tag: # Use current tag version unless a convert was supplied version = (self.args.convert_version or self.audio_file.tag.version) printWarning("Writing ID3 version %s" % id3.versionToString(version)) # DEFAULT_MAX_PADDING is not set up as argument default, # because we don't want to rewrite the file if the user # did not trigger that explicitly: max_padding = self.args.max_padding if max_padding is True: max_padding = DEFAULT_MAX_PADDING self.audio_file.tag.save( version=version, encoding=self.args.text_encoding, backup=self.args.backup, preserve_file_time=self.args.preserve_file_time, max_padding=max_padding) if self.args.rename_pattern: # Handle file renaming. from eyed3.id3.tag import TagTemplate template = TagTemplate(self.args.rename_pattern) name = template.substitute(self.audio_file.tag, zeropad=True) orig = self.audio_file.path try: self.audio_file.rename(name) printWarning(f"Renamed '{orig}' to '{self.audio_file.path}'") except IOError as ex: printError(str(ex)) printMsg(self._getHardRule(self.terminal_width)) def printHeader(self, file_path): printMsg(self._getFileHeader(file_path, self.terminal_width)) printMsg(self._getHardRule(self.terminal_width)) def printAudioInfo(self, info): if isinstance(info, mp3.Mp3AudioInfo): printMsg(boldText("Time: ") + "%s\tMPEG%d, Layer %s\t[ %s @ %s Hz - %s ]" % (formatTime(info.time_secs), info.mp3_header.version, "I" * info.mp3_header.layer, info.bit_rate_str, info.mp3_header.sample_freq, info.mp3_header.mode)) printMsg(self._getHardRule(self.terminal_width)) @staticmethod def _getDefaultNameForObject(obj_frame, suffix=""): if obj_frame.filename: name_str = obj_frame.filename else: name_str = obj_frame.description name_str += ".%s" % obj_frame.mime_type.split("/")[1] if suffix: name_str += suffix return name_str def printTag(self, tag): if isinstance(tag, id3.Tag): if self.args.quiet: printMsg(f"ID3 {id3.versionToString(tag.version)}: {len(tag.frame_set)} frames") return printMsg(f"ID3 {id3.versionToString(tag.version)}:") artist = tag.artist if tag.artist else "" title = tag.title if tag.title else "" album = tag.album if tag.album else "" printMsg("%s: %s" % (boldText("title"), title)) printMsg("%s: %s" % (boldText("artist"), artist)) printMsg("%s: %s" % (boldText("album"), album)) if tag.album_artist: printMsg("%s: %s" % (boldText("album artist"), tag.album_artist)) if tag.composer: printMsg("%s: %s" % (boldText("composer"), tag.composer)) if tag.original_artist: printMsg("%s: %s" % (boldText("original artist"), tag.original_artist)) for date, date_label in [ (tag.release_date, "release date"), (tag.original_release_date, "original release date"), (tag.recording_date, "recording date"), (tag.encoding_date, "encoding date"), (tag.tagging_date, "tagging date"), ]: if date: printMsg("%s: %s" % (boldText(date_label), str(date))) track_str = "" (track_num, track_total) = tag.track_num if track_num is not None: track_str = str(track_num) if track_total: track_str += "/%d" % track_total genre = tag.genre if not self.args.non_std_genres else tag.non_std_genre genre_str = f"{boldText('genre')}: {genre.name} (id {genre.id})" if genre else "" printMsg(f"{boldText('track')}: {track_str}\t\t{genre_str}") (num, total) = tag.disc_num if num is not None: disc_str = str(num) if total: disc_str += "/%d" % total printMsg("%s: %s" % (boldText("disc"), disc_str)) # PCNT play_count = tag.play_count if tag.play_count is not None: printMsg("%s %d" % (boldText("Play Count:"), play_count)) # POPM for popm in tag.popularities: printMsg("%s [email: %s] [rating: %d] [play count: %d]" % (boldText("Popularity:"), popm.email, popm.rating, popm.count)) # TBPM bpm = tag.bpm if bpm is not None: printMsg("%s %d" % (boldText("BPM:"), bpm)) # TPUB pub = tag.publisher if pub is not None: printMsg("%s %s" % (boldText("Publisher/label:"), pub)) # UFID for ufid in tag.unique_file_ids: printMsg("%s [%s] : %s" % (boldText("Unique File ID:"), ufid.owner_id, ufid.uniq_id.decode("unicode_escape"))) # COMM for c in tag.comments: printMsg("%s: [Description: %s] [Lang: %s]\n%s" % (boldText("Comment"), c.description or "", c.lang.decode("ascii") or "", c.text or "")) # USLT for l in tag.lyrics: printMsg("%s: [Description: %s] [Lang: %s]\n%s" % (boldText("Lyrics"), l.description or "", l.lang.decode("ascii") or "", l.text)) # TXXX for f in tag.user_text_frames: printMsg("%s: [Description: %s]\n%s" % (boldText("UserTextFrame"), f.description, f.text)) # URL frames for desc, url in (("Artist URL", tag.artist_url), ("Audio source URL", tag.audio_source_url), ("Audio file URL", tag.audio_file_url), ("Internet radio URL", tag.internet_radio_url), ("Commercial URL", tag.commercial_url), ("Payment URL", tag.payment_url), ("Publisher URL", tag.publisher_url), ("Copyright URL", tag.copyright_url), ): if url: printMsg("%s: %s" % (boldText(desc), url)) # user url frames for u in tag.user_url_frames: printMsg("%s [Description: %s]: %s" % (u.id, u.description, u.url)) # APIC for img in tag.images: if img.mime_type not in ImageFrame.URL_MIME_TYPE_VALUES: printMsg("%s: [Size: %d bytes] [Type: %s]" % (boldText(img.picTypeToString(img.picture_type) + " Image"), len(img.image_data), img.mime_type)) printMsg("Description: %s" % img.description) printMsg("") else: printMsg("%s: [Type: %s] [URL: %s]" % (boldText(img.picTypeToString(img.picture_type) + " Image"), img.mime_type, img.image_url)) printMsg("Description: %s" % img.description) printMsg("") # GOBJ for obj in tag.objects: printMsg("%s: [Size: %d bytes] [Type: %s]" % (boldText("GEOB"), len(obj.object_data), obj.mime_type)) printMsg("Description: %s" % obj.description) printMsg("Filename: %s" % obj.filename) printMsg("\n") if self.args.write_objects_dir: obj_path = "%s%s" % (self.args.write_objects_dir, os.sep) if not os.path.isdir(obj_path): raise IOError("Directory does not exist: %s" % obj_path) obj_file = self._getDefaultNameForObject(obj) count = 1 while os.path.exists(os.path.join(obj_path, obj_file)): obj_file = self._getDefaultNameForObject(obj, str(count)) count += 1 printWarning("Writing %s..." % os.path.join(obj_path, obj_file)) with open(os.path.join(obj_path, obj_file), "wb") as fp: fp.write(obj.object_data) # PRIV for p in tag.privates: printMsg("%s: [Data: %d bytes]" % (boldText("PRIV"), len(p.data))) printMsg("Owner Id: %s" % p.owner_id.decode("ascii")) # MCDI if tag.cd_id: printMsg("\n%s: [Data: %d bytes]" % (boldText("MCDI"), len(tag.cd_id))) # USER if tag.terms_of_use: printMsg("\nTerms of Use (%s): %s" % (boldText("USER"), tag.terms_of_use)) # --verbose if self.args.verbose: printMsg(self._getHardRule(self.terminal_width)) printMsg("%d ID3 Frames:" % len(tag.frame_set)) for fid in tag.frame_set: frames = tag.frame_set[fid] num_frames = len(frames) count = " x %d" % num_frames if num_frames > 1 else "" if not tag.isV1(): total_bytes = sum( tuple(frame.header.data_size + frame.header.size for frame in frames if frame.header)) else: total_bytes = 30 if total_bytes: printMsg("%s%s (%d bytes)" % (fid.decode("ascii"), count, total_bytes)) printMsg("%d bytes unused (padding)" % (tag.file_info.tag_padding_size, )) else: raise TypeError("Unknown tag type: " + str(type(tag))) def handleRemoves(self, tag): remove_version = 0 status = False rm_str = "" if self.args.remove_all: remove_version = id3.ID3_ANY_VERSION rm_str = "v1.x and/or v2.x" elif self.args.remove_v1: remove_version = id3.ID3_V1 rm_str = "v1.x" elif self.args.remove_v2: remove_version = id3.ID3_V2 rm_str = "v2.x" if remove_version: status = id3.Tag.remove(tag.file_info.name, remove_version, preserve_file_time=self.args.preserve_file_time) printWarning(f"Removing ID3 {rm_str} tag: {'SUCCESS' if status else 'FAIL'}") return status def handlePadding(self, tag): max_padding = self.args.max_padding if max_padding is None or max_padding is True: return False padding = tag.file_info.tag_padding_size needs_change = padding > max_padding return needs_change def handleEdits(self, tag): retval = False # --remove-all-*, Handling removes first means later options are still # applied for what, arg, fid in (("comments", self.args.remove_all_comments, id3.frames.COMMENT_FID), ("lyrics", self.args.remove_all_lyrics, id3.frames.LYRICS_FID), ("images", self.args.remove_all_images, id3.frames.IMAGE_FID), ("objects", self.args.remove_all_objects, id3.frames.OBJECT_FID), ): if arg and tag.frame_set[fid]: printWarning("Removing all %s..." % what) del tag.frame_set[fid] retval = True if self.args.remove_all_unknown: for fid in tag.unknown_frame_ids: printWarning("Removing unknown (%s)..." % fid) del tag.frame_set[fid] retval = True # --artist, --title, etc. All common/simple text frames. for (what, setFunc) in ( ("artist", partial(tag._setArtist, self.args.artist)), ("album", partial(tag._setAlbum, self.args.album)), ("album artist", partial(tag._setAlbumArtist, self.args.album_artist)), ("title", partial(tag._setTitle, self.args.title)), ("genre", partial(tag._setGenre, self.args.genre, id3_std=not self.args.non_std_genres)), ("release date", partial(tag._setReleaseDate, self.args.release_date)), ("original release date", partial(tag._setOrigReleaseDate, self.args.orig_release_date)), ("recording date", partial(tag._setRecordingDate, self.args.recording_date)), ("encoding date", partial(tag._setEncodingDate, self.args.encoding_date)), ("tagging date", partial(tag._setTaggingDate, self.args.tagging_date)), ("beats per minute", partial(tag._setBpm, self.args.bpm)), ("publisher", partial(tag._setPublisher, self.args.publisher)), ("composer", partial(tag._setComposer, self.args.composer)), ("orig-artist", partial(tag._setOrigArtist, self.args.orig_artist)), ): if setFunc.args[0] is not None: printWarning("Setting %s: %s" % (what, setFunc.args[0])) setFunc() retval = True def _checkNumberedArgTuples(curr, new): n = None if new not in [(None, None), curr]: n = [None] * 2 for i in (0, 1): if new[i] == 0: n[i] = None else: n[i] = new[i] or curr[i] n = tuple(n) # Returning None means do nothing, (None, None) would clear both vals return n # --artist-{city,state,country} origin = core.ArtistOrigin(self.args.artist_city, self.args.artist_state, self.args.artist_country) if origin or (dataclasses.astuple(origin) != (None, None, None) and tag.artist_origin): printWarning(f"Setting artist origin: {origin}") tag.artist_origin = origin retval = True # --track, --track-total track_info = _checkNumberedArgTuples(tag.track_num, (self.args.track, self.args.track_total)) if track_info is not None: printWarning("Setting track info: %s" % str(track_info)) tag.track_num = track_info retval = True # --track-offset if self.args.track_offset: offset = self.args.track_offset tag.track_num = (tag.track_num[0] + offset, tag.track_num[1]) printWarning("%s track info by %d: %d" % ("Incrementing" if offset > 0 else "Decrementing", offset, tag.track_num[0])) retval = True # --disc-num, --disc-total disc_info = _checkNumberedArgTuples(tag.disc_num, (self.args.disc_num, self.args.disc_total)) if disc_info is not None: printWarning("Setting disc info: %s" % str(disc_info)) tag.disc_num = disc_info retval = True # -Y, --release-year if self.args.release_year is not None: # empty string means clean, None means not given year = self.args.release_year printWarning(f"Setting release year: {year}") tag.release_date = int(year) if year else None retval = True # -c , simple comment if self.args.simple_comment: # Just add it as if it came in --add-comment self.args.comments.append((self.args.simple_comment, "", id3.DEFAULT_LANG)) # --remove-comment, remove-lyrics, --remove-image, --remove-object for what, arg, accessor in (("comment", self.args.remove_comment, tag.comments), ("lyrics", self.args.remove_lyrics, tag.lyrics), ("image", self.args.remove_image, tag.images), ("object", self.args.remove_object, tag.objects), ): for vals in arg: if type(vals) is str: frame = accessor.remove(vals) else: frame = accessor.remove(*vals) if frame: printWarning("Removed %s %s" % (what, str(vals))) retval = True else: printError("Removing %s failed, %s not found" % (what, str(vals))) # --add-comment, --add-lyrics for what, arg, accessor in (("comment", self.args.comments, tag.comments), ("lyrics", self.args.lyrics, tag.lyrics), ): for text, desc, lang in arg: printWarning("Setting %s: %s/%s" % (what, desc, str(lang, "ascii"))) accessor.set(text, desc, b(lang)) retval = True # --play-count playcount_arg = self.args.play_count if playcount_arg: increment, pc = playcount_arg if increment: printWarning("Increment play count by %d" % pc) tag.play_count += pc else: printWarning("Setting play count to %d" % pc) tag.play_count = pc retval = True # --add-popularity for email, rating, play_count in self.args.popularities: tag.popularities.set(email.encode("latin1"), rating, play_count) retval = True # --remove-popularity for email in self.args.remove_popularity: popm = tag.popularities.remove(email.encode("latin1")) if popm: retval = True # --text-frame, --url-frame for what, arg, setter in ( ("text frame", self.args.text_frames, tag.setTextFrame), ("url frame", self.args.url_frames, tag._setUrlFrame), ): for fid, text in arg: if text: printWarning("Setting %s %s to '%s'" % (fid, what, text)) else: printWarning("Removing %s %s" % (fid, what)) setter(fid, text) retval = True # --user-text-frame, --user-url-frame for what, arg, accessor in ( ("user text frame", self.args.user_text_frames, tag.user_text_frames), ("user url frame", self.args.user_url_frames, tag.user_url_frames), ): for desc, text in arg: if text: printWarning(f"Setting '{desc}' {what} to '{text}'") accessor.set(text, desc) else: printWarning(f"Removing '{desc}' {what}") accessor.remove(desc) retval = True # --add-image for img_path, img_type, img_mt, img_desc in self.args.images: assert img_path printWarning("Adding image %s" % img_path) if img_mt not in ImageFrame.URL_MIME_TYPE_VALUES: with open(img_path, "rb") as img_fp: tag.images.set(img_type, img_fp.read(), img_mt, img_desc) else: tag.images.set(img_type, None, None, img_desc, img_url=img_path) retval = True # --add-object for obj_path, obj_mt, obj_desc, obj_fname in self.args.objects or []: assert obj_path printWarning("Adding object %s" % obj_path) with open(obj_path, "rb") as obj_fp: tag.objects.set(obj_fp.read(), obj_mt, obj_desc, obj_fname) retval = True # --unique-file-id for arg in self.args.unique_file_ids: owner_id, id = arg if not id: if tag.unique_file_ids.remove(owner_id): printWarning("Removed unique file ID '%s'" % owner_id) retval = True else: printWarning("Unique file ID '%s' not found" % owner_id) else: tag.unique_file_ids.set(id, owner_id.encode("latin1")) printWarning("Setting unique file ID '%s' to %s" % (owner_id, id)) retval = True # --remove-frame for fid in self.args.remove_fids: assert(isinstance(fid, bytes)) if fid in tag.frame_set: del tag.frame_set[fid] retval = True return retval def _getTemplateKeys(): keys = list(id3.TagTemplate("")._makeMapping(None, False).keys()) keys.sort() return ", ".join(["$%s" % v for v in keys]) ARGS_HELP = { "--artist": "Set the artist name.", "--album": "Set the album name.", "--album-artist": "Set the album artist name. '%s', for example. " "Another example is collaborations when the " "track artist might be 'Eminem featuring Proof' " "the album artist would be 'Eminem'." % core.VARIOUS_ARTISTS, "--title": "Set the track title.", "--track": "Set the track number. Use 0 to clear.", "--track-total": "Set total number of tracks. Use 0 to clear.", "--disc-num": "Set the disc number. Use 0 to clear.", "--disc-total": "Set total number of discs in set. Use 0 to clear.", "--genre": "Set the genre. If the argument is a standard ID3 genre " "name or number both will be set. Otherwise, any string " "can be used. Run 'eyeD3 --plugin=genres' for a list of " "standard ID3 genre names/ids.", "--non-std-genres": "Disables certain ID3 genre standards, such as the " "mapping of numeric value to genre names. For example, " "genre=1 is taken literally, not mapped to 'Classic Rock'.", "--release-year": "Set the year the track was released. Use the date " "options for more precise values or dates other " "than release.", "--v1": "Only read and write ID3 v1.x tags. By default, v1.x tags are " "only read or written if there is not a v2 tag in the file.", "--v2": "Only read/write ID3 v2.x tags. This is the default unless " "the file only contains a v1 tag.", "--to-v1.1": "Convert the file's tag to ID3 v1.1 (Or 1.0 if there is " "no track number)", "--to-v2.3": "Convert the file's tag to ID3 v2.3", "--to-v2.4": "Convert the file's tag to ID3 v2.4", "--release-date": "Set the date the track/album was released", "--orig-release-date": "Set the original date the track/album was " "released", "--recording-date": "Set the date the track/album was recorded", "--encoding-date": "Set the date the file was encoded", "--tagging-date": "Set the date the file was tagged", "--comment": "Set a comment. In ID3 tags this is the comment with " "an empty description. See --add-comment to add multiple " "comment frames.", "--add-comment": "Add or replace a comment. There may be more than one comment in a " "tag, as long as the DESCRIPTION and LANG values are unique. The " "default DESCRIPTION is '' and the default language code is '%s'." % str(id3.DEFAULT_LANG, "ascii"), "--remove-comment": "Remove comment matching DESCRIPTION and LANG. " "The default language code is '%s'." % str(id3.DEFAULT_LANG, "ascii"), "--remove-all-comments": "Remove all comments from the tag.", "--remove-all-unknown": "Remove all unknown frames from the tag.", "--add-lyrics": "Add or replace a lyrics. There may be more than one set of lyrics " "in a tag, as long as the DESCRIPTION and LANG values are unique. " "The default DESCRIPTION is '' and the default language code is " "'%s'." % str(id3.DEFAULT_LANG, "ascii"), "--remove-lyrics": "Remove lyrics matching DESCRIPTION and LANG. " "The default language code is '%s'." % str(id3.DEFAULT_LANG, "ascii"), "--remove-all-lyrics": "Remove all lyrics from the tag.", "--publisher": "Set the publisher/label name", "--play-count": "Set the number of times played counter. If the " "argument value begins with '+' the tag's play count " "is incremented by N, otherwise the value is set to " "exactly N.", "--bpm": "Set the beats per minute value.", "--text-frame": "Set the value of a text frame. To remove the " "frame, specify an empty value. For example, " "--text-frame='TDRC:'", "--user-text-frame": "Set the value of a user text frame (i.e., TXXX). " "To remove the frame, specify an empty value. " "e.g., --user-text-frame='SomeDesc:'", "--url-frame": "Set the value of a URL frame. To remove the frame, " "specify an empty value. e.g., --url-frame='WCOM:'", "--user-url-frame": "Set the value of a user URL frame (i.e., WXXX). " "To remove the frame, specify an empty value. " "e.g., --user-url-frame='SomeDesc:'", "--add-image": "Add or replace an image. There may be more than one " "image in a tag, as long as the DESCRIPTION values are " "unique. The default DESCRIPTION is ''. If PATH begins " "with 'http[s]://' then it is interpreted as a URL " "instead of a file containing image data. The TYPE must " "be one of the following: %s." % (", ".join([ImageFrame.picTypeToString(t) for t in range(ImageFrame.MIN_TYPE, ImageFrame.MAX_TYPE + 1)]), ), "--remove-image": "Remove image matching DESCRIPTION.", "--remove-all-images": "Remove all images from the tag", "--write-images": "Causes all attached images (APIC frames) to be " "written to the specified directory.", "--add-object": "Add or replace an object. There may be more than one " "object in a tag, as long as the DESCRIPTION values " "are unique. The default DESCRIPTION is ''.", "--remove-object": "Remove object matching DESCRIPTION.", "--remove-all-objects": "Remove all objects from the tag", "--write-objects": "Causes all attached objects (GEOB frames) to be " "written to the specified directory.", "--add-popularity": "Adds a pupularity metric. There may be multiples " "popularity values, but each must have a unique " "email address component. The rating is a number " "between 0 (worst) and 255 (best). The play count " "is optional, and defaults to 0, since there is " "already a dedicated play count frame.", "--remove-popularity": "Removes the popularity frame with the " "specified email key.", "--remove-v1": "Remove ID3 v1.x tag.", "--remove-v2": "Remove ID3 v2.x tag.", "--remove-all": "Remove ID3 v1.x and v2.x tags.", "--remove-frame": "Remove all frames with the given ID. This option " "may be specified multiple times.", "--max-padding": "Shrink file if tag padding (unused space) exceeds " "the given number of bytes. " "(Useful e.g. after removal of large cover art.) " "Default is 64 KiB, file will be rewritten with " "default padding (1 KiB) or max padding, whichever " "is smaller.", "--no-max-padding": "Disable --max-padding altogether.", "--force-update": "Rewrite the tag despite there being no edit " "options.", "--verbose": "Show all available tag data", "--unique-file-id": "Add a unique file ID frame. If the ID arg is " "empty the frame is removed. An OWNER_ID is " "required. The ID may be no more than 64 bytes.", "--encoding": "Set the encoding that is used for all text frames. " "This option is only applied if the tag is updated " "as the result of an edit option (e.g. --artist, " "--title, etc.) or --force-update is specified.", "--rename": "Rename file (the extension is not affected) " "based on data in the tag using substitution " "variables: " + _getTemplateKeys(), "--preserve-file-times": "When writing, do not update file " "modification times.", "--track-offset": "Increment/decrement the track number by [-]N. " "This option is applied after --track=N is set.", "--composer": "Set the composer's name.", "--orig-artist": "Set the orignal artist's name. For example, a cover song can include " "the orignal author of the track.", } eyeD3-0.9.7/eyed3/plugins/extract.py000066400000000000000000000043341432016011500172130ustar00rootroot00000000000000import sys import binascii from pathlib import Path import eyed3.id3 import eyed3.plugins from eyed3.utils.log import getLogger log = getLogger(__name__) class ExtractPlugin(eyed3.plugins.LoaderPlugin): NAMES = ["extract"] SUMMARY = "Extract tags from audio files." def __init__(self, arg_parser): super().__init__(arg_parser, cache_files=True, track_images=False) self.arg_group.add_argument("-o", "--output-file", help="The the tag is written to this file in native format.") self.arg_group.add_argument("-H", "--hex", action="store_true", help="Output hexadecimal format.") self.arg_group.add_argument("--strip-padding", action="store_true", help="Exclude tag padding, if any.") def handleFile(self, f, *args, **kwargs): super().handleFile(f) if self.audio_file is None or self.audio_file.tag is None: return tag = self.audio_file.tag if not isinstance(tag, eyed3.id3.Tag): print("Only ID3 tags can be extracted currently.", file=sys.stderr) return 1 with open(tag.file_info.name, "rb") as tag_file: if tag.version[0] != 1: # info.tag_size includes padding. tag_data = tag_file.read(tag.file_info.tag_size) if self.args.strip_padding and tag.file_info.tag_padding_size: # --strip-padding tag_data = tag_data[:-tag.file_info.tag_padding_size] else: # ID3 v1.x tag_data = tag_file.read()[-128:] if self.args.output_file: # --output-file if Path(tag.file_info.name).resolve() == Path(self.args.output_file).resolve(): print("Input file overwriting not allowed, choose a different -o/--output-file", file=sys.stderr) return 1 with open(self.args.output_file, "wb") as out_file: out_file.write(tag_data) else: if self.args.hex: # --hex tag_data = str(binascii.hexlify(tag_data), "ascii") print(tag_data) eyeD3-0.9.7/eyed3/plugins/fixup.py000066400000000000000000000643201432016011500166750ustar00rootroot00000000000000import os from collections import defaultdict from eyed3.id3 import ID3_V2_4 from eyed3.id3.tag import TagTemplate from eyed3.plugins import LoaderPlugin from eyed3.utils import art from eyed3.utils.prompt import prompt from eyed3.utils.console import printMsg, Style, Fore from eyed3 import core from eyed3.core import (ALBUM_TYPE_IDS, TXXX_ALBUM_TYPE, EP_MAX_SIZE_HINT, LP_TYPE, EP_TYPE, COMP_TYPE, VARIOUS_TYPE, DEMO_TYPE, LIVE_TYPE, SINGLE_TYPE, VARIOUS_ARTISTS) NORMAL_FNAME_FORMAT = "${artist} - ${track:num} - ${title}" VARIOUS_FNAME_FORMAT = "${track:num} - ${artist} - ${title}" SINGLE_FNAME_FORMAT = "${artist} - ${title}" NORMAL_DNAME_FORMAT = "${best_date:prefer_release} - ${album}" LIVE_DNAME_FORMAT = "${best_date:prefer_recording} - ${album}" def _printChecking(msg, end='\n'): print(Style.BRIGHT + Fore.GREEN + "Checking" + Style.RESET_ALL + " %s" % msg, end=end) def _fixCase(s): if s: fixed_values = [] for word in s.split(): fixed_values.append(word.capitalize()) return " ".join(fixed_values) else: return s def dirDate(d): s = str(d) if "T" in s: s = s.split("T")[0] return s.replace('-', '.') class FixupPlugin(LoaderPlugin): NAMES = ["fixup"] SUMMARY = "Performs various checks and fixes to directories of audio files." DESCRIPTION = """ Operates on directories at a time, fixing each as a unit (album, compilation, live set, etc.). All of these should have common dates, for example but other characteristics may vary. The ``--type`` should be used whenever possible, ``lp`` is the default. The following test and fixes always apply: 1. Every file will be given an ID3 tag if one is missing. 2. Set ID3 v2.4. 3. Set a consistent album name for all files in the directory. 4. Set a consistent artist name for all files, unless the type is ``various`` in which case the artist may vary (but must exist). 5. Ensure each file has a title. 6. Ensure each file has a track # and track total. 7. Ensure all files have a release and original release date, unless the type is ``live`` in which case the recording date is set. 8. All ID3 frames of the following types are removed: USER, PRIV 9. All ID3 files have TLEN (track length in ms) set (or updated). 10. The album/dir type is set in the tag. Types of ``lp`` and ``various`` do not have this field set since the latter is the default and the former can be determined during sync. In ID3 terms the value is in TXXX (description: ``%(TXXX_ALBUM_TYPE)s``). 11. Files are renamed as follows: - Type ``various``: %(VARIOUS_FNAME_FORMAT)s - Type ``single``: %(SINGLE_FNAME_FORMAT)s - All other types: %(NORMAL_FNAME_FORMAT)s - A rename template can be supplied in --file-rename-pattern 12. Directories are renamed as follows: - Type ``live``: %(LIVE_DNAME_FORMAT)s - All other types: %(NORMAL_DNAME_FORMAT)s - A rename template can be supplied in --dir-rename-pattern Album types: - ``lp``: A traditinal "album" of songs from a single artist. No extra info is written to the tag since this is the default. - ``ep``: A short collection of songs from a single artist. The string 'ep' is written to the tag's ``%(TXXX_ALBUM_TYPE)s`` field. - ``various``: A collection of songs from different artists. The string 'various' is written to the tag's ``%(TXXX_ALBUM_TYPE)s`` field. - ``live``: A collection of live recordings from a single artist. The string 'live' is written to the tag's ``%(TXXX_ALBUM_TYPE)s`` field. - ``compilation``: A collection of songs from various recordings by a single artist. The string 'compilation' is written to the tag's ``%(TXXX_ALBUM_TYPE)s`` field. Compilation dates, unlike other types, may differ. - ``demo``: A demo recording by a single artist. The string 'demo' is written to the tag's ``%(TXXX_ALBUM_TYPE)s`` field. - ``single``: A track that should no be associated with an album (even if it has album metadata). The string 'single' is written to the tag's ``%(TXXX_ALBUM_TYPE)s`` field. """ % globals() def __init__(self, arg_parser): super(FixupPlugin, self).__init__(arg_parser, cache_files=True, track_images=True) g = self.arg_group self._handled_one = False g.add_argument("--type", choices=ALBUM_TYPE_IDS, dest="dir_type", default=None, help=ARGS_HELP["--type"]) g.add_argument("--fix-case", action="store_true", dest="fix_case", help=ARGS_HELP["--fix-case"]) g.add_argument("-n", "--dry-run", action="store_true", dest="dry_run", help=ARGS_HELP["--dry-run"]) g.add_argument("--no-prompt", action="store_true", dest="no_prompt", help=ARGS_HELP["--no-prompt"]) g.add_argument("--dotted-dates", action="store_true", help=ARGS_HELP["--dotted-dates"]) g.add_argument("--file-rename-pattern", dest="file_rename_pattern", help=ARGS_HELP["--file-rename-pattern"]) g.add_argument("--dir-rename-pattern", dest="dir_rename_pattern", help=ARGS_HELP["--dir-rename-pattern"]) g.add_argument("--no-dir-rename", action="store_true", help=ARGS_HELP["--no-dir-rename"]) self._curr_dir_type = None self._dir_files_to_remove = set() def _getOne(self, key, values, default=None, Type=str, required=True): values = set(values) if None in values: values.remove(None) if len(values) != 1: printMsg( "Detected %s %s names%s" % ("0" if len(values) == 0 else "multiple", key, "." if not values else (":\n\t%s" % "\n\t".join([str(v) for v in values])), )) value = prompt("Enter %s" % key.title(), default=default, type_=Type, required=required) else: value = values.pop() return value def _getDates(self, audio_files): tags = [f.tag for f in audio_files if f.tag] rel_dates = set([t.release_date for t in tags if t.release_date]) orel_dates = set([t.original_release_date for t in tags if t.original_release_date]) rec_dates = set([t.recording_date for t in tags if t.recording_date]) release_date, original_release_date, recording_date = None, None, None def reduceDate(type_str, dates_set, default_date=None): if len(dates_set or []) != 1: reduced = self._getOne(type_str, dates_set, default=str(default_date) if default_date else None, Type=core.Date.parse) else: reduced = dates_set.pop() return reduced if (False not in [a.tag.album_type == LIVE_TYPE for a in audio_files] or self._curr_dir_type == LIVE_TYPE): # The recording date is most meaningful for live music. recording_date = reduceDate("recording date", rec_dates | orel_dates | rel_dates) rec_dates = {recording_date} # Want when these set if they may recording time. orel_dates.difference_update(rec_dates) rel_dates.difference_update(rec_dates) if orel_dates: original_release_date = reduceDate("original release date", orel_dates | rel_dates) orel_dates = {original_release_date} if rel_dates | orel_dates: release_date = reduceDate("release date", rel_dates | orel_dates) elif (False not in [a.tag.album_type == COMP_TYPE for a in audio_files] or self._curr_dir_type == COMP_TYPE): # The release date is most meaningful for comps, other track dates # may differ. if len(rel_dates) != 1: release_date = reduceDate("release date", rel_dates | orel_dates) else: release_date = list(rel_dates)[0] else: if len(orel_dates) != 1: # The original release date is most meaningful for studio music. original_release_date = reduceDate("original release date", orel_dates | rel_dates | rec_dates) orel_dates = {original_release_date} else: original_release_date = list(orel_dates)[0] if len(rel_dates) != 1: release_date = reduceDate("release date", rel_dates | orel_dates) rel_dates = {release_date} else: release_date = list(rel_dates)[0] if rec_dates.difference(orel_dates | rel_dates): recording_date = reduceDate("recording date", rec_dates) return release_date, original_release_date, recording_date def _resolveArtistInfo(self, audio_files): assert(self._curr_dir_type != SINGLE_TYPE) tags = [f.tag for f in audio_files if f.tag] artists = set([t.album_artist for t in tags if t.album_artist]) # There can be 0 or 1 album artist values. album_artist = None if len(artists) > 1: album_artist = self._getOne("album artist", artists, required=False) elif artists: album_artist = artists.pop() artists = list(set([t.artist for t in tags if t.artist])) if len(artists) > 1: # There can be more then 1 artist when VARIOUS_TYPE or # album_artist != None. if not album_artist and self._curr_dir_type != VARIOUS_TYPE: if prompt("Multiple artist names exist, process directory as " "various artists", default=True): self._curr_dir_type = VARIOUS_TYPE else: artists = [self._getOne("artist", artists, required=True)] elif (album_artist == VARIOUS_ARTISTS and self._curr_dir_type != VARIOUS_TYPE): self._curr_dir_type = VARIOUS_TYPE elif len(artists) == 0: artists = [self._getOne("artist", [], required=True)] # Fix up artist and album artist discrepancies if len(artists) == 1 and album_artist: artist = artists[0] if album_artist != artist: print("When there is only one artist it should match the " "album artist. Choices are: ") for s in [artist, album_artist]: print("\t%s" % s) album_artist = prompt("Select common artist and album artist", choices=[artist, album_artist]) artists = [album_artist] if self.args.fix_case: album_artist = _fixCase(album_artist) artists = [_fixCase(a) for a in artists] return album_artist, artists def _getAlbum(self, audio_files): tags = [f.tag for f in audio_files if f.tag] albums = set([t.album for t in tags if t.album]) album_name = (albums.pop() if len(albums) == 1 else self._getOne("album", albums)) assert album_name return album_name if not self.args.fix_case else _fixCase(album_name) def _checkCoverArt(self, directory, audio_files): valid_cover = False # Check for cover file. _printChecking("for cover art...") for dimg in self._dir_images: art_type = art.matchArtFile(dimg) if art_type == art.FRONT_COVER: dimg_name = os.path.basename(dimg) print(f"\t{dimg_name}") valid_cover = True if not valid_cover: # FIXME: move the logic out fixup and into art. # Look for a cover in the tags. for tag in [af.tag for af in audio_files if af.tag]: if valid_cover: # It could be set below... break for img in tag.images: if img.picture_type == img.FRONT_COVER: file_name = img.makeFileName("cover") print("\tFound front cover in tag, writing '%s'" % file_name) with open(os.path.join(directory, file_name), "wb") as img_file: img_file.write(img.image_data) img_file.close() valid_cover = True # TODO: force rename of cover file to single/consistent name (e.g. cover) return valid_cover def start(self, args, config): import eyed3.utils.prompt eyed3.utils.prompt.DISABLE_PROMPT = "exit" if args.no_prompt else None super(FixupPlugin, self).start(args, config) def handleFile(self, f, *args, **kwargs): super(FixupPlugin, self).handleFile(f, *args, **kwargs) if not self.audio_file and f not in self._dir_images: self._dir_files_to_remove.add(f) def handleDirectory(self, directory, _): if not self._file_cache: return directory = os.path.abspath(directory) print("\n" + Style.BRIGHT + Fore.YELLOW + "Scanning directory%s %s" % (Style.RESET_ALL, directory)) def _path(af): return af.path self._handled_one = True # Make sure all of the audio files has a tag. for f in self._file_cache: if f.tag is None: f.initTag() audio_files = sorted(list(self._file_cache), key=_path) self._file_cache = [] edited_files = set() self._curr_dir_type = self.args.dir_type if self._curr_dir_type is None: types = set([a.tag.album_type for a in audio_files]) if len(types) == 1: self._curr_dir_type = types.pop() # Check for corrections to LP, EP, COMP if (self._curr_dir_type is None and len(audio_files) < EP_MAX_SIZE_HINT): # Do you want EP? if False in [a.tag.album_type == EP_TYPE for a in audio_files]: if prompt("Only %d audio files, process directory as an EP" % len(audio_files), default=True): self._curr_dir_type = EP_TYPE else: self._curr_dir_type = EP_TYPE elif (self._curr_dir_type in (EP_TYPE, DEMO_TYPE) and len(audio_files) > EP_MAX_SIZE_HINT): # Do you want LP? if prompt("%d audio files is large for type %s, process " "directory as an LP" % (len(audio_files), self._curr_dir_type), default=True): self._curr_dir_type = LP_TYPE last = defaultdict(lambda: None) album_artist = None artists = set() album = None if self._curr_dir_type != SINGLE_TYPE: album_artist, artists = self._resolveArtistInfo(audio_files) print(Fore.BLUE + "Album artist: " + Style.RESET_ALL + (album_artist or "")) print(Fore.BLUE + "Artist" + ("s" if len(artists) > 1 else "") + ": " + Style.RESET_ALL + ", ".join(artists)) album = self._getAlbum(audio_files) print(Fore.BLUE + "Album: " + Style.RESET_ALL + album) rel_date, orel_date, rec_date = self._getDates(audio_files) for what, d in [("Release", rel_date), ("Original", orel_date), ("Recording", rec_date)]: print(f"{Fore.BLUE} {what} date: {Style.RESET_ALL} {d}") num_audio_files = len(audio_files) track_nums = set([f.tag.track_num[0] for f in audio_files]) fix_track_nums = set(range(1, num_audio_files + 1)) != track_nums new_track_nums = [] dir_type = self._curr_dir_type for f in sorted(audio_files, key=_path): print(Style.BRIGHT + Fore.GREEN + "Checking" + Fore.RESET + Style.BRIGHT + (" %s" % os.path.basename(f.path)) + Style.RESET_ALL) if not f.tag: print("\tAdding new tag") f.initTag() edited_files.add(f) tag = f.tag if tag.version != ID3_V2_4: print("\tConverting to ID3 v2.4") tag.version = ID3_V2_4 edited_files.add(f) if dir_type != SINGLE_TYPE and album_artist != tag.album_artist: print("\tSetting album artist: %s" % album_artist) tag.album_artist = album_artist edited_files.add(f) if not tag.artist and dir_type in (VARIOUS_TYPE, SINGLE_TYPE): # Prompt artist tag.artist = prompt("Artist name", default=last["artist"]) last["artist"] = tag.artist elif len(artists) == 1 and tag.artist != artists[0]: assert(dir_type != SINGLE_TYPE) print("\tSetting artist: %s" % artists[0]) tag.artist = artists[0] edited_files.add(f) if tag.album != album and dir_type != SINGLE_TYPE: print("\tSetting album: %s" % album) tag.album = album edited_files.add(f) orig_title = tag.title if not tag.title: tag.title = prompt("Track title") tag.title = tag.title.strip() if self.args.fix_case: tag.title = _fixCase(tag.title) if orig_title != tag.title: print("\tSetting title: %s" % tag.title) edited_files.add(f) if dir_type != SINGLE_TYPE: # Track numbers tnum, ttot = tag.track_num update = False if ttot != num_audio_files: update = True ttot = num_audio_files if fix_track_nums or not (1 <= tnum <= num_audio_files): tnum = None while tnum is None: tnum = int(prompt("Track #", type_=int)) if not (1 <= tnum <= num_audio_files): print(Fore.RED + "Out of range: " + Fore.RESET + "1 <= %d <= %d" % (tnum, num_audio_files)) tnum = None elif tnum in new_track_nums: print(Fore.RED + "Duplicate value: " + Fore.RESET + str(tnum)) tnum = None else: update = True new_track_nums.append(tnum) if update: tag.track_num = (tnum, ttot) print("\tSetting track numbers: %s" % str(tag.track_num)) edited_files.add(f) else: # Singles if tag.track_num != (None, None): tag.track_num = (None, None) edited_files.add(f) if dir_type != SINGLE_TYPE: # Dates if rec_date and tag.recording_date != rec_date: print("\tSetting %s date (%s)" % ("recording", str(rec_date))) tag.recording_date = rec_date edited_files.add(f) if rel_date and tag.release_date != rel_date: print("\tSetting %s date (%s)" % ("release", str(rel_date))) tag.release_date = rel_date edited_files.add(f) if orel_date and tag.original_release_date != orel_date: print("\tSetting %s date (%s)" % ("original release", str(orel_date))) tag.original_release_date = orel_date edited_files.add(f) for frame in list(tag.frameiter(["USER", "PRIV"])): print("\tRemoving %s frames: %s" % (frame.id, frame.owner_id if frame.id == b"PRIV" else frame.text)) tag.frame_set[frame.id].remove(frame) edited_files.add(f) # Add TLEN tlen = tag.getTextFrame("TLEN") if tlen is not None: real_tlen_ms = f.info.time_secs * 1000 tlen_ms = float(tlen) if tlen_ms != real_tlen_ms: print("\tSetting TLEN (%d)" % real_tlen_ms) tag.setTextFrame("TLEN", str(real_tlen_ms)) edited_files.add(f) # Add custom album type if special and otherwise not able to be # determined. curr_type = tag.album_type if curr_type != dir_type: print("\tSetting %s = %s" % (TXXX_ALBUM_TYPE, dir_type)) tag.album_type = dir_type edited_files.add(f) try: if not self._checkCoverArt(directory, audio_files): if not prompt("Proceed without valid cover file", default=True): return finally: self._dir_images = [] # Determine other changes, like file and/or directory renames # so they can be reported before save confirmation. # File renaming file_renames = [] if self.args.file_rename_pattern: format_str = self.args.file_rename_pattern else: if dir_type == SINGLE_TYPE: format_str = SINGLE_FNAME_FORMAT elif dir_type in (VARIOUS_TYPE, COMP_TYPE): format_str = VARIOUS_FNAME_FORMAT else: format_str = NORMAL_FNAME_FORMAT for f in audio_files: orig_name, orig_ext = os.path.splitext(os.path.basename(f.path)) new_name = TagTemplate(format_str).substitute(f.tag, zeropad=True) if orig_name != new_name: printMsg("Rename file to %s%s" % (new_name, orig_ext)) file_renames.append((f, new_name, orig_ext)) # Directory renaming dir_rename = None if not self.args.no_dir_rename and dir_type != SINGLE_TYPE: if self.args.dir_rename_pattern: dir_format = self.args.dir_rename_pattern else: if dir_type == LIVE_TYPE: dir_format = LIVE_DNAME_FORMAT else: dir_format = NORMAL_DNAME_FORMAT template = TagTemplate(dir_format, dotted_dates=self.args.dotted_dates) pref_dir = template.substitute(audio_files[0].tag, zeropad=True) if os.path.basename(directory) != pref_dir: new_dir = os.path.join(os.path.dirname(directory), pref_dir) printMsg("Rename directory to %s" % new_dir) dir_rename = (directory, new_dir) # Cruft files to remove file_removes = [] if self._dir_files_to_remove: for f in self._dir_files_to_remove: print("Remove file: " + os.path.basename(f)) file_removes.append(f) self._dir_files_to_remove = set() if not self.args.dry_run: confirmed = False if (edited_files or file_renames or dir_rename or file_removes): confirmed = prompt("\nSave changes", default=True) if confirmed: for f in edited_files: print("Saving %s" % os.path.basename(f.path)) f.tag.save(version=ID3_V2_4, preserve_file_time=True) for f, new_name, orig_ext in file_renames: printMsg("Renaming file to %s%s" % (new_name, orig_ext)) f.rename(new_name, preserve_file_time=True) if file_removes: for f in file_removes: printMsg("Removing file %s" % os.path.basename(f)) os.remove(f) if dir_rename: printMsg("Renaming directory to %s" % dir_rename[1]) s = os.stat(dir_rename[0]) os.rename(dir_rename[0], dir_rename[1]) # With a rename use the origianl access time os.utime(dir_rename[1], (s.st_atime, s.st_atime)) else: printMsg("\nNo changes made (run without -n/--dry-run)") def handleDone(self): if not self._handled_one: printMsg("Nothing to do") def _getTemplateKeys(): from eyed3.id3.tag import TagTemplate keys = list(TagTemplate("")._makeMapping(None, False).keys()) keys.sort() return ", ".join(["$%s" % v for v in keys]) ARGS_HELP = { "--type": "How to treat each directory. The default is '%s', " "although you may be prompted for an alternate choice " "if the files look like another type." % ALBUM_TYPE_IDS[0], "--fix-case": "Fix casing on each string field by capitalizing each " "word.", "--dry-run": "Only print the operations that would take place, but do " "not execute them.", "--no-prompt": "Exit if prompted.", "--dotted-dates": "Separate date with '.' instead of '-' when naming " "directories.", "--file-rename-pattern": "Rename file (the extension is not affected) " "based on data in the tag using substitution " "variables: " + _getTemplateKeys(), "--dir-rename-pattern": "Rename directory based on data in the tag " "using substitution variables: " + _getTemplateKeys(), "--no-dir-rename": "Do not rename the directory.", } eyeD3-0.9.7/eyed3/plugins/genres.py000066400000000000000000000033531432016011500170240ustar00rootroot00000000000000import math from eyed3 import id3 from eyed3.plugins import Plugin class GenreListPlugin(Plugin): SUMMARY = "Display the full list of standard ID3 genres." DESCRIPTION = "ID3 v1 defined a list of genres and mapped them to "\ "to numeric values so they can be stored as a single "\ "byte.\nIt is *recommended* that these genres are used "\ "although most newer software (including eyeD3) does not "\ "care." NAMES = ["genres"] def __init__(self, arg_parser): super(GenreListPlugin, self).__init__(arg_parser) self.arg_group.add_argument("-1", "--single-column", action="store_true", help="List on genre per line.") def start(self, args, config): self._printGenres(args) @staticmethod def _printGenres(args): # Filter out 'Unknown' genre_ids = [i for i in id3.genres if type(i) is int and id3.genres[i] is not None] genre_ids.sort() if args.single_column: for gid in genre_ids: print("%3d: %s" % (gid, id3.genres[gid])) else: offset = int(math.ceil(float(len(genre_ids)) / 2)) for i in range(offset): if i < len(genre_ids): c1 = "%3d: %s" % (i, id3.genres[i]) else: c1 = "" if (i * 2) < len(genre_ids): try: c2 = "%3d: %s" % (i + offset, id3.genres[i + offset]) except IndexError: break else: c2 = "" print(c1 + (" " * (40 - len(c1))) + c2) print("") eyeD3-0.9.7/eyed3/plugins/itunes.py000066400000000000000000000037611432016011500170530ustar00rootroot00000000000000from eyed3.plugins import LoaderPlugin from eyed3.id3.apple import PCST, PCST_FID, WFED, WFED_FID class Podcast(LoaderPlugin): NAMES = ['itunes-podcast'] SUMMARY = "Adds (or removes) the tags necessary for Apple iTunes to "\ "identify the file as a podcast." def __init__(self, arg_parser): super(Podcast, self).__init__(arg_parser) g = self.arg_group g.add_argument("--add", action="store_true", help="Add the podcast frames.") g.add_argument("--remove", action="store_true", help="Remove the podcast frames.") def _add(self, tag): save = False if PCST_FID not in tag.frame_set: tag.frame_set[PCST_FID] = PCST() save = True if WFED_FID not in tag.frame_set: tag.frame_set[WFED_FID] = WFED("http://eyeD3.nicfit.net/") save = True if save: print("\tAdding...") tag.save(backup=self.args.backup) self._printStatus(tag) def _remove(self, tag): save = False for fid in [PCST_FID, WFED_FID]: try: del tag.frame_set[fid] save = True except KeyError: continue if save: print("\tRemoving...") tag.save(backup=self.args.backup) self._printStatus(tag) def _printStatus(self, tag): status = ":-(" if PCST_FID in tag.frame_set: status = ":-/" if WFED_FID in tag.frame_set: status = ":-)" print("\tiTunes podcast? %s" % status) def handleFile(self, f): super(Podcast, self).handleFile(f) if self.audio_file and self.audio_file.tag: print(f) tag = self.audio_file.tag self._printStatus(tag) if self.args.remove: self._remove(self.audio_file.tag) elif self.args.add: self._add(self.audio_file.tag) eyeD3-0.9.7/eyed3/plugins/jsontag.py000066400000000000000000000110011432016011500171730ustar00rootroot00000000000000import base64 import inspect import dataclasses from json import dumps import eyed3.core import eyed3.plugins import eyed3.id3.tag import eyed3.id3.headers from eyed3.utils.log import getLogger log = getLogger(__name__) class JsonTagPlugin(eyed3.plugins.LoaderPlugin): NAMES = ["json"] SUMMARY = "Outputs all tags as JSON." def __init__(self, arg_parser): super().__init__(arg_parser, cache_files=True, track_images=False) g = self.arg_group g.add_argument("-c", "--compact", action="store_true", help="Output in compact form, wound new lines or indentation.") g.add_argument("-s", "--sort", action="store_true", help="Output JSON in sorted by key.") def handleFile(self, f, *args, **kwargs): super().handleFile(f) if self.audio_file and self.audio_file.info and self.audio_file.tag: json = audioFileToJson(self.audio_file) print(dumps(json, indent=2 if not self.args.compact else None, sort_keys=self.args.sort)) def audioFileToJson(audio_file): tag = audio_file.tag tdict = dict(path=audio_file.path, info=dataclasses.asdict(audio_file.info)) # Tag fields for name in [m for m in sorted(dir(tag)) if not m.startswith("_") and m not in _tag_exclusions]: member = getattr(tag, name) if name not in _tag_map: if not inspect.ismethod(member) and not inspect.isfunction(member): log.warning(f"Unhandled Tag member: {name}") continue elif member is None: continue elif member.__class__ is not _tag_map[name]: log.warning(f"Unexpected type for member {name}: {member.__class__}") continue if isinstance(member, (str, int, bool)): tdict[name] = member elif isinstance(member, eyed3.core.Date): tdict[name] = str(member) elif isinstance(member, eyed3.id3.Genre): tdict[name] = member.name elif isinstance(member, bytes): tdict[name] = base64.b64encode(member).decode("ascii") elif isinstance(member, eyed3.id3.tag.ArtistOrigin): ... # TODO elif isinstance(member, (eyed3.core.CountAndTotalTuple,)): if any(member): tdict[name] = member._asdict() elif isinstance(member, (list, tuple)): ... # TODO elif isinstance(member, eyed3.id3.tag.AccessorBase): ... # TODO elif isinstance(member, (eyed3.id3.tag.TagHeader, eyed3.id3.tag.ExtendedTagHeader, eyed3.id3.tag.FileInfo, eyed3.id3.frames.FrameSet)): ... # TODO else: log.warning(f"Unhandled tag member {name}, type {member.__class__.__name__})") tdict["_eyeD3"] = eyed3.__about__.__version__ return tdict _tag_map = { 'album': str, 'album_artist': str, 'album_type': str, 'artist': str, 'original_artist': str, 'artist_origin': list, 'artist_url': str, 'audio_file_url': str, 'audio_source_url': str, 'best_release_date': eyed3.core.Date, 'bpm': int, 'cd_id': bytes, 'chapters': eyed3.id3.tag.ChaptersAccessor, 'comments': eyed3.id3.tag.CommentsAccessor, 'commercial_url': str, 'composer': str, 'copyright_url': str, 'disc_num': eyed3.core.CountAndTotalTuple, 'encoding_date': eyed3.core.Date, 'extended_header': eyed3.id3.headers.ExtendedTagHeader, 'file_info': eyed3.id3.tag.FileInfo, 'frame_set': eyed3.id3.frames.FrameSet, 'genre': eyed3.id3.Genre, 'header': eyed3.id3.headers.TagHeader, 'images': eyed3.id3.tag.ImagesAccessor, 'internet_radio_url': str, 'lyrics': eyed3.id3.tag.LyricsAccessor, 'non_std_genre': eyed3.id3.Genre, 'objects': eyed3.id3.tag.ObjectsAccessor, 'original_release_date': eyed3.core.Date, 'payment_url': str, 'play_count': int, 'popularities': eyed3.id3.tag.PopularitiesAccessor, 'privates': eyed3.id3.tag.PrivatesAccessor, 'publisher': str, 'publisher_url': str, 'recording_date': eyed3.core.Date, 'release_date': eyed3.core.Date, 'table_of_contents': eyed3.id3.tag.TocAccessor, 'tagging_date': eyed3.core.Date, 'terms_of_use': str, 'title': str, 'track_num': eyed3.core.CountAndTotalTuple, 'unique_file_ids': eyed3.id3.tag.UniqueFileIdAccessor, 'user_text_frames': eyed3.id3.tag.UserTextsAccessor, 'user_url_frames': eyed3.id3.tag.UserUrlsAccessor, 'version': tuple, } _tag_exclusions = { "read_only": bool, } eyeD3-0.9.7/eyed3/plugins/lameinfo.py000066400000000000000000000067701432016011500173410ustar00rootroot00000000000000import math from eyed3.utils import formatSize from eyed3.utils.console import printMsg, getTtySize from eyed3.plugins import LoaderPlugin class LameInfoPlugin(LoaderPlugin): NAMES = ["lameinfo", "xing"] SUMMARY = "Outputs lame header (if one exists) for file." DESCRIPTION = ( "The 'lame' (or xing) header provides extra information about the mp3 " "that is useful to players and encoders but not officially part of " "the mp3 specification. Variable bit rate mp3s, for example, use this " "header.\n\n" "For more details see `here `_" ) def printHeader(self, file_path): w = getTtySize()[1] printMsg(self._getFileHeader(file_path, w)) printMsg(self._getHardRule(w)) def handleFile(self, f, *_, **__): super().handleFile(f) if self.audio_file is None: return self.printHeader(f) if (self.audio_file.info is None or not self.audio_file.info.lame_tag): printMsg("No LAME Tag") return lt = self.audio_file.info.lame_tag if "infotag_crc" not in lt: try: printMsg(f"Encoder Version: {lt['encoder_version']}") except KeyError: pass return values = [("Encoder Version", lt['encoder_version']), ("LAME Tag Revision", lt['tag_revision']), ("VBR Method", lt['vbr_method']), ("Lowpass Filter", lt['lowpass_filter']), ] if "replaygain" in lt: try: peak = lt["replaygain"]["peak_amplitude"] db = 20 * math.log10(peak) val = "%.8f (%+.1f dB)" % (peak, db) values.append(("Peak Amplitude", val)) except KeyError: pass for type_ in ["radio", "audiofile"]: try: gain = lt["replaygain"][type_] name = "%s Replay Gain" % gain['name'].capitalize() val = "%s dB (%s)" % (gain['adjustment'], gain['originator']) values.append((name, val)) except KeyError: pass values.append(("Encoding Flags", " ".join((lt["encoding_flags"])))) if lt["nogap"]: values.append(("No Gap", " and ".join(lt["nogap"]))) values.append(("ATH Type", lt["ath_type"])) values.append(("Bitrate (%s)" % lt["bitrate"][1], lt["bitrate"][0])) values.append(("Encoder Delay", "%s samples" % lt["encoder_delay"])) values.append(("Encoder Padding", "%s samples" % lt["encoder_padding"])) values.append(("Noise Shaping", lt["noise_shaping"])) values.append(("Stereo Mode", lt["stereo_mode"])) values.append(("Unwise Settings", lt["unwise_settings"])) values.append(("Sample Frequency", lt["sample_freq"])) values.append(("MP3 Gain", "%s (%+.1f dB)" % (lt["mp3_gain"], lt["mp3_gain"] * 1.5))) values.append(("Preset", lt["preset"])) values.append(("Surround Info", lt["surround_info"])) values.append(("Music Length", "%s" % formatSize(lt["music_length"]))) values.append(("Music CRC-16", "%04X" % lt["music_crc"])) values.append(("LAME Tag CRC-16", "%04X" % lt["infotag_crc"])) for v in values: printMsg(f"{v[0]:<20}: {v[1]}") eyeD3-0.9.7/eyed3/plugins/lastfm.py000066400000000000000000000025101432016011500170210ustar00rootroot00000000000000from pylast import SIZE_EXTRA_LARGE, SIZE_LARGE, SIZE_MEDIUM, SIZE_MEGA, SIZE_SMALL from pylast import LastFMNetwork, WSError api_k = "a5f0ac61e7db2481b054ba52ff9a654f" api_s = "0c4a52ae5dcdbba1f9e782833a50b623" _network = None def Client(): global _network if not _network: _network = LastFMNetwork(api_key=api_k, api_secret=api_s) _network.enable_rate_limit() return _network def getArtist(artist): return Client().get_artist(artist) def getAlbum(artist, title): return Client().get_album(artist, title) def getAlbumArt(artist, title, size=SIZE_EXTRA_LARGE): return _getArt(getAlbum(artist, title), size=size) def getArtistArt(artist, size=SIZE_EXTRA_LARGE): return _getArt(getArtist(artist), size=size) def _getArt(obj, size=SIZE_EXTRA_LARGE): try: return obj.get_cover_image(size) except WSError: raise ValueError("{} not found.".format(obj.__class__.__name__)) if __name__ == "__main__": album = getAlbum("Melvins", "Houdini") for sz in (SIZE_SMALL, SIZE_MEGA, SIZE_MEDIUM, SIZE_LARGE, SIZE_EXTRA_LARGE): print(album.get_cover_image(sz)) melvins = getArtist("Melvins") print(melvins) for sz in (SIZE_SMALL, SIZE_MEGA, SIZE_MEDIUM, SIZE_LARGE, SIZE_EXTRA_LARGE): print(melvins.get_cover_image(sz)) eyeD3-0.9.7/eyed3/plugins/mimetype.py000066400000000000000000000056161432016011500173760ustar00rootroot00000000000000import time import pprint import eyed3 import eyed3.utils from pathlib import Path from collections import Counter from eyed3.mimetype import guessMimetype from eyed3.utils.log import getLogger log = getLogger(__name__) # python-magic try: import magic class MagicTypes(magic.Magic): def __init__(self): magic.Magic.__init__(self, mime=True, mime_encoding=False, keep_going=True) def guess_type(self, filename, all_types=False): try: types = self.from_file(filename) except UnicodeEncodeError: # https://github.com/ahupp/python-magic/pull/144 types = self.from_file(filename.encode("utf-8", 'surrogateescape')) delim = r"\012- " if all_types: return types.split(delim) else: return types.split(delim)[0] _python_magic = MagicTypes() except ImportError: _python_magic = None class MimetypesPlugin(eyed3.plugins.LoaderPlugin): NAMES = ["mimetypes"] def __init__(self, arg_parser): self._num_visited = 0 super().__init__(arg_parser, cache_files=False, track_images=False) g = self.arg_group g.add_argument("--status", action="store_true", help="Print dot status.") g.add_argument("--parse-files", action="store_true", help="Parse each file.") g.add_argument("--hide-notfound", action="store_true") if _python_magic: g.add_argument("-M", "--use-pymagic", action="store_true", help="Use python-magic to determine mimetype.") self.magic = None self.start_t = None self.mime_types = Counter() def start(self, args, config): super().start(args, config) self.magic = "pymagic" if self.args.use_pymagic else "filetype" self.start_t = time.time() def handleFile(self, f, *args, **kwargs): self._num_visited += 1 if self.args.parse_files: try: super().handleFile(f) except Exception as ex: log.critical(ex, exc_info=ex) else: self._num_loaded += 1 if self.magic == "pymagic": mtype = _python_magic.guess_type(f) else: mtype = guessMimetype(f) self.mime_types[mtype] += 1 if not self.args.hide_notfound: if mtype is None and Path(f).suffix.lower() in (".mp3",): print("None mimetype:", f) if self.args.status: print(".", end="", flush=True) def handleDone(self): t = time.time() - self.start_t print(f"\nVisited {self._num_visited} files") print(f"Processed {self._num_loaded} files") print(f"magic: {self.magic}") print(f"time: {eyed3.utils.formatTime(t)} seconds") if self.mime_types: pprint.pprint(self.mime_types) eyeD3-0.9.7/eyed3/plugins/nfo.py000066400000000000000000000111411432016011500163150ustar00rootroot00000000000000import time import eyed3 from eyed3.utils.console import printMsg from eyed3.utils import formatSize, formatTime from eyed3.id3 import versionToString from eyed3.plugins import LoaderPlugin class NfoPlugin(LoaderPlugin): NAMES = ["nfo"] SUMMARY = "Create NFO files for each directory scanned." DESCRIPTION = "Each directory scanned is treated as an album and a "\ "`NFO `_ file is "\ "written to standard out.\n\n"\ "NFO files are often found in music archives." def __init__(self, arg_parser): super(NfoPlugin, self).__init__(arg_parser) self.albums = {} def handleFile(self, f): super(NfoPlugin, self).handleFile(f) if self.audio_file and self.audio_file.tag: tag = self.audio_file.tag album = tag.album if album and album not in self.albums: self.albums[album] = [] self.albums[album].append(self.audio_file) elif album: self.albums[album].append(self.audio_file) def handleDone(self): if not self.albums: printMsg("No albums found.") return for album in self.albums: audio_files = self.albums[album] if not audio_files: continue audio_files.sort(key=lambda af: (af.tag.track_num[0] or 999, af.tag.track_num[1] or 999)) max_title_len = 0 avg_bitrate = 0 encoder_info = '' for audio_file in audio_files: tag = audio_file.tag # Compute maximum title length title_len = len(tag.title or "") if title_len > max_title_len: max_title_len = title_len # Compute average bitrate avg_bitrate += audio_file.info.bit_rate[1] # Grab the last lame version in case not all files have one if "encoder_version" in audio_file.info.lame_tag: version = audio_file.info.lame_tag['encoder_version'] encoder_info = (version or encoder_info) avg_bitrate = avg_bitrate / len(audio_files) printMsg("") printMsg("Artist : %s" % audio_files[0].tag.artist) printMsg("Album : %s" % album) printMsg("Released : %s" % (audio_files[0].tag.original_release_date or audio_files[0].tag.release_date)) printMsg("Recorded : %s" % audio_files[0].tag.recording_date) genre = audio_files[0].tag.genre if genre: genre = genre.name else: genre = "" printMsg("Genre : %s" % genre) printMsg("") printMsg("Source : ") printMsg("Encoder : %s" % encoder_info) printMsg("Codec : mp3") printMsg("Bitrate : ~%s K/s @ %s Hz, %s" % (avg_bitrate, audio_files[0].info.sample_freq, audio_files[0].info.mode)) printMsg("Tag : ID3 %s" % versionToString(audio_files[0].tag.version)) printMsg("") printMsg("Ripped By: ") printMsg("") printMsg("Track Listing") printMsg("-------------") count = 0 total_time = 0 total_size = 0 for audio_file in audio_files: tag = audio_file.tag count += 1 title = tag.title or "" title_len = len(title) padding = " " * ((max_title_len - title_len) + 3) time_secs = audio_file.info.time_secs total_time += time_secs total_size += audio_file.info.size_bytes zero_pad = "0" * (len(str(len(audio_files))) - len(str(count))) printMsg(" %s%d. %s%s(%s)" % (zero_pad, count, title, padding, formatTime(time_secs))) printMsg("") printMsg("Total play time : %s" % formatTime(total_time)) printMsg("Total size : %s" % formatSize(total_size)) printMsg("") printMsg("=" * 78) printMsg(".NFO file created with eyeD3 %s on %s" % (eyed3.version, time.asctime())) printMsg("For more information about eyeD3 go to %s" % "http://eyeD3.nicfit.net/") printMsg("=" * 78) eyeD3-0.9.7/eyed3/plugins/pymod.py000066400000000000000000000047771432016011500167040ustar00rootroot00000000000000import os import importlib.machinery from eyed3.plugins import LoaderPlugin _DEFAULT_MOD = "eyeD3mod.py" class PyModulePlugin(LoaderPlugin): SUMMARY = "Imports a Python module file and calls its functions for the "\ "the various plugin events." DESCRIPTION = f''' If no module if provided a file named {_DEFAULT_MOD} in the current working directory is imported. If any of the following methods exist they still be invoked: def audioFile(audio_file): """Invoked for every audio file that is encountered. The ``audio_file`` is of type ``eyed3.core.AudioFile``; currently this is the concrete type ``eyed3.mp3.Mp3AudioFile``.""" pass def audioDir(d, audio_files, images): """This function is invoked for any directory (``d``) that contains audio (``audio_files``) or image (``images``) media.""" pass def done(): """This method is invoke before successful exit.""" pass ''' NAMES = ["pymod"] def __init__(self, arg_parser): super(PyModulePlugin, self).__init__(arg_parser, cache_files=True, track_images=True) self._mod = None self.arg_group.add_argument("-m", "--module", dest="module", help="The Python module module to invoke. " f"The default is ./{_DEFAULT_MOD}") def start(self, args, config): mod_file = args.module or _DEFAULT_MOD try: mod_name = os.path.splitext(os.path.basename(mod_file))[0] loader = importlib.machinery.SourceFileLoader(mod_name, mod_file) mod = loader.load_module() self._mod = mod except IOError: raise IOError("Module file not found: %s" % mod_file) except (NameError, ImportError, SyntaxError) as ex: raise IOError("Module load error: %s" % str(ex)) def handleFile(self, f): super(PyModulePlugin, self).handleFile(f) if not self.audio_file: return if "audioFile" in dir(self._mod): self._mod.audioFile(self.audio_file) def handleDirectory(self, d, _): if not self._file_cache and not self._dir_images: return if "audioDir" in dir(self._mod): self._mod.audioDir(d, self._file_cache, self._dir_images) super(PyModulePlugin, self).handleDirectory(d, _) def handleDone(self): super(PyModulePlugin, self).handleDone() if "done" in dir(self._mod): self._mod.done() eyeD3-0.9.7/eyed3/plugins/stats.py000066400000000000000000000354521432016011500167040ustar00rootroot00000000000000import os import sys import operator from collections import Counter from eyed3 import id3, mp3 from eyed3.core import AUDIO_MP3 from eyed3.mimetype import guessMimetype from eyed3.utils.console import Fore, Style, printMsg from eyed3.plugins import LoaderPlugin from eyed3.id3 import frames ID3_VERSIONS = [id3.ID3_V1_0, id3.ID3_V1_1, id3.ID3_V2_2, id3.ID3_V2_3, id3.ID3_V2_4] _OP_STRINGS = {operator.le: "<=", operator.lt: "< ", operator.ge: ">=", operator.gt: "> ", operator.eq: "= ", operator.ne: "!=", } class Rule: def test(self, path, audio_file): raise NotImplementedError() PREFERRED_ID3_VERSIONS = [id3.ID3_V2_3, id3.ID3_V2_4, ] class Id3TagRules(Rule): def test(self, path, audio_file): scores = [] if audio_file is None: return None if not audio_file.tag: return [(-75, "Missing ID3 tag")] tag = audio_file.tag if tag.version not in PREFERRED_ID3_VERSIONS: scores.append((-30, "ID3 version not in %s" % PREFERRED_ID3_VERSIONS)) if not tag.title: scores.append((-30, "Tag missing title")) if not tag.artist: scores.append((-28, "Tag missing artist")) if not tag.album: scores.append((-26, "Tag missing album")) if not tag.track_num[0]: scores.append((-24, "Tag missing track number")) if not tag.track_num[1]: scores.append((-22, "Tag missing total # of tracks")) if not tag.getBestDate(): scores.append((-30, "Tag missing any useful dates")) else: if not tag.original_release_date: # Original release date is so rarely used but is almost always # what I mean or wanna know. scores.append((-10, "No original release date")) elif not tag.release_date: scores.append((-5, "No release date")) # TLEN, best gotten from audio_file.info.time_secs but having it in # the tag is good, I guess. if b"TLEN" not in tag.frame_set: scores.append((-5, "No TLEN frame")) return scores class BitrateRule(Rule): BITRATE_DEDUCTIONS = [(128, -20), (192, -10)] def test(self, path, audio_file): scores = [] if not audio_file: return None if not audio_file.info: # Detected as an audio file but not real audio data found. return [(-90, "No audio data found")] is_vbr, bitrate = audio_file.info.bit_rate for threshold, score in self.BITRATE_DEDUCTIONS: if bitrate < threshold: scores.append((score, "Bit rate < %d" % threshold)) break return scores VALID_MIME_TYPES = mp3.MIME_TYPES + ["image/png", "image/gif", "image/jpeg", ] class FileRule(Rule): def test(self, path, audio_file): mt = guessMimetype(path) for name in os.path.split(path): if name.startswith('.'): return [(-100, "Hidden file type")] if mt not in VALID_MIME_TYPES: return [(-100, "Unsupported file type: %s" % mt)] return None VALID_ARTWORK_NAMES = ("cover", "cover-front", "cover-back") class ArtworkRule(Rule): def test(self, path, audio_file): mt = guessMimetype(path) if mt and mt.startswith("image/"): name, ext = os.path.splitext(os.path.basename(path)) if name not in VALID_ARTWORK_NAMES: return [(-10, "Artwork file not in %s" % str(VALID_ARTWORK_NAMES))] return None BAD_FRAMES = [frames.PRIVATE_FID, frames.OBJECT_FID] class Id3FrameRules(Rule): def test(self, path, audio_file): scores = [] if not audio_file or not audio_file.tag: return tag = audio_file.tag for fid in tag.frame_set: if fid[0] == 'T' and fid != "TXXX" and len(tag.frame_set[fid]) > 1: scores.append((-10, "Multiple %s frames" % fid.decode('ascii'))) elif fid in BAD_FRAMES: scores.append((-13, "%s frames are bad, mmmkay?" % fid.decode('ascii'))) return scores class Stat(Counter): TOTAL = "total" def __init__(self, *args, **kwargs): super(Stat, self).__init__(*args, **kwargs) self[self.TOTAL] = 0 self._key_names = {} def compute(self, file, audio_file): self[self.TOTAL] += 1 self._compute(file, audio_file) def _compute(self, file, audio_file): pass def report(self): self._report() def _sortedKeys(self, most_common=False): def keyDisplayName(k): return self._key_names[k] if k in self._key_names else k key_map = {} for k in list(self.keys()): key_map[keyDisplayName(k)] = k if not most_common: sorted_names = [k for k in key_map.keys() if k] sorted_names.remove(self.TOTAL) sorted_names.sort() sorted_names.append(self.TOTAL) else: most_common = self.most_common() sorted_names = [] remainder_names = [] for k, v in most_common: if k != self.TOTAL and v > 0: sorted_names.append(keyDisplayName(k)) elif k != self.TOTAL: remainder_names.append(keyDisplayName(k)) remainder_names.sort() sorted_names = sorted_names + remainder_names sorted_names.append(self.TOTAL) return [key_map[name] for name in sorted_names] def _report(self, most_common=False): keys = self._sortedKeys(most_common=most_common) key_col_width = 0 val_col_width = 0 for key in keys: key = self._key_names[key] if key in self._key_names else key key_col_width = max(key_col_width, len(str(key))) val_col_width = max(val_col_width, len(str(self[key]))) key_col_width += 1 val_col_width += 1 for k in keys: key_name = self._key_names[k] if k in self._key_names else k if type(key_name) is bytes: key_name = key_name.decode("latin1") value = self[k] percent = self.percent(k) if value and k != "total" else "" print("{padding}{key}:{value}{percent}".format( padding=' ' * 4, key=str(key_name).ljust(key_col_width), value=str(value).rjust(val_col_width), percent=" ( %s%.2f%%%s )" % (Fore.GREEN, percent, Fore.RESET) if percent else "", )) def percent(self, key): return (float(self[key]) / float(self["total"])) * 100 class AudioStat(Stat): def compute(self, audio_file): self["total"] += 1 self._compute(audio_file) def _compute(self, audio_file): pass class FileCounterStat(Stat): SUPPORTED_AUDIO = "audio" UNSUPPORTED_AUDIO = "audio (unsupported)" HIDDEN_FILES = "hidden" OTHER_FILES = "other" def __init__(self): super(FileCounterStat, self).__init__() for k in ("audio", "hidden", "audio (unsupported)"): self[k] = 0 def _compute(self, file, audio_file): mt = guessMimetype(file) if audio_file: self[self.SUPPORTED_AUDIO] += 1 elif mt and mt.startswith("audio/"): self[self.UNSUPPORTED_AUDIO] += 1 elif os.path.basename(file).startswith('.'): self[self.HIDDEN_FILES] += 1 else: self[self.OTHER_FILES] += 1 def _report(self): print(Style.BRIGHT + Fore.YELLOW + "Files:" + Style.RESET_ALL) super(FileCounterStat, self)._report() class MimeTypeStat(Stat): def _compute(self, file, audio_file): mt = guessMimetype(file) self[mt] += 1 def _report(self): print(Style.BRIGHT + Fore.YELLOW + "Mime-Types:" + Style.RESET_ALL) super(MimeTypeStat, self)._report(most_common=True) class Id3VersionCounter(AudioStat): def __init__(self): super(Id3VersionCounter, self).__init__() for v in ID3_VERSIONS: self[v] = 0 self._key_names[v] = id3.versionToString(v) def _compute(self, audio_file): if audio_file.tag: self[audio_file.tag.version] += 1 else: self[None] += 1 def _report(self): print(Style.BRIGHT + Fore.YELLOW + "ID3 versions:" + Style.RESET_ALL) super(Id3VersionCounter, self)._report() class Id3FrameCounter(AudioStat): def _compute(self, audio_file): if audio_file.tag: for frame_id in audio_file.tag.frame_set: self[frame_id] += len(audio_file.tag.frame_set[frame_id]) def _report(self): print(Style.BRIGHT + Fore.YELLOW + "ID3 frames:" + Style.RESET_ALL) super(Id3FrameCounter, self)._report(most_common=True) class BitrateCounter(AudioStat): def __init__(self): super(BitrateCounter, self).__init__() self["cbr"] = 0 self["vbr"] = 0 self.bitrate_keys = [(operator.le, 96), (operator.le, 112), (operator.le, 128), (operator.le, 160), (operator.le, 192), (operator.le, 256), (operator.le, 320), (operator.gt, 320), ] for k in self.bitrate_keys: self[k] = 0 op, bitrate = k self._key_names[k] = "%s %d" % (_OP_STRINGS[op], bitrate) def _compute(self, audio_file): if audio_file.type != AUDIO_MP3 or audio_file.info is None: self["total"] -= 1 return vbr, br = audio_file.info.bit_rate if vbr: self["vbr"] += 1 else: self["cbr"] += 1 for key in self.bitrate_keys: key_op, key_br = key if key_op(br, key_br): self[key] += 1 break def _report(self): print(Style.BRIGHT + Fore.YELLOW + "MP3 bitrates:" + Style.RESET_ALL) super(BitrateCounter, self)._report(most_common=True) def _sortedKeys(self, most_common=False): keys = super(BitrateCounter, self)._sortedKeys(most_common=most_common) keys.remove("cbr") keys.remove("vbr") keys.insert(0, "cbr") keys.insert(1, "vbr") return keys class RuleViolationStat(Stat): def _report(self): print(Style.BRIGHT + Fore.YELLOW + "Rule Violations:" + Style.RESET_ALL) super(RuleViolationStat, self)._report(most_common=True) class Id3ImageTypeCounter(AudioStat): def __init__(self): super(Id3ImageTypeCounter, self).__init__() self._key_names = {} for attr in dir(frames.ImageFrame): val = getattr(frames.ImageFrame, attr) if isinstance(val, int) and not attr.endswith("_TYPE") and not attr.startswith("_"): self._key_names[val] = attr for v in self._key_names: self[v] = 0 def _compute(self, audio_file): if audio_file.tag: for img in audio_file.tag.images: self[img.picture_type] += 1 def _report(self): print(Style.BRIGHT + Fore.YELLOW + "APIC image types:" + Style.RESET_ALL) super(Id3ImageTypeCounter, self)._report() class StatisticsPlugin(LoaderPlugin): NAMES = ['stats'] SUMMARY = "Computes statistics for all audio files scanned." def __init__(self, arg_parser): super(StatisticsPlugin, self).__init__(arg_parser) self.arg_group.add_argument( "--verbose", action="store_true", default=False, help="Show details for each file with rule violations.") self._stats = [] self._rules_stat = RuleViolationStat() self._stats.append(FileCounterStat()) self._stats.append(MimeTypeStat()) self._stats.append(Id3VersionCounter()) self._stats.append(Id3FrameCounter()) self._stats.append(Id3ImageTypeCounter()) self._stats.append(BitrateCounter()) self._score_sum = 0 self._score_count = 0 self._rules_log = {} self._rules = [Id3TagRules(), FileRule(), ArtworkRule(), BitrateRule(), Id3FrameRules(), ] def handleFile(self, path): super(StatisticsPlugin, self).handleFile(path) if not self.args.quiet: sys.stdout.write('.') sys.stdout.flush() for stat in self._stats: if isinstance(stat, AudioStat): if self.audio_file: stat.compute(self.audio_file) else: stat.compute(path, self.audio_file) self._score_count += 1 total_score = 100 for rule in self._rules: scores = rule.test(path, self.audio_file) or [] if scores: if path not in self._rules_log: self._rules_log[path] = [] for score, text in scores: self._rules_stat[text] += 1 self._rules_log[path].append((score, text)) # += because negative values are returned total_score += score if total_score != 100: self._rules_stat[Stat.TOTAL] += 1 self._score_sum += total_score def handleDone(self): if self._num_loaded == 0: super(StatisticsPlugin, self).handleDone() return print() for stat in self._stats + [self._rules_stat]: stat.report() print() # Detailed rule violations if self.args.verbose: for path in self._rules_log: printMsg(path) # does the right thing for unicode for score, text in self._rules_log[path]: print(f"\t{Fore.RED}{str(score).center(3)}{Fore.RESET} ({text})") def prettyScore(): s = float(self._score_sum) / float(self._score_count) if s > 80: c = Fore.GREEN elif s > 70: c = Fore.YELLOW else: c = Fore.RED return s, c score, color = prettyScore() print(f"{Style.BRIGHT}Score{Style.RESET_BRIGHT} = {color}{score}%%{Fore.RESET}") if not self.args.verbose: print("Run with --verbose to see files and their rule violations") print() eyeD3-0.9.7/eyed3/plugins/xep_118.py000066400000000000000000000031001432016011500167140ustar00rootroot00000000000000from pathlib import Path from xml.sax.saxutils import escape from eyed3.plugins import LoaderPlugin from eyed3.utils.console import printMsg class Xep118Plugin(LoaderPlugin): NAMES = ["xep-118"] SUMMARY = "Outputs all tags in XEP-118 XML format. "\ "(see: http://xmpp.org/extensions/xep-0118.html)" def __init__(self, arg_parser): super().__init__(arg_parser, cache_files=True, track_images=False) g = self.arg_group g.add_argument("--no-pretty-print", action="store_true", help="Output without new lines or indentation.") def handleFile(self, f, *args, **kwargs): super().handleFile(f) if self.audio_file and self.audio_file.tag: xml = self.getXML(self.audio_file) printMsg(xml) def getXML(self, audio_file): tag = audio_file.tag pprint = not self.args.no_pretty_print nl = "\n" if pprint else "" indent = (" " * 2) if pprint else "" xml = f"{nl}" if tag.artist: xml += f"{indent}{escape(tag.artist)}{nl}" if tag.title: xml += f"{indent}{escape(tag.title)}{nl}" if tag.album: xml += f"{indent}{escape(tag.album)}{nl}" xml += f"{indent}file://{escape(str(Path(audio_file.path).absolute()))}{nl}" if audio_file.info: xml += f"{indent}{audio_file.info.time_secs:.2f}{nl}" xml += "" return xml eyeD3-0.9.7/eyed3/plugins/yamltag.py000066400000000000000000000017321432016011500171760ustar00rootroot00000000000000import eyed3.plugins from eyed3 import log from eyed3.plugins.jsontag import audioFileToJson _have_yaml = False try: import ruamel.yaml as yaml _have_yaml = True except ImportError: try: import yaml _have_yaml = True except ImportError: log.info("yaml plugin: Install `ruamel.yaml` or `pyyaml` for YAML support.") if _have_yaml: class YamlTagPlugin(eyed3.plugins.LoaderPlugin): NAMES = ["yaml"] SUMMARY = "Outputs all tags as YAML." def __init__(self, arg_parser): super().__init__(arg_parser, cache_files=True, track_images=False) def handleFile(self, f, *args, **kwargs): super().handleFile(f) if self.audio_file and self.audio_file.info and self.audio_file.tag: print(yaml.safe_dump(audioFileToJson(self.audio_file), indent=2, default_flow_style=False, explicit_start=True)) eyeD3-0.9.7/eyed3/utils/000077500000000000000000000000001432016011500146425ustar00rootroot00000000000000eyeD3-0.9.7/eyed3/utils/__init__.py000066400000000000000000000343601432016011500167610ustar00rootroot00000000000000import os import re import math import pathlib import logging import argparse import warnings import functools import deprecation from ..utils.log import getLogger from .. import LOCAL_FS_ENCODING from ..__about__ import __version__, __release_name__, __version_txt__ if hasattr(os, "fwalk"): os_walk = functools.partial(os.fwalk, follow_symlinks=True) def os_walk_unpack(w): return w[0:3] else: os_walk = functools.partial(os.walk, followlinks=True) def os_walk_unpack(w): return w log = getLogger(__name__) @deprecation.deprecated(deprecated_in="0.9a2", removed_in="1.0", current_version=__version__, details="Use eyed3.mimetype.guessMimetype() instead.") def guessMimetype(filename, with_encoding=False): from .. import mimetype retval = mimetype.guessMimetype(filename) if not with_encoding: return retval else: warnings.warn("File character encoding no longer returned, value is None", UserWarning, stacklevel=2) return retval, None def walk(handler, path, excludes=None, fs_encoding=LOCAL_FS_ENCODING, recursive=False): """A wrapper around os.walk which handles exclusion patterns and multiple path types (str, pathlib.Path, bytes). """ if isinstance(path, pathlib.Path): path = str(path) else: path = str(path, fs_encoding) if type(path) is not str else path excludes = excludes if excludes else [] excludes_re = [] for e in excludes: excludes_re.append(re.compile(e)) def _isExcluded(_p): for ex in excludes_re: match = ex.match(_p) if match: return True return False if not os.path.exists(path): raise IOError(f"file not found: {path}") elif os.path.isfile(path) and not _isExcluded(path): # If not given a directory, invoke the handler and return handler.handleFile(os.path.abspath(path)) return for root, dirs, files in [os_walk_unpack(w) for w in os_walk(path)]: root = root if type(root) is str else str(root, fs_encoding) dirs.sort() files.sort() for f in list(files): f_key = f f = f if type(f) is str else str(f, fs_encoding) f = os.path.abspath(os.path.join(root, f)) if not os.path.isfile(f) or _isExcluded(f): files.remove(f_key) continue try: handler.handleFile(f) except StopIteration: return if files: handler.handleDirectory(root, files) if not recursive: break class FileHandler(object): """A handler interface for :func:`eyed3.utils.walk` callbacks.""" def handleFile(self, f): """Called for each file walked. The file ``f`` is the full path and the return value is ignored. If the walk should abort the method should raise a ``StopIteration`` exception.""" pass def handleDirectory(self, d, files): """Called for each directory ``d`` **after** ``handleFile`` has been called for each file in ``files``. ``StopIteration`` may be raised to halt iteration.""" pass def handleDone(self): """Called when there are no more files to handle.""" pass def _requireArgType(arg_type, *args): arg_indices = [] kwarg_names = [] for a in args: if type(a) is int: arg_indices.append(a) else: kwarg_names.append(a) assert(arg_indices or kwarg_names) def wrapper(fn): def wrapped_fn(*args, **kwargs): for i in arg_indices: if i >= len(args): # The ith argument is not there, as in optional arguments break if args[i] is not None and not isinstance(args[i], arg_type): raise TypeError("%s(argument %d) must be %s" % (fn.__name__, i, str(arg_type))) for name in kwarg_names: if (name in kwargs and kwargs[name] is not None and not isinstance(kwargs[name], arg_type)): raise TypeError("%s(argument %s) must be %s" % (fn.__name__, name, str(arg_type))) return fn(*args, **kwargs) return wrapped_fn return wrapper def requireUnicode(*args): """Function decorator to enforce str/unicode argument types. ``None`` is a valid argument value, in all cases, regardless of not being unicode. ``*args`` Positional arguments may be numeric argument index values (requireUnicode(1, 3) - requires argument 1 and 3 are unicode) or keyword argument names (requireUnicode("title")) or a combination thereof. """ return _requireArgType(str, *args) def requireBytes(*args): """Function decorator to enforce byte string argument types. """ return _requireArgType(bytes, *args) def formatTime(seconds, total=None, short=False): """ Format ``seconds`` (number of seconds) as a string representation. When ``short`` is False (the default) the format is: HH:MM:SS. Otherwise, the format is exacly 6 characters long and of the form: 1w 3d 2d 4h 1h 5m 1m 4s 15s If ``total`` is not None it will also be formatted and appended to the result seperated by ' / '. """ seconds = round(seconds) def time_tuple(ts): if ts is None or ts < 0: ts = 0 hours = ts / 3600 mins = (ts % 3600) / 60 secs = (ts % 3600) % 60 tstr = '%02d:%02d' % (mins, secs) if int(hours): tstr = '%02d:%s' % (hours, tstr) return (int(hours), int(mins), int(secs), tstr) if not short: hours, mins, secs, curr_str = time_tuple(seconds) retval = curr_str if total: hours, mins, secs, total_str = time_tuple(total) retval += ' / %s' % total_str return retval else: units = [ ('y', 60 * 60 * 24 * 7 * 52), ('w', 60 * 60 * 24 * 7), ('d', 60 * 60 * 24), ('h', 60 * 60), ('m', 60), ('s', 1), ] seconds = int(seconds) if seconds < 60: return ' {0:02d}s'.format(seconds) for i in range(len(units) - 1): unit1, limit1 = units[i] unit2, limit2 = units[i + 1] if seconds >= limit1: return '{0:02d}{1}{2:02d}{3}'.format( seconds // limit1, unit1, (seconds % limit1) // limit2, unit2) return ' ~inf' # Number of bytes per KB (2^10) KB_BYTES = 1024 # Number of bytes per MB (2^20) MB_BYTES = 1048576 # Number of bytes per GB (2^30) GB_BYTES = 1073741824 # Kilobytes abbreviation KB_UNIT = "KB" # Megabytes abbreviation MB_UNIT = "MB" # Gigabytes abbreviation GB_UNIT = "GB" def formatSize(size, short=False): """Format ``size`` (number of bytes) into string format doing KB, MB, or GB conversion where necessary. When ``short`` is False (the default) the format is smallest unit of bytes and largest gigabytes; '234 GB'. The short version is 2-4 characters long and of the form 256b 64k 1.1G """ if not short: unit = "Bytes" if size >= GB_BYTES: size = float(size) / float(GB_BYTES) unit = GB_UNIT elif size >= MB_BYTES: size = float(size) / float(MB_BYTES) unit = MB_UNIT elif size >= KB_BYTES: size = float(size) / float(KB_BYTES) unit = KB_UNIT return "%.2f %s" % (size, unit) else: suffixes = ' kMGTPEH' if size == 0: num_scale = 0 else: num_scale = int(math.floor(math.log(size) / math.log(1000))) if num_scale > 7: suffix = '?' else: suffix = suffixes[num_scale] num_scale = int(math.pow(1000, num_scale)) value = size / num_scale str_value = str(value) if len(str_value) >= 3 and str_value[2] == '.': str_value = str_value[:2] else: str_value = str_value[:3] return "{0:>3s}{1}".format(str_value, suffix) def formatTimeDelta(td): """Format a timedelta object ``td`` into a string. """ days = td.days hours = td.seconds / 3600 mins = (td.seconds % 3600) / 60 secs = (td.seconds % 3600) % 60 tstr = "%02d:%02d:%02d" % (hours, mins, secs) if days: tstr = "%d days %s" % (days, tstr) return tstr def chunkCopy(src_fp, dest_fp, chunk_sz=(1024 * 512)): """Copy ``src_fp`` to ``dest_fp`` in ``chunk_sz`` byte increments.""" done = False while not done: data = src_fp.read(chunk_sz) if data: dest_fp.write(data) else: done = True del data class ArgumentParser(argparse.ArgumentParser): """Subclass of argparse.ArgumentParser that adds version and log level options.""" def __init__(self, *args, **kwargs): from eyed3 import version as VERSION from eyed3.utils.log import LEVELS from eyed3.utils.log import MAIN_LOGGER def pop_kwarg(name, default): if name in kwargs: value = kwargs.pop(name) or default else: value = default return value main_logger = pop_kwarg("main_logger", MAIN_LOGGER) version = pop_kwarg("version", VERSION) self.log_levels = [logging.getLevelName(l).lower() for l in LEVELS] formatter = argparse.RawDescriptionHelpFormatter super(ArgumentParser, self).__init__(*args, formatter_class=formatter, **kwargs) self.add_argument("--version", action="version", version=version, help="Display version information and exit") self.add_argument("--about", action="store_true", dest="about_eyed3", help="Display full version, release name, additional info, and exit") debug_group = self.add_argument_group("Debugging") debug_group.add_argument( "-l", "--log-level", metavar="LEVEL[:LOGGER]", action=LoggingAction, main_logger=main_logger, help="Set a log level. This option may be specified multiple " "times. If a logger name is specified than the level " "applies only to that logger, otherwise the level is set " "on the top-level logger. Acceptable levels are %s. " % (", ".join("'%s'" % l for l in self.log_levels))) debug_group.add_argument("--profile", action="store_true", default=False, dest="debug_profile", help="Run using python profiler.") debug_group.add_argument("--pdb", action="store_true", dest="debug_pdb", help="Drop into 'pdb' when errors occur.") def parse_args(self, *args, **kwargs): args = super().parse_args(*args, **kwargs) if "about_eyed3" in args and args.about_eyed3: action = [a for a in self._actions if isinstance(a, argparse._VersionAction)][0] version = action.version release_name = f" {__release_name__}" if __release_name__ else "" print(f"{version}{release_name}\n\n{__version_txt__}") self.exit() else: return args class LoggingAction(argparse._AppendAction): def __init__(self, *args, **kwargs): self.main_logger = kwargs.pop("main_logger") super(LoggingAction, self).__init__(*args, **kwargs) def __call__(self, parser, namespace, values, option_string=None): values = values.split(':') level, logger = values if len(values) > 1 else (values[0], self.main_logger) logger = logging.getLogger(logger) try: logger.setLevel(logging._nameToLevel[level.upper()]) except KeyError: msg = f"invalid level choice: {level} (choose from {parser.log_levels})" raise argparse.ArgumentError(self, msg) super(LoggingAction, self).__call__(parser, namespace, values, option_string) def datePicker(thing, prefer_recording_date=False): """This function returns a date of some sort, amongst all the possible dates (members called release_date, original_release_date, and recording_date of type eyed3.core.Date). The order of preference is: 1) date of original release 2) date of this versions release 3) the recording date. Unless ``prefer_recording_date`` is ``True`` in which case the order is 3, 1, 2. ``None`` will be returned if no dates are available.""" if not prefer_recording_date: return (thing.original_release_date or thing.release_date or thing.recording_date) else: return (thing.recording_date or thing.original_release_date or thing.release_date) def makeUniqueFileName(file_path, uniq=''): """The ``file_path`` is the desired file name, and it is returned if the file does not exist. In the case that it already exists the path is adjusted to be unique. First, the ``uniq`` string is added, and then a counter is used to find a unique name.""" path = os.path.dirname(file_path) file = os.path.basename(file_path) name, ext = os.path.splitext(file) count = 1 while os.path.exists(os.path.join(path, file)): if uniq: name = "%s_%s" % (name, uniq) file = "".join([name, ext]) uniq = '' else: file = "".join(["%s_%s" % (name, count), ext]) count += 1 return os.path.join(path, file) def b(x, encoder=None): """Converts `x` to a bytes string if not already. :param x: The string. :param encoder: Optional codec encoder to perform the conversion. The default is `codecs.latin_1_encode`. :return: The byte string if conversion was needed. """ if isinstance(x, bytes): return x else: import codecs encoder = encoder or codecs.latin_1_encode return encoder(x)[0] eyeD3-0.9.7/eyed3/utils/art.py000066400000000000000000000052451432016011500160100ustar00rootroot00000000000000from os.path import basename, splitext from fnmatch import fnmatch from ..id3.frames import ImageFrame FRONT_COVER = "FRONT_COVER" """Album front cover.""" BACK_COVER = "BACK_COVER" """Album back cover.""" MISC_COVER = "MISC_COVER" """Other part of the album cover; liner notes, gate-fold, etc.""" LOGO = "LOGO" """Artist/band logo.""" ARTIST = "ARTIST" """Artist/band images.""" LIVE = "LIVE" """Artist/band images.""" FILENAMES = { FRONT_COVER: ["cover-front", "cover-alternate*", "cover", "folder", "front", "cover-front_*", "flier"], BACK_COVER: ["cover-back", "back", "cover-back_*"], MISC_COVER: ["cover-insert*", "cover-liner*", "cover-disc", "cover-media*"], LOGO: ["logo*"], ARTIST: ["artist*"], LIVE: ["live*"], } """A mapping of art types to lists of filename patterns (excluding file extension): type -> [file_pattern, ..].""" TO_ID3_ART_TYPES = { FRONT_COVER: [ImageFrame.FRONT_COVER, ImageFrame.OTHER, ImageFrame.ICON, ImageFrame.LEAFLET], BACK_COVER: [ImageFrame.BACK_COVER], MISC_COVER: [ImageFrame.MEDIA], LOGO: [ImageFrame.BAND_LOGO], ARTIST: [ImageFrame.LEAD_ARTIST, ImageFrame.ARTIST, ImageFrame.BAND], LIVE: [ImageFrame.DURING_PERFORMANCE, ImageFrame.DURING_RECORDING] } """A mapping of art types to ID3 APIC (image) types: type -> [apic_type, ..]""" # ID3 image types not mapped above: # OTHER_ICON = 0x02 # CONDUCTOR = 0x09 # COMPOSER = 0x0B # LYRICIST = 0x0C # RECORDING_LOCATION = 0x0D # VIDEO = 0x10 # BRIGHT_COLORED_FISH = 0x11 # ILLUSTRATION = 0x12 # PUBLISHER_LOGO = 0x14 FROM_ID3_ART_TYPES = {} """A mapping of ID3 art types to eyeD3 art types; the opposite of TO_ID3_ART_TYPES.""" for _type in TO_ID3_ART_TYPES: for _id3_type in TO_ID3_ART_TYPES[_type]: FROM_ID3_ART_TYPES[_id3_type] = _type def matchArtFile(filename): """Compares ``filename`` (case insensitive) with lists of common art file names and returns the type of art that was matched, or None if no types were matched.""" base = splitext(basename(filename))[0] for type_ in FILENAMES.keys(): if True in [fnmatch(base.lower(), fname) for fname in FILENAMES[type_]]: return type_ return None def getArtFromTag(tag, type_=None): """Returns a list of eyed3.id3.frames.ImageFrame objects matching ``type_``, all if ``type_`` is None, or empty if tag does not contain art.""" art = [] for img in tag.images: if not type_ or type_ == img.picture_type: art.append(img) return art eyeD3-0.9.7/eyed3/utils/binfuncs.py000066400000000000000000000075641432016011500170370ustar00rootroot00000000000000################################################################################ # Copyright (C) 2001 Ryan Finne # Copyright (C) 2002-2011 Travis Shirk # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, see . # ################################################################################ import struct MAX_INT16 = (2 ** 16) // 2 MIN_INT16 = -(MAX_INT16 - 1) def bytes2bin(bites, sz=8): """Accepts a string of ``bytes`` (chars) and returns an array of bits representing the bytes in big endian byte order. An optional max ``sz`` for each byte (default 8 bits/byte) which can be used to mask out higher bits.""" if sz < 1 or sz > 8: raise ValueError(f"Invalid sz value: {sz}") retval = [] for b in [bytes([b]) for b in bites]: bits = [] b = ord(b) while b > 0: bits.append(b & 1) b >>= 1 if len(bits) < sz: bits.extend([0] * (sz - len(bits))) elif len(bits) > sz: bits = bits[:sz] # Big endian byte order. bits.reverse() retval.extend(bits) return retval def bin2bytes(x): """Convert an array of bits (MSB first) into a string of characters.""" bits = [] bits.extend(x) bits.reverse() i = 0 out = b'' multi = 1 ttl = 0 for b in bits: i += 1 ttl += b * multi multi *= 2 if i == 8: i = 0 out += bytes([ttl]) multi = 1 ttl = 0 if multi > 1: out += bytes([ttl]) out = bytearray(out) out.reverse() out = bytes(out) return out def bin2dec(x): """Convert ``x``, an array of "bits" (MSB first), to it's decimal value.""" bits = [] bits.extend(x) bits.reverse() # MSB multi = 1 value = 0 for b in bits: value += b * multi multi *= 2 return value def bytes2dec(bites, sz=8): return bin2dec(bytes2bin(bites, sz)) def dec2bin(n, p=1): """Convert a decimal value ``n`` to an array of bits (MSB first). Optionally, pad the overall size to ``p`` bits.""" assert n >= 0 if type(n) is not int: n = int(n) retval = [] while n > 0: retval.append(n & 1) n >>= 1 if p > 0: retval.extend([0] * (p - len(retval))) retval.reverse() return retval def dec2bytes(n, p=1): return bin2bytes(dec2bin(n, p)) def bin2synchsafe(x): """Convert ``x``, a list of bits (MSB first), to a synch safe list of bits. (section 6.2 of the ID3 2.4 spec).""" n = bin2dec(x) if len(x) > 32 or n > 268435456: # 2^28 raise ValueError("Invalid value: %s" % str(x)) elif len(x) < 8: return x bites = bytes([(n >> 21) & 0x7f, (n >> 14) & 0x7f, (n >> 7) & 0x7f, (n >> 0) & 0x7f, ]) bits = bytes2bin(bites) assert(len(bits) == 32) return bits def bytes2signedInt16(bites: bytes): if len(bites) != 2: raise ValueError("Signed 16 bit integer MUST be 2 bytes.") i = struct.unpack(">h", bites) return i[0] def signedInt162bytes(n: int): n = int(n) if MIN_INT16 <= n <= MAX_INT16: return struct.pack(">h", n) raise ValueError(f"Signed int16 out of range: {n}") eyeD3-0.9.7/eyed3/utils/console.py000066400000000000000000000422371432016011500166660ustar00rootroot00000000000000import os import struct import sys import time import typing from typing import Union try: import fcntl import termios import signal _CAN_RESIZE_TERMINAL = True except ImportError: _CAN_RESIZE_TERMINAL = False from . import formatSize, formatTime class AnsiCodes(object): _USE_ANSI = False _CSI = '\033[' def __init__(self, codes): def code_to_chars(code): return AnsiCodes._CSI + str(code) + 'm' for name in dir(codes): if not name.startswith('_'): value = getattr(codes, name) setattr(self, name, code_to_chars(value)) # Add color function for reset_name in ("RESET_%s" % name, "RESET"): if hasattr(codes, reset_name): reset_value = getattr(codes, reset_name) setattr(self, "%s" % name.lower(), AnsiCodes._mkfunc(code_to_chars(value), code_to_chars(reset_value))) break @staticmethod def _mkfunc(color, reset): def _cwrap(text, *styles): if not AnsiCodes._USE_ANSI: return text s = '' for st in styles: s += st s += color + text + reset if styles: s += Style.RESET_ALL return s return _cwrap def __getattribute__(self, name): attr = super(AnsiCodes, self).__getattribute__(name) if (hasattr(attr, "startswith") and attr.startswith(AnsiCodes._CSI) and not AnsiCodes._USE_ANSI): return "" else: return attr def __getitem__(self, name): return getattr(self, name.upper()) @classmethod def init(cls, allow_colors): cls._USE_ANSI = allow_colors and cls._term_supports_color() @staticmethod def _term_supports_color(): if (os.environ.get("TERM") == "dumb" or os.environ.get("OS") == "Windows_NT"): return False return hasattr(sys.stdout, "isatty") and sys.stdout.isatty() class AnsiFore: GREY = 30 # noqa RED = 31 # noqa GREEN = 32 # noqa YELLOW = 33 # noqa BLUE = 34 # noqa MAGENTA = 35 # noqa CYAN = 36 # noqa WHITE = 37 # noqa RESET = 39 # noqa class AnsiBack: GREY = 40 # noqa RED = 41 # noqa GREEN = 42 # noqa YELLOW = 43 # noqa BLUE = 44 # noqa MAGENTA = 45 # noqa CYAN = 46 # noqa WHITE = 47 # noqa RESET = 49 # noqa class AnsiStyle: RESET_ALL = 0 # noqa BRIGHT = 1 # noqa RESET_BRIGHT = 22 # noqa DIM = 2 # noqa RESET_DIM = RESET_BRIGHT # noqa ITALICS = 3 # noqa RESET_ITALICS = 23 # noqa UNDERLINE = 4 # noqa RESET_UNDERLINE = 24 # noqa BLINK_SLOW = 5 # noqa RESET_BLINK_SLOW = 25 # noqa BLINK_FAST = 6 # noqa RESET_BLINK_FAST = 26 # noqa INVERSE = 7 # noqa RESET_INVERSE = 27 # noqa STRIKE_THRU = 9 # noqa RESET_STRIKE_THRU = 29 # noqa Fore = AnsiCodes(AnsiFore) Back = AnsiCodes(AnsiBack) Style = AnsiCodes(AnsiStyle) def ERROR_COLOR(): return Fore.RED def WARNING_COLOR(): return Fore.YELLOW def HEADER_COLOR(): return Fore.GREEN class Spinner(object): """ A class to display a spinner in the terminal. It is designed to be used with the `with` statement:: with Spinner("Reticulating splines", "green") as s: for item in enumerate(items): s.next() """ _default_unicode_chars = "◓◑◒◐" _default_ascii_chars = "-/|\\" def __init__(self, msg, file=None, step=1, chars=None, use_unicode=True, print_done=True): self._msg = msg self._file = file or sys.stdout self._step = step if not chars: if use_unicode: chars = self._default_unicode_chars else: chars = self._default_ascii_chars self._chars = chars self._silent = not self._file.isatty() self._print_done = print_done def _iterator(self): chars = self._chars index = 0 write = self._file.write flush = self._file.flush while True: write("\r") write(self._msg) write(" ") write(chars[index]) flush() yield for _ in range(self._step): yield index += 1 if index == len(chars): index = 0 def __enter__(self): if self._silent: return self._silent_iterator() else: return self._iterator() def __exit__(self, exc_type, exc_value, traceback): write = self._file.write flush = self._file.flush if not self._silent: write("\r") write(self._msg) if self._print_done: if exc_type is None: write(Fore.GREEN + ' [Done]\n') else: write(Fore.RED + ' [Failed]\n') else: write("\n") flush() def _silent_iterator(self): self._file.write(self._msg) self._file.flush() while True: yield class ProgressBar(object): """ A class to display a progress bar in the terminal. It is designed to be used either with the `with` statement:: with ProgressBar(len(items)) as bar: for item in enumerate(items): bar.update() or as a generator:: for item in ProgressBar(items): item.process() """ def __init__(self, total_or_items: Union[int, typing.Sequence], file=None): """ total_or_items : int or sequence If an int, the number of increments in the process being tracked. If a sequence, the items to iterate over. file : writable file-like object, optional The file to write the progress bar to. Defaults to `sys.stdout`. If `file` is not a tty (as determined by calling its `isatty` member, if any), the scrollbar will be completely silent. """ self._file = file or sys.stdout if not self._file.isatty(): self.update = self._silent_update self._silent = True else: self._silent = False try: self._items = iter(total_or_items) self._total = len(total_or_items) except TypeError: try: self._total = int(total_or_items) self._items = iter(range(self._total)) except TypeError: raise TypeError("First argument must be int or sequence") self._start_time = time.time() self._should_handle_resize = ( _CAN_RESIZE_TERMINAL and self._file.isatty()) self._handle_resize() if self._should_handle_resize: signal.signal(signal.SIGWINCH, self._handle_resize) self._signal_set = True else: self._signal_set = False self.update(0) def _handle_resize(self, signum=None, frame=None): self._terminal_width = getTtySize(self._file, self._should_handle_resize)[1] def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): if not self._silent: if exc_type is None: self.update(self._total) self._file.write('\n') self._file.flush() if self._signal_set: signal.signal(signal.SIGWINCH, signal.SIG_DFL) def __iter__(self): return self def __next__(self): try: rv = next(self._items) except StopIteration: self.__exit__(None, None, None) raise else: self.update() return rv def update(self, value=None): """ Update the progress bar to the given value (out of the total given to the constructor). """ if value is None: value = self._current_value = self._current_value + 1 else: self._current_value = value if self._total == 0: frac = 1.0 else: frac = float(value) / float(self._total) file = self._file write = file.write suffix = self._formatSuffix(value, frac) self._bar_length = self._terminal_width - 37 bar_fill = int(float(self._bar_length) * frac) write("\r|") write(Fore.BLUE + '=' * bar_fill + Fore.RESET) if bar_fill < self._bar_length: write(Fore.GREEN + '>' + Fore.RESET) write("-" * (self._bar_length - bar_fill - 1)) write("|") write(suffix) self._file.flush() def _formatSuffix(self, value, frac): if value >= self._total: t = time.time() - self._start_time time_str = ' ' elif value <= 0: t = None time_str = '' else: t = ((time.time() - self._start_time) * (1.0 - frac)) / frac time_str = ' ETA ' if t is not None: time_str += formatTime(t, short=True) suffix = ' {0:>4s}/{1:>4s}'.format(formatSize(value, short=True), formatSize(self._total, short=True)) suffix += ' ({0:>6s}%)'.format("{0:.2f}".format(frac * 100.0)) suffix += time_str return suffix def _silent_update(self, value=None): pass @classmethod def map(cls, function, items, multiprocess=False, file=None): """ Does a `map` operation while displaying a progress bar with percentage complete. :: def work(i): print(i) ProgressBar.map(work, range(50)) Parameters: function : function Function to call for each step items : sequence Sequence where each element is a tuple of arguments to pass to *function*. multiprocess : bool, optional If `True`, use the `multiprocessing` module to distribute each task to a different processor core. file : writeable file-like object, optional The file to write the progress bar to. Defaults to `sys.stdout`. If `file` is not a tty (as determined by calling its `isatty` member, if any), the scrollbar will be completely silent. """ results = [] if file is None: file = sys.stdout with cls(len(items), file=file) as bar: step_size = max(200, bar._bar_length) steps = max(int(float(len(items)) / step_size), 1) if not multiprocess: for i, item in enumerate(items): function(item) if (i % steps) == 0: bar.update(i) else: import multiprocessing p = multiprocessing.Pool() for i, result in enumerate(p.imap_unordered(function, items, steps)): bar.update(i) results.append(result) return results def printMsg(s): fp = sys.stdout assert isinstance(s, str) try: fp.write("%s\n" % s) except UnicodeEncodeError: fp.write("%s\n" % str(s.encode("utf-8", "replace"), "utf-8")) fp.flush() def printError(s): _printWithColor(s, ERROR_COLOR(), sys.stderr) def printWarning(s): _printWithColor(s, WARNING_COLOR(), sys.stdout) def printHeader(s): _printWithColor(s, HEADER_COLOR(), sys.stdout) def boldText(s, c=None): return formatText(s, b=True, c=c) def formatText(s, b=False, c=None): return ((Style.BRIGHT if b else '') + (c or '') + s + (Fore.RESET if c else '') + (Style.RESET_BRIGHT if b else '')) def _printWithColor(s, color, file): assert isinstance(s, str) file.write(color + s + Fore.RESET + '\n') file.flush() def cformat(msg, fg, bg=None, styles=None): """Format ``msg`` with foreground and optional background. Optional ``styles`` lists will also be applied. The formatted string is returned.""" fg = fg or "" bg = bg or "" styles = "".join(styles or []) reset = Fore.RESET + Back.RESET + Style.RESET_ALL if (fg or bg or styles) \ else "" output = "%(fg)s%(bg)s%(styles)s%(msg)s%(reset)s" % locals() return output def getTtySize(fd=sys.stdout, check_tty=True): hw = None if check_tty: try: data = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * 4) hw = struct.unpack("hh", data) except (OSError, NameError): pass if not hw: try: hw = (int(os.environ.get('LINES')), int(os.environ.get('COLUMNS'))) except (TypeError, ValueError): hw = (78, 25) return hw def cprint(msg, fg, bg=None, styles=None, file=sys.stdout): """Calls ``cformat`` and prints the result to output stream ``file``.""" print(cformat(msg, fg, bg=bg, styles=styles), file=file) if __name__ == "__main__": AnsiCodes.init(True) def checkCode(c): return (c[0] != '_' and "RESET" not in c and c[0] == c[0].upper() ) for bg_name, bg_code in ((c, getattr(Back, c)) for c in dir(Back) if checkCode(c)): sys.stdout.write('%s%-7s%s %s ' % (bg_code, bg_name, Back.RESET, bg_code)) for _, fg_code in ((c, getattr(Fore, c)) for c in dir(Fore) if checkCode(c)): sys.stdout.write(fg_code) for st_name, st_code in ((c, getattr(Style, c)) for c in dir(Style) if checkCode(c)): sys.stdout.write('%s%s %s %s' % (st_code, st_name, getattr(Style, "RESET_%s" % st_name), bg_code)) sys.stdout.write("%s\n" % Style.RESET_ALL) sys.stdout.write("\n") with Spinner(Fore.GREEN + "Phase #1") as spinner: for _ in range(50): time.sleep(.05) next(spinner) with Spinner(Fore.RED + "Phase #2" + Fore.RESET, print_done=False) as spinner: for _ in range(50): time.sleep(.05) next(spinner) with Spinner("Phase #3", print_done=False, use_unicode=False) as spinner: for _ in range(50): next(spinner) time.sleep(.05) with Spinner("Phase #4", print_done=False, chars='.oO°Oo.') as spinner: for _ in range(50): next(spinner) time.sleep(.05) items = [x for x in range(200)] with ProgressBar(len(items)) as bar: for _ in enumerate(items): bar.update() time.sleep(.05) for _ in iter(ProgressBar(items)): time.sleep(.05) progress = 0 max_val = 320000000 with ProgressBar(max_val) as bar: while progress < max_val: progress += 23400 bar.update(progress) time.sleep(.001) eyeD3-0.9.7/eyed3/utils/log.py000066400000000000000000000027341432016011500160030ustar00rootroot00000000000000import logging from ..__about__ import __version__ as VERSION DEFAULT_FORMAT = '%(name)s:%(levelname)s: %(message)s' MAIN_LOGGER = "eyed3" # Add some levels logging.VERBOSE = logging.DEBUG + 1 logging.addLevelName(logging.VERBOSE, "VERBOSE") class Logger(logging.Logger): """Base class for all loggers""" def __init__(self, name): logging.Logger.__init__(self, name) # Using propagation of child to parent, by default self.propagate = True self.setLevel(logging.NOTSET) def verbose(self, msg, *args, **kwargs): """Log \a msg at 'verbose' level, debug < verbose < info""" self.log(logging.VERBOSE, msg, *args, **kwargs) def getLogger(name): og_class = logging.getLoggerClass() try: logging.setLoggerClass(Logger) return logging.getLogger(name) finally: logging.setLoggerClass(og_class) # The main 'eyed3' logger log = getLogger(MAIN_LOGGER) log.debug("eyeD3 version " + VERSION) del VERSION def initLogging(): """initialize the default logger with console output""" global log logging.basicConfig() # Don't propagate base 'eyed3' log.propagate = False console_handler = logging.StreamHandler() console_handler.setFormatter(logging.Formatter(DEFAULT_FORMAT)) log.addHandler(console_handler) log.setLevel(logging.ERROR) return log LEVELS = (logging.DEBUG, logging.VERBOSE, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL) eyeD3-0.9.7/eyed3/utils/prompt.py000066400000000000000000000052571432016011500165460ustar00rootroot00000000000000import sys as _sys from .console import Fore as fg DISABLE_PROMPT = None """Whenever a prompt occurs and this value is not ``None`` it can be ``exit`` to call sys.exit (see EXIT_STATUS) or ``raise`` to throw a RuntimeError, which can be caught if desired.""" EXIT_STATUS = 2 BOOL_TRUE_RESPONSES = ("yes", "y", "true") class PromptExit(RuntimeError): """Raised when ``DISABLE_PROMPT`` is 'raise' and ``prompt`` is called.""" pass def parseIntList(resp): ints = set() resp = resp.replace(',', ' ') for c in resp.split(): i = int(c) ints.add(i) return list(ints) def prompt(msg, default=None, required=True, type_=str, validate=None, choices=None): """Prompt user for input, the prequest is in ``msg``. If ``default`` is not ``None`` it will be displayed as the default and returned if not input is entered. The value ``None`` is only returned if ``required`` is ``False``. The response is passed to ``type_`` for conversion (default is unicode) before being returned. An optional list of valid responses can be provided in ``choices``.""" yes_no_prompt = default is True or default is False if yes_no_prompt: default_str = "Yn" if default is True else "yN" else: default_str = str(default) if default else None if default is not None: msg = "%s [%s]" % (msg, default_str) msg += ": " if not yes_no_prompt else "? " if DISABLE_PROMPT: if DISABLE_PROMPT == "exit": print(msg + "\nPrompting is disabled, exiting.") _sys.exit(EXIT_STATUS) else: raise PromptExit(msg) resp = None while resp is None: try: resp = input(msg) except EOFError: # Converting this allows main functions to catch without # catching other eofs raise PromptExit() if not resp and default not in (None, ""): resp = str(default) if resp: if yes_no_prompt: resp = True if resp.lower() in BOOL_TRUE_RESPONSES else False else: resp = resp.strip() try: resp = type_(resp) except Exception as ex: print(fg.red(str(ex))) resp = None elif not required: return None else: resp = None if ((choices and resp not in choices) or (validate and not validate(resp))): if choices: print(fg.red("Invalid response, choose from: ") + str(choices)) else: print(fg.red("Invalid response")) resp = None return resp eyeD3-0.9.7/poetry.lock000066400000000000000000003142101432016011500146660ustar00rootroot00000000000000[[package]] name = "alabaster" version = "0.7.12" description = "A configurable sidebar-enabled Sphinx theme" category = "dev" optional = false python-versions = "*" [[package]] name = "arrow" version = "1.2.3" description = "Better dates & times for Python" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] python-dateutil = ">=2.7.0" typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "atomicwrites" version = "1.4.1" description = "Atomic file writes." category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" version = "22.1.0" description = "Classes Without Boilerplate" category = "main" optional = false python-versions = ">=3.5" [package.extras] dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "Babel" version = "2.10.3" description = "Internationalization utilities" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] pytz = ">=2015.7" [[package]] name = "binaryornot" version = "0.4.4" description = "Ultra-lightweight pure Python package to check if a file is binary or text." category = "dev" optional = false python-versions = "*" [package.dependencies] chardet = ">=3.0.2" [[package]] name = "bleach" version = "5.0.1" description = "An easy safelist-based HTML-sanitizing tool." category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] six = ">=1.9.0" webencodings = "*" [package.extras] css = ["tinycss2 (>=1.1.0,<1.2)"] dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "mypy (==0.961)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)"] [[package]] name = "build" version = "0.8.0" description = "A simple, correct PEP 517 build frontend" category = "main" optional = true python-versions = ">=3.6" [package.dependencies] colorama = {version = "*", markers = "os_name == \"nt\""} importlib-metadata = {version = ">=0.22", markers = "python_version < \"3.8\""} packaging = ">=19.0" pep517 = ">=0.9.1" tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] docs = ["furo (>=2021.08.31)", "sphinx (>=4.0,<5.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)"] test = ["filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "toml (>=0.10.0)", "wheel (>=0.36.0)"] typing = ["importlib-metadata (>=4.6.4)", "mypy (==0.950)", "typing-extensions (>=3.7.4.3)"] virtualenv = ["virtualenv (>=20.0.35)"] [[package]] name = "certifi" version = "2022.9.24" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false python-versions = ">=3.6" [[package]] name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." category = "dev" optional = false python-versions = "*" [package.dependencies] pycparser = "*" [[package]] name = "chardet" version = "5.0.0" description = "Universal encoding detector for Python 3" category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "charset-normalizer" version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false python-versions = ">=3.6.0" [package.extras] unicode_backport = ["unicodedata2"] [[package]] name = "check-manifest" version = "0.45" description = "Check MANIFEST.in in a Python source package for completeness" category = "main" optional = true python-versions = ">=3.6" [package.dependencies] build = ">=0.1" setuptools = "*" toml = "*" [package.extras] test = ["mock (>=3.0.0)"] [[package]] name = "click" version = "8.1.3" description = "Composable command line interface toolkit" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "cogapp" version = "3.3.0" description = "Cog: A content generator for executing Python snippets in source files." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "colorama" version = "0.4.5" 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 = "dev" optional = false python-versions = "*" [package.extras] test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "cookiecutter" version = "2.1.1" description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] binaryornot = ">=0.4.4" click = ">=7.0,<9.0.0" Jinja2 = ">=2.7,<4.0.0" jinja2-time = ">=0.2.0" python-slugify = ">=4.0.0" pyyaml = ">=5.3.1" requests = ">=2.23.0" [[package]] name = "coverage" version = "5.5" description = "Code coverage measurement for Python" category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.dependencies] toml = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] toml = ["toml"] [[package]] name = "cryptography" version = "38.0.1" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] cffi = ">=1.12" [package.extras] docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] [[package]] name = "deprecation" version = "2.1.0" description = "A library to handle automated deprecations" category = "main" optional = false python-versions = "*" [package.dependencies] packaging = "*" [[package]] name = "distlib" version = "0.3.6" description = "Distribution utilities" category = "main" optional = true python-versions = "*" [[package]] name = "docutils" version = "0.17.1" description = "Docutils -- Python Documentation Utilities" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "factory-boy" version = "3.2.1" description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." category = "main" optional = true python-versions = ">=3.6" [package.dependencies] Faker = ">=0.7.0" [package.extras] dev = ["Django", "Pillow", "SQLAlchemy", "coverage", "flake8", "isort", "mongoengine", "tox", "wheel (>=0.32.0)", "zest.releaser[recommended]"] doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"] [[package]] name = "Faker" version = "15.0.0" description = "Faker is a Python package that generates fake data for you." category = "main" optional = true python-versions = ">=3.7" [package.dependencies] python-dateutil = ">=2.4" typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} [[package]] name = "filelock" version = "3.8.0" description = "A platform independent file lock." category = "main" optional = true python-versions = ">=3.7" [package.extras] docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] [[package]] name = "filetype" version = "1.1.0" description = "Infer file type and MIME type of any file/buffer. No external dependencies." category = "main" optional = false python-versions = "*" [[package]] name = "flake8" version = "3.9.2" description = "the modular source code checker: pep8 pyflakes and co" category = "main" optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.7.0,<2.8.0" pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "gitchangelog" version = "3.0.3" description = "gitchangelog generates a changelog thanks to git log." category = "dev" optional = false python-versions = "*" develop = false [package.extras] Mako = ["mako"] Mustache = ["pystache"] test = ["mako", "minimock", "nose", "pystache"] [package.source] type = "git" url = "https://github.com/nicfit/gitchangelog.git" reference = "nicfit.py" resolved_reference = "b86c6e90f48daf100cd701e0708973af3bb138e6" [[package]] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false python-versions = ">=3.5" [[package]] name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" version = "5.0.0" description = "Read metadata from Python packages" category = "main" optional = false python-versions = ">=3.7" [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] perf = ["ipython"] testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" category = "main" optional = true python-versions = "*" [[package]] name = "jaraco.classes" version = "3.2.3" description = "Utility functions for Python class constructs" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] more-itertools = "*" [package.extras] docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [[package]] name = "jeepney" version = "0.8.0" description = "Low-level, pure Python DBus protocol wrapper." category = "dev" optional = false python-versions = ">=3.7" [package.extras] test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] trio = ["async_generator", "trio"] [[package]] name = "Jinja2" version = "3.1.2" description = "A very fast and expressive template engine." category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] [[package]] name = "jinja2-time" version = "0.2.0" description = "Jinja2 Extension for Dates and Times" category = "dev" optional = false python-versions = "*" [package.dependencies] arrow = "*" jinja2 = "*" [[package]] name = "keyring" version = "23.9.3" description = "Store and access your passwords safely." category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} "jaraco.classes" = "*" jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [[package]] name = "MarkupSafe" version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "dev" optional = false python-versions = ">=3.7" [[package]] name = "mccabe" version = "0.6.1" description = "McCabe checker, plugin for flake8" category = "main" optional = true python-versions = "*" [[package]] name = "more-itertools" version = "8.14.0" description = "More routines for operating on iterables, beyond itertools" category = "dev" optional = false python-versions = ">=3.5" [[package]] name = "nicfit.py" version = "0.8.7" description = "Common Python utils (App, logging, config, etc.)" category = "dev" optional = false python-versions = "*" [package.dependencies] attrs = "*" cookiecutter = {version = "*", optional = true, markers = "extra == \"cookiecutter\""} deprecation = "*" PyYaml = "*" [package.extras] cookiecutter = ["cookiecutter"] shell = ["prompt-toolkit", "pygments"] [[package]] name = "packaging" version = "21.3" description = "Core utilities for Python packages" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "Paver" version = "1.3.4" description = "Easy build, distribution and deployment scripting" category = "dev" optional = false python-versions = "*" [package.dependencies] six = "*" [[package]] name = "pep517" version = "0.13.0" description = "Wrappers to build Python packages using PEP 517 hooks" category = "main" optional = true python-versions = ">=3.6" [package.dependencies] importlib_metadata = {version = "*", markers = "python_version < \"3.8\""} tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} zipp = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "Pillow" version = "9.2.0" description = "Python Imaging Library (Fork)" category = "main" optional = true python-versions = ">=3.7" [package.extras] docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] [[package]] name = "pkginfo" version = "1.8.3" description = "Query metadatdata from sdists / bdists / installed packages." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.extras] testing = ["coverage", "nose"] [[package]] name = "platformdirs" version = "2.5.2" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "main" optional = true python-versions = ">=3.7" [package.extras] docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" category = "main" optional = true python-versions = ">=3.6" [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "poetry-core" version = "1.3.2" description = "Poetry PEP 517 Build Backend" category = "dev" optional = false python-versions = ">=3.7,<4.0" [package.dependencies] importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} [[package]] name = "poetry2setup" version = "1.0.0" description = "Convert python-poetry to setup.py" category = "dev" optional = false python-versions = "~2.7 || ^3.5" develop = false [package.dependencies] poetry-core = "^1.0.0" [package.source] type = "git" url = "git@github.com:abersheeran/poetry2setup.git" reference = "HEAD" resolved_reference = "999c2611ab7dab8154efe3960523273bcc6563b6" [[package]] name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pycodestyle" version = "2.7.0" description = "Python style guide checker" category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pycparser" version = "2.21" description = "C parser in Python" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pyflakes" version = "2.3.1" description = "passive checker of Python programs" category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "Pygments" version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false python-versions = ">=3.6" [package.extras] plugins = ["importlib-metadata"] [[package]] name = "pylast" version = "4.5.0" description = "A Python interface to Last.fm and Libre.fm" category = "main" optional = true python-versions = ">=3.6" [package.dependencies] importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [package.extras] tests = ["flaky", "pytest", "pytest-cov", "pytest-random-order", "pyyaml"] [[package]] name = "pyparsing" version = "3.0.9" description = "pyparsing module - Classes and methods to define and execute parsing grammars" category = "main" optional = false python-versions = ">=3.6.8" [package.extras] diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" version = "6.2.5" description = "pytest: simple powerful testing with Python" category = "main" optional = true python-versions = ">=3.6" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" py = ">=1.8.2" toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] name = "pytest-cov" version = "2.12.1" description = "Pytest plugin for measuring coverage." category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] coverage = ">=5.2.1" pytest = ">=4.6" toml = "*" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" [package.dependencies] six = ">=1.5" [[package]] name = "python-slugify" version = "6.1.2" description = "A Python slugify application that also handles Unicode" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.dependencies] text-unidecode = ">=1.3" [package.extras] unidecode = ["Unidecode (>=1.1.1)"] [[package]] name = "pytz" version = "2022.4" description = "World timezone definitions, modern and historical" category = "dev" optional = false python-versions = "*" [[package]] name = "pywin32-ctypes" version = "0.2.0" description = "" category = "dev" optional = false python-versions = "*" [[package]] name = "PyYAML" version = "6.0" description = "YAML parser and emitter for Python" category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "readme-renderer" version = "37.2" description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] bleach = ">=2.1.0" docutils = ">=0.13.1" Pygments = ">=2.5.1" [package.extras] md = ["cmarkgfm (>=0.8.0)"] [[package]] name = "regarding" version = "0.1.4" description = "Create __about__.py files for Poetry and setup.py projects." category = "dev" optional = false python-versions = ">=3.6,<4.0" [package.dependencies] setuptools = ">=50.3.2,<51.0.0" toml = ">=0.10.2,<0.11.0" [[package]] name = "requests" version = "2.28.1" description = "Python HTTP for Humans." category = "main" optional = false python-versions = ">=3.7, <4" [package.dependencies] certifi = ">=2017.4.17" charset-normalizer = ">=2,<3" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-toolbelt" version = "0.10.0" description = "A utility belt for advanced users of python-requests" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.dependencies] requests = ">=2.0.1,<3.0.0" [[package]] name = "rfc3986" version = "2.0.0" description = "Validating URI References per RFC 3986" category = "dev" optional = false python-versions = ">=3.7" [package.extras] idna2008 = ["idna"] [[package]] name = "rich" version = "12.6.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "dev" optional = false python-versions = ">=3.6.3,<4.0.0" [package.dependencies] commonmark = ">=0.9.0,<0.10.0" pygments = ">=2.6.0,<3.0.0" typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] [[package]] name = "ruamel.yaml" version = "0.16.13" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" category = "main" optional = true python-versions = "*" [package.dependencies] "ruamel.yaml.clib" = {version = ">=0.1.2", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.10\""} [package.extras] docs = ["ryd"] jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] [[package]] name = "ruamel.yaml.clib" version = "0.2.6" description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" category = "main" optional = true python-versions = ">=3.5" [[package]] name = "SecretStorage" version = "3.3.3" description = "Python bindings to FreeDesktop.org Secret Service API" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] cryptography = ">=2.0" jeepney = ">=0.6" [[package]] name = "setuptools" version = "50.3.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false python-versions = ">=3.5" [package.extras] certs = ["certifi (==2016.9.26)"] docs = ["jaraco.packaging (>=6.1)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx"] ssl = ["wincertstore (==0.2)"] tests = ["coverage (>=4.5.1)", "flake8-2020", "jaraco.envs", "jaraco.test (>=3.1.1)", "mock", "paver", "pip (>=19.1)", "pytest (>=3.7)", "pytest-cov (>=2.5.1)", "pytest-flake8", "pytest-virtualenv (>=1.2.7)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." category = "dev" optional = false python-versions = "*" [[package]] name = "Sphinx" version = "5.2.3" description = "Python documentation generator" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] alabaster = ">=0.7,<0.8" babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} docutils = ">=0.14,<0.20" imagesize = ">=1.3" importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" Pygments = ">=2.12" requests = ">=2.5.0" snowballstemmer = ">=2.0" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"] test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] [[package]] name = "sphinx-issues" version = "3.0.1" description = "A Sphinx extension for linking to your project's issue tracker" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] sphinx = "*" [package.extras] dev = ["flake8 (==3.9.2)", "flake8-bugbear (==20.11.1)", "pre-commit (>=2.7,<3.0)", "pytest (>=6.2.0)", "tox"] lint = ["flake8 (==3.9.2)", "flake8-bugbear (==20.11.1)", "pre-commit (>=2.7,<3.0)"] tests = ["pytest (>=6.2.0)"] [[package]] name = "sphinx-rtd-theme" version = "1.0.0" description = "Read the Docs theme for Sphinx" category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" [package.dependencies] docutils = "<0.18" sphinx = ">=1.6" [package.extras] dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"] [[package]] name = "sphinxcontrib-applehelp" version = "1.0.2" description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" category = "dev" optional = false python-versions = ">=3.5" [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." category = "dev" optional = false python-versions = ">=3.5" [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.0.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" category = "dev" optional = false python-versions = ">=3.6" [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["html5lib", "pytest"] [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" category = "dev" optional = false python-versions = ">=3.5" [package.extras] test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." category = "dev" optional = false python-versions = ">=3.5" [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." category = "dev" optional = false python-versions = ">=3.5" [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] [[package]] name = "text-unidecode" version = "1.3" description = "The most basic Text::Unidecode port" category = "dev" optional = false python-versions = "*" [[package]] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" version = "2.0.1" description = "A lil' TOML parser" category = "main" optional = true python-versions = ">=3.7" [[package]] name = "tox" version = "3.26.0" description = "tox is a generic virtualenv management and test command line tool" category = "main" optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} filelock = ">=3.0.0" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} packaging = ">=14" pluggy = ">=0.12.0" py = ">=1.4.17" six = ">=1.14.0" tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" [package.extras] docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] [[package]] name = "twine" version = "4.0.1" description = "Collection of utilities for publishing packages on PyPI" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] importlib-metadata = ">=3.6" keyring = ">=15.1" pkginfo = ">=1.8.1" readme-renderer = ">=35.0" requests = ">=2.20" requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" rfc3986 = ">=1.4.0" rich = ">=12.0.0" urllib3 = ">=1.26.0" [[package]] name = "typing-extensions" version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false python-versions = ">=3.7" [[package]] name = "urllib3" version = "1.26.12" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" version = "20.16.5" description = "Virtual Python Environment builder" category = "main" optional = true python-versions = ">=3.6" [package.dependencies] distlib = ">=0.3.5,<1" filelock = ">=3.4.1,<4" importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} platformdirs = ">=2.4,<3" [package.extras] docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] [[package]] name = "webencodings" version = "0.5.1" description = "Character encoding aliases for legacy web content" category = "dev" optional = false python-versions = "*" [[package]] name = "wheel" version = "0.37.1" description = "A built-package format for Python" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.extras] test = ["pytest (>=3.0.0)", "pytest-cov"] [[package]] name = "zipp" version = "3.8.1" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" [package.extras] docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] art-plugin = ["Pillow", "pylast", "requests"] test = ["pytest", "pytest-cov", "tox", "factory-boy", "flake8", "check-manifest"] yaml-plugin = ["ruamel.yaml"] [metadata] lock-version = "1.1" python-versions = "^3.7" content-hash = "cf710315c822e731fd61f367c75bb2cbed1c085d72008a9e138f3f43bf4b1700" [metadata.files] alabaster = [ {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, ] arrow = [ {file = "arrow-1.2.3-py3-none-any.whl", hash = "sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2"}, {file = "arrow-1.2.3.tar.gz", hash = "sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1"}, ] atomicwrites = [ {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, ] attrs = [ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] Babel = [ {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, ] binaryornot = [ {file = "binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4"}, {file = "binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061"}, ] bleach = [ {file = "bleach-5.0.1-py3-none-any.whl", hash = "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a"}, {file = "bleach-5.0.1.tar.gz", hash = "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c"}, ] build = [ {file = "build-0.8.0-py3-none-any.whl", hash = "sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437"}, {file = "build-0.8.0.tar.gz", hash = "sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0"}, ] certifi = [ {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, ] cffi = [ {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, ] chardet = [ {file = "chardet-5.0.0-py3-none-any.whl", hash = "sha256:d3e64f022d254183001eccc5db4040520c0f23b1a3f33d6413e099eb7f126557"}, {file = "chardet-5.0.0.tar.gz", hash = "sha256:0368df2bfd78b5fc20572bb4e9bb7fb53e2c094f60ae9993339e8671d0afb8aa"}, ] charset-normalizer = [ {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, ] check-manifest = [ {file = "check-manifest-0.45.tar.gz", hash = "sha256:636b65a3b685374ad429ff22fe213966765b145f08bc560c8d033b604c7bee4c"}, {file = "check_manifest-0.45-py2.py3-none-any.whl", hash = "sha256:79dfd287348504a6f5195507dd15d0a6f66574feb34d3dbe1b33c80e24d2ceb9"}, ] click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] cogapp = [ {file = "cogapp-3.3.0-py2.py3-none-any.whl", hash = "sha256:8b5b5f6063d8ee231961c05da010cb27c30876b2279e23ad0eae5f8f09460d50"}, {file = "cogapp-3.3.0.tar.gz", hash = "sha256:1be95183f70282422d594fa42426be6923070a4bd8335621f6347f3aeee81db0"}, ] colorama = [ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] commonmark = [ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, ] cookiecutter = [ {file = "cookiecutter-2.1.1-py2.py3-none-any.whl", hash = "sha256:9f3ab027cec4f70916e28f03470bdb41e637a3ad354b4d65c765d93aad160022"}, {file = "cookiecutter-2.1.1.tar.gz", hash = "sha256:f3982be8d9c53dac1261864013fdec7f83afd2e42ede6f6dd069c5e149c540d5"}, ] coverage = [ {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] cryptography = [ {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:10d1f29d6292fc95acb597bacefd5b9e812099d75a6469004fd38ba5471a977f"}, {file = "cryptography-38.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3fc26e22840b77326a764ceb5f02ca2d342305fba08f002a8c1f139540cdfaad"}, {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b72c360427889b40f36dc214630e688c2fe03e16c162ef0aa41da7ab1455153"}, {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:194044c6b89a2f9f169df475cc167f6157eb9151cc69af8a2a163481d45cc407"}, {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca9f6784ea96b55ff41708b92c3f6aeaebde4c560308e5fbbd3173fbc466e94e"}, {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:16fa61e7481f4b77ef53991075de29fc5bacb582a1244046d2e8b4bb72ef66d0"}, {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d4ef6cc305394ed669d4d9eebf10d3a101059bdcf2669c366ec1d14e4fb227bd"}, {file = "cryptography-38.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3261725c0ef84e7592597606f6583385fed2a5ec3909f43bc475ade9729a41d6"}, {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0297ffc478bdd237f5ca3a7dc96fc0d315670bfa099c04dc3a4a2172008a405a"}, {file = "cryptography-38.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89ed49784ba88c221756ff4d4755dbc03b3c8d2c5103f6d6b4f83a0fb1e85294"}, {file = "cryptography-38.0.1-cp36-abi3-win32.whl", hash = "sha256:ac7e48f7e7261207d750fa7e55eac2d45f720027d5703cd9007e9b37bbb59ac0"}, {file = "cryptography-38.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ad7353f6ddf285aeadfaf79e5a6829110106ff8189391704c1d8801aa0bae45a"}, {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:896dd3a66959d3a5ddcfc140a53391f69ff1e8f25d93f0e2e7830c6de90ceb9d"}, {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d3971e2749a723e9084dd507584e2a2761f78ad2c638aa31e80bc7a15c9db4f9"}, {file = "cryptography-38.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:79473cf8a5cbc471979bd9378c9f425384980fcf2ab6534b18ed7d0d9843987d"}, {file = "cryptography-38.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9e69ae01f99abe6ad646947bba8941e896cb3aa805be2597a0400e0764b5818"}, {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5067ee7f2bce36b11d0e334abcd1ccf8c541fc0bbdaf57cdd511fdee53e879b6"}, {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3e3a2599e640927089f932295a9a247fc40a5bdf69b0484532f530471a382750"}, {file = "cryptography-38.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2e5856248a416767322c8668ef1845ad46ee62629266f84a8f007a317141013"}, {file = "cryptography-38.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64760ba5331e3f1794d0bcaabc0d0c39e8c60bf67d09c93dc0e54189dfd7cfe5"}, {file = "cryptography-38.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:b6c9b706316d7b5a137c35e14f4103e2115b088c412140fdbd5f87c73284df61"}, {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0163a849b6f315bf52815e238bc2b2346604413fa7c1601eea84bcddb5fb9ac"}, {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d1a5bd52d684e49a36582193e0b89ff267704cd4025abefb9e26803adeb3e5fb"}, {file = "cryptography-38.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:765fa194a0f3372d83005ab83ab35d7c5526c4e22951e46059b8ac678b44fa5a"}, {file = "cryptography-38.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:52e7bee800ec869b4031093875279f1ff2ed12c1e2f74923e8f49c916afd1d3b"}, {file = "cryptography-38.0.1.tar.gz", hash = "sha256:1db3d807a14931fa317f96435695d9ec386be7b84b618cc61cfa5d08b0ae33d7"}, ] deprecation = [ {file = "deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a"}, {file = "deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff"}, ] distlib = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, ] docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, ] factory-boy = [ {file = "factory_boy-3.2.1-py2.py3-none-any.whl", hash = "sha256:eb02a7dd1b577ef606b75a253b9818e6f9eaf996d94449c9d5ebb124f90dc795"}, {file = "factory_boy-3.2.1.tar.gz", hash = "sha256:a98d277b0c047c75eb6e4ab8508a7f81fb03d2cb21986f627913546ef7a2a55e"}, ] Faker = [ {file = "Faker-15.0.0-py3-none-any.whl", hash = "sha256:84c83f0ac1a2c8ecabd784c501aa0ef1d082d4aee52c3d797d586081c166434c"}, {file = "Faker-15.0.0.tar.gz", hash = "sha256:245fc7d23470dc57164bd9a59b7b1126e16289ffcf813d88a6c8e9b8a37ea3fb"}, ] filelock = [ {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, ] filetype = [ {file = "filetype-1.1.0-py2.py3-none-any.whl", hash = "sha256:117e25a50988d1a03a32ed510f4a15353e7291e683e94c63930497dd2c66ce24"}, {file = "filetype-1.1.0.tar.gz", hash = "sha256:afe4a00029601f66d239b72688065cc7c219dec1c927994f90b825e9e53d8f93"}, ] flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] gitchangelog = [] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] imagesize = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] importlib-metadata = [ {file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"}, {file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] "jaraco.classes" = [ {file = "jaraco.classes-3.2.3-py3-none-any.whl", hash = "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158"}, {file = "jaraco.classes-3.2.3.tar.gz", hash = "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a"}, ] jeepney = [ {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, ] Jinja2 = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] jinja2-time = [ {file = "jinja2-time-0.2.0.tar.gz", hash = "sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40"}, {file = "jinja2_time-0.2.0-py2.py3-none-any.whl", hash = "sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa"}, ] keyring = [ {file = "keyring-23.9.3-py3-none-any.whl", hash = "sha256:69732a15cb1433bdfbc3b980a8a36a04878a6cfd7cb99f497b573f31618001c0"}, {file = "keyring-23.9.3.tar.gz", hash = "sha256:69b01dd83c42f590250fe7a1f503fc229b14de83857314b1933a3ddbf595c4a5"}, ] MarkupSafe = [ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] more-itertools = [ {file = "more-itertools-8.14.0.tar.gz", hash = "sha256:c09443cd3d5438b8dafccd867a6bc1cb0894389e90cb53d227456b0b0bccb750"}, {file = "more_itertools-8.14.0-py3-none-any.whl", hash = "sha256:1bc4f91ee5b1b31ac7ceacc17c09befe6a40a503907baf9c839c229b5095cfd2"}, ] "nicfit.py" = [ {file = "nicfit.py-0.8.7-py3-none-any.whl", hash = "sha256:331b2288b57e2125ee814f044c3145687dcb5c1968008267c3460dc0e632972b"}, {file = "nicfit.py-0.8.7-py3.8.egg", hash = "sha256:246e452cad175aff1e790ee9166c17e3cc084d3470d4bcd9f181596982150b8f"}, {file = "nicfit.py-0.8.7.tar.gz", hash = "sha256:9b82ef588e6ec1fd6f24655058578a5943b23b540cfbda4973e5300c92ddf8f3"}, ] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] Paver = [ {file = "Paver-1.3.4-py2.py3-none-any.whl", hash = "sha256:aeca608dc680abf58675e12b78d02817beb6d7ea5ae58ff2f917776d22d176fd"}, {file = "Paver-1.3.4.tar.gz", hash = "sha256:d3e6498881485ab750efe40c5278982a9343bc627e137b11adced627719308c7"}, ] pep517 = [ {file = "pep517-0.13.0-py3-none-any.whl", hash = "sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b"}, {file = "pep517-0.13.0.tar.gz", hash = "sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59"}, ] Pillow = [ {file = "Pillow-9.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb"}, {file = "Pillow-9.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f"}, {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5"}, {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831e648102c82f152e14c1a0938689dbb22480c548c8d4b8b248b3e50967b88c"}, {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cc1d2451e8a3b4bfdb9caf745b58e6c7a77d2e469159b0d527a4554d73694d1"}, {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:136659638f61a251e8ed3b331fc6ccd124590eeff539de57c5f80ef3a9594e58"}, {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6e8c66f70fb539301e064f6478d7453e820d8a2c631da948a23384865cd95544"}, {file = "Pillow-9.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e"}, {file = "Pillow-9.2.0-cp310-cp310-win32.whl", hash = "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28"}, {file = "Pillow-9.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d"}, {file = "Pillow-9.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:adabc0bce035467fb537ef3e5e74f2847c8af217ee0be0455d4fec8adc0462fc"}, {file = "Pillow-9.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:336b9036127eab855beec9662ac3ea13a4544a523ae273cbf108b228ecac8437"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:15928f824870535c85dbf949c09d6ae7d3d6ac2d6efec80f3227f73eefba741c"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a"}, {file = "Pillow-9.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5b87da55a08acb586bad5c3aa3b86505f559b84f39035b233d5bf844b0834b1"}, {file = "Pillow-9.2.0-cp311-cp311-win32.whl", hash = "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf"}, {file = "Pillow-9.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6bf088c1ce160f50ea40764f825ec9b72ed9da25346216b91361eef8ad1b8f8c"}, {file = "Pillow-9.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069"}, {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eef7592281f7c174d3d6cbfbb7ee5984a671fcd77e3fc78e973d492e9bf0eb3f"}, {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcd7b9c7139dc8258d164b55696ecd16c04607f1cc33ba7af86613881ffe4ac8"}, {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a138441e95562b3c078746a22f8fca8ff1c22c014f856278bdbdd89ca36cff1b"}, {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:93689632949aff41199090eff5474f3990b6823404e45d66a5d44304e9cdc467"}, {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:f3fac744f9b540148fa7715a435d2283b71f68bfb6d4aae24482a890aed18b59"}, {file = "Pillow-9.2.0-cp37-cp37m-win32.whl", hash = "sha256:fa768eff5f9f958270b081bb33581b4b569faabf8774726b283edb06617101dc"}, {file = "Pillow-9.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:69bd1a15d7ba3694631e00df8de65a8cb031911ca11f44929c97fe05eb9b6c1d"}, {file = "Pillow-9.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:030e3460861488e249731c3e7ab59b07c7853838ff3b8e16aac9561bb345da14"}, {file = "Pillow-9.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:74a04183e6e64930b667d321524e3c5361094bb4af9083db5c301db64cd341f3"}, {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402"}, {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fd6f5e3c0e4697fa7eb45b6e93996299f3feee73a3175fa451f49a74d092b9f"}, {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8"}, {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:4134d3f1ba5f15027ff5c04296f13328fecd46921424084516bdb1b2548e66ff"}, {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1"}, {file = "Pillow-9.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1536ad017a9f789430fb6b8be8bf99d2f214c76502becc196c6f2d9a75b01b76"}, {file = "Pillow-9.2.0-cp38-cp38-win32.whl", hash = "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f"}, {file = "Pillow-9.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:ec52c351b35ca269cb1f8069d610fc45c5bd38c3e91f9ab4cbbf0aebc136d9c8"}, {file = "Pillow-9.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ed2c4ef2451de908c90436d6e8092e13a43992f1860275b4d8082667fbb2ffc"}, {file = "Pillow-9.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ad2f835e0ad81d1689f1b7e3fbac7b01bb8777d5a985c8962bedee0cc6d43da"}, {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea98f633d45f7e815db648fd7ff0f19e328302ac36427343e4432c84432e7ff4"}, {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7761afe0126d046974a01e030ae7529ed0ca6a196de3ec6937c11df0df1bc91c"}, {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a54614049a18a2d6fe156e68e188da02a046a4a93cf24f373bffd977e943421"}, {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:5aed7dde98403cd91d86a1115c78d8145c83078e864c1de1064f52e6feb61b20"}, {file = "Pillow-9.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:13b725463f32df1bfeacbf3dd197fb358ae8ebcd8c5548faa75126ea425ccb60"}, {file = "Pillow-9.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:808add66ea764ed97d44dda1ac4f2cfec4c1867d9efb16a33d158be79f32b8a4"}, {file = "Pillow-9.2.0-cp39-cp39-win32.whl", hash = "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885"}, {file = "Pillow-9.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:fac2d65901fb0fdf20363fbd345c01958a742f2dc62a8dd4495af66e3ff502a4"}, {file = "Pillow-9.2.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3"}, {file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c7b502bc34f6e32ba022b4a209638f9e097d7a9098104ae420eb8186217ebbb"}, {file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be"}, {file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:dfe4c1fedfde4e2fbc009d5ad420647f7730d719786388b7de0999bf32c0d9fd"}, {file = "Pillow-9.2.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:f07f1f00e22b231dd3d9b9208692042e29792d6bd4f6639415d2f23158a80013"}, {file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1802f34298f5ba11d55e5bb09c31997dc0c6aed919658dfdf0198a2fe75d5490"}, {file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17d4cafe22f050b46d983b71c707162d63d796a1235cdf8b9d7a112e97b15bac"}, {file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96b5e6874431df16aee0c1ba237574cb6dff1dcb173798faa6a9d8b399a05d0e"}, {file = "Pillow-9.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927"}, {file = "Pillow-9.2.0.tar.gz", hash = "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04"}, ] pkginfo = [ {file = "pkginfo-1.8.3-py2.py3-none-any.whl", hash = "sha256:848865108ec99d4901b2f7e84058b6e7660aae8ae10164e015a6dcf5b242a594"}, {file = "pkginfo-1.8.3.tar.gz", hash = "sha256:a84da4318dd86f870a9447a8c98340aa06216bfc6f2b7bdc4b8766984ae1867c"}, ] platformdirs = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] poetry-core = [ {file = "poetry-core-1.3.2.tar.gz", hash = "sha256:0ab006a40cb38d6a38b97264f6835da2f08a96912f2728ce668e9ac6a34f686f"}, {file = "poetry_core-1.3.2-py3-none-any.whl", hash = "sha256:ea0f5a90b339cde132b4e43cff78a1b440cd928db864bb67cfc97fdfcefe7218"}, ] poetry2setup = [] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, ] pycparser = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] Pygments = [ {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, ] pylast = [ {file = "pylast-4.5.0-py3-none-any.whl", hash = "sha256:47c3da663002f7fbacb7d561f8724cf007c22ac7c9c310c17ad987415273e7d0"}, {file = "pylast-4.5.0.tar.gz", hash = "sha256:62800b72e971dadac40c3d6f538c6188219d6f53853ddc48d69d9fc25659018f"}, ] pyparsing = [ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] pytest-cov = [ {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, ] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] python-slugify = [ {file = "python-slugify-6.1.2.tar.gz", hash = "sha256:272d106cb31ab99b3496ba085e3fea0e9e76dcde967b5e9992500d1f785ce4e1"}, {file = "python_slugify-6.1.2-py2.py3-none-any.whl", hash = "sha256:7b2c274c308b62f4269a9ba701aa69a797e9bca41aeee5b3a9e79e36b6656927"}, ] pytz = [ {file = "pytz-2022.4-py2.py3-none-any.whl", hash = "sha256:2c0784747071402c6e99f0bafdb7da0fa22645f06554c7ae06bf6358897e9c91"}, {file = "pytz-2022.4.tar.gz", hash = "sha256:48ce799d83b6f8aab2020e369b627446696619e79645419610b9facd909b3174"}, ] pywin32-ctypes = [ {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, ] PyYAML = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] readme-renderer = [ {file = "readme_renderer-37.2-py3-none-any.whl", hash = "sha256:d3f06a69e8c40fca9ab3174eca48f96d9771eddb43517b17d96583418427b106"}, {file = "readme_renderer-37.2.tar.gz", hash = "sha256:e8ad25293c98f781dbc2c5a36a309929390009f902f99e1798c761aaf04a7923"}, ] regarding = [ {file = "regarding-0.1.4-py3-none-any.whl", hash = "sha256:c128194beae914e2c50edb260dbbb5d72205fefad0ca5c399be993d8a6d965bd"}, {file = "regarding-0.1.4.tar.gz", hash = "sha256:c9c76b6135d4d267f596089434530db2b7194ec9ab84460230330a3f968716c9"}, ] requests = [ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, ] requests-toolbelt = [ {file = "requests-toolbelt-0.10.0.tar.gz", hash = "sha256:f695d6207931200b46c8ef6addbc8a921fb5d77cc4cd209c2e7d39293fcd2b30"}, {file = "requests_toolbelt-0.10.0-py2.py3-none-any.whl", hash = "sha256:64c6b8c51b515d123f9f708a29743f44eb70c4479440641ed2df8c4dea56d985"}, ] rfc3986 = [ {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, ] rich = [ {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, ] "ruamel.yaml" = [ {file = "ruamel.yaml-0.16.13-py2.py3-none-any.whl", hash = "sha256:64b06e7873eb8e1125525ecef7345447d786368cadca92a7cd9b59eae62e95a3"}, {file = "ruamel.yaml-0.16.13.tar.gz", hash = "sha256:bb48c514222702878759a05af96f4b7ecdba9b33cd4efcf25c86b882cef3a942"}, ] "ruamel.yaml.clib" = [ {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0"}, {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:066f886bc90cc2ce44df8b5f7acfc6a7e2b2e672713f027136464492b0c34d7c"}, {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7"}, {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win32.whl", hash = "sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee"}, {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de"}, {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"}, {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527"}, {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win32.whl", hash = "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5"}, {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win_amd64.whl", hash = "sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c"}, {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502"}, {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78"}, {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d3c620a54748a3d4cf0bcfe623e388407c8e85a4b06b8188e126302bcab93ea8"}, {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win32.whl", hash = "sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94"}, {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win_amd64.whl", hash = "sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468"}, {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd"}, {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99"}, {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:210c8fcfeff90514b7133010bf14e3bad652c8efde6b20e00c43854bf94fa5a6"}, {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win32.whl", hash = "sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb"}, {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe"}, {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233"}, {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84"}, {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:61bc5e5ca632d95925907c569daa559ea194a4d16084ba86084be98ab1cec1c6"}, {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win32.whl", hash = "sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b"}, {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277"}, {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed"}, {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0"}, {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:1b4139a6ffbca8ef60fdaf9b33dec05143ba746a6f0ae0f9d11d38239211d335"}, {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win32.whl", hash = "sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104"}, {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7"}, {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"}, ] SecretStorage = [ {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, ] setuptools = [ {file = "setuptools-50.3.2-py3-none-any.whl", hash = "sha256:2c242a0856fbad7efbe560df4a7add9324f340cf48df43651e9604924466794a"}, {file = "setuptools-50.3.2.zip", hash = "sha256:ed0519d27a243843b05d82a5e9d01b0b083d9934eaa3d02779a23da18077bd3c"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] snowballstemmer = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] Sphinx = [ {file = "Sphinx-5.2.3.tar.gz", hash = "sha256:5b10cb1022dac8c035f75767799c39217a05fc0fe2d6fe5597560d38e44f0363"}, {file = "sphinx-5.2.3-py3-none-any.whl", hash = "sha256:7abf6fabd7b58d0727b7317d5e2650ef68765bbe0ccb63c8795fa8683477eaa2"}, ] sphinx-issues = [ {file = "sphinx-issues-3.0.1.tar.gz", hash = "sha256:b7c1dc1f4808563c454d11c1112796f8c176cdecfee95f0fd2302ef98e21e3d6"}, {file = "sphinx_issues-3.0.1-py3-none-any.whl", hash = "sha256:8b25dc0301159375468f563b3699af7a63720fd84caf81c1442036fcd418b20c"}, ] sphinx-rtd-theme = [ {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, ] sphinxcontrib-applehelp = [ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, ] sphinxcontrib-devhelp = [ {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, ] sphinxcontrib-htmlhelp = [ {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, ] sphinxcontrib-jsmath = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, ] sphinxcontrib-qthelp = [ {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, ] sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] text-unidecode = [ {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] tox = [ {file = "tox-3.26.0-py2.py3-none-any.whl", hash = "sha256:bf037662d7c740d15c9924ba23bb3e587df20598697bb985ac2b49bdc2d847f6"}, {file = "tox-3.26.0.tar.gz", hash = "sha256:44f3c347c68c2c68799d7d44f1808f9d396fc8a1a500cbc624253375c7ae107e"}, ] twine = [ {file = "twine-4.0.1-py3-none-any.whl", hash = "sha256:42026c18e394eac3e06693ee52010baa5313e4811d5a11050e7d48436cf41b9e"}, {file = "twine-4.0.1.tar.gz", hash = "sha256:96b1cf12f7ae611a4a40b6ae8e9570215daff0611828f5fe1f37a16255ab24a0"}, ] typing-extensions = [ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] urllib3 = [ {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, ] virtualenv = [ {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"}, {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"}, ] webencodings = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] wheel = [ {file = "wheel-0.37.1-py2.py3-none-any.whl", hash = "sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a"}, {file = "wheel-0.37.1.tar.gz", hash = "sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4"}, ] zipp = [ {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, ] eyeD3-0.9.7/pyproject.toml000066400000000000000000000055561432016011500154200ustar00rootroot00000000000000[build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" [tool.regarding] release_name = "Sunshine" years = "2002-2022" [tool.poetry] name = "eyeD3" version = "0.9.7" description = "Python audio data toolkit (ID3 and MP3)" authors = ["Travis Shirk "] maintainers = ["Travis Shirk "] homepage = "https://eyeD3.nicfit.net/" repository = "https://github.com/nicfit/eyeD3" license = "GPL-3.0-or-later" classifiers = [ "Environment :: Console", "Intended Audience :: End Users/Desktop", "Topic :: Multimedia :: Sound/Audio :: Editors", "Topic :: Software Development :: Libraries :: Python Modules", "Intended Audience :: Developers", "Operating System :: POSIX", "Natural Language :: English", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Programming Language :: Python", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", ] keywords = ["id3", "mp3", "python"] readme = "README.rst" packages = [ {include = "eyed3"}, ] include = [ "poetry.lock", "README.rst", "LICENSE", "Makefile", "requirements.txt", "MANIFEST.in", "AUTHORS.rst", "CONTRIBUTING.rst", "HISTORY.rst", "tox.ini", "tests/**/*.py", "docs/", "requirements/*.txt", "examples/*", "eyed3/plugins/DisplayPattern.ebnf", ] exclude = [ "docs/_build", ] [tool.poetry.scripts] eyeD3 = "eyed3.main:_main" [tool.poetry.dependencies] python = "^3.7" filetype = "^1.0.7" deprecation = "^2.1.0" # yaml-plugin extra "ruamel.yaml" = {version = "^0.16.12", optional = true} # art-plugin extra Pillow = {version = ">=8.0.1,<10.0.0", optional = true} pylast = {version = "^4.0.0", optional = true} requests = {version = "^2.25.0", optional = true} # Test extra pytest = {version = "^6.2.1", optional = true} coverage = {version = "^5.3.1", optional = true, extras = ["toml"]} pytest-cov = {version = "^2.10.1", optional = true} tox = {version = "^3.20.1", optional = true} factory-boy = {version = "^3.1.0", optional = true} flake8 = {version = "^3.8.4", optional = true} check-manifest = {version = "^0.45", optional = true} [tool.poetry.extras] test = ["pytest", "pytest-cov", "tox", "factory-boy", "flake8", "check-manifest"] yaml-plugin = ["ruamel.yaml"] art-plugin = ["Pillow", "pylast", "requests"] [tool.poetry.group.dev.dependencies] gitchangelog = {git = "https://github.com/nicfit/gitchangelog.git", rev = "nicfit.py"} regarding = "^0.1.4" wheel = "^0.37.1" twine = "^4.0.1" Sphinx = "^5.2.3" sphinx-rtd-theme = "^1.0.0" sphinx-issues = "^3.0.1" cogapp = "^3.3.0" Paver = "^1.3.4" "nicfit.py" = {version = "0.8.7", extras = ["cookiecutter"]} poetry2setup = {git = "git@github.com:abersheeran/poetry2setup.git"} [tool.coverage.html] directory = "build/tests/coverage" eyeD3-0.9.7/requirements/000077500000000000000000000000001432016011500152145ustar00rootroot00000000000000eyeD3-0.9.7/requirements/dev-requirements.txt000066400000000000000000000125461432016011500212640ustar00rootroot00000000000000alabaster==0.7.12 ; python_version >= "3.7" and python_version < "4.0" arrow==1.2.3 ; python_version >= "3.7" and python_version < "4.0" attrs==22.1.0 ; python_version >= "3.7" and python_version < "4.0" babel==2.10.3 ; python_version >= "3.7" and python_version < "4.0" binaryornot==0.4.4 ; python_version >= "3.7" and python_version < "4.0" bleach==5.0.1 ; python_version >= "3.7" and python_version < "4.0" certifi==2022.9.24 ; python_version >= "3.7" and python_version < "4" cffi==1.15.1 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "linux" chardet==5.0.0 ; python_version >= "3.7" and python_version < "4.0" charset-normalizer==2.1.1 ; python_version >= "3.7" and python_version < "4" click==8.1.3 ; python_version >= "3.7" and python_version < "4.0" cogapp==3.3.0 ; python_version >= "3.7" and python_version < "4.0" colorama==0.4.5 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" or python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" commonmark==0.9.1 ; python_version >= "3.7" and python_version < "4.0" cookiecutter==2.1.1 ; python_version >= "3.7" and python_version < "4.0" cryptography==38.0.1 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "linux" deprecation==2.1.0 ; python_version >= "3.7" and python_version < "4.0" docutils==0.17.1 ; python_version >= "3.7" and python_version < "4.0" filetype==1.1.0 ; python_version >= "3.7" and python_version < "4.0" gitchangelog @ git+https://github.com/nicfit/gitchangelog.git@nicfit.py ; python_version >= "3.7" and python_version < "4.0" idna==3.4 ; python_version >= "3.7" and python_version < "4" imagesize==1.4.1 ; python_version >= "3.7" and python_version < "4.0" importlib-metadata==5.0.0 ; python_version >= "3.7" and python_version < "4.0" jaraco-classes==3.2.3 ; python_version >= "3.7" and python_version < "4.0" jeepney==0.8.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "linux" jinja2-time==0.2.0 ; python_version >= "3.7" and python_version < "4.0" jinja2==3.1.2 ; python_version >= "3.7" and python_version < "4.0" keyring==23.9.3 ; python_version >= "3.7" and python_version < "4.0" markupsafe==2.1.1 ; python_version >= "3.7" and python_version < "4.0" more-itertools==8.14.0 ; python_version >= "3.7" and python_version < "4.0" nicfit-py[cookiecutter]==0.8.7 ; python_version >= "3.7" and python_version < "4.0" packaging==21.3 ; python_version >= "3.7" and python_version < "4.0" paver==1.3.4 ; python_version >= "3.7" and python_version < "4.0" pkginfo==1.8.3 ; python_version >= "3.7" and python_version < "4.0" poetry-core==1.3.2 ; python_version >= "3.7" and python_version < "4.0" poetry2setup @ git+ssh://git@github.com/abersheeran/poetry2setup.git@HEAD ; python_version >= "3.7" and python_version < "4.0" pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "linux" pygments==2.13.0 ; python_version >= "3.7" and python_version < "4.0" pyparsing==3.0.9 ; python_version >= "3.7" and python_version < "4.0" python-dateutil==2.8.2 ; python_version >= "3.7" and python_version < "4.0" python-slugify==6.1.2 ; python_version >= "3.7" and python_version < "4.0" pytz==2022.4 ; python_version >= "3.7" and python_version < "4.0" pywin32-ctypes==0.2.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" pyyaml==6.0 ; python_version >= "3.7" and python_version < "4.0" readme-renderer==37.2 ; python_version >= "3.7" and python_version < "4.0" regarding==0.1.4 ; python_version >= "3.7" and python_version < "4.0" requests-toolbelt==0.10.0 ; python_version >= "3.7" and python_version < "4.0" requests==2.28.1 ; python_version >= "3.7" and python_version < "4" rfc3986==2.0.0 ; python_version >= "3.7" and python_version < "4.0" rich==12.6.0 ; python_version >= "3.7" and python_version < "4.0" secretstorage==3.3.3 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "linux" setuptools==50.3.2 ; python_version >= "3.7" and python_version < "4.0" six==1.16.0 ; python_version >= "3.7" and python_version < "4.0" snowballstemmer==2.2.0 ; python_version >= "3.7" and python_version < "4.0" sphinx-issues==3.0.1 ; python_version >= "3.7" and python_version < "4.0" sphinx-rtd-theme==1.0.0 ; python_version >= "3.7" and python_version < "4.0" sphinx==5.2.3 ; python_version >= "3.7" and python_version < "4.0" sphinxcontrib-applehelp==1.0.2 ; python_version >= "3.7" and python_version < "4.0" sphinxcontrib-devhelp==1.0.2 ; python_version >= "3.7" and python_version < "4.0" sphinxcontrib-htmlhelp==2.0.0 ; python_version >= "3.7" and python_version < "4.0" sphinxcontrib-jsmath==1.0.1 ; python_version >= "3.7" and python_version < "4.0" sphinxcontrib-qthelp==1.0.3 ; python_version >= "3.7" and python_version < "4.0" sphinxcontrib-serializinghtml==1.1.5 ; python_version >= "3.7" and python_version < "4.0" text-unidecode==1.3 ; python_version >= "3.7" and python_version < "4.0" toml==0.10.2 ; python_version >= "3.7" and python_version < "4.0" twine==4.0.1 ; python_version >= "3.7" and python_version < "4.0" typing-extensions==4.4.0 ; python_version >= "3.7" and python_version < "3.9" urllib3==1.26.12 ; python_version >= "3.7" and python_version < "4" webencodings==0.5.1 ; python_version >= "3.7" and python_version < "4.0" wheel==0.37.1 ; python_version >= "3.7" and python_version < "4.0" zipp==3.8.1 ; python_version >= "3.7" and python_version < "4.0" eyeD3-0.9.7/requirements/extra-requirements.txt000066400000000000000000000022341432016011500216220ustar00rootroot00000000000000certifi==2022.9.24 ; python_version >= "3.7" and python_version < "4" charset-normalizer==2.1.1 ; python_version >= "3.7" and python_version < "4" deprecation==2.1.0 ; python_version >= "3.7" and python_version < "4.0" filetype==1.1.0 ; python_version >= "3.7" and python_version < "4.0" idna==3.4 ; python_version >= "3.7" and python_version < "4" importlib-metadata==5.0.0 ; python_version >= "3.7" and python_version < "3.8" packaging==21.3 ; python_version >= "3.7" and python_version < "4.0" pillow==9.2.0 ; python_version >= "3.7" and python_version < "4.0" pylast==4.5.0 ; python_version >= "3.7" and python_version < "4.0" pyparsing==3.0.9 ; python_version >= "3.7" and python_version < "4.0" requests==2.28.1 ; python_version >= "3.7" and python_version < "4" ruamel-yaml-clib==0.2.6 ; platform_python_implementation == "CPython" and python_version < "3.10" and python_version >= "3.7" ruamel-yaml==0.16.13 ; python_version >= "3.7" and python_version < "4.0" typing-extensions==4.4.0 ; python_version >= "3.7" and python_version < "3.8" urllib3==1.26.12 ; python_version >= "3.7" and python_version < "4" zipp==3.8.1 ; python_version >= "3.7" and python_version < "3.8" eyeD3-0.9.7/requirements/requirements.txt000066400000000000000000000011601432016011500204760ustar00rootroot00000000000000certifi==2022.9.24 ; python_version >= "3.7" and python_version < "4" charset-normalizer==2.1.1 ; python_version >= "3.7" and python_version < "4" deprecation==2.1.0 ; python_version >= "3.7" and python_version < "4.0" filetype==1.1.0 ; python_version >= "3.7" and python_version < "4.0" idna==3.4 ; python_version >= "3.7" and python_version < "4" packaging==21.3 ; python_version >= "3.7" and python_version < "4.0" pyparsing==3.0.9 ; python_version >= "3.7" and python_version < "4.0" requests==2.28.1 ; python_version >= "3.7" and python_version < "4" urllib3==1.26.12 ; python_version >= "3.7" and python_version < "4" eyeD3-0.9.7/requirements/test-requirements.txt000066400000000000000000000057701432016011500214660ustar00rootroot00000000000000atomicwrites==1.4.1 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" attrs==22.1.0 ; python_version >= "3.7" and python_version < "4.0" build==0.8.0 ; python_version >= "3.7" and python_version < "4.0" certifi==2022.9.24 ; python_version >= "3.7" and python_version < "4" charset-normalizer==2.1.1 ; python_version >= "3.7" and python_version < "4" check-manifest==0.45 ; python_version >= "3.7" and python_version < "4.0" colorama==0.4.5 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" or python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" or python_version >= "3.7" and python_version < "4.0" and os_name == "nt" coverage==5.5 ; python_version >= "3.7" and python_version < "4" coverage[toml]==5.5 ; python_version >= "3.7" and python_version < "4" deprecation==2.1.0 ; python_version >= "3.7" and python_version < "4.0" distlib==0.3.6 ; python_version >= "3.7" and python_version < "4.0" factory-boy==3.2.1 ; python_version >= "3.7" and python_version < "4.0" faker==15.0.0 ; python_version >= "3.7" and python_version < "4.0" filelock==3.8.0 ; python_version >= "3.7" and python_version < "4.0" filetype==1.1.0 ; python_version >= "3.7" and python_version < "4.0" flake8==3.9.2 ; python_version >= "3.7" and python_version < "4.0" idna==3.4 ; python_version >= "3.7" and python_version < "4" importlib-metadata==5.0.0 ; python_version >= "3.7" and python_version < "3.8" iniconfig==1.1.1 ; python_version >= "3.7" and python_version < "4.0" mccabe==0.6.1 ; python_version >= "3.7" and python_version < "4.0" packaging==21.3 ; python_version >= "3.7" and python_version < "4.0" pep517==0.13.0 ; python_version >= "3.7" and python_version < "4.0" platformdirs==2.5.2 ; python_version >= "3.7" and python_version < "4.0" pluggy==1.0.0 ; python_version >= "3.7" and python_version < "4.0" py==1.11.0 ; python_version >= "3.7" and python_version < "4.0" pycodestyle==2.7.0 ; python_version >= "3.7" and python_version < "4.0" pyflakes==2.3.1 ; python_version >= "3.7" and python_version < "4.0" pyparsing==3.0.9 ; python_version >= "3.7" and python_version < "4.0" pytest-cov==2.12.1 ; python_version >= "3.7" and python_version < "4.0" pytest==6.2.5 ; python_version >= "3.7" and python_version < "4.0" python-dateutil==2.8.2 ; python_version >= "3.7" and python_version < "4.0" requests==2.28.1 ; python_version >= "3.7" and python_version < "4" setuptools==50.3.2 ; python_version >= "3.7" and python_version < "4.0" six==1.16.0 ; python_version >= "3.7" and python_version < "4.0" toml==0.10.2 ; python_version >= "3.7" and python_version < "4.0" tomli==2.0.1 ; python_version >= "3.7" and python_version < "3.11" tox==3.26.0 ; python_version >= "3.7" and python_version < "4.0" typing-extensions==4.4.0 ; python_version >= "3.7" and python_version < "3.8" urllib3==1.26.12 ; python_version >= "3.7" and python_version < "4" virtualenv==20.16.5 ; python_version >= "3.7" and python_version < "4.0" zipp==3.8.1 ; python_version >= "3.7" and python_version < "3.8" eyeD3-0.9.7/setup.cfg000066400000000000000000000002071432016011500143110ustar00rootroot00000000000000[flake8] statistics = 1 max-complexity = 62 max-line-length = 100 ignore = E121,E124,E126,E127,E128,E131,E252,E266,E741,F405,W503,W504 eyeD3-0.9.7/setup.py000066400000000000000000000120741432016011500142070ustar00rootroot00000000000000# -*- coding: utf-8 -*- from setuptools import setup packages = \ ['eyed3', 'eyed3.id3', 'eyed3.mp3', 'eyed3.plugins', 'eyed3.utils'] package_data = \ {'': ['*']} install_requires = \ ['deprecation>=2.1.0,<3.0.0', 'filetype>=1.0.7,<2.0.0'] extras_require = \ {'art-plugin': ['Pillow>=8.0.1,<10.0.0', 'pylast>=4.0.0,<5.0.0', 'requests>=2.25.0,<3.0.0'], 'test': ['pytest>=6.2.1,<7.0.0', 'pytest-cov>=2.10.1,<3.0.0', 'tox>=3.20.1,<4.0.0', 'factory-boy>=3.1.0,<4.0.0', 'flake8>=3.8.4,<4.0.0', 'check-manifest>=0.45,<0.46'], 'yaml-plugin': ['ruamel.yaml>=0.16.12,<0.17.0']} entry_points = \ {'console_scripts': ['eyeD3 = eyed3.main:_main']} setup_kwargs = { 'name': 'eyed3', 'version': '0.9.7', 'description': 'Python audio data toolkit (ID3 and MP3)', 'long_description': 'Status\n------\n.. image:: https://img.shields.io/pypi/v/eyeD3.svg\n :target: https://pypi.python.org/pypi/eyeD3/\n :alt: Latest Version\n.. image:: https://img.shields.io/pypi/status/eyeD3.svg\n :target: https://pypi.python.org/pypi/eyeD3/\n :alt: Project Status\n.. image:: https://travis-ci.org/nicfit/eyeD3.svg?branch=master\n :target: https://travis-ci.org/nicfit/eyeD3\n :alt: Build Status\n.. image:: https://img.shields.io/pypi/l/eyeD3.svg\n :target: https://pypi.python.org/pypi/eyeD3/\n :alt: License\n.. image:: https://img.shields.io/pypi/pyversions/eyeD3.svg\n :target: https://pypi.python.org/pypi/eyeD3/\n :alt: Supported Python versions\n.. image:: https://coveralls.io/repos/nicfit/eyeD3/badge.svg\n :target: https://coveralls.io/r/nicfit/eyeD3\n :alt: Coverage Status\n\n\nAbout\n-----\neyeD3_ is a Python tool for working with audio files, specifically MP3 files\ncontaining ID3_ metadata (i.e. song info).\n\nIt provides a command-line tool (``eyeD3``) and a Python library\n(``import eyed3``) that can be used to write your own applications or\nplugins that are callable from the command-line tool.\n\nFor example, to set some song information in an mp3 file called\n``song.mp3``::\n\n $ eyeD3 -a Integrity -A "Humanity Is The Devil" -t "Hollow" -n 2 song.mp3\n\nWith this command we\'ve set the artist (``-a/--artist``), album\n(``-A/--album``), title (``-t/--title``), and track number\n(``-n/--track-num``) properties in the ID3 tag of the file. This is the\nstandard interface that eyeD3 has always had in the past, therefore it\nis also the default plugin when no other is specified.\n\nThe results of this command can be seen by running the ``eyeD3`` with no\noptions.\n\n::\n\n $ eyeD3 song.mp3\n song.mp3\t[ 3.06 MB ]\n -------------------------------------------------------------------------\n ID3 v2.4:\n title: Hollow\n artist: Integrity\n album: Humanity Is The Devil\n album artist: None\n track: 2\n -------------------------------------------------------------------------\n\nThe same can be accomplished using Python.\n\n::\n\n import eyed3\n\n audiofile = eyed3.load("song.mp3")\n audiofile.tag.artist = "Token Entry"\n audiofile.tag.album = "Free For All Comp LP"\n audiofile.tag.album_artist = "Various Artists"\n audiofile.tag.title = "The Edge"\n audiofile.tag.track_num = 3\n\n audiofile.tag.save()\n\neyeD3_ is written and maintained by `Travis Shirk`_ and is licensed under\nversion 3 of the GPL_.\n\nFeatures\n--------\n\n* Python package (`import eyed3`) for writing applications and plugins.\n* `eyeD3` : Command-line tool driver script that supports plugins.\n* Easy ID3 editing/viewing of audio metadata from the command-line.\n* Plugins for: Tag to string formatting (display), album fixing (fixup),\n cover art downloading (art), collection stats (stats),\n and json/yaml/jabber/nfo output formats, and more included.\n* Support for ID3 versions 1.x, 2.2 (read-only), 2.3, and 2.4.\n* Support for the MP3 audio format exposing details such as play time, bit\n rate, sampling frequency, etc.\n* Abstract design allowing future support for different audio formats and\n metadata containers.\n\nGet Started\n-----------\n\nPython >= 3.7 is required.\n\nFor `installation instructions`_ or more complete `documentation`_ see\nhttp://eyeD3.nicfit.net/\n\nPlease post feedback and/or defects on the `issue tracker`_, or `mailing list`_.\n\n.. _eyeD3: http://eyeD3.nicfit.net/\n.. _Travis Shirk: travis@pobox.com\n.. _issue tracker: https://github.com/nicfit/eyeD3/issues\n.. _mailing list: https://groups.google.com/forum/?fromgroups#!forum/eyed3-users\n.. _installation instructions: http://eyeD3.nicfit.net/index.html#installation\n.. _documentation: http://eyeD3.nicfit.net/index.html#documentation\n.. _GPL: http://www.gnu.org/licenses/gpl-2.0.html\n.. _ID3: http://id3.org/\n\n', 'author': 'Travis Shirk', 'author_email': 'travis@pobox.com', 'maintainer': 'Travis Shirk', 'maintainer_email': 'travis@pobox.com', 'url': 'https://eyeD3.nicfit.net/', 'packages': packages, 'package_data': package_data, 'install_requires': install_requires, 'extras_require': extras_require, 'entry_points': entry_points, 'python_requires': '>=3.7,<4.0', } setup(**setup_kwargs) eyeD3-0.9.7/tests/000077500000000000000000000000001432016011500136335ustar00rootroot00000000000000eyeD3-0.9.7/tests/__init__.py000066400000000000000000000022301432016011500157410ustar00rootroot00000000000000from io import StringIO import eyed3 import os import sys import logging import unittest DATA_D = os.path.join(os.path.dirname(__file__), "data") eyed3.log.setLevel(logging.ERROR) class RedirectStdStreams(object): """This class is used to capture sys.stdout and sys.stderr for tests that invoke command line scripts and wish to inspect the output.""" def __init__(self, stdout=None, stderr=None, seek_on_exit=0): self.stdout = stdout or StringIO() self.stderr = stderr or StringIO() self._seek_offset = seek_on_exit def __enter__(self): self._orig_stdout, self._orig_stderr = sys.stdout, sys.stderr sys.stdout, sys.stderr = self.stdout, self.stderr return self def __exit__(self, exc_type, exc_value, traceback): try: for s in [self.stdout, self.stderr]: s.flush() if not s.isatty(): s.seek(self._seek_offset) finally: sys.stdout, sys.stderr = self._orig_stdout, self._orig_stderr class ExternalDataTestCase(unittest.TestCase): """Test case for external data files.""" def setUp(self): pass eyeD3-0.9.7/tests/conftest.py000066400000000000000000000032571432016011500160410ustar00rootroot00000000000000import shutil import pytest import eyed3 from uuid import uuid4 from pathlib import Path DATA_D = Path(__file__).parent / "data" def _tempCopy(src, dest_dir) -> Path: testfile = Path(str(dest_dir)) / "{}.mp3".format(uuid4()) shutil.copyfile(str(src), str(testfile)) return testfile @pytest.fixture(scope="function") def audiofile(request, tmpdir): """Makes a copy of test.mp3 and loads it using eyed3.load().""" if not Path(DATA_D).exists(): yield None return marker = request.node.get_closest_marker("audiofile_name") if marker: testfile = _tempCopy(DATA_D / marker.args[0], tmpdir) else: testfile = _tempCopy(DATA_D / "test.mp3", tmpdir) yield eyed3.load(testfile) if testfile.exists(): testfile.unlink() @pytest.fixture(scope="function") def id3tag(): """Returns a default-constructed eyed3.id3.Tag.""" from eyed3.id3 import Tag return Tag() @pytest.fixture(scope="function") def image(tmpdir): img_file = _tempCopy(DATA_D / "CypressHill3TemplesOfBoom.jpg", tmpdir) return img_file @pytest.fixture(scope="session") def eyeD3(): """A fixture for running `eyeD3` default plugin. `eyeD3(audiofile, args, expected_retval=0, reload_version=None)` """ from eyed3 import main def func(audiofile, args, expected_retval=0, reload_version=None): try: args, _, config = main.parseCommandLine(args + [audiofile.path]) retval = main.main(args, config) except SystemExit as sys_exit: retval = sys_exit.code assert retval == expected_retval return eyed3.load(audiofile.path, tag_version=reload_version) return func eyeD3-0.9.7/tests/id3/000077500000000000000000000000001432016011500143125ustar00rootroot00000000000000eyeD3-0.9.7/tests/id3/__init__.py000066400000000000000000000000001432016011500164110ustar00rootroot00000000000000eyeD3-0.9.7/tests/id3/test_frames.py000066400000000000000000000253571432016011500172140ustar00rootroot00000000000000import pytest import unittest from pathlib import Path from unittest.mock import patch import eyed3 from eyed3.id3 import (LATIN1_ENCODING, UTF_8_ENCODING, UTF_16_ENCODING, UTF_16BE_ENCODING) from eyed3.id3 import ID3_V1_0, ID3_V1_1, ID3_V2_3, ID3_V2_4 from eyed3.id3.frames import (Frame, TextFrame, FrameHeader, ImageFrame, LanguageCodeMixin, ObjectFrame, TermsOfUseFrame, DEFAULT_LANG, TOS_FID, OBJECT_FID) from .. import DATA_D class FrameTest(unittest.TestCase): def testCtor(self): f = Frame(b"ABCD") assert f.id == b"ABCD" assert f.header is None assert f.decompressed_size == 0 assert f.group_id is None assert f.encrypt_method is None assert f.data is None assert f.data_len == 0 assert f.encoding is None f = Frame(b"EFGH") assert f.id == b"EFGH" assert f.header is None assert f.decompressed_size == 0 assert f.group_id is None assert f.encrypt_method is None assert f.data is None assert f.data_len == 0 assert f.encoding is None def testTextDelim(self): for enc in [LATIN1_ENCODING, UTF_16BE_ENCODING, UTF_16_ENCODING, UTF_8_ENCODING]: f = Frame(b"XXXX") f.encoding = enc if enc in [LATIN1_ENCODING, UTF_8_ENCODING]: assert (f.text_delim == b"\x00") else: assert (f.text_delim == b"\x00\x00") def testInitEncoding(self): # Default encodings per version for ver in [ID3_V1_0, ID3_V1_1, ID3_V2_3, ID3_V2_4]: f = Frame(b"XXXX") f.header = FrameHeader(f.id, ver) f._initEncoding() if ver[0] == 1: assert (f.encoding == LATIN1_ENCODING) elif ver[:2] == (2, 3): assert (f.encoding == UTF_16_ENCODING) else: assert (f.encoding == UTF_8_ENCODING) # Invalid encoding for a version is coerced for ver in [ID3_V1_0, ID3_V1_1]: for enc in [UTF_8_ENCODING, UTF_16_ENCODING, UTF_16BE_ENCODING]: f = Frame(b"XXXX") f.header = FrameHeader(f.id, ver) f.encoding = enc f._initEncoding() assert (f.encoding == LATIN1_ENCODING) for ver in [ID3_V2_3]: for enc in [UTF_8_ENCODING, UTF_16BE_ENCODING]: f = Frame(b"XXXX") f.header = FrameHeader(f.id, ver) f.encoding = enc f._initEncoding() assert (f.encoding == UTF_16_ENCODING) # No coersion for v2.4 for ver in [ID3_V2_4]: for enc in [LATIN1_ENCODING, UTF_8_ENCODING, UTF_16BE_ENCODING, UTF_16_ENCODING]: f = Frame(b"XXXX") f.header = FrameHeader(f.id, ver) f.encoding = enc f._initEncoding() assert (f.encoding == enc) class TextFrameTest(unittest.TestCase): def testCtor(self): with pytest.raises(TypeError): TextFrame("TCON") f = TextFrame(b"TCON") assert f.text == "" f = TextFrame(b"TCON", "content") assert f.text == "content" def testRenderParse(self): fid = b"TPE1" for ver in [ID3_V2_3, ID3_V2_4]: h1 = FrameHeader(fid, ver) h2 = FrameHeader(fid, ver) f1 = TextFrame(b"TPE1", "Ambulance LTD") f1.header = h1 data = f1.render() # FIXME: right here is why parse should be static f2 = TextFrame(b"TIT2") f2.parse(data[h1.size:], h2) assert f1.id == f2.id assert f1.text == f2.text assert f1.encoding == f2.encoding class ImageFrameTest(unittest.TestCase): def testPicTypeConversions(self): count = 0 for s in ("OTHER", "ICON", "OTHER_ICON", "FRONT_COVER", "BACK_COVER", "LEAFLET", "MEDIA", "LEAD_ARTIST", "ARTIST", "CONDUCTOR", "BAND", "COMPOSER", "LYRICIST", "RECORDING_LOCATION", "DURING_RECORDING", "DURING_PERFORMANCE", "VIDEO", "BRIGHT_COLORED_FISH", "ILLUSTRATION", "BAND_LOGO", "PUBLISHER_LOGO"): c = getattr(ImageFrame, s) assert (ImageFrame.picTypeToString(c) == s) assert (ImageFrame.stringToPicType(s) == c) count += 1 assert (count == ImageFrame.MAX_TYPE + 1) assert (ImageFrame.MIN_TYPE == ImageFrame.OTHER) assert (ImageFrame.MAX_TYPE == ImageFrame.PUBLISHER_LOGO) assert ImageFrame.picTypeToString(ImageFrame.MAX_TYPE) == \ "PUBLISHER_LOGO" assert ImageFrame.picTypeToString(ImageFrame.MIN_TYPE) == "OTHER" with pytest.raises(ValueError): ImageFrame.picTypeToString(ImageFrame.MAX_TYPE + 1) with pytest.raises(ValueError): ImageFrame.picTypeToString(ImageFrame.MIN_TYPE - 1) with pytest.raises(ValueError): ImageFrame.stringToPicType("Prust") def test_DateFrame(): from eyed3.id3.frames import DateFrame from eyed3.core import Date # Default ctor df = DateFrame(b"TDRC") assert df.text == "" assert df.date is None # Ctor with eyed3.core.Date arg for d in [Date(2012), Date(2012, 1), Date(2012, 1, 4), Date(2012, 1, 4, 18), Date(2012, 1, 4, 18, 15), Date(2012, 1, 4, 18, 15, 30), ]: df = DateFrame(b"TDRC", d) assert df.text == str(d) # Comparison is on each member, not reference ID assert df.date == d # Test ctor str arg is converted for d in ["2012", "2010-01", "2010-01-04", "2010-01-04T18", "2010-01-04T06:20", "2010-01-04T06:20:15", "2012", "2010-01", "2010-01-04", "2010-01-04T18", "2010-01-04T06:20", "2010-01-04T06:20:15", ]: df = DateFrame(b"TDRC", d) dt = Date.parse(d) assert df.text == str(dt) assert df.text == str(d) # Comparison is on each member, not reference ID assert df.date == dt # Technically invalid, but supported for d in ["20180215"]: df = DateFrame(b"TDRC", d) dt = Date.parse(d) assert df.text == str(dt) # Comparison is on each member, not reference ID assert df.date == dt # Invalid dates for d in ["1234:12"]: date = DateFrame(b"TDRL") date.date = d assert not date.date def test_compression(): f = open(__file__, "rb") try: data = f.read() compressed = Frame.compress(data) assert data == Frame.decompress(compressed) finally: f.close() ''' FIXME: def test_tag_compression(id3tag): # FIXME: going to refactor FrameHeader, bbl data = Path(__file__).read_text() aframe = TextFrame(ARTIST_FID, text=data) aframe.header = FrameHeader(ARTIST_FID) import ipdb; ipdb.set_trace() pass ''' def test_encryption(): assert "Iceburn" == Frame.encrypt("Iceburn") assert "Iceburn" == Frame.decrypt("Iceburn") def test_LanguageCodeMixin(): with pytest.raises(TypeError): LanguageCodeMixin().lang = "eng" l = LanguageCodeMixin() l.lang = b"\x80" assert l.lang == b"eng" l.lang = b"" assert l.lang == b"" l.lang = None assert l.lang == b"" @pytest.mark.skipif(not Path(DATA_D).exists(), reason="test requires data files") def test_TermsOfUseFrame(audiofile, id3tag): terms = TermsOfUseFrame() assert terms.id == b"USER" assert terms.text == "" assert terms.lang == DEFAULT_LANG id3tag.terms_of_use = "Fucking MANDATORY!" audiofile.tag = id3tag audiofile.tag.save() file = eyed3.load(audiofile.path) assert file.tag.terms_of_use == "Fucking MANDATORY!" id3tag.terms_of_use = "Fucking MANDATORY!" audiofile.tag = id3tag audiofile.tag.save() file = eyed3.load(audiofile.path) assert file.tag.terms_of_use == "Fucking MANDATORY!" id3tag.terms_of_use = ("Fucking MANDATORY!", b"jib") audiofile.tag = id3tag audiofile.tag.save() file = eyed3.load(audiofile.path) assert file.tag.terms_of_use == "Fucking MANDATORY!" assert file.tag.frame_set[TOS_FID][0].lang == b"jib" id3tag.terms_of_use = ("Fucking MANDATORY!", b"en") audiofile.tag = id3tag audiofile.tag.save() file = eyed3.load(audiofile.path) assert file.tag.terms_of_use == "Fucking MANDATORY!" assert file.tag.frame_set[TOS_FID][0].lang == b"en" @pytest.mark.skipif(not Path(DATA_D).exists(), reason="test requires data files") def test_ObjectFrame(audiofile, id3tag): sixsixsix = b"\x29\x0a" * 666 with Path(__file__).open("rb") as fp: thisfile = fp.read() obj1 = ObjectFrame(description="Test Object", object_data=sixsixsix, filename="666.txt", mime_type="text/satan") obj2 = ObjectFrame(description="Test Object2", filename=str(__file__), mime_type="text/python", object_data=thisfile) id3tag.frame_set[OBJECT_FID] = obj1 id3tag.frame_set[OBJECT_FID].append(obj2) audiofile.tag = id3tag audiofile.tag.save() file = eyed3.load(audiofile.path) assert len(file.tag.objects) == 2 obj1_2 = file.tag.objects.get("Test Object") assert obj1_2.mime_type == "text/satan" assert obj1_2.object_data == sixsixsix assert obj1_2.filename == "666.txt" obj2_2 = file.tag.objects.get("Test Object2") assert obj2_2.mime_type == "text/python" assert obj2_2.object_data == thisfile assert obj2_2.filename == __file__ @pytest.mark.skipif(not Path(DATA_D).exists(), reason="test requires data files") def test_ObjectFrame_no_mimetype(audiofile, id3tag): # Setting no mime-type is invalid obj1 = ObjectFrame(object_data=b"Deep Purple") id3tag.frame_set[OBJECT_FID] = obj1 audiofile.tag = id3tag audiofile.tag.save() with patch("eyed3.core.parseError") as mock: file = eyed3.load(audiofile.path) assert mock.call_count == 2 obj1.mime_type = "Deep" audiofile.tag.save() with patch("eyed3.core.parseError") as mock: file = eyed3.load(audiofile.path) assert mock.call_count == 1 obj1.mime_type = "Deep/Purple" audiofile.tag.save() with patch("eyed3.core.parseError") as mock: file = eyed3.load(audiofile.path) mock.assert_not_called() obj1.object_data = b"" audiofile.tag.save() with patch("eyed3.core.parseError") as mock: file = eyed3.load(audiofile.path) assert mock.call_count == 1 assert file eyeD3-0.9.7/tests/id3/test_headers.py000066400000000000000000000413641432016011500173460ustar00rootroot00000000000000import unittest import pytest from eyed3.id3.headers import * from eyed3.id3 import ID3_DEFAULT_VERSION, TagException from io import BytesIO class TestTagHeader(unittest.TestCase): def testCtor(self): h = TagHeader() assert (h.version == ID3_DEFAULT_VERSION) assert not(h.unsync) assert not(h.extended) assert not(h.experimental) assert not(h.footer) assert h.tag_size == 0 def testTagVersion(self): for maj, min, rev in [(1, 0, 0), (1, 1, 0), (2, 2, 0), (2, 3, 0), (2, 4, 0)]: h = TagHeader((maj, min, rev)) assert (h.major_version == maj) assert (h.minor_version == min) assert (h.rev_version == rev) for maj, min, rev in [(1, 0, None), (1, None, 0), (2, 5, 0), (3, 4, 0)]: try: h = TagHeader((maj, min, rev)) except ValueError: pass else: assert not("Invalid version, expected ValueError") def testParse(self): # Incomplete headers for data in [b"", b"ID3", b"ID3\x04\x00", b"ID3\x02\x00\x00", b"ID3\x03\x00\x00", b"ID3\x04\x00\x00", ]: header = TagHeader() found = header.parse(BytesIO(data)) assert not(found) # Invalid versions for data in [b"ID3\x01\x00\x00", b"ID3\x05\x00\x00", b"ID3\x06\x00\x00", ]: header = TagHeader() try: found = header.parse(BytesIO(data)) except TagException: pass else: assert not("Expected TagException invalid version") # Complete headers for data in [b"ID3\x02\x00\x00", b"ID3\x03\x00\x00", b"ID3\x04\x00\x00", ]: for sz in [0, 10, 100, 1000, 2500, 5000, 7500, 10000]: sz_bytes = bin2bytes(bin2synchsafe(dec2bin(sz, 32))) header = TagHeader() found = header.parse(BytesIO(data + sz_bytes)) assert (found) assert header.tag_size == sz def testRenderWithUnsyncTrue(self): h = TagHeader() h.unsync = True with pytest.raises(NotImplementedError): h.render(100) def testRender(self): h = TagHeader() h.unsync = False header = h.render(100) h2 = TagHeader() found = h2.parse(BytesIO(header)) assert not(h2.unsync) assert (found) assert header == h2.render(100) h = TagHeader() h.footer = True h.extended = True header = h.render(666) h2 = TagHeader() found = h2.parse(BytesIO(header)) assert (found) assert not(h2.unsync) assert not(h2.experimental) assert h2.footer assert h2.extended assert (h2.tag_size == 666) assert (header == h2.render(666)) class TestExtendedHeader(unittest.TestCase): def testCtor(self): h = ExtendedTagHeader() assert (h.size == 0) assert (h._flags == 0) assert (h.crc is None) assert (h._restrictions == 0) assert not(h.update_bit) assert not(h.crc_bit) assert not(h.restrictions_bit) def testUpdateBit(self): h = ExtendedTagHeader() h.update_bit = 1 assert (h.update_bit) h.update_bit = 0 assert not(h.update_bit) h.update_bit = 1 assert (h.update_bit) h.update_bit = False assert not(h.update_bit) h.update_bit = True assert (h.update_bit) def testCrcBit(self): h = ExtendedTagHeader() h.update_bit = True h.crc_bit = 1 assert (h.update_bit) assert (h.crc_bit) h.crc_bit = 0 assert (h.update_bit) assert not(h.crc_bit) h.crc_bit = 1 assert (h.update_bit) assert (h.crc_bit) h.crc_bit = False assert (h.update_bit) assert not(h.crc_bit) h.crc_bit = True assert (h.update_bit) assert (h.crc_bit) def testRestrictionsBit(self): h = ExtendedTagHeader() h.update_bit = True h.crc_bit = True h.restrictions_bit = 1 assert (h.update_bit) assert (h.crc_bit) assert (h.restrictions_bit) h.restrictions_bit = 0 assert (h.update_bit) assert (h.crc_bit) assert not(h.restrictions_bit) h.restrictions_bit = 1 assert (h.update_bit) assert (h.crc_bit) assert (h.restrictions_bit) h.restrictions_bit = False assert (h.update_bit) assert (h.crc_bit) assert not(h.restrictions_bit) h.restrictions_bit = True assert (h.update_bit) assert (h.crc_bit) assert (h.restrictions_bit) h = ExtendedTagHeader() h.restrictions_bit = True assert (h.tag_size_restriction == ExtendedTagHeader.RESTRICT_TAG_SZ_LARGE) assert (h.text_enc_restriction == ExtendedTagHeader.RESTRICT_TEXT_ENC_NONE) assert (h.text_length_restriction == ExtendedTagHeader.RESTRICT_TEXT_LEN_NONE) assert (h.image_enc_restriction == ExtendedTagHeader.RESTRICT_IMG_ENC_NONE) assert (h.image_size_restriction == ExtendedTagHeader.RESTRICT_IMG_SZ_NONE) h.tag_size_restriction = ExtendedTagHeader.RESTRICT_TAG_SZ_TINY assert (h.tag_size_restriction == ExtendedTagHeader.RESTRICT_TAG_SZ_TINY) assert (h.text_enc_restriction == ExtendedTagHeader.RESTRICT_TEXT_ENC_NONE) assert (h.text_length_restriction == ExtendedTagHeader.RESTRICT_TEXT_LEN_NONE) assert (h.image_enc_restriction == ExtendedTagHeader.RESTRICT_IMG_ENC_NONE) assert (h.image_size_restriction == ExtendedTagHeader.RESTRICT_IMG_SZ_NONE) h.text_enc_restriction = ExtendedTagHeader.RESTRICT_TEXT_ENC_UTF8 assert (h.tag_size_restriction == ExtendedTagHeader.RESTRICT_TAG_SZ_TINY) assert (h.text_enc_restriction == ExtendedTagHeader.RESTRICT_TEXT_ENC_UTF8) assert (h.text_length_restriction == ExtendedTagHeader.RESTRICT_TEXT_LEN_NONE) assert (h.image_enc_restriction == ExtendedTagHeader.RESTRICT_IMG_ENC_NONE) assert (h.image_size_restriction == ExtendedTagHeader.RESTRICT_IMG_SZ_NONE) h.text_length_restriction = ExtendedTagHeader.RESTRICT_TEXT_LEN_30 assert (h.tag_size_restriction == ExtendedTagHeader.RESTRICT_TAG_SZ_TINY) assert (h.text_enc_restriction == ExtendedTagHeader.RESTRICT_TEXT_ENC_UTF8) assert (h.text_length_restriction == ExtendedTagHeader.RESTRICT_TEXT_LEN_30) assert (h.image_enc_restriction == ExtendedTagHeader.RESTRICT_IMG_ENC_NONE) assert (h.image_size_restriction == ExtendedTagHeader.RESTRICT_IMG_SZ_NONE) h.image_enc_restriction = ExtendedTagHeader.RESTRICT_IMG_ENC_PNG_JPG assert (h.tag_size_restriction == ExtendedTagHeader.RESTRICT_TAG_SZ_TINY) assert (h.text_enc_restriction == ExtendedTagHeader.RESTRICT_TEXT_ENC_UTF8) assert (h.text_length_restriction == ExtendedTagHeader.RESTRICT_TEXT_LEN_30) assert (h.image_enc_restriction == ExtendedTagHeader.RESTRICT_IMG_ENC_PNG_JPG) assert (h.image_size_restriction == ExtendedTagHeader.RESTRICT_IMG_SZ_NONE) h.image_size_restriction = ExtendedTagHeader.RESTRICT_IMG_SZ_256 assert (h.tag_size_restriction == ExtendedTagHeader.RESTRICT_TAG_SZ_TINY) assert (h.text_enc_restriction == ExtendedTagHeader.RESTRICT_TEXT_ENC_UTF8) assert (h.text_length_restriction == ExtendedTagHeader.RESTRICT_TEXT_LEN_30) assert (h.image_enc_restriction == ExtendedTagHeader.RESTRICT_IMG_ENC_PNG_JPG) assert (h.image_size_restriction == ExtendedTagHeader.RESTRICT_IMG_SZ_256) assert " 32 frames " in h.tag_size_restriction_description assert " 4 KB " in h.tag_size_restriction_description h.tag_size_restriction = ExtendedTagHeader.RESTRICT_TAG_SZ_LARGE assert " 128 frames " in h.tag_size_restriction_description h.tag_size_restriction = ExtendedTagHeader.RESTRICT_TAG_SZ_MED assert " 64 frames " in h.tag_size_restriction_description h.tag_size_restriction = ExtendedTagHeader.RESTRICT_TAG_SZ_SMALL assert " 32 frames " in h.tag_size_restriction_description assert " 40 KB " in h.tag_size_restriction_description assert (" UTF-8" in h.text_enc_restriction_description) h.text_enc_restriction = ExtendedTagHeader.RESTRICT_TEXT_ENC_NONE assert ("None" == h.text_enc_restriction_description) assert " 30 " in h.text_length_restriction_description h.text_length_restriction = ExtendedTagHeader.RESTRICT_TEXT_LEN_NONE assert ("None" == h.text_length_restriction_description) h.text_length_restriction = ExtendedTagHeader.RESTRICT_TEXT_LEN_1024 assert " 1024 " in h.text_length_restriction_description h.text_length_restriction = ExtendedTagHeader.RESTRICT_TEXT_LEN_128 assert " 128 " in h.text_length_restriction_description assert " PNG " in h.image_enc_restriction_description h.image_enc_restriction = ExtendedTagHeader.RESTRICT_IMG_ENC_NONE assert ("None" == h.image_enc_restriction_description) assert " 256x256 " in h.image_size_restriction_description h.image_size_restriction = ExtendedTagHeader.RESTRICT_IMG_SZ_NONE assert ("None" == h.image_size_restriction_description) h.image_size_restriction = ExtendedTagHeader.RESTRICT_IMG_SZ_64 assert (" 64x64 pixels or smaller" in h.image_size_restriction_description) h.image_size_restriction = ExtendedTagHeader.RESTRICT_IMG_SZ_64_EXACT assert "exactly 64x64 pixels" in h.image_size_restriction_description def testRender(self): version = (2, 4, 0) dummy_data = b"\xab" * 50 dummy_padding_len = 1024 h = ExtendedTagHeader() h.update_bit = 1 h.crc_bit = 1 h.tag_size_restriction = ExtendedTagHeader.RESTRICT_TAG_SZ_MED h.text_enc_restriction = ExtendedTagHeader.RESTRICT_TEXT_ENC_UTF8 h.text_length_restriction = ExtendedTagHeader.RESTRICT_TEXT_LEN_128 h.image_enc_restriction = ExtendedTagHeader.RESTRICT_IMG_ENC_PNG_JPG h.image_size_restriction = ExtendedTagHeader.RESTRICT_IMG_SZ_256 header = h.render(version, dummy_data, dummy_padding_len) h2 = ExtendedTagHeader() h2.parse(BytesIO(header), version) assert (h2.update_bit) assert (h2.crc_bit) assert (h2.restrictions_bit) assert (h.crc == h2.crc) assert (h.tag_size_restriction == h2.tag_size_restriction) assert (h.text_enc_restriction == h2.text_enc_restriction) assert (h.text_length_restriction == h2.text_length_restriction) assert (h.image_enc_restriction == h2.image_enc_restriction) assert (h.image_size_restriction == h2.image_size_restriction) assert h2.render(version, dummy_data, dummy_padding_len) == header # version 2.3 header_23 = h.render((2,3,0), dummy_data, dummy_padding_len) h3 = ExtendedTagHeader() h3.parse(BytesIO(header_23), (2,3,0)) assert not(h3.update_bit) assert (h3.crc_bit) assert not(h3.restrictions_bit) assert (h.crc == h3.crc) assert (0 == h3.tag_size_restriction) assert (0 == h3.text_enc_restriction) assert (0 == h3.text_length_restriction) assert (0 == h3.image_enc_restriction) assert (0 == h3.image_size_restriction) def testRenderCrcPadding(self): version = (2, 4, 0) h = ExtendedTagHeader() h.crc_bit = 1 header = h.render(version, b"\x01", 0) h2 = ExtendedTagHeader() h2.parse(BytesIO(header), version) assert h.crc == h2.crc def testInvalidFlagBits(self): for bad_flags in [b"\x00\x20", b"\x01\x01"]: h = ExtendedTagHeader() try: h.parse(BytesIO(b"\x00\x00\x00\xff" + bad_flags), (2, 4, 0)) except TagException: pass else: assert not("Bad ExtendedTagHeader flags, expected " "TagException") class TestFrameHeader(unittest.TestCase): def testCtor(self): h = FrameHeader(b"TIT2", ID3_DEFAULT_VERSION) assert (h.size == 10) assert (h.id == b"TIT2") assert (h.data_size == 0) assert (h._flags == [0] * 16) h = FrameHeader(b"TIT2", (2, 3, 0)) assert (h.size == 10) assert (h.id == b"TIT2") assert (h.data_size == 0) assert (h._flags == [0] * 16) h = FrameHeader(b"TIT2", (2, 2, 0)) assert (h.size == 6) assert (h.id == b"TIT2") assert (h.data_size == 0) assert (h._flags == [0] * 16) def testBitMask(self): for v in [(2, 2, 0), (2, 3, 0)]: h = FrameHeader(b"TXXX", v) assert (h.TAG_ALTER == 0) assert (h.FILE_ALTER == 1) assert (h.READ_ONLY == 2) assert (h.COMPRESSED == 8) assert (h.ENCRYPTED == 9) assert (h.GROUPED == 10) assert (h.UNSYNC == 14) assert (h.DATA_LEN == 4) for v in [(2, 4, 0), (1, 0, 0), (1, 1, 0)]: h = FrameHeader(b"TXXX", v) assert (h.TAG_ALTER == 1) assert (h.FILE_ALTER == 2) assert (h.READ_ONLY == 3) assert (h.COMPRESSED == 12) assert (h.ENCRYPTED == 13) assert (h.GROUPED == 9) assert (h.UNSYNC == 14) assert (h.DATA_LEN == 15) for v in [(2, 5, 0), (3, 0, 0)]: try: h = FrameHeader(b"TIT2", v) except ValueError: pass else: assert not("Expected a ValueError from invalid version, " "but got success") for v in [1, "yes", "no", True, 23]: h = FrameHeader(b"APIC", (2, 4, 0)) h.tag_alter = v h.file_alter = v h.read_only = v h.compressed = v h.encrypted = v h.grouped = v h.unsync = v h.data_length_indicator = v assert (h.tag_alter == 1) assert (h.file_alter == 1) assert (h.read_only == 1) assert (h.compressed == 1) assert (h.encrypted == 1) assert (h.grouped == 1) assert (h.unsync == 1) assert (h.data_length_indicator == 1) for v in [0, False, None]: h = FrameHeader(b"APIC", (2, 4, 0)) h.tag_alter = v h.file_alter = v h.read_only = v h.compressed = v h.encrypted = v h.grouped = v h.unsync = v h.data_length_indicator = v assert (h.tag_alter == 0) assert (h.file_alter == 0) assert (h.read_only == 0) assert (h.compressed == 0) assert (h.encrypted == 0) assert (h.grouped == 0) assert (h.unsync == 0) assert (h.data_length_indicator == 0) h1 = FrameHeader(b"APIC", (2, 3, 0)) h1.tag_alter = True h1.grouped = True h1.file_alter = 1 h1.encrypted = None h1.compressed = 4 h1.data_length_indicator = 0 h1.read_only = 1 h1.unsync = 1 h2 = FrameHeader(b"APIC", (2, 4, 0)) assert (h2.tag_alter == 0) assert (h2.grouped == 0) h2.copyFlags(h1) assert (h2.tag_alter) assert (h2.grouped) assert (h2.file_alter) assert not(h2.encrypted) assert (h2.compressed) assert not(h2.data_length_indicator) assert (h2.read_only) assert (h2.unsync) def testValidFrameId(self): for id in [b"", b"a", b"tx", b"tit", b"TIT", b"Tit2", b"aPic"]: assert not(FrameHeader._isValidFrameId(id)) for id in [b"TIT2", b"APIC", b"1234"]: assert FrameHeader._isValidFrameId(id) def testRenderWithUnsyncTrue(self): h = FrameHeader(b"TIT2", ID3_DEFAULT_VERSION) h.unsync = True with pytest.raises(NotImplementedError): h.render(100) eyeD3-0.9.7/tests/id3/test_id3.py000066400000000000000000000111341432016011500164020ustar00rootroot00000000000000import eyed3 import unittest import pytest from pathlib import Path from eyed3.id3 import * from .. import DATA_D ID3_VERSIONS = [(ID3_V1, (1, None, None), "v1.x"), (ID3_V1_0, (1, 0, 0), "v1.0"), (ID3_V1_1, (1, 1, 0), "v1.1"), (ID3_V2, (2, None, None), "v2.x"), (ID3_V2_2, (2, 2, 0), "v2.2"), (ID3_V2_3, (2, 3, 0), "v2.3"), (ID3_V2_4, (2, 4, 0), "v2.4"), (ID3_DEFAULT_VERSION, (2, 4, 0), "v2.4"), (ID3_ANY_VERSION, (1|2, None, None), "v1.x/v2.x"), ] with pytest.raises(TypeError): versionToString(666) with pytest.raises(ValueError): versionToString((3, 1, 0)) def testEmptyGenre(): g = Genre() assert g.id is None assert g.name is None def testValidGenres(): # Create with id for i in range(genres.GENRE_MAX): g = Genre() g.id = i assert g.id == i assert g.name == genres[i] g = Genre(id=i) assert g.id == i assert g.name == genres[i] # Create with name for name in [n for n in genres if n is not None and type(n) is not int]: g = Genre() g.name = name assert g.id == genres[name] assert g.name == genres[g.id] assert g.name.lower() == name g = Genre(name=name) assert g.id == genres[name] assert g.name.lower() == name def test255Padding(): for i in range(GenreMap.GENRE_MAX + 1, 256): assert genres[i] is None with pytest.raises(KeyError): genres.__getitem__(256) def testCustomGenres(): # Genres can be created for any name, their ID is None g = Genre(name="Grindcore") assert g.name == "Grindcore" assert g.id is None # But when constructing with IDs they must map. with pytest.raises(ValueError): Genre.__call__(id=1024) def testRemappedNames(): g = Genre(id=3, name="dance stuff") assert g.id == 3 assert g.name == "Dance" g = Genre(id=666, name="Funky") assert g.id is None assert g.name == "Funky" def testGenreEq(): for s in ["Hardcore", "(129)Hardcore", "(129)", "(0129)", "129", "0129"]: assert Genre.parse(s) == Genre.parse(s) assert Genre.parse(s) != Genre.parse("Blues") def testParseGenre(): test_list = ["Hardcore", "(129)Hardcore", "(129)", "(0129)", "129", "0129"] # This is typically what will happen when parsing tags, a blob of text # is parsed into Genre for s in test_list: g = Genre.parse(s) assert g.name == "Hardcore" assert g.id == 129 g = Genre.parse("") assert g is None g = Genre.parse("1") assert g.id == 1 assert g.name == "Classic Rock" g = Genre.parse("1", id3_std=False) assert g.id is None assert g.name == "1" def testToSting(): assert str(Genre("Hardcore")) == "(129)Hardcore" assert str(Genre("Grindcore")) == "Grindcore" def testId3Versions(): for v in [ID3_V1, ID3_V1_0, ID3_V1_1]: assert (v[0] == 1) assert (ID3_V1_0[1] == 0) assert (ID3_V1_0[2] == 0) assert (ID3_V1_1[1] == 1) assert (ID3_V1_1[2] == 0) for v in [ID3_V2, ID3_V2_2, ID3_V2_3, ID3_V2_4]: assert (v[0] == 2) assert (ID3_V2_2[1] == 2) assert (ID3_V2_3[1] == 3) assert (ID3_V2_4[1] == 4) assert (ID3_ANY_VERSION == (ID3_V1[0] | ID3_V2[0], None, None)) assert (ID3_DEFAULT_VERSION == ID3_V2_4) def test_versionToString(): for const, tple, string in ID3_VERSIONS: assert versionToString(const) == string def test_isValidVersion(): for v, _, _ in ID3_VERSIONS: assert isValidVersion(v) for _, v, _ in ID3_VERSIONS: if None in v: assert not isValidVersion(v, True) else: assert isValidVersion(v, True) assert not isValidVersion((3, 1, 1)) def testNormalizeVersion(): assert normalizeVersion(ID3_V1) == ID3_V1_1 assert normalizeVersion(ID3_V2) == ID3_V2_4 assert normalizeVersion(ID3_DEFAULT_VERSION) == ID3_V2_4 assert normalizeVersion(ID3_ANY_VERSION) == ID3_DEFAULT_VERSION # Correcting the bogus assert normalizeVersion((2, 2, 1)) == ID3_V2_2 # ID3 v2.2 @unittest.skipIf(not Path(DATA_D).exists(), "test requires data files") def test_id3v22(): data_file = Path(DATA_D) / "sample-ID3v2.2.0.tag" audio_file = eyed3.load(data_file) assert audio_file.tag.version == (2, 2, 0) assert audio_file.tag.title == "11.Portfolio Diaz.mp3" assert audio_file.tag.album == "Acrobatic Tenement" assert audio_file.tag.artist == "At the Drive-In" eyeD3-0.9.7/tests/id3/test_rva.py000066400000000000000000000205421432016011500165160ustar00rootroot00000000000000import dataclasses import pytest from pytest import approx from eyed3.id3 import ID3_V2_3, ID3_V2_4, ID3_V2_2 from eyed3.id3.frames import RelVolAdjFrameV23, RelVolAdjFrameV24, FrameException, FrameHeader def test_default_v23(): f = RelVolAdjFrameV23() assert f.id == b"RVAD" f.adjustments = RelVolAdjFrameV23.VolumeAdjustments() f.render() f2 = RelVolAdjFrameV23() f2.parse(f.data, f.header) assert f.adjustments == f2.adjustments assert set(dataclasses.astuple(f.adjustments)) == {0} def test_RelVolAdjFrameV23_invalid_version(): f = RelVolAdjFrameV23() f.adjustments = RelVolAdjFrameV23.VolumeAdjustments() f.render() f.header = FrameHeader(f.id, ID3_V2_4) with pytest.raises(FrameException): f.parse(f.data, f.header) def test_RelVolAdjFrameV23_outofbounds(): f = RelVolAdjFrameV23() f.adjustments = RelVolAdjFrameV23.VolumeAdjustments() with pytest.raises(ValueError): f.adjustments.front_right = 2**16 + 1 f.render() with pytest.raises(ValueError): f.adjustments.front_right = -(2**16) - 1 f.render() f.adjustments.front_right = 2**16 assert f.render() f2 = RelVolAdjFrameV23() data = bytearray(f.data) data[1] = 32 f.data = bytes(data) with pytest.raises(FrameException): f2.parse(f.data, f.header) def test_v23_supported(): f = RelVolAdjFrameV23() f.adjustments = RelVolAdjFrameV23.VolumeAdjustments( front_right=-10, front_left=2, front_right_peak=15, front_left_peak=15, back_right=54, back_left=-24, back_right_peak=100, back_left_peak=101, front_center=10, front_center_peak=15, bass=-666, bass_peak=5000, ) assert f.adjustments.has_front_channel assert f.adjustments.has_back_channel assert f.adjustments.has_front_channel assert f.adjustments.has_bass_channel assert not f.adjustments.has_master_channel assert not f.adjustments.has_other_channel assert not f.adjustments.has_back_center_channel f.render() f2 = RelVolAdjFrameV23() f2.parse(f.data, f.header) assert f.adjustments == f2.adjustments def test_v23_unsupported(): f = RelVolAdjFrameV23() f.adjustments = RelVolAdjFrameV23.VolumeAdjustments( master=999, master_peak=999, other=333, other_peak=333, back_center=-5, back_center_peak=1, front_right=10, front_left=-2, front_right_peak=15, front_left_peak=15, back_right=-54, back_left=-24, back_right_peak=100, back_left_peak=101, front_center=10, front_center_peak=15, bass=666, bass_peak=5000, ) assert f.adjustments.has_front_channel assert f.adjustments.has_back_channel assert f.adjustments.has_front_channel assert f.adjustments.has_bass_channel assert f.adjustments.has_master_channel assert f.adjustments.has_other_channel assert f.adjustments.has_back_center_channel f.render() f2 = RelVolAdjFrameV23() f2.parse(f.data, f.header) assert f2.adjustments.has_front_channel assert f2.adjustments.has_back_channel assert f2.adjustments.has_front_channel assert f2.adjustments.has_bass_channel assert not f2.adjustments.has_master_channel assert not f2.adjustments.has_other_channel assert not f2.adjustments.has_back_center_channel f.adjustments.master = f.adjustments.master_peak = 0 f.adjustments.other = f.adjustments.other_peak = 0 f.adjustments.back_center = f.adjustments.back_center_peak = 0 assert f.adjustments == f2.adjustments def test_v23_bounds(): f = RelVolAdjFrameV23() adjustments = dataclasses.asdict(RelVolAdjFrameV23.VolumeAdjustments()) for a in adjustments.keys(): values = dict(adjustments) for value, raises in [ (65537, True), (-65537, True), (65536, False), (-65536, False), (32769, False), (-32768, False), (777, False), (-999, False), (0, False), (-0, False), ]: values[a] = value if raises: with pytest.raises(ValueError): f.adjustments = RelVolAdjFrameV23.VolumeAdjustments(**values) f.render() else: f.adjustments = RelVolAdjFrameV23.VolumeAdjustments(**values) f.render() assert dataclasses.asdict(f.adjustments)[a] == value def test_v23_optionals(): f = RelVolAdjFrameV23() f.adjustments = RelVolAdjFrameV23.VolumeAdjustments( front_right=-10, front_left=2, front_right_peak=0, front_left_peak=0, ) f.render() assert len(f.data) == 10 f = RelVolAdjFrameV23() f.adjustments = RelVolAdjFrameV23.VolumeAdjustments( front_right=-10, front_left=2, front_right_peak=0, front_left_peak=0, bass=-666, bass_peak=5000, ) f.render() assert len(f.data) == 26 f = RelVolAdjFrameV23() f.adjustments = RelVolAdjFrameV23.VolumeAdjustments( front_right=-10, front_left=2, front_right_peak=0, front_left_peak=0, back_right=54, back_left=-24, back_right_peak=100, back_left_peak=101, front_center=10, front_center_peak=15, ) f.render() assert len(f.data) == 22 f = RelVolAdjFrameV23() f.adjustments = RelVolAdjFrameV23.VolumeAdjustments( front_right=-10, front_left=2, front_right_peak=0, front_left_peak=0, back_right=54, back_left=-24, back_right_peak=100, back_left_peak=101, ) f.render() assert len(f.data) == 18 def test_default_v24(): f = RelVolAdjFrameV24() assert f.id == b"RVA2" f.channel_type = RelVolAdjFrameV24.CHANNEL_TYPE_MASTER f.adjustment = -6.3 f.peak = 666 f.render() f2 = RelVolAdjFrameV24() f2.parse(f.data, f.header) assert f.adjustment == approx(-6.3) assert f2.peak == 666 def test_RVAD_RVA2(audiofile): # RVAD -> *RVA2 audiofile.initTag(version=ID3_V2_3) audiofile.tag.frame_set[b"RVAD"] = RelVolAdjFrameV23() assert audiofile.tag.frame_set[b"RVAD"][0].adjustments is None adj = RelVolAdjFrameV23.VolumeAdjustments(front_left=20, front_right=19, back_left=-1, bass_peak=1024) audiofile.tag.frame_set[b"RVAD"][0].adjustments = adj # Convert to RVA2 audiofile.tag.version = ID3_V2_4 rva2_frames = {frame.channel_type: frame for frame in audiofile.tag.frame_set[b"RVA2"]} assert len(rva2_frames) == 4 assert set(rva2_frames.keys()) == {RelVolAdjFrameV24.CHANNEL_TYPE_FRONT_LEFT, RelVolAdjFrameV24.CHANNEL_TYPE_FRONT_RIGHT, RelVolAdjFrameV24.CHANNEL_TYPE_BACK_LEFT, RelVolAdjFrameV24.CHANNEL_TYPE_BASS} assert rva2_frames[RelVolAdjFrameV24.CHANNEL_TYPE_FRONT_RIGHT].adjustment == approx(0.037109375) assert rva2_frames[RelVolAdjFrameV24.CHANNEL_TYPE_FRONT_LEFT].adjustment == approx(0.0390625) assert rva2_frames[RelVolAdjFrameV24.CHANNEL_TYPE_BACK_LEFT].adjustment == approx(-0.001953125) assert rva2_frames[RelVolAdjFrameV24.CHANNEL_TYPE_BASS].adjustment == 0 assert rva2_frames[RelVolAdjFrameV24.CHANNEL_TYPE_BASS].peak == 1024 # RVA2 --> RVAD audiofile.initTag(version=ID3_V2_4) assert len(audiofile.tag.frame_set) == 0 for frame in rva2_frames.values(): if b"RVA2" not in audiofile.tag.frame_set: audiofile.tag.frame_set[b"RVA2"] = frame else: audiofile.tag.frame_set[b"RVA2"].append(frame) assert len(audiofile.tag.frame_set) == 1 assert len(audiofile.tag.frame_set[b"RVA2"]) == 4 audiofile.tag.version = ID3_V2_3 assert len(audiofile.tag.frame_set) == 1 assert len(audiofile.tag.frame_set[b"RVAD"]) == 1 assert audiofile.tag.frame_set[b"RVAD"][0].adjustments == \ RelVolAdjFrameV23.VolumeAdjustments(front_left=20, front_right=19,back_left=-1, bass_peak=1024) def test_RelVolAdjFrameV24_channel_type(): for valid in range(9): RelVolAdjFrameV24().channel_type = valid for invalid in (-1, 9): with pytest.raises(ValueError): RelVolAdjFrameV24().channel_type = invalid def test_RelVolAdjFrameV24_render_invalid_peak(): rva2 = RelVolAdjFrameV24() rva2.peak = 2**32 - 1 assert rva2.render() rva2.peak = 2**32 with pytest.raises(ValueError): rva2.render() rva2.peak = 2**64 with pytest.raises(ValueError): rva2.render() eyeD3-0.9.7/tests/id3/test_tag.py000066400000000000000000001137511432016011500165060ustar00rootroot00000000000000import os import pytest import unittest import deprecation import eyed3 from eyed3.core import Date from eyed3.id3 import frames from eyed3.id3 import Tag, ID3_DEFAULT_VERSION, ID3_V2_3, ID3_V2_4 from .. import DATA_D def testTagImport(): import eyed3.id3.tag assert eyed3.id3.Tag == eyed3.id3.tag.Tag def testTagConstructor(): t = Tag() assert t.file_info is None assert t.header is not None assert t.extended_header is not None assert t.frame_set is not None assert len(t.frame_set) == 0 def testFileInfoConstructor(): from eyed3.id3.tag import FileInfo # Both bytes and unicode input file names must be accepted and the former # must be converted to unicode. for name in [__file__, str(__file__)]: fi = FileInfo(name) assert type(fi.name) is str assert name == str(name) assert fi.tag_size == 0 def testTagMainProps(): tag = Tag() # No version yet assert tag.version == ID3_DEFAULT_VERSION assert not(tag.isV1()) assert tag.isV2() assert tag.artist is None tag.artist = "Autolux" assert tag.artist == "Autolux" assert len(tag.frame_set) == 1 tag.artist = "" assert len(tag.frame_set) == 0 tag.artist = "Autolux" assert tag.album is None tag.album = "Future Perfect" assert tag.album == "Future Perfect" assert tag.album_artist is None tag.album_artist = "Various Artists" assert (tag.album_artist == "Various Artists") assert (tag.title is None) tag.title = "Robots in the Garden" assert (tag.title == "Robots in the Garden") assert (tag.track_num == (None, None)) tag.track_num = 7 assert (tag.track_num == (7, None)) tag.track_num = (7, None) assert (tag.track_num == (7, None)) tag.track_num = (7, 15) assert (tag.frame_set[frames.TRACKNUM_FID][0].text == "07/15") assert (tag.track_num == (7, 15)) tag.track_num = (7, 150) assert (tag.frame_set[frames.TRACKNUM_FID][0].text == "007/150") assert (tag.track_num == (7, 150)) tag.track_num = (1, 7) assert (tag.frame_set[frames.TRACKNUM_FID][0].text == "01/07") assert (tag.track_num == (1, 7)) tag.track_num = None assert (tag.track_num == (None, None)) tag.track_num = None, None def testTagDates(): tag = Tag() tag.release_date = 2004 assert tag.release_date == Date(2004) tag.release_date = None assert tag.release_date is None tag = Tag() for date in [Date(2002), Date(2002, 11, 26), Date(2002, 11, 26), Date(2002, 11, 26, 4), Date(2002, 11, 26, 4, 20), Date(2002, 11, 26, 4, 20), Date(2002, 11, 26, 4, 20, 10)]: tag.encoding_date = date assert (tag.encoding_date == date) tag.encoding_date = str(date) assert (tag.encoding_date == date) tag.release_date = date assert (tag.release_date == date) tag.release_date = str(date) assert (tag.release_date == date) tag.original_release_date = date assert (tag.original_release_date == date) tag.original_release_date = str(date) assert (tag.original_release_date == date) tag.recording_date = date assert (tag.recording_date == date) tag.recording_date = str(date) assert (tag.recording_date == date) tag.tagging_date = date assert (tag.tagging_date == date) tag.tagging_date = str(date) assert (tag.tagging_date == date) try: tag._setDate(b"TDRL", 2.4) except TypeError: pass # expected else: assert not("Invalid date type, expected TypeError") def testTagComments(): tag = Tag() for c in tag.comments: assert not("Expected not to be here") # Adds with pytest.raises(TypeError): tag.comments.set(b"bold") with pytest.raises(TypeError): tag.comments.set("bold", b"search") tag.comments.set("Always Try", "") assert (len(tag.comments) == 1) c = tag.comments[0] assert (c.description == "") assert (c.text == "Always Try") assert (c.lang == b"eng") tag.comments.set("Speak Out", "Bold") assert (len(tag.comments) == 2) c = tag.comments[1] assert (c.description == "Bold") assert (c.text == "Speak Out") assert (c.lang == b"eng") tag.comments.set("K Town Mosh Crew", "Crippled Youth", b"sxe") assert (len(tag.comments) == 3) c = tag.comments[2] assert (c.description == "Crippled Youth") assert (c.text == "K Town Mosh Crew") assert (c.lang == b"sxe") # Lang is different, new frame tag.comments.set("K Town Mosh Crew", "Crippled Youth", b"eng") assert (len(tag.comments) == 4) c = tag.comments[3] assert (c.description == "Crippled Youth") assert (c.text == "K Town Mosh Crew") assert (c.lang == b"eng") # Gets assert (tag.comments.get("", "fre") is None) assert (tag.comments.get("Crippled Youth", b"esp") is None) c = tag.comments.get("") assert c assert (c.description == "") assert (c.text == "Always Try") assert (c.lang == b"eng") assert tag.comments.get("Bold") is not None assert tag.comments.get("Bold", b"eng") is not None assert tag.comments.get("Crippled Youth", b"eng") is not None assert tag.comments.get("Crippled Youth", b"sxe") is not None assert (len(tag.comments) == 4) # Iterate count = 0 for c in tag.comments: count += 1 assert count == 4 # Index access assert tag.comments[0] assert tag.comments[1] assert tag.comments[2] assert tag.comments[3] try: c = tag.comments[4] except IndexError: pass # expected else: assert not("Expected IndexError, but got success") # Removal with pytest.raises(TypeError): tag.comments.remove(b"not unicode") assert (tag.comments.remove("foobazz") is None) c = tag.comments.get("Bold") assert c is not None c2 = tag.comments.remove("Bold") assert (c == c2) assert (len(tag.comments) == 3) c = tag.comments.get("Crippled Youth", b"eng") assert c is not None c2 = tag.comments.remove("Crippled Youth", b"eng") assert (c == c2) assert (len(tag.comments) == 2) assert (tag.comments.remove("Crippled Youth", b"eng") is None) assert (len(tag.comments) == 2) assert (tag.comments.get("") == tag.comments.remove("")) assert (len(tag.comments) == 1) assert (tag.comments.get("Crippled Youth", b"sxe") == tag.comments.remove("Crippled Youth", b"sxe")) assert (len(tag.comments) == 0) # Index Error when there are no comments try: c = tag.comments[0] except IndexError: pass # expected else: assert not("Expected IndexError, but got success") # Replacing frames thru add and frame object preservation tag = Tag() c1 = tag.comments.set("Snoop", "Dog", b"rap") assert tag.comments.get("Dog", b"rap").text == "Snoop" c1.text = "Lollipop" assert tag.comments.get("Dog", b"rap").text == "Lollipop" # now thru add c2 = tag.comments.set("Doggy", "Dog", b"rap") assert id(c1) == id(c2) assert tag.comments.get("Dog", b"rap").text == "Doggy" def testTagBPM(): tag = Tag() assert (tag.bpm is None) tag.bpm = 150 assert (tag.bpm == 150) assert (tag.frame_set[b"TBPM"]) tag.bpm = 180 assert (tag.bpm == 180) assert (tag.frame_set[b"TBPM"]) assert (len(tag.frame_set[b"TBPM"]) == 1) tag.bpm = 190.5 assert type(tag.bpm) is int assert tag.bpm == 191 assert len(tag.frame_set[b"TBPM"]) == 1 def testTagPlayCount(): tag = Tag() assert (tag.play_count is None) tag.play_count = 0 assert tag.play_count == 0 tag.play_count = 1 assert tag.play_count == 1 tag.play_count += 1 assert tag.play_count == 2 tag.play_count -= 1 assert tag.play_count == 1 tag.play_count *= 5 assert tag.play_count == 5 tag.play_count = None assert tag.play_count is None try: tag.play_count = -1 except ValueError: pass # expected else: assert not("Invalid play count, expected ValueError") def testTagPublisher(): t = Tag() assert (t.publisher is None) try: t.publisher = b"not unicode" except TypeError: pass #expected else: assert not("Expected TypeError when setting non-unicode publisher") t.publisher = "Dischord" assert t.publisher == "Dischord" t.publisher = "Infinity Cat" assert t.publisher == "Infinity Cat" t.publisher = None assert t.publisher is None def testTagCdId(): tag = Tag() assert tag.cd_id is None tag.cd_id = b"\x01\x02" assert tag.cd_id == b"\x01\x02" tag.cd_id = b"\xff" * 804 assert tag.cd_id == b"\xff" * 804 try: tag.cd_id = b"\x00" * 805 except ValueError: pass # expected else: assert not("CD id is too long, expected ValueError") def testTagImages(): from eyed3.id3.frames import ImageFrame tag = Tag() # No images assert len(tag.images) == 0 for i in tag.images: assert not("Expected no images") try: img = tag.images[0] except IndexError: pass #expected else: assert not("Expected IndexError for no images") assert (tag.images.get("") is None) # Image types must be within range for i in range(ImageFrame.MIN_TYPE, ImageFrame.MAX_TYPE): tag.images.set(i, b"\xff", b"img") for i in (ImageFrame.MIN_TYPE - 1, ImageFrame.MAX_TYPE + 1): try: tag.images.set(i, b"\xff", b"img") except ValueError: pass # expected else: assert not("Expected ValueError for invalid picture type") tag = Tag() tag.images.set(ImageFrame.FRONT_COVER, b"\xab\xcd", b"img/gif") assert (len(tag.images) == 1) assert (tag.images[0].description == "") assert (tag.images[0].picture_type == ImageFrame.FRONT_COVER) assert (tag.images[0].image_data == b"\xab\xcd") assert (tag.images[0].mime_type == "img/gif") assert (tag.images[0]._mime_type == b"img/gif") assert (tag.images[0].image_url is None) assert (tag.images.get("").description == "") assert (tag.images.get("").picture_type == ImageFrame.FRONT_COVER) assert (tag.images.get("").image_data == b"\xab\xcd") assert (tag.images.get("").mime_type == "img/gif") assert (tag.images.get("")._mime_type == b"img/gif") assert (tag.images.get("").image_url is None) tag.images.set(ImageFrame.FRONT_COVER, b"\xdc\xba", b"img/gif", "Different") assert len(tag.images) == 2 assert tag.images[1].description == "Different" assert tag.images[1].picture_type == ImageFrame.FRONT_COVER assert tag.images[1].image_data == b"\xdc\xba" assert tag.images[1].mime_type == "img/gif" assert tag.images[1]._mime_type == b"img/gif" assert tag.images[1].image_url is None assert (tag.images.get("Different").description == "Different") assert (tag.images.get("Different").picture_type == ImageFrame.FRONT_COVER) assert (tag.images.get("Different").image_data == b"\xdc\xba") assert (tag.images.get("Different").mime_type == "img/gif") assert (tag.images.get("Different")._mime_type == b"img/gif") assert (tag.images.get("Different").image_url is None) # This is an update (same description) tag.images.set(ImageFrame.BACK_COVER, b"\xff\xef", b"img/jpg", "Different") assert (len(tag.images) == 2) assert (tag.images[1].description == "Different") assert (tag.images[1].picture_type == ImageFrame.BACK_COVER) assert (tag.images[1].image_data == b"\xff\xef") assert (tag.images[1].mime_type == "img/jpg") assert (tag.images[1].image_url is None) assert (tag.images.get("Different").description == "Different") assert (tag.images.get("Different").picture_type == ImageFrame.BACK_COVER) assert (tag.images.get("Different").image_data == b"\xff\xef") assert (tag.images.get("Different").mime_type == "img/jpg") assert (tag.images.get("Different").image_url is None) count = 0 for img in tag.images: count += 1 assert count == 2 # Remove img = tag.images.remove("") assert (img.description == "") assert (img.picture_type == ImageFrame.FRONT_COVER) assert (img.image_data == b"\xab\xcd") assert (img.mime_type == "img/gif") assert (img.image_url is None) assert (len(tag.images) == 1) img = tag.images.remove("Different") assert img.description == "Different" assert img.picture_type == ImageFrame.BACK_COVER assert img.image_data == b"\xff\xef" assert img.mime_type == "img/jpg" assert img.image_url is None assert len(tag.images) == 0 assert (tag.images.remove("Lundqvist") is None) # Unicode enforcement with pytest.raises(TypeError): tag.images.get(b"not Unicode") with pytest.raises(TypeError): tag.images.set(ImageFrame.ICON, "\xff", "img", b"not Unicode") with pytest.raises(TypeError): tag.images.remove(b"not Unicode") # Image URL tag = Tag() tag.images.set(ImageFrame.BACK_COVER, None, None, "A URL", img_url=b"http://www.tumblr.com/tagged/ty-segall") img = tag.images.get("A URL") assert img is not None assert (img.image_data is None) assert (img.image_url == b"http://www.tumblr.com/tagged/ty-segall") assert (img.mime_type == "-->") assert (img._mime_type == b"-->") # Unicode mime-type in, converted to bytes tag = Tag() tag.images.set(ImageFrame.BACK_COVER, b"\x00", "img/jpg") img = tag.images[0] assert isinstance(img._mime_type, bytes) img.mime_type = "" assert isinstance(img._mime_type, bytes) img.mime_type = None assert isinstance(img._mime_type, bytes) assert img.mime_type == "" def testTagLyrics(): tag = Tag() for c in tag.lyrics: assert not("Expected not to be here") # Adds with pytest.raises(TypeError): tag.lyrics.set(b"bold") with pytest.raises(TypeError): tag.lyrics.set("bold", b"search") tag.lyrics.set("Always Try", "") assert (len(tag.lyrics) == 1) c = tag.lyrics[0] assert (c.description == "") assert (c.text == "Always Try") assert (c.lang == b"eng") tag.lyrics.set("Speak Out", "Bold") assert (len(tag.lyrics) == 2) c = tag.lyrics[1] assert (c.description == "Bold") assert (c.text == "Speak Out") assert (c.lang == b"eng") tag.lyrics.set("K Town Mosh Crew", "Crippled Youth", b"sxe") assert (len(tag.lyrics) == 3) c = tag.lyrics[2] assert (c.description == "Crippled Youth") assert (c.text == "K Town Mosh Crew") assert (c.lang == b"sxe") # Lang is different, new frame tag.lyrics.set("K Town Mosh Crew", "Crippled Youth", b"eng") assert (len(tag.lyrics) == 4) c = tag.lyrics[3] assert (c.description == "Crippled Youth") assert (c.text == "K Town Mosh Crew") assert (c.lang == b"eng") # Gets assert (tag.lyrics.get("", b"fre") is None) assert (tag.lyrics.get("Crippled Youth", b"esp") is None) c = tag.lyrics.get("") assert (c) assert (c.description == "") assert (c.text == "Always Try") assert (c.lang == b"eng") assert tag.lyrics.get("Bold") is not None assert tag.lyrics.get("Bold", b"eng") is not None assert tag.lyrics.get("Crippled Youth", b"eng") is not None assert tag.lyrics.get("Crippled Youth", b"sxe") is not None assert (len(tag.lyrics) == 4) # Iterate count = 0 for c in tag.lyrics: count += 1 assert (count == 4) # Index access assert (tag.lyrics[0]) assert (tag.lyrics[1]) assert (tag.lyrics[2]) assert (tag.lyrics[3]) try: c = tag.lyrics[4] except IndexError: pass # expected else: assert not("Expected IndexError, but got success") # Removal with pytest.raises(TypeError): tag.lyrics.remove(b"not unicode") assert tag.lyrics.remove("foobazz") is None c = tag.lyrics.get("Bold") assert c is not None c2 = tag.lyrics.remove("Bold") assert c == c2 assert len(tag.lyrics) == 3 c = tag.lyrics.get("Crippled Youth", b"eng") assert c is not None c2 = tag.lyrics.remove("Crippled Youth", b"eng") assert c == c2 assert len(tag.lyrics) == 2 assert tag.lyrics.remove("Crippled Youth", b"eng") is None assert len(tag.lyrics) == 2 assert tag.lyrics.get("") == tag.lyrics.remove("") assert len(tag.lyrics) == 1 assert (tag.lyrics.get("Crippled Youth", b"sxe") == tag.lyrics.remove("Crippled Youth", b"sxe")) assert len(tag.lyrics) == 0 # Index Error when there are no lyrics try: c = tag.lyrics[0] except IndexError: pass # expected else: assert not("Expected IndexError, but got success") def testTagObjects(): tag = Tag() # No objects assert len(tag.objects) == 0 for i in tag.objects: assert not("Expected no objects") try: img = tag.objects[0] except IndexError: pass #expected else: assert not("Expected IndexError for no objects") assert (tag.objects.get("") is None) tag = Tag() tag.objects.set(b"\xab\xcd", b"img/gif") assert (len(tag.objects) == 1) assert (tag.objects[0].description == "") assert (tag.objects[0].filename == "") assert (tag.objects[0].object_data == b"\xab\xcd") assert (tag.objects[0]._mime_type == b"img/gif") assert (tag.objects[0].mime_type == "img/gif") assert (tag.objects.get("").description == "") assert (tag.objects.get("").filename == "") assert (tag.objects.get("").object_data == b"\xab\xcd") assert (tag.objects.get("").mime_type == "img/gif") tag.objects.set(b"\xdc\xba", b"img/gif", "Different") assert (len(tag.objects) == 2) assert (tag.objects[1].description == "Different") assert (tag.objects[1].filename == "") assert (tag.objects[1].object_data == b"\xdc\xba") assert (tag.objects[1]._mime_type == b"img/gif") assert (tag.objects[1].mime_type == "img/gif") assert (tag.objects.get("Different").description == "Different") assert (tag.objects.get("Different").filename == "") assert (tag.objects.get("Different").object_data == b"\xdc\xba") assert (tag.objects.get("Different").mime_type == "img/gif") assert (tag.objects.get("Different")._mime_type == b"img/gif") # This is an update (same description) tag.objects.set(b"\xff\xef", b"img/jpg", "Different", "example_filename.XXX") assert (len(tag.objects) == 2) assert (tag.objects[1].description == "Different") assert (tag.objects[1].filename == "example_filename.XXX") assert (tag.objects[1].object_data == b"\xff\xef") assert (tag.objects[1].mime_type == "img/jpg") assert (tag.objects.get("Different").description == "Different") assert (tag.objects.get("Different").filename == "example_filename.XXX") assert (tag.objects.get("Different").object_data == b"\xff\xef") assert (tag.objects.get("Different").mime_type == "img/jpg") count = 0 for obj in tag.objects: count += 1 assert (count == 2) # Remove obj = tag.objects.remove("") assert (obj.description == "") assert (obj.filename == "") assert (obj.object_data == b"\xab\xcd") assert (obj.mime_type == "img/gif") assert (len(tag.objects) == 1) obj = tag.objects.remove("Different") assert (obj.description == "Different") assert (obj.filename == "example_filename.XXX") assert (obj.object_data == b"\xff\xef") assert (obj.mime_type == "img/jpg") assert (obj._mime_type == b"img/jpg") assert (len(tag.objects) == 0) assert (tag.objects.remove("Dubinsky") is None) # Unicode enforcement with pytest.raises(TypeError): tag.objects.get(b"not Unicode") with pytest.raises(TypeError): tag.objects.set("\xff", "img", b"not Unicode") with pytest.raises(TypeError): tag.objects.set("\xff", "img", "Unicode", b"not unicode") with pytest.raises(TypeError): tag.objects.remove(b"not Unicode") def testTagPrivates(): tag = Tag() # No private frames assert len(tag.privates) == 0 for i in tag.privates: assert not("Expected no privates") try: img = tag.privates[0] except IndexError: pass #expected else: assert not("Expected IndexError for no privates") assert (tag.privates.get(b"") is None) tag = Tag() tag.privates.set(b"\xab\xcd", b"owner1") assert (len(tag.privates) == 1) assert (tag.privates[0].owner_id == b"owner1") assert (tag.privates[0].owner_data == b"\xab\xcd") assert (tag.privates.get(b"owner1").owner_id == b"owner1") assert (tag.privates.get(b"owner1").owner_data == b"\xab\xcd") tag.privates.set(b"\xba\xdc", b"owner2") assert (len(tag.privates) == 2) assert (tag.privates[1].owner_id == b"owner2") assert (tag.privates[1].owner_data == b"\xba\xdc") assert (tag.privates.get(b"owner2").owner_id == b"owner2") assert (tag.privates.get(b"owner2").owner_data == b"\xba\xdc") # This is an update (same description) tag.privates.set(b"\x00\x00\x00", b"owner1") assert (len(tag.privates) == 2) assert (tag.privates[0].owner_id == b"owner1") assert (tag.privates[0].owner_data == b"\x00\x00\x00") assert (tag.privates.get(b"owner1").owner_id == b"owner1") assert (tag.privates.get(b"owner1").owner_data == b"\x00\x00\x00") count = 0 for f in tag.privates: count += 1 assert (count == 2) # Remove priv = tag.privates.remove(b"owner1") assert (priv.owner_id == b"owner1") assert (priv.owner_data == b"\x00\x00\x00") assert (len(tag.privates) == 1) priv = tag.privates.remove(b"owner2") assert (priv.owner_id == b"owner2") assert (priv.owner_data == b"\xba\xdc") assert (len(tag.privates) == 0) assert tag.objects.remove("Callahan") is None def testTagDiscNum(): tag = Tag() assert (tag.disc_num == (None, None)) tag.disc_num = 7 assert (tag.disc_num == (7, None)) tag.disc_num = (7, None) assert (tag.disc_num == (7, None)) tag.disc_num = (7, 15) assert (tag.frame_set[frames.DISCNUM_FID][0].text == "07/15") assert (tag.disc_num == (7, 15)) tag.disc_num = (7, 150) assert (tag.frame_set[frames.DISCNUM_FID][0].text == "007/150") assert (tag.disc_num == (7, 150)) tag.disc_num = (1, 7) assert (tag.frame_set[frames.DISCNUM_FID][0].text == "01/07") assert (tag.disc_num == (1, 7)) tag.disc_num = None assert (tag.disc_num == (None, None)) tag.disc_num = None, None def testTagGenre(): from eyed3.id3 import Genre tag = Tag() assert (tag.genre is None) try: tag.genre = b"Not Unicode" except TypeError: pass # expected else: assert not "Non unicode genre, expected TypeError" gobj = Genre("Hardcore") tag.genre = "Hardcore" assert (tag.genre.name == "Hardcore") assert (tag.genre == gobj) tag.genre = 130 assert tag.genre.id == 130 assert tag.genre.name == "Terror" tag.genre = 0 assert tag.genre.id == 0 assert tag.genre.name == "Blues" tag.genre = None assert tag.genre is None assert tag.frame_set[b"TCON"] is None def testTagUserTextFrames(): tag = Tag() assert (len(tag.user_text_frames) == 0) utf1 = tag.user_text_frames.set("Custom content") assert (tag.user_text_frames.get("").text == "Custom content") utf2 = tag.user_text_frames.set("Content custom", "Desc1") assert (tag.user_text_frames.get("Desc1").text == "Content custom") assert (len(tag.user_text_frames) == 2) utf3 = tag.user_text_frames.set("New content", "") assert (tag.user_text_frames.get("").text == "New content") assert (len(tag.user_text_frames) == 2) assert (id(utf1) == id(utf3)) assert (tag.user_text_frames[0] == utf1) assert (tag.user_text_frames[1] == utf2) assert (tag.user_text_frames.get("") == utf1) assert (tag.user_text_frames.get("Desc1") == utf2) tag.user_text_frames.remove("") assert (len(tag.user_text_frames) == 1) tag.user_text_frames.remove("Desc1") assert (len(tag.user_text_frames) == 0) tag.user_text_frames.set("Foobazz", "Desc2") assert (len(tag.user_text_frames) == 1) def testTagUrls(): tag = Tag() url = "http://example.com/" url2 = "http://sample.com/" tag.commercial_url = url assert (tag.commercial_url == url) tag.commercial_url = url2 assert (tag.commercial_url == url2) tag.commercial_url = None assert (tag.commercial_url is None) tag.copyright_url = url assert (tag.copyright_url == url) tag.copyright_url = url2 assert (tag.copyright_url == url2) tag.copyright_url = None assert (tag.copyright_url is None) tag.audio_file_url = url assert (tag.audio_file_url == url) tag.audio_file_url = url2 assert (tag.audio_file_url == url2) tag.audio_file_url = None assert (tag.audio_file_url is None) tag.audio_source_url = url assert (tag.audio_source_url == url) tag.audio_source_url = url2 assert (tag.audio_source_url == url2) tag.audio_source_url = None assert (tag.audio_source_url is None) tag.artist_url = url assert (tag.artist_url == url) tag.artist_url = url2 assert (tag.artist_url == url2) tag.artist_url = None assert (tag.artist_url is None) tag.internet_radio_url = url assert (tag.internet_radio_url == url) tag.internet_radio_url = url2 assert (tag.internet_radio_url == url2) tag.internet_radio_url = None assert (tag.internet_radio_url is None) tag.payment_url = url assert (tag.payment_url == url) tag.payment_url = url2 assert (tag.payment_url == url2) tag.payment_url = None assert (tag.payment_url is None) tag.publisher_url = url assert (tag.publisher_url == url) tag.publisher_url = url2 assert (tag.publisher_url == url2) tag.publisher_url = None assert (tag.publisher_url is None) # Frame ID enforcement with pytest.raises(ValueError): tag._setUrlFrame("WDDD", "url") with pytest.raises(ValueError): tag._getUrlFrame("WDDD") def testTagUniqIds(): tag = Tag() assert (len(tag.unique_file_ids) == 0) tag.unique_file_ids.set(b"http://music.com/12354", b"test") tag.unique_file_ids.set(b"1234", b"http://eyed3.nicfit.net") assert tag.unique_file_ids.get(b"test").uniq_id == b"http://music.com/12354" assert (tag.unique_file_ids.get(b"http://eyed3.nicfit.net").uniq_id == b"1234") assert len(tag.unique_file_ids) == 2 tag.unique_file_ids.remove(b"test") assert len(tag.unique_file_ids) == 1 tag.unique_file_ids.set(b"4321", b"http://eyed3.nicfit.net") assert len(tag.unique_file_ids) == 1 assert (tag.unique_file_ids.get(b"http://eyed3.nicfit.net").uniq_id == b"4321") tag.unique_file_ids.set("1111", "") assert len(tag.unique_file_ids) == 2 def testTagUniqIdsUnicode(): tag = Tag() assert (len(tag.unique_file_ids) == 0) tag.unique_file_ids.set("http://music.com/12354", "test") tag.unique_file_ids.set("1234", "http://eyed3.nicfit.net") assert tag.unique_file_ids.get("test").uniq_id == b"http://music.com/12354" assert (tag.unique_file_ids.get("http://eyed3.nicfit.net").uniq_id == b"1234") assert len(tag.unique_file_ids) == 2 tag.unique_file_ids.remove("test") assert len(tag.unique_file_ids) == 1 tag.unique_file_ids.set("4321", "http://eyed3.nicfit.net") assert len(tag.unique_file_ids) == 1 assert (tag.unique_file_ids.get("http://eyed3.nicfit.net").uniq_id == b"4321") def testTagUserUrls(): tag = Tag() assert (len(tag.user_url_frames) == 0) uuf1 = tag.user_url_frames.set(b"http://yo.yo.com/") assert (tag.user_url_frames.get("").url == "http://yo.yo.com/") utf2 = tag.user_url_frames.set("http://run.dmc.org", "URL") assert (tag.user_url_frames.get("URL").url == "http://run.dmc.org") assert len(tag.user_url_frames) == 2 utf3 = tag.user_url_frames.set(b"http://my.adidas.com", "") assert (tag.user_url_frames.get("").url == "http://my.adidas.com") assert (len(tag.user_url_frames) == 2) assert (id(uuf1) == id(utf3)) assert (tag.user_url_frames[0] == uuf1) assert (tag.user_url_frames[1] == utf2) assert (tag.user_url_frames.get("") == uuf1) assert (tag.user_url_frames.get("URL") == utf2) tag.user_url_frames.remove("") assert (len(tag.user_url_frames) == 1) tag.user_url_frames.remove("URL") assert (len(tag.user_url_frames) == 0) tag.user_url_frames.set("Foobazz", "Desc2") assert (len(tag.user_url_frames) == 1) def testSortOrderConversions(): test_file = "/tmp/soconvert.id3" tag = Tag() # 2.3 frames to 2.4 for fid in [b"XSOA", b"XSOP", b"XSOT"]: frame = frames.TextFrame(fid) frame.text = fid.decode("ascii") tag.frame_set[fid] = frame try: tag.save(test_file) # v2.4 is the default tag = eyed3.load(test_file).tag assert (tag.version == ID3_V2_4) assert (len(tag.frame_set) == 3) del tag.frame_set[b"TSOA"] del tag.frame_set[b"TSOP"] del tag.frame_set[b"TSOT"] assert (len(tag.frame_set) == 0) finally: os.remove(test_file) tag = Tag() # 2.4 frames to 2.3 for fid in [b"TSOA", b"TSOP", b"TSOT"]: frame = frames.TextFrame(fid) frame.text = str(fid) tag.frame_set[fid] = frame try: tag.save(test_file, version=eyed3.id3.ID3_V2_3) tag = eyed3.load(test_file).tag assert (tag.version == ID3_V2_3) assert (len(tag.frame_set) == 3) del tag.frame_set[b"XSOA"] del tag.frame_set[b"XSOP"] del tag.frame_set[b"XSOT"] assert (len(tag.frame_set) == 0) finally: os.remove(test_file) def test_XDOR_TDOR_Conversions(): test_file = "/tmp/xdortdrc.id3" tag = Tag() # 2.3 frames to 2.4 frame = frames.DateFrame(b"XDOR", "1990-06-24") tag.frame_set[b"XDOR"] = frame try: tag.save(test_file) # v2.4 is the default tag = eyed3.load(test_file).tag assert tag.version == ID3_V2_4 assert len(tag.frame_set) == 1 del tag.frame_set[b"TDOR"] assert len(tag.frame_set) == 0 finally: os.remove(test_file) tag = Tag() # 2.4 frames to 2.3 frame = frames.DateFrame(b"TDRC", "2012-10-21") tag.frame_set[frame.id] = frame try: tag.save(test_file, version=eyed3.id3.ID3_V2_3) tag = eyed3.load(test_file).tag assert tag.version == ID3_V2_3 assert len(tag.frame_set) == 2 del tag.frame_set[b"TYER"] del tag.frame_set[b"TDAT"] assert len(tag.frame_set) == 0 finally: os.remove(test_file) def test_TSST_Conversions(): test_file = "/tmp/tsst.id3" tag = Tag() # 2.4 TSST to 2.3 TIT3 tag.frame_set.setTextFrame(b"TSST", "Subtitle") try: tag.save(test_file) # v2.4 is the default tag = eyed3.load(test_file).tag assert tag.version == ID3_V2_4 assert len(tag.frame_set) == 1 del tag.frame_set[b"TSST"] assert len(tag.frame_set) == 0 tag.frame_set.setTextFrame(b"TSST", "Subtitle") tag.save(test_file, version=eyed3.id3.ID3_V2_3) tag = eyed3.load(test_file).tag assert b"TXXX" in tag.frame_set txxx = tag.frame_set[b"TXXX"][0] assert txxx.text == "Subtitle" assert txxx.description == "Subtitle (converted)" finally: os.remove(test_file) @unittest.skipIf(not os.path.exists(DATA_D), "test requires data files") def testChapterExampleTag(): tag = eyed3.load(os.path.join(DATA_D, "id3_chapters_example.mp3")).tag assert len(tag.table_of_contents) == 1 toc = list(tag.table_of_contents)[0] assert id(toc) == id(tag.table_of_contents.get(toc.element_id)) assert toc.element_id == b"toc1" assert toc.description is None assert toc.toplevel assert toc.ordered assert toc.child_ids == [b'ch1', b'ch2', b'ch3'] assert tag.chapters.get(b"ch1").title == "start" assert tag.chapters.get(b"ch1").subtitle is None assert tag.chapters.get(b"ch1").user_url is None assert tag.chapters.get(b"ch1").times == (0, 5000) assert tag.chapters.get(b"ch1").offsets == (None, None) assert len(tag.chapters.get(b"ch1").sub_frames) == 1 assert tag.chapters.get(b"ch2").title == "5 seconds" assert tag.chapters.get(b"ch2").subtitle is None assert tag.chapters.get(b"ch2").user_url is None assert tag.chapters.get(b"ch2").times == (5000, 10000) assert tag.chapters.get(b"ch2").offsets == (None, None) assert len(tag.chapters.get(b"ch2").sub_frames) == 1 assert tag.chapters.get(b"ch3").title == "10 seconds" assert tag.chapters.get(b"ch3").subtitle is None assert tag.chapters.get(b"ch3").user_url is None assert tag.chapters.get(b"ch3").times == (10000, 15000) assert tag.chapters.get(b"ch3").offsets == (None, None) assert len(tag.chapters.get(b"ch3").sub_frames) == 1 def testTableOfContents(): test_file = "/tmp/toc.id3" t = Tag() assert (len(t.table_of_contents) == 0) toc_main = t.table_of_contents.set(b"main", toplevel=True, child_ids=[b"c1", b"c2", b"c3", b"c4"], description="Table of Conents") assert toc_main is not None assert (len(t.table_of_contents) == 1) toc_dc = t.table_of_contents.set(b"director-cut", toplevel=False, ordered=False, child_ids=[b"d3", b"d1", b"d2"]) assert toc_dc is not None assert (len(t.table_of_contents) == 2) toc_dummy = t.table_of_contents.set(b"test") assert (len(t.table_of_contents) == 3) t.table_of_contents.remove(toc_dummy.element_id) assert (len(t.table_of_contents) == 2) t.save(test_file) try: t2 = eyed3.load(test_file).tag finally: os.remove(test_file) assert len(t.table_of_contents) == 2 assert t2.table_of_contents.get(b"main").toplevel assert t2.table_of_contents.get(b"main").ordered assert t2.table_of_contents.get(b"main").description == toc_main.description assert t2.table_of_contents.get(b"main").child_ids == toc_main.child_ids assert (t2.table_of_contents.get(b"director-cut").toplevel == toc_dc.toplevel) assert not t2.table_of_contents.get(b"director-cut").ordered assert (t2.table_of_contents.get(b"director-cut").description == toc_dc.description) assert (t2.table_of_contents.get(b"director-cut").child_ids == toc_dc.child_ids) def testChapters(): test_file = "/tmp/chapters.id3" t = Tag() ch1 = t.chapters.set(b"c1", (0, 200)) ch2 = t.chapters.set(b"c2", (200, 300)) ch3 = t.chapters.set(b"c3", (300, 375)) ch4 = t.chapters.set(b"c4", (375, 600)) assert len(t.chapters) == 4 for i, c in enumerate(iter(t.chapters), 1): if i != 2: c.title = "Chapter %d" % i c.subtitle = "Subtitle %d" % i c.user_url = "http://example.com/%d" % i t.save(test_file) try: t2 = eyed3.load(test_file).tag finally: os.remove(test_file) assert len(t2.chapters) == 4 for i in range(1, 5): c = t2.chapters.get(str("c%d" % i).encode("latin1")) if i == 2: assert c.title is None assert c.subtitle is None assert c.user_url is None else: assert c.title == "Chapter %d" % i assert c.subtitle == "Subtitle %d" % i assert c.user_url == "http://example.com/%d" % i def testReadOnly(): assert not(Tag.read_only) t = Tag() assert not(t.read_only) t.read_only = True with pytest.raises(RuntimeError): t.save() with pytest.raises(RuntimeError): t._saveV1Tag(None) with pytest.raises(RuntimeError): t._saveV2Tag(None, None, None) def testSetNumExceptions(): t = Tag() with pytest.raises(ValueError) as ex: t.track_num = (1, 2, 3) @deprecation.fail_if_not_removed def testNonStdGenre(): t = Tag() t.non_std_genre = "Black Lips" assert t.genre.id is None assert t.genre.name == "Black Lips" def testNumStringConvert(): t = Tag() t.track_num = "1" assert t.track_num == (1, None) t.disc_num = ("2", "6") assert t.disc_num == (2, 6) def testReleaseDate_v23_v24(): """v23 does not have release date, only original release date.""" date = Date.parse("1980-07-03") date2 = Date.parse("1926-07-05") year = Date(1966) tag = Tag() assert tag.version == ID3_DEFAULT_VERSION tag.version = ID3_V2_3 assert tag.version == ID3_V2_3 # Setting release date sets original release date # v2.3 TORY get the year, XDOR get the full date; getter prefers XDOR tag.release_date = "2020-03-08" assert b"TORY" in tag.frame_set assert b"XDOR" in tag.frame_set assert tag.release_date == Date.parse("2020-03-08") assert tag.original_release_date == Date(year=2020, month=3, day=8) # Setting original release date sets release date tag.original_release_date = year assert tag.original_release_date == Date(1966) assert tag.release_date == Date.parse("1966") assert b"TORY" in tag.frame_set # Year only value should clean up XDOR assert b"XDOR" not in tag.frame_set # Version convert to 2.4 converts original release date only tag.release_date = date assert b"TORY" in tag.frame_set assert b"XDOR" in tag.frame_set assert tag.original_release_date == date assert tag.release_date == date tag.version = ID3_V2_4 assert tag.original_release_date == date assert tag.release_date is None # v2.4 has both date types tag.release_date = date2 assert tag.original_release_date == date assert tag.release_date == date2 assert b"TORY" not in tag.frame_set assert b"XDOR" not in tag.frame_set # Convert back to 2.3 loses release date, only the year is copied to TORY tag.version = ID3_V2_3 assert b"TORY" in tag.frame_set assert b"XDOR" in tag.frame_set assert tag.original_release_date == date assert tag.release_date == Date.parse(str(date)) eyeD3-0.9.7/tests/mp3/000077500000000000000000000000001432016011500143325ustar00rootroot00000000000000eyeD3-0.9.7/tests/mp3/__init__.py000066400000000000000000000000001432016011500164310ustar00rootroot00000000000000eyeD3-0.9.7/tests/mp3/test_infos.py000066400000000000000000000044501432016011500170640ustar00rootroot00000000000000""" Test functions and data by Jason Penney. https://bitbucket.org/nicfit/eyed3/issue/32/mp3audioinfotime_secs-incorrect-for-mpeg2 To test individual files use::: python -m test.mp3.test_infos """ import eyed3 import sys import os from decimal import Decimal from .. import DATA_D, unittest def _do_test(reported, expected): if reported != expected: return (False, "eyed3 reported %s (expected %s)" % (str(reported), str(expected))) return (True, '') def _translate_mode(mode): if mode == 'simple': return 'Stereo' if mode == 'mono': return 'Mono' if mode == 'joint' or mode == 'force': return 'Joint stereo' if mode == 'dual-mono': return 'Dual channel stereo' raise RuntimeError("unknown mode: %s" % mode) def _test_file(pth): errors = [] info = os.path.splitext(os.path.basename(pth))[0].split(' ') fil = eyed3.load(pth) tests = [ ('mpeg_version', Decimal(str(fil.info.mp3_header.version)), Decimal(info[0][-3:])), ('sample_freq', Decimal(str(fil.info.mp3_header.sample_freq))/1000, Decimal(info[1][:-3])), ('vbr', fil.info.bit_rate[0], bool(info[2] == '__vbr__')), ('stereo_mode', fil.info.mode, _translate_mode(info[3])), ('duration', round(fil.info.time_secs), 10), ] if info[2] != '__vbr__': tests.append(('bit_rate', fil.info.bit_rate[1], int(info[2][:-4]))) for test, reported, expected in tests: (passed, msg) = _do_test(reported, expected) if not passed: errors.append("%s: %s" % (test, msg)) print("%s: %s" % (os.path.basename(pth), 'FAIL' if errors else 'ok')) for err in errors: print(" %s" % err) return errors @unittest.skipIf(not os.path.exists(DATA_D), "test requires data files") def test_mp3_infos(do_assert=True): data_d = os.path.join(DATA_D, "mp3_samples") mp3s = sorted([f for f in os.listdir(data_d) if f.endswith(".mp3")]) for mp3_file in mp3s: errors = _test_file(os.path.join(data_d, mp3_file)) if do_assert: assert(len(errors) == 0) if __name__ == "__main__": if len(sys.argv) < 2: test_mp3_infos(do_assert=False) else: for mp3_file in sys.argv[1:]: errors = _test_file(mp3_file) eyeD3-0.9.7/tests/mp3/test_mp3.py000066400000000000000000000065041432016011500164470ustar00rootroot00000000000000import os import unittest import deprecation from io import BytesIO from .. import DATA_D import eyed3 def testvalidHeader(): from eyed3.mp3.headers import isValidHeader # False sync, the layer is invalid assert not isValidHeader(0xffe00000) # False sync, bitrate is invalid assert not isValidHeader(0xffe20000) assert not isValidHeader(0xffe20001) assert not isValidHeader(0xffe2000f) # False sync, sample rate is invalid assert not isValidHeader(0xffe21c34) assert not isValidHeader(0xffe21c54) # False sync, version is invalid assert not isValidHeader(0xffea0000) assert not isValidHeader(0xffea0001) assert not isValidHeader(0xffeb0001) assert not isValidHeader(0xffec0001) assert not isValidHeader(0) assert not isValidHeader(0xffffffff) assert not isValidHeader(0xffe0ffff) assert not isValidHeader(0xffe00000) assert not isValidHeader(0xfffb0000) assert isValidHeader(0xfffb9064) assert isValidHeader(0xfffb9074) assert isValidHeader(0xfffb900c) assert isValidHeader(0xfffb1900) assert isValidHeader(0xfffbd204) assert isValidHeader(0xfffba040) assert isValidHeader(0xfffba004) assert isValidHeader(0xfffb83eb) assert isValidHeader(0xfffb7050) assert isValidHeader(0xfffb32c0) def testFindHeader(): from eyed3.mp3.headers import findHeader # No header buffer = BytesIO(b'\x00' * 1024) (offset, header_int, header_bytes) = findHeader(buffer, 0) assert header_int is None # Valid header buffer = BytesIO(b'\x11\x12\x23' * 1024 + b"\xff\xfb\x90\x64" + b"\x00" * 1024) (offset, header_int, header_bytes) = findHeader(buffer, 0) assert header_int == 0xfffb9064 # Same thing with a false sync in the mix buffer = BytesIO(b'\x11\x12\x23' * 1024 + b"\x11" * 100 + b"\xff\xea\x00\x00" + # false sync b"\x22" * 100 + b"\xff\xe2\x1c\x34" + # false sync b"\xee" * 100 + b"\xff\xfb\x90\x64" + b"\x00" * 1024) (offset, header_int, header_bytes) = findHeader(buffer, 0) assert header_int == 0xfffb9064 @unittest.skipIf(not os.path.exists(DATA_D), "test requires data files") def testBasicVbrMp3(): audio_file = eyed3.load(os.path.join(DATA_D, "notag-vbr.mp3")) assert isinstance(audio_file, eyed3.mp3.Mp3AudioFile) assert audio_file.info is not None assert round(audio_file.info.time_secs) == 262 assert audio_file.info.size_bytes == 6272220 # Variable bit rate, ~191 assert audio_file.info.bit_rate[0] == True assert audio_file.info.bit_rate[1] == 191 assert audio_file.info.bit_rate_str == "~191 kb/s" assert audio_file.info.mode == "Joint stereo" assert audio_file.info.sample_freq == 44100 assert audio_file.info.mp3_header is not None assert audio_file.info.mp3_header.version == 1.0 assert audio_file.info.mp3_header.layer == 3 assert audio_file.info.xing_header is not None assert audio_file.info.lame_tag is not None assert audio_file.info.vbri_header is None assert audio_file.tag is None @deprecation.fail_if_not_removed def test_compute_time_from_frame_deprecation(): from eyed3.mp3.headers import compute_time_per_frame compute_time_per_frame(None) eyeD3-0.9.7/tests/test__init__.py000066400000000000000000000010521432016011500166420ustar00rootroot00000000000000import eyed3 def testLocale(): assert eyed3.LOCAL_ENCODING assert eyed3.LOCAL_ENCODING != "ANSI_X3.4-1968" assert eyed3.LOCAL_FS_ENCODING def testException(): ex = eyed3.Error() assert isinstance(ex, Exception) msg = "this is a test" ex = eyed3.Error(msg) assert ex.message == msg assert ex.args == (msg,) ex = eyed3.Error(msg, 1, 2) assert ex.message == msg assert ex.args == (msg, 1, 2) def test_log(): from eyed3 import log assert log is not None log.verbose("Hiya from Dr. Know") eyeD3-0.9.7/tests/test_binfuncs.py000066400000000000000000000037401432016011500170570ustar00rootroot00000000000000import pytest from eyed3.utils.binfuncs import * def test_bytes2bin(): # test ones and zeros, sz==8 for i in range(1, 11): zeros = bytes2bin(b"\x00" * i) ones = bytes2bin(b"\xFF" * i) assert len(zeros) == (8 * i) and len(zeros) == len(ones) for i in range(len(zeros)): assert zeros[i] == 0 assert ones[i] == 1 # test 'sz' bounds checking with pytest.raises(ValueError): bytes2bin(b"a", -1) with pytest.raises(ValueError): bytes2bin(b"a", 0) with pytest.raises(ValueError): bytes2bin(b"a", 9) # Test 'sz' for sz in range(1, 9): res = bytes2bin(b"\x00\xFF", sz=sz) assert len(res) == 2 * sz assert res[:sz] == [0] * sz assert res[sz:] == [1] * sz def test_bin2bytes(): res = bin2bytes([0]) assert len(res) == 1 assert ord(res) == 0 res = bin2bytes([1] * 8) assert len(res) == 1 assert ord(res) == 255 def test_bin2dec(): assert bin2dec([1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0]) == 2730 def test_bytes2dec(): assert bytes2dec(b"\x00\x11\x22\x33") == 1122867 def test_dec2bin(): assert dec2bin(3036790792) == [1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0] assert dec2bin(1, p=8) == [0, 0, 0, 0, 0, 0, 0, 1] def test_dec2bytes(): assert dec2bytes(ord(b"a")) == b"\x61" def test_bin2syncsafe(): with pytest.raises(ValueError): bin2synchsafe(bytes2bin(b"\xff\xff\xff\xff")) with pytest.raises(ValueError): bin2synchsafe([0] * 33) assert bin2synchsafe([1] * 7) == [1] * 7 assert bin2synchsafe(dec2bin(255)) == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1] eyeD3-0.9.7/tests/test_classic_plugin.py000066400000000000000000001072711432016011500202530ustar00rootroot00000000000000import os import shutil import tempfile import unittest from pathlib import Path import pytest import eyed3 from eyed3 import main, id3, core, utils from . import DATA_D, RedirectStdStreams def testPluginOption(): for arg in ["--help", "-h"]: # When help is requested and no plugin is specified, use default with RedirectStdStreams() as out: try: args, _, config = main.parseCommandLine([arg]) except SystemExit as ex: assert ex.code == 0 out.stdout.seek(0) sout = out.stdout.read() assert sout.find("Plugin options:\n Classic eyeD3") != -1 # When help is requested and all default plugin names are specified for plugin_name in ["classic"]: for args in [["--plugin=%s" % plugin_name, "--help"]]: with RedirectStdStreams() as out: try: args, _, config = main.parseCommandLine(args) except SystemExit as ex: assert ex.code == 0 out.stdout.seek(0) sout = out.stdout.read() assert sout.find("Plugin options:\n Classic eyeD3") != -1 @unittest.skipIf(not Path(DATA_D).exists(), "test requires data files") def testReadEmptyMp3(): with RedirectStdStreams() as out: args, _, config = main.parseCommandLine([os.path.join(DATA_D, "test.mp3")]) retval = main.main(args, config) assert retval == 0 assert out.stderr.read().find("No ID3 v1.x/v2.x tag found") != -1 class TestDefaultPlugin(unittest.TestCase): def __init__(self, name): super(TestDefaultPlugin, self).__init__(name) self.orig_test_file = "%s/test.mp3" % DATA_D self.test_file = "/tmp/test.mp3" fd, self.test_file = tempfile.mkstemp(suffix=".mp3") os.close(fd) @unittest.skipIf(not os.path.exists(DATA_D), "test requires data files") def setUp(self): shutil.copy(self.orig_test_file, self.test_file) def tearDown(self): # TODO: could remove the tag and compare audio file to original os.remove(self.test_file) @staticmethod def _addVersionOpt(version, opts): if version == id3.ID3_DEFAULT_VERSION: return if version[0] == 1: opts.append("--to-v1.1") elif version[:2] == (2, 3): opts.append("--to-v2.3") elif version[:2] == (2, 4): opts.append("--to-v2.4") else: assert not "Unhandled version" def testNewTagArtist(self, version=id3.ID3_DEFAULT_VERSION): for opts in [ ["-a", "The Cramps", self.test_file], ["--artist=The Cramps", self.test_file] ]: self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert retval == 0 af = eyed3.load(self.test_file) assert af is not None assert af.tag is not None assert af.tag.artist == "The Cramps" def testNewTagComposer(self, version=id3.ID3_DEFAULT_VERSION): for opts in [ ["--composer=H.R.", self.test_file] ]: self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert retval == 0 af = eyed3.load(self.test_file) assert af is not None assert af.tag is not None assert af.tag.composer == "H.R." def testNewTagAlbum(self, version=id3.ID3_DEFAULT_VERSION): for opts in [ ["-A", "Psychedelic Jungle", self.test_file], ["--album=Psychedelic Jungle", self.test_file] ]: self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) assert (af.tag.album == "Psychedelic Jungle") def testNewTagAlbumArtist(self, version=id3.ID3_DEFAULT_VERSION): for opts in [ ["-b", "Various Artists", self.test_file], ["--album-artist=Various Artists", self.test_file] ]: self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert af is not None assert af.tag is not None assert af.tag.album_artist == "Various Artists" def testNewTagTitle(self, version=id3.ID3_DEFAULT_VERSION): for opts in [ ["-t", "Green Door", self.test_file], ["--title=Green Door", self.test_file] ]: self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) assert (af.tag.title == "Green Door") def testNewTagTrackNum(self, version=id3.ID3_DEFAULT_VERSION): for opts in [ ["-n", "14", self.test_file], ["--track=14", self.test_file] ]: self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) assert (af.tag.track_num[0] == 14) def testNewTagTrackNumInvalid(self): for opts in [ ["-n", "abc", self.test_file], ["--track=-14", self.test_file] ]: with RedirectStdStreams() as out: try: args, _, config = main.parseCommandLine(opts) except SystemExit as ex: assert ex.code != 0 else: assert not("Should not have gotten here") def testNewTagTrackTotal(self, version=id3.ID3_DEFAULT_VERSION): if version[0] == 1: # No support for this in v1.x return for opts in [ ["-N", "14", self.test_file], ["--track-total=14", self.test_file] ]: self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) assert (af.tag.track_num[1] == 14) def testNewTagGenre(self, version=id3.ID3_DEFAULT_VERSION): for opts in [ ["-G", "Rock", self.test_file], ["--genre=Rock", self.test_file] ]: self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) assert (af.tag.genre.name == "Rock") assert (af.tag.genre.id == 17) def testNewTagNonStdGenre(self, version=id3.ID3_DEFAULT_VERSION): for opts in (("-G", "108", "--non-std-genre", self.test_file), ("--genre=108", "--non-std-genre", self.test_file)): self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert retval == 0 af = eyed3.load(self.test_file) assert af.tag.non_std_genre.name == "108" assert af.tag.non_std_genre.id is None def testNewTagYear(self, version=id3.ID3_DEFAULT_VERSION): for opts in [ ["-Y", "1981", self.test_file], ["--release-year=1981", self.test_file] ]: self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) if version == id3.ID3_V2_3: assert (af.tag.original_release_date.year == 1981) else: assert (af.tag.release_date.year == 1981) def testNewTagReleaseDate(self, version=id3.ID3_DEFAULT_VERSION): for date in ["1981", "1981-03-06", "1981-03"]: orig_date = core.Date.parse(date) for opts in [ ["--release-date=%s" % str(date), self.test_file] ]: self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) assert (af.tag.release_date == orig_date) def testNewTagOrigRelease(self, version=id3.ID3_DEFAULT_VERSION): for opts in [ ["--orig-release-date=1981", self.test_file] ]: self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) assert (af.tag.original_release_date.year == 1981) def testNewTagRecordingDate(self, version=id3.ID3_DEFAULT_VERSION): for opts in [ ["--recording-date=1993-10-30", self.test_file] ]: self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) assert (af.tag.recording_date.year == 1993) assert (af.tag.recording_date.month == 10) assert (af.tag.recording_date.day == 30) def testNewTagEncodingDate(self, version=id3.ID3_DEFAULT_VERSION): for opts in [ ["--encoding-date=2012-10-23T20:22", self.test_file] ]: self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) assert (af.tag.encoding_date.year == 2012) assert (af.tag.encoding_date.month == 10) assert (af.tag.encoding_date.day == 23) assert (af.tag.encoding_date.hour == 20) assert (af.tag.encoding_date.minute == 22) def testNewTagTaggingDate(self, version=id3.ID3_DEFAULT_VERSION): for opts in [ ["--tagging-date=2012-10-23T20:22", self.test_file] ]: self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) assert (af.tag.tagging_date.year == 2012) assert (af.tag.tagging_date.month == 10) assert (af.tag.tagging_date.day == 23) assert (af.tag.tagging_date.hour == 20) assert (af.tag.tagging_date.minute == 22) def testNewTagPlayCount(self): for expected, opts in [ (0, ["--play-count=0", self.test_file]), (1, ["--play-count=+1", self.test_file]), (6, ["--play-count=+5", self.test_file]), (7, ["--play-count=7", self.test_file]), (10000, ["--play-count=10000", self.test_file]), ]: with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) assert (af.tag.play_count == expected) def testNewTagPlayCountInvalid(self): for expected, opts in [ (0, ["--play-count=", self.test_file]), (0, ["--play-count=-24", self.test_file]), (0, ["--play-count=+", self.test_file]), (0, ["--play-count=abc", self.test_file]), (0, ["--play-count=False", self.test_file]), ]: with RedirectStdStreams() as out: try: args, _, config = main.parseCommandLine(opts) except SystemExit as ex: assert ex.code != 0 else: assert not("Should not have gotten here") def testNewTagBpm(self): for expected, opts in [ (1, ["--bpm=1", self.test_file]), (180, ["--bpm=180", self.test_file]), (117, ["--bpm", "116.7", self.test_file]), (116, ["--bpm", "116.4", self.test_file]), ]: with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) assert (af.tag.bpm == expected) def testNewTagBpmInvalid(self): for expected, opts in [ (0, ["--bpm=", self.test_file]), (0, ["--bpm=-24", self.test_file]), (0, ["--bpm=+", self.test_file]), (0, ["--bpm=abc", self.test_file]), (0, ["--bpm", "=180", self.test_file]), ]: with RedirectStdStreams() as out: try: args, _, config = main.parseCommandLine(opts) except SystemExit as ex: assert ex.code != 0 else: assert not("Should not have gotten here") def testNewTagPublisher(self): for expected, opts in [ ("SST", ["--publisher", "SST", self.test_file]), ("Dischord", ["--publisher=Dischord", self.test_file]), ]: with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) assert (af.tag.publisher == expected) def testUniqueFileId_1(self): with RedirectStdStreams() as out: assert out args, _, config = main.parseCommandLine(["--unique-file-id", "Travis:Me", self.test_file]) retval = main.main(args, config) assert retval == 0 af = eyed3.load(self.test_file) assert len(af.tag.unique_file_ids) == 1 assert af.tag.unique_file_ids.get(b"Travis").uniq_id == b"Me" def testUniqueFileId_dup(self): with RedirectStdStreams() as out: assert out args, _, config = \ main.parseCommandLine(["--unique-file-id", "Travis:Me", "--unique-file-id=Travis:Me", self.test_file]) retval = main.main(args, config) assert retval == 0 af = eyed3.load(self.test_file) assert len(af.tag.unique_file_ids) == 1 assert af.tag.unique_file_ids.get(b"Travis").uniq_id == b"Me" def testUniqueFileId_N(self): # Add 3 with RedirectStdStreams() as out: assert out args, _, config = \ main.parseCommandLine(["--unique-file-id", "Travis:Me", "--unique-file-id=Engine:Kid", "--unique-file-id", "Owner:Kid", self.test_file]) retval = main.main(args, config) assert retval == 0 af = eyed3.load(self.test_file) assert len(af.tag.unique_file_ids) == 3 assert af.tag.unique_file_ids.get("Travis").uniq_id == b"Me" assert af.tag.unique_file_ids.get("Engine").uniq_id == b"Kid" assert af.tag.unique_file_ids.get(b"Owner").uniq_id == b"Kid" # Remove 2 with RedirectStdStreams() as out: assert out args, _, config = \ main.parseCommandLine(["--unique-file-id", "Travis:", "--unique-file-id=Engine:", "--unique-file-id", "Owner:Kid", self.test_file]) retval = main.main(args, config) assert retval == 0 af = eyed3.load(self.test_file) assert len(af.tag.unique_file_ids) == 1 # Remove not found ID with RedirectStdStreams() as out: args, _, config = \ main.parseCommandLine(["--unique-file-id", "Travis:", self.test_file]) retval = main.main(args, config) assert retval == 0 sout = out.stdout.read() assert "Unique file ID 'Travis' not found" in sout af = eyed3.load(self.test_file) assert len(af.tag.unique_file_ids) == 1 # TODO: # --text-frame, --user-text-frame # --url-frame, --user-user-frame # --add-image, --remove-image, --remove-all-images, --write-images # etc. # --rename, --force-update, -1, -2, --exclude def testNewTagSimpleComment(self, version=id3.ID3_DEFAULT_VERSION): if version[0] == 1: # No support for this in v1.x return for opts in [ ["-c", "Starlette", self.test_file], ["--comment=Starlette", self.test_file] ]: self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) assert (af.tag.comments[0].text == "Starlette") assert (af.tag.comments[0].description == "") def testAddRemoveComment(self, version=id3.ID3_DEFAULT_VERSION): if version[0] == 1: # No support for this in v1.x return comment = "Why can't I be you?" for i, (c, d, l) in enumerate([(comment, "c0", None), (comment, "c1", None), (comment, "c2", 'eng'), ("¿Por qué no puedo ser tú ?", "c2", 'esp'), ]): darg = ":{}".format(d) if d else "" larg = ":{}".format(l) if l else "" opts = ["--add-comment={c}{darg}{larg}".format(**locals()), self.test_file] self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) tag_comment = af.tag.comments.get(d or "", lang=utils.b(l if l else "eng")) assert (tag_comment.text == c) assert (tag_comment.description == d or "") assert (tag_comment.lang == utils.b(l if l else "eng")) for d, l in [("c0", None), ("c1", None), ("c2", "eng"), ("c2", "esp"), ]: larg = ":{}".format(l) if l else "" opts = ["--remove-comment={d}{larg}".format(**locals()), self.test_file] self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) tag_comment = af.tag.comments.get(d, lang=utils.b(l if l else "eng")) assert tag_comment is None assert (len(af.tag.comments) == 0) def testRemoveAllComments(self, version=id3.ID3_DEFAULT_VERSION): if version[0] == 1: # No support for this in v1.x return comment = "Why can't I be you?" for i, (c, d, l) in enumerate([(comment, "c0", None), (comment, "c1", None), (comment, "c2", 'eng'), ("¿Por qué no puedo ser tú ?", "c2", 'esp'), (comment, "c4", "ger"), (comment, "c4", "rus"), (comment, "c5", "rus"), ]): darg = ":{}".format(d) if d else "" larg = ":{}".format(l) if l else "" opts = ["--add-comment={c}{darg}{larg}".format(**locals()), self.test_file] self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) tag_comment = af.tag.comments.get(d or "", lang=utils.b(l if l else "eng")) assert (tag_comment.text == c) assert (tag_comment.description == d or "") assert (tag_comment.lang == utils.b(l if l else "eng")) opts = ["--remove-all-comments", self.test_file] self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (len(af.tag.comments) == 0) def testAddRemoveLyrics(self, version=id3.ID3_DEFAULT_VERSION): if version[0] == 1: # No support for this in v1.x return comment = "Why can't I be you?" for i, (c, d, l) in enumerate([(comment, "c0", None), (comment, "c1", None), (comment, "c2", 'eng'), ("¿Por qué no puedo ser tú ?", "c2", 'esp'), ]): darg = ":{}".format(d) if d else "" larg = ":{}".format(l) if l else "" opts = ["--add-comment={c}{darg}{larg}".format(**locals()), self.test_file] self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) assert (af is not None) assert (af.tag is not None) tag_comment = af.tag.comments.get(d or "", lang=utils.b(l if l else "eng")) assert (tag_comment.text == c) assert (tag_comment.description == d or "") assert (tag_comment.lang == utils.b(l if l else "eng")) for d, l in [("c0", None), ("c1", None), ("c2", "eng"), ("c2", "esp"), ]: larg = ":{}".format(l) if l else "" opts = ["--remove-comment={d}{larg}".format(**locals()), self.test_file] self._addVersionOpt(version, opts) with RedirectStdStreams() as out: args, _, config = main.parseCommandLine(opts) retval = main.main(args, config) assert (retval == 0) af = eyed3.load(self.test_file) tag_comment = af.tag.comments.get(d, lang=utils.b(l if l else "eng")) assert tag_comment is None assert (len(af.tag.comments) == 0) def testNewTagAll(self, version=id3.ID3_DEFAULT_VERSION): self.testNewTagArtist(version) self.testNewTagAlbum(version) self.testNewTagTitle(version) self.testNewTagTrackNum(version) self.testNewTagTrackTotal(version) self.testNewTagGenre(version) self.testNewTagYear(version) self.testNewTagSimpleComment(version) af = eyed3.load(self.test_file) assert (af.tag.artist == "The Cramps") assert (af.tag.album == "Psychedelic Jungle") assert (af.tag.title == "Green Door") assert (af.tag.track_num == (14, 14 if version[0] != 1 else None)) assert ((af.tag.genre.name, af.tag.genre.id) == ("Rock", 17)) if version == id3.ID3_V2_3: assert (af.tag.original_release_date.year == 1981) else: assert (af.tag.release_date.year == 1981) if version[0] != 1: assert (af.tag.comments[0].text == "Starlette") assert (af.tag.comments[0].description == "") assert (af.tag.version == version) def testNewTagAllVersion1(self): self.testNewTagAll(version=id3.ID3_V1_1) def testNewTagAllVersion2_3(self): self.testNewTagAll(version=id3.ID3_V2_3) def testNewTagAllVersion2_4(self): self.testNewTagAll(version=id3.ID3_V2_4) ## XXX: newer pytest test below. @pytest.mark.skipif(not Path(DATA_D).exists(), reason="test requires data files") def test_lyrics(audiofile, tmpdir, eyeD3): lyrics_files = [] for i in range(1, 4): lfile = tmpdir / "lryics{:d}".format(i) lfile.write_text((str(i) * (100 * i)), "utf8") lyrics_files.append(lfile) audiofile = eyeD3(audiofile, ["--add-lyrics", "{}".format(lyrics_files[0]), "--add-lyrics", "{}:desc".format(lyrics_files[1]), "--add-lyrics", "{}:foo:en".format(lyrics_files[1]), "--add-lyrics", "{}:foo:es".format(lyrics_files[2]), "--add-lyrics", "{}:foo:de".format(lyrics_files[0]), ]) assert len(audiofile.tag.lyrics) == 5 assert audiofile.tag.lyrics.get("").text == ("1" * 100) assert audiofile.tag.lyrics.get("desc").text == ("2" * 200) assert audiofile.tag.lyrics.get("foo", "en").text == ("2" * 200) assert audiofile.tag.lyrics.get("foo", "es").text == ("3" * 300) assert audiofile.tag.lyrics.get("foo", "de").text == ("1" * 100) audiofile = eyeD3(audiofile, ["--remove-lyrics", "foo:xxx"]) assert len(audiofile.tag.lyrics) == 5 audiofile = eyeD3(audiofile, ["--remove-lyrics", "foo:es"]) assert len(audiofile.tag.lyrics) == 4 audiofile = eyeD3(audiofile, ["--remove-lyrics", "desc"]) assert len(audiofile.tag.lyrics) == 3 audiofile = eyeD3(audiofile, ["--remove-all-lyrics"]) assert len(audiofile.tag.lyrics) == 0 eyeD3(audiofile, ["--add-lyrics", "eminem.txt"], expected_retval=2) @pytest.mark.skipif(not Path(DATA_D).exists(), reason="test requires data files") def test_all(audiofile, image, eyeD3): audiofile = eyeD3(audiofile, ["--artist", "Cibo Matto", "--album-artist", "Cibo Matto", "--album", "Viva! La Woman", "--title", "Apple", "--track=1", "--track-total=11", "--disc-num=1", "--disc-total=1", "--genre", "Pop", "--release-date=1996-01-16", "--orig-release-date=1996-01-16", "--recording-date=1995-01-16", "--encoding-date=1999-01-16", "--tagging-date=1999-01-16", "--comment", "From Japan", "--publisher=\'Warner Brothers\'", "--play-count=666", "--bpm=99", "--unique-file-id", "mishmash:777abc", "--add-comment", "Trip Hop", "--add-comment", "Quirky:Mood", "--add-comment", "Kimyōna:Mood:jp", "--add-comment", "Test:XXX", "--add-popularity", "travis@ppbox.com:212:999", "--fs-encoding=latin1", "--no-config", "--add-object", "{}:image/gif".format(image), "--composer", "Cibo Matto", "--remove-all-unknown", ]) assert audiofile @pytest.mark.skipif(not Path(DATA_D).exists(), reason="test requires data files") def test_removeTag_v1(audiofile, eyeD3): assert audiofile.tag is None audiofile = eyeD3(audiofile, ["-1", "-a", "Government Issue"]) assert audiofile.tag.version == id3.ID3_V1_0 audiofile = eyeD3(audiofile, ["--remove-v1"]) assert audiofile.tag is None @pytest.mark.skipif(not Path(DATA_D).exists(), reason="test requires data files") def test_removeTag_v2(audiofile, eyeD3): assert audiofile.tag is None audiofile = eyeD3(audiofile, ["-2", "-a", "Integrity"]) assert audiofile.tag.version == id3.ID3_V2_4 audiofile = eyeD3(audiofile, ["--remove-v2"]) assert audiofile.tag is None @pytest.mark.skipif(not Path(DATA_D).exists(), reason="test requires data files") def test_removeTagWithBoth_v1(audiofile, eyeD3): audiofile = eyeD3(eyeD3(audiofile, ["-1", "-a", "Face Value"]), ["-2", "-a", "Poison Idea"]) v1_view = eyeD3(audiofile, ["-1"], reload_version=id3.ID3_V1) v2_view = eyeD3(audiofile, ["-2"], reload_version=id3.ID3_V2) assert audiofile.tag.version == id3.ID3_V2_4 assert v1_view.tag.version == id3.ID3_V1_0 assert v2_view.tag.version == id3.ID3_V2_4 audiofile = eyeD3(audiofile, ["--remove-v1"]) assert audiofile.tag.version == id3.ID3_V2_4 assert eyeD3(audiofile, ["-1"], reload_version=id3.ID3_V1).tag is None v2_tag = eyeD3(audiofile, ["-2"], reload_version=id3.ID3_V2).tag assert v2_tag is not None assert v2_tag.artist == "Poison Idea" @pytest.mark.skipif(not Path(DATA_D).exists(), reason="test requires data files") def test_removeTagWithBoth_v2(audiofile, eyeD3): audiofile = eyeD3(eyeD3(audiofile, ["-1", "-a", "Face Value"]), ["-2", "-a", "Poison Idea"]) v1_view = eyeD3(audiofile, ["-1"], reload_version=id3.ID3_V1) v2_view = eyeD3(audiofile, ["-2"], reload_version=id3.ID3_V2) assert audiofile.tag.version == id3.ID3_V2_4 assert v1_view.tag.version == id3.ID3_V1_0 assert v2_view.tag.version == id3.ID3_V2_4 audiofile = eyeD3(audiofile, ["--remove-v2"]) assert audiofile.tag.version == id3.ID3_V1_0 assert eyeD3(audiofile, ["-2"], reload_version=id3.ID3_V2).tag is None v1_tag = eyeD3(audiofile, ["-1"], reload_version=id3.ID3_V1).tag assert v1_tag is not None and v1_tag.artist == "Face Value" @pytest.mark.skipif(not Path(DATA_D).exists(), reason="test requires data files") def test_removeTagWithBoth_v2_withConvert(audiofile, eyeD3): audiofile = eyeD3(eyeD3(audiofile, ["-1", "-a", "Face Value"]), ["-2", "-a", "Poison Idea"]) v1_view = eyeD3(audiofile, ["-1"], reload_version=id3.ID3_V1) v2_view = eyeD3(audiofile, ["-2"], reload_version=id3.ID3_V2) assert audiofile.tag.version == id3.ID3_V2_4 assert v1_view.tag.version == id3.ID3_V1_0 assert v2_view.tag.version == id3.ID3_V2_4 audiofile = eyeD3(audiofile, ["--remove-v2", "--to-v1"]) assert audiofile.tag.version == id3.ID3_V1_0 assert eyeD3(audiofile, ["-2"], reload_version=id3.ID3_V2).tag is None v1_tag = eyeD3(audiofile, ["-1"], reload_version=id3.ID3_V1).tag assert v1_tag is not None and v1_tag.artist == "Face Value" @pytest.mark.skipif(not Path(DATA_D).exists(), reason="test requires data files") def test_removeTagWithBoth_v1_withConvert(audiofile, eyeD3): audiofile = eyeD3(eyeD3(audiofile, ["-1", "-a", "Face Value"]), ["-2", "-a", "Poison Idea"]) v1_view = eyeD3(audiofile, ["-1"], reload_version=id3.ID3_V1) v2_view = eyeD3(audiofile, ["-2"], reload_version=id3.ID3_V2) assert audiofile.tag.version == id3.ID3_V2_4 assert v1_view.tag.version == id3.ID3_V1_0 assert v2_view.tag.version == id3.ID3_V2_4 audiofile = eyeD3(audiofile, ["--remove-v1", "--to-v2.3"]) assert audiofile.tag.version == id3.ID3_V2_3 assert eyeD3(audiofile, ["-1"], reload_version=id3.ID3_V1).tag is None v2_tag = eyeD3(audiofile, ["-2"], reload_version=id3.ID3_V2).tag assert v2_tag is not None and v2_tag.artist == "Poison Idea" def test_clearGenre(audiofile, eyeD3): audiofile = eyeD3(audiofile, ["--genre=Rock"]) assert audiofile.tag.genre.name, audiofile.tag.genre.name == ("Rock", 17) audiofile = eyeD3(audiofile, ["--genre", ""]) assert audiofile.tag.genre is None @pytest.mark.skipif(not Path(DATA_D).exists(), reason="test requires data files") @pytest.mark.audiofile_name("unknown-frame-ASDF.mp3") def test_removeUnknownTags(audiofile, eyeD3): assert audiofile.tag is not None assert len(audiofile.tag.frame_set) == 2 # ASDF and TSSE assert audiofile.tag.frame_set[b"ASDF"] and len(audiofile.tag.frame_set[b"ASDF"]) == 1 assert audiofile.tag.frame_set[b"ASDF"][0].unknown == True assert audiofile.tag.unknown_frame_ids == {b"ASDF"} audiofile = eyeD3(audiofile, ["--remove-all-unknown"]) assert len(audiofile.tag.frame_set) == 1 # TSSE assert audiofile.tag.frame_set[b"TSSE"] and len(audiofile.tag.frame_set[b"TSSE"]) == 1 assert audiofile.tag.frame_set[b"TSSE"][0].unknown == False assert audiofile.tag.unknown_frame_ids == set() eyeD3-0.9.7/tests/test_console.py000066400000000000000000000016361432016011500167140ustar00rootroot00000000000000"""Tests for eyed3.utils.console module""" import unittest from unittest import mock from eyed3.utils.console import AnsiCodes, Fore @mock.patch('sys.stdout.isatty', new=lambda: True) class AnsiCodesTC(unittest.TestCase): def setUp(self): AnsiCodes._USE_ANSI = False def test_init_color_enabled(self): AnsiCodes.init(True) self._assert_color_enabled() def test_init_color_disabled(self): AnsiCodes.init(False) self._assert_color_disabled() @mock.patch('sys.stdout.isatty', new=lambda: False) def test_init_color_enabled_not_tty(self): AnsiCodes.init(False) self._assert_color_disabled() def _assert_color_enabled(self): self.assertTrue(AnsiCodes._USE_ANSI) self.assertEqual(Fore.GREEN, '\x1b[32m') def _assert_color_disabled(self): self.assertFalse(AnsiCodes._USE_ANSI) self.assertEqual(Fore.GREEN, '') eyeD3-0.9.7/tests/test_core.py000066400000000000000000000113761432016011500162040ustar00rootroot00000000000000import os from pathlib import Path import pytest import eyed3 from eyed3 import core from . import DATA_D @pytest.mark.skipif(not Path(DATA_D).exists(), reason="test requires data files") def test_AudioFile_rename(audiofile): orig_path = audiofile.path # Happy path audiofile.rename("Spoon") assert Path(audiofile.path).exists() assert not Path(orig_path).exists() assert (Path(orig_path).parent / "Spoon{}".format(Path(orig_path).suffix)).exists() # File exist with pytest.raises(IOError): audiofile.rename("Spoon") # Parent dir does not exist with pytest.raises(IOError): audiofile.rename("subdir/BloodOnTheWall") def test_import_load(): assert eyed3.load == core.load # eyed3.load raises IOError for non files and non-existent files def test_ioerror_load(): # Non existent with pytest.raises(IOError): core.load("filedoesnotexist.txt") # Non file with pytest.raises(IOError): core.load(os.path.abspath(os.path.curdir)) def test_none_load(): # File mimetypes that are not supported return None assert core.load(__file__) is None def test_AudioFile(): from eyed3.core import AudioFile # Abstract method with pytest.raises(NotImplementedError): AudioFile("somefile.mp3") class DummyAudioFile(AudioFile): def _read(self): pass # precondition is that __file__ is already absolute assert os.path.isabs(__file__) af = DummyAudioFile(__file__) # All paths are turned into absolute paths assert str(af.path) == os.path.abspath(__file__) def test_AudioInfo(): from eyed3.core import AudioInfo info = AudioInfo(0, 0) assert (info.time_secs == 0) assert (info.size_bytes == 0) info = AudioInfo(size_bytes=1000, time_secs=3.14) assert (info.time_secs == 3.14) assert (info.size_bytes == 1000) def test_Date(): from eyed3.core import Date for d in [Date(1965), Date(year=1965), Date.parse("1965")]: assert d.year == 1965 assert d.month is None assert d.day is None assert d.hour is None assert d.minute is None assert d.second is None assert str(d) == "1965" for d in [Date(1965, 3), Date(year=1965, month=3), Date.parse("1965-03")]: assert d.year == 1965 assert d.month == 3 assert d.day is None assert d.hour is None assert d.minute is None assert d.second is None assert str(d) == "1965-03" for d in [Date(1965, 3, 6), Date(year=1965, month=3, day=6), Date.parse("1965-3-6")]: assert d.year == 1965 assert d.month == 3 assert d.day == 6 assert d.hour is None assert d.minute is None assert d.second is None assert (str(d) == "1965-03-06") for d in [Date(1965, 3, 6, 23), Date(year=1965, month=3, day=6, hour=23), Date.parse("1965-3-6T23")]: assert d.year == 1965 assert d.month == 3 assert d.day == 6 assert d.hour == 23 assert d.minute is None assert d.second is None assert str(d) == "1965-03-06T23" for d in [Date(1965, 3, 6, 23, 20), Date(year=1965, month=3, day=6, hour=23, minute=20), Date.parse("1965-3-6T23:20")]: assert d.year == 1965 assert d.month == 3 assert d.day == 6 assert d.hour == 23 assert d.minute == 20 assert d.second is None assert str(d) == "1965-03-06T23:20" for d in [Date(1965, 3, 6, 23, 20, 15), Date(year=1965, month=3, day=6, hour=23, minute=20, second=15), Date.parse("1965-3-6T23:20:15")]: assert d.year == 1965 assert d.month == 3 assert d.day == 6 assert d.hour == 23 assert d.minute == 20 assert d.second == 15 assert str(d) == "1965-03-06T23:20:15" with pytest.raises(ValueError): Date.parse("") with pytest.raises(ValueError): Date.parse("ABC") with pytest.raises(ValueError): Date.parse("2010/1/24") with pytest.raises(ValueError): Date(2012, 0) with pytest.raises(ValueError): Date(2012, 1, 35) with pytest.raises(ValueError): Date(2012, 1, 4, -1) with pytest.raises(ValueError): Date(2012, 1, 4, 24) with pytest.raises(ValueError): Date(2012, 1, 4, 18, 60) with pytest.raises(ValueError): Date(2012, 1, 4, 18, 14, 61) dt = Date(1965, 3, 6, 23, 20, 15) dp = Date(1980, 7, 3, 10, 5, 1) assert dt != dp assert dt < dp assert not dp < dt assert None < dp assert not dp < dp assert dp <= dp assert hash(dt) != hash(dp) eyeD3-0.9.7/tests/test_factory.py000066400000000000000000000007761432016011500167250ustar00rootroot00000000000000import eyed3.id3 import factory class TagFactory(factory.Factory): class Meta: model = eyed3.id3.Tag title = u"Track title" artist = u"Artist" album = u"Album" album_artist = artist track_num = None def test_factory(): tag = TagFactory() assert isinstance(tag, eyed3.id3.Tag) assert tag.title == u"Track title" assert tag.artist == u"Artist" assert tag.album == u"Album" assert tag.album_artist == tag.artist assert tag.track_num == (None, None) eyeD3-0.9.7/tests/test_issues.py000066400000000000000000000056171432016011500165700ustar00rootroot00000000000000from pathlib import Path import pytest import eyed3 from eyed3.id3 import Tag, ID3_V2_3, ID3_V2_4 from . import DATA_D @pytest.mark.skipif(not Path(DATA_D).exists(), reason="test requires data files") def testIssue76(audiofile): """ Writing lyrics deletes TSOP tag (ARTISTSORT) https://github.com/nicfit/eyeD3/issues/76 """ tag = audiofile.initTag(ID3_V2_4) tag.setTextFrame("TPE1", "Confederacy of Ruined Lives") tag.setTextFrame("TPE2", "Take as needed for pain") tag.setTextFrame("TSOP", "In the name of suffering") tag.setTextFrame("TSO2", "Dope sick") tag.save() audiofile = eyed3.load(audiofile.path) tag = audiofile.tag assert (set(tag.frame_set.keys()) == set([b"TPE1", b"TPE2", b"TSOP", b"TSO2"])) assert tag.getTextFrame("TSO2") == "Dope sick" assert tag.getTextFrame("TSOP") == "In the name of suffering" assert tag.getTextFrame("TPE2") == "Take as needed for pain" assert tag.getTextFrame("TPE1") == "Confederacy of Ruined Lives" audiofile.tag.lyrics.set("some lyrics") audiofile = eyed3.load(audiofile.path) tag = audiofile.tag assert (set(tag.frame_set.keys()) == set([b"TPE1", b"TPE2", b"TSOP", b"TSO2"])) assert tag.getTextFrame("TSO2") == "Dope sick" assert tag.getTextFrame("TSOP") == "In the name of suffering" assert tag.getTextFrame("TPE2") == "Take as needed for pain" assert tag.getTextFrame("TPE1") == "Confederacy of Ruined Lives" # Convert to v2.3 and verify conversions tag.save(version=ID3_V2_3) audiofile = eyed3.load(audiofile.path) tag = audiofile.tag assert (set(tag.frame_set.keys()) == set([b"TPE1", b"TPE2", b"XSOP", b"TSO2"])) assert tag.getTextFrame("TSO2") == "Dope sick" assert tag.getTextFrame("TPE2") == "Take as needed for pain" assert tag.getTextFrame("TPE1") == "Confederacy of Ruined Lives" assert tag.frame_set[b"XSOP"][0].text == "In the name of suffering" # Convert to v2.4 and verify conversions tag.save(version=ID3_V2_4) audiofile = eyed3.load(audiofile.path) tag = audiofile.tag assert (set(tag.frame_set.keys()) == set([b"TPE1", b"TPE2", b"TSOP", b"TSO2"])) assert tag.getTextFrame("TSO2") == "Dope sick" assert tag.getTextFrame("TPE2") == "Take as needed for pain" assert tag.getTextFrame("TPE1") == "Confederacy of Ruined Lives" assert tag.getTextFrame("TSOP") == "In the name of suffering" def test_issue382_genres(audiofile): """Tags always written in v2.3 format, always including ID. https://github.com/nicfit/eyeD3/issues/382 """ tag = Tag() tag.genre = "Dubstep" assert tag.genre.id == 189 assert tag.genre.name == "Dubstep" audiofile.tag = tag tag.save() new_audiofile = eyed3.load(audiofile.path) # Prior versions would be `(189)Dubstep`, now no index. assert new_audiofile.tag.frame_set[b"TCON"][0].text == "Dubstep" eyeD3-0.9.7/tests/test_jsonyaml_plugin.py000066400000000000000000000036221432016011500204610ustar00rootroot00000000000000import os import sys import stat from eyed3 import main, version from . import RedirectStdStreams def _initTag(afile): afile.initTag() afile.tag.artist = "Bad Religion" afile.tag.title = "Suffer" afile.tag.album = "Suffer" afile.tag.release_date = "1988" afile.tag.recording_date = "1987" afile.tag.track_num = (9, 15) afile.tag.save() def _runPlugin(afile, plugin) -> str: with RedirectStdStreams() as plugin_out: args, _, config = main.parseCommandLine(["-P", plugin, str(afile.path)]) assert main.main(args, config) == 0 stdout = plugin_out.stdout.read().strip() print(stdout) return stdout def _assertFormat(plugin: str, audio_file, format: str): output = _runPlugin(audio_file, plugin) print(output) size_bytes = os.stat(audio_file.path)[stat.ST_SIZE] assert output.strip() == format.strip() % dict(path=audio_file.path, version=version, size_bytes=size_bytes) def testJsonPlugin(audiofile): _initTag(audiofile) _assertFormat("json", audiofile, """ { "path": "%(path)s", "info": { "time_secs": 10.68, "size_bytes": %(size_bytes)d }, "album": "Suffer", "artist": "Bad Religion", "best_release_date": "1988", "recording_date": "1987", "release_date": "1988", "title": "Suffer", "track_num": { "count": 9, "total": 15 }, "_eyeD3": "%(version)s" } """) def testYamlPlugin(audiofile): _initTag(audiofile) omap, omap_list = "", " " if sys.version_info[:2] <= (3, 7): omap = " !!omap" omap_list = "- " _assertFormat("yaml", audiofile, f""" --- _eyeD3: %(version)s album: Suffer artist: Bad Religion best_release_date: '1988' info: size_bytes: %(size_bytes)d time_secs: 10.68 path: %(path)s recording_date: '1987' release_date: '1988' title: Suffer track_num:{omap} {omap_list}count: 9 {omap_list}total: 15 """) eyeD3-0.9.7/tests/test_lameinfo_plugin.py000066400000000000000000000032351432016011500204170ustar00rootroot00000000000000import unittest from pathlib import Path from eyed3 import main from . import DATA_D, RedirectStdStreams @unittest.skipIf(not Path(DATA_D).exists(), "test requires data files") def testLameInfoPlugin(): test_file = Path(DATA_D) / "mp3_samples/mpeg2.5 12.000kHz __vbr__ simple.mp3" with RedirectStdStreams() as plugin_out: args, _, config = main.parseCommandLine(["-P", "lameinfo", str(test_file)]) retval = main.main(args, config) assert retval == 0 stdout = plugin_out.stdout.read() assert stdout[stdout.index("Encoder Version"):].strip() == \ """ Encoder Version : LAME3.99r LAME Tag Revision : 0 VBR Method : Variable Bitrate method2 (mtrh) Lowpass Filter : 6000 Radio Replay Gain : 12.2 dB (Set automatically) Encoding Flags : --nspsytune --nssafejoint ATH Type : 5 Bitrate (Minimum) : 8 Encoder Delay : 576 samples Encoder Padding : 960 samples Noise Shaping : 1 Stereo Mode : Stereo Unwise Settings : False Sample Frequency : 44.1 kHz MP3 Gain : 0 (+0.0 dB) Preset : V0 Surround Info : None Music Length : 67.88 KB Music CRC-16 : 8707 LAME Tag CRC-16 : 0000 """.strip() @unittest.skipIf(not Path(DATA_D).exists(), "test requires data files") def testLameInfoPlugin_None(): test_file = Path(DATA_D) / "test.mp3" with RedirectStdStreams() as plugin_out: args, _, config = main.parseCommandLine(["-P", "lameinfo", str(test_file)]) retval = main.main(args, config) assert retval == 0 stdout = plugin_out.stdout.read() assert stdout[stdout.index("--\n") + 3:].strip() == "No LAME Tag" eyeD3-0.9.7/tests/test_main.py000066400000000000000000000062241432016011500161740ustar00rootroot00000000000000import unittest import deprecation from eyed3 import main from . import RedirectStdStreams def testHelpExitsSuccess(): with open("/dev/null", "w") as devnull: with RedirectStdStreams(stderr=devnull): for arg in ["--help", "-h"]: try: args, parser = main.parseCommandLine([arg]) except SystemExit as ex: assert ex.code == 0 def testHelpOutput(): for arg in ["--help", "-h"]: with RedirectStdStreams() as out: try: args, parser = main.parseCommandLine([arg]) except SystemExit as ex: # __exit__ seeks and we're not there yet so... out.stdout.seek(0) assert out.stdout.read().startswith(u"usage:") assert ex.code == 0 def testVersionExitsWithSuccess(): with open("/dev/null", "w") as devnull: with RedirectStdStreams(stderr=devnull): try: args, parser = main.parseCommandLine(["--version"]) except SystemExit as ex: assert ex.code == 0 def testListPluginsExitsWithSuccess(): try: args, _, _ = main.parseCommandLine(["--plugins"]) except SystemExit as ex: assert ex.code == 0 def testLoadPlugin(): from eyed3.plugins.classic import ClassicPlugin from eyed3.plugins.genres import GenreListPlugin args, _, _ = main.parseCommandLine([""]) assert args.plugin.__class__.__name__ == ClassicPlugin.__name__ args, _, _ = main.parseCommandLine(["--plugin=genres"]) assert args.plugin.__class__.__name__ == GenreListPlugin.__name__ with open("/dev/null", "w") as devnull: with RedirectStdStreams(stderr=devnull): try: args, _ = main.parseCommandLine(["--plugin=DNE"]) except SystemExit as ex: assert ex.code == 1 try: args, _, _ = main.parseCommandLine(["--plugin"]) except SystemExit as ex: assert ex.code == 2 def testLoggingOptions(): import logging from eyed3 import log with open("/dev/null", "w") as devnull: with RedirectStdStreams(stderr=devnull): try: _ = main.parseCommandLine(["-l", "critical"]) assert log.getEffectiveLevel() == logging.CRITICAL _ = main.parseCommandLine(["--log-level=error"]) assert log.getEffectiveLevel() == logging.ERROR _ = main.parseCommandLine(["-l", "warning:NewLogger"]) assert ( logging.getLogger("NewLogger").getEffectiveLevel() == logging.WARNING ) assert log.getEffectiveLevel() == logging.ERROR except SystemExit: assert not "Unexpected" try: _ = main.parseCommandLine(["--log-level=INVALID"]) assert not "Invalid log level, an Exception expected" except SystemExit: pass @deprecation.fail_if_not_removed def testConfigFileDeprecation(): main._deprecatedConfigFileCheck(None) eyeD3-0.9.7/tests/test_mimetype.py000066400000000000000000000022751432016011500171030ustar00rootroot00000000000000import os import pytest import deprecation from eyed3 import utils, mimetype from . import DATA_D mime_test_params = [("id3", ["application/x-id3"]), ("tag", ["application/x-id3"]), ("mka", ["video/x-matroska", "application/octet-stream"]), ("mp3", ["audio/mpeg"]), ("ogg", ["audio/ogg", "application/ogg"]), ("wav", ["audio/x-wav"]), ("wma", ["audio/x-ms-wma", "video/x-ms-wma", "video/x-ms-asf", "video/x-ms-wmv"]), ] @pytest.mark.skipif(not os.path.exists(DATA_D), reason="test requires data files") @deprecation.fail_if_not_removed def testSampleMimeTypesUtils(): for ext, valid_types in mime_test_params: guessed = utils.guessMimetype(os.path.join(DATA_D, f"sample.%s" % ext)) assert guessed in valid_types @pytest.mark.skipif(not os.path.exists(DATA_D), reason="test requires data files") @pytest.mark.parametrize(("ext", "valid_types"), mime_test_params) def testSampleMimeTypes(ext, valid_types): guessed = mimetype.guessMimetype(os.path.join(DATA_D, "sample.%s" % ext)) assert guessed in valid_types eyeD3-0.9.7/tests/test_plugins.py000066400000000000000000000016111432016011500167240ustar00rootroot00000000000000from eyed3.plugins import * def test_load(): plugins = load() assert "classic" in list(plugins.keys()) assert "genres" in list(plugins.keys()) assert load("classic") == plugins["classic"] assert load("genres") == plugins["genres"] assert (load("classic", reload=True).__class__.__name__ == plugins["classic"].__class__.__name__) assert (load("genres", reload=True).__class__.__name__ == plugins["genres"].__class__.__name__) assert load("DNE") is None def test_Plugin(): import argparse class MyPlugin(Plugin): pass p = MyPlugin(argparse.ArgumentParser()) assert p.arg_group is not None # In reality, this is parsed args p.start("dummy_args", "dummy_config") assert p.args == "dummy_args" assert p.config == "dummy_config" assert p.handleFile("f.txt") is None assert p.handleDone() is None eyeD3-0.9.7/tests/test_stats_plugins.py000066400000000000000000000015221432016011500201430ustar00rootroot00000000000000import os import tempfile import unittest import eyed3.id3 import eyed3.main from . import RedirectStdStreams class TestId3FrameRules(unittest.TestCase): def test_bad_frames(self): try: fd, tempf = tempfile.mkstemp(suffix='.id3') os.close(fd) tagfile = eyed3.id3.TagFile(tempf) tagfile.initTag() tagfile.tag.title = 'mytitle' tagfile.tag.privates.set(b'mydata', b'onwer0') tagfile.tag.save() args = ['--plugin', 'stats', tempf] args, _, config = eyed3.main.parseCommandLine(args) with RedirectStdStreams() as out: eyed3.main.main(args, config) finally: os.remove(tempf) print(out.stdout.getvalue()) self.assertIn('PRIV frames are bad', out.stdout.getvalue()) eyeD3-0.9.7/tests/test_utils.py000066400000000000000000000073731432016011500164160ustar00rootroot00000000000000from unittest.mock import MagicMock, call import eyed3.utils.console from eyed3.utils import walk from eyed3.utils.console import ( printMsg, printWarning, printHeader, Fore, WARNING_COLOR, HEADER_COLOR ) from . import RedirectStdStreams def test_printWarning(): eyed3.utils.console.USE_ANSI = False with RedirectStdStreams() as out: printWarning("Built To Spill") assert (out.stdout.read() == "Built To Spill\n") eyed3.utils.console.USE_ANSI = True with RedirectStdStreams() as out: printWarning("Built To Spill") assert (out.stdout.read() == "%sBuilt To Spill%s\n" % (WARNING_COLOR(), Fore.RESET)) def test_printMsg(): eyed3.utils.console.USE_ANSI = False with RedirectStdStreams() as out: printMsg("EYEHATEGOD") assert (out.stdout.read() == "EYEHATEGOD\n") eyed3.utils.console.USE_ANSI = True with RedirectStdStreams() as out: printMsg("EYEHATEGOD") assert (out.stdout.read() == "EYEHATEGOD\n") def test_printHeader(): eyed3.utils.console.USE_ANSI = False with RedirectStdStreams() as out: printHeader("Furthur") assert (out.stdout.read() == "Furthur\n") eyed3.utils.console.USE_ANSI = True with RedirectStdStreams() as out: printHeader("Furthur") assert (out.stdout.read() == "%sFurthur%s\n" % (HEADER_COLOR(), Fore.RESET)) def test_walk_recursive(tmpdir): root_d = tmpdir.mkdir("Root") d1 = root_d.mkdir("d1") f1 = d1 / "file1" f1.write_text("file1", "utf8") _ = root_d.mkdir("d2") d3 = root_d.mkdir("d3") handler = MagicMock() walk(handler, str(root_d), recursive=True) handler.handleFile.assert_called_with(str(f1)) handler.handleDirectory.assert_called_with(str(d1), [f1.basename]) # Only dirs with files are handled, so... f2 = d3 / "Neurosis" f2.write_text("Through Silver and Blood", "utf8") f3 = d3 / "High on Fire" f3.write_text("Surrounded By Thieves", "utf8") d4 = d3.mkdir("d4") f4 = d4 / "Cross Rot" f4.write_text("VII", "utf8") handler = MagicMock() walk(handler, str(root_d), recursive=True) handler.handleFile.assert_has_calls([call(str(f1)), call(str(f3)), call(str(f2)), call(str(f4)), ], any_order=True) handler.handleDirectory.assert_has_calls( [call(str(d1), [f1.basename]), call(str(d3), [f3.basename, f2.basename]), call(str(d4), [f4.basename]), ], any_order=True) def test_walk(tmpdir): root_d = tmpdir.mkdir("Root") d1 = root_d.mkdir("d1") f1 = d1 / "file1" f1.write_text("file1", "utf8") _ = root_d.mkdir("d2") d3 = root_d.mkdir("d3") f2 = d3 / "Neurosis" f2.write_text("Through Silver and Blood", "utf8") f3 = d3 / "High on Fire" f3.write_text("Surrounded By Thieves", "utf8") d4 = d3.mkdir("d4") f4 = d4 / "Cross Rot" f4.write_text("VII", "utf8") handler = MagicMock() walk(handler, str(root_d)) handler.handleFile.assert_not_called() handler.handleDirectory.assert_not_called() handler = MagicMock() walk(handler, str(root_d / "d1"), recursive=True) handler.handleFile.assert_called_with(str(f1)) handler.handleDirectory.assert_called_with(str(d1), [f1.basename]) handler = MagicMock() walk(handler, str(root_d / "d3")) handler.handleFile.assert_has_calls([call(str(f3)), call(str(f2))], any_order=True) handler.handleDirectory.assert_has_calls([call(str(d3), [f3.basename, f2.basename])], any_order=True) eyeD3-0.9.7/tox.ini000066400000000000000000000002731432016011500140060ustar00rootroot00000000000000[tox] envlist = py{310,39,38},pypy3 allowlist_externals=make test [testenv] deps = .[test] .[display-plugin] .[yaml-plugin] commands = make lint test PYTEST_ARGS={posargs}