~ubuntu-branches/ubuntu/karmic/quodlibet/karmic

« back to all changes in this revision

Viewing changes to browsers/playlists.py

  • Committer: Bazaar Package Importer
  • Author(s): Luca Falavigna
  • Date: 2009-01-30 23:55:34 UTC
  • mfrom: (1.1.12 upstream)
  • Revision ID: james.westby@ubuntu.com-20090130235534-l4e72ulw0vqfo17w
Tags: 2.0-1ubuntu1
* Merge from Debian experimental (LP: #276856), remaining Ubuntu changes:
  + debian/patches/40-use-music-profile.patch:
    - Use the "Music and Movies" pipeline per default.
* Refresh the above patch for new upstream release.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
# Copyright 2005 Joe Wreschnig
3
 
#
4
 
# This program is free software; you can redistribute it and/or modify
5
 
# it under the terms of the GNU General Public License version 2 as
6
 
# published by the Free Software Foundation
7
 
#
8
 
# $Id: playlists.py 3921 2006-10-21 03:54:54Z piman $
9
 
 
10
 
import os
11
 
import urllib
12
 
 
13
 
import gobject
14
 
import gtk
15
 
import pango
16
 
 
17
 
import config
18
 
import const
19
 
import formats
20
 
import qltk
21
 
import stock
22
 
import util
23
 
 
24
 
from tempfile import NamedTemporaryFile
25
 
 
26
 
from browsers._base import Browser
27
 
from formats._audio import AudioFile
28
 
from library import library
29
 
from qltk.songsmenu import SongsMenu
30
 
from qltk.views import RCMHintedTreeView
31
 
from qltk.wlw import WaitLoadWindow
32
 
from util.uri import URI
33
 
 
34
 
PLAYLISTS = os.path.join(const.USERDIR, "playlists")
35
 
if not os.path.isdir(PLAYLISTS): util.mkdir(PLAYLISTS)
36
 
 
37
 
def ParseM3U(filename):
38
 
    plname = util.fsdecode(os.path.basename(
39
 
        os.path.splitext(filename)[0])).encode('utf-8')
40
 
    filenames = []
41
 
    for line in file(filename):
42
 
        line = line.strip()
43
 
        if line.startswith("#"): continue
44
 
        else: filenames.append(line)
45
 
    return __ParsePlaylist(plname, filename, filenames)
46
 
 
47
 
def ParsePLS(filename, name=""):
48
 
    plname = util.fsdecode(os.path.basename(
49
 
        os.path.splitext(filename)[0])).encode('utf-8')
50
 
    filenames = []
51
 
    for line in file(filename):
52
 
        line = line.strip()
53
 
        if not line.lower().startswith("file"): continue
54
 
        else:
55
 
            try: line = line[line.index("=")+1:].strip()
56
 
            except ValueError: pass
57
 
            else: filenames.append(line)
58
 
    return __ParsePlaylist(plname, filename, filenames)
59
 
 
60
 
def __ParsePlaylist(name, plfilename, files):
61
 
    playlist = Playlist.new(name)
62
 
    songs = []
63
 
    win = WaitLoadWindow(
64
 
        None, len(files), _("Importing playlist.\n\n%d/%d songs added."),
65
 
        (0, 0))
66
 
    for i, filename in enumerate(files):
67
 
        try: uri = URI(filename)
68
 
        except ValueError:
69
 
            # Plain filename.
70
 
            filename = os.path.realpath(os.path.join(
71
 
                os.path.dirname(plfilename), filename))
72
 
            if filename in library: songs.append(library[filename])
73
 
            else: songs.append(formats.MusicFile(filename))
74
 
        else:
75
 
            if uri.scheme == "file":
76
 
                # URI-encoded local filename.
77
 
                filename = os.path.realpath(os.path.join(
78
 
                    os.path.dirname(plfilename), uri.filename))
79
 
                if filename in library: songs.append(library[filename])
80
 
                else: songs.append(formats.MusicFile(filename))
81
 
            else:
82
 
                # Who knows! Hand it off to GStreamer.
83
 
                songs.append(formats.remote.RemoteFile(uri))
84
 
        if win.step(i, len(files)): break
85
 
    win.destroy()
86
 
    playlist.extend(filter(None, songs))
87
 
    return playlist
88
 
 
89
 
class Playlist(list):
90
 
    quote = staticmethod(lambda text: urllib.quote(text, safe=""))    
91
 
    unquote = staticmethod(urllib.unquote)
92
 
 
93
 
    def new(klass, base=_("New Playlist")):
94
 
        p = Playlist("")
95
 
        i = 0
96
 
        try: p.rename(base)
97
 
        except ValueError:
98
 
            while not p.name:
99
 
                i += 1
100
 
                try: p.rename("%s %d" % (base, i))
101
 
                except ValueError: pass
102
 
        return p
103
 
    new = classmethod(new)
104
 
 
105
 
    def fromsongs(klass, songs):
106
 
        if len(songs) == 1: title = songs[0].comma("title")
107
 
        else: title = _("%(title)s and %(count)d more") % (
108
 
                {'title':songs[0].comma("title"), 'count':len(songs) - 1})
109
 
        playlist = klass.new(title)
110
 
        playlist.extend(songs)
111
 
        return playlist
112
 
    fromsongs = classmethod(fromsongs)
113
 
 
114
 
    def __init__(self, name):
115
 
        super(Playlist, self).__init__()
116
 
        if isinstance(name, unicode): name = name.encode('utf-8')
117
 
        self.name = name
118
 
        basename = self.quote(name)
119
 
        try:
120
 
            for line in file(os.path.join(PLAYLISTS, basename), "r"):
121
 
                line = line.rstrip()
122
 
                if line in library: self.append(library[line])
123
 
                elif library.masked(line): self.append(line)
124
 
        except IOError:
125
 
            if self.name: self.write()
126
 
 
127
 
    def rename(self, newname):
128
 
        if isinstance(newname, unicode): newname = newname.encode('utf-8')
129
 
        if newname == self.name: return
130
 
        elif os.path.exists(os.path.join(PLAYLISTS, self.quote(newname))):
131
 
            raise ValueError(
132
 
                _("A playlist named %s already exists.") % newname)
133
 
        else:
134
 
            try: os.unlink(os.path.join(PLAYLISTS, self.quote(self.name)))
135
 
            except EnvironmentError: pass
136
 
            self.name = newname
137
 
            self.write()
138
 
 
139
 
    def add_songs(self, filenames):
140
 
        changed = False
141
 
        for i in range(len(self)):
142
 
            if isinstance(self[i], basestring) and self[i] in filenames:
143
 
                self[i] = library[self[i]]
144
 
                changed = True
145
 
        return changed
146
 
 
147
 
    def remove_songs(self, songs):
148
 
        changed = False
149
 
        for song in songs:
150
 
            if library.masked(song("~filename")):
151
 
                while True:
152
 
                    try: self[self.index(song)] = song("~filename")
153
 
                    except ValueError: break
154
 
                    else: changed = True
155
 
            else:
156
 
                while song in self: self.remove(song)
157
 
                else: changed = True
158
 
        return changed
159
 
 
160
 
    def delete(self):
161
 
        del(self[:])
162
 
        try: os.unlink(os.path.join(PLAYLISTS, self.quote(self.name)))
163
 
        except EnvironmentError: pass
164
 
 
165
 
    def write(self):
166
 
        basename = self.quote(self.name)
167
 
        f = file(os.path.join(PLAYLISTS, basename), "w")
168
 
        for song in self:
169
 
            try: f.write(song("~filename") + "\n")
170
 
            except TypeError: f.write(song + "\n")
171
 
        f.close()
172
 
 
173
 
    def format(self):
174
 
        return "<b>%s</b>\n<small>%s (%s)</small>" % (
175
 
            util.escape(self.name),
176
 
            ngettext("%d song", "%d songs", len(self)) % len(self),
177
 
            util.format_time(sum([t.get("~#length") for t in self
178
 
                                  if isinstance(t, AudioFile)])))
179
 
 
180
 
    def __cmp__(self, other):
181
 
        try: return cmp(self.name, other.name)
182
 
        except AttributeError: return -1
183
 
 
184
 
class Menu(gtk.Menu):
185
 
    def __init__(self, songs):
186
 
        super(Menu, self).__init__()
187
 
        i = gtk.MenuItem(_("_New Playlist"))
188
 
        i.connect_object('activate', self.__add_to_playlist, None, songs)
189
 
        self.append(i)
190
 
        self.append(gtk.SeparatorMenuItem())
191
 
        self.set_size_request(int(i.size_request()[0] * 2), -1)
192
 
 
193
 
        for playlist in Playlists.playlists():
194
 
            i = gtk.MenuItem(playlist.name)
195
 
            i.child.set_ellipsize(pango.ELLIPSIZE_END)
196
 
            i.connect_object(
197
 
                'activate', self.__add_to_playlist, playlist, songs)
198
 
            self.append(i)
199
 
 
200
 
    def __add_to_playlist(playlist, songs):
201
 
        if playlist is None:
202
 
            if len(songs) == 1: title = songs[0].comma("title")
203
 
            else: title = _("%(title)s and %(count)d more") % (
204
 
                {'title':songs[0].comma("title"), 'count':len(songs) - 1})
205
 
            playlist = Playlist.new(title)
206
 
        playlist.extend(songs)
207
 
        Playlists.changed(playlist)
208
 
    __add_to_playlist = staticmethod(__add_to_playlist)
209
 
 
210
 
class Playlists(gtk.VBox, Browser):
211
 
    __gsignals__ = Browser.__gsignals__
212
 
    expand = qltk.RHPaned
213
 
 
214
 
    name = _("Playlists")
215
 
    accelerated_name = _("_Playlists")
216
 
    priority = 2
217
 
    replaygain_profiles = ["track"]
218
 
 
219
 
    def init(klass, library):
220
 
        model = klass.__lists.get_model()
221
 
        for playlist in os.listdir(PLAYLISTS):
222
 
            try: model.append(row=[Playlist(Playlist.unquote(playlist))])
223
 
            except EnvironmentError: pass
224
 
        library.connect('removed', klass.__removed)
225
 
        library.connect('added', klass.__added)
226
 
        library.connect('changed', klass.__changed)
227
 
    init = classmethod(init)
228
 
 
229
 
    def playlists(klass): return [row[0] for row in klass.__lists]
230
 
    playlists = classmethod(playlists)
231
 
 
232
 
    def changed(klass, playlist, refresh=True):
233
 
        model = klass.__lists
234
 
        for row in model:
235
 
            if row[0] is playlist:
236
 
                if refresh:
237
 
                    klass.__lists.row_changed(row.path, row.iter)
238
 
                playlist.write()
239
 
                break
240
 
        else:
241
 
            model.get_model().append(row=[playlist])
242
 
            playlist.write()
243
 
    changed = classmethod(changed)
244
 
 
245
 
    def __removed(klass, library, songs):
246
 
        for playlist in klass.playlists():
247
 
            if playlist.remove_songs(songs): Playlists.changed(playlist)
248
 
    __removed = classmethod(__removed)
249
 
 
250
 
    def __added(klass, library, songs):
251
 
        filenames = set([song("~filename") for song in songs])
252
 
        for playlist in klass.playlists():
253
 
            if playlist.add_songs(filenames):
254
 
                Playlists.changed(playlist)
255
 
    __added = classmethod(__added)
256
 
 
257
 
    def __changed(klass, library, songs):
258
 
        for playlist in klass.playlists():
259
 
            for song in songs:
260
 
                if song in playlist:
261
 
                    Playlists.changed(playlist, refresh=False)
262
 
                    break
263
 
    __changed = classmethod(__changed)
264
 
 
265
 
    def cell_data(col, render, model, iter):
266
 
        render.markup = model[iter][0].format()
267
 
        render.set_property('markup', render.markup)
268
 
    cell_data = staticmethod(cell_data)
269
 
 
270
 
    def Menu(self, songs, songlist, library):
271
 
        menu = super(Playlists, self).Menu(songs, songlist, library)
272
 
        model, rows = songlist.get_selection().get_selected_rows()
273
 
        iters = map(model.get_iter, rows)
274
 
        i = qltk.MenuItem(_("_Remove from Playlist"), gtk.STOCK_REMOVE)
275
 
        i.connect_object('activate', self.__remove, iters, model)
276
 
        i.set_sensitive(bool(self.__view.get_selection().get_selected()[1]))
277
 
        menu.preseparate()
278
 
        menu.prepend(i)
279
 
        return menu
280
 
 
281
 
    __lists = gtk.TreeModelSort(gtk.ListStore(object))
282
 
    __lists.set_default_sort_func(lambda m, a, b: cmp(m[a][0], m[b][0]))
283
 
 
284
 
    def __init__(self, library, player):
285
 
        super(Playlists, self).__init__(spacing=6)
286
 
        self.__main = bool(player)
287
 
        self.__view = view = RCMHintedTreeView()
288
 
        self.__view.set_enable_search(True)
289
 
        self.__view.set_search_column(0)
290
 
        self.__view.set_search_equal_func(
291
 
            lambda model, col, key, iter:
292
 
            not model[iter][col].name.lower().startswith(key.lower()))
293
 
        self.__render = render = gtk.CellRendererText()
294
 
        render.set_property('ellipsize', pango.ELLIPSIZE_END)
295
 
        render.connect('editing-started', self.__start_editing)
296
 
        render.connect('edited', self.__edited)
297
 
        col = gtk.TreeViewColumn("Playlists", render)
298
 
        col.set_cell_data_func(render, Playlists.cell_data)
299
 
        view.append_column(col)
300
 
        view.set_model(self.__lists)
301
 
        view.set_rules_hint(True)
302
 
        view.set_headers_visible(False)
303
 
        swin = gtk.ScrolledWindow()
304
 
        swin.set_shadow_type(gtk.SHADOW_IN)
305
 
        swin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
306
 
        swin.add(view)
307
 
        self.pack_start(swin)
308
 
 
309
 
        newpl = gtk.Button(stock=gtk.STOCK_NEW)
310
 
        newpl.connect('clicked', self.__new_playlist)
311
 
        importpl = qltk.Button(_("_Import"), gtk.STOCK_ADD)
312
 
        importpl.connect('clicked', self.__import, library)
313
 
        hb = gtk.HBox(spacing=6)
314
 
        hb.set_homogeneous(True)
315
 
        hb.pack_start(newpl)
316
 
        hb.pack_start(importpl)
317
 
        self.pack_start(hb, expand=False)
318
 
 
319
 
        view.connect('popup-menu', self.__popup_menu, library)
320
 
 
321
 
        targets = [("text/x-quodlibet-songs", gtk.TARGET_SAME_APP, 0),
322
 
                   ("text/uri-list", 0, 1),
323
 
                   ("text/x-moz-url", 0, 2)]
324
 
        view.drag_dest_set(gtk.DEST_DEFAULT_ALL, targets,
325
 
                           gtk.gdk.ACTION_COPY|gtk.gdk.ACTION_DEFAULT)
326
 
        view.connect('drag-data-received', self.__drag_data_received, library)
327
 
        view.connect('drag-motion', self.__drag_motion)
328
 
        view.connect('drag-leave', self.__drag_leave)
329
 
        if player: view.connect('row-activated', self.__play, player)
330
 
        else: render.set_property('editable', True)
331
 
        view.get_selection().connect('changed', self.activate)
332
 
 
333
 
        s = view.get_model().connect('row-changed', self.__check_current)
334
 
        self.connect_object('destroy', view.get_model().disconnect, s)
335
 
 
336
 
        self.accelerators = gtk.AccelGroup()
337
 
        keyval, mod = gtk.accelerator_parse("F2")
338
 
        self.accelerators.connect_group(keyval, mod, 0, self.__rename)
339
 
 
340
 
        self.show_all()
341
 
 
342
 
    def __rename(self, group, acceleratable, keyval, modifier):
343
 
        model, iter = self.__view.get_selection().get_selected()
344
 
        if iter:
345
 
            self.__render.set_property('editable', True)
346
 
            self.__view.set_cursor(model.get_path(iter),
347
 
                                   self.__view.get_columns()[0],
348
 
                                   start_editing=True)
349
 
 
350
 
    def __play(self, view, path, column, player):
351
 
        player.reset()
352
 
 
353
 
    def __check_current(self, model, path, iter):
354
 
        model, citer = self.__view.get_selection().get_selected()
355
 
        if citer and model.get_path(citer) == path:
356
 
            songlist = qltk.get_top_parent(self).songlist
357
 
            self.activate(resort=not songlist.is_sorted())
358
 
 
359
 
    def __drag_motion(self, view, ctx, x, y, time):
360
 
        if "text/x-quodlibet-songs" in ctx.targets:
361
 
            try: path = view.get_dest_row_at_pos(x, y)[0]
362
 
            except TypeError:
363
 
                path = (len(view.get_model()) - 1,)
364
 
                pos = gtk.TREE_VIEW_DROP_AFTER
365
 
            else: pos = gtk.TREE_VIEW_DROP_INTO_OR_AFTER
366
 
            if path > (-1,): view.set_drag_dest_row(path, pos)
367
 
            return True
368
 
        else:
369
 
            # Highlighting the view itself doesn't work.
370
 
            view.parent.drag_highlight()
371
 
            return True
372
 
 
373
 
    def __drag_leave(self, view, ctx, time):
374
 
        view.parent.drag_unhighlight()
375
 
 
376
 
    def __remove(self, iters, smodel):
377
 
        model, iter = self.__view.get_selection().get_selected()
378
 
        if iter:
379
 
            map(smodel.remove, iters)
380
 
            playlist = model[iter][0]            
381
 
            del(playlist[:])
382
 
            for row in smodel: playlist.append(row[0])
383
 
            Playlists.changed(playlist)
384
 
            self.activate()
385
 
 
386
 
    def __drag_data_received(self, view, ctx, x, y, sel, tid, etime, library):
387
 
        # TreeModelSort doesn't support GtkTreeDragDestDrop.
388
 
        view.emit_stop_by_name('drag-data-received')
389
 
        model = view.get_model()
390
 
        if tid == 0:
391
 
            filenames = sel.data.split("\x00")
392
 
            songs = filter(None, map(library.get, filenames))
393
 
            if not songs: return True
394
 
            try: path, pos = view.get_dest_row_at_pos(x, y)
395
 
            except TypeError:
396
 
                playlist = Playlist.fromsongs(songs)
397
 
                gobject.idle_add(self.__select_playlist, playlist)
398
 
            else:
399
 
                playlist = model[path][0]
400
 
                playlist.extend(songs)
401
 
            Playlists.changed(playlist)
402
 
            ctx.finish(True, False, etime)
403
 
        else:
404
 
            if tid == 1:
405
 
                uri = sel.get_uris()[0]
406
 
                name = os.path.basename(uri)
407
 
            elif tid == 2:
408
 
                uri, name = sel.data.decode('utf16', 'replace').split('\n')
409
 
            else:
410
 
                ctx.finish(False, False, etime)
411
 
                return
412
 
            name = name or os.path.basename(uri) or _("New Playlist")
413
 
            uri = uri.encode('utf-8')
414
 
            sock = urllib.urlopen(uri)
415
 
            f = NamedTemporaryFile()
416
 
            f.write(sock.read()); f.flush()
417
 
            if uri.lower().endswith('.pls'): playlist = ParsePLS(f.name)
418
 
            elif uri.lower().endswith('.m3u'): playlist = ParseM3U(f.name)
419
 
            else: playlist = None
420
 
            if playlist:
421
 
                library.add_filename(playlist)
422
 
                if name: playlist.rename(name)
423
 
                Playlists.changed(playlist)
424
 
                ctx.finish(True, False, etime)
425
 
            else:
426
 
                ctx.finish(False, False, etime)
427
 
                qltk.ErrorMessage(
428
 
                    qltk.get_top_parent(self),
429
 
                    _("Unable to import playlist"),
430
 
                    _("Quod Libet can only import playlists in the M3U "
431
 
                      "and PLS formats.")).run()
432
 
 
433
 
    def __select_playlist(self, playlist):
434
 
        view = self.__view
435
 
        model = view.get_model()
436
 
        for row in model:
437
 
            if row[0] is playlist:
438
 
                view.get_selection().select_iter(row.iter)
439
 
 
440
 
    def __popup_menu(self, view, library):
441
 
        model, iter = view.get_selection().get_selected()
442
 
        if iter is None:
443
 
            return
444
 
        songs = list(model[iter][0])
445
 
        menu = SongsMenu(library, songs, playlists=False, remove=False)
446
 
        menu.preseparate()
447
 
 
448
 
        rem = gtk.ImageMenuItem(gtk.STOCK_DELETE)
449
 
        def remove(model, iter):
450
 
            model[iter][0].delete()
451
 
            model.get_model().remove(
452
 
                model.convert_iter_to_child_iter(None, iter))
453
 
        rem.connect_object('activate', remove, model, iter)
454
 
        menu.prepend(rem)
455
 
 
456
 
        ren = gtk.ImageMenuItem(stock.RENAME)
457
 
        keyval, mod = gtk.accelerator_parse("F2")
458
 
        ren.add_accelerator(
459
 
            'activate', self.accelerators, keyval, mod, gtk.ACCEL_VISIBLE)
460
 
        def rename(path):
461
 
            self.__render.set_property('editable', True)
462
 
            view.set_cursor(path, view.get_columns()[0], start_editing=True)
463
 
        ren.connect_object('activate', rename, model.get_path(iter))
464
 
        menu.prepend(ren)
465
 
 
466
 
        menu.show_all()
467
 
        return view.popup_menu(menu, 0, gtk.get_current_event_time())
468
 
 
469
 
    def activate(self, widget=None, resort=True):
470
 
        model, iter = self.__view.get_selection().get_selected()
471
 
        songs = iter and list(model[iter][0]) or []
472
 
        songs = filter(lambda s: isinstance(s, AudioFile), songs)
473
 
        name = iter and model[iter][0].name or ""
474
 
        if self.__main: config.set("browsers", "playlist", name)
475
 
        self.emit('songs-selected', songs, resort)
476
 
 
477
 
    def __new_playlist(self, activator):
478
 
        playlist = Playlist.new()
479
 
        self.__lists.get_model().append(row=[playlist])
480
 
        self.__select_playlist(playlist)
481
 
 
482
 
    def __start_editing(self, render, editable, path):
483
 
        editable.set_text(self.__lists[path][0].name)
484
 
 
485
 
    def __edited(self, render, path, newname):
486
 
        try: self.__lists[path][0].rename(newname)
487
 
        except ValueError, s:
488
 
            qltk.ErrorMessage(
489
 
                None, _("Unable to rename playlist"), s).run()
490
 
        else: self.__lists[path] = self.__lists[path]
491
 
        render.set_property('editable', not self.__main)
492
 
 
493
 
    def __import(self, activator, library):
494
 
        filt = lambda fn: fn.endswith(".pls") or fn.endswith(".m3u")
495
 
        from qltk.chooser import FileChooser
496
 
        chooser = FileChooser(self, _("Import Playlist"), filt, const.HOME)
497
 
        files = chooser.run()
498
 
        chooser.destroy()
499
 
        for filename in files:
500
 
            if filename.endswith(".m3u"):
501
 
                playlist = ParseM3U(filename)
502
 
            elif filename.endswith(".pls"):
503
 
                playlist = ParsePLS(filename)
504
 
            else:
505
 
                qltk.ErrorMessage(
506
 
                    qltk.get_top_parent(self),
507
 
                    _("Unable to import playlist"),
508
 
                    _("Quod Libet can only import playlists in the M3U "
509
 
                      "and PLS formats.")).run()
510
 
                return
511
 
            Playlists.changed(playlist)
512
 
            library.add(playlist)
513
 
 
514
 
    def restore(self):
515
 
        try: name = config.get("browsers", "playlist")
516
 
        except: pass
517
 
        else:
518
 
            for i, row in enumerate(self.__lists):
519
 
                if row[0].name == name:
520
 
                    self.__view.get_selection().select_path((i,))
521
 
                    break
522
 
 
523
 
    def reordered(self, songlist):
524
 
        songs = songlist.get_songs()
525
 
        model, iter = self.__view.get_selection().get_selected()
526
 
        if iter:
527
 
            playlist = model[iter][0]
528
 
            playlist[:] = songs
529
 
        else:
530
 
            playlist = Playlist.fromsongs(songs)
531
 
            gobject.idle_add(self.__select_playlist, playlist)
532
 
        Playlists.changed(playlist, refresh=False)
533
 
 
534
 
browsers = [Playlists]