3
# IDJCmultitagger.py: GTK based id3 and vorbis tagger for mp3/ogg/flac files.
4
# Copyright (C) 2006 Stephen Fairchild
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; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
22
import gtk, os, eyeD3, sys
23
from IDJCfree import *
24
from idjc_config import *
25
from langheader import *
27
class mp4tagger(gtk.VBox):
28
def load_tag(self, pathname):
29
self.idjc.mixer_write("MP4P=%s\nACTN=mp4readtagrequest\nend\n" % pathname, True)
32
line = self.idjc.mixer_read()
34
print "mixer crashed during mp4 tag read operation"
36
if line == "idjcmixer: mp4fileinfo Not Valid\n":
38
if line == "idjcmixer: mp4tagread end\n":
40
if line.startswith("idjcmixer: mp4tagread "):
42
kvplist = self.make_pairs(data)
43
recommended = [ u"artist", u"title", u"track", u"totaltracks", u"album", u"genre", u"date", u"comment" ]
45
for recommend in recommended:
46
for key, value in kvplist:
50
precede = precede + recommend + u"=\n"
51
self.textbuffer.set_text(precede + data)
53
def save_tag(self, pathname):
54
kvp = self.make_pairs()
56
self.idjc.mixer_write("MP4T=\n")
57
for key, value in kvp:
58
self.idjc.mixer_write("+MP4T=" + key + "=" + value + "\n")
59
self.idjc.mixer_write("MP4P=" + pathname + "\n")
60
self.idjc.mixer_write("ACTN=mp4writetagrequest\nend\n", True)
63
def make_pairs(self, data = None): # Returns a list of tuples of key value pairs
64
kvp = [ ] # Any keys containing a space are rejected
66
textiter = self.textbuffer.get_start_iter()
67
data = textiter.get_text(self.textbuffer.get_end_iter())
68
data = data.splitlines()
71
key, value = line.split("=", 1)
74
if key != "" and value != "" and key.count(" ") == 0:
75
kvp.append((key.strip(), value.strip()))
78
def __init__(self, pathname, idjcroot):
80
gtk.VBox.__init__(self)
81
scrolled_window = gtk.ScrolledWindow()
82
scrolled_window.set_size_request(350, 250)
83
scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
84
textview = gtk.TextView()
85
self.textbuffer = textview.get_buffer()
86
scrolled_window.add(textview)
88
gtk.VBox.add(self, scrolled_window)
89
scrolled_window.show()
90
textview.set_editable(True)
91
textview.set_cursor_visible(True)
92
textview.set_wrap_mode(gtk.WRAP_NONE)
93
textview.set_justification(gtk.JUSTIFY_LEFT)
95
class vorbistagger(gtk.VBox):
96
def load_ogg_tag(self, pathname):
97
self.vc = os.getenv("vorbiscomment")
98
if self.vc == "" or self.vc == None:
99
self.vc = "vorbiscomment" # For standalone version
100
if self.vc != "missing":
102
(write, read) = os.popen2([ self.vc, "--raw", pathname ])
107
print "Problem reading in file", pathname
111
def load_flac_tag(self, pathname):
112
self.mf = os.getenv("metaflac")
113
if self.mf == "" or self.mf == None:
114
self.mf = "metaflac" # For standalone version
115
if self.mf != "missing":
117
(write, read) = os.popen2([ self.mf, "--no-utf8-convert", "--export-tags-to=-" , pathname ])
122
print "Problem reading in file", pathname
126
def load_tag(self, data):
127
# Have some fun here by adding empty keys that are recommended like ALBUM=
128
# whenever none is present in the tag. It saves the user from having to type them.
129
kvplist = self.make_pairs(data)
130
recommended = [ u"ARTIST", u"TITLE", u"TRACKNUMBER", u"ALBUM", u"GENRE", u"DATE" ]
132
for recommend in recommended:
133
for key, value in kvplist:
137
precede = precede + recommend + u"=\n"
138
self.textbuffer.set_text(precede + data)
140
def save_ogg_tag(self, pathname):
141
if self.vc != "missing":
142
kvp = self.make_pairs()
144
for key, value in kvp:
145
text = text + key + "=" + value + "\n"
146
write, read = os.popen2([self.vc, "--raw", "-w", pathname])
154
print "Ogg tag saved"
156
def save_flac_tag(self, pathname):
157
if self.mf != "missing":
158
kvp = self.make_pairs()
160
for key, value in kvp:
161
text = text + key + "=" + value + "\n"
162
write, read = os.popen2(["metaflac", "--no-utf8-convert", "--remove-all-tags", "--import-tags-from=-", pathname])
170
print "Flac tag saved"
172
def make_pairs(self, data = None): # Returns a list of tuples of key value pairs
173
kvp = [ ] # Any keys containing a space are rejected
175
textiter = self.textbuffer.get_start_iter()
176
data = textiter.get_text(self.textbuffer.get_end_iter())
177
data = data.splitlines()
180
key, value = line.split("=", 1)
182
value = value.strip()
183
if key != "" and value != "" and key.count(" ") == 0:
184
kvp.append((key.strip(), value.strip()))
187
def __init__(self, pathname):
188
gtk.VBox.__init__(self)
189
scrolled_window = gtk.ScrolledWindow()
190
scrolled_window.set_size_request(350, 250)
191
scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
192
textview = gtk.TextView()
193
self.textbuffer = textview.get_buffer()
194
scrolled_window.add(textview)
196
gtk.VBox.add(self, scrolled_window)
197
scrolled_window.show()
198
textview.set_editable(True)
199
textview.set_cursor_visible(True)
200
textview.set_wrap_mode(gtk.WRAP_NONE)
201
textview.set_justification(gtk.JUSTIFY_LEFT)
203
class id3tagger(gtk.VBox):
204
def textfilter(self, text):
207
while text != "" and text[-1] == "\x00":
209
return unicode(text.strip())
211
def cb_genre(self, combo, textbox):
212
if combo.get_active() != 255:
213
newtext = eyeD3.genres[combo.get_active()]
214
textbox.set_text(newtext)
215
if newtext == "Unknown":
216
combo.set_active(255)
218
def cb_genre_unpress(self, textbox, event, combo):
219
lt = textbox.get_text().lstrip()
221
lt = lt.lower().strip()
222
cursor_pos = textbox.get_position()
224
combo.set_active(255)
227
for each in eyeD3.genres:
228
if each.lower() == lt:
229
combo.set_active(value)
233
combo.set_active(255)
234
textbox.set_position(cursor_pos)
237
def load_tag(self, pathname, extension):
238
self.tag = eyeD3.Tag()
240
if True or extension == "mp3":
242
self.tagv2found = self.tag.link(pathname, eyeD3.ID3_V2)
247
if self.tagv2found == 0:
249
self.tagv1found = self.tag.link(pathname, eyeD3.ID3_V1)
250
except eyeD3.tag.TagException:
252
self.tag.link(pathname, eyeD3.ID3_V2) # Use a blank v2 tag as the basis
253
# Now we can load up the fields from what is in the tag.
254
self.title.set_text(self.textfilter(self.tag.getTitle()))
255
self.artist.set_text(self.textfilter(self.tag.getArtist()))
256
self.album.set_text(self.textfilter(self.tag.getAlbum()))
257
self.year.set_text(self.textfilter(self.tag.getYear()))
258
genre = self.tag.getGenre()
259
if genre == None or genre.id == None or genre.id == 255:
261
self.genre_text.set_text("Unknown")
263
genre_value = genre.id
264
self.std_genre.set_active(genre_value)
265
track = self.tag.getTrackNum()
266
if type(track[0]) is int:
267
tracktext = str(track[0])
268
if type(track[1]) is int and track[1] > 0:
269
tracktext = tracktext + "/" + str(track[1])
270
self.track.set_text(tracktext)
271
comments = self.tag.getComments()
273
for self.ourcomment in comments: # Show the idjc comment in preference to the others
274
if self.ourcomment.description == self.idjc_comment_desc:
277
self.ourcomment = comments[0]
278
self.comment.set_text(self.textfilter(self.ourcomment.comment))
280
self.ourcomment = None
282
def save_tag(self, pathname):
283
self.tag.setVersion(eyeD3.ID3_V2_4)
284
self.tag.setTextEncoding(eyeD3.UTF_8_ENCODING)
285
self.tag.setArtist(self.textfilter(self.artist.get_text()))
286
self.tag.setTitle(self.textfilter(self.title.get_text()))
287
self.tag.setAlbum(self.textfilter(self.album.get_text()))
288
trackstring = self.track.get_text().split("/")
289
if len(trackstring) >= 1 and len(trackstring) <= 2:
291
tracknum = int(trackstring[0].strip())
294
if len(trackstring) == 2:
295
tracktotal = int(trackstring[1].strip())
301
print "Bad entry for track"
302
self.tag.setTrackNum([ None, None ])
304
self.tag.setTrackNum([ tracknum, tracktotal])
305
self.tag.frames.removeFramesByID("TYER")
307
datevalue = int(self.textfilter(self.year.get_text()))
308
if datevalue >= 1000 and datevalue <= 9999:
309
self.tag.setDate(int(self.textfilter(self.year.get_text())))
313
print "Year field will be left blank"
314
self.tag.setGenre(self.std_genre.get_active())
315
self.tag.addComment(self.textfilter(self.comment.get_text()), self.idjc_comment_desc, self.idjc_comment_lang)
317
self.tag.update(eyeD3.ID3_V1_1)
318
except UnicodeEncodeError:
319
print "ID3 v1.1 tag was not written because of an illegal character in one of the fields (not a member of the ISO-8859-1 character set)"
321
print "Wrote ID3 v1.1 tag."
322
if True or os.path.splitext(pathname)[1] == ".mp3":
323
self.tag.update(eyeD3.ID3_V2_4)
324
print "Wrote ID3 v2.4 tag."
326
print "ID3 v2.4 tag not written for non-mp3 files."
328
def __init__(self, pathname):
329
gtk.VBox.__init__(self)
330
self.idjc_comment_desc = "idjc-comment"
331
self.idjc_comment_lang = "XXX"
333
frame = gtk.Frame(" " + standard_tags_text + " ")
334
frame.set_border_width(6)
335
gtk.VBox.pack_start(self, frame, False, False, 0)
337
sthbox.set_spacing(2)
338
sthbox.set_border_width(8)
343
sthbox.pack_start(stlvbox, False, False, 1)
346
sthbox.pack_start(strvbox, True, True, 1)
349
title = gtk.Label(tag_title_text)
353
self.title = gtk.Entry()
354
self.title.set_width_chars(30)
355
strvbox.add(self.title)
358
artist = gtk.Label(tag_artist_text)
362
self.artist = gtk.Entry()
363
strvbox.add(self.artist)
366
album = gtk.Label(tag_album_text)
370
self.album = gtk.Entry()
371
strvbox.add(self.album)
374
track = gtk.Label(tag_track_text)
378
self.track = gtk.Entry()
379
strvbox.add(self.track)
382
year = gtk.Label(tag_year_text)
386
self.year = gtk.Entry()
387
strvbox.add(self.year)
390
genre = gtk.Label(tag_genre_text)
394
genre_box = gtk.HBox()
395
self.genre_text = gtk.Entry()
396
genre_box.pack_start(self.genre_text, True, True, 0)
397
self.genre_text.show()
398
self.genre_text.set_text("Unknown")
399
self.std_genre = gtk.combo_box_new_text()
400
self.std_genre.set_wrap_width(5)
402
for each in eyeD3.genres:
403
self.std_genre.append_text(each + " (" + str(value) + ")")
405
self.std_genre.connect("changed", self.cb_genre, self.genre_text)
406
self.genre_text.connect("key_release_event", self.cb_genre_unpress, self.std_genre)
407
self.std_genre.set_active(255)
408
genre_box.pack_end(self.std_genre, False, False, 0)
409
self.std_genre.show()
410
strvbox.add(genre_box)
413
comment = gtk.Label(tag_comment_text)
417
self.comment = gtk.Entry()
418
strvbox.add(self.comment)
423
def destroy_and_quit(self, widget, data = None):
427
def update_playlists(self, pathname, idjcroot):
428
newplaylistdata = idjcroot.player_left.get_media_metadata(pathname)
429
idjcroot.player_left.update_playlist(newplaylistdata)
430
idjcroot.player_right.update_playlist(newplaylistdata)
432
def is_supported(self, pathname):
433
supported = [ "mp3", "ogg", "flac" ]
436
supported.append("mp4")
437
supported.append("m4a")
440
extension = os.path.splitext(pathname)[1][1:].lower()
441
if supported.count(extension) != 1:
442
print "File type", extension, "is not supported."
447
def __init__(self, pathname = None, encoding = "latin1", idjcroot = None):
450
extension = self.is_supported(pathname)
451
if extension == False:
454
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
455
if idjcroot is not None:
456
idjcroot.window_group.add_window(self.window)
457
self.window.set_title(tagger_window_title_text)
458
self.window.set_destroy_with_parent(True)
459
self.window.set_border_width(9)
460
self.window.set_resizable(True)
461
self.window.set_icon_from_file(pkgdatadir + "icon" + gfext)
463
self.window.connect("destroy", self.destroy_and_quit)
465
self.window.add(vbox)
469
label.set_markup(u"<b>" + tagger_filename_text + u" " + rich_safe(unicode(os.path.split(pathname)[1], encoding).encode("utf-8", "replace")) + u"</b>")
471
label.set_markup(u"<b>" + tagger_filename_text + u" " + rich_safe(unicode(os.path.split(pathname)[1], "latin1").encode("utf-8", "replace")) + u"</b>")
472
vbox.pack_start(label, False, False, 6)
476
hbox.set_border_width(2)
477
apply_button = gtk.Button(None, gtk.STOCK_APPLY)
478
if idjcroot is not None:
479
apply_button.connect_object_after("clicked", self.update_playlists, pathname, idjcroot)
480
hbox.pack_end(apply_button, False, False, 0)
482
close_button = gtk.Button(None, gtk.STOCK_CLOSE)
483
close_button.connect_object("clicked", gtk.Window.destroy, self.window)
484
hbox.pack_end(close_button, False, False, 10)
486
reload_button = gtk.Button(None, gtk.STOCK_REVERT_TO_SAVED)
487
hbox.pack_start(reload_button, False, False, 10)
489
vbox.pack_end(hbox, False, False, 0)
492
vbox.pack_end(hbox, False, False, 2)
495
notebook = gtk.Notebook()
496
notebook.set_border_width(2)
497
vbox.pack_start(notebook, True, True, 0)
499
if (extension == "mp4" or extension == "m4a") and idjcroot != None and mp4enabled:
500
self.mp4 = mp4tagger(pathname, idjcroot)
501
reload_button.connect_object("clicked", self.mp4.load_tag, pathname)
502
apply_button.connect_object("clicked", self.mp4.save_tag, pathname)
503
reload_button.clicked()
504
label = gtk.Label("MP4 Tag")
505
notebook.append_page(self.mp4, label)
508
elif extension == "mp3":
509
self.id3 = id3tagger(pathname)
510
reload_button.connect_object("clicked", self.id3.load_tag, pathname, extension)
511
apply_button.connect_object("clicked", self.id3.save_tag, pathname)
512
reload_button.clicked()
514
label = gtk.Label("ID3 Tag")
515
notebook.append_page(self.id3, label)
519
self.vorbis = vorbistagger(pathname)
520
if extension == "ogg":
521
reload_button.connect_object("clicked", self.vorbis.load_ogg_tag, pathname)
522
apply_button.connect_object("clicked", self.vorbis.save_ogg_tag, pathname)
523
elif extension == "flac":
524
reload_button.connect_object("clicked", self.vorbis.load_flac_tag, pathname)
525
apply_button.connect_object("clicked", self.vorbis.save_flac_tag, pathname)
526
reload_button.clicked()
527
label = gtk.Label(vorbis_tag_text)
528
notebook.append_page(self.vorbis, label)
531
self.id3 = id3tagger(pathname)
532
self.id3.load_tag(pathname, extension)
533
if (self.id3.tagv2found == 0 and self.id3.tagv1found == 0) or extension == "ogg":
537
reload_button.connect_object("clicked", self.id3.load_tag, pathname, extension)
538
apply_button.connect_object("clicked", self.id3.save_tag, pathname)
539
label = gtk.Label(id3_tag_text)
540
notebook.append_page(self.id3, label)
543
apply_button.connect_object_after("clicked", gtk.Window.destroy, self.window)
546
if __name__ == "__main__":
547
os.environ["CHARSET"]="UTF-8"
548
if len(sys.argv) != 2 or os.path.isfile(sys.argv[1]) == False:
549
print "The media tagger from IDJC. Copyright Stephen Fairchild 2006\nReleased under the GNU General Public License V2.0\nUsage:", sys.argv[0], "filename"
551
multitag(sys.argv[1])