├── 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 | moov M udta = meta - !ilst cpil data
--------------------------------------------------------------------------------
/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 | FRM8 rDSD FVER PROP FSND FS + CHNL C CMPR DSD not compressed DSD
--------------------------------------------------------------------------------
/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 GTCOMM This is a comment!TCON Relaxation..? :)TDRC 2023TRCK 1TALB Mutagen Bug ReportsTIT2 One 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 ID3 TIT2 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 |
--------------------------------------------------------------------------------