~ubuntu-branches/ubuntu/trusty/eyed3/trusty

« back to all changes in this revision

Viewing changes to src/eyeD3/tag.py

  • Committer: Bazaar Package Importer
  • Author(s): Alexander Wirt
  • Date: 2006-11-18 01:47:38 UTC
  • mfrom: (1.2.1 upstream) (2.1.5 feisty)
  • Revision ID: james.westby@ubuntu.com-20061118014738-bcwcoe9gywrgorz2
New upstream version  

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
################################################################################
 
1
###############################################################################
2
2
#
3
 
#  Copyright (C) 2002-2005  Travis Shirk <travis@pobox.com>
 
3
#  Copyright (C) 2002-2006  Travis Shirk <travis@pobox.com>
4
4
#
5
5
#  This program is free software; you can redistribute it and/or modify
6
6
#  it under the terms of the GNU General Public License as published by
25
25
import eyeD3.mp3;
26
26
from frames import *;
27
27
from binfuncs import *;
 
28
import math;
28
29
 
29
30
ID3_V1_COMMENT_DESC = "ID3 v1 Comment";
30
31
 
31
32
################################################################################
32
 
class TagException:
33
 
   msg = "";
34
 
 
35
 
   def __init__(self, msg):
36
 
      self.msg = msg;
37
 
 
38
 
   def __str__(self):
39
 
      return self.msg;
 
33
class TagException(Exception):
 
34
   '''error reading tag'''
40
35
 
41
36
################################################################################
42
37
class TagHeader:
56
51
 
57
52
   # The size in the most recently parsed header.
58
53
   tagSize = 0;
59
 
   
 
54
 
60
55
   # Constructor
61
56
   def __init__(self):
62
57
      self.clear();
120
115
      TRACE_MSG("TagHeader [major]: " + str(major));
121
116
      TRACE_MSG("TagHeader [minor]: " + str(minor));
122
117
      TRACE_MSG("TagHeader [revis]: " + str(rev));
123
 
      if not (major == 2 and (minor == 3 or minor == 4)):
 
118
      if not (major == 2 and (minor >= 2 and minor <= 4)):
124
119
         raise TagException("ID3 v" + str(major) + "." + str(minor) +\
125
120
                            " is not supported.");
126
121
      # Get all the version madness in sync.
174
169
   restrictions = 0;
175
170
 
176
171
   def isUpdate(self):
177
 
      return self.flags & 0x40;
 
172
       return self.flags & 0x40;
178
173
   def hasCRC(self):
179
 
      return self.flags & 0x20;
180
 
   def hasRestrictions(self):
181
 
      return self.flags & 0x10;
 
174
       return self.flags & 0x20;
 
175
   def hasRestrictions(self, minor_version = None):
 
176
       return self.flags & 0x10;
182
177
 
183
178
   def setSizeRestrictions(self, v):
184
179
      assert(v >= 0 and v <= 3);
252
247
         return "All images are exactly 64x64 pixels, unless required "\
253
248
                "otherwise.";
254
249
 
 
250
   def _syncsafeCRC(self):
 
251
       bites = ""
 
252
       bites += chr((self.crc >> 28) & 0x7f);
 
253
       bites += chr((self.crc >> 21) & 0x7f);
 
254
       bites += chr((self.crc >> 14) & 0x7f);
 
255
       bites += chr((self.crc >>  7) & 0x7f);
 
256
       bites += chr((self.crc >>  0) & 0x7f);
 
257
       return bites;
 
258
 
 
259
 
255
260
   def render(self, header, frameData, padding = 0):
256
261
      assert(header.majorVersion == 2);
257
262
 
258
263
      data = "";
 
264
      crc = None;
259
265
      if header.minorVersion == 4:
260
266
         # Version 2.4
261
 
         crc = None;
262
267
         size = 6;
263
268
         # Extended flags.
264
269
         if self.isUpdate():
265
 
            data = "\x00";
 
270
            data += "\x00";
266
271
         if self.hasCRC():
267
272
            data += "\x05";
268
 
            self.crc = binascii.crc32(data);
269
 
            data += bin2bytes(dec2bin(self.crc, 40));
 
273
            # XXX: Using the absolute value of the CRC.  The spec is unclear
 
274
            # about the type of this data.
 
275
            self.crc = int(math.fabs(binascii.crc32(frameData +\
 
276
                                                    ("\x00" * padding))));
 
277
            crc_data = self._syncsafeCRC();
 
278
            if len(crc_data) < 5:
 
279
                crc_data = ("\x00" * (5 - len(crc_data))) + crc_data
 
280
            assert(len(crc_data) == 5)
 
281
            data += crc_data
270
282
         if self.hasRestrictions():
271
283
            data += "\x01";
272
284
            assert(len(self.restrictions) == 1);
273
285
            data += self.restrictions;
 
286
         TRACE_MSG("Rendered extended header data (%d bytes)" % len(data));
274
287
 
