Commits

Travis Shirk committed c6adb36

Native frame type, tag acessor, etc. for POPM - Popularity meter.

  • Participants
  • Parent commits 4ad8103
  • Branches stable

Comments (0)

Files changed (3)

File src/eyed3/id3/frames.py

 CDID_FID           = "MCDI"
 PRIVATE_FID        = "PRIV"
 TOS_FID            = "USER"
+POPULARITY_FID     = "POPM"
 
 URL_COMMERCIAL_FID = "WCOM"
 URL_COPYRIGHT_FID  = "WCOP"
         self.data = dec2bytes(self.count, 32)
         return super(PlayCountFrame, self).render()
 
+
+class PopularityFrame(Frame):
+    '''Frame type for 'POPM' frames; popularity.
+    Frame format:
+    <Header for 'Popularimeter', ID: "POPM">
+    Email to user   <text string> $00
+    Rating          $xx
+    Counter         $xx xx xx xx (xx ...)
+    '''
+    def __init__(self, id=POPULARITY_FID, email=b"", rating=0, count=0):
+        super(PopularityFrame, self).__init__(id)
+        assert(self.id == POPULARITY_FID)
+
+        self.email = email or b""
+        self.rating = rating
+        if count is None or count < 0:
+            raise ValueError("Invalid count value: %s" % str(count))
+        self.count = count
+
+    @property
+    def rating(self):
+        return self._rating
+    @rating.setter
+    def rating(self, rating):
+        if rating < 0 or rating > 255:
+            raise ValueError("Popularity rating must be >= 0 and <=255")
+        self._rating = rating
+
+    @property
+    def email(self):
+        return self._email
+    @email.setter
+    def email(self, email):
+        self._email = email.encode("ascii")
+
+    @property
+    def count(self):
+        return self._count
+    @count.setter
+    def count(self, count):
+        if count < 0:
+            raise ValueError("Popularity count must be > 0")
+        self._count = count
+
+    def parse(self, data, frame_header):
+        super(PopularityFrame, self).parse(data, frame_header)
+        data = self.data
+
+        null_byte = data.find('\x00')
+        self.email = data[:null_byte]
+        data = data[null_byte + 1:]
+
+        self.rating = bytes2dec(data[0])
+
+        data = data[1:]
+        if len(self.data) < 4:
+            log.warning("Invalid POPM play count: less than 32 bits")
+        self.count = bytes2dec(data)
+
+    def render(self):
+        data = (self.email or b"") + '\x00'
+        data += dec2bytes(self.rating)
+        data += dec2bytes(self.count, 32)
+
+        self.data = data
+        return super(PopularityFrame, self).render()
+
 class UniqueFileIDFrame(Frame):
     def __init__(self, id=UNIQUE_FILE_ID_FID, owner_id=None, uniq_id=None):
         super(UniqueFileIDFrame, self).__init__(id)
 
                "PRIV": ("Private frame", ID3_V2, PrivateFrame),
                "PCNT": ("Play counter", ID3_V2, PlayCountFrame),
-               "POPM": ("Popularimeter", ID3_V2, None),
+               "POPM": ("Popularimeter", ID3_V2, PopularityFrame),
                "POSS": ("Position synchronisation frame", ID3_V2, None),
 
                "RBUF": ("Recommended buffer size", ID3_V2, None),

File src/eyed3/id3/tag.py

         self._user_texts = UserTextsAccessor(self.frame_set)
         self._unique_file_ids = UniqueFileIdAccessor(self.frame_set)
         self._user_urls = UserUrlsAccessor(self.frame_set)
+        self._popularities = PopularitiesAccessor(self.frame_set)
         self.file_info = None
 
     def parse(self, fileobj, version=ID3_ANY_VERSION):
     def privates(self):
         return self._privates
 
+    @property
+    def popularities(self):
+        return self._popularities
+
     def _getGenre(self):
         f = self.frame_set[frames.GENRE_FID]
         if f and f[0].text:
     def get(self, description):
         return super(UserUrlsAccessor, self).get(description)
 
