Commits

Travis Shirk committed 1caa5e3 Merge

Merge from default.

Comments (0)

Files changed (9)

examples/chapters.py

     print("== Chapter '%s'" % chapter.element_id)
     # TIT2 sub frame
     print("-- Title:", chapter.title)
-    # TIT333ub frame
+    # TIT3 sub frame
     print("-- subtitle:", chapter.subtitle)
+    # WXXX sub frame
+    print("-- url:", chapter.user_url)
     # Start and end time - tuple
     print("-- Start time: %d; End time: %d" % chapter.times)
     # Start and end offset - tuple. None is used to set to "no offset"
 chapter_frame = tag.chapters.get("final chapter")
 chapter_frame.element_id = b"Final Chapter"
 chapter_frame.offsets = (800000, None)
+chapter_frame.user_url = "http://example.com/foo"
+chapter_frame.user_url = "http://example.com/chapter#final"
+chapter_frame.user_url = None
 
 print("-" * 80)
 for chap in tag.chapters:
 
 RELEASE_D = "~/www/eyeD3/releases"
 
-def host_type():
-    run('uname -a')
 
 def deploy_sdist():
+    '''Deploy .tgz, .zip, and .md5'''
     put("./dist/%s" % SRC_DIST_TGZ, RELEASE_D)
     put("./dist/%s" % SRC_DIST_ZIP, RELEASE_D)
     put("./dist/%s.md5" % os.path.splitext(SRC_DIST_TGZ)[0], RELEASE_D)
 
+
 def deploy_docs():
+    '''Deploy docs tarball and install.'''
     put("./dist/%s" % DOC_DIST, RELEASE_D)
     run("tar xzf %s -C ./www/eyeD3 --strip-components=1" %
             os.path.join(RELEASE_D, DOC_DIST))
 
+
 def deploy():
     deploy_sdist()
     deploy_docs()
     for f in path(".").walk(pattern="*.orig"):
         f.remove()
     path("src/eyed3/info.py").remove()
+    path(".ropeproject").rmtree()
 
 
 @task

src/eyed3/id3/frames.py

 # -*- coding: utf-8 -*-
 ################################################################################
-#  Copyright (C) 2012  Travis Shirk <travis@pobox.com>
+#  Copyright (C) 2012-2013  Travis Shirk <travis@pobox.com>
 #
 #  This program is free software; you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 
         # Any data remaining must be a TIT2 frame
         self.description = None
-        if data:
+        if data and data[:4] != "TIT2":
+            log.warning("Invalid toc data, TIT2 frame expected")
+            return
+        elif data:
+            data = StringIO(data)
+            frame_header = FrameHeader.parse(data, self.header.version)
+            data = data.read()
             description_frame = TextFrame(TITLE_FID)
             description_frame.parse(data, frame_header)
+
             self.description = description_frame.text
 
     def render(self):
         flags = [0] * 8
         if self.toplevel:
-            flags[TOP_LEVEL_FLAG_BIT] = 1
+            flags[self.TOP_LEVEL_FLAG_BIT] = 1
         if self.ordered:
-            flags[ORDERED_FLAG_BIT] = 1
+            flags[self.ORDERED_FLAG_BIT] = 1
 
         data = (self.element_id.encode('ascii') + '\x00' +
                 bin2bytes(flags) + dec2bytes(len(self.child_ids)))
             data += id + '\x00'
 
         if self.description is not None:
-            data += TextFrame(TITLE_FID, self.description).render()
+            desc_frame = TextFrame(TITLE_FID, self.description)
+            desc_frame.header = FrameHeader(TITLE_FID, self.header.version)
+            data += desc_frame.render()
 
         self.data = data
         return super(TocFrame, self).render()
 
 StartEndTuple = namedtuple("StartEndTuple", ["start", "end"])
+'''A 2-tuple, with names 'start' and 'end'.'''
 
 
 class ChapterFrame(Frame):
             dummy_tag_header.tag_size = len(data)
             padding = self.sub_frames.parse(StringIO(data), dummy_tag_header,
                                             ExtendedTagHeader())
+        else:
+            self.sub_frames = FrameSet()
 
     def render(self):
         data = self.element_id.encode('ascii') + '\x00'
 
         for n in self.times + self.offsets:
             if n is not None:
-                data += dec2bytes(n)
+                data += dec2bytes(n, 32)
             else:
                 data += b'\xff\xff\xff\xff'
 
         for f in self.sub_frames.getAllFrames():
+            f.header = FrameHeader(f.id, self.header.version)
             data += f.render()
 
+        self.data = data
         return super(ChapterFrame, self).render()
 
     @property
 
     @subtitle.setter
     def subtitle(self, subtitle):