275
288
         # Extended header size.
276
289
         size = bin2bytes(bin2synchsafe(dec2bin(len(data) + 6, 32)))
277
290
         assert(len(size) == 4);
278
291
 
279
 
         assert(len(self.flags) == 2);
280
 
         data = size + "\x01" + self.flags + data;
 
292
         data = size + "\x01" + bin2bytes(dec2bin(self.flags)) + data;
 
293
         TRACE_MSG("Rendered extended header of size %d" % len(data));
281
294
      else:
282
295
         # Version 2.3
283
 
         crc = None;
284
 
         size = 6;
 
296
         size = 10;
285
297
         # Extended flags.
286
298
         f = [0] * 16;
287
299
         if self.hasCRC():
288
300
            f[0] = 1;
289
 
            self.crc = binascii.crc32(data);
 
301
            # XXX: Using the absolute value of the CRC.  The spec is unclear
 
302
            # about the type of this type.
 
303
            self.crc = int(math.fabs(binascii.crc32(frameData +\
 
304
                                                    ("\x00" * padding))));
290
305
            crc = bin2bytes(dec2bin(self.crc));
291
306
            assert(len(crc) == 4);
292
307
            size += 4;
301
316
         data = size + flags + paddingSize;
302
317
         if crc:
303
318
            data += crc;
304
 
      return data; 
 
319
      return data;
305
320
 
306
321
   # Only call this when you *know* there is an extened header.
307
322
   def parse(self, fp, header):
308
323
      assert(header.majorVersion == 2);
309
324
 
 
325
      TRACE_MSG("Parsing extended header @ 0x%x" % fp.tell());
310
326
      # First 4 bytes is the size of the extended header.
311
327
      data = fp.read(4);
312
328
      if header.minorVersion == 4:
313
 
         TRACE_MSG("Parsing extended header for v2.4");
314
329
         # sync-safe
315
330
         sz = bin2dec(bytes2bin(data, 7));
316
 
         TRACE_MSG("Extended header size: %d" % (sz - 4));
 
331
         TRACE_MSG("Extended header size (not including 4 byte size): %d" %\
 
332
                   (sz - 4));
317
333
         data = fp.read(sz - 4);
318
334
 
319
335
         if ord(data[0]) != 1 or (ord(data[1]) & 0x8f):
345
361
            self.restrictions = ord(data[offset]);
346
362
            offset += 1;
347
363
      else:
348
 
         TRACE_MSG("Parsing extended header for v2.3");
349
364
         # v2.3 is totally different... *sigh*
350
365
         sz = bin2dec(bytes2bin(data));
351
 
         TRACE_MSG("Extended header size: %d" % sz);
352
 
         data = fp.read(sz);
 
366
         TRACE_MSG("Extended header size (not including 4 bytes size): %d" %\
 
367
                   sz);
353
368
         tmpFlags = fp.read(2);
 
369
         # Read the padding size, but it'll be computed during the parse.
 
370
         ps = fp.read(4);
 
371
         TRACE_MSG("Extended header says there is %d bytes of padding" %\
 
372
                   bin2dec(bytes2bin(ps)));
354
373
         # Make this look like a v2.4 mask.
355
374
         self.flags = ord(tmpFlags[0]) >> 2;
356
375
         if self.hasCRC():
358
377
            crcData = fp.read(4);
359
378
            self.crc = bin2dec(bytes2bin(crcData));
360
379
            TRACE_MSG("Extended header CRC: %d" % self.crc);
361
 
         # Read the padding size, but it'll be computed during the parse.
362
 
         fp.read(4);
363
380
 
364
381
 
365
382
################################################################################
398
415
      self.header = TagHeader();
399
416
      self.frames = FrameSet(self.header);
400
417
      self.iterIndex = None;
401
 
      linkedFile = None;
402
418
 
403
419
   # Returns an read-only iterator for all frames.
404
420
   def __iter__(self):
427
443
   # Converts all ID3v1 data into ID3v2 frames internally.
428
444
   # May throw IOError, or TagException if parsing fails.
429
445
   def link(self, f, v = ID3_ANY_VERSION):
 
446
      self.linkedFile = None;
430
447
      self.clear();
431
448
 
432
449
      fileName = "";
433
450
      if isinstance(f, file):
434
451
         fileName = f.name;
435
 
      elif isinstance(f, str):
 
452
      elif isinstance(f, str) or isinstance(f, unicode):
436
453
         fileName = f;
437
454
      else:
