3
# mutagengui.py: GTK based file tagger.
4
# Copyright (C) 2009 Stephen Fairchild (s-fairchild@users.sourceforge.net)
6
# This program is free software: you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation, either version 2 of the License, or
9
# (at your option) any later version.
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with this program in the file entitled COPYING.
18
# If not, see <http://www.gnu.org/licenses/>.
21
__all__ = ['MutagenGUI']
34
import mutagen.id3 as id3
36
from idjc_config import *
37
from IDJCfree import *
38
from ln_text import ln
39
from mutagen.mp3 import MP3
40
from mutagen.apev2 import APEv2, APETextValue
41
from mutagen.musepack import Musepack
42
from mutagen.monkeysaudio import MonkeysAudio
43
from mutagen.asf import ASF, ASFUnicodeAttribute
46
"""Dummy tooltips setter."""
51
class LeftLabel(gtk.HBox):
52
"""Use in place of gtk.Label where left justification is needed."""
54
def __init__(self, text):
55
gtk.HBox.__init__(self)
56
self.label = gtk.Label(text)
57
self.pack_start(self.label, False, False, 0)
60
class RightLabel(gtk.HBox):
61
"""Use in place of gtk.Label where right justification is needed."""
63
def __init__(self, text):
64
gtk.HBox.__init__(self)
65
self.pack_end(gtk.Label(text), False, False, 0)
68
class FreeTagFrame(gtk.Frame):
70
gtk.Frame.__init__(self)
71
sw = gtk.ScrolledWindow()
72
sw.set_border_width(5)
73
sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
76
self.tb = gtk.TextBuffer()
77
tv = gtk.TextView(self.tb)
78
tv.set_wrap_mode(gtk.WRAP_CHAR)
79
tv.modify_font(pango.FontDescription('sans 12'))
84
class MutagenTagger(gtk.VBox):
85
"""Base class for ID3Tagger and NativeTagger."""
87
def __init__(self, pathname):
88
gtk.VBox.__init__(self)
89
self.pathname = pathname
91
class WMATagger(MutagenTagger):
92
"""Handles tagging of WMA files"""
94
primary_data = ("Title", "Author")
95
secondaries = ("WM/AlbumTitle", "WM/AlbumArtist", "WM/Year", "WM/Genre")
98
"""Updates the tag with the GUI data."""
101
tb = self.tag_frame.tb
103
for key in self.text_set:
109
for each in self.primary_line:
110
val = each[1].get_text().strip()
119
lines = tb.get_text(tb.get_start_iter(), tb.get_end_iter()).splitlines()
122
key, val = line.split("=", 1)
130
tag[key] += [ASFUnicodeAttribute(val.decode("utf-8"))]
131
except (KeyError, AttributeError):
133
tag[key] = [ASFUnicodeAttribute(val.decode("utf-8"))]
135
print "Unacceptable key", key
139
"""(re)Writes the tag data to the GUI."""
143
for each in self.primary_line:
149
each[1].set_text("/".join(unicode(y) for y in data))
153
for key in self.secondaries:
154
values = tag.get(key, [ASFUnicodeAttribute("")])
156
additional.append(key.encode("utf-8") + "=" + unicode(val).encode("utf-8"))
158
for key in self.text_set:
159
if key not in self.primary_data and key not in self.secondaries:
162
additional.append(key.encode("utf-8") + "=" + unicode(val).encode("utf-8"))
164
self.tag_frame.tb.set_text("\n".join(additional))
166
def __init__(self, pathname):
167
MutagenTagger.__init__(self, pathname)
169
self.tag = mutagen.asf.ASF(pathname)
170
if not isinstance(self.tag, mutagen.asf.ASF):
171
raise mutagen.asf.error
172
except mutagen.asf.error:
173
print "Not a real wma/asf file apparently."
178
hbox.set_border_width(5)
180
self.pack_start(hbox, False, False, 0)
181
vbox_text = gtk.VBox()
182
hbox.pack_start(vbox_text, False, False, 0)
183
vbox_entry = gtk.VBox()
184
hbox.pack_start(vbox_entry, True, True, 0)
186
self.primary_line = []
187
for text, entry in ((x, gtk.Entry()) for x in self.primary_data):
188
self.primary_line.append((text, entry))
189
vbox_text.add(LeftLabel(text))
190
vbox_entry.add(entry)
193
self.tag_frame = FreeTagFrame()
194
self.tag_frame.set_border_width(5)
195
self.add(self.tag_frame)
196
self.tag_frame.show()
200
for key, val in self.tag.iteritems():
201
if key not in self.primary_line and all(isinstance(v, (ASFUnicodeAttribute, unicode)) for v in val):
202
self.text_set.append(key)
205
class ID3Tagger(MutagenTagger):
206
"""ID3 tagging with Mutagen."""
208
primary_data = (("TIT2", ln.id3title), ("TPE1", ln.id3artist),
209
("TALB", ln.id3album), ("TRCK", ln.id3track),
210
("TCON", ln.id3genre), ("TDRC", ln.id3recorddate))
213
"""Updates the tag with the GUI data."""
217
# Remove all text tags.
218
for fid in tag.iterkeys():
222
# Add the primary tags.
223
for fid, entry in self.primary_line:
224
text = entry.get_text().strip()
226
frame = getattr(id3, fid)
227
tag[fid] = frame(3, [text])
229
# Add the freeform text tags.
230
tb = self.tag_frame.tb
231
lines = tb.get_text(tb.get_start_iter(), tb.get_end_iter()).splitlines()
235
fid, val = line.split(":", 1)
241
val = val.strip().decode("utf-8")
244
frame = id3.Frames[fid]
248
if not issubclass(frame, id3.TextFrame):
251
if frame is id3.TXXX:
253
key, val = val.split(u"=", 1)
258
f = frame(3, key.strip(), [val.strip()])
263
val_list = tag[fid].text
265
tag[fid] = frame(3, [val])
272
"""(re)Writes the tag data to the GUI."""
277
for fid, entry in self.primary_line:
279
frame = self.tag[fid]
282
entry.set_text(frame.text[0])
284
# Handle occurrence of ID3Timestamp.
285
entry.set_text(str(frame.text[0]))
286
for each in frame.text[1:]:
287
additional.append(fid + ":" + each.encode("utf-8"))
293
for fid, frame in self.tag.iteritems():
294
if fid[0] == "T" and fid not in done:
295
sep = "=" if fid.startswith("TXXX:") else ":"
296
for text in frame.text:
297
additional.append(fid + sep + text.encode("utf-8"))
299
self.tag_frame.tb.set_text("\n".join(additional))
301
def __init__(self, pathname, force=False):
302
MutagenTagger.__init__(self, pathname)
305
self.tag = mutagen.File(pathname)
306
if not isinstance(self.tag, MP3):
307
raise mutagen.mp3.error
308
except mutagen.mp3.error:
309
print "Not a real mp3 file apparently."
314
print "Added ID3 tags to", pathname
315
except mutagen.id3.error:
316
print "Existing ID3 tags found."
319
# Obtain ID3 tags from a non mp3 file.
320
self.tag = mutagen.id3.ID3(pathname)
321
except mutagen.id3.error:
326
hbox.set_border_width(5)
328
self.pack_start(hbox, False, False, 0)
329
vbox_frame = gtk.VBox()
330
hbox.pack_start(vbox_frame, False, False, 0)
331
vbox_text = gtk.VBox()
332
hbox.pack_start(vbox_text, False, False, 0)
333
vbox_entry = gtk.VBox()
334
hbox.pack_start(vbox_entry, True, True, 0)
336
self.primary_line = []
337
for frame, text, entry in ((x, y, gtk.Entry()) for x, y in self.primary_data):
338
self.primary_line.append((frame, entry))
339
vbox_frame.add(LeftLabel(frame))
340
vbox_text.add(RightLabel(text))
341
vbox_entry.add(entry)
344
self.tag_frame = FreeTagFrame()
345
set_tip(self.tag_frame, ln.id3freeform)
346
self.tag_frame.set_border_width(5)
347
self.tag_frame.set_label(ln.id3textframes)
348
self.add(self.tag_frame)
349
self.tag_frame.show()
352
class MP4Tagger(MutagenTagger):
353
"""MP4 tagging with Mutagen."""
355
primary_data = (("\xa9nam", ln.mp4title), ("\xa9ART", ln.mp4artist),
356
("\xa9alb", ln.mp4album), ("trkn", ln.mp4track),
357
("\xa9gen", ln.mp4genre), ("\xa9day", ln.mp4year))
360
"""Updates the tag with the GUI data."""
363
for fid, entry in self.primary_line:
364
text = entry.get_text().strip()
366
mo1 = re.search("\d+", text)
368
track = int(text[mo1.start():mo1.end()])
369
except AttributeError:
372
text = text[mo1.end():]
373
mo2 = re.search("\d+", text)
375
total = int(text[mo2.start():mo2.end()])
376
except AttributeError:
377
new_val = [(track, 0)]
379
new_val = [(track, total)]
381
new_val = [text] if text else None
383
if new_val is not None:
394
"""(re)Writes the tag data to the GUI."""
398
for fid, entry in self.primary_line:
400
frame = self.tag[fid][0]
406
entry.set_text("%d/%d" % frame)
408
entry.set_text(str(frame[0]))
410
entry.set_text(frame)
412
def __init__(self, pathname):
413
MutagenTagger.__init__(self, pathname)
415
self.tag = mutagen.mp4.MP4(pathname)
416
if not isinstance(self.tag, mutagen.mp4.MP4):
417
raise mutagen.mp4.error
418
except mutagen.mp4.error:
419
print "Not a real mp4 file apparently."
424
hbox.set_border_width(5)
426
self.pack_start(hbox, False, False, 0)
427
vbox_text = gtk.VBox()
428
hbox.pack_start(vbox_text, False, False, 0)
429
vbox_entry = gtk.VBox()
430
hbox.pack_start(vbox_entry, True, True, 0)
432
self.primary_line = []
433
for frame, text, entry in ((x, y, gtk.Entry()) for x, y in self.primary_data):
434
self.primary_line.append((frame, entry))
435
vbox_text.add(LeftLabel(text))
436
vbox_entry.add(entry)
440
class NativeTagger(MutagenTagger):
441
"""Native format tagging with Mutagen. Mostly FLAC and Ogg."""
443
blacklist = "coverart", "metadata_block_picture"
446
"""Updates the tag with the GUI data."""
450
for key in tag.iterkeys():
451
if key not in self.blacklist:
454
tb = self.tag_frame.tb
455
lines = tb.get_text(tb.get_start_iter(), tb.get_end_iter()).splitlines()
459
key, val = line.split("=", 1)
465
if key not in self.blacklist and val:
467
tag[key] += [val.decode("utf-8")]
468
except (KeyError, AttributeError):
470
tag[key] = [val.decode("utf-8")]
472
print "Unacceptable key", key
477
"""(re)Writes the tag data to the GUI."""
481
primaries = "title", "artist", "author", "album",\
482
"tracknumber", "tracktotal", "genre", "date"
484
for key in primaries:
488
lines.append(key + "=")
491
lines.append(key + "=" + val.encode("utf-8"))
493
for key, values in tag.iteritems():
494
if key not in primaries and key not in self.blacklist:
496
lines.append(key + "=" + val.encode("utf-8"))
498
self.tag_frame.tb.set_text("\n".join(lines))
500
def __init__(self, pathname, ext):
501
MutagenTagger.__init__(self, pathname)
502
self.tag = mutagen.File(pathname)
503
if isinstance(self.tag, (MP3, APEv2)):
504
# MP3 and APEv2 have their own specialised tagger.
508
self.tag_frame = FreeTagFrame()
509
self.add(self.tag_frame)
510
self.tag_frame.show()
513
class ApeTagger(MutagenTagger):
514
"""APEv2 tagging with Mutagen."""
516
opener = {"ape": MonkeysAudio, "mpc": Musepack }
519
"""Updates the tag with the GUI data."""
523
for key, values in tag.iteritems():
524
if isinstance(values, APETextValue):
527
tb = self.tag_frame.tb
528
lines = tb.get_text(tb.get_start_iter(), tb.get_end_iter()).splitlines()
532
key, val = line.split("=", 1)
540
tag[key].value += "\0" + val.decode("utf-8")
541
except (KeyError, AttributeError):
543
tag[key] = APETextValue(val.decode("utf-8"), 0)
545
print "Unacceptable key", key
550
"""(re)Writes the tag data to the GUI."""
554
primaries = "TITLE", "ARTIST", "AUTHOR", "ALBUM",\
555
"TRACKNUMBER", "TRACKTOTAL", "GENRE", "DATE"
557
for key in primaries:
561
lines.append(key + "=")
564
lines.append(key + "=" + val.encode("utf-8"))
566
for key, values in tag.iteritems():
567
if key not in primaries and isinstance(values, APETextValue):
569
lines.append(key + "=" + val.encode("utf-8"))
571
self.tag_frame.tb.set_text("\n".join(lines))
573
def __init__(self, pathname, extension):
574
MutagenTagger.__init__(self, pathname)
577
self.tag = self.opener[extension](pathname)
580
self.tag = APEv2(pathname)
582
print "ape tag not found"
586
print "ape tag found on non-native format"
588
print "failed to create tagger for native format"
595
print "ape tag found on native format"
597
print "no existing ape tags found"
599
self.tag_frame = FreeTagFrame()
600
self.add(self.tag_frame)
601
self.tag_frame.show()
605
ext2name = { "mp3": "ID3", "mp4": "MP4", "m4a": "MP4", "spx": "Speex",
606
"flac": "FLAC", "ogg": "Ogg Vorbis", "oga": "XIPH Ogg audio",
607
"m4b": "MP4", "m4p": "MP4", "wma": "Windows Media Audio" }
609
def destroy_and_quit(self, widget, data = None):
613
def update_playlists(self, pathname, idjcroot):
614
newplaylistdata = idjcroot.player_left.get_media_metadata(pathname)
615
idjcroot.player_left.update_playlist(newplaylistdata)
616
idjcroot.player_right.update_playlist(newplaylistdata)
619
def is_supported(pathname):
620
supported = [ "mp3", "ogg", "oga" ]
621
if avcodec and avformat:
622
supported += ["mp4", "m4a", "m4b", "m4p", "ape", "mpc", "wma"]
624
supported.append("flac")
626
supported.append("spx")
627
extension = os.path.splitext(pathname)[1][1:].lower()
628
if supported.count(extension) != 1:
630
print "File type", extension, "is not supported for tagging"
635
def __init__(self, pathname, encoding, idjcroot = None):
637
print "Tagger not supplied any pathname."
640
extension = self.is_supported(pathname)
641
if extension == False:
642
print "Tagger file extension", extension, "not supported."
647
set_tip = idjcroot.tooltips.set_tip
649
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
650
if idjcroot is not None:
651
idjcroot.window_group.add_window(self.window)
652
self.window.set_size_request(550, 450)
653
self.window.set_title(ln.tagger_window_title)
654
self.window.set_destroy_with_parent(True)
655
self.window.set_border_width(9)
656
self.window.set_resizable(True)
657
self.window.set_icon_from_file(pkgdatadir + "icon" + gfext)
659
self.window.connect("destroy", self.destroy_and_quit)
661
self.window.add(vbox)
665
label.set_markup(u"<b>" + ln.tagger_filename + u" " + rich_safe(unicode(os.path.split(pathname)[1], encoding).encode("utf-8", "replace")) + u"</b>")
667
label.set_markup(u"<b>" + ln.tagger_filename + u" " + rich_safe(unicode(os.path.split(pathname)[1], "latin1").encode("utf-8", "replace")) + u"</b>")
668
vbox.pack_start(label, False, False, 6)
672
hbox.set_border_width(2)
673
apply_button = gtk.Button(None, gtk.STOCK_APPLY)
674
if idjcroot is not None:
675
apply_button.connect_object_after("clicked", self.update_playlists, pathname, idjcroot)
676
hbox.pack_end(apply_button, False, False, 0)
678
close_button = gtk.Button(None, gtk.STOCK_CLOSE)
679
close_button.connect_object("clicked", gtk.Window.destroy, self.window)
680
hbox.pack_end(close_button, False, False, 10)
682
reload_button = gtk.Button(None, gtk.STOCK_REVERT_TO_SAVED)
683
hbox.pack_start(reload_button, False, False, 10)
685
vbox.pack_end(hbox, False, False, 0)
688
vbox.pack_end(hbox, False, False, 2)
691
notebook = gtk.Notebook()
692
notebook.set_border_width(2)
693
vbox.pack_start(notebook, True, True, 0)
696
self.ape = ApeTagger(pathname, extension)
698
if extension == "mp3":
699
self.id3 = ID3Tagger(pathname, True)
702
self.id3 = ID3Tagger(pathname, False)
703
if extension in ("mp4", "m4a", "m4b", "m4p"):
704
self.native = MP4Tagger(pathname)
705
elif extension == "wma":
706
self.native = WMATagger(pathname)
707
elif extension in ("ape", "mpc"):
708
# APE tags are native to this format.
711
self.native = NativeTagger(pathname, ext=extension)
713
if self.id3 is not None and self.id3.tag is not None:
714
reload_button.connect("clicked", lambda x: self.id3.load_tag())
715
apply_button.connect("clicked", lambda x: self.id3.save_tag())
716
label = gtk.Label("ID3")
717
notebook.append_page(self.id3, label)
720
if self.ape is not None and self.ape.tag is not None:
721
reload_button.connect("clicked", lambda x: self.ape.load_tag())
722
apply_button.connect("clicked", lambda x: self.ape.save_tag())
723
label = gtk.Label("APE v2")
724
notebook.append_page(self.ape, label)
727
if self.native is not None and self.native.tag is not None:
728
reload_button.connect("clicked", lambda x: self.native.load_tag())
729
apply_button.connect("clicked", lambda x: self.native.save_tag())
730
label = gtk.Label(ln.native + " (" + self.ext2name[extension] + ")")
731
notebook.append_page(self.native, label)
734
reload_button.clicked()
736
apply_button.connect_object_after("clicked", gtk.Window.destroy, self.window)