-        self.sub_frames.setTextFrame(TITLE_FID, subtitle)
+        self.sub_frames.setTextFrame(SUBTITLE_FID, subtitle)
+
+    @property
+    def user_url(self):
+        if USERURL_FID in self.sub_frames:
+            frame = self.sub_frames[USERURL_FID][0]
+            # Not returning frame description, it is always the same since it
+            # allows only 1 URL.
+            return frame.url
+        return None
+
+    @user_url.setter
+    def user_url(self, url):
+        DESCRIPTION = u"chapter url"
+
+        if url is None:
+            del self.sub_frames[USERURL_FID]
+        else:
+            if USERURL_FID in self.sub_frames:
+                for frame in self.sub_frames[USERURL_FID]:
+                    if frame.description == DESCRIPTION:
+                        frame.url = url
+                        return
+
+            self.sub_frames[USERURL_FID] = UserUrlFrame(USERURL_FID,
+                                                        DESCRIPTION, url)
 
 
 class FrameSet(dict):

src/eyed3/id3/tag.py

         else:
             assert(not "Version bug: %s" % str(version))
 
-        if preserve_file_time:
+        if preserve_file_time and None not in (self.file_info.atime,
+                                               self.file_info.mtime):
             os.utime(self.file_info.name,
                      (self.file_info.atime, self.file_info.mtime))
         else:
         self.tag_size = 0  # This includes the padding byte count.
         self.tag_padding_size = 0
 
-        s = os.stat(self.name)
-        self.atime, self.mtime = s.st_atime, s.st_mtime
+        try:
+            s = os.stat(self.name)
+        except OSError:
+            self.atime, self.mtime = None, None
+        else:
+            self.atime, self.mtime = s.st_atime, s.st_mtime
 
 
 class AccessorBase(object):
             if chap.element_id == element_id:
                 # update
                 chap.times, chap.offsets = times, offsets
-                chap.sub_frames = sub_frames
+                if sub_frames:
+                    chap.sub_frames = sub_frames
                 return chap
 
         chap = frames.ChapterFrame(element_id=element_id,
                                    times=times, offsets=offsets,
-                                   sub_frames=sub_frames or [])
+                                   sub_frames=sub_frames)
         self._fs[frames.CHAPTER_FID] = chap
         return chap
 
         return toc
 
     def remove(self, element_id):
-        return super(ChaptersAccessor, self).remove(element_id)
+        return super(TocAccessor, self).remove(element_id)
 
     def get(self, element_id):