438
 
         raise TagException("Invalid type passed to Tag.link: " + 
 
455
         raise TagException("Invalid type passed to Tag.link: " +
439
456
                            str(type(f)));
440
457
 
441
458
      if v != ID3_V1 and v != ID3_V2 and v != ID3_ANY_VERSION:
482
499
 
483
500
      self.setVersion(version);
484
501
      version = self.getVersion();
485
 
      # If v1.0 is being for explicitly then so be it, if not and there is
 
502
      if version == ID3_V2_2:
 
503
          raise TagException("Unable to write ID3 v2.2");
 
504
      # If v1.0 is being requested explicitly then so be it, if not and there is
486
505
      # a track number then bumping to v1.1 is /probably/ best.
487
506
      if self.header.majorVersion == 1 and self.header.minorVersion == 0 and\
488
507
         self.getTrackNum()[0] != None and version != ID3_V1_0:
489
508
         version = ID3_V1_1;
490
509
         self.setVersion(version);
491
 
      
 
510
 
492
511
      # If there are no frames then simply remove the current tag.
493
512
      if len(self.frames) == 0:
494
513
         self.remove(version);
520
539
      if version == ID3_CURRENT_VERSION:
521
540
         version = self.getVersion();
522
541
 
523
 
      retval = 0;   
 
542
      retval = 0;
524
543
      if version & ID3_V1 or version == ID3_ANY_VERSION:
525
544
         tagFile = file(self.linkedFile.name, "r+b");
526
545
         tagFile.seek(-128, 2);
535
554
          self.header.tagSize:
536
555
         tagFile = file(self.linkedFile.name, "r+b");
537
556
         if tagFile.read(3) == "ID3":
 
557
            # FIXME:
 
558
            print "POINT #2"
538
559
            TRACE_MSG("Removing ID3 v2.x Tag");
539
560
            tagSize = self.header.tagSize + self.header.SIZE;
540
561
            tagFile.seek(tagSize);
541
 
            data = tagFile.read();
542
 
            tagFile.seek(0);
543
 
            tagFile.write(data);
 
562
 
 
563
            # Open tmp file
 
564
            tmpName = tempfile.mktemp();
 
565
            tmpFile = file(tmpName, "w+b");
 
566
 
 
567
            # Write audio data in chunks
 
568
            self.__copyRemaining(tagFile, tmpFile);
544
569
            tagFile.truncate();
545
570
            tagFile.close();
 
571
 
 
572
            tmpFile.close();
 
573
 
 
574
            # Move tmp to orig.
 
575
            shutil.copyfile(tmpName, self.linkedFile.name);
 
576
            os.unlink(tmpName);
 
577
 
546
578
            retval |= 1;
547
579
 
548
580
      return retval;
549
581
 
550
 
 
551
 
 
552
582
   # Get artist.  There are a few frames that can contain this information,
553
583
   # and they are subtley different. 
554
584
   #   eyeD3.frames.ARTIST_FID - Lead performer(s)/Soloist(s)
572
602
         f = self.frames[fid];
573
603
         if f:
574
604
             return f[0].text;
575
 
      return u""; 
576
 
 
 
605
      return u"";
 
606
 
577
607
   def getAlbum(self):
578
608
      f = self.frames[ALBUM_FID];
579
609
      if f:
596
626
 
597
627
   def getDate(self, fid = None):
598
628
       if not fid:
599
 
           for fid in ["TDRL", "TDOR", "TDRC",
600
 
                       OBSOLETE_YEAR_FID, OBSOLETE_DATE_FID]:
 
629
           for fid in DATE_FIDS:
601
630
               if self.frames[fid]:
602
631
                   return self.frames[fid];
603
632
           return None;
609
638
           return dateFrame[0].getYear();
610
639
       else:
611
640
           return None;
612
 
            
 
641
 
613
642
   # Throws GenreException when the tag contains an unrecognized genre format.
614
643
   # Note this method returns a eyeD3.Genre object, not a raw string.
615
644
   def getGenre(self):
621
650
      else:
622
651
         return None;
623
652
 
 
653
   def _getNum(self, fid):
 
654
      tn = None
 
655
      tt = None
 
656
      f = self.frames[fid];
 
657
      if f:
 
658
         n = f[0].text.split('/')
 
659
         if len(n) == 1:
 
660
            tn = self.toInt(n[0])
 
661
         elif len(n) == 2:
 
662
            tn = self.toInt(n[0])
 
663
            tt = self.toInt(n[1])
 
664
      return (tn, tt)
 
665
 
624
666
   # Returns a tuple with the first value containing the track number and the
625
667
   # second the total number of tracks.  One or both of these values may be
626
668
   # None depending on what is available in the tag. 
627
669
   def getTrackNum(self):
628
 
      f = self.frames[TRACKNUM_FID];
629
 
      if f:
630
 
         n = string.split(f[0].text, '/');
631
 
         tn = self.toInt(n[0]);
632
 
         tt = None;
633
 
         if len(n) == 2:
634
 
            tt = self.toInt(n[1]);
635
 
         if tn != None and tt != None:
636
 
            return (tn, tt);
637
 
         elif tn != None:
638
 
            return (tn, None);
639
 
      return (None, None);
 
670
      return self._getNum(TRACKNUM_FID)
 
671
 
 
672
   # Like TrackNum, except for DiscNum--that is, position in a set. Most
 
673
   # songs won't have this or it will be 1/1.
 
674
   def getDiscNum(self):
 
675
      return self._getNum(DISCNUM_FID)
640
676
 
641
677
   # Since multiple comment frames are allowed this returns a list with 0
642
678
   # or more elements.  The elements are not the comment strings, they are
644
680
   def getComments(self):
645
681
      return self.frames[COMMENT_FID];
646
682
 
 
683
   # Since multiple lyrics frames are allowed this returns a list with 0
 
684
   # or more elements.  The elements are not the lyrics strings, they are
 
685
   # eyeD3.frames.LyricsFrame objects.
 
686
   def getLyrics(self):
 
687
      return self.frames[LYRICS_FID];
 
688
 
647
689
   # Returns a list (possibly empty) of eyeD3.frames.ImageFrame objects.
648
690
   def getImages(self):
649
691
      return self.frames[IMAGE_FID];
650
692
 
 
693
   # Returns a list (possibly empty) of eyeD3.frames.ObjectFrame objects.
 
694
   def getObjects(self):
 
695
      return self.frames[OBJECT_FID];
 
696
 
651
697
   # Returns a list (possibly empty) of eyeD3.frames.URLFrame objects.
652
698
   # Both URLFrame and UserURLFrame objects are returned.  UserURLFrames
653
699
   # add a description and encoding, and have a different frame ID.
694
740
   def setDate(self, year, month = None, dayOfMonth = None,
695
741
               hour = None, minute = None, second = None, fid = None):
696
742
      if not year and not fid:
697
 
          dateFrame = self.getDate;
698
 
          self.frames.removeFramesByID(dateFrame.id);
 
743
          dateFrames = self.getDate();
 
744
          if dateFrames:
 
745
              self.frames.removeFramesByID(dateFrames[0].header.id);
699
746
          return;
700
747
      elif not year:
701
748
          self.frames.removeFramesByID(fid);
758
805
   # second the total number of tracks.  One or both of these values may be
759
806
   # None.  If both values are None, the frame is removed.
760
807
   def setTrackNum(self, n):
 
808
      self.setNum(TRACKNUM_FID, n)
 
809
 
 
810
   def setDiscNum(self, n):
 
811
      self.setNum(DISCNUM_FID, n)
 
812
 
 
813
   def setNum(self, fid, n):
761
814
      if n[0] == None and n[1] == None:
762
 
         self.frames.removeFramesByID(TRACKNUM_FID);
 
815
         self.frames.removeFramesByID(fid);
763
816
         return;
764
817
 
765
818
      totalStr = "";
788
841
      elif trackStr and not totalStr:
789
842
         s = trackStr;
790
843
 
791
 
      self.frames.setTextFrame(TRACKNUM_FID, self.strToUnicode(s),
 
844
      self.frames.setTextFrame(fid, self.strToUnicode(s),
792
845
                               self.encoding);
793
846
 
 
847
 
794
848
   # Add a comment.  This adds a comment unless one is already present with
795
849
   # the same language and description in which case the current value is
796
850
   # either changed (cmt != "") or removed (cmt equals "" or None).
808
862
                                     self.strToUnicode(desc),
809
863
                                     lang, self.encoding);
810
864
 
 
865
   # Add lyrics.  Semantics similar to addComment
 
866
   def addLyrics(self, lyr, desc = u"", lang = DEFAULT_LANG):
 
867
      if not lyr:
 
868
         # A little more than a call to removeFramesByID is involved since we
 
869
         # need to look at more than the frame ID.
 
870
         lyrics = self.frames[LYRICS_FID];
 
871
         for l in lyrics:
 
872
            if l.lang == lang and l.description == desc:
 
873
               self.frames.remove(l);
 
874
               break;
 
875
      else:
 
876
         self.frames.setLyricsFrame(self.strToUnicode(lyr),
 
877
                                     self.strToUnicode(desc),
 
878
                                     lang, self.encoding);
 
879
 
811
880
   # Semantics similar to addComment
812
881
   def addUserTextFrame(self, desc, text):
813
882
      if not text:
823
892
   def removeComments(self):
824
893
       return self.frames.removeFramesByID(COMMENT_FID);
825
894
 
 
895
   def removeLyrics(self):
 
896
       return self.frames.removeFramesByID(LYRICS_FID);
 
897
 
826
898
   def addImage(self, type, image_file_path, desc = u""):
827
 
       image_frame = ImageFrame.create(type, image_file_path, desc);
828
 
       self.frames.addFrame(image_frame);
 
899
       if image_file_path:
 
900
           image_frame = ImageFrame.create(type, image_file_path, desc);
 
901
           self.frames.addFrame(image_frame);
 
902
       else:
 
903
           image_frames = self.frames[IMAGE_FID];
 
904
           for i in image_frames:
 
905
               if i.pictureType == type:
 
906
                   self.frames.remove(i);
 
907
                   break;
 
908
 
 
909
   def addObject(self, object_file_path, mime = "", desc = u"",
 
910
                 filename = None ):
 
911
       object_frames = self.frames[OBJECT_FID];
 
912
       for i in object_frames:
 
913
           if i.description == desc:
 
914
               self.frames.remove(i);
 
915
       if object_file_path:
 
916
           object_frame = ObjectFrame.create(object_file_path, mime, desc,
 
917
                                             filename);
 
918
           self.frames.addFrame(object_frame);
829
919
 
830
920
   def getPlayCount(self):
831
921
       if self.frames[PLAYCOUNT_FID]:
846
936
           frameHeader.id = PLAYCOUNT_FID;
847
937
           pc = PlayCountFrame(frameHeader, count = count);
848
938
           self.frames.addFrame(pc);
849
 
   
 
939
 
850
940
   def incrementPlayCount(self, n = 1):
851
941
       pc = self.getPlayCount();
852
942
       if pc != None:
867
957
      else:
868
958
         self.frames.setUniqueFileIDFrame(owner_id, id);
869
959
 
 
960
   def getBPM(self):
 
961
      bpm = self.frames[BPM_FID];
 
962
      if bpm:
 
963
          return int(bpm[0].text);
 
964
      else:
 
965
          return None;
 
966
 
 
967
   def setBPM(self, bpm):
 
968
       self.setTextFrame(BPM_FID, self.strToUnicode(str(bpm)));
 
969
 
 
970
   def getPublisher(self):
 
971
      pub = self.frames[PUBLISHER_FID];
 
972
      if pub:
 
973
          return pub[0].text or None;
 
974
 
 
975
   def setPublisher(self, p):
 
976
       self.setTextFrame(PUBLISHER_FID, self.strToUnicode(str(p)));
 
977
 
870
978
   # Test ID3 major version.
871
979
   def isV1(self):
872
980
      return self.header.majorVersion == 1;
882
990
      if v != ID3_CURRENT_VERSION:
883
991
         self.header.setVersion(v);
884
992
         self.frames.setTagHeader(self.header);
885
 
   
 
993
 
886
994
   def setTextFrame(self, fid, txt):
887
995
       if not txt:
888
996
          self.frames.removeFramesByID(fid);
889
997
       else:
890
998
          self.frames.setTextFrame(fid, self.strToUnicode(txt), self.encoding);
891
 
   
 
999
 
892
1000
   def setTextEncoding(self, enc):
893
1001
       if enc != LATIN1_ENCODING and enc != UTF_16_ENCODING and\
894
1002
          enc != UTF_16BE_ENCODING and enc != UTF_8_ENCODING:
961
1069
            cmt = cmt[0:28] + "\x00" + chr(int(track) & 0xff);
962
1070
      tag += cmt;
963
1071
 
964
 
      if not self.getGenre():
 
1072
      if not self.getGenre() or self.getGenre().getId() is None:
965
1073
         genre = 0;
966
1074
      else:
967
1075
         genre = self.getGenre().getId();
984
1092
      tagFile.write(tag);
985
1093
      tagFile.flush();
986
1094
      tagFile.close();
987
 
      
 
1095
 
988
1096
   def _fixToWidth(self, s, n):
989
1097
      retval = str(s);
990
1098
      retval = retval[0:n];
993
1101
 
994
1102
   # Returns false when an ID3 v1 tag is not present, or contains no data.
995
1103
   def __loadV1Tag(self, f):
996
 
      if isinstance(f, str):
 
1104
      if isinstance(f, str) or isinstance(f, unicode):
997
1105
         fp = file(f, "rb")
998
1106
         closeFile = 1;
999
1107
      else:
1110
1218
      rewriteFile = 0;
1111
1219
      paddingSize = 0;
1112
1220
      headerData = "";
 
1221
      extHeaderData = "";
1113
1222
 
1114
1223
      # Extended header
1115
1224
      if self.header.extended:
1119
1228
         rewriteFile = 1;
1120
1229
         paddingSize = 2048;
1121
1230
         TRACE_MSG("Rendering extended header");
1122
 
         headerData += self.extendedHeader.render(self.header, frameData,
1123
 
                                                  paddingSize);
 
1231
         extHeaderData += self.extendedHeader.render(self.header, frameData,
 
1232
                                                     paddingSize);
1124
1233
 
1125
 
      if rewriteFile or (10 + len(headerData) + len(frameData)) >= currTagSize:
 
1234
      if rewriteFile or (10 + len(headerData) + len(extHeaderData) +\
 
1235
                         len(frameData)) >= currTagSize:
1126
1236
         TRACE_MSG("File rewrite required");
1127
1237
         rewriteFile = 1;
1128
1238
         paddingSize = 2048;
1129
1239
      else:
1130
 
         paddingSize = currTagSize - (len(headerData) + len(frameData));
 
1240
         paddingSize = currTagSize - (len(headerData) + len(extHeaderData) +\
 
1241
                                      len(frameData));
1131
1242
      frameData += ("\x00" * paddingSize);
1132
1243
 
1133
1244
      # Render the tag header.
1136
1247
      headerData = self.header.render(len(frameData));
1137
1248
 
1138
1249
      # Assemble frame.
1139
 
      tagData = headerData + frameData;
 
1250
      tagData = headerData + extHeaderData + frameData;
1140
1251
 
1141
1252
      # Write the tag.
1142
1253
      if not rewriteFile:
1145
1256
         tagFile.write(tagData);
1146
1257
         tagFile.close();
1147
1258
      else:
1148
 
         # Open original
1149
 
         tagFile = file(self.linkedFile.name, "rb");
1150
 
         # Read all audio data
1151
 
         tagFile.seek(currTagSize);
1152
 
         audioData = tagFile.read();
1153
 
         tagFile.close();
1154
 
 
 
1259
         # FIXME
 
1260
         print "POINT #1"
1155
1261
         # Open tmp file
1156
1262
         tmpName = tempfile.mktemp();
1157
1263
         tmpFile = file(tmpName, "w+b");
1158
1264
         TRACE_MSG("Writing %d bytes of tag data" % len(tagData));
1159
1265
         tmpFile.write(tagData);
1160
 
         tmpFile.write(audioData);
 
1266
 
 
1267
         # Write audio data in chunks
 
1268
         tagFile = file(self.linkedFile.name, "rb");
 
1269
         tagFile.seek(currTagSize);
 
1270
         self.__copyRemaining(tagFile, tmpFile);
 
1271
 
 
1272
         tagFile.close();
1161
1273
         tmpFile.close();
1162
1274
 
1163
1275
         # Move tmp to orig.
1174
1286
   # Returns >= 0 to indicate the padding size of the read frame; -1 returned
1175
1287
   # when not tag was found.
1176
1288
   def __loadV2Tag(self, f):
1177
 
      if isinstance(f, str):
 
1289
      if isinstance(f, str) or isinstance(f, unicode):
1178
1290
         fp = file(f, "rb")
1179
1291
         closeFile = 1;
1180
1292
      else:
1196
1308
         padding = self.frames.parse(fp, self.header);
1197
1309
         TRACE_MSG("Tag contains %d bytes of padding." % padding);
1198
1310
      except FrameException, ex:
1199
 
         fp.close();
1200
 
         raise TagException(str(ex));
 
1311
         if utils.strictID3():
 
1312
            fp.close();
 
1313
            raise TagException(str(ex));
1201
1314
      except TagException:
1202
1315
         fp.close();
1203
1316
         raise;
1224
1337
         raise TagException("Invalid date field: " + fStr);
1225
1338
      return fStr;
1226
1339
 
 
1340
   def __copyRemaining(self, src_fp, dest_fp):
 
1341
       # Write audio data in chunks
 
1342
       done = False
 
1343
       amt = 1024 * 512
 
1344
       while not done:
 
1345
           data = src_fp.read(amt)
 
1346
           if data:
 
1347
               dest_fp.write(data)
 
1348
           else:
 
1349
               done = True
 
1350
           del data
 
1351
 
1227
1352
   # DEPRECATED
1228
1353
   # This method will return the first comment in the FrameSet
1229
1354
   # and not all of them.  Multiple COMM frames are common and useful.  Use
1238
1363
 
1239
1364
################################################################################
1240
1365
class GenreException(Exception):
1241
 
   msg = "";
1242
 
 
1243
 
   def __init__(self, msg):
1244
 
      self.msg = msg;
1245
 
 
1246
 
   def __str__(self):
1247
 
      return self.msg;
1248
 
 
 
1366
   '''Problem looking up genre'''
1249
1367
 
1250
1368
################################################################################
1251
1369
class Genre:
1252
1370
   id = None;
1253
1371
   name = None;
1254
1372
 
1255
 
   def __init__(self):
1256
 
      pass;
 
1373
   def __init__(self, id = None, name = None):
 
1374
      if id is not None:
 
1375
         self.setId(id);
 
1376
      elif name is not None:
 
1377
         self.setName(name);
1257
1378
 
1258
1379
   def getId(self):
1259
1380
      return self.id;
1326
1447
         except:
1327
1448
            raise GenreException("eyeD3.genres[" + str(id) + "] " +\
1328
1449
                                 "does not match " + name);
1329
 
      
 
1450
 
1330
1451
   # Parses genre information from genreStr. 
1331
1452
   # The following formats are supported:
1332
1453
   # 01, 2, 23, 125 - ID3 v1 style.
1367
1488
         (id, name) = m.groups();
1368
1489
         if len(id) != 1 and id[0] == '0':
1369
1490
            id = id[1:];
1370
 
            
 
1491
 
1371
1492
         if id and name:
1372
1493
            self.set(int(id), name.strip());
1373
1494
         else:
1376
1497
 
1377
1498
      # Non standard, but witnessed.
1378
1499
      # Match genreName alone.  e.g. Rap, Rock, blues.
1379
 
      regex = re.compile("^[A-Z 0-9+/\-&]+\00*$", re.IGNORECASE);
 
1500
      regex = re.compile("^[A-Z 0-9'+/\-&]+\00*$", re.IGNORECASE);
1380
1501
      if regex.match(genreStr):
1381
1502
         self.setName(genreStr);
1382
1503
         return;
1392
1513
      return s;
1393
1514
 
1394
1515
################################################################################
1395
 
class InvalidAudioFormatException:
1396
 
   msg = "";
1397
 
 
1398
 
   def __init__(self, msg):
1399
 
      self.msg = msg;
1400
 
 
1401
 
   def __str__(self):
1402
 
      return self.msg;
 
1516
class InvalidAudioFormatException(Exception):
 
1517
   '''Problems with audio format'''
1403
1518
 
1404
1519
################################################################################
1405
1520
class TagFile:
1406
1521
   fileName = str("");
1407
1522
   fileSize = int(0);
1408
1523
   tag      = None;
 
1524
   # Number of seconds required to play the audio file.
 
1525
   play_time = int(0);
1409
1526
 
1410
1527
   def __init__(self, fileName):
1411
1528
       self.fileName = fileName;
1418
1535
         self.fileSize = os.stat(self.fileName)[ST_SIZE];
1419
1536
      return self.fileSize;
1420
1537
 
1421
 
   def rename(self, name):
 
1538
   def rename(self, name, fsencoding):
1422
1539
       base = os.path.basename(self.fileName);
1423
1540
       base_ext = os.path.splitext(base)[1];
1424
1541
       dir = os.path.dirname(self.fileName);
1425
 
       new_name = dir + os.sep + name.encode("iso8859-1") + base_ext;
 
1542
       if not dir:
 
1543
           dir = ".";
 
1544
       new_name = dir + os.sep + name.encode(fsencoding) + base_ext;
1426
1545
       try:
1427
1546
           os.rename(self.fileName, new_name);
1428
1547
           self.fileName = new_name;
1430
1549
           raise TagException("Error renaming '%s' to '%s'" % (self.fileName,
1431
1550
                                                               new_name));
1432
1551
 
 
1552
   def getPlayTime(self):
 
1553
      return self.play_time;
 
1554
 
 
1555
   def getPlayTimeString(self):
 
1556
      total = self.getPlayTime();
 
1557
      h = total / 3600;
 
1558
      m = (total % 3600) / 60;
 
1559
      s = (total % 3600) % 60;
 
1560
      if h:
 
1561
         timeStr = "%d:%.2d:%.2d" % (h, m, s);
 
1562
      else:
 
1563
         timeStr = "%d:%.2d" % (m, s);
 
1564
      return timeStr;
 
1565
 
1433
1566
 
1434
1567
################################################################################
1435
1568
class Mp3AudioFile(TagFile):
1436
1569
   header         = mp3.Header();
1437
1570
   xingHeader     = None;
1438
1571
   invalidFileExc = InvalidAudioFormatException("File is not mp3");
1439
 
   # Number of seconds required to play the audio file.
1440
 
   playTime       = None;
1441
1572
 
1442
1573
   def __init__(self, fileName, tagVersion = ID3_ANY_VERSION):
1443
1574
      TagFile.__init__(self, fileName);
1444
1575
 
1445
 
      self.playTime = None;
1446
 
 
1447
1576
      if not isMp3File(fileName):
1448
1577
         raise self.invalidFileExc;
1449
1578
 
1450
1579
      # Parse ID3 tag.
1451
 
      f = file(fileName, "rb");
 
1580
      f = file(self.fileName, "rb");
1452
1581
      tag = Tag();
1453
1582
      hasTag = tag.link(f, tagVersion);
1454
1583
      # Find the first mp3 frame.
1461
1590
         # XXX: Note that v2.4 allows for appended tags; account for that.
1462
1591
         framePos = tag.header.SIZE + tag.header.tagSize;
1463
1592
 
1464
 
      # XXX: This algorithm is slow, if the file is long it could take awhile.
1465
1593
      f.seek(framePos);
1466
1594
      bString = f.read(4);
1467
1595
      if len(bString) < 4:
1469
1597
                                           "frame");
