eyeD3-0.8.4/0000755000175000017500000000000013203726215013337 5ustar travistravis00000000000000eyeD3-0.8.4/Makefile0000644000175000017500000002031713203725473015007 0ustar travistravis00000000000000.PHONY: help build test clean dist install coverage pre-release release \ docs clean-docs lint tags coverage-view changelog \ clean-pyc clean-build clean-patch clean-local clean-test-data \ test-all test-data build-release freeze-release tag-release \ pypi-release web-release github-release cookiecutter requirements SRC_DIRS = ./src/eyed3 TEST_DIR = ./src/test NAME ?= Travis Shirk EMAIL ?= travis@pobox.com GITHUB_USER ?= nicfit GITHUB_REPO ?= eyeD3 PYPI_REPO = pypitest PROJECT_NAME = $(shell python setup.py --name 2> /dev/null) VERSION = $(shell python setup.py --version 2> /dev/null) RELEASE_NAME = $(shell python setup.py --release-name 2> /dev/null) 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 TEST_DATA_DIR ?= $(shell pwd)/src/test help: @echo "test - run tests quickly with the default Python" @echo "docs - generate Sphinx HTML documentation, including API docs" @echo "clean - remove all build, test, coverage and Python artifacts" @echo "clean-build - remove build artifacts" @echo "clean-pyc - remove Python file artifacts" @echo "clean-test - remove test and coverage artifacts" @echo "clean-docs - remove autogenerating doc artifacts" @echo "clean-patch - remove patch artifacts (.rej, .orig)" @echo "build - byte-compile python files and generate other build objects" @echo "lint - check style with flake8" @echo "test - run tests quickly with the default Python" @echo "test-all - run tests on every Python version with tox" @echo "coverage - check code coverage quickly with the default Python" @echo "test-all - run tests on various Python versions with tox" @echo "release - package and upload a release" @echo " PYPI_REPO=[pypitest]|pypi" @echo "pre-release - check repo and show version, generate changelog, etc." @echo "dist - package" @echo "install - install the package to the active Python's site-packages" @echo "build - build package source files" @echo "" @echo "Options:" @echo "TEST_PDB - If defined PDB options are added when 'pytest' is invoked" @echo "BROWSER - HTML viewer used by docs-view/coverage-view" @echo "CC_MERGE - Set to no to disable cookiecutter merging." @echo "CC_OPTS - OVerrided the default options (--no-input) with your own." build: python setup.py build clean: clean-local clean-build clean-pyc clean-test clean-patch clean-docs clean-local: -rm tags -rm all.id3 example.id3 clean-build: rm -fr build/ rm -fr dist/ rm -fr .eggs/ find . -name '*.egg-info' -exec rm -fr {} + find . -name '*.egg' -exec rm -f {} + clean-pyc: find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + find . -name '__pycache__' -exec rm -fr {} + clean-test: rm -fr .tox/ rm -f .coverage clean-patch: find . -name '*.rej' -exec rm -f '{}' \; find . -name '*.orig' -exec rm -f '{}' \; lint: flake8 $(SRC_DIRS) _PYTEST_OPTS= ifdef TEST_PDB _PDB_OPTS=--pdb -s endif test: pytest $(_PYTEST_OPTS) $(_PDB_OPTS) ${TEST_DIR} test-all: tox test-most: tox -e py27,py36 test-data: # Move these to eyed3.nicfit.net test -f ${TEST_DATA_DIR}/${TEST_DATA_FILE} || \ wget --quiet "http://nicfit.net/files/${TEST_DATA_FILE}" \ -O ${TEST_DATA_DIR}/${TEST_DATA_FILE} tar xzf ${TEST_DATA_DIR}/${TEST_DATA_FILE} -C ${TEST_DATA_DIR} cd src/test && rm -f ./data && ln -s ${TEST_DATA_DIR}/${TEST_DATA} ./data clean-test-data: -rm src/test/data -rm src/test/${TEST_DATA_FILE} pkg-test-data: tar czf ./build/${TEST_DATA_FILE} -C ./src/test ./eyeD3-test-data coverage: pytest --cov=./src/eyed3 \ --cov-report=html --cov-report term \ --cov-config=setup.cfg ${TEST_DIR} coverage-view: coverage ${BROWSER} build/tests/coverage/index.html;\ docs: rm -f docs/eyed3.rst rm -f docs/modules.rst sphinx-apidoc -o docs/ ${SRC_DIRS} $(MAKE) -C docs clean etc/mycog.py $(MAKE) -C docs html -rm example.id3 docs-view: docs $(BROWSER) docs/_build/html/index.html clean-docs: $(MAKE) -C docs clean -rm README.html pre-release: lint test changelog requirements @# Keep docs off pre-release target list, else it is pruned during 'release' but @# after a clean. @$(MAKE) docs @echo "VERSION: $(VERSION)" $(eval RELEASE_TAG = v${VERSION}) @echo "RELEASE_TAG: $(RELEASE_TAG)" @echo "RELEASE_NAME: $(RELEASE_NAME)" check-manifest @if git tag -l | grep -x ${RELEASE_TAG} > /dev/null; then \ echo "Version tag '${RELEASE_TAG}' already exists!"; \ false; \ fi IFS=$$'\n';\ for auth in `git authors --list | sed 's/.* <\(.*\)>/\1/'`; do \ echo "Checking $$auth...";\ grep "$$auth" AUTHORS.rst || echo "* $$auth" >> AUTHORS.rst;\ done @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 requirements: nicfit requirements pip-compile -U requirements.txt -o ./requirements.txt 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 build-release: test-most dist freeze-release: @(git diff --quiet && git diff --quiet --staged) || \ (printf "\n!!! Working repo has uncommited/unstaged changes. !!!\n" && \ printf "\nCommit and try again.\n" && false) tag-release: git tag -a $(RELEASE_TAG) -m "Release $(RELEASE_TAG)" git push --tags origin release: pre-release freeze-release build-release tag-release upload-release 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 -P444 $$f eyed3.nicfit.net:eyeD3-releases/`basename $$f`; \ done upload-release: github-release pypi-release web-release pypi-release: for f in `find dist -type f -name ${PROJECT_NAME}-${VERSION}.tar.gz \ -o -name \*.egg -o -name \*.whl`; do \ if test -f $$f ; then \ twine upload -r ${PYPI_REPO} --skip-existing $$f ; \ fi \ done sdist: build python setup.py sdist --formats=gztar,zip python setup.py bdist_egg python setup.py bdist_wheel dist: clean sdist docs cd docs/_build && \ tar czvf ../../dist/${PROJECT_NAME}-${VERSION}_docs.tar.gz html @# 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 -l dist install: clean python setup.py install tags: ctags -R ${SRC_DIRS} README.html: README.rst rst2html5.py README.rst >| README.html if test -n "${BROWSER}"; then \ ${BROWSER} README.html;\ fi CC_MERGE ?= yes CC_OPTS ?= --no-input 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 eyeD3-0.8.4/requirements.txt0000644000175000017500000000030513203726152016621 0ustar travistravis00000000000000# # This file is autogenerated by pip-compile # To update, run: # # pip-compile --output-file ./requirements.txt requirements.txt # grako==3.99.9 pathlib==1.0.1 python-magic==0.4.13 six==1.11.0 eyeD3-0.8.4/setup.cfg0000644000175000017500000000046413203726215015164 0ustar travistravis00000000000000[wheel] universal = 1 [flake8] max-line-length = 80 statistics = 1 ignore = E121,E124,E126,E127,E128,E131,E266 [aliases] test = pytest [html] directory = build/tests/coverage [run] omit = /tmp/* [tool:pytest] addopts = --verbose [metadata] license_file = LICENSE [egg_info] tag_build = tag_date = 0 eyeD3-0.8.4/LICENSE0000644000175000017500000010451413061344514014351 0ustar travistravis00000000000000 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.8.4/README.rst0000644000175000017500000000705313153052736015037 0ustar travistravis00000000000000Status ------ .. 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 = u"Integrity" audiofile.tag.album = u"Humanity Is The Devil" audiofile.tag.album_artist = u"Integrity" audiofile.tag.title = u"Hollow" audiofile.tag.track_num = 2 audiofile.tag.save() eyeD3_ is written and maintained by `Travis Shirk`_ and is licensed under version 3 of the GPL_. Features -------- * Python package for writing application and/or plugins. * Command-line tool driver script that supports plugins. viewer/editor interface. * Easy editing/viewing of audio metadata from the command-line, using the 'classic' plugin. * 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 2.7, >= 3.3 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://bitbucket.org/nicfit/eyed3/issues?status=new&status=open .. _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.8.4/CONTRIBUTING.rst0000644000175000017500000000604213153052736016006 0ustar travistravis00000000000000============ 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. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature Now you can make your changes locally. 5. 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. 6. 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 7. 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.8.4/src/0000755000175000017500000000000013203726215014126 5ustar travistravis00000000000000eyeD3-0.8.4/src/test/0000755000175000017500000000000013203726215015105 5ustar travistravis00000000000000eyeD3-0.8.4/src/test/mp3/0000755000175000017500000000000013203726215015604 5ustar travistravis00000000000000eyeD3-0.8.4/src/test/mp3/test_mp3.py0000644000175000017500000000776113161501620017721 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2009 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 unittest import os 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 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 eyeD3-0.8.4/src/test/mp3/__init__.py0000644000175000017500000000000013041005225017672 0ustar travistravis00000000000000eyeD3-0.8.4/src/test/mp3/test_infos.py0000644000175000017500000000450713041005225020330 0ustar travistravis00000000000000''' Test fucntions 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 ''' from __future__ import print_function 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', 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.8.4/src/test/test_classic_plugin.py0000644000175000017500000010265213203722311021514 0ustar travistravis00000000000000# -*- coding: utf-8 -*- import os import shutil import unittest import six import pytest import eyed3 from eyed3 import main, id3, core, compat 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 os.path.exists(DATA_D), "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" @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) def _addVersionOpt(self, 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 == u"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 == u"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 == u"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 == u"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 == u"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 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("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("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 = u"Why can't I be you?" for i, (c, d, l) in enumerate([(comment, u"c0", None), (comment, u"c1", None), (comment, u"c2", 'eng'), (u"¿Por qué no puedo ser tú ?", u"c2", 'esp'), ]): darg = u":{}".format(d) if d else "" larg = u":{}".format(l) if l else "" opts = [u"--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 u"", lang=compat.b(l if l else "eng")) assert (tag_comment.text == c) assert (tag_comment.description == d or u"") assert (tag_comment.lang == compat.b(l if l else "eng")) for d, l in [(u"c0", None), (u"c1", None), (u"c2", "eng"), (u"c2", "esp"), ]: larg = u":{}".format(l) if l else "" opts = [u"--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=compat.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 = u"Why can't I be you?" for i, (c, d, l) in enumerate([(comment, u"c0", None), (comment, u"c1", None), (comment, u"c2", 'eng'), (u"¿Por qué no puedo ser tú ?", u"c2", 'esp'), (comment, u"c4", "ger"), (comment, u"c4", "rus"), (comment, u"c5", "rus"), ]): darg = u":{}".format(d) if d else "" larg = u":{}".format(l) if l else "" opts = [u"--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 u"", lang=compat.b(l if l else "eng")) assert (tag_comment.text == c) assert (tag_comment.description == d or u"") assert (tag_comment.lang == compat.b(l if l else "eng")) opts = [u"--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 = u"Why can't I be you?" for i, (c, d, l) in enumerate([(comment, u"c0", None), (comment, u"c1", None), (comment, u"c2", 'eng'), (u"¿Por qué no puedo ser tú ?", u"c2", 'esp'), ]): darg = u":{}".format(d) if d else "" larg = u":{}".format(l) if l else "" opts = [u"--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 u"", lang=compat.b(l if l else "eng")) assert (tag_comment.text == c) assert (tag_comment.description == d or u"") assert (tag_comment.lang == compat.b(l if l else "eng")) for d, l in [(u"c0", None), (u"c1", None), (u"c2", "eng"), (u"c2", "esp"), ]: larg = u":{}".format(l) if l else "" opts = [u"--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=compat.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 == u"The Cramps") assert (af.tag.album == u"Psychedelic Jungle") assert (af.tag.title == u"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. def test_lyrics(audiofile, tmpdir, eyeD3): lyrics_files = [] for i in range(1, 4): lfile = tmpdir / "lryics{:d}".format(i) lfile.write_text((six.u(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(u"").text == ("1" * 100) assert audiofile.tag.lyrics.get(u"desc").text == ("2" * 200) assert audiofile.tag.lyrics.get(u"foo", "en").text == ("2" * 200) assert audiofile.tag.lyrics.get(u"foo", "es").text == ("3" * 300) assert audiofile.tag.lyrics.get(u"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.coveragewhore 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", ]) 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 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 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" 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" 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" 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" eyeD3-0.8.4/src/test/compat.py0000644000175000017500000000244513161501620016741 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2013 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 sys # assert functions that are not in unittest in python 2.6, and therefore not # import from nost.tools as in python >= 2.7 if sys.version_info[:2] == (2, 6): def assert_is_none(data): assert data is None def assert_is_not_none(data): assert data is not None def assert_in(data, container): assert data in container def assert_is(data1, data2): assert data1 is data2 eyeD3-0.8.4/src/test/test__init__.py0000644000175000017500000000267013161501620020115 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 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 eyed3 from .compat import * 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.8.4/src/test/test_display_plugin.py0000644000175000017500000001166713203722311021545 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # 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 . # ################################################################################ import sys import unittest import pytest from eyed3.id3 import TagFile from eyed3.plugins.display import * class TestDisplayPlugin(unittest.TestCase): def __init__(self, name): super(TestDisplayPlugin, self).__init__(name) def testSimpleTags(self): self.file.tag.artist = u"The Artist" self.file.tag.title = u"Some Song" self.file.tag.composer = u"Some Composer" self.__checkOutput(u"%a% - %t% - %C%", u"The Artist - Some Song - Some Composer") def testComposer(self): self.file.tag.composer = u"Bad Brains" self.__checkOutput(u"%C% - %composer%", u"Bad Brains - Bad Brains") def testCommentsTag(self): self.file.tag.comments.set(u"TEXT", description=None, lang=b"DE") self.file.tag.comments.set(u"#d-tag", description=u"#l-tag", lang=b"#t-tag") # Langs are chopped to 3 bytes (are are codes), so #t- is expected. self.__checkOutput(u"%comments,output=#d #l #t,separation=|%", u" DE TEXT|#l-tag #t- #d-tag") def testRepeatFunction(self): self.__checkOutput(u"$repeat(*,3)", u"***") self.__checkException(u"$repeat(*,three)", DisplayException) def testNotEmptyFunction(self): self.__checkOutput(u"$not-empty(foo,hello #t,nothing)", u"hello foo") self.__checkOutput(u"$not-empty(,hello #t,nothing)", u"nothing") def testNumberFormatFunction(self): self.__checkOutput(u"$num(123,5)", u"00123") self.__checkOutput(u"$num(123,3)", u"123") self.__checkOutput(u"$num(123,0)", u"123") self.__checkException(u"$num(nan,1)", DisplayException) self.__checkException(u"$num(1,foo)", DisplayException) self.__checkException(u"$num(1,)", DisplayException) def __checkOutput(self, pattern, expected): output = Pattern(pattern).output_for(self.file) assert output == expected def __checkException(self, pattern, exception_type): with pytest.raises(exception_type): Pattern(pattern).output_for(self.file) def setUp(self): import tempfile with tempfile.NamedTemporaryFile() as temp: temp.flush() self.file = TagFile(temp.name) self.file.initTag() def tearDown(self): pass class TestDisplayParser(unittest.TestCase): def __init__(self, name): super(TestDisplayParser, self).__init__(name) def testTextPattern(self): pattern = Pattern(u"hello") assert isinstance(pattern.sub_patterns[0], TextPattern) assert len(pattern.sub_patterns) == 1 def testTagPattern(self): pattern = Pattern(u"%comments,desc,lang,separation=|%") assert len(pattern.sub_patterns) == 1 assert isinstance(pattern.sub_patterns[0], TagPattern) comments_tag = pattern.sub_patterns[0] assert (len(comments_tag.parameters) == 4) assert comments_tag._parameter_value(u"description", None) == u"desc" assert comments_tag._parameter_value(u"language", None) == u"lang" assert (comments_tag._parameter_value(u"output", None) == AllCommentsTagPattern.PARAMETERS[2].default) assert comments_tag._parameter_value(u"separation", None) == u"|" def testComplexPattern(self): pattern = Pattern(u"Output: $format(Artist: $not-empty(%artist%,#t,none),bold=y)") assert len(pattern.sub_patterns) == 2 assert isinstance(pattern.sub_patterns[0], TextPattern) assert isinstance(pattern.sub_patterns[1], FunctionFormatPattern) text_patten = pattern.sub_patterns[1].parameters['text'].value assert len(text_patten.sub_patterns) == 2 assert isinstance(text_patten.sub_patterns[0], TextPattern) assert isinstance(text_patten.sub_patterns[1], FunctionNotEmptyPattern) def testCompileException(self): with pytest.raises(PatternCompileException): Pattern(u"$bad-pattern").output_for(None) with pytest.raises(PatternCompileException): Pattern(u"$unknown-function()").output_for(None) def setUp(self): pass def tearDown(self): pass eyeD3-0.8.4/src/test/test_main.py0000644000175000017500000001154613161501620017443 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2012-2015 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 unittest from eyed3 import main from eyed3.compat import PY2 from . import RedirectStdStreams class ParseCommandLineTest(unittest.TestCase): def testHelpExitsSuccess(self): 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(self): 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(self): 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(self): try: args, _, _ = main.parseCommandLine(["--plugins"]) except SystemExit as ex: assert ex.code == 0 def testLoadPlugin(self): from eyed3 import plugins from eyed3.plugins.classic import ClassicPlugin from eyed3.plugins.genres import GenreListPlugin # XXX: in python3 the import of main is treated differently, in this # case it adds confusing isinstance semantics demonstrated below # where isinstance works with PY2 and does not in PY3. This is old, # long before python3 but it is the closest explanantion I can find. #http://mail.python.org/pipermail/python-bugs-list/2004-June/023326.html args, _, _ = main.parseCommandLine([""]) if PY2: assert isinstance(args.plugin, ClassicPlugin) else: assert args.plugin.__class__.__name__ == ClassicPlugin.__name__ args, _, _ = main.parseCommandLine(["--plugin=genres"]) if PY2: assert isinstance(args.plugin, GenreListPlugin) else: 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(self): 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 eyeD3-0.8.4/src/test/test_factory.py0000644000175000017500000000077613153052737020204 0ustar travistravis00000000000000import 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.8.4/src/test/test_stats_plugins.py0000644000175000017500000000157213177420270021424 0ustar travistravis00000000000000from __future__ import unicode_literals import 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.8.4/src/test/__init__.py0000644000175000017500000000415513131306305017215 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2010-2015 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 eyed3 from eyed3.compat import StringIO import os import sys import logging if sys.version_info[:2] == (2, 6): import unittest2 as unittest else: 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.8.4/src/test/id3/0000755000175000017500000000000013203726215015564 5ustar travistravis00000000000000eyeD3-0.8.4/src/test/id3/test_frames.py0000644000175000017500000002520013153052737020456 0ustar travistravis00000000000000# -*- coding: utf-8 -*- import sys import pytest import unittest from pathlib import Path 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 eyed3.compat import unicode if sys.version_info[0:2] > (2, 7): from unittest.mock import patch else: from mock import patch 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(u"TCON") f = TextFrame(b"TCON") assert f.text == u"" f = TextFrame(b"TCON", u"content") assert f.text == u"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", u"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 == u"" 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 == unicode(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", u"2012", u"2010-01", u"2010-01-04", u"2010-01-04T18", u"2010-01-04T06:20", u"2010-01-04T06:20:15", ]: df = DateFrame(b"TDRC", d) dt = Date.parse(d) assert (df.text == unicode(str(dt))) assert (df.text == unicode(d)) # 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 try: date.date = 9 except TypeError: pass else: pytest.fail("TypeError not thrown") 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(): with pytest.raises(NotImplementedError): Frame.encrypt("Iceburn") with pytest.raises(NotImplementedError): Frame.decrypt("Iceburn") def test_LanguageCodeMixin(): with pytest.raises(TypeError): LanguageCodeMixin().lang = u"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"" def test_TermsOfUseFrame(audiofile, id3tag): terms = TermsOfUseFrame() assert terms.id == b"USER" assert terms.text == u"" assert terms.lang == DEFAULT_LANG id3tag.terms_of_use = u"Fucking MANDATORY!" audiofile.tag = id3tag audiofile.tag.save() file = eyed3.load(audiofile.path) assert file.tag.terms_of_use == u"Fucking MANDATORY!" id3tag.terms_of_use = u"Fucking MANDATORY!" audiofile.tag = id3tag audiofile.tag.save() file = eyed3.load(audiofile.path) assert file.tag.terms_of_use == u"Fucking MANDATORY!" id3tag.terms_of_use = (u"Fucking MANDATORY!", b"jib") audiofile.tag = id3tag audiofile.tag.save() file = eyed3.load(audiofile.path) assert file.tag.terms_of_use == u"Fucking MANDATORY!" assert file.tag.frame_set[TOS_FID][0].lang == b"jib" id3tag.terms_of_use = (u"Fucking MANDATORY!", b"en") audiofile.tag = id3tag audiofile.tag.save() file = eyed3.load(audiofile.path) assert file.tag.terms_of_use == u"Fucking MANDATORY!" assert file.tag.frame_set[TOS_FID][0].lang == b"en" def test_ObjectFrame(audiofile, id3tag): sixsixsix = b"\x29\x0a" * 666 with Path(__file__).open("rb") as fp: thisfile = fp.read() obj1 = ObjectFrame(description=u"Test Object", object_data=sixsixsix, filename=u"666.txt", mime_type="text/satan") obj2 = ObjectFrame(description=u"Test Object2", filename=unicode(__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(u"Test Object") assert obj1_2.mime_type == "text/satan" assert obj1_2.object_data == sixsixsix assert obj1_2.filename == u"666.txt" obj2_2 = file.tag.objects.get(u"Test Object2") assert obj2_2.mime_type == "text/python" assert obj2_2.object_data == thisfile assert obj2_2.filename == __file__ 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.8.4/src/test/id3/test_tag.py0000644000175000017500000011037413161501620017750 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2011-2012 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 os import pytest import unittest import eyed3 from eyed3.core import Date from eyed3.id3 import frames from eyed3.compat import unicode, BytesType from eyed3.id3 import Tag, ID3_DEFAULT_VERSION, ID3_V2_3, ID3_V2_4 from ..compat import * 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__, unicode(__file__)]: fi = FileInfo(name) assert type(fi.name) is unicode assert name == unicode(name) assert fi.tag_size == 0 # FIXME Passing invalid unicode 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 = u"Autolux" assert tag.artist == u"Autolux" assert len(tag.frame_set) == 1 tag.artist = u"" assert len(tag.frame_set) == 0 tag.artist = u"Autolux" assert tag.album is None tag.album = u"Future Perfect" assert tag.album == u"Future Perfect" assert tag.album_artist is None tag.album_artist = u"Various Artists" assert (tag.album_artist == u"Various Artists") assert (tag.title is None) tag.title = u"Robots in the Garden" assert (tag.title == u"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(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(u"bold", b"search") tag.comments.set(u"Always Try", u"") assert (len(tag.comments) == 1) c = tag.comments[0] assert (c.description == u"") assert (c.text == u"Always Try") assert (c.lang == b"eng") tag.comments.set(u"Speak Out", u"Bold") assert (len(tag.comments) == 2) c = tag.comments[1] assert (c.description == u"Bold") assert (c.text == u"Speak Out") assert (c.lang == b"eng") tag.comments.set(u"K Town Mosh Crew", u"Crippled Youth", b"sxe") assert (len(tag.comments) == 3) c = tag.comments[2] assert (c.description == u"Crippled Youth") assert (c.text == u"K Town Mosh Crew") assert (c.lang == b"sxe") # Lang is different, new frame tag.comments.set(u"K Town Mosh Crew", u"Crippled Youth", b"eng") assert (len(tag.comments) == 4) c = tag.comments[3] assert (c.description == u"Crippled Youth") assert (c.text == u"K Town Mosh Crew") assert (c.lang == b"eng") # Gets assert (tag.comments.get(u"", "fre") is None) assert (tag.comments.get(u"Crippled Youth", b"esp") is None) c = tag.comments.get(u"") assert c assert (c.description == u"") assert (c.text == u"Always Try") assert (c.lang == b"eng") assert tag.comments.get(u"Bold") is not None assert tag.comments.get(u"Bold", b"eng") is not None assert tag.comments.get(u"Crippled Youth", b"eng") is not None assert tag.comments.get(u"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(u"foobazz") is None) c = tag.comments.get(u"Bold") assert c is not None c2 = tag.comments.remove(u"Bold") assert (c == c2) assert (len(tag.comments) == 3) c = tag.comments.get(u"Crippled Youth", b"eng") assert c is not None c2 = tag.comments.remove(u"Crippled Youth", b"eng") assert (c == c2) assert (len(tag.comments) == 2) assert (tag.comments.remove(u"Crippled Youth", b"eng") is None) assert (len(tag.comments) == 2) assert (tag.comments.get(u"") == tag.comments.remove(u"")) assert (len(tag.comments) == 1) assert (tag.comments.get(u"Crippled Youth", b"sxe") == tag.comments.remove(u"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(u"Snoop", u"Dog", b"rap") assert tag.comments.get(u"Dog", b"rap").text == u"Snoop" c1.text = u"Lollipop" assert tag.comments.get(u"Dog", b"rap").text == u"Lollipop" # now thru add c2 = tag.comments.set(u"Doggy", u"Dog", b"rap") assert id(c1) == id(c2) assert tag.comments.get(u"Dog", b"rap").text == u"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 = u"Dischord" assert t.publisher == u"Dischord" t.publisher = u"Infinity Cat" assert t.publisher == u"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(u"") 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 == u"") 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(u"").description == u"") assert (tag.images.get(u"").picture_type == ImageFrame.FRONT_COVER) assert (tag.images.get(u"").image_data == b"\xab\xcd") assert (tag.images.get(u"").mime_type == "img/gif") assert (tag.images.get(u"")._mime_type == b"img/gif") assert (tag.images.get(u"").image_url is None) tag.images.set(ImageFrame.FRONT_COVER, b"\xdc\xba", b"img/gif", u"Different") assert len(tag.images) == 2 assert tag.images[1].description == u"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(u"Different").description == u"Different") assert (tag.images.get(u"Different").picture_type == ImageFrame.FRONT_COVER) assert (tag.images.get(u"Different").image_data == b"\xdc\xba") assert (tag.images.get(u"Different").mime_type == "img/gif") assert (tag.images.get(u"Different")._mime_type == b"img/gif") assert (tag.images.get(u"Different").image_url is None) # This is an update (same description) tag.images.set(ImageFrame.BACK_COVER, b"\xff\xef", b"img/jpg", u"Different") assert (len(tag.images) == 2) assert (tag.images[1].description == u"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(u"Different").description == u"Different") assert (tag.images.get(u"Different").picture_type == ImageFrame.BACK_COVER) assert (tag.images.get(u"Different").image_data == b"\xff\xef") assert (tag.images.get(u"Different").mime_type == "img/jpg") assert (tag.images.get(u"Different").image_url is None) count = 0 for img in tag.images: count += 1 assert count == 2 # Remove img = tag.images.remove(u"") assert (img.description == u"") 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(u"Different") assert img.description == u"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(u"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, u"A URL", img_url=b"http://www.tumblr.com/tagged/ty-segall") img = tag.images.get(u"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, coverted to bytes tag = Tag() tag.images.set(ImageFrame.BACK_COVER, b"\x00", u"img/jpg") img = tag.images[0] assert isinstance(img._mime_type, BytesType) img.mime_type = u"" assert isinstance(img._mime_type, BytesType) img.mime_type = None assert isinstance(img._mime_type, BytesType) 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(u"bold", b"search") tag.lyrics.set(u"Always Try", u"") assert (len(tag.lyrics) == 1) c = tag.lyrics[0] assert (c.description == u"") assert (c.text == u"Always Try") assert (c.lang == b"eng") tag.lyrics.set(u"Speak Out", u"Bold") assert (len(tag.lyrics) == 2) c = tag.lyrics[1] assert (c.description == u"Bold") assert (c.text == u"Speak Out") assert (c.lang == b"eng") tag.lyrics.set(u"K Town Mosh Crew", u"Crippled Youth", b"sxe") assert (len(tag.lyrics) == 3) c = tag.lyrics[2] assert (c.description == u"Crippled Youth") assert (c.text == u"K Town Mosh Crew") assert (c.lang == b"sxe") # Lang is different, new frame tag.lyrics.set(u"K Town Mosh Crew", u"Crippled Youth", b"eng") assert (len(tag.lyrics) == 4) c = tag.lyrics[3] assert (c.description == u"Crippled Youth") assert (c.text == u"K Town Mosh Crew") assert (c.lang == b"eng") # Gets assert (tag.lyrics.get(u"", b"fre") is None) assert (tag.lyrics.get(u"Crippled Youth", b"esp") is None) c = tag.lyrics.get(u"") assert (c) assert (c.description == u"") assert (c.text == u"Always Try") assert (c.lang == b"eng") assert tag.lyrics.get(u"Bold") is not None assert tag.lyrics.get(u"Bold", b"eng") is not None assert tag.lyrics.get(u"Crippled Youth", b"eng") is not None assert tag.lyrics.get(u"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(u"foobazz") is None c = tag.lyrics.get(u"Bold") assert c is not None c2 = tag.lyrics.remove(u"Bold") assert c == c2 assert len(tag.lyrics) == 3 c = tag.lyrics.get(u"Crippled Youth", b"eng") assert c is not None c2 = tag.lyrics.remove(u"Crippled Youth", b"eng") assert c == c2 assert len(tag.lyrics) == 2 assert tag.lyrics.remove(u"Crippled Youth", b"eng") is None assert len(tag.lyrics) == 2 assert tag.lyrics.get(u"") == tag.lyrics.remove(u"") assert len(tag.lyrics) == 1 assert (tag.lyrics.get(u"Crippled Youth", b"sxe") == tag.lyrics.remove(u"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(u"") is None) tag = Tag() tag.objects.set(b"\xab\xcd", b"img/gif") assert (len(tag.objects) == 1) assert (tag.objects[0].description == u"") assert (tag.objects[0].filename == u"") 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(u"").description == u"") assert (tag.objects.get(u"").filename == u"") assert (tag.objects.get(u"").object_data == b"\xab\xcd") assert (tag.objects.get(u"").mime_type == "img/gif") tag.objects.set(b"\xdc\xba", b"img/gif", u"Different") assert (len(tag.objects) == 2) assert (tag.objects[1].description == u"Different") assert (tag.objects[1].filename == u"") 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(u"Different").description == u"Different") assert (tag.objects.get(u"Different").filename == u"") assert (tag.objects.get(u"Different").object_data == b"\xdc\xba") assert (tag.objects.get(u"Different").mime_type == "img/gif") assert (tag.objects.get(u"Different")._mime_type == b"img/gif") # This is an update (same description) tag.objects.set(b"\xff\xef", b"img/jpg", u"Different", u"example_filename.XXX") assert (len(tag.objects) == 2) assert (tag.objects[1].description == u"Different") assert (tag.objects[1].filename == u"example_filename.XXX") assert (tag.objects[1].object_data == b"\xff\xef") assert (tag.objects[1].mime_type == "img/jpg") assert (tag.objects.get(u"Different").description == u"Different") assert (tag.objects.get(u"Different").filename == u"example_filename.XXX") assert (tag.objects.get(u"Different").object_data == b"\xff\xef") assert (tag.objects.get(u"Different").mime_type == "img/jpg") count = 0 for obj in tag.objects: count += 1 assert (count == 2) # Remove obj = tag.objects.remove(u"") assert (obj.description == u"") assert (obj.filename == u"") assert (obj.object_data == b"\xab\xcd") assert (obj.mime_type == "img/gif") assert (len(tag.objects) == 1) obj = tag.objects.remove(u"Different") assert (obj.description == u"Different") assert (obj.filename == u"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(u"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", u"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(u"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(u"Hardcore") tag.genre = u"Hardcore" assert (tag.genre.name == u"Hardcore") assert (tag.genre == gobj) tag.genre = 130 assert tag.genre.id == 130 assert tag.genre.name == u"Terror" tag.genre = 0 assert tag.genre.id == 0 assert tag.genre.name == u"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(u"Custom content") assert (tag.user_text_frames.get(u"").text == u"Custom content") utf2 = tag.user_text_frames.set(u"Content custom", u"Desc1") assert (tag.user_text_frames.get(u"Desc1").text == u"Content custom") assert (len(tag.user_text_frames) == 2) utf3 = tag.user_text_frames.set(u"New content", u"") assert (tag.user_text_frames.get(u"").text == u"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(u"") == utf1) assert (tag.user_text_frames.get(u"Desc1") == utf2) tag.user_text_frames.remove(u"") assert (len(tag.user_text_frames) == 1) tag.user_text_frames.remove(u"Desc1") assert (len(tag.user_text_frames) == 0) tag.user_text_frames.set(u"Foobazz", u"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") 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(u"").url == b"http://yo.yo.com/") utf2 = tag.user_url_frames.set(b"http://run.dmc.org", u"URL") assert (tag.user_url_frames.get(u"URL").url == b"http://run.dmc.org") assert len(tag.user_url_frames) == 2 utf3 = tag.user_url_frames.set(b"http://my.adidas.com", u"") assert (tag.user_url_frames.get(u"").url == b"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(u"") == uuf1) assert (tag.user_url_frames.get(u"URL") == utf2) tag.user_url_frames.remove(u"") assert (len(tag.user_url_frames) == 1) tag.user_url_frames.remove(u"URL") assert (len(tag.user_url_frames) == 0) tag.user_url_frames.set(b"Foobazz", u"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 = unicode(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", u"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", u"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 == u"Subtitle" assert txxx.description == u"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=u"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 = u"Chapter %d" % i c.subtitle = u"Subtitle %d" % i c.user_url = unicode("http://example.com/%d" % i).encode("ascii") 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(unicode("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 == u"Chapter %d" % i assert c.subtitle == u"Subtitle %d" % i assert (c.user_url == unicode("http://example.com/%d" % i).encode("ascii")) 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) eyeD3-0.8.4/src/test/id3/__init__.py0000644000175000017500000000000013041005225017652 0ustar travistravis00000000000000eyeD3-0.8.4/src/test/id3/test_headers.py0000644000175000017500000004320313161501620020604 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2012 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 unittest import pytest from eyed3.id3.headers import * from eyed3.id3 import ID3_DEFAULT_VERSION, TagException from ..compat import * 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.8.4/src/test/id3/test_id3.py0000644000175000017500000001331213161501620017646 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2012 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 unittest import pytest from eyed3.id3 import * from eyed3.compat import unicode class GenreTests(unittest.TestCase): def testEmptyGenre(self): g = Genre() assert g.id is None assert g.name is None def testValidGenres(self): # 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(self): for i in range(GenreMap.GENRE_MAX + 1, 256): assert genres[i] is None with pytest.raises(KeyError): genres.__getitem__(256) def testCustomGenres(self): # Genres can be created for any name, their ID is None g = Genre(name=u"Grindcore") assert g.name == u"Grindcore" assert g.id is None # But when constructing with IDs they must map. with pytest.raises(ValueError): Genre.__call__(id=1024) def testRemappedNames(self): g = Genre(id=3, name=u"dance stuff") assert (g.id == 3) assert (g.name == u"Dance") g = Genre(id=666, name=u"Funky") assert (g.id is None) assert (g.name == u"Funky") def testGenreEq(self): for s in [u"Hardcore", u"(129)Hardcore", u"(129)", u"(0129)", u"129", u"0129"]: assert Genre.parse(s) == Genre.parse(s) assert Genre.parse(s) != Genre.parse(u"Blues") def testParseGenre(self): test_list = [u"Hardcore", u"(129)Hardcore", u"(129)", u"(0129)", u"129", u"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 == u"Hardcore" assert g.id == 129 g = Genre.parse(u"") assert g is None g = Genre.parse(u"1") assert (g.id == 1) assert(g.name == u"Classic Rock") def testUnicode(self): assert (unicode(Genre(u"Hardcore")) == u"(129)Hardcore") assert (unicode(Genre(u"Grindcore")) == u"Grindcore") class VersionTests(unittest.TestCase): def setUp(self): self.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"), ] def testId3Versions(self): 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(self): for const, tple, string in self.id3_versions: assert versionToString(const) == string with pytest.raises(TypeError): versionToString(666) with pytest.raises(ValueError): versionToString((3,1,0)) def test_isValidVersion(self): for v, _, _ in self.id3_versions: assert isValidVersion(v) for _, v, _ in self.id3_versions: if None in v: assert not isValidVersion(v, True) else: assert isValidVersion(v, True) assert not isValidVersion((3, 1, 1)) def testNormalizeVersion(self): 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) eyeD3-0.8.4/src/test/test_plugins.py0000644000175000017500000000347713161501620020204 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 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 . # ################################################################################ from eyed3.plugins import * from .compat 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 from eyed3.utils import FileHandler 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.8.4/src/test/conftest.py0000644000175000017500000000246413203715221017305 0ustar travistravis00000000000000import shutil import pytest import eyed3 from uuid import uuid4 from pathlib import Path DATA_D = Path(__file__).parent / "data" def _tempCopy(src, dest_dir): testfile = Path(str(dest_dir)) / "{}.mp3".format(uuid4()) shutil.copyfile(str(src), str(testfile)) return testfile @pytest.fixture(scope="function") def audiofile(tmpdir): """Makes a copy of test.mp3 and loads it using eyed3.load().""" 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(): 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 exit: retval = exit.code assert retval == expected_retval return eyed3.load(audiofile.path, tag_version=reload_version) return func eyeD3-0.8.4/src/test/test_utils.py0000644000175000017500000000560413161501620017655 0ustar travistravis00000000000000# -*- coding: utf-8 -*- import os import pytest import eyed3.utils.console from eyed3.utils import guessMimetype from eyed3.utils.console import (printMsg, printWarning, printHeader, Fore, WARNING_COLOR, HEADER_COLOR) from . import DATA_D, RedirectStdStreams @pytest.mark.skipif(not os.path.exists(DATA_D), reason="test requires data files") @pytest.mark.parametrize(("ext", "valid_types"), [("id3", ["application/x-id3"]), ("tag", ["application/x-id3"]), ("aac", ["audio/x-aac", "audio/x-hx-aac-adts"]), ("aiff", ["audio/x-aiff"]), ("amr", ["audio/amr", "application/octet-stream"]), ("au", ["audio/basic"]), ("m4a", ["audio/mp4", "audio/x-m4a"]), ("mka", ["video/x-matroska", "application/octet-stream"]), ("mp3", ["audio/mpeg"]), ("mp4", ["video/mp4", "audio/x-m4a"]), ("mpg", ["video/mpeg"]), ("ogg", ["audio/ogg", "application/ogg"]), ("ra", ["audio/x-pn-realaudio", "application/vnd.rn-realmedia"]), ("wav", ["audio/x-wav"]), ("wma", ["audio/x-ms-wma", "video/x-ms-wma", "video/x-ms-asf"])]) def testSampleMimeTypes(ext, valid_types): guessed = guessMimetype(os.path.join(DATA_D, "sample.%s" % ext)) if guessed: assert guessed in valid_types 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)) eyeD3-0.8.4/src/test/test_core.py0000644000175000017500000001277413161501620017453 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 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 os from pathlib import Path import pytest import eyed3 from eyed3 import core 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__) == 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 af.path == os.path.abspath(__file__) def test_AudioInfo(): from eyed3.core import AudioInfo info = AudioInfo() assert (info.time_secs == 0) assert (info.size_bytes == 0) def test_Date(): from eyed3.core import Date for d in [Date(1973), Date(year=1973), Date.parse("1973")]: assert (d.year == 1973) assert (d.month == None) assert (d.day == None) assert (d.hour == None) assert (d.minute == None) assert (d.second == None) assert (str(d) == "1973") for d in [Date(1973, 3), Date(year=1973, month=3), Date.parse("1973-03")]: assert (d.year == 1973) assert (d.month == 3) assert (d.day == None) assert (d.hour == None) assert (d.minute == None) assert (d.second == None) assert (str(d) == "1973-03") for d in [Date(1973, 3, 6), Date(year=1973, month=3, day=6), Date.parse("1973-3-6")]: assert (d.year == 1973) assert (d.month == 3) assert (d.day == 6) assert (d.hour == None) assert (d.minute == None) assert (d.second == None) assert (str(d) == "1973-03-06") for d in [Date(1973, 3, 6, 23), Date(year=1973, month=3, day=6, hour=23), Date.parse("1973-3-6T23")]: assert (d.year == 1973) assert (d.month == 3) assert (d.day == 6) assert (d.hour == 23) assert (d.minute == None) assert (d.second == None) assert (str(d) == "1973-03-06T23") for d in [Date(1973, 3, 6, 23, 20), Date(year=1973, month=3, day=6, hour=23, minute=20), Date.parse("1973-3-6T23:20")]: assert (d.year == 1973) assert (d.month == 3) assert (d.day == 6) assert (d.hour == 23) assert (d.minute == 20) assert (d.second == None) assert (str(d) == "1973-03-06T23:20") for d in [Date(1973, 3, 6, 23, 20, 15), Date(year=1973, month=3, day=6, hour=23, minute=20, second=15), Date.parse("1973-3-6T23:20:15")]: assert (d.year == 1973) assert (d.month == 3) assert (d.day == 6) assert (d.hour == 23) assert (d.minute == 20) assert (d.second == 15) assert (str(d) == "1973-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(1973, 3, 6, 23, 20, 15) assert not dt == None 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.8.4/src/test/test_binfuncs.py0000644000175000017500000000552113161501620020322 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2009 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 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.8.4/src/eyed3/0000755000175000017500000000000013203726215015137 5ustar travistravis00000000000000eyeD3-0.8.4/src/eyed3/plugins/0000755000175000017500000000000013203726215016620 5ustar travistravis00000000000000eyeD3-0.8.4/src/eyed3/plugins/genres.py0000644000175000017500000000525613061344514020465 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2012 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 . # ################################################################################ from __future__ import print_function import math from eyed3 import id3 from eyed3.plugins import Plugin class GenreListPlugin(Plugin): SUMMARY = u"Display the full list of standard ID3 genres." DESCRIPTION = u"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) def _printGenres(self, 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(u"%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 = u"%3d: %s" % (i, id3.genres[i]) else: c1 = u"" if (i * 2) < len(genre_ids): try: c2 = u"%3d: %s" % (i + offset, id3.genres[i + offset]) except IndexError: break else: c2 = u"" print(c1 + (u" " * (40 - len(c1))) + c2) print(u"") eyeD3-0.8.4/src/eyed3/plugins/classic.py0000644000175000017500000015317613203722311020621 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2007-2016 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 . # ################################################################################ from __future__ import print_function import os import re from functools import partial from argparse import ArgumentTypeError from eyed3 import LOCAL_ENCODING from eyed3.plugins import LoaderPlugin from eyed3 import core, id3, mp3, utils, compat from eyed3.utils import makeUniqueFileName from eyed3.utils.console import (printMsg, printError, printWarning, boldText, HEADER_COLOR, Fore, getTtySize) from eyed3.id3.frames import ImageFrame from eyed3.utils.log import getLogger log = getLogger(__name__) FIELD_DELIM = ':' DEFAULT_MAX_PADDING = 64 * 1024 class ClassicPlugin(LoaderPlugin): SUMMARY = u"Classic eyeD3 interface for viewing and editing tags." DESCRIPTION = u""" 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 UnicodeArg(arg): return _unicodeArgValue(arg) def PositiveIntArg(i): i = int(i) if i < 0: raise ArgumentTypeError("positive number required") return i # Common options g.add_argument("-a", "--artist", type=UnicodeArg, dest="artist", metavar="STRING", help=ARGS_HELP["--artist"]) g.add_argument("-A", "--album", type=UnicodeArg, dest="album", metavar="STRING", help=ARGS_HELP["--album"]) g.add_argument("-b", "--album-artist", type=UnicodeArg, dest="album_artist", metavar="STRING", help=ARGS_HELP["--album-artist"]) g.add_argument("-t", "--title", type=UnicodeArg, 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", type=UnicodeArg, dest="composer", metavar="STRING", help=ARGS_HELP["--composer"]) 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", type=UnicodeArg, 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", type=UnicodeArg, metavar="STRING", help=ARGS_HELP["--comment"]) 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 _unicodeArgValue(arg): if not isinstance(arg, compat.UnicodeType): return compat.unicode(arg, LOCAL_ENCODING) else: return arg def DescLangArg(arg): """DESCRIPTION[:LANG]""" arg = _unicodeArgValue(arg) vals = _splitArgs(arg, 2) desc = vals[0] lang = vals[1] if len(vals) > 1 else id3.DEFAULT_LANG return (desc, compat.b(lang)[:3] or id3.DEFAULT_LANG) def DescTextArg(arg): """DESCRIPTION:TEXT""" arg = _unicodeArgValue(arg) vals = _splitArgs(arg, 2) desc = vals[0].strip() text = FIELD_DELIM.join(vals[1:] if len(vals) > 1 else []) return (desc or u"", text or u"") KeyValueArg = DescTextArg def DescUrlArg(arg): desc, url = DescTextArg(arg) return (desc, url.encode("latin1")) def FidArg(arg): arg = _unicodeArgValue(arg) fid = arg.strip().encode("ascii") if not fid: raise ArgumentTypeError("No frame ID") return fid def TextFrameArg(arg): """FID:TEXT""" arg = _unicodeArgValue(arg) 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 u"" 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] """ arg = _unicodeArgValue(arg) vals = _splitArgs(arg, 3) text = vals[0] if not text: raise ArgumentTypeError("text required") desc = vals[1] if len(vals) > 1 else u"" lang = vals[2] if len(vals) > 2 else id3.DEFAULT_LANG return (text, desc, compat.b(lang)[:3]) def LyricsArg(arg): text, desc, lang = CommentArg(arg) try: with open(text, "rb") as fp: data = fp.read() except Exception: # noqa: B901 raise ArgumentTypeError("Unable to read file") return (_unicodeArgValue(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 = UnicodeArg(args[2]) if len(args) > 2 else u"" mt = None try: type_id = id3.frames.ImageFrame.stringToPicType(type_str) except: # 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 = utils.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] mt = None desc = None filename = None if path: mt = args[1] desc = UnicodeArg(args[2]) if len(args) > 2 else u"" filename = UnicodeArg(args[3]) \ if len(args) > 3 \ else UnicodeArg(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", type=UnicodeArg, 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("--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", type=UnicodeArg, 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", type=UnicodeArg, 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-popularty"]) 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 super(ClassicPlugin, self).handleFile(f, tag_version=parse_version) if not self.audio_file: return self.terminal_width = getTtySize()[1] self.printHeader(f) printMsg("-" * self.terminal_width) if 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("No ID3 %s tag found!" % id3.versionToString(self.args.tag_version)) return self.printTag(self.audio_file.tag) 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("Renamed '%s' to '%s'" % (orig, self.audio_file.path)) except IOError as ex: printError(str(ex)) printMsg("-" * self.terminal_width) def printHeader(self, file_path): file_len = len(file_path) from stat import ST_SIZE file_size = os.stat(file_path)[ST_SIZE] size_str = utils.formatSize(file_size) size_len = len(size_str) + 5 if file_len + size_len >= self.terminal_width: file_path = "..." + file_path[-(75 - size_len):] file_len = len(file_path) pat_len = self.terminal_width - file_len - size_len printMsg("%s%s%s[ %s ]%s" % (boldText(file_path, c=HEADER_COLOR()), HEADER_COLOR(), " " * pat_len, size_str, Fore.RESET)) def printAudioInfo(self, info): if isinstance(info, mp3.Mp3AudioInfo): printMsg(boldText("Time: ") + "%s\tMPEG%d, Layer %s\t[ %s @ %s Hz - %s ]" % (utils.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.terminal_width) def _getDefaultNameForObject(self, 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("ID3 %s: %d frames" % (id3.versionToString(tag.version), len(tag.frame_set))) return printMsg("ID3 %s:" % id3.versionToString(tag.version)) artist = tag.artist if tag.artist else u"" title = tag.title if tag.title else u"" album = tag.album if tag.album else u"" 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)) 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._getGenre(id3_std=not self.args.non_std_genres) genre_str = "%s: %s (id %s)" % (boldText("genre"), genre.name, str(genre.id)) if genre else u"" printMsg("%s: %s\t\t%s" % (boldText("track"), track_str, genre_str)) disc_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 u"", 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("") if self.args.write_images_dir: img_path = "%s%s" % (self.args.write_images_dir, os.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) 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.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 "" total_bytes = 0 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("Removing ID3 %s tag: %s" % (rm_str, "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 # --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)), ): 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) # Returing None means do nothing, (None, None) would clear both vals return n # --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("Setting release year: %s" % 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, u"", 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) in compat.StringTypes: 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, compat.unicode(lang, "ascii"))) accessor.set(text, desc, compat.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-popularty 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("Setting '%s' %s to '%s'" % (desc, what, text)) accessor.set(text, desc) else: printWarning("Removing '%s' %s" % (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, compat.BytesType)) 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": u"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.", "--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'." % compat.unicode(id3.DEFAULT_LANG, "ascii"), "--remove-comment": "Remove comment matching DESCRIPTION and LANG. " "The default language code is '%s'." % compat.unicode(id3.DEFAULT_LANG, "ascii"), "--remove-all-comments": "Remove all comments 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'." % compat.unicode(id3.DEFAULT_LANG, "ascii"), "--remove-lyrics": "Remove lyrics matching DESCRIPTION and LANG. " "The default language code is '%s'." % compat.unicode(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-popularty": "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.", } eyeD3-0.8.4/src/eyed3/plugins/nfo.py0000644000175000017500000001301413153052737017760 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2009-2012 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 . # ################################################################################ from __future__ import print_function import 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 = u"Create NFO files for each directory scanned." DESCRIPTION = u"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(u"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 u"") 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 u"" 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.8.4/src/eyed3/plugins/xep_118.py0000644000175000017500000000401313061344514020355 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2009 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 os from eyed3 import compat from eyed3.plugins import LoaderPlugin from eyed3.utils.console import printMsg class Xep118Plugin(LoaderPlugin): NAMES = ["xep-118"] SUMMARY = u"Outputs all tags in XEP-118 XML format. "\ "(see: http://xmpp.org/extensions/xep-0118.html)" def handleFile(self, f): super(Xep118Plugin, self).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 xml = u"\n" if tag.artist: xml += " %s\n" % tag.artist if tag.title: xml += " %s\n" % tag.title if tag.album: xml += " %s\n" % tag.album xml += (" file://%s\n" % compat.unicode(os.path.abspath(audio_file.path))) if audio_file.info: xml += " %s\n" % \ compat.unicode(audio_file.info.time_secs) xml += "\n" return xml eyeD3-0.8.4/src/eyed3/plugins/itunes.py0000644000175000017500000000562213061344514020506 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2012 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 . # ################################################################################ from __future__ import print_function from eyed3.plugins import LoaderPlugin from eyed3.id3.apple import PCST, PCST_FID, WFED, WFED_FID class Podcast(LoaderPlugin): NAMES = ['itunes-podcast'] SUMMARY = u"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(u"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.8.4/src/eyed3/plugins/art.py0000644000175000017500000002061213061344514017761 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2014 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 . # ################################################################################ from __future__ import print_function import os import hashlib from eyed3.utils import art from eyed3 import compat from eyed3.utils import guessMimetype from eyed3.utils import makeUniqueFileName from eyed3.plugins import LoaderPlugin from eyed3.utils.console import printMsg, printWarning from eyed3.id3.frames import ImageFrame try: import PIL # noqa _have_PIL = True except ImportError: _have_PIL = False DESCR_FNAME_PREFIX = "filename: " 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 = u"Art for albums, artists, etc." DESCRIPTION = u"" 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("--update-files", action="store_true", help="Write art files from tag images.") g.add_argument("--update-tags", action="store_true", help="Write tag image from art files.") def start(self, args, config): 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 handleDirectory(self, d, _): global md5_file_cache md5_file_cache.clear() try: if not self._file_cache: print("%s: nothing to do." % d) return printMsg("\nProcessing %s" % 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(compat.unicode(ex)) continue if art_file.art_type: printMsg("file %s: %s\n\t%s" % (img_base, art_file.art_type, pilImageDetails(pil_img))) dir_art.append(art_file) else: printMsg("file %s: unknown (ignored)" % img_base) if not dir_art: print("No art files found.") self._retval += 1 # Tag images all_tags = sorted([f.tag for f in self._file_cache], key=lambda x: x.file_info.name) for tag in all_tags: file_base = os.path.basename(tag.file_info.name) for img in tag.images: try: pil_img = pilImage(img) except IOError as ex: printWarning(compat.unicode(ex)) continue if img.picture_type in art.FROM_ID3_ART_TYPES: img_type = art.FROM_ID3_ART_TYPES[img.picture_type] printMsg("tag %s: %s (Description: %s)\n\t%s" % (file_base, img_type, img.description, pilImageDetails(pil_img))) 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." % img_file) 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: printMsg("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: descr = "filename: %s" % \ os.path.splitext( os.path.basename(art_file.file_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): if not _have_PIL: return None from PIL import Image if isinstance(source, ImageFrame): return Image.open(compat.StringIO(source.image_data)) else: return Image.open(source) def pilImageDetails(img): if not img: return '' return "[%dx%d %s md5:%s]" % (img.size[0], img.size[1], img.format.lower(), md5Data(img.tobytes())) def md5Data(data): md5 = hashlib.md5() md5.update(data) return md5.hexdigest() md5_file_cache = {} 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.8.4/src/eyed3/plugins/display.py0000644000175000017500000012046313203723345020646 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2014-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 . # ################################################################################ from __future__ import print_function import os import re import abc from argparse import ArgumentTypeError from eyed3 import id3, compat from eyed3.utils import console, formatSize, formatTime from eyed3.plugins import LoaderPlugin try: from eyed3.plugins._display_parser import DisplayPatternParser _have_grako = True except ImportError: _have_grako = False class Pattern(object): def __init__(self, text=None, sub_patterns=None): self.__text = text self.__sub_patterns = sub_patterns def output_for(self, audio_file): output = u"" for sub_pattern in self.sub_patterns or []: output += sub_pattern.output_for(audio_file) return output def __get_sub_patterns(self): if self.__sub_patterns is None and self.__text is not None: self.__compile() return self.__sub_patterns def __set_sub_patterns(self, sub_patterns): self.__sub_patterns = sub_patterns sub_patterns = property(__get_sub_patterns, __set_sub_patterns) def __compile(self): # TODO: add support for comments in pattern parser = DisplayPatternParser(whitespace='') try: asts = parser.parse(self.__text, rule_name='start') self.sub_patterns = self.__compile_asts(asts) self.__text = None except BaseException as parsing_error: raise PatternCompileException(compat.unicode(parsing_error)) def __compile_asts(self, asts): patterns = [] for ast in asts: patterns.append(self.__compile_ast(ast)) return patterns def __compile_ast(self, ast): if ast is None: return None if "text" in ast: return TextPattern(ast["text"]) if "tag" in ast: parameters = self.__compile_parameters(ast["parameters"]) return self.__create_complex_pattern(TagPattern, ast["name"], parameters) if "function" in ast: parameters = self.__compile_parameters(ast["parameters"]) if len(parameters) == 1 and parameters[0][0] is None and len( parameters[0][1].sub_patterns) == 0: parameters = [] return self.__create_complex_pattern(FunctionPattern, ast["name"], parameters) def __compile_parameters(self, parameter_asts): parameters = [] for parameter_ast in parameter_asts: sub_patterns = self.__compile_asts(parameter_ast["value"]) parameters.append((parameter_ast["name"], Pattern(sub_patterns=sub_patterns))) return parameters def __create_complex_pattern(self, base_class, class_name, parameters): pattern_class = self.__find_pattern_class(base_class, class_name) if pattern_class is not None: return pattern_class(class_name, parameters) raise PatternCompileException("Unknown " + base_class.TYPE + " '" + class_name + "'") def __find_pattern_class(self, base_class, class_name): for pattern_class in Pattern.sub_pattern_classes(base_class): if class_name in pattern_class.NAMES: return pattern_class @staticmethod def sub_pattern_classes(base_class): sub_classes = [] for pattern_class in base_class.__subclasses__(): if len(pattern_class.__subclasses__()) > 0: sub_classes.extend(Pattern.sub_pattern_classes(pattern_class)) continue sub_classes.append(pattern_class) return sub_classes @staticmethod def pattern_class_parameters(pattern_class): try: return pattern_class.PARAMETERS except AttributeError: return [] def __repr__(self): return self.__str__() def __str__(self): return str(self.__sub_patterns) class TextPattern(Pattern): SPECIAL_CHARACTERS = list("\\%$,()=nt") SPECIAL_CHARACTERS_DESCRIPTIONS = list("\\%$,()=") + ["New line", "Tab"] def __init__(self, text): super(TextPattern, self).__init__(text) self.__text = text self.__replace_escapes() def __replace_escapes(self): escape_matches = list(re.compile('\\\\.').finditer(self.__text)) escape_matches.reverse() for escape_match in escape_matches: character = self.__text[escape_match.end() - 1] if character not in TextPattern.SPECIAL_CHARACTERS: raise PatternCompileException("Unknown escape character '" + character + "'") if character == 'n': character = os.linesep if character == 't': character = '\t' self.__text = self.__text[:escape_match.start()] + character + \ self.__text[escape_match.end():] def output_for(self, audio_file): return self.__text def __str__(self): return "text:" + self.__text class ComplexPattern(Pattern): __metaclass__ = abc.ABCMeta TYPE = "unknown" NAMES = [] DESCRIPTION = "" PARAMETERS = [] class ExpectedParameter(object): def __init__(self, name, **kwargs): self.name = name if "default" in kwargs: self.requried = False self.default = kwargs["default"] else: self.requried = True def __repr__(self): return self.__str__() def __str__(self): if self.requried: return self.name return self.name + "(" + str(self.default) + ")" class Parameter(object): def __init__(self, value, provided): self.value = value self.provided = provided def __repr__(self): return self.__str__() def __str__(self): return str(self.value) + "(" + str(self.provided) + ")" def __init__(self, name, parameters): super(ComplexPattern, self).__init__() self.__name = name self.__import_parameters(parameters) def output_for(self, audio_file): output = self._get_output_for(audio_file) return output or "" @abc.abstractmethod def _get_output_for(self, audio_file): pass def __import_parameters(self, parameters): expected_parameters = Pattern.pattern_class_parameters(self.__class__) self.__parameters = {} for expected_parameter in expected_parameters: found = False for parameter in parameters: if (parameter[0] is None or parameter[0] == expected_parameter.name): self.__parameters[expected_parameter.name] = \ ComplexPattern.Parameter(parameter[1], True) parameters.remove(parameter) found = True break if parameter[0] is not None: break if not expected_parameter.requried: self.__parameters[expected_parameter.name] = \ ComplexPattern.Parameter( Pattern(text=expected_parameter.default), True) found = True break raise PatternCompileException( self._error_message("Unexpected parameter")) if not found: if expected_parameter.requried: raise PatternCompileException(self._error_message( "Missing required parameter '" + expected_parameter.name + "'")) self.__parameters[expected_parameter.name] = \ ComplexPattern.Parameter( Pattern(text=expected_parameter.default), False) if len(parameters) > 0: raise PatternCompileException( self._error_message("Unexpected parameter")) def __get_parameters(self): return self.__parameters parameters = property(__get_parameters) def _parameter_value(self, name, audio_file): return self.parameters[name].value.output_for(audio_file) def _parameter_bool(self, name, audio_file): value = self._parameter_value(name, audio_file) return value.lower() in ("yes", "true", "y", "t", "1", "on") def __get_name(self): return self.__name name = property(__get_name) def _error_message(self, message): return self.TYPE.capitalize() + " " + self.__name + ": " + message def __str__(self): return self.TYPE + ":" + self.name + str(self.parameters) class PlaceholderUsagePattern(object): __metaclass__ = abc.ABCMeta def _replace_placeholders(self, text, replacements): if len(replacements) == 0: return text replacement = replacements.pop(0) subtexts = [] for subtext in text.split(replacement[0]): subtexts.append( self._replace_placeholders(subtext, list(replacements))) return (replacement[1] or "").join(subtexts) class TagPattern(ComplexPattern): __metaclass__ = abc.ABCMeta TYPE = "tag" class ArtistTagPattern(TagPattern): NAMES = ["a", "artist"] DESCRIPTION = "Artist" def _get_output_for(self, audio_file): return audio_file.tag.artist class AlbumTagPattern(TagPattern): NAMES = ["A", "album"] DESCRIPTION = "Album" def _get_output_for(self, audio_file): return audio_file.tag.album class AlbumArtistTagPattern(TagPattern): NAMES = ["b", "album-artist"] DESCRIPTION = "Album artist" def _get_output_for(self, audio_file): return audio_file.tag.album_artist class ComposerTagPattern(TagPattern): NAMES = ["C", "composer"] DESCRIPTION = "Composer" def _get_output_for(self, audio_file): return audio_file.tag.composer class TitleTagPattern(TagPattern): NAMES = ["t", "title"] DESCRIPTION = "Title" def _get_output_for(self, audio_file): return audio_file.tag.title class TrackTagPattern(TagPattern): NAMES = ["n", "track"] DESCRIPTION = "Track number" def _get_output_for(self, audio_file): n = audio_file.tag.track_num[0] return str(n or "") class TrackTotalTagPattern(TagPattern): NAMES = ["N", "track-total"] DESCRIPTION = "Total track number" def _get_output_for(self, audio_file): n = audio_file.tag.track_num[1] return str(n or "") class DiscTagPattern(TagPattern): NAMES = ["d", "disc", "disc-num"] DESCRIPTION = "Disc number" def _get_output_for(self, audio_file): n = audio_file.tag.disc_num[0] return str(n or "") class DiscTotalTagPattern(TagPattern): NAMES = ["D", "disc-total"] DESCRIPTION = "Total disc number" def _get_output_for(self, audio_file): n = audio_file.tag.disc_num[1] return str(n or "") class GenreTagPattern(TagPattern): NAMES = ["G", "genre"] DESCRIPTION = "Genre" def _get_output_for(self, audio_file): return audio_file.tag.genre.name class GenreIdTagPattern(TagPattern): NAMES = ["genre-id"] DESCRIPTION = "Genre ID" def _get_output_for(self, audio_file): return str(audio_file.tag.genre.id) if audio_file.tag.genre else None class YearTagPattern(TagPattern): NAMES = ["Y", "year"] DESCRIPTION = "Release year" def _get_output_for(self, audio_file): return audio_file.tag.release_date.year class DescriptableTagPattern(TagPattern): __metaclass__ = abc.ABCMeta PARAMETERS = [ComplexPattern.ExpectedParameter("description", default=None), ComplexPattern.ExpectedParameter("language", default=None)] def _get_matching_elements(self, elements, audio_file): matching_elements = [] for element in elements: if (self.__matches("description", element.description, audio_file) and self.__matches("language", element.lang, audio_file)): matching_elements.append(element) return matching_elements def __matches(self, parameter_name, comment_attribute_value, audio_file): if not self.parameters[parameter_name].provided: return True if self.parameters[parameter_name].value is None: return (comment_attribute_value is None or comment_attribute_value == "") return (self._parameter_value(parameter_name, audio_file) == comment_attribute_value) class CommentTagPattern(DescriptableTagPattern): NAMES = ["c", "comment"] PARAMETERS = DescriptableTagPattern.PARAMETERS DESCRIPTION = "First comment that matches description and language." def _get_output_for(self, audio_file): matching_comments = self._get_matching_elements(audio_file.tag.comments, audio_file) return matching_comments[0].text if len(matching_comments) > 0 else None class AllCommentsTagPattern(DescriptableTagPattern, PlaceholderUsagePattern): NAMES = ["comments"] PARAMETERS = DescriptableTagPattern.PARAMETERS + \ [ComplexPattern.ExpectedParameter("output", default="Comment: [Description: #d] [Lang: #l]: #t"), ComplexPattern.ExpectedParameter("separation", default="\\n")] DESCRIPTION = "All comments that are matching description and language " \ "(with output placeholders #d as description, #l as " \ " language & #t as text)." def _get_output_for(self, audio_file): output_pattern = self._parameter_value("output", audio_file) separation = self._parameter_value("separation", audio_file) outputs = [] for comment in self._get_matching_elements(audio_file.tag.comments, audio_file): replacements = [["#d", comment.description], ["#l", comment.lang.decode("ascii")], ["#t", comment.text]] outputs.append(self._replace_placeholders(output_pattern, replacements)) return separation.join(outputs) class AbstractDateTagPattern(TagPattern): __metaclass__ = abc.ABCMeta def _get_output_for(self, audio_file): return str(self._get_date(audio_file) or "") @abc.abstractmethod def _get_date(self, audio_file): pass class ReleaseDateTagPattern(AbstractDateTagPattern): NAMES = ["release-date"] DESCRIPTION = "Relase date" def _get_date(self, audio_file): return audio_file.tag.release_date class OriginalReleaseDateTagPattern(AbstractDateTagPattern): NAMES = ["original-release-date"] DESCRIPTION = "Original Relase date" def _get_date(self, audio_file): return audio_file.tag.original_release_date class RecordingDateTagPattern(AbstractDateTagPattern): NAMES = ["recording-date"] DESCRIPTION = "Recording date" def _get_date(self, audio_file): return audio_file.tag.recording_date class EncodingDateTagPattern(AbstractDateTagPattern): NAMES = ["encoding-date"] DESCRIPTION = "Encoding date" def _get_date(self, audio_file): return audio_file.tag.encoding_date class TaggingDateTagPattern(AbstractDateTagPattern): NAMES = ["tagging-date"] DESCRIPTION = "Tagging date" def _get_date(self, audio_file): return audio_file.tag.tagging_date class PlayCountTagPattern(TagPattern): NAMES = ["play-count"] DESCRIPTION = "Play count" def _get_output_for(self, audio_file): return audio_file.tag.play_count class PopularitiesTagPattern(TagPattern, PlaceholderUsagePattern): NAMES = ["popm", "popularities"] PARAMETERS = [ComplexPattern.ExpectedParameter("output", default="Popularity: [email: #e] [rating: #r] [play count: #c]"), ComplexPattern.ExpectedParameter("separation", default="\\n")] DESCRIPTION = "Popularities (with output placeholders #e as email, "\ "#r as rating & #c as count)" def _get_output_for(self, audio_file): output_pattern = self._parameter_value("output", audio_file) separation = self._parameter_value("separation", audio_file) outputs = [] for popularity in audio_file.tag.popularities: replacements = [["#e", popularity.email], ["#r", popularity.rating], ["#c", popularity.count]] outputs.append(self._replace_placeholders(output_pattern, replacements)) return separation.join(outputs) class BPMTagPattern(TagPattern): NAMES = ["bpm"] DESCRIPTION = "BPM" def _get_output_for(self, audio_file): return audio_file.tag.bpm class PublisherTagPattern(TagPattern): NAMES = ["publisher"] DESCRIPTION = "Publisher" def _get_output_for(self, audio_file): return audio_file.tag.publisher class UniqueFileIDTagPattern(TagPattern, PlaceholderUsagePattern): NAMES = ["ufids", "unique-file-ids"] PARAMETERS = [ComplexPattern.ExpectedParameter("output", default="Unique File ID: [#o] : #i"), ComplexPattern.ExpectedParameter("separation", default="\\n")] DESCRIPTION = "Unique File IDs (with output placeholders #o as owner & #i "\ " as unique id)" def _get_output_for(self, audio_file): output_pattern = self._parameter_value("output", audio_file) separation = self._parameter_value("separation", audio_file) outputs = [] for ufid in audio_file.tag.unique_file_ids: replacements = [["#o", ufid.owner_id], ["#i", ufid.uniq_id.encode("string_escape")]] outputs.append(self._replace_placeholders(output_pattern, replacements)) return separation.join(outputs) class LyricsTagPattern(DescriptableTagPattern, PlaceholderUsagePattern): NAMES = ["lyrics"] PARAMETERS = DescriptableTagPattern.PARAMETERS + \ [ComplexPattern.ExpectedParameter( "output", default="Lyrics: [Description: #d] [Lang: #l]: #t"), ComplexPattern.ExpectedParameter("separation", default="\\n")] DESCRIPTION = "All lyrics that are matching description and language " + \ "(with output placeholders #d as description, #l as "\ "language & #t as text)." def _get_output_for(self, audio_file): output_pattern = self._parameter_value("output", audio_file) separation = self._parameter_value("separation", audio_file) outputs = [] for l in self._get_matching_elements(audio_file.tag.lyrics, audio_file): replacements = [["#d", l.description], ["#l", l.lang.decode("ascii")], ["#t", l.text]] outputs.append(self._replace_placeholders(output_pattern, replacements)) return separation.join(outputs) class TextsTagPattern(TagPattern, PlaceholderUsagePattern): NAMES = ["txxx", "texts"] PARAMETERS = [ ComplexPattern.ExpectedParameter( "output", default="UserTextFrame: [Description: #d] #t"), ComplexPattern.ExpectedParameter("separation", default="\\n")] DESCRIPTION = "User text frames (with output placeholders #d as "\ "description & #t as text)" def _get_output_for(self, audio_file): output_pattern = self._parameter_value("output", audio_file) separation = self._parameter_value("separation", audio_file) outputs = [] for frame in audio_file.tag.user_text_frames: replacements = [["#d", frame.description], ["#t", frame.text]] outputs.append(self._replace_placeholders(output_pattern, replacements)) return separation.join(outputs) class ArtistURLTagPattern(TagPattern): NAMES = ["artist-url"] DESCRIPTION = "Artist URL" def _get_output_for(self, audio_file): return audio_file.tag.artist_url class AudioSourceURLTagPattern(TagPattern): NAMES = ["audio-source-url"] DESCRIPTION = "Audio source URL" def _get_output_for(self, audio_file): return audio_file.tag.audio_source_url class AudioFileURLTagPattern(TagPattern): NAMES = ["audio-file-url"] DESCRIPTION = "Audio file URL" def _get_output_for(self, audio_file): return audio_file.tag.audio_file_url class InternetRadioURLTagPattern(TagPattern): NAMES = ["internet-radio-url"] DESCRIPTION = "Internet radio URL" def _get_output_for(self, audio_file): return audio_file.tag.internet_radio_url class CommercialURLTagPattern(TagPattern): NAMES = ["commercial-url"] DESCRIPTION = "Comercial URL" def _get_output_for(self, audio_file): return audio_file.tag.copyright_url class PaymentURLTagPattern(TagPattern): NAMES = ["payment-url"] DESCRIPTION = "Payment URL" def _get_output_for(self, audio_file): return audio_file.tag.payment_url class PublisherURLTagPattern(TagPattern): NAMES = ["publisher-url"] DESCRIPTION = "Publisher URL" def _get_output_for(self, audio_file): return audio_file.tag.publisher_url class CopyrightTagPattern(TagPattern): NAMES = ["copyright-url"] DESCRIPTION = "Copyright URL" def _get_output_for(self, audio_file): return audio_file.tag.copyright_url class UserURLsTagPattern(TagPattern, PlaceholderUsagePattern): NAMES = ["user-urls"] PARAMETERS = [ComplexPattern.ExpectedParameter("output", default="#i [Description: #d]: #u"), ComplexPattern.ExpectedParameter("separation", default="\\n")] DESCRIPTION = "User URL frames (with output placeholders #i as frame id, "\ "#d as description & #u as url)" def _get_output_for(self, audio_file): output_pattern = self._parameter_value("output", audio_file) separation = self._parameter_value("separation", audio_file) outputs = [] for frame in audio_file.tag.user_url_frames: replacements = [["#i", frame.id], ["#d", frame.description], ["#u", frame.url]] outputs.append(self._replace_placeholders(output_pattern, replacements)) return separation.join(outputs) class ImagesTagPattern(TagPattern, PlaceholderUsagePattern): NAMES = ["images", "apic"] PARAMETERS = [ComplexPattern.ExpectedParameter( "output", default="#t Image: [Type: #m] [Size: #b bytes] #d"), ComplexPattern.ExpectedParameter("separation", default="\\n")] DESCRIPTION = "Attached pictures (APIC)" \ "(with output placeholders #t as image type, "\ "#m as mime type, #s as size in bytes & #d as description)" def _get_output_for(self, audio_file): output_pattern = self._parameter_value("output", audio_file) separation = self._parameter_value("separation", audio_file) outputs = [] for img in audio_file.tag.images: if img.mime_type not in id3.frames.ImageFrame.URL_MIME_TYPE_VALUES: replacements = [["#t", img.picTypeToString(img.picture_type)], ["#m", img.mime_type.decode("ascii")], ["#s", len(img.image_data)], ["#d", img.description]] outputs.append(self._replace_placeholders(output_pattern, replacements)) return separation.join(outputs) class ImageURLsTagPattern(TagPattern, PlaceholderUsagePattern): NAMES = ["image-urls"] PARAMETERS = [ComplexPattern.ExpectedParameter( "output", default="#t Image: [Type: #m] [URL: #u] #d"), ComplexPattern.ExpectedParameter("separation", default="\\n")] DESCRIPTION = "Attached pictures URLs" \ "(with output placeholders #t as image type, "\ "#m as mime type, #u as URL & #d as description)" def _get_output_for(self, audio_file): output_pattern = self._parameter_value("output", audio_file) separation = self._parameter_value("separation", audio_file) outputs = [] for img in audio_file.tag.images: if img.mime_type in id3.frames.ImageFrame.URL_MIME_TYPE_VALUES: replacements = [["#t", img.picTypeToString(img.picture_type)], ["#m", img.mime_type], ["#u", img.image_url], ["#d", img.description]] outputs.append(self._replace_placeholders(output_pattern, replacements)) return separation.join(outputs) class ObjectsTagPattern(TagPattern, PlaceholderUsagePattern): NAMES = ["objects", "gobj"] PARAMETERS = [ComplexPattern.ExpectedParameter("output", default="GEOB: [Size: #s bytes] [Type: #t] " "Description: #d | Filename: #f"), ComplexPattern.ExpectedParameter("separation", default="\\n")] DESCRIPTION = "Objects (GOBJ)" \ "(with output placeholders #s as size, #m as mime type, "\ "#d as description and #f as file name)" def _get_output_for(self, audio_file): output_pattern = self._parameter_value("output", audio_file) separation = self._parameter_value("separation", audio_file) outputs = [] for obj in audio_file.tag.objects: replacements = [["#s", len(obj.object_data)], ["#m", obj.mime_type], ["#d", obj.description], ["#f", obj.filename]] outputs.append(self._replace_placeholders(output_pattern, replacements)) return separation.join(outputs) class PrivatesTagPattern(TagPattern, PlaceholderUsagePattern): NAMES = ["privates", "priv"] PARAMETERS = [ComplexPattern.ExpectedParameter("output", default="PRIV-Content: #b bytes | Owner: #o"), ComplexPattern.ExpectedParameter("separation", default="\\n")] DESCRIPTION = "Privates (APIC) (with output placeholders #c as content, "\ "#b as number of bytes & #o as owner)" def _get_output_for(self, audio_file): output_pattern = self._parameter_value("output", audio_file) separation = self._parameter_value("separation", audio_file) outputs = [] for private in audio_file.tag.privates: replacements = [["#b", "%i" % len(private.data)], ["#c", private.data.decode("ascii")], ["#o", private.owner_id.decode("ascii")]] outputs.append(self._replace_placeholders(output_pattern, replacements)) return separation.join(outputs) class MusicCDIdTagPattern(TagPattern): NAMES = ["music-cd-id", "mcdi"] DESCRIPTION = "Music CD Identification" def _get_output_for(self, audio_file): if audio_file.tag.cd_id is not None: return audio_file.tag.cd_id.decode("ascii") else: return None class TermsOfUseTagPattern(TagPattern): NAMES = ["terms-of-use"] DESCRIPTION = "Terms of use" def _get_output_for(self, audio_file): return audio_file.tag.terms_of_use class FunctionPattern(ComplexPattern): __metaclass__ = abc.ABCMeta TYPE = "function" class FunctionFormatPattern(FunctionPattern): NAMES = ["format"] PARAMETERS = [ComplexPattern.ExpectedParameter("text"), ComplexPattern.ExpectedParameter("bold", default=None), ComplexPattern.ExpectedParameter("color", default=None)] DESCRIPTION = "Formats text bold and colored (grey, red, green, yellow, "\ "blue, magenta, cyan or white)" def _get_output_for(self, audio_file): text = self._parameter_value("text", audio_file) bold = self._parameter_bool("bold", audio_file) color_name = self._parameter_value("color", audio_file) return console.formatText(text, b=bold, c=self.__color(color_name)) def __color(self, color_name): return {"GREY": console.Fore.GREY, "RED": console.Fore.RED, "GREEN": console.Fore.GREEN, "YELLOW": console.Fore.YELLOW, "BLUE": console.Fore.BLUE, "MAGENTA": console.Fore.MAGENTA, "CYAN": console.Fore.CYAN, "WHITE": console.Fore.WHITE}.get(color_name.upper(), None) class FunctionNumberPattern(FunctionPattern): NAMES = ["num", "number-format"] PARAMETERS = [ComplexPattern.ExpectedParameter("number"), ComplexPattern.ExpectedParameter("digits")] DESCRIPTION = "Appends leading zeros" def _get_output_for(self, audio_file): number = self._parameter_value("number", audio_file) digits = self._parameter_value("digits", audio_file) try: number = int(number) except ValueError: raise DisplayException(self._error_message("'" + number + "' is not a number.")) try: digits = int(digits) except ValueError: raise DisplayException(self._error_message("'" + digits + "' is not a number.")) output = str(number) return ("0" * max(0, digits - len(output))) + output class FunctionFilenamePattern(FunctionPattern): NAMES = ["filename", "fn"] PARAMETERS = [ComplexPattern.ExpectedParameter("basename", default=None)] DESCRIPTION = "File name" def _get_output_for(self, audio_file): if self._parameter_bool("basename", audio_file): return os.path.basename(audio_file.path) return audio_file.path class FunctionFilesizePattern(FunctionPattern): NAMES = ["filesize"] DESCRIPTION = "Size of file" def _get_output_for(self, audio_file): from stat import ST_SIZE file_size = os.stat(audio_file.path)[ST_SIZE] return formatSize(file_size) class FunctionTagVersionPattern(FunctionPattern): NAMES = ["tag-version"] DESCRIPTION = "Tag version" def _get_output_for(self, audio_file): return id3.versionToString(audio_file.tag.version) class FunctionLengthPattern(FunctionPattern): NAMES = ["length"] DESCRIPTION = "Length of aufio file" def _get_output_for(self, audio_file): return formatTime(audio_file.info.time_secs) class FunctionMPEGVersionPattern(FunctionPattern, PlaceholderUsagePattern): NAMES = ["mpeg-version"] PARAMETERS = [ComplexPattern.ExpectedParameter("output", default="MPEG#v\, Layer #l")] DESCRIPTION = "MPEG version (with output placeholders #v as version & "\ "#l as layer)" def _get_output_for(self, audio_file): output = self._parameter_value("output", audio_file) replacements = [["#v", str(audio_file.info.mp3_header.version)], ["#l", "I" * audio_file.info.mp3_header.layer]] return self._replace_placeholders(output, replacements) class FunctionBitRatePattern(FunctionPattern): NAMES = ["bit-rate"] DESCRIPTION = "Bit rate of aufio file" def _get_output_for(self, audio_file): return audio_file.info.bit_rate_str class FunctionSampleFrequencePattern(FunctionPattern): NAMES = ["sample-freq"] DESCRIPTION = "Sample frequence of aufio file in Hz" def _get_output_for(self, audio_file): return str(audio_file.info.mp3_header.sample_freq) class FunctionAudioModePattern(FunctionPattern): NAMES = ["audio-mode"] DESCRIPTION = "Mode of aufio file: mono/stereo" def _get_output_for(self, audio_file): return audio_file.info.mp3_header.mode class FunctionNotEmptyPattern(FunctionPattern, PlaceholderUsagePattern): NAMES = ["not-empty"] PARAMETERS = [ComplexPattern.ExpectedParameter("text"), ComplexPattern.ExpectedParameter("output", default="#t"), ComplexPattern.ExpectedParameter("empty", default=None)] DESCRIPTION = "If condition is not empty (with output placeholder #t as "\ "text)" def _get_output_for(self, audio_file): text = self._parameter_value("text", audio_file) if len(text) > 0: output = self._parameter_value("output", audio_file) return self._replace_placeholders(output, [["#t", text]]) return output.replace("#t", text) else: return self._parameter_value("empty", audio_file) class FunctionRepeatPattern(FunctionPattern): NAMES = ["repeat"] PARAMETERS = [ComplexPattern.ExpectedParameter("text"), ComplexPattern.ExpectedParameter("count")] DESCRIPTION = "Repeats text" def _get_output_for(self, audio_file): output = u"" content = self._parameter_value("text", audio_file) count = self._parameter_value("count", audio_file) try: count = int(count) except ValueError: raise DisplayException(self._error_message("'" + count + "' is not a number.")) for i in range(count): output += content return output class DisplayPlugin(LoaderPlugin): NAMES = ["display"] SUMMARY = u"Tag Display" DESCRIPTION = ""u""" Prints specific tag information. """ def __init__(self, arg_parser): super(DisplayPlugin, self).__init__(arg_parser) def filename(fn): if not os.path.exists(fn): raise ArgumentTypeError("The file %s does not exist!" % fn) return fn pattern_group = \ self.arg_group.add_mutually_exclusive_group(required=True) pattern_group.add_argument("--pattern-help", action="store_true", dest="pattern_help", help=ARGS_HELP["--pattern-help"]) pattern_group.add_argument("-p", "--pattern", dest="pattern_string", metavar="STRING", help=ARGS_HELP["--pattern"]) pattern_group.add_argument("-f", "--pattern-file", dest="pattern_file", metavar="FILE", type=filename, help=ARGS_HELP["--pattern-file"]) self.arg_group.add_argument("--no-newline", action="store_true", dest="no_newline", help=ARGS_HELP["--no-newline"]) self.__pattern = None self.__return_code = 0 def start(self, args, config): super(DisplayPlugin, self).start(args, config) if args.pattern_help: self.__print_pattern_help() return if not _have_grako: console.printError(u"Unknown module 'grako'" + os.linesep + u"Please install grako! " + u"E.g. $ pip install grako") self.__return_code = 2 return if args.pattern_string is not None: self.__pattern = Pattern(args.pattern_string) if args.pattern_file is not None: pfile = open(args.pattern_file, "r") self.__pattern = Pattern(''.join(pfile.read().splitlines())) pfile.close() self.__output_ending = "" if args.no_newline else os.linesep def handleFile(self, f, *args, **kwargs): if self.args.pattern_help: return if self.__return_code != 0: return super(DisplayPlugin, self).handleFile(f) if not self.audio_file: return try: print(self.__pattern.output_for(self.audio_file), end=self.__output_ending) except PatternCompileException as e: self.__return_code = 1 console.printError(e.message) except DisplayException as e: self.__return_code = 1 console.printError(e.message) def handleDone(self): return self.__return_code def __print_pattern_help(self): # FIXME: Force some order print(console.formatText("ID3 Tags:", b=True)) self.__print_complex_pattern_help(TagPattern) print(os.linesep) print(console.formatText("Functions:", b=True)) self.__print_complex_pattern_help(FunctionPattern) print(os.linesep) print(console.formatText("Special characters:", b=True)) print(console.formatText("\tescape seq. character")) for i in range(len(TextPattern.SPECIAL_CHARACTERS)): print(("\t\\%s" + (" " * 12) + "%s") % (TextPattern.SPECIAL_CHARACTERS[i], TextPattern.SPECIAL_CHARACTERS_DESCRIPTIONS[i])) def __print_complex_pattern_help(self, base_class): rows = [] # TODO line wrap for description for pattern_class in Pattern.sub_pattern_classes(base_class): rows.append([", ".join(pattern_class.NAMES), pattern_class.DESCRIPTION]) parameters = Pattern.pattern_class_parameters(pattern_class) if len(parameters) > 0: rows.append(["", "Parameter" + ("s:" if len(parameters) > 1 else ":")]) for parameter in parameters: parameter_desc = parameter.name if not parameter.requried: default = ", default='" + parameter.default + \ "'" if parameter.default else "" parameter_desc += " (optional" + default + ")" rows.append(["", " " + parameter_desc]) self.__print_rows(rows, "\t", " ") def __print_rows(self, rows, indent, spacing): row_widths = [] for row in rows: for n in range(len(row)): width = len(row[n]) if len(row_widths) <= n: row_widths.append(width) else: row_widths[n] = max(row_widths[n], width) for row in rows: out = indent for n in range(len(row)): out += row[n] if n < len(row) - 1: out += (" " * (row_widths[n] - len(row[n]))) + spacing print(out) class DisplayException(Exception): def __init__(self, message): self.__message = message def __get_message(self): return self.__message message = property(__get_message) class PatternCompileException(Exception): def __init__(self, message): self.__message = message def __get_message(self): return self.__message message = property(__get_message) ARGS_HELP = { "--pattern-help": "Detailed pattern help", "--pattern": "Pattern string", "--pattern-file": "Pattern file", "--no-newline": "Print no newline after each output" } eyeD3-0.8.4/src/eyed3/plugins/pymod.py0000644000175000017500000000645513061344514020334 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2014 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 . # ################################################################################ from __future__ import print_function from eyed3.plugins import LoaderPlugin from eyed3.compat import importmod _DEFAULT_MOD = "eyeD3mod.py" class PyModulePlugin(LoaderPlugin): SUMMARY = u"Imports a Python module file and calls its functions for the "\ "the various plugin events." DESCRIPTION = u""" If no module if provided (see -m/--module) a file named %(_DEFAULT_MOD)s 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 """ % globals() 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. " "The default is ./%s" % _DEFAULT_MOD) def start(self, args, config): mod_file = args.module or _DEFAULT_MOD try: self._mod = importmod(mod_file) except IOError as ex: raise IOError("Module file not found: %s" % mod_file) except (NameError, IndentationError, 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.8.4/src/eyed3/plugins/stats.py0000644000175000017500000003732013177420270020337 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2009 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 . # ################################################################################ from __future__ import print_function import os import sys import operator from collections import Counter from eyed3 import id3, mp3 from eyed3.core import AUDIO_MP3 from eyed3.utils 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(object): def test(self): 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 = list(key_map.keys()) 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 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): assert(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.GREY + "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.GREY + "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.GREY + "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.GREY + "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.GREY + "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.GREY + "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"): 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.GREY + "APIC image types:" + Style.RESET_ALL) super(Id3ImageTypeCounter, self)._report() class StatisticsPlugin(LoaderPlugin): NAMES = ['stats'] SUMMARY = u"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("\t%s%s%s (%s)" % (Fore.RED, str(score).center(3), Fore.RESET, text)) def prettyScore(): score = float(self._score_sum) / float(self._score_count) if score > 80: color = Fore.GREEN elif score > 70: color = Fore.YELLOW else: color = Fore.RED return (score, color) score, color = prettyScore() print("%sScore%s = %s%d%%%s" % (Style.BRIGHT, 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.8.4/src/eyed3/plugins/__init__.py0000644000175000017500000001700113177420270020732 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2012 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 . # ################################################################################ from __future__ import print_function import os import sys from eyed3 import core, utils from eyed3.utils import guessMimetype from eyed3.utils.console import printMsg, printError from eyed3.utils.log import getLogger _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("Extra plugin paths: %s" % paths) for d in [os.path.dirname(__file__)] + (paths if paths else []): log.debug("Searching '%s' for plugins", d) 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.warning("Plugin '%s' requires packages that are not " "installed: %s" % ((f, d), ex)) continue except Exception as ex: log.exception("Bad plugin '%s'", (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("loading plugin '%s' from '%s%s%s'", mod, 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("Plugins loaded: %s", _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""" SUMMARY = u"eyeD3 plugin" """One line about the plugin""" DESCRIPTION = u"" """Detailed info about the plugin""" NAMES = [] """A list of **at least** one name for invoking the plugin, values [1:] are treated as alias""" def __init__(self, arg_parser): self.arg_parser = arg_parser self.arg_group = arg_parser.add_argument_group( "Plugin options", u"%s\n%s" % (self.SUMMARY, 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 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(LoaderPlugin, self).__init__(arg_parser) self._num_loaded = 0 self._file_cache = [] if cache_files else None self._dir_images = [] if track_images else 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`. """ self.audio_file = None 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("Nothing to do") eyeD3-0.8.4/src/eyed3/plugins/fixup.py0000644000175000017500000006637413061344514020345 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2013-2014 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 . # ################################################################################ from __future__ import print_function import 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.compat import UnicodeType from eyed3.utils import art from eyed3.utils.prompt import prompt from eyed3.utils.console import printMsg, Style, Fore from eyed3 import core, compat from eyed3.core import (ALBUM_TYPE_IDS, TXXX_ALBUM_TYPE, LP_TYPE, EP_TYPE, COMP_TYPE, VARIOUS_TYPE, DEMO_TYPE, LIVE_TYPE, SINGLE_TYPE, VARIOUS_ARTISTS) EP_MAX_HINT = 9 LP_MAX_HINT = 19 NORMAL_FNAME_FORMAT = u"${artist} - ${track:num} - ${title}" VARIOUS_FNAME_FORMAT = u"${track:num} - ${artist} - ${title}" SINGLE_FNAME_FORMAT = u"${artist} - ${title}" NORMAL_DNAME_FORMAT = u"${best_date:prefer_release} - ${album}" LIVE_DNAME_FORMAT = u"${best_date:prefer_recording} - ${album}" def _printChecking(msg, end='\n'): print(Style.BRIGHT + Fore.GREEN + u"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 u" ".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 = \ u"Performs various checks and fixes to directories of audio files." DESCRIPTION = u""" 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("-t", "--type", choices=ALBUM_TYPE_IDS, dest="dir_type", default=None, type=UnicodeType, 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"]) self._curr_dir_type = None self._dir_files_to_remove = set() def _getOne(self, key, values, default=None, Type=UnicodeType, required=True): values = set(values) if None in values: values.remove(None) if len(values) != 1: printMsg( u"Detected %s %s names%s" % ("0" if len(values) == 0 else "multiple", key, "." if not values else (":\n\t%s" % "\n\t".join([compat.unicode(v) for v in values])), )) value = prompt(u"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 = set([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 = set([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) rel_dates = set([release_date]) 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 = set([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 = set([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("\t%s" % 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 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.GREY + "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_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_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 + u"Album artist: " + Style.RESET_ALL + (album_artist or u"")) print(Fore.BLUE + "Artist" + ("s" if len(artists) > 1 else "") + ": " + Style.RESET_ALL + u", ".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(Fore.BLUE + ("%s date: " % what) + Style.RESET_ALL + str(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 + u"Checking" + Fore.RESET + Fore.GREY + (" %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(u"\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(u"\tSetting artist: %s" % artists[0]) tag.artist = artists[0] edited_files.add(f) if tag.album != album and dir_type != SINGLE_TYPE: print(u"\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(u"\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") real_tlen = f.info.time_secs * 1000 if tlen is None or int(tlen) != real_tlen: print("\tSetting TLEN (%d)" % real_tlen) tag.setTextFrame("TLEN", UnicodeType(real_tlen)) 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(u"Rename file to %s%s" % (new_name, orig_ext)) file_renames.append((f, new_name, orig_ext)) # Directory renaming dir_rename = None if 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(u"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(u"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(), } eyeD3-0.8.4/src/eyed3/plugins/_display_parser.py0000644000175000017500000001370713061344514022362 0ustar travistravis00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # CAVEAT UTILITOR # # This file was automatically generated by Grako. # # https://pypi.python.org/pypi/grako/ # # Any changes you make to it will be overwritten the next time # the file is generated. from __future__ import (print_function, division, absolute_import, unicode_literals) from grako.parsing import graken, Parser from grako.util import re, RE_FLAGS # noqa __version__ = (2016, 2, 17, 20, 35, 22, 2) __all__ = [ 'DisplayPatternParser', 'DisplayPatternSemantics', 'main' ] class DisplayPatternParser(Parser): def __init__(self, whitespace=None, nameguard=None, comments_re=None, eol_comments_re=None, ignorecase=None, left_recursion=True, **kwargs): super(DisplayPatternParser, self).__init__( whitespace=whitespace, nameguard=nameguard, comments_re=comments_re, eol_comments_re=eol_comments_re, ignorecase=ignorecase, left_recursion=left_recursion, **kwargs ) @graken() def _start_(self): self._pattern_() self._check_eof() @graken() def _pattern_(self): def block0(): with self._choice(): with self._option(): self._text_() with self._option(): self._tag_() with self._option(): self._function_() self._error('no available options') self._closure(block0) @graken() def _tag_(self): with self._group(): self._token('%') self._string_() self.ast['name'] = self.last_node def block2(): self._token(',') with self._group(): self._parameter_() self.ast.setlist('parameters', self.last_node) self._closure(block2) self._token('%') self.ast['tag'] = self.last_node self.ast._define( ['tag', 'name'], ['parameters'] ) @graken() def _function_(self): with self._group(): self._token('$') self._string_() self.ast['name'] = self.last_node self._token('(') with self._optional(): with self._group(): self._parameter_() self.ast.setlist('parameters', self.last_node) def block3(): self._token(',') with self._group(): self._parameter_() self.ast.setlist('parameters', self.last_node) self._closure(block3) self._token(')') self.ast['function'] = self.last_node self.ast._define( ['function', 'name'], ['parameters'] ) @graken() def _parameter_(self): with self._optional(): def block0(): self._token(' ') self._closure(block0) self._string_() self.ast['name'] = self.last_node self._token('=') with self._optional(): self._pattern_() self.ast['value'] = self.last_node self.ast._define( ['name', 'value'], [] ) @graken() def _text_(self): self._pattern(r'(\\\\|\\%|\\\$|\\,|\\\(|\\\)|\\=|\\n|\\t|[^\\%$,()])+') self.ast['text'] = self.last_node self.ast._define( ['text'], [] ) @graken() def _string_(self): self._pattern(r'([^\\%$,()=])+') class DisplayPatternSemantics(object): def start(self, ast): return ast def pattern(self, ast): return ast def tag(self, ast): return ast def function(self, ast): return ast def parameter(self, ast): return ast def text(self, ast): return ast def string(self, ast): return ast def main(filename, startrule, trace=False, whitespace=None, nameguard=None): import json with open(filename) as f: text = f.read() parser = DisplayPatternParser(parseinfo=False) ast = parser.parse( text, startrule, filename=filename, trace=trace, whitespace=whitespace, nameguard=nameguard) print('AST:') print(ast) print() print('JSON:') print(json.dumps(ast, indent=2)) print() if __name__ == '__main__': import argparse import string import sys class ListRules(argparse.Action): def __call__(self, parser, namespace, values, option_string): print('Rules:') for r in DisplayPatternParser.rule_list(): print(r) print() sys.exit(0) parser = argparse.ArgumentParser( description="Simple parser for DisplayPattern.") parser.add_argument('-l', '--list', action=ListRules, nargs=0, help="list all rules and exit") parser.add_argument('-n', '--no-nameguard', action='store_true', dest='no_nameguard', help="disable the 'nameguard' feature") parser.add_argument('-t', '--trace', action='store_true', help="output trace information") parser.add_argument('-w', '--whitespace', type=str, default=string.whitespace, help="whitespace specification") parser.add_argument('file', metavar="FILE", help="the input file to parse") parser.add_argument('startrule', metavar="STARTRULE", help="the start rule for parsing") args = parser.parse_args() main( args.file, args.startrule, trace=args.trace, whitespace=args.whitespace, nameguard=not args.no_nameguard ) eyeD3-0.8.4/src/eyed3/plugins/DisplayPattern.ebnf0000644000175000017500000000272113061344514022421 0ustar travistravis00000000000000(* ################################################################################ # 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 -o src/eyed3/plugins/display_parser.py src/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.8.4/src/eyed3/plugins/lameinfo.py0000644000175000017500000001131013153052736020764 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2009 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 os import math from eyed3.utils import formatSize from eyed3.utils.console import printMsg, boldText, Fore, HEADER_COLOR from eyed3.plugins import LoaderPlugin class LameInfoPlugin(LoaderPlugin): NAMES = ["lameinfo", "xing"] SUMMARY = u"Outputs lame header (if one exists) for file." DESCRIPTION = ( u"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, filePath): from stat import ST_SIZE fileSize = os.stat(filePath)[ST_SIZE] size_str = formatSize(fileSize) print("\n%s\t%s[ %s ]%s" % (boldText(os.path.basename(filePath), HEADER_COLOR()), HEADER_COLOR(), size_str, Fore.RESET)) print("-" * 79) def handleFile(self, f): super(LameInfoPlugin, self).handleFile(f) self.printHeader(f) if (self.audio_file is None or self.audio_file.info is None or not self.audio_file.info.lame_tag): printMsg('No LAME Tag') return format = '%-20s: %s' lt = self.audio_file.info.lame_tag if "infotag_crc" not in lt: try: printMsg('%s: %s' % ('Encoder Version', lt['encoder_version'])) except KeyError: pass return values = [] values.append(('Encoder Version', lt['encoder_version'])) values.append(('LAME Tag Revision', lt['tag_revision'])) values.append(('VBR Method', lt['vbr_method'])) values.append(('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(format % (v)) eyeD3-0.8.4/src/eyed3/mp3/0000755000175000017500000000000013203726215015636 5ustar travistravis00000000000000eyeD3-0.8.4/src/eyed3/mp3/__init__.py0000644000175000017500000001716613177420270017764 0ustar travistravis00000000000000################################################################################ # Copyright (C) 2002-2007 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 os import re from .. import Error from .. import id3 from .. import core, utils from ..utils.log import getLogger log = getLogger(__name__) class Mp3Exception(Error): """Used to signal mp3-related errors.""" pass NAME = "mpeg" 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", ] '''Mime-types that are recognized at MP3''' OTHER_MIME_TYPES = ['application/octet-stream', # ??? 'audio/x-hx-aac-adts', # ??? 'audio/x-wav', # RIFF wrapped mp3s ] '''Mime-types that have been seen to contain mp3 data.''' EXTENSIONS = [".mp3"] '''Valid file extensions.''' def isMp3File(file_name): '''Does a mime-type check on ``file_name`` and returns ``True`` it the file is mp3, and ``False`` otherwise.''' return utils.guessMimetype(file_name) in MIME_TYPES 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) core.AudioInfo.__init__(self) self.mp3_header = None self.xing_header = None self.vbri_header = None self.lame_tag = None '''If not ``None``, the Lame header. See :class:`eyed3.mp3.headers.LameHeader`''' self.bit_rate = (None, None) '''2-tuple, (vrb?:boolean, bitrate:int)''' 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 import stat self.size_bytes = os.stat(file_obj.name)[stat.ST_SIZE] # Compute track play time. tpf = None if self.xing_header and self.xing_header.vbr: tpf = timePerFrame(self.mp3_header, True) self.time_secs = int(tpf * self.xing_header.numFrames) elif self.vbri_header and self.vbri_header.version == 1: tpf = timePerFrame(self.mp3_header, True) self.time_secs = int(tpf * self.vbri_header.num_frames) else: tpf = timePerFrame(self.mp3_header, False) length = self.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 self.time_secs = int((length / self.mp3_header.frame_length) * tpf) # Compute bitate 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 ## # 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 brs = "%d kb/s" % bit_rate if vbr: brs = "~" + brs return brs class Mp3AudioFile(core.AudioFile): """Audio file container for mp3 files.""" def __init__(self, path, version=id3.ID3_ANY_VERSION): self._tag_version = version core.AudioFile.__init__(self, 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) @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.8.4/src/eyed3/mp3/headers.py0000644000175000017500000007765113177420270017645 0ustar travistravis00000000000000################################################################################ # Copyright (C) 2002-2015 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 . # ################################################################################ from math import log10 from . import Mp3Exception from ..utils.binfuncs import bytes2bin, bytes2dec, bin2dec from .. import compat from ..utils.log import getLogger 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 entireley 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 find_sync(fp, start_pos=0): CHUNK_SIZE = 8192 # Measured as optimal fp.seek(start_pos) data = fp.read(CHUNK_SIZE) while data: sync_pos = data.find(b'\xff', 0) if sync_pos >= 0: header = data[sync_pos:sync_pos + 4] if len(header) == 4: return (start_pos + sync_pos, header) data = fp.read(CHUNK_SIZE) 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 (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)) def compute_time_per_frame(mp3_header): """Deprecated, use timePerFrame instead.""" import warnings warnings.warn("Use timePerFrame instead", DeprecationWarning, stacklevel=2) 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 = (compat.byteOrd(frame[1]) >> 3) & 0x1 # channel mode. mode = (compat.byteOrd(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 ## # \brief 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 # 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] # \endcode # # 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 # 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 # \endcode # # Radio and Audiofile Replay Gain are often referrered to as Track and Album # gain, respectively. See http://replaygain.hydrogenaudio.org/ for futher # details on Replay Gain. # # See http://gabriel.mp3-tech.org/mp3infotag.html for the gory details of the # LAME Tag. class LameHeader(dict): # 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. """ self.decode(frame) def _crc16(self, data, val=0): """Compute a CRC-16 checksum on a data stream.""" for c in compat.byteiter(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: # noqa: B901 return log.debug('Lame info tag found at position %d' % pos) # check the info tag crc.Iif 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') try: # Encoder short VersionString, 9 bytes self['encoder_version'] = \ compat.unicode(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 # FIXME Lame 3.95.1 and above use 89dB as a reference instead of # 83dB as defined by the Replay Gain spec. Should this be # compensated for? # 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 ## # \brief 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 lamevercmp(x, y): x = x.ljust(5) y = y.ljust(5) if x[:5] == y[:5]: return 0 ret = compat.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 compat.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 """ key = None if version == 2.5: key = 2 else: key = int(version - 1) assert(0 <= key <= 2) return key eyeD3-0.8.4/src/eyed3/compat.py0000644000175000017500000001035013134312320016762 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2013 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 . # ################################################################################ ''' Compatibility for various versions of Python (e.g. 2.6, 2.7, and 3.3) ''' import os import sys import types PY2 = sys.version_info[0] == 2 if PY2: # Python2 StringTypes = types.StringTypes UnicodeType = unicode # noqa BytesType = str unicode = unicode # noqa _og_chr = chr from ConfigParser import SafeConfigParser as ConfigParser from ConfigParser import Error as ConfigParserError from StringIO import StringIO def chr(i): '''byte strings units are single byte strings''' return _og_chr(i) input = raw_input # noqa cmp = cmp # noqa else: # Python3 StringTypes = (str,) UnicodeType = str BytesType = bytes unicode = str from configparser import ConfigParser # noqa from configparser import Error as ConfigParserError # noqa from io import StringIO # noqa def chr(i): '''byte strings units are ints''' return intToByteString(i) input = input def cmp(a, b): return (a > b) - (a < b) if sys.version_info[0:2] < (3, 4): # py3.4 has two maps, nice nicer. Make it so for other versions. import logging logging._nameToLevel = {_k: _v for _k, _v in logging._levelNames.items() if isinstance(_k, str)} def b(x, encoder=None): if isinstance(x, BytesType): return x else: import codecs encoder = encoder or codecs.latin_1_encode return encoder(x)[0] def intToByteString(n): '''Convert the integer ``n`` to a single character byte string.''' if PY2: return chr(n) else: return bytes((n,)) def byteiter(bites): assert(isinstance(bites, BytesType)) for b in bites: yield b if PY2 else intToByteString(b) def byteOrd(bite): '''The utility handles the following difference with byte strings in Python 2 and 3: b"123"[1] == b"2" (Python2) b"123"[1] == 50 (Python3) As this function name implies, the oridinal value is returned given either a byte string of length 1 (python2) or a integer value (python3). With Python3 the value is simply return. ''' if PY2: assert(isinstance(bite, BytesType)) return ord(bite) else: assert(isinstance(bite, int)) return bite def importmod(mod_file): '''Imports a Ptyhon module referenced by absolute or relative path ``mod_file``. The module is retured.''' mod_name = os.path.splitext(os.path.basename(mod_file))[0] if PY2: import imp mod = imp.load_source(mod_name, mod_file) else: import importlib.machinery loader = importlib.machinery.SourceFileLoader(mod_name, mod_file) mod = loader.load_module() return mod class UnicodeMixin(object): '''A shim to handlke __unicode__ missing from Python3. Inspired by: http://lucumr.pocoo.org/2011/1/22/forwards-compatible-python/ ''' if PY2: def __str__(self): return unicode(self).encode('utf-8') else: def __str__(self): return self.__unicode__() eyeD3-0.8.4/src/eyed3/core.py0000644000175000017500000003111013166564566016456 0ustar travistravis00000000000000# -*- coding: utf-8 -*- """Basic core types and utilities.""" import os import sys import time import functools import pathlib from . import LOCAL_FS_ENCODING from .utils import guessMimetype from . import compat from .utils.log import getLogger log = getLogger(__name__) AUDIO_NONE = 0 """Audio type selecter for no audio.""" AUDIO_MP3 = 1 """Audio type selecter for mpeg (mp3) audio.""" AUDIO_TYPES = (AUDIO_NONE, AUDIO_MP3) LP_TYPE = u"lp" EP_TYPE = u"ep" COMP_TYPE = u"compilation" LIVE_TYPE = u"live" VARIOUS_TYPE = u"various" DEMO_TYPE = u"demo" SINGLE_TYPE = u"single" ALBUM_TYPE_IDS = [LP_TYPE, EP_TYPE, COMP_TYPE, LIVE_TYPE, VARIOUS_TYPE, DEMO_TYPE, SINGLE_TYPE] VARIOUS_ARTISTS = u"Various Artists" TXXX_ALBUM_TYPE = u"eyeD3#album_type" """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_ARTIST_ORIGIN = u"eyeD3#artist_origin" """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""" def load(path, tag_version=None): """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 mp3, id3 if not isinstance(path, pathlib.Path): if compat.PY2: path = pathlib.Path(path.encode(sys.getfilesystemencoding())) else: path = pathlib.Path(path) log.debug("Loading file: %s" % path) if path.exists(): if not path.is_file(): raise IOError("not a file: %s" % path) else: raise IOError("file not found: %s" % path) mtype = guessMimetype(path) log.debug("File mime-type: %s" % mtype) if (mtype in mp3.MIME_TYPES or (mtype in mp3.OTHER_MIME_TYPES and path.suffix.lower() in mp3.EXTENSIONS)): return mp3.Mp3AudioFile(path, tag_version) elif mtype == "application/x-id3": return id3.TagFile(path, tag_version) else: return None class AudioInfo(object): """A base container for common audio details.""" time_secs = 0 """The number of seconds of audio data (i.e., the playtime)""" size_bytes = 0 """The number of bytes of audio data.""" class Tag(object): """An abstract interface for audio tag (meta) data (e.g. artist, title, etc.) """ read_only = False def _setArtist(self, val): raise NotImplementedError def _getArtist(self): raise NotImplementedError def _getAlbumArtist(self): raise NotImplementedError def _setAlbumArtist(self, val): raise NotImplementedError def _setAlbum(self, val): raise NotImplementedError def _getAlbum(self): raise NotImplementedError def _setTitle(self, val): raise NotImplementedError def _getTitle(self): raise NotImplementedError def _setTrackNum(self, val): raise NotImplementedError def _getTrackNum(self): raise NotImplementedError @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): """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(object): """Abstract base class for audio file types (AudioInfo + Tag)""" def _read(self): """Subclasses MUST override this method and set ``self._info``, ``self._tag`` and ``self.type``. """ 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(u"File '%s' exists, will not overwrite" % new_path) elif not new_path.parent.exists(): raise IOError(u"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, t): """Set the path""" from os.path import abspath, realpath, normpath self._path = normpath(realpath(abspath(t))) @property def info(self): """Returns a concrete implemenation of :class:`eyed3.core.AudioInfo`""" return self._info @property def tag(self): """Returns a concrete implemenation 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() @functools.total_ordering class Date(object): """ 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", ] """Valid time stamp formats per ISO 8601 and used by \c 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 l, r 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)): l = l if l is not None else -1 r = r if r is not None else -1 if l < r: return True elif l > r: return False return False def __hash__(self): return hash(str(self)) @staticmethod def _validateFormat(s): pdate = 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("Invalid date string: %s" % s) assert(pdate) return pdate, fmt @staticmethod def parse(s): """Parses date strings that conform to ISO-8601.""" if not isinstance(s, compat.UnicodeType): 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 __unicode__(self): return compat.unicode(str(self), "latin1") def parseError(ex): """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) eyeD3-0.8.4/src/eyed3/__init__.py0000644000175000017500000000242413177421624017260 0ustar travistravis00000000000000# -*- coding: utf-8 -*- import sys import locale from .__about__ import __version__ as version # noqa _DEFAULT_ENCODING = "latin1" LOCAL_ENCODING = locale.getpreferredencoding(do_setlocale=True) '''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.''' if not LOCAL_ENCODING or LOCAL_ENCODING == "ANSI_X3.4-1968": # pragma: no cover LOCAL_ENCODING = _DEFAULT_ENCODING LOCAL_FS_ENCODING = sys.getfilesystemencoding() '''The local file system encoding, the default is ``latin1`` if it cannot be determined.''' 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(Error, self).__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 from .core import load # noqa del sys del locale eyeD3-0.8.4/src/eyed3/id3/0000755000175000017500000000000013203726215015616 5ustar travistravis00000000000000eyeD3-0.8.4/src/eyed3/id3/__init__.py0000644000175000017500000003450313177420270017736 0ustar travistravis00000000000000################################################################################ # Copyright (C) 2002-2014 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 re from .. import core from .. import Error from .. import compat from ..utils import requireUnicode from ..utils.log import getLogger log = getLogger(__name__) # Version constants and helpers ID3_V1 = (1, None, None) '''Version 1, 1.0 or 1.1''' ID3_V1_0 = (1, 0, 0) '''Version 1.0, specifically''' ID3_V1_1 = (1, 1, 0) '''Version 1.1, specifically''' ID3_V2 = (2, None, None) '''Version 2, 2.2, 2.3 or 2.4''' ID3_V2_2 = (2, 2, 0) '''Version 2.2, specifically''' ID3_V2_3 = (2, 3, 0) '''Version 2.3, specifically''' ID3_V2_4 = (2, 4, 0) '''Version 2.4, specifically''' ID3_DEFAULT_VERSION = ID3_V2_4 '''The default version for eyeD3 tags and save operations.''' ID3_ANY_VERSION = (ID3_V1[0] | ID3_V2[0], None, None) '''Useful for operations where any version will suffice.''' LATIN1_ENCODING = b"\x00" '''Byte code for latin1''' UTF_16_ENCODING = b"\x01" '''Byte code for UTF-16''' UTF_16BE_ENCODING = b"\x02" '''Byte code for UTF-16 (big endian)''' UTF_8_ENCODING = b"\x03" '''Byte code for UTF-8 (Not supported in ID3 versions < 2.4)''' DEFAULT_LANG = b"eng" '''Default language code for frames that contain a language portion.''' 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.''' class Genre(compat.UnicodeMixin): '''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``.''' @requireUnicode("name") def __init__(self, name=None, id=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 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("Genre ID takes precedence and remapped " "'%s' to '%s'" % (name, self.name)) except ValueError: log.warning("Invalid numeric genre ID: %d" % 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): global genres if val is None: self._id = None return val = int(val) if val not in list(genres.keys()) or not genres[val]: raise ValueError("Invalid numeric genre ID: %d" % val) name = genres[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 @requireUnicode(1) def name(self, val): global genres if val is None: self._name = None return if val.lower() in list(genres.keys()): self._id = genres[val] # normalize the name self._name = genres[self._id] else: log.warning("Non standard genre name: %s" % val) self._id = None self._name = val @staticmethod @requireUnicode(1) 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(u"0") else: return s if id3_std: # ID3 v1 style. # Match 03, 34, 129. regex = re.compile("[0-9][0-9]*$") if regex.match(g_str): return Genre(id=int(strip0Padding(g_str))) # ID3 v2 style. # Match (03), (0)Blues, (15) Rap regex = re.compile("\(([0-9][0-9]*)\)(.*)$") m = regex.match(g_str) if m: (id, name) = m.groups() id = int(strip0Padding(id)) if id and name: id = id name = name.strip() else: id = id name = None return Genre(id=id, name=name) # Let everything else slide, genres suck anyway return Genre(id=None, name=g_str) def __unicode__(self): '''When Python2 support is dropped this method must be renamed __str__ and the UnicodeMixin base class is dropped.''' s = u"" if self.id is not None: s += u"(%d)" % self.id if self.name: s += self.name return s def __eq__(self, rhs): return self.id == rhs.id and self.name == rhs.name def __ne__(self, rhs): return not self.__eq__(rhs) 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 def __init__(self, *args): '''The optional ``*args`` are passed directly to the ``dict`` constructor.''' global ID3_GENRES super(GenreMap, self).__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 __getitem__(self, key): if key and type(key) is not int: key = key.lower() return super(GenreMap, self).__getitem__(key) 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 = [ u'Blues', u'Classic Rock', u'Country', u'Dance', u'Disco', u'Funk', u'Grunge', u'Hip-Hop', u'Jazz', u'Metal', u'New Age', u'Oldies', u'Other', u'Pop', u'R&B', u'Rap', u'Reggae', u'Rock', u'Techno', u'Industrial', u'Alternative', u'Ska', u'Death Metal', u'Pranks', u'Soundtrack', u'Euro-Techno', u'Ambient', u'Trip-Hop', u'Vocal', u'Jazz+Funk', u'Fusion', u'Trance', u'Classical', u'Instrumental', u'Acid', u'House', u'Game', u'Sound Clip', u'Gospel', u'Noise', u'AlternRock', u'Bass', u'Soul', u'Punk', u'Space', u'Meditative', u'Instrumental Pop', u'Instrumental Rock', u'Ethnic', u'Gothic', u'Darkwave', u'Techno-Industrial', u'Electronic', u'Pop-Folk', u'Eurodance', u'Dream', u'Southern Rock', u'Comedy', u'Cult', u'Gangsta Rap', u'Top 40', u'Christian Rap', u'Pop / Funk', u'Jungle', u'Native American', u'Cabaret', u'New Wave', u'Psychedelic', u'Rave', u'Showtunes', u'Trailer', u'Lo-Fi', u'Tribal', u'Acid Punk', u'Acid Jazz', u'Polka', u'Retro', u'Musical', u'Rock & Roll', u'Hard Rock', u'Folk', u'Folk-Rock', u'National Folk', u'Swing', u'Fast Fusion', u'Bebob', u'Latin', u'Revival', u'Celtic', u'Bluegrass', u'Avantgarde', u'Gothic Rock', u'Progressive Rock', u'Psychedelic Rock', u'Symphonic Rock', u'Slow Rock', u'Big Band', u'Chorus', u'Easy Listening', u'Acoustic', u'Humour', u'Speech', u'Chanson', u'Opera', u'Chamber Music', u'Sonata', u'Symphony', u'Booty Bass', u'Primus', u'Porn Groove', u'Satire', u'Slow Jam', u'Club', u'Tango', u'Samba', u'Folklore', u'Ballad', u'Power Ballad', u'Rhythmic Soul', u'Freestyle', u'Duet', u'Punk Rock', u'Drum Solo', u'A Cappella', u'Euro-House', u'Dance Hall', u'Goa', u'Drum & Bass', u'Club-House', u'Hardcore', u'Terror', u'Indie', u'BritPop', u'Negerpunk', u'Polsk Punk', u'Beat', u'Christian Gangsta Rap', u'Heavy Metal', u'Black Metal', u'Crossover', u'Contemporary Christian', u'Christian Rock', u'Merengue', u'Salsa', u'Thrash Metal', u'Anime', u'JPop', u'Synthpop', # https://de.wikipedia.org/wiki/Liste_der_ID3v1-Genres u'Abstract', u'Art Rock', u'Baroque', u'Bhangra', u'Big Beat', u'Breakbeat', u'Chillout', u'Downtempo', u'Dub', u'EBM', u'Eclectic', u'Electro', u'Electroclash', u'Emo', u'Experimental', u'Garage', u'Global', u'IDM', u'Illbient', u'Industro-Goth', u'Jam Band', u'Krautrock', u'Leftfield', u'Lounge', u'Math Rock', u'New Romantic', u'Nu-Breakz', u'Post-Punk', u'Post-Rock', u'Psytrance', u'Shoegaze', u'Space Rock', u'Trop Rock', u'World Music', u'Neoclassical', u'Audiobook', u'Audio Theatre', u'Neue Deutsche Welle', u'Podcast', u'Indie Rock', u'G-Funk', u'Dubstep', u'Garage Rock', u'Psybient', ] '''ID3 genres, as defined in ID3 v1. The position in the list is the genre's numeric byte value.''' genres = GenreMap() '''A map of standard genre names and IDs per the ID3 v1 genre definition.''' from . import frames # noqa from .tag import Tag, TagException, TagTemplate, FileInfo # noqa eyeD3-0.8.4/src/eyed3/id3/apple.py0000644000175000017500000000415013061344514017271 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2012 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 . # ################################################################################ ''' 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" class PCST(Frame): '''Indicates a podcast. The 4 bytes of data is undefined, and is typically all 0.''' def __init__(self, id=PCST_FID): super(PCST, self).__init__(PCST_FID) def render(self): self.data = b"\x00" * 4 return super(PCST, self).render() class TKWD(TextFrame): '''Podcast keywords.''' def __init__(self, id=TKWD_FID): super(TKWD, self).__init__(TKWD_FID) class TDES(TextFrame): '''Podcast description. One encoding byte followed by text per encoding.''' def __init__(self, id=TDES_FID): super(TDES, self).__init__(TDES_FID) class TGID(TextFrame): '''Podcast URL of the audio file. This should be a W frame!''' def __init__(self, id=TGID_FID): super(TGID, self).__init__(TGID_FID) class WFED(TextFrame): '''Another podcast URL, the feed URL it is said.''' def __init__(self, id=WFED_FID, url=""): super(WFED, self).__init__(WFED_FID, url) eyeD3-0.8.4/src/eyed3/id3/tag.py0000644000175000017500000020516513203722311016745 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2007-2012 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 os import string import shutil import tempfile from functools import partial from codecs import ascii_encode from ..utils import requireUnicode, chunkCopy, datePicker from .. import core from ..core import TXXX_ALBUM_TYPE, TXXX_ARTIST_ORIGIN, ALBUM_TYPE_IDS from .. import Error from . import (ID3_ANY_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 .. import compat from ..compat import StringTypes, BytesType, unicode, UnicodeType, b from ..utils.log import getLogger log = getLogger(__name__) class TagException(Error): pass ID3_V1_COMMENT_DESC = u"ID3v1.x Comment" DEFAULT_PADDING = 256 class Tag(core.Tag): def __init__(self, **kwargs): self.clear() core.Tag.__init__(self, **kwargs) def clear(self): """Reset all tag data.""" # ID3 tag header self.header = TagHeader() # 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) self.file_info = None def parse(self, fileobj, version=ID3_ANY_VERSION): assert(fileobj) self.clear() version = version or ID3_ANY_VERSION close_file = False try: filename = fileobj.name except AttributeError: if type(fileobj) in StringTypes: filename = fileobj fileobj = open(filename, "rb") close_file = True else: raise ValueError("Invalid type: %s" % str(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)""" padding = 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 STRIP_CHARS = compat.b(string.whitespace) + b"\x00" title = tag_data[3:33].strip(STRIP_CHARS) log.debug("Tite: %s" % title) if title: self.title = unicode(title, v1_enc) artist = tag_data[33:63].strip(STRIP_CHARS) log.debug("Artist: %s" % artist) if artist: self.artist = unicode(artist, v1_enc) album = tag_data[63:93].strip(STRIP_CHARS) log.debug("Album: %s" % album) if album: self.album = unicode(album, v1_enc) year = tag_data[93:97].strip(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 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" and comment[-1:] != b"\x00"): log.debug("Track Num found, setting version to v1.1") self.version = ID3_V1_1 track = compat.byteOrd(comment[-1]) self.track_num = (track, None) log.debug("Track: " + str(track)) comment = comment[:-2].strip(STRIP_CHARS) # There may only have been a track # if comment: log.debug("Comment: %s" % comment) self.comments.set(unicode(comment, v1_enc), ID3_V1_COMMENT_DESC) genre = ord(tag_data[127:128]) log.debug("Genre ID: %d" % 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, txt): fid = b(fid, 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) def getTextFrame(self, fid): fid = b(fid, 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): return self._splitNum(frames.TRACKNUM_FID) def _splitNum(self, fid): 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 (first, second) def _setNum(self, fid, val): if type(val) is tuple: tn, tt = val elif type(val) is int: tn, tt = val, None elif val is None: tn, tt = None, None 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 n[1] >= 0 and 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, unicode(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 u"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, unicode(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 = BytesType(toc) else: self.frame_set[frames.CDID_FID] = \ frames.MusicCDIdFrame(toc=toc) @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 alll 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): return self._getDate(b"TDRL") if self.version == ID3_V2_4 \ else self._getV23OrignalReleaseDate() def _setReleaseDate(self, date): self._setDate(b"TDRL" if self.version == ID3_V2_4 else b"TORY", date) release_date = property(_getReleaseDate, _setReleaseDate) """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.""" def _getOrigReleaseDate(self): return self._getDate(b"TDOR") or self._getV23OrignalReleaseDate() def _setOrigReleaseDate(self, date): self._setDate(b"TDOR", date) original_release_date = property(_getOrigReleaseDate, _setOrigReleaseDate) """The date the work was originally released.""" def _getRecordingDate(self): return self._getDate(b"TDRC") or self._getV23RecordingDate() def _setRecordingDate(self, date): if date is 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: self._setDate(b"TYER", unicode(date.year)) if None not in (date.month, date.day): date_str = u"%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 = u"%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 _getV23OrignalReleaseDate(self): date, date_str = None, None try: for fid in (b"XDOR", b"TORY"): # Prefering XDOR over TORY since it can contain full date. 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("Invalid v2.3 TORY/XDOR frame: %s" % 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): assert(fid in frames.DATE_FIDS or fid in frames.DEPRECATED_DATE_FIDS) if date is None: try: del self.frame_set[fid] except KeyError: pass 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 in StringTypes: date = core.Date.parse(date) elif not isinstance(date, core.Date): raise TypeError("Invalid type: %s" % str(type(date))) date_text = unicode(str(date)) if fid in self.frame_set: self.frame_set[fid][0].date = date else: self.frame_set[fid] = frames.DateFrame(fid, date_text) def _getDate(self, fid): if fid in (b"TORY", b"XDOR"): return self._getV23OrignalReleaseDate() 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._splitNum(frames.DISCNUM_FID) @disc_num.setter def disc_num(self, val): self._setNum(frames.DISCNUM_FID, 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: 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 is None: if self.frame_set[frames.GENRE_FID]: del self.frame_set[frames.GENRE_FID] return if isinstance(g, unicode): g = Genre.parse(g, id3_std=id3_std) elif isinstance(g, int): g = Genre(id=g) elif not isinstance(g, Genre): raise TypeError("Invalid genre data type: %s" % str(type(g))) self.frame_set.setTextFrame(frames.GENRE_FID, unicode(g)) genre = property(_getGenre, _setGenre) """genre property.""" non_std_genre = property(partial(_getGenre, id3_std=False), partial(_setGenre, id3_std=False)) """Non-standard genres.""" @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 _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 orignal 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 BytesType) 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"", 30) tag += pack(encode(self.artist) if self.artist else b"", 30) tag += pack(encode(self.album) if self.album else b"", 30) release_date = self.getBestDate() year = unicode(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 == u"": cmt = c.text # Keep searching in case we find the description eyeD3 uses. cmt = pack(encode(cmt), 30) if version != ID3_V1_0: track = self.track_num[0] if track is not None: cmt = cmt[0:28] + b"\x00" + compat.chr(int(track) & 0xff) tag += cmt if not self.genre or self.genre.id is None: genre = 12 # Other else: genre = self.genre.id tag += compat.chr(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("Rendering frame: %s" % frame_header.id) raw_frame = f.render() log.debug("Rendered %d bytes" % len(raw_frame)) 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) padding_size = 0 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("Using %d bytes of padding" % padding_size) # 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 log.debug("Rendering %s tag header with size %d" % (versionToString(version), total_size - TagHeader.SIZE)) header_data = self.header.render(total_size - TagHeader.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 coersed 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(self, std_frames, convert_list, version): """Maps frame incompatibilities between ID3 v2.3 and v2.4. 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) 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: 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._getV23OrignalReleaseDate() if date: converted_frames.append(DateFrame(b"TDOR", date)) for fid in (b"TORY", b"XDOR"): if fid in flist: flist.remove(date_frames[fid]) del date_frames[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: flist.remove(date_frames[fid]) del date_frames[fid] elif version == ID3_V2_3: if b"TDOR" in date_frames: date = date_frames[b"TDOR"].date if date: converted_frames.append(DateFrame(b"TORY", unicode(date.year))) flist.remove(date_frames[b"TDOR"]) del date_frames[b"TDOR"] if b"TDRC" in date_frames: date = date_frames[b"TDRC"].date if date: converted_frames.append(DateFrame(b"TYER", unicode(date.year))) if None not in (date.month, date.day): date_str = u"%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 = u"%s%s" %\ (str(date.hour).rjust(2, "0"), str(date.minute).rjust(2, "0")) converted_frames.append(TextFrame(b"TIME", date_str)) flist.remove(date_frames[b"TDRC"]) del date_frames[b"TDRC"] if b"TDRL" in date_frames: # TDRL -> XDOR date = date_frames[b"TDRL"].date if date: converted_frames.append(DateFrame(b"XDOR", str(date))) flist.remove(date_frames[b"TDRL"]) del date_frames[b"TDRL"] # All other date frames have no conversion for fid in date_frames: log.warning("%s frame being dropped due to conversion to %s" % (fid, 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=u"Subtitle (converted)", text=tsst_frame.text) converted_frames.append(tsst_frame) # Raise an error for frames that could not be converted. if len(flist) != 0: unconverted = u", ".join([f.id.decode("ascii") for f in flist]) if version[0] != 1: raise TagException("Unable to covert the following frames to " "version %s: %s" % (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): 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 a 3-tuple: (city, state, country) Any may be ``None``.""" if TXXX_ARTIST_ORIGIN in self.user_text_frames: origin = self.user_text_frames.get(TXXX_ARTIST_ORIGIN).text vals = origin.split('\t') else: vals = [None] * 3 vals.extend([None] * (3 - len(vals))) vals = [None if not v else v for v in vals] assert(len(vals) == 3) return vals @artist_origin.setter def artist_origin(self, city, state, country): vals = (city, state, country) vals = [None if not v else v for v in vals] if vals == (None, None, None): self.user_text_frames.remove(TXXX_ARTIST_ORIGIN) else: assert(len(vals) == 3) self.user_text_frames.set('\t'.join(vals), 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, ascii_encode) if isinstance(f, UnicodeType) else f) for f in fids] for f in self.frame_set.getAllFrames(): if not fids or f.id in fids: yield f class FileInfo: """ This class is for storing information about a parsed file. It containts 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 unicode: self.name = file_name else: try: self.name = unicode(file_name, LOCAL_FS_ENCODING) except UnicodeDecodeError: # Work around the local encoding not matching that of a mounted # filesystem log.warning(u"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.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(object): 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): 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, BytesType) else lang.encode("ascii"))) super(DltAccessor, self).__init__(fid, fs, match_func) self.FrameClass = FrameClass @requireUnicode(1, 2) def set(self, text, description=u"", 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(DltAccessor, self).remove(description, lang=lang or DEFAULT_LANG) @requireUnicode(1) def get(self, description, lang=DEFAULT_LANG): return super(DltAccessor, self).get(description, lang=lang or DEFAULT_LANG) class CommentsAccessor(DltAccessor): def __init__(self, fs): super(CommentsAccessor, self).__init__(frames.CommentFrame, frames.COMMENT_FID, fs) class LyricsAccessor(DltAccessor): def __init__(self, fs): super(LyricsAccessor, self).__init__(frames.LyricsFrame, frames.LYRICS_FID, fs) class ImagesAccessor(AccessorBase): def __init__(self, fs): def match_func(frame, description): return frame.description == description super(ImagesAccessor, self).__init__(frames.IMAGE_FID, fs, match_func) @requireUnicode("description") def set(self, type_, img_data, mime_type, description=u"", 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(ImagesAccessor, self).remove(description) @requireUnicode(1) def get(self, description): return super(ImagesAccessor, self).get(description) class ObjectsAccessor(AccessorBase): def __init__(self, fs): def match_func(frame, description): return frame.description == description super(ObjectsAccessor, self).__init__(frames.OBJECT_FID, fs, match_func) @requireUnicode("description", "filename") def set(self, data, mime_type, description=u"", filename=u""): 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(ObjectsAccessor, self).remove(description) @requireUnicode(1) def get(self, description): return super(ObjectsAccessor, self).get(description) class PrivatesAccessor(AccessorBase): def __init__(self, fs): def match_func(frame, owner_id): return frame.owner_id == owner_id super(PrivatesAccessor, self).__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(PrivatesAccessor, self).remove(owner_id) def get(self, owner_id): return super(PrivatesAccessor, self).get(owner_id) class UserTextsAccessor(AccessorBase): def __init__(self, fs): def match_func(frame, description): return frame.description == description super(UserTextsAccessor, self).__init__(frames.USERTEXT_FID, fs, match_func) @requireUnicode(1, "description") def set(self, text, description=u""): 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(UserTextsAccessor, self).remove(description) @requireUnicode(1) def get(self, description): return super(UserTextsAccessor, self).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(UniqueFileIdAccessor, self).__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(UniqueFileIdAccessor, self).remove(owner_id) def get(self, owner_id): owner_id = b(owner_id) return super(UniqueFileIdAccessor, self).get(owner_id) class UserUrlsAccessor(AccessorBase): def __init__(self, fs): def match_func(frame, description): return frame.description == description super(UserUrlsAccessor, self).__init__(frames.USERURL_FID, fs, match_func) @requireUnicode("description") def set(self, url, description=u""): 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(UserUrlsAccessor, self).remove(description) @requireUnicode(1) def get(self, description): return super(UserUrlsAccessor, self).get(description) class PopularitiesAccessor(AccessorBase): def __init__(self, fs): def match_func(frame, email): return frame.email == email super(PopularitiesAccessor, self).__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(PopularitiesAccessor, self).remove(email) def get(self, email): return super(PopularitiesAccessor, self).get(email) class ChaptersAccessor(AccessorBase): def __init__(self, fs): def match_func(frame, element_id): return frame.element_id == element_id super(ChaptersAccessor, self).__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(ChaptersAccessor, self).remove(element_id) def get(self, element_id): return super(ChaptersAccessor, self).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(TocAccessor, self).__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=u""): 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(TocAccessor, self).remove(element_id) def get(self, element_id): return super(TocAccessor, self).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=True, dotted_dates=False): super(TagTemplate, self).__init__(pattern) 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 u'%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 u'%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 u'%s' % func(tag, braced, *args) return u'%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) return name.replace('/', '-') if self._path_friendly else 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 = unicode(date.year) elif date: dstr = unicode(date) else: dstr = u"" if self._dotted_dates: dstr = dstr.replace('-', '.') return dstr def _nums(self, num_tuple, param, zeropad): nn, nt = ((unicode(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) def _file(self, 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.8.4/src/eyed3/id3/headers.py0000644000175000017500000006124013166607546017623 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2002-2015 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 math import logging import binascii from ..utils import requireBytes from ..utils.binfuncs import (bin2dec, bytes2bin, bin2bytes, bin2synchsafe, dec2bin) from .. import core from .. import compat from ..compat import byteOrd from . import ID3_DEFAULT_VERSION, isValidVersion, normalizeVersion from ..utils.log import getLogger log = getLogger(__name__) NULL_FRAME_FLAGS = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] class TagHeader(object): SIZE = 10 def __init__(self, version=ID3_DEFAULT_VERSION): self.clear() self.version = version def clear(self): self.tag_size = 0 # Flag bits self.unsync = False self.extended = False self.experimental = False # v2.4 addition self.footer = False @property def version(self): return tuple([v for v in self._version]) @version.setter def version(self, v): v = normalizeVersion(v) if not isValidVersion(v, fully_qualified=True): raise ValueError("Invalid version: %s" % str(v)) self._version = v @property def major_version(self): return self._version[0] @property def minor_version(self): return self._version[1] @property def rev_version(self): return self._version[2] def parse(self, f): '''Parse an ID3 v2 header starting at the current position of ``f``. If a header is parsed ``True`` is returned, otherwise ``False``. If a header is found but malformed an ``eyed3.id3.tag.TagException`` is thrown. ''' from .tag import TagException self.clear() # 3 bytes: v2 header is "ID3". if f.read(3) != b"ID3": return False log.debug("Located ID3 v2 tag") # 2 bytes: the minor and revision versions. version = f.read(2) if len(version) != 2: return False major = 2 minor = byteOrd(version[0]) rev = byteOrd(version[1]) log.debug("TagHeader [major]: %d " % major) log.debug("TagHeader [minor]: %d " % minor) log.debug("TagHeader [rev]: %d " % rev) if not (major == 2 and (minor >= 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" % (byteOrd(tag_size_bytes[0]), byteOrd(tag_size_bytes[1]), byteOrd(tag_size_bytes[2]), byteOrd(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 += compat.chr(self.minor_version) + compat.chr(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): bites = b"" bites += compat.chr((self.crc >> 28) & 0x7f) bites += compat.chr((self.crc >> 21) & 0x7f) bites += compat.chr((self.crc >> 14) & 0x7f) bites += compat.chr((self.crc >> 7) & 0x7f) bites += compat.chr((self.crc >> 0) & 0x7f) return bites def render(self, version, frame_data, padding=0): assert(version[0] == 2) 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 += compat.chr(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 byteOrd(data[0]) != 1 or (byteOrd(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 = byteOrd(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(byteOrd(data[offset]) == 0) offset += 1 if self.crc_bit: log.debug("Extended header has CRC bit set") assert(byteOrd(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(byteOrd(data[offset]) == 1) offset += 1 self._restrictions = byteOrd(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 = byteOrd(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(object): '''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 compat.BytesType) 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, byteOrd(frame_id_22[0]), byteOrd(frame_id_22[1]), byteOrd(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, byteOrd(frame_id[0]), byteOrd(frame_id[1]), byteOrd(frame_id[2]), byteOrd(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.8.4/src/eyed3/id3/frames.py0000644000175000017500000020657613203722311017456 0ustar travistravis00000000000000# -*- coding: utf-8 -*- from io import BytesIO from codecs import ascii_encode from collections import namedtuple from .. import core from ..utils import requireUnicode, requireBytes from ..utils.binfuncs import (bin2bytes, bin2dec, bytes2bin, dec2bin, bytes2dec, dec2bytes) from ..compat import unicode, UnicodeType, BytesType, byteiter from .. import Error from . import ID3_V2, 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.log import getLogger log = getLogger(__name__) 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 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 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): @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 @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): raise NotImplementedError("Frame decryption not yet supported") @staticmethod def encrypt(data): raise NotImplementedError("Frame encryption not yet supported") @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) 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 @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): raise ValueError("Unknown encoding value {}".format(enc)) self._encoding = enc 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"]) self.text = text or u"" @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(TextFrame, self).parse(data, frame_header) try: self.encoding = self.data[0:1] text_data = self.data[1:] except ValueError as err: log.warning("{err}; using latin1".format(err=err)) self.encoding = LATIN1_ENCODING text_data = self.data[:] try: self.text = decodeUnicode(text_data, self.encoding) except UnicodeDecodeError as err: log.warning("Error decoding text frame {fid}: {err}" .format(fid=self.id, err=err)) self.test = u"" 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) == BytesType) return super(TextFrame, self).render() class UserTextFrame(TextFrame): @requireUnicode("description", "text") def __init__(self, id=USERTEXT_FID, description=u"", text=u""): 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) self.encoding = encoding = self.data[0:1] (d, t) = splitUnicode(self.data[1:], encoding) self.description = decodeUnicode(d, encoding) log.debug("UserTextFrame description: %s" % self.description) self.text = decodeUnicode(t, 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=u""): assert(id in DATE_FIDS or id in DEPRECATED_DATE_FIDS) super(DateFrame, self).__init__(id, text=unicode(date)) self.date = self.text self.encoding = LATIN1_ENCODING def parse(self, data, frame_header): super(DateFrame, self).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(u"Invalid date: " + self.text)) self.text = u'' @property def date(self): return core.Date.parse(self.text.encode("latin1")) if self.text \ else None # \a date Either an ISO 8601 date string or a eyed3.core.Date object. @date.setter def date(self, date): if not date: self.text = u"" return try: if type(date) is str: date = core.Date.parse(date) elif type(date) is unicode: date = core.Date.parse(date.encode("latin1")) elif not isinstance(date, core.Date): raise TypeError("str, unicode, and eyed3.core.Date type " "expected") except ValueError: log.warning("Invalid date text: %s" % date) self.text = u"" return self.text = unicode(str(date)) def _initEncoding(self): # Dates are always latin1 since they are always represented in ISO 8601 self.encoding = LATIN1_ENCODING class UrlFrame(Frame): @requireBytes("url") def __init__(self, id, url=b""): assert(id in URL_FIDS or id == USERURL_FID) super(UrlFrame, self).__init__(id) self.encoding = LATIN1_ENCODING self.url = url @property def url(self): return self._url @requireBytes(1) @url.setter def url(self, url): self._url = url def parse(self, data, frame_header): super(UrlFrame, self).parse(data, frame_header) # The URL is ascii, ensure try: self.url = unicode(self.data, "ascii").encode("ascii") except UnicodeDecodeError: log.warning("Non ascii url, clearing.") self.url = "" def render(self): self.data = self.url return super(UrlFrame, self).render() class UserUrlFrame(UrlFrame): """ Data string format: encoding (one byte) + description + b"\x00" + url (ascii) """ @requireUnicode("description") def __init__(self, id=USERURL_FID, description=u"", url=b""): 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(UserUrlFrame, self).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 = unicode(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) self.data = data # Calling Frame, not the base. return Frame.render(self) ## # 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 = u"-->" # noqa URL_MIME_TYPE_VALUES = (URL_MIME_TYPE, URL_MIME_TYPE_STR) @requireUnicode("description") def __init__(self, id=IMAGE_FID, description=u"", 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 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 unicode(self._mime_type, "ascii") @mime_type.setter def mime_type(self, m): m = m or b'' self._mime_type = m if isinstance(m, BytesType) 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(ImageFrame, self).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 = u"" # 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 if self.description: self._initEncoding() else: self.encoding = LATIN1_ENCODING 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, id=OBJECT_FID, description=u"", filename=u"", object_data=None, mime_type=None): super(ObjectFrame, self).__init__(OBJECT_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 unicode(self._mime_type, "ascii") @mime_type.setter def mime_type(self, m): m = m or b'' self._mime_type = m if isinstance(m, BytesType) 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(ObjectFrame, self).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 != b"\x00": 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 = u"" self.description = u"" # 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""" def __init__(self, id=PRIVATE_FID, owner_id=b"", owner_data=b""): super(PrivateFrame, self).__init__(id) assert(id == PRIVATE_FID) self.owner_id = owner_id self.owner_data = owner_data def parse(self, data, frame_header): super(PrivateFrame, self).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(MusicCDIdFrame, self).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(PlayCountFrame, self).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, UnicodeType): self._email = email.encode(ascii_encode) elif isinstance(email, BytesType): _ = 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(PopularityFrame, self).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=None, uniq_id=None): super(UniqueFileIDFrame, self).__init__(id) assert(self.id == UNIQUE_FILE_ID_FID) self.owner_id = owner_id self.uniq_id = uniq_id def parse(self, data, frame_header): """ Data format Owner identifier $00 Identifier up to 64 bytes binary data> """ super(UniqueFileIDFrame, self).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"", split_data[0:1] log.debug("UFID owner_id: %s" % self.owner_id) log.debug("UFID id: %s" % self.uniq_id) if len(self.owner_id) == 0: 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): self.data = self.owner_id + b"\x00" + self.uniq_id return super(UniqueFileIDFrame, self).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(DescriptionLangTextFrame, self).__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(DescriptionLangTextFrame, self).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)) try: (d, t) = splitUnicode(self.data[4:], encoding) self.description = decodeUnicode(d, encoding) log.debug("%s description: %s" % (self.id, self.description)) self.text = decodeUnicode(t, encoding) log.debug("%s text: %s" % (self.id, self.text)) except ValueError: log.warning("Invalid %s frame; no description/text" % self.id) self.description = u"" self.text = u"" 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=u"", lang=DEFAULT_LANG, text=u""): super(CommentFrame, self).__init__(id, description, lang, text) assert(self.id == COMMENT_FID) class LyricsFrame(DescriptionLangTextFrame): def __init__(self, id=LYRICS_FID, description=u"", lang=DEFAULT_LANG, text=u""): 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=u"", 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(TermsOfUseFrame, self).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(TocFrame, self).__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(TocFrame, self).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 i 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(TocFrame, self).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(ChapterFrame, self).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 = u"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 sucks, or it is beautiful anarchy class FrameSet(dict): def __init__(self): dict.__init__(self) 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 valule of 0 does not mean error.""" self.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 # # 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 . # ################################################################################ from __future__ import print_function import os import sys import textwrap import warnings import eyed3 import eyed3.utils import eyed3.utils.console import eyed3.plugins import eyed3.__about__ from eyed3.compat import ConfigParser, ConfigParserError, StringIO, UnicodeType from eyed3.utils.log import initLogging initLogging() DEFAULT_PLUGIN = "classic" DEFAULT_CONFIG = os.path.expandvars("${HOME}/.eyeD3/config.ini") USER_PLUGINS_DIR = 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) # Process paths (files/directories) for p in args.paths: eyed3.utils.walk(args.plugin, p, excludes=args.excludes, fs_encoding=args.fs_encoding) retval = args.plugin.handleDone() return retval or 0 def _listPlugins(config): from eyed3.utils.console import Fore, Style print("") 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("Type 'eyeD3 --plugin= --help' for more help") print("") plugin_names.sort() for name in plugin_names: plugin = all_plugins[name] alt_names = plugin.NAMES[1:] alt_names = " (%s)" % ", ".join(alt_names) if alt_names else "" print("%s %s:" % (header(name), alt_names)) for l in textwrap.wrap(plugin.SUMMARY, initial_indent=' ' * 2, subsequent_indent=' ' * 2): print(Style.BRIGHT + Fore.GREY + l + Style.RESET_ALL) print("") def _loadConfig(args): import os config = None config_file = None if args.config: config_file = os.path.abspath(config_file) elif args.no_config is False: config_file = DEFAULT_CONFIG if not config_file: return None if os.path.isfile(config_file): try: config = ConfigParser() config.read(config_file) except ConfigParserError as ex: eyed3.log.warning("User config error: " + str(ex)) return None elif config_file != DEFAULT_CONFIG: raise IOError("User config not found: %s" % config_file) return config 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, paths_metavar="PATH", paths_help="Files or directory paths"): 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. " "Default as it was detected is '%s' but this option is still " "useful when reading from mounted file systems." % eyed3.LOCAL_FS_ENCODING) 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="Specify which plugin to use. The default is '%s'" % DEFAULT_PLUGIN) p.add_argument("-C", "--config", action="store", dest="config", default=None, metavar="FILE", help="Supply a configuration file. The default is " "'%s', although even that is optional." % DEFAULT_CONFIG) 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="Do not load the default user config '%s'. " "The -c/--config options are still honored if " "present." % DEFAULT_CONFIG) 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 assert(plugin_name) 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()) # Reparse 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""" 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(UnicodeType(ex)) retval = 1 except Exception as ex: eyed3.utils.console.printError("Uncaught exception: %s\n" % str(ex)) 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 except ImportError: import pdb e, m, tb = sys.exc_info() pdb.post_mortem(tb) sys.exit(retval) if __name__ == "__main__": # pragma: no cover _main() eyeD3-0.8.4/src/eyed3/utils/0000755000175000017500000000000013203726215016277 5ustar travistravis00000000000000eyeD3-0.8.4/src/eyed3/utils/binfuncs.py0000644000175000017500000000741613061344514020470 0ustar travistravis00000000000000################################################################################ # 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 . # ################################################################################ from ..compat import intToByteString, BytesType, byteiter def bytes2bin(bytes, 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("Invalid sz value: %d" % sz) ''' # I was willing to bet this implementation was gonna be faster, tis not retval = [] for bite in bytes: bits = [int(b) for b in bin(ord(bite))[2:].zfill(8)][-sz:] assert(len(bits) == sz) retval.extend(bits) return retval ''' retVal = [] for b in byteiter(bytes): 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 # Convert an array of bits (MSB first) into a string of characters. def bin2bytes(x): 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 += intToByteString(ttl) multi = 1 ttl = 0 if multi > 1: out += intToByteString(ttl) out = bytearray(out) out.reverse() out = BytesType(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(bytes, sz=8): return bin2dec(bytes2bin(bytes, 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) 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 = b"" bites += intToByteString((n >> 21) & 0x7f) bites += intToByteString((n >> 14) & 0x7f) bites += intToByteString((n >> 7) & 0x7f) bites += intToByteString((n >> 0) & 0x7f) bits = bytes2bin(bites) assert(len(bits) == 32) return bits eyeD3-0.8.4/src/eyed3/utils/art.py0000644000175000017500000000706513041005225017436 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2014 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 . # ################################################################################ from 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 del _type del _id3_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.8.4/src/eyed3/utils/__init__.py0000644000175000017500000003746013203715221020415 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2002-2015 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 . # ################################################################################ from __future__ import print_function import os import re import math import pathlib import logging import argparse import warnings import magic from ..compat import unicode, PY2 from ..utils.log import getLogger from .. import LOCAL_ENCODING, LOCAL_FS_ENCODING log = getLogger(__name__) ID3_MIME_TYPE = "application/x-id3" ID3_MIME_TYPE_EXTENSIONS = (".id3", ".tag") class MagicTypes(magic.Magic): def __init__(self): magic.Magic.__init__(self, mime=True, mime_encoding=False, keep_going=False) def guess_type(self, filename): if os.path.splitext(filename)[1] in ID3_MIME_TYPE_EXTENSIONS: return ID3_MIME_TYPE try: return self.from_file(filename) except UnicodeEncodeError as enc_err: # https://github.com/ahupp/python-magic/pull/144 return self.from_file(filename.encode("utf-8", 'surrogateescape')) _mime_types = MagicTypes() def guessMimetype(filename, with_encoding=False): """Return the mime-type for ``filename``. If ``with_encoding`` is True the encoding is included and a 2-tuple is returned, (mine, enc).""" filename = str(filename) if isinstance(filename, pathlib.Path) else filename mime = _mime_types.guess_type(filename) if not with_encoding: return mime else: warnings.warn("File character encoding no lopng return, value is None", UserWarning, stacklevel=2) return mime, None def walk(handler, path, excludes=None, fs_encoding=LOCAL_FS_ENCODING): """A wrapper around os.walk which handles exclusion patterns and multiple path types (unicode, pathlib.Path, bytes). """ if isinstance(path, pathlib.Path): path = str(path) else: path = unicode(path, fs_encoding) if type(path) is not unicode 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("file not found: %s" % 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(path): root = root if type(root) is unicode else unicode(root, fs_encoding) dirs.sort() files.sort() for f in files: f = f if type(f) is unicode else unicode(f, fs_encoding) f = os.path.abspath(os.path.join(root, f)) if not _isExcluded(f): try: handler.handleFile(f) except StopIteration: return if files: handler.handleDirectory(root, files) 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 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(unicode, *args) def requireBytes(*args): """Function decorator to enforce 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(bytes, *args) def encodeUnicode(replace=True): warnings.warn("use compat PY2 and be more python3", DeprecationWarning, stacklevel=2) enc_err = "replace" if replace else "strict" if PY2: def wrapper(fn): def wrapped_fn(*args, **kwargs): new_args = [] for a in args: if type(a) is unicode: new_args.append(a.encode(LOCAL_ENCODING, enc_err)) else: new_args.append(a) args = tuple(new_args) for kw in kwargs: if type(kwargs[kw]) is unicode: kwargs[kw] = kwargs[kw].encode(LOCAL_ENCODING, enc_err) return fn(*args, **kwargs) return wrapped_fn return wrapper else: # This decorator is used to encode unicode to bytes for sys.std* # write calls. In python3 unicode (or str) is required by these # functions, the encodig happens internally.. So return a noop def noop(fn): def call(*args, **kwargs): return fn(*args, **kwargs) return noop 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 ' / '. """ 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 = [ (u'y', 60 * 60 * 24 * 7 * 52), (u'w', 60 * 60 * 24 * 7), (u'd', 60 * 60 * 24), (u'h', 60 * 60), (u'm', 60), (u's', 1), ] seconds = int(seconds) if seconds < 60: return u' {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 u'{0:02d}{1}{2:02d}{3}'.format( seconds // limit1, unit1, (seconds % limit1) // limit2, unit2) return u' ~inf' KB_BYTES = 1024 """Number of bytes per KB (2^10)""" MB_BYTES = 1048576 """Number of bytes per MB (2^20)""" GB_BYTES = 1073741824 """Number of bytes per GB (2^30)""" KB_UNIT = "KB" """Kilobytes abbreviation""" MB_UNIT = "MB" """Megabytes abbreviation""" GB_UNIT = "GB" """Gigabytes abbreviation""" def formatSize(size, short=False): """Format ``size`` (nuber 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 = u' 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") 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.") 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 = "invalid level choice: %s (choose from %s)" % \ (level, 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=u''): """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 couter 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 = u'' else: file = "".join(["%s_%s" % (name, count), ext]) count += 1 return os.path.join(path, file) eyeD3-0.8.4/src/eyed3/utils/prompt.py0000644000175000017500000000736713061344514020207 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2013 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 sys as _sys from .. import LOCAL_ENCODING from .console import Fore as fg from .. import compat 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_=compat.UnicodeType, validate=None, choices=None): '''Prompt user for imput, 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 = compat.input(msg) if not isinstance(resp, compat.UnicodeType): # Python2 resp = resp.decode(LOCAL_ENCODING) 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.8.4/src/eyed3/utils/log.py0000644000175000017500000000452613061344514017441 0ustar travistravis00000000000000# -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2002-2015 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 logging from ..__about__ import __version__ as VERSION logging.basicConfig() 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 propogation 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 # Don't propgate base 'eyed3' log.propagate = False console_handler = logging.StreamHandler() console_handler.setFormatter(logging.Formatter(DEFAULT_FORMAT)) log.addHandler(console_handler) log.setLevel(logging.WARNING) return log LEVELS = (logging.DEBUG, logging.VERBOSE, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL) eyeD3-0.8.4/src/eyed3/utils/console.py0000644000175000017500000004420113177420270020316 0ustar travistravis00000000000000# -*- coding: utf-8 -*- from __future__ import print_function import os import sys import time import struct from .. import compat from .. import LOCAL_ENCODING from . import formatSize, formatTime from .log import log try: import fcntl import termios import signal _CAN_RESIZE_TERMINAL = True except ImportError: _CAN_RESIZE_TERMINAL = False 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 = u'' 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()) @staticmethod def init(enabled): if not enabled: AnsiCodes._USE_ANSI = False else: AnsiCodes._USE_ANSI = True if (("TERM" in os.environ and os.environ["TERM"] == "dumb") or ("OS" in os.environ and os.environ["OS"] == "Windows_NT")): AnsiCodes._USE_ANSI = False 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 = u"◓◑◒◐" _default_ascii_chars = u"-/|\\" 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(u'\r') write(self._msg) write(u' ') write(chars[index]) flush() yield for i 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(u'\r') write(self._msg) if self._print_done: if exc_type is None: write(Fore.GREEN + u' [Done]\n') else: write(Fore.RED + u' [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, 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(u'\r|') write(Fore.BLUE + u'=' * bar_fill + Fore.RESET) if bar_fill < self._bar_length: write(Fore.GREEN + u'>' + Fore.RESET) write(u'-' * (self._bar_length - bar_fill - 1)) write(u'|') 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 = u' 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 += u' ({0:>6s}%)'.format(u'{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 _encode(s): '''This is a helper for output of unicode. With Python2 it is necessary to do encoding to the LOCAL_ENCODING since by default unicode will be encoded to ascii. In python3 this conversion is not necessary for the user to to perform; in fact sys.std*.write, for example, requires unicode strings be passed in. This function will encode for python2 and do nothing for python3 (except assert that ``s`` is a unicode type).''' if compat.PY2: if isinstance(s, compat.unicode): try: return s.encode(LOCAL_ENCODING) except Exception as ex: log.error("Encoding error: " + str(ex)) return s.encode(LOCAL_ENCODING, "replace") elif isinstance(s, str): return s else: raise TypeError("Argument must be str or unicode") else: assert(isinstance(s, str)) return s def printMsg(s): fp = sys.stdout s = _encode(s) try: fp.write("%s\n" % s) except UnicodeEncodeError: fp.write("%s\n" % compat.unicode(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, fp=sys.stdout, 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): s = _encode(s) 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, IOError): 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_name, 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 + u"Phase #1") as spinner: for i in range(50): time.sleep(.05) spinner.next() with Spinner(Fore.RED + u"Phase #2" + Fore.RESET, print_done=False) as spinner: for i in range(50): time.sleep(.05) spinner.next() with Spinner(u"Phase #3", print_done=False, use_unicode=False) as spinner: for i in range(50): spinner.next() time.sleep(.05) with Spinner(u"Phase #4", print_done=False, chars='.oO°Oo.') as spinner: for i in range(50): spinner.next() time.sleep(.05) items = range(200) with ProgressBar(len(items)) as bar: for item in enumerate(items): bar.update() time.sleep(.05) for item in ProgressBar(items): time.sleep(.05) progress = 0 max = 320000000 with ProgressBar(max) as bar: while progress < max: progress += 23400 bar.update(progress) time.sleep(.001) eyeD3-0.8.4/src/eyed3/__about__.py0000644000175000017500000000302113203715221017406 0ustar travistravis00000000000000# -*- coding: utf-8 -*- from collections import namedtuple def __parse_version(v): # pragma: nocover ver, rel = v, "final" for c in ("a", "b", "c"): parsed = v.split(c) if len(parsed) == 2: ver, rel = (parsed[0], c + parsed[1]) v = tuple((int(v) for v in ver.split("."))) ver_info = namedtuple("Version", "major, minor, maint, release")( *(v + (tuple((0,)) * (3 - len(v))) + tuple((rel,)))) return ver, rel, ver_info __version__ = "0.8.4" __release_name__ = "The Cold Vein" __years__ = "2002-2017" _, __release__, __version_info__ = __parse_version(__version__) __project_name__ = "eyeD3" __project_slug__ = "eyed3" __pypi_name__ = "eyeD3" __author__ = "Travis Shirk" __author_email__ = "travis@pobox.com" __url__ = "http://eyed3.nicfit.net/" __description__ = "Python audio data toolkit (ID3 and MP3)" # 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__ = """ %(__name__)s %(__version__)s (C) 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.8.4/PKG-INFO0000644000175000017500000010367413203726215014447 0ustar travistravis00000000000000Metadata-Version: 1.1 Name: eyeD3 Version: 0.8.4 Summary: Python audio data toolkit (ID3 and MP3) Home-page: http://eyed3.nicfit.net/ Author: Travis Shirk Author-email: travis@pobox.com License: GNU GPL v3.0 Download-URL: https://github.com/nicfit/eyeD3/releases/downloads/v0.8.4/eyeD3-0.8.4.tar.gz Description-Content-Type: UNKNOWN Description: Status ------ .. 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 = u"Integrity" audiofile.tag.album = u"Humanity Is The Devil" audiofile.tag.album_artist = u"Integrity" audiofile.tag.title = u"Hollow" audiofile.tag.track_num = 2 audiofile.tag.save() eyeD3_ is written and maintained by `Travis Shirk`_ and is licensed under version 3 of the GPL_. Features -------- * Python package for writing application and/or plugins. * Command-line tool driver script that supports plugins. viewer/editor interface. * Easy editing/viewing of audio metadata from the command-line, using the 'classic' plugin. * 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 2.7, >= 3.3 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://bitbucket.org/nicfit/eyed3/issues?status=new&status=open .. _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/ Release History =============== 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: * [:bbissue:`78`] - 'NoneType' object has no attribute 'year' * [:bbissue:`108`] - Multiple date related fixes. * [:bbissue:`110`] - Allow superfluous --no-tagging-ttme-frame option for backward compatibility. * [:bbissue:`111`] - The --version option now prints a short, version-only, message. * [:bbissue:`116`] - Allow --year option for backward compatibility. Converts to --release-year. * [:bbissue:`117`] - Fixes for --user-text-frame with multiple colons and similar fixes. * [:bbissue:`125`] - ID3 v1.1 encoding fixes. .. _release-0.7.10: 0.7.10 - 12.10.2016 (Hollow) --------------------------------- Bug Fixes: * [:bbissue:`97`] - Missing import * [:bbissue:`105`] - 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. * [:bbissue:`81`] - Replaced invalid Unicode. * [:bbissue:`91`] - Disabled ANSI codes on Windows * [:bbissue:`92`] - 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: * [:bbissue:`50`] Crashing on --remove-frame PRIV * [:bbissue:`75`] Parse lameinfo even if crc16 is not correct * [:bbissue:`77`] Typo in docs/installation.rst * [:bbissue:`79`] 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: * [:bbissue:`49`] 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. * [:bbissue:`9`] 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. * [:bbissue:`46`] Disable ANSI color codes when TERM == "dumb" * [:bbissue:`47`] Locking around libmagic. * [:bbissue:`54`] Work around for zero-padded utf16 strings. * [:bbissue:`65`] Safer tempfile usage. * [:bbissue:`65`] 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`_. * [:bbissue:`28`] [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. * [:bbissue:`10`] Magic module `hasattr` fix. * [:bbissue:`12`] More robust handling of bogus play count values. * [:bbissue:`13`] More robust handling of bogus date values. * [:bbissue:`18`] Proper unicode handling of APIC descriptions. * [:bbissue:`19`] Proper use of argparse.ArgumentTypeError * [:bbissue:`26`] Allow TCMP frames when parsing. * [:bbissue:`30`] Accept more invalid frame types (iTunes) * [:bbissue:`31`] Documentation fixes. * [:bbissue:`31`] Fix for bash completion script. * [:bbissue:`32`] 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: * [:bbissue:`5`] 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'. * [:bbissue:`6`] 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. Keywords: id3,mp3,python Platform: Any Classifier: Environment :: Console Classifier: Intended Audience :: End Users/Desktop Classifier: Topic :: Multimedia :: Sound/Audio :: Editors Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Intended Audience :: Developers Classifier: Operating System :: POSIX Classifier: Natural Language :: English Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Development Status :: 5 - Production/Stable eyeD3-0.8.4/tox.ini0000644000175000017500000000073213177421624014662 0ustar travistravis00000000000000[tox] envlist = clean, py27, pypy, py33, py34, py35, py36, pypy3, report [testenv:clean] commands = coverage erase [testenv] deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements/test.txt py27,py33,pypy: -r{toxinidir}/requirements/test_py33.txt commands = coverage run --rcfile=setup.cfg --source ./src/eyed3 --append -m pytest ./src/test [testenv:report] commands = coverage report --rcfile=setup.cfg coverage html --rcfile=setup.cfg eyeD3-0.8.4/requirements/0000755000175000017500000000000013203726215016062 5ustar travistravis00000000000000eyeD3-0.8.4/requirements/main.txt0000644000175000017500000000006013203726151017542 0ustar travistravis00000000000000pathlib==1.0.1 python-magic==0.4.13 six==1.11.0 eyeD3-0.8.4/requirements/dev.txt0000644000175000017500000000060513203726151017401 0ustar travistravis00000000000000Sphinx==1.6.5 check-manifest==0.35 cogapp==2.5.1 flake8==3.5.0 git+https://github.com/nicfit/gitchangelog.git hg+https://nicfit@bitbucket.org/nicfit/sphinxcontrib-bitbucket ipdb==0.10.3 nicfit.py[cookiecutter]==0.6.6 paver==1.2.4 pip-tools==1.10.1 pss==1.41 pyaml==17.10.0 sphinx-issues==0.3.1 sphinx_rtd_theme==0.2.4 sphinxcontrib-paverutils==1.16.0 tox==2.9.1 twine==1.9.1 wheel==0.30.0 eyeD3-0.8.4/requirements/test.txt0000644000175000017500000000010613203726151017576 0ustar travistravis00000000000000factory-boy==2.9.2 pytest==3.2.5 pytest-cov==2.5.1 pytest-runner==3.0 eyeD3-0.8.4/requirements/requirements.yml0000644000175000017500000000141613177421624021340 0ustar travistravis00000000000000main: - six - pathlib # Remove with python 2.7 and 3.3 - python-magic extra_display-plugin: - grako test: - pytest - pytest-cov - pytest-runner - factory-boy test_py33: - pathlib - mock dev: - nicfit.py[cookiecutter] - Sphinx - sphinx_rtd_theme - check-manifest - flake8 - git+https://github.com/nicfit/gitchangelog.git - ipdb - pip-tools - tox - twine - wheel - paver - cogapp - sphinxcontrib-paverutils - pyaml - pss - sphinx-issues # Upstream PR: https://bitbucket.org/dhellmann/sphinxcontrib-bitbucket/pull-requests/1/use-setuptools-over-distribute-python3/diff #- sphinxcontrib-bitbucket - hg+https://nicfit@bitbucket.org/nicfit/sphinxcontrib-bitbucket eyeD3-0.8.4/requirements/test_py33.txt0000644000175000017500000000003313203726151020453 0ustar travistravis00000000000000mock==2.0.0 pathlib==1.0.1 eyeD3-0.8.4/requirements/extra_display-plugin.txt0000644000175000017500000000001613203726151022763 0ustar travistravis00000000000000grako==3.99.9 eyeD3-0.8.4/AUTHORS.rst0000644000175000017500000000224213203726021015211 0ustar travistravis00000000000000Authors ------- 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 * mafro * Gaetano Guerriero * Grun Seid * pyup-bot * Chris Newton eyeD3-0.8.4/docs/0000755000175000017500000000000013203726215014267 5ustar travistravis00000000000000eyeD3-0.8.4/docs/plugins/0000755000175000017500000000000013203726215015750 5ustar travistravis00000000000000eyeD3-0.8.4/docs/plugins/xep118_plugin.rst0000644000175000017500000000053513061344514021111 0ustar travistravis00000000000000xep-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 extra options supported .. {{{end}}} eyeD3-0.8.4/docs/plugins/display_plugin.rst0000644000175000017500000002043413061344514021530 0ustar travistravis00000000000000display - Display tag information by pattern ============================================ *Prints specific tag information which are specified by a pattern.* Names ----- display Description ----------- Displays tag information for each file. With a pattern the concrete output can be specified. The pattern EBNF: .. code-block:: text pattern := { | tag | function }* tag := '%' { ',' parameter }* '%' function := '$' '(' [ parameter { ',' parameter }* ] ')' parameter := [ '=' ] [ pattern ] := string with escaped special characters := string without special characters Tags are surrounded by two '%'. There are also functions that starts with a '$'. Both tag and function could be parametrized. Options ------- .. code-block:: text --pattern-help Detailed pattern help -p STRING, --pattern STRING Pattern string -f FILE, --pattern-file FILE Pattern file --no-newline Print no newline after each output Pattern elements ---------------- ID3 Tags: .. code-block:: text a, artist Artist A, album Album b, album-artist Album artist t, title Title n, track Track number N, track-total Total track number d, disc, disc-num Disc number D, disc-total Total disc number G, genre Genre genre-id Genre ID Y, year Release year c, comment First comment that matches description and language. Parameters: description (optional) language (optional) comments All comments that are matching description and language (with output placeholders #d as description, #l as language & #t as text). Parameters: description (optional) language (optional) output (optional, default='Comment: [Description: #d] [Lang: #l]: #t') separation (optional, default='\n') lyrics All lyrics that are matching description and language (with output placeholders #d as description, #l as language & #t as text). Parameters: description (optional) language (optional) output (optional, default='Lyrics: [Description: #d] [Lang: #l]: #t') separation (optional, default='\n') release-date Relase date original-release-date Original Relase date recording-date Recording date encoding-date Encoding date tagging-date Tagging date play-count Play count popm, popularities Popularities (with output placeholders #e as email, #r as rating & #c as count) Parameters: output (optional, default='Popularity: [email: #e] [rating: #r] [play count: #c]') separation (optional, default='\n') bpm BPM publisher Publisher ufids, unique-file-ids Unique File IDs (with output placeholders #o as owner & #i as unique id) Parameters: output (optional, default='Unique File ID: [#o] : #i') separation (optional, default='\n') txxx, texts User text frames (with output placeholders #d as description & #t as text) Parameters: output (optional, default='UserTextFrame: [Description: #d] #t') separation (optional, default='\n') user-urls User URL frames (with output placeholders #i as frame id, #d as description & #u as url) Parameters: output (optional, default='#i [Description: #d]: #u') separation (optional, default='\n') artist-url Artist URL audio-source-url Audio source URL audio-file-url Audio file URL internet-radio-url Internet radio URL commercial-url Comercial URL payment-url Payment URL publisher-url Publisher URL copyright-url Copyright URL images, apic Attached pictures (APIC) (with output placeholders #t as image type, #m as mime type, #s as size in bytes & #d as description) Parameters: output (optional, default='#t Image: [Type: #m] [Size: #b bytes] #d') separation (optional, default='\n') image-urls Attached pictures URLs (with output placeholders #t as image type, #m as mime type, #u as URL & #d as description) Parameters: output (optional, default='#t Image: [Type: #m] [URL: #u] #d') separation (optional, default='\n') objects, gobj Objects (GOBJ) (with output placeholders #s as size, #m as mime type, #d as description and #f as file name) Parameters: output (optional, default='GEOB: [Size: #s bytes] [Type: #t] Description: #d | Filename: #f') separation (optional, default='\n') privates, priv Privates (with output placeholders #c as content, #b as number of bytes & #o as owner) Parameters: output (optional, default='PRIV-Content: #b bytes | Owner: #o') separation (optional, default='\n') music-cd-id, mcdi Music CD Identification terms-of-use Terms of use Functions: .. code-block:: text format Formats text bold and colored (grey, red, green, yellow, blue, magenta, cyan or white) Parameters: text bold (optional) color (optional) num, number-format Appends leading zeros Parameters: number digits filename, fn File name Parameter: basename (optional) filesize Size of file tag-version Tag version length Length of aufio file mpeg-version MPEG version (with output placeholders #v as version & #l as layer) Parameter: output (optional, default='MPEG#v\, Layer #l') bit-rate Bit rate of aufio file sample-freq Sample frequence of aufio file in Hz audio-mode Mode of aufio file: mono/stereo not-empty If condition is not empty (with output placeholder #t as text) Parameters: text output (optional, default='#t') empty (optional) repeat Repeats text Parameters: text count Special characters: .. code-block:: text escape seq. character \\ \ \% % \$ $ \, , \( ( \) ) \= = \n New line \t Tab Example ------- Asuming an audio file with artist 'Madonna', titel 'Frozen' and album 'Ray of Light' .. code-block:: text %artist% - %album% - %title% %a% - %A% - %t% Both patterns produce the following output: Madonna - Ray of Light - Frozen .. code-block:: text $format(title:,bold=y) %title%\n This pattern produces th output: **title:** Frozen eyeD3-0.8.4/docs/plugins/classic_plugin.rst0000644000175000017500000004527613153052736021523 0ustar travistravis00000000000000classic - 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 -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. -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. -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. --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. --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 -o example.id3 -rw-r--r-- 1 travis 0 Feb 26 17:14 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/git/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/git/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/git/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/git/example.id3 [ 394.00 Bytes ] ------------------------------------------------------------------------------- ID3 v1.0: title: artist: album: album artist: None 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/git/example.id3 [ 394.00 Bytes ] ------------------------------------------------------------------------------- Setting artist: id3v1 ID3 v1.0: title: artist: id3v1 album: album artist: None 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/git/example.id3 [ 394.00 Bytes ] ------------------------------------------------------------------------------- Setting artist: id3v2 ID3 v2.4: title: artist: id3v2 album: album artist: None 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/git/example.id3 [ 394.00 Bytes ] ------------------------------------------------------------------------------- ID3 v2.4: title: artist: id3v2 album: album artist: None 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/git/example.id3 [ 394.00 Bytes ] ------------------------------------------------------------------------------- ID3 v1.0: title: artist: id3v2 album: album artist: None 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/git/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 invidual 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 '\\'. .. {{{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/git/example.id3 [ 0.00 Bytes ] ------------------------------------------------------------------------------- Adding image http://example.com/cover.jpg ID3 v2.4: title: artist: album: album artist: None track: FRONT_COVER Image: [Type: -->] [URL: b'http://example.com/cover.jpg'] Description: Writing ID3 version v2.4 ------------------------------------------------------------------------------- .. {{{end}}} eyeD3-0.8.4/docs/plugins/itunes_plugin.rst0000644000175000017500000000202413153052736021371 0ustar travistravis00000000000000itunes-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/git/example.id3 iTunes podcast? :-( $ eyeD3 -P itunes-podcast example.id3 --add /home/travis/devel/eyeD3/git/example.id3 iTunes podcast? :-( Adding... iTunes podcast? :-) $ eyeD3 -P itunes-podcast example.id3 --remove /home/travis/devel/eyeD3/git/example.id3 iTunes podcast? :-) Removing... iTunes podcast? :-( .. {{{end}}} eyeD3-0.8.4/docs/plugins/nfo_plugin.rst0000644000175000017500000000266513061344514020653 0ustar travistravis00000000000000nfo - (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.8.4/docs/plugins/lameinfo_plugin.rst0000644000175000017500000000301613061344514021652 0ustar travistravis00000000000000lameinfo (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 src/test/data/notag-vbr.mp3 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.8.4/docs/plugins/stats_plugin.rst0000644000175000017500000000052713061344514021222 0ustar travistravis00000000000000stats - 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.8.4/docs/plugins/genres_plugin.rst0000644000175000017500000001374713153052736021363 0ustar travistravis00000000000000genres - 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.8.4/docs/plugins/fixup_plugin.rst0000644000175000017500000001527113061344514021221 0ustar travistravis00000000000000fixup - 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 --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. -t {lp,ep,compilation,live,various,demo,single}, --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 .. {{{end}}} eyeD3-0.8.4/docs/plugins/pymod_plugin.rst0000644000175000017500000000223113061344514021206 0ustar travistravis00000000000000pymod - 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 (see -m/--module) 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.8.4/docs/_config.yml0000644000175000017500000000003113203715221016403 0ustar travistravis00000000000000theme: jekyll-theme-slateeyeD3-0.8.4/docs/Makefile0000644000175000017500000001514613153052736015742 0ustar travistravis00000000000000# 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.8.4/docs/usage.rst0000644000175000017500000000012113153052736016123 0ustar travistravis00000000000000:orphan: ======== Usage ======== To use eyeD3 in a project:: import eyed3 eyeD3-0.8.4/docs/readme.rst0000644000175000017500000000004513153052736016261 0ustar travistravis00000000000000:orphan: .. include:: ../README.rst eyeD3-0.8.4/docs/installation.rst0000644000175000017500000000413613153052736017532 0ustar travistravis00000000000000============ 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 Dependencies ============ eyeD3 |version| has been tested with Python 2.7, >=3.3 (see the 0.7.x series 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 .. _Paver: http://paver.github.com/paver/ 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.8.4/docs/eyed3.id3.rst0000644000175000017500000000133013153052736016511 0ustar travistravis00000000000000eyed3.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.8.4/docs/eyed3.rst0000644000175000017500000000120513203726153016031 0ustar travistravis00000000000000eyed3 package ============= Subpackages ----------- .. toctree:: eyed3.id3 eyed3.mp3 eyed3.plugins eyed3.utils Submodules ---------- eyed3\.compat module -------------------- .. automodule:: eyed3.compat :members: :undoc-members: :show-inheritance: eyed3\.core module ------------------ .. automodule:: eyed3.core :members: :undoc-members: :show-inheritance: eyed3\.main module ------------------ .. automodule:: eyed3.main :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: eyed3 :members: :undoc-members: :show-inheritance: eyeD3-0.8.4/docs/_static/0000755000175000017500000000000013203726215015715 5ustar travistravis00000000000000eyeD3-0.8.4/docs/_static/rtd.css0000644000175000017500000003563413041005225017222 0ustar travistravis00000000000000/* * 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.8.4/docs/_static/.keep0000644000175000017500000000000013153052736016634 0ustar travistravis00000000000000eyeD3-0.8.4/docs/modules.rst0000644000175000017500000000006413203726153016472 0ustar travistravis00000000000000eyed3 ===== .. toctree:: :maxdepth: 4 eyed3 eyeD3-0.8.4/docs/make.bat0000644000175000017500000001447113061344514015703 0ustar travistravis00000000000000@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.8.4/docs/_templates/0000755000175000017500000000000013203726215016424 5ustar travistravis00000000000000eyeD3-0.8.4/docs/_templates/.keepme0000644000175000017500000000000013041005225017650 0ustar travistravis00000000000000eyeD3-0.8.4/docs/history.rst0000644000175000017500000000003413061344514016517 0ustar travistravis00000000000000.. include:: ../HISTORY.rst eyeD3-0.8.4/docs/cli.rst0000644000175000017500000001016213061344514015570 0ustar travistravis00000000000000 '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/classic_plugin plugins/display_plugin plugins/fixup_plugin plugins/itunes_plugin plugins/genres_plugin plugins/lameinfo_plugin plugins/nfo_plugin plugins/pymod_plugin plugins/stats_plugin plugins/xep118_plugin .. _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 is where the plugin should does whatever interesting it things it would like to do with the files it is passed. 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` eyeD3-0.8.4/docs/eyed3.mp3.rst0000644000175000017500000000050013153052736016527 0ustar travistravis00000000000000eyed3.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.8.4/docs/contributing.rst0000644000175000017500000000004113153052736017527 0ustar travistravis00000000000000.. include:: ../CONTRIBUTING.rst eyeD3-0.8.4/docs/index.rst0000644000175000017500000000322613161505634016136 0ustar travistravis00000000000000================ 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 Mercurial 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 compliance modules 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.8.4/docs/eyed3.plugins.rst0000644000175000017500000000347113153052736017523 0ustar travistravis00000000000000eyed3.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.display module ---------------------------- .. automodule:: eyed3.plugins.display :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.lameinfo module ----------------------------- .. automodule:: eyed3.plugins.lameinfo :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: Module contents --------------- .. automodule:: eyed3.plugins :members: :undoc-members: :show-inheritance: eyeD3-0.8.4/docs/conf.py0000644000175000017500000002125313177420270015573 0ustar travistravis00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # 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("./src", 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.__about__ 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'] extensions.append("sphinxcontrib.bitbucket") bitbucket_project_url = 'https://bitbucket.org/nicfit/eyed3' extensions.append("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. # version = __version__ 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.8.4/docs/eyeD3.10000644000175000017500000004064513153052736015337 0ustar travistravis00000000000000.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.8.4/docs/eyed3.utils.rst0000644000175000017500000000161113153052736017174 0ustar travistravis00000000000000eyed3.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.8.4/docs/authors.rst0000644000175000017500000000003413153052736016507 0ustar travistravis00000000000000.. include:: ../AUTHORS.rst eyeD3-0.8.4/docs/compliance.rst0000644000175000017500000000672213061344514017142 0ustar travistravis00000000000000########## 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.8.4/MANIFEST.in0000644000175000017500000000107513153052736015104 0ustar travistravis00000000000000include 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 src/test *.py exclude .cookiecutter.yml exclude .gitchangelog.rc global-exclude __pycache__ global-exclude .editorconfig global-exclude *.py[co] include requirements.txt recursive-include requirements *.txt *.yml recursive-include src/eyed3 *.py include src/eyed3/plugins/DisplayPattern.ebnf recursive-include examples * exclude fabfile.py exclude mkenv.sh exclude pavement.py prune etc eyeD3-0.8.4/eyeD3.egg-info/0000755000175000017500000000000013203726215016002 5ustar travistravis00000000000000eyeD3-0.8.4/eyeD3.egg-info/SOURCES.txt0000644000175000017500000000676213203726215017701 0ustar travistravis00000000000000AUTHORS.rst CONTRIBUTING.rst HISTORY.rst LICENSE MANIFEST.in Makefile README.rst requirements.txt setup.cfg setup.py tox.ini ./src/eyed3/__about__.py ./src/eyed3/__init__.py ./src/eyed3/compat.py ./src/eyed3/core.py ./src/eyed3/main.py ./src/eyed3/id3/__init__.py ./src/eyed3/id3/apple.py ./src/eyed3/id3/frames.py ./src/eyed3/id3/headers.py ./src/eyed3/id3/tag.py ./src/eyed3/mp3/__init__.py ./src/eyed3/mp3/headers.py ./src/eyed3/plugins/__init__.py ./src/eyed3/plugins/_display_parser.py ./src/eyed3/plugins/art.py ./src/eyed3/plugins/classic.py ./src/eyed3/plugins/display.py ./src/eyed3/plugins/fixup.py ./src/eyed3/plugins/genres.py ./src/eyed3/plugins/itunes.py ./src/eyed3/plugins/lameinfo.py ./src/eyed3/plugins/nfo.py ./src/eyed3/plugins/pymod.py ./src/eyed3/plugins/stats.py ./src/eyed3/plugins/xep_118.py ./src/eyed3/utils/__init__.py ./src/eyed3/utils/art.py ./src/eyed3/utils/binfuncs.py ./src/eyed3/utils/console.py ./src/eyed3/utils/log.py ./src/eyed3/utils/prompt.py docs/Makefile docs/_config.yml docs/authors.rst docs/cli.rst docs/compliance.rst docs/conf.py docs/contributing.rst docs/eyeD3.1 docs/eyed3.id3.rst docs/eyed3.mp3.rst docs/eyed3.plugins.rst docs/eyed3.rst docs/eyed3.utils.rst docs/history.rst docs/index.rst docs/installation.rst docs/make.bat docs/modules.rst docs/readme.rst docs/usage.rst docs/_static/.keep docs/_static/rtd.css docs/_templates/.keepme docs/plugins/classic_plugin.rst docs/plugins/display_plugin.rst docs/plugins/fixup_plugin.rst docs/plugins/genres_plugin.rst docs/plugins/itunes_plugin.rst docs/plugins/lameinfo_plugin.rst docs/plugins/nfo_plugin.rst docs/plugins/pymod_plugin.rst docs/plugins/stats_plugin.rst docs/plugins/xep118_plugin.rst examples/chapters.py examples/cli_examples.sh examples/config.ini examples/tag_example.py examples/plugins/echo.py examples/plugins/echo2.py eyeD3.egg-info/PKG-INFO eyeD3.egg-info/SOURCES.txt eyeD3.egg-info/dependency_links.txt eyeD3.egg-info/entry_points.txt eyeD3.egg-info/not-zip-safe eyeD3.egg-info/requires.txt eyeD3.egg-info/top_level.txt requirements/dev.txt requirements/extra_display-plugin.txt requirements/main.txt requirements/requirements.yml requirements/test.txt requirements/test_py33.txt src/eyed3/__about__.py src/eyed3/__init__.py src/eyed3/compat.py src/eyed3/core.py src/eyed3/main.py src/eyed3/id3/__init__.py src/eyed3/id3/apple.py src/eyed3/id3/frames.py src/eyed3/id3/headers.py src/eyed3/id3/tag.py src/eyed3/mp3/__init__.py src/eyed3/mp3/headers.py src/eyed3/plugins/DisplayPattern.ebnf src/eyed3/plugins/__init__.py src/eyed3/plugins/_display_parser.py src/eyed3/plugins/art.py src/eyed3/plugins/classic.py src/eyed3/plugins/display.py src/eyed3/plugins/fixup.py src/eyed3/plugins/genres.py src/eyed3/plugins/itunes.py src/eyed3/plugins/lameinfo.py src/eyed3/plugins/nfo.py src/eyed3/plugins/pymod.py src/eyed3/plugins/stats.py src/eyed3/plugins/xep_118.py src/eyed3/utils/__init__.py src/eyed3/utils/art.py src/eyed3/utils/binfuncs.py src/eyed3/utils/console.py src/eyed3/utils/log.py src/eyed3/utils/prompt.py src/test/__init__.py src/test/compat.py src/test/conftest.py src/test/test__init__.py src/test/test_binfuncs.py src/test/test_classic_plugin.py src/test/test_core.py src/test/test_display_plugin.py src/test/test_factory.py src/test/test_main.py src/test/test_plugins.py src/test/test_stats_plugins.py src/test/test_utils.py src/test/id3/__init__.py src/test/id3/test_frames.py src/test/id3/test_headers.py src/test/id3/test_id3.py src/test/id3/test_tag.py src/test/mp3/__init__.py src/test/mp3/test_infos.py src/test/mp3/test_mp3.pyeyeD3-0.8.4/eyeD3.egg-info/requires.txt0000644000175000017500000000006113203726215020377 0ustar travistravis00000000000000six pathlib python-magic [display-plugin] grako eyeD3-0.8.4/eyeD3.egg-info/PKG-INFO0000644000175000017500000010367413203726215017112 0ustar travistravis00000000000000Metadata-Version: 1.1 Name: eyeD3 Version: 0.8.4 Summary: Python audio data toolkit (ID3 and MP3) Home-page: http://eyed3.nicfit.net/ Author: Travis Shirk Author-email: travis@pobox.com License: GNU GPL v3.0 Download-URL: https://github.com/nicfit/eyeD3/releases/downloads/v0.8.4/eyeD3-0.8.4.tar.gz Description-Content-Type: UNKNOWN Description: Status ------ .. 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 = u"Integrity" audiofile.tag.album = u"Humanity Is The Devil" audiofile.tag.album_artist = u"Integrity" audiofile.tag.title = u"Hollow" audiofile.tag.track_num = 2 audiofile.tag.save() eyeD3_ is written and maintained by `Travis Shirk`_ and is licensed under version 3 of the GPL_. Features -------- * Python package for writing application and/or plugins. * Command-line tool driver script that supports plugins. viewer/editor interface. * Easy editing/viewing of audio metadata from the command-line, using the 'classic' plugin. * 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 2.7, >= 3.3 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://bitbucket.org/nicfit/eyed3/issues?status=new&status=open .. _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/ Release History =============== 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: * [:bbissue:`78`] - 'NoneType' object has no attribute 'year' * [:bbissue:`108`] - Multiple date related fixes. * [:bbissue:`110`] - Allow superfluous --no-tagging-ttme-frame option for backward compatibility. * [:bbissue:`111`] - The --version option now prints a short, version-only, message. * [:bbissue:`116`] - Allow --year option for backward compatibility. Converts to --release-year. * [:bbissue:`117`] - Fixes for --user-text-frame with multiple colons and similar fixes. * [:bbissue:`125`] - ID3 v1.1 encoding fixes. .. _release-0.7.10: 0.7.10 - 12.10.2016 (Hollow) --------------------------------- Bug Fixes: * [:bbissue:`97`] - Missing import * [:bbissue:`105`] - 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. * [:bbissue:`81`] - Replaced invalid Unicode. * [:bbissue:`91`] - Disabled ANSI codes on Windows * [:bbissue:`92`] - 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: * [:bbissue:`50`] Crashing on --remove-frame PRIV * [:bbissue:`75`] Parse lameinfo even if crc16 is not correct * [:bbissue:`77`] Typo in docs/installation.rst * [:bbissue:`79`] 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: * [:bbissue:`49`] 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. * [:bbissue:`9`] 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. * [:bbissue:`46`] Disable ANSI color codes when TERM == "dumb" * [:bbissue:`47`] Locking around libmagic. * [:bbissue:`54`] Work around for zero-padded utf16 strings. * [:bbissue:`65`] Safer tempfile usage. * [:bbissue:`65`] 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`_. * [:bbissue:`28`] [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. * [:bbissue:`10`] Magic module `hasattr` fix. * [:bbissue:`12`] More robust handling of bogus play count values. * [:bbissue:`13`] More robust handling of bogus date values. * [:bbissue:`18`] Proper unicode handling of APIC descriptions. * [:bbissue:`19`] Proper use of argparse.ArgumentTypeError * [:bbissue:`26`] Allow TCMP frames when parsing. * [:bbissue:`30`] Accept more invalid frame types (iTunes) * [:bbissue:`31`] Documentation fixes. * [:bbissue:`31`] Fix for bash completion script. * [:bbissue:`32`] 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: * [:bbissue:`5`] 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'. * [:bbissue:`6`] 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. Keywords: id3,mp3,python Platform: Any Classifier: Environment :: Console Classifier: Intended Audience :: End Users/Desktop Classifier: Topic :: Multimedia :: Sound/Audio :: Editors Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Intended Audience :: Developers Classifier: Operating System :: POSIX Classifier: Natural Language :: English Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Development Status :: 5 - Production/Stable eyeD3-0.8.4/eyeD3.egg-info/entry_points.txt0000644000175000017500000000005413203726215021277 0ustar travistravis00000000000000[console_scripts] eyeD3 = eyed3.main:_main eyeD3-0.8.4/eyeD3.egg-info/top_level.txt0000644000175000017500000000000613203726215020530 0ustar travistravis00000000000000eyed3 eyeD3-0.8.4/eyeD3.egg-info/dependency_links.txt0000644000175000017500000000000113203726215022050 0ustar travistravis00000000000000 eyeD3-0.8.4/eyeD3.egg-info/not-zip-safe0000644000175000017500000000000113203726214020227 0ustar travistravis00000000000000 eyeD3-0.8.4/examples/0000755000175000017500000000000013203726215015155 5ustar travistravis00000000000000eyeD3-0.8.4/examples/plugins/0000755000175000017500000000000013203726215016636 5ustar travistravis00000000000000eyeD3-0.8.4/examples/plugins/echo.py0000644000175000017500000000055513041005225020122 0ustar travistravis00000000000000from __future__ import print_function import eyed3 from eyed3.plugins import Plugin from eyed3.utils import guessMimetype eyed3.require((0, 7)) 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.8.4/examples/plugins/echo2.py0000644000175000017500000000111613041005225020176 0ustar travistravis00000000000000# -*- coding: utf-8 -*- from __future__ import print_function import eyed3 from eyed3.plugins import LoaderPlugin eyed3.require((0, 7)) 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.8.4/examples/tag_example.py0000755000175000017500000001541313041005225020013 0ustar travistravis00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2012 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 ################################################################################ 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 = u"M.O.P." t.title = u"How About Some Hardcore" t.album = u"To The Death" t.genre = u"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(u"Gritty, yo!") t.comments.set(u"Brownsville, Brooklyn", u"Origin") t.user_text_frames.set(u"****", u"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("Secrets", "Billy Danzenie") t.terms_of_use = u"Blunted" t.lyrics.set(u""" [ 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.8.4/examples/cli_examples.sh0000755000175000017500000000355413041005225020157 0ustar travistravis00000000000000#!/bin/bash shopt -s expand_aliases alias eyeD3='eyeD3 --no-color --no-config' # [[[section SETUP]]] rm -f example.id3 touch example.id3 ls -o 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 src/test/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.8.4/examples/chapters.py0000755000175000017500000000651013041005225017334 0ustar travistravis00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- ################################################################################ # Copyright (C) 2012 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 ################################################################################ from __future__ import print_function 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.8.4/examples/config.ini0000644000175000017500000000062113041005225017111 0ustar travistravis00000000000000# 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.8.4/setup.py0000755000175000017500000001400713203715230015051 0ustar travistravis00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import io import os import re import sys import warnings from setuptools import setup, find_packages from setuptools.command.install import install 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 :: 2.7", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", ] def getPackageInfo(): info_dict = {} info_keys = ["version", "name", "author", "author_email", "url", "license", "description", "release_name", "github_url"] key_remap = {"name": "pypi_name"} # __about__ info_fpath = os.path.join(os.path.abspath(os.path.dirname(__file__)), "src", "eyed3", "__about__.py") with io.open(info_fpath, encoding='utf-8') as infof: for line in infof: for what in info_keys: rex = re.compile(r"__{what}__\s*=\s*['\"](.*?)['\"]" .format(what=what if what not in key_remap else key_remap[what])) m = rex.match(line.strip()) if not m: continue info_dict[what] = m.groups()[0] if sys.version_info[:2] >= (3, 4): vparts = info_dict["version"].split("-", maxsplit=1) else: vparts = info_dict["version"].split("-", 1) info_dict["release"] = vparts[1] if len(vparts) > 1 else "final" # Requirements requirements, extras = requirements_yaml() info_dict["install_requires"] = requirements["main"] \ if "main" in requirements else [] info_dict["tests_require"] = requirements["test"] \ if "test" in requirements else [] info_dict["extras_require"] = extras # Info readme = "" if os.path.exists("README.rst"): with io.open("README.rst", encoding='utf-8') as readme_file: readme = readme_file.read() history = "" if os.path.exists("HISTORY.rst"): with io.open("HISTORY.rst", encoding='utf-8') as history_file: history = history_file.read().replace(".. :changelog:", "") info_dict["long_description"] = readme + "\n\n" + history return info_dict, requirements def requirements_yaml(): prefix = "extra_" reqs = {} reqfile = os.path.join("requirements", "requirements.yml") if os.path.exists(reqfile): with io.open(reqfile, encoding='utf-8') as fp: curr = None for line in [l for l in [l.strip() for l in fp.readlines()] if l and not l.startswith("#")]: if curr is None or line[0] != "-": curr = line.split(":")[0] reqs[curr] = [] else: assert line[0] == "-" r = line[1:].strip().split()[0] if r: reqs[curr].append(r.strip()) return (reqs, {x[len(prefix):]: vals for x, vals in reqs.items() if x.startswith(prefix)}) class PipInstallCommand(install, object): def run(self): reqs = " ".join(["'%s'" % r for r in PKG_INFO["install_requires"]]) os.system("pip install " + reqs) # XXX: py27 compatible return super(PipInstallCommand, self).run() PKG_INFO, REQUIREMENTS = getPackageInfo() if PKG_INFO["release"].startswith("a"): #classifiers.append("Development Status :: 1 - Planning") #classifiers.append("Development Status :: 2 - Pre-Alpha") classifiers.append("Development Status :: 3 - Alpha") elif PKG_INFO["release"].startswith("b"): classifiers.append("Development Status :: 4 - Beta") else: classifiers.append("Development Status :: 5 - Production/Stable") #classifiers.append("Development Status :: 6 - Mature") #classifiers.append("Development Status :: 7 - Inactive") gz = "{name}-{version}.tar.gz".format(**PKG_INFO) PKG_INFO["download_url"] = ( "{github_url}/releases/downloads/v{version}/{gz}" .format(gz=gz, **PKG_INFO) ) def package_files(directory, prefix=".."): paths = [] for (path, _, filenames) in os.walk(directory): if "__pycache__" in path: continue for filename in filenames: if filename.endswith(".pyc"): continue paths.append(os.path.join(prefix, path, filename)) return paths if sys.argv[1:] and sys.argv[1] == "--release-name": print(PKG_INFO["release_name"]) sys.exit(0) else: test_requirements = REQUIREMENTS["test"] if sys.version_info[:2] < (3, 4): test_requirements += REQUIREMENTS["test_py33"] # The extra command line options we added cause warnings, quell that. with warnings.catch_warnings(): warnings.filterwarnings("ignore", message="Unknown distribution option") warnings.filterwarnings("ignore", message="Normalizing") setup(classifiers=classifiers, package_dir={"eyed3": "./src/eyed3"}, packages=find_packages("./src", exclude=["test", "test.*"]), zip_safe=False, platforms=["Any"], keywords=["id3", "mp3", "python"], test_suite="./src/tests", include_package_data=True, package_data={}, entry_points={ "console_scripts": [ "eyeD3 = eyed3.main:_main", ] }, cmdclass={ "install": PipInstallCommand, }, **PKG_INFO ) eyeD3-0.8.4/HISTORY.rst0000644000175000017500000005743613203724503015247 0ustar travistravis00000000000000Release History =============== .. :changelog: 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: * [:bbissue:`78`] - 'NoneType' object has no attribute 'year' * [:bbissue:`108`] - Multiple date related fixes. * [:bbissue:`110`] - Allow superfluous --no-tagging-ttme-frame option for backward compatibility. * [:bbissue:`111`] - The --version option now prints a short, version-only, message. * [:bbissue:`116`] - Allow --year option for backward compatibility. Converts to --release-year. * [:bbissue:`117`] - Fixes for --user-text-frame with multiple colons and similar fixes. * [:bbissue:`125`] - ID3 v1.1 encoding fixes. .. _release-0.7.10: 0.7.10 - 12.10.2016 (Hollow) --------------------------------- Bug Fixes: * [:bbissue:`97`] - Missing import * [:bbissue:`105`] - 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. * [:bbissue:`81`] - Replaced invalid Unicode. * [:bbissue:`91`] - Disabled ANSI codes on Windows * [:bbissue:`92`] - 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: * [:bbissue:`50`] Crashing on --remove-frame PRIV * [:bbissue:`75`] Parse lameinfo even if crc16 is not correct * [:bbissue:`77`] Typo in docs/installation.rst * [:bbissue:`79`] 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: * [:bbissue:`49`] 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. * [:bbissue:`9`] 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. * [:bbissue:`46`] Disable ANSI color codes when TERM == "dumb" * [:bbissue:`47`] Locking around libmagic. * [:bbissue:`54`] Work around for zero-padded utf16 strings. * [:bbissue:`65`] Safer tempfile usage. * [:bbissue:`65`] 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`_. * [:bbissue:`28`] [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. * [:bbissue:`10`] Magic module `hasattr` fix. * [:bbissue:`12`] More robust handling of bogus play count values. * [:bbissue:`13`] More robust handling of bogus date values. * [:bbissue:`18`] Proper unicode handling of APIC descriptions. * [:bbissue:`19`] Proper use of argparse.ArgumentTypeError * [:bbissue:`26`] Allow TCMP frames when parsing. * [:bbissue:`30`] Accept more invalid frame types (iTunes) * [:bbissue:`31`] Documentation fixes. * [:bbissue:`31`] Fix for bash completion script. * [:bbissue:`32`] 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: * [:bbissue:`5`] 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'. * [:bbissue:`6`] 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.