1
# IDJCmedia.py: GUI code for main media players in IDJC
2
# Copyright (C) 2005-2007 Stephen Fairchild (s-fairchild@users.sourceforge.net)
4
# This program is free software: you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation, either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program in the file entitled COPYING.
16
# If not, see <http://www.gnu.org/licenses/>.
18
__all__ = [ 'IDJC_Media_Player', 'make_arrow_button', 'supported' ]
28
import xml.dom.minidom as mdom
32
from collections import deque, namedtuple, defaultdict
33
from functools import partial
40
from mutagen.mp3 import MP3
41
from mutagen.flac import FLAC
42
from mutagen.mp4 import MP4
43
from mutagen.easyid3 import EasyID3
44
from mutagen.apev2 import APEv2
45
from mutagen.asf import ASF
47
from idjc import FGlobs, PGlobs
48
from . import popupwindow
49
from .mutagentagger import *
50
from .freefunctions import *
51
from .gtkstuff import threadslock
52
from .prelims import *
53
from .tooltips import main_tips
56
t = gettext.translation(FGlobs.package_name, FGlobs.localedir, fallback=True)
62
set_tip = main_tips.set_tip
65
# Suppress warning when None is placed in a ListStore element where some kind of GObject should go.
66
warnings.filterwarnings("ignore", r"g_object_set_qdata: assertion `G_IS_OBJECT \(object\)' failed")
67
# Suppress warning when drag 'n dropping tracks to an empty playlist window.
68
warnings.filterwarnings("ignore", "IA__gtk_tree_view_scroll_to_cell: assertion `tree_view->priv->tree != NULL' failed.*")
71
# Named tuple for a playlist row.
72
class PlayerRow(namedtuple("PlayerRow", "rsmeta filename length meta encoding title artist replaygain cuesheet album")):
73
def __nonzero__(self):
74
return self.rsmeta != "<s>valid</s>"
76
# Playlist value indicating a file isn't valid.
77
NOTVALID = PlayerRow("<s>valid</s>", "", 0, "", "latin1", "", "", 0.0, None, "")
79
# Replay Gain value to indicate default.
82
# Pathname is an absolute file path or 'missing' or 'pregap'.
83
CueSheetTrack = namedtuple("CueSheetTrack", "pathname play tracknum index performer title offset duration replaygain")
85
class CueSheetListStore(gtk.ListStore):
86
_columns = (str, int, int, int, str, str, int, int, float)
87
assert len(_columns) == len(CueSheetTrack._fields)
88
def __nonzero__(self):
91
def __getitem__(self, i):
92
return CueSheetTrack(*gtk.ListStore.__getitem__(self, i))
105
gtk.ListStore.__init__(self, *self._columns)
107
class NumberedLabel(gtk.Label):
108
attrs = pango.AttrList()
109
attrs.insert(pango.AttrFamily("Monospace" , 0, 3))
110
#attrs.insert(pango.AttrWeight(pango.WEIGHT_BOLD, 0, 3))
112
def set_value(self, value):
113
self.set_text("--" if value is None else "%02d" % value)
116
text = self.get_text()
117
return None if text == "--" else int(self.text)
119
def __init__(self, value=None):
120
gtk.Label.__init__(self)
121
self.set_attributes(self.attrs)
122
self.set_value(value)
124
class CellRendererDuration(gtk.CellRendererText):
125
"""Render a value in frames as a time mm:ss:hs right justified."""
127
__gproperties__ = { "duration" : (gobject.TYPE_UINT64, "duration",
128
"playback time expressed in CD audio frames",
129
0, long(3e9), 0, gobject.PARAM_WRITABLE) }
132
gtk.CellRendererText.__init__(self)
133
self.set_property("xalign", 1.0)
135
def do_set_property(self, property, value):
136
if property.name == "duration":
137
s, f = divmod(value, 75)
139
self.props.text = "%d:%02d.%02d" % (m, s, f // 0.75)
141
class CuesheetPlaylist(gtk.Frame):
142
def description_col_func(self, column, cell, model, iter):
143
line = model[model.get_path(iter)[0]]
144
desc = " - ".join(x for x in (line.performer, line.title) if x)
145
desc = desc or os.path.splitext(os.path.split(line.pathname)[1])[0]
146
cell.props.text = desc
148
def play_clicked(self, cellrenderer, path):
149
model = self.treeview.get_model()
150
iter = model.get_iter(path)
151
col = CueSheetTrack._fields.index("play")
152
val = model.get_value(iter, col)
153
model.set_value(iter, col, not val)
156
gtk.Frame.__init__(self, " %s " % _('Cuesheet Playlist'))
157
self.set_border_width(3)
160
vbox.set_border_width(4)
166
vbox.pack_start(hbox, False)
168
def nextprev_unit(label_text):
169
def icon_button(stock_item):
170
button = gtk.Button()
171
image = gtk.image_new_from_stock(stock_item, gtk.ICON_SIZE_MENU)
172
button.set_image(image)
178
prev = icon_button(gtk.STOCK_MEDIA_PREVIOUS)
183
box.pack_start(lhbox, False)
186
label = gtk.Label(label_text + " ")
187
lhbox.pack_start(label, False)
189
numbered = NumberedLabel()
190
lhbox.pack_start(numbered, False)
193
next = icon_button(gtk.STOCK_MEDIA_NEXT)
197
return box, prev, next
200
box_t, self.prev_track, self.next_track = nextprev_unit(_('Track'))
202
box_i, self.prev_index, self.next_index = nextprev_unit(_('Index'))
203
hbox.pack_start(box_t, fill=False)
204
hbox.pack_start(box_i, fill=False)
207
scrolled = gtk.ScrolledWindow()
208
scrolled.set_size_request(-1, 117)
209
scrolled.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
210
scrolled.set_shadow_type(gtk.SHADOW_ETCHED_IN)
211
vbox.pack_start(scrolled)
213
self.treeview = gtk.TreeView()
214
#self.treeview.set_headers_visible(True)
215
scrolled.add(self.treeview)
217
#self.treeview.set_fixed_height_mode(True)
219
renderer_toggle = gtk.CellRendererToggle()
220
renderer_toggle.connect("toggled", self.play_clicked)
221
renderer_text_desc = gtk.CellRendererText()
222
renderer_text_desc.set_property("ellipsize", pango.ELLIPSIZE_END)
223
renderer_text_rjust = gtk.CellRendererText()
224
renderer_text_rjust.set_property("xalign", 0.9)
225
renderer_duration = CellRendererDuration()
227
# TC: Column heading, whether to play.
228
play = gtk.TreeViewColumn(_('Play'), renderer_toggle, active=1)
229
self.treeview.append_column(play)
230
# TC: Column heading, the track number.
231
track = gtk.TreeViewColumn(_('Trk'), renderer_text_rjust, text=2)
232
self.treeview.append_column(track)
233
# TC: Column heading, the index number.
234
index = gtk.TreeViewColumn(_('Ind'), renderer_text_rjust, text=3)
235
self.treeview.append_column(index)
236
description = gtk.TreeViewColumn(_('Description'), renderer_text_desc)
237
description.set_expand(True)
238
description.set_cell_data_func(renderer_text_desc, self.description_col_func)
239
self.treeview.append_column(description)
241
duration = gtk.TreeViewColumn(_('Duration'), renderer_duration)
242
duration.add_attribute(renderer_duration, "duration", 7)
243
self.treeview.append_column(duration)
245
class ButtonFrame(gtk.Frame):
246
def __init__(self, title):
247
gtk.Frame.__init__(self)
248
attrlist = pango.AttrList()
249
attrlist.insert(pango.AttrSize(8000, 0, len(title)))
250
label = gtk.Label(title)
251
label.set_attributes(attrlist)
252
self.set_label_widget(label)
254
self.hbox = gtk.HBox()
257
self.set_shadow_type(gtk.SHADOW_NONE)
258
self.set_label_align(0.5, 0.5)
260
class ExternalPL(gtk.Frame):
262
next = self._get_next()
264
return self._get_next()
268
if self.active.get_active():
270
line = self.gen.next()
271
except StopIteration:
272
self.gen = self.player.get_elements_from([self.pathname])
277
def cb_active(self, widget):
278
if widget.get_active():
279
self.pathname = (self.filechooser, self.directorychooser)[self.radio_directory.get_active()].get_filename()
280
if self.pathname is not None:
281
self.gen = self.player.get_elements_from([self.pathname])
283
line = self.gen.next()
284
except StopIteration:
285
widget.set_active(False)
287
self.player.stop.clicked()
288
self.player.liststore.clear()
289
self.player.liststore.append(line)
290
self.player.treeview.get_selection().select_path(0)
291
self.vbox.set_sensitive(False)
293
widget.set_active(False)
295
self.vbox.set_sensitive(True)
297
def cb_newselection(self, widget, radio):
298
radio.set_active(True)
300
def make_line(self, radio, dialog):
301
button = gtk.FileChooserButton(dialog)
302
dialog.set_current_folder(os.path.expanduser("~"))
304
hbox.pack_start(radio, False, False, 0)
305
hbox.pack_start(button, True, True, 0)
310
def __init__(self, player):
312
gtk.Frame.__init__(self, " %s " % _('External Playlist'))
313
self.set_border_width(4)
316
hbox.set_border_width(8)
319
self.vbox = gtk.VBox()
320
hbox.pack_start(self.vbox, True, True, 0)
322
# TC: Button text to activate an external playlist.
323
self.active = gtk.ToggleButton(" %s " % _('Active'))
324
self.active.connect("toggled", self.cb_active)
325
hbox.pack_end(self.active, False, False, 0)
328
filefilter = gtk.FileFilter()
329
filefilter.add_pattern("*.m3u")
330
filefilter.add_pattern("*.pls")
331
filefilter.add_pattern("*.xspf")
333
self.filechooser = gtk.FileChooserDialog(title = _('Choose a playlist file') + pm.title_extra, buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT))
334
self.filechooser.set_filter(filefilter)
335
self.directorychooser = gtk.FileChooserDialog(title = _('Choose a media directory') + pm.title_extra, action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT))
337
self.radio_file = gtk.RadioButton()
338
self.radio_directory = gtk.RadioButton(self.radio_file)
340
self.filechooser.connect("selection-changed", self.cb_newselection, self.radio_file)
341
self.directorychooser.connect("selection-changed", self.cb_newselection, self.radio_directory)
343
fbox = self.make_line(self.radio_file, self.filechooser)
344
set_tip(fbox, _('Choose a playlist file.'))
345
dbox = self.make_line(self.radio_directory, self.directorychooser)
346
set_tip(dbox, _('Choose a folder/directory of music.'))
348
self.vbox.pack_start(fbox, True, True, 0)
349
self.vbox.pack_start(dbox, True, True, 0)
354
class AnnouncementDialog(gtk.Dialog):
355
def write_changes(self, widget):
356
m = "%02d" % int(self.minutes.get_value())
357
s = "%02d" % int(self.seconds.get_value())
358
self.model.set_value(self.iter, 3, "00" + m + s)
359
b = self.tv.get_buffer()
360
text = b.get_text(b.get_start_iter(), b.get_end_iter())
361
self.model.set_value(self.iter, 4, urllib.quote(text))
362
self.player.reselect_please = True
363
def restore_mic_playnext(self, widget):
364
self.player.parent.mic_opener.close_all()
365
if self.model.iter_next(self.iter) is not None:
366
self.player.play.clicked()
367
def delete_announcement(self, widget, event=None):
368
self.model.remove(self.iter)
369
self.player.reselect_please = True
370
gtk.Dialog.destroy(self)
371
def timeout_remove(self, widget):
372
gobject.source_remove(self.timeout)
373
def timer_update(self, lock = True):
375
gtk.gdk.threads_enter()
376
inttime = int(self.cdt - time.time())
377
if inttime != self.oldinttime:
379
stime = "%2d:%02d" % divmod(inttime, 60)
380
self.countdownlabel.set_text(stime)
382
self.attrlist.change(self.fontcolour_red)
384
gtk.gdk.threads_leave()
387
self.countdownlabel.set_text("--:--")
388
self.attrlist.change(self.fontcolour_black)
390
gtk.gdk.threads_leave()
393
gtk.gdk.threads_leave()
395
def cb_keypress(self, widget, event):
396
self.player.parent.cb_key_capture(widget, event)
397
if event.keyval == 65307:
399
if event.keyval == 65288 and self.mode == "active":
400
self.cancel_button.clicked()
401
def __init__(self, player, model, iter, mode):
406
if mode == "initial":
407
model.set_value(iter, 3, "110000")
408
gtk.Dialog.__init__(self, _('Create a new announcement'), player.parent.window, gtk.DIALOG_MODAL)
409
elif mode == "delete_modify":
410
gtk.Dialog.__init__(self, _('Modify or Delete this announcement'), player.parent.window, gtk.DIALOG_MODAL)
411
elif mode == "active":
412
gtk.Dialog.__init__(self, _('Announcement'), player.parent.window, gtk.DIALOG_MODAL)
413
self.connect("key-press-event", self.cb_keypress)
415
ivbox.set_border_width(10)
419
sw = gtk.ScrolledWindow()
420
sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
421
sw.set_shadow_type(gtk.SHADOW_IN)
422
sw.set_size_request(500, 200)
423
ivbox.pack_start(sw, True, True, 0)
425
self.tv = gtk.TextView()
427
self.tv.unset_flags(gtk.CAN_FOCUS)
431
ivbox.pack_start(ihbox, False, False, 0)
436
if mode == "initial" or mode == "delete_modify":
437
# TC: The time format as minutes and seconds.
438
countdown_label = gtk.Label('(%s) ' % _("mm:ss"))
439
chbox.pack_start(countdown_label, False, False, 0)
440
countdown_label.show()
441
minutes_adj = gtk.Adjustment(0.0, 0.0, 59.0, 1.0)
442
seconds_adj = gtk.Adjustment(0.0, 0.0, 59.0, 1.0)
443
self.minutes = gtk.SpinButton(minutes_adj)
444
self.seconds = gtk.SpinButton(seconds_adj)
446
chbox.pack_start(self.minutes, False, False, 0)
448
chbox.pack_start(sep, False, False, 0)
450
chbox.pack_start(self.seconds, False, False, 0)
454
cdtime = model.get_value(iter, 3)[2:6]
456
cd = int(cdtime[:2]) * 60 + int(cdtime[2:])
457
self.cdt = time.time() + cd + 1
458
self.countdownlabel = gtk.Label()
459
self.attrlist = pango.AttrList()
460
fontdesc = pango.FontDescription("monospace bold condensed 15")
461
self.attrlist.insert(pango.AttrFontDesc(fontdesc, 0, 5))
462
self.fontcolour_black = pango.AttrForeground(0, 0, 0, 0, 5)
463
self.fontcolour_red = pango.AttrForeground(65535, 0, 0, 0, 5)
464
self.attrlist.insert(self.fontcolour_black)
465
self.countdownlabel.set_attributes(self.attrlist)
467
self.timer_update(False)
468
self.timeout = gobject.timeout_add(100, self.timer_update)
469
self.connect("destroy", self.timeout_remove)
470
chbox.pack_start(self.countdownlabel, True, False, 0)
471
self.countdownlabel.show()
473
ihbox.pack_start(chbox, True, False, 0)
476
if mode == "delete_modify" or mode == "active":
477
lr = model.get_value(iter, 3)
478
text = model.get_value(iter, 4)
479
self.tv.get_buffer().set_text(urllib.unquote(text))
480
if mode == "delete_modify":
481
self.minutes.set_value((int(lr[2:4])))
482
self.seconds.set_value((int(lr[4:6])))
484
self.player.parent.mic_opener.open_auto("announcement")
488
ivbox.pack_start(thbox, False, False, 0)
490
# TC: Alongside the name of the next track.
491
label = gtk.Label(_('Next track'))
492
thbox.pack_start(label, False, False, 0)
495
entry.set_editable(False)
496
entry.unset_flags(gtk.CAN_FOCUS)
497
ni = model.iter_next(iter)
498
if ni and model.get_value(ni, 0)[0] != ">" :
499
entry.set_text(model.get_value(ni, 3))
500
thbox.pack_start(entry, True, True, 0)
503
self.ok_button = gtk.Button(gtk.STOCK_OK)
504
if mode == "initial" or mode == "delete_modify":
505
self.ok_button.connect("clicked", self.write_changes)
507
self.ok_button.connect("clicked", self.restore_mic_playnext)
508
self.ok_button.connect_object("clicked", gtk.Dialog.destroy, self)
509
self.ok_button.set_use_stock(True)
510
self.action_area.add(self.ok_button)
511
self.ok_button.show()
512
if mode == "delete_modify":
513
self.delete_button = gtk.Button(gtk.STOCK_DELETE)
514
self.delete_button.connect("clicked", self.delete_announcement)
515
self.delete_button.set_use_stock(True)
516
self.action_area.add(self.delete_button)
517
self.delete_button.show()
518
self.cancel_button = gtk.Button(gtk.STOCK_CANCEL)
519
if mode == "initial":
520
self.connect("delete-event", self.delete_announcement)
521
self.cancel_button.connect("clicked", self.delete_announcement)
523
self.cancel_button.connect_object("clicked", gtk.Dialog.destroy, self)
524
self.cancel_button.set_use_stock(True)
525
self.action_area.add(self.cancel_button)
526
self.cancel_button.show()
528
self.ok_button.grab_focus()
530
class Supported(object):
531
def _check(self, pathname, which):
532
ext = os.path.splitext(pathname)[1].lower()
533
return ext in which and ext or False
534
def playlists_as_text(self):
535
return "(*" + ", *".join(self.playlists) + ")"
536
def media_as_text(self):
537
return "(*" + ", *".join(self.media) + ")"
538
def check_media(self, pathname):
539
return self._check(pathname, self.media)
540
def check_playlists(self, pathname):
541
return self._check(pathname, self.playlists)
543
self.media = [ ".ogg", ".oga", ".wav", ".aiff", ".au", ".txt", ".cue" ]
544
self.playlists = [ ".m3u", ".xspf", ".pls" ]
546
if FGlobs.avcodec and FGlobs.avformat:
547
self.media.append(".avi")
548
self.media.append(".wma")
549
self.media.append(".ape")
550
self.media.append(".mpc")
551
self.media.append(".mp4")
552
self.media.append(".m4a")
553
self.media.append(".m4b")
554
self.media.append(".m4p")
555
if FGlobs.flacenabled:
556
self.media.append(".flac")
557
if FGlobs.speexenabled:
558
self.media.append(".spx")
560
supported = Supported()
562
# Arrow button creation helper function
563
def make_arrow_button(self, arrow_type, shadow_type, data):
564
button = gtk.Button();
565
arrow = gtk.Arrow(arrow_type, shadow_type);
567
button.connect("clicked", self.callback, data)
572
def get_number_for(token, string):
574
end = string.rindex(token)
576
while start >= 0 and (string[start].isdigit() or string[start] == "."):
578
return int(float(string[start+1:end]))
582
class nice_listen_togglebutton(gtk.ToggleButton):
583
def __init__(self, label = None, use_underline = True):
585
gtk.ToggleButton.__init__(self, label, use_underline)
587
gtk.ToggleButton.__init__(self, label)
589
return gtk.ToggleButton.__str__(self) + " auto inconsistent when insensitive"
590
def set_sensitive(self, bool):
592
gtk.ToggleButton.set_sensitive(self, False)
593
gtk.ToggleButton.set_inconsistent(self, True)
595
gtk.ToggleButton.set_sensitive(self, True)
596
gtk.ToggleButton.set_inconsistent(self, False)
599
class CueSheet(object):
600
"""A class for parsing cue sheets."""
602
_operands = (("PERFORMER", "SONGWRITER", "TITLE", "PREGAP", "POSTGAP"),
603
("FILE", "TRACK", "INDEX"))
604
_operands = dict((k, v + 1) for v, o in enumerate(_operands) for k in o)
606
# Try to split a string into three parts with a large quoted section
607
# and parts fore and aft.
608
_quoted = re.compile(r'(.*?)[ \t]"(.*)"[ \t](.*)').match
610
def _time_handler(self, time_str):
611
"""Returns the number of frames of audio (75ths of seconds)
613
Minutes can exceed 99 going beyond the cue sheet standard.
617
mm, ss, ff = [int(x) for x in time_str.split(":")]
619
raise ValueError("time must be in (m*)mm:ss:ff format %s" % self.line)
621
if ff < 0 or ff > 74 or ss < 0 or ss > 59 or mm < 0:
622
raise ValueError("a time value is out of range %s" % self.line)
624
return ff + 75 * ss + 75 * 60 * mm
626
def _int_handler(self, int_str):
627
"""Attempt to convert to an integer."""
632
raise ValueError("expected integer value for %s %s", (int_str, self.line))
636
def _tokenize(cls, iterable):
637
"""Scanner/tokenizer for cue sheets.
639
This routine will iteratively take one line at a time and return
640
the line number, command, and any operands related to the command.
642
Quoted text will have spaces and tabs intact and counts as one,
643
otherwise all consecutive non-whitespace is a token. The first
644
token is the command, the rest are it's operands.
647
for i, line in enumerate(iterable):
648
line = line.strip() + " "
649
match = cls._quoted(line)
651
left, quoted, right = match.groups()
652
left = left.replace("\t", " ").split()
653
right = right.replace("\t", " ").split()
655
left = line.replace("\t", " ").split()
659
tokens = filter(lambda x: x, left + [quoted] + right)
660
yield i + 1, tokens[0].upper(), tokens[1:]
662
def _parse_PERFORMER(self):
663
self.segment[self.tracknum][self.command].append(self.operand[0])
665
_parse_SONGWRITER = _parse_TITLE = _parse_PERFORMER
667
def _parse_FILE(self):
668
if not self.operand[1] in ("WAVE", "MP3", "AIFF"):
669
raise ValueError("unsupported file type %s" % self.line)
671
self.filename = self.operand[0]
674
def _parse_TRACK(self):
675
if self.filename is None:
676
raise ValueError("no filename yet specified %s" % self.line)
678
if self.tracknum and self.index < 1:
679
raise ValueError("track %02d lacks a 01 index" % self.tracknum)
681
if self.operand[1] != "AUDIO":
682
raise ValueError("only AUDIO track datatype supported %s" % self.line)
684
num = self._int_handler(self.operand[0])
687
if num != self.tracknum:
688
raise ValueError("unexpected track number %s" % self.line)
690
def _parse_PREGAP(self):
691
if self.tracknum == 0 or self.index != -1 or "PREGAP" in self.segment[self.tracknum]:
692
raise ValueError("unexpected PREGAP command %s" % self.line)
694
self.segment[self.tracknum]["PREGAP"] = self._time_handler(self.operand[0])
696
def _parse_INDEX(self):
697
if self.tracknum == 0:
698
raise ValueError("no track yet specified %s" % self.line)
700
if "POSTGAP" in self.segment[self.tracknum]:
701
raise ValueError("INDEX command following POSTGAP %s" % self.line)
703
num = self._int_handler(self.operand[0])
704
frames = self._time_handler(self.operand[1])
706
if self.tracknum == 1 and self.index == -1 and frames != 0:
707
raise ValueError("first index must be zero for a file %s" % self.line)
709
if self.index == -1 and num == 1:
712
if num != self.index:
713
raise ValueError("unexpected index number %s" % self.line)
715
if frames < self.prevframes:
716
raise ValueError("index time before the previous index %s" % self.line)
718
if self.prevframes and frames == self.prevframes:
719
raise ValueError("index time no different than previously %s" % self.line)
721
self.segment[self.tracknum][self.index] = (self.filename, frames)
723
self.prevframes = frames
725
def _parse_POSTGAP(self):
726
if self.tracknum == 0 or self.index < 1 or "POSTGAP" in self.segment[self.tracknum]:
727
raise ValueError("unexpected POSTGAP command %s" % self.line)
729
self.segment[self.tracknum]["POSTGAP"] = self._time_handler(operand[0])
731
def parse(self, iterable):
732
"""Return a parsed cuesheet object."""
736
self.segment = defaultdict(partial(defaultdict, list))
738
for self.i, self.command, self.operand in self._tokenize(iterable):
739
if self.command not in self._operands:
742
self.line = "on line %d" % self.i
744
if len(self.operand) != self._operands[self.command]:
745
raise ValueError("wrong number of operands got %d required %d %s" %
746
(len(self.operand), self._operands[self.command], self.line))
748
getattr(self, "_parse_" + self.command)()
750
if self.tracknum == 0:
751
raise ValueError("no tracks")
754
raise ValueError("track %02d lacks a 01 index" % tracknum)
756
for each in self.segment.itervalues():
757
del each.default_factory
758
del self.segment.default_factory
763
class IDJC_Media_Player:
764
playlisttype_extension = tuple(zip(
765
# File format selection items from a list (user can pick only one).
766
(_('By Extension'), _('M3U playlist'), _('XSPF playlist'), _('PLS playlist')),
767
('', 'm3u', 'xspf', 'pls'),))
769
def make_cuesheet_playlist_entry(self, cue_pathname):
770
cuesheet_liststore = CueSheetListStore()
772
with open(cue_pathname) as f:
773
segment_data = CueSheet().parse(f)
774
except (IOError, ValueError), e:
775
print "failed reading cue sheet", cue_pathname
779
basepath = os.path.split(cue_pathname)[0]
781
totalframes = trackframes = cumulativeframes = 0
782
global_cue_performer = global_cue_title = ""
784
for key, val in sorted(segment_data.iteritems()):
786
cue_performer = ", ".join(val.get("PERFORMER", []))
787
cue_title = ", ".join(val.get("TITLE", []))
789
global_cue_performer = cue_performer
790
global_cue_title = cue_title
792
for key2, val2 in sorted(val.iteritems()):
793
if isinstance(key2, int):
795
filename, frames = val2
796
if filename != oldfilename:
797
oldfilename = filename
798
pathname = os.path.join(basepath, filename)
799
track_data = self.get_media_metadata(pathname)
801
trackframes = 75 * track_data.length
802
totalframes += trackframes
803
replaygain = track_data.replaygain
809
if not cue_performer:
810
cue_performer = track_data.artist or global_cue_performer
812
cue_title = track_data.title or global_cue_title
815
nextoffset = val[index + 1][1]
818
nextoffset = segment_data[track + 1][0][1]
821
nextoffset = segment_data[track + 1][1][1]
823
nextoffset = trackframes
826
nextoffset = trackframes
827
duration = nextoffset - frames
829
duration = frames = 0
831
element = CueSheetTrack(pathname, bool(pathname), track, index,
832
cue_performer, cue_title, frames, duration, replaygain)
833
cuesheet_liststore.append(element)
835
if global_cue_performer and global_cue_title:
836
metadata = global_cue_performer + " - " + global_cue_title
838
metadata = global_cue_performer or global_cue_title
839
# TC: Missing metadata text.
840
metadata = metadata or _('Unknown')
842
# TC: Cuesheet data element as shown in the playlist.
843
element = PlayerRow('<span foreground="dark green">%s</span>' % _("(Cue sheet)") + rich_safe(metadata),
844
cue_pathname, totalframes // 75 + 1, metadata, "utf-8",
845
global_cue_title, global_cue_performer, RGDEF, cuesheet_liststore, "")
849
def get_media_metadata(self, filename):
858
# Strip away any file:// prefix
859
if filename.count("file://", 0, 7):
860
filename = filename[7:]
861
elif filename.count("file:", 0, 5):
862
filename = filename[5:]
864
filext = supported.check_media(filename)
865
if filext == False or os.path.isfile(filename) == False:
866
return NOTVALID._replace(filename=filename)
868
if filext in (".cue", ".txt"):
869
return self.make_cuesheet_playlist_entry(filename)
873
# Use this name for metadata when we can't get anything from tags.
874
# The name will also appear grey to indicate a tagless state.
875
meta_name = os.path.splitext(glib.filename_display_basename(filename))[0].lstrip("0123456789 -")
876
encoding = None # Obsolete
877
# TC: Playlist text meaning the metadata tag is missing or incomplete.
878
rsmeta_name = '<span foreground="dark red">(%s)</span> %s' % (_('Bad Tag'), rich_safe(meta_name))
879
title_retval = meta_name
882
# Obtain as much metadata from ubiquitous tags as possible.
883
# Files can have ape and id3 tags. ID3 has priority in this case.
885
audio = APEv2(filename)
891
rg = float(audio["REPLAYGAIN_TRACK_GAIN"][0].rstrip(" dB"))
894
artist = audio.get("ARTIST", [u""])
895
title = audio.get("TITLE", [u""])
896
album = audio.get("ALBUM", [u""])
897
# ID3 is tried second so it can supercede APE tag data.
899
audio = EasyID3(filename)
904
rg = float(audio["replaygain_track_gain"][0].rstrip(" dB"))
908
artist = audio["artist"]
912
title = audio["title"]
916
album = audio["album"]
921
# Trying for metadata from native tagging formats.
922
if FGlobs.avcodec and FGlobs.avformat and filext == ".avi":
923
self.parent.mixer_write("AVFP=%s\nACTN=avformatinforequest\nend\n" % filename, True)
925
line = self.parent.mixer_read()
926
if line.startswith("avformatinfo: artist="):
927
artist = line[21:].strip()
928
if line.startswith("avformatinfo: title="):
929
title = line[20:].strip()
930
if line.startswith("avformatinfo: album="):
931
album = line[20:].strip()
932
if line.startswith("avformatinfo: duration="):
933
length = int(line[23:-1])
934
if line == "avformatinfo: end\n":
937
elif (filext == ".wav" or filext == ".aiff" or filext == ".au"):
938
self.parent.mixer_write("SNDP=%s\nACTN=sndfileinforequest\nend\n" % filename, True)
940
line = self.parent.mixer_read()
941
if line == "idjcmixer: sndfileinfo Not Valid\n" or line == "":
942
return NOTVALID._replace(filename=filename)
943
if line.startswith("idjcmixer: sndfileinfo length="):
944
length = int(line[30:-1])
945
if line.startswith("idjcmixer: sndfileinfo artist="):
947
if line.startswith("idjcmixer: sndfileinfo title="):
949
if line.startswith("idjcmixer: sndfileinfo album="):
951
if line == "idjcmixer: sndfileinfo end\n":
954
return NOTVALID._replace(filename=filename)
956
# This handles chained ogg files as generated by IDJC.
957
elif filext == ".ogg" or filext == ".oga" or filext == ".spx":
958
self.parent.mixer_write("OGGP=%s\nACTN=ogginforequest\nend\n" % filename, True)
960
line = self.parent.mixer_read()
961
if line == "OIR:NOT VALID\n" or line == "":
962
return NOTVALID._replace(filename=filename)
963
if line.startswith("OIR:ARTIST="):
964
artist = line[11:].strip()
965
if line.startswith("OIR:TITLE="):
966
title = line[10:].strip()
967
if line.startswith("OIR:ALBUM="):
968
album = line[10:].strip()
969
if line.startswith("OIR:LENGTH="):
970
length = int(float(line[11:].strip()))
971
if line.startswith("OIR:REPLAYGAIN_TRACK_GAIN="):
973
rg = float(line[26:].rstrip(" dB\n"))
976
if line == "OIR:end\n":
979
# Mutagen used for all remaining formats.
981
audio = mutagen.File(filename)
983
return NOTVALID._replace(filename=filename)
985
length = int(audio.info.length)
986
if isinstance(audio, MP4):
988
artist = audio["\xa9ART"][0]
992
title = audio["\xa9nam"][0]
996
album = audio["\xa9alb"][0]
999
elif isinstance(audio, MP3):
1000
# The LAME tag is the last port of call for Replay Gain info
1001
# due to it frequently being based on the source audio.
1004
rg = audio.info.track_gain
1011
x = list(audio.get("Artist", []))
1012
x += list(audio.get("Author", []))
1014
artist = "/".join((unicode(y) for y in x))
1017
x = list(audio["Title"])
1021
title = "/".join((unicode(y) for y in x))
1024
x = list(audio["Album"])
1028
album = "/".join((unicode(y) for y in x))
1031
rg = float(unicode(audio["replaygain_track_gain"][-1]).rstrip(" dB"))
1035
if isinstance(artist, list):
1036
artist = u"/".join(artist)
1038
if isinstance(title, list):
1039
title = u"/".join(title)
1041
if isinstance(album, list):
1042
album = u"/".join(album)
1044
if isinstance(artist, str):
1046
artist = artist.decode("utf-8", "strict")
1048
artist = artist.decode("latin1", "replace")
1050
if isinstance(title, str):
1052
title = title.decode("utf-8", "strict")
1054
title = title.decode("latin1", "replace")
1056
if isinstance(album, str):
1058
album = album.decode("utf-8", "strict")
1060
album = album.decode("latin1", "replace")
1062
assert(isinstance(artist, unicode))
1063
assert(isinstance(title, unicode))
1064
assert(isinstance(album, unicode))
1069
if artist and title:
1070
meta_name = artist + u" - " + title
1071
return PlayerRow(rich_safe(meta_name), filename, length, meta_name, encoding, title, artist, rg, cuesheet, album)
1073
return PlayerRow(rsmeta_name, filename, length, meta_name, encoding, title_retval, artist, rg, cuesheet, album)
1075
# Update playlist entries for a given filename e.g. when tag has been edited
1076
def update_playlist(self, newdata):
1078
for item in self.liststore:
1079
if item[1] == newdata[1]:
1080
if item[0].startswith("<b>"):
1081
item[0] = u"<b>" + newdata[0] + u"</b>"
1084
item[0] = newdata[0]
1085
for i in range(2, len(item)):
1086
item[i] = newdata[i]
1087
if active is not None:
1088
self.songname = active[3] # update metadata on server
1089
self.title = active[5].encode("utf-8")
1090
self.artist = active[6].encode("utf-8")
1091
self.album = active[9].encode("utf-8")
1092
self.player_restart()
1093
self.parent.send_new_mixer_stats()
1095
# Shut down our media players when we exit.
1098
if self.player_is_playing:
1102
def save_session(self):
1103
fh = open(self.session_filename, "w")
1104
extlist = self.external_pl.filechooser.get_filename()
1105
if extlist is not None:
1106
fh.write("extlist=" + extlist + "\n")
1107
extdir = self.external_pl.directorychooser.get_filename()
1108
if extdir is not None:
1109
fh.write("extdir=" + extdir + "\n")
1110
fh.write("digiprogress_type=" + str(int(self.digiprogress_type)) + "\n")
1111
fh.write("stream_button=" + str(int(self.stream.get_active())) + "\n")
1112
fh.write("listen_button=" + str(int(self.listen.get_active())) + "\n")
1113
fh.write("playlist_mode=" + str(self.pl_mode.get_active()) + "\n")
1114
fh.write("plsave_filetype=" + str(self.plsave_filetype) + "\n")
1115
fh.write("plsave_open=" + str(int(self.plsave_open)) + "\n")
1116
fh.write("fade_mode=" + str(self.pl_delay.get_active()) + "\n")
1117
if self.plsave_folder is not None:
1118
fh.write("plsave_folder=" + self.plsave_folder + "\n")
1120
for entry in self.liststore:
1122
if entry[0].startswith("<b>"): # clean off any accidental bold tags
1123
entry[0] = entry[0][3:-4]
1125
if isinstance(item, int):
1128
elif isinstance(item, float):
1131
elif isinstance(item, str):
1133
elif isinstance(item, CueSheetListStore):
1136
item = "(%s, )" % ", ".join(repr(x) for x in item)
1142
fh.write(str(len(item)) + ":" + item)
1144
model, iter = self.treeview.get_selection().get_selected()
1145
if iter is not None:
1146
fh.write("select=" + str(model.get_path(iter)[0]) + "\n")
1149
def restore_session(self):
1151
fh = open(self.session_filename, "r")
1156
line = fh.readline()
1162
if line.startswith("extlist="):
1163
self.external_pl.filechooser.set_filename(line[8:-1])
1164
if line.startswith("extdir="):
1165
self.external_pl.directorychooser.set_current_folder(line[7:-1])
1166
if line.startswith("digiprogress_type="):
1167
if int(line[18]) != self.digiprogress_type:
1168
self.digiprogress_click()
1169
if line.startswith("stream_button="):
1170
self.stream.set_active(int(line[14]))
1171
if line.startswith("listen_button="):
1172
self.listen.set_active(int(line[14]))
1173
if line.startswith("playlist_mode="):
1174
self.pl_mode.set_active(int(line[14]))
1175
if line.startswith("plsave_filetype="):
1176
self.plsave_filetype=int(line[16])
1177
if line.startswith("plsave_open="):
1178
self.plsave_open=bool(int(line[12]))
1179
if line.startswith("plsave_folder="):
1180
self.plsave_folder=line[14:-1]
1181
if line.startswith("fade_mode="):
1182
self.pl_delay.set_active(int(line[10]))
1183
if line.startswith("pe="):
1184
playlist_entry = self.pl_unpack(line[3:])
1185
if not playlist_entry or self.playlist_todo:
1186
self.playlist_todo.append(playlist_entry.filename)
1188
self.liststore.append(playlist_entry)
1189
if line.startswith("select="):
1192
self.treeview.get_selection().select_path(path)
1193
self.treeview.scroll_to_cell(path, None, False)
1198
if self.playlist_todo:
1199
print self.playername + " player: the stored playlist data is not compatible with this version\nfiles placed in a queue for rescanning"
1200
gobject.idle_add(self.cb_playlist_todo)
1203
def cb_playlist_todo(self):
1204
if self.no_more_files:
1207
pathname = self.playlist_todo.popleft()
1210
line = self.get_media_metadata(pathname)
1212
self.liststore.append(line)
1214
print "file missing or type unsupported %s" % pathname
1217
def pl_unpack(self, text): # converts a string encoded list to a python list
1221
while text[start] != "\n":
1223
while text[end] != ":":
1225
nextstart = int(text[start + 1 : end]) + end + 1
1227
value = text[end + 1 : nextstart]
1233
value = float(value)
1237
csts = eval(value, {"__builtins__":None},{"CueSheetTrack":CueSheetTrack})
1238
value = CueSheetListStore()
1243
except Exception, e:
1244
print "pl_unpack: playlist line not valid", e
1246
return NOTVALID._replace(filename=reply[1])
1253
return PlayerRow._make(reply)
1255
return NOTVALID._replace(filename=reply[1])
1257
def handle_stop_button(self, widget):
1258
self.restart_cancel = True
1259
if self.is_playing == True:
1260
self.is_playing = False
1261
if self.timeout_source_id:
1262
gobject.source_remove(self.timeout_source_id)
1263
# This will make our play button code branch to its shutdown code.
1264
self.is_stopping = True
1265
# This will emit a signal which will trigger the play button handler code.
1266
self.play.set_active(False)
1267
# Must do pause as well if it is pressed.
1268
if self.pause.get_active() == True:
1269
self.pause.set_active(False)
1270
self.parent.send_new_mixer_stats()
1272
def handle_pause_button(self, widget, selected):
1273
if self.is_playing == True:
1274
if self.is_paused == False:
1275
# Player pause code goes here
1276
print "Player paused"
1277
self.is_paused = True
1278
self.parent.send_new_mixer_stats()
1280
# Player unpause code goes here
1281
print "Player unpaused"
1282
self.is_paused = False
1283
self.parent.send_new_mixer_stats()
1285
# Prevent the pause button going into its on state when not playing.
1287
# We must unselect it.
1288
widget.set_active(False)
1290
self.is_paused = False
1292
def handle_play_button(self, widget, selected):
1293
if selected == False:
1294
# Prevent the button from being toggled off by clicking on a selected play button.
1295
# This emits a toggled signal so we will handle this eventuality too.
1296
# Note that when flag is_stopping is set we don't want to reactivate this button.
1297
if self.is_stopping == False:
1298
widget.set_active(True)
1300
self.is_stopping = False
1301
if self.player_is_playing == True:
1302
self.player_shutdown()
1304
if self.is_playing == True:
1305
if self.new_title == True:
1306
self.new_title = False
1307
self.player_shutdown()
1308
self.parent.send_new_mixer_stats()
1309
self.player_is_playing = self.player_startup()
1310
if self.player_is_playing == False:
1311
self.player_is_playing = True
1313
self.pause.set_active(False)
1315
print "Someone probably clicked Play when we were already playing"
1317
self.is_playing = True
1318
self.new_title = False
1319
if self.player_startup():
1320
self.player_is_playing = True
1321
print "Player has started"
1325
def player_startup(self):
1326
# remember which player started last so we can decide on metadata
1327
print "player_startup %s" % self.playername
1328
self.parent.last_player = self.playername
1330
if self.player_is_playing == True:
1331
# Use the current song if one is playing.
1332
model = self.model_playing
1333
iter = self.iter_playing
1335
# Get our next playlist item.
1336
treeselection = self.treeview.get_selection()
1337
(model, iter) = treeselection.get_selected()
1339
print "Nothing selected in the playlist - trying the first entry."
1341
iter = model.get_iter(0)
1343
print "Playlist is empty"
1345
print "We start at the beginning"
1346
treeselection.select_iter(iter)
1348
self.treeview.scroll_to_cell(model.get_path(iter)[0], None, False)
1350
self.music_filename = model.get_value(iter, 1)
1351
if self.music_filename != "":
1352
# Songname is used for metadata for mp3
1353
self.songname = unicode(model.get_value(iter, 3))
1354
# These two are used for ogg metadata
1355
self.title = unicode(model.get_value(iter, 5)).encode("utf-8", "replace")
1356
self.artist = unicode(model.get_value(iter, 6)).encode("utf-8", "replace")
1357
self.album = unicode(model.get_value(iter, 9)).encode("utf-8", "replace")
1358
self.parent.send_new_mixer_stats()
1359
# rt is the run time in seconds of our song
1360
rt = model.get_value(iter, 2)
1362
rt = 0 # playlist controls have negative numbers
1363
# Calculate our seek time scaling from old slider settings.
1364
# Used for when seek is moved before play is pressed.
1365
if os.path.isfile(self.music_filename):
1367
self.start_time = int(self.progressadj.get_value() / self.max_seek * float(rt))
1368
except ZeroDivisionError:
1371
self.start_time = rt # Seek to the end when file is missing.
1372
print "Seek time is %d seconds" % self.start_time
1374
if self.parent.prefs_window.rg_adjust.get_active():
1375
self.gain = model.get_value(iter, 7)
1376
if self.gain == RGDEF:
1377
self.gain = self.parent.prefs_window.rg_defaultgain.get_value()
1378
self.gain += self.parent.prefs_window.rg_boost.get_value()
1379
print "final gain value of %f dB" % self.gain
1382
print "not using replay gain"
1384
# Now we recalibrate the progress bar to the current song length
1385
self.digiprogress_f = True
1386
self.progressadj.set_all(float (self.start_time) , 0.0, rt, rt/1000.0, rt/100.0, 0.0)
1387
self.progressadj.emit("changed")
1388
# Set the stop figure used by the progress bar's timeout function
1389
self.progress_stop_figure = model.get_value(iter, 2)
1390
self.progress_current_figure = self.start_time
1392
self.player_is_playing = True
1394
# Bold highlight the file we are playing
1395
text = model.get_value(iter, 0)
1396
if not text.startswith("<b>"):
1397
text = "<b>" + text + "</b>"
1398
model.set_value(iter, 0, text)
1399
self.iter_playing = iter
1400
self.model_playing = model
1402
self.silence_count = 0
1404
if self.music_filename != "":
1405
self.parent.mixer_write("PLRP=%s\nSEEK=%d\nSIZE=%d\nRGDB=%f\nACTN=playnoflush%s\nend\n" % (
1406
self.music_filename, self.start_time, self.max_seek, self.gain, self.playername), True)
1408
line = self.parent.mixer_read()
1409
if line.startswith("context_id="):
1410
self.player_cid = int(line[11:-1])
1413
self.player_cid = -1
1416
print "skipping play for empty filename"
1417
self.player_cid = -1
1418
if self.player_cid == -1:
1419
print "player startup was unsuccessful for file", self.music_filename
1420
# we will treat this as successful and not return false so that end of track processing will take care of it
1421
self.timeout_source_id = gobject.timeout_add(0, self.cb_play_progress_timeout, self.player_cid)
1423
print "player context id is %d\n" % self.player_cid
1424
if self.player_cid & 1:
1425
self.timeout_source_id = gobject.timeout_add(200, self.cb_play_progress_timeout, self.player_cid)
1427
self.invoke_end_of_track_policy()
1430
def player_shutdown(self):
1431
print "player shutdown code was called"
1433
if self.iter_playing:
1434
# Unhighlight this track
1435
text = self.model_playing.get_value(self.iter_playing, 0)
1436
if text[:3] == "<b>":
1438
self.model_playing.set_value(self.iter_playing, 0, text)
1439
self.file_iter_playing = 0
1441
self.player_is_playing = False
1442
if self.timeout_source_id:
1443
gobject.source_remove(self.timeout_source_id)
1445
self.progress_current_figure = 0
1446
self.playtime_elapsed.set_value(0)
1447
self.progressadj.set_value(0.0)
1448
self.progressadj.value_changed()
1450
if self.gapless == False:
1451
self.parent.mixer_write("ACTN=stop%s\nend\n" % self.playername, True)
1453
self.digiprogress_f = False
1454
self.other_player_initiated = False
1455
self.crossfader_initiated = False
1457
def set_fade_mode(self, mode):
1458
if self.parent.simplemixer:
1460
self.parent.mixer_write("FADE=%d\nACTN=fademode_%s\nend\n" % (mode, self.playername), True)
1462
def player_restart(self):
1463
# remember which player started last so we can decide on metadata
1464
print "player_restart %s" % self.playername
1465
self.parent.last_player = self.playername
1467
gobject.source_remove(self.timeout_source_id)
1468
self.start_time = int (self.progressadj.get_value())
1469
self.silence_count = 0
1470
self.parent.mixer_write("PLRP=%s\nSEEK=%d\nACTN=play%s\nend\n" % (
1471
self.music_filename, self.start_time, self.playername), True)
1473
line = self.parent.mixer_read()
1474
if line.startswith("context_id="):
1475
self.player_cid = int(line[11:-1])
1478
self.player_cid = -1
1480
if self.player_cid == -1:
1481
print "player startup was unsuccessful for file", self.music_filename
1484
print "player context id is %d\n" % self.player_cid
1486
# Restart a callback to update the progressbar.
1487
self.timeout_source_id = gobject.timeout_add(100, self.cb_play_progress_timeout, self.player_cid)
1490
def next_real_track(self, i):
1494
m = self.model_playing
1499
if m.get_value(i, 0)[0] != ">":
1502
def first_real_track(self):
1503
m = self.model_playing
1504
i = m.get_iter_first()
1509
if m.get_value(i, 0)[0] != ">":
1511
i = m.get_iter_next(i)
1513
def invoke_end_of_track_policy(self, mode_text=None):
1514
# This is where we implement the playlist modes for the most part.
1515
if mode_text is None:
1516
mode_text = self.pl_mode.get_active_text()
1517
if self.is_playing == False:
1518
print "Assertion failed in: invoke_end_of_track_policy"
1521
if mode_text == N_('Manual'):
1522
# For Manual mode just stop the player at the end of the track.
1523
print "Stopping in accordance with manual mode"
1525
elif mode_text == N_('Play All'):
1526
if self.music_filename == "":
1527
self.handle_playlist_control()
1530
treeselection = self.treeview.get_selection()
1531
if self.is_playing == False:
1532
treeselection.select_path(0) # park on the first menu item
1533
elif mode_text == N_('Loop All') or mode_text == N_('Cue Up') or mode_text == N_('Fade Over'):
1534
iter = self.next_real_track(self.iter_playing)
1536
iter = self.first_real_track()
1538
if iter is not None:
1539
treeselection = self.treeview.get_selection()
1540
treeselection.select_iter(iter)
1541
if mode_text == N_('Loop All'):
1544
treeselection.select_path(0)
1545
elif mode_text == N_('Random'):
1546
# Not truly random. Effort is made to break the appearance of
1547
# having a set play order to a long term listener without
1548
# re-playing the same track too soon.
1552
poolsize = len(self.liststore) // 10
1557
if poolsize > len(self.liststore):
1558
poolsize = len(self.liststore)
1560
if self.parent.server_window.is_streaming or self.parent.server_window.is_recording:
1561
fp = self.parent.files_played
1563
fp = self.parent.files_played_offline
1564
timestamped_pathnames = []
1565
while not timestamped_pathnames:
1566
random_pathnames = [PlayerRow(*x).filename for x in random.sample(self.liststore, poolsize)]
1567
timestamped_pathnames = [(fp.get(pn, 0), pn) for pn in random_pathnames if pn]
1568
timestamped_pathnames.sort()
1569
least_recent_ts = timestamped_pathnames[0][0]
1570
timestamped_pathnames = [x for x in timestamped_pathnames if x[0] == least_recent_ts]
1571
least_recent = random.choice(timestamped_pathnames)[1]
1573
for path, entry in enumerate(self.liststore):
1574
entry_filename = PlayerRow(*entry).filename
1575
if least_recent == entry_filename:
1578
treeselection = self.treeview.get_selection()
1579
treeselection.select_path(path)
1581
elif mode_text == N_('External'):
1582
path = self.model_playing.get_path(self.iter_playing)[0]
1584
next_track = self.external_pl.get_next()
1585
if next_track is None:
1586
print "Cannot obtain anything from external directory/playlist - stopping"
1588
self.model_playing.insert_after(self.iter_playing, next_track)
1589
self.model_playing.remove(self.iter_playing)
1590
treeselection = self.treeview.get_selection()
1591
treeselection.select_path(path)
1593
elif mode_text == N_('Alternate') or mode_text == N_('Random Hop'):
1594
iter = self.next_real_track(self.iter_playing)
1596
iter = self.first_real_track()
1598
treeselection = self.treeview.get_selection()
1599
if iter is not None:
1600
treeselection.select_iter(iter)
1602
treeselection.select_path(0)
1603
if self.playername == "left":
1604
self.parent.passright.clicked()
1605
other_player = self.parent.player_right
1607
self.parent.passleft.clicked()
1608
other_player = self.parent.player_left
1609
if mode_text == N_('Alternate'):
1610
other_player.play.clicked()
1611
elif mode_text == N_('Random Hop'):
1612
other_player.invoke_end_of_track_policy(N_('Random'))
1614
print 'The mode "%s" is not currently supported - stopping' % mode_text
1617
def handle_playlist_control(self):
1618
treeselection = self.treeview.get_selection()
1619
model = self.model_playing
1620
iter = self.iter_playing
1621
control = model.get_value(iter, 0)
1622
print "control is", control
1624
if control == "<b>>normalspeed</b>":
1625
self.pbspeedzerobutton.clicked()
1627
if self.is_playing == False:
1628
treeselection.select_path(0)
1629
def x(control_type, open_auto_type):
1630
if control == "<b>>%s</b>" % control_type:
1631
print "player", self.playername, "stopping due to playlist control"
1632
if (self.playername == "left" and self.parent.crossfade.get_value() < 50) or (self.playername == "right" and self.parent.crossfade.get_value() >= 50):
1633
self.parent.mic_opener.open_auto(open_auto_type)
1635
if model.iter_next(iter):
1636
treeselection.select_iter(model.iter_next(iter))
1638
treeselection.select_iter(model.get_iter_first())
1639
x("stopplayer", "stop_control")
1640
x("stopplayer2", "stop_control2")
1641
if control == "<b>>jumptotop</b>":
1643
treeselection.select_path(0)
1645
if control == "<b>>announcement</b>":
1646
dia = AnnouncementDialog(self, model, iter, "active")
1649
if model.iter_next(iter):
1650
treeselection.select_iter(model.iter_next(iter))
1652
treeselection.select_iter(model.get_iter_first())
1653
if control == "<b>>crossfade</b>":
1654
print "player", self.playername, "stopping, crossfade complete"
1656
if model.iter_next(iter):
1657
treeselection.select_iter(model.iter_next(iter))
1659
treeselection.select_path(0)
1660
if control == "<b>>stopstreaming</b>":
1662
self.parent.server_window.stop_streaming_all()
1663
if self.is_playing == False:
1664
treeselection.select_path(0)
1665
if control == "<b>>stoprecording</b>":
1667
self.parent.server_window.stop_recording_all()
1668
if self.is_playing == False:
1669
treeselection.select_path(0)
1670
if control == "<b>>transfer</b>":
1671
if self.playername == "left":
1672
otherplayer = self.parent.player_right
1673
self.parent.passright.clicked()
1675
otherplayer = self.parent.player_left
1676
self.parent.passleft.clicked()
1677
print "transferring to player", otherplayer.playername
1678
otherplayer.play.clicked()
1680
if model.iter_next(iter):
1681
treeselection.select_iter(model.iter_next(iter))
1683
treeselection.select_path(0)
1684
if control.startswith("<b>>fade"):
1685
if control.endswith("e5</b>"):
1686
self.set_fade_mode(1)
1687
elif control.endswith("e10</b>"):
1688
self.set_fade_mode(2)
1690
self.set_fade_mode(0)
1691
if self.is_playing == False:
1692
treeselection.select_path(0)
1694
def get_pl_block_size(self, iter):
1696
speedfactor = self.pbspeedfactor
1697
while iter is not None:
1698
length = self.liststore.get_value(iter, 2)
1700
text = self.liststore.get_value(iter, 0)
1701
if text.startswith("<b>"):
1703
if text in (">stopplayer", ">stopplayer2", ">transfer", ">crossfade", ">announcement", ">jumptotop"):
1705
if text == ">normalspeed":
1708
size += int(length / speedfactor)
1709
iter = self.liststore.iter_next(iter)
1712
def update_time_stats(self):
1713
if self.pl_mode.get_active() != 0: # optimisation -- this function uses a lot of cpu
1715
if self.player_is_playing:
1716
tr = int((self.max_seek - self.progressadj.value) / self.pbspeedfactor)
1717
model = self.model_playing
1718
iter = model.iter_next(self.iter_playing)
1719
tr += self.get_pl_block_size(iter)
1722
selection = self.treeview.get_selection()
1723
model, iter = selection.get_selected()
1728
iter = model.get_iter_first()
1729
bs = self.get_pl_block_size(iter)
1732
if model.get_value(iter, 0)[0:3] == "<b>":
1735
bs = self.get_pl_block_size(iter)
1737
print "Playlist data is fucked up"
1739
bsm, bss = divmod(bs, 60)
1741
trm, trs = divmod(tr, 60)
1742
tm_end = time.localtime(int(time.time()) + tr)
1743
tm_end_h = tm_end[3]
1744
tm_end_m = tm_end[4]
1745
tm_end_s = tm_end[5]
1747
self.statusbar_update("%s -%2d:%02d | %s %02d:%02d:%02d" % (
1748
# TC: The remaining playlist time.
1749
_('Remaining'), trm, trs,
1750
# TC: The estimated finish time of the playlist.
1751
_('Finish'), tm_end_h, tm_end_m, tm_end_s))
1753
self.statusbar_update("%s -%2d:%02d | %s %02d:%02d:%02d | %s %2d:%02d" % (_('Remaining'), trm, trs, _('Finish'), tm_end_h, tm_end_m, tm_end_s, _('Block size'), bsm, bss))
1756
self.statusbar_update("")
1758
bft = time.localtime(time.time() + bs)
1762
self.statusbar_update("%s %2d:%02d | %s %02d:%02d:%02d" % (
1763
# TC: The remaining play time of the block of audio tracks from the highlighted track onwards until the next interruption.
1764
_('Block size'), bsm, bss, _('Finish'), bf_h, bf_m, bf_s))
1766
def statusbar_update(self, newtext): # optimisation -- only update the status bars when the text changes
1767
if newtext != self.oldstatusbartext:
1768
if self.pbspeedfactor < 0.999 or self.pbspeedfactor > 1.001:
1769
newtext = ("%03.1f%% | " % (self.pbspeedfactor * 100)) + newtext
1770
self.pl_statusbar.push(1, newtext)
1771
self.oldstatusbartext = newtext
1773
def check_mixer_signal(self):
1774
if self.parent.menu_feature_set.get_active() and self.progress_press == False and self.progressadj.upper - self.progress_current_figure < float(self.silence) and self.progressadj.upper > 10.0:
1775
if self.mixer_signal_f.value == 0 and int(self.mixer_cid) == self.player_cid + 1 and self.parent.prefs_window.silence_killer.get_active() and self.eos_inspect() == False:
1776
print "termination by check mixer signal"
1777
self.invoke_end_of_track_policy()
1780
def cb_play_progress_timeout(self, cid):
1782
# player started at end of track
1783
self.invoke_end_of_track_policy()
1786
if self.reselect_cursor_please:
1787
treeselection = self.treeview.get_selection()
1788
(model, iter) = treeselection.get_selected()
1789
if iter is not None:
1790
self.treeview.scroll_to_cell(model.get_path(iter)[0], None, False)
1792
self.reselect_please = True
1793
self.reselect_cursor_please = False
1794
if self.reselect_please:
1795
print "Set cursor on track playing"
1796
# This code reselects the playing track after a drag operation.
1797
treeselection = self.treeview.get_selection()
1799
treeselection.select_iter(self.iter_playing)
1801
print "Iter was cancelled probably due to song dragging"
1802
self.reselect_please = False
1803
if self.progress_press == False:
1804
if self.runout.value and self.is_paused == False and self.mixer_cid.value > self.player_cid:
1806
print "termination due to end of track"
1807
self.invoke_end_of_track_policy()
1808
self.gapless = False
1810
if self.mixer_signal_f.value == False:
1811
self.silence_count += 1
1812
if self.parent.menu_feature_set.get_active() and self.silence_count >= 120 and self.playtime_elapsed.value > 15 and self.parent.prefs_window.bonus_killer.get_active():
1813
print "termination due to excessive silence"
1814
self.invoke_end_of_track_policy()
1817
self.silence_count = 0
1819
if self.progress_current_figure != self.playtime_elapsed.value:
1820
# Code runs once a second.
1822
# Check whether a track is hitting a stream or being recorded.
1823
if self.stream.get_active() and (self.parent.server_window.is_streaming or
1824
self.parent.server_window.is_recording) and (
1825
(self.playername == "left" and self.parent.crossadj.value < 90)
1827
(self.playername == "right" and self.parent.crossadj.value > 10)):
1828
# Log the time the file was last played.
1829
self.parent.files_played[self.music_filename] = time.time()
1831
self.parent.files_played_offline[self.music_filename] = time.time()
1833
self.progress_current_figure = self.playtime_elapsed.value
1834
self.progressadj.set_value(self.playtime_elapsed.value)
1835
if self.max_seek == 0:
1836
self.progressadj.emit("value_changed")
1837
self.update_time_stats()
1839
# we stop monitoring the play progress during the progress bar drag operation
1840
# by cancelling this timeout
1842
# Calclulate when to sound the DJ alarm (end of music notification)
1843
# Bugs: does not deep scan the playlist controls for >stopplayer so the alarm will not sound if
1844
# preceeded by another playlist control
1845
if self.progress_current_figure == self.progress_stop_figure -10 and self.progressadj.upper > 11 and self.parent.prefs_window.djalarm.get_active():
1846
if ((self.playername == "left" and self.parent.crossadj.get_value() < 50) or (self.playername == "right" and self.parent.crossadj.get_value() >= 50)) and (self.pl_mode.get_active() == 3 or self.pl_mode.get_active() == 4 or (self.pl_mode.get_active() == 0 and (self.model_playing.iter_next(self.iter_playing) is None or (self.pl_mode.get_active() == 0 and self.stop_inspect())))):
1847
if self.alarm_cid != cid:
1848
gobject.timeout_add(1000, self.deferred_alarm)
1849
self.alarm_cid = cid
1850
# Check if the crossfade needs scheduling.
1851
if self.pl_mode.get_active_text() == N_('Fade Over') or (self.pl_mode.get_active() == 0 and self.fade_inspect()):
1852
eot_crosstime = int(self.progress_stop_figure) - self.parent.passspeed_adj.props.value - int(self.progress_current_figure)
1853
# Start other player
1854
if not self.other_player_initiated and eot_crosstime <= 1:
1855
if self.playername == "left":
1856
self.parent.player_right.play.clicked()
1858
self.parent.player_left.play.clicked()
1859
self.other_player_initiated = True
1860
# Now do the crossfade
1861
if not self.crossfader_initiated and eot_crosstime <= 0:
1862
self.parent.passbutton.clicked()
1863
self.crossfader_initiated = True
1864
desired_direction = (self.playername == "left")
1865
if desired_direction != self.parent.crossdirection:
1866
self.parent.passbutton.clicked()
1868
rem = self.progress_stop_figure - self.progress_current_figure
1869
if (rem == 5 or rem == 10) and not self.crossfader_initiated and not self.parent.simplemixer:
1870
mode = self.pl_mode.get_active()
1871
next = self.model_playing.iter_next(self.iter_playing)
1872
if next is not None:
1873
nextval = self.model_playing.get_value(next, 0)
1876
if mode == 0 and nextval.startswith(">"):
1877
if rem == 5 and nextval == ">fade5":
1879
elif rem == 10 and nextval == ">fade10":
1884
self.set_fade_mode(fade)
1886
treeselection = self.treeview.get_selection()
1887
next = self.model_playing.iter_next(next)
1888
if next is not None:
1889
path = self.model_playing.get_path(next)
1890
treeselection.select_path(path)
1893
treeselection.select_path(0)
1894
self.set_fade_mode(0)
1896
fade = self.pl_delay.get_active()
1897
if (fade == 1 and rem == 10) or (fade == 2 and rem == 5) or self.pl_mode.get_active() in (3, 4, 6) or (mode == 0 and self.islastinplaylist()):
1900
self.set_fade_mode(fade)
1901
self.invoke_end_of_track_policy()
1902
self.set_fade_mode(0)
1907
def deferred_alarm(self):
1908
self.parent.alarm = True
1909
self.parent.send_new_mixer_stats()
1912
def stop_inspect(self):
1913
stoppers = (">stopplayer", ">stopplayer2", ">announcement")
1914
horizon = (">transfer", ">crossfade", ">jumptotop")
1915
i = self.iter_playing
1916
m = self.model_playing
1921
v = m.get_value(i, 0)
1922
if v and v[0] != ">":
1929
def fade_inspect(self):
1930
stoppers = (">crossfade")
1931
horizon = (">transfer", ">stopplayer", ">stopplayer2", ">announcement", ">jumptotop")
1932
i = self.iter_playing
1933
m = self.model_playing
1938
v = m.get_value(i, 0)
1939
if v and v[0] != ">":
1946
def eos_inspect(self):
1947
# Returns true when playlist ended or stream disconnect is imminent.
1948
if self.pl_mode.get_active():
1950
if self.islastinplaylist():
1952
stoppers = (">stopstreaming", )
1953
horizon = (">transfer", ">crossfade")
1954
i = self.iter_playing
1955
m = self.model_playing
1960
v = m.get_value(i, 0)
1961
if v and v[0] != ">":
1968
def islastinplaylist(self):
1969
iter = self.model_playing.iter_next(self.iter_playing)
1976
treeselection = self.treeview.get_selection()
1977
(model, iter) = treeselection.get_selected()
1979
print "Nothing is selected"
1981
path = model.get_path(iter)
1983
other_iter = model.get_iter(path[0]-1)
1984
self.liststore.swap(iter, other_iter)
1985
self.treeview.scroll_to_cell(path[0]-1, None, False)
1987
def arrow_down(self):
1988
treeselection = self.treeview.get_selection()
1989
(model, iter) = treeselection.get_selected()
1991
print "Nothing is selected"
1993
path = model.get_path(iter)
1995
other_iter = model.get_iter(path[0]+1)
1996
self.liststore.swap(iter, other_iter)
1997
self.treeview.scroll_to_cell(path[0]+1, None, False)
2002
#self.set_fade_mode(self.pl_delay.get_active())
2004
self.parent.mic_opener.open_auto("advance")
2005
path = self.model_playing.get_path(self.iter_playing)[0]+1
2007
treeselection = self.treeview.get_selection()
2008
treeselection.select_path(path)
2009
self.treeview.scroll_to_cell(path, None, False)
2011
self.parent.mic_opener.close_all()
2013
#self.set_fade_mode(0)
2015
def callback(self, widget, data):
2016
if data == "pbspeedzero":
2017
self.pbspeedbar.set_value(0.0)
2019
if data == "Arrow Up":
2022
if data == "Arrow Dn":
2026
self.handle_stop_button(widget)
2030
path = self.model_playing.get_path(self.iter_playing)[0]+1
2034
self.model_playing.get_iter(path)
2038
treeselection = self.treeview.get_selection()
2039
treeselection.select_path(path)
2040
self.new_title = True
2045
treeselection = self.treeview.get_selection()
2046
path = self.model_playing.get_path(self.iter_playing)
2049
treeselection.select_path(path[0]-1)
2050
self.new_title = True
2053
# This is for adding files to the playlist using the file requester.
2054
if data == "Add Files":
2055
if self.showing_file_requester == False:
2056
if self.playername == "left":
2057
# TC: File dialog title text.
2058
filerqtext = _('Add music to left playlist')
2060
# TC: File dialog title text.
2061
filerqtext = _('Add music to right playlist')
2062
self.filerq = gtk.FileChooserDialog(filerqtext + pm.title_extra, None, gtk.FILE_CHOOSER_ACTION_OPEN, (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
2063
self.filerq.set_select_multiple(True)
2064
self.filerq.set_current_folder(str(self.file_requester_start_dir))
2065
self.filerq.add_filter(self.plfilefilter_all)
2066
self.filerq.add_filter(self.plfilefilter_playlists)
2067
self.filerq.add_filter(self.plfilefilter_media)
2068
self.filerq.set_filter(self.plsave_filtertype)
2069
# TC: File filter text.
2070
frame = gtk.Frame(" %s " % _('Supported Media Formats'))
2072
box.set_border_width(3)
2075
entry.unset_flags(gtk.CAN_FOCUS)
2076
entry.set_has_frame(False)
2077
text = "*" + ", *".join(supported.media)
2078
entry.set_text(text)
2082
self.filerq.set_extra_widget(frame)
2083
self.filerq.connect("response", self.file_response)
2084
self.filerq.connect("destroy", self.file_destroy)
2086
self.showing_file_requester = True
2088
self.filerq.present()
2090
def file_response(self, dialog, response_id):
2091
chosenfiles = self.filerq.get_filenames()
2093
self.file_requester_start_dir.set_text(os.path.split(chosenfiles[0])[0])
2094
self.plsave_filtertype = self.filerq.get_filter()
2095
self.filerq.destroy()
2096
if response_id != gtk.RESPONSE_ACCEPT:
2098
gen = self.get_elements_from(chosenfiles)
2100
if self.no_more_files:
2101
self.no_more_files = False
2103
self.liststore.append(each)
2104
while gtk.events_pending():
2105
gtk.main_iteration()
2107
def file_destroy(self, widget):
2108
self.showing_file_requester = False
2110
def plfile_new_savetype(self, widget):
2111
self.plsave_filetype = self.pltreeview.get_selection().get_selected_rows()[1][0][0]
2112
# TC: Expander text which appears as "Select File Type (.pls)" for the pls file type.
2113
self.expander.set_label(_('Select File Type') + " (" + self.playlisttype_extension[self.plsave_filetype][0] + ")")
2115
def plfile_response(self, dialog, response_id):
2116
self.plsave_filtertype = dialog.get_filter()
2117
self.plsave_open = self.expander.get_expanded()
2118
self.plsave_folder = dialog.get_current_folder()
2120
if response_id == gtk.RESPONSE_ACCEPT:
2121
chosenfile = self.plfilerq.get_filename()
2122
self.plfilerq.destroy()
2123
if response_id != gtk.RESPONSE_ACCEPT:
2126
main, ext = os.path.splitext(chosenfile)
2128
if self.plsave_filetype == 0:
2129
if not ext in supported.playlists:
2131
ext = ".m3u" # default to m3u playlist format
2133
t = self.plsave_filetype - 1
2134
useext = supported.playlists[t]
2135
others = list(supported.playlists)
2138
if not ext in others:
2141
chosenfile = main + ext
2143
validlist = [ x for x in self.liststore if x[0][0] != ">" and x[2] >= 0 ]
2145
print "Chosenfile is", chosenfile
2147
pl = open(chosenfile, "w")
2149
print "Can't open file for writing. Permissions problem?"
2153
pl.write("#EXTM3U\r\n")
2154
for each in validlist:
2155
pl.write("#EXTINF:%d,%s\r\n" % (each[2], each[3].decode("UTF-8").encode("ISO8859-1", "replace")))
2156
pl.write(each[1] + "\r\n")
2161
print "That was odd\n"
2164
pl.write("[playlist]\r\nNumberOfEntries=%d\r\n\r\n" % len(validlist))
2165
for i in range(1, len(validlist) + 1):
2166
each = validlist[i - 1]
2167
pl.write("File%d=%s\r\n" % (i, each[1]))
2168
pl.write("Title%d=%s\r\n" % (i, each[3]))
2169
pl.write("Length%d=%d\r\n\r\n" % (i, each[2]))
2170
pl.write("Version=2\r\n")
2173
doc = mdom.getDOMImplementation().createDocument('http://xspf.org/ns/0/', 'playlist', None)
2175
playlist = doc.documentElement
2176
playlist.setAttribute('version', '1')
2177
playlist.setAttribute('xmlns', 'http://xspf.org/ns/0/')
2178
playlist.setAttribute('xmlns:idjc', 'http://idjc.sourceforge.net/ns/')
2180
trackList = doc.createElement('trackList')
2181
playlist.appendChild(trackList)
2183
for each in self.liststore:
2184
row = PlayerRow(*each)
2186
track = doc.createElement('track')
2187
trackList.appendChild(track)
2189
if row.rsmeta.startswith(">"):
2190
extension = doc.createElement('extension')
2191
track.appendChild(extension)
2192
extension.setAttribute('application', 'http://idjc.sourceforge.net/ns/')
2194
pld = doc.createElementNS('http://idjc.sourceforge.net/ns/', 'idjc:pld')
2195
extension.appendChild(pld)
2196
pld.setAttribute('rsmeta', row.rsmeta)
2197
pld.setAttribute('length', str(row.length))
2199
location = doc.createElement('location')
2200
track.appendChild(location)
2201
locationText = doc.createTextNode("file://" + urllib.quote(each[1]))
2202
location.appendChild(locationText)
2205
creator = doc.createElement('creator')
2206
track.appendChild(creator)
2207
creatorText = doc.createTextNode(each[6])
2208
creator.appendChild(creatorText)
2211
title = doc.createElement('title')
2212
track.appendChild(title)
2213
titleText = doc.createTextNode(each[5])
2214
title.appendChild(titleText)
2217
album = doc.createElement('album')
2218
track.appendChild(album)
2219
albumText = doc.createTextNode(each[9])
2220
album.appendChild(albumText)
2222
duration = doc.createElement('duration')
2223
track.appendChild(duration)
2224
durationText = doc.createTextNode(str(each[2] * 1000))
2225
duration.appendChild(durationText)
2227
xmltext = doc.toxml("UTF-8").replace("><", ">\n<").splitlines()
2229
for i in range(len(xmltext)):
2230
if xmltext[i][1] == "/":
2232
if len(xmltext[i]) < 3 or xmltext[i].startswith("<?") or xmltext[i][-2] == "/" or xmltext[i].count("<") == 2:
2233
xmltext[i] = spc + xmltext[i]
2235
xmltext[i] = spc + xmltext[i]
2236
if xmltext[i][len(spc) + 1] != "/":
2238
pl.write("\r\n".join(xmltext))
2243
def plfile_destroy(self, widget):
2244
self.showing_pl_save_requester = False
2246
def cb_toggle(self, widget, data):
2247
print "Toggle %s recieved for signal: %s" % (("OFF","ON")[widget.get_active()], data)
2250
self.handle_play_button(widget, widget.get_active())
2252
self.handle_pause_button(widget, widget.get_active())
2253
if data == "Stream":
2254
self.parent.send_new_mixer_stats();
2255
if data == "Listen":
2256
self.parent.send_new_mixer_stats();
2258
def cb_progress(self, progress):
2259
if self.digiprogress_f:
2260
if self.max_seek > 0:
2261
if self.digiprogress_type == 0 or self.player_is_playing == False:
2262
count = int(progress.value)
2264
count = self.max_seek - int(progress.value)
2266
count = self.progress_current_figure
2267
hours = int(count / 3600)
2268
count = count - (hours * 3600)
2269
minutes = count / 60
2270
seconds = count - (minutes * 60)
2271
if self.digiprogress_type == 0:
2272
self.digiprogress.set_text("%d:%02d:%02d" % (hours, minutes, seconds))
2274
if self.max_seek != 0:
2275
self.digiprogress.set_text(" -%02d:%02d " % (minutes, seconds))
2277
self.digiprogress.set_text(" -00:00 ")
2278
if self.handle_motion_as_drop:
2279
self.handle_motion_as_drop = False
2280
if self.player_restart() == False:
2283
if self.pause.get_active():
2284
self.pause.set_active(False)
2286
def cb_pbspeed(self, widget, data=None):
2287
self.pbspeedfactor = pow(10.0, widget.get_value() * 0.05)
2288
self.parent.send_new_mixer_stats()
2290
def digiprogress_click(self):
2291
self.digiprogress_type = not self.digiprogress_type
2292
if not self.digiprogress_f:
2293
if self.digiprogress_type == 0:
2294
self.digiprogress.set_text("0:00:00")
2296
self.digiprogress.set_text(" -00:00 ")
2298
self.cb_progress(self.progressadj)
2300
def cb_event(self, widget, event, callback_data):
2301
# Handle click to the play progress indicator
2302
if callback_data == "DigitalProgressPress":
2303
if event.button == 1:
2304
self.digiprogress_click()
2305
if event.button == 3:
2306
self.parent.app_menu.popup(None, None, None, event.button, event.time)
2307
return True # Prevent any focus therefore any cursor appearing
2308
if event.button == 1:
2309
# Handle click to the play progress bar
2310
if callback_data == "ProgressPress":
2311
self.progress_press = True
2312
if self.timeout_source_id:
2313
gobject.source_remove(self.timeout_source_id)
2314
elif callback_data == "ProgressRelease":
2315
self.progress_press = False
2316
if self.player_is_playing:
2317
self.progress_current_figure = self.progressadj.get_value()
2318
self.handle_motion_as_drop = True
2319
gobject.idle_add(self.player_progress_value_changed_emitter)
2322
# This is really a very convoluted workaround to achieve the effect of a connect_after
2323
# method on a button_release_event on the player progress bar to run player_restart
2324
# I tried using connect_after but no such luck hence this retarded idle function.
2326
def player_progress_value_changed_emitter(self):
2327
self.progressadj.emit("value_changed")
2330
def cb_menu_select(self, widget, data):
2331
print "The %s was chosen from the %s menu" % (data, self.playername)
2333
def delete_event(self, widget, event, data=None):
2336
def get_elements_from(self, pathnames):
2337
self.no_more_files = False
2340
ext = os.path.splitext(pathnames[0])[1]
2342
return self.get_elements_from_m3u(pathnames[0])
2344
return self.get_elements_from_pls(pathnames[0])
2345
elif ext == ".xspf":
2346
return self.get_elements_from_xspf(pathnames[0])
2347
elif os.path.isdir(pathnames[0]):
2348
return self.get_elements_from_directory(pathnames[0], set(), 2)
2349
return self.get_elements_from_chosen(pathnames)
2351
def get_elements_from_chosen(self, chosenfiles):
2352
for each in chosenfiles:
2353
meta = self.get_media_metadata(each)
2357
def get_elements_from_directory_orig(self, chosendir):
2358
files = os.listdir(chosendir)
2361
path = "/".join((chosendir, each))
2362
meta = self.get_media_metadata(path)
2366
def get_elements_from_directory(self, chosendir, visited, depth):
2368
chosendir = os.path.realpath(chosendir)
2369
if chosendir in visited or not os.path.isdir(chosendir):
2372
visited.add(chosendir)
2377
files = os.listdir(chosendir)
2379
for filename in files:
2380
pathname = "/".join((chosendir, filename))
2381
if os.path.isdir(pathname):
2382
#if os.path.realpath(pathname) == pathname:
2383
if not filename.startswith("."):
2384
directories.add(filename)
2386
meta = self.get_media_metadata(pathname)
2391
for subdir in directories:
2392
print "examining", "/".join((chosendir, subdir))
2393
gen = self.get_elements_from_directory("/".join((chosendir, subdir)), visited, depth)
2397
def get_elements_from_m3u(self, filename):
2399
file = open(filename, "r")
2400
data = file.read().strip()
2403
print "Problem reading file", filename
2405
basepath = os.path.split(filename)[0] + "/"
2406
data = data.splitlines()
2407
for line, each in enumerate(data):
2411
each = basepath + each
2412
# handle special case of a single element referring to a directory
2413
if line == 0 and len(data) == 1 and os.path.isdir(each):
2414
gen = self.get_elements_from_directory(each)
2418
meta = self.get_media_metadata(each)
2423
def get_elements_from_pls(self, filename):
2425
cfg = ConfigParser.RawConfigParser()
2427
cfg.readfp(open(filename))
2429
print "Problem reading file"
2431
if cfg.sections() != ['playlist']:
2432
print "wrong number of sections in pls file"
2434
if cfg.getint('playlist', 'Version') != 2:
2435
print "can handle version 2 pls playlists only"
2438
n = cfg.getint('playlist', 'NumberOfEntries')
2439
except ConfigParser.NoOptionError:
2440
print "NumberOfEntries is missing from playlist"
2443
print "NumberOfEntries is not an int"
2444
for i in range(1, n + 1):
2446
path = cfg.get('playlist', 'File%d' % i)
2448
print "Problem getting file path from playlist"
2450
if os.path.isfile(path):
2451
meta = self.get_media_metadata(path)
2455
def get_elements_from_xspf(self, filename):
2456
class BadXspf(ValueError):
2458
class GotLocation(Exception):
2465
dom = mdom.parse(filename)
2469
if dom.hasChildNodes() and len(dom.childNodes) == 1 and dom.documentElement.nodeName == u'playlist':
2470
playlist = dom.documentElement
2474
if playlist.namespaceURI != u"http://xspf.org/ns/0/":
2478
v = int(playlist.getAttribute('version'))
2482
print "only xspf playlist versions 0 and 1 supported"
2486
# obtain base URLs for relative URLs encountered in trackList
2487
# only one location tag is allowed
2488
locations = [ x for x in playlist.childNodes if x.nodeName == u"location" ]
2489
if len(locations) == 1:
2490
url = locations[0].childNodes[0].wholeText
2491
if url.startswith(u"file:///"):
2495
# lesser priority given to the path of the playlist in the filesystem
2496
baseurl.append(u"file://" + urllib.quote(os.path.split(os.path.realpath(filename))[0].decode("ASCII") + u"/"))
2497
# as before but without realpath
2498
baseurl.append(u"file://" + urllib.quote(os.path.split(filename)[0].decode("ASCII") + u"/"))
2499
if baseurl[-1] == baseurl[-2]:
2502
trackLists = playlist.getElementsByTagName('trackList')
2503
if len(trackLists) != 1:
2505
trackList = trackLists[0]
2506
if trackList.parentNode != playlist:
2509
tracks = trackList.getElementsByTagName('track')
2510
for track in tracks:
2511
if track.parentNode != trackList:
2513
locations = track.getElementsByTagName('location')
2515
for location in locations:
2516
for base in baseurl:
2517
url = urllib.unquote(urllib.basejoin(base, location.firstChild.wholeText).encode("ASCII"))
2518
meta = self.get_media_metadata(url)
2522
# Support namespaced pld tag for literal playlist data.
2523
# This is only used for non playable data such as playlist controls.
2524
extensions = track.getElementsByTagName('extension')
2525
for extension in extensions:
2526
if extension.getAttribute("application") == "http://idjc.sourceforge.net/ns/":
2527
customtags = extension.getElementsByTagNameNS("http://idjc.sourceforge.net/ns/", "pld")
2528
for tag in customtags:
2530
literal_entry = NOTVALID._replace(**dict((k, type(getattr(NOTVALID, k))(tag.attributes.get(k).nodeValue)) for k in tag.attributes.keys()))
2531
except Exception, e:
2541
print "could not parse playlist", filename
2544
def drag_data_delete(self, treeview, context):
2545
if context.action == gtk.gdk.ACTION_MOVE:
2546
treeselection = treeview.get_selection()
2547
model, iter = treeselection.get_selected()
2548
data = model.get_value(iter, 0)
2549
if data[:3] == "<b>":
2550
self.iter_playing = 0
2553
def drag_data_get_data(self, treeview, context, selection, target_id, etime):
2554
treeselection = treeview.get_selection()
2555
model, iter = treeselection.get_selected()
2556
if model.get_value(iter, 1) != "":
2557
data = "file://" + model.get_value(iter, 1)
2559
data = "idjcplayercontrol://" + model.get_value(iter, 0) + "+" + str(model.get_value(iter, 2)) + "+" + model.get_value(iter, 3) + "+" + model.get_value(iter, 4)
2560
print "data for drag_get =", data
2561
selection.set(selection.target, 8, data)
2562
self.reselect_please = True
2565
def drag_data_received_data(self, treeview, context, x, y, dragged, info, etime):
2567
text = str(dragged.data)
2568
if text[:20] == "idjcplayercontrol://":
2569
ct = text[20:].split("+")
2570
newrow = [ ct[0], "", int(ct[1]), ct[2], ct[3], "", "" ]
2571
drop_info = treeview.get_dest_row_at_pos(x, y)
2572
model = treeview.get_model()
2573
if drop_info == None:
2574
model.append(newrow)
2576
path, position = drop_info
2577
dest_iter = model.get_iter(path)
2578
if(position == gtk.TREE_VIEW_DROP_BEFORE or position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE):
2579
model.insert_before(dest_iter, newrow)
2581
model.insert_after(dest_iter, newrow)
2582
if context.action == gtk.gdk.ACTION_MOVE:
2583
context.finish(True, True, etime)
2585
if context.action == gtk.gdk.ACTION_MOVE:
2586
context.finish(True, True, etime)
2587
gobject.idle_add(self.drag_data_received_data_idle, treeview, x, y, text)
2589
treeselection = treeview.get_selection()
2590
model, iter = treeselection.get_selected()
2591
drop_info = treeview.get_dest_row_at_pos(x, y)
2592
if drop_info == None:
2593
self.liststore.move_before(iter, None)
2595
path, position = drop_info
2596
dest_iter = model.get_iter(path)
2597
if(position == gtk.TREE_VIEW_DROP_BEFORE or position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE):
2598
self.liststore.move_before(iter, dest_iter)
2600
self.liststore.move_after(iter, dest_iter)
2601
if context.action == gtk.gdk.ACTION_MOVE:
2602
context.finish(False, False, etime)
2605
def drag_data_received_data_idle(self, treeview, x, y, dragged):
2606
gtk.gdk.threads_enter()
2607
model = treeview.get_model()
2608
gtk.gdk.threads_leave()
2610
pathnames = [ urllib.unquote(t[7:]) for t in dragged.strip().splitlines() if t.startswith("file://") ]
2611
gen = self.get_elements_from(pathnames)
2614
for media_data in gen:
2615
if self.no_more_files:
2616
self.no_more_files = False
2619
gtk.gdk.threads_enter()
2620
drop_info = treeview.get_dest_row_at_pos(x, y)
2621
gtk.gdk.threads_leave()
2623
path, position = drop_info
2624
gtk.gdk.threads_enter()
2625
iter = model.get_iter(path)
2626
gtk.gdk.threads_leave()
2627
if(position == gtk.TREE_VIEW_DROP_BEFORE or position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE):
2628
gtk.gdk.threads_enter()
2629
iter = model.insert_before(iter, media_data)
2630
gtk.gdk.threads_leave()
2632
gtk.gdk.threads_enter()
2633
iter = model.insert_after(iter, media_data)
2634
gtk.gdk.threads_leave()
2636
gtk.gdk.threads_enter()
2637
iter = model.append(media_data)
2638
gtk.gdk.threads_leave()
2641
gtk.gdk.threads_enter()
2642
iter = model.insert_after(iter, media_data)
2643
gtk.gdk.threads_leave()
2644
gtk.gdk.threads_enter()
2645
while gtk.events_pending():
2646
gtk.gdk.threads_leave()
2647
gtk.gdk.threads_enter()
2648
gtk.main_iteration()
2649
gtk.gdk.threads_leave()
2650
gtk.gdk.threads_enter()
2651
gtk.gdk.threads_leave()
2652
self.reselect_please = True
2656
('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_WIDGET, 0),
2657
('text/plain', 0, 1),
2663
('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_WIDGET, 0),
2664
('text/plain', 0, 1),
2667
('text/uri-list', 0, 4) # Need drop target for prokyon3
2670
def cb_doubleclick(self, treeview, path, tvcolumn, user_data):
2671
# Our play button handler will manage to get the song title.
2672
# All we need to do is set a flag and issue a play button started signal.
2674
# The new_title flag allows a new song to be played when the player is going.
2675
self.new_title = True
2680
def cb_selection_changed(self, treeselection):
2681
self.cuesheet_playlist.hide()
2682
self.cuesheet_playlist.treeview.set_model(None)
2683
model, iter = treeselection.get_selected()
2685
row = PlayerRow._make(self.liststore[model.get_path(iter)[0]])
2687
self.cuesheet_playlist.treeview.set_model(row.cuesheet)
2688
self.cuesheet_playlist.show()
2689
self.update_time_stats()
2691
def cb_playlist_changed(self, treemodel, path, iter = None):
2692
self.playlist_changed = True # used by the request system
2694
def menu_activate(self, widget, event):
2695
if event.type == gtk.gdk.BUTTON_PRESS and event.button == 3:
2696
self.menu_model = self.treeview.get_model()
2697
row_info = self.treeview.get_dest_row_at_pos(int(event.x + 0.5), int(event.y + 0.5))
2700
path, position = row_info
2701
selection = self.treeview.get_selection()
2702
selection.select_path(path)
2703
self.menu_iter = self.menu_model.get_iter(path) # store the context of the menu action
2704
pathname = self.menu_model.get_value(self.menu_iter, 1)
2705
self.item_tag.set_sensitive(MutagenGUI.is_supported(pathname) != False)
2708
self.menu_iter = None
2710
self.item_duplicate.set_sensitive(sens)
2711
self.remove_this.set_sensitive(sens)
2712
self.remove_from_here.set_sensitive(sens)
2713
self.remove_to_here.set_sensitive(sens)
2714
self.item_tojingles.set_sensitive(pathname != "")
2715
model = self.treeview.get_model()
2716
if model.get_iter_first() == None:
2720
self.pl_menu_item.set_sensitive(sens2)
2721
self.playlist_save.set_sensitive(sens2)
2722
self.playlist_copy.set_sensitive(sens2)
2723
self.playlist_transfer.set_sensitive(sens2)
2724
self.playlist_empty.set_sensitive(sens2)
2725
if self.pl_mode.get_active() != 0:
2726
self.pl_menu_control.set_sensitive(False)
2728
self.pl_menu_control.set_sensitive(True)
2730
if self.playername == "left": # determine if anything is selected in the other playlist
2731
tv = self.parent.player_right.treeview.get_selection()
2733
tv = self.parent.player_left.treeview.get_selection()
2734
model, iter = tv.get_selected()
2739
self.copy_append_cursor.set_sensitive(sens3)
2740
self.copy_prepend_cursor.set_sensitive(sens3)
2741
self.transfer_append_cursor.set_sensitive(sens3)
2742
self.transfer_prepend_cursor.set_sensitive(sens3)
2744
widget.popup(None, None, None, event.button, event.time)
2748
def cb_plexpander(self, widget, param_spec):
2749
if widget.get_expanded():
2754
def menuitem_response(self, widget, text):
2755
print "The %s menu option was chosen" % text
2756
model = self.menu_model
2757
iter = self.menu_iter
2759
if text == "Announcement Control" and iter is not None and model.get_value(iter, 0) == ">announcement":
2760
# modify existing announcement dialog
2761
dia = AnnouncementDialog(self, model, iter, "delete_modify")
2766
"Stop Control" : ">stopplayer",
2767
"Stop Control 2" : ">stopplayer2",
2768
"Transfer Control" : ">transfer",
2769
"Crossfade Control" : ">crossfade",
2770
"Stream Disconnect Control" : ">stopstreaming",
2771
"Stop Recording Control" : ">stoprecording",
2772
"Normal Speed Control" : ">normalspeed",
2773
"Announcement Control" : ">announcement",
2774
"Fade 10" : ">fade10",
2775
"Fade 5" : ">fade5",
2776
"Fade none" : ">fadenone",
2777
"Jump To Top Control" : ">jumptotop",
2779
if dict.has_key(text):
2780
if iter is not None:
2781
iter = model.insert_after(iter)
2783
iter = model.append()
2784
model.set_value(iter, 0, dict[text])
2785
model.set_value(iter, 1, "")
2786
model.set_value(iter, 2, -11)
2787
model.set_value(iter, 3, "")
2788
model.set_value(iter, 4, "")
2789
model.set_value(iter, 5, "")
2790
model.set_value(iter, 6, "")
2791
self.treeview.get_selection().select_iter(iter)
2793
if text == "Announcement Control":
2794
# brand new announcement dialog
2795
dia = AnnouncementDialog(self, model, iter, "initial")
2799
if text == "MetaTag":
2801
pathname = model.get_value(iter, 1)
2805
MutagenGUI(pathname, model.get_value(iter, 4) , self.parent)
2807
if text == "Add File":
2810
if text == "Playlist Save":
2811
if self.showing_pl_save_requester == False:
2812
if self.playername == "left":
2813
filerqtext = _('Save left playlist')
2815
filerqtext = _('Save right playlist')
2817
self.expander = gtk.Expander()
2818
self.expander.connect("notify::expanded", self.cb_plexpander)
2819
vbox.add(self.expander)
2820
self.expander.show()
2822
self.plframe = gtk.Frame()
2823
self.plliststore = gtk.ListStore(str, str)
2824
for row in self.playlisttype_extension:
2825
self.plliststore.append(row)
2826
self.pltreeview = gtk.TreeView(self.plliststore)
2827
self.plframe.add(self.pltreeview)
2829
self.pltreeview.show()
2830
self.pltreeview.set_rules_hint(True)
2831
cellrenderer1 = gtk.CellRendererText()
2832
self.pltreeviewcol1 = gtk.TreeViewColumn(_('File Type'), cellrenderer1, text = 0)
2833
self.pltreeviewcol1.set_expand(True)
2834
cellrenderer2 = gtk.CellRendererText()
2835
# TC: File extension.
2836
self.pltreeviewcol2 = gtk.TreeViewColumn(_('Extension'), cellrenderer2, text = 1)
2837
self.pltreeview.append_column(self.pltreeviewcol1)
2838
self.pltreeview.append_column(self.pltreeviewcol2)
2839
self.pltreeview.connect("cursor-changed", self.plfile_new_savetype)
2840
self.pltreeview.set_cursor(self.plsave_filetype)
2842
if (self.plsave_open):
2843
self.expander.set_expanded(True)
2844
vbox.add(self.plframe)
2846
self.plfilerq = gtk.FileChooserDialog(filerqtext + pm.title_extra, None, gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT, gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
2847
self.plfilerq.set_current_folder(self.home)
2848
self.plfilerq.add_filter(self.plfilefilter_all)
2849
self.plfilerq.add_filter(self.plfilefilter_playlists)
2850
self.plfilerq.set_filter(self.plsave_filtertype)
2851
self.plfilerq.set_extra_widget(vbox)
2852
if self.plsave_folder is not None:
2853
self.plfilerq.set_current_folder(self.plsave_folder)
2854
if self.plsave_filetype == 0:
2855
self.plfilerq.set_current_name("idjcplaylist.m3u")
2857
self.plfilerq.set_current_name("idjcplaylist")
2858
self.plfilerq.connect("response", self.plfile_response)
2859
self.plfilerq.connect("destroy", self.plfile_destroy)
2860
self.plfilerq.show()
2861
self.showing_pl_save_requester = True
2863
self.plfilerq.present()
2865
if text == "Remove All":
2868
self.no_more_files = True
2869
self.liststore.clear()
2871
if text == "Remove This" and iter != None:
2872
name = model.get_value(iter, 0)
2873
if name[:3] == "<b>":
2875
self.liststore.remove(iter)
2877
if text == "Remove From Here" and iter != None:
2878
path = model.get_path(iter)
2881
iter = model.get_iter(path)
2882
if model.get_value(iter, 0)[:3] == "<b>":
2884
self.no_more_files = True
2885
self.liststore.remove(iter)
2887
print "Nothing more to delete"
2889
if text == "Remove To Here" and iter != None:
2890
self.no_more_files = True
2891
path = model.get_path(iter)[0] -1
2893
iter = model.get_iter(path)
2894
if model.get_value(iter, 0)[:3] == "<b>":
2896
self.liststore.remove(iter)
2899
if text == "Duplicate" and iter != None:
2900
row = list(model[model.get_path(iter)])
2901
if row[0][:3] == "<b>": # strip off any bold tags
2902
row[0] = row[0][3:-4]
2903
model.insert_after(iter, row)
2905
if text == "Playlist Exchange":
2906
self.no_more_files = True
2907
if self.playername == "left":
2908
opposite = self.parent.player_right
2910
opposite = self.parent.player_left
2912
opposite.stop.clicked()
2916
self.templist.append(self.liststore[i])
2920
self.liststore.clear()
2924
self.liststore.append(opposite.liststore[i])
2928
opposite.liststore.clear()
2932
opposite.liststore.append(self.templist[i])
2936
self.templist.clear()
2938
if text == "Copy Append":
2939
self.copy_playlist("end")
2941
if text == "Transfer Append":
2942
self.copy_playlist("end")
2944
self.liststore.clear()
2946
if text == "Copy Prepend":
2947
self.copy_playlist("start")
2949
if text == "Transfer Prepend":
2950
self.copy_playlist("start")
2952
self.liststore.clear()
2954
if text == "Copy Append Cursor":
2955
self.copy_playlist("after")
2957
if text == "Transfer Append Cursor":
2958
self.copy_playlist("after")
2960
self.liststore.clear()
2962
if text == "Copy Prepend Cursor":
2963
self.copy_playlist("before")
2965
if text == "Transfer Prepend Cursor":
2966
self.copy_playlist("before")
2968
self.liststore.clear()
2970
if text == "ToJingles":
2971
source = model.get_value(iter, 1)
2972
dest = pm.jinglesdir / os.path.split(source)[1]
2974
source = open(source, "r")
2975
dest = open(dest, "w")
2977
data = source.read(4096)
2979
if len(data) < 4096:
2982
print "IOError occurred"
2985
self.parent.jingles.refresh.clicked()
2987
if self.player_is_playing: # put the cursor on the file playing after a brief pause.
2988
self.reselect_please = True
2990
def stripbold(self, playlist_item):
2991
copy = list(playlist_item)
2992
if copy[0][:3] == "<b>":
2993
copy[0] = copy[0][3:-4]
2996
def copy_playlist(self, dest):
2997
if self.playername == "left":
2998
other = self.parent.player_right
3000
other = self.parent.player_left
3005
other.liststore.insert(i, self.stripbold(self.liststore[i]))
3009
other.liststore.append(self.stripbold(self.liststore[i]))
3012
(model, iter) = other.treeview.get_selection().get_selected()
3016
iter = other.liststore.insert_after(iter, self.stripbold(self.liststore[i]))
3018
if dest == "before":
3020
other.liststore.insert_before(iter, self.stripbold(self.liststore[i]))
3025
def cb_keypress(self, widget, event):
3026
# Handle shifted arrow keys for rearranging stuff in the playlist.
3027
if event.state & gtk.gdk.SHIFT_MASK:
3028
if event.keyval == 65362:
3031
if event.keyval == 65364:
3034
if event.keyval == 65361 and self.playername == "right":
3035
treeselection = widget.get_selection()
3036
s_model, s_iter = treeselection.get_selected()
3037
if s_iter is not None:
3038
name = s_model.get_value(s_iter, 0)
3039
if name[:3] == "<b>":
3041
otherselection = self.parent.player_left.treeview.get_selection()
3042
d_model, d_iter = otherselection.get_selected()
3043
row = list(s_model[s_model.get_path(s_iter)])
3044
path = s_model.get_path(s_iter)
3045
s_model.remove(s_iter)
3046
treeselection.select_path(path)
3048
d_iter = d_model.append(row)
3050
d_iter = d_model.insert_after(d_iter, row)
3051
otherselection.select_iter(d_iter)
3052
self.parent.player_left.treeview.set_cursor(d_model.get_path(d_iter))
3053
self.parent.player_left.treeview.scroll_to_cell(d_model.get_path(d_iter), None, False)
3055
if event.keyval == 65363 and self.playername == "left":
3056
treeselection = widget.get_selection()
3057
s_model, s_iter = treeselection.get_selected()
3058
if s_iter is not None:
3059
name = s_model.get_value(s_iter, 0)
3060
if name[:3] == "<b>":
3062
otherselection = self.parent.player_right.treeview.get_selection()
3063
d_model, d_iter = otherselection.get_selected()
3064
row = list(s_model[s_model.get_path(s_iter)])
3065
path = s_model.get_path(s_iter)
3066
s_model.remove(s_iter)
3067
treeselection.select_path(path)
3069
d_iter = d_model.append(row)
3071
d_iter = d_model.insert_after(d_iter, row)
3072
otherselection.select_iter(d_iter)
3073
self.parent.player_right.treeview.set_cursor(d_model.get_path(d_iter))
3074
self.parent.player_right.treeview.scroll_to_cell(d_model.get_path(d_iter), None, False)
3076
if event.keyval == 65361 and self.playername == "right":
3077
treeselection = self.parent.player_left.treeview.get_selection()
3078
model, iter = treeselection.get_selected()
3079
if iter is not None:
3080
self.parent.player_left.treeview.set_cursor(model.get_path(iter))
3082
treeselection.select_path(0)
3083
self.parent.player_left.treeview.grab_focus()
3085
if event.keyval == 65363 and self.playername == "left":
3086
treeselection = self.parent.player_right.treeview.get_selection()
3087
model, iter = treeselection.get_selected()
3088
if iter is not None:
3089
self.parent.player_right.treeview.set_cursor(model.get_path(iter))
3091
treeselection.select_path(0)
3092
self.parent.player_right.treeview.grab_focus()
3094
if event.keyval == 65535 or event.keyval == 65439: # The Main and NK Delete keys
3095
treeselection = widget.get_selection() # Delete to cause playlist entry removal
3096
model, iter = treeselection.get_selected()
3097
if iter is not None:
3098
path = model.get_path(iter)
3100
prev = model.get_iter(path[0]-1)
3104
next = model.get_iter(path[0]+1)
3107
name = model.get_value(iter, 0)
3108
if name[:3] == "<b>":
3110
self.liststore.remove(iter)
3111
if next is not None:
3112
treeselection.select_iter(next)
3113
widget.set_cursor(model.get_path(next))
3114
self.treeview.scroll_to_cell(model.get_path(next), None, False)
3115
elif prev is not None:
3116
treeselection.select_iter(prev)
3117
widget.set_cursor(model.get_path(prev))
3118
self.treeview.scroll_to_cell(model.get_path(prev), None, False)
3120
print "Playlist is empty!"
3122
# Allow certain key presses to work but not allow a text entry box to appear.
3123
if event.string =="\r":
3127
if event.string == "":
3131
def rgrowconfig(self, tv_column, cell_renderer, model, iter):
3134
self.rowconfig(tv_column, cell_renderer, model, iter)
3135
if model.get_value(iter, 0)[0] == ">":
3136
cell_renderer.set_property("text", " ")
3138
if model.get_value(iter, 7) == RGDEF:
3140
cell_renderer.set_property("markup", '<span foreground="dark red">▵</span>')
3142
# Small green bullet point.
3143
cell_renderer.set_property("markup", '<span foreground="dark green">•</span>')
3145
def playtimerowconfig(self, tv_column, cell_renderer, model, iter):
3148
playtime = model.get_value(iter, 2)
3149
self.rowconfig(tv_column, cell_renderer, model, iter)
3150
cell_renderer.set_property("xalign", 1.0)
3152
if model.get_value(iter, 0) == ">announcement":
3153
length = model.get_value(iter, 3)[2:6]
3156
if length == "0000":
3157
cell_renderer.set_property("text", "")
3159
if length[0] == "0":
3160
length = " " + length[1] + ":" + length[2:]
3162
length = length[:2] + ":" + length[2:]
3163
cell_renderer.set_property("text", length)
3165
cell_renderer.set_property("text", "")
3167
cell_renderer.set_property("text", "? : ??")
3169
secs = playtime % 60
3171
mins = playtime / 60
3172
text = "%d:%02d" % (mins, secs)
3173
cell_renderer.set_property("text", text)
3175
def rowconfig(self, tv_column, cell_renderer, model, iter):
3178
celltext = model.get_value(iter, 0)
3179
if celltext[:4] == "<b>>":
3180
celltext = celltext[3:-4]
3181
if celltext[0] == ">":
3182
cell_renderer.set_property("xalign", 0.45)
3183
cell_renderer.set_property("ypad", 0)
3184
cell_renderer.set_property("scale", 0.75)
3185
cell_renderer.set_property("cell-background-set", True)
3186
cell_renderer.set_property("background-set", True)
3187
cell_renderer.set_property("foreground-set", True)
3188
if self.pl_mode.get_active() == 0:
3189
if celltext == ">fade10":
3190
cell_renderer.set_property("cell-background", "dark red")
3191
cell_renderer.set_property("background", "gray")
3192
cell_renderer.set_property("foreground", "dark red")
3193
# TC: Playlist control.
3194
cell_renderer.set_property("text", _('Fade 10s'))
3195
if celltext == ">fade5":
3196
cell_renderer.set_property("cell-background", "dark red")
3197
cell_renderer.set_property("background", "gray")
3198
cell_renderer.set_property("foreground", "dark red")
3199
# TC: Playlist control.
3200
cell_renderer.set_property("text", _('Fade 5s'))
3201
if celltext == ">fadenone":
3202
cell_renderer.set_property("cell-background", "dark red")
3203
cell_renderer.set_property("background", "gray")
3204
cell_renderer.set_property("foreground", "dark red")
3205
# TC: Playlist control.
3206
cell_renderer.set_property("text", _('No Fade'))
3207
if celltext == ">announcement":
3208
cell_renderer.set_property("cell-background", "dark blue")
3209
cell_renderer.set_property("background", "gray")
3210
cell_renderer.set_property("foreground", "dark blue")
3211
# TC: Playlist control.
3212
cell_renderer.set_property("text", _('Announcement'))
3213
if celltext == ">normalspeed":
3214
cell_renderer.set_property("cell-background", "dark green")
3215
cell_renderer.set_property("background", "gray")
3216
cell_renderer.set_property("foreground", "dark green")
3217
# TC: Playlist control.
3218
cell_renderer.set_property("text", _('>> Normal Speed <<'))
3219
if celltext == ">stopplayer":
3220
cell_renderer.set_property("cell-background", "red")
3221
cell_renderer.set_property("background", "gray")
3222
cell_renderer.set_property("foreground", "red")
3223
# TC: Playlist control.
3224
cell_renderer.set_property("text", _('Player stop'))
3225
if celltext == ">stopplayer2":
3226
cell_renderer.set_property("cell-background", "red")
3227
cell_renderer.set_property("background", "gray")
3228
cell_renderer.set_property("foreground", "red")
3229
# TC: Playlist control.
3230
cell_renderer.set_property("text", _('Player stop 2'))
3231
if celltext == ">jumptotop":
3232
cell_renderer.set_property("cell-background", "dark magenta")
3233
cell_renderer.set_property("background", "gray")
3234
cell_renderer.set_property("foreground", "dark magenta")
3235
# TC: Playlist control.
3236
cell_renderer.set_property("text", _('Jump To Top'))
3237
if celltext == ">stopstreaming":
3238
cell_renderer.set_property("cell-background", "black")
3239
cell_renderer.set_property("background", "gray")
3240
cell_renderer.set_property("foreground", "black")
3241
# TC: Playlist control.
3242
cell_renderer.set_property("text", _('Stop streaming'))
3243
if celltext == ">stoprecording":
3244
cell_renderer.set_property("cell-background", "black")
3245
cell_renderer.set_property("background", "gray")
3246
cell_renderer.set_property("foreground", "black")
3247
# TC: Playlist control.
3248
cell_renderer.set_property("text", _('Stop recording'))
3249
if celltext == ">transfer":
3250
cell_renderer.set_property("cell-background", "magenta")
3251
cell_renderer.set_property("background", "gray")
3252
cell_renderer.set_property("foreground", "magenta")
3253
if self.playername == "left":
3254
# TC: Playlist control.
3255
cell_renderer.set_property("text", _('>>> Transfer across >>>'))
3257
# TC: Playlist control.
3258
cell_renderer.set_property("text", _('<<< Transfer across <<<'))
3259
if celltext == ">crossfade":
3260
cell_renderer.set_property("cell-background", "blue")
3261
cell_renderer.set_property("background", "gray")
3262
cell_renderer.set_property("foreground", "blue")
3263
if self.playername == "left":
3264
# TC: Playlist control.
3265
cell_renderer.set_property("text", _('>>> Fade across >>>'))
3267
# TC: Playlist control.
3268
cell_renderer.set_property("text", _('<<< Fade across <<<'))
3270
cell_renderer.set_property("cell-background", "darkgray")
3271
cell_renderer.set_property("background", "darkgray")
3272
cell_renderer.set_property("foreground", "white")
3273
# TC: Playlist control.
3274
cell_renderer.set_property("markup", "<i>%s</i>" % _("Ignored playlist control"))
3276
cell_renderer.set_property("foreground-set", False)
3277
cell_renderer.set_property("cell-background-set", False)
3278
cell_renderer.set_property("background-set", False)
3279
cell_renderer.set_property("scale", 1.0)
3280
cell_renderer.set_property("xalign", 0.0)
3281
cell_renderer.set_property("ypad", 2)
3283
def cb_playlist_delay(self, widget):
3284
print "inter track fade was changed"
3286
def cb_playlist_mode(self, widget):
3287
self.pl_delay.set_sensitive(self.pl_mode.get_active() in (0, 1, 2, 5))
3288
if widget.get_active() == 0:
3289
self.pl_statusbar.show()
3291
self.pl_statusbar.hide()
3292
if widget.get_active() == 5:
3293
self.external_pl.show()
3295
self.external_pl.hide()
3297
def popupwindow_populate(self, window, parentwidget, parent_x, parent_y):
3299
frame.set_shadow_type(gtk.SHADOW_OUT)
3303
hbox.set_border_width(10)
3307
image.set_from_file(FGlobs.pkgdatadir / "icon.png")
3310
separator = gtk.VSeparator()
3321
tracktitle = self.songname
3323
for each in self.liststore:
3327
if each[0][:3] == "<b>":
3328
tracknum = trackscount
3329
if each[0] == ">announcement":
3330
duration += int(each[3][2:4]) * 60 + int(each[3][4:6])
3332
duration, seconds = divmod(duration, 60)
3333
hours, minutes = divmod(duration, 60)
3334
hms = hours and "%d:%02d:%02d" % (hours, minutes, seconds) or "%d:%02d" % (minutes, seconds)
3336
label1 = gtk.Label(_('Playing track {0} of {1}').format(tracknum, trackscount))
3340
blank = gtk.Label("")
3343
label2 = gtk.Label(tracktitle)
3347
# TC: Previous line: Playing track {0} of {1}
3348
label3 = gtk.Label(_('From the album, %s') % self.album)
3351
blank = gtk.Label("")
3355
label3 = gtk.Label(_('Total number of tracks %d') % trackscount)
3359
label4 = gtk.Label(_('Total play duration %s') % hms)
3361
label4 = gtk.Label(_('Total play duration %s'))
3367
def popupwindow_inhibit(self):
3368
return self.pl_menu.flags() & gtk.MAPPED # we don't want a popup window when there is a popup menu around
3370
def pl_mode_data_function(self, celllayout, cell, model, iter):
3371
cell.props.text = t.gettext(model.get_value(iter, 0))
3373
def __init__(self, pbox, name, parent):
3374
self.parent = parent
3375
self.exiting = False # used to stop the running of problem code - don't ask
3376
# A box for the Stop/Start/Pause widgets
3377
self.hbox1 = gtk.HBox(True, 0)
3378
self.hbox1.set_border_width(2)
3379
self.hbox1.set_spacing(3)
3381
frame.set_border_width(3)
3382
frame.set_shadow_type(gtk.SHADOW_IN)
3383
frame.add(self.hbox1)
3385
pbox.pack_start(frame, False, False, 0)
3387
# A box for the progress bar, which may at a later date contain other stuff
3388
# Like an elapsed timer for instance.
3389
self.progressbox = gtk.HBox(False, 0)
3390
self.progressbox.set_border_width(3)
3391
self.progressbox.set_spacing(4)
3392
pbox.pack_start(self.progressbox, False, False, 0)
3394
# The numerical play progress box
3395
self.digiprogress = gtk.Entry()
3396
self.digiprogress.set_text("0:00:00")
3397
self.digiprogress.set_width_chars(6)
3398
self.digiprogress.set_editable(False)
3399
self.digiprogress.connect("button_press_event", self.cb_event, "DigitalProgressPress")
3400
self.progressbox.pack_start(self.digiprogress, False, False, 1)
3401
self.digiprogress.show()
3402
set_tip(self.digiprogress, _('Left click toggles between showing the amount of time elapsed or remaining on the current track being played.'))
3404
# The play progress and seek bar
3405
self.progressadj = gtk.Adjustment(0.0, 0.0, 100.0, 0.1, 1.0, 0.0)
3406
self.progressadj.connect("value_changed", self.cb_progress)
3407
self.progressbar = gtk.HScale(self.progressadj)
3408
self.progressbar.set_update_policy(gtk.UPDATE_CONTINUOUS)
3409
self.progressbar.set_digits(1)
3410
self.progressbar.set_value_pos(gtk.POS_TOP)
3411
self.progressbar.set_draw_value(False)
3412
self.progressbar.connect("button_press_event", self.cb_event, "ProgressPress")
3413
self.progressbar.connect("button_release_event", self.cb_event, "ProgressRelease")
3414
self.progressbox.pack_start(self.progressbar, True, True, 0)
3415
self.progressbar.show()
3416
set_tip(self.progressbar, _('This slider acts as both a play progress indicator and as a means for seeking within the currently playing track.'))
3418
# Finished filling the progress box so lets show it.
3419
self.progressbox.show()
3421
# A frame for our playlist
3423
plframe = gtk.Frame(" %s " % _('Playlist 1'))
3425
plframe = gtk.Frame(" %s " % _('Playlist 2'))
3426
plframe.set_border_width(4)
3427
plframe.set_shadow_type(gtk.SHADOW_ETCHED_IN)
3432
# The scrollable window box that will contain our playlist.
3433
self.scrolllist = gtk.ScrolledWindow()
3434
self.scrolllist.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS)
3435
self.scrolllist.set_size_request(-1, 117)
3436
self.scrolllist.set_border_width(4)
3437
self.scrolllist.set_shadow_type(gtk.SHADOW_IN)
3438
# A liststore object for our playlist
3439
# The first one gets rendered and is derived from id3 tags or just is the filename
3440
# when the id3 tag is not sufficient.
3441
# The second one always is the filename and is passed to the player.
3442
self.liststore = gtk.ListStore(str, str, int, str, str, str, str, float, CueSheetListStore, str)
3443
self.templist = gtk.ListStore(str, str, int, str, str, str, str, float, CueSheetListStore, str)
3444
self.treeview = gtk.TreeView(self.liststore)
3445
self.rgcellrender = gtk.CellRendererText()
3446
self.playtimecellrender = gtk.CellRendererText()
3447
self.cellrender = gtk.CellRendererText()
3448
self.cellrender.set_property("ellipsize", pango.ELLIPSIZE_END)
3449
self.rgtvcolumn = gtk.TreeViewColumn("", self.rgcellrender)
3450
self.playtimetvcolumn = gtk.TreeViewColumn("Time", self.playtimecellrender)
3451
self.tvcolumn = gtk.TreeViewColumn("Playlist", self.cellrender, markup=0)
3452
self.rgtvcolumn.set_cell_data_func(self.rgcellrender, self.rgrowconfig)
3453
self.playtimetvcolumn.set_cell_data_func(self.playtimecellrender, self.playtimerowconfig)
3454
self.tvcolumn.set_cell_data_func(self.cellrender, self.rowconfig)
3455
self.playtimetvcolumn.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
3456
self.tvcolumn.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
3457
self.tvcolumn.set_expand(True)
3458
self.treeview.append_column(self.tvcolumn)
3459
self.treeview.append_column(self.playtimetvcolumn)
3460
self.treeview.set_search_column(0)
3461
self.treeview.set_headers_visible(False)
3462
self.treeview.set_enable_search(False)
3463
self.treeview.enable_model_drag_source( gtk.gdk.BUTTON1_MASK,
3465
gtk.gdk.ACTION_DEFAULT |
3466
gtk.gdk.ACTION_MOVE)
3467
self.treeview.enable_model_drag_dest( self.droptargets,
3468
gtk.gdk.ACTION_DEFAULT)
3470
self.treeview.connect("drag_data_get", self.drag_data_get_data)
3471
self.treeview.connect("drag_data_received", self.drag_data_received_data)
3472
self.treeview.connect("drag_data_delete", self.drag_data_delete)
3474
self.treeview.connect("row_activated", self.cb_doubleclick, "Double click")
3475
self.treeview.get_selection().connect("changed", self.cb_selection_changed)
3477
self.treeview.connect("key_press_event", self.cb_keypress)
3479
self.liststore.connect("row-inserted", self.cb_playlist_changed)
3480
self.liststore.connect("row-deleted", self.cb_playlist_changed)
3482
self.scrolllist.add(self.treeview)
3483
self.treeview.show()
3485
plvbox.pack_start(self.scrolllist, True, True, 0)
3486
self.scrolllist.show()
3488
# Cue sheet playlist controls.
3490
self.cuesheet_playlist = CuesheetPlaylist()
3491
plvbox.pack_start(self.cuesheet_playlist)
3493
# External playlist control unit
3494
self.external_pl = ExternalPL(self)
3495
plvbox.pack_start(self.external_pl, False, False, 0)
3497
# File filters for file dialogs
3498
self.plfilefilter_all = gtk.FileFilter()
3499
# TC: File filter text.
3500
self.plfilefilter_all.set_name(_('All file types'))
3501
self.plfilefilter_all.add_pattern("*")
3502
self.plfilefilter_playlists = gtk.FileFilter()
3503
# TC: File filter text.
3504
self.plfilefilter_playlists.set_name(_('Playlist types (*.m3u, *.xspf, *.pls)'))
3505
self.plfilefilter_playlists.add_mime_type("audio/x-mpegurl")
3506
self.plfilefilter_playlists.add_mime_type("application/xspf+xml")
3507
self.plfilefilter_playlists.add_mime_type("audio/x-scpls")
3508
self.plfilefilter_media = gtk.FileFilter()
3509
self.plfilefilter_media.set_name(_('Supported media'))
3510
for each in supported.media:
3511
self.plfilefilter_media.add_pattern("*" + each)
3512
self.plfilefilter_media.add_pattern("*" + each.upper())
3514
# An information display for playlist stats
3515
self.pl_statusbar = gtk.Statusbar()
3516
self.pl_statusbar.set_has_resize_grip(False)
3517
plvbox.pack_start(self.pl_statusbar, False, False, 0)
3518
self.pl_statusbar.show()
3519
set_tip(self.pl_statusbar, _("'Block size' indicates the amount of time that it will take to play from the currently selected track to the next stop.\n'Remaining' is the amount of time until the next stop.\n'Finish' Is the computed time when the tracks will have finished playing."))
3521
pbox.pack_start(plframe, True, True, 0)
3523
# A box for the playback speed controls
3524
self.pbspeedbox = gtk.HBox(False, 0)
3525
self.pbspeedbox.set_border_width(3)
3526
self.pbspeedbox.set_spacing(3)
3527
pbox.pack_start(self.pbspeedbox, False, False, 0)
3529
# The playback speed control
3530
self.pbspeedadj = gtk.Adjustment(0.0, -12.0, 12.0, 0.0125, 0.0, 0.0)
3531
self.pbspeedadj.connect("value_changed", self.cb_pbspeed)
3532
self.pbspeedbar = gtk.HScale(self.pbspeedadj)
3533
self.pbspeedbar.set_update_policy(gtk.UPDATE_CONTINUOUS)
3534
self.pbspeedbar.set_digits(1)
3535
self.pbspeedbar.set_value_pos(gtk.POS_TOP)
3536
self.pbspeedbar.set_draw_value(False)
3537
self.pbspeedbox.pack_start(self.pbspeedbar, True, True, 0)
3538
self.pbspeedbar.show()
3539
set_tip(self.pbspeedbar, _('This adjusts the playback speed anywhere from 25% to 400%.'))
3541
self.pbspeedzerobutton = gtk.Button()
3542
self.pbspeedzerobutton.connect("clicked", self.callback, "pbspeedzero")
3543
pixbuf = gtk.gdk.pixbuf_new_from_file(FGlobs.pkgdatadir / "speedicon.png")
3544
pixbuf = pixbuf.scale_simple(55, 14, gtk.gdk.INTERP_BILINEAR)
3546
image.set_from_pixbuf(pixbuf)
3548
self.pbspeedzerobutton.add(image)
3549
self.pbspeedbox.pack_start(self.pbspeedzerobutton, False, False, 1)
3550
self.pbspeedzerobutton.show()
3551
set_tip(self.pbspeedzerobutton, _('This sets the playback speed back to normal.'))
3553
# The box for the mute widgets.
3554
self.hbox2 = gtk.HBox(False, 0)
3555
self.hbox2.set_border_width(4)
3556
self.hbox2.set_spacing(4)
3558
frame.set_border_width(4)
3559
frame.add(self.hbox2)
3560
pbox.pack_start(frame, False, False, 0)
3563
# A set of buttons for hbox1 namely Prev/Play/Pause/Stop/Next/Playlist : XMMS order
3565
image.set_from_file(FGlobs.pkgdatadir / "prev.png")
3567
self.prev = gtk.Button()
3568
self.prev.add(image)
3569
self.prev.connect("clicked", self.callback, "Prev")
3570
self.hbox1.add(self.prev)
3572
set_tip(self.prev, _('Previous track.'))
3574
pixbuf = gtk.gdk.pixbuf_new_from_file(FGlobs.pkgdatadir / "play2.png")
3575
pixbuf = pixbuf.scale_simple(14, 14, gtk.gdk.INTERP_BILINEAR)
3577
image.set_from_pixbuf(pixbuf)
3579
self.play = gtk.ToggleButton()
3580
self.play.add(image)
3581
self.play.connect("toggled", self.cb_toggle, "Play")
3582
self.hbox1.add(self.play)
3584
set_tip(self.play, _('Play.'))
3587
image.set_from_file(FGlobs.pkgdatadir / "pause.png")
3589
self.pause = gtk.ToggleButton()
3590
self.pause.add(image)
3591
self.pause.connect("toggled", self.cb_toggle, "Pause")
3592
self.hbox1.add(self.pause)
3594
set_tip(self.pause, _('Pause.'))
3597
image.set_from_file(FGlobs.pkgdatadir / "stop.png")
3599
self.stop = gtk.Button()
3600
self.stop.add(image)
3601
self.stop.connect("clicked", self.callback, "Stop")
3602
self.hbox1.add(self.stop)
3604
set_tip(self.stop, _('Stop.'))
3607
image.set_from_file(FGlobs.pkgdatadir / "next.png")
3609
self.next = gtk.Button()
3610
self.next.add(image)
3611
self.next.connect("clicked", self.callback, "Next")
3612
self.hbox1.add(self.next)
3614
set_tip(self.next, _('Next track.'))
3616
pixbuf = gtk.gdk.pixbuf_new_from_file(FGlobs.pkgdatadir / "add3.png")
3617
pixbuf = pixbuf.scale_simple(14, 14, gtk.gdk.INTERP_HYPER)
3619
image.set_from_pixbuf(pixbuf)
3621
self.add = gtk.Button()
3623
self.add.connect("clicked", self.callback, "Add Files")
3624
self.hbox1.add(self.add)
3626
set_tip(self.add, _('Add tracks to the playlist.'))
3628
# hbox1 is done so it is time to show it
3631
# The playlist mode dropdown menu.
3633
frame = ButtonFrame(_('Playlist Mode'))
3634
self.hbox2.pack_start(frame, True, True, 0)
3637
self.pl_mode = gtk.combo_box_new_text()
3638
self.pl_mode.set_cell_data_func(self.pl_mode.get_cells()[0], self.pl_mode_data_function)
3639
self.pl_mode.append_text(N_('Play All'))
3640
self.pl_mode.append_text(N_('Loop All'))
3641
self.pl_mode.append_text(N_('Random'))
3642
self.pl_mode.append_text(N_('Manual'))
3643
self.pl_mode.append_text(N_('Cue Up'))
3644
self.pl_mode.append_text(N_('External'))
3645
self.pl_mode.append_text(N_('Alternate'))
3646
self.pl_mode.append_text(N_('Fade Over'))
3647
self.pl_mode.append_text(N_('Random Hop'))
3648
self.pl_mode.set_active(0)
3649
self.pl_mode.connect("changed", self.cb_playlist_mode)
3650
set_tip(self.pl_mode, _("This sets the playlist mode which defines player behaviour after a track has finished playing.\n\n'Play All' is the most versatile mode since it allows the use of embeddable playlist control elements which are accessible using the right click context menu in the playlist. When no playlist controls are present the tracks are played sequentially until the end of the playlist is reached at which point the player will stop.\n\n'Loop All' causes the tracks to be played in sequence, restarting with the first track once the end of the playlist is reached.\n\n'Random' causes the tracks to be played indefinitely with the tracks selected at random.\n\n'Manual' causes the player to stop at the end of each track.\n\n'Cue Up' is similar to manual except that the next track in the playlist will also be highlighted.\n\n'External' draws it's tracks from an external playlist or directory one at a time. Useful for when you want to stream massive playlists.\n\n'Alternate' causes the next track to be cued up before starting the opposite player. The crossfader is moved over.\n\n'Fade Over' will crossfade to the other player at the end of every track.\n\n'Random Hop' will pick a track at random from the other playlist."))
3652
frame.hbox.pack_start(self.pl_mode, True, True, 0)
3655
# TC: Fade time heading.
3656
frame = ButtonFrame(_('Fade'))
3657
self.hbox2.pack_start(frame, True, True, 0)
3660
self.pl_delay = gtk.combo_box_new_text()
3661
# TC: Fade time is zero. No fade, none.
3662
self.pl_delay.append_text(_('None'))
3663
self.pl_delay.append_text("5")
3664
self.pl_delay.append_text("10")
3665
self.pl_delay.set_active(0)
3666
self.pl_delay.connect("changed", self.cb_playlist_delay)
3667
set_tip(self.pl_delay, _('This controls the amount of fade between tracks.'))
3669
frame.hbox.pack_start(self.pl_delay, True, True, 0)
3670
self.pl_delay.show()
3674
# TC: The audio feed (mix) that the DJ listens to, could be DJ mix or Stream mix.
3675
frame = ButtonFrame(" %s " % _('Audio Feed'))
3676
self.hbox2.pack_start(frame, True, True, 0)
3679
self.stream = gtk.ToggleButton(" %s " % _('Stream'))
3680
self.stream.set_active(True)
3681
self.stream.connect("toggled", self.cb_toggle, "Stream")
3682
frame.hbox.pack_start(self.stream, True, True, 0)
3684
set_tip(self.stream, _('Make output from this player available for streaming.'))
3686
self.listen = nice_listen_togglebutton(" %s " % _('DJ'))
3687
self.listen.set_active(True)
3688
self.listen.connect("toggled", self.cb_toggle, "Listen")
3689
frame.hbox.pack_start(self.listen, True, True, 0)
3691
set_tip(self.listen, _('Make output from this player audible to the DJ.'))
3693
# hbox2 is now filled so lets show it
3696
# Popup menu code here
3699
self.pl_menu = gtk.Menu()
3701
# TC: Insert playlist control.
3702
self.pl_menu_control = gtk.MenuItem(_('Insert control'))
3703
self.pl_menu.append(self.pl_menu_control)
3704
self.pl_menu_control.show()
3706
separator = gtk.SeparatorMenuItem()
3707
self.pl_menu.append(separator)
3710
# TC: The Item submenu.
3711
self.pl_menu_item = gtk.MenuItem(_('Item'))
3712
self.pl_menu.append(self.pl_menu_item)
3713
self.pl_menu_item.show()
3715
# TC: The Playlist submenu.
3716
self.pl_menu_playlist = gtk.MenuItem(_('Playlist'))
3717
self.pl_menu.append(self.pl_menu_playlist)
3718
self.pl_menu_playlist.show()
3722
# Control element submenu of main popup menu
3724
self.control_menu = gtk.Menu()
3726
# TC: Insert playlist control to set playback speed to normal.
3727
self.control_normal_speed_control = gtk.MenuItem(_('Normal Speed'))
3728
self.control_normal_speed_control.connect("activate", self.menuitem_response, "Normal Speed Control")
3729
self.control_menu.append(self.control_normal_speed_control)
3730
self.control_normal_speed_control.show()
3732
# TC: Insert playlist control to stop the player.
3733
self.control_menu_stop_control = gtk.MenuItem(_('Player Stop'))
3734
self.control_menu_stop_control.connect("activate", self.menuitem_response, "Stop Control")
3735
self.control_menu.append(self.control_menu_stop_control)
3736
self.control_menu_stop_control.show()
3738
# TC: Insert playlist control to stop the player.
3739
self.control_menu_stop_control = gtk.MenuItem(_('Player Stop 2'))
3740
self.control_menu_stop_control.connect("activate", self.menuitem_response, "Stop Control 2")
3741
self.control_menu.append(self.control_menu_stop_control)
3742
self.control_menu_stop_control.show()
3744
# TC: Insert playlist control to jump to the top of the playlist.
3745
self.control_menu_jumptop_control = gtk.MenuItem(_('Jump To Top'))
3746
self.control_menu_jumptop_control.connect("activate", self.menuitem_response, "Jump To Top Control")
3747
self.control_menu.append(self.control_menu_jumptop_control)
3748
self.control_menu_jumptop_control.show()
3750
# TC: Insert playlist control to transfer to the opposite player.
3751
self.control_menu_transfer_control = gtk.MenuItem(_('Transfer'))
3752
self.control_menu_transfer_control.connect("activate", self.menuitem_response, "Transfer Control")
3753
self.control_menu.append(self.control_menu_transfer_control)
3754
self.control_menu_transfer_control.show()
3756
# TC: Insert playlist control to crossfade to the opposite player.
3757
self.control_menu_crossfade_control = gtk.MenuItem(_('Crossfade'))
3758
self.control_menu_crossfade_control.connect("activate", self.menuitem_response, "Crossfade Control")
3759
self.control_menu.append(self.control_menu_crossfade_control)
3760
self.control_menu_crossfade_control.show()
3762
# TC: Embed a DJ announcement text into the playlist.
3763
self.control_menu_announcement_control = gtk.MenuItem(_('Announcement'))
3764
self.control_menu_announcement_control.connect("activate", self.menuitem_response, "Announcement Control")
3765
self.control_menu.append(self.control_menu_announcement_control)
3766
self.control_menu_announcement_control.show()
3768
separator = gtk.SeparatorMenuItem()
3769
self.control_menu.append(separator)
3772
# TC: Insert playlist control to do a ten second fade to the next track.
3773
self.control_menu_fade_10_control = gtk.MenuItem(_('Fade 10s'))
3774
self.control_menu_fade_10_control.connect("activate", self.menuitem_response, "Fade 10")
3775
self.control_menu.append(self.control_menu_fade_10_control)
3776
self.control_menu_fade_10_control.show()
3778
# TC: Insert playlist control to do a five second fade to the next track.
3779
self.control_menu_fade_5_control = gtk.MenuItem(_('Fade 5s'))
3780
self.control_menu_fade_5_control.connect("activate", self.menuitem_response, "Fade 5")
3781
self.control_menu.append(self.control_menu_fade_5_control)
3782
self.control_menu_fade_5_control.show()
3784
# TC: Insert playlist control to not do a fade to the next track.
3785
self.control_menu_fade_none_control = gtk.MenuItem(_('No Fade'))
3786
self.control_menu_fade_none_control.connect("activate", self.menuitem_response, "Fade none")
3787
self.control_menu.append(self.control_menu_fade_none_control)
3788
self.control_menu_fade_none_control.show()
3790
separator = gtk.SeparatorMenuItem()
3791
self.control_menu.append(separator)
3794
# TC: Insert playlist control to stop all the streams.
3795
self.control_menu_stream_disconnect_control = gtk.MenuItem(_('Stop streaming'))
3796
self.control_menu_stream_disconnect_control.connect("activate", self.menuitem_response, "Stream Disconnect Control")
3797
self.control_menu.append(self.control_menu_stream_disconnect_control)
3798
self.control_menu_stream_disconnect_control.show()
3800
# TC: Insert playlist control to stop all recording.
3801
self.control_menu_stop_recording_control = gtk.MenuItem(_('Stop recording'))
3802
self.control_menu_stop_recording_control.connect("activate", self.menuitem_response, "Stop Recording Control")
3803
self.control_menu.append(self.control_menu_stop_recording_control)
3804
self.control_menu_stop_recording_control.show()
3806
self.pl_menu_control.set_submenu(self.control_menu)
3807
self.control_menu.show()
3809
# Item submenu of main popup menu
3810
self.item_menu = gtk.Menu()
3812
# TC: Menu item. Opens the metadata tagger on the selected track.
3813
self.item_tag = gtk.MenuItem(_('Meta Tag'))
3814
self.item_tag.connect("activate", self.menuitem_response, "MetaTag")
3815
self.item_menu.append(self.item_tag)
3816
self.item_tag.show()
3818
# TC: Menu Item. Duplicates the selected track in the playlist.
3819
self.item_duplicate = gtk.MenuItem(_('Duplicate'))
3820
self.item_duplicate.connect("activate", self.menuitem_response, "Duplicate")
3821
self.item_menu.append(self.item_duplicate)
3822
self.item_duplicate.show()
3824
# TC: Menu Item. Remove the selected track.
3825
self.item_remove = gtk.MenuItem(_('Remove'))
3826
self.item_menu.append(self.item_remove)
3827
self.item_remove.show()
3829
# TC: Menu Item. The selected track is copied to the jingles catalogue.
3830
self.item_tojingles = gtk.MenuItem(_('Add To Jingles'))
3831
self.item_tojingles.connect("activate", self.menuitem_response, "ToJingles")
3832
self.item_menu.append(self.item_tojingles)
3833
self.item_tojingles.show()
3835
self.pl_menu_item.set_submenu(self.item_menu)
3836
self.item_menu.show()
3838
# Remove submenu of Item submenu
3839
self.remove_menu = gtk.Menu()
3841
# TC: Submenu Item. Parent menu item is Remove.
3842
self.remove_this = gtk.MenuItem(_('This'))
3843
self.remove_this.connect("activate", self.menuitem_response, "Remove This")
3844
self.remove_menu.append(self.remove_this)
3845
self.remove_this.show()
3847
# TC: Submenu Item. Parent menu item is Remove.
3848
self.remove_all = gtk.MenuItem(_('All'))
3849
self.remove_all.connect("activate", self.menuitem_response, "Remove All")
3850
self.remove_menu.append(self.remove_all)
3851
self.remove_all.show()
3853
# TC: Submenu Item. Parent menu item is Remove.
3854
self.remove_from_here = gtk.MenuItem(_('From Here'))
3855
self.remove_from_here.connect("activate", self.menuitem_response, "Remove From Here")
3856
self.remove_menu.append(self.remove_from_here)
3857
self.remove_from_here.show()
3859
# TC: Submenu Item. Parent menu item is Remove.
3860
self.remove_to_here = gtk.MenuItem(_('To Here'))
3861
self.remove_to_here.connect("activate", self.menuitem_response, "Remove To Here")
3862
self.remove_menu.append(self.remove_to_here)
3863
self.remove_to_here.show()
3865
self.item_remove.set_submenu(self.remove_menu)
3866
self.remove_menu.show()
3868
# Playlist submenu of main popup menu.
3869
self.playlist_menu = gtk.Menu()
3871
# TC: Open the file dialog for adding music to the chosen playlist.
3872
self.playlist_add_file = gtk.MenuItem(_('Add Music'))
3873
self.playlist_add_file.connect("activate", self.menuitem_response, "Add File")
3874
self.playlist_menu.append(self.playlist_add_file)
3875
self.playlist_add_file.show()
3877
# TC: Submenu Item. Parent menu is Playlist.
3878
self.playlist_save = gtk.MenuItem(_('Save'))
3879
self.playlist_save.connect("activate", self.menuitem_response, "Playlist Save")
3880
self.playlist_menu.append(self.playlist_save)
3881
self.playlist_save.show()
3883
separator = gtk.SeparatorMenuItem()
3884
self.playlist_menu.append(separator)
3887
# TC: Submenu Item. Parent menu is Playlist.
3888
self.playlist_copy = gtk.MenuItem(_('Copy'))
3889
self.playlist_menu.append(self.playlist_copy)
3890
self.playlist_copy.show()
3892
# TC: Submenu Item. Parent menu is Playlist.
3893
self.playlist_transfer = gtk.MenuItem(_('Transfer'))
3894
self.playlist_menu.append(self.playlist_transfer)
3895
self.playlist_transfer.show()
3897
# TC: Submenu Item. Parent menu is Playlist.
3898
self.playlist_exchange = gtk.MenuItem(_('Exchange'))
3899
self.playlist_exchange.connect("activate", self.menuitem_response, "Playlist Exchange")
3900
self.playlist_menu.append(self.playlist_exchange)
3901
self.playlist_exchange.show()
3903
# TC: Submenu Item. Parent menu is Playlist.
3904
self.playlist_empty = gtk.MenuItem(_('Empty'))
3905
self.playlist_empty.connect("activate", self.menuitem_response, "Remove All")
3906
self.playlist_menu.append(self.playlist_empty)
3907
self.playlist_empty.show()
3909
self.pl_menu_playlist.set_submenu(self.playlist_menu)
3910
self.playlist_menu.show()
3912
# Position Submenu of Playlist-Copy menu item
3914
self.copy_menu = gtk.Menu()
3916
# TC: Submenu Item. Parent menus are Playlist->Copy.
3917
self.copy_append = gtk.MenuItem(_('Append'))
3918
self.copy_append.connect("activate", self.menuitem_response, "Copy Append")
3919
self.copy_menu.append(self.copy_append)
3920
self.copy_append.show()
3922
# TC: Submenu Item. Parent menus are Playlist->Copy.
3923
self.copy_prepend = gtk.MenuItem(_('Prepend'))
3924
self.copy_prepend.connect("activate", self.menuitem_response, "Copy Prepend")
3925
self.copy_menu.append(self.copy_prepend)
3926
self.copy_prepend.show()
3928
separator = gtk.SeparatorMenuItem()
3929
self.copy_menu.append(separator)
3932
# TC: Submenu Item. Parent menus are Playlist->Copy.
3933
self.copy_append_cursor = gtk.MenuItem(_('Append Cursor'))
3934
self.copy_append_cursor.connect("activate", self.menuitem_response, "Copy Append Cursor")
3935
self.copy_menu.append(self.copy_append_cursor)
3936
self.copy_append_cursor.show()
3938
# TC: Submenu Item. Parent menus are Playlist->Copy.
3939
self.copy_prepend_cursor = gtk.MenuItem(_('Prepend Cursor'))
3940
self.copy_prepend_cursor.connect("activate", self.menuitem_response, "Copy Prepend Cursor")
3941
self.copy_menu.append(self.copy_prepend_cursor)
3942
self.copy_prepend_cursor.show()
3944
self.playlist_copy.set_submenu(self.copy_menu)
3945
self.copy_menu.show()
3947
# Position Submenu of Playlist-Transfer menu item
3949
self.transfer_menu = gtk.Menu()
3951
# TC: Submenu Item. Parent menus are Playlist->Transfer.
3952
self.transfer_append = gtk.MenuItem(_('Append'))
3953
self.transfer_append.connect("activate", self.menuitem_response, "Transfer Append")
3954
self.transfer_menu.append(self.transfer_append)
3955
self.transfer_append.show()
3957
# TC: Submenu Item. Parent menus are Playlist->Transfer.
3958
self.transfer_prepend = gtk.MenuItem(_('Prepend'))
3959
self.transfer_prepend.connect("activate", self.menuitem_response, "Transfer Prepend")
3960
self.transfer_menu.append(self.transfer_prepend)
3961
self.transfer_prepend.show()
3963
separator = gtk.SeparatorMenuItem()
3964
self.transfer_menu.append(separator)
3967
# TC: Submenu Item. Parent menus are Playlist->Transfer.
3968
self.transfer_append_cursor = gtk.MenuItem(_('Append at Cursor'))
3969
self.transfer_append_cursor.connect("activate", self.menuitem_response, "Transfer Append Cursor")
3970
self.transfer_menu.append(self.transfer_append_cursor)
3971
self.transfer_append_cursor.show()
3973
# TC: Submenu Item. Parent menus are Playlist->Transfer.
3974
self.transfer_prepend_cursor = gtk.MenuItem(_('Prepend at Cursor'))
3975
self.transfer_prepend_cursor.connect("activate", self.menuitem_response, "Transfer Prepend Cursor")
3976
self.transfer_menu.append(self.transfer_prepend_cursor)
3977
self.transfer_prepend_cursor.show()
3979
self.playlist_transfer.set_submenu(self.transfer_menu)
3980
self.transfer_menu.show()
3983
self.treeview.connect_object("event", self.menu_activate, self.pl_menu)
3984
popupwindow.PopupWindow(self.treeview, 12, 120, 10, self.popupwindow_populate, self.popupwindow_inhibit)
3987
self.playername = name
3988
self.showing_file_requester = False
3989
self.showing_pl_save_requester = False
3991
# Get the users home directory and test that the environment variable is okay.
3992
# Actually this is more than bullet proof because when HOME is wrong I cant even run it.
3993
self.home = os.getenv("HOME")
3994
if os.path.exists(self.home):
3995
if os.path.isdir(self.home) == False:
3996
self.home = os.path.basename(self.home)
3998
# Make sure there is a trailing slash or the filerequester will mess up a bit.
3999
if self.home[-1:] != '/':
4000
self.home = self.home + '/'
4002
self.file_requester_start_dir = slot_object(self.home)
4003
self.plsave_filetype = 0
4004
self.plsave_open = False
4005
self.plsave_filtertype = self.plfilefilter_all
4006
self.plsave_folder = None
4008
# This is used for mouse-click debounce when deleting files in the playlist.
4009
self.last_time = time.time()
4011
# This flag symbolises if we are playing music or not.
4012
self.is_playing = False
4013
self.is_paused = False
4014
self.is_stopping = False
4015
self.player_is_playing = False
4016
self.new_title = False
4017
self.timeout_source_id = 0
4018
self.progress_press = False
4020
# The maximum value from the progress bar at startup
4021
self.max_seek = 100.0
4022
self.reselect_please = False
4023
self.reselect_cursor_please = False
4029
self.gapless = False
4030
self.seek_file_valid = False
4031
self.digiprogress_type = 0
4032
self.digiprogress_f = 0
4033
self.handle_motion_as_drop = False
4034
self.other_player_initiated = False
4035
self.crossfader_initiated = False
4036
self.music_filename = ""
4037
self.session_filename = pm.basedir / (self.playername + "_session")
4038
self.oldstatusbartext = ""
4039
self.pbspeedfactor = 1.0
4040
self.playlist_changed = True
4042
self.playlist_todo = deque()
4043
self.no_more_files = False