1470
1598
      frameHead = bin2dec(bytes2bin(bString));
1471
1599
      header = mp3.Header();
 
1600
      it_count = 0;
1472
1601
      # Keep reading until we find a valid mp3 frame header.
1473
1602
      while not header.isValid(frameHead):
 
1603
         # Originally, the search was one byte at a time.  Occasionally a tag
 
1604
         # was heavily, and incorrectly, padded or a corrupt mp3 would cause
 
1605
         # a byte by byte search to take a long time.  This algorithm speeds
 
1606
         # up this particular case.
 
1607
         if it_count > 9:
 
1608
             f.seek(f.tell() + 128);
 
1609
             bString = f.read(4);
 
1610
             if len(bString) < 4:
 
1611
                raise InvalidAudioFormatException("Unable to find a valid mp3 "\
 
1612
                                                  "frame");
 
1613
             frameHead = bin2dec(bytes2bin(bString));
 
1614
             it_count = 0;
 
1615
             continue;
 
1616
 
1474
1617
         frameHead <<= 8;
1475
1618
         bString = f.read(1);
1476
1619
         if len(bString) != 1:
1477
1620
            raise InvalidAudioFormatException("Unable to find a valid mp3 "\
1478
1621
                                              "frame");
1479
1622
         frameHead |= ord(bString[0]);
 
