diff -Nru mutagen-1.33.2/debian/changelog mutagen-1.34/debian/changelog --- mutagen-1.33.2/debian/changelog 2016-07-16 16:29:10.000000000 +0000 +++ mutagen-1.34/debian/changelog 2016-08-03 10:14:49.000000000 +0000 @@ -1,3 +1,11 @@ +mutagen (1.34-1) unstable; urgency=medium + + * Update Homepage: link (closes: #833332). + * Fix debian/watch. + * New upstream release. + + -- Tristan Seligmann Wed, 03 Aug 2016 12:14:49 +0200 + mutagen (1.33.2-1) unstable; urgency=medium * New upstream release. diff -Nru mutagen-1.33.2/debian/control mutagen-1.34/debian/control --- mutagen-1.33.2/debian/control 2016-07-16 16:29:10.000000000 +0000 +++ mutagen-1.34/debian/control 2016-08-03 10:14:49.000000000 +0000 @@ -25,7 +25,7 @@ X-Python-Version: >= 2.6 Vcs-Git: https://anonscm.debian.org/git/python-modules/packages/mutagen.git Vcs-Browser: https://anonscm.debian.org/cgit/python-modules/packages/mutagen.git -Homepage: https://bitbucket.org/lazka/mutagen +Homepage: https://github.com/quodlibet/mutagen Package: python-mutagen Architecture: all diff -Nru mutagen-1.33.2/debian/.git-dpm mutagen-1.34/debian/.git-dpm --- mutagen-1.33.2/debian/.git-dpm 2016-07-16 16:29:10.000000000 +0000 +++ mutagen-1.34/debian/.git-dpm 2016-08-03 10:14:49.000000000 +0000 @@ -1,11 +1,11 @@ # see git-dpm(1) from git-dpm package -9ce7c75d542f0b5373f67a65f1e44e5c65a6d07c -9ce7c75d542f0b5373f67a65f1e44e5c65a6d07c -ffdba0c7efe4bedb0f42e4c7ab3e159d20330384 -ffdba0c7efe4bedb0f42e4c7ab3e159d20330384 -mutagen_1.33.2.orig.tar.gz -e4379cb2c4e663e055d45e66ab582fe90ec0ac75 -863803 +62899a25be792d92564cc1d6f989ff648fc0a6ec +62899a25be792d92564cc1d6f989ff648fc0a6ec +7023b1b670d3efec46ff34058d13e04bd7e511bb +7023b1b670d3efec46ff34058d13e04bd7e511bb +mutagen_1.34.orig.tar.gz +ce3d707b87f84ad6b6ab51ddcf58ef249ba1d43f +871691 debianTag="debian/%e%v" patchedTag="patched/%e%v" upstreamTag="upstream/%e%u" diff -Nru mutagen-1.33.2/debian/patches/use-rtd-package mutagen-1.34/debian/patches/use-rtd-package --- mutagen-1.33.2/debian/patches/use-rtd-package 2016-07-16 16:29:10.000000000 +0000 +++ mutagen-1.34/debian/patches/use-rtd-package 2016-08-03 10:14:49.000000000 +0000 @@ -1,4 +1,4 @@ -From 32711a6077c931818c95190faa16aefc9c28b963 Mon Sep 17 00:00:00 2001 +From f0547cae9be5480a6fcb67b7c1555a9d9830db06 Mon Sep 17 00:00:00 2001 From: Tristan Seligmann Date: Thu, 8 Oct 2015 09:57:12 -0700 Subject: Use the Debian package of the sphinx-rtd theme diff -Nru mutagen-1.33.2/debian/patches/use-system-inventory mutagen-1.34/debian/patches/use-system-inventory --- mutagen-1.33.2/debian/patches/use-system-inventory 2016-07-16 16:29:10.000000000 +0000 +++ mutagen-1.34/debian/patches/use-system-inventory 2016-08-03 10:14:49.000000000 +0000 @@ -1,4 +1,4 @@ -From 9ce7c75d542f0b5373f67a65f1e44e5c65a6d07c Mon Sep 17 00:00:00 2001 +From 62899a25be792d92564cc1d6f989ff648fc0a6ec Mon Sep 17 00:00:00 2001 From: Tristan Seligmann Date: Thu, 8 Oct 2015 09:57:13 -0700 Subject: Use the system copy of the Python documentation inventory diff -Nru mutagen-1.33.2/debian/python-mutagen-doc.docs mutagen-1.34/debian/python-mutagen-doc.docs --- mutagen-1.33.2/debian/python-mutagen-doc.docs 2016-07-16 16:29:10.000000000 +0000 +++ mutagen-1.34/debian/python-mutagen-doc.docs 2016-08-03 10:14:49.000000000 +0000 @@ -1 +1 @@ -docs/html +.pybuild/docs/* diff -Nru mutagen-1.33.2/debian/rules mutagen-1.34/debian/rules --- mutagen-1.33.2/debian/rules 2016-07-16 16:29:10.000000000 +0000 +++ mutagen-1.34/debian/rules 2016-08-03 10:14:49.000000000 +0000 @@ -26,14 +26,8 @@ http_proxy='127.0.0.1:9' \ https_proxy='127.0.0.1:9' \ sphinx-build -N -b html docs/ $(CURDIR)/.pybuild/docs/html/ -endif - -override_dh_auto_build: - dh_auto_build - PYTHONPATH=$(CURDIR) docs/id3_frames_gen.py > docs/api/id3_frames.rst - $(MAKE) -C docs - mv docs/_build docs/html $(MAKE) -C docs/man +endif override_dh_auto_install: @@ -51,7 +45,7 @@ override_dh_clean: dh_clean - rm -rf docs/html docs/man/_man + rm -rf docs/man/_man override_dh_auto_test: diff -Nru mutagen-1.33.2/debian/watch mutagen-1.34/debian/watch --- mutagen-1.33.2/debian/watch 2016-07-16 16:29:10.000000000 +0000 +++ mutagen-1.34/debian/watch 2016-08-03 10:14:49.000000000 +0000 @@ -1,3 +1,5 @@ version=3 -opts=pgpsigurlmangle=s/$/.sig/ \ - http://bitbucket.org/lazka/mutagen/downloads/mutagen-([0-9.]+).tar.gz +opts="pgpsigurlmangle=s/$/.sig/,\ + downloadurlmangle=s{/[^/]*/lazka/mutagen/downloads/mutagen-}{/mutagen-}" \ + https://bitbucket.org/lazka/mutagen/downloads/ \ + .*/mutagen-([0-9.]+).tar.gz diff -Nru mutagen-1.33.2/docs/api/id3_frames.rst mutagen-1.34/docs/api/id3_frames.rst --- mutagen-1.33.2/docs/api/id3_frames.rst 2016-04-07 16:23:09.000000000 +0000 +++ mutagen-1.34/docs/api/id3_frames.rst 2016-07-16 10:49:13.000000000 +0000 @@ -7,47 +7,42 @@ :members: -.. autoclass:: mutagen.id3.BinaryFrame(data='None') +.. autoclass:: mutagen.id3.BinaryFrame(data='') :show-inheritance: :members: -.. autoclass:: mutagen.id3.FrameOpt() +.. autoclass:: mutagen.id3.PairedTextFrame(encoding=, people=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.PairedTextFrame(encoding=None, people=[]) +.. autoclass:: mutagen.id3.TextFrame(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TextFrame(encoding=None, text=[]) +.. autoclass:: mutagen.id3.UrlFrame(url=u'') :show-inheritance: :members: -.. autoclass:: mutagen.id3.UrlFrame(url=u'None') +.. autoclass:: mutagen.id3.NumericPartTextFrame(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.NumericPartTextFrame(encoding=None, text=[]) +.. autoclass:: mutagen.id3.NumericTextFrame(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.NumericTextFrame(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TimeStampTextFrame(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TimeStampTextFrame(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.UrlFrameU(url=u'None') +.. autoclass:: mutagen.id3.UrlFrameU(url=u'') :show-inheritance: :members: @@ -55,795 +50,512 @@ ---------------- -.. autoclass:: mutagen.id3.AENC(owner=u'None', preview_start=None, preview_length=None) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.APIC(encoding=None, mime=u'None', type=None, desc=u'None', data='None') - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.ASPI(S=None, L=None, N=None, b=None, Fi=None) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.COMM(encoding=None, lang=None, desc=u'None', text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.COMR(encoding=None, price=u'None', valid_until=None, contact=u'None', format=None, seller=u'None', desc=u'None') - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.ENCR(owner=u'None', method=None, data='None') - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.EQU2(method=None, desc=u'None', adjustments=None) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.ETCO(format=None, events=None) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.GEOB(encoding=None, mime=u'None', filename=u'None', desc=u'None', data='None') - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.GRID(owner=u'None', group=None) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.IPLS(encoding=None, people=[]) +.. autoclass:: mutagen.id3.AENC(owner=u'', preview_start=0, preview_length=0) :show-inheritance: :members: -.. autoclass:: mutagen.id3.LINK(frameid=None, url=u'None') +.. autoclass:: mutagen.id3.APIC(encoding=, mime=u'', type=, desc=u'', data='') :show-inheritance: :members: -.. autoclass:: mutagen.id3.MCDI(data='None') +.. autoclass:: mutagen.id3.ASPI(S=0, L=0, N=0, b=0, Fi=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.MLLT(frames=None, bytes=None, milliseconds=None, bits_for_bytes=None, bits_for_milliseconds=None, data='None') +.. autoclass:: mutagen.id3.CHAP(element_id=u'', start_time=0, end_time=0, start_offset=4294967295, end_offset=4294967295, sub_frames={}) :show-inheritance: :members: -.. autoclass:: mutagen.id3.OWNE(encoding=None, price=u'None', date=None, seller=u'None') +.. autoclass:: mutagen.id3.COMM(encoding=, lang='XXX', desc=u'', text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.PCNT(count=None) +.. autoclass:: mutagen.id3.COMR(encoding=, price=u'', valid_until='19700101', contact=u'', format=0, seller=u'', desc=u'') :show-inheritance: :members: -.. autoclass:: mutagen.id3.POPM(email=u'None', rating=None) +.. autoclass:: mutagen.id3.CTOC(element_id=u'', flags=<0: 0>, child_element_ids=[], sub_frames={}) :show-inheritance: :members: -.. autoclass:: mutagen.id3.POSS(format=None, position=None) +.. autoclass:: mutagen.id3.ENCR(owner=u'', method=128, data='') :show-inheritance: :members: -.. autoclass:: mutagen.id3.PRIV(owner=u'None', data='None') +.. autoclass:: mutagen.id3.EQU2(method=0, desc=u'', adjustments=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.RBUF(size=None) +.. autoclass:: mutagen.id3.ETCO(format=1, events=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.RVA2(desc=u'None', channel=None, gain=None, peak=None) +.. autoclass:: mutagen.id3.GEOB(encoding=, mime=u'', filename=u'', desc=u'', data='') :show-inheritance: :members: -.. autoclass:: mutagen.id3.RVRB(left=None, right=None, bounce_left=None, bounce_right=None, feedback_ltl=None, feedback_ltr=None, feedback_rtr=None, feedback_rtl=None, premix_ltr=None, premix_rtl=None) +.. autoclass:: mutagen.id3.GRID(owner=u'', group=128) :show-inheritance: :members: -.. autoclass:: mutagen.id3.SEEK(offset=None) +.. autoclass:: mutagen.id3.IPLS(encoding=, people=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.SIGN(group=None, sig='None') +.. autoclass:: mutagen.id3.LINK(frameid='XXXX', url=u'') :show-inheritance: :members: -.. autoclass:: mutagen.id3.SYLT(encoding=None, lang=None, format=None, type=None, desc=u'None', text=None) +.. autoclass:: mutagen.id3.MCDI(data='') :show-inheritance: :members: -.. autoclass:: mutagen.id3.SYTC(format=None, data='None') +.. autoclass:: mutagen.id3.MLLT(frames=0, bytes=0, milliseconds=0, bits_for_bytes=0, bits_for_milliseconds=0, data='') :show-inheritance: :members: -.. autoclass:: mutagen.id3.TALB(encoding=None, text=[]) +.. autoclass:: mutagen.id3.OWNE(encoding=, price=u'', date='19700101', seller=u'') :show-inheritance: :members: -.. autoclass:: mutagen.id3.TBPM(encoding=None, text=[]) +.. autoclass:: mutagen.id3.PCNT(count=0) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TCMP(encoding=None, text=[]) +.. autoclass:: mutagen.id3.PCST(value=0) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TCOM(encoding=None, text=[]) +.. autoclass:: mutagen.id3.POPM(email=u'', rating=0) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TCON(encoding=None, text=[]) +.. autoclass:: mutagen.id3.POSS(format=1, position=0) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TCOP(encoding=None, text=[]) +.. autoclass:: mutagen.id3.PRIV(owner=u'', data='') :show-inheritance: :members: -.. autoclass:: mutagen.id3.TDAT(encoding=None, text=[]) +.. autoclass:: mutagen.id3.RBUF(size=0) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TDEN(encoding=None, text=[]) +.. autoclass:: mutagen.id3.RVA2(desc=u'', channel=1, gain=1, peak=1) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TDES(encoding=None, text=[]) +.. autoclass:: mutagen.id3.RVAD(adjustments=[0, 0]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TDLY(encoding=None, text=[]) +.. autoclass:: mutagen.id3.RVRB(left=0, right=0, bounce_left=0, bounce_right=0, feedback_ltl=0, feedback_ltr=0, feedback_rtr=0, feedback_rtl=0, premix_ltr=0, premix_rtl=0) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TDOR(encoding=None, text=[]) +.. autoclass:: mutagen.id3.SEEK(offset=0) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TDRC(encoding=None, text=[]) +.. autoclass:: mutagen.id3.SIGN(group=128, sig='') :show-inheritance: :members: -.. autoclass:: mutagen.id3.TDRL(encoding=None, text=[]) +.. autoclass:: mutagen.id3.SYLT(encoding=, lang='XXX', format=1, type=0, desc=u'', text=u'') :show-inheritance: :members: -.. autoclass:: mutagen.id3.TDTG(encoding=None, text=[]) +.. autoclass:: mutagen.id3.SYTC(format=1, data='') :show-inheritance: :members: -.. autoclass:: mutagen.id3.TENC(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TALB(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TEXT(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TBPM(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TFLT(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TCAT(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TGID(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TCMP(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TIME(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TCOM(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TIPL(encoding=None, people=[]) +.. autoclass:: mutagen.id3.TCON(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TIT1(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TCOP(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TIT2(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TDAT(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TIT3(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TDEN(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TKEY(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TDES(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TLAN(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TLEN(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TMCL(encoding=None, people=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TMED(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TMOO(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TOAL(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TOFN(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TOLY(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TOPE(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TORY(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TOWN(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TPE1(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TPE2(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TPE3(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TPE4(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TPOS(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TPRO(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TPUB(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TRCK(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TRDA(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TRSN(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TRSO(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TSIZ(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TSO2(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TSOA(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TSOC(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TSOP(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TSOT(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TSRC(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TSSE(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TSST(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TXXX(encoding=None, desc=u'None', text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.TYER(encoding=None, text=[]) - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.UFID(owner=u'None', data='None') - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.USER(encoding=None, lang=None, text=u'None') - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.USLT(encoding=None, lang=None, desc=u'None', text=u'None') - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.WCOM(url=u'None') - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.WCOP(url=u'None') - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.WFED(url=u'None') - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.WOAF(url=u'None') - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.WOAR(url=u'None') - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.WOAS(url=u'None') - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.WORS(url=u'None') - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.WPAY(url=u'None') - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.WPUB(url=u'None') - :show-inheritance: - :members: - - -.. autoclass:: mutagen.id3.WXXX(encoding=None, desc=u'None', url=u'None') - :show-inheritance: - :members: - -ID3v2.2 Frames --------------- - - -.. autoclass:: mutagen.id3.BUF(size=None) +.. autoclass:: mutagen.id3.TDLY(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.CNT(count=None) +.. autoclass:: mutagen.id3.TDOR(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.COM(encoding=None, lang=None, desc=u'None', text=[]) +.. autoclass:: mutagen.id3.TDRC(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.CRA(owner=u'None', preview_start=None, preview_length=None) +.. autoclass:: mutagen.id3.TDRL(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.CRM(owner=u'None', desc=u'None', data='None') +.. autoclass:: mutagen.id3.TDTG(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.ETC(format=None, events=None) +.. autoclass:: mutagen.id3.TENC(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.GEO(encoding=None, mime=u'None', filename=u'None', desc=u'None', data='None') +.. autoclass:: mutagen.id3.TEXT(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.IPL(encoding=None, people=[]) +.. autoclass:: mutagen.id3.TFLT(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.LNK(frameid=None, url=u'None') +.. autoclass:: mutagen.id3.TGID(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.MCI(data='None') +.. autoclass:: mutagen.id3.TIME(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.MLL(frames=None, bytes=None, milliseconds=None, bits_for_bytes=None, bits_for_milliseconds=None, data='None') +.. autoclass:: mutagen.id3.TIPL(encoding=, people=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.PIC(encoding=None, mime=None, type=None, desc=u'None', data='None') +.. autoclass:: mutagen.id3.TIT1(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.POP(email=u'None', rating=None) +.. autoclass:: mutagen.id3.TIT2(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.REV(left=None, right=None, bounce_left=None, bounce_right=None, feedback_ltl=None, feedback_ltr=None, feedback_rtr=None, feedback_rtl=None, premix_ltr=None, premix_rtl=None) +.. autoclass:: mutagen.id3.TIT3(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.SLT(encoding=None, lang=None, format=None, type=None, desc=u'None', text=None) +.. autoclass:: mutagen.id3.TKEY(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.STC(format=None, data='None') +.. autoclass:: mutagen.id3.TKWD(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TAL(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TLAN(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TBP(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TLEN(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TCM(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TMCL(encoding=, people=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TCO(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TMED(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TCP(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TMOO(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TCR(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TOAL(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TDA(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TOFN(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TDY(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TOLY(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TEN(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TOPE(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TFT(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TORY(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TIM(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TOWN(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TKE(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TPE1(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TLA(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TPE2(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TLE(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TPE3(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TMT(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TPE4(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TOA(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TPOS(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TOF(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TPRO(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TOL(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TPUB(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TOR(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TRCK(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TOT(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TRDA(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TP1(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TRSN(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TP2(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TRSO(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TP3(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TSIZ(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TP4(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TSO2(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TPA(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TSOA(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TPB(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TSOC(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TRC(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TSOP(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TRD(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TSOT(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TRK(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TSRC(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TSI(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TSSE(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TSS(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TSST(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TT1(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TXXX(encoding=, desc=u'', text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TT2(encoding=None, text=[]) +.. autoclass:: mutagen.id3.TYER(encoding=, text=[]) :show-inheritance: :members: -.. autoclass:: mutagen.id3.TT3(encoding=None, text=[]) +.. autoclass:: mutagen.id3.UFID(owner=u'', data='') :show-inheritance: :members: -.. autoclass:: mutagen.id3.TXT(encoding=None, text=[]) +.. autoclass:: mutagen.id3.USER(encoding=, lang='XXX', text=u'') :show-inheritance: :members: -.. autoclass:: mutagen.id3.TXX(encoding=None, desc=u'None', text=[]) +.. autoclass:: mutagen.id3.USLT(encoding=, lang='XXX', desc=u'', text=u'') :show-inheritance: :members: -.. autoclass:: mutagen.id3.TYE(encoding=None, text=[]) +.. autoclass:: mutagen.id3.WCOM(url=u'') :show-inheritance: :members: -.. autoclass:: mutagen.id3.UFI(owner=u'None', data='None') +.. autoclass:: mutagen.id3.WCOP(url=u'') :show-inheritance: :members: -.. autoclass:: mutagen.id3.ULT(encoding=None, lang=None, desc=u'None', text=u'None') +.. autoclass:: mutagen.id3.WFED(url=u'') :show-inheritance: :members: -.. autoclass:: mutagen.id3.WAF(url=u'None') +.. autoclass:: mutagen.id3.WOAF(url=u'') :show-inheritance: :members: -.. autoclass:: mutagen.id3.WAR(url=u'None') +.. autoclass:: mutagen.id3.WOAR(url=u'') :show-inheritance: :members: -.. autoclass:: mutagen.id3.WAS(url=u'None') +.. autoclass:: mutagen.id3.WOAS(url=u'') :show-inheritance: :members: -.. autoclass:: mutagen.id3.WCM(url=u'None') +.. autoclass:: mutagen.id3.WORS(url=u'') :show-inheritance: :members: -.. autoclass:: mutagen.id3.WCP(url=u'None') +.. autoclass:: mutagen.id3.WPAY(url=u'') :show-inheritance: :members: -.. autoclass:: mutagen.id3.WPB(url=u'None') +.. autoclass:: mutagen.id3.WPUB(url=u'') :show-inheritance: :members: -.. autoclass:: mutagen.id3.WXX(encoding=None, desc=u'None', url=u'None') +.. autoclass:: mutagen.id3.WXXX(encoding=, desc=u'', url=u'') :show-inheritance: :members: diff -Nru mutagen-1.33.2/docs/api/id3.rst mutagen-1.34/docs/api/id3.rst --- mutagen-1.33.2/docs/api/id3.rst 2016-06-23 14:51:21.000000000 +0000 +++ mutagen-1.34/docs/api/id3.rst 2016-07-16 10:43:47.000000000 +0000 @@ -20,19 +20,27 @@ :members: :member-order: bysource - .. autoclass:: mutagen.id3.Encoding :members: :member-order: bysource +.. autoclass:: mutagen.id3.CTOCFlags + :members: + :member-order: bysource + + ID3 --- -.. autoclass:: mutagen.id3.ID3 +.. autoclass:: mutagen.id3.ID3Tags :show-inheritance: :members: :exclude-members: loaded_frame +.. autoclass:: mutagen.id3.ID3 + :show-inheritance: + :members: + .. autoclass:: mutagen.id3.ID3FileType :members: :exclude-members: ID3 diff -Nru mutagen-1.33.2/docs/id3_frames_gen.py mutagen-1.34/docs/id3_frames_gen.py --- mutagen-1.33.2/docs/id3_frames_gen.py 2016-04-07 16:23:09.000000000 +0000 +++ mutagen-1.34/docs/id3_frames_gen.py 2016-07-16 10:42:38.000000000 +0000 @@ -52,5 +52,3 @@ print_frames(BaseFrames, sort_mro=True) print_header("ID3v2.3/4 Frames") print_frames(Frames) - print_header("ID3v2.2 Frames") - print_frames(Frames_2_2) diff -Nru mutagen-1.33.2/docs/index.rst mutagen-1.34/docs/index.rst --- mutagen-1.33.2/docs/index.rst 2016-06-21 12:17:17.000000000 +0000 +++ mutagen-1.34/docs/index.rst 2016-07-07 14:00:43.000000000 +0000 @@ -21,8 +21,8 @@ MP3s. ID3 and APEv2 tags can be edited regardless of audio format. It can also manipulate Ogg streams on an individual packet/page level. -Mutagen works with Python 2.6, 2.7, 3.3+ (CPython and PyPy) on Linux, Windows -and macOS, and has no dependencies outside the Python standard library. +Mutagen works with Python 2.7, 3.3+ (CPython and PyPy) on Linux, Windows and +macOS, and has no dependencies outside the Python standard library. There is a :doc:`brief tutorial with several API examples. ` diff -Nru mutagen-1.33.2/docs/user/examples/fileobj-iface.py mutagen-1.34/docs/user/examples/fileobj-iface.py --- mutagen-1.33.2/docs/user/examples/fileobj-iface.py 2016-06-26 20:11:53.000000000 +0000 +++ mutagen-1.34/docs/user/examples/fileobj-iface.py 2016-07-20 17:16:28.000000000 +0000 @@ -78,6 +78,8 @@ The current position or given size will never be larger than the file size. + This has to flush write buffers in case writing is buffered. + Returns Nothing. Raises IOError. """ diff -Nru mutagen-1.33.2/docs/user/id3.rst mutagen-1.34/docs/user/id3.rst --- mutagen-1.33.2/docs/user/id3.rst 2016-06-21 11:37:06.000000000 +0000 +++ mutagen-1.34/docs/user/id3.rst 2016-07-20 15:57:20.000000000 +0000 @@ -48,13 +48,14 @@ For more control the following functions are important: * :func:`ID3` which loads the tags and if ``translate=True`` - (default) calls either :meth:`ID3.update_to_v24` or - :meth:`ID3.update_to_v23` depending on the ``v2_version`` + (default) calls either :meth:`ID3Tags.update_to_v24` or + :meth:`ID3Tags.update_to_v23` depending on the ``v2_version`` argument (defaults to ``4``) -* :meth:`ID3.update_to_v24` which upgrades v2.2/3 frames to v2.4 +* :meth:`ID3Tags.update_to_v24` which upgrades v2.2/3 frames to v2.4 -* :meth:`ID3.update_to_v23` which downgrades v2.4 and upgrades v2.2 frames to v2.3 +* :meth:`ID3Tags.update_to_v23` which downgrades v2.4 and upgrades v2.2 + frames to v2.3 * :meth:`ID3.save` which will save as v2.3 if ``v2_version=3`` (defaults to ``4``) and also allows specifying a separator for joining multiple text @@ -82,7 +83,7 @@ Saving v2.4 frames in v2.3 tags While not standard conform, you can exclude certain v2.4 frames from being - thrown out by :meth:`ID3.update_to_v23` by removing them temporarily:: + thrown out by :meth:`ID3Tags.update_to_v23` by removing them temporarily:: audio = ID3("example.mp3", translate=False) keep_these = audio.getall("TSOP") @@ -140,6 +141,36 @@ audio.pprint() +Chapter Extension +^^^^^^^^^^^^^^^^^ + +The following code adds two chapters to a file: + +:: + + from mutagen.id3 import ID3, CTOC, CHAP, TIT2, CTOCFlags + + audio = ID3("example.mp3") + audio.add( + CTOC(element_id=u"toc", flags=CTOCFlags.TOP_LEVEL | CTOCFlags.ORDERED, + child_element_ids=[u"chp1", "chp2"], + sub_frames=[ + TIT2(text=[u"I'm a TOC"]), + ])) + audio.add( + CHAP(element_id=u"chp1", start_time=0, end_time=42000, + sub_frames=[ + TIT2(text=[u"I'm the first chapter"]), + ])) + audio.add( + CHAP(element_id=u"chp2", start_time=42000, end_time=84000, + sub_frames=[ + TIT2(text=[u"I'm the second chapter"]), + ])) + audio.save() + + + Compatibility / Bugs ^^^^^^^^^^^^^^^^^^^^ diff -Nru mutagen-1.33.2/mutagen/aac.py mutagen-1.34/mutagen/aac.py --- mutagen-1.33.2/mutagen/aac.py 2016-07-05 10:48:05.000000000 +0000 +++ mutagen-1.34/mutagen/aac.py 2016-07-13 07:06:55.000000000 +0000 @@ -15,6 +15,7 @@ from mutagen._file import FileType from mutagen._util import BitReader, BitReaderError, MutagenError, loadfile, \ convert_error +from mutagen.id3._util import BitPaddedInt from mutagen._compat import endswith, xrange @@ -287,7 +288,6 @@ # skip id3v2 header start_offset = 0 header = fileobj.read(10) - from mutagen.id3 import BitPaddedInt if header.startswith(b"ID3"): size = BitPaddedInt(header[6:]) start_offset = size + 10 diff -Nru mutagen-1.33.2/mutagen/easyid3.py mutagen-1.34/mutagen/easyid3.py --- mutagen-1.33.2/mutagen/easyid3.py 2016-06-28 20:44:36.000000000 +0000 +++ mutagen-1.34/mutagen/easyid3.py 2016-07-12 05:18:50.000000000 +0000 @@ -150,19 +150,14 @@ return list(id3[frameid]) def setter(id3, key, value): - try: - frame = id3[frameid] - except KeyError: - enc = 0 - # Store 8859-1 if we can, per MusicBrainz spec. - for v in value: - if v and max(v) > u'\x7f': - enc = 3 - break + enc = 0 + # Store 8859-1 if we can, per MusicBrainz spec. + for v in value: + if v and max(v) > u'\x7f': + enc = 3 + break - id3.add(mutagen.id3.TXXX(encoding=enc, text=value, desc=desc)) - else: - frame.text = value + id3.add(mutagen.id3.TXXX(encoding=enc, text=value, desc=desc)) def deleter(id3, key): del(id3[frameid]) diff -Nru mutagen-1.33.2/mutagen/flac.py mutagen-1.34/mutagen/flac.py --- mutagen-1.33.2/mutagen/flac.py 2016-06-27 19:09:19.000000000 +0000 +++ mutagen-1.34/mutagen/flac.py 2016-07-13 07:06:55.000000000 +0000 @@ -30,7 +30,7 @@ from mutagen._util import resize_bytes, MutagenError, get_size, loadfile, \ convert_error from mutagen._tags import PaddingInfo -from mutagen.id3 import BitPaddedInt +from mutagen.id3._util import BitPaddedInt from functools import reduce diff -Nru mutagen-1.33.2/mutagen/id3/_file.py mutagen-1.34/mutagen/id3/_file.py --- mutagen-1.33.2/mutagen/id3/_file.py 1970-01-01 00:00:00.000000000 +0000 +++ mutagen-1.34/mutagen/id3/_file.py 2016-07-16 10:41:55.000000000 +0000 @@ -0,0 +1,406 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2005 Michael Urman +# 2006 Lukas Lalinsky +# 2013 Christoph Reiter +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. + +import struct + +import mutagen +from mutagen._util import insert_bytes, delete_bytes, enum, \ + loadfile, convert_error, read_full +from mutagen._tags import PaddingInfo + +from ._util import error, ID3NoHeaderError, ID3UnsupportedVersionError, \ + BitPaddedInt +from ._tags import ID3Tags, ID3Header, ID3SaveConfig +from ._id3v1 import MakeID3v1, find_id3v1 + + +@enum +class ID3v1SaveOptions(object): + + REMOVE = 0 + """ID3v1 tags will be removed""" + + UPDATE = 1 + """ID3v1 tags will be updated but not added""" + + CREATE = 2 + """ID3v1 tags will be created and/or updated""" + + +class ID3(ID3Tags, mutagen.Metadata): + """ID3(filething=None) + + A file with an ID3v2 tag. + + If any arguments are given, the :meth:`load` is called with them. If no + arguments are given then an empty `ID3` object is created. + + :: + + ID3("foo.mp3") + # same as + t = ID3() + t.load("foo.mp3") + + Arguments: + filething (filething): or `None` + + Attributes: + version (Tuple[int]): ID3 tag version as a tuple + unknown_frames (List[bytes]): raw frame data of any unknown frames + found + size (int): the total size of the ID3 tag, including the header + """ + + __module__ = "mutagen.id3" + + PEDANTIC = True + """`bool`: + + .. deprecated:: 1.28 + + Doesn't have any effect + """ + + filename = None + + def __init__(self, *args, **kwargs): + self._header = None + self._version = (2, 4, 0) + super(ID3, self).__init__(*args, **kwargs) + + @property + def version(self): + """`tuple`: ID3 tag version as a tuple (of the loaded file)""" + + if self._header is not None: + return self._header.version + return self._version + + @version.setter + def version(self, value): + self._version = value + + @property + def f_unsynch(self): + if self._header is not None: + return self._header.f_unsynch + return False + + @property + def f_extended(self): + if self._header is not None: + return self._header.f_extended + return False + + @property + def size(self): + if self._header is not None: + return self._header.size + return 0 + + def _pre_load_header(self, fileobj): + # XXX: for aiff to adjust the offset.. + pass + + @convert_error(IOError, error) + @loadfile() + def load(self, filething, known_frames=None, translate=True, v2_version=4): + """load(filething, known_frames=None, translate=True, v2_version=4) + + Load tags from a filename. + + Args: + filename (filething): filename or file object to load tag data from + known_frames (Dict[`mutagen.text`, `Frame`]): dict mapping frame + IDs to Frame objects + translate (bool): Update all tags to ID3v2.3/4 internally. If you + intend to save, this must be true or you have to + call update_to_v23() / update_to_v24() manually. + v2_version (int): if update_to_v23 or update_to_v24 get called + (3 or 4) + + Example of loading a custom frame:: + + my_frames = dict(mutagen.id3.Frames) + class XMYF(Frame): ... + my_frames["XMYF"] = XMYF + mutagen.id3.ID3(filename, known_frames=my_frames) + """ + + fileobj = filething.fileobj + + if v2_version not in (3, 4): + raise ValueError("Only 3 and 4 possible for v2_version") + + self.unknown_frames = [] + self._header = None + self._padding = 0 + + self._pre_load_header(fileobj) + + try: + self._header = ID3Header(fileobj) + except (ID3NoHeaderError, ID3UnsupportedVersionError): + frames, offset = find_id3v1(fileobj) + if frames is None: + raise + + self.version = ID3Header._V11 + for v in frames.values(): + self.add(v) + else: + # XXX: attach to the header object so we have it in spec parsing.. + if known_frames is not None: + self._header._known_frames = known_frames + + data = read_full(fileobj, self.size - 10) + remaining_data = self._read(self._header, data) + self._padding = len(remaining_data) + + if translate: + if v2_version == 3: + self.update_to_v23() + else: + self.update_to_v24() + + def _prepare_data(self, fileobj, start, available, v2_version, v23_sep, + pad_func): + + if v2_version not in (3, 4): + raise ValueError("Only 3 or 4 allowed for v2_version") + + config = ID3SaveConfig(v2_version, v23_sep) + framedata = self._write(config) + + needed = len(framedata) + 10 + + fileobj.seek(0, 2) + trailing_size = fileobj.tell() - start + + info = PaddingInfo(available - needed, trailing_size) + new_padding = info._get_padding(pad_func) + if new_padding < 0: + raise error("invalid padding") + new_size = needed + new_padding + + new_framesize = BitPaddedInt.to_str(new_size - 10, width=4) + header = struct.pack( + '>3sBBB4s', b'ID3', v2_version, 0, 0, new_framesize) + + data = header + framedata + assert new_size >= len(data) + data += (new_size - len(data)) * b'\x00' + assert new_size == len(data) + + return data + + @convert_error(IOError, error) + @loadfile(writable=True, create=True) + def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None): + """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None) + + Save changes to a file. + + Args: + filename (fspath): + Filename to save the tag to. If no filename is given, + the one most recently loaded is used. + v1 (ID3v1SaveOptions): + if 0, ID3v1 tags will be removed. + if 1, ID3v1 tags will be updated but not added. + if 2, ID3v1 tags will be created and/or updated + v2 (int): + version of ID3v2 tags (3 or 4). + v23_sep (text): + the separator used to join multiple text values + if v2_version == 3. Defaults to '/' but if it's None + will be the ID3v2v2.4 null separator. + padding (PaddingFunction) + + Raises: + mutagen.MutagenError + + By default Mutagen saves ID3v2.4 tags. If you want to save ID3v2.3 + tags, you must call method update_to_v23 before saving the file. + + The lack of a way to update only an ID3v1 tag is intentional. + """ + + f = filething.fileobj + + try: + header = ID3Header(filething.fileobj) + except ID3NoHeaderError: + old_size = 0 + else: + old_size = header.size + + data = self._prepare_data( + f, 0, old_size, v2_version, v23_sep, padding) + new_size = len(data) + + if (old_size < new_size): + insert_bytes(f, new_size - old_size, old_size) + elif (old_size > new_size): + delete_bytes(f, old_size - new_size, new_size) + f.seek(0) + f.write(data) + + self.__save_v1(f, v1) + + def __save_v1(self, f, v1): + tag, offset = find_id3v1(f) + has_v1 = tag is not None + + f.seek(offset, 2) + if v1 == ID3v1SaveOptions.UPDATE and has_v1 or \ + v1 == ID3v1SaveOptions.CREATE: + f.write(MakeID3v1(self)) + else: + f.truncate() + + @loadfile(writable=True) + def delete(self, filething, delete_v1=True, delete_v2=True): + """delete(filething=None, delete_v1=True, delete_v2=True) + + Remove tags from a file. + + Args: + filething (filething): A filename or `None` to use the one used + when loading. + delete_v1 (bool): delete any ID3v1 tag + delete_v2 (bool): delete any ID3v2 tag + + If no filename is given, the one most recently loaded is used. + """ + + delete(filething, delete_v1, delete_v2) + self.clear() + + +@convert_error(IOError, error) +@loadfile(method=False, writable=True) +def delete(filething, delete_v1=True, delete_v2=True): + """Remove tags from a file. + + Args: + delete_v1 (bool): delete any ID3v1 tag + delete_v2 (bool): delete any ID3v2 tag + + Raises: + mutagen.MutagenError: In case deleting failed + """ + + f = filething.fileobj + + if delete_v1: + tag, offset = find_id3v1(f) + if tag is not None: + f.seek(offset, 2) + f.truncate() + + # technically an insize=0 tag is invalid, but we delete it anyway + # (primarily because we used to write it) + if delete_v2: + f.seek(0, 0) + idata = f.read(10) + try: + id3, vmaj, vrev, flags, insize = struct.unpack('>3sBBB4s', idata) + except struct.error: + pass + else: + insize = BitPaddedInt(insize) + if id3 == b'ID3' and insize >= 0: + delete_bytes(f, insize + 10, 0) + + +class ID3FileType(mutagen.FileType): + """ID3FileType(filething, ID3=None, **kwargs) + + An unknown type of file with ID3 tags. + + Args: + filething (filething): A filename or file-like object + ID3 (ID3): An ID3 subclass to use for tags. + + Raises: + mutagen.MutagenError: In case loading the file failed + + Load stream and tag information from a file. + + A custom tag reader may be used in instead of the default + mutagen.id3.ID3 object, e.g. an EasyID3 reader. + """ + + __module__ = "mutagen.id3" + + ID3 = ID3 + + class _Info(mutagen.StreamInfo): + length = 0 + + def __init__(self, fileobj, offset): + pass + + @staticmethod + def pprint(): + return u"Unknown format with ID3 tag" + + @staticmethod + def score(filename, fileobj, header_data): + return header_data.startswith(b"ID3") + + def add_tags(self, ID3=None): + """Add an empty ID3 tag to the file. + + Args: + ID3 (ID3): An ID3 subclass to use or `None` to use the one + that used when loading. + + A custom tag reader may be used in instead of the default + `ID3` object, e.g. an `mutagen.easyid3.EasyID3` reader. + """ + + if ID3 is None: + ID3 = self.ID3 + if self.tags is None: + self.ID3 = ID3 + self.tags = ID3() + else: + raise error("an ID3 tag already exists") + + @loadfile() + def load(self, filething, ID3=None, **kwargs): + # see __init__ for docs + + fileobj = filething.fileobj + + if ID3 is None: + ID3 = self.ID3 + else: + # If this was initialized with EasyID3, remember that for + # when tags are auto-instantiated in add_tags. + self.ID3 = ID3 + + try: + self.tags = ID3(fileobj, **kwargs) + except ID3NoHeaderError: + self.tags = None + + if self.tags is not None: + try: + offset = self.tags.size + except AttributeError: + offset = None + else: + offset = None + + self.info = self._Info(fileobj, offset) diff -Nru mutagen-1.33.2/mutagen/id3/_frames.py mutagen-1.34/mutagen/id3/_frames.py --- mutagen-1.33.2/mutagen/id3/_frames.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/mutagen/id3/_frames.py 2016-07-20 17:37:44.000000000 +0000 @@ -9,19 +9,17 @@ import zlib from struct import unpack -from ._util import ID3JunkFrameError, ID3EncryptionUnsupportedError, unsynch -from ._specs import ( - BinaryDataSpec, StringSpec, Latin1TextSpec, EncodedTextSpec, ByteSpec, - EncodingSpec, ASPIIndexSpec, SizedIntegerSpec, IntegerSpec, - VolumeAdjustmentsSpec, VolumePeakSpec, VolumeAdjustmentSpec, - ChannelSpec, MultiSpec, SynchronizedTextSpec, KeyEventSpec, TimeStampSpec, - EncodedNumericPartTextSpec, EncodedNumericTextSpec, SpecError, - PictureTypeSpec) -from .._compat import text_type, string_types, swap_to_string, iteritems, izip - - -def is_valid_frame_id(frame_id): - return frame_id.isalnum() and frame_id.isupper() +from ._util import ID3JunkFrameError, ID3EncryptionUnsupportedError, unsynch, \ + ID3SaveConfig, error +from ._specs import BinaryDataSpec, StringSpec, Latin1TextSpec, \ + EncodedTextSpec, ByteSpec, EncodingSpec, ASPIIndexSpec, SizedIntegerSpec, \ + IntegerSpec, Encoding, VolumeAdjustmentsSpec, VolumePeakSpec, \ + VolumeAdjustmentSpec, ChannelSpec, MultiSpec, SynchronizedTextSpec, \ + KeyEventSpec, TimeStampSpec, EncodedNumericPartTextSpec, \ + EncodedNumericTextSpec, SpecError, PictureTypeSpec, ID3FramesSpec, \ + Latin1TextListSpec, CTOCFlagsSpec, FrameIDSpec, RVASpec +from .._compat import text_type, string_types, swap_to_string, iteritems, \ + izip, itervalues def _bytes2key(b): @@ -54,6 +52,7 @@ FLAG24_DATALEN = 0x0001 _framespec = [] + _optionalspec = [] def __init__(self, *args, **kwargs): if len(args) == 1 and len(kwargs) == 0 and \ @@ -65,22 +64,43 @@ for checker, val in izip(self._framespec, args): setattr(self, checker.name, val) for checker in self._framespec[len(args):]: - setattr(self, checker.name, kwargs.get(checker.name)) + setattr(self, checker.name, + kwargs.get(checker.name, checker.default)) + for spec in self._optionalspec: + if spec.name in kwargs: + setattr(self, spec.name, kwargs[spec.name]) + else: + break def __setattr__(self, name, value): for checker in self._framespec: if checker.name == name: - self.__dict__[name] = checker.validate(self, value) + self._setattr(name, checker.validate(self, value)) + return + for checker in self._optionalspec: + if checker.name == name: + self._setattr(name, checker.validate(self, value)) return super(Frame, self).__setattr__(name, value) + def _setattr(self, name, value): + self.__dict__[name] = value + def _to_other(self, other): # this impl covers subclasses with the same framespec if other._framespec is not self._framespec: raise ValueError for checker in other._framespec: - setattr(other, checker.name, getattr(self, checker.name)) + other._setattr(checker.name, getattr(self, checker.name)) + + # this impl covers subclasses with the same optionalspec + if other._optionalspec is not self._optionalspec: + raise ValueError + + for checker in other._optionalspec: + if hasattr(self, checker.name): + other._setattr(checker.name, getattr(self, checker.name)) def _get_v23_frame(self, **kwargs): """Returns a frame copy which is suitable for writing into a v2.3 tag. @@ -93,6 +113,13 @@ name = checker.name value = getattr(self, name) new_kwargs[name] = checker._validate23(self, value, **kwargs) + + for checker in self._optionalspec: + name = checker.name + if hasattr(self, name): + value = getattr(self, name) + new_kwargs[name] = checker._validate23(self, value, **kwargs) + return type(self)(**new_kwargs) @property @@ -118,27 +145,64 @@ # so repr works during __init__ if hasattr(self, attr.name): kw.append('%s=%r' % (attr.name, getattr(self, attr.name))) + for attr in self._optionalspec: + if hasattr(self, attr.name): + kw.append('%s=%r' % (attr.name, getattr(self, attr.name))) return '%s(%s)' % (type(self).__name__, ', '.join(kw)) - def _readData(self, data): + def _readData(self, id3, data): """Raises ID3JunkFrameError; Returns leftover data""" for reader in self._framespec: - if len(data): + if len(data) or reader.handle_nodata: try: - value, data = reader.read(self, data) + value, data = reader.read(id3, self, data) except SpecError as e: raise ID3JunkFrameError(e) else: raise ID3JunkFrameError("no data left") - setattr(self, reader.name, value) + self._setattr(reader.name, value) + + for reader in self._optionalspec: + if len(data) or reader.handle_nodata: + try: + value, data = reader.read(id3, self, data) + except SpecError as e: + raise ID3JunkFrameError(e) + else: + break + self._setattr(reader.name, value) return data - def _writeData(self): + def _writeData(self, config=None): + """Raises error""" + + if config is None: + config = ID3SaveConfig() + + if config.v2_version == 3: + frame = self._get_v23_frame(sep=config.v23_separator) + else: + frame = self + data = [] for writer in self._framespec: - data.append(writer.write(self, getattr(self, writer.name))) + try: + data.append( + writer.write(config, frame, getattr(frame, writer.name))) + except SpecError as e: + raise error(e) + + for writer in self._optionalspec: + try: + data.append( + writer.write(config, frame, getattr(frame, writer.name))) + except AttributeError: + break + except SpecError as e: + raise error(e) + return b''.join(data) def pprint(self): @@ -149,7 +213,7 @@ return "[unrepresentable data]" @classmethod - def _fromData(cls, id3, tflags, data): + def _fromData(cls, header, tflags, data): """Construct this ID3 frame from raw string data. Raises: @@ -159,7 +223,7 @@ ID3EncryptionUnsupportedError in case the frame is encrypted. """ - if id3.version >= id3._V24: + if header.version >= header._V24: if tflags & (Frame.FLAG24_COMPRESS | Frame.FLAG24_DATALEN): # The data length int is syncsafe in 2.4 (but not 2.3). # However, we don't actually need the data length int, @@ -167,7 +231,7 @@ # all we need are the raw bytes. datalen_bytes = data[:4] data = data[4:] - if tflags & Frame.FLAG24_UNSYNCH or id3.f_unsynch: + if tflags & Frame.FLAG24_UNSYNCH or header.f_unsynch: try: data = unsynch.decode(data) except ValueError: @@ -191,7 +255,7 @@ raise ID3JunkFrameError( 'zlib: %s: %r' % (err, data)) - elif id3.version >= id3._V23: + elif header.version >= header._V23: if tflags & Frame.FLAG23_COMPRESS: usize, = unpack('>L', data[:4]) data = data[4:] @@ -204,93 +268,93 @@ raise ID3JunkFrameError('zlib: %s: %r' % (err, data)) frame = cls() - frame._readData(data) + frame._readData(header, data) return frame def __hash__(self): raise TypeError("Frame objects are unhashable") -class FrameOpt(Frame): - """A frame with optional parts. +class CHAP(Frame): + """Chapter""" - Some ID3 frames have optional data; this class extends Frame to - provide support for those parts. - """ + _framespec = [ + Latin1TextSpec("element_id"), + SizedIntegerSpec("start_time", 4, default=0), + SizedIntegerSpec("end_time", 4, default=0), + SizedIntegerSpec("start_offset", 4, default=0xffffffff), + SizedIntegerSpec("end_offset", 4, default=0xffffffff), + ID3FramesSpec("sub_frames"), + ] - _optionalspec = [] + @property + def HashKey(self): + return '%s:%s' % (self.FrameID, self.element_id) - def __init__(self, *args, **kwargs): - super(FrameOpt, self).__init__(*args, **kwargs) - for spec in self._optionalspec: - if spec.name in kwargs: - setattr(self, spec.name, kwargs[spec.name]) - else: - break + def __eq__(self, other): + if not isinstance(other, CHAP): + return False - def __setattr__(self, name, value): - for checker in self._optionalspec: - if checker.name == name: - self.__dict__[name] = checker.validate(self, value) - return - super(FrameOpt, self).__setattr__(name, value) + self_frames = self.sub_frames or {} + other_frames = other.sub_frames or {} + if sorted(self_frames.values()) != sorted(other_frames.values()): + return False - def _to_other(self, other): - super(FrameOpt, self)._to_other(other) + return self.element_id == other.element_id and \ + self.start_time == other.start_time and \ + self.end_time == other.end_time and \ + self.start_offset == other.start_offset and \ + self.end_offset == other.end_offset - # this impl covers subclasses with the same optionalspec - if other._optionalspec is not self._optionalspec: - raise ValueError + __hash__ = Frame.__hash__ - for checker in other._optionalspec: - if hasattr(self, checker.name): - setattr(other, checker.name, getattr(self, checker.name)) + def _pprint(self): + frame_pprint = u"" + for frame in itervalues(self.sub_frames): + for line in frame.pprint().splitlines(): + frame_pprint += "\n" + " " * 4 + line + return u"%s time=%d..%d offset=%d..%d%s" % ( + self.element_id, self.start_time, self.end_time, + self.start_offset, self.end_offset, frame_pprint) - def _readData(self, data): - """Raises ID3JunkFrameError; Returns leftover data""" - for reader in self._framespec: - if len(data): - try: - value, data = reader.read(self, data) - except SpecError as e: - raise ID3JunkFrameError(e) - else: - raise ID3JunkFrameError("no data left") - setattr(self, reader.name, value) +class CTOC(Frame): + """Table of contents""" - if data: - for reader in self._optionalspec: - if len(data): - try: - value, data = reader.read(self, data) - except SpecError as e: - raise ID3JunkFrameError(e) - else: - break - setattr(self, reader.name, value) + _framespec = [ + Latin1TextSpec("element_id"), + CTOCFlagsSpec("flags", default=0), + Latin1TextListSpec("child_element_ids"), + ID3FramesSpec("sub_frames"), + ] - return data + @property + def HashKey(self): + return '%s:%s' % (self.FrameID, self.element_id) - def _writeData(self): - data = [] - for writer in self._framespec: - data.append(writer.write(self, getattr(self, writer.name))) - for writer in self._optionalspec: - try: - data.append(writer.write(self, getattr(self, writer.name))) - except AttributeError: - break - return b''.join(data) + __hash__ = Frame.__hash__ - def __repr__(self): - kw = [] - for attr in self._framespec: - kw.append('%s=%r' % (attr.name, getattr(self, attr.name))) - for attr in self._optionalspec: - if hasattr(self, attr.name): - kw.append('%s=%r' % (attr.name, getattr(self, attr.name))) - return '%s(%s)' % (type(self).__name__, ', '.join(kw)) + def __eq__(self, other): + if not isinstance(other, CTOC): + return False + + self_frames = self.sub_frames or {} + other_frames = other.sub_frames or {} + if sorted(self_frames.values()) != sorted(other_frames.values()): + return False + + return self.element_id == other.element_id and \ + self.flags == other.flags and \ + self.child_element_ids == other.child_element_ids + + def _pprint(self): + frame_pprint = u"" + if getattr(self, "sub_frames", None): + frame_pprint += "\n" + "\n".join( + [" " * 4 + f.pprint() for f in self.sub_frames.values()]) + return u"%s flags=%d child_element_ids=%s%s" % ( + self.element_id, int(self.flags), + u",".join(self.child_element_ids), frame_pprint) @swap_to_string @@ -310,8 +374,8 @@ """ _framespec = [ - EncodingSpec('encoding'), - MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000'), + EncodingSpec('encoding', default=Encoding.UTF16), + MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000', default=[]), ] def __bytes__(self): @@ -359,8 +423,9 @@ """ _framespec = [ - EncodingSpec('encoding'), - MultiSpec('text', EncodedNumericTextSpec('text'), sep=u'\u0000'), + EncodingSpec('encoding', default=Encoding.UTF16), + MultiSpec('text', EncodedNumericTextSpec('text'), sep=u'\u0000', + default=[]), ] def __pos__(self): @@ -379,8 +444,9 @@ """ _framespec = [ - EncodingSpec('encoding'), - MultiSpec('text', EncodedNumericPartTextSpec('text'), sep=u'\u0000'), + EncodingSpec('encoding', default=Encoding.UTF16), + MultiSpec('text', EncodedNumericPartTextSpec('text'), sep=u'\u0000', + default=[]), ] def __pos__(self): @@ -396,8 +462,8 @@ """ _framespec = [ - EncodingSpec('encoding'), - MultiSpec('text', TimeStampSpec('stamp'), sep=u','), + EncodingSpec('encoding', default=Encoding.UTF16), + MultiSpec('text', TimeStampSpec('stamp'), sep=u',', default=[]), ] def __bytes__(self): @@ -423,7 +489,9 @@ ASCII. """ - _framespec = [Latin1TextSpec('url')] + _framespec = [ + Latin1TextSpec('url'), + ] def __bytes__(self): return self.url.encode('utf-8') @@ -550,6 +618,14 @@ "iTunes Podcast Description" +class TKWD(TextFrame): + "iTunes Podcast Keywords" + + +class TCAT(TextFrame): + "iTunes Podcast Category" + + class TDOR(TimeStampTextFrame): "Original Release Time" @@ -741,7 +817,7 @@ _framespec = [ EncodingSpec('encoding'), EncodedTextSpec('desc'), - MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000'), + MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000', default=[]), ] @property @@ -795,7 +871,7 @@ """ _framespec = [ - EncodingSpec('encoding'), + EncodingSpec('encoding', default=Encoding.UTF16), EncodedTextSpec('desc'), Latin1TextSpec('url'), ] @@ -818,10 +894,10 @@ """ _framespec = [ - EncodingSpec('encoding'), + EncodingSpec('encoding', default=Encoding.UTF16), MultiSpec('people', EncodedTextSpec('involvement'), - EncodedTextSpec('person')) + EncodedTextSpec('person'), default=[]) ] def __eq__(self, other): @@ -848,7 +924,9 @@ The 'data' attribute contains the raw byte string. """ - _framespec = [BinaryDataSpec('data')] + _framespec = [ + BinaryDataSpec('data'), + ] def __eq__(self, other): return self.data == other @@ -864,8 +942,8 @@ """Event timing codes.""" _framespec = [ - ByteSpec("format"), - KeyEventSpec("events"), + ByteSpec("format", default=1), + KeyEventSpec("events", default=[]), ] def __eq__(self, other): @@ -882,11 +960,11 @@ """ _framespec = [ - SizedIntegerSpec('frames', 2), - SizedIntegerSpec('bytes', 3), - SizedIntegerSpec('milliseconds', 3), - ByteSpec('bits_for_bytes'), - ByteSpec('bits_for_milliseconds'), + SizedIntegerSpec('frames', size=2, default=0), + SizedIntegerSpec('bytes', size=3, default=0), + SizedIntegerSpec('milliseconds', size=3, default=0), + ByteSpec('bits_for_bytes', default=0), + ByteSpec('bits_for_milliseconds', default=0), BinaryDataSpec('data'), ] @@ -904,8 +982,8 @@ """ _framespec = [ - ByteSpec("format"), - BinaryDataSpec("data"), + ByteSpec("format", default=1), + BinaryDataSpec("data", default=b""), ] def __eq__(self, other): @@ -923,8 +1001,8 @@ """ _framespec = [ - EncodingSpec('encoding'), - StringSpec('lang', 3), + EncodingSpec('encoding', default=Encoding.UTF16), + StringSpec('lang', length=3, default=u"XXX"), EncodedTextSpec('desc'), EncodedTextSpec('text'), ] @@ -951,9 +1029,9 @@ _framespec = [ EncodingSpec('encoding'), - StringSpec('lang', 3), - ByteSpec('format'), - ByteSpec('type'), + StringSpec('lang', length=3, default=u"XXX"), + ByteSpec('format', default=1), + ByteSpec('type', default=0), EncodedTextSpec('desc'), SynchronizedTextSpec('text'), ] @@ -983,9 +1061,9 @@ _framespec = [ EncodingSpec('encoding'), - StringSpec('lang', 3), + StringSpec('lang', length=3, default="XXX"), EncodedTextSpec('desc'), - MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000'), + MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000', default=[]), ] @property @@ -1015,9 +1093,9 @@ _framespec = [ Latin1TextSpec('desc'), - ChannelSpec('channel'), - VolumeAdjustmentSpec('gain'), - VolumePeakSpec('peak'), + ChannelSpec('channel', default=1), + VolumeAdjustmentSpec('gain', default=1), + VolumePeakSpec('peak', default=1), ] _channels = ["Other", "Master volume", "Front right", "Front left", @@ -1055,9 +1133,9 @@ """ _framespec = [ - ByteSpec("method"), + ByteSpec("method", default=0), Latin1TextSpec("desc"), - VolumeAdjustmentsSpec("adjustments"), + VolumeAdjustmentsSpec("adjustments", default=[]), ] def __eq__(self, other): @@ -1070,7 +1148,21 @@ return '%s:%s' % (self.FrameID, self.desc) -# class RVAD: unsupported +class RVAD(Frame): + """Relative volume adjustment""" + + _framespec = [ + RVASpec("adjustments", stereo_only=False), + ] + + __hash__ = Frame.__hash__ + + def __eq__(self, other): + if not isinstance(other, RVAD): + return False + return self.adjustments == other.adjustments + + # class EQUA: unsupported @@ -1078,16 +1170,16 @@ """Reverb.""" _framespec = [ - SizedIntegerSpec('left', 2), - SizedIntegerSpec('right', 2), - ByteSpec('bounce_left'), - ByteSpec('bounce_right'), - ByteSpec('feedback_ltl'), - ByteSpec('feedback_ltr'), - ByteSpec('feedback_rtr'), - ByteSpec('feedback_rtl'), - ByteSpec('premix_ltr'), - ByteSpec('premix_rtl'), + SizedIntegerSpec('left', size=2, default=0), + SizedIntegerSpec('right', size=2, default=0), + ByteSpec('bounce_left', default=0), + ByteSpec('bounce_right', default=0), + ByteSpec('feedback_ltl', default=0), + ByteSpec('feedback_ltr', default=0), + ByteSpec('feedback_rtr', default=0), + ByteSpec('feedback_rtl', default=0), + ByteSpec('premix_ltr', default=0), + ByteSpec('premix_rtl', default=0), ] def __eq__(self, other): @@ -1127,12 +1219,6 @@ def HashKey(self): return '%s:%s' % (self.FrameID, self.desc) - def _validate_from_22(self, other, checker): - if checker.name == "mime": - self.mime = other.mime.decode("ascii", "ignore") - else: - super(APIC, self)._validate_from_22(other, checker) - def _pprint(self): type_desc = text_type(self.type) if hasattr(self.type, "_pprint"): @@ -1151,7 +1237,9 @@ This frame is basically obsoleted by POPM. """ - _framespec = [IntegerSpec('count')] + _framespec = [ + IntegerSpec('count', default=0), + ] def __eq__(self, other): return self.count == other @@ -1165,7 +1253,26 @@ return text_type(self.count) -class POPM(FrameOpt): +class PCST(Frame): + """iTunes Podcast Flag""" + + _framespec = [ + IntegerSpec('value', default=0), + ] + + def __eq__(self, other): + return self.value == other + + __hash__ = Frame.__hash__ + + def __pos__(self): + return self.value + + def _pprint(self): + return text_type(self.value) + + +class POPM(Frame): """Popularimeter. This frame keys a rating (out of 255) and a play count to an email @@ -1180,10 +1287,12 @@ _framespec = [ Latin1TextSpec('email'), - ByteSpec('rating'), + ByteSpec('rating', default=0), ] - _optionalspec = [IntegerSpec('count')] + _optionalspec = [ + IntegerSpec('count', default=0), + ] @property def HashKey(self): @@ -1192,7 +1301,7 @@ def __eq__(self, other): return self.rating == other - __hash__ = FrameOpt.__hash__ + __hash__ = Frame.__hash__ def __pos__(self): return self.rating @@ -1234,7 +1343,7 @@ __hash__ = Frame.__hash__ -class RBUF(FrameOpt): +class RBUF(Frame): """Recommended buffer size. Attributes: @@ -1246,24 +1355,26 @@ Mutagen will not find the next tag itself. """ - _framespec = [SizedIntegerSpec('size', 3)] + _framespec = [ + SizedIntegerSpec('size', size=3, default=0), + ] _optionalspec = [ - ByteSpec('info'), - SizedIntegerSpec('offset', 4), + ByteSpec('info', default=0), + SizedIntegerSpec('offset', size=4, default=0), ] def __eq__(self, other): return self.size == other - __hash__ = FrameOpt.__hash__ + __hash__ = Frame.__hash__ def __pos__(self): return self.size @swap_to_string -class AENC(FrameOpt): +class AENC(Frame): """Audio encryption. Attributes: @@ -1278,11 +1389,13 @@ _framespec = [ Latin1TextSpec('owner'), - SizedIntegerSpec('preview_start', 2), - SizedIntegerSpec('preview_length', 2), + SizedIntegerSpec('preview_start', size=2, default=0), + SizedIntegerSpec('preview_length', size=2, default=0), ] - _optionalspec = [BinaryDataSpec('data')] + _optionalspec = [ + BinaryDataSpec('data'), + ] @property def HashKey(self): @@ -1297,10 +1410,10 @@ def __eq__(self, other): return self.owner == other - __hash__ = FrameOpt.__hash__ + __hash__ = Frame.__hash__ -class LINK(FrameOpt): +class LINK(Frame): """Linked information. Attributes: @@ -1311,7 +1424,7 @@ """ _framespec = [ - StringSpec('frameid', 4), + FrameIDSpec('frameid', length=4), Latin1TextSpec('url'), ] @@ -1331,7 +1444,7 @@ except AttributeError: return (self.frameid, self.url) == other - __hash__ = FrameOpt.__hash__ + __hash__ = Frame.__hash__ class POSS(Frame): @@ -1344,8 +1457,8 @@ """ _framespec = [ - ByteSpec('format'), - IntegerSpec('position'), + ByteSpec('format', default=1), + IntegerSpec('position', default=0), ] def __pos__(self): @@ -1368,7 +1481,7 @@ _framespec = [ Latin1TextSpec('owner'), - BinaryDataSpec('data'), + BinaryDataSpec('data', default=b""), ] @property @@ -1400,7 +1513,7 @@ _framespec = [ EncodingSpec('encoding'), - StringSpec('lang', 3), + StringSpec('lang', length=3, default=u"XXX"), EncodedTextSpec('text'), ] @@ -1430,7 +1543,7 @@ _framespec = [ EncodingSpec('encoding'), Latin1TextSpec('price'), - StringSpec('date', 8), + StringSpec('date', length=8, default=u"19700101"), EncodedTextSpec('seller'), ] @@ -1446,15 +1559,15 @@ __hash__ = Frame.__hash__ -class COMR(FrameOpt): +class COMR(Frame): """Commercial frame.""" _framespec = [ EncodingSpec('encoding'), Latin1TextSpec('price'), - StringSpec('valid_until', 8), + StringSpec('valid_until', length=8, default=u"19700101"), Latin1TextSpec('contact'), - ByteSpec('format'), + ByteSpec('format', default=0), EncodedTextSpec('seller'), EncodedTextSpec('desc'), ] @@ -1471,7 +1584,7 @@ def __eq__(self, other): return self._writeData() == other._writeData() - __hash__ = FrameOpt.__hash__ + __hash__ = Frame.__hash__ @swap_to_string @@ -1484,8 +1597,8 @@ _framespec = [ Latin1TextSpec('owner'), - ByteSpec('method'), - BinaryDataSpec('data'), + ByteSpec('method', default=0x80), + BinaryDataSpec('data', default=b""), ] @property @@ -1502,12 +1615,12 @@ @swap_to_string -class GRID(FrameOpt): +class GRID(Frame): """Group identification registration.""" _framespec = [ Latin1TextSpec('owner'), - ByteSpec('group'), + ByteSpec('group', default=0x80), ] _optionalspec = [BinaryDataSpec('data')] @@ -1528,7 +1641,7 @@ def __eq__(self, other): return self.owner == other or self.group == other - __hash__ = FrameOpt.__hash__ + __hash__ = Frame.__hash__ @swap_to_string @@ -1562,7 +1675,7 @@ """Signature frame.""" _framespec = [ - ByteSpec('group'), + ByteSpec('group', default=0x80), BinaryDataSpec('sig'), ] @@ -1585,7 +1698,9 @@ Mutagen does not find tags at seek offsets. """ - _framespec = [IntegerSpec('offset')] + _framespec = [ + IntegerSpec('offset', default=0), + ] def __pos__(self): return self.offset @@ -1602,12 +1717,13 @@ Attributes: S, L, N, b, and Fi. For the meaning of these, see the ID3v2.4 specification. Fi is a list of integers. """ + _framespec = [ - SizedIntegerSpec("S", 4), - SizedIntegerSpec("L", 4), - SizedIntegerSpec("N", 2), - ByteSpec("b"), - ASPIIndexSpec("Fi"), + SizedIntegerSpec("S", size=4, default=0), + SizedIntegerSpec("L", size=4, default=0), + SizedIntegerSpec("N", size=2, default=0), + ByteSpec("b", default=0), + ASPIIndexSpec("Fi", default=[]), ] def __eq__(self, other): @@ -1725,6 +1841,26 @@ "Encoder" +class TST(TSOT): + "Title Sort Order key" + + +class TSA(TSOA): + "Album Sort Order key" + + +class TS2(TSO2): + "iTunes Album Artist Sort" + + +class TSP(TSOP): + "Perfomer Sort Order key" + + +class TSC(TSOC): + "iTunes Composer Sort" + + class TSS(TSSE): "Encoder settings" @@ -1829,7 +1965,19 @@ "Comment" -# class RVA(RVAD) +class RVA(RVAD): + "Relative volume adjustment" + + _framespec = [ + RVASpec("adjustments", stereo_only=True), + ] + + def _to_other(self, other): + if not isinstance(other, RVAD): + raise TypeError + + other.adjustments = list(self.adjustments) + # class EQU(EQUA) @@ -1846,10 +1994,10 @@ _framespec = [ EncodingSpec('encoding'), - StringSpec('mime', 3), + StringSpec('mime', length=3, default="JPG"), PictureTypeSpec('type'), EncodedTextSpec('desc'), - BinaryDataSpec('data') + BinaryDataSpec('data'), ] def _to_other(self, other): @@ -1881,8 +2029,12 @@ class CRM(Frame): """Encrypted meta frame""" - _framespec = [Latin1TextSpec('owner'), Latin1TextSpec('desc'), - BinaryDataSpec('data')] + + _framespec = [ + Latin1TextSpec('owner'), + Latin1TextSpec('desc'), + BinaryDataSpec('data'), + ] def __eq__(self, other): return self.data == other @@ -1897,23 +2049,29 @@ """Linked information""" _framespec = [ - StringSpec('frameid', 3), + FrameIDSpec('frameid', length=3), Latin1TextSpec('url') ] - _optionalspec = [BinaryDataSpec('data')] + _optionalspec = [ + BinaryDataSpec('data'), + ] def _to_other(self, other): if not isinstance(other, LINK): raise TypeError if isinstance(other, LNK): - other.frameid = self.frameid + new_frameid = self.frameid else: try: - other.frameid = Frames_2_2[self.frameid].__bases__[0].__name__ + new_frameid = Frames_2_2[self.frameid].__bases__[0].__name__ except KeyError: - other.frameid = self.frameid.ljust(4) + new_frameid = self.frameid.ljust(4) + + # we could end up with invalid IDs here, so bypass the validation + other._setattr("frameid", new_frameid) + other.url = self.url if hasattr(self, "data"): other.data = self.data diff -Nru mutagen-1.33.2/mutagen/id3/_id3v1.py mutagen-1.34/mutagen/id3/_id3v1.py --- mutagen-1.33.2/mutagen/id3/_id3v1.py 1970-01-01 00:00:00.000000000 +0000 +++ mutagen-1.34/mutagen/id3/_id3v1.py 2016-07-13 07:06:55.000000000 +0000 @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2005 Michael Urman +# 2006 Lukas Lalinsky +# 2013 Christoph Reiter +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. + +import errno +from struct import error as StructError, unpack + +from mutagen._util import chr_, text_type + +from ._frames import TCON, TRCK, COMM, TDRC, TALB, TPE1, TIT2 + + +def find_id3v1(fileobj): + """Returns a tuple of (id3tag, offset_to_end) or (None, 0) + + offset mainly because we used to write too short tags in some cases and + we need the offset to delete them. + """ + + # id3v1 is always at the end (after apev2) + + extra_read = b"APETAGEX".index(b"TAG") + + try: + fileobj.seek(-128 - extra_read, 2) + except IOError as e: + if e.errno == errno.EINVAL: + # If the file is too small, might be ok since we wrote too small + # tags at some point. let's see how the parsing goes.. + fileobj.seek(0, 0) + else: + raise + + data = fileobj.read(128 + extra_read) + try: + idx = data.index(b"TAG") + except ValueError: + return (None, 0) + else: + # FIXME: make use of the apev2 parser here + # if TAG is part of APETAGEX assume this is an APEv2 tag + try: + ape_idx = data.index(b"APETAGEX") + except ValueError: + pass + else: + if idx == ape_idx + extra_read: + return (None, 0) + + tag = ParseID3v1(data[idx:]) + if tag is None: + return (None, 0) + + offset = idx - len(data) + return (tag, offset) + + +# ID3v1.1 support. +def ParseID3v1(data): + """Parse an ID3v1 tag, returning a list of ID3v2.4 frames. + + Returns a {frame_name: frame} dict or None. + """ + + try: + data = data[data.index(b"TAG"):] + except ValueError: + return None + if 128 < len(data) or len(data) < 124: + return None + + # Issue #69 - Previous versions of Mutagen, when encountering + # out-of-spec TDRC and TYER frames of less than four characters, + # wrote only the characters available - e.g. "1" or "" - into the + # year field. To parse those, reduce the size of the year field. + # Amazingly, "0s" works as a struct format string. + unpack_fmt = "3s30s30s30s%ds29sBB" % (len(data) - 124) + + try: + tag, title, artist, album, year, comment, track, genre = unpack( + unpack_fmt, data) + except StructError: + return None + + if tag != b"TAG": + return None + + def fix(data): + return data.split(b"\x00")[0].strip().decode('latin1') + + title, artist, album, year, comment = map( + fix, [title, artist, album, year, comment]) + + frames = {} + if title: + frames["TIT2"] = TIT2(encoding=0, text=title) + if artist: + frames["TPE1"] = TPE1(encoding=0, text=[artist]) + if album: + frames["TALB"] = TALB(encoding=0, text=album) + if year: + frames["TDRC"] = TDRC(encoding=0, text=year) + if comment: + frames["COMM"] = COMM( + encoding=0, lang="eng", desc="ID3v1 Comment", text=comment) + # Don't read a track number if it looks like the comment was + # padded with spaces instead of nulls (thanks, WinAmp). + if track and ((track != 32) or (data[-3] == b'\x00'[0])): + frames["TRCK"] = TRCK(encoding=0, text=str(track)) + if genre != 255: + frames["TCON"] = TCON(encoding=0, text=str(genre)) + return frames + + +def MakeID3v1(id3): + """Return an ID3v1.1 tag string from a dict of ID3v2.4 frames.""" + + v1 = {} + + for v2id, name in {"TIT2": "title", "TPE1": "artist", + "TALB": "album"}.items(): + if v2id in id3: + text = id3[v2id].text[0].encode('latin1', 'replace')[:30] + else: + text = b"" + v1[name] = text + (b"\x00" * (30 - len(text))) + + if "COMM" in id3: + cmnt = id3["COMM"].text[0].encode('latin1', 'replace')[:28] + else: + cmnt = b"" + v1["comment"] = cmnt + (b"\x00" * (29 - len(cmnt))) + + if "TRCK" in id3: + try: + v1["track"] = chr_(+id3["TRCK"]) + except ValueError: + v1["track"] = b"\x00" + else: + v1["track"] = b"\x00" + + if "TCON" in id3: + try: + genre = id3["TCON"].genres[0] + except IndexError: + pass + else: + if genre in TCON.GENRES: + v1["genre"] = chr_(TCON.GENRES.index(genre)) + if "genre" not in v1: + v1["genre"] = b"\xff" + + if "TDRC" in id3: + year = text_type(id3["TDRC"]).encode('ascii') + elif "TYER" in id3: + year = text_type(id3["TYER"]).encode('ascii') + else: + year = b"" + v1["year"] = (year + b"\x00\x00\x00\x00")[:4] + + return ( + b"TAG" + + v1["title"] + + v1["artist"] + + v1["album"] + + v1["year"] + + v1["comment"] + + v1["track"] + + v1["genre"] + ) diff -Nru mutagen-1.33.2/mutagen/id3/__init__.py mutagen-1.34/mutagen/id3/__init__.py --- mutagen-1.33.2/mutagen/id3/__init__.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/mutagen/id3/__init__.py 2016-07-18 22:15:22.000000000 +0000 @@ -30,1121 +30,36 @@ interested in the :class:`ID3` class to start with. """ -__all__ = ['ID3', 'ID3FileType', 'Frames', 'Open', 'delete'] - -import struct -import errno - -from struct import unpack, pack, error as StructError - -import mutagen -from mutagen._util import insert_bytes, delete_bytes, DictProxy, enum, \ - loadfile, convert_error -from mutagen._tags import PaddingInfo -from .._compat import chr_, PY3 - -from ._util import * -from ._frames import * -from ._specs import * - - -@enum -class ID3v1SaveOptions(object): - - REMOVE = 0 - """ID3v1 tags will be removed""" - - UPDATE = 1 - """ID3v1 tags will be updated but not added""" - - CREATE = 2 - """ID3v1 tags will be created and/or updated""" - - -def _fullread(fileobj, size): - """Read a certain number of bytes from the source file. - - Raises ValueError on invalid size input or EOFError/IOError. - """ - - if size < 0: - raise ValueError('Requested bytes (%s) less than zero' % size) - data = fileobj.read(size) - if len(data) != size: - raise EOFError("Not enough data to read") - return data - - -class ID3Header(object): - - _V24 = (2, 4, 0) - _V23 = (2, 3, 0) - _V22 = (2, 2, 0) - _V11 = (1, 1) - - f_unsynch = property(lambda s: bool(s._flags & 0x80)) - f_extended = property(lambda s: bool(s._flags & 0x40)) - f_experimental = property(lambda s: bool(s._flags & 0x20)) - f_footer = property(lambda s: bool(s._flags & 0x10)) - - @convert_error(IOError, error) - def __init__(self, fileobj=None): - """Raises ID3NoHeaderError, ID3UnsupportedVersionError or error""" - - if fileobj is None: - # for testing - self._flags = 0 - return - - fn = getattr(fileobj, "name", "") - try: - data = _fullread(fileobj, 10) - except EOFError: - raise ID3NoHeaderError("%s: too small" % fn) - - id3, vmaj, vrev, flags, size = unpack('>3sBBB4s', data) - self._flags = flags - self.size = BitPaddedInt(size) + 10 - self.version = (2, vmaj, vrev) - - if id3 != b'ID3': - raise ID3NoHeaderError("%r doesn't start with an ID3 tag" % fn) - - if vmaj not in [2, 3, 4]: - raise ID3UnsupportedVersionError("%r ID3v2.%d not supported" - % (fn, vmaj)) - - if not BitPaddedInt.has_valid_padding(size): - raise error("Header size not synchsafe") - - if (self.version >= self._V24) and (flags & 0x0f): - raise error( - "%r has invalid flags %#02x" % (fn, flags)) - elif (self._V23 <= self.version < self._V24) and (flags & 0x1f): - raise error( - "%r has invalid flags %#02x" % (fn, flags)) - - if self.f_extended: - try: - extsize_data = _fullread(fileobj, 4) - except EOFError: - raise error("%s: too small" % fn) - - if PY3: - frame_id = extsize_data.decode("ascii", "replace") - else: - frame_id = extsize_data - - if frame_id in Frames: - # Some tagger sets the extended header flag but - # doesn't write an extended header; in this case, the - # ID3 data follows immediately. Since no extended - # header is going to be long enough to actually match - # a frame, and if it's *not* a frame we're going to be - # completely lost anyway, this seems to be the most - # correct check. - # https://github.com/quodlibet/quodlibet/issues/126 - self._flags ^= 0x40 - extsize = 0 - fileobj.seek(-4, 1) - elif self.version >= self._V24: - # "Where the 'Extended header size' is the size of the whole - # extended header, stored as a 32 bit synchsafe integer." - extsize = BitPaddedInt(extsize_data) - 4 - if not BitPaddedInt.has_valid_padding(extsize_data): - raise error( - "Extended header size not synchsafe") - else: - # "Where the 'Extended header size', currently 6 or 10 bytes, - # excludes itself." - extsize = unpack('>L', extsize_data)[0] - - try: - self._extdata = _fullread(fileobj, extsize) - except EOFError: - raise error("%s: too small" % fn) - - -class ID3(DictProxy, mutagen.Metadata): - """ID3(filething=None) - - A file with an ID3v2 tag. - - If any arguments are given, the :meth:`load` is called with them. If no - arguments are given then an empty `ID3` object is created. - - :: - - ID3("foo.mp3") - # same as - t = ID3() - t.load("foo.mp3") - - Arguments: - filething (filething): or `None` - - Attributes: - version (Tuple[int]): ID3 tag version as a tuple - unknown_frames (List[bytes]): raw frame data of any unknown frames - found - size (int): the total size of the ID3 tag, including the header - """ - - __module__ = "mutagen.id3" - - PEDANTIC = True - """`bool`: - - .. deprecated:: 1.28 - - Doesn't have any effect - """ - - filename = None - - def __init__(self, *args, **kwargs): - self.unknown_frames = [] - self.__unknown_version = None - self._header = None - self._version = (2, 4, 0) - super(ID3, self).__init__(*args, **kwargs) - - @property - def version(self): - """`tuple`: ID3 tag version as a tuple (of the loaded file)""" - - if self._header is not None: - return self._header.version - return self._version - - @version.setter - def version(self, value): - self._version = value - - @property - def f_unsynch(self): - if self._header is not None: - return self._header.f_unsynch - return False - - @property - def f_extended(self): - if self._header is not None: - return self._header.f_extended - return False - - @property - def size(self): - if self._header is not None: - return self._header.size - return 0 - - def _pre_load_header(self, fileobj): - # XXX: for aiff to adjust the offset.. - pass - - @convert_error(IOError, error) - @loadfile() - def load(self, filething, known_frames=None, translate=True, v2_version=4): - """load(filething, known_frames=None, translate=True, v2_version=4) - - Load tags from a filename. - - Args: - filename (filething): filename or file object to load tag data from - known_frames (Dict[`mutagen.text`, `Frame`]): dict mapping frame - IDs to Frame objects - translate (bool): Update all tags to ID3v2.3/4 internally. If you - intend to save, this must be true or you have to - call update_to_v23() / update_to_v24() manually. - v2_version (int): if update_to_v23 or update_to_v24 get called - (3 or 4) - - Example of loading a custom frame:: - - my_frames = dict(mutagen.id3.Frames) - class XMYF(Frame): ... - my_frames["XMYF"] = XMYF - mutagen.id3.ID3(filename, known_frames=my_frames) - """ - - fileobj = filething.fileobj - - if v2_version not in (3, 4): - raise ValueError("Only 3 and 4 possible for v2_version") - - self.unknown_frames = [] - self.__known_frames = known_frames - self._header = None - self._padding = 0 # for testing - - self._pre_load_header(fileobj) - - try: - self._header = ID3Header(fileobj) - except (ID3NoHeaderError, ID3UnsupportedVersionError): - frames, offset = _find_id3v1(fileobj) - if frames is None: - raise - - self.version = ID3Header._V11 - for v in frames.values(): - self.add(v) - else: - frames = self.__known_frames - if frames is None: - if self.version >= ID3Header._V23: - frames = Frames - elif self.version >= ID3Header._V22: - frames = Frames_2_2 - - try: - data = _fullread(fileobj, self.size - 10) - except (ValueError, EOFError, IOError) as e: - raise error(e) - - for frame in self.__read_frames(data, frames=frames): - if isinstance(frame, Frame): - self.add(frame) - else: - self.unknown_frames.append(frame) - self.__unknown_version = self.version[:2] - - if translate: - if v2_version == 3: - self.update_to_v23() - else: - self.update_to_v24() - - def getall(self, key): - """Return all frames with a given name (the list may be empty). - - Args: - key (text): key for frames to get - - This is best explained by examples:: - - id3.getall('TIT2') == [id3['TIT2']] - id3.getall('TTTT') == [] - id3.getall('TXXX') == [TXXX(desc='woo', text='bar'), - TXXX(desc='baz', text='quuuux'), ...] - - Since this is based on the frame's HashKey, which is - colon-separated, you can use it to do things like - ``getall('COMM:MusicMatch')`` or ``getall('TXXX:QuodLibet:')``. - """ - if key in self: - return [self[key]] - else: - key = key + ":" - return [v for s, v in self.items() if s.startswith(key)] - - def delall(self, key): - """Delete all tags of a given kind; see getall. - - Args: - key (text): key for frames to delete - """ - - if key in self: - del(self[key]) - else: - key = key + ":" - for k in list(self.keys()): - if k.startswith(key): - del(self[k]) - - def setall(self, key, values): - """Delete frames of the given type and add frames in 'values'. - - Args: - key (text): key for frames to delete - values (List[`Frame`]): frames to add - """ - - self.delall(key) - for tag in values: - self[tag.HashKey] = tag - - def pprint(self): - """ - Returns: - text: tags in a human-readable format. - - "Human-readable" is used loosely here. The format is intended - to mirror that used for Vorbis or APEv2 output, e.g. - - ``TIT2=My Title`` - - However, ID3 frames can have multiple keys: - - ``POPM=user@example.org=3 128/255`` - """ - - frames = sorted(Frame.pprint(s) for s in self.values()) - return "\n".join(frames) - - def loaded_frame(self, tag): - """Deprecated; use the add method.""" - # turn 2.2 into 2.3/2.4 tags - if len(type(tag).__name__) == 3: - tag = type(tag).__base__(tag) - self[tag.HashKey] = tag - - # add = loaded_frame (and vice versa) break applications that - # expect to be able to override loaded_frame (e.g. Quod Libet), - # as does making loaded_frame call add. - def add(self, frame): - """Add a frame to the tag.""" - return self.loaded_frame(frame) - - def __setitem__(self, key, tag): - if not isinstance(tag, Frame): - raise TypeError("%r not a Frame instance" % tag) - super(ID3, self).__setitem__(key, tag) - - def __read_frames(self, data, frames): - assert self.version >= ID3Header._V22 - - if self.version < ID3Header._V24 and self.f_unsynch: - try: - data = unsynch.decode(data) - except ValueError: - pass - - if self.version >= ID3Header._V23: - if self.version < ID3Header._V24: - bpi = int - else: - bpi = _determine_bpi(data, frames) - - while data: - header = data[:10] - try: - name, size, flags = unpack('>4sLH', header) - except struct.error: - return # not enough header - if name.strip(b'\x00') == b'': - return - - size = bpi(size) - framedata = data[10:10 + size] - data = data[10 + size:] - self._padding = len(data) - if size == 0: - continue # drop empty frames - - if PY3: - try: - name = name.decode('ascii') - except UnicodeDecodeError: - continue - - try: - # someone writes 2.3 frames with 2.2 names - if name[-1] == "\x00": - tag = Frames_2_2[name[:-1]] - name = tag.__base__.__name__ - - tag = frames[name] - except KeyError: - if is_valid_frame_id(name): - yield header + framedata - else: - try: - yield tag._fromData(self._header, flags, framedata) - except NotImplementedError: - yield header + framedata - except ID3JunkFrameError: - pass - elif self.version >= ID3Header._V22: - while data: - header = data[0:6] - try: - name, size = unpack('>3s3s', header) - except struct.error: - return # not enough header - size, = struct.unpack('>L', b'\x00' + size) - if name.strip(b'\x00') == b'': - return - - framedata = data[6:6 + size] - data = data[6 + size:] - self._padding = len(data) - if size == 0: - continue # drop empty frames - - if PY3: - try: - name = name.decode('ascii') - except UnicodeDecodeError: - continue - - try: - tag = frames[name] - except KeyError: - if is_valid_frame_id(name): - yield header + framedata - else: - try: - yield tag._fromData(self._header, 0, framedata) - except (ID3EncryptionUnsupportedError, - NotImplementedError): - yield header + framedata - except ID3JunkFrameError: - pass - - def _prepare_data(self, fileobj, start, available, v2_version, v23_sep, - pad_func): - if v2_version == 3: - version = ID3Header._V23 - elif v2_version == 4: - version = ID3Header._V24 - else: - raise ValueError("Only 3 or 4 allowed for v2_version") - - # Sort frames by 'importance' - order = ["TIT2", "TPE1", "TRCK", "TALB", "TPOS", "TDRC", "TCON"] - order = dict((b, a) for a, b in enumerate(order)) - last = len(order) - frames = sorted(self.items(), - key=lambda a: (order.get(a[0][:4], last), a[0])) - - framedata = [self.__save_frame(frame, version=version, v23_sep=v23_sep) - for (key, frame) in frames] - - # only write unknown frames if they were loaded from the version - # we are saving with or upgraded to it - if self.__unknown_version == version[:2]: - framedata.extend(data for data in self.unknown_frames - if len(data) > 10) - - needed = sum(map(len, framedata)) + 10 - - fileobj.seek(0, 2) - trailing_size = fileobj.tell() - start - - info = PaddingInfo(available - needed, trailing_size) - new_padding = info._get_padding(pad_func) - if new_padding < 0: - raise error("invalid padding") - new_size = needed + new_padding - - new_framesize = BitPaddedInt.to_str(new_size - 10, width=4) - header = pack('>3sBBB4s', b'ID3', v2_version, 0, 0, new_framesize) - - data = bytearray(header) - for frame in framedata: - data += frame - assert new_size >= len(data) - data += (new_size - len(data)) * b'\x00' - assert new_size == len(data) - - return data - - @convert_error(IOError, error) - @loadfile(writable=True, create=True) - def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None): - """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None) - - Save changes to a file. - - Args: - filename (fspath): - Filename to save the tag to. If no filename is given, - the one most recently loaded is used. - v1 (ID3v1SaveOptions): - if 0, ID3v1 tags will be removed. - if 1, ID3v1 tags will be updated but not added. - if 2, ID3v1 tags will be created and/or updated - v2 (int): - version of ID3v2 tags (3 or 4). - v23_sep (text): - the separator used to join multiple text values - if v2_version == 3. Defaults to '/' but if it's None - will be the ID3v2v2.4 null separator. - padding (PaddingFunction) - - Raises: - mutagen.MutagenError - - By default Mutagen saves ID3v2.4 tags. If you want to save ID3v2.3 - tags, you must call method update_to_v23 before saving the file. - - The lack of a way to update only an ID3v1 tag is intentional. - """ - - f = filething.fileobj - - try: - header = ID3Header(filething.fileobj) - except ID3NoHeaderError: - old_size = 0 - else: - old_size = header.size - - data = self._prepare_data( - f, 0, old_size, v2_version, v23_sep, padding) - new_size = len(data) - - if (old_size < new_size): - insert_bytes(f, new_size - old_size, old_size) - elif (old_size > new_size): - delete_bytes(f, old_size - new_size, new_size) - f.seek(0) - f.write(data) - - self.__save_v1(f, v1) - - def __save_v1(self, f, v1): - tag, offset = _find_id3v1(f) - has_v1 = tag is not None - - f.seek(offset, 2) - if v1 == ID3v1SaveOptions.UPDATE and has_v1 or \ - v1 == ID3v1SaveOptions.CREATE: - f.write(MakeID3v1(self)) - else: - f.truncate() - - @loadfile(writable=True) - def delete(self, filething, delete_v1=True, delete_v2=True): - """delete(filething=None, delete_v1=True, delete_v2=True) - - Remove tags from a file. - - Args: - filething (filething): A filename or `None` to use the one used - when loading. - delete_v1 (bool): delete any ID3v1 tag - delete_v2 (bool): delete any ID3v2 tag - - If no filename is given, the one most recently loaded is used. - """ - - delete(filething, delete_v1, delete_v2) - self.clear() - - def __save_frame(self, frame, name=None, version=ID3Header._V24, - v23_sep=None): - flags = 0 - if isinstance(frame, TextFrame): - if len(str(frame)) == 0: - return b'' - - if version == ID3Header._V23: - framev23 = frame._get_v23_frame(sep=v23_sep) - framedata = framev23._writeData() - else: - framedata = frame._writeData() - - usize = len(framedata) - if usize > 2048: - # Disabled as this causes iTunes and other programs - # to fail to find these frames, which usually includes - # e.g. APIC. - # framedata = BitPaddedInt.to_str(usize) + framedata.encode('zlib') - # flags |= Frame.FLAG24_COMPRESS | Frame.FLAG24_DATALEN - pass - - if version == ID3Header._V24: - bits = 7 - elif version == ID3Header._V23: - bits = 8 - else: - raise ValueError - - datasize = BitPaddedInt.to_str(len(framedata), width=4, bits=bits) - - if name is not None: - assert isinstance(name, bytes) - frame_name = name - else: - frame_name = type(frame).__name__ - if PY3: - frame_name = frame_name.encode("ascii") - - header = pack('>4s4sH', frame_name, datasize, flags) - return header + framedata - - def __update_common(self): - """Updates done by both v23 and v24 update""" - - if "TCON" in self: - # Get rid of "(xx)Foobr" format. - self["TCON"].genres = self["TCON"].genres - - mimes = {"PNG": "image/png", "JPG": "image/jpeg"} - for pic in self.getall("APIC"): - if pic.mime in mimes: - newpic = APIC( - encoding=pic.encoding, mime=mimes[pic.mime], - type=pic.type, desc=pic.desc, data=pic.data) - self.add(newpic) - - def update_to_v24(self): - """Convert older tags into an ID3v2.4 tag. - - This updates old ID3v2 frames to ID3v2.4 ones (e.g. TYER to - TDRC). If you intend to save tags, you must call this function - at some point; it is called by default when loading the tag. - """ - - self.__update_common() - - if self.__unknown_version == (2, 3): - # convert unknown 2.3 frames (flags/size) to 2.4 - converted = [] - for frame in self.unknown_frames: - try: - name, size, flags = unpack('>4sLH', frame[:10]) - except struct.error: - continue - - try: - frame = BinaryFrame._fromData( - self._header, flags, frame[10:]) - except (error, NotImplementedError): - continue - - converted.append(self.__save_frame(frame, name=name)) - self.unknown_frames[:] = converted - self.__unknown_version = (2, 4) - - # TDAT, TYER, and TIME have been turned into TDRC. - try: - date = text_type(self.get("TYER", "")) - if date.strip(u"\x00"): - self.pop("TYER") - dat = text_type(self.get("TDAT", "")) - if dat.strip("\x00"): - self.pop("TDAT") - date = "%s-%s-%s" % (date, dat[2:], dat[:2]) - time = text_type(self.get("TIME", "")) - if time.strip("\x00"): - self.pop("TIME") - date += "T%s:%s:00" % (time[:2], time[2:]) - if "TDRC" not in self: - self.add(TDRC(encoding=0, text=date)) - except UnicodeDecodeError: - # Old ID3 tags have *lots* of Unicode problems, so if TYER - # is bad, just chuck the frames. - pass - - # TORY can be the first part of a TDOR. - if "TORY" in self: - f = self.pop("TORY") - if "TDOR" not in self: - try: - self.add(TDOR(encoding=0, text=str(f))) - except UnicodeDecodeError: - pass - - # IPLS is now TIPL. - if "IPLS" in self: - f = self.pop("IPLS") - if "TIPL" not in self: - self.add(TIPL(encoding=f.encoding, people=f.people)) - - # These can't be trivially translated to any ID3v2.4 tags, or - # should have been removed already. - for key in ["RVAD", "EQUA", "TRDA", "TSIZ", "TDAT", "TIME", "CRM"]: - if key in self: - del(self[key]) - - def update_to_v23(self): - """Convert older (and newer) tags into an ID3v2.3 tag. - - This updates incompatible ID3v2 frames to ID3v2.3 ones. If you - intend to save tags as ID3v2.3, you must call this function - at some point. - - If you want to to go off spec and include some v2.4 frames - in v2.3, remove them before calling this and add them back afterwards. - """ - - self.__update_common() - - # we could downgrade unknown v2.4 frames here, but given that - # the main reason to save v2.3 is compatibility and this - # might increase the chance of some parser breaking.. better not - - # TMCL, TIPL -> TIPL - if "TIPL" in self or "TMCL" in self: - people = [] - if "TIPL" in self: - f = self.pop("TIPL") - people.extend(f.people) - if "TMCL" in self: - f = self.pop("TMCL") - people.extend(f.people) - if "IPLS" not in self: - self.add(IPLS(encoding=f.encoding, people=people)) - - # TDOR -> TORY - if "TDOR" in self: - f = self.pop("TDOR") - if f.text: - d = f.text[0] - if d.year and "TORY" not in self: - self.add(TORY(encoding=f.encoding, text="%04d" % d.year)) - - # TDRC -> TYER, TDAT, TIME - if "TDRC" in self: - f = self.pop("TDRC") - if f.text: - d = f.text[0] - if d.year and "TYER" not in self: - self.add(TYER(encoding=f.encoding, text="%04d" % d.year)) - if d.month and d.day and "TDAT" not in self: - self.add(TDAT(encoding=f.encoding, - text="%02d%02d" % (d.day, d.month))) - if d.hour and d.minute and "TIME" not in self: - self.add(TIME(encoding=f.encoding, - text="%02d%02d" % (d.hour, d.minute))) - - # New frames added in v2.4 - v24_frames = [ - 'ASPI', 'EQU2', 'RVA2', 'SEEK', 'SIGN', 'TDEN', 'TDOR', - 'TDRC', 'TDRL', 'TDTG', 'TIPL', 'TMCL', 'TMOO', 'TPRO', - 'TSOA', 'TSOP', 'TSOT', 'TSST', - ] - - for key in v24_frames: - if key in self: - del(self[key]) - - -@convert_error(IOError, error) -@loadfile(method=False, writable=True) -def delete(filething, delete_v1=True, delete_v2=True): - """Remove tags from a file. - - Args: - delete_v1 (bool): delete any ID3v1 tag - delete_v2 (bool): delete any ID3v2 tag - - Raises: - mutagen.MutagenError: In case deleting failed - """ - - f = filething.fileobj - - if delete_v1: - tag, offset = _find_id3v1(f) - if tag is not None: - f.seek(offset, 2) - f.truncate() - - # technically an insize=0 tag is invalid, but we delete it anyway - # (primarily because we used to write it) - if delete_v2: - f.seek(0, 0) - idata = f.read(10) - try: - id3, vmaj, vrev, flags, insize = unpack('>3sBBB4s', idata) - except struct.error: - pass - else: - insize = BitPaddedInt(insize) - if id3 == b'ID3' and insize >= 0: - delete_bytes(f, insize + 10, 0) - +from ._file import ID3, ID3FileType, delete, ID3v1SaveOptions +from ._specs import Encoding, PictureType, CTOCFlags, ID3TimeStamp +from ._frames import Frames, Frames_2_2, Frame, TextFrame, UrlFrame, \ + UrlFrameU, TimeStampTextFrame, BinaryFrame, NumericPartTextFrame, \ + NumericTextFrame, PairedTextFrame +from ._util import ID3NoHeaderError, error, ID3UnsupportedVersionError +from ._id3v1 import ParseID3v1, MakeID3v1 +from ._tags import ID3Tags + +# deprecated +from ._util import ID3EncryptionUnsupportedError, ID3JunkFrameError, \ + ID3BadUnsynchData, ID3BadCompressedData, ID3TagError, ID3Warning + + +for f in Frames: + globals()[f] = Frames[f] +for f in Frames_2_2: + globals()[f] = Frames_2_2[f] # support open(filename) as interface Open = ID3 +# pyflakes +ID3, ID3FileType, delete, ID3v1SaveOptions, Encoding, PictureType, CTOCFlags, +ID3TimeStamp, Frames, Frames_2_2, Frame, TextFrame, UrlFrame, UrlFrameU, +TimeStampTextFrame, BinaryFrame, NumericPartTextFrame, NumericTextFrame, +PairedTextFrame, ID3NoHeaderError, error, ID3UnsupportedVersionError, +ParseID3v1, MakeID3v1, ID3Tags, ID3EncryptionUnsupportedError, +ID3JunkFrameError, ID3BadUnsynchData, ID3BadCompressedData, ID3TagError, +ID3Warning -def _determine_bpi(data, frames, EMPTY=b"\x00" * 10): - """Takes id3v2.4 frame data and determines if ints or bitpaddedints - should be used for parsing. Needed because iTunes used to write - normal ints for frame sizes. - """ - # count number of tags found as BitPaddedInt and how far past - o = 0 - asbpi = 0 - while o < len(data) - 10: - part = data[o:o + 10] - if part == EMPTY: - bpioff = -((len(data) - o) % 10) - break - name, size, flags = unpack('>4sLH', part) - size = BitPaddedInt(size) - o += 10 + size - if PY3: - try: - name = name.decode("ascii") - except UnicodeDecodeError: - continue - if name in frames: - asbpi += 1 - else: - bpioff = o - len(data) - - # count number of tags found as int and how far past - o = 0 - asint = 0 - while o < len(data) - 10: - part = data[o:o + 10] - if part == EMPTY: - intoff = -((len(data) - o) % 10) - break - name, size, flags = unpack('>4sLH', part) - o += 10 + size - if PY3: - try: - name = name.decode("ascii") - except UnicodeDecodeError: - continue - if name in frames: - asint += 1 - else: - intoff = o - len(data) - - # if more tags as int, or equal and bpi is past and int is not - if asint > asbpi or (asint == asbpi and (bpioff >= 1 and intoff <= 1)): - return int - return BitPaddedInt - - -def _find_id3v1(fileobj): - """Returns a tuple of (id3tag, offset_to_end) or (None, 0) - - offset mainly because we used to write too short tags in some cases and - we need the offset to delete them. - """ - - # id3v1 is always at the end (after apev2) - - extra_read = b"APETAGEX".index(b"TAG") - - try: - fileobj.seek(-128 - extra_read, 2) - except IOError as e: - if e.errno == errno.EINVAL: - # If the file is too small, might be ok since we wrote too small - # tags at some point. let's see how the parsing goes.. - fileobj.seek(0, 0) - else: - raise - - data = fileobj.read(128 + extra_read) - try: - idx = data.index(b"TAG") - except ValueError: - return (None, 0) - else: - # FIXME: make use of the apev2 parser here - # if TAG is part of APETAGEX assume this is an APEv2 tag - try: - ape_idx = data.index(b"APETAGEX") - except ValueError: - pass - else: - if idx == ape_idx + extra_read: - return (None, 0) - - tag = ParseID3v1(data[idx:]) - if tag is None: - return (None, 0) - - offset = idx - len(data) - return (tag, offset) - - -# ID3v1.1 support. -def ParseID3v1(data): - """Parse an ID3v1 tag, returning a list of ID3v2.4 frames. - - Returns a {frame_name: frame} dict or None. - """ - - try: - data = data[data.index(b"TAG"):] - except ValueError: - return None - if 128 < len(data) or len(data) < 124: - return None - - # Issue #69 - Previous versions of Mutagen, when encountering - # out-of-spec TDRC and TYER frames of less than four characters, - # wrote only the characters available - e.g. "1" or "" - into the - # year field. To parse those, reduce the size of the year field. - # Amazingly, "0s" works as a struct format string. - unpack_fmt = "3s30s30s30s%ds29sBB" % (len(data) - 124) - - try: - tag, title, artist, album, year, comment, track, genre = unpack( - unpack_fmt, data) - except StructError: - return None - - if tag != b"TAG": - return None - - def fix(data): - return data.split(b"\x00")[0].strip().decode('latin1') - - title, artist, album, year, comment = map( - fix, [title, artist, album, year, comment]) - - frames = {} - if title: - frames["TIT2"] = TIT2(encoding=0, text=title) - if artist: - frames["TPE1"] = TPE1(encoding=0, text=[artist]) - if album: - frames["TALB"] = TALB(encoding=0, text=album) - if year: - frames["TDRC"] = TDRC(encoding=0, text=year) - if comment: - frames["COMM"] = COMM( - encoding=0, lang="eng", desc="ID3v1 Comment", text=comment) - # Don't read a track number if it looks like the comment was - # padded with spaces instead of nulls (thanks, WinAmp). - if track and ((track != 32) or (data[-3] == b'\x00'[0])): - frames["TRCK"] = TRCK(encoding=0, text=str(track)) - if genre != 255: - frames["TCON"] = TCON(encoding=0, text=str(genre)) - return frames - - -def MakeID3v1(id3): - """Return an ID3v1.1 tag string from a dict of ID3v2.4 frames.""" - - v1 = {} - - for v2id, name in {"TIT2": "title", "TPE1": "artist", - "TALB": "album"}.items(): - if v2id in id3: - text = id3[v2id].text[0].encode('latin1', 'replace')[:30] - else: - text = b"" - v1[name] = text + (b"\x00" * (30 - len(text))) - - if "COMM" in id3: - cmnt = id3["COMM"].text[0].encode('latin1', 'replace')[:28] - else: - cmnt = b"" - v1["comment"] = cmnt + (b"\x00" * (29 - len(cmnt))) - - if "TRCK" in id3: - try: - v1["track"] = chr_(+id3["TRCK"]) - except ValueError: - v1["track"] = b"\x00" - else: - v1["track"] = b"\x00" - - if "TCON" in id3: - try: - genre = id3["TCON"].genres[0] - except IndexError: - pass - else: - if genre in TCON.GENRES: - v1["genre"] = chr_(TCON.GENRES.index(genre)) - if "genre" not in v1: - v1["genre"] = b"\xff" - - if "TDRC" in id3: - year = text_type(id3["TDRC"]).encode('ascii') - elif "TYER" in id3: - year = text_type(id3["TYER"]).encode('ascii') - else: - year = b"" - v1["year"] = (year + b"\x00\x00\x00\x00")[:4] - - return ( - b"TAG" + - v1["title"] + - v1["artist"] + - v1["album"] + - v1["year"] + - v1["comment"] + - v1["track"] + - v1["genre"] - ) - - -class ID3FileType(mutagen.FileType): - """ID3FileType(filething, ID3=None, **kwargs) - - An unknown type of file with ID3 tags. - - Args: - filething (filething): A filename or file-like object - ID3 (ID3): An ID3 subclass to use for tags. - - Raises: - mutagen.MutagenError: In case loading the file failed - - Load stream and tag information from a file. - - A custom tag reader may be used in instead of the default - mutagen.id3.ID3 object, e.g. an EasyID3 reader. - """ - - ID3 = ID3 - - class _Info(mutagen.StreamInfo): - length = 0 - - def __init__(self, fileobj, offset): - pass - - @staticmethod - def pprint(): - return u"Unknown format with ID3 tag" - - @staticmethod - def score(filename, fileobj, header_data): - return header_data.startswith(b"ID3") - - def add_tags(self, ID3=None): - """Add an empty ID3 tag to the file. - - Args: - ID3 (ID3): An ID3 subclass to use or `None` to use the one - that used when loading. - - A custom tag reader may be used in instead of the default - `ID3` object, e.g. an `mutagen.easyid3.EasyID3` reader. - """ - - if ID3 is None: - ID3 = self.ID3 - if self.tags is None: - self.ID3 = ID3 - self.tags = ID3() - else: - raise error("an ID3 tag already exists") - - @loadfile() - def load(self, filething, ID3=None, **kwargs): - # see __init__ for docs - - fileobj = filething.fileobj - - if ID3 is None: - ID3 = self.ID3 - else: - # If this was initialized with EasyID3, remember that for - # when tags are auto-instantiated in add_tags. - self.ID3 = ID3 - - try: - self.tags = ID3(fileobj, **kwargs) - except ID3NoHeaderError: - self.tags = None - - if self.tags is not None: - try: - offset = self.tags.size - except AttributeError: - offset = None - else: - offset = None - - self.info = self._Info(fileobj, offset) +__all__ = ['ID3', 'ID3FileType', 'Frames', 'Open', 'delete'] diff -Nru mutagen-1.33.2/mutagen/id3/_specs.py mutagen-1.34/mutagen/id3/_specs.py --- mutagen-1.33.2/mutagen/id3/_specs.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/mutagen/id3/_specs.py 2016-07-14 10:23:04.000000000 +0000 @@ -11,8 +11,8 @@ from .._compat import text_type, chr_, PY3, swap_to_string, string_types, \ xrange -from .._util import total_ordering, decode_terminated, enum, izip -from ._util import BitPaddedInt +from .._util import total_ordering, decode_terminated, enum, izip, flags, cdata +from ._util import BitPaddedInt, is_valid_frame_id @enum @@ -88,14 +88,30 @@ return text_type(self).split(".", 1)[-1].lower().replace("_", " ") +@flags +class CTOCFlags(object): + + TOP_LEVEL = 0x2 + """Identifies the CTOC root frame""" + + ORDERED = 0x1 + """Child elements are ordered""" + + class SpecError(Exception): pass class Spec(object): - def __init__(self, name): + handle_nodata = False + """If reading empty data is possible and writing it back will again + result in no data. + """ + + def __init__(self, name, default): self.name = name + self.default = default def __hash__(self): raise TypeError("Spec objects are unhashable") @@ -107,25 +123,46 @@ return value - def read(self, frame, data): - """Returns the (value, left_data) or raises SpecError""" + def read(self, header, frame, data): + """ + Returns: + (value: object, left_data: bytes) + Raises: + SpecError + """ raise NotImplementedError - def write(self, frame, value): + def write(self, config, frame, value): + """ + Returns: + bytes: The serialized data + Raises: + SpecError + """ raise NotImplementedError def validate(self, frame, value): - """Returns the validated data or raises ValueError/TypeError""" + """ + Returns: + the validated value + Raises: + ValueError + TypeError + """ raise NotImplementedError class ByteSpec(Spec): - def read(self, frame, data): + + def __init__(self, name, default=0): + super(ByteSpec, self).__init__(name, default) + + def read(self, header, frame, data): return bytearray(data)[0], data[1:] - def write(self, frame, value): + def write(self, config, frame, value): return chr_(value) def validate(self, frame, value): @@ -136,8 +173,11 @@ class PictureTypeSpec(ByteSpec): - def read(self, frame, data): - value, data = ByteSpec.read(self, frame, data) + def __init__(self, name, default=PictureType.COVER_FRONT): + super(PictureTypeSpec, self).__init__(name, default) + + def read(self, header, frame, data): + value, data = ByteSpec.read(self, header, frame, data) return PictureType(value), data def validate(self, frame, value): @@ -147,11 +187,24 @@ return value +class CTOCFlagsSpec(ByteSpec): + + def read(self, header, frame, data): + value, data = ByteSpec.read(self, header, frame, data) + return CTOCFlags(value), data + + def validate(self, frame, value): + value = ByteSpec.validate(self, frame, value) + if value is not None: + return CTOCFlags(value) + return value + + class IntegerSpec(Spec): - def read(self, frame, data): + def read(self, header, frame, data): return int(BitPaddedInt(data, bits=8)), b'' - def write(self, frame, value): + def write(self, config, frame, value): return BitPaddedInt.to_str(value, bits=8, width=-1) def validate(self, frame, value): @@ -159,13 +212,15 @@ class SizedIntegerSpec(Spec): - def __init__(self, name, size): + + def __init__(self, name, size, default): self.name, self.__sz = name, size + self.default = default - def read(self, frame, data): + def read(self, header, frame, data): return int(BitPaddedInt(data[:self.__sz], bits=8)), data[self.__sz:] - def write(self, frame, value): + def write(self, config, frame, value): return BitPaddedInt.to_str(value, bits=8, width=self.__sz) def validate(self, frame, value): @@ -191,8 +246,11 @@ class EncodingSpec(ByteSpec): - def read(self, frame, data): - enc, data = super(EncodingSpec, self).read(frame, data) + def __init__(self, name, default=Encoding.UTF16): + super(EncodingSpec, self).__init__(name, default) + + def read(self, header, frame, data): + enc, data = super(EncodingSpec, self).read(header, frame, data) if enc not in (Encoding.LATIN1, Encoding.UTF16, Encoding.UTF16BE, Encoding.UTF8): raise SpecError('Invalid Encoding: %r' % enc) @@ -200,7 +258,7 @@ def validate(self, frame, value): if value is None: - return None + raise TypeError if value not in (Encoding.LATIN1, Encoding.UTF16, Encoding.UTF16BE, Encoding.UTF8): raise ValueError('Invalid Encoding: %r' % value) @@ -216,11 +274,13 @@ class StringSpec(Spec): """A fixed size ASCII only payload.""" - def __init__(self, name, length): - super(StringSpec, self).__init__(name) + def __init__(self, name, length, default=None): + if default is None: + default = u" " * length + super(StringSpec, self).__init__(name, default) self.len = length - def read(s, frame, data): + def read(s, header, frame, data): chunk = data[:s.len] try: ascii = chunk.decode("ascii") @@ -232,39 +292,132 @@ return chunk, data[s.len:] - def write(s, frame, value): - if value is None: - return b'\x00' * s.len - else: - if PY3: - value = value.encode("ascii") - return (bytes(value) + b'\x00' * s.len)[:s.len] + def write(self, config, frame, value): + if PY3: + value = value.encode("ascii") + return (bytes(value) + b'\x00' * self.len)[:self.len] - def validate(s, frame, value): + def validate(self, frame, value): if value is None: - return None - + raise TypeError if PY3: if not isinstance(value, str): - raise TypeError("%s has to be str" % s.name) + raise TypeError("%s has to be str" % self.name) value.encode("ascii") else: if not isinstance(value, bytes): value = value.encode("ascii") - if len(value) == s.len: + if len(value) == self.len: return value - raise ValueError('Invalid StringSpec[%d] data: %r' % (s.len, value)) + raise ValueError('Invalid StringSpec[%d] data: %r' % (self.len, value)) + + +class RVASpec(Spec): + + def __init__(self, name, stereo_only, default=[0, 0]): + # two_chan: RVA has only 2 channels, while RVAD has 6 channels + super(RVASpec, self).__init__(name, default) + self._max_values = 4 if stereo_only else 12 + + def read(self, header, frame, data): + # inc/dec flags + spec = ByteSpec("flags", 0) + flags, data = spec.read(header, frame, data) + if not data: + raise SpecError("truncated") + + # how many bytes per value + bits, data = spec.read(header, frame, data) + if bits == 0: + # not allowed according to spec + raise SpecError("bits used has to be > 0") + bytes_per_value = (bits + 7) // 8 + + values = [] + while len(data) >= bytes_per_value and len(values) < self._max_values: + v = BitPaddedInt(data[:bytes_per_value], bits=8) + data = data[bytes_per_value:] + values.append(v) + + if len(values) < 2: + raise SpecError("First two values not optional") + + # if the respective flag bit is zero, take as decrement + for bit, index in enumerate([0, 1, 4, 5, 8, 10]): + if not cdata.test_bit(flags, bit): + try: + values[index] = -values[index] + except IndexError: + break + + return values, data + + def write(self, config, frame, values): + if len(values) < 2 or len(values) > self._max_values: + raise SpecError( + "at least two volume change values required, max %d" % + self._max_values) + + spec = ByteSpec("flags", 0) + + flags = 0 + values = list(values) + for bit, index in enumerate([0, 1, 4, 5, 8, 10]): + try: + if values[index] < 0: + values[index] = -values[index] + else: + flags |= (1 << bit) + except IndexError: + break + + buffer_ = bytearray() + buffer_.extend(spec.write(config, frame, flags)) + + # serialized and make them all the same size (min 2 bytes) + byte_values = [ + BitPaddedInt.to_str(v, bits=8, width=-1, minwidth=2) + for v in values] + max_bytes = max([len(v) for v in byte_values]) + byte_values = [v.ljust(max_bytes, b"\x00") for v in byte_values] + + bits = max_bytes * 8 + buffer_.extend(spec.write(config, frame, bits)) + + for v in byte_values: + buffer_.extend(v) + + return bytes(buffer_) + + def validate(self, frame, values): + if len(values) < 2 or len(values) > self._max_values: + raise ValueError("needs list of length 2..%d" % self._max_values) + return values + + +class FrameIDSpec(StringSpec): + + def __init__(self, name, length): + super(FrameIDSpec, self).__init__(name, length, u"X" * length) + + def validate(self, frame, value): + value = super(FrameIDSpec, self).validate(frame, value) + if not is_valid_frame_id(value): + raise ValueError("Invalid frame ID") + return value class BinaryDataSpec(Spec): - def read(self, frame, data): + + def __init__(self, name, default=b""): + super(BinaryDataSpec, self).__init__(name, default) + + def read(self, header, frame, data): return data, b'' - def write(self, frame, value): - if value is None: - return b"" + def write(self, config, frame, value): if isinstance(value, bytes): return value value = text_type(value).encode("ascii") @@ -272,8 +425,7 @@ def validate(self, frame, value): if value is None: - return None - + raise TypeError if isinstance(value, bytes): return value elif PY3: @@ -292,7 +444,10 @@ Encoding.UTF8: ('utf8', b'\x00'), } - def read(self, frame, data): + def __init__(self, name, default=u""): + super(EncodedTextSpec, self).__init__(name, default) + + def read(self, header, frame, data): enc, term = self._encodings[frame.encoding] try: # allow missing termination @@ -308,9 +463,12 @@ except ValueError: raise SpecError("Decoding error") - def write(self, frame, value): + def write(self, config, frame, value): enc, term = self._encodings[frame.encoding] - return value.encode(enc) + term + try: + return value.encode(enc) + term + except UnicodeEncodeError as e: + raise SpecError(e) def validate(self, frame, value): return text_type(value) @@ -318,16 +476,16 @@ class MultiSpec(Spec): def __init__(self, name, *specs, **kw): - super(MultiSpec, self).__init__(name) + super(MultiSpec, self).__init__(name, default=kw.get('default')) self.specs = specs self.sep = kw.get('sep') - def read(self, frame, data): + def read(self, header, frame, data): values = [] while data: record = [] for spec in self.specs: - value, data = spec.read(frame, data) + value, data = spec.read(header, frame, data) record.append(value) if len(self.specs) != 1: values.append(record) @@ -335,20 +493,18 @@ values.append(record[0]) return values, data - def write(self, frame, value): + def write(self, config, frame, value): data = [] if len(self.specs) == 1: for v in value: - data.append(self.specs[0].write(frame, v)) + data.append(self.specs[0].write(config, frame, v)) else: for record in value: for v, s in izip(record, self.specs): - data.append(s.write(frame, v)) + data.append(s.write(config, frame, v)) return b''.join(data) def validate(self, frame, value): - if value is None: - return [] if self.sep and isinstance(value, string_types): value = value.split(self.sep) if isinstance(value, list): @@ -388,21 +544,87 @@ pass -class Latin1TextSpec(EncodedTextSpec): - def read(self, frame, data): +class Latin1TextSpec(Spec): + + def __init__(self, name, default=u""): + super(Latin1TextSpec, self).__init__(name, default) + + def read(self, header, frame, data): if b'\x00' in data: data, ret = data.split(b'\x00', 1) else: ret = b'' return data.decode('latin1'), ret - def write(self, data, value): + def write(self, config, data, value): return value.encode('latin1') + b'\x00' def validate(self, frame, value): return text_type(value) +class ID3FramesSpec(Spec): + + handle_nodata = True + + def __init__(self, name, default=[]): + super(ID3FramesSpec, self).__init__(name, default) + + def read(self, header, frame, data): + from ._tags import ID3Tags + + tags = ID3Tags() + return tags, tags._read(header, data) + + def _validate23(self, frame, value, **kwargs): + from ._tags import ID3Tags + + v = ID3Tags() + for frame in value.values(): + v.add(frame._get_v23_frame(**kwargs)) + return v + + def write(self, config, frame, value): + return bytes(value._write(config)) + + def validate(self, frame, value): + from ._tags import ID3Tags + + if isinstance(value, ID3Tags): + return value + + tags = ID3Tags() + for v in value: + tags.add(v) + + return tags + + +class Latin1TextListSpec(Spec): + + def __init__(self, name, default=[]): + super(Latin1TextListSpec, self).__init__(name, default) + self._bspec = ByteSpec("entry_count", default=0) + self._lspec = Latin1TextSpec("child_element_id") + + def read(self, header, frame, data): + count, data = self._bspec.read(header, frame, data) + entries = [] + for i in xrange(count): + entry, data = self._lspec.read(header, frame, data) + entries.append(entry) + return entries, data + + def write(self, config, frame, value): + b = self._bspec.write(config, frame, len(value)) + for v in value: + b += self._lspec.write(config, frame, v) + return b + + def validate(self, frame, value): + return [self._lspec.validate(frame, v) for v in value] + + @swap_to_string @total_ordering class ID3TimeStamp(object): @@ -475,12 +697,12 @@ class TimeStampSpec(EncodedTextSpec): - def read(self, frame, data): - value, data = super(TimeStampSpec, self).read(frame, data) + def read(self, header, frame, data): + value, data = super(TimeStampSpec, self).read(header, frame, data) return self.validate(frame, value), data - def write(self, frame, data): - return super(TimeStampSpec, self).write(frame, + def write(self, config, frame, data): + return super(TimeStampSpec, self).write(config, frame, data.text.replace(' ', 'T')) def validate(self, frame, value): @@ -496,11 +718,11 @@ class VolumeAdjustmentSpec(Spec): - def read(self, frame, data): + def read(self, header, frame, data): value, = unpack('>h', data[0:2]) return value / 512.0, data[2:] - def write(self, frame, value): + def write(self, config, frame, value): number = int(round(value * 512)) # pack only fails in 2.7, do it manually in 2.6 if not -32768 <= number <= 32767: @@ -510,14 +732,14 @@ def validate(self, frame, value): if value is not None: try: - self.write(frame, value) + self.write(None, frame, value) except SpecError: raise ValueError("out of range") return value class VolumePeakSpec(Spec): - def read(self, frame, data): + def read(self, header, frame, data): # http://bugs.xmms.org/attachment.cgi?id=113&action=view peak = 0 data_array = bytearray(data) @@ -533,7 +755,7 @@ peak *= 2 ** shift return (float(peak) / (2 ** 31 - 1)), data[1 + vol_bytes:] - def write(self, frame, value): + def write(self, config, frame, value): number = int(round(value * 32768)) # pack only fails in 2.7, do it manually in 2.6 if not 0 <= number <= 65535: @@ -544,14 +766,14 @@ def validate(self, frame, value): if value is not None: try: - self.write(frame, value) + self.write(None, frame, value) except SpecError: raise ValueError("out of range") return value class SynchronizedTextSpec(EncodedTextSpec): - def read(self, frame, data): + def read(self, header, frame, data): texts = [] encoding, term = self._encodings[frame.encoding] while data: @@ -568,7 +790,7 @@ data = data[4:] return texts, b"" - def write(self, frame, value): + def write(self, config, frame, value): data = [] encoding, term = self._encodings[frame.encoding] for text, time in value: @@ -581,14 +803,14 @@ class KeyEventSpec(Spec): - def read(self, frame, data): + def read(self, header, frame, data): events = [] while len(data) >= 5: events.append(struct.unpack(">bI", data[:5])) data = data[5:] return events, data - def write(self, frame, value): + def write(self, config, frame, value): return b"".join(struct.pack(">bI", *event) for event in value) def validate(self, frame, value): @@ -597,7 +819,7 @@ class VolumeAdjustmentsSpec(Spec): # Not to be confused with VolumeAdjustmentSpec. - def read(self, frame, data): + def read(self, header, frame, data): adjustments = {} while len(data) >= 4: freq, adj = struct.unpack(">Hh", data[:4]) @@ -608,7 +830,7 @@ adjustments = sorted(adjustments.items()) return adjustments, data - def write(self, frame, value): + def write(self, config, frame, value): value.sort() return b"".join(struct.pack(">Hh", int(freq * 2), int(adj * 512)) for (freq, adj) in value) @@ -618,7 +840,8 @@ class ASPIIndexSpec(Spec): - def read(self, frame, data): + + def read(self, header, frame, data): if frame.b == 16: format = "H" size = 2 @@ -635,7 +858,7 @@ except struct.error as e: raise SpecError(e) - def write(self, frame, values): + def write(self, config, frame, values): if frame.b == 16: format = "H" elif frame.b == 8: diff -Nru mutagen-1.33.2/mutagen/id3/_tags.py mutagen-1.34/mutagen/id3/_tags.py --- mutagen-1.33.2/mutagen/id3/_tags.py 1970-01-01 00:00:00.000000000 +0000 +++ mutagen-1.34/mutagen/id3/_tags.py 2016-07-20 15:41:57.000000000 +0000 @@ -0,0 +1,568 @@ +# -*- coding: utf-8 -*- +# Copyright 2005 Michael Urman +# Copyright 2016 Christoph Reiter +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. + +import struct + +from mutagen._tags import Tags +from mutagen._util import DictProxy, convert_error, read_full +from mutagen._compat import PY3, text_type + +from ._util import BitPaddedInt, unsynch, ID3JunkFrameError, \ + ID3EncryptionUnsupportedError, is_valid_frame_id, error, \ + ID3NoHeaderError, ID3UnsupportedVersionError, ID3SaveConfig +from ._frames import TDRC, APIC, TDOR, TIME, TIPL, TORY, TDAT, Frames_2_2, \ + TextFrame, TYER, Frame, IPLS, Frames + + +class ID3Header(object): + + _V24 = (2, 4, 0) + _V23 = (2, 3, 0) + _V22 = (2, 2, 0) + _V11 = (1, 1) + + f_unsynch = property(lambda s: bool(s._flags & 0x80)) + f_extended = property(lambda s: bool(s._flags & 0x40)) + f_experimental = property(lambda s: bool(s._flags & 0x20)) + f_footer = property(lambda s: bool(s._flags & 0x10)) + + _known_frames = None + + @property + def known_frames(self): + if self._known_frames is not None: + return self._known_frames + elif self.version >= ID3Header._V23: + return Frames + elif self.version >= ID3Header._V22: + return Frames_2_2 + + @convert_error(IOError, error) + def __init__(self, fileobj=None): + """Raises ID3NoHeaderError, ID3UnsupportedVersionError or error""" + + if fileobj is None: + # for testing + self._flags = 0 + return + + fn = getattr(fileobj, "name", "") + data = fileobj.read(10) + if len(data) != 10: + raise ID3NoHeaderError("%s: too small" % fn) + + id3, vmaj, vrev, flags, size = struct.unpack('>3sBBB4s', data) + self._flags = flags + self.size = BitPaddedInt(size) + 10 + self.version = (2, vmaj, vrev) + + if id3 != b'ID3': + raise ID3NoHeaderError("%r doesn't start with an ID3 tag" % fn) + + if vmaj not in [2, 3, 4]: + raise ID3UnsupportedVersionError("%r ID3v2.%d not supported" + % (fn, vmaj)) + + if not BitPaddedInt.has_valid_padding(size): + raise error("Header size not synchsafe") + + if (self.version >= self._V24) and (flags & 0x0f): + raise error( + "%r has invalid flags %#02x" % (fn, flags)) + elif (self._V23 <= self.version < self._V24) and (flags & 0x1f): + raise error( + "%r has invalid flags %#02x" % (fn, flags)) + + if self.f_extended: + extsize_data = read_full(fileobj, 4) + + if PY3: + frame_id = extsize_data.decode("ascii", "replace") + else: + frame_id = extsize_data + + if frame_id in Frames: + # Some tagger sets the extended header flag but + # doesn't write an extended header; in this case, the + # ID3 data follows immediately. Since no extended + # header is going to be long enough to actually match + # a frame, and if it's *not* a frame we're going to be + # completely lost anyway, this seems to be the most + # correct check. + # https://github.com/quodlibet/quodlibet/issues/126 + self._flags ^= 0x40 + extsize = 0 + fileobj.seek(-4, 1) + elif self.version >= self._V24: + # "Where the 'Extended header size' is the size of the whole + # extended header, stored as a 32 bit synchsafe integer." + extsize = BitPaddedInt(extsize_data) - 4 + if not BitPaddedInt.has_valid_padding(extsize_data): + raise error( + "Extended header size not synchsafe") + else: + # "Where the 'Extended header size', currently 6 or 10 bytes, + # excludes itself." + extsize = struct.unpack('>L', extsize_data)[0] + + self._extdata = read_full(fileobj, extsize) + + +def determine_bpi(data, frames, EMPTY=b"\x00" * 10): + """Takes id3v2.4 frame data and determines if ints or bitpaddedints + should be used for parsing. Needed because iTunes used to write + normal ints for frame sizes. + """ + + # count number of tags found as BitPaddedInt and how far past + o = 0 + asbpi = 0 + while o < len(data) - 10: + part = data[o:o + 10] + if part == EMPTY: + bpioff = -((len(data) - o) % 10) + break + name, size, flags = struct.unpack('>4sLH', part) + size = BitPaddedInt(size) + o += 10 + size + if PY3: + try: + name = name.decode("ascii") + except UnicodeDecodeError: + continue + if name in frames: + asbpi += 1 + else: + bpioff = o - len(data) + + # count number of tags found as int and how far past + o = 0 + asint = 0 + while o < len(data) - 10: + part = data[o:o + 10] + if part == EMPTY: + intoff = -((len(data) - o) % 10) + break + name, size, flags = struct.unpack('>4sLH', part) + o += 10 + size + if PY3: + try: + name = name.decode("ascii") + except UnicodeDecodeError: + continue + if name in frames: + asint += 1 + else: + intoff = o - len(data) + + # if more tags as int, or equal and bpi is past and int is not + if asint > asbpi or (asint == asbpi and (bpioff >= 1 and intoff <= 1)): + return int + return BitPaddedInt + + +class ID3Tags(DictProxy, Tags): + + __module__ = "mutagen.id3" + + def __init__(self, *args, **kwargs): + self.unknown_frames = [] + self._unknown_v2_version = 4 + super(ID3Tags, self).__init__(*args, **kwargs) + + def _read(self, header, data): + frames, unknown_frames, data = read_frames( + header, data, header.known_frames) + for frame in frames: + self.add(frame) + self.unknown_frames = unknown_frames + self._unknown_v2_version = header.version[1] + return data + + def _write(self, config): + # Sort frames by 'importance' + order = ["TIT2", "TPE1", "TRCK", "TALB", "TPOS", "TDRC", "TCON"] + order = dict((b, a) for a, b in enumerate(order)) + last = len(order) + frames = sorted(self.items(), + key=lambda a: (order.get(a[0][:4], last), a[0])) + + framedata = [save_frame(frame, config=config) + for (key, frame) in frames] + + # only write unknown frames if they were loaded from the version + # we are saving with. Theoretically we could upgrade frames + # but some frames can be nested like CHAP, so there is a chance + # we create a mixed frame mess. + if self._unknown_v2_version == config.v2_version: + framedata.extend(data for data in self.unknown_frames + if len(data) > 10) + + return bytearray().join(framedata) + + def getall(self, key): + """Return all frames with a given name (the list may be empty). + + Args: + key (text): key for frames to get + + This is best explained by examples:: + + id3.getall('TIT2') == [id3['TIT2']] + id3.getall('TTTT') == [] + id3.getall('TXXX') == [TXXX(desc='woo', text='bar'), + TXXX(desc='baz', text='quuuux'), ...] + + Since this is based on the frame's HashKey, which is + colon-separated, you can use it to do things like + ``getall('COMM:MusicMatch')`` or ``getall('TXXX:QuodLibet:')``. + """ + if key in self: + return [self[key]] + else: + key = key + ":" + return [v for s, v in self.items() if s.startswith(key)] + + def setall(self, key, values): + """Delete frames of the given type and add frames in 'values'. + + Args: + key (text): key for frames to delete + values (List[`Frame`]): frames to add + """ + + self.delall(key) + for tag in values: + self[tag.HashKey] = tag + + def delall(self, key): + """Delete all tags of a given kind; see getall. + + Args: + key (text): key for frames to delete + """ + + if key in self: + del(self[key]) + else: + key = key + ":" + for k in list(self.keys()): + if k.startswith(key): + del(self[k]) + + def pprint(self): + """ + Returns: + text: tags in a human-readable format. + + "Human-readable" is used loosely here. The format is intended + to mirror that used for Vorbis or APEv2 output, e.g. + + ``TIT2=My Title`` + + However, ID3 frames can have multiple keys: + + ``POPM=user@example.org=3 128/255`` + """ + + frames = sorted(Frame.pprint(s) for s in self.values()) + return "\n".join(frames) + + def loaded_frame(self, tag): + """Deprecated; use the add method.""" + # turn 2.2 into 2.3/2.4 tags + if len(type(tag).__name__) == 3: + tag = type(tag).__base__(tag) + self[tag.HashKey] = tag + + # add = loaded_frame (and vice versa) break applications that + # expect to be able to override loaded_frame (e.g. Quod Libet), + # as does making loaded_frame call add. + def add(self, frame): + """Add a frame to the tag.""" + return self.loaded_frame(frame) + + def __setitem__(self, key, tag): + if not isinstance(tag, Frame): + raise TypeError("%r not a Frame instance" % tag) + super(ID3Tags, self).__setitem__(key, tag) + + def __update_common(self): + """Updates done by both v23 and v24 update""" + + if "TCON" in self: + # Get rid of "(xx)Foobr" format. + self["TCON"].genres = self["TCON"].genres + + mimes = {"PNG": "image/png", "JPG": "image/jpeg"} + for pic in self.getall("APIC"): + if pic.mime in mimes: + newpic = APIC( + encoding=pic.encoding, mime=mimes[pic.mime], + type=pic.type, desc=pic.desc, data=pic.data) + self.add(newpic) + + def update_to_v24(self): + """Convert older tags into an ID3v2.4 tag. + + This updates old ID3v2 frames to ID3v2.4 ones (e.g. TYER to + TDRC). If you intend to save tags, you must call this function + at some point; it is called by default when loading the tag. + """ + + self.__update_common() + + # TDAT, TYER, and TIME have been turned into TDRC. + try: + date = text_type(self.get("TYER", "")) + if date.strip(u"\x00"): + self.pop("TYER") + dat = text_type(self.get("TDAT", "")) + if dat.strip("\x00"): + self.pop("TDAT") + date = "%s-%s-%s" % (date, dat[2:], dat[:2]) + time = text_type(self.get("TIME", "")) + if time.strip("\x00"): + self.pop("TIME") + date += "T%s:%s:00" % (time[:2], time[2:]) + if "TDRC" not in self: + self.add(TDRC(encoding=0, text=date)) + except UnicodeDecodeError: + # Old ID3 tags have *lots* of Unicode problems, so if TYER + # is bad, just chuck the frames. + pass + + # TORY can be the first part of a TDOR. + if "TORY" in self: + f = self.pop("TORY") + if "TDOR" not in self: + try: + self.add(TDOR(encoding=0, text=str(f))) + except UnicodeDecodeError: + pass + + # IPLS is now TIPL. + if "IPLS" in self: + f = self.pop("IPLS") + if "TIPL" not in self: + self.add(TIPL(encoding=f.encoding, people=f.people)) + + # These can't be trivially translated to any ID3v2.4 tags, or + # should have been removed already. + for key in ["RVAD", "EQUA", "TRDA", "TSIZ", "TDAT", "TIME"]: + if key in self: + del(self[key]) + + # Recurse into chapters + for f in self.getall("CHAP"): + f.sub_frames.update_to_v24() + for f in self.getall("CTOC"): + f.sub_frames.update_to_v24() + + def update_to_v23(self): + """Convert older (and newer) tags into an ID3v2.3 tag. + + This updates incompatible ID3v2 frames to ID3v2.3 ones. If you + intend to save tags as ID3v2.3, you must call this function + at some point. + + If you want to to go off spec and include some v2.4 frames + in v2.3, remove them before calling this and add them back afterwards. + """ + + self.__update_common() + + # TMCL, TIPL -> TIPL + if "TIPL" in self or "TMCL" in self: + people = [] + if "TIPL" in self: + f = self.pop("TIPL") + people.extend(f.people) + if "TMCL" in self: + f = self.pop("TMCL") + people.extend(f.people) + if "IPLS" not in self: + self.add(IPLS(encoding=f.encoding, people=people)) + + # TDOR -> TORY + if "TDOR" in self: + f = self.pop("TDOR") + if f.text: + d = f.text[0] + if d.year and "TORY" not in self: + self.add(TORY(encoding=f.encoding, text="%04d" % d.year)) + + # TDRC -> TYER, TDAT, TIME + if "TDRC" in self: + f = self.pop("TDRC") + if f.text: + d = f.text[0] + if d.year and "TYER" not in self: + self.add(TYER(encoding=f.encoding, text="%04d" % d.year)) + if d.month and d.day and "TDAT" not in self: + self.add(TDAT(encoding=f.encoding, + text="%02d%02d" % (d.day, d.month))) + if d.hour and d.minute and "TIME" not in self: + self.add(TIME(encoding=f.encoding, + text="%02d%02d" % (d.hour, d.minute))) + + # New frames added in v2.4 + v24_frames = [ + 'ASPI', 'EQU2', 'RVA2', 'SEEK', 'SIGN', 'TDEN', 'TDOR', + 'TDRC', 'TDRL', 'TDTG', 'TIPL', 'TMCL', 'TMOO', 'TPRO', + 'TSOA', 'TSOP', 'TSOT', 'TSST', + ] + + for key in v24_frames: + if key in self: + del(self[key]) + + # Recurse into chapters + for f in self.getall("CHAP"): + f.sub_frames.update_to_v23() + for f in self.getall("CTOC"): + f.sub_frames.update_to_v23() + + +def save_frame(frame, name=None, config=None): + if config is None: + config = ID3SaveConfig() + + flags = 0 + if isinstance(frame, TextFrame): + if len(str(frame)) == 0: + return b'' + + framedata = frame._writeData(config) + + usize = len(framedata) + if usize > 2048: + # Disabled as this causes iTunes and other programs + # to fail to find these frames, which usually includes + # e.g. APIC. + # framedata = BitPaddedInt.to_str(usize) + framedata.encode('zlib') + # flags |= Frame.FLAG24_COMPRESS | Frame.FLAG24_DATALEN + pass + + if config.v2_version == 4: + bits = 7 + elif config.v2_version == 3: + bits = 8 + else: + raise ValueError + + datasize = BitPaddedInt.to_str(len(framedata), width=4, bits=bits) + + if name is not None: + assert isinstance(name, bytes) + frame_name = name + else: + frame_name = type(frame).__name__ + if PY3: + frame_name = frame_name.encode("ascii") + + header = struct.pack('>4s4sH', frame_name, datasize, flags) + return header + framedata + + +def read_frames(id3, data, frames): + """Does not error out""" + + assert id3.version >= ID3Header._V22 + + result = [] + unsupported_frames = [] + + if id3.version < ID3Header._V24 and id3.f_unsynch: + try: + data = unsynch.decode(data) + except ValueError: + pass + + if id3.version >= ID3Header._V23: + if id3.version < ID3Header._V24: + bpi = int + else: + bpi = determine_bpi(data, frames) + + while data: + header = data[:10] + try: + name, size, flags = struct.unpack('>4sLH', header) + except struct.error: + break # not enough header + if name.strip(b'\x00') == b'': + break + + size = bpi(size) + framedata = data[10:10 + size] + data = data[10 + size:] + if size == 0: + continue # drop empty frames + + if PY3: + try: + name = name.decode('ascii') + except UnicodeDecodeError: + continue + + try: + # someone writes 2.3 frames with 2.2 names + if name[-1] == "\x00": + tag = Frames_2_2[name[:-1]] + name = tag.__base__.__name__ + + tag = frames[name] + except KeyError: + if is_valid_frame_id(name): + unsupported_frames.append(header + framedata) + else: + try: + result.append(tag._fromData(id3, flags, framedata)) + except NotImplementedError: + unsupported_frames.append(header + framedata) + except ID3JunkFrameError: + pass + elif id3.version >= ID3Header._V22: + while data: + header = data[0:6] + try: + name, size = struct.unpack('>3s3s', header) + except struct.error: + break # not enough header + size, = struct.unpack('>L', b'\x00' + size) + if name.strip(b'\x00') == b'': + break + + framedata = data[6:6 + size] + data = data[6 + size:] + if size == 0: + continue # drop empty frames + + if PY3: + try: + name = name.decode('ascii') + except UnicodeDecodeError: + continue + + try: + tag = frames[name] + except KeyError: + if is_valid_frame_id(name): + unsupported_frames.append(header + framedata) + else: + try: + result.append( + tag._fromData(id3, 0, framedata)) + except (ID3EncryptionUnsupportedError, + NotImplementedError): + unsupported_frames.append(header + framedata) + except ID3JunkFrameError: + pass + + return result, unsupported_frames, data diff -Nru mutagen-1.33.2/mutagen/id3/_util.py mutagen-1.34/mutagen/id3/_util.py --- mutagen-1.33.2/mutagen/id3/_util.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/mutagen/id3/_util.py 2016-07-13 07:06:55.000000000 +0000 @@ -8,8 +8,20 @@ # it under the terms of version 2 of the GNU General Public License as # published by the Free Software Foundation. -from .._compat import long_, integer_types, PY3 -from .._util import MutagenError +from mutagen._compat import long_, integer_types, PY3 +from mutagen._util import MutagenError + + +def is_valid_frame_id(frame_id): + return frame_id.isalnum() and frame_id.isupper() + + +class ID3SaveConfig(object): + + def __init__(self, v2_version=4, v23_separator=None): + assert v2_version in (3, 4) + self.v2_version = v2_version + self.v23_separator = v23_separator class error(MutagenError): diff -Nru mutagen-1.33.2/mutagen/__init__.py mutagen-1.34/mutagen/__init__.py --- mutagen-1.33.2/mutagen/__init__.py 2016-07-05 15:28:05.000000000 +0000 +++ mutagen-1.34/mutagen/__init__.py 2016-07-20 19:11:23.000000000 +0000 @@ -24,7 +24,7 @@ from mutagen._file import FileType, StreamInfo, File from mutagen._tags import Tags, Metadata, PaddingInfo -version = (1, 33, 2) +version = (1, 34) """Version tuple.""" version_string = ".".join(map(str, version)) diff -Nru mutagen-1.33.2/mutagen/mp3/__init__.py mutagen-1.34/mutagen/mp3/__init__.py --- mutagen-1.33.2/mutagen/mp3/__init__.py 2016-06-27 18:17:47.000000000 +0000 +++ mutagen-1.34/mutagen/mp3/__init__.py 2016-07-13 07:06:55.000000000 +0000 @@ -14,7 +14,8 @@ from mutagen._util import MutagenError, enum, BitReader, BitReaderError, \ convert_error from mutagen._compat import endswith, xrange -from mutagen.id3 import ID3FileType, BitPaddedInt, delete +from mutagen.id3 import ID3FileType, delete +from mutagen.id3._util import BitPaddedInt from ._util import XingHeader, XingHeaderError, VBRIHeader, VBRIHeaderError diff -Nru mutagen-1.33.2/mutagen/musepack.py mutagen-1.34/mutagen/musepack.py --- mutagen-1.33.2/mutagen/musepack.py 2016-06-27 17:10:38.000000000 +0000 +++ mutagen-1.34/mutagen/musepack.py 2016-07-13 07:06:55.000000000 +0000 @@ -22,7 +22,7 @@ from ._compat import endswith, xrange from mutagen import StreamInfo from mutagen.apev2 import APEv2File, error, delete -from mutagen.id3 import BitPaddedInt +from mutagen.id3._util import BitPaddedInt from mutagen._util import cdata, convert_error diff -Nru mutagen-1.33.2/mutagen/_util.py mutagen-1.34/mutagen/_util.py --- mutagen-1.33.2/mutagen/_util.py 2016-07-05 14:42:56.000000000 +0000 +++ mutagen-1.34/mutagen/_util.py 2016-07-18 21:36:17.000000000 +0000 @@ -16,6 +16,7 @@ import struct import codecs import errno +import mmap from collections import namedtuple from contextlib import contextmanager @@ -27,8 +28,9 @@ def is_fileobj(fileobj): - """Returns if an argument passed ot mutagen should be treated as a - file object + """Returns: + bool: if an argument passed ot mutagen should be treated as a + file object """ # open() only handles str/bytes, so we can be strict @@ -39,8 +41,12 @@ """Verifies that the passed fileobj is a file like object which we can use. + Args: + writable (bool): verify that the file object is writable as well + Raises: - ValueError + ValueError: In case the object is not a file object that is readable + (or writable if required) or is not opened in bytes mode. """ try: @@ -64,13 +70,21 @@ def verify_filename(filename): + """Checks of the passed in filename has the correct type. + + Raises: + ValueError: if not a filename + """ + if is_fileobj(filename): raise ValueError("%r not a filename" % filename) def fileobj_name(fileobj): - """Returns a potential filename for a file object. - Always a valid path type, but might be empty or non-existent. + """ + Returns: + text: A potential filename for a file object. Always a valid + path type, but might be empty or non-existent. """ value = getattr(fileobj, "name", u"") @@ -80,6 +94,17 @@ def loadfile(method=True, writable=False, create=False): + """A decorator for functions taking a `filething` as a first argument. + + Passes a FileThing instance as the first argument to the wrapped function. + + Args: + method (bool): If the wrapped functions is a method + writable (bool): If a filename is passed opens the file readwrite, if + passed a file object verifies that it is writable. + create (bool): If passed a filename that does not exist will create + a new empty file. + """ def convert_file_args(args, kwargs): filething = args[0] if args else None @@ -113,6 +138,10 @@ def convert_error(exc_src, exc_dest): """A decorator for reraising exceptions with a different type. Mostly useful for IOError. + + Args: + exc_src (type): The source exception type + exc_dest (type): The target exception type. """ def wrap(func): @@ -207,6 +236,11 @@ def total_ordering(cls): + """Adds all possible ordering methods to a class. + + Needs a working __eq__ and __lt__ and will supply the rest. + """ + assert "__eq__" in cls.__dict__ assert "__lt__" in cls.__dict__ @@ -236,6 +270,25 @@ def enum(cls): + """A decorator for creating an int enum class. + + Makes the values a subclass of the type and implements repr/str. + The new class will be a subclass of int. + + Args: + cls (type): The class to convert to an enum + + Returns: + type: A new class + + :: + + @enum + class Foo(object): + FOO = 1 + BAR = 2 + """ + assert cls.__bases__ == (object,) d = dict(cls.__dict__) @@ -265,6 +318,60 @@ return new_type +def flags(cls): + """A decorator for creating an int flags class. + + Makes the values a subclass of the type and implements repr/str. + The new class will be a subclass of int. + + Args: + cls (type): The class to convert to an flags + + Returns: + type: A new class + + :: + + @flags + class Foo(object): + FOO = 1 + BAR = 2 + """ + + assert cls.__bases__ == (object,) + + d = dict(cls.__dict__) + new_type = type(cls.__name__, (int,), d) + new_type.__module__ = cls.__module__ + + map_ = {} + for key, value in iteritems(d): + if key.upper() == key and isinstance(value, integer_types): + value_instance = new_type(value) + setattr(new_type, key, value_instance) + map_[value] = key + + def str_(self): + value = int(self) + matches = [] + for k, v in map_.items(): + if value & k: + matches.append("%s.%s" % (type(self).__name__, v)) + value &= ~k + if value != 0 or not matches: + matches.append(text_type(value)) + + return " | ".join(matches) + + def repr_(self): + return "<%s: %d>" % (str(self), int(self)) + + setattr(new_type, "__repr__", repr_) + setattr(new_type, "__str__", str_) + + return new_type + + @total_ordering class DictMixin(object): """Implement the dict API using keys() and __*item__ methods. @@ -455,7 +562,12 @@ """Returns the size of the file. The position when passed in will be preserved if no error occurs. - In case of an error raises IOError. + Args: + fileobj (fileobj) + Returns: + int: The size of the file + Raises: + IOError """ old_pos = fileobj.tell() @@ -466,6 +578,29 @@ fileobj.seek(old_pos, 0) +def read_full(fileobj, size): + """Like fileobj.read but raises IOError if no all requested data is + returned. + + If you want to distinguish IOError and the EOS case, better handle + the error yourself instead of using this. + + Args: + fileobj (fileobj) + size (int): amount of bytes to read + Raises: + IOError: In case read fails or not enough data is read + """ + + if size < 0: + raise ValueError("size must not be negative") + + data = fileobj.read(size) + if len(data) != size: + raise IOError + return data + + def seek_end(fileobj, offset): """Like fileobj.seek(-offset, 2), but will not try to go beyond the start @@ -474,7 +609,12 @@ To make things easier for custom implementations, instead of allowing both behaviors, we just don't do it. - Can raise IOError + Args: + fileobj (fileobj) + offset (int): how many bytes away from the end backwards to seek to + + Raises: + IOError """ if offset < 0: @@ -486,62 +626,173 @@ fileobj.seek(-offset, 2) +def mmap_move(fileobj, dest, src, count): + """Mmaps the file object if possible and moves 'count' data + from 'src' to 'dest'. All data has to be inside the file size + (enlarging the file through this function isn't possible) + + Will adjust the file offset. + + Args: + fileobj (fileobj) + dest (int): The destination offset + src (int): The source offset + count (int) The amount of data to move + Raises: + mmap.error: In case move failed + IOError: In case an operation on the fileobj fails + ValueError: In case invalid parameters were given + """ + + if dest < 0 or src < 0 or count < 0: + raise ValueError("Invalid parameters") + + try: + fileno = fileobj.fileno() + except (AttributeError, IOError): + raise mmap.error( + "File object does not expose/support a file descriptor") + + fileobj.seek(0, 2) + filesize = fileobj.tell() + length = max(dest, src) + count + + if length > filesize: + raise ValueError("Not in file size boundary") + + offset = ((min(dest, src) // mmap.ALLOCATIONGRANULARITY) * + mmap.ALLOCATIONGRANULARITY) + assert dest >= offset + assert src >= offset + assert offset % mmap.ALLOCATIONGRANULARITY == 0 + + # Windows doesn't handle empty mappings, add a fast path here instead + if count == 0: + return + + # fast path + if src == dest: + return + + fileobj.flush() + file_map = mmap.mmap(fileno, length - offset, offset=offset) + try: + file_map.move(dest - offset, src - offset, count) + finally: + file_map.close() + + +def resize_file(fobj, diff, BUFFER_SIZE=2 ** 16): + """Resize a file by `diff`. + + New space will be filled with zeros. + + Args: + fobj (fileobj) + diff (int): amount of size to change + Raises: + IOError + """ + + fobj.seek(0, 2) + filesize = fobj.tell() + + if diff < 0: + if filesize + diff < 0: + raise ValueError + # truncate flushes internally + fobj.truncate(filesize + diff) + elif diff > 0: + try: + while diff: + addsize = min(BUFFER_SIZE, diff) + fobj.write(b"\x00" * addsize) + diff -= addsize + fobj.flush() + except IOError as e: + if e.errno == errno.ENOSPC: + # To reduce the chance of corrupt files in case of missing + # space try to revert the file expansion back. Of course + # in reality every in-file-write can also fail due to COW etc. + # Note: IOError gets also raised in flush() due to buffering + fobj.truncate(filesize) + raise + + +def fallback_move(fobj, dest, src, count, BUFFER_SIZE=2 ** 16): + """Moves data around using read()/write(). + + Args: + fileobj (fileobj) + dest (int): The destination offset + src (int): The source offset + count (int) The amount of data to move + Raises: + IOError: In case an operation on the fileobj fails + ValueError: In case invalid parameters were given + """ + + if dest < 0 or src < 0 or count < 0: + raise ValueError + + fobj.seek(0, 2) + filesize = fobj.tell() + + if max(dest, src) + count > filesize: + raise ValueError("area outside of file") + + if src > dest: + moved = 0 + while count - moved: + this_move = min(BUFFER_SIZE, count - moved) + fobj.seek(src + moved) + buf = fobj.read(this_move) + fobj.seek(dest + moved) + fobj.write(buf) + moved += this_move + fobj.flush() + else: + while count: + this_move = min(BUFFER_SIZE, count) + fobj.seek(src + count - this_move) + buf = fobj.read(this_move) + fobj.seek(count + dest - this_move) + fobj.write(buf) + count -= this_move + fobj.flush() + + def insert_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16): """Insert size bytes of empty space starting at offset. fobj must be an open file object, open rb+ or equivalent. Mutagen tries to use mmap to resize the file, but falls back to a significantly slower method if mmap fails. + + Args: + fobj (fileobj) + size (int): The amount of space to insert + offset (int): The offset at which to insert the space + Raises: + IOError """ - assert 0 < size - assert 0 <= offset + if size < 0 or offset < 0: + raise ValueError fobj.seek(0, 2) filesize = fobj.tell() movesize = filesize - offset - fobj.write(b'\x00' * size) - fobj.flush() - try: - import mmap - file_map = mmap.mmap(fobj.fileno(), filesize + size) - try: - file_map.move(offset + size, offset, movesize) - finally: - file_map.close() - except (ValueError, EnvironmentError, ImportError, AttributeError): - # handle broken mmap scenarios, BytesIO() - fobj.truncate(filesize) - - fobj.seek(0, 2) - padsize = size - # Don't generate an enormous string if we need to pad - # the file out several megs. - while padsize: - addsize = min(BUFFER_SIZE, padsize) - fobj.write(b"\x00" * addsize) - padsize -= addsize - - fobj.seek(filesize, 0) - while movesize: - # At the start of this loop, fobj is pointing at the end - # of the data we need to move, which is of movesize length. - thismove = min(BUFFER_SIZE, movesize) - # Seek back however much we're going to read this frame. - fobj.seek(-thismove, 1) - nextpos = fobj.tell() - # Read it, so we're back at the end. - data = fobj.read(thismove) - # Seek back to where we need to write it. - fobj.seek(-thismove + size, 1) - # Write it. - fobj.write(data) - # And seek back to the end of the unmoved data. - fobj.seek(nextpos) - movesize -= thismove + if movesize < 0: + raise ValueError - fobj.flush() + resize_file(fobj, size, BUFFER_SIZE) + + try: + mmap_move(fobj, offset + size, offset, movesize) + except mmap.error: + fallback_move(fobj, offset + size, offset, movesize, BUFFER_SIZE) def delete_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16): @@ -550,42 +801,44 @@ fobj must be an open file object, open rb+ or equivalent. Mutagen tries to use mmap to resize the file, but falls back to a significantly slower method if mmap fails. + + Args: + fobj (fileobj) + size (int): The amount of space to delete + offset (int): The start of the space to delete + Raises: + IOError """ - assert 0 < size - assert 0 <= offset + if size < 0 or offset < 0: + raise ValueError fobj.seek(0, 2) filesize = fobj.tell() movesize = filesize - offset - size - assert 0 <= movesize - if movesize > 0: - fobj.flush() - try: - import mmap - file_map = mmap.mmap(fobj.fileno(), filesize) - try: - file_map.move(offset, offset + size, movesize) - finally: - file_map.close() - except (ValueError, EnvironmentError, ImportError, AttributeError): - # handle broken mmap scenarios, BytesIO() - fobj.seek(offset + size) - buf = fobj.read(BUFFER_SIZE) - while buf: - fobj.seek(offset) - fobj.write(buf) - offset += len(buf) - fobj.seek(offset + size) - buf = fobj.read(BUFFER_SIZE) - fobj.truncate(filesize - size) - fobj.flush() + if movesize < 0: + raise ValueError + + try: + mmap_move(fobj, offset, offset + size, movesize) + except mmap.error: + fallback_move(fobj, offset, offset + size, movesize, BUFFER_SIZE) + + resize_file(fobj, -size, BUFFER_SIZE) def resize_bytes(fobj, old_size, new_size, offset): """Resize an area in a file adding and deleting at the end of it. Does nothing if no resizing is needed. + + Args: + fobj (fileobj) + old_size (int): The area starting at offset + new_size (int): The new size of the area + offset (int): The start of the area + Raises: + IOError """ if new_size < old_size: @@ -601,6 +854,15 @@ def dict_match(d, key, default=None): """Like __getitem__ but works as if the keys() are all filename patterns. Returns the value of any dict key that matches the passed key. + + Args: + d (dict): A dict with filename patterns as keys + key (str): A key potentially matching any of the keys + default (object): The object to return if no pattern matched the + passed in key + Returns: + object: The dict value where the dict key matched the passed in key. + Or default if there was no match. """ if key in d and "[" not in key: @@ -616,11 +878,21 @@ """Returns the decoded data until the first NULL terminator and all data after it. - In case the data can't be decoded raises UnicodeError. - In case the encoding is not found raises LookupError. - In case the data isn't null terminated (even if it is encoded correctly) - raises ValueError except if strict is False, then the decoded string - will be returned anyway. + Args: + data (bytes): data to decode + encoding (str): The codec to use + strict (bool): If True will raise ValueError in case no NULL is found + but the available data decoded successfully. + Returns: + Tuple[`text`, `bytes`]: A tuple containing the decoded text and the + remaining data after the found NULL termination. + + Raises: + UnicodeError: In case the data can't be decoded. + LookupError:In case the encoding is not found. + ValueError: In case the data isn't null terminated (even if it is + encoded correctly) except if strict is False, then the decoded + string will be returned anyway. """ codec_info = codecs.lookup(encoding) diff -Nru mutagen-1.33.2/NEWS mutagen-1.34/NEWS --- mutagen-1.33.2/NEWS 2016-07-05 15:22:01.000000000 +0000 +++ mutagen-1.34/NEWS 2016-07-20 19:04:35.000000000 +0000 @@ -1,3 +1,23 @@ +1.34 - 2016.07.20 +----------------- + +* ID3: + + * Add `CTOC ` and `CHAP ` frames. New classes: + `ID3Tags `, `CTOCFlags `. :bug:`6` + * Add `TCAT `, `TKWD `, `PCST ` frames. + :bug:`249` + * Validate user provided LNK/LINK frameid. :bug:`242` + * Add `RVAD `, RVA frames + * Add TST, TSA, TS2, TSP and TSC frames + * Fix not writing optional fields when saving to v2.3 + * Add default field values for all frames + +* Drop Python 2.6 support +* EasyID3: Fix TXXX frame encoding when setting a non-latin1 encodable + value after a latin1 one. :bug:`263` + + 1.33.2 - 2016.07.05 ------------------- diff -Nru mutagen-1.33.2/PKG-INFO mutagen-1.34/PKG-INFO --- mutagen-1.33.2/PKG-INFO 2016-07-05 15:28:26.000000000 +0000 +++ mutagen-1.34/PKG-INFO 2016-07-20 19:11:31.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: mutagen -Version: 1.33.2 +Version: 1.34 Summary: read and write audio tags for many formats Home-page: https://github.com/quodlibet/mutagen Author: Michael Urman @@ -18,7 +18,6 @@ Platform: UNKNOWN Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 diff -Nru mutagen-1.33.2/setup.py mutagen-1.34/setup.py --- mutagen-1.33.2/setup.py 2016-06-07 11:07:56.000000000 +0000 +++ mutagen-1.34/setup.py 2016-07-07 14:02:42.000000000 +0000 @@ -246,7 +246,6 @@ classifiers=[ 'Operating System :: OS Independent', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', diff -Nru mutagen-1.33.2/tests/__init__.py mutagen-1.34/tests/__init__.py --- mutagen-1.33.2/tests/__init__.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/tests/__init__.py 2016-07-07 16:31:21.000000000 +0000 @@ -7,6 +7,9 @@ import os import sys import unittest +import warnings +import shutil +from tempfile import mkstemp from unittest import TestCase as BaseTestCase @@ -28,6 +31,30 @@ "Try setting LANG=C.UTF-8") +# Make sure we see all deprecation warnings so we either have to avoid them +# or capture them in the test suite +warnings.simplefilter("always") +warnings.simplefilter("ignore", PendingDeprecationWarning) + + +def get_temp_copy(path): + """Returns a copy of the file with the same extension""" + + ext = os.path.splitext(path)[-1] + fd, filename = mkstemp(suffix=ext) + os.close(fd) + shutil.copy(path, filename) + return filename + + +def get_temp_empty(ext=""): + """Returns an empty file with the extension""" + + fd, filename = mkstemp(suffix=ext) + os.close(fd) + return filename + + class TestCase(BaseTestCase): def failUnlessRaisesRegexp(self, exc, re_, fun, *args, **kwargs): @@ -47,6 +74,9 @@ failUnlessAlmostEqual = BaseTestCase.assertAlmostEqual failIfEqual = BaseTestCase.assertNotEqual failIfAlmostEqual = BaseTestCase.assertNotAlmostEqual + assertEquals = BaseTestCase.assertEqual + assertNotEquals = BaseTestCase.assertNotEqual + assert_ = BaseTestCase.assertTrue def assertReallyEqual(self, a, b): self.assertEqual(a, b) diff -Nru mutagen-1.33.2/tests/quality/test_pep8.py mutagen-1.34/tests/quality/test_pep8.py --- mutagen-1.33.2/tests/quality/test_pep8.py 2016-04-09 07:33:25.000000000 +0000 +++ mutagen-1.34/tests/quality/test_pep8.py 2016-07-07 13:38:07.000000000 +0000 @@ -38,8 +38,9 @@ self.p = p def result(self): - if self.p.wait() != 0: - return self.p.communicate() + result = self.p.communicate() + if self.p.returncode != 0: + return result return Future(p) diff -Nru mutagen-1.33.2/tests/quality/test_pyflakes.py mutagen-1.34/tests/quality/test_pyflakes.py --- mutagen-1.33.2/tests/quality/test_pyflakes.py 2016-04-09 07:35:03.000000000 +0000 +++ mutagen-1.34/tests/quality/test_pyflakes.py 2016-07-13 07:06:55.000000000 +0000 @@ -27,12 +27,11 @@ UNABLE_DETECT_UNDEF = "unable to detect undefined names" UNDEFINED_PY2_NAME = \ "undefined name '(unicode|long|basestring|xrange|cmp)'" - MAYBE_UNDEF = "may be undefined, or defined from star imports" class FakeStream(object): # skip these by default - BL = [Error.UNABLE_DETECT_UNDEF, Error.MAYBE_UNDEF] + BL = [] if _compat.PY3: BL.append(Error.UNDEFINED_PY2_NAME) diff -Nru mutagen-1.33.2/tests/test_aac.py mutagen-1.34/tests/test_aac.py --- mutagen-1.33.2/tests/test_aac.py 2016-06-16 19:05:36.000000000 +0000 +++ mutagen-1.34/tests/test_aac.py 2016-07-07 16:59:05.000000000 +0000 @@ -1,21 +1,19 @@ # -*- coding: utf-8 -*- import os -from tempfile import mkstemp -import shutil from mutagen.id3 import ID3, TIT1 from mutagen.aac import AAC, AACError -from tests import TestCase, DATA_DIR + +from tests import TestCase, DATA_DIR, get_temp_copy class TADTS(TestCase): def setUp(self): original = os.path.join(DATA_DIR, "empty.aac") - fd, self.filename = mkstemp(suffix='.aac') - os.close(fd) - shutil.copy(original, self.filename) + self.filename = get_temp_copy(original) + tag = ID3() tag.add(TIT1(text=[u"a" * 5000], encoding=3)) tag.save(self.filename) @@ -60,9 +58,8 @@ def setUp(self): original = os.path.join(DATA_DIR, "adif.aac") - fd, self.filename = mkstemp(suffix='.aac') - os.close(fd) - shutil.copy(original, self.filename) + self.filename = get_temp_copy(original) + tag = ID3() tag.add(TIT1(text=[u"a" * 5000], encoding=3)) tag.save(self.filename) diff -Nru mutagen-1.33.2/tests/test_aiff.py mutagen-1.34/tests/test_aiff.py --- mutagen-1.33.2/tests/test_aiff.py 2016-06-23 09:18:49.000000000 +0000 +++ mutagen-1.34/tests/test_aiff.py 2016-07-07 16:58:13.000000000 +0000 @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- import os -import shutil -from tests import TestCase, DATA_DIR from mutagen._compat import cBytesIO from mutagen.aiff import AIFF, AIFFInfo, delete, IFFFile, IFFChunk from mutagen.aiff import error as AIFFError -from tempfile import mkstemp + +from tests import TestCase, DATA_DIR, get_temp_copy class TAIFF(TestCase): @@ -21,13 +20,8 @@ no_tags = os.path.join(DATA_DIR, '8k-1ch-1s-silence.aif') def setUp(self): - fd, self.filename_1 = mkstemp(suffix='.aif') - os.close(fd) - shutil.copy(self.has_tags, self.filename_1) - - fd, self.filename_2 = mkstemp(suffix='.aif') - os.close(fd) - shutil.copy(self.no_tags, self.filename_2) + self.filename_1 = get_temp_copy(self.has_tags) + self.filename_2 = get_temp_copy(self.no_tags) self.aiff_tmp_id3 = AIFF(self.filename_1) self.aiff_tmp_no_id3 = AIFF(self.filename_2) @@ -189,15 +183,11 @@ self.file_2 = open(self.no_tags, 'rb') self.iff_2 = IFFFile(self.file_2) - fd_1, self.tmp_1_name = mkstemp(suffix='.aif') - os.close(fd_1) - shutil.copy(self.has_tags, self.tmp_1_name) + self.tmp_1_name = get_temp_copy(self.has_tags) self.file_1_tmp = open(self.tmp_1_name, 'rb+') self.iff_1_tmp = IFFFile(self.file_1_tmp) - fd_2, self.tmp_2_name = mkstemp(suffix='.aif') - os.close(fd_2) - shutil.copy(self.no_tags, self.tmp_2_name) + self.tmp_2_name = get_temp_copy(self.no_tags) self.file_2_tmp = open(self.tmp_2_name, 'rb+') self.iff_2_tmp = IFFFile(self.file_2_tmp) diff -Nru mutagen-1.33.2/tests/test_apev2.py mutagen-1.34/tests/test_apev2.py --- mutagen-1.33.2/tests/test_apev2.py 2016-06-23 16:00:27.000000000 +0000 +++ mutagen-1.34/tests/test_apev2.py 2016-07-07 16:41:37.000000000 +0000 @@ -5,9 +5,7 @@ import os import shutil -from tempfile import mkstemp - -from tests import TestCase, DATA_DIR +from tests import TestCase, DATA_DIR, get_temp_copy import mutagen.apev2 from mutagen import MutagenError @@ -172,11 +170,12 @@ class TAPEv2(TestCase): def setUp(self): - fd, self.filename = mkstemp(".apev2") - os.close(fd) - shutil.copy(OLD, self.filename) + self.filename = get_temp_copy(OLD) self.audio = APEv2(self.filename) + def tearDown(self): + os.unlink(self.filename) + def test_invalid_key(self): self.failUnlessRaises( KeyError, self.audio.__setitem__, u"\u1234", "foo") @@ -270,9 +269,6 @@ def test_pprint(self): self.failUnless(self.audio.pprint()) - def tearDown(self): - os.unlink(self.filename) - class TAPEv2ThenID3v1(TAPEv2): diff -Nru mutagen-1.33.2/tests/test_asf.py mutagen-1.34/tests/test_asf.py --- mutagen-1.33.2/tests/test_asf.py 2016-06-29 18:09:37.000000000 +0000 +++ mutagen-1.34/tests/test_asf.py 2016-07-07 16:49:48.000000000 +0000 @@ -1,10 +1,7 @@ # -*- coding: utf-8 -*- import os -import shutil import warnings -from tempfile import mkstemp -from tests import TestCase, DATA_DIR from mutagen._compat import PY3, text_type, PY2, izip, cBytesIO from mutagen.asf import ASF, ASFHeaderError, ASFValue, UNICODE, DWORD, QWORD @@ -18,6 +15,8 @@ ASFBoolAttribute, ASFDWordAttribute, ASFQWordAttribute, ASFWordAttribute, \ ASFGUIDAttribute +from tests import TestCase, DATA_DIR, get_temp_copy + class TASFFile(TestCase): @@ -102,9 +101,7 @@ class TASF(TestCase): def setUp(self): - fd, self.filename = mkstemp(suffix='wma') - os.close(fd) - shutil.copy(self.original, self.filename) + self.filename = get_temp_copy(self.original) self.audio = ASF(self.filename) def tearDown(self): @@ -432,9 +429,7 @@ original = os.path.join(DATA_DIR, "issue_29.wma") def setUp(self): - fd, self.filename = mkstemp(suffix='wma') - os.close(fd) - shutil.copy(self.original, self.filename) + self.filename = get_temp_copy(self.original) self.audio = ASF(self.filename) def tearDown(self): @@ -476,9 +471,7 @@ original = os.path.join(DATA_DIR, "silence-1.wma") def setUp(self): - fd, self.filename = mkstemp(suffix='wma') - os.close(fd) - shutil.copy(self.original, self.filename) + self.filename = get_temp_copy(self.original) audio = ASF(self.filename) audio.clear() audio.save() @@ -554,9 +547,7 @@ original = os.path.join(DATA_DIR, "silence-1.wma") def setUp(self): - fd, self.filename = mkstemp(suffix='wma') - os.close(fd) - shutil.copy(self.original, self.filename) + self.filename = get_temp_copy(self.original) def tearDown(self): os.unlink(self.filename) @@ -611,9 +602,7 @@ original = os.path.join(DATA_DIR, "silence-1.wma") def setUp(self): - fd, self.filename = mkstemp(suffix='.wma') - os.close(fd) - shutil.copy(self.original, self.filename) + self.filename = get_temp_copy(self.original) self.audio = ASF(self.filename) def tearDown(self): diff -Nru mutagen-1.33.2/tests/test_easyid3.py mutagen-1.34/tests/test_easyid3.py --- mutagen-1.33.2/tests/test_easyid3.py 2016-06-28 20:33:33.000000000 +0000 +++ mutagen-1.34/tests/test_easyid3.py 2016-07-12 05:19:40.000000000 +0000 @@ -1,26 +1,33 @@ # -*- coding: utf-8 -*- import os -import shutil import pickle -from tests import TestCase, DATA_DIR + from mutagen import MutagenError from mutagen.id3 import ID3FileType, ID3, RVA2 from mutagen.easyid3 import EasyID3, error as ID3Error from mutagen._compat import PY3 -from tempfile import mkstemp + +from tests import TestCase, DATA_DIR, get_temp_copy class TEasyID3(TestCase): def setUp(self): - fd, self.filename = mkstemp('.mp3') - os.close(fd) - empty = os.path.join(DATA_DIR, 'emptyfile.mp3') - shutil.copy(empty, self.filename) + self.filename = get_temp_copy(os.path.join(DATA_DIR, 'emptyfile.mp3')) self.id3 = EasyID3() self.realid3 = self.id3._EasyID3__id3 + def tearDown(self): + os.unlink(self.filename) + + def test_txxx_latin_first_then_non_latin(self): + self.id3["performer"] = [u"foo"] + self.id3["performer"] = [u"\u0243"] + self.id3.save(self.filename) + new = EasyID3(self.filename) + self.assertEqual(new["performer"], [u"\u0243"]) + def test_remember_ctr(self): empty = os.path.join(DATA_DIR, 'emptyfile.mp3') mp3 = ID3FileType(empty, ID3=EasyID3) @@ -384,6 +391,3 @@ self.id3.save(self.filename) id3 = EasyID3(self.filename) self.failUnlessEqual(id3[tag], [u"foo"]) - - def tearDown(self): - os.unlink(self.filename) diff -Nru mutagen-1.33.2/tests/test_easymp4.py mutagen-1.34/tests/test_easymp4.py --- mutagen-1.33.2/tests/test_easymp4.py 2016-06-21 13:58:20.000000000 +0000 +++ mutagen-1.34/tests/test_easymp4.py 2016-07-07 16:33:36.000000000 +0000 @@ -1,23 +1,23 @@ # -*- coding: utf-8 -*- import os -import shutil -from tests import TestCase, DATA_DIR + from mutagen import MutagenError from mutagen.easymp4 import EasyMP4, error as MP4Error -from tempfile import mkstemp + +from tests import TestCase, DATA_DIR, get_temp_copy class TEasyMP4(TestCase): def setUp(self): - fd, self.filename = mkstemp('.mp4') - os.close(fd) - empty = os.path.join(DATA_DIR, 'has-tags.m4a') - shutil.copy(empty, self.filename) + self.filename = get_temp_copy(os.path.join(DATA_DIR, 'has-tags.m4a')) self.mp4 = EasyMP4(self.filename) self.mp4.delete() + def tearDown(self): + os.unlink(self.filename) + def test_pprint(self): self.mp4["artist"] = "baz" self.mp4.pprint() @@ -146,6 +146,3 @@ self.failUnlessRaises( ValueError, self.mp4.__setitem__, tag, "hello") - - def tearDown(self): - os.unlink(self.filename) diff -Nru mutagen-1.33.2/tests/test_flac.py mutagen-1.34/tests/test_flac.py --- mutagen-1.33.2/tests/test_flac.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/tests/test_flac.py 2016-07-07 16:53:06.000000000 +0000 @@ -1,17 +1,15 @@ # -*- coding: utf-8 -*- -import shutil import os import subprocess -from tempfile import mkstemp - -from tests import TestCase, DATA_DIR from mutagen import MutagenError from mutagen.id3 import ID3, TIT2, ID3NoHeaderError from mutagen.flac import to_int_be, Padding, VCFLACDict, MetadataBlock, error from mutagen.flac import StreamInfo, SeekTable, CueSheet, FLAC, delete, Picture from mutagen._compat import PY3 + +from tests import TestCase, DATA_DIR, get_temp_copy from tests.test__vorbis import TVCommentDict, VComment @@ -284,9 +282,7 @@ SAMPLE = os.path.join(DATA_DIR, "silence-44-s.flac") def setUp(self): - fd, self.NEW = mkstemp(".flac") - os.close(fd) - shutil.copy(self.SAMPLE, self.NEW) + self.NEW = get_temp_copy(self.SAMPLE) self.flac = FLAC(self.NEW) def tearDown(self): @@ -365,8 +361,9 @@ def test_write_nochange(self): f = FLAC(self.NEW) f.save() - self.failUnlessEqual(open(self.SAMPLE, "rb").read(), - open(self.NEW, "rb").read()) + with open(self.SAMPLE, "rb") as a: + with open(self.NEW, "rb") as b: + self.failUnlessEqual(a.read(), b.read()) def test_write_changetitle(self): f = FLAC(self.NEW) @@ -586,9 +583,7 @@ TOO_SHORT = os.path.join(DATA_DIR, "52-too-short-block-size.flac") def setUp(self): - fd, self.NEW = mkstemp(".flac") - os.close(fd) - shutil.copy(self.TOO_SHORT, self.NEW) + self.NEW = get_temp_copy(self.TOO_SHORT) def tearDown(self): os.unlink(self.NEW) @@ -599,16 +594,16 @@ flac.save() flac2 = FLAC(self.NEW) self.failUnlessEqual(flac["title"], flac2["title"]) - data = open(self.NEW, "rb").read(1024) + with open(self.NEW, "rb") as h: + data = h.read(1024) self.failIf(b"Tunng" in data) class TFLACBadBlockSizeOverflow(TestCase): def setUp(self): - fd, self.filename = mkstemp(".flac") - os.close(fd) - shutil.copy(os.path.join(DATA_DIR, "silence-44-s.flac"), self.filename) + self.filename = get_temp_copy( + os.path.join(DATA_DIR, "silence-44-s.flac")) def tearDown(self): os.unlink(self.filename) diff -Nru mutagen-1.33.2/tests/test__id3frames.py mutagen-1.34/tests/test__id3frames.py --- mutagen-1.33.2/tests/test__id3frames.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/tests/test__id3frames.py 2016-07-18 22:30:33.000000000 +0000 @@ -1,28 +1,564 @@ # -*- coding: utf-8 -*- -from tests import TestCase - -from mutagen.id3 import Frames, Frames_2_2, ID3, ID3Header -from mutagen._compat import text_type, xrange, PY2 -from mutagen.id3 import APIC - -_22 = ID3() -_22._header = ID3Header() -_22._header.version = (2, 2, 0) +import operator -_23 = ID3() -_23._header = ID3Header() -_23._header.version = (2, 3, 0) +from tests import TestCase -_24 = ID3() -_24._header = ID3Header() -_24._header.version = (2, 4, 0) +from mutagen._compat import text_type, xrange, PY2, PY3, iteritems, izip, \ + integer_types +from mutagen._constants import GENRES +from mutagen.id3._tags import read_frames, save_frame, ID3Header +from mutagen.id3._util import ID3SaveConfig, is_valid_frame_id, \ + ID3JunkFrameError +from mutagen.id3 import APIC, CTOC, CHAP, TPE2, Frames, Frames_2_2, CRA, \ + AENC, PIC, LNK, LINK, SIGN, PRIV, GRID, ENCR, COMR, USER, UFID, GEOB, \ + POPM, EQU2, RVA2, COMM, SYLT, USLT, WXXX, TXXX, WCOM, TextFrame, \ + UrlFrame, NumericTextFrame, NumericPartTextFrame, TPE1, TIT2, \ + TimeStampTextFrame, TCON, ID3TimeStamp, Frame, RVRB, RBUF, CTOCFlags, \ + PairedTextFrame, BinaryFrame, ETCO, MLLT, SYTC, PCNT, PCST, POSS, OWNE, \ + SEEK, ASPI, PictureType, CRM, RVAD, RVA, ID3Tags + +_22 = ID3Header() +_22.version = (2, 2, 0) + +_23 = ID3Header() +_23.version = (2, 3, 0) + +_24 = ID3Header() +_24.version = (2, 4, 0) + + +class TVariousFrames(TestCase): + + DATA = [ + ['TALB', b'\x00a/b', 'a/b', '', dict(encoding=0)], + ['TBPM', b'\x00120', '120', 120, dict(encoding=0)], + ['TCMP', b'\x001', '1', 1, dict(encoding=0)], + ['TCMP', b'\x000', '0', 0, dict(encoding=0)], + ['TCOM', b'\x00a/b', 'a/b', '', dict(encoding=0)], + ['TCON', b'\x00(21)Disco', '(21)Disco', '', dict(encoding=0)], + ['TCOP', b'\x001900 c', '1900 c', '', dict(encoding=0)], + ['TDAT', b'\x00a/b', 'a/b', '', dict(encoding=0)], + ['TDEN', b'\x001987', '1987', '', dict(encoding=0, year=[1987])], + [ + 'TDOR', b'\x001987-12', '1987-12', '', + dict(encoding=0, year=[1987], month=[12]) + ], + ['TDRC', b'\x001987\x00', '1987', '', dict(encoding=0, year=[1987])], + [ + 'TDRL', b'\x001987\x001988', '1987,1988', '', + dict(encoding=0, year=[1987, 1988]) + ], + ['TDTG', b'\x001987', '1987', '', dict(encoding=0, year=[1987])], + ['TDLY', b'\x001205', '1205', 1205, dict(encoding=0)], + ['TENC', b'\x00a b/c d', 'a b/c d', '', dict(encoding=0)], + ['TEXT', b'\x00a b\x00c d', ['a b', 'c d'], '', dict(encoding=0)], + ['TFLT', b'\x00MPG/3', 'MPG/3', '', dict(encoding=0)], + ['TIME', b'\x001205', '1205', '', dict(encoding=0)], + [ + 'TIPL', b'\x02\x00a\x00\x00\x00b', [["a", "b"]], '', + dict(encoding=2) + ], + ['TIT1', b'\x00a/b', 'a/b', '', dict(encoding=0)], + # TIT2 checks misaligned terminator '\x00\x00' across crosses utf16 + # chars + [ + 'TIT2', b'\x01\xff\xfe\x38\x00\x00\x38', u'8\u3800', '', + dict(encoding=1) + ], + ['TIT3', b'\x00a/b', 'a/b', '', dict(encoding=0)], + ['TKEY', b'\x00A#m', 'A#m', '', dict(encoding=0)], + ['TLAN', b'\x006241', '6241', '', dict(encoding=0)], + ['TLEN', b'\x006241', '6241', 6241, dict(encoding=0)], + [ + 'TMCL', b'\x02\x00a\x00\x00\x00b', [["a", "b"]], '', + dict(encoding=2) + ], + ['TMED', b'\x00med', 'med', '', dict(encoding=0)], + ['TMOO', b'\x00moo', 'moo', '', dict(encoding=0)], + ['TOAL', b'\x00alb', 'alb', '', dict(encoding=0)], + ['TOFN', b'\x0012 : bar', '12 : bar', '', dict(encoding=0)], + ['TOLY', b'\x00lyr', 'lyr', '', dict(encoding=0)], + ['TOPE', b'\x00own/lic', 'own/lic', '', dict(encoding=0)], + ['TORY', b'\x001923', '1923', 1923, dict(encoding=0)], + ['TOWN', b'\x00own/lic', 'own/lic', '', dict(encoding=0)], + ['TPE1', b'\x00ab', ['ab'], '', dict(encoding=0)], + [ + 'TPE2', b'\x00ab\x00cd\x00ef', ['ab', 'cd', 'ef'], '', + dict(encoding=0) + ], + ['TPE3', b'\x00ab\x00cd', ['ab', 'cd'], '', dict(encoding=0)], + ['TPE4', b'\x00ab\x00', ['ab'], '', dict(encoding=0)], + ['TPOS', b'\x0008/32', '08/32', 8, dict(encoding=0)], + ['TPRO', b'\x00pro', 'pro', '', dict(encoding=0)], + ['TPUB', b'\x00pub', 'pub', '', dict(encoding=0)], + ['TRCK', b'\x004/9', '4/9', 4, dict(encoding=0)], + ['TRDA', b'\x00Sun Jun 12', 'Sun Jun 12', '', dict(encoding=0)], + ['TRSN', b'\x00ab/cd', 'ab/cd', '', dict(encoding=0)], + ['TRSO', b'\x00ab', 'ab', '', dict(encoding=0)], + ['TSIZ', b'\x0012345', '12345', 12345, dict(encoding=0)], + ['TSOA', b'\x00ab', 'ab', '', dict(encoding=0)], + ['TSOP', b'\x00ab', 'ab', '', dict(encoding=0)], + ['TSOT', b'\x00ab', 'ab', '', dict(encoding=0)], + ['TSO2', b'\x00ab', 'ab', '', dict(encoding=0)], + ['TSOC', b'\x00ab', 'ab', '', dict(encoding=0)], + ['TSRC', b'\x0012345', '12345', '', dict(encoding=0)], + ['TSSE', b'\x0012345', '12345', '', dict(encoding=0)], + ['TSST', b'\x0012345', '12345', '', dict(encoding=0)], + ['TYER', b'\x002004', '2004', 2004, dict(encoding=0)], + [ + 'TXXX', b'\x00usr\x00a/b\x00c', ['a/b', 'c'], '', + dict(encoding=0, desc='usr') + ], + ['WCOM', b'http://foo', 'http://foo', '', {}], + ['WCOP', b'http://bar', 'http://bar', '', {}], + ['WOAF', b'http://baz', 'http://baz', '', {}], + ['WOAR', b'http://bar', 'http://bar', '', {}], + ['WOAS', b'http://bar', 'http://bar', '', {}], + ['WORS', b'http://bar', 'http://bar', '', {}], + ['WPAY', b'http://bar', 'http://bar', '', {}], + ['WPUB', b'http://bar', 'http://bar', '', {}], + ['WXXX', b'\x00usr\x00http', 'http', '', dict(encoding=0, desc='usr')], + [ + 'IPLS', b'\x00a\x00A\x00b\x00B\x00', [['a', 'A'], ['b', 'B']], '', + dict(encoding=0) + ], + ['MCDI', b'\x01\x02\x03\x04', b'\x01\x02\x03\x04', '', {}], + [ + 'ETCO', b'\x01\x12\x00\x00\x7f\xff', [(18, 32767)], '', + dict(format=1) + ], + [ + 'COMM', b'\x00ENUT\x00Com', 'Com', '', + dict(desc='T', lang='ENU', encoding=0) + ], + [ + 'APIC', b'\x00-->\x00\x03cover\x00cover.jpg', b'cover.jpg', '', + dict(mime='-->', type=3, desc='cover', encoding=0) + ], + ['USER', b'\x00ENUCom', 'Com', '', dict(lang='ENU', encoding=0)], + [ + 'RVA2', b'testdata\x00\x01\xfb\x8c\x10\x12\x23', + 'Master volume: -2.2266 dB/0.1417', '', + dict(desc='testdata', channel=1, gain=-2.22656, peak=0.14169) + ], + [ + 'RVA2', b'testdata\x00\x01\xfb\x8c\x24\x01\x22\x30\x00\x00', + 'Master volume: -2.2266 dB/0.1417', '', + dict(desc='testdata', channel=1, gain=-2.22656, peak=0.14169) + ], + [ + 'RVA2', b'testdata2\x00\x01\x04\x01\x00', + 'Master volume: +2.0020 dB/0.0000', '', + dict(desc='testdata2', channel=1, gain=2.001953125, peak=0) + ], + ['PCNT', b'\x00\x00\x00\x11', 17, 17, dict(count=17)], + [ + 'POPM', b'foo@bar.org\x00\xde\x00\x00\x00\x11', 222, 222, + dict(email="foo@bar.org", rating=222, count=17) + ], + [ + 'POPM', b'foo@bar.org\x00\xde\x00', 222, 222, + dict(email="foo@bar.org", rating=222, count=0) + ], + # Issue #33 - POPM may have no playcount at all. + [ + 'POPM', b'foo@bar.org\x00\xde', 222, 222, + dict(email="foo@bar.org", rating=222) + ], + ['UFID', b'own\x00data', b'data', '', dict(data=b'data', owner='own')], + ['UFID', b'own\x00\xdd', b'\xdd', '', dict(data=b'\xdd', owner='own')], + [ + 'GEOB', b'\x00mime\x00name\x00desc\x00data', b'data', '', + dict(encoding=0, mime='mime', filename='name', desc='desc') + ], + [ + 'USLT', b'\x00engsome lyrics\x00woo\nfun', 'woo\nfun', '', + dict(encoding=0, lang='eng', desc='some lyrics', text='woo\nfun') + ], + [ + 'SYLT', (b'\x00eng\x02\x01some lyrics\x00foo\x00\x00\x00\x00\x01' + b'bar\x00\x00\x00\x00\x10'), "foobar", '', + dict(encoding=0, lang='eng', type=1, format=2, desc='some lyrics') + ], + ['POSS', b'\x01\x0f', 15, 15, dict(format=1, position=15)], + [ + 'OWNE', b'\x00USD10.01\x0020041010CDBaby', 'CDBaby', 'CDBaby', + dict(encoding=0, price="USD10.01", date='20041010', + seller='CDBaby') + ], + [ + 'PRIV', b'a@b.org\x00random data', b'random data', 'random data', + dict(owner='a@b.org', data=b'random data') + ], + [ + 'PRIV', b'a@b.org\x00\xdd', b'\xdd', '\xdd', + dict(owner='a@b.org', data=b'\xdd') + ], + ['SIGN', b'\x92huh?', b'huh?', 'huh?', dict(group=0x92, sig=b'huh?')], + [ + 'ENCR', b'a@b.org\x00\x92Data!', b'Data!', 'Data!', + dict(owner='a@b.org', method=0x92, data=b'Data!') + ], + [ + 'SEEK', b'\x00\x12\x00\x56', + 0x12 * 256 * 256 + 0x56, 0x12 * 256 * 256 + 0x56, + dict(offset=0x12 * 256 * 256 + 0x56) + ], + [ + 'SYTC', b"\x01\x10obar", b'\x10obar', '', + dict(format=1, data=b'\x10obar') + ], + [ + 'RBUF', b'\x00\x12\x00', 0x12 * 256, 0x12 * 256, + dict(size=0x12 * 256) + ], + [ + 'RBUF', b'\x00\x12\x00\x01', 0x12 * 256, 0x12 * 256, + dict(size=0x12 * 256, info=1) + ], + [ + 'RBUF', b'\x00\x12\x00\x01\x00\x00\x00\x23', + 0x12 * 256, 0x12 * 256, + dict(size=0x12 * 256, info=1, offset=0x23) + ], + [ + 'RVRB', b'\x12\x12\x23\x23\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11', + (0x12 * 256 + 0x12, 0x23 * 256 + 0x23), '', + dict(left=0x12 * 256 + 0x12, right=0x23 * 256 + 0x23) + ], + [ + 'AENC', b'a@b.org\x00\x00\x12\x00\x23', 'a@b.org', 'a@b.org', + dict(owner='a@b.org', preview_start=0x12, preview_length=0x23) + ], + [ + 'AENC', b'a@b.org\x00\x00\x12\x00\x23!', 'a@b.org', 'a@b.org', + dict(owner='a@b.org', preview_start=0x12, + preview_length=0x23, data=b'!') + ], + [ + 'GRID', b'a@b.org\x00\x99', 'a@b.org', 0x99, + dict(owner='a@b.org', group=0x99) + ], + [ + 'GRID', b'a@b.org\x00\x99data', 'a@b.org', 0x99, + dict(owner='a@b.org', group=0x99, data=b'data') + ], + [ + 'COMR', + (b'\x00USD10.00\x0020051010ql@sc.net\x00\x09Joe\x00A song\x00' + b'x-image/fake\x00some data'), + COMR(encoding=0, price="USD10.00", valid_until="20051010", + contact="ql@sc.net", format=9, seller="Joe", desc="A song", + mime='x-image/fake', logo=b'some data'), '', + dict(encoding=0, price="USD10.00", valid_until="20051010", + contact="ql@sc.net", format=9, seller="Joe", desc="A song", + mime='x-image/fake', logo=b'some data') + ], + [ + 'COMR', + b'\x00USD10.00\x0020051010ql@sc.net\x00\x09Joe\x00A song\x00', + COMR(encoding=0, price="USD10.00", valid_until="20051010", + contact="ql@sc.net", format=9, seller="Joe", desc="A song"), + '', + dict(encoding=0, price="USD10.00", valid_until="20051010", + contact="ql@sc.net", format=9, seller="Joe", desc="A song") + ], + [ + 'MLLT', b'\x00\x01\x00\x00\x02\x00\x00\x03\x04\x08foobar', + b'foobar', '', + dict(frames=1, bytes=2, milliseconds=3, bits_for_bytes=4, + bits_for_milliseconds=8, data=b'foobar') + ], + [ + 'EQU2', b'\x00Foobar\x00\x01\x01\x04\x00', [(128.5, 2.0)], '', + dict(method=0, desc="Foobar") + ], + [ + 'ASPI', + b'\x00\x00\x00\x00\x00\x00\x00\x10\x00\x03\x08\x01\x02\x03', + [1, 2, 3], '', dict(S=0, L=16, N=3, b=8) + ], + [ + 'ASPI', b'\x00\x00\x00\x00\x00\x00\x00\x10\x00\x03\x10' + b'\x00\x01\x00\x02\x00\x03', [1, 2, 3], '', + dict(S=0, L=16, N=3, b=16) + ], + [ + 'LINK', b'TIT1http://www.example.org/TIT1.txt\x00', + ("TIT1", 'http://www.example.org/TIT1.txt'), '', + dict(frameid='TIT1', url='http://www.example.org/TIT1.txt') + ], + [ + 'LINK', b'COMMhttp://www.example.org/COMM.txt\x00engfoo', + ("COMM", 'http://www.example.org/COMM.txt', b'engfoo'), '', + dict(frameid='COMM', url='http://www.example.org/COMM.txt', + data=b'engfoo') + ], + # iTunes podcast frames + ['TGID', b'\x00i', u'i', '', dict(encoding=0)], + ['TDES', b'\x00ii', u'ii', '', dict(encoding=0)], + ['TKWD', b'\x00ii', u'ii', '', dict(encoding=0)], + ['TCAT', b'\x00ii', u'ii', '', dict(encoding=0)], + ['WFED', b'http://zzz', 'http://zzz', '', {}], + ['PCST', b'\x00\x00\x00\x00', 0, 0, dict(value=0)], + + # Chapter extension + ['CHAP', (b'foo\x00\x11\x11\x11\x11\x22\x22\x22\x22' + b'\x33\x33\x33\x33\x44\x44\x44\x44'), + CHAP(element_id=u'foo', start_time=286331153, end_time=572662306, + start_offset=858993459, end_offset=1145324612), '', dict()], + ['CTOC', b'foo\x00\x03\x01bla\x00', + CTOC(element_id=u'foo', + flags=CTOCFlags.ORDERED | CTOCFlags.TOP_LEVEL, + child_element_ids=[u'bla']), + '', dict()], + + ['RVAD', b'\x03\x10\x00\x00\x00\x00', + RVAD(adjustments=[0, 0]), '', dict()], + ['RVAD', b'\x03\x08\x00\x01\x02\x03\x04\x05\x06\x07\x00\x00\x00\x00', + RVAD(adjustments=[0, 1, 2, 3, -4, -5, 6, 7, 0, 0, 0, 0]), '', dict()], + + # 2.2 tags + ['RVA', b'\x03\x10\x00\x00\x00\x00', + RVA(adjustments=[0, 0]), '', dict()], + ['UFI', b'own\x00data', b'data', '', dict(data=b'data', owner='own')], + [ + 'SLT', (b'\x00eng\x02\x01some lyrics\x00foo\x00\x00\x00\x00\x01bar' + b'\x00\x00\x00\x00\x10'), + "foobar", '', + dict(encoding=0, lang='eng', type=1, format=2, desc='some lyrics') + ], + ['TT1', b'\x00ab\x00', 'ab', '', dict(encoding=0)], + ['TT2', b'\x00ab', 'ab', '', dict(encoding=0)], + ['TT3', b'\x00ab', 'ab', '', dict(encoding=0)], + ['TP1', b'\x00ab\x00', 'ab', '', dict(encoding=0)], + ['TP2', b'\x00ab', 'ab', '', dict(encoding=0)], + ['TP3', b'\x00ab', 'ab', '', dict(encoding=0)], + ['TP4', b'\x00ab', 'ab', '', dict(encoding=0)], + ['TCM', b'\x00ab/cd', 'ab/cd', '', dict(encoding=0)], + ['TXT', b'\x00lyr', 'lyr', '', dict(encoding=0)], + ['TLA', b'\x00ENU', 'ENU', '', dict(encoding=0)], + ['TCO', b'\x00gen', 'gen', '', dict(encoding=0)], + ['TAL', b'\x00alb', 'alb', '', dict(encoding=0)], + ['TPA', b'\x001/9', '1/9', 1, dict(encoding=0)], + ['TRK', b'\x002/8', '2/8', 2, dict(encoding=0)], + ['TRC', b'\x00isrc', 'isrc', '', dict(encoding=0)], + ['TYE', b'\x001900', '1900', 1900, dict(encoding=0)], + ['TDA', b'\x002512', '2512', '', dict(encoding=0)], + ['TIM', b'\x001225', '1225', '', dict(encoding=0)], + ['TRD', b'\x00Jul 17', 'Jul 17', '', dict(encoding=0)], + ['TMT', b'\x00DIG/A', 'DIG/A', '', dict(encoding=0)], + ['TFT', b'\x00MPG/3', 'MPG/3', '', dict(encoding=0)], + ['TBP', b'\x00133', '133', 133, dict(encoding=0)], + ['TCP', b'\x001', '1', 1, dict(encoding=0)], + ['TCP', b'\x000', '0', 0, dict(encoding=0)], + ['TCR', b'\x00Me', 'Me', '', dict(encoding=0)], + ['TPB', b'\x00Him', 'Him', '', dict(encoding=0)], + ['TEN', b'\x00Lamer', 'Lamer', '', dict(encoding=0)], + ['TSS', b'\x00ab', 'ab', '', dict(encoding=0)], + ['TOF', b'\x00ab:cd', 'ab:cd', '', dict(encoding=0)], + ['TLE', b'\x0012', '12', 12, dict(encoding=0)], + ['TSI', b'\x0012', '12', 12, dict(encoding=0)], + ['TDY', b'\x0012', '12', 12, dict(encoding=0)], + ['TKE', b'\x00A#m', 'A#m', '', dict(encoding=0)], + ['TOT', b'\x00org', 'org', '', dict(encoding=0)], + ['TOA', b'\x00org', 'org', '', dict(encoding=0)], + ['TOL', b'\x00org', 'org', '', dict(encoding=0)], + ['TOR', b'\x001877', '1877', 1877, dict(encoding=0)], + ['TXX', b'\x00desc\x00val', 'val', '', dict(encoding=0, desc='desc')], + ['TSC', b'\x00ab', 'ab', '', dict(encoding=0)], + ['TSA', b'\x00ab', 'ab', '', dict(encoding=0)], + ['TS2', b'\x00ab', 'ab', '', dict(encoding=0)], + ['TST', b'\x00ab', 'ab', '', dict(encoding=0)], + ['TSP', b'\x00ab', 'ab', '', dict(encoding=0)], + + ['WAF', b'http://zzz', 'http://zzz', '', {}], + ['WAR', b'http://zzz', 'http://zzz', '', {}], + ['WAS', b'http://zzz', 'http://zzz', '', {}], + ['WCM', b'http://zzz', 'http://zzz', '', {}], + ['WCP', b'http://zzz', 'http://zzz', '', {}], + ['WPB', b'http://zzz', 'http://zzz', '', {}], + [ + 'WXX', b'\x00desc\x00http', 'http', '', + dict(encoding=0, desc='desc') + ], + [ + 'IPL', b'\x00a\x00A\x00b\x00B\x00', [['a', 'A'], ['b', 'B']], '', + dict(encoding=0) + ], + ['MCI', b'\x01\x02\x03\x04', b'\x01\x02\x03\x04', '', {}], + [ + 'ETC', b'\x01\x12\x00\x00\x7f\xff', [(18, 32767)], '', + dict(format=1) + ], + [ + 'COM', b'\x00ENUT\x00Com', 'Com', '', + dict(desc='T', lang='ENU', encoding=0) + ], + [ + 'PIC', b'\x00-->\x03cover\x00cover.jpg', b'cover.jpg', '', + dict(mime='-->', type=3, desc='cover', encoding=0) + ], + [ + 'POP', b'foo@bar.org\x00\xde\x00\x00\x00\x11', 222, 222, + dict(email="foo@bar.org", rating=222, count=17) + ], + ['CNT', b'\x00\x00\x00\x11', 17, 17, dict(count=17)], + [ + 'GEO', b'\x00mime\x00name\x00desc\x00data', b'data', '', + dict(encoding=0, mime='mime', filename='name', desc='desc') + ], + [ + 'ULT', b'\x00engsome lyrics\x00woo\nfun', 'woo\nfun', '', + dict(encoding=0, lang='eng', desc='some lyrics', text='woo\nfun')], + [ + 'BUF', b'\x00\x12\x00', 0x12 * 256, 0x12 * 256, + dict(size=0x12 * 256) + ], + [ + 'CRA', b'a@b.org\x00\x00\x12\x00\x23', 'a@b.org', 'a@b.org', + dict(owner='a@b.org', preview_start=0x12, preview_length=0x23) + ], + [ + 'CRA', b'a@b.org\x00\x00\x12\x00\x23!', 'a@b.org', 'a@b.org', + dict(owner='a@b.org', preview_start=0x12, + preview_length=0x23, data=b'!') + ], + [ + 'REV', b'\x12\x12\x23\x23\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11', + (0x12 * 256 + 0x12, 0x23 * 256 + 0x23), '', + dict(left=0x12 * 256 + 0x12, right=0x23 * 256 + 0x23) + ], + [ + 'STC', b"\x01\x10obar", b'\x10obar', '', + dict(format=1, data=b'\x10obar') + ], + [ + 'MLL', b'\x00\x01\x00\x00\x02\x00\x00\x03\x04\x08foobar', + b'foobar', '', + dict(frames=1, bytes=2, milliseconds=3, bits_for_bytes=4, + bits_for_milliseconds=8, data=b'foobar') + ], + [ + 'LNK', b'TT1http://www.example.org/TIT1.txt\x00', + ("TT1", 'http://www.example.org/TIT1.txt'), '', + dict(frameid='TT1', url='http://www.example.org/TIT1.txt') + ], + [ + 'CRM', b'foo@example.org\x00test\x00woo', b'woo', '', + dict(owner='foo@example.org', desc='test', data=b'woo') + ], + ] + + def _get_frame(self, id_): + return getattr(getattr(__import__('mutagen.id3'), "id3"), id_) + + def test_all_tested(self): + check = dict.fromkeys(list(Frames.keys()) + list(Frames_2_2.keys())) + tested = [l[0] for l in self.DATA] + for t in tested: + check.pop(t, None) + self.assertEqual(list(check.keys()), []) + + def test_tag_repr(self): + for frame_id, data, value, intval, info in self.DATA: + kind = self._get_frame(frame_id) + tag = kind._fromData(_23, 0, data) + self.assertTrue(isinstance(tag.__str__(), str)) + self.assertTrue(isinstance(tag.__repr__(), str)) + if PY2: + if hasattr(tag, "__unicode__"): + self.assertTrue(isinstance(tag.__unicode__(), unicode)) + else: + if hasattr(tag, "__bytes__"): + self.assertTrue(isinstance(tag.__bytes__(), bytes)) + + def test_tag_write(self): + for frame_id, data, value, intval, info in self.DATA: + kind = self._get_frame(frame_id) + tag = kind._fromData(_24, 0, data) + towrite = tag._writeData() + tag2 = kind._fromData(_24, 0, towrite) + for spec in kind._framespec: + attr = spec.name + self.assertEquals(getattr(tag, attr), getattr(tag2, attr)) + for spec in kind._optionalspec: + attr = spec.name + other = object() + self.assertEquals( + getattr(tag, attr, other), getattr(tag2, attr, other)) + + def test_tag_write_v23(self): + for frame_id, data, value, intval, info in self.DATA: + kind = self._get_frame(frame_id) + tag = kind._fromData(_24, 0, data) + config = ID3SaveConfig(3, "/") + towrite = tag._writeData(config) + tag2 = kind._fromData(_23, 0, towrite) + tag3 = kind._fromData(_23, 0, tag2._writeData(config)) + for spec in kind._framespec: + attr = spec.name + self.assertEquals(getattr(tag2, attr), getattr(tag3, attr)) + for spec in kind._optionalspec: + attr = spec.name + other = object() + self.assertEquals( + getattr(tag2, attr, other), getattr(tag3, attr, other)) + self.assertEqual(hasattr(tag, attr), hasattr(tag2, attr)) + + def test_tag(self): + for frame_id, data, value, intval, info in self.DATA: + kind = self._get_frame(frame_id) + tag = kind._fromData(_23, 0, data) + self.failUnless(tag.HashKey) + self.failUnless(tag.pprint()) + self.assertEquals(value, tag) + if 'encoding' not in info: + self.assertRaises(AttributeError, getattr, tag, 'encoding') + for attr, value in iteritems(info): + t = tag + if not isinstance(value, list): + value = [value] + t = [t] + for value, t in izip(value, iter(t)): + if isinstance(value, float): + self.failUnlessAlmostEqual(value, getattr(t, attr), 5) + else: + self.assertEquals(value, getattr(t, attr)) + + if isinstance(intval, integer_types): + self.assertEquals(intval, operator.pos(t)) + else: + self.assertRaises(TypeError, operator.pos, t) + + +class TPCST(TestCase): + + def test_default(self): + frame = PCST() + self.assertEqual(frame.value, 0) + + +class TETCO(TestCase): + + def test_default(self): + frame = ETCO() + self.assertEqual(frame.format, 1) + self.assertEqual(frame.events, []) + + +class TSYTC(TestCase): + + def test_default(self): + frame = SYTC() + self.assertEqual(frame.format, 1) + self.assertEqual(frame.data, b"") -class FrameSanityChecks(TestCase): +class TCRA(TestCase): - def test_CRA_upgrade(self): - from mutagen.id3 import CRA, AENC + def test_upgrade(self): frame = CRA(owner="a", preview_start=1, preview_length=2, data=b"foo") new = AENC(frame) self.assertEqual(new.owner, "a") @@ -34,8 +570,18 @@ new = AENC(frame) self.assertFalse(hasattr(new, "data")) - def test_PIC_upgrade(self): - from mutagen.id3 import PIC + +class TPIC(TestCase): + + def test_default(self): + frame = PIC() + self.assertEqual(frame.encoding, 1) + self.assertEqual(frame.mime, u"JPG") + self.assertEqual(frame.type, PictureType.COVER_FRONT) + self.assertEqual(frame.desc, u"") + self.assertEqual(frame.data, b"") + + def test_upgrade(self): frame = PIC(encoding=0, mime="PNG", desc="bla", type=3, data=b"\x00") new = APIC(frame) self.assertEqual(new.encoding, 0) @@ -49,8 +595,15 @@ new = APIC(frame) self.assertEqual(new.mime, "foo") - def test_LNK_upgrade(self): - from mutagen.id3 import LNK, LINK + +class TLNK(TestCase): + + def test_default(self): + frame = LNK() + self.assertEqual(frame.frameid, u"XXX") + self.assertEqual(frame.url, u"") + + def test_upgrade(self): url = "http://foo.bar" frame = LNK(frameid="PIC", url=url, data=b"\x00") @@ -59,18 +612,44 @@ self.assertEqual(new.url, url) self.assertEqual(new.data, b"\x00") - frame = LNK(frameid="o_O") + frame = LNK(frameid="XYZ") new = LINK(frame) - self.assertEqual(new.frameid, "o_O ") + self.assertEqual(new.frameid, "XYZ ") + + +class TSIGN(TestCase): + + def test_default(self): + frame = SIGN() + self.assertEqual(frame.group, 0x80) + self.assertEqual(frame.sig, b"") - def test_SIGN(self): - from mutagen.id3 import SIGN + def test_hash(self): frame = SIGN(group=1, sig=b"foo") self.assertEqual(frame.HashKey, "SIGN:1:foo") + + def test_pprint(self): + frame = SIGN(group=1, sig=b"foo") frame._pprint() - def test_PRIV(self): - from mutagen.id3 import PRIV + +class TCRM(TestCase): + + def test_default(self): + frame = CRM() + self.assertEqual(frame.owner, u"") + self.assertEqual(frame.desc, u"") + self.assertEqual(frame.data, b"") + + +class TPRIV(TestCase): + + def test_default(self): + frame = PRIV() + self.assertEqual(frame.owner, u"") + self.assertEqual(frame.data, b"") + + def test_hash(self): frame = PRIV(owner="foo", data=b"foo") self.assertEqual(frame.HashKey, "PRIV:foo:foo") frame._pprint() @@ -79,23 +658,57 @@ self.assertEqual(frame.HashKey, u"PRIV:foo:\x00\xff") frame._pprint() - def test_GRID(self): - from mutagen.id3 import GRID +class TGRID(TestCase): + + def test_default(self): + frame = GRID() + self.assertEqual(frame.owner, u"") + self.assertEqual(frame.group, 0x80) + + def test_hash(self): frame = GRID(owner="foo", group=42) self.assertEqual(frame.HashKey, "GRID:42") frame._pprint() - def test_ENCR(self): - from mutagen.id3 import ENCR +class TENCR(TestCase): + + def test_default(self): + frame = ENCR() + self.assertEqual(frame.owner, u"") + self.assertEqual(frame.method, 0x80) + self.assertEqual(frame.data, b"") + + def test_hash(self): frame = ENCR(owner="foo", method=42, data=b"\xff") self.assertEqual(frame.HashKey, "ENCR:foo") frame._pprint() - def test_COMR(self): - from mutagen.id3 import COMR +class TOWNE(TestCase): + + def test_default(self): + frame = OWNE() + self.assertEqual(frame.encoding, 1) + self.assertEqual(frame.price, u"") + self.assertEqual(frame.date, u"19700101") + self.assertEqual(frame.seller, u"") + + +class TCOMR(TestCase): + + def test_default(self): + frame = COMR() + self.assertEqual(frame.encoding, 1) + self.assertEqual(frame.price, u"") + self.assertEqual(frame.valid_until, u"19700101") + self.assertEqual(frame.contact, u"") + self.assertEqual(frame.format, 0) + self.assertEqual(frame.seller, u"") + self.assertEqual(frame.desc, u"") + + def test_hash(self): frame = COMR( encoding=0, price="p", valid_until="v" * 8, contact="c", format=42, seller="s", desc="d", mime="m", logo=b"\xff") @@ -103,23 +716,104 @@ frame.HashKey, u"COMR:\x00p\x00vvvvvvvvc\x00*s\x00d\x00m\x00\xff") frame._pprint() - def test_USER(self): - from mutagen.id3 import USER +class TBinaryFrame(TestCase): + + def test_default(self): + frame = BinaryFrame() + self.assertEqual(frame.data, b"") + + +class TUSER(TestCase): + + def test_default(self): + frame = USER() + self.assertEqual(frame.encoding, 1) + self.assertEqual(frame.lang, u"XXX") + self.assertEqual(frame.text, u"") + + def test_hash(self): frame = USER(encoding=0, lang="foo", text="bla") self.assertEqual(frame.HashKey, "USER:foo") frame._pprint() - def test_UFID(self): - from mutagen.id3 import UFID + self.assertEquals(USER(text="a").HashKey, USER(text="b").HashKey) + self.assertNotEquals( + USER(lang="abc").HashKey, USER(lang="def").HashKey) + + +class TMLLT(TestCase): + + def test_default(self): + frame = MLLT() + self.assertEqual(frame.frames, 0) + self.assertEqual(frame.bytes, 0) + self.assertEqual(frame.milliseconds, 0) + self.assertEqual(frame.bits_for_bytes, 0) + self.assertEqual(frame.bits_for_milliseconds, 0) + self.assertEqual(frame.data, b"") + + +class TTIT2(TestCase): + + def test_hash(self): + self.assertEquals(TIT2(text="a").HashKey, TIT2(text="b").HashKey) + +class TUFID(TestCase): + + def test_default(self): + frame = UFID() + self.assertEqual(frame.owner, u"") + self.assertEqual(frame.data, b"") + + def test_hash(self): frame = UFID(owner="foo", data=b"\x42") self.assertEqual(frame.HashKey, "UFID:foo") frame._pprint() - def test_LINK(self): - from mutagen.id3 import LINK + self.assertEquals(UFID(data=b"1").HashKey, UFID(data=b"2").HashKey) + self.assertNotEquals(UFID(owner="a").HashKey, UFID(owner="b").HashKey) + + +class TPairedTextFrame(TestCase): + + def test_default(self): + frame = PairedTextFrame() + self.assertEqual(frame.encoding, 1) + self.assertEqual(frame.people, []) + +class TRVAD(TestCase): + + def test_default(self): + frame = RVAD() + self.assertEqual(frame.adjustments, [0, 0]) + + def test_hash(self): + frame = RVAD() + self.assertEqual(frame.HashKey, "RVAD") + + def test_upgrade(self): + rva = RVA(adjustments=[1, 2]) + self.assertEqual(RVAD(rva).adjustments, [1, 2]) + + +class TLINK(TestCase): + + def test_read(self): + frame = LINK() + frame._readData(_24, b"XXX\x00Foo\x00") + # either we can read invalid frame ids or we fail properly, atm we read + # them. + self.assertEqual(frame.frameid, "XXX\x00") + + def test_default(self): + frame = LINK() + self.assertEqual(frame.frameid, u"XXXX") + self.assertEqual(frame.url, u"") + + def test_hash(self): frame = LINK(frameid="TPE1", url="http://foo.bar", data=b"\x42") self.assertEqual(frame.HashKey, "LINK:TPE1:http://foo.bar:B") frame._pprint() @@ -127,254 +821,428 @@ frame = LINK(frameid="TPE1", url="http://foo.bar") self.assertEqual(frame.HashKey, "LINK:TPE1:http://foo.bar") - def test_AENC(self): - from mutagen.id3 import AENC +class TAENC(TestCase): + + def test_default(self): + frame = AENC() + self.assertEqual(frame.owner, u"") + self.assertEqual(frame.preview_start, 0) + self.assertEqual(frame.preview_length, 0) + + def test_hash(self): frame = AENC( owner="foo", preview_start=1, preview_length=2, data=b"\x42") self.assertEqual(frame.HashKey, "AENC:foo") frame._pprint() - def test_GEOB(self): - from mutagen.id3 import GEOB +class TGEOB(TestCase): + + def test_default(self): + frame = GEOB() + self.assertEqual(frame.encoding, 1) + self.assertEqual(frame.mime, u"") + self.assertEqual(frame.filename, u"") + self.assertEqual(frame.desc, u"") + self.assertEqual(frame.data, b"") + + def test_hash(self): frame = GEOB( encoding=0, mtime="m", filename="f", desc="d", data=b"\x42") self.assertEqual(frame.HashKey, "GEOB:d") frame._pprint() - def test_POPM(self): - from mutagen.id3 import POPM + self.assertEquals(GEOB(data=b"1").HashKey, GEOB(data=b"2").HashKey) + self.assertNotEquals(GEOB(desc="a").HashKey, GEOB(desc="b").HashKey) + + +class TPOPM(TestCase): + + def test_default(self): + frame = POPM() + self.assertEqual(frame.email, u"") + self.assertEqual(frame.rating, 0) + self.assertFalse(hasattr(frame, "count")) + def test_hash(self): frame = POPM(email="e", rating=42) self.assertEqual(frame.HashKey, "POPM:e") frame._pprint() - def test_EQU2(self): - from mutagen.id3 import EQU2 + self.assertEquals(POPM(count=1).HashKey, POPM(count=2).HashKey) + self.assertNotEquals(POPM(email="a").HashKey, POPM(email="b").HashKey) + +class TEQU2(TestCase): + + def test_default(self): + frame = EQU2() + self.assertEqual(frame.method, 0) + self.assertEqual(frame.desc, u"") + self.assertEqual(frame.adjustments, []) + + def test_hash(self): frame = EQU2(method=42, desc="d", adjustments=[(0, 0)]) self.assertEqual(frame.HashKey, "EQU2:d") frame._pprint() - def test_RVA2(self): - from mutagen.id3 import RVA2 - frame = RVA2(method=42, desc="d", channel=1, gain=1, peak=1) - self.assertEqual(frame.HashKey, "RVA2:d") - frame._pprint() +class TSEEK(TestCase): + + def test_default(self): + frame = SEEK() + self.assertEqual(frame.offset, 0) + + +class TPOSS(TestCase): - def test_COMM(self): - from mutagen.id3 import COMM + def test_default(self): + frame = POSS() + self.assertEqual(frame.format, 1) + self.assertEqual(frame.position, 0) + +class TCOMM(TestCase): + + def test_default(self): + frame = COMM() + self.assertEqual(frame.encoding, 1) + self.assertEqual(frame.lang, u"XXX") + self.assertEqual(frame.desc, u"") + self.assertEqual(frame.text, []) + + def test_hash(self): frame = COMM(encoding=0, lang="foo", desc="d") self.assertEqual(frame.HashKey, "COMM:d:foo") frame._pprint() - def test_SYLT(self): - from mutagen.id3 import SYLT + self.assertEquals(COMM(text="a").HashKey, COMM(text="b").HashKey) + self.assertNotEquals(COMM(desc="a").HashKey, COMM(desc="b").HashKey) + self.assertNotEquals( + COMM(lang="abc").HashKey, COMM(lang="def").HashKey) + def test_bad_unicodedecode(self): + # 7 bytes of "UTF16" data. + data = b'\x01\x00\x00\x00\xff\xfe\x00\xff\xfeh\x00' + self.assertRaises(ID3JunkFrameError, COMM._fromData, _24, 0x00, data) + + +class TSYLT(TestCase): + + def test_default(self): + frame = SYLT() + self.assertEqual(frame.encoding, 1) + self.assertEqual(frame.lang, u"XXX") + self.assertEqual(frame.format, 1) + self.assertEqual(frame.type, 0) + self.assertEqual(frame.desc, u"") + self.assertEqual(frame.text, u"") + + def test_hash(self): frame = SYLT(encoding=0, lang="foo", format=1, type=2, desc="d", text=[("t", 0)]) self.assertEqual(frame.HashKey, "SYLT:d:foo") frame._pprint() - def test_USLT(self): - from mutagen.id3 import USLT + def test_bad_sylt(self): + self.assertRaises( + ID3JunkFrameError, SYLT._fromData, _24, 0x0, + b"\x00eng\x01description\x00foobar") + self.assertRaises( + ID3JunkFrameError, SYLT._fromData, _24, 0x0, + b"\x00eng\x01description\x00foobar\x00\xFF\xFF\xFF") + + +class TRVRB(TestCase): + + def test_default(self): + frame = RVRB() + self.assertEqual(frame.left, 0) + self.assertEqual(frame.right, 0) + self.assertEqual(frame.bounce_left, 0) + self.assertEqual(frame.bounce_right, 0) + self.assertEqual(frame.feedback_ltl, 0) + self.assertEqual(frame.feedback_ltr, 0) + self.assertEqual(frame.feedback_rtr, 0) + self.assertEqual(frame.feedback_rtl, 0) + self.assertEqual(frame.premix_ltr, 0) + self.assertEqual(frame.premix_rtl, 0) + + def test_extradata(self): + self.assertEqual(RVRB()._readData(_24, b'L1R1BBFFFFPP#xyz'), b'#xyz') + + +class TRBUF(TestCase): + + def test_default(self): + frame = RBUF() + self.assertEqual(frame.size, 0) + self.assertFalse(hasattr(frame, "info")) + self.assertFalse(hasattr(frame, "offset")) + + def test_extradata(self): + self.assertEqual( + RBUF()._readData( + _24, b'\x00\x01\x00\x01\x00\x00\x00\x00#xyz'), b'#xyz') + +class TUSLT(TestCase): + + def test_default(self): + frame = USLT() + self.assertEqual(frame.encoding, 1) + self.assertEqual(frame.lang, u"XXX") + self.assertEqual(frame.desc, u"") + self.assertEqual(frame.text, u"") + + def test_hash(self): frame = USLT(encoding=0, lang="foo", desc="d", text="t") self.assertEqual(frame.HashKey, "USLT:d:foo") frame._pprint() - def test_WXXX(self): - from mutagen.id3 import WXXX +class TWXXX(TestCase): + + def test_default(self): + frame = WXXX() + self.assertEqual(frame.encoding, 1) + self.assertEqual(frame.desc, u"") + self.assertEqual(frame.url, u"") + + def test_hash(self): self.assert_(isinstance(WXXX(url='durl'), WXXX)) frame = WXXX(encoding=0, desc="d", url="u") self.assertEqual(frame.HashKey, "WXXX:d") frame._pprint() - def test_TXXX(self): - from mutagen.id3 import TXXX - self.assert_(isinstance(TXXX(desc='d', text='text'), TXXX)) + self.assertEquals(WXXX(text="a").HashKey, WXXX(text="b").HashKey) + self.assertNotEquals(WXXX(desc="a").HashKey, WXXX(desc="b").HashKey) + +class TTXXX(TestCase): + + def test_default(self): + self.assertEqual(TXXX(), TXXX(desc=u"", encoding=1, text=[])) + + def test_hash(self): frame = TXXX(encoding=0, desc="d", text=[]) self.assertEqual(frame.HashKey, "TXXX:d") frame._pprint() - def test_WCOM(self): - from mutagen.id3 import WCOM + self.assertEquals(TXXX(text="a").HashKey, TXXX(text="b").HashKey) + self.assertNotEquals(TXXX(desc="a").HashKey, TXXX(desc="b").HashKey) + + +class TWCOM(TestCase): + def test_hash(self): frame = WCOM(url="u") self.assertEqual(frame.HashKey, "WCOM:u") frame._pprint() - def test_TF(self): - from mutagen.id3 import TextFrame - self.assert_(isinstance(TextFrame(text='text'), TextFrame)) - - def test_UF(self): - from mutagen.id3 import UrlFrame - self.assert_(isinstance(UrlFrame('url'), UrlFrame)) - - def test_NTF(self): - from mutagen.id3 import NumericTextFrame - self.assert_(isinstance(NumericTextFrame(text='1'), NumericTextFrame)) - - def test_NTPF(self): - from mutagen.id3 import NumericPartTextFrame - self.assert_( - isinstance(NumericPartTextFrame(text='1/2'), NumericPartTextFrame)) - - def test_MTF(self): - from mutagen.id3 import TextFrame - self.assert_(isinstance(TextFrame(text=['a', 'b']), TextFrame)) - def test_22_uses_direct_ints(self): - data = b'TT1\x00\x00\x83\x00' + (b'123456789abcdef' * 16) - tag = list(_22._ID3__read_frames(data, Frames_2_2))[0] - self.assertEquals(data[7:7 + 0x82].decode('latin1'), tag.text[0]) +class TUrlFrame(TestCase): - def test_frame_too_small(self): - self.assertEquals( - [], list(_24._ID3__read_frames(b'012345678', Frames))) - self.assertEquals( - [], list(_23._ID3__read_frames(b'012345678', Frames))) - self.assertEquals( - [], list(_22._ID3__read_frames(b'01234', Frames_2_2))) - self.assertEquals( - [], list(_22._ID3__read_frames(b'TT1' + b'\x00' * 3, Frames_2_2))) + def test_default(self): + self.assertEqual(UrlFrame(), UrlFrame(url=u"")) - def test_unknown_22_frame(self): - data = b'XYZ\x00\x00\x01\x00' - self.assertEquals([data], list(_22._ID3__read_frames(data, {}))) + def test_main(self): + self.assertEqual(UrlFrame("url").url, "url") - def test_zlib_latin1(self): - from mutagen.id3 import TPE1 - tag = TPE1._fromData( - _24._header, 0x9, b'\x00\x00\x00\x0f' - b'x\x9cc(\xc9\xc8,V\x00\xa2D\xfd\x92\xd4\xe2\x12\x00&\x7f\x05%' - ) - self.assertEquals(tag.encoding, 0) - self.assertEquals(tag, ['this is a/test']) - def test_datalen_but_not_compressed(self): - from mutagen.id3 import TPE1 - tag = TPE1._fromData(_24._header, 0x01, b'\x00\x00\x00\x06\x00A test') - self.assertEquals(tag.encoding, 0) - self.assertEquals(tag, ['A test']) +class TNumericTextFrame(TestCase): - def test_utf8(self): - from mutagen.id3 import TPE1 - tag = TPE1._fromData(_23._header, 0x00, b'\x03this is a test') - self.assertEquals(tag.encoding, 3) - self.assertEquals(tag, 'this is a test') + def test_default(self): + self.assertEqual( + NumericTextFrame(), NumericTextFrame(encoding=1, text=[])) - def test_zlib_utf16(self): - from mutagen.id3 import TPE1 - data = (b'\x00\x00\x00\x1fx\x9cc\xfc\xff\xaf\x84!\x83!\x93\xa1\x98A' - b'\x01J&2\xe83\x940\xa4\x02\xd9%\x0c\x00\x87\xc6\x07#') - tag = TPE1._fromData(_23._header, 0x80, data) - self.assertEquals(tag.encoding, 1) - self.assertEquals(tag, ['this is a/test']) + def test_main(self): + self.assertEqual(NumericTextFrame(text='1').text, ["1"]) + self.assertEqual(+NumericTextFrame(text='1'), 1) - tag = TPE1._fromData(_24._header, 0x08, data) - self.assertEquals(tag.encoding, 1) - self.assertEquals(tag, ['this is a/test']) - def test_load_write(self): - from mutagen.id3 import TPE1, Frames - artists = [s.decode('utf8') for s in - [b'\xc2\xb5', b'\xe6\x97\xa5\xe6\x9c\xac']] - artist = TPE1(encoding=3, text=artists) - id3 = ID3() - id3._header = ID3Header() - id3._header.version = (2, 4, 0) - tag = list(id3._ID3__read_frames( - id3._ID3__save_frame(artist), Frames))[0] - self.assertEquals('TPE1', type(tag).__name__) - self.assertEquals(artist.text, tag.text) +class TNumericPartTextFrame(TestCase): - def test_22_to_24(self): - from mutagen.id3 import TT1 - id3 = ID3() - tt1 = TT1(encoding=0, text=u'whatcha staring at?') - id3.loaded_frame(tt1) - tit1 = id3['TIT1'] - - self.assertEquals(tt1.encoding, tit1.encoding) - self.assertEquals(tt1.text, tit1.text) - self.assert_('TT1' not in id3) + def test_default(self): + self.assertEqual( + NumericPartTextFrame(), + NumericPartTextFrame(encoding=1, text=[])) - def test_single_TXYZ(self): - from mutagen.id3 import TIT2 - self.assertEquals(TIT2(text="a").HashKey, TIT2(text="b").HashKey) + def test_main(self): + self.assertEqual(NumericPartTextFrame(text='1/2').text, ["1/2"]) + self.assertEqual(+NumericPartTextFrame(text='1/2'), 1) + + +class Tread_frames_load_frame(TestCase): + + def test_detect_23_ints_in_24_frames(self): + head = b'TIT1\x00\x00\x01\x00\x00\x00\x00' + tail = b'TPE1\x00\x00\x00\x05\x00\x00\x00Yay!' + + tagsgood = read_frames(_24, head + b'a' * 127 + tail, Frames)[0] + tagsbad = read_frames(_24, head + b'a' * 255 + tail, Frames)[0] + self.assertEquals(2, len(tagsgood)) + self.assertEquals(2, len(tagsbad)) + self.assertEquals('a' * 127, tagsgood[0]) + self.assertEquals('a' * 255, tagsbad[0]) + self.assertEquals('Yay!', tagsgood[1]) + self.assertEquals('Yay!', tagsbad[1]) + + tagsgood = read_frames(_24, head + b'a' * 127, Frames)[0] + tagsbad = read_frames(_24, head + b'a' * 255, Frames)[0] + self.assertEquals(1, len(tagsgood)) + self.assertEquals(1, len(tagsbad)) + self.assertEquals('a' * 127, tagsgood[0]) + self.assertEquals('a' * 255, tagsbad[0]) + + def test_zerolength_framedata(self): + tail = b'\x00' * 6 + for head in b'WOAR TENC TCOP TOPE WXXX'.split(): + data = head + tail + self.assertEquals( + 0, len(list(read_frames(_24, data, Frames)[1]))) + + def test_drops_truncated_frames(self): + tail = b'\x00\x00\x00\x03\x00\x00' b'\x01\x02\x03' + for head in b'RVA2 TXXX APIC'.split(): + data = head + tail + self.assertEquals( + 0, len(read_frames(_24, data, Frames)[1])) + + def test_drops_nonalphanum_frames(self): + tail = b'\x00\x00\x00\x03\x00\x00' b'\x01\x02\x03' + for head in [b'\x06\xaf\xfe\x20', b'ABC\x00', b'A ']: + data = head + tail + self.assertEquals( + 0, len(read_frames(_24, data, Frames)[0])) - def test_multi_TXXX(self): - from mutagen.id3 import TXXX - self.assertEquals(TXXX(text="a").HashKey, TXXX(text="b").HashKey) - self.assertNotEquals(TXXX(desc="a").HashKey, TXXX(desc="b").HashKey) + def test_frame_too_small(self): + self.assertEquals([], read_frames(_24, b'012345678', Frames)[0]) + self.assertEquals([], read_frames(_23, b'012345678', Frames)[0]) + self.assertEquals([], read_frames(_22, b'01234', Frames_2_2)[0]) + self.assertEquals( + [], read_frames(_22, b'TT1' + b'\x00' * 3, Frames_2_2)[0]) - def test_multi_WXXX(self): - from mutagen.id3 import WXXX - self.assertEquals(WXXX(text="a").HashKey, WXXX(text="b").HashKey) - self.assertNotEquals(WXXX(desc="a").HashKey, WXXX(desc="b").HashKey) + def test_unknown_22_frame(self): + data = b'XYZ\x00\x00\x01\x00' + self.assertEquals([data], read_frames(_22, data, {})[1]) - def test_multi_COMM(self): - from mutagen.id3 import COMM - self.assertEquals(COMM(text="a").HashKey, COMM(text="b").HashKey) - self.assertNotEquals(COMM(desc="a").HashKey, COMM(desc="b").HashKey) - self.assertNotEquals( - COMM(lang="abc").HashKey, COMM(lang="def").HashKey) + def test_22_uses_direct_ints(self): + data = b'TT1\x00\x00\x83\x00' + (b'123456789abcdef' * 16) + tag = read_frames(_22, data, Frames_2_2)[0][0] + self.assertEquals(data[7:7 + 0x82].decode('latin1'), tag.text[0]) - def test_multi_RVA2(self): - from mutagen.id3 import RVA2 - self.assertEquals(RVA2(gain=1).HashKey, RVA2(gain=2).HashKey) - self.assertNotEquals(RVA2(desc="a").HashKey, RVA2(desc="b").HashKey) + def test_load_write(self): + artists = [s.decode('utf8') for s in + [b'\xc2\xb5', b'\xe6\x97\xa5\xe6\x9c\xac']] + artist = TPE1(encoding=3, text=artists) + config = ID3SaveConfig() + tag = read_frames(_24, save_frame(artist, config=config), Frames)[0][0] + self.assertEquals('TPE1', type(tag).__name__) + self.assertEquals(artist.text, tag.text) - def test_multi_POPM(self): - from mutagen.id3 import POPM - self.assertEquals(POPM(count=1).HashKey, POPM(count=2).HashKey) - self.assertNotEquals(POPM(email="a").HashKey, POPM(email="b").HashKey) - def test_multi_GEOB(self): - from mutagen.id3 import GEOB - self.assertEquals(GEOB(data=b"1").HashKey, GEOB(data=b"2").HashKey) - self.assertNotEquals(GEOB(desc="a").HashKey, GEOB(desc="b").HashKey) +class TTPE2(TestCase): - def test_multi_UFID(self): - from mutagen.id3 import UFID - self.assertEquals(UFID(data=b"1").HashKey, UFID(data=b"2").HashKey) - self.assertNotEquals(UFID(owner="a").HashKey, UFID(owner="b").HashKey) + def test_unsynch(self): + header = ID3Header() + header.version = (2, 4, 0) + header._flags = 0x80 + badsync = b'\x00\xff\x00ab\x00' + + self.assertEquals(TPE2._fromData(header, 0, badsync), [u"\xffab"]) + + header._flags = 0x00 + self.assertEquals(TPE2._fromData(header, 0x02, badsync), [u"\xffab"]) + + tag = TPE2._fromData(header, 0, badsync) + self.assertEquals(tag, [u"\xff", u"ab"]) + + +class TTPE1(TestCase): + + def test_badencoding(self): + self.assertRaises( + ID3JunkFrameError, TPE1._fromData, _24, 0, b"\x09ab") + self.assertRaises(ValueError, TPE1, encoding=9, text="ab") + + def test_badsync(self): + frame = TPE1._fromData(_24, 0x02, b"\x00\xff\xfe") + self.assertEqual(frame.text, [u'\xff\xfe']) + + def test_noencrypt(self): + self.assertRaises( + NotImplementedError, TPE1._fromData, _24, 0x04, b"\x00") + self.assertRaises( + NotImplementedError, TPE1._fromData, _23, 0x40, b"\x00") + + def test_badcompress(self): + self.assertRaises( + ID3JunkFrameError, TPE1._fromData, _24, 0x08, + b"\x00\x00\x00\x00#") + self.assertRaises( + ID3JunkFrameError, TPE1._fromData, _23, 0x80, + b"\x00\x00\x00\x00#") + + def test_junkframe(self): + self.assertRaises( + ID3JunkFrameError, TPE1._fromData, _24, 0, b"") + + def test_lengthone_utf16(self): + tpe1 = TPE1._fromData(_24, 0, b'\x01\x00') + self.assertEquals(u'', tpe1) + tpe1 = TPE1._fromData(_24, 0, b'\x01\x00\x00\x00\x00') + self.assertEquals([u'', u''], tpe1) + + def test_utf16_wrongnullterm(self): + # issue 169 + tpe1 = TPE1._fromData( + _24, 0, b'\x01\xff\xfeH\x00e\x00l\x00l\x00o\x00\x00') + self.assertEquals(tpe1, [u'Hello']) + + def test_zlib_bpi(self): + tpe1 = TPE1(encoding=0, text="a" * (0xFFFF - 2)) + data = save_frame(tpe1) + datalen_size = data[4 + 4 + 2:4 + 4 + 2 + 4] + self.failIf( + max(datalen_size) >= b'\x80'[0], "data is not syncsafe: %r" % data) - def test_multi_USER(self): - from mutagen.id3 import USER - self.assertEquals(USER(text="a").HashKey, USER(text="b").HashKey) - self.assertNotEquals( - USER(lang="abc").HashKey, USER(lang="def").HashKey) + def test_ql_0_12_missing_uncompressed_size(self): + tag = TPE1._fromData( + _24, 0x08, + b'x\x9cc\xfc\xff\xaf\x84!\x83!\x93' + b'\xa1\x98A\x01J&2\xe83\x940\xa4\x02\xd9%\x0c\x00\x87\xc6\x07#' + ) + self.assertEquals(tag.encoding, 1) + self.assertEquals(tag, ['this is a/test']) + def test_zlib_latin1_missing_datalen(self): + tag = TPE1._fromData( + _24, 0x8, + b'\x00\x00\x00\x0f' + b'x\x9cc(\xc9\xc8,V\x00\xa2D\xfd\x92\xd4\xe2\x12\x00&\x7f\x05%' + ) + self.assertEquals(tag.encoding, 0) + self.assertEquals(tag, ['this is a/test']) -class Genres(TestCase): - from mutagen.id3 import TCON - TCON = TCON - from mutagen._constants import GENRES - GENRES = GENRES +class TTCON(TestCase): def _g(self, s): - return self.TCON(text=s).genres + return TCON(text=s).genres def test_empty(self): self.assertEquals(self._g(""), []) def test_num(self): - for i in xrange(len(self.GENRES)): - self.assertEquals(self._g("%02d" % i), [self.GENRES[i]]) + for i in xrange(len(GENRES)): + self.assertEquals(self._g("%02d" % i), [GENRES[i]]) def test_parened_num(self): - for i in xrange(len(self.GENRES)): - self.assertEquals(self._g("(%02d)" % i), [self.GENRES[i]]) + for i in xrange(len(GENRES)): + self.assertEquals(self._g("(%02d)" % i), [GENRES[i]]) def test_unknown(self): self.assertEquals(self._g("(255)"), ["Unknown"]) @@ -417,47 +1285,44 @@ self._g("(20)\x00Alternative"), ["Alternative", "Alternative"]) def test_set_genre(self): - gen = self.TCON(encoding=0, text="") + gen = TCON(encoding=0, text="") self.assertEquals(gen.genres, []) gen.genres = ["a genre", "another"] self.assertEquals(gen.genres, ["a genre", "another"]) def test_set_string(self): - gen = self.TCON(encoding=0, text="") + gen = TCON(encoding=0, text="") gen.genres = "foo" self.assertEquals(gen.genres, ["foo"]) def test_nodoubledecode(self): - gen = self.TCON(encoding=1, text=u"(255)genre") + gen = TCON(encoding=1, text=u"(255)genre") gen.genres = gen.genres self.assertEquals(gen.genres, [u"Unknown", u"genre"]) -class TimeStamp(TestCase): - - from mutagen.id3 import ID3TimeStamp as Stamp - Stamp = Stamp +class TID3TimeStamp(TestCase): def test_Y(self): - s = self.Stamp('1234') + s = ID3TimeStamp('1234') self.assertEquals(s.year, 1234) self.assertEquals(s.text, '1234') def test_yM(self): - s = self.Stamp('1234-56') + s = ID3TimeStamp('1234-56') self.assertEquals(s.year, 1234) self.assertEquals(s.month, 56) self.assertEquals(s.text, '1234-56') def test_ymD(self): - s = self.Stamp('1234-56-78') + s = ID3TimeStamp('1234-56-78') self.assertEquals(s.year, 1234) self.assertEquals(s.month, 56) self.assertEquals(s.day, 78) self.assertEquals(s.text, '1234-56-78') def test_ymdH(self): - s = self.Stamp('1234-56-78T12') + s = ID3TimeStamp('1234-56-78T12') self.assertEquals(s.year, 1234) self.assertEquals(s.month, 56) self.assertEquals(s.day, 78) @@ -465,7 +1330,7 @@ self.assertEquals(s.text, '1234-56-78 12') def test_ymdhM(self): - s = self.Stamp('1234-56-78T12:34') + s = ID3TimeStamp('1234-56-78T12:34') self.assertEquals(s.year, 1234) self.assertEquals(s.month, 56) self.assertEquals(s.day, 78) @@ -474,7 +1339,7 @@ self.assertEquals(s.text, '1234-56-78 12:34') def test_ymdhmS(self): - s = self.Stamp('1234-56-78T12:34:56') + s = ID3TimeStamp('1234-56-78T12:34:56') self.assertEquals(s.year, 1234) self.assertEquals(s.month, 56) self.assertEquals(s.day, 78) @@ -484,76 +1349,261 @@ self.assertEquals(s.text, '1234-56-78 12:34:56') def test_Ymdhms(self): - s = self.Stamp('1234-56-78T12:34:56') + s = ID3TimeStamp('1234-56-78T12:34:56') s.month = None self.assertEquals(s.text, '1234') def test_alternate_reprs(self): - s = self.Stamp('1234-56.78 12:34:56') + s = ID3TimeStamp('1234-56.78 12:34:56') self.assertEquals(s.text, '1234-56-78 12:34:56') def test_order(self): - s = self.Stamp('1234') - t = self.Stamp('1233-12') - u = self.Stamp('1234-01') + s = ID3TimeStamp('1234') + t = ID3TimeStamp('1233-12') + u = ID3TimeStamp('1234-01') self.assert_(t < s < u) self.assert_(u > s > t) + def test_types(self): + if PY3: + self.assertRaises(TypeError, ID3TimeStamp, b"blah") + self.assertEquals( + text_type(ID3TimeStamp(u"2000-01-01")), u"2000-01-01") + self.assertEquals( + bytes(ID3TimeStamp(u"2000-01-01")), b"2000-01-01") + + +class TFrameTest(object): + + FRAME = None -class NoHashFrame(TestCase): + def test_has_doc(self): + self.failUnless(self.FRAME.__doc__, "%s has no docstring" % self.FRAME) - def test_frame(self): - from mutagen.id3 import TIT1 + def test_fake_zlib(self): + header = ID3Header() + header.version = (2, 4, 0) + self.assertRaises(ID3JunkFrameError, self.FRAME._fromData, header, + Frame.FLAG24_COMPRESS, b'\x03abcdefg') + + def test_no_hash(self): self.failUnlessRaises( - TypeError, {}.__setitem__, TIT1(encoding=0, text="foo"), None) + TypeError, {}.__setitem__, self.FRAME(), None) + + def test_is_valid_frame_id(self): + self.assertTrue(is_valid_frame_id(self.FRAME.__name__)) + + def test_all_specs_have_default(self): + for spec in self.FRAME._framespec: + self.assertTrue( + spec.default is not None, + msg="%r:%r" % (self.FRAME, spec.name)) + + @classmethod + def create_frame_tests(cls): + for kind in (list(Frames.values()) + list(Frames_2_2.values())): + new_type = type(cls.__name__ + kind.__name__, + (cls, TestCase), {"FRAME": kind}) + assert new_type.__name__ not in globals() + globals()[new_type.__name__] = new_type + + +TFrameTest.create_frame_tests() class FrameIDValidate(TestCase): def test_valid(self): - from mutagen.id3 import is_valid_frame_id self.failUnless(is_valid_frame_id("APIC")) self.failUnless(is_valid_frame_id("TPE2")) def test_invalid(self): - from mutagen.id3 import is_valid_frame_id self.failIf(is_valid_frame_id("MP3e")) self.failIf(is_valid_frame_id("+ABC")) -class TimeStampTextFrame(TestCase): +class TTimeStampTextFrame(TestCase): - from mutagen.id3 import TimeStampTextFrame as Frame - Frame = Frame + def test_default(self): + self.assertEqual( + TimeStampTextFrame(), TimeStampTextFrame(encoding=1, text=[])) def test_compare_to_unicode(self): - frame = self.Frame(encoding=0, text=[u'1987', u'1988']) + frame = TimeStampTextFrame(encoding=0, text=[u'1987', u'1988']) self.failUnlessEqual(frame, text_type(frame)) class TTextFrame(TestCase): - def test_list_iface(self): - from mutagen.id3 import TextFrame + def test_defaults(self): + self.assertEqual(TextFrame(), TextFrame(encoding=1, text=[])) + + def test_main(self): + self.assertEqual(TextFrame(text='text').text, ["text"]) + self.assertEqual(TextFrame(text=['a', 'b']).text, ["a", "b"]) + def test_list_iface(self): frame = TextFrame() frame.append("a") frame.extend(["b", "c"]) self.assertEqual(frame.text, ["a", "b", "c"]) + def test_zlib_latin1(self): + tag = TextFrame._fromData( + _24, 0x9, b'\x00\x00\x00\x0f' + b'x\x9cc(\xc9\xc8,V\x00\xa2D\xfd\x92\xd4\xe2\x12\x00&\x7f\x05%' + ) + self.assertEquals(tag.encoding, 0) + self.assertEquals(tag, ['this is a/test']) + + def test_datalen_but_not_compressed(self): + tag = TextFrame._fromData(_24, 0x01, b'\x00\x00\x00\x06\x00A test') + self.assertEquals(tag.encoding, 0) + self.assertEquals(tag, ['A test']) + + def test_utf8(self): + tag = TextFrame._fromData(_23, 0x00, b'\x03this is a test') + self.assertEquals(tag.encoding, 3) + self.assertEquals(tag, 'this is a test') + + def test_zlib_utf16(self): + data = (b'\x00\x00\x00\x1fx\x9cc\xfc\xff\xaf\x84!\x83!\x93\xa1\x98A' + b'\x01J&2\xe83\x940\xa4\x02\xd9%\x0c\x00\x87\xc6\x07#') + tag = TextFrame._fromData(_23, 0x80, data) + self.assertEquals(tag.encoding, 1) + self.assertEquals(tag, ['this is a/test']) + + tag = TextFrame._fromData(_24, 0x08, data) + self.assertEquals(tag.encoding, 1) + self.assertEquals(tag, ['this is a/test']) + class TRVA2(TestCase): + def test_default(self): + frame = RVA2() + self.assertEqual(frame.desc, u"") + self.assertEqual(frame.channel, 1) + self.assertEqual(frame.gain, 1) + self.assertEqual(frame.peak, 1) + def test_basic(self): - from mutagen.id3 import RVA2 r = RVA2(gain=1, channel=1, peak=1) self.assertEqual(r, r) self.assertNotEqual(r, 42) + def test_hash_key(self): + frame = RVA2(method=42, desc="d", channel=1, gain=1, peak=1) + self.assertEqual(frame.HashKey, "RVA2:d") + + self.assertEquals(RVA2(gain=1).HashKey, RVA2(gain=2).HashKey) + self.assertNotEquals(RVA2(desc="a").HashKey, RVA2(desc="b").HashKey) + + def test_pprint(self): + frame = RVA2(method=42, desc="d", channel=1, gain=1, peak=1) + frame._pprint() + + def test_wacky_truncated(self): + data = b'\x01{\xf0\x10\xff\xff\x00' + self.assertRaises(ID3JunkFrameError, RVA2._fromData, _24, 0x00, data) + + def test_bad_number_of_bits(self): + data = b'\x00\x00\x01\xe6\xfc\x10{\xd7' + self.assertRaises(ID3JunkFrameError, RVA2._fromData, _24, 0x00, data) + + +class TCTOC(TestCase): + + def test_defaults(self): + self.assertEqual(CTOC(), CTOC(element_id=u"", flags=0, + child_element_ids=[], sub_frames=[])) + + def test_hash(self): + frame = CTOC(element_id=u"foo", flags=3, + child_element_ids=[u"ch0"], + sub_frames=[TPE2(encoding=3, text=[u"foo"])]) + self.assertEqual(frame.HashKey, "CTOC:foo") + + def test_pprint(self): + frame = CTOC(element_id=u"foo", flags=3, + child_element_ids=[u"ch0"], + sub_frames=[TPE2(encoding=3, text=[u"foo"])]) + self.assertEqual( + frame.pprint(), + "CTOC=foo flags=3 child_element_ids=ch0\n TPE2=foo") + + def test_write(self): + frame = CTOC(element_id=u"foo", flags=3, + child_element_ids=[u"ch0"], + sub_frames=[TPE2(encoding=3, text=[u"f", u"b"])]) + config = ID3SaveConfig(3, "/") + data = (b"foo\x00\x03\x01ch0\x00TPE2\x00\x00\x00\x0b\x00\x00\x01" + b"\xff\xfef\x00/\x00b\x00\x00\x00") + self.assertEqual(frame._writeData(config), data) + + def test_eq(self): + self.assertEqual(CTOC(), CTOC()) + self.assertNotEqual(CTOC(), object()) + + +class TASPI(TestCase): + + def test_default(self): + frame = ASPI() + self.assertEqual(frame.S, 0) + self.assertEqual(frame.L, 0) + self.assertEqual(frame.N, 0) + self.assertEqual(frame.b, 0) + self.assertEqual(frame.Fi, []) + + +class TCHAP(TestCase): + + def test_default(self): + frame = CHAP() + self.assertEqual(frame.element_id, u"") + self.assertEqual(frame.start_time, 0) + self.assertEqual(frame.end_time, 0) + self.assertEqual(frame.start_offset, 0xffffffff) + self.assertEqual(frame.end_offset, 0xffffffff) + self.assertEqual(frame.sub_frames, ID3Tags()) + + def test_hash(self): + frame = CHAP(element_id=u"foo", start_time=0, end_time=0, + start_offset=0, end_offset=0, + sub_frames=[TPE2(encoding=3, text=[u"foo"])]) + self.assertEqual(frame.HashKey, "CHAP:foo") + + def test_pprint(self): + frame = CHAP(element_id=u"foo", start_time=0, end_time=0, + start_offset=0, end_offset=0, + sub_frames=[TPE2(encoding=3, text=[u"foo"])]) + self.assertEqual( + frame.pprint(), "CHAP=foo time=0..0 offset=0..0\n TPE2=foo") + + def test_eq(self): + self.assertEqual(CHAP(), CHAP()) + self.assertNotEqual(CHAP(), object()) + + +class TPCNT(TestCase): + + def test_default(self): + frame = PCNT() + self.assertEqual(frame.count, 0) + class TAPIC(TestCase): + def test_default(self): + frame = APIC() + self.assertEqual(frame.encoding, 1) + self.assertEqual(frame.mime, u"") + self.assertEqual(frame.type, 3) + self.assertEqual(frame.desc, u"") + self.assertEqual(frame.data, b"") + def test_hash(self): frame = APIC(encoding=0, mime=u"m", type=3, desc=u"d", data=b"\x42") self.assertEqual(frame.HashKey, "APIC:d") @@ -580,5 +1630,5 @@ self.assertEqual(repr(frame), expected) new_frame = APIC() - new_frame._readData(frame._writeData()) + new_frame._readData(_24, frame._writeData()) self.assertEqual(repr(new_frame), expected) diff -Nru mutagen-1.33.2/tests/test_id3.py mutagen-1.34/tests/test_id3.py --- mutagen-1.33.2/tests/test_id3.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/tests/test_id3.py 2016-07-20 17:41:06.000000000 +0000 @@ -1,116 +1,271 @@ # -*- coding: utf-8 -*- import os -from os.path import join -import shutil -from tests import TestCase, DATA_DIR + from mutagen import id3 from mutagen import MutagenError from mutagen.apev2 import APEv2 -from mutagen.id3 import ID3, COMR, Frames, Frames_2_2, ID3Warning, \ - ID3JunkFrameError, ID3Header, ID3UnsupportedVersionError, _fullread, TIT2 +from mutagen.id3 import ID3, Frames, ID3UnsupportedVersionError, TIT2, \ + CHAP, CTOC, TT1, TCON, COMM, TORY, PIC, MakeID3v1, TRCK, TYER, TDRC, \ + TDAT, TIME, LNK, IPLS, TPE1, BinaryFrame, TIT3, POPM, APIC, \ + TALB, TPE2, TSOT, TDEN, TIPL, ParseID3v1, Encoding, ID3Tags, RVAD from mutagen.id3._util import BitPaddedInt, error as ID3Error -from mutagen._compat import cBytesIO, PY2, iteritems, integer_types, izip -import warnings -from tempfile import mkstemp -warnings.simplefilter('error', ID3Warning) - -_22 = ID3Header() -_22.version = (2, 2, 0) -_23 = ID3Header() -_23.version = (2, 3, 0) -_24 = ID3Header() -_24.version = (2, 4, 0) +from mutagen.id3._tags import determine_bpi, ID3Header, \ + save_frame, ID3SaveConfig +from mutagen.id3._id3v1 import find_id3v1 +from mutagen._compat import cBytesIO +from tests import TestCase, DATA_DIR, get_temp_copy, get_temp_empty -class ID3GetSetDel(TestCase): - def setUp(self): - self.frames = [ - TIT2(text=["1"]), TIT2(text=["2"]), - TIT2(text=["3"]), TIT2(text=["4"])] - self.i = ID3() - self.i["BLAH"] = self.frames[0] - self.i["QUUX"] = self.frames[1] - self.i["FOOB:ar"] = self.frames[2] - self.i["FOOB:az"] = self.frames[3] +class TID3Read(TestCase): - def test_getnormal(self): - self.assertEquals(self.i.getall("BLAH"), [self.frames[0]]) - self.assertEquals(self.i.getall("QUUX"), [self.frames[1]]) - self.assertEquals(self.i.getall("FOOB:ar"), [self.frames[2]]) - self.assertEquals(self.i.getall("FOOB:az"), [self.frames[3]]) + empty = os.path.join(DATA_DIR, 'emptyfile.mp3') + silence = os.path.join(DATA_DIR, 'silence-44-s.mp3') + unsynch = os.path.join(DATA_DIR, 'id3v23_unsynch.id3') + v22 = os.path.join(DATA_DIR, "id3v22-test.mp3") + bad_tyer = os.path.join(DATA_DIR, 'bad-TYER-frame.mp3') - def test_getlist(self): - self.assertTrue( - self.i.getall("FOOB") in [[self.frames[2], self.frames[3]], - [self.frames[3], self.frames[2]]]) + def test_PIC_in_23(self): + filename = get_temp_empty(".mp3") - def test_delnormal(self): - self.assert_("BLAH" in self.i) - self.i.delall("BLAH") - self.assert_("BLAH" not in self.i) + try: + with open(filename, "wb") as h: + # contains a bad upgraded frame, 2.3 structure with 2.2 name. + # PIC was upgraded to APIC, but mime was not + h.write(b"ID3\x03\x00\x00\x00\x00\x08\x00PIC\x00\x00\x00" + b"\x00\x0b\x00\x00\x00JPG\x00\x03foo\x00\x42" + b"\x00" * 100) + id3 = ID3(filename) + self.assertEqual(id3.version, (2, 3, 0)) + self.assertTrue(id3.getall("APIC")) + frame = id3.getall("APIC")[0] + self.assertEqual(frame.mime, "image/jpeg") + self.assertEqual(frame.data, b"\x42") + self.assertEqual(frame.type, 3) + self.assertEqual(frame.desc, "foo") + finally: + os.remove(filename) - def test_delone(self): - self.i.delall("FOOB:ar") - self.assertEquals(self.i.getall("FOOB"), [self.frames[3]]) + def test_bad_tyer(self): + audio = ID3(self.bad_tyer) + self.failIf("TYER" in audio) + self.failUnless("TIT2" in audio) - def test_delall(self): - self.assert_("FOOB:ar" in self.i) - self.assert_("FOOB:az" in self.i) - self.i.delall("FOOB") - self.assert_("FOOB:ar" not in self.i) - self.assert_("FOOB:az" not in self.i) + def test_tdrc(self): + tags = ID3() + tags.add(id3.TDRC(encoding=1, text="2003-04-05 12:03")) + tags.update_to_v23() + self.failUnlessEqual(tags["TYER"].text, ["2003"]) + self.failUnlessEqual(tags["TDAT"].text, ["0504"]) + self.failUnlessEqual(tags["TIME"].text, ["1203"]) - def test_setone(self): - class TEST(TIT2): - HashKey = "" + def test_tdor(self): + tags = ID3() + tags.add(id3.TDOR(encoding=1, text="2003-04-05 12:03")) + tags.update_to_v23() + self.failUnlessEqual(tags["TORY"].text, ["2003"]) - t = TEST() - t.HashKey = "FOOB:ar" - self.i.setall("FOOB", [t]) - self.assertEquals(self.i["FOOB:ar"], t) - self.assertEquals(self.i.getall("FOOB"), [t]) + def test_genre_from_v24_1(self): + tags = ID3() + tags.add(id3.TCON(encoding=1, text=["4", "Rock"])) + tags.update_to_v23() + self.failUnlessEqual(tags["TCON"].text, ["Disco", "Rock"]) - def test_settwo(self): - class TEST(TIT2): - HashKey = "" + def test_genre_from_v24_2(self): + tags = ID3() + tags.add(id3.TCON(encoding=1, text=["RX", "3", "CR"])) + tags.update_to_v23() + self.failUnlessEqual(tags["TCON"].text, ["Remix", "Dance", "Cover"]) - t = TEST() - t.HashKey = "FOOB:ar" - t2 = TEST() - t2.HashKey = "FOOB:az" - self.i.setall("FOOB", [t, t2]) - self.assertEquals(self.i["FOOB:ar"], t) - self.assertEquals(self.i["FOOB:az"], t2) - self.assert_(self.i.getall("FOOB") in [[t, t2], [t2, t]]) + def test_genre_from_v23_1(self): + tags = ID3() + tags.add(id3.TCON(encoding=1, text=["(4)Rock"])) + tags.update_to_v23() + self.failUnlessEqual(tags["TCON"].text, ["Disco", "Rock"]) + def test_genre_from_v23_2(self): + tags = ID3() + tags.add(id3.TCON(encoding=1, text=["(RX)(3)(CR)"])) + tags.update_to_v23() + self.failUnlessEqual(tags["TCON"].text, ["Remix", "Dance", "Cover"]) -class ID3Loading(TestCase): + def test_ipls_to_v23(self): + tags = ID3() + tags.version = (2, 3) + tags.add(id3.TIPL(encoding=0, people=[["a", "b"], ["c", "d"]])) + tags.add(id3.TMCL(encoding=0, people=[["e", "f"], ["g", "h"]])) + tags.update_to_v23() + self.failUnlessEqual(tags["IPLS"], [["a", "b"], ["c", "d"], + ["e", "f"], ["g", "h"]]) - empty = join(DATA_DIR, 'emptyfile.mp3') - silence = join(DATA_DIR, 'silence-44-s.mp3') - unsynch = join(DATA_DIR, 'id3v23_unsynch.id3') + def test_tags(self): + tags = ID3(self.v22) + self.failUnless(tags["TRCK"].text == ["3/11"]) + self.failUnless(tags["TPE1"].text == ["Anais Mitchell"]) def test_empty_file(self): - name = self.empty - self.assertRaises(ID3Error, ID3, filename=name) + self.assertRaises(ID3Error, ID3, filename=self.empty) def test_nonexistent_file(self): - name = join(DATA_DIR, 'does', 'not', 'exist') + name = os.path.join(DATA_DIR, 'does', 'not', 'exist') self.assertRaises(MutagenError, ID3, name) def test_read_padding(self): self.assertEqual(ID3(self.silence)._padding, 1142) self.assertEqual(ID3(self.unsynch)._padding, 0) + def test_load_v23_unsynch(self): + id3 = ID3(self.unsynch) + self.assertEquals(id3["TPE1"], ["Nina Simone"]) + + def test_bad_extended_header_flags(self): + # Files with bad extended header flags failed to read tags. + # Ensure the extended header is turned off, and the frames are + # read. + id3 = ID3(os.path.join(DATA_DIR, 'issue_21.id3')) + self.failIf(id3.f_extended) + self.failUnless("TIT2" in id3) + self.failUnless("TALB" in id3) + self.failUnlessEqual(id3["TIT2"].text, [u"Punk To Funk"]) + + def test_no_known_frames(self): + id3 = ID3(self.silence, known_frames={}) + self.assertEquals(0, len(id3.keys())) + self.assertEquals(9, len(id3.unknown_frames)) + + def test_unknown_reset(self): + id3 = ID3(self.silence, known_frames={}) + self.assertEquals(9, len(id3.unknown_frames)) + id3.load(self.silence, known_frames={}) + self.assertEquals(9, len(id3.unknown_frames)) + + def test_23_multiframe_hack(self): + + class ID3hack(ID3): + "Override 'correct' behavior with desired behavior" + def loaded_frame(self, tag): + if tag.HashKey in self: + self[tag.HashKey].extend(tag[:]) + else: + self[tag.HashKey] = tag + + id3 = ID3hack(self.silence) + self.assertEquals(8, len(id3.keys())) + self.assertEquals(0, len(id3.unknown_frames)) + self.assertEquals('Quod Libet Test Data', id3['TALB']) + self.assertEquals('Silence', str(id3['TCON'])) + self.assertEquals('Silence', str(id3['TIT1'])) + self.assertEquals('Silence', str(id3['TIT2'])) + self.assertEquals(3000, +id3['TLEN']) + self.assertEquals(['piman', 'jzig'], id3['TPE1']) + self.assertEquals('02/10', id3['TRCK']) + self.assertEquals(2, +id3['TRCK']) + self.assertEquals('2004', id3['TDRC']) + + def test_chap_subframes(self): + id3 = ID3() + id3.version = (2, 3) + id3.add(CHAP(element_id="foo", start_time=0, end_time=0, + start_offset=0, end_offset=0, + sub_frames=[TYER(encoding=0, text="2006")])) + id3.update_to_v24() + chap = id3.getall("CHAP:foo")[0] + self.assertEqual(chap.sub_frames.getall("TDRC")[0], u"2006") + self.assertFalse(chap.sub_frames.getall("TYER")) + id3.update_to_v23() + self.assertEqual(chap.sub_frames.getall("TYER")[0], u"2006") + + def test_ctoc_subframes(self): + id3 = ID3() + id3.version = (2, 3) + id3.add(CTOC(sub_frames=[TYER(encoding=0, text="2006")])) + id3.update_to_v24() + ctoc = id3.getall("CTOC")[0] + self.assertEqual(ctoc.sub_frames.getall("TDRC")[0], u"2006") + self.assertFalse(ctoc.sub_frames.getall("TYER")) + id3.update_to_v23() + self.assertEqual(ctoc.sub_frames.getall("TYER")[0], u"2006") + + def test_pic(self): + id3 = ID3() + id3.version = (2, 2) + id3.add(PIC(encoding=0, mime="PNG", desc="cover", type=3, data=b"")) + id3.update_to_v24() + self.failUnlessEqual(id3["APIC:cover"].mime, "image/png") + + def test_lnk(self): + id3 = ID3() + id3.version = (2, 2) + id3.add(LNK(frameid="PIC", url="http://foo.bar")) + id3.update_to_v24() + self.assertTrue(id3.getall("LINK")) + + def test_tyer(self): + id3 = ID3() + id3.version = (2, 3) + id3.add(TYER(encoding=0, text="2006")) + id3.update_to_v24() + self.failUnlessEqual(id3["TDRC"], "2006") + + def test_tyer_tdat(self): + id3 = ID3() + id3.version = (2, 3) + id3.add(TYER(encoding=0, text="2006")) + id3.add(TDAT(encoding=0, text="0603")) + id3.update_to_v24() + self.failUnlessEqual(id3["TDRC"], "2006-03-06") + + def test_tyer_tdat_time(self): + id3 = ID3() + id3.version = (2, 3) + id3.add(TYER(encoding=0, text="2006")) + id3.add(TDAT(encoding=0, text="0603")) + id3.add(TIME(encoding=0, text="1127")) + id3.update_to_v24() + self.failUnlessEqual(id3["TDRC"], "2006-03-06 11:27:00") + + def test_tory(self): + id3 = ID3() + id3.version = (2, 3) + id3.add(TORY(encoding=0, text="2006")) + id3.update_to_v24() + self.failUnlessEqual(id3["TDOR"], "2006") + + def test_ipls(self): + id3 = ID3() + id3.version = (2, 3) + id3.add(IPLS(encoding=0, people=[["a", "b"], ["c", "d"]])) + id3.update_to_v24() + self.failUnlessEqual(id3["TIPL"], [["a", "b"], ["c", "d"]]) + + def test_time_dropped(self): + id3 = ID3() + id3.version = (2, 3) + id3.add(TIME(encoding=0, text=["1155"])) + id3.update_to_v24() + self.assertFalse(id3.getall("TIME")) + + def test_rvad_dropped(self): + id3 = ID3() + id3.version = (2, 3) + id3.add(RVAD()) + id3.update_to_v24() + self.assertFalse(id3.getall("RVAD")) + + +class TID3Header(TestCase): + + silence = os.path.join(DATA_DIR, 'silence-44-s.mp3') + empty = os.path.join(DATA_DIR, 'emptyfile.mp3') + def test_header_empty(self): - fileobj = open(self.empty, 'rb') - self.assertRaises(ID3Error, ID3Header, fileobj) + with open(self.empty, 'rb') as fileobj: + self.assertRaises(ID3Error, ID3Header, fileobj) def test_header_silence(self): - fileobj = open(self.silence, 'rb') - header = ID3Header(fileobj) + with open(self.silence, 'rb') as fileobj: + header = ID3Header(fileobj) self.assertEquals(header.version, (2, 3, 0)) self.assertEquals(header.size, 1314) @@ -174,77 +329,9 @@ header = ID3Header(fileobj) self.assertEquals(header._extdata, b'\x00\x00\x56\x78\x9a\xbc') - def test_unsynch(self): - header = ID3Header() - header.version = (2, 4, 0) - header._flags = 0x80 - badsync = b'\x00\xff\x00ab\x00' - - self.assertEquals( - Frames["TPE2"]._fromData(header, 0, badsync), [u"\xffab"]) - - header._flags = 0x00 - self.assertEquals( - Frames["TPE2"]._fromData(header, 0x02, badsync), [u"\xffab"]) - - tag = Frames["TPE2"]._fromData(header, 0, badsync) - self.assertEquals(tag, [u"\xff", u"ab"]) - - def test_load_v23_unsynch(self): - id3 = ID3(self.unsynch) - self.assertEquals(id3["TPE1"], ["Nina Simone"]) - - def test_insane__ID3__fullread(self): - fileobj = cBytesIO() - self.assertRaises(ValueError, _fullread, fileobj, -3) - self.assertRaises(EOFError, _fullread, fileobj, 3) - - -class Issue21(TestCase): - - # Files with bad extended header flags failed to read tags. - # Ensure the extended header is turned off, and the frames are - # read. - def setUp(self): - self.id3 = ID3(join(DATA_DIR, 'issue_21.id3')) - - def test_no_ext(self): - self.failIf(self.id3.f_extended) - - def test_has_tags(self): - self.failUnless("TIT2" in self.id3) - self.failUnless("TALB" in self.id3) - - def test_tit2_value(self): - self.failUnlessEqual(self.id3["TIT2"].text, [u"Punk To Funk"]) - - -class ID3Tags(TestCase): - - def setUp(self): - self.silence = join(DATA_DIR, 'silence-44-s.mp3') - - def test_set_wrong_type(self): - id3 = ID3(self.silence) - self.assertRaises(TypeError, id3.__setitem__, "FOO", object()) - - def test_None(self): - id3 = ID3(self.silence, known_frames={}) - self.assertEquals(0, len(id3.keys())) - self.assertEquals(9, len(id3.unknown_frames)) - - def test_unknown_reset(self): - id3 = ID3(self.silence, known_frames={}) - self.assertEquals(9, len(id3.unknown_frames)) - id3.load(self.silence, known_frames={}) - self.assertEquals(9, len(id3.unknown_frames)) - - def test_has_docs(self): - for Kind in (list(Frames.values()) + list(Frames_2_2.values())): - self.failUnless(Kind.__doc__, "%s has no docstring" % Kind) - def test_23(self): id3 = ID3(self.silence) + self.assertEqual(id3.version, (2, 3, 0)) self.assertEquals(8, len(id3.keys())) self.assertEquals(0, len(id3.unknown_frames)) self.assertEquals('Quod Libet Test Data', id3['TALB']) @@ -257,76 +344,91 @@ self.assertEquals(2, +id3['TRCK']) self.assertEquals('2004', id3['TDRC']) - def test_23_multiframe_hack(self): - class ID3hack(ID3): - "Override 'correct' behavior with desired behavior" - def loaded_frame(self, tag): - if tag.HashKey in self: - self[tag.HashKey].extend(tag[:]) - else: - self[tag.HashKey] = tag - id3 = ID3hack(self.silence) - self.assertEquals(8, len(id3.keys())) - self.assertEquals(0, len(id3.unknown_frames)) - self.assertEquals('Quod Libet Test Data', id3['TALB']) - self.assertEquals('Silence', str(id3['TCON'])) - self.assertEquals('Silence', str(id3['TIT1'])) - self.assertEquals('Silence', str(id3['TIT2'])) - self.assertEquals(3000, +id3['TLEN']) - self.assertEquals(['piman', 'jzig'], id3['TPE1']) - self.assertEquals('02/10', id3['TRCK']) - self.assertEquals(2, +id3['TRCK']) - self.assertEquals('2004', id3['TDRC']) +class TID3Tags(TestCase): + + silence = os.path.join(DATA_DIR, 'silence-44-s.mp3') + + def setUp(self): + self.frames = [ + TIT2(text=["1"]), TIT2(text=["2"]), + TIT2(text=["3"]), TIT2(text=["4"])] + self.i = ID3Tags() + self.i["BLAH"] = self.frames[0] + self.i["QUUX"] = self.frames[1] + self.i["FOOB:ar"] = self.frames[2] + self.i["FOOB:az"] = self.frames[3] + + def test_update_v22_add(self): + id3 = ID3Tags() + tt1 = TT1(encoding=0, text=u'whatcha staring at?') + id3.loaded_frame(tt1) + tit1 = id3['TIT1'] + + self.assertEquals(tt1.encoding, tit1.encoding) + self.assertEquals(tt1.text, tit1.text) + self.assert_('TT1' not in id3) + + def test_getnormal(self): + self.assertEquals(self.i.getall("BLAH"), [self.frames[0]]) + self.assertEquals(self.i.getall("QUUX"), [self.frames[1]]) + self.assertEquals(self.i.getall("FOOB:ar"), [self.frames[2]]) + self.assertEquals(self.i.getall("FOOB:az"), [self.frames[3]]) + + def test_getlist(self): + self.assertTrue( + self.i.getall("FOOB") in [[self.frames[2], self.frames[3]], + [self.frames[3], self.frames[2]]]) + + def test_delnormal(self): + self.assert_("BLAH" in self.i) + self.i.delall("BLAH") + self.assert_("BLAH" not in self.i) + + def test_delone(self): + self.i.delall("FOOB:ar") + self.assertEquals(self.i.getall("FOOB"), [self.frames[3]]) + + def test_delall(self): + self.assert_("FOOB:ar" in self.i) + self.assert_("FOOB:az" in self.i) + self.i.delall("FOOB") + self.assert_("FOOB:ar" not in self.i) + self.assert_("FOOB:az" not in self.i) + + def test_setone(self): + class TEST(TIT2): + HashKey = "" + + t = TEST() + t.HashKey = "FOOB:ar" + self.i.setall("FOOB", [t]) + self.assertEquals(self.i["FOOB:ar"], t) + self.assertEquals(self.i.getall("FOOB"), [t]) - def test_badencoding(self): - self.assertRaises( - ID3JunkFrameError, Frames["TPE1"]._fromData, _24, 0, b"\x09ab") - self.assertRaises( - ValueError, Frames["TPE1"], encoding=9, text="ab") - - def test_badsync(self): - frame = Frames["TPE1"]._fromData(_24, 0x02, b"\x00\xff\xfe") - self.assertEqual(frame.text, [u'\xff\xfe']) - - def test_noencrypt(self): - self.assertRaises( - NotImplementedError, Frames["TPE1"]._fromData, _24, 0x04, b"\x00") - self.assertRaises( - NotImplementedError, Frames["TPE1"]._fromData, _23, 0x40, b"\x00") - - def test_badcompress(self): - self.assertRaises( - ID3JunkFrameError, Frames["TPE1"]._fromData, _24, 0x08, - b"\x00\x00\x00\x00#") - self.assertRaises( - ID3JunkFrameError, Frames["TPE1"]._fromData, _23, 0x80, - b"\x00\x00\x00\x00#") - - def test_junkframe(self): - self.assertRaises( - ID3JunkFrameError, Frames["TPE1"]._fromData, _24, 0, b"") - - def test_bad_sylt(self): - self.assertRaises( - ID3JunkFrameError, Frames["SYLT"]._fromData, _24, 0x0, - b"\x00eng\x01description\x00foobar") - self.assertRaises( - ID3JunkFrameError, Frames["SYLT"]._fromData, _24, 0x0, - b"\x00eng\x01description\x00foobar\x00\xFF\xFF\xFF") - - def test_extradata(self): - from mutagen.id3 import RVRB, RBUF - self.assertEqual(RVRB()._readData(b'L1R1BBFFFFPP#xyz'), b'#xyz') - self.assertEqual( - RBUF()._readData(b'\x00\x01\x00\x01\x00\x00\x00\x00#xyz'), b'#xyz') + def test_settwo(self): + class TEST(TIT2): + HashKey = "" + + t = TEST() + t.HashKey = "FOOB:ar" + t2 = TEST() + t2.HashKey = "FOOB:az" + self.i.setall("FOOB", [t, t2]) + self.assertEquals(self.i["FOOB:ar"], t) + self.assertEquals(self.i["FOOB:az"], t2) + self.assert_(self.i.getall("FOOB") in [[t, t2], [t2, t]]) + + def test_set_wrong_type(self): + id3 = ID3Tags() + self.assertRaises(TypeError, id3.__setitem__, "FOO", object()) class ID3v1Tags(TestCase): def setUp(self): - self.silence = join(DATA_DIR, 'silence-44-s-v1.mp3') - self.id3 = ID3(self.silence) + self.filename = os.path.join(DATA_DIR, 'silence-44-s-v1.mp3') + self.id3 = ID3(self.filename) def test_album(self): self.assertEquals('Quod Libet Test Data', self.id3['TALB']) @@ -348,7 +450,6 @@ self.assertEquals('2004', self.id3['TDRC']) def test_v1_not_v11(self): - from mutagen.id3 import MakeID3v1, ParseID3v1, TRCK self.id3["TRCK"] = TRCK(encoding=0, text="32") tag = MakeID3v1(self.id3) self.failUnless(32, ParseID3v1(tag)["TRCK"]) @@ -358,7 +459,6 @@ self.failIf("TRCK" in ParseID3v1(tag)) def test_nulls(self): - from mutagen.id3 import ParseID3v1 s = u'TAG%(title)30s%(artist)30s%(album)30s%(year)4s%(cmt)29s\x03\x01' s = s % dict(artist=u'abcd\00fg', title=u'hijklmn\x00p', album=u'qrst\x00v', cmt=u'wxyz', year=u'1224') @@ -368,7 +468,6 @@ self.assertEquals(b'qrst'.decode('latin1'), tags['TALB']) def test_nonascii(self): - from mutagen.id3 import ParseID3v1 s = u'TAG%(title)30s%(artist)30s%(album)30s%(year)4s%(cmt)29s\x03\x01' s = s % dict(artist=u'abcd\xe9fg', title=u'hijklmn\xf3p', album=u'qrst\xfcv', cmt=u'wxyz', year=u'1234') @@ -381,14 +480,12 @@ self.assertEquals("1234", tags['TDRC']) def test_roundtrip(self): - from mutagen.id3 import ParseID3v1, MakeID3v1 frames = {} for key in ["TIT2", "TALB", "TPE1", "TDRC"]: frames[key] = self.id3[key] self.assertEquals(ParseID3v1(MakeID3v1(frames)), frames) def test_make_from_empty(self): - from mutagen.id3 import MakeID3v1, TCON, COMM empty = b'TAG' + b'\x00' * 124 + b'\xff' self.assertEquals(MakeID3v1({}), empty) self.assertEquals(MakeID3v1({'TCON': TCON()}), empty) @@ -396,7 +493,6 @@ MakeID3v1({'COMM': COMM(encoding=0, text="")}), empty) def test_make_v1_from_tyer(self): - from mutagen.id3 import ParseID3v1, MakeID3v1, TYER, TDRC self.assertEquals( MakeID3v1({"TDRC": TDRC(text="2010-10-10")}), MakeID3v1({"TYER": TYER(text="2010")})) @@ -405,18 +501,15 @@ ParseID3v1(MakeID3v1({"TYER": TYER(text="2010")}))) def test_invalid(self): - from mutagen.id3 import ParseID3v1 self.failUnless(ParseID3v1(b"") is None) def test_invalid_track(self): - from mutagen.id3 import ParseID3v1, MakeID3v1, TRCK tag = {} tag["TRCK"] = TRCK(encoding=0, text="not a number") v1tag = MakeID3v1(tag) self.failIf("TRCK" in ParseID3v1(v1tag)) def test_v1_genre(self): - from mutagen.id3 import ParseID3v1, MakeID3v1, TCON tag = {} tag["TCON"] = TCON(encoding=0, text="Pop") v1tag = MakeID3v1(tag) @@ -424,23 +517,21 @@ class TestWriteID3v1(TestCase): - SILENCE = os.path.join(DATA_DIR, "silence-44-s.mp3") def setUp(self): - fd, self.filename = mkstemp(suffix='.mp3') - os.close(fd) - shutil.copy(self.SILENCE, self.filename) + self.filename = get_temp_copy( + os.path.join(DATA_DIR, "silence-44-s.mp3")) self.audio = ID3(self.filename) def failIfV1(self): - fileobj = open(self.filename, "rb") - fileobj.seek(-128, 2) - self.failIf(fileobj.read(3) == b"TAG") + with open(self.filename, "rb") as fileobj: + fileobj.seek(-128, 2) + self.failIf(fileobj.read(3) == b"TAG") def failUnlessV1(self): - fileobj = open(self.filename, "rb") - fileobj.seek(-128, 2) - self.failUnless(fileobj.read(3) == b"TAG") + with open(self.filename, "rb") as fileobj: + fileobj.seek(-128, 2) + self.failUnless(fileobj.read(3) == b"TAG") def test_save_delete(self): self.audio.save(v1=0) @@ -464,803 +555,248 @@ os.unlink(self.filename) -class TestV22Tags(TestCase): +class Issue97_UpgradeUnknown23(TestCase): def setUp(self): - filename = os.path.join(DATA_DIR, "id3v22-test.mp3") - self.tags = ID3(filename) - - def test_tags(self): - self.failUnless(self.tags["TRCK"].text == ["3/11"]) - self.failUnless(self.tags["TPE1"].text == ["Anais Mitchell"]) - - -def create_read_tag_tests(): - tests = [ - ['TALB', b'\x00a/b', 'a/b', '', dict(encoding=0)], - ['TBPM', b'\x00120', '120', 120, dict(encoding=0)], - ['TCMP', b'\x001', '1', 1, dict(encoding=0)], - ['TCMP', b'\x000', '0', 0, dict(encoding=0)], - ['TCOM', b'\x00a/b', 'a/b', '', dict(encoding=0)], - ['TCON', b'\x00(21)Disco', '(21)Disco', '', dict(encoding=0)], - ['TCOP', b'\x001900 c', '1900 c', '', dict(encoding=0)], - ['TDAT', b'\x00a/b', 'a/b', '', dict(encoding=0)], - ['TDEN', b'\x001987', '1987', '', dict(encoding=0, year=[1987])], - [ - 'TDOR', b'\x001987-12', '1987-12', '', - dict(encoding=0, year=[1987], month=[12]) - ], - ['TDRC', b'\x001987\x00', '1987', '', dict(encoding=0, year=[1987])], - [ - 'TDRL', b'\x001987\x001988', '1987,1988', '', - dict(encoding=0, year=[1987, 1988]) - ], - ['TDTG', b'\x001987', '1987', '', dict(encoding=0, year=[1987])], - ['TDLY', b'\x001205', '1205', 1205, dict(encoding=0)], - ['TENC', b'\x00a b/c d', 'a b/c d', '', dict(encoding=0)], - ['TEXT', b'\x00a b\x00c d', ['a b', 'c d'], '', dict(encoding=0)], - ['TFLT', b'\x00MPG/3', 'MPG/3', '', dict(encoding=0)], - ['TIME', b'\x001205', '1205', '', dict(encoding=0)], - [ - 'TIPL', b'\x02\x00a\x00\x00\x00b', [["a", "b"]], '', - dict(encoding=2) - ], - ['TIT1', b'\x00a/b', 'a/b', '', dict(encoding=0)], - # TIT2 checks misaligned terminator '\x00\x00' across crosses utf16 - # chars - [ - 'TIT2', b'\x01\xff\xfe\x38\x00\x00\x38', u'8\u3800', '', - dict(encoding=1) - ], - ['TIT3', b'\x00a/b', 'a/b', '', dict(encoding=0)], - ['TKEY', b'\x00A#m', 'A#m', '', dict(encoding=0)], - ['TLAN', b'\x006241', '6241', '', dict(encoding=0)], - ['TLEN', b'\x006241', '6241', 6241, dict(encoding=0)], - [ - 'TMCL', b'\x02\x00a\x00\x00\x00b', [["a", "b"]], '', - dict(encoding=2) - ], - ['TMED', b'\x00med', 'med', '', dict(encoding=0)], - ['TMOO', b'\x00moo', 'moo', '', dict(encoding=0)], - ['TOAL', b'\x00alb', 'alb', '', dict(encoding=0)], - ['TOFN', b'\x0012 : bar', '12 : bar', '', dict(encoding=0)], - ['TOLY', b'\x00lyr', 'lyr', '', dict(encoding=0)], - ['TOPE', b'\x00own/lic', 'own/lic', '', dict(encoding=0)], - ['TORY', b'\x001923', '1923', 1923, dict(encoding=0)], - ['TOWN', b'\x00own/lic', 'own/lic', '', dict(encoding=0)], - ['TPE1', b'\x00ab', ['ab'], '', dict(encoding=0)], - [ - 'TPE2', b'\x00ab\x00cd\x00ef', ['ab', 'cd', 'ef'], '', - dict(encoding=0) - ], - ['TPE3', b'\x00ab\x00cd', ['ab', 'cd'], '', dict(encoding=0)], - ['TPE4', b'\x00ab\x00', ['ab'], '', dict(encoding=0)], - ['TPOS', b'\x0008/32', '08/32', 8, dict(encoding=0)], - ['TPRO', b'\x00pro', 'pro', '', dict(encoding=0)], - ['TPUB', b'\x00pub', 'pub', '', dict(encoding=0)], - ['TRCK', b'\x004/9', '4/9', 4, dict(encoding=0)], - ['TRDA', b'\x00Sun Jun 12', 'Sun Jun 12', '', dict(encoding=0)], - ['TRSN', b'\x00ab/cd', 'ab/cd', '', dict(encoding=0)], - ['TRSO', b'\x00ab', 'ab', '', dict(encoding=0)], - ['TSIZ', b'\x0012345', '12345', 12345, dict(encoding=0)], - ['TSOA', b'\x00ab', 'ab', '', dict(encoding=0)], - ['TSOP', b'\x00ab', 'ab', '', dict(encoding=0)], - ['TSOT', b'\x00ab', 'ab', '', dict(encoding=0)], - ['TSO2', b'\x00ab', 'ab', '', dict(encoding=0)], - ['TSOC', b'\x00ab', 'ab', '', dict(encoding=0)], - ['TSRC', b'\x0012345', '12345', '', dict(encoding=0)], - ['TSSE', b'\x0012345', '12345', '', dict(encoding=0)], - ['TSST', b'\x0012345', '12345', '', dict(encoding=0)], - ['TYER', b'\x002004', '2004', 2004, dict(encoding=0)], - [ - 'TXXX', b'\x00usr\x00a/b\x00c', ['a/b', 'c'], '', - dict(encoding=0, desc='usr') - ], - ['WCOM', b'http://foo', 'http://foo', '', {}], - ['WCOP', b'http://bar', 'http://bar', '', {}], - ['WOAF', b'http://baz', 'http://baz', '', {}], - ['WOAR', b'http://bar', 'http://bar', '', {}], - ['WOAS', b'http://bar', 'http://bar', '', {}], - ['WORS', b'http://bar', 'http://bar', '', {}], - ['WPAY', b'http://bar', 'http://bar', '', {}], - ['WPUB', b'http://bar', 'http://bar', '', {}], - ['WXXX', b'\x00usr\x00http', 'http', '', dict(encoding=0, desc='usr')], - [ - 'IPLS', b'\x00a\x00A\x00b\x00B\x00', [['a', 'A'], ['b', 'B']], '', - dict(encoding=0) - ], - ['MCDI', b'\x01\x02\x03\x04', b'\x01\x02\x03\x04', '', {}], - [ - 'ETCO', b'\x01\x12\x00\x00\x7f\xff', [(18, 32767)], '', - dict(format=1) - ], - [ - 'COMM', b'\x00ENUT\x00Com', 'Com', '', - dict(desc='T', lang='ENU', encoding=0) - ], - [ - 'APIC', b'\x00-->\x00\x03cover\x00cover.jpg', b'cover.jpg', '', - dict(mime='-->', type=3, desc='cover', encoding=0) - ], - ['USER', b'\x00ENUCom', 'Com', '', dict(lang='ENU', encoding=0)], - [ - 'RVA2', b'testdata\x00\x01\xfb\x8c\x10\x12\x23', - 'Master volume: -2.2266 dB/0.1417', '', - dict(desc='testdata', channel=1, gain=-2.22656, peak=0.14169) - ], - [ - 'RVA2', b'testdata\x00\x01\xfb\x8c\x24\x01\x22\x30\x00\x00', - 'Master volume: -2.2266 dB/0.1417', '', - dict(desc='testdata', channel=1, gain=-2.22656, peak=0.14169) - ], - [ - 'RVA2', b'testdata2\x00\x01\x04\x01\x00', - 'Master volume: +2.0020 dB/0.0000', '', - dict(desc='testdata2', channel=1, gain=2.001953125, peak=0) - ], - ['PCNT', b'\x00\x00\x00\x11', 17, 17, dict(count=17)], - [ - 'POPM', b'foo@bar.org\x00\xde\x00\x00\x00\x11', 222, 222, - dict(email="foo@bar.org", rating=222, count=17) - ], - [ - 'POPM', b'foo@bar.org\x00\xde\x00', 222, 222, - dict(email="foo@bar.org", rating=222, count=0) - ], - # Issue #33 - POPM may have no playcount at all. - [ - 'POPM', b'foo@bar.org\x00\xde', 222, 222, - dict(email="foo@bar.org", rating=222) - ], - ['UFID', b'own\x00data', b'data', '', dict(data=b'data', owner='own')], - ['UFID', b'own\x00\xdd', b'\xdd', '', dict(data=b'\xdd', owner='own')], - [ - 'GEOB', b'\x00mime\x00name\x00desc\x00data', b'data', '', - dict(encoding=0, mime='mime', filename='name', desc='desc') - ], - [ - 'USLT', b'\x00engsome lyrics\x00woo\nfun', 'woo\nfun', '', - dict(encoding=0, lang='eng', desc='some lyrics', text='woo\nfun') - ], - [ - 'SYLT', (b'\x00eng\x02\x01some lyrics\x00foo\x00\x00\x00\x00\x01' - b'bar\x00\x00\x00\x00\x10'), "foobar", '', - dict(encoding=0, lang='eng', type=1, format=2, desc='some lyrics') - ], - ['POSS', b'\x01\x0f', 15, 15, dict(format=1, position=15)], - [ - 'OWNE', b'\x00USD10.01\x0020041010CDBaby', 'CDBaby', 'CDBaby', - dict(encoding=0, price="USD10.01", date='20041010', - seller='CDBaby') - ], - [ - 'PRIV', b'a@b.org\x00random data', b'random data', 'random data', - dict(owner='a@b.org', data=b'random data') - ], - [ - 'PRIV', b'a@b.org\x00\xdd', b'\xdd', '\xdd', - dict(owner='a@b.org', data=b'\xdd') - ], - ['SIGN', b'\x92huh?', b'huh?', 'huh?', dict(group=0x92, sig=b'huh?')], - [ - 'ENCR', b'a@b.org\x00\x92Data!', b'Data!', 'Data!', - dict(owner='a@b.org', method=0x92, data=b'Data!') - ], - [ - 'SEEK', b'\x00\x12\x00\x56', - 0x12 * 256 * 256 + 0x56, 0x12 * 256 * 256 + 0x56, - dict(offset=0x12 * 256 * 256 + 0x56) - ], - [ - 'SYTC', b"\x01\x10obar", b'\x10obar', '', - dict(format=1, data=b'\x10obar') - ], - [ - 'RBUF', b'\x00\x12\x00', 0x12 * 256, 0x12 * 256, - dict(size=0x12 * 256) - ], - [ - 'RBUF', b'\x00\x12\x00\x01', 0x12 * 256, 0x12 * 256, - dict(size=0x12 * 256, info=1) - ], - [ - 'RBUF', b'\x00\x12\x00\x01\x00\x00\x00\x23', - 0x12 * 256, 0x12 * 256, - dict(size=0x12 * 256, info=1, offset=0x23) - ], - [ - 'RVRB', b'\x12\x12\x23\x23\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11', - (0x12 * 256 + 0x12, 0x23 * 256 + 0x23), '', - dict(left=0x12 * 256 + 0x12, right=0x23 * 256 + 0x23) - ], - [ - 'AENC', b'a@b.org\x00\x00\x12\x00\x23', 'a@b.org', 'a@b.org', - dict(owner='a@b.org', preview_start=0x12, preview_length=0x23) - ], - [ - 'AENC', b'a@b.org\x00\x00\x12\x00\x23!', 'a@b.org', 'a@b.org', - dict(owner='a@b.org', preview_start=0x12, - preview_length=0x23, data=b'!') - ], - [ - 'GRID', b'a@b.org\x00\x99', 'a@b.org', 0x99, - dict(owner='a@b.org', group=0x99) - ], - [ - 'GRID', b'a@b.org\x00\x99data', 'a@b.org', 0x99, - dict(owner='a@b.org', group=0x99, data=b'data') - ], - [ - 'COMR', - (b'\x00USD10.00\x0020051010ql@sc.net\x00\x09Joe\x00A song\x00' - b'x-image/fake\x00some data'), - COMR(encoding=0, price="USD10.00", valid_until="20051010", - contact="ql@sc.net", format=9, seller="Joe", desc="A song", - mime='x-image/fake', logo=b'some data'), '', - dict(encoding=0, price="USD10.00", valid_until="20051010", - contact="ql@sc.net", format=9, seller="Joe", desc="A song", - mime='x-image/fake', logo=b'some data') - ], - [ - 'COMR', - b'\x00USD10.00\x0020051010ql@sc.net\x00\x09Joe\x00A song\x00', - COMR(encoding=0, price="USD10.00", valid_until="20051010", - contact="ql@sc.net", format=9, seller="Joe", desc="A song"), - '', - dict(encoding=0, price="USD10.00", valid_until="20051010", - contact="ql@sc.net", format=9, seller="Joe", desc="A song") - ], - [ - 'MLLT', b'\x00\x01\x00\x00\x02\x00\x00\x03\x04\x08foobar', - b'foobar', '', - dict(frames=1, bytes=2, milliseconds=3, bits_for_bytes=4, - bits_for_milliseconds=8, data=b'foobar') - ], - [ - 'EQU2', b'\x00Foobar\x00\x01\x01\x04\x00', [(128.5, 2.0)], '', - dict(method=0, desc="Foobar") - ], - [ - 'ASPI', - b'\x00\x00\x00\x00\x00\x00\x00\x10\x00\x03\x08\x01\x02\x03', - [1, 2, 3], '', dict(S=0, L=16, N=3, b=8) - ], - [ - 'ASPI', b'\x00\x00\x00\x00\x00\x00\x00\x10\x00\x03\x10' - b'\x00\x01\x00\x02\x00\x03', [1, 2, 3], '', - dict(S=0, L=16, N=3, b=16) - ], - [ - 'LINK', b'TIT1http://www.example.org/TIT1.txt\x00', - ("TIT1", 'http://www.example.org/TIT1.txt'), '', - dict(frameid='TIT1', url='http://www.example.org/TIT1.txt') - ], - [ - 'LINK', b'COMMhttp://www.example.org/COMM.txt\x00engfoo', - ("COMM", 'http://www.example.org/COMM.txt', b'engfoo'), '', - dict(frameid='COMM', url='http://www.example.org/COMM.txt', - data=b'engfoo') - ], - # iTunes podcast frames - ['TGID', b'\x00i', u'i', '', dict(encoding=0)], - ['TDES', b'\x00ii', u'ii', '', dict(encoding=0)], - ['WFED', b'http://zzz', 'http://zzz', '', {}], - - # 2.2 tags - ['UFI', b'own\x00data', b'data', '', dict(data=b'data', owner='own')], - [ - 'SLT', (b'\x00eng\x02\x01some lyrics\x00foo\x00\x00\x00\x00\x01bar' - b'\x00\x00\x00\x00\x10'), - "foobar", '', - dict(encoding=0, lang='eng', type=1, format=2, desc='some lyrics') - ], - ['TT1', b'\x00ab\x00', 'ab', '', dict(encoding=0)], - ['TT2', b'\x00ab', 'ab', '', dict(encoding=0)], - ['TT3', b'\x00ab', 'ab', '', dict(encoding=0)], - ['TP1', b'\x00ab\x00', 'ab', '', dict(encoding=0)], - ['TP2', b'\x00ab', 'ab', '', dict(encoding=0)], - ['TP3', b'\x00ab', 'ab', '', dict(encoding=0)], - ['TP4', b'\x00ab', 'ab', '', dict(encoding=0)], - ['TCM', b'\x00ab/cd', 'ab/cd', '', dict(encoding=0)], - ['TXT', b'\x00lyr', 'lyr', '', dict(encoding=0)], - ['TLA', b'\x00ENU', 'ENU', '', dict(encoding=0)], - ['TCO', b'\x00gen', 'gen', '', dict(encoding=0)], - ['TAL', b'\x00alb', 'alb', '', dict(encoding=0)], - ['TPA', b'\x001/9', '1/9', 1, dict(encoding=0)], - ['TRK', b'\x002/8', '2/8', 2, dict(encoding=0)], - ['TRC', b'\x00isrc', 'isrc', '', dict(encoding=0)], - ['TYE', b'\x001900', '1900', 1900, dict(encoding=0)], - ['TDA', b'\x002512', '2512', '', dict(encoding=0)], - ['TIM', b'\x001225', '1225', '', dict(encoding=0)], - ['TRD', b'\x00Jul 17', 'Jul 17', '', dict(encoding=0)], - ['TMT', b'\x00DIG/A', 'DIG/A', '', dict(encoding=0)], - ['TFT', b'\x00MPG/3', 'MPG/3', '', dict(encoding=0)], - ['TBP', b'\x00133', '133', 133, dict(encoding=0)], - ['TCP', b'\x001', '1', 1, dict(encoding=0)], - ['TCP', b'\x000', '0', 0, dict(encoding=0)], - ['TCR', b'\x00Me', 'Me', '', dict(encoding=0)], - ['TPB', b'\x00Him', 'Him', '', dict(encoding=0)], - ['TEN', b'\x00Lamer', 'Lamer', '', dict(encoding=0)], - ['TSS', b'\x00ab', 'ab', '', dict(encoding=0)], - ['TOF', b'\x00ab:cd', 'ab:cd', '', dict(encoding=0)], - ['TLE', b'\x0012', '12', 12, dict(encoding=0)], - ['TSI', b'\x0012', '12', 12, dict(encoding=0)], - ['TDY', b'\x0012', '12', 12, dict(encoding=0)], - ['TKE', b'\x00A#m', 'A#m', '', dict(encoding=0)], - ['TOT', b'\x00org', 'org', '', dict(encoding=0)], - ['TOA', b'\x00org', 'org', '', dict(encoding=0)], - ['TOL', b'\x00org', 'org', '', dict(encoding=0)], - ['TOR', b'\x001877', '1877', 1877, dict(encoding=0)], - ['TXX', b'\x00desc\x00val', 'val', '', dict(encoding=0, desc='desc')], - - ['WAF', b'http://zzz', 'http://zzz', '', {}], - ['WAR', b'http://zzz', 'http://zzz', '', {}], - ['WAS', b'http://zzz', 'http://zzz', '', {}], - ['WCM', b'http://zzz', 'http://zzz', '', {}], - ['WCP', b'http://zzz', 'http://zzz', '', {}], - ['WPB', b'http://zzz', 'http://zzz', '', {}], - [ - 'WXX', b'\x00desc\x00http', 'http', '', - dict(encoding=0, desc='desc') - ], - [ - 'IPL', b'\x00a\x00A\x00b\x00B\x00', [['a', 'A'], ['b', 'B']], '', - dict(encoding=0) - ], - ['MCI', b'\x01\x02\x03\x04', b'\x01\x02\x03\x04', '', {}], - [ - 'ETC', b'\x01\x12\x00\x00\x7f\xff', [(18, 32767)], '', - dict(format=1) - ], - [ - 'COM', b'\x00ENUT\x00Com', 'Com', '', - dict(desc='T', lang='ENU', encoding=0) - ], - [ - 'PIC', b'\x00-->\x03cover\x00cover.jpg', b'cover.jpg', '', - dict(mime='-->', type=3, desc='cover', encoding=0) - ], - [ - 'POP', b'foo@bar.org\x00\xde\x00\x00\x00\x11', 222, 222, - dict(email="foo@bar.org", rating=222, count=17) - ], - ['CNT', b'\x00\x00\x00\x11', 17, 17, dict(count=17)], - [ - 'GEO', b'\x00mime\x00name\x00desc\x00data', b'data', '', - dict(encoding=0, mime='mime', filename='name', desc='desc') - ], - [ - 'ULT', b'\x00engsome lyrics\x00woo\nfun', 'woo\nfun', '', - dict(encoding=0, lang='eng', desc='some lyrics', text='woo\nfun')], - [ - 'BUF', b'\x00\x12\x00', 0x12 * 256, 0x12 * 256, - dict(size=0x12 * 256) - ], - [ - 'CRA', b'a@b.org\x00\x00\x12\x00\x23', 'a@b.org', 'a@b.org', - dict(owner='a@b.org', preview_start=0x12, preview_length=0x23) - ], - [ - 'CRA', b'a@b.org\x00\x00\x12\x00\x23!', 'a@b.org', 'a@b.org', - dict(owner='a@b.org', preview_start=0x12, - preview_length=0x23, data=b'!') - ], - [ - 'REV', b'\x12\x12\x23\x23\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11', - (0x12 * 256 + 0x12, 0x23 * 256 + 0x23), '', - dict(left=0x12 * 256 + 0x12, right=0x23 * 256 + 0x23) - ], - [ - 'STC', b"\x01\x10obar", b'\x10obar', '', - dict(format=1, data=b'\x10obar') - ], - [ - 'MLL', b'\x00\x01\x00\x00\x02\x00\x00\x03\x04\x08foobar', - b'foobar', '', - dict(frames=1, bytes=2, milliseconds=3, bits_for_bytes=4, - bits_for_milliseconds=8, data=b'foobar') - ], - [ - 'LNK', b'TT1http://www.example.org/TIT1.txt\x00', - ("TT1", 'http://www.example.org/TIT1.txt'), '', - dict(frameid='TT1', url='http://www.example.org/TIT1.txt') - ], - [ - 'CRM', b'foo@example.org\x00test\x00woo', b'woo', '', - dict(owner='foo@example.org', desc='test', data=b'woo') - ], - ] - - load_tests = {} - repr_tests = {} - write_tests = {} - for i, (tag, data, value, intval, info) in enumerate(tests): - info = info.copy() - - def test_tag(self, tag=tag, data=data, value=value, intval=intval, - info=info): - from operator import pos - id3 = __import__('mutagen.id3', globals(), locals(), [tag]) - TAG = getattr(id3, tag) - tag = TAG._fromData(_23, 0, data) - self.failUnless(tag.HashKey) - self.failUnless(tag.pprint()) - self.assertEquals(value, tag) - if 'encoding' not in info: - self.assertRaises(AttributeError, getattr, tag, 'encoding') - for attr, value in iteritems(info): - t = tag - if not isinstance(value, list): - value = [value] - t = [t] - for value, t in izip(value, iter(t)): - if isinstance(value, float): - self.failUnlessAlmostEqual(value, getattr(t, attr), 5) - else: - self.assertEquals(value, getattr(t, attr)) - - if isinstance(intval, integer_types): - self.assertEquals(intval, pos(t)) - else: - self.assertRaises(TypeError, pos, t) - - load_tests['test_%s_%d' % (tag, i)] = test_tag - - def test_tag_repr(self, tag=tag, data=data): - id3 = __import__('mutagen.id3', globals(), locals(), [tag]) - TAG = getattr(id3, tag) - tag = TAG._fromData(_23, 0, data) - self.assertTrue(isinstance(tag.__str__(), str)) - if PY2: - if hasattr(tag, "__unicode__"): - self.assertTrue(isinstance(tag.__unicode__(), unicode)) - else: - if hasattr(tag, "__bytes__"): - self.assertTrue(isinstance(tag.__bytes__(), bytes)) + self.filename = get_temp_copy( + os.path.join(DATA_DIR, "97-unknown-23-update.mp3")) - repr_tests['test_repr_%s_%d' % (tag, i)] = test_tag_repr - - def test_tag_write(self, tag=tag, data=data): - id3 = __import__('mutagen.id3', globals(), locals(), [tag]) - TAG = getattr(id3, tag) - tag = TAG._fromData(_24, 0, data) - towrite = tag._writeData() - tag2 = TAG._fromData(_24, 0, towrite) - for spec in TAG._framespec: - attr = spec.name - self.assertEquals(getattr(tag, attr), getattr(tag2, attr)) - write_tests['test_write_%s_%d' % (tag, i)] = test_tag_write - - testcase = type('TestReadTags', (TestCase,), load_tests) - assert testcase.__name__ not in globals() - globals()[testcase.__name__] = testcase - testcase = type('TestReadReprTags', (TestCase,), repr_tests) - assert testcase.__name__ not in globals() - globals()[testcase.__name__] = testcase - testcase = type('TestReadWriteTags', (TestCase,), write_tests) - assert testcase.__name__ not in globals() - globals()[testcase.__name__] = testcase - - from mutagen.id3 import Frames, Frames_2_2 - check = dict.fromkeys(list(Frames.keys()) + list(Frames_2_2.keys())) - tested_tags = dict.fromkeys([row[0] for row in tests]) - for tag in check: - def check(self, tag=tag): - self.assert_(tag in tested_tags) - tested_tags['test_' + tag + '_tested'] = check - testcase = type('TestTestedTags', (TestCase,), tested_tags) - assert testcase.__name__ not in globals() - globals()[testcase.__name__] = testcase + def tearDown(self): + os.unlink(self.filename) -create_read_tag_tests() + def test_unknown(self): + orig = ID3(self.filename) + self.failUnlessEqual(orig.version, (2, 3, 0)) + # load a 2.3 file and pretend we don't support TIT2 + unknown = ID3(self.filename, known_frames={"TPE1": TPE1}, + translate=False) + # TIT2 ends up in unknown_frames + self.failUnlessEqual(unknown.unknown_frames[0][:4], b"TIT2") + # save as 2.3 + unknown.save(v2_version=3) + # load again with support for TIT2, all should be there again + new = ID3(self.filename) + self.failUnlessEqual(new["TIT2"].text, orig["TIT2"].text) + self.failUnlessEqual(new["TPE1"].text, orig["TPE1"].text) -class UpdateTo24(TestCase): + def test_unknown_invalid(self): + frame = BinaryFrame(data=b"\xff" * 50) + f = ID3(self.filename) + self.assertEqual(f.version, ID3Header._V23) + config = ID3SaveConfig(3, None) + f.unknown_frames = [save_frame(frame, b"NOPE", config)] + f.save() + f = ID3(self.filename) + self.assertFalse(f.unknown_frames) - def test_pic(self): - from mutagen.id3 import PIC - id3 = ID3() - id3.version = (2, 2) - id3.add(PIC(encoding=0, mime="PNG", desc="cover", type=3, data=b"")) - id3.update_to_v24() - self.failUnlessEqual(id3["APIC:cover"].mime, "image/png") - def test_lnk(self): - from mutagen.id3 import LNK - id3 = ID3() - id3.version = (2, 2) - id3.add(LNK(frameid="PIC", url="http://foo.bar")) - id3.update_to_v24() - self.assertTrue(id3.getall("LINK")) +class TID3Write(TestCase): - def test_tyer(self): - from mutagen.id3 import TYER - id3 = ID3() - id3.version = (2, 3) - id3.add(TYER(encoding=0, text="2006")) - id3.update_to_v24() - self.failUnlessEqual(id3["TDRC"], "2006") + def setUp(self): + self.filename = get_temp_copy( + os.path.join(DATA_DIR, 'silence-44-s.mp3')) - def test_tyer_tdat(self): - from mutagen.id3 import TYER, TDAT - id3 = ID3() - id3.version = (2, 3) - id3.add(TYER(encoding=0, text="2006")) - id3.add(TDAT(encoding=0, text="0603")) - id3.update_to_v24() - self.failUnlessEqual(id3["TDRC"], "2006-03-06") + def tearDown(self): + try: + os.unlink(self.filename) + except OSError: + pass - def test_tyer_tdat_time(self): - from mutagen.id3 import TYER, TDAT, TIME - id3 = ID3() - id3.version = (2, 3) - id3.add(TYER(encoding=0, text="2006")) - id3.add(TDAT(encoding=0, text="0603")) - id3.add(TIME(encoding=0, text="1127")) - id3.update_to_v24() - self.failUnlessEqual(id3["TDRC"], "2006-03-06 11:27:00") + def test_corrupt_header_too_small(self): + with open(self.filename, "r+b") as h: + h.truncate(5) + self.assertRaises(id3.error, ID3, self.filename) - def test_tory(self): - from mutagen.id3 import TORY - id3 = ID3() - id3.version = (2, 3) - id3.add(TORY(encoding=0, text="2006")) - id3.update_to_v24() - self.failUnlessEqual(id3["TDOR"], "2006") + def test_corrupt_tag_too_small(self): + with open(self.filename, "r+b") as h: + h.truncate(50) + self.assertRaises(id3.error, ID3, self.filename) - def test_ipls(self): - from mutagen.id3 import IPLS - id3 = ID3() - id3.version = (2, 3) - id3.add(IPLS(encoding=0, people=[["a", "b"], ["c", "d"]])) - id3.update_to_v24() - self.failUnlessEqual(id3["TIPL"], [["a", "b"], ["c", "d"]]) + def test_corrupt_save(self): + with open(self.filename, "r+b") as h: + h.seek(5, 0) + h.write(b"nope") + self.assertRaises(id3.error, ID3().save, self.filename) - def test_dropped(self): - from mutagen.id3 import TIME - id3 = ID3() - id3.version = (2, 3) - id3.add(TIME(encoding=0, text=["1155"])) - id3.update_to_v24() - self.assertFalse(id3.getall("TIME")) + def test_padding_fill_all(self): + tag = ID3(self.filename) + self.assertEqual(tag._padding, 1142) + tag.delall("TPE1") + # saving should increase the padding not decrease the tag size + tag.save() + tag = ID3(self.filename) + self.assertEqual(tag._padding, 1166) + def test_padding_remove_add_padding(self): + ID3(self.filename).save() -class Issue97_UpgradeUnknown23(TestCase): - SILENCE = os.path.join(DATA_DIR, "97-unknown-23-update.mp3") + tag = ID3(self.filename) + old_padding = tag._padding + old_size = os.path.getsize(self.filename) + tag.save(padding=lambda x: 0) + self.assertEqual(os.path.getsize(self.filename), + old_size - old_padding) + old_size = old_size - old_padding + tag.save(padding=lambda x: 137) + self.assertEqual(os.path.getsize(self.filename), + old_size + 137) - def setUp(self): - fd, self.filename = mkstemp(suffix='.mp3') - os.close(fd) - shutil.copy(self.SILENCE, self.filename) + def test_save_id3_over_ape(self): + id3.delete(self.filename, delete_v2=False) - def test_unknown(self): - from mutagen.id3 import TPE1 - orig = ID3(self.filename) - self.failUnlessEqual(orig.version, (2, 3, 0)) + ape_tag = APEv2() + ape_tag["oh"] = ["no"] + ape_tag.save(self.filename) - # load a 2.3 file and pretend we don't support TIT2 - unknown = ID3(self.filename, known_frames={"TPE1": TPE1}, - translate=False) + ID3(self.filename).save() + self.assertEqual(APEv2(self.filename)["oh"], "no") - # TIT2 ends up in unknown_frames - self.failUnlessEqual(unknown.unknown_frames[0][:4], b"TIT2") + def test_delete_id3_with_ape(self): + ID3(self.filename).save(v1=2) - # frame should be different now - orig_unknown = unknown.unknown_frames[0] - unknown.update_to_v24() - self.failIfEqual(unknown.unknown_frames[0], orig_unknown) + ape_tag = APEv2() + ape_tag["oh"] = ["no"] + ape_tag.save(self.filename) - # save as 2.4 - unknown.save() + id3.delete(self.filename, delete_v2=False) + self.assertEqual(APEv2(self.filename)["oh"], "no") - # load again with support for TIT2, all should be there again - new = ID3(self.filename) - self.failUnlessEqual(new.version, (2, 4, 0)) - self.failUnlessEqual(new["TIT2"].text, orig["TIT2"].text) - self.failUnlessEqual(new["TPE1"].text, orig["TPE1"].text) + def test_ape_id3_lookalike(self): + # mp3 with apev2 tag that parses as id3v1 (at least with ParseID3v1) - def test_double_update(self): - from mutagen.id3 import TPE1 - unknown = ID3(self.filename, known_frames={"TPE1": TPE1}) - # Make sure the data doesn't get updated again - unknown.update_to_v24() - unknown.unknown_frames = [b"foobar"] - unknown.update_to_v24() - self.failUnless(unknown.unknown_frames) + id3.delete(self.filename, delete_v2=False) - def test_unknown_invalid(self): - f = ID3(self.filename, translate=False) - f.unknown_frames = [b"foobar", b"\xff" * 50] - # throw away invalid frames - f.update_to_v24() - self.failIf(f.unknown_frames) + ape_tag = APEv2() + ape_tag["oh"] = [ + "noooooooooo0000000000000000000000000000000000ooooooooooo"] + ape_tag.save(self.filename) - def tearDown(self): - os.unlink(self.filename) + ID3(self.filename).save() + self.assertTrue(APEv2(self.filename)) + def test_update_to_v23_on_load(self): + audio = ID3(self.filename) + audio.add(TSOT(text=["Ha"], encoding=3)) + audio.save() -class BrokenDiscarded(TestCase): + # update_to_v23 called + id3 = ID3(self.filename, v2_version=3) + self.assertFalse(id3.getall("TSOT")) - def test_empty(self): - from mutagen.id3 import TPE1, ID3JunkFrameError - self.assertRaises(ID3JunkFrameError, TPE1._fromData, _24, 0x00, b'') + # update_to_v23 not called + id3 = ID3(self.filename, v2_version=3, translate=False) + self.assertTrue(id3.getall("TSOT")) - def test_wacky_truncated_RVA2(self): - from mutagen.id3 import RVA2, ID3JunkFrameError - data = b'\x01{\xf0\x10\xff\xff\x00' - self.assertRaises(ID3JunkFrameError, RVA2._fromData, _24, 0x00, data) - - def test_bad_number_of_bits_RVA2(self): - from mutagen.id3 import RVA2, ID3JunkFrameError - data = b'\x00\x00\x01\xe6\xfc\x10{\xd7' - self.assertRaises(ID3JunkFrameError, RVA2._fromData, _24, 0x00, data) + def test_load_save_inval_version(self): + audio = ID3(self.filename) + self.assertRaises(ValueError, audio.save, v2_version=5) + self.assertRaises(ValueError, ID3, self.filename, v2_version=5) - def test_drops_truncated_frames(self): - from mutagen.id3 import Frames - id3 = ID3() - id3._header = ID3Header() - id3._header.version = (2, 4, 0) - tail = b'\x00\x00\x00\x03\x00\x00' b'\x01\x02\x03' - for head in b'RVA2 TXXX APIC'.split(): - data = head + tail - self.assertEquals( - 0, len(list(id3._ID3__read_frames(data, Frames)))) + def test_save(self): + audio = ID3(self.filename) + strings = ["one", "two", "three"] + audio.add(TPE1(text=strings, encoding=3)) + audio.save(v2_version=3) - def test_drops_nonalphanum_frames(self): - from mutagen.id3 import Frames - id3 = ID3() - tail = b'\x00\x00\x00\x03\x00\x00' b'\x01\x02\x03' - for head in [b'\x06\xaf\xfe\x20', b'ABC\x00', b'A ']: - data = head + tail - self.assertEquals( - 0, len(list(id3._ID3__read_frames(data, Frames)))) - - def test_bad_unicodedecode(self): - from mutagen.id3 import COMM, ID3JunkFrameError - # 7 bytes of "UTF16" data. - data = b'\x01\x00\x00\x00\xff\xfe\x00\xff\xfeh\x00' - self.assertRaises(ID3JunkFrameError, COMM._fromData, _24, 0x00, data) + frame = audio["TPE1"] + self.assertEqual(frame.encoding, 3) + self.assertEqual(frame.text, strings) + id3 = ID3(self.filename, translate=False) + self.assertEqual(id3.version, (2, 3, 0)) + frame = id3["TPE1"] + self.assertEqual(frame.encoding, 1) + self.assertEqual(frame.text, ["/".join(strings)]) -class BrokenButParsed(TestCase): + # null separator, mutagen can still read it + audio.save(v2_version=3, v23_sep=None) - def test_zerolength_framedata(self): - from mutagen.id3 import Frames - id3 = ID3() - tail = b'\x00' * 6 - for head in b'WOAR TENC TCOP TOPE WXXX'.split(): - data = head + tail - self.assertEquals( - 0, len(list(id3._ID3__read_frames(data, Frames)))) - - def test_lengthone_utf16(self): - from mutagen.id3 import TPE1 - tpe1 = TPE1._fromData(_24, 0, b'\x01\x00') - self.assertEquals(u'', tpe1) - tpe1 = TPE1._fromData(_24, 0, b'\x01\x00\x00\x00\x00') - self.assertEquals([u'', u''], tpe1) - - def test_utf16_wrongnullterm(self): - # issue 169 - from mutagen.id3 import TPE1 - tpe1 = TPE1._fromData( - _24, 0, b'\x01\xff\xfeH\x00e\x00l\x00l\x00o\x00\x00') - self.assertEquals(tpe1, [u'Hello']) - - def test_fake_zlib(self): - from mutagen.id3 import TPE1, Frame - header = ID3Header() - header.version = (2, 4, 0) - self.assertRaises(ID3JunkFrameError, TPE1._fromData, header, - Frame.FLAG24_COMPRESS, b'\x03abcdefg') + id3 = ID3(self.filename, translate=False) + self.assertEqual(id3.version, (2, 3, 0)) + frame = id3["TPE1"] + self.assertEqual(frame.encoding, 1) + self.assertEqual(frame.text, strings) - def test_zlib_bpi(self): - from mutagen.id3 import TPE1 - id3 = ID3() - tpe1 = TPE1(encoding=0, text="a" * (0xFFFF - 2)) - data = id3._ID3__save_frame(tpe1) - datalen_size = data[4 + 4 + 2:4 + 4 + 2 + 4] - self.failIf( - max(datalen_size) >= b'\x80'[0], "data is not syncsafe: %r" % data) - - def test_ql_0_12_missing_uncompressed_size(self): - from mutagen.id3 import TPE1 - tag = TPE1._fromData( - _24, 0x08, - b'x\x9cc\xfc\xff\xaf\x84!\x83!\x93' - b'\xa1\x98A\x01J&2\xe83\x940\xa4\x02\xd9%\x0c\x00\x87\xc6\x07#' - ) - self.assertEquals(tag.encoding, 1) - self.assertEquals(tag, ['this is a/test']) + def test_save_off_spec_frames(self): + # These are not defined in v2.3 and shouldn't be written. + # Still make sure reading them again works and the encoding + # is at least changed - def test_zlib_latin1_missing_datalen(self): - from mutagen.id3 import TPE1 - tag = TPE1._fromData( - _24, 0x8, - b'\x00\x00\x00\x0f' - b'x\x9cc(\xc9\xc8,V\x00\xa2D\xfd\x92\xd4\xe2\x12\x00&\x7f\x05%' - ) - self.assertEquals(tag.encoding, 0) - self.assertEquals(tag, ['this is a/test']) + audio = ID3(self.filename) + dates = ["2013", "2014"] + frame = TDEN(text=dates, encoding=3) + audio.add(frame) + tipl_frame = TIPL(people=[("a", "b"), ("c", "d")], encoding=2) + audio.add(tipl_frame) + audio.save(v2_version=3) - def test_detect_23_ints_in_24_frames(self): - from mutagen.id3 import Frames - head = b'TIT1\x00\x00\x01\x00\x00\x00\x00' - tail = b'TPE1\x00\x00\x00\x05\x00\x00\x00Yay!' + id3 = ID3(self.filename, translate=False) + self.assertEqual(id3.version, (2, 3, 0)) - id3 = ID3() - id3._header = ID3Header() - id3._header.version = (2, 4, 0) + self.assertEqual([stamp.text for stamp in id3["TDEN"].text], dates) + self.assertEqual(id3["TDEN"].encoding, 1) - tagsgood = list( - id3._ID3__read_frames(head + b'a' * 127 + tail, Frames)) - tagsbad = list(id3._ID3__read_frames(head + b'a' * 255 + tail, Frames)) - self.assertEquals(2, len(tagsgood)) - self.assertEquals(2, len(tagsbad)) - self.assertEquals('a' * 127, tagsgood[0]) - self.assertEquals('a' * 255, tagsbad[0]) - self.assertEquals('Yay!', tagsgood[1]) - self.assertEquals('Yay!', tagsbad[1]) - - tagsgood = list(id3._ID3__read_frames(head + b'a' * 127, Frames)) - tagsbad = list(id3._ID3__read_frames(head + b'a' * 255, Frames)) - self.assertEquals(1, len(tagsgood)) - self.assertEquals(1, len(tagsbad)) - self.assertEquals('a' * 127, tagsgood[0]) - self.assertEquals('a' * 255, tagsbad[0]) - - -class OddWrites(TestCase): - silence = join(DATA_DIR, 'silence-44-s.mp3') - newsilence = join(DATA_DIR, 'silence-written.mp3') + self.assertEqual(id3["TIPL"].people, tipl_frame.people) + self.assertEqual(id3["TIPL"].encoding, 1) - def setUp(self): - shutil.copy(self.silence, self.newsilence) + def test_wrong_encoding(self): + t = ID3(self.filename) + t.add(TIT2(encoding=Encoding.LATIN1, text=[u"\u0243"])) + self.assertRaises(MutagenError, t.save) def test_toemptyfile(self): - os.unlink(self.newsilence) - open(self.newsilence, "wb").close() - ID3(self.silence).save(self.newsilence) + t = ID3(self.filename) + os.unlink(self.filename) + open(self.filename, "wb").close() + t.save(self.filename) def test_tononfile(self): - os.unlink(self.newsilence) - ID3(self.silence).save(self.newsilence) + t = ID3(self.filename) + os.unlink(self.filename) + t.save(self.filename) def test_1bfile(self): - os.unlink(self.newsilence) - f = open(self.newsilence, "wb") - f.write(b"!") - f.close() - ID3(self.silence).save(self.newsilence) - self.assert_(os.path.getsize(self.newsilence) > 1) - self.assertEquals(open(self.newsilence, "rb").read()[-1], b"!"[0]) - - def tearDown(self): - try: - os.unlink(self.newsilence) - except OSError: - pass - + t = ID3(self.filename) + os.unlink(self.filename) + with open(self.filename, "wb") as f: + f.write(b"!") + t.save(self.filename) + self.assert_(os.path.getsize(self.filename) > 1) + with open(self.filename, "rb") as h: + self.assertEquals(h.read()[-1], b"!"[0]) + + def test_unknown_chap(self): + # add ctoc + id3 = ID3(self.filename) + id3.add(CTOC(element_id="foo", flags=3, child_element_ids=["ch0"], + sub_frames=[TIT2(encoding=3, text=["bla"])])) + id3.save() -class WriteRoundtrip(TestCase): - silence = join(DATA_DIR, 'silence-44-s.mp3') - newsilence = join(DATA_DIR, 'silence-written.mp3') + # pretend we don't know ctoc and save + id3 = ID3(self.filename, known_frames={"CTOC": CTOC}) + ctoc = id3.getall("CTOC")[0] + self.assertFalse(ctoc.sub_frames) + self.assertTrue(ctoc.sub_frames.unknown_frames) + id3.save() - def setUp(self): - shutil.copy(self.silence, self.newsilence) + # make sure we wrote all sub frames back + id3 = ID3(self.filename) + self.assertEqual( + id3.getall("CTOC")[0].sub_frames.getall("TIT2")[0].text, ["bla"]) def test_same(self): - ID3(self.newsilence).save() - id3 = ID3(self.newsilence) + ID3(self.filename).save() + id3 = ID3(self.filename) self.assertEquals(id3["TALB"], "Quod Libet Test Data") self.assertEquals(id3["TCON"], "Silence") self.assertEquals(id3["TIT2"], "Silence") self.assertEquals(id3["TPE1"], ["jzig"]) def test_same_v23(self): - id3 = ID3(self.newsilence, v2_version=3) + id3 = ID3(self.filename, v2_version=3) id3.save(v2_version=3) - id3 = ID3(self.newsilence) + id3 = ID3(self.filename) self.assertEqual(id3.version, (2, 3, 0)) self.assertEquals(id3["TALB"], "Quod Libet Test Data") self.assertEquals(id3["TCON"], "Silence") @@ -1268,102 +804,100 @@ self.assertEquals(id3["TPE1"], "jzig") def test_addframe(self): - from mutagen.id3 import TIT3 - f = ID3(self.newsilence) + f = ID3(self.filename) self.assert_("TIT3" not in f) f["TIT3"] = TIT3(encoding=0, text="A subtitle!") f.save() - id3 = ID3(self.newsilence) + id3 = ID3(self.filename) self.assertEquals(id3["TIT3"], "A subtitle!") def test_changeframe(self): - f = ID3(self.newsilence) + f = ID3(self.filename) self.assertEquals(f["TIT2"], "Silence") f["TIT2"].text = [u"The sound of silence."] f.save() - id3 = ID3(self.newsilence) + id3 = ID3(self.filename) self.assertEquals(id3["TIT2"], "The sound of silence.") def test_replaceframe(self): - from mutagen.id3 import TPE1 - f = ID3(self.newsilence) + f = ID3(self.filename) self.assertEquals(f["TPE1"], "jzig") f["TPE1"] = TPE1(encoding=0, text=u"jzig\x00piman") f.save() - id3 = ID3(self.newsilence) + id3 = ID3(self.filename) self.assertEquals(id3["TPE1"], ["jzig", "piman"]) def test_compressibly_large(self): - from mutagen.id3 import TPE2 - f = ID3(self.newsilence) + f = ID3(self.filename) self.assert_("TPE2" not in f) f["TPE2"] = TPE2(encoding=0, text="Ab" * 1025) f.save() - id3 = ID3(self.newsilence) + id3 = ID3(self.filename) self.assertEquals(id3["TPE2"], "Ab" * 1025) def test_nofile_silencetag(self): - id3 = ID3(self.newsilence) - os.unlink(self.newsilence) - id3.save(self.newsilence) - self.assertEquals(b'ID3', open(self.newsilence, 'rb').read(3)) + id3 = ID3(self.filename) + os.unlink(self.filename) + id3.save(self.filename) + with open(self.filename, 'rb') as h: + self.assertEquals(b'ID3', h.read(3)) self.test_same() def test_emptyfile_silencetag(self): - id3 = ID3(self.newsilence) - open(self.newsilence, 'wb').truncate() - id3.save(self.newsilence) - self.assertEquals(b'ID3', open(self.newsilence, 'rb').read(3)) + id3 = ID3(self.filename) + with open(self.filename, 'wb') as h: + h.truncate() + id3.save(self.filename) + with open(self.filename, 'rb') as h: + self.assertEquals(b'ID3', h.read(3)) self.test_same() def test_empty_plustag_minustag_empty(self): - id3 = ID3(self.newsilence) - open(self.newsilence, 'wb').truncate() + id3 = ID3(self.filename) + with open(self.filename, 'wb') as h: + h.truncate() id3.save() id3.delete() self.failIf(id3) - self.assertEquals(open(self.newsilence, 'rb').read(10), b'') + with open(self.filename, 'rb') as h: + self.assertEquals(h.read(10), b'') def test_delete_invalid_zero(self): - f = open(self.newsilence, 'wb') - f.write(b'ID3\x04\x00\x00\x00\x00\x00\x00abc') - f.close() - ID3(self.newsilence).delete() - self.assertEquals(open(self.newsilence, 'rb').read(10), b'abc') + with open(self.filename, 'wb') as f: + f.write(b'ID3\x04\x00\x00\x00\x00\x00\x00abc') + ID3(self.filename).delete() + with open(self.filename, 'rb') as f: + self.assertEquals(f.read(10), b'abc') def test_frame_order(self): - from mutagen.id3 import TIT2, APIC, TALB, COMM - f = ID3(self.newsilence) + f = ID3(self.filename) f["TIT2"] = TIT2(encoding=0, text="A title!") f["APIC"] = APIC(encoding=0, mime="b", type=3, desc='', data=b"a") f["TALB"] = TALB(encoding=0, text="c") f["COMM"] = COMM(encoding=0, desc="x", text="y") f.save() - data = open(self.newsilence, 'rb').read() + with open(self.filename, 'rb') as h: + data = h.read() self.assert_(data.find(b"TIT2") < data.find(b"APIC")) self.assert_(data.find(b"TIT2") < data.find(b"COMM")) self.assert_(data.find(b"TALB") < data.find(b"APIC")) self.assert_(data.find(b"TALB") < data.find(b"COMM")) self.assert_(data.find(b"TIT2") < data.find(b"TALB")) - def tearDown(self): - try: - os.unlink(self.newsilence) - except EnvironmentError: - pass - class WriteForEyeD3(TestCase): - silence = join(DATA_DIR, 'silence-44-s.mp3') - newsilence = join(DATA_DIR, 'silence-written.mp3') def setUp(self): - shutil.copy(self.silence, self.newsilence) + self.silence = os.path.join(DATA_DIR, 'silence-44-s.mp3') + self.newsilence = get_temp_copy(self.silence) + # remove ID3v1 tag - f = open(self.newsilence, "rb+") - f.seek(-128, 2) - f.truncate() - f.close() + with open(self.newsilence, "rb+") as f: + f.seek(-128, 2) + f.truncate() + + def tearDown(self): + os.unlink(self.newsilence) def test_same(self): ID3(self.newsilence).save() @@ -1378,7 +912,6 @@ self.assertEquals(id3.frames["TPE1"][0].text, "jzig") def test_addframe(self): - from mutagen.id3 import TIT3 f = ID3(self.newsilence) self.assert_("TIT3" not in f) f["TIT3"] = TIT3(encoding=0, text="A subtitle!") @@ -1396,54 +929,28 @@ id3.link(self.newsilence) self.assertEquals(id3.frames["TIT2"][0].text, "The sound of silence.") - def tearDown(self): - os.unlink(self.newsilence) - - -class BadTYER(TestCase): - - filename = join(DATA_DIR, 'bad-TYER-frame.mp3') - - def setUp(self): - self.audio = ID3(self.filename) - - def test_no_year(self): - self.failIf("TYER" in self.audio) - - def test_has_title(self): - self.failUnless("TIT2" in self.audio) - - def tearDown(self): - del(self.audio) - class BadPOPM(TestCase): - filename = join(DATA_DIR, 'bad-POPM-frame.mp3') - newfilename = join(DATA_DIR, 'bad-POPM-frame-written.mp3') - def setUp(self): - shutil.copy(self.filename, self.newfilename) + self.filename = get_temp_copy( + os.path.join(DATA_DIR, 'bad-POPM-frame.mp3')) def tearDown(self): - try: - os.unlink(self.newfilename) - except EnvironmentError: - pass + os.unlink(self.filename) def test_read_popm_long_counter(self): - f = ID3(self.newfilename) + f = ID3(self.filename) self.failUnless("POPM:Windows Media Player 9 Series" in f) popm = f["POPM:Windows Media Player 9 Series"] self.assertEquals(popm.rating, 255) self.assertEquals(popm.count, 2709193061) def test_write_popm_long_counter(self): - from mutagen.id3 import POPM - f = ID3(self.newfilename) + f = ID3(self.filename) f.add(POPM(email="foo@example.com", rating=125, count=2 ** 32 + 1)) f.save() - f = ID3(self.newfilename) + f = ID3(self.filename) self.failUnless("POPM:foo@example.com" in f) self.failUnless("POPM:Windows Media Player 9 Series" in f) popm = f["POPM:foo@example.com"] @@ -1454,8 +961,6 @@ class Issue69_BadV1Year(TestCase): def test_missing_year(self): - from mutagen.id3 import ParseID3v1 - tag = ParseID3v1( b'ABCTAGhello world\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' @@ -1469,8 +974,6 @@ self.failUnlessEqual(tag["TIT2"], "hello world") def test_short_year(self): - from mutagen.id3 import ParseID3v1, _find_id3v1 - data = ( b'XTAGhello world\x00\x00\x00\x00\x00\x00\x00\x00' b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' @@ -1485,237 +988,35 @@ self.failUnlessEqual(tag["TIT2"], "hello world") self.failUnlessEqual(tag["TDRC"], "0001") - frames, offset = _find_id3v1(cBytesIO(data)) + frames, offset = find_id3v1(cBytesIO(data)) self.assertEqual(offset, -125) self.assertEqual(frames, tag) def test_none(self): - from mutagen.id3 import ParseID3v1, MakeID3v1 s = MakeID3v1(dict()) self.failUnlessEqual(len(s), 128) tag = ParseID3v1(s) self.failIf("TDRC" in tag) def test_empty(self): - from mutagen.id3 import ParseID3v1, MakeID3v1 s = MakeID3v1(dict(TDRC="")) self.failUnlessEqual(len(s), 128) tag = ParseID3v1(s) self.failIf("TDRC" in tag) def test_short(self): - from mutagen.id3 import ParseID3v1, MakeID3v1 s = MakeID3v1(dict(TDRC="1")) self.failUnlessEqual(len(s), 128) tag = ParseID3v1(s) self.failUnlessEqual(tag["TDRC"], "0001") def test_long(self): - from mutagen.id3 import ParseID3v1, MakeID3v1 s = MakeID3v1(dict(TDRC="123456789")) self.failUnlessEqual(len(s), 128) tag = ParseID3v1(s) self.failUnlessEqual(tag["TDRC"], "1234") -class UpdateTo23(TestCase): - - def test_tdrc(self): - tags = ID3() - tags.add(id3.TDRC(encoding=1, text="2003-04-05 12:03")) - tags.update_to_v23() - self.failUnlessEqual(tags["TYER"].text, ["2003"]) - self.failUnlessEqual(tags["TDAT"].text, ["0504"]) - self.failUnlessEqual(tags["TIME"].text, ["1203"]) - - def test_tdor(self): - tags = ID3() - tags.add(id3.TDOR(encoding=1, text="2003-04-05 12:03")) - tags.update_to_v23() - self.failUnlessEqual(tags["TORY"].text, ["2003"]) - - def test_genre_from_v24_1(self): - tags = ID3() - tags.add(id3.TCON(encoding=1, text=["4", "Rock"])) - tags.update_to_v23() - self.failUnlessEqual(tags["TCON"].text, ["Disco", "Rock"]) - - def test_genre_from_v24_2(self): - tags = ID3() - tags.add(id3.TCON(encoding=1, text=["RX", "3", "CR"])) - tags.update_to_v23() - self.failUnlessEqual(tags["TCON"].text, ["Remix", "Dance", "Cover"]) - - def test_genre_from_v23_1(self): - tags = ID3() - tags.add(id3.TCON(encoding=1, text=["(4)Rock"])) - tags.update_to_v23() - self.failUnlessEqual(tags["TCON"].text, ["Disco", "Rock"]) - - def test_genre_from_v23_2(self): - tags = ID3() - tags.add(id3.TCON(encoding=1, text=["(RX)(3)(CR)"])) - tags.update_to_v23() - self.failUnlessEqual(tags["TCON"].text, ["Remix", "Dance", "Cover"]) - - def test_ipls(self): - tags = ID3() - tags.version = (2, 3) - tags.add(id3.TIPL(encoding=0, people=[["a", "b"], ["c", "d"]])) - tags.add(id3.TMCL(encoding=0, people=[["e", "f"], ["g", "h"]])) - tags.update_to_v23() - self.failUnlessEqual(tags["IPLS"], [["a", "b"], ["c", "d"], - ["e", "f"], ["g", "h"]]) - - -class WriteTo23(TestCase): - - SILENCE = os.path.join(DATA_DIR, "silence-44-s.mp3") - - def setUp(self): - fd, self.filename = mkstemp(suffix='.mp3') - os.close(fd) - shutil.copy(self.SILENCE, self.filename) - self.audio = ID3(self.filename) - - def tearDown(self): - os.unlink(self.filename) - - def test_update_to_v23_on_load(self): - from mutagen.id3 import TSOT - self.audio.add(TSOT(text=["Ha"], encoding=3)) - self.audio.save() - - # update_to_v23 called - id3 = ID3(self.filename, v2_version=3) - self.assertFalse(id3.getall("TSOT")) - - # update_to_v23 not called - id3 = ID3(self.filename, v2_version=3, translate=False) - self.assertTrue(id3.getall("TSOT")) - - def test_load_save_inval_version(self): - self.assertRaises(ValueError, self.audio.save, v2_version=5) - self.assertRaises(ValueError, ID3, self.filename, v2_version=5) - - def test_save(self): - strings = ["one", "two", "three"] - from mutagen.id3 import TPE1 - self.audio.add(TPE1(text=strings, encoding=3)) - self.audio.save(v2_version=3) - - frame = self.audio["TPE1"] - self.assertEqual(frame.encoding, 3) - self.assertEqual(frame.text, strings) - - id3 = ID3(self.filename, translate=False) - self.assertEqual(id3.version, (2, 3, 0)) - frame = id3["TPE1"] - self.assertEqual(frame.encoding, 1) - self.assertEqual(frame.text, ["/".join(strings)]) - - # null separator, mutagen can still read it - self.audio.save(v2_version=3, v23_sep=None) - - id3 = ID3(self.filename, translate=False) - self.assertEqual(id3.version, (2, 3, 0)) - frame = id3["TPE1"] - self.assertEqual(frame.encoding, 1) - self.assertEqual(frame.text, strings) - - def test_save_off_spec_frames(self): - # These are not defined in v2.3 and shouldn't be written. - # Still make sure reading them again works and the encoding - # is at least changed - - from mutagen.id3 import TDEN, TIPL - dates = ["2013", "2014"] - frame = TDEN(text=dates, encoding=3) - self.audio.add(frame) - tipl_frame = TIPL(people=[("a", "b"), ("c", "d")], encoding=2) - self.audio.add(tipl_frame) - self.audio.save(v2_version=3) - - id3 = ID3(self.filename, translate=False) - self.assertEqual(id3.version, (2, 3, 0)) - - self.assertEqual([stamp.text for stamp in id3["TDEN"].text], dates) - self.assertEqual(id3["TDEN"].encoding, 1) - - self.assertEqual(id3["TIPL"].people, tipl_frame.people) - self.assertEqual(id3["TIPL"].encoding, 1) - - -class Read22FrameNamesin23(TestCase): - - def test_PIC_in_23(self): - fd, filename = mkstemp(suffix='.mp3') - os.close(fd) - - try: - with open(filename, "wb") as h: - # contains a bad upgraded frame, 2.3 structure with 2.2 name. - # PIC was upgraded to APIC, but mime was not - h.write(b"ID3\x03\x00\x00\x00\x00\x08\x00PIC\x00\x00\x00" - b"\x00\x0b\x00\x00\x00JPG\x00\x03foo\x00\x42" - b"\x00" * 100) - id3 = ID3(filename) - self.assertEqual(id3.version, (2, 3, 0)) - self.assertTrue(id3.getall("APIC")) - frame = id3.getall("APIC")[0] - self.assertEqual(frame.mime, "image/jpeg") - self.assertEqual(frame.data, b"\x42") - self.assertEqual(frame.type, 3) - self.assertEqual(frame.desc, "foo") - finally: - os.remove(filename) - - -class ID3V1_vs_APEv2(TestCase): - SILENCE = os.path.join(DATA_DIR, "silence-44-s.mp3") - - def setUp(self): - fd, self.filename = mkstemp(suffix='.mp3') - os.close(fd) - shutil.copy(self.SILENCE, self.filename) - - def test_save_id3_over_ape(self): - id3.delete(self.filename, delete_v2=False) - - ape_tag = APEv2() - ape_tag["oh"] = ["no"] - ape_tag.save(self.filename) - - ID3(self.filename).save() - self.assertEqual(APEv2(self.filename)["oh"], "no") - - def test_delete_id3_with_ape(self): - ID3(self.filename).save(v1=2) - - ape_tag = APEv2() - ape_tag["oh"] = ["no"] - ape_tag.save(self.filename) - - id3.delete(self.filename, delete_v2=False) - self.assertEqual(APEv2(self.filename)["oh"], "no") - - def test_ape_id3_lookalike(self): - # mp3 with apev2 tag that parses as id3v1 (at least with ParseID3v1) - - id3.delete(self.filename, delete_v2=False) - - ape_tag = APEv2() - ape_tag["oh"] = [ - "noooooooooo0000000000000000000000000000000000ooooooooooo"] - ape_tag.save(self.filename) - - ID3(self.filename).save() - self.assertTrue(APEv2(self.filename)) - - def tearDown(self): - os.remove(self.filename) - - class TID3Misc(TestCase): def test_main(self): @@ -1724,7 +1025,6 @@ self.assertEqual(id3.PictureType.COVER_FRONT, 3) def test_determine_bpi(self): - determine_bpi = id3._determine_bpi # default to BitPaddedInt self.assertTrue(determine_bpi("", {}) is BitPaddedInt) @@ -1752,69 +1052,8 @@ self.assertTrue(determine_bpi(d, Frames) is BitPaddedInt) -class TID3Padding(TestCase): - - def setUp(self): - fd, self.filename = mkstemp(suffix='.mp3') - os.close(fd) - orig = os.path.join(DATA_DIR, "silence-44-s.mp3") - shutil.copy(orig, self.filename) - - def tearDown(self): - os.remove(self.filename) - - def test_fill_all(self): - tag = ID3(self.filename) - self.assertEqual(tag._padding, 1142) - tag.delall("TPE1") - # saving should increase the padding not decrease the tag size - tag.save() - tag = ID3(self.filename) - self.assertEqual(tag._padding, 1166) - - def test_remove_add_padding(self): - ID3(self.filename).save() - - tag = ID3(self.filename) - old_padding = tag._padding - old_size = os.path.getsize(self.filename) - tag.save(padding=lambda x: 0) - self.assertEqual(os.path.getsize(self.filename), - old_size - old_padding) - old_size = old_size - old_padding - tag.save(padding=lambda x: 137) - self.assertEqual(os.path.getsize(self.filename), - old_size + 137) - - -class TID3Corrupt(TestCase): - - def setUp(self): - fd, self.filename = mkstemp(suffix='.mp3') - os.close(fd) - orig = os.path.join(DATA_DIR, "silence-44-s.mp3") - shutil.copy(orig, self.filename) - - def tearDown(self): - os.remove(self.filename) - - def test_header_too_small(self): - with open(self.filename, "r+b") as h: - h.truncate(5) - self.assertRaises(id3.error, ID3, self.filename) - - def test_tag_too_small(self): - with open(self.filename, "r+b") as h: - h.truncate(50) - self.assertRaises(id3.error, ID3, self.filename) - - def test_save(self): - with open(self.filename, "r+b") as h: - h.seek(5, 0) - h.write(b"nope") - self.assertRaises(id3.error, ID3().save, self.filename) - try: import eyeD3 except ImportError: + print("WARNING: Skipping eyeD3 tests.") del WriteForEyeD3 diff -Nru mutagen-1.33.2/tests/test__id3specs.py mutagen-1.34/tests/test__id3specs.py --- mutagen-1.33.2/tests/test__id3specs.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/tests/test__id3specs.py 2016-07-13 20:05:53.000000000 +0000 @@ -4,96 +4,19 @@ from tests import TestCase -from mutagen._compat import PY2, PY3, text_type -from mutagen.id3 import BitPaddedInt, BitPaddedLong, unsynch -from mutagen.id3._specs import SpecError +from mutagen._compat import PY3 +from mutagen.id3._specs import SpecError, Latin1TextListSpec, ID3FramesSpec, \ + ASPIIndexSpec, ByteSpec, EncodingSpec, StringSpec, BinaryDataSpec, \ + EncodedTextSpec, VolumePeakSpec, VolumeAdjustmentSpec, CTOCFlagsSpec, \ + Spec, SynchronizedTextSpec, TimeStampSpec, FrameIDSpec, RVASpec +from mutagen.id3._frames import Frame +from mutagen.id3._tags import ID3Header, ID3Tags, ID3SaveConfig +from mutagen.id3 import TIT3, ASPI, CTOCFlags, ID3TimeStamp -class SpecSanityChecks(TestCase): +class TSynchronizedTextSpec(TestCase): - def test_aspiindexspec(self): - from mutagen.id3 import ASPIIndexSpec - from mutagen.id3 import ASPI - - frame = ASPI(b=16, N=2) - s = ASPIIndexSpec('name') - self.assertRaises(SpecError, s.read, frame, b'') - self.assertEqual(s.read(frame, b'\x01\x00\x00\x01'), ([256, 1], b"")) - frame = ASPI(b=42) - self.assertRaises(SpecError, s.read, frame, b'') - - def test_bytespec(self): - from mutagen.id3 import ByteSpec - s = ByteSpec('name') - self.assertEquals((97, b'bcdefg'), s.read(None, b'abcdefg')) - self.assertEquals(b'a', s.write(None, 97)) - self.assertRaises(TypeError, s.write, None, b'abc') - self.assertRaises(TypeError, s.write, None, None) - - def test_encodingspec(self): - from mutagen.id3 import EncodingSpec - s = EncodingSpec('name') - self.assertEquals((3, b'abcdefg'), s.read(None, b'\x03abcdefg')) - self.assertRaises(SpecError, s.read, None, b'\x04abcdefg') - self.assertEquals(b'\x00', s.write(None, 0)) - self.assertRaises(TypeError, s.write, None, b'abc') - self.assertRaises(TypeError, s.write, None, None) - - def test_stringspec(self): - from mutagen.id3 import StringSpec - s = StringSpec('name', 3) - self.assertEquals(('abc', b'defg'), s.read(None, b'abcdefg')) - self.assertEquals(b'abc', s.write(None, 'abcdefg')) - self.assertEquals(b'\x00\x00\x00', s.write(None, None)) - self.assertEquals(b'\x00\x00\x00', s.write(None, '\x00')) - self.assertEquals(b'a\x00\x00', s.write(None, 'a')) - self.assertRaises(SpecError, s.read, None, b'\xff') - - def test_binarydataspec(self): - from mutagen.id3 import BinaryDataSpec - s = BinaryDataSpec('name') - self.assertEquals((b'abcdefg', b''), s.read(None, b'abcdefg')) - self.assertEquals(b'', s.write(None, None)) - self.assertEquals(b'43', s.write(None, 43)) - self.assertEquals(b'abc', s.write(None, b'abc')) - - def test_encodedtextspec(self): - from mutagen.id3 import EncodedTextSpec, Frame - s = EncodedTextSpec('name') - f = Frame() - f.encoding = 0 - self.assertEquals((u'abcd', b'fg'), s.read(f, b'abcd\x00fg')) - self.assertEquals(b'abcdefg\x00', s.write(f, u'abcdefg')) - self.assertRaises(AttributeError, s.write, f, None) - - def test_timestampspec(self): - from mutagen.id3 import TimeStampSpec, Frame, ID3TimeStamp - s = TimeStampSpec('name') - f = Frame() - f.encoding = 0 - self.assertEquals((ID3TimeStamp('ab'), b'fg'), s.read(f, b'ab\x00fg')) - self.assertEquals((ID3TimeStamp('1234'), b''), s.read(f, b'1234\x00')) - self.assertEquals(b'1234\x00', s.write(f, ID3TimeStamp('1234'))) - self.assertRaises(AttributeError, s.write, f, None) - if PY3: - self.assertRaises(TypeError, ID3TimeStamp, b"blah") - self.assertEquals( - text_type(ID3TimeStamp(u"2000-01-01")), u"2000-01-01") - self.assertEquals( - bytes(ID3TimeStamp(u"2000-01-01")), b"2000-01-01") - - def test_volumeadjustmentspec(self): - from mutagen.id3 import VolumeAdjustmentSpec - s = VolumeAdjustmentSpec('gain') - self.assertEquals((0.0, b''), s.read(None, b'\x00\x00')) - self.assertEquals((2.0, b''), s.read(None, b'\x04\x00')) - self.assertEquals((-2.0, b''), s.read(None, b'\xfc\x00')) - self.assertEquals(b'\x00\x00', s.write(None, 0.0)) - self.assertEquals(b'\x04\x00', s.write(None, 2.0)) - self.assertEquals(b'\xfc\x00', s.write(None, -2.0)) - - def test_synchronizedtextspec(self): - from mutagen.id3 import SynchronizedTextSpec, Frame + def test_write(self): s = SynchronizedTextSpec('name') f = Frame() @@ -101,8 +24,9 @@ # utf-16 f.encoding = 1 - self.assertEqual(s.read(f, s.write(f, values)), (values, b"")) - data = s.write(f, [(u"A", 100)]) + self.assertEqual( + s.read(None, f, s.write(None, f, values)), (values, b"")) + data = s.write(None, f, [(u"A", 100)]) if sys.byteorder == 'little': self.assertEquals( data, b"\xff\xfeA\x00\x00\x00\x00\x00\x00d") @@ -112,207 +36,340 @@ # utf-16be f.encoding = 2 - self.assertEqual(s.read(f, s.write(f, values)), (values, b"")) + self.assertEqual( + s.read(None, f, s.write(None, f, values)), (values, b"")) self.assertEquals( - s.write(f, [(u"A", 100)]), b"\x00A\x00\x00\x00\x00\x00d") + s.write(None, f, [(u"A", 100)]), b"\x00A\x00\x00\x00\x00\x00d") # utf-8 f.encoding = 3 - self.assertEqual(s.read(f, s.write(f, values)), (values, b"")) - self.assertEquals(s.write(f, [(u"A", 100)]), b"A\x00\x00\x00\x00d") + self.assertEqual( + s.read(None, f, s.write(None, f, values)), (values, b"")) + self.assertEquals( + s.write(None, f, [(u"A", 100)]), b"A\x00\x00\x00\x00d") -class SpecValidateChecks(TestCase): +class TTimeStampSpec(TestCase): - def test_volumeadjustmentspec(self): - from mutagen.id3 import VolumeAdjustmentSpec - s = VolumeAdjustmentSpec('gain') - self.assertRaises(ValueError, s.validate, None, 65) + def test_read(self): + s = TimeStampSpec('name') + f = Frame() + f.encoding = 0 + self.assertEquals( + (ID3TimeStamp('ab'), b'fg'), s.read(None, f, b'ab\x00fg')) + self.assertEquals( + (ID3TimeStamp('1234'), b''), s.read(None, f, b'1234\x00')) - def test_volumepeakspec(self): - from mutagen.id3 import VolumePeakSpec - s = VolumePeakSpec('peak') - self.assertRaises(ValueError, s.validate, None, 2) + def test_write(self): + s = TimeStampSpec('name') + f = Frame() + f.encoding = 0 + self.assertEquals(b'1234\x00', s.write(None, f, ID3TimeStamp('1234'))) + self.assertRaises(AttributeError, s.write, None, f, None) - def test_bytespec(self): - from mutagen.id3 import ByteSpec - s = ByteSpec('byte') - self.assertRaises(ValueError, s.validate, None, 1000) - def test_stringspec(self): - from mutagen.id3 import StringSpec - s = StringSpec('byte', 3) - self.assertEqual(s.validate(None, None), None) - self.assertEqual(s.validate(None, "ABC"), "ABC") - self.assertEqual(s.validate(None, u"ABC"), u"ABC") - self.assertRaises(ValueError, s.validate, None, "abc2") - self.assertRaises(ValueError, s.validate, None, "ab") +class TEncodedTextSpec(TestCase): - if PY3: - self.assertRaises(TypeError, s.validate, None, b"ABC") - self.assertRaises(ValueError, s.validate, None, u"\xf6\xe4\xfc") + def test_read(self): + s = EncodedTextSpec('name') + f = Frame() + f.encoding = 0 + self.assertEquals((u'abcd', b'fg'), s.read(None, f, b'abcd\x00fg')) - def test_binarydataspec(self): - from mutagen.id3 import BinaryDataSpec - s = BinaryDataSpec('name') - self.assertEqual(s.validate(None, None), None) - self.assertEqual(s.validate(None, b"abc"), b"abc") - if PY3: - self.assertRaises(TypeError, s.validate, None, "abc") - else: - self.assertEqual(s.validate(None, u"abc"), b"abc") - self.assertRaises(ValueError, s.validate, None, u"\xf6\xe4\xfc") + def test_write(self): + s = EncodedTextSpec('name') + f = Frame() + f.encoding = 0 + self.assertEquals(b'abcdefg\x00', s.write(None, f, u'abcdefg')) + self.assertRaises(AttributeError, s.write, None, f, None) -class NoHashSpec(TestCase): +class TEncodingSpec(TestCase): - def test_spec(self): - from mutagen.id3 import Spec - self.failUnlessRaises(TypeError, {}.__setitem__, Spec("foo"), None) + def test_read(self): + s = EncodingSpec('name') + self.assertEquals((3, b'abcdefg'), s.read(None, None, b'\x03abcdefg')) + self.assertRaises(SpecError, s.read, None, None, b'\x04abcdefg') + def test_write(self): + s = EncodingSpec('name') + self.assertEquals(b'\x00', s.write(None, None, 0)) + self.assertRaises(TypeError, s.write, None, None, b'abc') + self.assertRaises(TypeError, s.write, None, None, None) -class BitPaddedIntTest(TestCase): + def test_validate(self): + s = EncodingSpec('name') + self.assertRaises(TypeError, s.validate, None, None) - def test_long(self): - if PY2: - data = BitPaddedInt.to_str(sys.maxint + 1, width=16) - val = BitPaddedInt(data) - self.assertEqual(val, sys.maxint + 1) - self.assertTrue(isinstance(val, BitPaddedLong)) - else: - self.assertTrue(BitPaddedInt is BitPaddedLong) - def test_negative(self): - self.assertRaises(ValueError, BitPaddedInt, -1) +class TASPIIndexSpec(TestCase): - def test_zero(self): - self.assertEquals(BitPaddedInt(b'\x00\x00\x00\x00'), 0) + def test_read(self): + frame = ASPI(b=16, N=2) + s = ASPIIndexSpec('name', []) + self.assertRaises(SpecError, s.read, None, frame, b'') + self.assertEqual( + s.read(None, frame, b'\x01\x00\x00\x01'), ([256, 1], b"")) + frame = ASPI(b=42) + self.assertRaises(SpecError, s.read, None, frame, b'') - def test_1(self): - self.assertEquals(BitPaddedInt(b'\x00\x00\x00\x01'), 1) - def test_1l(self): - self.assertEquals( - BitPaddedInt(b'\x01\x00\x00\x00', bigendian=False), 1) +class TVolumeAdjustmentSpec(TestCase): - def test_129(self): - self.assertEquals(BitPaddedInt(b'\x00\x00\x01\x01'), 0x81) + def test_validate(self): + s = VolumeAdjustmentSpec('gain', 0) + self.assertRaises(ValueError, s.validate, None, 65) - def test_129b(self): - self.assertEquals(BitPaddedInt(b'\x00\x00\x01\x81'), 0x81) + def test_read(self): + s = VolumeAdjustmentSpec('gain', 0) + self.assertEquals((0.0, b''), s.read(None, None, b'\x00\x00')) + self.assertEquals((2.0, b''), s.read(None, None, b'\x04\x00')) + self.assertEquals((-2.0, b''), s.read(None, None, b'\xfc\x00')) + + def test_write(self): + s = VolumeAdjustmentSpec('gain', 0) + self.assertEquals(b'\x00\x00', s.write(None, None, 0.0)) + self.assertEquals(b'\x04\x00', s.write(None, None, 2.0)) + self.assertEquals(b'\xfc\x00', s.write(None, None, -2.0)) - def test_65(self): - self.assertEquals(BitPaddedInt(b'\x00\x00\x01\x81', 6), 0x41) - def test_32b(self): - self.assertEquals(BitPaddedInt(b'\xFF\xFF\xFF\xFF', bits=8), - 0xFFFFFFFF) +class TByteSpec(TestCase): - def test_32bi(self): - self.assertEquals(BitPaddedInt(0xFFFFFFFF, bits=8), 0xFFFFFFFF) + def test_validate(self): + s = ByteSpec('byte') + self.assertRaises(ValueError, s.validate, None, 1000) - def test_s32b(self): - self.assertEquals(BitPaddedInt(b'\xFF\xFF\xFF\xFF', bits=8).as_str(), - b'\xFF\xFF\xFF\xFF') + def test_read(self): + s = ByteSpec('name') + self.assertEquals((97, b'bcdefg'), s.read(None, None, b'abcdefg')) - def test_s0(self): - self.assertEquals(BitPaddedInt.to_str(0), b'\x00\x00\x00\x00') + def test_write(self): + s = ByteSpec('name') + self.assertEquals(b'a', s.write(None, None, 97)) + self.assertRaises(TypeError, s.write, None, None, b'abc') + self.assertRaises(TypeError, s.write, None, None, None) - def test_s1(self): - self.assertEquals(BitPaddedInt.to_str(1), b'\x00\x00\x00\x01') - def test_s1l(self): - self.assertEquals( - BitPaddedInt.to_str(1, bigendian=False), b'\x01\x00\x00\x00') +class TVolumePeakSpec(TestCase): - def test_s129(self): - self.assertEquals(BitPaddedInt.to_str(129), b'\x00\x00\x01\x01') + def test_validate(self): + s = VolumePeakSpec('peak', 0) + self.assertRaises(ValueError, s.validate, None, 2) - def test_s65(self): - self.assertEquals(BitPaddedInt.to_str(0x41, 6), b'\x00\x00\x01\x01') - def test_w129(self): - self.assertEquals(BitPaddedInt.to_str(129, width=2), b'\x01\x01') +class TStringSpec(TestCase): - def test_w129l(self): - self.assertEquals( - BitPaddedInt.to_str(129, width=2, bigendian=False), b'\x01\x01') + def test_validate(self): + s = StringSpec('byte', 3) + self.assertEqual(s.validate(None, "ABC"), "ABC") + self.assertEqual(s.validate(None, u"ABC"), u"ABC") + self.assertRaises(ValueError, s.validate, None, "abc2") + self.assertRaises(ValueError, s.validate, None, "ab") + self.assertRaises(TypeError, s.validate, None, None) + + if PY3: + self.assertRaises(TypeError, s.validate, None, b"ABC") + self.assertRaises(ValueError, s.validate, None, u"\xf6\xe4\xfc") + + def test_read(self): + s = StringSpec('name', 3) + self.assertEquals(('abc', b'defg'), s.read(None, None, b'abcdefg')) + self.assertRaises(SpecError, s.read, None, None, b'\xff') - def test_wsmall(self): - self.assertRaises(ValueError, BitPaddedInt.to_str, 129, width=1) + def test_write(self): + s = StringSpec('name', 3) + self.assertEquals(b'abc', s.write(None, None, 'abcdefg')) + self.assertEquals(b'\x00\x00\x00', s.write(None, None, '\x00')) + self.assertEquals(b'a\x00\x00', s.write(None, None, 'a')) - def test_str_int_init(self): - from struct import pack - self.assertEquals(BitPaddedInt(238).as_str(), - BitPaddedInt(pack('>L', 238)).as_str()) - - def test_varwidth(self): - self.assertEquals(len(BitPaddedInt.to_str(100)), 4) - self.assertEquals(len(BitPaddedInt.to_str(100, width=-1)), 4) - self.assertEquals(len(BitPaddedInt.to_str(2 ** 32, width=-1)), 5) - def test_minwidth(self): - self.assertEquals( - len(BitPaddedInt.to_str(100, width=-1, minwidth=6)), 6) +class TBinaryDataSpec(TestCase): + + def test_validate(self): + s = BinaryDataSpec('name') + self.assertRaises(TypeError, s.validate, None, None) + self.assertEqual(s.validate(None, b"abc"), b"abc") + if PY3: + self.assertRaises(TypeError, s.validate, None, "abc") + else: + self.assertEqual(s.validate(None, u"abc"), b"abc") + self.assertRaises(ValueError, s.validate, None, u"\xf6\xe4\xfc") + + def test_read(self): + s = BinaryDataSpec('name') + self.assertEquals((b'abcdefg', b''), s.read(None, None, b'abcdefg')) + + def test_write(self): + s = BinaryDataSpec('name') + self.assertEquals(b'43', s.write(None, None, 43)) + self.assertEquals(b'abc', s.write(None, None, b'abc')) + - def test_inval_input(self): - self.assertRaises(TypeError, BitPaddedInt, None) +class TSpec(TestCase): - if PY2: - def test_promote_long(self): - l = BitPaddedInt(sys.maxint ** 2) - self.assertTrue(isinstance(l, long)) - self.assertEqual(BitPaddedInt(l.as_str(width=-1)), l) - - def test_has_valid_padding(self): - self.failUnless(BitPaddedInt.has_valid_padding(b"\xff\xff", bits=8)) - self.failIf(BitPaddedInt.has_valid_padding(b"\xff")) - self.failIf(BitPaddedInt.has_valid_padding(b"\x00\xff")) - self.failUnless(BitPaddedInt.has_valid_padding(b"\x7f\x7f")) - self.failIf(BitPaddedInt.has_valid_padding(b"\x7f", bits=6)) - self.failIf(BitPaddedInt.has_valid_padding(b"\x9f", bits=6)) - self.failUnless(BitPaddedInt.has_valid_padding(b"\x3f", bits=6)) - - self.failUnless(BitPaddedInt.has_valid_padding(0xff, bits=8)) - self.failIf(BitPaddedInt.has_valid_padding(0xff)) - self.failIf(BitPaddedInt.has_valid_padding(0xff << 8)) - self.failUnless(BitPaddedInt.has_valid_padding(0x7f << 8)) - self.failIf(BitPaddedInt.has_valid_padding(0x9f << 32, bits=6)) - self.failUnless(BitPaddedInt.has_valid_padding(0x3f << 16, bits=6)) - - -class TestUnsynch(TestCase): - - def test_unsync_encode_decode(self): - pairs = [ - (b'', b''), - (b'\x00', b'\x00'), - (b'\x44', b'\x44'), - (b'\x44\xff', b'\x44\xff\x00'), - (b'\xe0', b'\xe0'), - (b'\xe0\xe0', b'\xe0\xe0'), - (b'\xe0\xff', b'\xe0\xff\x00'), - (b'\xff', b'\xff\x00'), - (b'\xff\x00', b'\xff\x00\x00'), - (b'\xff\x00\x00', b'\xff\x00\x00\x00'), - (b'\xff\x01', b'\xff\x01'), - (b'\xff\x44', b'\xff\x44'), - (b'\xff\xe0', b'\xff\x00\xe0'), - (b'\xff\xe0\xff', b'\xff\x00\xe0\xff\x00'), - (b'\xff\xf0\x0f\x00', b'\xff\x00\xf0\x0f\x00'), - (b'\xff\xff', b'\xff\x00\xff\x00'), - (b'\xff\xff\x01', b'\xff\x00\xff\x01'), - (b'\xff\xff\xff\xff', b'\xff\x00\xff\x00\xff\x00\xff\x00'), - ] - - for d, e in pairs: - self.assertEqual(unsynch.encode(d), e) - self.assertEqual(unsynch.decode(e), d) - self.assertEqual(unsynch.decode(unsynch.encode(e)), e) - self.assertEqual(unsynch.decode(e + e), d + d) - - def test_unsync_decode_invalid(self): - self.assertRaises(ValueError, unsynch.decode, b'\xff\xff\xff\xff') - self.assertRaises(ValueError, unsynch.decode, b'\xff\xf0\x0f\x00') - self.assertRaises(ValueError, unsynch.decode, b'\xff\xe0') - self.assertRaises(ValueError, unsynch.decode, b'\xff') + def test_no_hash(self): + self.failUnlessRaises( + TypeError, {}.__setitem__, Spec("foo", None), None) + + +class TRVASpec(TestCase): + + def test_read(self): + spec = RVASpec("name", False) + val, rest = spec.read( + None, None, + b"\x03\x10\xc7\xc7\xc7\xc7\x00\x00\x00\x00\x00\x00\x00\x00") + self.assertEqual(rest, b"") + self.assertEqual(val, [51143, 51143, 0, 0, 0, 0]) + + def test_read_stereo_only(self): + spec = RVASpec("name", True) + val, rest = spec.read( + None, None, + b"\x03\x10\xc7\xc7\xc7\xc7\x00\x00\x00\x00\x00\x00\x00\x00") + self.assertEqual(rest, b"\x00\x00\x00\x00") + self.assertEqual(val, [51143, 51143, 0, 0]) + + def test_write(self): + spec = RVASpec("name", False) + data = spec.write(None, None, [0, 1, 2, 3, -4, -5]) + self.assertEqual( + data, b"\x03\x10\x00\x00\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05") + + def test_write_stereo_only(self): + spec = RVASpec("name", True) + self.assertRaises( + SpecError, spec.write, None, None, [0, 0, 0, 0, 0, 0]) + + def test_validate(self): + spec = RVASpec("name", False) + self.assertRaises(ValueError, spec.validate, None, []) + self.assertEqual(spec.validate(None, [1, 2]), [1, 2]) + + +class TFrameIDSpec(TestCase): + + def test_read(self): + spec = FrameIDSpec("name", 3) + self.assertEqual(spec.read(None, None, b"FOOX"), (u"FOO", b"X")) + + def test_validate(self): + spec = FrameIDSpec("name", 3) + self.assertRaises(ValueError, spec.validate, None, u"123") + self.assertRaises(ValueError, spec.validate, None, u"TXXX") + self.assertEqual(spec.validate(None, u"TXX"), u"TXX") + + spec = FrameIDSpec("name", 4) + self.assertEqual(spec.validate(None, u"TXXX"), u"TXXX") + + +class TCTOCFlagsSpec(TestCase): + + def test_read(self): + spec = CTOCFlagsSpec("name") + v, r = spec.read(None, None, b"\x03") + self.assertEqual(r, b"") + self.assertEqual(v, 3) + self.assertTrue(isinstance(v, CTOCFlags)) + + def test_write(self): + spec = CTOCFlagsSpec("name") + self.assertEqual(spec.write(None, None, CTOCFlags.ORDERED), b"\x01") + + def test_validate(self): + spec = CTOCFlagsSpec("name") + self.assertEqual(spec.validate(None, 3), 3) + self.assertTrue(isinstance(spec.validate(None, 3), CTOCFlags)) + self.assertEqual(spec.validate(None, None), None) + + +class TID3FramesSpec(TestCase): + + def test_read_empty(self): + header = ID3Header() + header.version = (2, 4, 0) + spec = ID3FramesSpec("name") + + value, data = spec.read(header, None, b"") + self.assertEqual(data, b"") + self.assertTrue(isinstance(value, ID3Tags)) + + def test_read_tit3(self): + header = ID3Header() + header.version = (2, 4, 0) + spec = ID3FramesSpec("name") + + value, data = spec.read(header, None, + b"TIT3" + b"\x00\x00\x00\x03" + b"\x00\x00" + b"\x03" + b"F\x00") + + self.assertTrue(isinstance(value, ID3Tags)) + self.assertEqual(data, b"") + frames = value.getall("TIT3") + self.assertEqual(len(frames), 1) + self.assertEqual(frames[0].encoding, 3) + self.assertEqual(frames[0].text, [u"F"]) + + def test_write_empty(self): + header = ID3Header() + header.version = (2, 4, 0) + spec = ID3FramesSpec("name") + config = ID3SaveConfig() + + tags = ID3Tags() + self.assertEqual(spec.write(config, None, tags), b"") + + def test_write_tit3(self): + spec = ID3FramesSpec("name") + config = ID3SaveConfig() + + tags = ID3Tags() + tags.add(TIT3(encoding=3, text=[u"F", u"B"])) + self.assertEqual(spec.write(config, None, tags), + b"TIT3" + b"\x00\x00\x00\x05" + b"\x00\x00" + + b"\x03" + b"F\x00" + b"B\x00") + + def test_write_tit3_v23(self): + spec = ID3FramesSpec("name") + config = ID3SaveConfig(3, "/") + + tags = ID3Tags() + tags.add(TIT3(encoding=3, text=[u"F", u"B"])) + self.assertEqual(spec.write(config, None, tags), + b"TIT3" + b"\x00\x00\x00\x0B" + b"\x00\x00" + + b"\x01" + b"\xff\xfeF\x00/\x00B\x00\x00\x00") + + def test_validate(self): + header = ID3Header() + header.version = (2, 4, 0) + spec = ID3FramesSpec("name") + + self.assertRaises(TypeError, spec.validate, None, None) + self.assertTrue(isinstance(spec.validate(None, []), ID3Tags)) + + v = spec.validate(None, [TIT3(encoding=3, text=[u"foo"])]) + self.assertEqual(v.getall("TIT3")[0].text, [u"foo"]) + + +class TLatin1TextListSpec(TestCase): + + def test_read(self): + spec = Latin1TextListSpec("name") + self.assertEqual(spec.read(None, None, b"\x00xxx"), ([], b"xxx")) + self.assertEqual( + spec.read(None, None, b"\x01foo\x00"), ([u"foo"], b"")) + self.assertEqual( + spec.read(None, None, b"\x01\x00"), ([u""], b"")) + self.assertEqual( + spec.read(None, None, b"\x02f\x00o\x00"), ([u"f", u"o"], b"")) + + def test_write(self): + spec = Latin1TextListSpec("name") + self.assertEqual(spec.write(None, None, []), b"\x00") + self.assertEqual(spec.write(None, None, [u""]), b"\x01\x00") + + def test_validate(self): + spec = Latin1TextListSpec("name") + self.assertRaises(TypeError, spec.validate, None, object()) + self.assertRaises(TypeError, spec.validate, None, None) + self.assertEqual(spec.validate(None, [u"foo"]), [u"foo"]) + self.assertEqual(spec.validate(None, []), []) diff -Nru mutagen-1.33.2/tests/test__id3util.py mutagen-1.34/tests/test__id3util.py --- mutagen-1.33.2/tests/test__id3util.py 1970-01-01 00:00:00.000000000 +0000 +++ mutagen-1.34/tests/test__id3util.py 2016-07-07 12:07:48.000000000 +0000 @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- + +import sys +import struct + +from tests import TestCase + +from mutagen._compat import PY2 +from mutagen.id3._util import BitPaddedInt, BitPaddedLong, unsynch + + +class BitPaddedIntTest(TestCase): + + def test_long(self): + if PY2: + data = BitPaddedInt.to_str(sys.maxint + 1, width=16) + val = BitPaddedInt(data) + self.assertEqual(val, sys.maxint + 1) + self.assertTrue(isinstance(val, BitPaddedLong)) + else: + self.assertTrue(BitPaddedInt is BitPaddedLong) + + def test_negative(self): + self.assertRaises(ValueError, BitPaddedInt, -1) + + def test_zero(self): + self.assertEquals(BitPaddedInt(b'\x00\x00\x00\x00'), 0) + + def test_1(self): + self.assertEquals(BitPaddedInt(b'\x00\x00\x00\x01'), 1) + + def test_1l(self): + self.assertEquals( + BitPaddedInt(b'\x01\x00\x00\x00', bigendian=False), 1) + + def test_129(self): + self.assertEquals(BitPaddedInt(b'\x00\x00\x01\x01'), 0x81) + + def test_129b(self): + self.assertEquals(BitPaddedInt(b'\x00\x00\x01\x81'), 0x81) + + def test_65(self): + self.assertEquals(BitPaddedInt(b'\x00\x00\x01\x81', 6), 0x41) + + def test_32b(self): + self.assertEquals(BitPaddedInt(b'\xFF\xFF\xFF\xFF', bits=8), + 0xFFFFFFFF) + + def test_32bi(self): + self.assertEquals(BitPaddedInt(0xFFFFFFFF, bits=8), 0xFFFFFFFF) + + def test_s32b(self): + self.assertEquals(BitPaddedInt(b'\xFF\xFF\xFF\xFF', bits=8).as_str(), + b'\xFF\xFF\xFF\xFF') + + def test_s0(self): + self.assertEquals(BitPaddedInt.to_str(0), b'\x00\x00\x00\x00') + + def test_s1(self): + self.assertEquals(BitPaddedInt.to_str(1), b'\x00\x00\x00\x01') + + def test_s1l(self): + self.assertEquals( + BitPaddedInt.to_str(1, bigendian=False), b'\x01\x00\x00\x00') + + def test_s129(self): + self.assertEquals(BitPaddedInt.to_str(129), b'\x00\x00\x01\x01') + + def test_s65(self): + self.assertEquals(BitPaddedInt.to_str(0x41, 6), b'\x00\x00\x01\x01') + + def test_w129(self): + self.assertEquals(BitPaddedInt.to_str(129, width=2), b'\x01\x01') + + def test_w129l(self): + self.assertEquals( + BitPaddedInt.to_str(129, width=2, bigendian=False), b'\x01\x01') + + def test_wsmall(self): + self.assertRaises(ValueError, BitPaddedInt.to_str, 129, width=1) + + def test_str_int_init(self): + self.assertEquals(BitPaddedInt(238).as_str(), + BitPaddedInt(struct.pack('>L', 238)).as_str()) + + def test_varwidth(self): + self.assertEquals(len(BitPaddedInt.to_str(100)), 4) + self.assertEquals(len(BitPaddedInt.to_str(100, width=-1)), 4) + self.assertEquals(len(BitPaddedInt.to_str(2 ** 32, width=-1)), 5) + + def test_minwidth(self): + self.assertEquals( + len(BitPaddedInt.to_str(100, width=-1, minwidth=6)), 6) + + def test_inval_input(self): + self.assertRaises(TypeError, BitPaddedInt, None) + + if PY2: + def test_promote_long(self): + l = BitPaddedInt(sys.maxint ** 2) + self.assertTrue(isinstance(l, long)) + self.assertEqual(BitPaddedInt(l.as_str(width=-1)), l) + + def test_has_valid_padding(self): + self.failUnless(BitPaddedInt.has_valid_padding(b"\xff\xff", bits=8)) + self.failIf(BitPaddedInt.has_valid_padding(b"\xff")) + self.failIf(BitPaddedInt.has_valid_padding(b"\x00\xff")) + self.failUnless(BitPaddedInt.has_valid_padding(b"\x7f\x7f")) + self.failIf(BitPaddedInt.has_valid_padding(b"\x7f", bits=6)) + self.failIf(BitPaddedInt.has_valid_padding(b"\x9f", bits=6)) + self.failUnless(BitPaddedInt.has_valid_padding(b"\x3f", bits=6)) + + self.failUnless(BitPaddedInt.has_valid_padding(0xff, bits=8)) + self.failIf(BitPaddedInt.has_valid_padding(0xff)) + self.failIf(BitPaddedInt.has_valid_padding(0xff << 8)) + self.failUnless(BitPaddedInt.has_valid_padding(0x7f << 8)) + self.failIf(BitPaddedInt.has_valid_padding(0x9f << 32, bits=6)) + self.failUnless(BitPaddedInt.has_valid_padding(0x3f << 16, bits=6)) + + +class TestUnsynch(TestCase): + + def test_unsync_encode_decode(self): + pairs = [ + (b'', b''), + (b'\x00', b'\x00'), + (b'\x44', b'\x44'), + (b'\x44\xff', b'\x44\xff\x00'), + (b'\xe0', b'\xe0'), + (b'\xe0\xe0', b'\xe0\xe0'), + (b'\xe0\xff', b'\xe0\xff\x00'), + (b'\xff', b'\xff\x00'), + (b'\xff\x00', b'\xff\x00\x00'), + (b'\xff\x00\x00', b'\xff\x00\x00\x00'), + (b'\xff\x01', b'\xff\x01'), + (b'\xff\x44', b'\xff\x44'), + (b'\xff\xe0', b'\xff\x00\xe0'), + (b'\xff\xe0\xff', b'\xff\x00\xe0\xff\x00'), + (b'\xff\xf0\x0f\x00', b'\xff\x00\xf0\x0f\x00'), + (b'\xff\xff', b'\xff\x00\xff\x00'), + (b'\xff\xff\x01', b'\xff\x00\xff\x01'), + (b'\xff\xff\xff\xff', b'\xff\x00\xff\x00\xff\x00\xff\x00'), + ] + + for d, e in pairs: + self.assertEqual(unsynch.encode(d), e) + self.assertEqual(unsynch.decode(e), d) + self.assertEqual(unsynch.decode(unsynch.encode(e)), e) + self.assertEqual(unsynch.decode(e + e), d + d) + + def test_unsync_decode_invalid(self): + self.assertRaises(ValueError, unsynch.decode, b'\xff\xff\xff\xff') + self.assertRaises(ValueError, unsynch.decode, b'\xff\xf0\x0f\x00') + self.assertRaises(ValueError, unsynch.decode, b'\xff\xe0') + self.assertRaises(ValueError, unsynch.decode, b'\xff') diff -Nru mutagen-1.33.2/tests/test___init__.py mutagen-1.34/tests/test___init__.py --- mutagen-1.33.2/tests/test___init__.py 2016-07-05 14:41:37.000000000 +0000 +++ mutagen-1.34/tests/test___init__.py 2016-07-07 17:02:05.000000000 +0000 @@ -6,7 +6,7 @@ import shutil import warnings -from tests import TestCase, DATA_DIR +from tests import TestCase, DATA_DIR, get_temp_copy from mutagen._compat import cBytesIO, text_type, xrange from mutagen import File, Metadata, FileType, MutagenError, PaddingInfo from mutagen._util import loadfile, get_size @@ -100,7 +100,9 @@ filename = os.path.join(DATA_DIR, "empty.ogg") def test_old_argument_handling(self): - f = MyFileType() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + f = MyFileType() self.assertFalse(hasattr(f, "a")) f = MyFileType(self.filename) @@ -155,9 +157,7 @@ def setUp(self): self.vorbis = File(os.path.join(DATA_DIR, "empty.ogg")) - fd, filename = mkstemp(".mp3") - os.close(fd) - shutil.copy(os.path.join(DATA_DIR, "xing.mp3"), filename) + filename = get_temp_copy(os.path.join(DATA_DIR, "xing.mp3")) self.mp3_notags = File(filename) self.mp3_filename = filename @@ -168,7 +168,9 @@ self.failUnlessRaises(KeyError, self.vorbis.__delitem__, "foobar") def test_add_tags(self): - self.failUnlessRaises(NotImplementedError, FileType().add_tags) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.failUnlessRaises(NotImplementedError, FileType().add_tags) def test_delitem(self): self.vorbis["foobar"] = "quux" @@ -279,9 +281,7 @@ KIND = None def setUp(self): - fd, self.filename = mkstemp("." + self.PATH.rsplit(".", 1)[-1]) - os.close(fd) - shutil.copy(self.PATH, self.filename) + self.filename = get_temp_copy(self.PATH) self.audio = self.KIND(self.filename) def tearDown(self): diff -Nru mutagen-1.33.2/tests/test_m4a.py mutagen-1.34/tests/test_m4a.py --- mutagen-1.33.2/tests/test_m4a.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/tests/test_m4a.py 2016-07-05 15:28:44.000000000 +0000 @@ -21,7 +21,9 @@ self.assertRaises(error, delete, self.SOME_FILE) M4AInfo # pyflakes - a = M4A() + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + a = M4A() a.add_tags() self.assertEqual(a.tags.items(), []) diff -Nru mutagen-1.33.2/tests/test_mp3.py mutagen-1.34/tests/test_mp3.py --- mutagen-1.33.2/tests/test_mp3.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/tests/test_mp3.py 2016-07-07 16:42:20.000000000 +0000 @@ -1,16 +1,14 @@ # -*- coding: utf-8 -*- import os -import shutil -from tests import TestCase, DATA_DIR +from tests import TestCase, DATA_DIR, get_temp_copy from mutagen._compat import cBytesIO, text_type, xrange from mutagen.mp3 import MP3, error as MP3Error, delete, MPEGInfo, EasyMP3, \ BitrateMode, iter_sync from mutagen.mp3._util import XingHeader, XingHeaderError, VBRIHeader, \ VBRIHeaderError, LAMEHeader, LAMEError from mutagen.id3 import ID3 -from tempfile import mkstemp class TMP3Util(TestCase): @@ -46,10 +44,9 @@ lame_peak = os.path.join(DATA_DIR, 'lame-peak.mp3') def setUp(self): - original = os.path.join(DATA_DIR, "silence-44-s.mp3") - fd, self.filename = mkstemp(suffix='.mp3') - os.close(fd) - shutil.copy(original, self.filename) + self.filename = get_temp_copy( + os.path.join(DATA_DIR, "silence-44-s.mp3")) + self.mp3 = MP3(self.filename) self.mp3_2 = MP3(self.silence_nov2) self.mp3_3 = MP3(self.silence_mpeg2) @@ -100,10 +97,10 @@ self.failUnlessEqual(self.mp3_2.tags, ID3(self.silence_nov2)) def test_length(self): - self.assertAlmostEquals(self.mp3.info.length, 3.77, 2) - self.assertAlmostEquals(self.mp3_2.info.length, 3.77, 2) - self.assertAlmostEquals(self.mp3_3.info.length, 3.68475, 4) - self.assertAlmostEquals(self.mp3_4.info.length, 3.68475, 4) + self.assertAlmostEqual(self.mp3.info.length, 3.77, 2) + self.assertAlmostEqual(self.mp3_2.info.length, 3.77, 2) + self.assertAlmostEqual(self.mp3_3.info.length, 3.68475, 4) + self.assertAlmostEqual(self.mp3_4.info.length, 3.68475, 4) def test_version(self): self.failUnlessEqual(self.mp3.info.version, 1) @@ -228,7 +225,8 @@ def test_not_real_file(self): filename = os.path.join(DATA_DIR, "silence-44-s-v1.mp3") - fileobj = cBytesIO(open(filename, "rb").read(20)) + with open(filename, "rb") as h: + fileobj = cBytesIO(h.read(20)) self.failUnlessRaises(MP3Error, MPEGInfo, fileobj) def test_empty(self): @@ -239,10 +237,8 @@ class TEasyMP3(TestCase): def setUp(self): - original = os.path.join(DATA_DIR, "silence-44-s.mp3") - fd, self.filename = mkstemp(suffix='.mp3') - os.close(fd) - shutil.copy(original, self.filename) + self.filename = get_temp_copy( + os.path.join(DATA_DIR, "silence-44-s.mp3")) self.mp3 = EasyMP3(self.filename) def test_artist(self): @@ -257,7 +253,8 @@ # the tags and get the right offset of the first frame easy = self.mp3.info noneasy = MP3(self.filename).info - nonid3 = MPEGInfo(open(self.filename, "rb")) + with open(self.filename, "rb") as h: + nonid3 = MPEGInfo(h) self.failUnlessEqual(easy.length, noneasy.length) self.failUnlessEqual(noneasy.length, nonid3.length) diff -Nru mutagen-1.33.2/tests/test_mp4.py mutagen-1.34/tests/test_mp4.py --- mutagen-1.33.2/tests/test_mp4.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/tests/test_mp4.py 2016-07-07 16:48:36.000000000 +0000 @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- import os -import shutil import struct import subprocess from mutagen._compat import cBytesIO, PY3, text_type, PY2, izip -from tempfile import mkstemp -from tests import TestCase, DATA_DIR +from tests import TestCase, DATA_DIR, get_temp_copy from mutagen.mp4 import (MP4, Atom, Atoms, MP4Tags, MP4Info, delete, MP4Cover, MP4MetadataError, MP4FreeForm, error, AtomDataType, AtomError, _item_sort_key) @@ -96,7 +94,8 @@ filename = os.path.join(DATA_DIR, "has-tags.m4a") def setUp(self): - self.atoms = Atoms(open(self.filename, "rb")) + with open(self.filename, "rb") as h: + self.atoms = Atoms(h) def test_getitem(self): self.failUnless(self.atoms[b"moov"]) @@ -320,9 +319,7 @@ # save it into an existing mp4 original = os.path.join(DATA_DIR, "has-tags.m4a") - fd, filename = mkstemp(suffix='.mp4') - os.close(fd) - shutil.copy(original, filename) + filename = get_temp_copy(original) try: delete(filename) @@ -438,9 +435,7 @@ class TMP4(TestCase): def setUp(self): - fd, self.filename = mkstemp(suffix='.m4a') - os.close(fd) - shutil.copy(self.original, self.filename) + self.filename = get_temp_copy(self.original) self.audio = MP4(self.filename) def tearDown(self): @@ -871,9 +866,7 @@ original = os.path.join(DATA_DIR, "64bit.mp4") def setUp(self): - fd, self.filename = mkstemp(suffix='.mp4') - os.close(fd) - shutil.copy(self.original, self.filename) + self.filename = get_temp_copy(self.original) def test_update_parents(self): with open(self.filename, "rb") as fileobj: diff -Nru mutagen-1.33.2/tests/test_musepack.py mutagen-1.34/tests/test_musepack.py --- mutagen-1.33.2/tests/test_musepack.py 2016-04-08 15:01:09.000000000 +0000 +++ mutagen-1.34/tests/test_musepack.py 2016-07-07 16:39:29.000000000 +0000 @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- import os -import shutil -from tempfile import mkstemp from mutagen.id3 import ID3, TIT2 from mutagen.musepack import Musepack, MusepackInfo, MusepackHeaderError from mutagen._compat import cBytesIO -from tests import TestCase, DATA_DIR +from tests import TestCase, DATA_DIR, get_temp_copy class TMusepack(TestCase): @@ -103,27 +101,21 @@ class TMusepackWithID3(TestCase): - SAMPLE = os.path.join(DATA_DIR, "click.mpc") def setUp(self): - fd, self.NEW = mkstemp(suffix='mpc') - os.close(fd) - shutil.copy(self.SAMPLE, self.NEW) - with open(self.SAMPLE, "rb") as h1: - with open(self.NEW, "rb") as h2: - self.failUnlessEqual(h1.read(), h2.read()) + self.filename = get_temp_copy(os.path.join(DATA_DIR, "click.mpc")) def tearDown(self): - os.unlink(self.NEW) + os.unlink(self.filename) def test_ignore_id3(self): id3 = ID3() id3.add(TIT2(encoding=0, text='id3 title')) - id3.save(self.NEW) - f = Musepack(self.NEW) + id3.save(self.filename) + f = Musepack(self.filename) f['title'] = 'apev2 title' f.save() - id3 = ID3(self.NEW) + id3 = ID3(self.filename) self.failUnlessEqual(id3['TIT2'], 'id3 title') - f = Musepack(self.NEW) + f = Musepack(self.filename) self.failUnlessEqual(f['title'], 'apev2 title') diff -Nru mutagen-1.33.2/tests/test_oggflac.py mutagen-1.34/tests/test_oggflac.py --- mutagen-1.33.2/tests/test_oggflac.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/tests/test_oggflac.py 2016-07-07 16:55:10.000000000 +0000 @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- import os -import shutil - -from tempfile import mkstemp from mutagen._compat import cBytesIO from mutagen.oggflac import OggFLAC, OggFLACStreamInfo, delete, error from mutagen.ogg import OggPage, error as OggError -from tests import TestCase, DATA_DIR + +from tests import TestCase, DATA_DIR, get_temp_copy from tests.test_ogg import TOggFileTypeMixin from tests.test_flac import have_flac, call_flac @@ -18,10 +16,7 @@ PADDING_SUPPORT = False def setUp(self): - original = os.path.join(DATA_DIR, "empty.oggflac") - fd, self.filename = mkstemp(suffix='.ogg') - os.close(fd) - shutil.copy(original, self.filename) + self.filename = get_temp_copy(os.path.join(DATA_DIR, "empty.oggflac")) self.audio = OggFLAC(self.filename) def tearDown(self): @@ -33,16 +28,19 @@ self.failUnlessRaises(KeyError, self.audio.tags.__getitem__, "vendor") def test_streaminfo_bad_marker(self): - page = OggPage(open(self.filename, "rb")).write() + with open(self.filename, "rb") as h: + page = OggPage(h).write() page = page.replace(b"fLaC", b"!fLa", 1) self.failUnlessRaises(error, OggFLACStreamInfo, cBytesIO(page)) def test_streaminfo_too_short(self): - page = OggPage(open(self.filename, "rb")).write() + with open(self.filename, "rb") as h: + page = OggPage(h).write() self.failUnlessRaises(OggError, OggFLACStreamInfo, cBytesIO(page[:10])) def test_streaminfo_bad_version(self): - page = OggPage(open(self.filename, "rb")).write() + with open(self.filename, "rb") as h: + page = OggPage(h).write() page = page.replace(b"\x01\x00", b"\x02\x00", 1) self.failUnlessRaises(error, OggFLACStreamInfo, cBytesIO(page)) diff -Nru mutagen-1.33.2/tests/test_oggopus.py mutagen-1.34/tests/test_oggopus.py --- mutagen-1.33.2/tests/test_oggopus.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/tests/test_oggopus.py 2016-07-07 16:32:40.000000000 +0000 @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- import os -import shutil -from tempfile import mkstemp from mutagen._compat import BytesIO from mutagen.oggopus import OggOpus, OggOpusInfo, delete, error from mutagen.ogg import OggPage -from tests import TestCase, DATA_DIR +from tests import TestCase, DATA_DIR, get_temp_copy from tests.test_ogg import TOggFileTypeMixin @@ -15,10 +13,7 @@ Kind = OggOpus def setUp(self): - original = os.path.join(DATA_DIR, "example.opus") - fd, self.filename = mkstemp(suffix='.opus') - os.close(fd) - shutil.copy(original, self.filename) + self.filename = get_temp_copy(os.path.join(DATA_DIR, "example.opus")) self.audio = self.Kind(self.filename) def tearDown(self): @@ -41,12 +36,14 @@ self.failUnless("audio/ogg; codecs=opus" in self.audio.mime) def test_invalid_not_first(self): - page = OggPage(open(self.filename, "rb")) + with open(self.filename, "rb") as h: + page = OggPage(h) page.first = False self.failUnlessRaises(error, OggOpusInfo, BytesIO(page.write())) def test_unsupported_version(self): - page = OggPage(open(self.filename, "rb")) + with open(self.filename, "rb") as h: + page = OggPage(h) data = bytearray(page.packets[0]) data[8] = 0x03 diff -Nru mutagen-1.33.2/tests/test_ogg.py mutagen-1.34/tests/test_ogg.py --- mutagen-1.33.2/tests/test_ogg.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/tests/test_ogg.py 2016-07-07 16:42:15.000000000 +0000 @@ -2,15 +2,13 @@ import os import random -import shutil import subprocess from mutagen._compat import BytesIO, xrange -from tests import TestCase, DATA_DIR +from tests import TestCase, DATA_DIR, get_temp_copy from mutagen.ogg import OggPage, error as OggError from mutagen._util import cdata from mutagen import _util -from tempfile import mkstemp class TOggPage(TestCase): @@ -237,19 +235,14 @@ def test_renumber_reread(self): try: - fd, filename = mkstemp(suffix=".ogg") - os.close(fd) - shutil.copy(os.path.join(DATA_DIR, "multipagecomment.ogg"), - filename) + filename = get_temp_copy( + os.path.join(DATA_DIR, "multipagecomment.ogg")) with open(filename, "rb+") as fileobj: OggPage.renumber(fileobj, 1002429366, 20) with open(filename, "rb+") as fileobj: OggPage.renumber(fileobj, 1002429366, 0) finally: - try: - os.unlink(filename) - except OSError: - pass + os.unlink(filename) def test_renumber_muxed(self): pages = [OggPage() for i in xrange(10)] @@ -346,13 +339,16 @@ except (IOError, OSError): print("WARNING: Random data round trip test disabled.") return - for i in xrange(10): - num_packets = random.randrange(2, 100) - lengths = [random.randrange(10, 10000) - for i in xrange(num_packets)] - packets = list(map(random_file.read, lengths)) - self.failUnlessEqual( - packets, OggPage.to_packets(OggPage.from_packets(packets))) + try: + for i in xrange(10): + num_packets = random.randrange(2, 100) + lengths = [random.randrange(10, 10000) + for i in xrange(num_packets)] + packets = list(map(random_file.read, lengths)) + self.failUnlessEqual( + packets, OggPage.to_packets(OggPage.from_packets(packets))) + finally: + random_file.close() def test_packet_exactly_255(self): page = OggPage() diff -Nru mutagen-1.33.2/tests/test_oggspeex.py mutagen-1.34/tests/test_oggspeex.py --- mutagen-1.33.2/tests/test_oggspeex.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/tests/test_oggspeex.py 2016-07-07 16:41:29.000000000 +0000 @@ -6,19 +6,15 @@ from mutagen._compat import cBytesIO from mutagen.ogg import OggPage from mutagen.oggspeex import OggSpeex, OggSpeexInfo, delete, error -from tests import TestCase, DATA_DIR +from tests import TestCase, DATA_DIR, get_temp_copy from tests.test_ogg import TOggFileTypeMixin -from tempfile import mkstemp class TOggSpeex(TestCase, TOggFileTypeMixin): Kind = OggSpeex def setUp(self): - original = os.path.join(DATA_DIR, "empty.spx") - fd, self.filename = mkstemp(suffix='.ogg') - os.close(fd) - shutil.copy(original, self.filename) + self.filename = get_temp_copy(os.path.join(DATA_DIR, "empty.spx")) self.audio = self.Kind(self.filename) def tearDown(self): @@ -39,7 +35,8 @@ self.failUnlessEqual(0, self.audio.info.bitrate) def test_invalid_not_first(self): - page = OggPage(open(self.filename, "rb")) + with open(self.filename, "rb") as h: + page = OggPage(h) page.first = False self.failUnlessRaises(error, OggSpeexInfo, cBytesIO(page.write())) diff -Nru mutagen-1.33.2/tests/test_oggtheora.py mutagen-1.34/tests/test_oggtheora.py --- mutagen-1.33.2/tests/test_oggtheora.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/tests/test_oggtheora.py 2016-07-07 16:34:07.000000000 +0000 @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- import os -import shutil - -from tempfile import mkstemp from mutagen._compat import cBytesIO from mutagen.oggtheora import OggTheora, OggTheoraInfo, delete, error from mutagen.ogg import OggPage -from tests import TestCase, DATA_DIR + +from tests import TestCase, DATA_DIR, get_temp_copy from tests.test_ogg import TOggFileTypeMixin @@ -16,10 +14,9 @@ Kind = OggTheora def setUp(self): - original = os.path.join(DATA_DIR, "sample.oggtheora") - fd, self.filename = mkstemp(suffix='.ogg') - os.close(fd) - shutil.copy(original, self.filename) + self.filename = get_temp_copy( + os.path.join(DATA_DIR, "sample.oggtheora")) + self.audio = OggTheora(self.filename) self.audio2 = OggTheora( os.path.join(DATA_DIR, "sample_length.oggtheora")) @@ -30,7 +27,8 @@ os.unlink(self.filename) def test_theora_bad_version(self): - page = OggPage(open(self.filename, "rb")) + with open(self.filename, "rb") as h: + page = OggPage(h) packet = page.packets[0] packet = packet[:7] + b"\x03\x00" + packet[9:] page.packets = [packet] @@ -38,7 +36,8 @@ self.failUnlessRaises(error, OggTheoraInfo, fileobj) def test_theora_not_first_page(self): - page = OggPage(open(self.filename, "rb")) + with open(self.filename, "rb") as h: + page = OggPage(h) page.first = False fileobj = cBytesIO(page.write()) self.failUnlessRaises(error, OggTheoraInfo, fileobj) diff -Nru mutagen-1.33.2/tests/test_oggvorbis.py mutagen-1.34/tests/test_oggvorbis.py --- mutagen-1.33.2/tests/test_oggvorbis.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/tests/test_oggvorbis.py 2016-07-07 16:53:52.000000000 +0000 @@ -6,19 +6,16 @@ from mutagen._compat import cBytesIO from mutagen.ogg import OggPage from mutagen.oggvorbis import OggVorbis, OggVorbisInfo, delete, error -from tests import TestCase, DATA_DIR + +from tests import TestCase, DATA_DIR, get_temp_copy from tests.test_ogg import TOggFileTypeMixin -from tempfile import mkstemp class TOggVorbis(TestCase, TOggFileTypeMixin): Kind = OggVorbis def setUp(self): - original = os.path.join(DATA_DIR, "empty.ogg") - fd, self.filename = mkstemp(suffix='.ogg') - os.close(fd) - shutil.copy(original, self.filename) + self.filename = get_temp_copy(os.path.join(DATA_DIR, "empty.ogg")) self.audio = self.Kind(self.filename) def tearDown(self): @@ -39,12 +36,14 @@ self.failUnlessEqual(44100, self.audio.info.sample_rate) def test_invalid_not_first(self): - page = OggPage(open(self.filename, "rb")) + with open(self.filename, "rb") as h: + page = OggPage(h) page.first = False self.failUnlessRaises(error, OggVorbisInfo, cBytesIO(page.write())) def test_avg_bitrate(self): - page = OggPage(open(self.filename, "rb")) + with open(self.filename, "rb") as h: + page = OggPage(h) packet = page.packets[0] packet = (packet[:16] + b"\x00\x00\x01\x00" + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00" + packet[28:]) @@ -53,7 +52,8 @@ self.failUnlessEqual(info.bitrate, 32768) def test_overestimated_bitrate(self): - page = OggPage(open(self.filename, "rb")) + with open(self.filename, "rb") as h: + page = OggPage(h) packet = page.packets[0] packet = (packet[:16] + b"\x00\x00\x01\x00" + b"\x00\x00\x00\x01" + b"\x00\x00\x00\x00" + packet[28:]) @@ -62,7 +62,8 @@ self.failUnlessEqual(info.bitrate, 65536) def test_underestimated_bitrate(self): - page = OggPage(open(self.filename, "rb")) + with open(self.filename, "rb") as h: + page = OggPage(h) packet = page.packets[0] packet = (packet[:16] + b"\x00\x00\x01\x00" + b"\x01\x00\x00\x00" + b"\x00\x00\x01\x00" + packet[28:]) @@ -71,7 +72,8 @@ self.failUnlessEqual(info.bitrate, 65536) def test_negative_bitrate(self): - page = OggPage(open(self.filename, "rb")) + with open(self.filename, "rb") as h: + page = OggPage(h) packet = page.packets[0] packet = (packet[:16] + b"\xff\xff\xff\xff" + b"\xff\xff\xff\xff" + b"\xff\xff\xff\xff" + packet[28:]) diff -Nru mutagen-1.33.2/tests/test_tools_mid3iconv.py mutagen-1.34/tests/test_tools_mid3iconv.py --- mutagen-1.33.2/tests/test_tools_mid3iconv.py 2016-04-07 16:23:09.000000000 +0000 +++ mutagen-1.34/tests/test_tools_mid3iconv.py 2016-07-07 16:35:39.000000000 +0000 @@ -1,15 +1,13 @@ # -*- coding: utf-8 -*- import os -from tempfile import mkstemp -import shutil from mutagen.id3 import ID3 from mutagen._toolsutil import fsnative as fsn from mutagen._compat import text_type from tests.test_tools import _TTools -from tests import DATA_DIR +from tests import DATA_DIR, get_temp_copy AMBIGUOUS = b"\xc3\xae\xc3\xa5\xc3\xb4\xc3\xb2 \xc3\xa0\xc3\xa9\xc3\xa7\xc3" \ @@ -24,10 +22,8 @@ def setUp(self): super(TMid3Iconv, self).setUp() - original = os.path.join(DATA_DIR, fsn(u'silence-44-s.mp3')) - fd, self.filename = mkstemp(suffix=fsn(u'.mp3')) - os.close(fd) - shutil.copy(original, self.filename) + self.filename = get_temp_copy( + os.path.join(DATA_DIR, fsn(u'silence-44-s.mp3'))) def tearDown(self): super(TMid3Iconv, self).tearDown() diff -Nru mutagen-1.33.2/tests/test_tools_moggsplit.py mutagen-1.34/tests/test_tools_moggsplit.py --- mutagen-1.33.2/tests/test_tools_moggsplit.py 2016-04-07 16:23:09.000000000 +0000 +++ mutagen-1.34/tests/test_tools_moggsplit.py 2016-07-07 17:00:06.000000000 +0000 @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- import os -from tempfile import mkstemp -import shutil from mutagen._compat import text_type from mutagen._toolsutil import fsnative as fsn from tests.test_tools import _TTools -from tests import DATA_DIR +from tests import DATA_DIR, get_temp_copy class TMOggSPlit(_TTools): @@ -17,11 +15,8 @@ def setUp(self): super(TMOggSPlit, self).setUp() - original = os.path.join( - DATA_DIR, fsn(u'multipagecomment.ogg')) - fd, self.filename = mkstemp(suffix=fsn(u'.ogg')) - os.close(fd) - shutil.copy(original, self.filename) + self.filename = get_temp_copy( + os.path.join(DATA_DIR, fsn(u'multipagecomment.ogg'))) # append the second file first = open(self.filename, "ab") diff -Nru mutagen-1.33.2/tests/test_tools.py mutagen-1.34/tests/test_tools.py --- mutagen-1.33.2/tests/test_tools.py 2016-07-05 14:41:02.000000000 +0000 +++ mutagen-1.34/tests/test_tools.py 2016-07-05 15:28:44.000000000 +0000 @@ -3,6 +3,7 @@ import os import sys import imp +import warnings import mutagen from mutagen._compat import StringIO, text_type, PY2 @@ -17,7 +18,9 @@ dont_write_bytecode = sys.dont_write_bytecode sys.dont_write_bytecode = True try: - mod = imp.load_source(tool_name, tool_path) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + mod = imp.load_source(tool_name, tool_path) finally: sys.dont_write_bytecode = dont_write_bytecode return getattr(mod, entry) diff -Nru mutagen-1.33.2/tests/test_trueaudio.py mutagen-1.34/tests/test_trueaudio.py --- mutagen-1.33.2/tests/test_trueaudio.py 2016-06-23 16:30:27.000000000 +0000 +++ mutagen-1.34/tests/test_trueaudio.py 2016-07-07 16:52:08.000000000 +0000 @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- import os -import shutil + from mutagen.trueaudio import TrueAudio, delete, error from mutagen.id3 import TIT1 -from tests import TestCase, DATA_DIR -from tempfile import mkstemp + +from tests import TestCase, DATA_DIR, get_temp_copy class TTrueAudio(TestCase): @@ -37,10 +37,8 @@ self.failUnless(self.audio.pprint()) def test_save_reload(self): + filename = get_temp_copy(self.audio.filename) try: - fd, filename = mkstemp(suffix='.tta') - os.close(fd) - shutil.copy(self.audio.filename, filename) audio = TrueAudio(filename) audio.add_tags() audio.tags.add(TIT1(encoding=0, text="A Title")) diff -Nru mutagen-1.33.2/tests/test__util.py mutagen-1.34/tests/test__util.py --- mutagen-1.33.2/tests/test__util.py 2016-07-05 14:47:37.000000000 +0000 +++ mutagen-1.34/tests/test__util.py 2016-07-18 21:36:17.000000000 +0000 @@ -1,13 +1,17 @@ # -*- coding: utf-8 -*- -from mutagen._util import DictMixin, cdata, insert_bytes, delete_bytes -from mutagen._util import decode_terminated, dict_match, enum, get_size -from mutagen._util import BitReader, BitReaderError, resize_bytes, seek_end +from mutagen._util import DictMixin, cdata, insert_bytes, delete_bytes, \ + decode_terminated, dict_match, enum, get_size, BitReader, BitReaderError, \ + resize_bytes, seek_end, mmap_move, verify_fileobj, fileobj_name, \ + read_full, flags, resize_file, fallback_move from mutagen._compat import text_type, itervalues, iterkeys, iteritems, PY2, \ - cBytesIO, xrange -from tests import TestCase + cBytesIO, xrange, BytesIO +from tests import TestCase, get_temp_empty +import os import random +import tempfile import mmap +import errno try: import fcntl @@ -261,9 +265,129 @@ self.failIf(cdata.test_bit(v, 13)) +class Tresize_file(TestCase): + + def get_named_file(self, content): + filename = get_temp_empty() + h = open(filename, "wb+") + h.write(content) + h.seek(0) + return h + + def test_resize(self): + with self.get_named_file(b"") as h: + resize_file(h, 0) + self.assertEqual(os.path.getsize(h.name), 0) + self.assertRaises(ValueError, resize_file, h, -1) + resize_file(h, 1) + self.assertEqual(os.path.getsize(h.name), 1) + h.seek(0) + self.assertEqual(h.read(), b"\x00") + resize_file(h, 2 ** 17) + self.assertEqual(os.path.getsize(h.name), 2 ** 17 + 1) + h.seek(0) + self.assertEqual(h.read(), b"\x00" * (2 ** 17 + 1)) + + def test_resize_content(self): + with self.get_named_file(b"abc") as h: + self.assertRaises(ValueError, resize_file, h, -4) + resize_file(h, -1) + h.seek(0) + self.assertEqual(h.read(), b"ab") + resize_file(h, 2) + h.seek(0) + self.assertEqual(h.read(), b"ab\x00\x00") + + def test_resize_dev_full(self): + + def raise_no_space(*args): + raise IOError(errno.ENOSPC, os.strerror(errno.ENOSPC)) + + # fail on first write + h = BytesIO(b"abc") + h.write = raise_no_space + self.assertRaises(IOError, resize_file, h, 1) + h.seek(0, 2) + self.assertEqual(h.tell(), 3) + + # fail on flush + h = BytesIO(b"abc") + h.flush = raise_no_space + self.assertRaises(IOError, resize_file, h, 1) + h.seek(0, 2) + self.assertEqual(h.tell(), 3) + + +class TMoveMixin(object): + + MOVE = None + + def file(self, contents): + temp = tempfile.TemporaryFile() + temp.write(contents) + temp.flush() + temp.seek(0) + return temp + + def read(self, fobj): + fobj.seek(0, 0) + return fobj.read() + + def test_basic(self): + with self.file(b"abc123") as h: + self.MOVE(h, 0, 1, 4) + self.assertEqual(self.read(h), b"bc1223") + + with self.file(b"abc123") as h: + self.MOVE(h, 1, 0, 4) + self.assertEqual(self.read(h), b"aabc13") + + def test_invalid_params(self): + with self.file(b"foo") as o: + self.assertRaises(ValueError, self.MOVE, o, -1, 0, 0) + self.assertRaises(ValueError, self.MOVE, o, 0, -1, 0) + self.assertRaises(ValueError, self.MOVE, o, 0, 0, -1) + + def test_outside_file(self): + with self.file(b"foo") as o: + self.assertRaises(ValueError, self.MOVE, o, 0, 0, 4) + self.assertRaises(ValueError, self.MOVE, o, 0, 1, 3) + self.assertRaises(ValueError, self.MOVE, o, 1, 0, 3) + + def test_ok(self): + with self.file(b"foo") as o: + self.MOVE(o, 0, 1, 2) + self.MOVE(o, 1, 0, 2) + + def test_larger_than_page_size(self): + off = mmap.ALLOCATIONGRANULARITY + with self.file(b"f" * off * 2) as o: + self.MOVE(o, off, off + 1, off - 1) + self.MOVE(o, off + 1, off, off - 1) + + with self.file(b"f" * off * 2 + b"x") as o: + self.MOVE(o, off * 2 - 1, off * 2, 1) + self.assertEqual(self.read(o)[-3:], b"fxx") + + +class Tfallback_move(TestCase, TMoveMixin): + + MOVE = staticmethod(fallback_move) + + +class MmapMove(TestCase, TMoveMixin): + + MOVE = staticmethod(mmap_move) + + def test_stringio(self): + self.assertRaises(mmap.error, mmap_move, cBytesIO(), 0, 0, 0) + + def test_no_fileno(self): + self.assertRaises(mmap.error, mmap_move, object(), 0, 0, 0) + + class FileHandling(TestCase): def file(self, contents): - import tempfile temp = tempfile.TemporaryFile() temp.write(contents) temp.flush() @@ -275,103 +399,109 @@ return fobj.read() def test_resize_decrease(self): - o = self.file(b'abcd') - resize_bytes(o, 2, 1, 1) - self.assertEqual(self.read(o), b"abd") + with self.file(b'abcd') as o: + resize_bytes(o, 2, 1, 1) + self.assertEqual(self.read(o), b"abd") def test_resize_increase(self): - o = self.file(b'abcd') - resize_bytes(o, 2, 4, 1) - self.assertEqual(self.read(o), b"abcd\x00d") + with self.file(b'abcd') as o: + resize_bytes(o, 2, 4, 1) + self.assertEqual(self.read(o), b"abcd\x00d") def test_resize_nothing(self): - o = self.file(b'abcd') - resize_bytes(o, 2, 2, 1) - self.assertEqual(self.read(o), b"abcd") + with self.file(b'abcd') as o: + resize_bytes(o, 2, 2, 1) + self.assertEqual(self.read(o), b"abcd") def test_insert_into_empty(self): - o = self.file(b'') - insert_bytes(o, 8, 0) - self.assertEquals(b'\x00' * 8, self.read(o)) + with self.file(b'') as o: + insert_bytes(o, 8, 0) + self.assertEqual(b'\x00' * 8, self.read(o)) def test_insert_before_one(self): - o = self.file(b'a') - insert_bytes(o, 8, 0) - self.assertEquals(b'a' + b'\x00' * 7 + b'a', self.read(o)) + with self.file(b'a') as o: + insert_bytes(o, 8, 0) + self.assertEqual(b'a' + b'\x00' * 7 + b'a', self.read(o)) def test_insert_after_one(self): - o = self.file(b'a') - insert_bytes(o, 8, 1) - self.assertEquals(b'a' + b'\x00' * 8, self.read(o)) + with self.file(b'a') as o: + insert_bytes(o, 8, 1) + self.assertEqual(b'a' + b'\x00' * 8, self.read(o)) + + def test_insert_after_file(self): + with self.file(b'a') as o: + self.assertRaises(ValueError, insert_bytes, o, 1, 2) def test_smaller_than_file_middle(self): - o = self.file(b'abcdefghij') - insert_bytes(o, 4, 4) - self.assertEquals(b'abcdefghefghij', self.read(o)) + with self.file(b'abcdefghij') as o: + insert_bytes(o, 4, 4) + self.assertEqual(b'abcdefghefghij', self.read(o)) def test_smaller_than_file_to_end(self): - o = self.file(b'abcdefghij') - insert_bytes(o, 4, 6) - self.assertEquals(b'abcdefghijghij', self.read(o)) + with self.file(b'abcdefghij') as o: + insert_bytes(o, 4, 6) + self.assertEqual(b'abcdefghijghij', self.read(o)) def test_smaller_than_file_across_end(self): - o = self.file(b'abcdefghij') - insert_bytes(o, 4, 8) - self.assertEquals(b'abcdefghij\x00\x00ij', self.read(o)) + with self.file(b'abcdefghij') as o: + insert_bytes(o, 4, 8) + self.assertEqual(b'abcdefghij\x00\x00ij', self.read(o)) def test_smaller_than_file_at_end(self): - o = self.file(b'abcdefghij') - insert_bytes(o, 3, 10) - self.assertEquals(b'abcdefghij\x00\x00\x00', self.read(o)) + with self.file(b'abcdefghij') as o: + insert_bytes(o, 3, 10) + self.assertEqual(b'abcdefghij\x00\x00\x00', self.read(o)) def test_smaller_than_file_at_beginning(self): - o = self.file(b'abcdefghij') - insert_bytes(o, 3, 0) - self.assertEquals(b'abcabcdefghij', self.read(o)) + with self.file(b'abcdefghij') as o: + insert_bytes(o, 3, 0) + self.assertEqual(b'abcabcdefghij', self.read(o)) def test_zero(self): - o = self.file(b'abcdefghij') - self.assertRaises((AssertionError, ValueError), insert_bytes, o, 0, 1) + with self.file(b'abcdefghij') as o: + insert_bytes(o, 0, 1) + self.assertEqual(b'abcdefghij', self.read(o)) def test_negative(self): - o = self.file(b'abcdefghij') - self.assertRaises((AssertionError, ValueError), insert_bytes, o, 8, -1) + with self.file(b'abcdefghij') as o: + self.assertRaises(ValueError, insert_bytes, o, 8, -1) def test_delete_one(self): - o = self.file(b'a') - delete_bytes(o, 1, 0) - self.assertEquals(b'', self.read(o)) + with self.file(b'a') as o: + delete_bytes(o, 1, 0) + self.assertEqual(b'', self.read(o)) def test_delete_first_of_two(self): - o = self.file(b'ab') - delete_bytes(o, 1, 0) - self.assertEquals(b'b', self.read(o)) + with self.file(b'ab') as o: + delete_bytes(o, 1, 0) + self.assertEqual(b'b', self.read(o)) def test_delete_second_of_two(self): - o = self.file(b'ab') - delete_bytes(o, 1, 1) - self.assertEquals(b'a', self.read(o)) + with self.file(b'ab') as o: + delete_bytes(o, 1, 1) + self.assertEqual(b'a', self.read(o)) def test_delete_third_of_two(self): - o = self.file(b'ab') - self.assertRaises(AssertionError, delete_bytes, o, 1, 2) + with self.file(b'ab') as o: + self.assertRaises(ValueError, delete_bytes, o, 1, 2) def test_delete_middle(self): - o = self.file(b'abcdefg') - delete_bytes(o, 3, 2) - self.assertEquals(b'abfg', self.read(o)) + with self.file(b'abcdefg') as o: + delete_bytes(o, 3, 2) + self.assertEqual(b'abfg', self.read(o)) def test_delete_across_end(self): - o = self.file(b'abcdefg') - self.assertRaises(AssertionError, delete_bytes, o, 4, 8) + with self.file(b'abcdefg') as o: + self.assertRaises(ValueError, delete_bytes, o, 4, 8) def test_delete_zero(self): - o = self.file(b'abcdefg') - self.assertRaises(AssertionError, delete_bytes, o, 0, 3) + with self.file(b'abcdefg') as o: + delete_bytes(o, 0, 3) + self.assertEqual(b'abcdefg', self.read(o)) def test_delete_negative(self): - o = self.file(b'abcdefg') - self.assertRaises(AssertionError, delete_bytes, o, 4, -8) + with self.file(b'abcdefg') as o: + self.assertRaises(ValueError, delete_bytes, o, 4, -8) def test_insert_6106_79_51760(self): # This appears to be due to ANSI C limitations in read/write on rb+ @@ -379,9 +509,9 @@ # code for transfers of this or similar sizes. data = u''.join(map(text_type, xrange(12574))) # 51760 bytes data = data.encode("ascii") - o = self.file(data) - insert_bytes(o, 6106, 79) - self.failUnless(data[:6106 + 79] + data[79:] == self.read(o)) + with self.file(data) as o: + insert_bytes(o, 6106, 79) + self.failUnless(data[:6106 + 79] + data[79:] == self.read(o)) def test_delete_6106_79_51760(self): # This appears to be due to ANSI C limitations in read/write on rb+ @@ -389,9 +519,9 @@ # code for transfers of this or similar sizes. data = u''.join(map(text_type, xrange(12574))) # 51760 bytes data = data.encode("ascii") - o = self.file(data[:6106 + 79] + data[79:]) - delete_bytes(o, 6106, 79) - self.failUnless(data == self.read(o)) + with self.file(data[:6106 + 79] + data[79:]) as o: + delete_bytes(o, 6106, 79) + self.failUnless(data == self.read(o)) # Generate a bunch of random insertions, apply them, delete them, # and make sure everything is still correct. @@ -410,62 +540,42 @@ "Given testing parameters make this test useless") for j in xrange(num_runs): data = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ" * 1024 - fobj = self.file(data) - filesize = len(data) - # Generate the list of changes to apply - changes = [] - for i in xrange(num_changes): - change_size = random.randrange( - min_change_size, max_change_size) - change_offset = random.randrange(0, filesize) - filesize += change_size - changes.append((change_offset, change_size)) - - # Apply the changes, and make sure they all took. - for offset, size in changes: - buffer_size = random.randrange( - min_buffer_size, max_buffer_size) - insert_bytes(fobj, size, offset, BUFFER_SIZE=buffer_size) - fobj.seek(0) - self.failIfEqual(fobj.read(len(data)), data) - fobj.seek(0, 2) - self.failUnlessEqual(fobj.tell(), filesize) - - # Then, undo them. - changes.reverse() - for offset, size in changes: - buffer_size = random.randrange( - min_buffer_size, max_buffer_size) - delete_bytes(fobj, size, offset, BUFFER_SIZE=buffer_size) - fobj.seek(0) - self.failUnless(fobj.read() == data) - - -class FileHandlingMockedMMapMove(FileHandling): - - def setUp(self): - class MockMMap(object): - def __init__(self, *args, **kwargs): - pass - - def move(self, dest, src, count): - raise ValueError - - def close(self): - pass - - self._orig_mmap = mmap.mmap - mmap.mmap = MockMMap - - def tearDown(self): - mmap.mmap = self._orig_mmap + with self.file(data) as fobj: + filesize = len(data) + # Generate the list of changes to apply + changes = [] + for i in xrange(num_changes): + change_size = random.randrange( + min_change_size, max_change_size) + change_offset = random.randrange(0, filesize) + filesize += change_size + changes.append((change_offset, change_size)) + + # Apply the changes, and make sure they all took. + for offset, size in changes: + buffer_size = random.randrange( + min_buffer_size, max_buffer_size) + insert_bytes(fobj, size, offset, BUFFER_SIZE=buffer_size) + fobj.seek(0) + self.failIfEqual(fobj.read(len(data)), data) + fobj.seek(0, 2) + self.failUnlessEqual(fobj.tell(), filesize) + + # Then, undo them. + changes.reverse() + for offset, size in changes: + buffer_size = random.randrange( + min_buffer_size, max_buffer_size) + delete_bytes(fobj, size, offset, BUFFER_SIZE=buffer_size) + fobj.seek(0) + self.failUnless(fobj.read() == data) class FileHandlingMockedMMap(FileHandling): def setUp(self): def MockMMap2(*args, **kwargs): - raise EnvironmentError + raise mmap.error self._orig_mmap = mmap.mmap mmap.mmap = MockMMap2 @@ -512,10 +622,61 @@ self.assertTrue(isinstance(repr(Foo.FOO), str)) +class Tflags(TestCase): + + def test_enum(self): + @flags + class Foo(object): + FOO = 1 + BAR = 2 + + self.assertEqual(Foo.FOO, 1) + self.assertTrue(isinstance(Foo.FOO, Foo)) + self.assertEqual(repr(Foo.FOO), "") + self.assertEqual(repr(Foo(3)), "") + self.assertEqual(repr(Foo(42)), "") + self.assertEqual(str(Foo(42)), "Foo.BAR | 40") + self.assertEqual(int(Foo(42)), 42) + self.assertEqual(str(Foo(1)), "Foo.FOO") + self.assertEqual(int(Foo(1)), 1) + self.assertEqual(str(Foo(0)), "0") + + self.assertTrue(isinstance(str(Foo.FOO), str)) + self.assertTrue(isinstance(repr(Foo.FOO), str)) + + +class Tverify_fileobj(TestCase): + + def test_verify_fileobj_fail(self): + self.assertRaises(ValueError, verify_fileobj, object()) + with tempfile.TemporaryFile(mode="rb") as h: + self.assertRaises(ValueError, verify_fileobj, h, writable=True) + + def test_verify_fileobj(self): + with tempfile.TemporaryFile(mode="rb") as h: + verify_fileobj(h) + + with tempfile.TemporaryFile(mode="rb+") as h: + verify_fileobj(h, writable=True) + + +class Tfileobj_name(TestCase): + + def test_fileobj_name_other_type(self): + + class Foo(object): + name = 123 + + self.assertEqual(fileobj_name(Foo()), "123") + + def test_fileobj_name(self): + with tempfile.TemporaryFile(mode="rb") as h: + self.assertEqual(fileobj_name(h), text_type(h.name)) + + class Tseek_end(TestCase): def file(self, contents): - import tempfile temp = tempfile.TemporaryFile() temp.write(contents) temp.flush() @@ -541,6 +702,14 @@ self.assertEqual(f.tell(), 0) +class Tread_full(TestCase): + + def test_read_full(self): + fileobj = cBytesIO() + self.assertRaises(ValueError, read_full, fileobj, -3) + self.assertRaises(IOError, read_full, fileobj, 3) + + class Tget_size(TestCase): def test_get_size(self): @@ -575,6 +744,8 @@ UnicodeDecodeError, decode_terminated, b"\xff\xfe\x00", "utf-16") # not null terminated self.assertRaises(ValueError, decode_terminated, b"abc", "utf-8") + self.assertRaises( + ValueError, decode_terminated, b"\xff\xfea\x00", "utf-16") # invalid encoding self.assertRaises(LookupError, decode_terminated, b"abc", "foobar") @@ -602,6 +773,22 @@ v = r.bits(i) << (64 - i) | r.bits(64 - i) self.assertEqual(v, ref) + def test_bits_null(self): + r = BitReader(cBytesIO(b"")) + self.assertEqual(r.bits(0), 0) + + def test_bits_error(self): + r = BitReader(cBytesIO(b"")) + self.assertRaises(ValueError, r.bits, -1) + + def test_bytes_error(self): + r = BitReader(cBytesIO(b"")) + self.assertRaises(ValueError, r.bytes, -1) + + def test_skip_error(self): + r = BitReader(cBytesIO(b"")) + self.assertRaises(ValueError, r.skip, -1) + def test_read_too_much(self): r = BitReader(cBytesIO(b"")) self.assertEqual(r.bits(0), 0)