-        return super(ChaptersAccessor, self).get(element_id)
+        return super(TocAccessor, self).get(element_id)
 
     def __getitem__(self, elem_id):
         '''Overiding the index based __getitem__ for one indexed with table

src/test/__init__.py

 import os
 import sys
 import logging
+import unittest
 import eyed3
 
 DATA_D = os.path.join(os.path.abspath(os.path.curdir), "src", "test", "data")
             if not s.isatty():
                 s.seek(self._seek_offset)
         sys.stdout, sys.stderr = self._orig_stdout, self._orig_stderr
+
+
+class ExternalDataTestCase(unittest.TestCase):
+    '''Test case for external data files.'''
+    def setUp(self):
+        pass

src/test/compat.py

+# -*- coding: utf-8 -*-
+################################################################################
+#  Copyright (C) 2013  Travis Shirk <travis@pobox.com>
+#
+#  This program is free software; you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation; either version 2 of the License, or
+#  (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+################################################################################
+import sys
+from nose.tools import assert_true
+
+# assert functions that are not in unittest in python 2.6, and therefore not
+# import from nost.tools as in python >= 2.7
+if sys.version_info[:2] == (2, 6):
+
+    def assert_is_none(data):
+        assert_true(data is None)
+
+    def assert_is_not_none(data):
+        assert_true(data is not None)
+
+    def assert_in(data, container):
+        assert_true(data in container)
+
+    def assert_is(data1, data2):
+        assert_true(data1 is data2)

src/test/id3/test_frames.py

                 f._initEncoding()
                 assert_equal(f.encoding, enc)
 
+
 class TextFrameTest(unittest.TestCase):
     def testCtor(self):
         assert_raises(TypeError, TextFrame, "TCON", "not unicode")
             assert_equal(f1.text, f2.text)
             assert_equal(f1.encoding, f2.encoding)
 
+
 class ImageFrameTest(unittest.TestCase):
     def testPicTypeConversions(self):
         count = 0
             assert_false("TypeError not thrown")
 
 
-
 def test_compression():
     data = open(__file__).read()
     compressed = Frame.compress(data)
     assert_equal(data, Frame.decompress(compressed))
 
+
 def test_encryption():
     assert_raises(NotImplementedError, Frame.encrypt, "Iceburn")
     assert_raises(NotImplementedError, Frame.decrypt, "Iceburn")
 
+

src/test/id3/test_tag.py

 from eyed3.id3 import Tag, ID3_DEFAULT_VERSION, ID3_V2_3, ID3_V2_4
 from eyed3.id3 import frames
 from ..compat import *
+from .. import ExternalDataTestCase, DATA_D
 
 
 def testTagImport():
     finally:
         os.remove(test_file)
 
+@unittest.skipIf(not os.path.exists(DATA_D), "test requires data files")
+def testChapterExampleTag():
+    tag = eyed3.load(os.path.join(DATA_D, "id3_chapters_example.mp3")).tag
+
+    assert_equal(len(tag.table_of_contents), 1)
+    toc = list(tag.table_of_contents)[0]
+
+    assert_equal(id(toc), id(tag.table_of_contents.get(toc.element_id)))
+
+    assert_equal(toc.element_id, "toc1")
+    assert_is_none(toc.description)
+    assert_true(toc.toplevel)
+    assert_true(toc.ordered)
+    assert_equal(toc.child_ids, ['ch1', 'ch2', 'ch3'])
+
+    assert_equal(tag.chapters.get("ch1").title, "start")
+    assert_equal(tag.chapters.get("ch1").subtitle, None)
+    assert_equal(tag.chapters.get("ch1").user_url, None)
+    assert_equal(tag.chapters.get("ch1").times, (0, 5000))
+    assert_equal(tag.chapters.get("ch1").offsets, (None, None))
+    assert_equal(len(tag.chapters.get("ch1").sub_frames), 1)
+
+    assert_equal(tag.chapters.get("ch2").title, "5 seconds")
+    assert_equal(tag.chapters.get("ch2").subtitle, None)
+    assert_equal(tag.chapters.get("ch2").user_url, None)
+    assert_equal(tag.chapters.get("ch2").times, (5000, 10000))
+    assert_equal(tag.chapters.get("ch2").offsets, (None, None))
+    assert_equal(len(tag.chapters.get("ch2").sub_frames), 1)
+
+    assert_equal(tag.chapters.get("ch3").title, "10 seconds")
+    assert_equal(tag.chapters.get("ch3").subtitle, None)
+    assert_equal(tag.chapters.get("ch3").user_url, None)
+    assert_equal(tag.chapters.get("ch3").times, (10000, 15000))
+    assert_equal(tag.chapters.get("ch3").offsets, (None, None))
+    assert_equal(len(tag.chapters.get("ch3").sub_frames), 1)
+
+
+def testTableOfContents():
+    test_file = "/tmp/toc.id3"
+    t = Tag()
+
+    assert_equal(len(t.table_of_contents), 0)
+
+    toc_main = t.table_of_contents.set("main", toplevel=True,
+                                       child_ids=["c1", "c2", "c3", "c4"],
+                                       description=u"Table of Conents")
+    assert_is_not_none(toc_main)
+    assert_equal(len(t.table_of_contents), 1)
+
+    toc_dc = t.table_of_contents.set("director-cut", toplevel=False,
+                                     ordered=False,
+                                     child_ids=["d3", "d1", "d2"])
+    assert_is_not_none(toc_dc)
+    assert_equal(len(t.table_of_contents), 2)
+
+    toc_dummy = t.table_of_contents.set("test")
+    assert_equal(len(t.table_of_contents), 3)
+    t.table_of_contents.remove(toc_dummy.element_id)
+    assert_equal(len(t.table_of_contents), 2)
+
+    t.save(test_file)
+    try:
+        t2 = eyed3.load(test_file).tag
+    finally:
+        os.remove(test_file)
+
+    assert_equal(len(t.table_of_contents), 2)
+
+    assert_equal(t2.table_of_contents.get("main").toplevel, True)
+    assert_equal(t2.table_of_contents.get("main").ordered, True)
+    assert_equal(t2.table_of_contents.get("main").description,
+                 toc_main.description)
+    assert_equal(t2.table_of_contents.get("main").child_ids, toc_main.child_ids)
+
+    assert_equal(t2.table_of_contents.get("director-cut").toplevel,
+                 toc_dc.toplevel)
+    assert_equal(t2.table_of_contents.get("director-cut").ordered, False)
+    assert_equal(t2.table_of_contents.get("director-cut").description,
+                 toc_dc.description)
+    assert_equal(t2.table_of_contents.get("director-cut").child_ids,
+                 toc_dc.child_ids)
+
+
+def testChapters():
+    test_file = "/tmp/chapters.id3"
+    t = Tag()
+
+    ch1 = t.chapters.set("c1", (0, 200))
+    ch2 = t.chapters.set("c2", (200, 300))
+    ch3 = t.chapters.set("c3", (300, 375))
+    ch4 = t.chapters.set("c4", (375, 600))
+
+    assert_equal(len(t.chapters), 4)
+
+    for i, c in enumerate(iter(t.chapters), 1):
+        if i != 2:
+            c.title = u"Chapter %d" % i
+            c.subtitle = u"Subtitle %d" % i
+            c.user_url = "http://example.com/%d" % i
+
+    t.save(test_file)
+
+    try:
+        t2 = eyed3.load(test_file).tag
+    finally:
+        os.remove(test_file)
+
+    assert_equal(len(t2.chapters), 4)
+    for i in range(1, 5):
+        c = t2.chapters.get("c%d" % i)
+        if i == 2:
+            assert_is_none(c.title)
+            assert_is_none(c.subtitle)
+            assert_is_none(c.user_url)
+        else:
+            assert_equal(c.title, u"Chapter %d" % i)
+            assert_equal(c.subtitle, u"Subtitle %d" % i)
+            assert_equal(c.user_url, u"http://example.com/%d" % i)
+
+
+
+