1623
         it_count += 1;
1480
1624
      TRACE_MSG("mp3 header %x found at position: %d (0x%x)" % \
1481
1625
                (frameHead, f.tell() - 4, f.tell() - 4));
1482
1626
 
1499
1643
      # Compute track play time.
1500
1644
      tpf = mp3.computeTimePerFrame(header);
1501
1645
      if xingHeader:
1502
 
         self.playTime = int(tpf * xingHeader.numFrames);
 
1646
         self.play_time = int(tpf * xingHeader.numFrames);
1503
1647
      else:
1504
1648
         length = self.getSize();
1505
1649
         if tag and tag.isV2():
1510
1654
               length -= 128;
1511
1655
         elif tag and tag.isV1():
1512
1656
            length -= 128;
1513
 
         self.playTime = int((length / header.frameLength) * tpf);    
 
1657
         self.play_time = int((length / header.frameLength) * tpf);
1514
1658
 
1515
1659
      self.header = header;
1516
1660
      self.xingHeader = xingHeader;
1517
1661
      self.tag = tag;
1518
1662
      f.close();
1519
1663
 
1520
 
   def getPlayTime(self):
1521
 
      return self.playTime;
1522
 
 
1523
 
   def getPlayTimeString(self):
1524
 
      total = self.getPlayTime();