+class PopularitiesAccessor(AccessorBase):
+    def __init__(self, fs):
+        def match_func(frame, email):
+            return frame.email == email
+        super(PopularitiesAccessor, self).__init__(frames.POPULARITY_FID, fs,
+                                                   match_func)
+
+    def set(self, email, rating, play_count):
+        flist = self._fs[frames.POPULARITY_FID] or []
+        for popm in flist:
+            if popm.email == email:
+                # update
+                popm.rating = rating
+                popm.count = play_count
+                return popm
+
+        popm = frames.PopularityFrame(email=email, rating=rating,
+                                      count=play_count)
+        self._fs[frames.POPULARITY_FID] = popm
+        return popm
+
+    def remove(self, email):
+        return super(PopularitiesAccessor, self).remove(email)
+
+    def get(self, email):
+        return super(PopularitiesAccessor, self).get(email)
+
 
 import string
 class TagTemplate(string.Template):

File src/eyed3/plugins/classic.py

             if len(id) > 64:
                 raise ValueError("id must be <= 64 bytes")
             return (owner_id, id)
+        def PopularityArg(arg):
+            '''EMAIL:RATING[:PLAY_COUNT]
+            Returns (email, rating, play_count)'''
+            args = _splitArgs(arg)
+            if len(args) < 2:
+                raise ValueError("Incorrect number of argument components")
+            email = args[0]
+            rating = int(args[1])
+            if rating < 0 or rating > 255:
+                raise ValueError("Rating out-of-range")
+            play_count = 0
+            if len(args) > 2:
+                play_count = int(args[2])
+            if play_count < 0:
+                raise ValueError("Play count out-of-range")
+            return (email, rating, play_count)
 
         # Tag versions
         gid3.add_argument("-1", "--v1", action="store_const", const=id3.ID3_V1,
                           dest="remove_all_objects",
                           help=ARGS_HELP["--remove-all-objects"])
 
+        gid3.add_argument("--add-popularity", action="append",
+                          type=PopularityArg, dest="popularities", default=[],
+                          metavar="EMAIL:RATING[:PLAY_COUNT]",
+                          help=ARGS_HELP["--add-popularty"])
+        gid3.add_argument("--remove-popularity", action="append", type=str,
+                          dest="remove_popularity", default=[],
+                          metavar="EMAIL",
+                          help=ARGS_HELP["--remove-popularity"])
+
         gid3.add_argument("--remove-v1", action="store_true", dest="remove_v1",
                           default=False, help=ARGS_HELP["--remove-v1"])
         gid3.add_argument("--remove-v2", action="store_true", dest="remove_v2",
             if tag.play_count is not None:
                  printMsg("%s %d" % (boldText("Play Count:"), play_count))
 
+            # POPM
+            for popm in tag.popularities:
+                printMsg("%s [email: %s] [rating: %d] [play count: %d]" %
+                         (boldText("Popularity:"), popm.email, popm.rating,
+                          popm.count))
+
             # TBPM
             bpm = tag.bpm
             if bpm is not None:
                 tag.play_count = pc
             retval = True
 
+        # --add-popularty
+        for email, rating, play_count in self.args.popularities:
+            tag.popularities.set(email, rating, play_count)
+            retval = True
+
+        # --remove-popularity
+        for email in self.args.remove_popularity:
+            popm = tag.popularities.remove(email)
+            if popm:
+                retval = True
+
         # --text-frame, --url-frame
         for what, arg, setter in (
                 ("text frame", self.args.text_frames, tag.setTextFrame),
         "--write-objects": "Causes all attached objects (GEOB frames) to be "
                            "written to the specified directory.",
 
+        "--add-popularty": "Adds a pupularity metric. There may be multiples "
+                           "popularity values, but each must have a unique "
+                           "email address component. The rating is a number "
+                           "between 0 (worst) and 255 (best). The play count "
+                           "is optional, and defaults to 0, since there is "
+                           "already a dedicated play count frame.",
+        "--remove-popularity": "Removes the popularity frame with the "
+                               "specified email key.",
+
         "--remove-v1": "Remove ID3 v1.x tag.",
         "--remove-v2": "Remove ID3 v2.x tag.",
         "--remove-all": "Remove ID3 v1.x and v2.x tags.",