Commits

Travis Shirk committed 2e54deb Merge

merge

  • Participants
  • Parent commits 958a296, cc06d3c

Comments (0)

Files changed (7)

docs/changelog.rst

 
 **0.7.1** - TBD (TBD)
   New Features:
+    * A new plugin for toggling the state of iTunes podcast
+      files. In other words, PCST and WFED support. Additionally, the Apple
+      "extensions" frames TKWD, TDES, and TGID are supported.
+      Run ``eyeD3 -P itunes-podcast --help`` for more info.
+    * Native frame type for POPM (Popularity meter).
+      See the :func:`eyed3.id3.tag.Tag.popularities` accessor method.
+    * Plugins can deal with traversed directories instead of only file-by-file.
+      Also, :class:`eyed3.plugins.LoaderPlugin` can optionally cache the
+      loaded audio file objects for each callback to ``handleDirectory``.
 
   Bug fixes:
+    * Fixed a very old bug where certain values of 0 would be written to
+      the tag as '' instead of '\x00'.
     * [classic plugin] Use the system text encoding (locale) when converting
       lyrics files to Unicode.
     * Don't crash on malformed (invalid) UFID frames. Fixes :bbissue:`6`.
+    * Handle timestamps that are terminated with 'Z' to show the time is UTC.
 
 .. _release-0.7:
 
    :maxdepth: 1
 
    plugins/classic_plugin
+   plugins/itunes_plugin
    plugins/genres_plugin
    plugins/lameinfo_plugin
    plugins/nfo_plugin

docs/plugins/itunes_plugin.rst

+itunes-podcast - Convert files so iTunes recognizes them as poscasts
+====================================================================
+
+.. {{{cog
+.. cog.out(cog_pluginHelp("itunes-podcast"))
+.. }}}
+.. {{{end}}}
+
+Example
+-------
+
+.. {{{cog cli_example("examples/cli_examples.sh", "ITUNES_PODCAST_PLUGIN", lang="bash") }}}
+.. {{{end}}}

examples/cli_examples.sh

 eyeD3 --add-image http\\://example.com/cover.jpg:FRONT_COVER example.id3
 # [[[endsection]]]
 
-
-# [[[section REMOVE_ALL_TAGS]]]
-eyeD3 --remove-all example.id3
-# [[[endsection]]]
-
 # [[[section GENRES_PLUGIN1]]]
 eyeD3 --plugin=genres
 # [[[endsection]]]
 # [[[section PLUGINS_LIST]]]
 eyeD3 --plugins
 # [[[endsection]]]
+
+# [[[section ITUNES_PODCAST_PLUGIN]]]
+eyeD3 -P itunes-podcast example.id3
+eyeD3 -P itunes-podcast example.id3 --add
+eyeD3 -P itunes-podcast example.id3 --remove
+# [[[endsection]]]
+
+# [[[section REMOVE_ALL_TAGS]]]
+eyeD3 --remove-all example.id3
+# [[[endsection]]]

src/eyed3/id3/apple.py

+# -*- coding: utf-8 -*-
+################################################################################
+#  Copyright (C) 2012  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
+#
+################################################################################
+'''
+Here lies Apple frames, all of which are non-standard. All of these would have
+been standard user text frames by anyone not being a bastard, on purpose.
+'''
+from .frames import Frame, TextFrame
+
+
+class PCST(Frame):
+    '''Indicates a podcast. The 4 bytes of data is undefined, and is typically
+    all 0.'''
+
+    def __init__(self, id="PCST"):
+        super(PCST, self).__init__("PCST")
+
+    def render(self):
+        self.data = b"\x00" * 4
+        return super(PCST, self).render()
+
+
+class TKWD(TextFrame):
+    '''Podcast keywords.'''
+
+    def __init__(self, id="TKWD"):
+        super(TKWD, self).__init__("TKWD")
+
+
+class TDES(TextFrame):
+    '''Podcast description. One encoding byte followed by text per encoding.'''
+
+    def __init__(self, id="TDES"):
+        super(TDES, self).__init__("TDES")
+
+class TGID(TextFrame):
+    '''Podcast URL of the audio file. This should be a W frame!'''
+
+    def __init__(self, id="TGID"):
+        super(TGID, self).__init__("TGID")
+
+class WFED(TextFrame):
+    '''Another podcast URL, the feed URL it is said.'''
+
+    def __init__(self, id="WFED", url=""):
+        super(WFED, self).__init__("WFED", unicode(url))

src/eyed3/id3/frames.py

         assert(LATIN1_ENCODING <= self.encoding <= UTF_8_ENCODING)
 
 
-## 
-# Text frames: Data string format: encoding (one byte) + text
 class TextFrame(Frame):
+    '''Text frames.
+    Data string format: encoding (one byte) + text
+    '''
     @requireUnicode("text")
     def __init__(self, id, text=None):
         super(TextFrame, self).__init__(id)