1525
 
      h = total / 3600;
1526
 
      m = (total % 3600) / 60;
1527
 
      s = (total % 3600) % 60;
1528
 
      if h:
1529
 
         timeStr = "%d:%.2d:%.2d" % (h, m, s);
1530
 
      else:
1531
 
         timeStr = "%d:%.2d" % (m, s);
1532
 
      return timeStr;
1533
 
 
1534
1664
   # Returns a tuple.  The first value is a boolean which if true means the
1535
1665
   # bit rate returned in the second value is variable.
1536
1666
   def getBitRate(self):
1754
1884
      while count < 256:
1755
1885
         self.append("Unknown");
1756
1886
         count += 1;
1757
 
      
 
1887
 
1758
1888
      for index in range(len(self)):
1759
1889
         if self[index]:
1760
1890
            self.reverseDict[string.lower(self[index])] = index
1764
1894
   tagSize = 0;  # This includes the padding byte count.
1765
1895
 
1766
1896
   def __init__(self, fileName):
1767
 
      self.name = fileName;
 
1897
       if isinstance(fileName, str):
 
1898
           try:
 
1899
               self.name = unicode(fileName, sys.getfilesystemencoding());
 
1900
           except:
 
1901
               # Work around the local encoding not matching that of a mounted
 
1902
               # filesystem
 
