Commits

Travis Shirk committed 7cda8eb Merge

merge

  • Participants
  • Parent commits 3157708, e64c6ce

Comments (0)

Files changed (6)

     ),
 
     test=Bunch(
-       debug=False,
+       pdb=False,
        coverage=False,
     ),
 
 
 @task
 @needs("build")
-@cmdopts([("debug", "",
+@cmdopts([("pdb", "",
            u"Run with all output and launch pdb for errors and failures"),
           ("coverage", "", u"Run tests with coverage analysis"),
          ])
 def test(options):
     '''Runs all tests'''
-    if options.test and options.test.debug:
+    if options.test and options.test.pdb:
         debug_opts = "--pdb --pdb-failures -s"
     else:
         debug_opts = ""

File src/eyed3/id3/frames.py

                 bin2bytes(dec2bin(self.picture_type, 8)) +
                 self.description.encode(id3EncodingToString(self.encoding)) +
                 self.text_delim)
+
         if self.image_data:
             data += self.image_data
-        else:
+        elif self.image_url:
             data += self.image_url.encode("ascii")
 
         self.data = data
 
 
 class PrivateFrame(Frame):
+    '''PRIV'''
 
     def __init__(self, id=PRIVATE_FID, owner_id=b"", owner_data=b""):
         super(PrivateFrame, self).__init__(id)

File src/eyed3/id3/tag.py

         try:
             tag_found = False
             padding = 0
+            # The & is for supporting the "meta" versions, any, etc.
             if version[0] & 2:
                 tag_found, padding = self._loadV2Tag(fileobj)
 
             if tag_found and self.isV2():
                 self.file_info.tag_size = (TagHeader.SIZE +
                                            self.header.tag_size)
+            if tag_found:
+                self.file_info.tag_padding_size = padding
+
         finally:
             if close_file:
                 fileobj.close()
 
         if not txt and self.frame_set[fid]:
             del self.frame_set[fid]
-        else:
+        elif txt:
             self.frame_set.setTextFrame(fid, txt)
 
     def getTextFrame(self, fid):
             flist.remove(frame)
             converted_frames.append(frame)
 
+        # TSIZ (v2.3) are completely deprecated, remove them
+        if version == ID3_V2_4:
+            flist = [f for f in flist if f.id != "TSIZ"]
+
         # Raise an error for frames that could not be converted.
         if len(flist) != 0:
             unconverted = ", ".join([f.id for f in flist])
             raise ValueError('Unrecognized named group in pattern',
                              self.pattern)
 
-        return self.pattern.sub(convert, self.template)
+        name = self.pattern.sub(convert, self.template)
+        return name.replace('/', '-') if self._path_friendly else name
 
     safe_substitute = substitute
 
             return str(date)
 
     def _track(self, tag, param, zeropad):
-        tn, tt = (str(n) for n in tag.track_num)
+        tn, tt = (str(n) if n else None for n in tag.track_num)
         if zeropad:
-            tt = tt.rjust(2, "0")
-            tn = tn.rjust(len(tt), "0")
+            if tt:
+                tt = tt.rjust(2, "0")
+            tn = tn.rjust(len(tt) if tt else 2, "0")
 
         if param.endswith(":num"):
             return tn

File src/eyed3/plugins/classic.py

                           metavar="DATE", help=ARGS_HELP["--tagging-date"])
 
         # Misc
-        gid3.add_argument("-p", "--publisher", action="store", type=UnicodeArg,
+        gid3.add_argument("--publisher", action="store", type=UnicodeArg,
                           dest="publisher", metavar="STRING",
                           help=ARGS_HELP["--publisher"])
         gid3.add_argument("--play-count", type=PlayCountArg, dest="play_count",

File src/eyed3/plugins/statistics.py

 import sys, os, operator
 from collections import Counter
 
-from eyed3 import id3
+from eyed3 import id3, mp3
 from eyed3.core import AUDIO_MP3
 from eyed3.utils import guessMimetype, cli
 from eyed3.plugins import LoaderPlugin
                operator.ne: "!=",
               }
 
+class Rule(object):
+    def test(self):
+        raise NotImplementedError()
+
+
+PREFERRED_ID3_VERSIONS = [ id3.ID3_V2_3,
+                           id3.ID3_V2_4,
+                         ]
+class Id3TagRules(Rule):
+    def test(self, path, audio_file):
+        scores = []
+
+        if audio_file is None:
+            return None
+
+        if not audio_file.tag:
+            return [(-75, "Missing ID3 tag")];
+
+        tag = audio_file.tag
+        if tag.version not in PREFERRED_ID3_VERSIONS:
+            scores.append((-30, "ID3 version not in %s" %
+                                PREFERRED_ID3_VERSIONS))
+        if not tag.title:
+            scores.append((-30, "Tag missing title"))
+        if not tag.artist:
+            scores.append((-28, "Tag missing artist"))
+        if not tag.album:
+            scores.append((-26, "Tag missing album"))
+        if not tag.track_num[0]:
+            scores.append((-24, "Tag missing track number"))
+        if not tag.track_num[1]:
+            scores.append((-22, "Tag missing total # of tracks"))
+
+        if not tag.best_release_date:
+            scores.append((-30, "Tag missing any useful dates"))
+        else:
+            if not tag.original_release_date:
+                # Original release date is so rarely used but is almost always
+                # what I mean or wanna know.
+                scores.append((-10, "No original release date"))
+            elif not tag.release_date:
+                scores.append((-5, "No release date"))
+
+        # TLEN, best gotten from audio_file.info.time_secs but having it in
+        # the tag is good, I guess.
+        if "TLEN" not in tag.frame_set:
+            scores.append((-5, "No TLEN frame"))
+
+        return scores
+
+
+BITRATE_DEDUCTIONS = [(192, -20), (256, -10)]
+class BitrateRule(Rule):
+    def test(self, path, audio_file):
+        scores = []
+
+        if not audio_file:
+            return None
+
+        if not audio_file.info:
+            # Detected as an audio file but not real audio data found.
+            return [(-90, "No audio data found")]
+
+        is_vbr, bitrate = audio_file.info.bit_rate
+        for threshold, score in BITRATE_DEDUCTIONS:
+            if bitrate < threshold:
+                scores.append((score, "Bit rate < %d" % threshold))
+                break
+
+        return scores
+
+
+VALID_MIME_TYPES = mp3.MIME_TYPES + [ "image/png",
+                                      "image/gif",
+                                      "image/jpeg",
+                                    ]
+class FileRule(Rule):
+    def test(self, path, audio_file):
+        mt = guessMimetype(path)
+
+        for name in os.path.split(path):
+            if name.startswith('.'):
+                return [(-100, "Hidden file type")]
+
+        if mt not in VALID_MIME_TYPES:
+            return [(-100, "Unsupported file type: %s" % mt)]
+        return None
+
+
+VALID_ARTWORK_NAMES = ("cover", "cover-front", "cover-back")
+class ArtworkRule(Rule):
+    def test(self, path, audio_file):
+        mt = guessMimetype(path)
+        if mt and mt.startswith("image/"):
+            name, ext = os.path.splitext(os.path.basename(path))
+            if name not in VALID_ARTWORK_NAMES:
+                return [(-10, "Artwork file not in %s" %
+                              str(VALID_ARTWORK_NAMES))]
+
+        return None
+
+
+BAD_FRAMES = ["PRIV", "GEOB"]
+class Id3FrameRules(Rule):
+    def test(self, path, audio_file):
+        scores = []
+        if not audio_file or not audio_file.tag:
+            return
+
+        tag = audio_file.tag
+        for fid in tag.frame_set:
+            if fid[0] == 'T' and fid != "TXXX" and len(tag.frame_set[fid]) > 1:
+                scores.append((-10, "Multiple %s frames" % fid))
+            elif fid in BAD_FRAMES:
+                scores.append((-13, "%s frames are bad, mmmkay?" % fid))
+
+        return scores
+
+
 class Stat(Counter):
     TOTAL = "total"
 
         super(Id3VersionCounter, self)._report()
 
 
+class Id3FrameCounter(AudioStat):
+    def _compute(self, audio_file):
+        if audio_file.tag:
+            for frame_id in audio_file.tag.frame_set:
+                self[frame_id] += len(audio_file.tag.frame_set[frame_id])
+
+    def _report(self):
+        print(cli.BOLD + cli.GREY + "ID3 frames:" + cli.RESET)
+        super(Id3FrameCounter, self)._report(most_common=True)
+
+
 class BitrateCounter(AudioStat):
     def __init__(self):
         super(BitrateCounter, self).__init__()
         return keys
 
 
+class RuleViolationStat(Stat):
+    def _report(self):
+        print(cli.BOLD + cli.GREY + "Rule Violations:" + cli.RESET)
+        super(RuleViolationStat, self)._report(most_common=True)
+
+
 class StatisticsPlugin(LoaderPlugin):
     NAMES = ['stats']
     SUMMARY = u"Computes statistics for all audio files scanned."
 
     def __init__(self, arg_parser):
         super(StatisticsPlugin, self).__init__(arg_parser)
+
+        self.arg_group.add_argument(
+                "--verbose", action="store_true", default=False,
+                help="Show details for each file with rule violations.")
+
         self._stats = []
+        self._rules_stat = RuleViolationStat()
 
-        self.file_counter = FileCounterStat()
-        self._stats.append(self.file_counter)
+        self._stats.append(FileCounterStat())
+        self._stats.append(MimeTypeStat())
+        self._stats.append(Id3VersionCounter())
+        self._stats.append(Id3FrameCounter())
+        self._stats.append(BitrateCounter())
 
-        self.mt_stat = MimeTypeStat()
-        self._stats.append(self.mt_stat)
+        self._score_sum = 0
+        self._score_count = 0
+        self._rules_log = {}
+        self._rules = [ Id3TagRules(),
+                        FileRule(),
+                        ArtworkRule(),
+                        BitrateRule(),
+                        Id3FrameRules(),
+                      ]
 
-        self.id3_version_counter = Id3VersionCounter()
-        self._stats.append(self.id3_version_counter)
-
-        self.bitrates = BitrateCounter()
-        self._stats.append(self.bitrates)
-
-    def handleFile(self, f):
-        super(StatisticsPlugin, self).handleFile(f)
+    def handleFile(self, path):
+        super(StatisticsPlugin, self).handleFile(path)
         sys.stdout.write('.')
         sys.stdout.flush()
 
                 if self.audio_file:
                     stat.compute(self.audio_file)
             else:
-                stat.compute(f, self.audio_file)
+                stat.compute(path, self.audio_file)
+
+        self._score_count += 1
+        total_score = 100
+        for rule in self._rules:
+            scores = rule.test(path, self.audio_file) or []
+            if scores:
+                if path not in self._rules_log:
+                    self._rules_log[path] = []
+
+                for score, text in scores:
+                    self._rules_stat[text] += 1
+                    self._rules_log[path].append((score, text))
+                    # += because negative values are returned
+                    total_score += score
+
+        if total_score != 100:
+            self._rules_stat[Stat.TOTAL] += 1
+
+        self._score_sum += total_score
 
     def handleDone(self):
-        print("\n")
-        for stat in self._stats:
+
+        print()
+        for stat in self._stats + [self._rules_stat]:
             stat.report()
-            print("\n")
+            print()
+
+        # Detailed rule violations
+        if self.args.verbose:
+            for path in self._rules_log:
+                print(path)
+                for score, text in self._rules_log[path]:
+                    print("\t%s%s%s (%s)" % (cli.RED, str(score).center(3),
+                                             cli.RESET, text))
+
+        def prettyScore():
+            score = float(self._score_sum) / float(self._score_count)
+            if score > 80:
+                color = cli.GREEN
+            elif score > 70:
+                color = cli.YELLOW
+            else:
+                color = cli.RED
+            return (score, color)
+
+        score, color = prettyScore()
+        print("%sScore%s = %s%d%%%s" % (cli.BOLD, cli.BOLD_OFF,
+                                        color, score, cli.RESET))
+        if not self.args.verbose:
+            print("Run with --verbose to see files and their rule violations")
         print()
 
+
+

File src/eyed3/utils/log.py

     console_handler.setFormatter(logging.Formatter(DEFAULT_FORMAT))
     log.addHandler(console_handler)
 
-    log.setLevel(logging.NOTSET)
+    log.setLevel(logging.ERROR)
 
     return log