├── mutagen ├── py.typed ├── _tools │ ├── __init__.py │ ├── mutagen_inspect.py │ ├── _util.py │ ├── moggsplit.py │ ├── mutagen_pony.py │ └── mid3cp.py ├── mp4 │ └── _util.py ├── __init__.py ├── _riff.py ├── m4a.py ├── trueaudio.py ├── optimfrog.py ├── monkeysaudio.py ├── _constants.py ├── _tags.py ├── id3 │ └── __init__.py └── wavpack.py ├── tests ├── data │ ├── emptyfile.mp3 │ ├── almostempty.mpc │ ├── dsd.wv │ ├── adif.aac │ ├── alac.m4a │ ├── click.mpc │ ├── empty.aac │ ├── empty.ofr │ ├── empty.ofs │ ├── empty.ogg │ ├── empty.spx │ ├── empty.tta │ ├── ep7.m4b │ ├── ep9.m4b │ ├── image.jpg │ ├── lame.mp3 │ ├── vbri.mp3 │ ├── xing.mp3 │ ├── mac-396.ape │ ├── mac-399.ape │ ├── no-tags.3g2 │ ├── no-tags.m4a │ ├── no-tags.mp3 │ ├── sample.mid │ ├── bad-xing.mp3 │ ├── empty.oggflac │ ├── example.opus │ ├── has-tags.m4a │ ├── has-tags.tak │ ├── issue_21.id3 │ ├── issue_29.wma │ ├── lame-peak.mp3 │ ├── no-tags.flac │ ├── no_length.wv │ ├── oldtag.apev2 │ ├── silence-1.wma │ ├── silence-2.wma │ ├── silence-3.wma │ ├── sv4_header.mpc │ ├── sv5_header.mpc │ ├── sv8_header.mpc │ ├── too-short.mp3 │ ├── with-id3.aif │ ├── 64bit.mp4 │ ├── brokentag.apev2 │ ├── id3v22-test.mp3 │ ├── mac-390-hdr.ape │ ├── multiplexed.spx │ ├── sample.oggtheora │ ├── silence-44-s.ac3 │ ├── silence-44-s.mp3 │ ├── silence-44-s.tak │ ├── silence-44-s.wv │ ├── apev2-lyricsv2.mp3 │ ├── bad-POPM-frame.mp3 │ ├── bad-TYER-frame.mp3 │ ├── covr-with-name.m4a │ ├── id3v23_unsynch.id3 │ ├── lame397v9short.mp3 │ ├── multipage-setup.ogg │ ├── nero-chapters.m4b │ ├── ooming-header.flac │ ├── silence-44-s-v1.mp3 │ ├── silence-44-s.eac3 │ ├── silence-44-s.flac │ ├── truncated-64bit.mp4 │ ├── variable-block.flac │ ├── 8k-1ch-1s-silence.aif │ ├── 8k-4ch-1s-silence.aif │ ├── flac_application.flac │ ├── id3v1v2-combined.mp3 │ ├── multipagecomment.ogg │ ├── 11k-1ch-2s-silence.aif │ ├── 48k-2ch-s16-silence.aif │ ├── 8k-1ch-3.5s-silence.aif │ ├── 97-unknown-23-update.mp3 │ ├── sample_bitrate.oggtheora │ ├── sample_length.oggtheora │ ├── silence-2s-44100-16.ofr │ ├── silence-2s-44100-16.ofs │ ├── silence-44-s-mpeg2.mp3 │ ├── silence-44-s-mpeg25.mp3 │ ├── 106-invalid-streaminfo.flac │ ├── 145-invalid-item-count.apev2 │ ├── 52-overwritten-metadata.flac │ ├── 52-too-short-block-size.flac │ ├── 5644800-2ch-s01-silence.dff │ ├── 5644800-2ch-s01-silence.dsf │ ├── 5644800-2ch-s01-silence-dst.dff │ ├── 106-short-picture-block-size.flac │ ├── audacious-trailing-id32-apev2.mp3 │ ├── audacious-trailing-id32-id31.mp3 │ ├── silence-2s-PCM-16000-08-ID3v23.wav │ ├── silence-2s-PCM-16000-08-notags.wav │ ├── silence-2s-PCM-44100-16-ID3v23.wav │ ├── 2822400-1ch-0s-silence.dff │ ├── id3v24_extended_header.id3 │ ├── with-id3.dsf │ ├── without-id3.dsf │ └── 2822400-1ch-0s-silence.dsf ├── test__iff.py ├── test_tools_mutagen_pony.py ├── test_tools_mutagen_inspect.py ├── test_smf.py ├── test_m4a.py ├── test_tools_moggsplit.py ├── test_tools.py ├── test_tools_util.py ├── test_ac3.py ├── test_trueaudio.py ├── test_tak.py ├── test_monkeysaudio.py ├── test_oggspeex.py ├── test_optimfrog.py ├── test_oggtheora.py ├── test_wavpack.py ├── test_oggopus.py ├── test_dsdiff.py ├── test_tools_mid3iconv.py ├── test_aac.py ├── __init__.py ├── test_oggflac.py ├── test_dsf.py └── test_musepack.py ├── .codecov.yml ├── fuzzing ├── .gitignore ├── check_crashes.sh ├── sut.py ├── run.sh ├── README.rst └── fuzztools.py ├── docs ├── requirements.txt ├── images │ ├── favicon.ico │ └── Makefile ├── changelog.rst ├── dev │ ├── index.rst │ └── id3.rst ├── api │ ├── ogg.rst │ ├── vcomment.rst │ ├── tak.rst │ ├── dsf.rst │ ├── aiff.rst │ ├── dsdiff.rst │ ├── smf.rst │ ├── wave.rst │ ├── wavpack.rst │ ├── oggflac.rst │ ├── optimfrog.rst │ ├── oggopus.rst │ ├── musepack.rst │ ├── oggspeex.rst │ ├── oggtheora.rst │ ├── oggvorbis.rst │ ├── aac.rst │ ├── ac3.rst │ ├── monkeysaudio.rst │ ├── trueaudio.rst │ ├── mp3.rst │ ├── index.rst │ ├── ape.rst │ ├── flac.rst │ ├── mp4.rst │ ├── id3.rst │ ├── asf.rst │ └── base.rst ├── man │ ├── Makefile │ ├── mutagen-pony.rst │ ├── mutagen-inspect.rst │ ├── index.rst │ ├── mid3iconv.rst │ ├── mid3cp.rst │ ├── moggsplit.rst │ └── mid3v2.rst ├── user │ ├── index.rst │ ├── mp4.rst │ ├── apev2.rst │ ├── padding.rst │ ├── filelike.rst │ ├── gettingstarted.rst │ ├── classes.rst │ ├── vcomment.rst │ └── examples │ │ └── fileobj-iface.py ├── Makefile ├── contact.rst ├── extra.css ├── conf.py ├── id3_frames_gen.py └── index.rst ├── .flake8 ├── .mypy.ini ├── .readthedocs.yaml ├── .gitignore ├── MANIFEST.in ├── README.rst ├── man ├── mutagen-pony.1 ├── mutagen-inspect.1 ├── mid3iconv.1 ├── moggsplit.1 └── mid3cp.1 ├── pyproject.toml └── .github └── workflows └── test.yml /mutagen/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/emptyfile.mp3: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /fuzzing/.gitignore: -------------------------------------------------------------------------------- 1 | _results 2 | _examples* -------------------------------------------------------------------------------- /tests/data/almostempty.mpc: -------------------------------------------------------------------------------- 1 | ID3 dsfdsfds MP+ 2 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=7.0,<8.0 2 | sphinx-rtd-theme>=2.0,<3.0 -------------------------------------------------------------------------------- /tests/data/dsd.wv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/dsd.wv -------------------------------------------------------------------------------- /tests/data/adif.aac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/adif.aac -------------------------------------------------------------------------------- /tests/data/alac.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/alac.m4a -------------------------------------------------------------------------------- /tests/data/click.mpc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/click.mpc -------------------------------------------------------------------------------- /tests/data/empty.aac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/empty.aac -------------------------------------------------------------------------------- /tests/data/empty.ofr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/empty.ofr -------------------------------------------------------------------------------- /tests/data/empty.ofs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/empty.ofs -------------------------------------------------------------------------------- /tests/data/empty.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/empty.ogg -------------------------------------------------------------------------------- /tests/data/empty.spx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/empty.spx -------------------------------------------------------------------------------- /tests/data/empty.tta: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/empty.tta -------------------------------------------------------------------------------- /tests/data/ep7.m4b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/ep7.m4b -------------------------------------------------------------------------------- /tests/data/ep9.m4b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/ep9.m4b -------------------------------------------------------------------------------- /tests/data/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/image.jpg -------------------------------------------------------------------------------- /tests/data/lame.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/lame.mp3 -------------------------------------------------------------------------------- /tests/data/vbri.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/vbri.mp3 -------------------------------------------------------------------------------- /tests/data/xing.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/xing.mp3 -------------------------------------------------------------------------------- /tests/data/mac-396.ape: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/mac-396.ape -------------------------------------------------------------------------------- /tests/data/mac-399.ape: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/mac-399.ape -------------------------------------------------------------------------------- /tests/data/no-tags.3g2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/no-tags.3g2 -------------------------------------------------------------------------------- /tests/data/no-tags.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/no-tags.m4a -------------------------------------------------------------------------------- /tests/data/no-tags.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/no-tags.mp3 -------------------------------------------------------------------------------- /tests/data/sample.mid: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/sample.mid -------------------------------------------------------------------------------- /docs/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/docs/images/favicon.ico -------------------------------------------------------------------------------- /tests/data/bad-xing.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/bad-xing.mp3 -------------------------------------------------------------------------------- /tests/data/empty.oggflac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/empty.oggflac -------------------------------------------------------------------------------- /tests/data/example.opus: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/example.opus -------------------------------------------------------------------------------- /tests/data/has-tags.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/has-tags.m4a -------------------------------------------------------------------------------- /tests/data/has-tags.tak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/has-tags.tak -------------------------------------------------------------------------------- /tests/data/issue_21.id3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/issue_21.id3 -------------------------------------------------------------------------------- /tests/data/issue_29.wma: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/issue_29.wma -------------------------------------------------------------------------------- /tests/data/lame-peak.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/lame-peak.mp3 -------------------------------------------------------------------------------- /tests/data/no-tags.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/no-tags.flac -------------------------------------------------------------------------------- /tests/data/no_length.wv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/no_length.wv -------------------------------------------------------------------------------- /tests/data/oldtag.apev2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/oldtag.apev2 -------------------------------------------------------------------------------- /tests/data/silence-1.wma: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/silence-1.wma -------------------------------------------------------------------------------- /tests/data/silence-2.wma: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/silence-2.wma -------------------------------------------------------------------------------- /tests/data/silence-3.wma: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/silence-3.wma -------------------------------------------------------------------------------- /tests/data/sv4_header.mpc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/sv4_header.mpc -------------------------------------------------------------------------------- /tests/data/sv5_header.mpc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/sv5_header.mpc -------------------------------------------------------------------------------- /tests/data/sv8_header.mpc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/sv8_header.mpc -------------------------------------------------------------------------------- /tests/data/too-short.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/too-short.mp3 -------------------------------------------------------------------------------- /tests/data/with-id3.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/with-id3.aif -------------------------------------------------------------------------------- /tests/data/64bit.mp4: -------------------------------------------------------------------------------- 1 | moovMudta=meta-!ilstcpildata -------------------------------------------------------------------------------- /tests/data/brokentag.apev2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/brokentag.apev2 -------------------------------------------------------------------------------- /tests/data/id3v22-test.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/id3v22-test.mp3 -------------------------------------------------------------------------------- /tests/data/mac-390-hdr.ape: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/mac-390-hdr.ape -------------------------------------------------------------------------------- /tests/data/multiplexed.spx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/multiplexed.spx -------------------------------------------------------------------------------- /tests/data/sample.oggtheora: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/sample.oggtheora -------------------------------------------------------------------------------- /tests/data/silence-44-s.ac3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/silence-44-s.ac3 -------------------------------------------------------------------------------- /tests/data/silence-44-s.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/silence-44-s.mp3 -------------------------------------------------------------------------------- /tests/data/silence-44-s.tak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/silence-44-s.tak -------------------------------------------------------------------------------- /tests/data/silence-44-s.wv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/silence-44-s.wv -------------------------------------------------------------------------------- /tests/data/apev2-lyricsv2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/apev2-lyricsv2.mp3 -------------------------------------------------------------------------------- /tests/data/bad-POPM-frame.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/bad-POPM-frame.mp3 -------------------------------------------------------------------------------- /tests/data/bad-TYER-frame.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/bad-TYER-frame.mp3 -------------------------------------------------------------------------------- /tests/data/covr-with-name.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/covr-with-name.m4a -------------------------------------------------------------------------------- /tests/data/id3v23_unsynch.id3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/id3v23_unsynch.id3 -------------------------------------------------------------------------------- /tests/data/lame397v9short.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/lame397v9short.mp3 -------------------------------------------------------------------------------- /tests/data/multipage-setup.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/multipage-setup.ogg -------------------------------------------------------------------------------- /tests/data/nero-chapters.m4b: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/nero-chapters.m4b -------------------------------------------------------------------------------- /tests/data/ooming-header.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/ooming-header.flac -------------------------------------------------------------------------------- /tests/data/silence-44-s-v1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/silence-44-s-v1.mp3 -------------------------------------------------------------------------------- /tests/data/silence-44-s.eac3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/silence-44-s.eac3 -------------------------------------------------------------------------------- /tests/data/silence-44-s.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/silence-44-s.flac -------------------------------------------------------------------------------- /tests/data/truncated-64bit.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/truncated-64bit.mp4 -------------------------------------------------------------------------------- /tests/data/variable-block.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/variable-block.flac -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | .. py:currentmodule:: mutagen 5 | 6 | .. include:: ../NEWS 7 | -------------------------------------------------------------------------------- /tests/data/8k-1ch-1s-silence.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/8k-1ch-1s-silence.aif -------------------------------------------------------------------------------- /tests/data/8k-4ch-1s-silence.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/8k-4ch-1s-silence.aif -------------------------------------------------------------------------------- /tests/data/flac_application.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/flac_application.flac -------------------------------------------------------------------------------- /tests/data/id3v1v2-combined.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/id3v1v2-combined.mp3 -------------------------------------------------------------------------------- /tests/data/multipagecomment.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/multipagecomment.ogg -------------------------------------------------------------------------------- /tests/data/11k-1ch-2s-silence.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/11k-1ch-2s-silence.aif -------------------------------------------------------------------------------- /tests/data/48k-2ch-s16-silence.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/48k-2ch-s16-silence.aif -------------------------------------------------------------------------------- /tests/data/8k-1ch-3.5s-silence.aif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/8k-1ch-3.5s-silence.aif -------------------------------------------------------------------------------- /tests/data/97-unknown-23-update.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/97-unknown-23-update.mp3 -------------------------------------------------------------------------------- /tests/data/sample_bitrate.oggtheora: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/sample_bitrate.oggtheora -------------------------------------------------------------------------------- /tests/data/sample_length.oggtheora: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/sample_length.oggtheora -------------------------------------------------------------------------------- /tests/data/silence-2s-44100-16.ofr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/silence-2s-44100-16.ofr -------------------------------------------------------------------------------- /tests/data/silence-2s-44100-16.ofs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/silence-2s-44100-16.ofs -------------------------------------------------------------------------------- /tests/data/silence-44-s-mpeg2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/silence-44-s-mpeg2.mp3 -------------------------------------------------------------------------------- /tests/data/silence-44-s-mpeg25.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/silence-44-s-mpeg25.mp3 -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore=E128,W601,E402,E731,W503,E741,E305,E121,E124,W504 3 | exclude=build,dist,.venv 4 | max-line-length=88 5 | -------------------------------------------------------------------------------- /tests/data/106-invalid-streaminfo.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/106-invalid-streaminfo.flac -------------------------------------------------------------------------------- /tests/data/145-invalid-item-count.apev2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/145-invalid-item-count.apev2 -------------------------------------------------------------------------------- /tests/data/52-overwritten-metadata.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/52-overwritten-metadata.flac -------------------------------------------------------------------------------- /tests/data/52-too-short-block-size.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/52-too-short-block-size.flac -------------------------------------------------------------------------------- /tests/data/5644800-2ch-s01-silence.dff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/5644800-2ch-s01-silence.dff -------------------------------------------------------------------------------- /tests/data/5644800-2ch-s01-silence.dsf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/5644800-2ch-s01-silence.dsf -------------------------------------------------------------------------------- /tests/data/5644800-2ch-s01-silence-dst.dff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/5644800-2ch-s01-silence-dst.dff -------------------------------------------------------------------------------- /docs/dev/index.rst: -------------------------------------------------------------------------------- 1 | Developer Guide 2 | =============== 3 | 4 | .. toctree:: 5 | :titlesonly: 6 | :maxdepth: 2 7 | 8 | id3 9 | -------------------------------------------------------------------------------- /tests/data/106-short-picture-block-size.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/106-short-picture-block-size.flac -------------------------------------------------------------------------------- /tests/data/audacious-trailing-id32-apev2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/audacious-trailing-id32-apev2.mp3 -------------------------------------------------------------------------------- /tests/data/audacious-trailing-id32-id31.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/audacious-trailing-id32-id31.mp3 -------------------------------------------------------------------------------- /tests/data/silence-2s-PCM-16000-08-ID3v23.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/silence-2s-PCM-16000-08-ID3v23.wav -------------------------------------------------------------------------------- /tests/data/silence-2s-PCM-16000-08-notags.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/silence-2s-PCM-16000-08-notags.wav -------------------------------------------------------------------------------- /tests/data/silence-2s-PCM-44100-16-ID3v23.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quodlibet/mutagen/HEAD/tests/data/silence-2s-PCM-44100-16-ID3v23.wav -------------------------------------------------------------------------------- /fuzzing/check_crashes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | export PYTHONPATH="$(pwd)/.." 6 | RESULTS=_results 7 | 8 | python fuzztools.py "$RESULTS" -------------------------------------------------------------------------------- /docs/api/ogg.rst: -------------------------------------------------------------------------------- 1 | OGG 2 | === 3 | 4 | .. automodule:: mutagen.ogg 5 | 6 | .. autoclass:: mutagen.ogg.OggFileType 7 | :show-inheritance: 8 | :members: 9 | -------------------------------------------------------------------------------- /tests/data/2822400-1ch-0s-silence.dff: -------------------------------------------------------------------------------- 1 | FRM8rDSD FVERPROPFSND FS +CHNLC CMPRDSD not compressedDSD -------------------------------------------------------------------------------- /docs/man/Makefile: -------------------------------------------------------------------------------- 1 | # update man pages 2 | 3 | all: mid3cp.1 mid3iconv.1 mid3v2.1 moggsplit.1 mutagen-inspect.1 mutagen-pony.1 4 | 5 | %.1:%.rst 6 | rst2man $< > ../../man/$@ 7 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.10 3 | ignore_missing_imports = True 4 | 5 | [mypy-setup] 6 | ignore_errors = True 7 | 8 | [mypy-tests.*] 9 | ignore_errors = True 10 | -------------------------------------------------------------------------------- /docs/api/vcomment.rst: -------------------------------------------------------------------------------- 1 | Vorbis Comment 2 | ============== 3 | 4 | .. autoclass:: mutagen._vorbis.VComment() 5 | :show-inheritance: 6 | 7 | .. autoclass:: mutagen._vorbis.VCommentDict() 8 | :show-inheritance: 9 | -------------------------------------------------------------------------------- /docs/api/tak.rst: -------------------------------------------------------------------------------- 1 | TAK 2 | === 3 | 4 | .. automodule:: mutagen.tak 5 | 6 | .. autoclass:: mutagen.tak.TAK 7 | :show-inheritance: 8 | :members: 9 | 10 | .. autoclass:: mutagen.tak.TAKInfo 11 | :members: 12 | -------------------------------------------------------------------------------- /tests/data/id3v24_extended_header.id3: -------------------------------------------------------------------------------- 1 | ID3@8  GTCOMMThis is a comment!TCONRelaxation..? :)TDRC2023TRCK1TALBMutagen Bug ReportsTIT2One Second of SilenceTPE1 Snild Dolkow -------------------------------------------------------------------------------- /docs/api/dsf.rst: -------------------------------------------------------------------------------- 1 | DSF 2 | --- 3 | 4 | .. automodule:: mutagen.dsf 5 | 6 | .. autoclass:: mutagen.dsf.DSF(filething) 7 | :show-inheritance: 8 | :members: 9 | 10 | .. autoclass:: mutagen.dsf.DSFInfo() 11 | :members: 12 | -------------------------------------------------------------------------------- /tests/test__iff.py: -------------------------------------------------------------------------------- 1 | 2 | from mutagen._iff import is_valid_chunk_id 3 | 4 | 5 | def test_is_valid_chunk_id(): 6 | assert not is_valid_chunk_id("") 7 | assert is_valid_chunk_id("QUUX") 8 | assert not is_valid_chunk_id("FOOBAR") 9 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.10" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /docs/api/aiff.rst: -------------------------------------------------------------------------------- 1 | AIFF 2 | ---- 3 | 4 | .. automodule:: mutagen.aiff 5 | 6 | .. autoclass:: mutagen.aiff.AIFF(filename) 7 | :show-inheritance: 8 | :members: 9 | 10 | .. autoclass:: mutagen.aiff.AIFFInfo() 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/api/dsdiff.rst: -------------------------------------------------------------------------------- 1 | DSDIFF 2 | ====== 3 | 4 | .. automodule:: mutagen.dsdiff 5 | 6 | .. autoclass:: mutagen.dsdiff.DSDIFF 7 | :show-inheritance: 8 | :members: 9 | 10 | .. autoclass:: mutagen.dsdiff.DSDIFFInfo 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/api/smf.rst: -------------------------------------------------------------------------------- 1 | Standard MIDI File 2 | ================== 3 | 4 | .. automodule:: mutagen.smf 5 | 6 | .. autoclass:: mutagen.smf.SMF 7 | :show-inheritance: 8 | :members: 9 | 10 | .. autoclass:: mutagen.smf.SMFInfo 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/api/wave.rst: -------------------------------------------------------------------------------- 1 | WAVE 2 | ---- 3 | 4 | .. automodule:: mutagen.wave 5 | 6 | .. autoclass:: mutagen.wave.WAVE(filename) 7 | :show-inheritance: 8 | :members: 9 | 10 | .. autoclass:: mutagen.wave.WaveStreamInfo() 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/api/wavpack.rst: -------------------------------------------------------------------------------- 1 | WavPack 2 | ======= 3 | 4 | .. automodule:: mutagen.wavpack 5 | 6 | .. autoclass:: mutagen.wavpack.WavPack 7 | :show-inheritance: 8 | :members: 9 | 10 | .. autoclass:: mutagen.wavpack.WavPackInfo 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/api/oggflac.rst: -------------------------------------------------------------------------------- 1 | Ogg FLAC 2 | ======== 3 | 4 | .. automodule:: mutagen.oggflac 5 | 6 | .. autoclass:: mutagen.oggflac.OggFLAC 7 | :show-inheritance: 8 | 9 | .. autoclass:: mutagen.oggflac.OggFLACStreamInfo 10 | :show-inheritance: 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/api/optimfrog.rst: -------------------------------------------------------------------------------- 1 | OptimFROG 2 | ========= 3 | 4 | .. automodule:: mutagen.optimfrog 5 | 6 | .. autoclass:: mutagen.optimfrog.OptimFROG 7 | :show-inheritance: 8 | :members: 9 | 10 | .. autoclass:: mutagen.optimfrog.OptimFROGInfo 11 | :members: 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | docs/_build 3 | docs/_rtd_theme 4 | dist 5 | build 6 | coverage/*.cover 7 | MANIFEST 8 | coverage 9 | .cache 10 | *.egg-info 11 | .coverage 12 | *.iml 13 | .idea 14 | .hypothesis 15 | .pytest_cache 16 | .mypy_cache 17 | /poetry.lock 18 | /uv.lock 19 | -------------------------------------------------------------------------------- /docs/user/index.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | User Guide 3 | ========== 4 | 5 | .. toctree:: 6 | :titlesonly: 7 | :maxdepth: 2 8 | 9 | gettingstarted 10 | classes 11 | filelike 12 | padding 13 | 14 | id3 15 | vcomment 16 | apev2 17 | mp4 18 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | python3 -m sphinx -j auto -b html -n . _build 3 | 4 | clean: 5 | rm -rf _build 6 | 7 | linkcheck: 8 | sphinx-build -b linkcheck -n . _build 9 | 10 | watch: 11 | sphinx-autobuild -j auto -b html -n . _build 12 | 13 | .PHONY: clean watch linkcheck 14 | -------------------------------------------------------------------------------- /docs/api/oggopus.rst: -------------------------------------------------------------------------------- 1 | Ogg Opus 2 | ======== 3 | 4 | .. automodule:: mutagen.oggopus 5 | 6 | .. autoclass:: mutagen.oggopus.OggOpus 7 | :show-inheritance: 8 | :members: 9 | 10 | .. autoclass:: mutagen.oggopus.OggOpusInfo 11 | :show-inheritance: 12 | :members: 13 | -------------------------------------------------------------------------------- /docs/api/musepack.rst: -------------------------------------------------------------------------------- 1 | Musepack 2 | ======== 3 | 4 | .. automodule:: mutagen.musepack 5 | 6 | .. autoclass:: mutagen.musepack.Musepack 7 | :show-inheritance: 8 | :members: 9 | 10 | .. autoclass:: mutagen.musepack.MusepackInfo 11 | :show-inheritance: 12 | :members: 13 | -------------------------------------------------------------------------------- /docs/api/oggspeex.rst: -------------------------------------------------------------------------------- 1 | Ogg Speex 2 | ========= 3 | 4 | .. automodule:: mutagen.oggspeex 5 | 6 | .. autoclass:: mutagen.oggspeex.OggSpeex 7 | :show-inheritance: 8 | :members: 9 | 10 | .. autoclass:: mutagen.oggspeex.OggSpeexInfo 11 | :show-inheritance: 12 | :members: 13 | -------------------------------------------------------------------------------- /docs/api/oggtheora.rst: -------------------------------------------------------------------------------- 1 | Ogg Theora 2 | ========== 3 | 4 | .. automodule:: mutagen.oggtheora 5 | 6 | .. autoclass:: mutagen.oggtheora.OggTheora 7 | :show-inheritance: 8 | :members: 9 | 10 | .. autoclass:: mutagen.oggtheora.OggTheoraInfo 11 | :show-inheritance: 12 | :members: 13 | -------------------------------------------------------------------------------- /docs/api/oggvorbis.rst: -------------------------------------------------------------------------------- 1 | Ogg Vorbis 2 | ---------- 3 | 4 | .. automodule:: mutagen.oggvorbis 5 | 6 | .. autoclass:: mutagen.oggvorbis.OggVorbis 7 | :show-inheritance: 8 | :members: 9 | 10 | .. autoclass:: mutagen.oggvorbis.OggVorbisInfo 11 | :show-inheritance: 12 | :members: 13 | -------------------------------------------------------------------------------- /docs/api/aac.rst: -------------------------------------------------------------------------------- 1 | AAC 2 | === 3 | 4 | .. automodule:: mutagen.aac 5 | 6 | .. autoexception:: mutagen.aac.AACError 7 | 8 | .. autoclass:: mutagen.aac.AAC(filename) 9 | :show-inheritance: 10 | :members: 11 | 12 | .. autoclass:: mutagen.aac.AACInfo() 13 | :show-inheritance: 14 | :members: 15 | -------------------------------------------------------------------------------- /docs/api/ac3.rst: -------------------------------------------------------------------------------- 1 | AC3 2 | === 3 | 4 | .. automodule:: mutagen.ac3 5 | 6 | .. autoexception:: mutagen.ac3.AC3Error 7 | 8 | .. autoclass:: mutagen.ac3.AC3(filename) 9 | :show-inheritance: 10 | :members: 11 | 12 | .. autoclass:: mutagen.ac3.AC3Info() 13 | :show-inheritance: 14 | :members: 15 | -------------------------------------------------------------------------------- /docs/api/monkeysaudio.rst: -------------------------------------------------------------------------------- 1 | Monkey's Audio 2 | ============== 3 | 4 | .. automodule:: mutagen.monkeysaudio 5 | 6 | .. autoclass:: mutagen.monkeysaudio.MonkeysAudio 7 | :show-inheritance: 8 | :members: 9 | 10 | .. autoclass:: mutagen.monkeysaudio.MonkeysAudioInfo 11 | :show-inheritance: 12 | :members: 13 | -------------------------------------------------------------------------------- /mutagen/_tools/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Christoph Reiter 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include COPYING 2 | include NEWS 3 | include README.rst 4 | include .mypy.ini 5 | include .flake8 6 | include MANIFEST.in 7 | include tests/data/* 8 | include tests/*.py 9 | include man/*.1 10 | recursive-include mutagen *.pyi *.typed 11 | recursive-include mutagen README.rst 12 | recursive-include docs *.py Makefile *.rst *.png *.svg *.ico *.css 13 | prune docs/_build 14 | -------------------------------------------------------------------------------- /tests/test_tools_mutagen_pony.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | 4 | from tests.test_tools import _TTools 5 | 6 | 7 | class TMutagenPony(_TTools): 8 | 9 | TOOL_NAME = u"mutagen-pony" 10 | 11 | def test_basic(self): 12 | base = os.path.join('tests', 'data') 13 | res, out = self.call(base) 14 | self.failIf(res) 15 | self.failUnless("Report for %s" % base in out) 16 | -------------------------------------------------------------------------------- /docs/api/trueaudio.rst: -------------------------------------------------------------------------------- 1 | TrueAudio 2 | ========= 3 | 4 | .. automodule:: mutagen.trueaudio 5 | 6 | .. autoclass:: mutagen.trueaudio.TrueAudio 7 | :show-inheritance: 8 | :members: 9 | 10 | .. autoclass:: mutagen.trueaudio.TrueAudioInfo 11 | :members: 12 | 13 | .. autoclass:: mutagen.trueaudio.EasyTrueAudio 14 | :show-inheritance: 15 | :members: 16 | :exclude-members: ID3 17 | -------------------------------------------------------------------------------- /docs/api/mp3.rst: -------------------------------------------------------------------------------- 1 | MP3 2 | === 3 | 4 | .. automodule:: mutagen.mp3 5 | 6 | .. autoclass:: mutagen.mp3.MP3 7 | :show-inheritance: 8 | :members: 9 | 10 | .. autoclass:: mutagen.mp3.MPEGInfo 11 | :show-inheritance: 12 | :members: 13 | 14 | .. autoclass:: mutagen.mp3.BitrateMode 15 | :members: 16 | 17 | .. autoclass:: mutagen.mp3.EasyMP3 18 | :show-inheritance: 19 | :members: 20 | :exclude-members: ID3 21 | -------------------------------------------------------------------------------- /docs/contact.rst: -------------------------------------------------------------------------------- 1 | Contact 2 | ------- 3 | 4 | For historical and practical reasons, Mutagen shares a `mailing list 5 | `_ and IRC channel 6 | (#quodlibet on irc.oftc.net) with Quod Libet. 7 | 8 | If you need help using Mutagen or would like to discuss the library, please 9 | use the IRC channel, our `issue tracker 10 | `_ or the mailing list. 11 | -------------------------------------------------------------------------------- /fuzzing/sut.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import afl 4 | import os 5 | 6 | from fuzztools import run_all 7 | 8 | 9 | def main(): 10 | run_all(b"smoke test") 11 | 12 | buffer = sys.stdin.buffer 13 | while afl.loop(1000): 14 | data = buffer.read() 15 | try: 16 | run_all(data) 17 | finally: 18 | buffer.seek(0) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | os._exit(0) 24 | -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. toctree:: 5 | 6 | base 7 | aac 8 | ac3 9 | aiff 10 | ape 11 | asf 12 | dsdiff 13 | dsf 14 | flac 15 | id3 16 | monkeysaudio 17 | mp3 18 | mp4 19 | musepack 20 | ogg 21 | oggflac 22 | oggopus 23 | oggspeex 24 | oggtheora 25 | oggvorbis 26 | optimfrog 27 | smf 28 | tak 29 | trueaudio 30 | vcomment 31 | wave 32 | wavpack 33 | -------------------------------------------------------------------------------- /docs/api/ape.rst: -------------------------------------------------------------------------------- 1 | APEv2 2 | ===== 3 | 4 | .. automodule:: mutagen.apev2 5 | 6 | .. autoexception:: mutagen.apev2.error 7 | 8 | .. autoexception:: mutagen.apev2.APENoHeaderError 9 | 10 | .. autoexception:: mutagen.apev2.APEUnsupportedVersionError 11 | 12 | .. autoexception:: mutagen.apev2.APEBadItemError 13 | 14 | .. autoclass:: mutagen.apev2.APEv2File 15 | :show-inheritance: 16 | :members: 17 | 18 | .. autoclass:: mutagen.apev2.APEv2 19 | :members: 20 | 21 | :bases: `mutagen.Metadata` 22 | -------------------------------------------------------------------------------- /docs/man/mutagen-pony.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | mutagen-pony 3 | ============== 4 | 5 | --------------------------------- 6 | scan a collection of MP3 files 7 | --------------------------------- 8 | 9 | :Manual section: 1 10 | 11 | 12 | SYNOPSIS 13 | ======== 14 | 15 | **mutagen-pony** *directory* ... 16 | 17 | 18 | DESCRIPTION 19 | =========== 20 | 21 | **mutagen-pony** scans any directories given and reports on the kinds of 22 | tags in the MP3s it finds in them. Ride the pony. 23 | 24 | It is primarily intended as a debugging tool for Mutagen. 25 | 26 | 27 | AUTHORS 28 | ======= 29 | 30 | Michael Urman and Joe Wreschnig 31 | -------------------------------------------------------------------------------- /docs/man/mutagen-inspect.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | mutagen-inspect 3 | ================= 4 | 5 | --------------------------------- 6 | view Mutagen-supported audio tags 7 | --------------------------------- 8 | 9 | :Manual section: 1 10 | 11 | 12 | SYNOPSIS 13 | ======== 14 | 15 | **mutagen-inspect** *filename* ... 16 | 17 | 18 | DESCRIPTION 19 | =========== 20 | 21 | **mutagen-inspect** loads and prints information about an audio file and 22 | its tags. 23 | 24 | It is primarily intended as a debugging tool for Mutagen, but can be useful 25 | for extracting tags from the command line. 26 | 27 | 28 | AUTHOR 29 | ====== 30 | 31 | Joe Wreschnig 32 | -------------------------------------------------------------------------------- /docs/user/mp4.rst: -------------------------------------------------------------------------------- 1 | === 2 | MP4 3 | === 4 | 5 | There is no MPEG-4 iTunes metadata standard. Mutagen's features are 6 | known to lead to problems in other implementations. For example, FAAD 7 | will crash when reading a file with multiple "tmpo" atoms. iTunes 8 | itself is our main compatibility target. 9 | 10 | 11 | Compatibility / Bugs 12 | ^^^^^^^^^^^^^^^^^^^^ 13 | 14 | * Prior to 1.16, the MP4 cover atom used a .format attribute to indicate 15 | the image format (JPEG/PNG). Python 2.6 added a str.format method which 16 | conflicts with this. 1.17 provides .imageformat when running on any version, 17 | and still provides .format when running on a version before 2.6. 18 | -------------------------------------------------------------------------------- /tests/test_tools_mutagen_inspect.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import glob 4 | 5 | from tests.test_tools import _TTools 6 | 7 | 8 | class TMutagenInspect(_TTools): 9 | 10 | TOOL_NAME = u"mutagen-inspect" 11 | 12 | def test_basic(self): 13 | base = os.path.join('tests', 'data') 14 | self.paths = glob.glob(os.path.join(base, "empty*")) 15 | self.paths += glob.glob(os.path.join(base, "silence-*")) 16 | 17 | for path in self.paths: 18 | res, out = self.call(path) 19 | self.failIf(res) 20 | self.failUnless(out.strip()) 21 | self.failIf("Unknown file type" in out) 22 | self.failIf("Errno" in out) 23 | -------------------------------------------------------------------------------- /fuzzing/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | export PYTHONPATH="$(pwd)/.." 6 | export AFL_HANG_TMOUT=1000 7 | export AFL_SKIP_CPUFREQ=1 8 | export AFL_NO_AFFINITY=1 9 | export AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1 10 | 11 | EXAMPLES=_examples 12 | RESULTS=_results 13 | 14 | mkdir -p "$RESULTS" 15 | mkdir -p "$EXAMPLES" 16 | 17 | cp -f ../tests/data/* "$EXAMPLES" 18 | 19 | for i in `seq 2 $(nproc)`; do 20 | py-afl-fuzz -i "$EXAMPLES" -o "$RESULTS" -S "worker-$i" -- $(which python) sut.py > /dev/null 2>&1 & 21 | done 22 | 23 | py-afl-fuzz -i "$EXAMPLES" -o "$RESULTS" -M "main" -- $(which python) sut.py > /dev/null 2>&1 & 24 | watch -n 1 -c afl-whatsup -s "$RESULTS" 25 | pkill afl-fuzz 26 | -------------------------------------------------------------------------------- /docs/api/flac.rst: -------------------------------------------------------------------------------- 1 | FLAC 2 | ==== 3 | 4 | .. currentmodule:: mutagen 5 | 6 | .. automodule:: mutagen.flac 7 | 8 | .. autoclass:: mutagen.flac.FLAC 9 | :show-inheritance: 10 | :members: 11 | :exclude-members: vc, METADATA_BLOCKS, load, add_vorbiscomment 12 | 13 | .. autoclass:: mutagen.flac.StreamInfo 14 | :members: 15 | 16 | :bases: `mutagen.StreamInfo` 17 | 18 | .. autoclass:: mutagen.flac.Picture 19 | :members: 20 | 21 | .. autoclass:: mutagen.flac.CueSheet 22 | :members: 23 | 24 | .. autoclass:: mutagen.flac.CueSheetTrack 25 | :members: 26 | 27 | .. autoclass:: mutagen.flac.CueSheetTrackIndex 28 | :members: 29 | 30 | .. autoclass:: mutagen.flac.SeekTable 31 | :members: 32 | -------------------------------------------------------------------------------- /mutagen/mp4/_util.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2014 Christoph Reiter 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | 8 | from mutagen._util import cdata 9 | 10 | 11 | def parse_full_atom(data): 12 | """Some atoms are versioned. Split them up in (version, flags, payload). 13 | Can raise ValueError. 14 | """ 15 | 16 | if len(data) < 4: 17 | raise ValueError("not enough data") 18 | 19 | version = ord(data[0:1]) 20 | flags = cdata.uint_be(b"\x00" + data[1:4]) 21 | return version, flags, data[4:] 22 | -------------------------------------------------------------------------------- /tests/test_smf.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | 4 | from mutagen.smf import SMF, SMFError 5 | from tests import TestCase, DATA_DIR 6 | 7 | 8 | class TSMF(TestCase): 9 | 10 | def setUp(self): 11 | self.audio = SMF(os.path.join(DATA_DIR, "sample.mid")) 12 | 13 | def test_length(self): 14 | self.failUnlessAlmostEqual(self.audio.info.length, 127.997, 2) 15 | 16 | def test_not_my_file(self): 17 | self.failUnlessRaises( 18 | SMFError, SMF, os.path.join(DATA_DIR, "empty.ogg")) 19 | 20 | def test_pprint(self): 21 | self.audio.pprint() 22 | self.audio.info.pprint() 23 | 24 | def test_mime(self): 25 | self.assertTrue("audio/x-midi" in self.audio.mime) 26 | self.assertTrue("audio/midi" in self.audio.mime) 27 | -------------------------------------------------------------------------------- /docs/api/mp4.rst: -------------------------------------------------------------------------------- 1 | MP4 2 | === 3 | 4 | .. automodule:: mutagen.mp4 5 | 6 | MP4 7 | --- 8 | 9 | .. autoclass:: mutagen.mp4.MP4 10 | :show-inheritance: 11 | :members: 12 | :exclude-members: MP4Tags 13 | 14 | .. autoclass:: mutagen.mp4.MP4Tags 15 | :show-inheritance: 16 | :members: 17 | 18 | .. autoclass:: mutagen.mp4.MP4Info 19 | :show-inheritance: 20 | :members: 21 | 22 | .. autoclass:: mutagen.mp4.MP4Cover 23 | :members: 24 | 25 | .. autoclass:: mutagen.mp4.MP4FreeForm 26 | :members: 27 | 28 | .. autoclass:: mutagen.mp4.AtomDataType 29 | :members: 30 | 31 | 32 | EasyMP4 33 | ------- 34 | 35 | .. automodule:: mutagen.easymp4 36 | 37 | .. autoclass:: mutagen.easymp4.EasyMP4 38 | :show-inheritance: 39 | :members: 40 | :exclude-members: MP4Tags 41 | 42 | 43 | .. autoclass:: mutagen.easymp4.EasyMP4Tags 44 | :show-inheritance: 45 | :members: 46 | -------------------------------------------------------------------------------- /docs/images/Makefile: -------------------------------------------------------------------------------- 1 | SOURCE=logo.svg 2 | DEST=favicon.ico 3 | ARGS=--export-area 19:-4:209:186 --export-png 4 | 5 | all: $(DEST) 6 | 7 | favicon-16.png: $(SOURCE) 8 | inkscape $(SOURCE) $(ARGS) $@ -w 16 -h 16 9 | 10 | favicon-24.png: $(SOURCE) 11 | inkscape $(SOURCE) $(ARGS) $@ -w 24 -h 24 12 | 13 | favicon-32.png: $(SOURCE) 14 | inkscape $(SOURCE) $(ARGS) $@ -w 32 -h 32 15 | 16 | favicon-48.png: $(SOURCE) 17 | inkscape $(SOURCE) $(ARGS) $@ -w 48 -h 48 18 | 19 | favicon-64.png: $(SOURCE) 20 | inkscape $(SOURCE) $(ARGS) $@ -w 64 -h 64 21 | 22 | $(DEST): favicon-16.png favicon-24.png favicon-32.png favicon-48.png favicon-64.png 23 | convert favicon-16.png favicon-24.png favicon-32.png favicon-48.png favicon-64.png $(DEST) 24 | rm favicon-16.png favicon-24.png favicon-32.png favicon-48.png favicon-64.png 25 | 26 | .PHONY: clean 27 | 28 | clean: 29 | rm -f favicon-16.png favicon-24.png favicon-32.png favicon-48.png favicon-64.png favicon.ico 30 | -------------------------------------------------------------------------------- /docs/man/index.rst: -------------------------------------------------------------------------------- 1 | Command Line Tools 2 | ================== 3 | 4 | .. toctree:: 5 | :hidden: 6 | :titlesonly: 7 | 8 | mid3cp 9 | mid3iconv 10 | mid3v2 11 | moggsplit 12 | mutagen-inspect 13 | mutagen-pony 14 | 15 | 16 | In addition to the Python library mutagen installs some command line tools: 17 | 18 | :doc:`mid3cp` 19 | copies the ID3 tags from a source file to a destination file 20 | 21 | :doc:`mid3iconv` 22 | converts ID3 tags from legacy encodings to Unicode and stores them using 23 | the ID3v2 format 24 | 25 | :doc:`mid3v2` 26 | is a Mutagen-based replacement for id3lib’s id3v2 27 | 28 | :doc:`moggsplit` 29 | splits a multiplexed Ogg stream into separate files 30 | 31 | :doc:`mutagen-inspect` 32 | loads and prints information about an audio file and its tags 33 | 34 | :doc:`mutagen-pony` 35 | scans any directories given and reports on the kinds of tags in the MP3s 36 | it finds in them 37 | -------------------------------------------------------------------------------- /fuzzing/README.rst: -------------------------------------------------------------------------------- 1 | Fuzzing 2 | ======= 3 | 4 | Local fuzzing via `python-afl `__ and 5 | `afl++ `__: 6 | 7 | * Install afl, for example ``sudo apt install afl++`` on Debian/Ubuntu 8 | * ``uv sync --group fuzzing`` 9 | * Add some example files into ``_examples`` 10 | * ``uv run ./run.sh`` will start multiple afl-fuzz instances 11 | * CTRL+C to stop 12 | * Run ``uv run ./check_crashes.sh`` to get a summary of the errors found 13 | 14 | Fuzzing via `OSS-Fuzz `__: 15 | 16 | * mutagen was initially added here: https://github.com/google/oss-fuzz/pull/10072 17 | * The integration code is here: https://github.com/google/oss-fuzz/tree/master/projects/mutagen 18 | * Maintainers get emails about new findings, and after 90 days they are made public 19 | * Already public crash results can be found here: https://bugs.chromium.org/p/oss-fuzz/issues/list?q=label:Proj-mutagen 20 | * Once fixed in mutagen, the reports will be closed automatically 21 | -------------------------------------------------------------------------------- /docs/man/mid3iconv.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | mid3iconv 3 | =========== 4 | 5 | ------------------------- 6 | convert ID3 tag encodings 7 | ------------------------- 8 | 9 | :Manual section: 1 10 | 11 | 12 | SYNOPSIS 13 | ======== 14 | 15 | **mid3iconv** [*options*] *filename* ... 16 | 17 | 18 | DESCRIPTION 19 | =========== 20 | 21 | **mid3iconv** converts ID3 tags from legacy encodings to Unicode and stores 22 | them using the ID3v2 format. 23 | 24 | 25 | OPTIONS 26 | ======= 27 | 28 | 29 | --debug, -d 30 | Print updated tags 31 | 32 | --dry-run, -p 33 | Do not actually modify files 34 | 35 | --encoding, -e 36 | Convert from this encoding. By default, your locale's default encoding is 37 | used. 38 | 39 | --force-v1 40 | Use an ID3v1 tag even if an ID3v2 tag is present 41 | 42 | --quiet, -q 43 | Only output errors 44 | 45 | --remove-v1 46 | Remove any ID3v1 tag after processing the files 47 | 48 | 49 | AUTHOR 50 | ====== 51 | 52 | Emfox Zhou. 53 | 54 | Based on id3iconv (http://www.cs.berkeley.edu/~zf/id3iconv/) by Feng Zhou. 55 | -------------------------------------------------------------------------------- /tests/test_m4a.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import warnings 4 | 5 | from tests import TestCase, DATA_DIR 6 | 7 | with warnings.catch_warnings(): 8 | warnings.simplefilter("ignore", DeprecationWarning) 9 | from mutagen.m4a import (M4A, M4ATags, M4AInfo, delete, M4ACover, 10 | error) 11 | 12 | 13 | class TM4ADeprecation(TestCase): 14 | 15 | SOME_FILE = os.path.join(DATA_DIR, "no-tags.m4a") 16 | 17 | def test_fail(self): 18 | self.assertRaises(error, M4A, self.SOME_FILE) 19 | self.assertRaises(error, delete, self.SOME_FILE) 20 | self.assertRaises(error, delete, self.SOME_FILE) 21 | 22 | M4AInfo # flake8 23 | with warnings.catch_warnings(): 24 | warnings.simplefilter("ignore", DeprecationWarning) 25 | a = M4A() 26 | a.add_tags() 27 | self.assertEqual(a.tags.items(), []) 28 | 29 | some_cover = M4ACover(b"foo", M4ACover.FORMAT_JPEG) 30 | self.assertEqual(some_cover.imageformat, M4ACover.FORMAT_JPEG) 31 | 32 | tags = M4ATags() 33 | self.assertRaises(error, tags.save, self.SOME_FILE) 34 | -------------------------------------------------------------------------------- /tests/data/with-id3.dsf: -------------------------------------------------------------------------------- 1 | DSD {\fmt 4+data ID3TIT2 DSF title -------------------------------------------------------------------------------- /docs/user/apev2.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | APEv2 3 | ===== 4 | 5 | Python 2.5 forced an API change in the APEv2 reading code. Some things 6 | which were case-insensitive are now case-sensitive. For example, 7 | given:: 8 | 9 | tag = APEv2() 10 | tag["Foo"] = "Bar" 11 | print "foo" in tag.keys() 12 | 13 | Mutagen 1.7.1 and earlier would print "True", as the keys were a str 14 | subclass that compared case-insensitively. However, Mutagen 1.8 and 15 | above print "False", as the keys are normal strings. 16 | 17 | :: 18 | 19 | print "foo" in tag 20 | 21 | Still prints "True", however, as __getitem__, __delitem__, and 22 | __setitem__ (and so any operations on the dict itself) remain 23 | case-insensitive. 24 | 25 | As of 1.10.1, Mutagen no longer allows non-ASCII keys in APEv2 26 | tags. This is in accordance with the APEv2 standard. A KeyError is 27 | raised if you try. 28 | 29 | 30 | Compatibility / Bugs 31 | ^^^^^^^^^^^^^^^^^^^^ 32 | 33 | * Prior to 1.10.1, Mutagen wrote an incorrect flag for APEv2 tags that 34 | claimed they did not have footers. This has been fixed, however it means 35 | that all APEv2 tags written before 1.10.1 are corrupt. 36 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://raw.githubusercontent.com/quodlibet/mutagen/main/docs/images/logo.svg 2 | :align: center 3 | :width: 400px 4 | 5 | | 6 | 7 | Mutagen is a Python module to handle audio metadata. It supports ASF, FLAC, 8 | MP4, Monkey's Audio, MP3, Musepack, Ogg Opus, Ogg FLAC, Ogg Speex, Ogg Theora, 9 | Ogg Vorbis, True Audio, WavPack, OptimFROG, and AIFF audio files. All 10 | versions of ID3v2 are supported, and all standard ID3v2.4 frames are parsed. 11 | It can read Xing headers to accurately calculate the bitrate and length of 12 | MP3s. ID3 and APEv2 tags can be edited regardless of audio format. It can also 13 | manipulate Ogg streams on an individual packet/page level. 14 | 15 | Mutagen works with Python 3.10+ (CPython and PyPy) on Linux, Windows and macOS, 16 | and has no dependencies outside the Python standard library. Mutagen is licensed 17 | under `the GPL version 2 or 18 | later `__. 19 | 20 | For more information visit https://mutagen.readthedocs.org 21 | 22 | .. image:: https://codecov.io/gh/quodlibet/mutagen/branch/main/graph/badge.svg 23 | :target: https://codecov.io/gh/quodlibet/mutagen 24 | -------------------------------------------------------------------------------- /docs/man/mid3cp.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | mid3cp 3 | ======== 4 | 5 | ------------- 6 | copy ID3 tags 7 | ------------- 8 | 9 | :Manual section: 1 10 | 11 | 12 | SYNOPSIS 13 | ======== 14 | 15 | **mid3cp** [*options*] *source* *dest* 16 | 17 | 18 | DESCRIPTION 19 | =========== 20 | 21 | **mid3cp** copies the ID3 tags from a source file to a destination file. 22 | 23 | It is designed to provide similar functionality to id3lib's id3cp tool, and can 24 | optionally write ID3v1 tags. It can also exclude specific tags from being 25 | copied. 26 | 27 | 28 | OPTIONS 29 | ======= 30 | 31 | --verbose, -v 32 | Be verbose: state all operations performed, and list tags in source file. 33 | 34 | --write-v1 35 | Write ID3v1 tags to the destination file, derived from the ID3v2 tags. 36 | 37 | --exclude-tag, -x 38 | Exclude a specific tag from being copied. Can be specified multiple times. 39 | 40 | --merge 41 | Copy over frames instead of replacing the whole ID3 tag. The tag version 42 | of *dest* will be used. In case *dest* has no ID3 tag this option has no 43 | effect. 44 | 45 | 46 | AUTHOR 47 | ====== 48 | 49 | Marcus Sundman. 50 | 51 | Based on id3cp (part of id3lib) by Dirk Mahoney and Scott Thomas Haug. 52 | -------------------------------------------------------------------------------- /docs/api/id3.rst: -------------------------------------------------------------------------------- 1 | ID3 2 | === 3 | 4 | .. automodule:: mutagen.id3 5 | 6 | 7 | ID3 Frames 8 | ---------- 9 | 10 | .. toctree:: 11 | :titlesonly: 12 | 13 | id3_frames 14 | 15 | .. autoclass:: mutagen.id3.ID3v1SaveOptions 16 | :members: 17 | :member-order: bysource 18 | 19 | .. autoclass:: mutagen.id3.PictureType 20 | :members: 21 | :member-order: bysource 22 | 23 | .. autoclass:: mutagen.id3.Encoding 24 | :members: 25 | :member-order: bysource 26 | 27 | .. autoclass:: mutagen.id3.CTOCFlags 28 | :members: 29 | :member-order: bysource 30 | 31 | 32 | ID3 33 | --- 34 | 35 | .. autoclass:: mutagen.id3.ID3Tags 36 | :show-inheritance: 37 | :members: 38 | :exclude-members: loaded_frame 39 | 40 | .. autoclass:: mutagen.id3.ID3 41 | :show-inheritance: 42 | :members: 43 | 44 | .. autoclass:: mutagen.id3.ID3FileType 45 | :members: 46 | :exclude-members: ID3 47 | 48 | 49 | EasyID3 50 | ------- 51 | 52 | .. automodule:: mutagen.easyid3 53 | 54 | .. autoclass:: mutagen.easyid3.EasyID3 55 | :show-inheritance: 56 | :members: 57 | 58 | .. autoclass:: mutagen.easyid3.EasyID3FileType 59 | :show-inheritance: 60 | :members: 61 | :exclude-members: ID3 62 | -------------------------------------------------------------------------------- /docs/man/moggsplit.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | moggsplit 3 | =========== 4 | 5 | ------------------------- 6 | split Ogg logical streams 7 | ------------------------- 8 | 9 | :Manual section: 1 10 | 11 | 12 | SYNOPSIS 13 | ======== 14 | 15 | **moggsplit** *filename* ... 16 | 17 | 18 | DESCRIPTION 19 | =========== 20 | 21 | **moggsplit** splits a multiplexed Ogg stream into separate files. For 22 | example, it can separate an OGM into separate Ogg DivX and Ogg Vorbis 23 | streams, or a chained Ogg Vorbis file into two separate files. 24 | 25 | 26 | OPTIONS 27 | ======= 28 | 29 | --extension 30 | Use the supplied extension when generating new files; the default is 31 | **ogg**. 32 | 33 | --pattern 34 | Use the supplied pattern when generating new files. This is a Python 35 | keyword format string with three variables, *base* for the original 36 | file's base name, *stream* for the stream's serial number, and ext for 37 | the extension give by **--extension**. 38 | 39 | The default is ``%(base)s-%(stream)d.%(ext)s``. 40 | 41 | --m3u 42 | Generate an m3u playlist along with the newly generated files. Useful 43 | for large chained Oggs. 44 | 45 | 46 | AUTHOR 47 | ====== 48 | 49 | Joe Wreschnig 50 | -------------------------------------------------------------------------------- /mutagen/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2005 Michael Urman 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | 8 | """Mutagen aims to be an all purpose multimedia tagging library. 9 | 10 | :: 11 | 12 | import mutagen.[format] 13 | metadata = mutagen.[format].Open(filename) 14 | 15 | ``metadata`` acts like a dictionary of tags in the file. Tags are generally a 16 | list of string-like values, but may have additional methods available 17 | depending on tag or format. They may also be entirely different objects 18 | for certain keys, again depending on format. 19 | """ 20 | 21 | from mutagen._util import MutagenError 22 | from mutagen._file import FileType, StreamInfo, File 23 | from mutagen._tags import Tags, Metadata, PaddingInfo 24 | 25 | version = (1, 47, 1) 26 | """Version tuple.""" 27 | 28 | version_string = ".".join(map(str, version)) 29 | """Version string.""" 30 | 31 | MutagenError 32 | 33 | FileType 34 | 35 | StreamInfo 36 | 37 | File 38 | 39 | Tags 40 | 41 | Metadata 42 | 43 | PaddingInfo 44 | -------------------------------------------------------------------------------- /docs/extra.css: -------------------------------------------------------------------------------- 1 | .wy-side-nav-search { 2 | background-color: initial; 3 | } 4 | 5 | .wy-nav-side, .wy-nav-top { 6 | background-color: #24292E; 7 | } 8 | 9 | .wy-side-nav-search input[type="text"] { 10 | border-color: transparent; 11 | } 12 | 13 | .wy-nav-content { 14 | margin: initial; 15 | } 16 | 17 | .rst-content div[role=navigation], footer { 18 | font-size: 0.85em; 19 | color: #999; 20 | } 21 | 22 | .rst-content div[role=navigation] hr { 23 | margin-top: 6px; 24 | } 25 | 26 | footer hr { 27 | margin-bottom: 6px; 28 | } 29 | 30 | .rst-footer-buttons { 31 | display: none; 32 | } 33 | 34 | .versionmodified { 35 | color: #008000; 36 | } 37 | 38 | a.icon-home, a.icon-home:hover { 39 | display: inline-block; 40 | padding: 4px 4px 4px 23px; 41 | background: transparent url(logo-small.svg) center left no-repeat; 42 | background-size: 1.5em; 43 | margin-top: 0.2em; 44 | margin-bottom: 1em; 45 | } 46 | 47 | 48 | .fa-home::before, .icon-home::before { 49 | content: ""; 50 | } 51 | 52 | .wy-nav-top a { 53 | margin: -2em; 54 | background: transparent url(logo-small.svg) center left no-repeat; 55 | background-size: 1.5em; 56 | padding: 4px 4px 4px 26px; 57 | } 58 | -------------------------------------------------------------------------------- /tests/test_tools_moggsplit.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | 4 | from tests.test_tools import _TTools 5 | from tests import DATA_DIR, get_temp_copy 6 | 7 | 8 | class TMOggSPlit(_TTools): 9 | 10 | TOOL_NAME = u"moggsplit" 11 | 12 | def setUp(self): 13 | super(TMOggSPlit, self).setUp() 14 | self.filename = get_temp_copy( 15 | os.path.join(DATA_DIR, 'multipagecomment.ogg')) 16 | 17 | # append the second file 18 | with open(self.filename, "ab") as first: 19 | to_append = os.path.join( 20 | DATA_DIR, 'multipage-setup.ogg') 21 | with open(to_append, "rb") as second: 22 | first.write(second.read()) 23 | 24 | def tearDown(self): 25 | super(TMOggSPlit, self).tearDown() 26 | os.unlink(self.filename) 27 | 28 | def test_basic(self): 29 | d = os.path.dirname(self.filename) 30 | p = os.path.join(d, "%(stream)d.%(ext)s") 31 | res, out = self.call("--pattern", p, self.filename) 32 | self.failIf(res) 33 | self.failIf(out) 34 | 35 | for stream in [1002429366, 1806412655]: 36 | stream_path = os.path.join( 37 | d, str(stream) + ".ogg") 38 | self.failUnless(os.path.exists(stream_path)) 39 | os.unlink(stream_path) 40 | -------------------------------------------------------------------------------- /man/mutagen-pony.1: -------------------------------------------------------------------------------- 1 | .\" Man page generated from reStructuredText. 2 | . 3 | .TH MUTAGEN-PONY 1 "" "" "" 4 | .SH NAME 5 | mutagen-pony \- scan a collection of MP3 files 6 | . 7 | .nr rst2man-indent-level 0 8 | . 9 | .de1 rstReportMargin 10 | \\$1 \\n[an-margin] 11 | level \\n[rst2man-indent-level] 12 | level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] 13 | - 14 | \\n[rst2man-indent0] 15 | \\n[rst2man-indent1] 16 | \\n[rst2man-indent2] 17 | .. 18 | .de1 INDENT 19 | .\" .rstReportMargin pre: 20 | . RS \\$1 21 | . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] 22 | . nr rst2man-indent-level +1 23 | .\" .rstReportMargin post: 24 | .. 25 | .de UNINDENT 26 | . RE 27 | .\" indent \\n[an-margin] 28 | .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] 29 | .nr rst2man-indent-level -1 30 | .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] 31 | .in \\n[rst2man-indent\\n[rst2man-indent-level]]u 32 | .. 33 | .SH SYNOPSIS 34 | .sp 35 | \fBmutagen\-pony\fP \fIdirectory\fP ... 36 | .SH DESCRIPTION 37 | .sp 38 | \fBmutagen\-pony\fP scans any directories given and reports on the kinds of 39 | tags in the MP3s it finds in them. Ride the pony. 40 | .sp 41 | It is primarily intended as a debugging tool for Mutagen. 42 | .SH AUTHORS 43 | .sp 44 | Michael Urman and Joe Wreschnig 45 | .\" Generated by docutils manpage writer. 46 | . 47 | -------------------------------------------------------------------------------- /man/mutagen-inspect.1: -------------------------------------------------------------------------------- 1 | .\" Man page generated from reStructuredText. 2 | . 3 | .TH MUTAGEN-INSPECT 1 "" "" "" 4 | .SH NAME 5 | mutagen-inspect \- view Mutagen-supported audio tags 6 | . 7 | .nr rst2man-indent-level 0 8 | . 9 | .de1 rstReportMargin 10 | \\$1 \\n[an-margin] 11 | level \\n[rst2man-indent-level] 12 | level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] 13 | - 14 | \\n[rst2man-indent0] 15 | \\n[rst2man-indent1] 16 | \\n[rst2man-indent2] 17 | .. 18 | .de1 INDENT 19 | .\" .rstReportMargin pre: 20 | . RS \\$1 21 | . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] 22 | . nr rst2man-indent-level +1 23 | .\" .rstReportMargin post: 24 | .. 25 | .de UNINDENT 26 | . RE 27 | .\" indent \\n[an-margin] 28 | .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] 29 | .nr rst2man-indent-level -1 30 | .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] 31 | .in \\n[rst2man-indent\\n[rst2man-indent-level]]u 32 | .. 33 | .SH SYNOPSIS 34 | .sp 35 | \fBmutagen\-inspect\fP \fIfilename\fP ... 36 | .SH DESCRIPTION 37 | .sp 38 | \fBmutagen\-inspect\fP loads and prints information about an audio file and 39 | its tags. 40 | .sp 41 | It is primarily intended as a debugging tool for Mutagen, but can be useful 42 | for extracting tags from the command line. 43 | .SH AUTHOR 44 | .sp 45 | Joe Wreschnig 46 | .\" Generated by docutils manpage writer. 47 | . 48 | -------------------------------------------------------------------------------- /docs/api/asf.rst: -------------------------------------------------------------------------------- 1 | ASF 2 | === 3 | 4 | .. currentmodule:: mutagen 5 | 6 | .. automodule:: mutagen.asf 7 | 8 | 9 | .. autoclass:: mutagen.asf.ASF 10 | :show-inheritance: 11 | :members: 12 | 13 | 14 | .. autoclass:: mutagen.asf.ASFInfo 15 | :show-inheritance: 16 | :members: 17 | 18 | 19 | .. autoclass:: mutagen.asf.ASFTags 20 | :show-inheritance: 21 | :members: 22 | 23 | 24 | .. autoclass:: mutagen.asf.ASFBaseAttribute() 25 | :members: 26 | 27 | 28 | .. autoclass:: mutagen.asf.ASFGUIDAttribute(value) 29 | :members: 30 | 31 | :bases: `ASFBaseAttribute` 32 | 33 | 34 | .. autoclass:: mutagen.asf.ASFWordAttribute(value) 35 | :members: 36 | 37 | :bases: `ASFBaseAttribute` 38 | 39 | 40 | .. autoclass:: mutagen.asf.ASFDWordAttribute(value) 41 | :members: 42 | 43 | :bases: `ASFBaseAttribute` 44 | 45 | 46 | .. autoclass:: mutagen.asf.ASFQWordAttribute(value) 47 | :members: 48 | 49 | :bases: `ASFBaseAttribute` 50 | 51 | 52 | .. autoclass:: mutagen.asf.ASFBoolAttribute(value) 53 | :members: 54 | 55 | :bases: `ASFBaseAttribute` 56 | 57 | 58 | .. autoclass:: mutagen.asf.ASFByteArrayAttribute(value) 59 | :members: 60 | 61 | :bases: `ASFBaseAttribute` 62 | 63 | 64 | .. autoclass:: mutagen.asf.ASFUnicodeAttribute(value) 65 | :members: 66 | 67 | :bases: `ASFBaseAttribute` 68 | -------------------------------------------------------------------------------- /mutagen/_tools/mutagen_inspect.py: -------------------------------------------------------------------------------- 1 | # Copyright 2005 Joe Wreschnig 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | 8 | """Full tag list for any given file.""" 9 | 10 | import sys 11 | 12 | from ._util import SignalHandler, OptionParser 13 | 14 | 15 | _sig = SignalHandler() 16 | 17 | 18 | def main(argv): 19 | from mutagen import File 20 | 21 | parser = OptionParser(usage="usage: %prog [options] FILE [FILE...]") 22 | parser.add_option("--no-flac", help="Compatibility; does nothing.") 23 | parser.add_option("--no-mp3", help="Compatibility; does nothing.") 24 | parser.add_option("--no-apev2", help="Compatibility; does nothing.") 25 | 26 | (options, args) = parser.parse_args(argv[1:]) 27 | if not args: 28 | raise SystemExit(parser.print_help() or 1) 29 | 30 | for filename in args: 31 | print(u"--", filename) 32 | try: 33 | print(u"-", File(filename).pprint()) 34 | except AttributeError: 35 | print(u"- Unknown file type") 36 | except Exception as err: 37 | print(str(err)) 38 | print(u"") 39 | 40 | 41 | def entry_point(): 42 | _sig.init() 43 | return main(sys.argv) 44 | -------------------------------------------------------------------------------- /tests/test_tools.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import importlib 4 | from io import StringIO 5 | 6 | from tests import TestCase 7 | 8 | 9 | def get_var(tool_name, entry="main"): 10 | mod = importlib.import_module( 11 | "mutagen._tools.%s" % tool_name.replace("-", "_")) 12 | return getattr(mod, entry) 13 | 14 | 15 | class _TTools(TestCase): 16 | TOOL_NAME = None 17 | 18 | def setUp(self): 19 | self.assertTrue(isinstance(self.TOOL_NAME, str)) 20 | self._main = get_var(self.TOOL_NAME) 21 | 22 | def get_var(self, name): 23 | return get_var(self.TOOL_NAME, name) 24 | 25 | def call2(self, *args): 26 | for arg in args: 27 | self.assertTrue(isinstance(arg, str)) 28 | old_stdout = sys.stdout 29 | old_stderr = sys.stderr 30 | try: 31 | out = StringIO() 32 | err = StringIO() 33 | sys.stdout = out 34 | sys.stderr = err 35 | try: 36 | ret = self._main([self.TOOL_NAME] + list(args)) 37 | except SystemExit as e: 38 | ret = e.code 39 | ret = ret or 0 40 | out_val = out.getvalue() 41 | err_val = err.getvalue() 42 | return (ret, out_val, err_val) 43 | finally: 44 | sys.stdout = old_stdout 45 | sys.stderr = old_stderr 46 | 47 | def call(self, *args): 48 | return self.call2(*args)[:2] 49 | 50 | def tearDown(self): 51 | del self._main 52 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | 5 | dir_ = os.path.dirname(os.path.realpath(__file__)) 6 | sys.path.insert(0, dir_) 7 | sys.path.insert(0, os.path.abspath(os.path.join(dir_, ".."))) 8 | 9 | needs_sphinx = "1.3" 10 | 11 | extensions = [ 12 | 'sphinx.ext.autodoc', 13 | 'sphinx.ext.napoleon', 14 | 'sphinx.ext.intersphinx', 15 | 'sphinx.ext.extlinks', 16 | ] 17 | intersphinx_mapping = { 18 | 'python': ('https://docs.python.org/3', None), 19 | } 20 | source_suffix = '.rst' 21 | master_doc = 'index' 22 | project = 'mutagen' 23 | copyright = u'2016, Joe Wreschnig, Michael Urman, Lukáš Lalinský, ' \ 24 | u'Christoph Reiter, Ben Ockmore & others' 25 | html_title = project 26 | exclude_patterns = ['_build'] 27 | 28 | extlinks = { 29 | 'bug': ('https://github.com/quodlibet/mutagen/issues/%s', '#%s'), 30 | 'pr': ('https://github.com/quodlibet/mutagen/pull/%s', '#pr%s'), 31 | 'commit': ('https://github.com/quodlibet/mutagen/commit/%s', '%s'), 32 | 'user': ('https://github.com/%s', '%s'), 33 | } 34 | 35 | 36 | autodoc_member_order = "bysource" 37 | default_role = "obj" 38 | 39 | html_theme = "sphinx_rtd_theme" 40 | html_favicon = "images/favicon.ico" 41 | html_theme_options = { 42 | "display_version": False, 43 | } 44 | 45 | html_context = { 46 | 'extra_css_files': [ 47 | '_static/extra.css', 48 | ], 49 | } 50 | 51 | html_static_path = [ 52 | "extra.css", 53 | "images/logo-small.svg", 54 | ] 55 | 56 | suppress_warnings = ["image.nonlocal_uri"] 57 | -------------------------------------------------------------------------------- /docs/id3_frames_gen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright 2013 Christoph Reiter 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | 9 | """ 10 | ./id3_frames_gen.py > api/id3_frames.rst 11 | """ 12 | 13 | import sys 14 | import os 15 | 16 | sys.path.insert(0, os.path.abspath('../')) 17 | 18 | import mutagen.id3 19 | from mutagen.id3 import Frames, Frames_2_2, Frame 20 | 21 | 22 | BaseFrames = dict([(k, v) for (k, v) in vars(mutagen.id3).items() 23 | if v not in Frames.values() and v not in Frames_2_2.values() 24 | and isinstance(v, type) and 25 | (issubclass(v, Frame) or v is Frame)]) 26 | 27 | 28 | def print_header(header, type_="-"): 29 | print(header) 30 | print(type_ * len(header)) 31 | print("") 32 | 33 | 34 | def print_frames(frames, sort_mro=False): 35 | if sort_mro: 36 | # less bases first, then by name 37 | sort_func = lambda x: (len(x[1].__mro__), x[0]) 38 | else: 39 | sort_func = lambda x: x 40 | 41 | for name, cls in sorted(frames.items(), key=sort_func): 42 | print(""" 43 | .. autoclass:: mutagen.id3.%s 44 | :show-inheritance: 45 | :members: 46 | """ % repr(cls())) 47 | 48 | 49 | if __name__ == "__main__": 50 | 51 | print_header("Frame Base Classes") 52 | print_frames(BaseFrames, sort_mro=True) 53 | print_header("ID3v2.3/4 Frames") 54 | print_frames(Frames) 55 | -------------------------------------------------------------------------------- /docs/dev/id3.rst: -------------------------------------------------------------------------------- 1 | ID3 2 | === 3 | 4 | `ID3 Website `__ 5 | 6 | ID3v2.4.0: 7 | * `ID3v2.4.0 Main Structure `__ 8 | * `ID3v2.4.0 Native Frames `__ 9 | * `ID3v2.4.0 Changes `__ 10 | 11 | ID3v2.3.0: 12 | * `ID3v2.3.0 Informal standard `__ 13 | * `ID3v2 Programming Guidlines `__ 14 | 15 | ID3v2.2.0: 16 | * `ID3v2.2.0 Informal standard `__ 17 | 18 | Additional standards: 19 | * `ID3v2 Chapter Frame Addendum v1.0 `__ 20 | * `ID3v2 Accessibility Addendum v1.0 `__ 21 | 22 | 23 | Mutagen Implementation Details 24 | ------------------------------ 25 | 26 | * We don't interpret or write the "ID3v2 extended header". An existing extended 27 | header gets removed when saving the tags. 28 | 29 | * https://github.com/quodlibet/mutagen/pull/631#issuecomment-1742118078 30 | 31 | * We don't support appended tags. They will neither be found nor updated. 32 | 33 | * https://github.com/quodlibet/mutagen/issues/78 34 | 35 | * We always NULL-terminate multivalue text frames and ignore them when parsing 36 | 37 | * https://github.com/quodlibet/mutagen/issues/379#issuecomment-486852503 38 | -------------------------------------------------------------------------------- /docs/user/padding.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Tag Padding 3 | =========== 4 | 5 | .. currentmodule:: mutagen 6 | 7 | Many formats mutagen supports include a notion of metadata padding, empty 8 | space in the file following the metadata. In case the size of the metadata 9 | increases, this empty space can be claimed and written into. The alternative 10 | would be to resize the whole file, which means everything after the metadata 11 | needs to be rewritten. This can be a time consuming operation if the file is 12 | large and should be avoided. 13 | 14 | For formats where mutagen supports using such a padding it will use the 15 | existing padding for extending metadata, add additional padding if the added 16 | data exceeds the size of the existing padding and reduce the padding size if 17 | it makes up more than a significant part of the file size. 18 | 19 | It also provides additional API to control the padding usage. Some `FileType` 20 | and `Metadata` subclasses provide a ``save()`` method which can be passed a 21 | ``padding`` callback. This callback gets called with a `PaddingInfo` instance 22 | and should return the amount of padding to write to the file. 23 | 24 | :: 25 | 26 | from mutagen.mp3 import MP3 27 | 28 | def no_padding(info): 29 | # this will remove all padding 30 | return 0 31 | 32 | def default_implementation(info): 33 | # this is the default implementation, which can be extended 34 | return info.get_default_padding() 35 | 36 | def no_new_padding(info): 37 | # this will use existing padding but never add new one 38 | return max(info.padding, 0) 39 | 40 | f = MP3("somefile.mp3") 41 | f.save(padding=no_padding) 42 | f.save(padding=default_implementation) 43 | f.save(padding=no_new_padding) 44 | -------------------------------------------------------------------------------- /tests/test_tools_util.py: -------------------------------------------------------------------------------- 1 | 2 | from mutagen._tools._util import split_escape 3 | 4 | from tests import TestCase 5 | 6 | 7 | class Tsplit_escape(TestCase): 8 | def test_split_escape(self): 9 | inout = [ 10 | (("", ":"), [""]), 11 | ((":", ":"), ["", ""]), 12 | ((":", ":", 0), [":"]), 13 | ((":b:c:", ":", 0), [":b:c:"]), 14 | ((":b:c:", ":", 1), ["", "b:c:"]), 15 | ((":b:c:", ":", 2), ["", "b", "c:"]), 16 | ((":b:c:", ":", 3), ["", "b", "c", ""]), 17 | (("a\\:b:c", ":"), ["a:b", "c"]), 18 | (("a\\\\:b:c", ":"), ["a\\", "b", "c"]), 19 | (("a\\\\\\:b:c\\:", ":"), ["a\\:b", "c:"]), 20 | (("\\", ":"), [""]), 21 | (("\\\\", ":"), ["\\"]), 22 | (("\\\\a\\b", ":"), ["\\a\\b"]), 23 | ] 24 | 25 | for inargs, out in inout: 26 | self.assertEqual(split_escape(*inargs), out) 27 | 28 | def test_types(self): 29 | parts = split_escape(b"\xff:\xff", b":") 30 | self.assertEqual(parts, [b"\xff", b"\xff"]) 31 | self.assertTrue(isinstance(parts[0], bytes)) 32 | 33 | parts = split_escape(b"", b":") 34 | self.assertEqual(parts, [b""]) 35 | self.assertTrue(isinstance(parts[0], bytes)) 36 | 37 | parts = split_escape(u"a:b", u":") 38 | self.assertEqual(parts, [u"a", u"b"]) 39 | self.assertTrue(all(isinstance(p, str) for p in parts)) 40 | 41 | parts = split_escape(u"", u":") 42 | self.assertEqual(parts, [u""]) 43 | self.assertTrue(all(isinstance(p, str) for p in parts)) 44 | 45 | parts = split_escape(u":", u":") 46 | self.assertEqual(parts, [u"", u""]) 47 | self.assertTrue(all(isinstance(p, str) for p in parts)) 48 | -------------------------------------------------------------------------------- /tests/test_ac3.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import io 4 | 5 | from mutagen.ac3 import AC3, AC3Error 6 | 7 | from tests import TestCase, DATA_DIR 8 | 9 | 10 | class TAC3(TestCase): 11 | 12 | def setUp(self): 13 | self.ac3 = AC3(os.path.join(DATA_DIR, "silence-44-s.ac3")) 14 | self.eac3 = AC3(os.path.join(DATA_DIR, "silence-44-s.eac3")) 15 | 16 | def test_channels(self): 17 | self.failUnlessEqual(self.ac3.info.channels, 2) 18 | self.failUnlessEqual(self.eac3.info.channels, 2) 19 | 20 | def test_bitrate(self): 21 | self.failUnlessEqual(self.ac3.info.bitrate, 192000) 22 | self.failUnlessAlmostEqual(self.eac3.info.bitrate, 192000, delta=500) 23 | 24 | def test_sample_rate(self): 25 | self.failUnlessEqual(self.ac3.info.sample_rate, 44100) 26 | self.failUnlessEqual(self.eac3.info.sample_rate, 44100) 27 | 28 | def test_length(self): 29 | self.failUnlessAlmostEqual(self.ac3.info.length, 3.70, delta=0.009) 30 | self.failUnlessAlmostEqual(self.eac3.info.length, 3.70, delta=0.009) 31 | 32 | def test_type(self): 33 | self.failUnlessEqual(self.ac3.info.codec, "ac-3") 34 | self.failUnlessEqual(self.eac3.info.codec, "ec-3") 35 | 36 | def test_not_my_file(self): 37 | self.failUnlessRaises( 38 | AC3Error, AC3, 39 | os.path.join(DATA_DIR, "empty.ogg")) 40 | 41 | self.failUnlessRaises( 42 | AC3Error, AC3, 43 | os.path.join(DATA_DIR, "silence-44-s.mp3")) 44 | 45 | def test_pprint(self): 46 | self.assertTrue("ac-3" in self.ac3.pprint()) 47 | self.assertTrue("ec-3" in self.eac3.pprint()) 48 | 49 | def test_fuzz_extra_bitstream_info(self): 50 | with self.assertRaises(AC3Error): 51 | AC3(io.BytesIO(b'\x0bwII\x00\x00\xe3\xe3//')) 52 | -------------------------------------------------------------------------------- /man/mid3iconv.1: -------------------------------------------------------------------------------- 1 | .\" Man page generated from reStructuredText. 2 | . 3 | .TH MID3ICONV 1 "" "" "" 4 | .SH NAME 5 | mid3iconv \- convert ID3 tag encodings 6 | . 7 | .nr rst2man-indent-level 0 8 | . 9 | .de1 rstReportMargin 10 | \\$1 \\n[an-margin] 11 | level \\n[rst2man-indent-level] 12 | level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] 13 | - 14 | \\n[rst2man-indent0] 15 | \\n[rst2man-indent1] 16 | \\n[rst2man-indent2] 17 | .. 18 | .de1 INDENT 19 | .\" .rstReportMargin pre: 20 | . RS \\$1 21 | . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] 22 | . nr rst2man-indent-level +1 23 | .\" .rstReportMargin post: 24 | .. 25 | .de UNINDENT 26 | . RE 27 | .\" indent \\n[an-margin] 28 | .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] 29 | .nr rst2man-indent-level -1 30 | .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] 31 | .in \\n[rst2man-indent\\n[rst2man-indent-level]]u 32 | .. 33 | .SH SYNOPSIS 34 | .sp 35 | \fBmid3iconv\fP [\fIoptions\fP] \fIfilename\fP ... 36 | .SH DESCRIPTION 37 | .sp 38 | \fBmid3iconv\fP converts ID3 tags from legacy encodings to Unicode and stores 39 | them using the ID3v2 format. 40 | .SH OPTIONS 41 | .INDENT 0.0 42 | .TP 43 | .B \-\-debug\fP,\fB \-d 44 | Print updated tags 45 | .TP 46 | .B \-\-dry\-run\fP,\fB \-p 47 | Do not actually modify files 48 | .TP 49 | .B \-\-encoding\fP,\fB \-e 50 | Convert from this encoding. By default, your locale\(aqs default encoding is 51 | used. 52 | .TP 53 | .B \-\-force\-v1 54 | Use an ID3v1 tag even if an ID3v2 tag is present 55 | .TP 56 | .B \-\-quiet\fP,\fB \-q 57 | Only output errors 58 | .TP 59 | .B \-\-remove\-v1 60 | Remove any ID3v1 tag after processing the files 61 | .UNINDENT 62 | .SH AUTHOR 63 | .sp 64 | Emfox Zhou. 65 | .sp 66 | Based on id3iconv (\fI\%http://www.cs.berkeley.edu/~zf/id3iconv/\fP) by Feng Zhou. 67 | .\" Generated by docutils manpage writer. 68 | . 69 | -------------------------------------------------------------------------------- /tests/test_trueaudio.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import io 4 | 5 | from mutagen.trueaudio import TrueAudio, delete, error 6 | from mutagen.id3 import TIT1 7 | 8 | from tests import TestCase, DATA_DIR, get_temp_copy 9 | 10 | 11 | class TTrueAudio(TestCase): 12 | 13 | def setUp(self): 14 | self.audio = TrueAudio(os.path.join(DATA_DIR, "empty.tta")) 15 | 16 | def test_tags(self): 17 | self.failUnless(self.audio.tags is None) 18 | 19 | def test_length(self): 20 | self.failUnlessAlmostEqual(self.audio.info.length, 3.7, 1) 21 | 22 | def test_sample_rate(self): 23 | self.failUnlessEqual(44100, self.audio.info.sample_rate) 24 | 25 | def test_not_my_file(self): 26 | filename = os.path.join(DATA_DIR, "empty.ogg") 27 | self.failUnlessRaises(error, TrueAudio, filename) 28 | 29 | def test_module_delete(self): 30 | delete(os.path.join(DATA_DIR, "empty.tta")) 31 | 32 | def test_delete(self): 33 | self.audio.delete() 34 | self.failIf(self.audio.tags) 35 | 36 | def test_pprint(self): 37 | self.failUnless(self.audio.pprint()) 38 | 39 | def test_save_reload(self): 40 | filename = get_temp_copy(self.audio.filename) 41 | try: 42 | audio = TrueAudio(filename) 43 | audio.add_tags() 44 | audio.tags.add(TIT1(encoding=0, text="A Title")) 45 | audio.save() 46 | audio = TrueAudio(filename) 47 | self.failUnlessEqual(audio["TIT1"], "A Title") 48 | finally: 49 | os.unlink(filename) 50 | 51 | def test_zero_sample_rate(self): 52 | header = io.BytesIO(b'TTA \x00AD\xa0\x00\x00\x00\x00\x00\x00\x00\xfe3') 53 | assert TrueAudio(header).info.length == 0.0 54 | 55 | def test_mime(self): 56 | self.failUnless("audio/x-tta" in self.audio.mime) 57 | -------------------------------------------------------------------------------- /man/moggsplit.1: -------------------------------------------------------------------------------- 1 | .\" Man page generated from reStructuredText. 2 | . 3 | .TH MOGGSPLIT 1 "" "" "" 4 | .SH NAME 5 | moggsplit \- split Ogg logical streams 6 | . 7 | .nr rst2man-indent-level 0 8 | . 9 | .de1 rstReportMargin 10 | \\$1 \\n[an-margin] 11 | level \\n[rst2man-indent-level] 12 | level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] 13 | - 14 | \\n[rst2man-indent0] 15 | \\n[rst2man-indent1] 16 | \\n[rst2man-indent2] 17 | .. 18 | .de1 INDENT 19 | .\" .rstReportMargin pre: 20 | . RS \\$1 21 | . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] 22 | . nr rst2man-indent-level +1 23 | .\" .rstReportMargin post: 24 | .. 25 | .de UNINDENT 26 | . RE 27 | .\" indent \\n[an-margin] 28 | .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] 29 | .nr rst2man-indent-level -1 30 | .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] 31 | .in \\n[rst2man-indent\\n[rst2man-indent-level]]u 32 | .. 33 | .SH SYNOPSIS 34 | .sp 35 | \fBmoggsplit\fP \fIfilename\fP ... 36 | .SH DESCRIPTION 37 | .sp 38 | \fBmoggsplit\fP splits a multiplexed Ogg stream into separate files. For 39 | example, it can separate an OGM into separate Ogg DivX and Ogg Vorbis 40 | streams, or a chained Ogg Vorbis file into two separate files. 41 | .SH OPTIONS 42 | .INDENT 0.0 43 | .TP 44 | .B \-\-extension 45 | Use the supplied extension when generating new files; the default is 46 | \fBogg\fP\&. 47 | .TP 48 | .B \-\-pattern 49 | Use the supplied pattern when generating new files. This is a Python 50 | keyword format string with three variables, \fIbase\fP for the original 51 | file\(aqs base name, \fIstream\fP for the stream\(aqs serial number, and ext for 52 | the extension give by \fB\-\-extension\fP\&. 53 | .sp 54 | The default is \fB%(base)s\-%(stream)d.%(ext)s\fP\&. 55 | .TP 56 | .B \-\-m3u 57 | Generate an m3u playlist along with the newly generated files. Useful 58 | for large chained Oggs. 59 | .UNINDENT 60 | .SH AUTHOR 61 | .sp 62 | Joe Wreschnig 63 | .\" Generated by docutils manpage writer. 64 | . 65 | -------------------------------------------------------------------------------- /man/mid3cp.1: -------------------------------------------------------------------------------- 1 | .\" Man page generated from reStructuredText. 2 | . 3 | .TH MID3CP 1 "" "" "" 4 | .SH NAME 5 | mid3cp \- copy ID3 tags 6 | . 7 | .nr rst2man-indent-level 0 8 | . 9 | .de1 rstReportMargin 10 | \\$1 \\n[an-margin] 11 | level \\n[rst2man-indent-level] 12 | level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] 13 | - 14 | \\n[rst2man-indent0] 15 | \\n[rst2man-indent1] 16 | \\n[rst2man-indent2] 17 | .. 18 | .de1 INDENT 19 | .\" .rstReportMargin pre: 20 | . RS \\$1 21 | . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] 22 | . nr rst2man-indent-level +1 23 | .\" .rstReportMargin post: 24 | .. 25 | .de UNINDENT 26 | . RE 27 | .\" indent \\n[an-margin] 28 | .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] 29 | .nr rst2man-indent-level -1 30 | .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] 31 | .in \\n[rst2man-indent\\n[rst2man-indent-level]]u 32 | .. 33 | .SH SYNOPSIS 34 | .sp 35 | \fBmid3cp\fP [\fIoptions\fP] \fIsource\fP \fIdest\fP 36 | .SH DESCRIPTION 37 | .sp 38 | \fBmid3cp\fP copies the ID3 tags from a source file to a destination file. 39 | .sp 40 | It is designed to provide similar functionality to id3lib\(aqs id3cp tool, and can 41 | optionally write ID3v1 tags. It can also exclude specific tags from being 42 | copied. 43 | .SH OPTIONS 44 | .INDENT 0.0 45 | .TP 46 | .B \-\-verbose\fP,\fB \-v 47 | Be verbose: state all operations performed, and list tags in source file. 48 | .TP 49 | .B \-\-write\-v1 50 | Write ID3v1 tags to the destination file, derived from the ID3v2 tags. 51 | .TP 52 | .B \-\-exclude\-tag\fP,\fB \-x 53 | Exclude a specific tag from being copied. Can be specified multiple times. 54 | .TP 55 | .B \-\-merge 56 | Copy over frames instead of replacing the whole ID3 tag. The tag version 57 | of \fIdest\fP will be used. In case \fIdest\fP has no ID3 tag this option has no 58 | effect. 59 | .UNINDENT 60 | .SH AUTHOR 61 | .sp 62 | Marcus Sundman. 63 | .sp 64 | Based on id3cp (part of id3lib) by Dirk Mahoney and Scott Thomas Haug. 65 | .\" Generated by docutils manpage writer. 66 | . 67 | -------------------------------------------------------------------------------- /tests/test_tak.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import io 4 | 5 | from mutagen.tak import TAK, TAKHeaderError 6 | from tests import TestCase, DATA_DIR 7 | 8 | 9 | class TTAK(TestCase): 10 | 11 | def setUp(self): 12 | self.tak_no_tags = TAK(os.path.join(DATA_DIR, "silence-44-s.tak")) 13 | self.tak_tags = TAK(os.path.join(DATA_DIR, "has-tags.tak")) 14 | 15 | def test_channels(self): 16 | self.failUnlessEqual(self.tak_no_tags.info.channels, 2) 17 | self.failUnlessEqual(self.tak_tags.info.channels, 2) 18 | 19 | def test_length(self): 20 | self.failUnlessAlmostEqual(self.tak_no_tags.info.length, 3.68, 21 | delta=0.009) 22 | self.failUnlessAlmostEqual(self.tak_tags.info.length, 0.08, 23 | delta=0.009) 24 | 25 | def test_sample_rate(self): 26 | self.failUnlessEqual(self.tak_no_tags.info.sample_rate, 44100) 27 | self.failUnlessEqual(self.tak_tags.info.sample_rate, 44100) 28 | 29 | def test_bits_per_sample(self): 30 | self.failUnlessEqual(self.tak_no_tags.info.bits_per_sample, 16) 31 | self.failUnlessAlmostEqual(self.tak_tags.info.bits_per_sample, 16) 32 | 33 | def test_encoder_info(self): 34 | self.failUnlessEqual(self.tak_no_tags.info.encoder_info, "TAK 2.3.0") 35 | self.failUnlessEqual(self.tak_tags.info.encoder_info, "TAK 2.3.0") 36 | 37 | def test_not_my_file(self): 38 | self.failUnlessRaises( 39 | TAKHeaderError, TAK, 40 | os.path.join(DATA_DIR, "empty.ogg")) 41 | self.failUnlessRaises( 42 | TAKHeaderError, TAK, 43 | os.path.join(DATA_DIR, "click.mpc")) 44 | 45 | def test_mime(self): 46 | self.failUnless("audio/x-tak" in self.tak_no_tags.mime) 47 | 48 | def test_pprint(self): 49 | self.failUnless(self.tak_no_tags.pprint()) 50 | self.failUnless(self.tak_tags.pprint()) 51 | 52 | def test_fuzz_only_end(self): 53 | with self.assertRaises(TAKHeaderError): 54 | TAK(io.BytesIO(b'tBaK\x00\x00\x00\x00')) 55 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mutagen" 3 | version = "1.47.1" 4 | description = "read and write audio tags for many formats" 5 | readme = { file = "README.rst", content-type = "text/x-rst" } 6 | authors = [{ name = "Christoph Reiter", email = "reiter.christoph@gmail.com" }] 7 | license = "GPL-2.0-or-later" 8 | requires-python = ">=3.10, <4" 9 | classifiers = [ 10 | "Operating System :: OS Independent", 11 | "Programming Language :: Python :: 3", 12 | "Programming Language :: Python :: Implementation :: CPython", 13 | "Programming Language :: Python :: Implementation :: PyPy", 14 | "Topic :: Multimedia :: Sound/Audio", 15 | ] 16 | 17 | [project.urls] 18 | Homepage = "https://mutagen.readthedocs.io" 19 | Repository = "https://github.com/quodlibet/mutagen" 20 | 21 | [project.scripts] 22 | mid3cp = "mutagen._tools.mid3cp:entry_point" 23 | mid3iconv = "mutagen._tools.mid3iconv:entry_point" 24 | mid3v2 = "mutagen._tools.mid3v2:entry_point" 25 | moggsplit = "mutagen._tools.moggsplit:entry_point" 26 | mutagen-inspect = "mutagen._tools.mutagen_inspect:entry_point" 27 | mutagen-pony = "mutagen._tools.mutagen_pony:entry_point" 28 | 29 | [dependency-groups] 30 | dev = [ 31 | "pytest>=8.2,<10", 32 | "hypothesis>=6.50.1,<7", 33 | "flake8>=7.1.0,<8", 34 | "mypy==1.18.2", 35 | "coverage>=7.2.5,<8", 36 | "setuptools>=77.0.0,<78", 37 | ] 38 | fuzzing = ["python-afl>=0.7.3,<0.8"] 39 | docs = [ 40 | "Sphinx~=7.1", 41 | "sphinx-rtd-theme>=2.0.0,<3", 42 | "sphinx-autobuild>=2021.3.14,<2022", 43 | ] 44 | 45 | [tool.poetry.group.fuzzing] 46 | optional = true 47 | 48 | [tool.poetry.group.docs] 49 | optional = true 50 | 51 | [tool.setuptools] 52 | packages = [ 53 | "mutagen", 54 | "mutagen.id3", 55 | "mutagen.mp4", 56 | "mutagen.asf", 57 | "mutagen.mp3", 58 | "mutagen._tools", 59 | ] 60 | 61 | [tool.setuptools.package-data] 62 | mutagen = ["py.typed"] 63 | 64 | [tool.setuptools.data-files] 65 | "share/man/man1" = ["man/*.1"] 66 | 67 | [build-system] 68 | requires = ["setuptools>=77"] 69 | build-backend = "setuptools.build_meta" 70 | 71 | [tool.coverage.run] 72 | include=["mutagen/*", "tests/*"] 73 | -------------------------------------------------------------------------------- /docs/user/filelike.rst: -------------------------------------------------------------------------------- 1 | ============================== 2 | Working with File-like Objects 3 | ============================== 4 | 5 | .. currentmodule:: mutagen 6 | 7 | The first argument passed to a :class:`FileType` or :class:`Metadata` can 8 | either be a file name or a file-like object, such as `BytesIO ` 9 | and mutagen will figure out what to do. 10 | 11 | :: 12 | 13 | MP3("myfile.mp3") 14 | MP3(myfileobj) 15 | 16 | 17 | If for some reason the automatic type detection fails, it's possible to pass 18 | them using a named argument which skips the type guessing. 19 | 20 | :: 21 | 22 | MP3(filename="myfile.mp3") 23 | MP3(fileobj=myfileobj) 24 | 25 | 26 | Mutagen expects the file offset to be at 0 for all file objects passed to it. 27 | 28 | The file-like object has to implement the following interface (It's a limited 29 | subset of real buffered file objects and StringIO/BytesIO) 30 | 31 | .. literalinclude:: examples/fileobj-iface.py 32 | 33 | 34 | Gio Example Implementation 35 | -------------------------- 36 | 37 | The following implements a file-like object using `PyGObject 38 | `__ and `Gio 39 | `__. It depends on the 40 | `giofile `__ Python library. 41 | 42 | 43 | .. code:: python 44 | 45 | import mutagen 46 | import giofile 47 | from gi.repository import Gio 48 | 49 | gio_file = Gio.File.new_for_uri( 50 | "http://people.xiph.org/~giles/2012/opus/ehren-paper_lights-96.opus") 51 | 52 | cancellable = Gio.Cancellable.new() 53 | with giofile.open(gio_file, "rb", cancellable=cancellable) as gfile: 54 | print(mutagen.File(gfile).pprint()) 55 | 56 | .. code:: sh 57 | 58 | $ python example.py 59 | Ogg Opus, 228.11 seconds (audio/ogg) 60 | ENCODER=opusenc from opus-tools 0.1.5 61 | artist=Ehren Starks 62 | title=Paper Lights 63 | album=Lines Build Walls 64 | date=2005-09-05 65 | copyright=Copyright 2005 Ehren Starks 66 | license=http://creativecommons.org/licenses/by-nc-sa/1.0/ 67 | organization=magnatune.com 68 | -------------------------------------------------------------------------------- /docs/api/base.rst: -------------------------------------------------------------------------------- 1 | Main Module 2 | ----------- 3 | 4 | .. automodule:: mutagen 5 | :members: File, version, version_string 6 | 7 | 8 | Base Classes 9 | ~~~~~~~~~~~~ 10 | 11 | .. autoclass:: mutagen.FileType 12 | :members: pprint, add_tags, mime, save, delete 13 | :show-inheritance: 14 | 15 | 16 | .. autoclass:: mutagen.Tags 17 | :members: pprint 18 | 19 | 20 | .. autoclass:: mutagen.Metadata 21 | :show-inheritance: 22 | :members: save, delete 23 | 24 | 25 | .. autoclass:: mutagen.StreamInfo 26 | :members: pprint 27 | 28 | 29 | .. autoclass:: mutagen.PaddingInfo 30 | :members: 31 | 32 | 33 | .. autoclass:: mutagen.MutagenError 34 | 35 | 36 | Internal Classes 37 | ~~~~~~~~~~~~~~~~ 38 | 39 | .. automodule:: mutagen._util 40 | 41 | .. autoclass:: mutagen._util.DictMixin 42 | 43 | .. autoclass:: mutagen._util.DictProxy 44 | :show-inheritance: 45 | 46 | 47 | Other Classes and Functions 48 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 49 | 50 | .. currentmodule:: mutagen 51 | 52 | .. class:: text() 53 | 54 | This type only exists for documentation purposes. It represents 55 | :obj:`python:str` under Python 3. 56 | 57 | 58 | .. class:: bytes() 59 | 60 | This type only exists for documentation purposes. It represents 61 | :obj:`python:bytes` under Python 3. 62 | 63 | 64 | .. class:: fspath() 65 | 66 | This type only exists for documentation purposes. It represents a file 67 | name which can be :obj:`python:str` or :obj:`python:bytes` under Python 3. 68 | 69 | .. class:: fileobj() 70 | 71 | This type only exists for documentation purposes. A file-like object. 72 | See :doc:`/user/filelike` for more information. 73 | 74 | .. class:: filething() 75 | 76 | This type only exists for documentation purposes. Either a `fspath` or 77 | a `fileobj`. 78 | 79 | 80 | .. function:: PaddingFunction(info) 81 | 82 | A function you can implement and pass to various ``save()`` methods for 83 | controlling the amount of padding to use. See :doc:`/user/padding` for 84 | more information. 85 | 86 | :param PaddingInfo info: 87 | :returns: The amount of padding to use 88 | :rtype: int 89 | -------------------------------------------------------------------------------- /mutagen/_riff.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2017 Borewit 2 | # Copyright (C) 2019-2020 Philipp Wolfer 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | 9 | """Resource Interchange File Format (RIFF).""" 10 | 11 | import struct 12 | from struct import pack 13 | 14 | from mutagen._iff import ( 15 | IffChunk, 16 | IffContainerChunkMixin, 17 | IffFile, 18 | InvalidChunk, 19 | ) 20 | 21 | 22 | class RiffChunk(IffChunk): 23 | """Generic RIFF chunk""" 24 | 25 | @classmethod 26 | def parse_header(cls, header): 27 | return struct.unpack('<4sI', header) 28 | 29 | @classmethod 30 | def get_class(cls, id): 31 | if id in (u'LIST', u'RIFF'): 32 | return RiffListChunk 33 | else: 34 | return cls 35 | 36 | def write_new_header(self, id_, size): 37 | self._fileobj.write(pack('<4sI', id_, size)) 38 | 39 | def write_size(self): 40 | self._fileobj.write(pack('` 28 | 29 | 30 | Installing 31 | ---------- 32 | 33 | :: 34 | 35 | python3 -m pip install mutagen 36 | 37 | or 38 | 39 | :: 40 | 41 | sudo apt-get install python3-mutagen 42 | 43 | 44 | Where do I get it? 45 | ------------------ 46 | 47 | Mutagen is hosted on `GitHub `_. The 48 | `download page `_ or `PyPI 49 | `_ will have the latest version or check out 50 | the git repository:: 51 | 52 | $ git clone https://github.com/quodlibet/mutagen.git 53 | 54 | 55 | Why Mutagen? 56 | ------------ 57 | 58 | Quod Libet has more strenuous requirements in a tagging library than most 59 | programs that deal with tags. Therefore we felt it was necessary to write our 60 | own. 61 | 62 | * Mutagen has a simple API, that is roughly the same across all tag formats 63 | and versions and integrates into Python's builtin types and interfaces. 64 | * New frame types and file formats are easily added, and the behavior of the 65 | current formats can be changed by extending them. 66 | * Freeform keys, multiple values, Unicode, and other advanced features were 67 | considered from the start and are fully supported. 68 | * All ID3v2 versions and all ID3v2.4 frames are covered, including rare ones 69 | like POPM or RVA2. 70 | * We take automated testing very seriously. All bug fixes are committed with a 71 | test that prevents them from recurring, and new features are committed with 72 | a full test suite. 73 | 74 | 75 | Real World Use 76 | -------------- 77 | 78 | Mutagen can load nearly every MP3 we have thrown at it (when it hasn't, we 79 | make it do so). Scripts are included so you can run the same tests on your 80 | collection. 81 | 82 | The following software projects are using Mutagen for tagging: 83 | 84 | * `Ex Falso and Quod Libet `_, a flexible tagger and player 85 | * `Beets `_, a music library manager and MusicBrainz tagger 86 | * `Picard `_, cross-platform MusicBrainz tagger 87 | * `Puddletag `_, an audio tag editor 88 | * `Exaile `_, a media player aiming to be similar to KDE's AmaroK, but for GTK+ 89 | -------------------------------------------------------------------------------- /mutagen/_tools/_util.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Christoph Reiter 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | 8 | import os 9 | import signal 10 | import contextlib 11 | import optparse 12 | 13 | from mutagen._util import iterbytes 14 | 15 | 16 | def split_escape(string, sep, maxsplit=None, escape_char="\\"): 17 | """Like unicode/str/bytes.split but allows for the separator to be escaped 18 | 19 | If passed unicode/str/bytes will only return list of unicode/str/bytes. 20 | """ 21 | 22 | assert len(sep) == 1 23 | assert len(escape_char) == 1 24 | 25 | if isinstance(string, bytes): 26 | if isinstance(escape_char, str): 27 | escape_char = escape_char.encode("ascii") 28 | iter_ = iterbytes 29 | else: 30 | iter_ = iter 31 | 32 | if maxsplit is None: 33 | maxsplit = len(string) 34 | 35 | empty = string[:0] 36 | result = [] 37 | current = empty 38 | escaped = False 39 | for char in iter_(string): 40 | if escaped: 41 | if char != escape_char and char != sep: 42 | current += escape_char 43 | current += char 44 | escaped = False 45 | else: 46 | if char == escape_char: 47 | escaped = True 48 | elif char == sep and len(result) < maxsplit: 49 | result.append(current) 50 | current = empty 51 | else: 52 | current += char 53 | result.append(current) 54 | return result 55 | 56 | 57 | class SignalHandler(object): 58 | 59 | def __init__(self): 60 | self._interrupted = False 61 | self._nosig = False 62 | self._init = False 63 | 64 | def init(self): 65 | signal.signal(signal.SIGINT, self._handler) 66 | signal.signal(signal.SIGTERM, self._handler) 67 | if os.name != "nt": 68 | signal.signal(signal.SIGHUP, self._handler) 69 | 70 | def _handler(self, signum, frame): 71 | self._interrupted = True 72 | if not self._nosig: 73 | raise SystemExit("Aborted...") 74 | 75 | @contextlib.contextmanager 76 | def block(self): 77 | """While this context manager is active any signals for aborting 78 | the process will be queued and exit the program once the context 79 | is left. 80 | """ 81 | 82 | self._nosig = True 83 | yield 84 | self._nosig = False 85 | if self._interrupted: 86 | raise SystemExit("Aborted...") 87 | 88 | 89 | OptionParser = optparse.OptionParser 90 | -------------------------------------------------------------------------------- /mutagen/_tools/moggsplit.py: -------------------------------------------------------------------------------- 1 | # Copyright 2006 Joe Wreschnig 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | 8 | """Split a multiplex/chained Ogg file into its component parts.""" 9 | 10 | import os 11 | import sys 12 | 13 | import mutagen.ogg 14 | 15 | from ._util import SignalHandler, OptionParser 16 | 17 | 18 | _sig = SignalHandler() 19 | 20 | 21 | def main(argv): 22 | from mutagen.ogg import OggPage 23 | parser = OptionParser( 24 | usage="%prog [options] filename.ogg ...", 25 | description="Split Ogg logical streams using Mutagen.", 26 | version="Mutagen %s" % ".".join(map(str, mutagen.version)) 27 | ) 28 | 29 | parser.add_option( 30 | "--extension", dest="extension", default="ogg", metavar='ext', 31 | help="use this extension (default 'ogg')") 32 | parser.add_option( 33 | "--pattern", dest="pattern", default="%(base)s-%(stream)d.%(ext)s", 34 | metavar='pattern', help="name files using this pattern") 35 | parser.add_option( 36 | "--m3u", dest="m3u", action="store_true", default=False, 37 | help="generate an m3u (playlist) file") 38 | 39 | (options, args) = parser.parse_args(argv[1:]) 40 | if not args: 41 | raise SystemExit(parser.print_help() or 1) 42 | 43 | format = {'ext': options.extension} 44 | for filename in args: 45 | with _sig.block(): 46 | fileobjs = {} 47 | format["base"] = os.path.splitext(os.path.basename(filename))[0] 48 | with open(filename, "rb") as fileobj: 49 | if options.m3u: 50 | m3u = open(format["base"] + ".m3u", "w") 51 | fileobjs["m3u"] = m3u 52 | else: 53 | m3u = None 54 | while True: 55 | try: 56 | page = OggPage(fileobj) 57 | except EOFError: 58 | break 59 | else: 60 | format["stream"] = page.serial 61 | if page.serial not in fileobjs: 62 | new_filename = options.pattern % format 63 | new_fileobj = open(new_filename, "wb") 64 | fileobjs[page.serial] = new_fileobj 65 | if m3u: 66 | m3u.write(new_filename + "\r\n") 67 | fileobjs[page.serial].write(page.write()) 68 | for f in fileobjs.values(): 69 | f.close() 70 | 71 | 72 | def entry_point(): 73 | _sig.init() 74 | return main(sys.argv) 75 | -------------------------------------------------------------------------------- /docs/user/gettingstarted.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Getting Started 3 | =============== 4 | 5 | .. currentmodule:: mutagen 6 | 7 | The `File` functions takes any audio file, guesses its type and returns a 8 | `FileType` instance or `None`. 9 | 10 | :: 11 | 12 | >>> import mutagen 13 | >>> mutagen.File("11. The Way It Is.ogg") 14 | {'album': [u'Always Outnumbered, Never Outgunned'], 15 | 'title': [u'The Way It Is'], 'artist': [u'The Prodigy'], 16 | 'tracktotal': [u'12'], 'albumartist': [u'The Prodigy'],'date': [u'2004'], 17 | 'tracknumber': [u'11'], 18 | >>> _.info.pprint() 19 | u'Ogg Vorbis, 346.43 seconds, 499821 bps' 20 | >>> 21 | 22 | The following code loads a FLAC file, sets its title, prints all tag data, 23 | then saves the file. 24 | 25 | :: 26 | 27 | from mutagen.flac import FLAC 28 | 29 | audio = FLAC("example.flac") 30 | audio["title"] = u"An example" 31 | audio.pprint() 32 | audio.save() 33 | 34 | 35 | The following example gets the length and bitrate of an MP3 file. 36 | 37 | :: 38 | 39 | from mutagen.mp3 import MP3 40 | 41 | audio = MP3("example.mp3") 42 | print(audio.info.length) 43 | print(audio.info.bitrate) 44 | 45 | 46 | The following deletes an ID3 tag from an MP3 file. 47 | 48 | :: 49 | 50 | from mutagen.id3 import ID3 51 | 52 | audio = ID3("example.mp3") 53 | audio.delete() 54 | 55 | 56 | Here we parse a Vorbis file as FLAC, which leads to an `MutagenError` being 57 | raised. 58 | 59 | 60 | :: 61 | 62 | >>> import mutagen.flac 63 | >>> mutagen.flac.FLAC("11. The Way It Is.ogg") 64 | Traceback (most recent call last): 65 | File "", line 1, in 66 | File "/usr/lib/python2.7/dist-packages/mutagen/_file.py", line 42, in __init__ 67 | self.load(filename, *args, **kwargs) 68 | File "/usr/lib/python2.7/dist-packages/mutagen/flac.py", line 759, in load 69 | self.__check_header(fileobj) 70 | File "/usr/lib/python2.7/dist-packages/mutagen/flac.py", line 867, in __check_header 71 | "%r is not a valid FLAC file" % fileobj.name) 72 | mutagen.flac.FLACNoHeaderError: '11. The Way It Is.ogg' is not a valid FLAC file 73 | 74 | 75 | Unicode 76 | ^^^^^^^ 77 | 78 | Mutagen has full Unicode support for all formats. When you assign text 79 | strings, we strongly recommend using Python unicode objects rather 80 | than str objects. If you use str objects, Mutagen will assume they are 81 | in UTF-8. (This does not apply to filenames.) 82 | 83 | 84 | Multiple Values 85 | ^^^^^^^^^^^^^^^ 86 | 87 | Most tag formats support multiple values for each key, so when you 88 | access them (e.g. ``audio["title"]``) you will get a list of strings 89 | rather than a single one (``[u"An example"]`` rather than ``u"An example"``). 90 | Similarly, you can assign a list of strings rather than a single one. 91 | -------------------------------------------------------------------------------- /tests/test_wavpack.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | 4 | from mutagen.wavpack import WavPack, error as WavPackError 5 | from tests import TestCase, DATA_DIR 6 | 7 | 8 | class TWavPack(TestCase): 9 | 10 | def setUp(self): 11 | self.audio = WavPack(os.path.join(DATA_DIR, "silence-44-s.wv")) 12 | 13 | def test_version(self): 14 | self.failUnlessEqual(self.audio.info.version, 0x403) 15 | 16 | def test_channels(self): 17 | self.failUnlessEqual(self.audio.info.channels, 2) 18 | 19 | def test_sample_rate(self): 20 | self.failUnlessEqual(self.audio.info.sample_rate, 44100) 21 | 22 | def test_bits_per_sample(self): 23 | self.failUnlessEqual(self.audio.info.bits_per_sample, 16) 24 | 25 | def test_length(self): 26 | self.failUnlessAlmostEqual(self.audio.info.length, 3.68, 2) 27 | 28 | def test_not_my_file(self): 29 | self.failUnlessRaises( 30 | WavPackError, WavPack, os.path.join(DATA_DIR, "empty.ogg")) 31 | 32 | def test_pprint(self): 33 | self.audio.pprint() 34 | 35 | def test_mime(self): 36 | self.failUnless("audio/x-wavpack" in self.audio.mime) 37 | 38 | 39 | class TWavPackNoLength(TestCase): 40 | 41 | def setUp(self): 42 | self.audio = WavPack(os.path.join(DATA_DIR, "no_length.wv")) 43 | 44 | def test_version(self): 45 | self.failUnlessEqual(self.audio.info.version, 0x407) 46 | 47 | def test_channels(self): 48 | self.failUnlessEqual(self.audio.info.channels, 2) 49 | 50 | def test_sample_rate(self): 51 | self.failUnlessEqual(self.audio.info.sample_rate, 44100) 52 | 53 | def test_bits_per_sample(self): 54 | self.failUnlessEqual(self.audio.info.bits_per_sample, 16) 55 | 56 | def test_length(self): 57 | self.failUnlessAlmostEqual(self.audio.info.length, 3.705, 3) 58 | 59 | def test_pprint(self): 60 | self.audio.pprint() 61 | 62 | def test_mime(self): 63 | self.failUnless("audio/x-wavpack" in self.audio.mime) 64 | 65 | 66 | class TWavPackDSD(TestCase): 67 | 68 | def setUp(self): 69 | self.audio = WavPack(os.path.join(DATA_DIR, "dsd.wv")) 70 | 71 | def test_version(self): 72 | self.failUnlessEqual(self.audio.info.version, 0x410) 73 | 74 | def test_channels(self): 75 | self.failUnlessEqual(self.audio.info.channels, 2) 76 | 77 | def test_sample_rate(self): 78 | self.failUnlessEqual(self.audio.info.sample_rate, 352800) 79 | 80 | def test_bits_per_sample(self): 81 | self.failUnlessEqual(self.audio.info.bits_per_sample, 1) 82 | 83 | def test_length(self): 84 | self.failUnlessAlmostEqual(self.audio.info.length, 0.01, 3) 85 | 86 | def test_pprint(self): 87 | self.audio.pprint() 88 | 89 | def test_mime(self): 90 | self.failUnless("audio/x-wavpack" in self.audio.mime) 91 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | permissions: {} 6 | 7 | jobs: 8 | 9 | test: 10 | runs-on: ${{ matrix.os }} 11 | permissions: 12 | contents: read 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', 'pypy-3.11'] 18 | exclude: 19 | # too slow 20 | - os: macos-latest 21 | python-version: pypy-3.11 22 | - os: windows-latest 23 | python-version: pypy-3.11 24 | include: 25 | - os: ubuntu-latest 26 | pip-cache: ~/.cache/pip 27 | uv-cache: ~/.cache/uv 28 | - os: macos-latest 29 | pip-cache: ~/Library/Caches/pip 30 | uv-cache: ~/.cache/uv 31 | - os: windows-latest 32 | pip-cache: ~\AppData\Local\pip\Cache 33 | uv-cache: ~\AppData\Local\uv\cache 34 | - os: ubuntu-latest 35 | python-version: 3.10 36 | build-docs: true 37 | - os: ubuntu-latest 38 | python-version: 3.12 39 | build-docs: true 40 | build-dist: true 41 | - python-version: 3.12 42 | run-mypy: true 43 | steps: 44 | - uses: actions/checkout@v5 45 | with: 46 | persist-credentials: false 47 | - name: Set up Python ${{ matrix.python-version }} 48 | uses: actions/setup-python@v6 49 | with: 50 | python-version: ${{ matrix.python-version }} 51 | allow-prereleases: true 52 | 53 | - uses: actions/cache@v4 54 | with: 55 | path: | 56 | ${{ matrix.pip-cache }} 57 | ${{ matrix.uv-cache }} 58 | key: ${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }} 59 | restore-keys: | 60 | ${{ runner.os }}- 61 | 62 | - name: Install dependencies 63 | run: | 64 | pipx install uv build 65 | uv sync 66 | - name: Run tests 67 | run: | 68 | uv run coverage run --branch setup.py test 69 | uv run coverage xml -i 70 | - name: Run mypy 71 | if: matrix.run-mypy 72 | run: | 73 | uv run mypy . 74 | - name: Run flake8 75 | run: | 76 | uv run flake8 77 | - name: Upload coverage to Codecov 78 | uses: codecov/codecov-action@v5 79 | - name: Build docs 80 | if: matrix.build-docs 81 | run: | 82 | uv sync --group docs 83 | uv run sphinx-build -W -a -E -b html -n docs docs/_build 84 | - name: Build dist 85 | if: matrix.build-dist 86 | run: | 87 | pyproject-build 88 | - name: Upload dist 89 | if: matrix.build-dist 90 | uses: actions/upload-artifact@v5 91 | with: 92 | name: dist 93 | path: dist/* 94 | -------------------------------------------------------------------------------- /tests/test_oggopus.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | from io import BytesIO 4 | 5 | from mutagen.oggopus import OggOpus, OggOpusInfo, delete, error 6 | from mutagen.ogg import OggPage 7 | from tests import TestCase, DATA_DIR, get_temp_copy 8 | from tests.test_ogg import TOggFileTypeMixin 9 | 10 | 11 | class TOggOpus(TestCase, TOggFileTypeMixin): 12 | Kind = OggOpus 13 | 14 | def setUp(self): 15 | self.filename = get_temp_copy(os.path.join(DATA_DIR, "example.opus")) 16 | self.audio = self.Kind(self.filename) 17 | 18 | def tearDown(self): 19 | os.unlink(self.filename) 20 | 21 | def test_length(self): 22 | self.failUnlessAlmostEqual(self.audio.info.length, 11.35, 2) 23 | 24 | def test_misc(self): 25 | self.failUnlessEqual(self.audio.info.channels, 1) 26 | self.failUnless(self.audio.tags.vendor.startswith("libopus")) 27 | 28 | def test_module_delete(self): 29 | delete(self.filename) 30 | self.scan_file() 31 | self.failIf(self.Kind(self.filename).tags) 32 | 33 | def test_mime(self): 34 | self.failUnless("audio/ogg" in self.audio.mime) 35 | self.failUnless("audio/ogg; codecs=opus" in self.audio.mime) 36 | 37 | def test_invalid_not_first(self): 38 | with open(self.filename, "rb") as h: 39 | page = OggPage(h) 40 | page.first = False 41 | self.failUnlessRaises(error, OggOpusInfo, BytesIO(page.write())) 42 | 43 | def test_unsupported_version(self): 44 | with open(self.filename, "rb") as h: 45 | page = OggPage(h) 46 | data = bytearray(page.packets[0]) 47 | 48 | data[8] = 0x03 49 | page.packets[0] = bytes(data) 50 | OggOpusInfo(BytesIO(page.write())) 51 | 52 | data[8] = 0x10 53 | page.packets[0] = bytes(data) 54 | self.failUnlessRaises(error, OggOpusInfo, BytesIO(page.write())) 55 | 56 | def test_preserve_non_padding(self): 57 | self.audio["FOO"] = ["BAR"] 58 | self.audio.save() 59 | 60 | extra_data = b"\xde\xad\xbe\xef" 61 | 62 | with open(self.filename, "r+b") as fobj: 63 | OggPage(fobj) # header 64 | page = OggPage(fobj) 65 | data = OggPage.to_packets([page])[0] 66 | data = data.rstrip(b"\x00") + b"\x01" + extra_data 67 | new_pages = OggPage.from_packets([data], page.sequence) 68 | OggPage.replace(fobj, [page], new_pages) 69 | 70 | OggOpus(self.filename).save() 71 | 72 | with open(self.filename, "rb") as fobj: 73 | OggPage(fobj) # header 74 | page = OggPage(fobj) 75 | data = OggPage.to_packets([page])[0] 76 | self.assertTrue(data.endswith(b"\x01" + extra_data)) 77 | 78 | self.assertEqual(OggOpus(self.filename).tags._padding, 0) 79 | 80 | def test_init_padding(self): 81 | self.assertEqual(self.audio.tags._padding, 196) 82 | -------------------------------------------------------------------------------- /tests/test_dsdiff.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | 4 | from mutagen.dsdiff import DSDIFF, IffError 5 | 6 | from tests import TestCase, DATA_DIR, get_temp_copy 7 | 8 | 9 | class TDSDIFF(TestCase): 10 | silence_1 = os.path.join(DATA_DIR, '2822400-1ch-0s-silence.dff') 11 | silence_2 = os.path.join(DATA_DIR, '5644800-2ch-s01-silence.dff') 12 | silence_dst = os.path.join(DATA_DIR, '5644800-2ch-s01-silence-dst.dff') 13 | 14 | def setUp(self): 15 | self.dff_1 = DSDIFF(self.silence_1) 16 | self.dff_2 = DSDIFF(self.silence_2) 17 | self.dff_dst = DSDIFF(self.silence_dst) 18 | 19 | self.dff_id3 = DSDIFF(get_temp_copy(self.silence_dst)) 20 | self.dff_no_id3 = DSDIFF(get_temp_copy(self.silence_2)) 21 | 22 | def test_channels(self): 23 | self.failUnlessEqual(self.dff_1.info.channels, 1) 24 | self.failUnlessEqual(self.dff_2.info.channels, 2) 25 | self.failUnlessEqual(self.dff_dst.info.channels, 2) 26 | 27 | def test_length(self): 28 | self.failUnlessEqual(self.dff_1.info.length, 0) 29 | self.failUnlessEqual(self.dff_2.info.length, 0.01) 30 | self.failUnlessEqual(self.dff_dst.info.length, 0) 31 | 32 | def test_sampling_frequency(self): 33 | self.failUnlessEqual(self.dff_1.info.sample_rate, 2822400) 34 | self.failUnlessEqual(self.dff_2.info.sample_rate, 5644800) 35 | self.failUnlessEqual(self.dff_dst.info.sample_rate, 5644800) 36 | 37 | def test_bits_per_sample(self): 38 | self.failUnlessEqual(self.dff_1.info.bits_per_sample, 1) 39 | 40 | def test_bitrate(self): 41 | self.failUnlessEqual(self.dff_1.info.bitrate, 2822400) 42 | self.failUnlessEqual(self.dff_2.info.bitrate, 11289600) 43 | self.failUnlessEqual(self.dff_dst.info.bitrate, 0) 44 | 45 | def test_notdsf(self): 46 | self.failUnlessRaises(IffError, DSDIFF, os.path.join( 47 | DATA_DIR, '2822400-1ch-0s-silence.dsf')) 48 | 49 | def test_pprint(self): 50 | self.failUnless(self.dff_1.pprint()) 51 | 52 | def test_mime(self): 53 | self.failUnless("audio/x-dff" in self.dff_1.mime) 54 | 55 | def test_update_tags(self): 56 | from mutagen.id3 import TIT1 57 | tags = self.dff_id3.tags 58 | tags.add(TIT1(encoding=3, text="foobar")) 59 | tags.save() 60 | 61 | new = DSDIFF(self.dff_id3.filename) 62 | self.failUnlessEqual(new["TIT1"], ["foobar"]) 63 | 64 | def test_delete_tags(self): 65 | self.dff_id3.tags.delete() 66 | new = DSDIFF(self.dff_id3.filename) 67 | self.failUnlessEqual(new.tags, None) 68 | 69 | def test_save_tags(self): 70 | from mutagen.id3 import TIT1 71 | self.dff_no_id3.add_tags() 72 | tags = self.dff_no_id3.tags 73 | tags.add(TIT1(encoding=3, text="foobar")) 74 | tags.save(self.dff_no_id3.filename) 75 | 76 | new = DSDIFF(self.dff_no_id3.filename) 77 | self.failUnlessEqual(new["TIT1"], ["foobar"]) 78 | -------------------------------------------------------------------------------- /tests/test_tools_mid3iconv.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | 4 | from mutagen.id3 import ID3 5 | 6 | from tests.test_tools import _TTools 7 | from tests import DATA_DIR, get_temp_copy 8 | 9 | 10 | AMBIGUOUS = b"\xc3\xae\xc3\xa5\xc3\xb4\xc3\xb2 \xc3\xa0\xc3\xa9\xc3\xa7\xc3" \ 11 | b"\xa5\xc3\xa3 \xc3\xb9\xc3\xac \xc3\xab\xc3\xa5\xc3\xa5\xc3\xb8" \ 12 | b"\xc3\xba" 13 | CODECS = ["utf8", "latin-1", "Windows-1255", "gbk"] 14 | 15 | 16 | class TMid3Iconv(_TTools): 17 | 18 | TOOL_NAME = u"mid3iconv" 19 | 20 | def setUp(self): 21 | super(TMid3Iconv, self).setUp() 22 | self.filename = get_temp_copy( 23 | os.path.join(DATA_DIR, 'silence-44-s.mp3')) 24 | 25 | def tearDown(self): 26 | super(TMid3Iconv, self).tearDown() 27 | os.unlink(self.filename) 28 | 29 | def test_noop(self): 30 | res, out = self.call() 31 | self.failIf(res) 32 | self.failUnless("Usage:" in out) 33 | 34 | def test_debug(self): 35 | res, out = self.call("-d", "-p", self.filename) 36 | self.failIf(res) 37 | self.assertFalse("b'" in out) 38 | self.failUnless("TCON=Silence" in out) 39 | 40 | def test_quiet(self): 41 | res, out = self.call("-q", self.filename) 42 | self.failIf(res) 43 | self.failIf(out) 44 | 45 | def test_test_data(self): 46 | results = set() 47 | for codec in CODECS: 48 | results.add(AMBIGUOUS.decode(codec)) 49 | self.failUnlessEqual(len(results), len(CODECS)) 50 | 51 | def test_conv_basic(self): 52 | from mutagen.id3 import TALB 53 | 54 | for codec in CODECS: 55 | f = ID3(self.filename) 56 | f.add(TALB(text=[AMBIGUOUS.decode("latin-1")], encoding=0)) 57 | f.save() 58 | res, out = self.call( 59 | "-d", "-e", str(codec), self.filename) 60 | f = ID3(self.filename) 61 | self.failUnlessEqual(f["TALB"].encoding, 1) 62 | self.failUnlessEqual(f["TALB"].text[0], AMBIGUOUS.decode(codec)) 63 | 64 | def test_comm(self): 65 | from mutagen.id3 import COMM 66 | 67 | for codec in CODECS: 68 | f = ID3(self.filename) 69 | frame = COMM(desc="", lang="eng", encoding=0, 70 | text=[AMBIGUOUS.decode("latin-1")]) 71 | f.add(frame) 72 | f.save() 73 | res, out = self.call( 74 | "-d", "-e", str(codec), self.filename) 75 | f = ID3(self.filename) 76 | new_frame = f[frame.HashKey] 77 | self.failUnlessEqual(new_frame.encoding, 1) 78 | self.failUnlessEqual(new_frame.text[0], AMBIGUOUS.decode(codec)) 79 | 80 | def test_remove_v1(self): 81 | from mutagen.id3 import ParseID3v1 82 | res, out = self.call("--remove-v1", self.filename) 83 | 84 | with open(self.filename, "rb") as h: 85 | h.seek(-128, 2) 86 | data = h.read() 87 | self.failUnlessEqual(len(data), 128) 88 | self.failIf(ParseID3v1(data)) 89 | -------------------------------------------------------------------------------- /mutagen/trueaudio.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2006 Joe Wreschnig 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | 8 | """True Audio audio stream information and tags. 9 | 10 | True Audio is a lossless format designed for real-time encoding and 11 | decoding. This module is based on the documentation at 12 | http://tausoft.org/wiki/True_Audio_Codec_Format 13 | 14 | True Audio files use ID3 tags. 15 | """ 16 | 17 | __all__ = ["TrueAudio", "Open", "delete", "EasyTrueAudio"] 18 | 19 | from mutagen import StreamInfo 20 | from mutagen.id3 import ID3FileType, delete 21 | from mutagen._util import cdata, MutagenError, convert_error, endswith 22 | 23 | 24 | class error(MutagenError): 25 | pass 26 | 27 | 28 | class TrueAudioHeaderError(error): 29 | pass 30 | 31 | 32 | class TrueAudioInfo(StreamInfo): 33 | """TrueAudioInfo() 34 | 35 | True Audio stream information. 36 | 37 | Attributes: 38 | length (`float`): audio length, in seconds 39 | sample_rate (`int`): audio sample rate, in Hz 40 | """ 41 | 42 | @convert_error(IOError, TrueAudioHeaderError) 43 | def __init__(self, fileobj, offset): 44 | """Raises TrueAudioHeaderError""" 45 | 46 | fileobj.seek(offset or 0) 47 | header = fileobj.read(18) 48 | if len(header) != 18 or not header.startswith(b"TTA"): 49 | raise TrueAudioHeaderError("TTA header not found") 50 | self.sample_rate = cdata.uint_le(header[10:14]) 51 | samples = cdata.uint_le(header[14:18]) 52 | self.length = 0.0 53 | if self.sample_rate != 0: 54 | self.length = float(samples) / self.sample_rate 55 | 56 | def pprint(self): 57 | return u"True Audio, %.2f seconds, %d Hz." % ( 58 | self.length, self.sample_rate) 59 | 60 | 61 | class TrueAudio(ID3FileType): 62 | """TrueAudio(filething, ID3=None) 63 | 64 | A True Audio file. 65 | 66 | Arguments: 67 | filething (filething) 68 | ID3 (mutagen.id3.ID3) 69 | 70 | Attributes: 71 | info (`TrueAudioInfo`) 72 | tags (`mutagen.id3.ID3`) 73 | """ 74 | 75 | _Info = TrueAudioInfo 76 | _mimes = ["audio/x-tta"] 77 | 78 | @staticmethod 79 | def score(filename, fileobj, header): 80 | return (header.startswith(b"ID3") + header.startswith(b"TTA") + 81 | endswith(filename.lower(), b".tta") * 2) 82 | 83 | 84 | Open = TrueAudio 85 | 86 | 87 | class EasyTrueAudio(TrueAudio): 88 | """EasyTrueAudio(filething, ID3=None) 89 | 90 | Like MP3, but uses EasyID3 for tags. 91 | 92 | Arguments: 93 | filething (filething) 94 | ID3 (mutagen.id3.ID3) 95 | 96 | Attributes: 97 | info (`TrueAudioInfo`) 98 | tags (`mutagen.easyid3.EasyID3`) 99 | """ 100 | 101 | from mutagen.easyid3 import EasyID3 as ID3 102 | ID3 = ID3 # type: ignore 103 | -------------------------------------------------------------------------------- /docs/user/classes.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Class Overview 3 | ============== 4 | 5 | .. currentmodule:: mutagen 6 | 7 | The main notable classes in mutagen are :class:`FileType`, 8 | :class:`StreamInfo`, :class:`Tags`, :class:`Metadata` and for error handling 9 | the :class:`MutagenError` exception. 10 | 11 | FileType 12 | -------- 13 | 14 | A :class:`FileType` is used to represent container formats which contain 15 | audio/video and tags. In some cases, like MP4, the tagging format and the 16 | container are inseparable, while in other cases, like MP3, the container does 17 | not know about tags and the metadata is loosely attached to the file. 18 | 19 | A :class:`FileType` always represents only one combination of an audio stream 20 | (StreamInfo) and tags (Tags). In case there are multiple audio streams in one 21 | file, mutagen exposes the primary/first one. In case tags are associated with 22 | the audio/video stream the FileType represents the stream and its tags. Given 23 | a file containing Ogg Theora video with Vorbis audio, depending on whether you 24 | use :class:`oggvorbis.OggVorbis` or :class:`oggtheora.OggTheora` for parsing, 25 | you get the respective tags and stream info associated with either the vorbis 26 | or the theora stream. 27 | 28 | It provides a dict-like interface which acts as a proxy to the containing 29 | `Tags` instance. 30 | 31 | :: 32 | 33 | >>> from mutagen.oggvorbis import OggVorbis 34 | >>> f = OggVorbis("11. The Way It Is.ogg") 35 | >>> type(f) 36 | 37 | >>> 38 | 39 | 40 | Tags 41 | ---- 42 | 43 | Each FileType has an attribute tags which holds a :class:`Tags` instance. The 44 | Tags interface depends mostly on each format. It exposes a dict-like interface 45 | where the type of keys and values depends on the implementation of each 46 | format. 47 | 48 | :: 49 | 50 | >>> type(f.tags) 51 | 52 | >>> 53 | 54 | 55 | StreamInfo 56 | ---------- 57 | 58 | Similar to Tags, a FileType also holds a :class:`StreamInfo` instance. It 59 | represents one audio/video stream and contains its length, codec information, 60 | number of channels, etc. 61 | 62 | :: 63 | 64 | >>> type(f.info) 65 | 66 | >>> print(f.info.pprint()) 67 | Ogg Vorbis, 346.43 seconds, 499821 bps 68 | >>> 69 | 70 | 71 | Metadata 72 | -------- 73 | 74 | `Metadata` is a mixture between `FileType` and `Tags` and is used for 75 | tagging formats which are not depending on a container format. They can be 76 | attached to any file. This includes ID3 and APEv2. 77 | 78 | :: 79 | 80 | >>> from mutagen.id3 import ID3 81 | >>> m = ID3("08. Firestarter.mp3") 82 | >>> type(m) 83 | 84 | >>> 85 | 86 | 87 | MutagenError 88 | ------------ 89 | 90 | The :class:`MutagenError` exception is the base class for all custom 91 | exceptions in mutagen. 92 | 93 | 94 | :: 95 | 96 | from mutagen import MutagenError 97 | 98 | try: 99 | f = OggVorbis("11. The Way It Is.ogg") 100 | except MutagenError: 101 | print("Loading failed :(") 102 | -------------------------------------------------------------------------------- /docs/user/vcomment.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Vorbis Comment 3 | ============== 4 | 5 | VorbisComment is the tagging format used in Ogg and FLAC container formats. In 6 | mutagen this corresponds to the tags in all subclasses of 7 | :class:`mutagen.ogg.OggFileType` and the :class:`mutagen.flac.FLAC` class. 8 | 9 | Embedded Images 10 | ~~~~~~~~~~~~~~~ 11 | 12 | The most common way to include images in VorbisComment (except in FLAC) is to 13 | store a base64 encoded FLAC Picture block with the key 14 | ``metadata_block_picture`` (see 15 | https://wiki.xiph.org/VorbisComment#Cover_art). See the following code example 16 | on how to read and write images this way:: 17 | 18 | # READING / SAVING 19 | import base64 20 | from mutagen.oggvorbis import OggVorbis 21 | from mutagen.flac import Picture, error as FLACError 22 | 23 | file_ = OggVorbis("somefile.ogg") 24 | 25 | for b64_data in file_.get("metadata_block_picture", []): 26 | try: 27 | data = base64.b64decode(b64_data) 28 | except (TypeError, ValueError): 29 | continue 30 | 31 | try: 32 | picture = Picture(data) 33 | except FLACError: 34 | continue 35 | 36 | extensions = { 37 | "image/jpeg": "jpg", 38 | "image/png": "png", 39 | "image/gif": "gif", 40 | } 41 | ext = extensions.get(picture.mime, "jpg") 42 | 43 | with open("image.%s" % ext, "wb") as h: 44 | h.write(picture.data) 45 | 46 | :: 47 | 48 | # WRITING 49 | import base64 50 | from mutagen.oggvorbis import OggVorbis 51 | from mutagen.flac import Picture 52 | 53 | file_ = OggVorbis("somefile.ogg") 54 | 55 | with open("image.jpeg", "rb") as h: 56 | data = h.read() 57 | 58 | picture = Picture() 59 | picture.data = data 60 | picture.type = 17 61 | picture.desc = u"A bright coloured fish" 62 | picture.mime = u"image/jpeg" 63 | picture.width = 100 64 | picture.height = 100 65 | picture.depth = 24 66 | 67 | picture_data = picture.write() 68 | encoded_data = base64.b64encode(picture_data) 69 | vcomment_value = encoded_data.decode("ascii") 70 | 71 | file_["metadata_block_picture"] = [vcomment_value] 72 | file_.save() 73 | 74 | 75 | Some programs also write base64 encoded image data directly into the 76 | ``coverart`` field and sometimes a corresponding mime type into the 77 | ``coverartmime`` field:: 78 | 79 | # READING 80 | import base64 81 | import itertools 82 | from mutagen.oggvorbis import OggVorbis 83 | 84 | file_ = OggVorbis("somefile.ogg") 85 | 86 | values = file_.get("coverart", []) 87 | mimes = file_.get("coverartmime", []) 88 | for value, mime in itertools.izip_longest(values, mimes, fillvalue=u""): 89 | try: 90 | image_data = base64.b64decode(value.encode("ascii")) 91 | except (TypeError, ValueError): 92 | continue 93 | 94 | print(mime) 95 | print(image_data) 96 | 97 | 98 | FLAC supports images directly, see :class:`mutagen.flac.Picture`, 99 | :attr:`mutagen.flac.FLAC.pictures`, :meth:`mutagen.flac.FLAC.add_picture` and 100 | :meth:`mutagen.flac.FLAC.clear_pictures`. 101 | -------------------------------------------------------------------------------- /docs/user/examples/fileobj-iface.py: -------------------------------------------------------------------------------- 1 | class IOInterface(object): 2 | """This is the interface mutagen expects from custom file-like 3 | objects. 4 | 5 | For loading read(), tell() and seek() have to be implemented. "name" 6 | is optional. 7 | 8 | For saving/deleting write(), flush() and truncate() have to be 9 | implemented in addition. fileno() is optional. 10 | """ 11 | 12 | # For loading 13 | 14 | def tell(self): 15 | """Returns he current offset as int. Always >= 0. 16 | 17 | Raises IOError in case fetching the position is for some reason 18 | not possible. 19 | """ 20 | 21 | raise NotImplementedError 22 | 23 | def read(self, size=-1): 24 | """Returns 'size' amount of bytes or less if there is no more data. 25 | If no size is given all data is returned. size can be >= 0. 26 | 27 | Raises IOError in case reading failed while data was available. 28 | """ 29 | 30 | raise NotImplementedError 31 | 32 | def seek(self, offset, whence=0): 33 | """Move to a new offset either relative or absolute. whence=0 is 34 | absolute, whence=1 is relative, whence=2 is relative to the end. 35 | 36 | Any relative or absolute seek operation which would result in a 37 | negative position is undefined and that case can be ignored 38 | in the implementation. 39 | 40 | Any seek operation which moves the position after the stream 41 | should succeed. tell() should report that position and read() 42 | should return an empty bytes object. 43 | 44 | Returns Nothing. 45 | Raise IOError in case the seek operation asn't possible. 46 | """ 47 | 48 | raise NotImplementedError 49 | 50 | # For loading, but optional 51 | 52 | @property 53 | def name(self): 54 | """Should return text. For example the file name. 55 | 56 | If not available the attribute can be missing or can return 57 | an empty string. 58 | 59 | Will be used for error messages and type detection. 60 | """ 61 | 62 | raise NotImplementedError 63 | 64 | # For writing 65 | 66 | def write(self, data): 67 | """Write data to the file. 68 | 69 | Returns Nothing. 70 | Raises IOError 71 | """ 72 | 73 | raise NotImplementedError 74 | 75 | def truncate(self, size=None): 76 | """Truncate to the current position or size if size is given. 77 | 78 | The current position or given size will never be larger than the 79 | file size. 80 | 81 | This has to flush write buffers in case writing is buffered. 82 | 83 | Returns Nothing. 84 | Raises IOError. 85 | """ 86 | 87 | raise NotImplementedError 88 | 89 | def flush(self): 90 | """Flush the write buffer. 91 | 92 | Returns Nothing. 93 | Raises IOError. 94 | """ 95 | 96 | raise NotImplementedError 97 | 98 | # For writing, but optional 99 | 100 | def fileno(self): 101 | """Returns the file descriptor (int) or raises IOError 102 | if there is none. 103 | 104 | Will be used for low level operations if available. 105 | """ 106 | 107 | raise NotImplementedError 108 | -------------------------------------------------------------------------------- /tests/test_aac.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | 4 | from mutagen.id3 import ID3, TIT1 5 | from mutagen.aac import AAC, AACError 6 | 7 | from tests import TestCase, DATA_DIR, get_temp_copy 8 | 9 | 10 | class TADTS(TestCase): 11 | 12 | def setUp(self): 13 | original = os.path.join(DATA_DIR, "empty.aac") 14 | self.filename = get_temp_copy(original) 15 | 16 | tag = ID3() 17 | tag.add(TIT1(text=[u"a" * 5000], encoding=3)) 18 | tag.save(self.filename) 19 | 20 | self.aac = AAC(original) 21 | self.aac_id3 = AAC(self.filename) 22 | 23 | def tearDown(self): 24 | os.remove(self.filename) 25 | 26 | def test_channels(self): 27 | self.failUnlessEqual(self.aac.info.channels, 2) 28 | self.failUnlessEqual(self.aac_id3.info.channels, 2) 29 | 30 | def test_bitrate(self): 31 | self.failUnlessEqual(self.aac.info.bitrate, 3159) 32 | self.failUnlessEqual(self.aac_id3.info.bitrate, 3159) 33 | 34 | def test_sample_rate(self): 35 | self.failUnlessEqual(self.aac.info.sample_rate, 44100) 36 | self.failUnlessEqual(self.aac_id3.info.sample_rate, 44100) 37 | 38 | def test_length(self): 39 | self.failUnlessAlmostEqual(self.aac.info.length, 3.70, 2) 40 | self.failUnlessAlmostEqual(self.aac_id3.info.length, 3.70, 2) 41 | 42 | def test_not_my_file(self): 43 | self.failUnlessRaises( 44 | AACError, AAC, 45 | os.path.join(DATA_DIR, "empty.ogg")) 46 | 47 | self.failUnlessRaises( 48 | AACError, AAC, 49 | os.path.join(DATA_DIR, "silence-44-s.mp3")) 50 | 51 | def test_pprint(self): 52 | self.assertEqual(self.aac.pprint(), self.aac_id3.pprint()) 53 | self.assertTrue("ADTS" in self.aac.pprint()) 54 | 55 | 56 | class TADIF(TestCase): 57 | 58 | def setUp(self): 59 | original = os.path.join(DATA_DIR, "adif.aac") 60 | self.filename = get_temp_copy(original) 61 | 62 | tag = ID3() 63 | tag.add(TIT1(text=[u"a" * 5000], encoding=3)) 64 | tag.save(self.filename) 65 | 66 | self.aac = AAC(original) 67 | self.aac_id3 = AAC(self.filename) 68 | 69 | def tearDown(self): 70 | os.remove(self.filename) 71 | 72 | def test_channels(self): 73 | self.failUnlessEqual(self.aac.info.channels, 2) 74 | self.failUnlessEqual(self.aac_id3.info.channels, 2) 75 | 76 | def test_bitrate(self): 77 | self.failUnlessEqual(self.aac.info.bitrate, 128000) 78 | self.failUnlessEqual(self.aac_id3.info.bitrate, 128000) 79 | 80 | def test_sample_rate(self): 81 | self.failUnlessEqual(self.aac.info.sample_rate, 48000) 82 | self.failUnlessEqual(self.aac_id3.info.sample_rate, 48000) 83 | 84 | def test_length(self): 85 | self.failUnlessAlmostEqual(self.aac.info.length, 0.25, 2) 86 | self.failUnlessAlmostEqual(self.aac_id3.info.length, 0.25, 2) 87 | 88 | def test_not_my_file(self): 89 | self.failUnlessRaises( 90 | AACError, AAC, 91 | os.path.join(DATA_DIR, "empty.ogg")) 92 | 93 | self.failUnlessRaises( 94 | AACError, AAC, 95 | os.path.join(DATA_DIR, "silence-44-s.mp3")) 96 | 97 | def test_pprint(self): 98 | self.assertEqual(self.aac.pprint(), self.aac_id3.pprint()) 99 | self.assertTrue("ADIF" in self.aac.pprint()) 100 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import re 3 | import os 4 | import sys 5 | import shutil 6 | import contextlib 7 | from io import StringIO 8 | from tempfile import mkstemp 9 | from unittest import TestCase as BaseTestCase 10 | 11 | try: 12 | import pytest 13 | except ImportError: 14 | raise SystemExit("pytest missing: sudo apt-get install python-pytest") 15 | 16 | 17 | DATA_DIR = os.path.join( 18 | os.path.dirname(os.path.realpath(__file__)), "data") 19 | assert isinstance(DATA_DIR, str) 20 | 21 | 22 | _fs_enc = sys.getfilesystemencoding() 23 | if "öäü".encode(_fs_enc, "replace").decode(_fs_enc) != u"öäü": 24 | raise RuntimeError("This test suite needs a unicode locale encoding. " 25 | "Try setting LANG=C.UTF-8") 26 | 27 | 28 | def get_temp_copy(path): 29 | """Returns a copy of the file with the same extension""" 30 | 31 | ext = os.path.splitext(path)[-1] 32 | fd, filename = mkstemp(suffix=ext) 33 | os.close(fd) 34 | shutil.copy(path, filename) 35 | return filename 36 | 37 | 38 | def get_temp_empty(ext=""): 39 | """Returns an empty file with the extension""" 40 | 41 | fd, filename = mkstemp(suffix=ext) 42 | os.close(fd) 43 | return filename 44 | 45 | 46 | @contextlib.contextmanager 47 | def capture_output(): 48 | """ 49 | with capture_output() as (stdout, stderr): 50 | some_action() 51 | print stdout.getvalue(), stderr.getvalue() 52 | """ 53 | 54 | err = StringIO() 55 | out = StringIO() 56 | old_err = sys.stderr 57 | old_out = sys.stdout 58 | sys.stderr = err 59 | sys.stdout = out 60 | 61 | try: 62 | yield (out, err) 63 | finally: 64 | sys.stderr = old_err 65 | sys.stdout = old_out 66 | 67 | 68 | class TestCase(BaseTestCase): 69 | 70 | def failUnlessRaisesRegexp(self, exc, re_, fun, *args, **kwargs): 71 | def wrapped(*args, **kwargs): 72 | try: 73 | fun(*args, **kwargs) 74 | except Exception as e: 75 | self.failUnless(re.search(re_, str(e))) 76 | raise 77 | self.failUnlessRaises(exc, wrapped, *args, **kwargs) 78 | 79 | # silence deprec warnings about useless renames 80 | failUnless = BaseTestCase.assertTrue 81 | failIf = BaseTestCase.assertFalse 82 | failUnlessEqual = BaseTestCase.assertEqual 83 | failUnlessRaises = BaseTestCase.assertRaises 84 | failUnlessAlmostEqual = BaseTestCase.assertAlmostEqual 85 | failIfEqual = BaseTestCase.assertNotEqual 86 | failIfAlmostEqual = BaseTestCase.assertNotAlmostEqual 87 | assertEquals = BaseTestCase.assertEqual 88 | assertNotEquals = BaseTestCase.assertNotEqual 89 | assert_ = BaseTestCase.assertTrue 90 | 91 | def assertReallyEqual(self, a, b): 92 | self.assertEqual(a, b) 93 | self.assertEqual(b, a) 94 | self.assertTrue(a == b) 95 | self.assertTrue(b == a) 96 | self.assertFalse(a != b) 97 | self.assertFalse(b != a) 98 | 99 | def assertReallyNotEqual(self, a, b): 100 | self.assertNotEqual(a, b) 101 | self.assertNotEqual(b, a) 102 | self.assertFalse(a == b) 103 | self.assertFalse(b == a) 104 | self.assertTrue(a != b) 105 | self.assertTrue(b != a) 106 | 107 | 108 | def unit(run=[], exitfirst=False): 109 | args = [] 110 | 111 | if run: 112 | args.append("-k") 113 | args.append(" or ".join(run)) 114 | 115 | if exitfirst: 116 | args.append("-x") 117 | 118 | args.append("tests") 119 | 120 | return pytest.main(args=args) 121 | -------------------------------------------------------------------------------- /mutagen/optimfrog.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2006 Lukas Lalinsky 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | 8 | """OptimFROG audio streams with APEv2 tags. 9 | 10 | OptimFROG is a lossless audio compression program. Its main goal is to 11 | reduce at maximum the size of audio files, while permitting bit 12 | identical restoration for all input. It is similar with the ZIP 13 | compression, but it is highly specialized to compress audio data. 14 | 15 | Only versions 4.5 and higher are supported. 16 | 17 | For more information, see http://www.losslessaudio.org/ 18 | """ 19 | 20 | __all__ = ["OptimFROG", "Open", "delete"] 21 | 22 | import struct 23 | 24 | from ._util import convert_error, endswith 25 | from mutagen import StreamInfo 26 | from mutagen.apev2 import APEv2File, error, delete 27 | 28 | 29 | SAMPLE_TYPE_BITS = { 30 | 0: 8, 31 | 1: 8, 32 | 2: 16, 33 | 3: 16, 34 | 4: 24, 35 | 5: 24, 36 | 6: 32, 37 | 7: 32, 38 | } 39 | 40 | 41 | class OptimFROGHeaderError(error): 42 | pass 43 | 44 | 45 | class OptimFROGInfo(StreamInfo): 46 | """OptimFROGInfo() 47 | 48 | OptimFROG stream information. 49 | 50 | Attributes: 51 | channels (`int`): number of audio channels 52 | length (`float`): file length in seconds, as a float 53 | sample_rate (`int`): audio sampling rate in Hz 54 | bits_per_sample (`int`): the audio sample size 55 | encoder_info (`mutagen.text`): encoder version, e.g. "5.100" 56 | """ 57 | 58 | @convert_error(IOError, OptimFROGHeaderError) 59 | def __init__(self, fileobj): 60 | """Raises OptimFROGHeaderError""" 61 | 62 | header = fileobj.read(76) 63 | if len(header) != 76 or not header.startswith(b"OFR "): 64 | raise OptimFROGHeaderError("not an OptimFROG file") 65 | data_size = struct.unpack("= 15: 79 | encoder_id = struct.unpack("> 4) + 4500) 81 | self.encoder_info = "%s.%s" % (version[0], version[1:]) 82 | else: 83 | self.encoder_info = "" 84 | 85 | def pprint(self): 86 | return u"OptimFROG, %.2f seconds, %d Hz" % (self.length, 87 | self.sample_rate) 88 | 89 | 90 | class OptimFROG(APEv2File): 91 | """OptimFROG(filething) 92 | 93 | Attributes: 94 | info (`OptimFROGInfo`) 95 | tags (`mutagen.apev2.APEv2`) 96 | """ 97 | 98 | _Info = OptimFROGInfo 99 | 100 | @staticmethod 101 | def score(filename, fileobj, header): 102 | filename = filename.lower() 103 | 104 | return (header.startswith(b"OFR") + endswith(filename, b".ofr") + 105 | endswith(filename, b".ofs")) 106 | 107 | Open = OptimFROG 108 | -------------------------------------------------------------------------------- /mutagen/monkeysaudio.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2006 Lukas Lalinsky 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | 8 | """Monkey's Audio streams with APEv2 tags. 9 | 10 | Monkey's Audio is a very efficient lossless audio compressor developed 11 | by Matt Ashland. 12 | 13 | For more information, see http://www.monkeysaudio.com/. 14 | """ 15 | 16 | __all__ = ["MonkeysAudio", "Open", "delete"] 17 | 18 | import struct 19 | 20 | from mutagen import StreamInfo 21 | from mutagen.apev2 import APEv2File, error, delete 22 | from mutagen._util import cdata, convert_error, endswith 23 | 24 | 25 | class MonkeysAudioHeaderError(error): 26 | pass 27 | 28 | 29 | class MonkeysAudioInfo(StreamInfo): 30 | """MonkeysAudioInfo() 31 | 32 | Monkey's Audio stream information. 33 | 34 | Attributes: 35 | channels (`int`): number of audio channels 36 | length (`float`): file length in seconds, as a float 37 | sample_rate (`int`): audio sampling rate in Hz 38 | bits_per_sample (`int`): bits per sample 39 | version (`float`): Monkey's Audio stream version, as a float (eg: 3.99) 40 | """ 41 | 42 | @convert_error(IOError, MonkeysAudioHeaderError) 43 | def __init__(self, fileobj): 44 | """Raises MonkeysAudioHeaderError""" 45 | 46 | header = fileobj.read(76) 47 | if len(header) != 76 or not header.startswith(b"MAC "): 48 | raise MonkeysAudioHeaderError("not a Monkey's Audio file") 49 | self.version = cdata.ushort_le(header[4:6]) 50 | if self.version >= 3980: 51 | (blocks_per_frame, final_frame_blocks, total_frames, 52 | self.bits_per_sample, self.channels, 53 | self.sample_rate) = struct.unpack("= 3950: 61 | blocks_per_frame = 73728 * 4 62 | elif self.version >= 3900 or (self.version >= 3800 and 63 | compression_level == 4): 64 | blocks_per_frame = 73728 65 | else: 66 | blocks_per_frame = 9216 67 | self.bits_per_sample = 0 68 | if header[48:].startswith(b"WAVEfmt"): 69 | self.bits_per_sample = struct.unpack(" 0): 73 | total_blocks = ((total_frames - 1) * blocks_per_frame + 74 | final_frame_blocks) 75 | self.length = float(total_blocks) / self.sample_rate 76 | 77 | def pprint(self): 78 | return u"Monkey's Audio %.2f, %.2f seconds, %d Hz" % ( 79 | self.version, self.length, self.sample_rate) 80 | 81 | 82 | class MonkeysAudio(APEv2File): 83 | """MonkeysAudio(filething) 84 | 85 | Arguments: 86 | filething (filething) 87 | 88 | Attributes: 89 | info (`MonkeysAudioInfo`) 90 | """ 91 | 92 | _Info = MonkeysAudioInfo 93 | _mimes = ["audio/ape", "audio/x-ape"] 94 | 95 | @staticmethod 96 | def score(filename, fileobj, header): 97 | return header.startswith(b"MAC ") + endswith(filename.lower(), ".ape") 98 | 99 | 100 | Open = MonkeysAudio 101 | -------------------------------------------------------------------------------- /mutagen/_tools/mutagen_pony.py: -------------------------------------------------------------------------------- 1 | # Copyright 2005 Joe Wreschnig, Michael Urman 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | 8 | import os 9 | import sys 10 | import traceback 11 | 12 | from ._util import SignalHandler 13 | 14 | 15 | class Report(object): 16 | def __init__(self, pathname): 17 | self.name = pathname 18 | self.files = 0 19 | self.unsync = 0 20 | self.missings = 0 21 | self.errors = [] 22 | self.exceptions = {} 23 | self.versions = {} 24 | 25 | def missing(self, filename): 26 | self.missings += 1 27 | self.files += 1 28 | 29 | def error(self, filename): 30 | Ex, value, trace = sys.exc_info() 31 | self.exceptions.setdefault(Ex, 0) 32 | self.exceptions[Ex] += 1 33 | self.errors.append((filename, Ex, value, trace)) 34 | self.files += 1 35 | 36 | def success(self, id3): 37 | self.versions.setdefault(id3.version, 0) 38 | self.versions[id3.version] += 1 39 | self.files += 1 40 | if id3.f_unsynch: 41 | self.unsync += 1 42 | 43 | def __str__(self): 44 | strings = ["-- Report for %s --" % self.name] 45 | if self.files == 0: 46 | return strings[0] + "\n" + "No MP3 files found.\n" 47 | 48 | good = self.files - len(self.errors) 49 | strings.append("Loaded %d/%d files (%d%%)" % ( 50 | good, self.files, (float(good) / self.files) * 100)) 51 | strings.append("%d files with unsynchronized frames." % self.unsync) 52 | strings.append("%d files without tags." % self.missings) 53 | 54 | strings.append("\nID3 Versions:") 55 | items = list(self.versions.items()) 56 | items.sort() 57 | for v, i in items: 58 | strings.append(" %s\t%d" % (".".join(map(str, v)), i)) 59 | 60 | if self.exceptions: 61 | strings.append("\nExceptions:") 62 | items = list(self.exceptions.items()) 63 | items.sort() 64 | for Ex, i in items: 65 | strings.append(" %-20s\t%d" % (Ex.__name__, i)) 66 | 67 | if self.errors: 68 | strings.append("\nERRORS:\n") 69 | for filename, Ex, value, trace in self.errors: 70 | strings.append("\nReading %s:" % filename) 71 | strings.append( 72 | "".join(traceback.format_exception(Ex, value, trace)[1:])) 73 | else: 74 | strings.append("\nNo errors!") 75 | 76 | return "\n".join(strings) 77 | 78 | 79 | def check_dir(path): 80 | from mutagen.mp3 import MP3 81 | 82 | rep = Report(path) 83 | print(u"Scanning", path) 84 | for path, dirs, files in os.walk(path): 85 | files.sort() 86 | for fn in files: 87 | if not fn.lower().endswith('.mp3'): 88 | continue 89 | ffn = os.path.join(path, fn) 90 | try: 91 | mp3 = MP3(ffn) 92 | except Exception: 93 | rep.error(ffn) 94 | else: 95 | if mp3.tags is None: 96 | rep.missing(ffn) 97 | else: 98 | rep.success(mp3.tags) 99 | 100 | print(str(rep)) 101 | 102 | 103 | def main(argv): 104 | if len(argv) == 1: 105 | print(u"Usage:", argv[0], u"directory ...") 106 | else: 107 | for path in argv[1:]: 108 | check_dir(path) 109 | 110 | 111 | def entry_point(): 112 | SignalHandler().init() 113 | return main(sys.argv) 114 | -------------------------------------------------------------------------------- /tests/test_oggflac.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | from io import BytesIO 4 | 5 | from mutagen.oggflac import OggFLAC, OggFLACStreamInfo, delete, error 6 | from mutagen.ogg import OggPage, error as OggError 7 | 8 | from tests import TestCase, DATA_DIR, get_temp_copy 9 | from tests.test_ogg import TOggFileTypeMixin 10 | from tests.test_flac import have_flac, call_flac 11 | 12 | 13 | class TOggFLAC(TestCase, TOggFileTypeMixin): 14 | Kind = OggFLAC 15 | PADDING_SUPPORT = False 16 | 17 | def setUp(self): 18 | self.filename = get_temp_copy(os.path.join(DATA_DIR, "empty.oggflac")) 19 | self.audio = OggFLAC(self.filename) 20 | 21 | def tearDown(self): 22 | os.unlink(self.filename) 23 | 24 | def test_vendor(self): 25 | self.failUnless( 26 | self.audio.tags.vendor.startswith("reference libFLAC")) 27 | self.failUnlessRaises(KeyError, self.audio.tags.__getitem__, "vendor") 28 | 29 | def test_streaminfo_bad_marker(self): 30 | with open(self.filename, "rb") as h: 31 | page = OggPage(h).write() 32 | page = page.replace(b"fLaC", b"!fLa", 1) 33 | self.failUnlessRaises(error, OggFLACStreamInfo, BytesIO(page)) 34 | 35 | def test_streaminfo_too_short(self): 36 | with open(self.filename, "rb") as h: 37 | page = OggPage(h).write() 38 | self.failUnlessRaises(OggError, OggFLACStreamInfo, BytesIO(page[:10])) 39 | 40 | def test_streaminfo_bad_version(self): 41 | with open(self.filename, "rb") as h: 42 | page = OggPage(h).write() 43 | page = page.replace(b"\x01\x00", b"\x02\x00", 1) 44 | self.failUnlessRaises(error, OggFLACStreamInfo, BytesIO(page)) 45 | 46 | def test_flac_reference_simple_save(self): 47 | if not have_flac: 48 | return 49 | self.audio.save() 50 | self.scan_file() 51 | self.assertEqual(call_flac("--ogg", "-t", self.filename), 0) 52 | 53 | def test_flac_reference_really_big(self): 54 | if not have_flac: 55 | return 56 | self.test_really_big() 57 | self.audio.save() 58 | self.scan_file() 59 | self.assertEqual(call_flac("--ogg", "-t", self.filename), 0) 60 | 61 | def test_module_delete(self): 62 | delete(self.filename) 63 | self.scan_file() 64 | self.failIf(OggFLAC(self.filename).tags) 65 | 66 | def test_flac_reference_delete(self): 67 | if not have_flac: 68 | return 69 | self.audio.delete() 70 | self.scan_file() 71 | self.assertEqual(call_flac("--ogg", "-t", self.filename), 0) 72 | 73 | def test_flac_reference_medium_sized(self): 74 | if not have_flac: 75 | return 76 | self.audio["foobar"] = "foobar" * 1000 77 | self.audio.save() 78 | self.scan_file() 79 | self.assertEqual(call_flac("--ogg", "-t", self.filename), 0) 80 | 81 | def test_flac_reference_delete_readd(self): 82 | if not have_flac: 83 | return 84 | self.audio.delete() 85 | self.audio.tags.clear() 86 | self.audio["foobar"] = "foobar" * 1000 87 | self.audio.save() 88 | self.scan_file() 89 | self.assertEqual(call_flac("--ogg", "-t", self.filename), 0) 90 | 91 | def test_not_my_ogg(self): 92 | fn = os.path.join(DATA_DIR, 'empty.ogg') 93 | self.failUnlessRaises(error, type(self.audio), fn) 94 | self.failUnlessRaises(error, self.audio.save, fn) 95 | self.failUnlessRaises(error, self.audio.delete, fn) 96 | 97 | def test_mime(self): 98 | self.failUnless("audio/x-oggflac" in self.audio.mime) 99 | 100 | def test_info_pprint(self): 101 | self.assertTrue(self.audio.info.pprint().startswith(u"Ogg FLAC")) 102 | -------------------------------------------------------------------------------- /tests/data/without-id3.dsf: -------------------------------------------------------------------------------- 1 | DSD \fmt 4+data  -------------------------------------------------------------------------------- /tests/data/2822400-1ch-0s-silence.dsf: -------------------------------------------------------------------------------- 1 | DSD \fmt 4+data  -------------------------------------------------------------------------------- /fuzzing/fuzztools.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | 3 | from mutagen import File, Metadata 4 | from mutagen import MutagenError 5 | from mutagen.asf import ASF 6 | from mutagen.apev2 import APEv2File, APEv2 7 | from mutagen.flac import FLAC 8 | from mutagen.easyid3 import EasyID3FileType, EasyID3 9 | from mutagen.id3 import ID3FileType, ID3 10 | from mutagen.mp3 import MP3 11 | from mutagen.mp3 import EasyMP3 12 | from mutagen.oggflac import OggFLAC 13 | from mutagen.oggspeex import OggSpeex 14 | from mutagen.oggtheora import OggTheora 15 | from mutagen.oggvorbis import OggVorbis 16 | from mutagen.oggopus import OggOpus 17 | from mutagen.trueaudio import EasyTrueAudio 18 | from mutagen.trueaudio import TrueAudio 19 | from mutagen.wavpack import WavPack 20 | from mutagen.easymp4 import EasyMP4 21 | from mutagen.mp4 import MP4 22 | from mutagen.musepack import Musepack 23 | from mutagen.monkeysaudio import MonkeysAudio 24 | from mutagen.optimfrog import OptimFROG 25 | from mutagen.aiff import AIFF 26 | from mutagen.aac import AAC 27 | from mutagen.ac3 import AC3 28 | from mutagen.smf import SMF 29 | from mutagen.tak import TAK 30 | from mutagen.dsf import DSF 31 | from mutagen.wave import WAVE 32 | from mutagen.dsdiff import DSDIFF 33 | 34 | 35 | OPENERS = [ 36 | MP3, TrueAudio, OggTheora, OggSpeex, OggVorbis, OggFLAC, 37 | FLAC, AIFF, APEv2File, MP4, ID3FileType, WavPack, 38 | Musepack, MonkeysAudio, OptimFROG, ASF, OggOpus, AC3, 39 | TAK, DSF, EasyMP3, EasyID3FileType, EasyTrueAudio, EasyMP4, 40 | File, SMF, AAC, EasyID3, ID3, APEv2, WAVE, DSDIFF] 41 | 42 | # If you only want to test one: 43 | # OPENERS = [AAC] 44 | 45 | 46 | def run(opener, f): 47 | try: 48 | res = opener(f) 49 | except MutagenError: 50 | return 51 | 52 | # File is special and returns None if loading fails 53 | if opener is File and res is None: 54 | return 55 | 56 | # These can still fail because we might need to parse more data 57 | # to rewrite the file 58 | 59 | f.seek(0) 60 | try: 61 | res.save(f) 62 | except MutagenError: 63 | pass 64 | 65 | f.seek(0) 66 | res = opener(f) 67 | 68 | f.seek(0) 69 | try: 70 | res.delete(f) 71 | except MutagenError: 72 | pass 73 | 74 | # These can also save to empty files 75 | if isinstance(res, Metadata): 76 | f = BytesIO() 77 | res.save(f) 78 | f.seek(0) 79 | opener(f) 80 | f.seek(0) 81 | res.delete(f) 82 | 83 | 84 | def run_all(data): 85 | f = BytesIO(data) 86 | [run(opener, f) for opener in OPENERS] 87 | 88 | 89 | def group_crashes(result_path): 90 | """Re-checks all errors, and groups them by stack trace 91 | and error type. 92 | """ 93 | 94 | crash_paths = [] 95 | pattern = os.path.join(result_path, '**', 'crashes', '*') 96 | for path in glob.glob(pattern): 97 | if os.path.splitext(path)[-1] == ".txt": 98 | continue 99 | crash_paths.append(path) 100 | 101 | if not crash_paths: 102 | print("No crashes found") 103 | return 104 | 105 | def norm_exc(): 106 | lines = traceback.format_exc().splitlines() 107 | if ":" in lines[-1]: 108 | lines[-1], message = lines[-1].split(":", 1) 109 | else: 110 | message = "" 111 | return "\n".join(lines), message.strip() 112 | 113 | traces = {} 114 | messages = {} 115 | for path in crash_paths: 116 | with open(path, "rb") as h: 117 | data = h.read() 118 | try: 119 | run_all(data) 120 | except Exception: 121 | trace, message = norm_exc() 122 | messages.setdefault(trace, set()).add(message) 123 | traces.setdefault(trace, []).append(path) 124 | 125 | for trace, paths in traces.items(): 126 | print('-' * 80) 127 | print("\n".join(paths)) 128 | print() 129 | print(textwrap.indent(trace, ' ')) 130 | print(messages[trace]) 131 | 132 | print("%d crashes with %d traces" % (len(crash_paths), len(traces))) 133 | 134 | 135 | if __name__ == '__main__': 136 | import sys 137 | import glob 138 | import os 139 | import traceback 140 | import textwrap 141 | group_crashes(sys.argv[1]) 142 | -------------------------------------------------------------------------------- /tests/test_dsf.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | 4 | from mutagen.dsf import DSF, DSFFile, delete 5 | from mutagen.dsf import error as DSFError 6 | 7 | from tests import TestCase, DATA_DIR, get_temp_copy 8 | 9 | 10 | class TDSF(TestCase): 11 | silence_1 = os.path.join(DATA_DIR, '2822400-1ch-0s-silence.dsf') 12 | silence_2 = os.path.join(DATA_DIR, '5644800-2ch-s01-silence.dsf') 13 | 14 | has_tags = os.path.join(DATA_DIR, 'with-id3.dsf') 15 | no_tags = os.path.join(DATA_DIR, 'without-id3.dsf') 16 | 17 | def setUp(self): 18 | self.filename_1 = get_temp_copy(self.has_tags) 19 | self.filename_2 = get_temp_copy(self.no_tags) 20 | 21 | self.dsf_tmp_id3 = DSF(self.filename_1) 22 | self.dsf_tmp_no_id3 = DSF(self.filename_2) 23 | 24 | self.dsf_1 = DSF(self.silence_1) 25 | self.dsf_2 = DSF(self.silence_2) 26 | 27 | def test_channels(self): 28 | self.failUnlessEqual(self.dsf_1.info.channels, 1) 29 | self.failUnlessEqual(self.dsf_2.info.channels, 2) 30 | 31 | def test_length(self): 32 | self.failUnlessEqual(self.dsf_1.info.length, 0) 33 | self.failUnlessEqual(self.dsf_2.info.length, 0.01) 34 | 35 | def test_sampling_frequency(self): 36 | self.failUnlessEqual(self.dsf_1.info.sample_rate, 2822400) 37 | self.failUnlessEqual(self.dsf_2.info.sample_rate, 5644800) 38 | 39 | def test_bits_per_sample(self): 40 | self.failUnlessEqual(self.dsf_1.info.bits_per_sample, 1) 41 | 42 | def test_notdsf(self): 43 | self.failUnlessRaises( 44 | DSFError, DSF, os.path.join(DATA_DIR, 'empty.ofr')) 45 | 46 | def test_pprint(self): 47 | self.failUnless(self.dsf_tmp_id3.pprint()) 48 | 49 | def test_delete(self): 50 | self.dsf_tmp_id3.delete() 51 | self.failIf(self.dsf_tmp_id3.tags) 52 | self.failUnless(DSF(self.filename_1).tags is None) 53 | 54 | def test_module_delete(self): 55 | delete(self.filename_1) 56 | self.failUnless(DSF(self.filename_1).tags is None) 57 | 58 | def test_module_double_delete(self): 59 | delete(self.filename_1) 60 | delete(self.filename_1) 61 | 62 | def test_pprint_no_tags(self): 63 | self.dsf_tmp_id3.tags = None 64 | self.failUnless(self.dsf_tmp_id3.pprint()) 65 | 66 | def test_save_no_tags(self): 67 | self.dsf_tmp_id3.tags = None 68 | self.dsf_tmp_id3.save() 69 | self.assertTrue(self.dsf_tmp_id3.tags is None) 70 | 71 | def test_add_tags_already_there(self): 72 | self.failUnless(self.dsf_tmp_id3.tags) 73 | self.failUnlessRaises(Exception, self.dsf_tmp_id3.add_tags) 74 | 75 | def test_mime(self): 76 | self.failUnless("audio/dsf" in self.dsf_tmp_id3.mime) 77 | 78 | def test_loaded_tags(self): 79 | self.failUnless(self.dsf_tmp_id3["TIT2"] == "DSF title") 80 | 81 | def test_roundtrip(self): 82 | self.failUnlessEqual(self.dsf_tmp_id3["TIT2"], ["DSF title"]) 83 | self.dsf_tmp_id3.save() 84 | new = DSF(self.dsf_tmp_id3.filename) 85 | self.failUnlessEqual(new["TIT2"], ["DSF title"]) 86 | 87 | def test_save_tags(self): 88 | from mutagen.id3 import TIT2 89 | tags = self.dsf_tmp_id3.tags 90 | tags.add(TIT2(encoding=3, text="foobar")) 91 | tags.save() 92 | 93 | new = DSF(self.dsf_tmp_id3.filename) 94 | self.failUnlessEqual(new["TIT2"], ["foobar"]) 95 | 96 | def test_corrupt_tag(self): 97 | with open(self.filename_1, "r+b") as h: 98 | chunk = DSFFile(h).dsd_chunk 99 | h.seek(chunk.offset_metdata_chunk) 100 | h.seek(4, 1) 101 | h.write(b"\xff\xff") 102 | self.assertRaises(DSFError, DSF, self.filename_1) 103 | 104 | def test_padding(self): 105 | DSF(self.filename_1).save() 106 | self.assertEqual(DSF(self.filename_1).tags._padding, 1024) 107 | DSF(self.filename_1).save() 108 | self.assertEqual(DSF(self.filename_1).tags._padding, 1024) 109 | 110 | tags = DSF(self.filename_1) 111 | tags.save(padding=lambda x: 1) 112 | self.assertEqual(DSF(self.filename_1).tags._padding, 1) 113 | 114 | tags = DSF(self.filename_1) 115 | tags.save(padding=lambda x: 100) 116 | self.assertEqual(DSF(self.filename_1).tags._padding, 100) 117 | 118 | tags = DSF(self.filename_1) 119 | self.assertRaises(DSFError, tags.save, padding=lambda x: -1) 120 | 121 | def tearDown(self): 122 | os.unlink(self.filename_1) 123 | os.unlink(self.filename_2) 124 | -------------------------------------------------------------------------------- /mutagen/_constants.py: -------------------------------------------------------------------------------- 1 | # 2 | # This program is free software; you can redistribute it and/or modify 3 | # it under the terms of the GNU General Public License as published by 4 | # the Free Software Foundation; either version 2 of the License, or 5 | # (at your option) any later version. 6 | 7 | """Constants used by Mutagen.""" 8 | 9 | GENRES = [ 10 | u"Blues", 11 | u"Classic Rock", 12 | u"Country", 13 | u"Dance", 14 | u"Disco", 15 | u"Funk", 16 | u"Grunge", 17 | u"Hip-Hop", 18 | u"Jazz", 19 | u"Metal", 20 | u"New Age", 21 | u"Oldies", 22 | u"Other", 23 | u"Pop", 24 | u"R&B", 25 | u"Rap", 26 | u"Reggae", 27 | u"Rock", 28 | u"Techno", 29 | u"Industrial", 30 | u"Alternative", 31 | u"Ska", 32 | u"Death Metal", 33 | u"Pranks", 34 | u"Soundtrack", 35 | u"Euro-Techno", 36 | u"Ambient", 37 | u"Trip-Hop", 38 | u"Vocal", 39 | u"Jazz+Funk", 40 | u"Fusion", 41 | u"Trance", 42 | u"Classical", 43 | u"Instrumental", 44 | u"Acid", 45 | u"House", 46 | u"Game", 47 | u"Sound Clip", 48 | u"Gospel", 49 | u"Noise", 50 | u"Alt. Rock", 51 | u"Bass", 52 | u"Soul", 53 | u"Punk", 54 | u"Space", 55 | u"Meditative", 56 | u"Instrumental Pop", 57 | u"Instrumental Rock", 58 | u"Ethnic", 59 | u"Gothic", 60 | u"Darkwave", 61 | u"Techno-Industrial", 62 | u"Electronic", 63 | u"Pop-Folk", 64 | u"Eurodance", 65 | u"Dream", 66 | u"Southern Rock", 67 | u"Comedy", 68 | u"Cult", 69 | u"Gangsta Rap", 70 | u"Top 40", 71 | u"Christian Rap", 72 | u"Pop/Funk", 73 | u"Jungle", 74 | u"Native American", 75 | u"Cabaret", 76 | u"New Wave", 77 | u"Psychedelic", 78 | u"Rave", 79 | u"Showtunes", 80 | u"Trailer", 81 | u"Lo-Fi", 82 | u"Tribal", 83 | u"Acid Punk", 84 | u"Acid Jazz", 85 | u"Polka", 86 | u"Retro", 87 | u"Musical", 88 | u"Rock & Roll", 89 | u"Hard Rock", 90 | u"Folk", 91 | u"Folk-Rock", 92 | u"National Folk", 93 | u"Swing", 94 | u"Fast-Fusion", 95 | u"Bebop", 96 | u"Latin", 97 | u"Revival", 98 | u"Celtic", 99 | u"Bluegrass", 100 | u"Avantgarde", 101 | u"Gothic Rock", 102 | u"Progressive Rock", 103 | u"Psychedelic Rock", 104 | u"Symphonic Rock", 105 | u"Slow Rock", 106 | u"Big Band", 107 | u"Chorus", 108 | u"Easy Listening", 109 | u"Acoustic", 110 | u"Humour", 111 | u"Speech", 112 | u"Chanson", 113 | u"Opera", 114 | u"Chamber Music", 115 | u"Sonata", 116 | u"Symphony", 117 | u"Booty Bass", 118 | u"Primus", 119 | u"Porn Groove", 120 | u"Satire", 121 | u"Slow Jam", 122 | u"Club", 123 | u"Tango", 124 | u"Samba", 125 | u"Folklore", 126 | u"Ballad", 127 | u"Power Ballad", 128 | u"Rhythmic Soul", 129 | u"Freestyle", 130 | u"Duet", 131 | u"Punk Rock", 132 | u"Drum Solo", 133 | u"A Cappella", 134 | u"Euro-House", 135 | u"Dance Hall", 136 | u"Goa", 137 | u"Drum & Bass", 138 | u"Club-House", 139 | u"Hardcore", 140 | u"Terror", 141 | u"Indie", 142 | u"BritPop", 143 | u"Afro-Punk", 144 | u"Polsk Punk", 145 | u"Beat", 146 | u"Christian Gangsta Rap", 147 | u"Heavy Metal", 148 | u"Black Metal", 149 | u"Crossover", 150 | u"Contemporary Christian", 151 | u"Christian Rock", 152 | u"Merengue", 153 | u"Salsa", 154 | u"Thrash Metal", 155 | u"Anime", 156 | u"JPop", 157 | u"Synthpop", 158 | u"Abstract", 159 | u"Art Rock", 160 | u"Baroque", 161 | u"Bhangra", 162 | u"Big Beat", 163 | u"Breakbeat", 164 | u"Chillout", 165 | u"Downtempo", 166 | u"Dub", 167 | u"EBM", 168 | u"Eclectic", 169 | u"Electro", 170 | u"Electroclash", 171 | u"Emo", 172 | u"Experimental", 173 | u"Garage", 174 | u"Global", 175 | u"IDM", 176 | u"Illbient", 177 | u"Industro-Goth", 178 | u"Jam Band", 179 | u"Krautrock", 180 | u"Leftfield", 181 | u"Lounge", 182 | u"Math Rock", 183 | u"New Romantic", 184 | u"Nu-Breakz", 185 | u"Post-Punk", 186 | u"Post-Rock", 187 | u"Psytrance", 188 | u"Shoegaze", 189 | u"Space Rock", 190 | u"Trop Rock", 191 | u"World Music", 192 | u"Neoclassical", 193 | u"Audiobook", 194 | u"Audio Theatre", 195 | u"Neue Deutsche Welle", 196 | u"Podcast", 197 | u"Indie Rock", 198 | u"G-Funk", 199 | u"Dubstep", 200 | u"Garage Rock", 201 | u"Psybient", 202 | ] 203 | """The ID3v1 genre list.""" 204 | -------------------------------------------------------------------------------- /mutagen/_tools/mid3cp.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Marcus Sundman 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | 8 | """A program replicating the functionality of id3lib's id3cp, using mutagen for 9 | tag loading and saving. 10 | """ 11 | 12 | import sys 13 | import os.path 14 | 15 | import mutagen 16 | import mutagen.id3 17 | 18 | from ._util import SignalHandler, OptionParser 19 | 20 | 21 | VERSION = (0, 1) 22 | _sig = SignalHandler() 23 | 24 | 25 | class ID3OptionParser(OptionParser): 26 | def __init__(self): 27 | mutagen_version = mutagen.version_string 28 | my_version = ".".join(map(str, VERSION)) 29 | version = "mid3cp %s\nUses Mutagen %s" % (my_version, mutagen_version) 30 | self.disable_interspersed_args() 31 | OptionParser.__init__( 32 | self, version=version, 33 | usage="%prog [option(s)] ", 34 | description=("Copies ID3 tags from to . Mutagen-based " 35 | "replacement for id3lib's id3cp.")) 36 | 37 | 38 | def copy(src, dst, merge, write_v1=True, excluded_tags=None, verbose=False): 39 | """Returns 0 on success""" 40 | 41 | if excluded_tags is None: 42 | excluded_tags = [] 43 | 44 | try: 45 | id3 = mutagen.id3.ID3(src, translate=False) 46 | except mutagen.id3.ID3NoHeaderError: 47 | print(u"No ID3 header found in ", src, file=sys.stderr) 48 | return 1 49 | except Exception as err: 50 | print(str(err), file=sys.stderr) 51 | return 1 52 | 53 | if verbose: 54 | print(u"File", src, u"contains:", file=sys.stderr) 55 | print(id3.pprint(), file=sys.stderr) 56 | 57 | for tag in excluded_tags: 58 | id3.delall(tag) 59 | 60 | if merge: 61 | try: 62 | target = mutagen.id3.ID3(dst, translate=False) 63 | except mutagen.id3.ID3NoHeaderError: 64 | # no need to merge 65 | pass 66 | except Exception as err: 67 | print(str(err), file=sys.stderr) 68 | return 1 69 | else: 70 | for frame in id3.values(): 71 | target.add(frame) 72 | 73 | id3 = target 74 | 75 | # if the source is 2.3 save it as 2.3 76 | if id3.version < (2, 4, 0): 77 | id3.update_to_v23() 78 | v2_version = 3 79 | else: 80 | id3.update_to_v24() 81 | v2_version = 4 82 | 83 | try: 84 | id3.save(dst, v1=(2 if write_v1 else 0), v2_version=v2_version) 85 | except Exception as err: 86 | print(u"Error saving", dst, u":\n%s" % str(err), 87 | file=sys.stderr) 88 | return 1 89 | else: 90 | if verbose: 91 | print(u"Successfully saved", dst, file=sys.stderr) 92 | return 0 93 | 94 | 95 | def main(argv): 96 | parser = ID3OptionParser() 97 | parser.add_option("-v", "--verbose", action="store_true", dest="verbose", 98 | help="print out saved tags", default=False) 99 | parser.add_option("--write-v1", action="store_true", dest="write_v1", 100 | default=False, help="write id3v1 tags") 101 | parser.add_option("-x", "--exclude-tag", metavar="TAG", action="append", 102 | dest="x", help="exclude the specified tag", default=[]) 103 | parser.add_option("--merge", action="store_true", 104 | help="Copy over frames instead of the whole ID3 tag", 105 | default=False) 106 | (options, args) = parser.parse_args(argv[1:]) 107 | 108 | if len(args) != 2: 109 | parser.print_help(file=sys.stderr) 110 | return 1 111 | 112 | (src, dst) = args 113 | 114 | if not os.path.isfile(src): 115 | print(u"File not found:", src, file=sys.stderr) 116 | parser.print_help(file=sys.stderr) 117 | return 1 118 | 119 | if not os.path.isfile(dst): 120 | print(u"File not found:", dst, file=sys.stderr) 121 | parser.print_help(file=sys.stderr) 122 | return 1 123 | 124 | # Strip tags - "-x FOO" adds whitespace at the beginning of the tag name 125 | excluded_tags = [x.strip() for x in options.x] 126 | 127 | with _sig.block(): 128 | return copy(src, dst, options.merge, options.write_v1, excluded_tags, 129 | options.verbose) 130 | 131 | 132 | def entry_point(): 133 | _sig.init() 134 | return main(sys.argv) 135 | -------------------------------------------------------------------------------- /mutagen/_tags.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2005 Michael Urman 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; either version 2 of the License, or 6 | # (at your option) any later version. 7 | 8 | from ._util import loadfile 9 | 10 | 11 | class PaddingInfo(object): 12 | """PaddingInfo() 13 | 14 | Abstract padding information object. 15 | 16 | This will be passed to the callback function that can be used 17 | for saving tags. 18 | 19 | :: 20 | 21 | def my_callback(info: PaddingInfo): 22 | return info.get_default_padding() 23 | 24 | The callback should return the amount of padding to use (>= 0) based on 25 | the content size and the padding of the file after saving. The actual used 26 | amount of padding might vary depending on the file format (due to 27 | alignment etc.) 28 | 29 | The default implementation can be accessed using the 30 | :meth:`get_default_padding` method in the callback. 31 | 32 | Attributes: 33 | padding (`int`): The amount of padding left after saving in bytes 34 | (can be negative if more data needs to be added as padding is 35 | available) 36 | size (`int`): The amount of data following the padding 37 | """ 38 | 39 | def __init__(self, padding: int, size: int): 40 | self.padding = padding 41 | self.size = size 42 | 43 | def get_default_padding(self) -> int: 44 | """The default implementation which tries to select a reasonable 45 | amount of padding and which might change in future versions. 46 | 47 | Returns: 48 | int: Amount of padding after saving 49 | """ 50 | 51 | high = 1024 * 10 + self.size // 100 # 10 KiB + 1% of trailing data 52 | low = 1024 + self.size // 1000 # 1 KiB + 0.1% of trailing data 53 | 54 | if self.padding >= 0: 55 | # enough padding left 56 | if self.padding > high: 57 | # padding too large, reduce 58 | return low 59 | # just use existing padding as is 60 | return self.padding 61 | else: 62 | # not enough padding, add some 63 | return low 64 | 65 | def _get_padding(self, user_func): 66 | if user_func is None: 67 | return self.get_default_padding() 68 | else: 69 | return user_func(self) 70 | 71 | def __repr__(self): 72 | return "<%s size=%d padding=%d>" % ( 73 | type(self).__name__, self.size, self.padding) 74 | 75 | 76 | class Tags(object): 77 | """`Tags` is the base class for many of the tag objects in Mutagen. 78 | 79 | In many cases it has a dict like interface. 80 | """ 81 | 82 | __module__ = "mutagen" 83 | 84 | def pprint(self): 85 | """ 86 | Returns: 87 | text: tag information 88 | """ 89 | 90 | raise NotImplementedError 91 | 92 | 93 | class Metadata(Tags): 94 | """Metadata(filething=None, **kwargs) 95 | 96 | Args: 97 | filething (filething): a filename or a file-like object or `None` 98 | to create an empty instance (like ``ID3()``) 99 | 100 | Like :class:`Tags` but for standalone tagging formats that are not 101 | solely managed by a container format. 102 | 103 | Provides methods to load, save and delete tags. 104 | """ 105 | 106 | __module__ = "mutagen" 107 | 108 | def __init__(self, *args, **kwargs): 109 | if args or kwargs: 110 | self.load(*args, **kwargs) 111 | 112 | @loadfile() 113 | def load(self, filething, **kwargs): 114 | raise NotImplementedError 115 | 116 | @loadfile(writable=False) 117 | def save(self, filething=None, **kwargs): 118 | """save(filething=None, **kwargs) 119 | 120 | Save changes to a file. 121 | 122 | Args: 123 | filething (filething): or `None` 124 | Raises: 125 | MutagenError: if saving wasn't possible 126 | """ 127 | 128 | raise NotImplementedError 129 | 130 | @loadfile(writable=False) 131 | def delete(self, filething=None): 132 | """delete(filething=None) 133 | 134 | Remove tags from a file. 135 | 136 | In most cases this means any traces of the tag will be removed 137 | from the file. 138 | 139 | Args: 140 | filething (filething): or `None` 141 | Raises: 142 | MutagenError: if deleting wasn't possible 143 | """ 144 | 145 | raise NotImplementedError 146 | -------------------------------------------------------------------------------- /docs/man/mid3v2.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | mid3v2 3 | ========= 4 | 5 | ----------------------------------- 6 | audio tag editor similar to 'id3v2' 7 | ----------------------------------- 8 | 9 | :Manual section: 1 10 | 11 | 12 | SYNOPSIS 13 | ======== 14 | 15 | **mid3v2** [*options*] *filename* ... 16 | 17 | 18 | DESCRIPTION 19 | =========== 20 | 21 | **mid3v2** is a Mutagen-based replacement for id3lib's id3v2. It supports 22 | ID3v2.4 and more frames; it also does not have the numerous bugs that plague 23 | id3v2. 24 | 25 | This program exists mostly for compatibility with programs that want to tag 26 | files using id3v2. For a more usable interface, we recommend Ex Falso. 27 | 28 | 29 | OPTIONS 30 | ======= 31 | 32 | -q, --quiet 33 | Be quiet: do not mention file operations that perform the user's 34 | request. Warnings will still be printed. 35 | 36 | -v, --verbose 37 | Be verbose: state all operations performed. This is the opposite of 38 | --quiet. This is the default. 39 | 40 | -e, --escape 41 | Enable interpretation of backslash escapes for tag values. 42 | Makes it possible to escape the colon-separator in TXXX, WXXX, COMM 43 | values like '\\:' and insert escape sequences like '\\n', '\\t' etc. 44 | 45 | -f, --list-frames 46 | Display all supported ID3v2.3/2.4 frames and their meanings. 47 | 48 | -L, --list-genres 49 | List all ID3v1 numeric genres. These can be used to set TCON frames, 50 | but it is not recommended. 51 | 52 | -l, --list 53 | List all tags in the files. The output format is *not* the same as 54 | id3v2's; instead, it is easily parsable and readable. Some tags may not 55 | have human-readable representations. 56 | 57 | --list-raw 58 | List all tags in the files, in raw format. Although this format is 59 | nominally human-readable, it may be very long if the tag contains 60 | embedded binary data. 61 | 62 | -d, --delete-v2 63 | Delete ID3v2 tags. 64 | 65 | -s, --delete-v1 66 | Delete ID3v1 tags. 67 | 68 | -D, --delete-all 69 | Delete all ID3 tags. 70 | 71 | --delete-frames=FRAMES 72 | Delete specific ID3v2 frames (or groups of frames) from the files. 73 | ``FRAMES`` is a "," separated list of frame names e.g. ``"TPE1,TALB"`` 74 | 75 | -C, --convert 76 | Convert ID3v1 tags to ID3v2 tags. This will also happen automatically 77 | during any editing. 78 | 79 | -a, --artist=ARTIST 80 | Set the artist information (TPE1). 81 | 82 | -A, --album=ALBUM 83 | Set the album information (TALB). 84 | 85 | -t, --song=TITLE 86 | Set the title information (TIT2). 87 | 88 | -c, --comment= 89 | Set a comment (COMM). The language and description may be omitted, in 90 | which case the language defaults to English, and the description to an 91 | empty string. 92 | 93 | -p, --picture= 94 | Set the attached picture (APIC). Everything except the filename can be 95 | omitted in which case default values will be used. 96 | 97 | -g, --genre=GENRE 98 | Set the genre information (TCON). 99 | 100 | -y, --year=, --date= 101 | Set the year/date information (TDRC). 102 | 103 | -T, --track= 104 | Set the track number (TRCK). 105 | 106 | Any text or URL frame (those beginning with T or W) can be modified or 107 | added by prefixing the name of the frame with "--". For example, ``--TIT3 108 | "Monkey!"`` will set the TIT3 (subtitle) frame to ``Monkey!``. 109 | 110 | The TXXX frame has the format ; many TXXX frames may be 111 | set in the file as long as they have different keys. To set this key, just 112 | separate the text with a colon, e.g. ``--TXXX "ALBUMARTISTSORT:Examples, 113 | The"``. The description can be omitted in which case it defaults to an empty 114 | string. 115 | 116 | The WXXX frame has the same format as TXXX but since URLs usually contain a 117 | ":" you have provide a description or enable escaping (-e): 118 | ``--WXXX "desc:http://foo.bar"`` or ``-e --WXXX "http\\://foo.bar"`` 119 | 120 | The USLT frame has the format . The language and 121 | description may be omitted, in which case the language defaults to English, 122 | and the description to an empty string. 123 | 124 | The special POPM frame can be set in a similar way: ``--POPM 125 | "bob@example.com:128:2"`` to set Bob's rating to 128/255 with 2 plays. 126 | 127 | 128 | BUGS 129 | ==== 130 | 131 | No sanity checking is done on the editing operations you perform, so mid3v2 132 | will happily accept --TSIZ when editing an ID3v2.4 frame. However, it will 133 | also automatically throw it out during the next edit operation. 134 | 135 | 136 | AUTHOR 137 | ====== 138 | 139 | Joe Wreschnig is the author of mid3v2, but he doesn't like to admit it. 140 | -------------------------------------------------------------------------------- /mutagen/id3/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2005 Michael Urman 2 | # 2006 Lukas Lalinsky 3 | # 2013 Christoph Reiter 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | 10 | """ID3v2 reading and writing. 11 | 12 | This is based off of the following references: 13 | 14 | * http://id3.org/id3v2.4.0-structure 15 | * http://id3.org/id3v2.4.0-frames 16 | * http://id3.org/id3v2.3.0 17 | * http://id3.org/id3v2-00 18 | * http://id3.org/ID3v1 19 | 20 | Its largest deviation from the above (versions 2.3 and 2.2) is that it 21 | will not interpret the / characters as a separator, and will almost 22 | always accept null separators to generate multi-valued text frames. 23 | 24 | Because ID3 frame structure differs between frame types, each frame is 25 | implemented as a different class (e.g. TIT2 as mutagen.id3.TIT2). Each 26 | frame's documentation contains a list of its attributes. 27 | 28 | Since this file's documentation is a little unwieldy, you are probably 29 | interested in the :class:`ID3` class to start with. 30 | """ 31 | 32 | from ._file import ID3, ID3FileType, delete, ID3v1SaveOptions 33 | from ._specs import Encoding, PictureType, CTOCFlags, ID3TimeStamp 34 | from ._frames import Frames, Frames_2_2, Frame, TextFrame, UrlFrame, \ 35 | UrlFrameU, TimeStampTextFrame, BinaryFrame, NumericPartTextFrame, \ 36 | NumericTextFrame, PairedTextFrame 37 | from ._util import ID3NoHeaderError, error, ID3UnsupportedVersionError 38 | from ._id3v1 import ParseID3v1, MakeID3v1 39 | from ._tags import ID3Tags 40 | from ._frames import (AENC, APIC, ASPI, BUF, CHAP, CNT, COM, COMM, COMR, CRA, 41 | CRM, CTOC, ENCR, EQU2, ETC, ETCO, GEO, GEOB, GP1, GRID, GRP1, IPL, IPLS, 42 | LINK, LNK, MCDI, MCI, MLL, MLLT, MVI, MVIN, MVN, MVNM, OWNE, PCNT, PCST, 43 | PIC, POP, POPM, POSS, PRIV, RBUF, REV, RVA, RVA2, RVAD, RVRB, SEEK, SIGN, 44 | SLT, STC, SYLT, SYTC, TAL, TALB, TBP, TBPM, TCAT, TCM, TCMP, TCO, TCOM, 45 | TCON, TCOP, TCP, TCR, TDA, TDAT, TDEN, TDES, TDLY, TDOR, TDRC, TDRL, TDTG, 46 | TDY, TEN, TENC, TEXT, TFLT, TFT, TGID, TIM, TIME, TIPL, TIT1, TIT2, TIT3, 47 | TKE, TKEY, TKWD, TLA, TLAN, TLE, TLEN, TMCL, TMED, TMOO, TMT, TOA, TOAL, 48 | TOF, TOFN, TOL, TOLY, TOPE, TOR, TORY, TOT, TOWN, TP1, TP2, TP3, TP4, TPA, 49 | TPB, TPE1, TPE2, TPE3, TPE4, TPOS, TPRO, TPUB, TRC, TRCK, TRD, TRDA, TRK, 50 | TRSN, TRSO, TS2, TSA, TSC, TSI, TSIZ, TSO2, TSOA, TSOC, TSOP, TSOT, TSP, 51 | TSRC, TSS, TSSE, TSST, TST, TT1, TT2, TT3, TXT, TXX, TXXX, TYE, TYER, UFI, 52 | UFID, ULT, USER, USLT, WAF, WAR, WAS, WCM, WCOM, WCOP, WCP, WFED, WOAF, 53 | WOAR, WOAS, WORS, WPAY, WPB, WPUB, WXX, WXXX) 54 | 55 | # deprecated 56 | from ._util import ID3EncryptionUnsupportedError, ID3JunkFrameError, \ 57 | ID3BadUnsynchData, ID3BadCompressedData, ID3TagError, ID3Warning, \ 58 | BitPaddedInt as _BitPaddedIntForPicard 59 | 60 | # support open(filename) as interface 61 | Open = ID3 62 | 63 | # flake8 64 | ID3, ID3FileType, delete, ID3v1SaveOptions, Encoding, PictureType, CTOCFlags, 65 | ID3TimeStamp, Frames, Frames_2_2, Frame, TextFrame, UrlFrame, UrlFrameU, 66 | TimeStampTextFrame, BinaryFrame, NumericPartTextFrame, NumericTextFrame, 67 | PairedTextFrame, ID3NoHeaderError, error, ID3UnsupportedVersionError, 68 | ParseID3v1, MakeID3v1, ID3Tags, ID3EncryptionUnsupportedError, 69 | ID3JunkFrameError, ID3BadUnsynchData, ID3BadCompressedData, ID3TagError, 70 | ID3Warning 71 | 72 | AENC, APIC, ASPI, BUF, CHAP, CNT, COM, COMM, COMR, CRA, CRM, CTOC, ENCR, EQU2, 73 | ETC, ETCO, GEO, GEOB, GP1, GRID, GRP1, IPL, IPLS, LINK, LNK, MCDI, MCI, MLL, 74 | MLLT, MVI, MVIN, MVN, MVNM, OWNE, PCNT, PCST, PIC, POP, POPM, POSS, PRIV, 75 | RBUF, REV, RVA, RVA2, RVAD, RVRB, SEEK, SIGN, SLT, STC, SYLT, SYTC, TAL, TALB, 76 | TBP, TBPM, TCAT, TCM, TCMP, TCO, TCOM, TCON, TCOP, TCP, TCR, TDA, TDAT, TDEN, 77 | TDES, TDLY, TDOR, TDRC, TDRL, TDTG, TDY, TEN, TENC, TEXT, TFLT, TFT, TGID, 78 | TIM, TIME, TIPL, TIT1, TIT2, TIT3, TKE, TKEY, TKWD, TLA, TLAN, TLE, TLEN, 79 | TMCL, TMED, TMOO, TMT, TOA, TOAL, TOF, TOFN, TOL, TOLY, TOPE, TOR, TORY, TOT, 80 | TOWN, TP1, TP2, TP3, TP4, TPA, TPB, TPE1, TPE2, TPE3, TPE4, TPOS, TPRO, TPUB, 81 | TRC, TRCK, TRD, TRDA, TRK, TRSN, TRSO, TS2, TSA, TSC, TSI, TSIZ, TSO2, TSOA, 82 | TSOC, TSOP, TSOT, TSP, TSRC, TSS, TSSE, TSST, TST, TT1, TT2, TT3, TXT, TXX, 83 | TXXX, TYE, TYER, UFI, UFID, ULT, USER, USLT, WAF, WAR, WAS, WCM, WCOM, WCOP, 84 | WCP, WFED, WOAF, WOAR, WOAS, WORS, WPAY, WPB, WPUB, WXX, WXXX 85 | 86 | 87 | # Workaround for http://tickets.musicbrainz.org/browse/PICARD-833 88 | class _DummySpecForPicard(object): 89 | write = None 90 | 91 | EncodedTextSpec = MultiSpec = _DummySpecForPicard 92 | BitPaddedInt = _BitPaddedIntForPicard 93 | 94 | 95 | __all__ = ['ID3', 'ID3FileType', 'Frames', 'Open', 'delete'] 96 | -------------------------------------------------------------------------------- /tests/test_musepack.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | from io import BytesIO 4 | 5 | from mutagen.id3 import ID3, TIT2 6 | from mutagen.musepack import Musepack, MusepackInfo, MusepackHeaderError 7 | from tests import TestCase, DATA_DIR, get_temp_copy 8 | 9 | 10 | class TMusepack(TestCase): 11 | 12 | def setUp(self): 13 | self.sv8 = Musepack(os.path.join(DATA_DIR, "sv8_header.mpc")) 14 | self.sv7 = Musepack(os.path.join(DATA_DIR, "click.mpc")) 15 | self.sv5 = Musepack(os.path.join(DATA_DIR, "sv5_header.mpc")) 16 | self.sv4 = Musepack(os.path.join(DATA_DIR, "sv4_header.mpc")) 17 | 18 | def test_bad_header(self): 19 | self.failUnlessRaises( 20 | MusepackHeaderError, 21 | Musepack, os.path.join(DATA_DIR, "almostempty.mpc")) 22 | 23 | def test_channels(self): 24 | self.failUnlessEqual(self.sv8.info.channels, 2) 25 | self.failUnlessEqual(self.sv7.info.channels, 2) 26 | self.failUnlessEqual(self.sv5.info.channels, 2) 27 | self.failUnlessEqual(self.sv4.info.channels, 2) 28 | 29 | def test_sample_rate(self): 30 | self.failUnlessEqual(self.sv8.info.sample_rate, 44100) 31 | self.failUnlessEqual(self.sv7.info.sample_rate, 44100) 32 | self.failUnlessEqual(self.sv5.info.sample_rate, 44100) 33 | self.failUnlessEqual(self.sv4.info.sample_rate, 44100) 34 | 35 | def test_bitrate(self): 36 | self.failUnlessEqual(self.sv8.info.bitrate, 609) 37 | self.failUnlessEqual(self.sv7.info.bitrate, 194530) 38 | self.failUnlessEqual(self.sv5.info.bitrate, 39) 39 | self.failUnlessEqual(self.sv4.info.bitrate, 39) 40 | 41 | def test_length(self): 42 | self.failUnlessAlmostEqual(self.sv8.info.length, 1.49, 1) 43 | self.failUnlessAlmostEqual(self.sv7.info.length, 0.07, 2) 44 | self.failUnlessAlmostEqual(self.sv5.info.length, 26.3, 1) 45 | self.failUnlessAlmostEqual(self.sv4.info.length, 26.3, 1) 46 | 47 | def test_gain(self): 48 | self.failUnlessAlmostEqual(self.sv8.info.title_gain, -4.668, 3) 49 | self.failUnlessAlmostEqual(self.sv8.info.title_peak, 0.5288, 3) 50 | self.failUnlessEqual( 51 | self.sv8.info.title_gain, self.sv8.info.album_gain) 52 | self.failUnlessEqual( 53 | self.sv8.info.title_peak, self.sv8.info.album_peak) 54 | self.failUnlessAlmostEqual(self.sv7.info.title_gain, 9.27, 6) 55 | self.failUnlessAlmostEqual(self.sv7.info.title_peak, 0.1149, 4) 56 | self.failUnlessEqual( 57 | self.sv7.info.title_gain, self.sv7.info.album_gain) 58 | self.failUnlessEqual( 59 | self.sv7.info.title_peak, self.sv7.info.album_peak) 60 | self.failUnlessRaises(AttributeError, getattr, self.sv5, 'title_gain') 61 | 62 | def test_not_my_file(self): 63 | self.failUnlessRaises( 64 | MusepackHeaderError, Musepack, 65 | os.path.join(DATA_DIR, "empty.ogg")) 66 | self.failUnlessRaises( 67 | MusepackHeaderError, Musepack, 68 | os.path.join(DATA_DIR, "emptyfile.mp3")) 69 | 70 | def test_almost_my_file(self): 71 | self.failUnlessRaises( 72 | MusepackHeaderError, MusepackInfo, BytesIO(b"MP+" + b"\x00" * 32)) 73 | self.failUnlessRaises( 74 | MusepackHeaderError, 75 | MusepackInfo, 76 | BytesIO(b"MP+" + b"\x00" * 100)) 77 | self.failUnlessRaises( 78 | MusepackHeaderError, 79 | MusepackInfo, 80 | BytesIO(b"MPCK" + b"\x00" * 100)) 81 | 82 | def test_pprint(self): 83 | self.sv8.pprint() 84 | self.sv7.pprint() 85 | self.sv5.pprint() 86 | self.sv4.pprint() 87 | 88 | def test_mime(self): 89 | self.failUnless("audio/x-musepack" in self.sv7.mime) 90 | 91 | def test_zero_padded_sh_packet(self): 92 | # https://github.com/quodlibet/mutagen/issues/198 93 | data = (b"MPCKSH\x10\x95 Q\xa2\x08\x81\xb8\xc9T\x00\x1e\x1b" 94 | b"\x00RG\x0c\x01A\xcdY\x06?\x80Z\x06EI") 95 | 96 | fileobj = BytesIO(data) 97 | info = MusepackInfo(fileobj) 98 | self.assertEqual(info.channels, 2) 99 | self.assertEqual(info.samples, 3024084) 100 | 101 | 102 | class TMusepackWithID3(TestCase): 103 | 104 | def setUp(self): 105 | self.filename = get_temp_copy(os.path.join(DATA_DIR, "click.mpc")) 106 | 107 | def tearDown(self): 108 | os.unlink(self.filename) 109 | 110 | def test_ignore_id3(self): 111 | id3 = ID3() 112 | id3.add(TIT2(encoding=0, text='id3 title')) 113 | id3.save(self.filename) 114 | f = Musepack(self.filename) 115 | f['title'] = 'apev2 title' 116 | f.save() 117 | id3 = ID3(self.filename) 118 | self.failUnlessEqual(id3['TIT2'], 'id3 title') 119 | f = Musepack(self.filename) 120 | self.failUnlessEqual(f['title'], 'apev2 title') 121 | -------------------------------------------------------------------------------- /mutagen/wavpack.py: -------------------------------------------------------------------------------- 1 | # Copyright 2006 Joe Wreschnig 2 | # 2014 Christoph Reiter 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | 9 | """WavPack reading and writing. 10 | 11 | WavPack is a lossless format that uses APEv2 tags. Read 12 | 13 | * http://www.wavpack.com/ 14 | * http://www.wavpack.com/file_format.txt 15 | 16 | for more information. 17 | """ 18 | 19 | __all__ = ["WavPack", "Open", "delete"] 20 | 21 | from mutagen import StreamInfo 22 | from mutagen.apev2 import APEv2File, error, delete 23 | from mutagen._util import cdata, convert_error 24 | 25 | 26 | class WavPackHeaderError(error): 27 | pass 28 | 29 | RATES = [6000, 8000, 9600, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 30 | 48000, 64000, 88200, 96000, 192000] 31 | 32 | 33 | class _WavPackHeader(object): 34 | 35 | def __init__(self, block_size, version, track_no, index_no, total_samples, 36 | block_index, block_samples, flags, crc): 37 | 38 | self.block_size = block_size 39 | self.version = version 40 | self.track_no = track_no 41 | self.index_no = index_no 42 | self.total_samples = total_samples 43 | self.block_index = block_index 44 | self.block_samples = block_samples 45 | self.flags = flags 46 | self.crc = crc 47 | 48 | @classmethod 49 | @convert_error(IOError, WavPackHeaderError) 50 | def from_fileobj(cls, fileobj): 51 | """A new _WavPackHeader or raises WavPackHeaderError""" 52 | 53 | header = fileobj.read(32) 54 | if len(header) != 32 or not header.startswith(b"wvpk"): 55 | raise WavPackHeaderError("not a WavPack header: %r" % header) 56 | 57 | block_size = cdata.uint_le(header[4:8]) 58 | version = cdata.ushort_le(header[8:10]) 59 | track_no = ord(header[10:11]) 60 | index_no = ord(header[11:12]) 61 | samples = cdata.uint_le(header[12:16]) 62 | if samples == 2 ** 32 - 1: 63 | samples = -1 64 | block_index = cdata.uint_le(header[16:20]) 65 | block_samples = cdata.uint_le(header[20:24]) 66 | flags = cdata.uint_le(header[24:28]) 67 | crc = cdata.uint_le(header[28:32]) 68 | 69 | return _WavPackHeader(block_size, version, track_no, index_no, 70 | samples, block_index, block_samples, flags, crc) 71 | 72 | 73 | class WavPackInfo(StreamInfo): 74 | """WavPack stream information. 75 | 76 | Attributes: 77 | channels (int): number of audio channels (1 or 2) 78 | length (float): file length in seconds, as a float 79 | sample_rate (int): audio sampling rate in Hz 80 | bits_per_sample (int): audio sample size 81 | version (int): WavPack stream version 82 | """ 83 | 84 | def __init__(self, fileobj): 85 | try: 86 | header = _WavPackHeader.from_fileobj(fileobj) 87 | except WavPackHeaderError: 88 | raise WavPackHeaderError("not a WavPack file") 89 | 90 | self.version = header.version 91 | self.channels = bool(header.flags & 4) or 2 92 | self.sample_rate = RATES[(header.flags >> 23) & 0xF] 93 | self.bits_per_sample = ((header.flags & 3) + 1) * 8 94 | 95 | # most common multiplier (DSD64) 96 | if (header.flags >> 31) & 1: 97 | self.sample_rate *= 4 98 | self.bits_per_sample = 1 99 | 100 | if header.total_samples == -1 or header.block_index != 0: 101 | # TODO: we could make this faster by using the tag size 102 | # and search backwards for the last block, then do 103 | # last.block_index + last.block_samples - initial.block_index 104 | samples = header.block_samples 105 | while 1: 106 | fileobj.seek(header.block_size - 32 + 8, 1) 107 | try: 108 | header = _WavPackHeader.from_fileobj(fileobj) 109 | except WavPackHeaderError: 110 | break 111 | samples += header.block_samples 112 | else: 113 | samples = header.total_samples 114 | 115 | self.length = float(samples) / self.sample_rate 116 | 117 | def pprint(self): 118 | return u"WavPack, %.2f seconds, %d Hz" % (self.length, 119 | self.sample_rate) 120 | 121 | 122 | class WavPack(APEv2File): 123 | """WavPack(filething) 124 | 125 | Arguments: 126 | filething (filething) 127 | 128 | Attributes: 129 | info (`WavPackInfo`) 130 | """ 131 | 132 | _Info = WavPackInfo 133 | _mimes = ["audio/x-wavpack"] 134 | 135 | @staticmethod 136 | def score(filename, fileobj, header): 137 | return header.startswith(b"wvpk") * 2 138 | 139 | 140 | Open = WavPack 141 | --------------------------------------------------------------------------------