1903
               self.name = fileName
 
1904
       else:
 
1905
           self.name = fileName;
1768
1906
 
1769
1907
def tagToUserTune(tag):
1770
1908
    audio_file = None;
1771
1909
    if isinstance(tag, Mp3AudioFile):
1772
1910
        audio_file = tag;
1773
1911
        tag = audio_file.getTag();
1774
 
        
1775
 
    tune =  "<tune xmlns='http://jabber.org/protocol/tune'>\n";
 
1912
 
 
1913
    tune =  u"<tune xmlns='http://jabber.org/protocol/tune'>\n";
1776
1914
    if tag.getArtist():
1777
1915
        tune += "  <artist>" + tag.getArtist() + "</artist>\n";
1778
1916
    if tag.getTitle():
1779
1917
        tune += "  <title>" + tag.getTitle() + "</title>\n";
1780
1918
    if tag.getAlbum():
1781
1919
        tune += "  <source>" + tag.getAlbum() + "</source>\n";
1782
 
    tune += "  <track>" + "file://" + os.path.abspath(tag.linkedFile.name) +\
 
1920
    tune += "  <track>" +\
 
1921
            "file://" + unicode(os.path.abspath(tag.linkedFile.name)) +\
1783
1922
            "</track>\n";
1784
1923
    if audio_file:
1785
 
        tune += "  <length>" + str(audio_file.getPlayTime()) + "</length>\n";
 
1924
        tune += "  <length>" + unicode(audio_file.getPlayTime()) +\
 
1925
                "</length>\n";
1786
1926
    tune += "</tune>\n";
1787
1927
    return tune;
1788
1928