-        assert(self.id[0] == 'T' or self.id in ["XSOA", "XSOP", "XSOT", "XDOR"])
+        assert(self.id[0] == 'T' or self.id in ["XSOA", "XSOP", "XSOT", "XDOR",
+                                                "WFED"])
         self.text = text or u""
 
     @property
         self.uniq_id = uniq_id
 
     def parse(self, data, frame_header):
-        # Data format
-        # Owner identifier <text string> $00
-        # Identifier       up to 64 bytes binary data>
+        '''
+        Data format
+        Owner identifier <text string> $00
+        Identifier       up to 64 bytes binary data>
+        '''
         super(UniqueFileIDFrame, self).parse(data, frame_header)
         split_data = self.data.split('\x00', 1)
         if len(split_data) == 2:
             else:
                 self[fid] = TextFrame(fid, text=text)
 
-
 def deunsyncData(data):
     output = []
     safe = True
     if fid in ID3_FRAMES:
         (desc, ver, FrameClass) = ID3_FRAMES[fid]
     elif fid in NONSTANDARD_ID3_FRAMES:
-        log.warning("Non standard frame '%s' encountered" % fid)
+        log.verbose("Non standard frame '%s' encountered" % fid)
         (desc, ver, FrameClass) = NONSTANDARD_ID3_FRAMES[fid]
     else:
         log.warning("Unknown ID3 frame ID: %s" % fid)
     "LNK" : "LINK", # LINKEDINFO linked information
     # Extension workarounds i.e., ignore them
     "TCP" : "TCP ", # iTunes "extension" for compilation marking
-    "CM1" : "CM1 "  # Seems to be some script kiddie tagging the tag.
+    "CM1" : "CM1 ", # Seems to be some script kiddie tagging the tag.
                     # For example, [rH] join #rH on efnet [rH]
+    "PCS" : "PCST", # iTunes extension for podcast marking. 
 }
 
+import apple
 NONSTANDARD_ID3_FRAMES = {
         "NCON": ("Undefined MusicMatch extension", ID3_V2, Frame),
         "TCMP": ("iTunes complilation flag extension", ID3_V2, TextFrame),
                  ID3_V2_3, TextFrame),
         "XDOR": ("MusicBrainz release date (full) extension for v2.3",
                  ID3_V2_3, TextFrame),
+
+        "PCST": ("iTunes extension; marks the file as a podcast",
+                 ID3_V2, apple.PCST),
+        "TKWD": ("iTunes extension; podcast keywords?",
+                 ID3_V2, apple.TKWD),
+        "TDES": ("iTunes extension; podcast description?",
+                 ID3_V2, apple.TDES),
+        "TGID": ("iTunes extension; podcast ?????",
+                 ID3_V2, apple.TGID),
+        "WFED": ("iTunes extension; podcast feed URL?",
+                 ID3_V2, apple.WFED),
 }
+

src/eyed3/plugins/itunes.py

+# -*- coding: utf-8 -*-
+################################################################################
+#  Copyright (C) 2012  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
+#
+################################################################################
+from __future__ import print_function
+from eyed3.plugins import LoaderPlugin
+from eyed3.id3.apple import PCST, WFED
+
+class Podcast(LoaderPlugin):
+    NAMES = ['itunes-podcast']
+    SUMMARY = u"Adds (or removes) the tags necessary for Apple iTunes to "\
+               "identify the file as a podcast."
+
+    def __init__(self, arg_parser):
+        super(Podcast, self).__init__(arg_parser)
+        g = self.arg_group
+        g.add_argument("--add", action="store_true",
+                       help="Add the podcast frames.")
+        g.add_argument("--remove", action="store_true",
+                       help="Remove the podcast frames.")
+
+    def _add(self, tag):
+        save = False
+        if "PCST" not in tag.frame_set:
+            tag.frame_set["PCST"] = PCST()
+            save = True
+        if "WFED" not in tag.frame_set:
+            tag.frame_set["WFED"] = WFED(u"http://eyeD3.nicfit.net/")
+            save = True
+
+        if save:
+            print("\tAdding...")
+            tag.save()
+            self._printStatus(tag)
+
+    def _remove(self, tag):
+        save = False
+        for fid in ["PCST", "WFED"]:
+            try:
+                del tag.frame_set[fid]
+                save = True
+            except KeyError:
+                continue
+
+        if save:
+            print("\tRemoving...")
+            tag.save()
+            self._printStatus(tag)
+
+    def _printStatus(self, tag):
+        status = ":-("
+        if "PCST" in tag.frame_set:
+            status = ":-/"
+            if "WFED" in tag.frame_set:
+                status = ":-)"
+        print("\tiTunes podcast? %s" % status)
+
+    def handleFile(self, f):
+        super(Podcast, self).handleFile(f)
+
+        if self.audio_file and self.audio_file.tag:
+            print(f)
+            tag = self.audio_file.tag
+            self._printStatus(tag)
+            if self.args.remove:
+                self._remove(self.audio_file.tag)
+            elif self.args.add:
+                self._add(self.audio_file.tag)
+