Merge lp:~keirangtp/dockmanager/clementine_helper into lp:dockmanager

Proposed by Paweł Bara
Status: Merged
Merged at revision: 92
Proposed branch: lp:~keirangtp/dockmanager/clementine_helper
Merge into: lp:dockmanager
Diff against target: 727 lines (+696/-0)
4 files modified
metadata/Makefile.am (+1/-0)
metadata/clementine_control.py.info (+6/-0)
scripts/Makefile.am (+1/-0)
scripts/clementine_control.py (+688/-0)
To merge this branch: bzr merge lp:~keirangtp/dockmanager/clementine_helper
Reviewer Review Type Date Requested Status
Rico Tzschichholz Needs Fixing
Review via email: mp+50639@code.launchpad.net

Description of the change

Features:
- cover art as icon
- now-playing tooltip
- now-playing's current time badge
- controls (next, previous, play / pause, stop)

Is this all the Banshee helper does? I tried to mimick it (in terms of functionality at least).

To post a comment you must log in.
Revision history for this message
Rico Tzschichholz (ricotz) wrote :

Could you add the same cover-art modification like rhythmbox and banshee helpers have.

review: Needs Fixing
Revision history for this message
Paweł Bara (keirangtp) wrote :

you mean the weird vinyl overlay stuff?

Revision history for this message
Rico Tzschichholz (ricotz) wrote :

Yes, this would create a consistent behaviour and look and feel of the Mediaplayer helpers.

But infact it would also be great if you could make this helper generic and let it work for all Mediaplayer which implement the MPRIS Mediaplayer2 DBus Interface. (like Rhythmbox, Spotify, Clementine, ...)

84. By keirangtp

vinyl overlay in album covers

Revision history for this message
Paweł Bara (keirangtp) wrote :

I've just commited the code that overlays covers.

As for the generic helper, I unfortunately have no time to do it anymore. When I've started writing the helper, I wasn't working on Clementine yet but I am now. ;)

Anyway, as far as I can see there are no hardcoded dependencies on Clementine in my code (except for the "branding") so if somebody would like to generalize it - please do! It wouldn't hurt to simplify it a bit while you're at it because 600 lines for a stupid remote control is a whole lot of code!

As for me, I (and all the Clementine + Docky pair users) will be happy if you decide to include this non-generic Clementine helper into your codebase. Of course I can try to fix other things first if you have any more comments.

Revision history for this message
Paweł Bara (keirangtp) wrote :

Are there any news on this?

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'metadata/Makefile.am'
2--- metadata/Makefile.am 2010-10-19 18:14:46 +0000
3+++ metadata/Makefile.am 2011-02-23 17:37:14 +0000
4@@ -2,6 +2,7 @@
5
6 dist_script_DATA = \
7 banshee_control.py.info \
8+ clementine_control.py.info \
9 deluge_badge.py.info \
10 emesene_control.py.info \
11 gajim_badge.py.info \
12
13=== added file 'metadata/clementine_control.py.info'
14--- metadata/clementine_control.py.info 1970-01-01 00:00:00 +0000
15+++ metadata/clementine_control.py.info 2011-02-23 17:37:14 +0000
16@@ -0,0 +1,6 @@
17+[DockmanagerHelper]
18+Name=Clementine Controls
19+Description=Control Clementine media playback
20+Icon=application-x-clementine
21+AppName=clementine
22+DBusName=org.mpris.MediaPlayer2.clementine
23
24=== modified file 'scripts/Makefile.am'
25--- scripts/Makefile.am 2010-10-19 18:14:46 +0000
26+++ scripts/Makefile.am 2011-02-23 17:37:14 +0000
27@@ -2,6 +2,7 @@
28
29 dist_script_SCRIPTS = \
30 banshee_control.py \
31+ clementine_control.py \
32 deluge_badge.py \
33 emesene_control.py \
34 gajim_badge.py \
35
36=== added file 'scripts/clementine_control.py'
37--- scripts/clementine_control.py 1970-01-01 00:00:00 +0000
38+++ scripts/clementine_control.py 2011-02-23 17:37:14 +0000
39@@ -0,0 +1,688 @@
40+#!/usr/bin/env python
41+
42+#
43+# Copyright (C) 2010 Pawel Bara
44+#
45+# This program is free software: you can redistribute it and/or modify
46+# it under the terms of the GNU General Public License as published by
47+# the Free Software Foundation, either version 3 of the License, or
48+# (at your option) any later version.
49+#
50+# This program is distributed in the hope that it will be useful,
51+# but WITHOUT ANY WARRANTY; without even the implied warranty of
52+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
53+# GNU General Public License for more details.
54+#
55+# You should have received a copy of the GNU General Public License
56+# along with this program. If not, see <http://www.gnu.org/licenses/>.
57+#
58+
59+import atexit
60+import dbus
61+import gobject
62+import os
63+
64+try:
65+ import gtk
66+ from dockmanager.dockmanager import DockManagerItem, DockManagerSink
67+ from dockmanager.dockmanager import DOCKITEM_IFACE, RESOURCESDIR
68+ from signal import signal, SIGTERM
69+ from sys import exit
70+except ImportError, e:
71+ print e
72+ exit()
73+
74+
75+# cover overlay related constants
76+album_art_tmpfile = "/tmp/dockmanager_%s_clementine_control.png" % os.getenv('USERNAME')
77+
78+# 0 - none, 1 - jewel, 2 - vinyl
79+overlay = 2
80+
81+
82+freedesktop_iface_name = 'org.freedesktop.DBus.Properties'
83+
84+def connect_to_dbus_properties_changed(dbus_object, callback):
85+ """
86+ Connects the callback with the 'PropertiesChanged' signal of the
87+ dbus_object.
88+ """
89+
90+ property_iface = dbus.Interface(dbus_object, dbus_interface=freedesktop_iface_name)
91+ return property_iface.connect_to_signal('PropertiesChanged', callback)
92+
93+def get_dbus_property(dbus_object, property_name):
94+ """
95+ Gets current value of the property property_name from the dbus_object.
96+ """
97+
98+ property_iface = dbus.Interface(dbus_object, dbus_interface=freedesktop_iface_name)
99+ return property_iface.Get(dbus_object.dbus_interface, property_name)
100+
101+
102+class ClementineItem(DockManagerItem):
103+ """
104+ Clementine's helper. It's working while Clementine's on and pauses when
105+ Clementine is being closed.
106+
107+ It's functionalities are:
108+ - control menu for Clementine. It always has the "next", "previous" and
109+ "stop" controls. It also has a "play" or "pause" control, depending on
110+ the current PlaybackStatus of Clementine
111+ - tooltip with information about the current song
112+ - badge with the now-playing time of the current song
113+ - the current cover art as the Clementine's icon on the dock
114+ """
115+
116+ def __init__(self, sink, path):
117+ DockManagerItem.__init__(self, sink, path)
118+
119+ self.timer = None
120+
121+ self.synch_counter = 0
122+ self.pos = 0
123+
124+ self.clementine = ClementineDBusFacade(self.turn_helper_on,
125+ self.turn_helper_off,
126+ self.on_playback_status_changed,
127+ self.on_song_changed,
128+ self.on_cover_art_changed,
129+ self.on_seeked)
130+
131+ def turn_helper_on(self):
132+ """
133+ Effectively turn's helper on (after every Clementine's
134+ startup).
135+ """
136+
137+ self.prepare_menu()
138+
139+ self.update_tooltip()
140+
141+ self.update_badge()
142+ self.set_timer()
143+
144+ self.change_cover_art()
145+
146+ def turn_helper_off(self):
147+ """
148+ Effectively turn's helper off (after every Clementine's
149+ shutdown).
150+ """
151+
152+ self.reset_icon()
153+
154+ self.stop_timer()
155+ self.reset_badge()
156+
157+ self.reset_tooltip()
158+
159+ self.clear_menu()
160+
161+ def prepare_menu(self, is_playing=None):
162+ """
163+ Recreates the menu of the helper.
164+
165+ The is_playing flag indicates which button will be shown (play or
166+ pause). If the flag's value is not given, it will be determined by
167+ the method.
168+ """
169+
170+ self.clear_menu()
171+
172+ try:
173+
174+ if is_playing is None:
175+ is_playing = self.clementine.check_is_playing()
176+
177+ except ClementineTurnedOffError:
178+
179+ # leave menu empty if Clementine's off
180+ return
181+
182+ self.add_menu_item("Previous", "media-skip-backward")
183+
184+ if is_playing:
185+ self.add_menu_item("Pause", "media-playback-pause")
186+ else:
187+ self.add_menu_item("Play", "media-playback-start")
188+
189+ self.add_menu_item("Stop", "media-playback-stop")
190+ self.add_menu_item("Next", "media-skip-forward")
191+
192+ def clear_menu(self):
193+ """
194+ Clears menu of the helper.
195+ """
196+
197+ for k in self.id_map.keys():
198+ self.remove_menu_item(k)
199+
200+ def on_seeked(self, position):
201+ """
202+ Moves time marker when Clementine's been seeked.
203+ """
204+
205+ self.pos = position
206+ self.update_badge()
207+
208+ def update_badge(self):
209+ """
210+ Updates the helper's badge with the now-playing time. The method
211+ can be used from timers (returns boolean indicating whether
212+ something went wrong).
213+ """
214+
215+ try:
216+
217+ # synchronize position with Clementine every tenth timer's tick
218+ if(self.synch_counter % 10 == 0):
219+ self.pos = self.clementine.get_play_time()
220+ else:
221+ self.pos += 1000000
222+
223+ self.synch_counter += 1
224+
225+ except ClementineTurnedOffError:
226+
227+ # if Clementine's off, reset the badge
228+ self.reset_badge()
229+ return False
230+
231+ if self.pos == 0:
232+ self.reset_badge()
233+ else:
234+ pos = self.pos / 1000000
235+ badge_text = '%i:%02i' % (pos / 60, pos % 60)
236+
237+ self.set_badge(badge_text)
238+
239+ return True
240+
241+ def update_tooltip(self, song_info=None):
242+ """
243+ Updates the helper's tooltip with basic information about
244+ the currently playing song.
245+
246+ If the information (song's metadata) is not given,
247+ it will try to determine those itself.
248+ """
249+
250+ try:
251+ if song_info is None:
252+ song_info = self.clementine.get_current_song_metadata()
253+ except ClementineTurnedOffError:
254+ # if Clementine's off, reset the tooltip
255+ self.reset_tooltip()
256+ return
257+
258+ if self.proper_metadata(song_info):
259+ # first artist from artist's list or Unknown if there's none
260+ artist = song_info.get("xesam:artist", ["Unknown"])[0]
261+ if len(artist) == 0:
262+ artist = "Unknown"
263+
264+ # do we know the length of the song?
265+ if 'mpris:length' in song_info:
266+ # duration is in microseconds - translating it to seconds
267+ duration = song_info['mpris:length'] / 1000000
268+
269+ final_info = '%s - %s (%i:%02i)' % (artist,
270+ song_info.get("xesam:title", "Unknown"),
271+ duration / 60, duration % 60)
272+ else:
273+ final_info = '%s - %s (?)' % (artist,
274+ song_info.get("xesam:title", "Unknown"))
275+
276+ self.set_tooltip(final_info)
277+
278+ else:
279+
280+ self.reset_tooltip()
281+
282+ def proper_metadata(self, metadata):
283+ return metadata != None and len(metadata) > 0
284+
285+ def start_timer(self):
286+ """
287+ Starts the timer that updates the "current time" badge.
288+ """
289+
290+ self.stop_timer()
291+
292+ if not self.timer:
293+ self.timer = gobject.timeout_add (1000, self.update_badge)
294+
295+ def stop_timer(self):
296+ """
297+ Stops the timer that updates the "current time" badge.
298+ """
299+
300+ self.synch_counter = 0
301+ self.pos = 0
302+
303+ if self.timer:
304+ gobject.source_remove (self.timer)
305+ self.timer = None
306+
307+ def change_cover_art(self, cover_art_url=None):
308+ """
309+ Changes the helper's icon to icon from file 'cover_art_url'.
310+ """
311+
312+ try:
313+
314+ # initial phase - 'pull' the overlayed covert art's path (if any)
315+ if cover_art_url is None:
316+ cover_art_url = self.clementine.get_path_for_overlayed_cover()
317+
318+ except ClementineTurnedOffError:
319+
320+ # reset icon if Clementine's off or no song is currently playing
321+ self.reset_icon()
322+ return
323+
324+ if cover_art_url is not None:
325+ self.set_icon(cover_art_url)
326+ else:
327+ self.reset_icon()
328+
329+ def reset(self):
330+ """
331+ Resets the helper.
332+ """
333+
334+ self.reset_icon()
335+ self.stop_timer()
336+ self.reset_badge()
337+ self.reset_tooltip()
338+
339+ def on_playback_status_changed(self, new_playback_status):
340+ """
341+ The callback for changes of Clementine's PlaybackStatus.
342+ """
343+
344+ self.set_timer(new_playback_status)
345+
346+ is_playing = self.clementine.is_playing(new_playback_status)
347+
348+ # reset the helper if Clementine's been stopped
349+ if self.clementine.is_stopped(new_playback_status):
350+ self.reset()
351+
352+ # recreate the helper's menu
353+ self.prepare_menu(is_playing)
354+
355+ def set_timer(self, playback_status=None):
356+ """
357+ Turns the badge's timer on or off depending on the Clementine's
358+ current PlaybackStatus.
359+ """
360+
361+ try:
362+ if playback_status == None:
363+ is_playing = self.clementine.check_is_playing()
364+ else:
365+ is_playing = self.clementine.is_playing(playback_status)
366+
367+ # pausing the badge's timer when current song is
368+ # paused or stopped
369+ if is_playing:
370+ self.start_timer()
371+ else:
372+ self.stop_timer()
373+
374+ except ClementineTurnedOffError:
375+ # Clementine's off - stop the badge timer
376+ self.stop_timer()
377+
378+ def on_song_changed(self, new_metadata):
379+ """
380+ The callback for changes of the currently playing Clementine's song.
381+ """
382+
383+ if 'mpris:trackid' in new_metadata:
384+ self.update_tooltip(new_metadata)
385+ self.reset_badge()
386+
387+ # change the cover art too if it's already present in the message
388+ try:
389+ if 'mpris:artUrl' in new_metadata:
390+ overlayed = self.clementine.get_path_for_overlayed_cover()
391+ self.change_cover_art(overlayed)
392+ except ClementineTurnedOffError:
393+ self.reset_icon()
394+ else:
395+ self.reset()
396+
397+ def on_cover_art_changed(self, cover_art_url):
398+ """
399+ The callback for changes of the current cover art in Clementine.
400+ """
401+
402+ if cover_art_url != None:
403+ self.change_cover_art(cover_art_url)
404+ else:
405+ self.reset_icon()
406+
407+ def menu_pressed(self, menu_id):
408+ """
409+ Performs an action invoked by clicking one of the helper's context
410+ menu items.
411+ """
412+
413+ try:
414+
415+ if self.id_map[menu_id] == "Previous":
416+ self.clementine.prev()
417+ elif self.id_map[menu_id] == "Stop":
418+ self.clementine.stop()
419+ elif self.id_map[menu_id] == "Play":
420+ self.clementine.play()
421+ elif self.id_map[menu_id] == "Pause":
422+ self.clementine.pause()
423+ elif self.id_map[menu_id] == "Next":
424+ self.clementine.next()
425+
426+ except ClementineTurnedOffError:
427+
428+ # Clementine's off - do nothing
429+ return
430+
431+
432+clementine_bus_name = "org.mpris.MediaPlayer2.clementine"
433+
434+player_object_path = "/org/mpris/MediaPlayer2"
435+player_iface_name = "org.mpris.MediaPlayer2.Player"
436+
437+tlist_object_path = "/org/mpris/MediaPlayer2"
438+tlist_iface_name = "org.mpris.MediaPlayer2.Tracklist"
439+
440+class ClementineDBusFacade:
441+
442+ def __init__(self, on_callback, off_callback, playback_status_changed_callback,
443+ song_changed_callback, cover_art_changed_callback, seeked_callback):
444+ self.on_callback = on_callback
445+ self.off_callback = off_callback
446+ self.playback_status_changed_callback = playback_status_changed_callback
447+ self.song_changed_callback = song_changed_callback
448+ self.cover_art_changed_callback = cover_art_changed_callback
449+ self.seeked_callback = seeked_callback
450+
451+ self.cl_player = None
452+ self.cl_tlist = None
453+
454+ self.bus = dbus.SessionBus()
455+ # we track Clementine's on / off status
456+ self.bus.watch_name_owner(clementine_bus_name, self.on_dbus_name_change)
457+
458+ def is_on(self):
459+ """
460+ Returns a flag saying whether Clementine's on.
461+ """
462+
463+ return self.cl_player is not None
464+
465+ def prev(self):
466+ """
467+ Changes the current song to the previous one. Might throw
468+ ClementineTurnedOffError if Clementine's been turned off.
469+ """
470+
471+ if not self.is_on():
472+ raise ClementineTurnedOffError()
473+
474+ self.cl_player.Previous()
475+
476+ def stop(self):
477+ """
478+ Stops Clementine's playback. Might throw ClementineTurnedOffError
479+ if Clementine's been turned off.
480+ """
481+
482+ if not self.is_on():
483+ raise ClementineTurnedOffError()
484+
485+ self.cl_player.Stop()
486+
487+ def pause(self):
488+ """
489+ Pauses Clementine's playback. Might throw ClementineTurnedOffError
490+ if Clementine's been turned off.
491+ """
492+
493+ if not self.is_on():
494+ raise ClementineTurnedOffError()
495+
496+ self.cl_player.Pause()
497+
498+ def play(self):
499+ """
500+ Starts Clementine's playback. Might throw ClementineTurnedOffError
501+ if Clementine's been turned off.
502+ """
503+
504+ if not self.is_on():
505+ raise ClementineTurnedOffError()
506+
507+ self.cl_player.Play()
508+
509+ def next(self):
510+ """
511+ Changes the current song to the next one. Might throw
512+ ClementineTurnedOffError if Clementine's been turned off.
513+ """
514+
515+ if not self.is_on():
516+ raise ClementineTurnedOffError()
517+
518+ self.cl_player.Next()
519+
520+ def get_play_time(self):
521+ """
522+ Gets the current song's now-playing position in microseconds. Might
523+ throw ClementineTurnedOffError if Clementine's been turned off.
524+ """
525+
526+ if not self.is_on():
527+ raise ClementineTurnedOffError()
528+
529+ return get_dbus_property(self.cl_player, 'Position')
530+
531+ def get_current_song_metadata(self):
532+ """
533+ Returns all of the current song's metadata. Might throw
534+ ClementineTurnedOffError if Clementine's been turned off.
535+ """
536+
537+ if not self.is_on():
538+ raise ClementineTurnedOffError()
539+
540+ return get_dbus_property(self.cl_player, 'Metadata')
541+
542+ def check_is_playing(self):
543+ """
544+ Checks whether Clementine is currently playing a song. Might
545+ throw ClementineTurnedOffError if Clementine's been turned off.
546+ """
547+
548+ if not self.is_on():
549+ raise ClementineTurnedOffError()
550+
551+ playback_status = get_dbus_property(self.cl_player, 'PlaybackStatus')
552+ return self.is_playing(playback_status)
553+
554+ def is_playing(self, status):
555+ """
556+ Decodes Clementine's status and returns a flag saying whether it's
557+ currently playing a song.
558+ """
559+
560+ return status == 'Playing'
561+
562+ def is_stopped(self, status):
563+ """
564+ Decodes Clementine's status and returns a flag saying whether it's
565+ currently stopped.
566+ """
567+
568+ return status == 'Stopped'
569+
570+ def on_properties_changed(self, iface_name, changed_props, invalidated_props):
571+ """
572+ A callback for the Clementine's 'PropertiesChanged' signal.
573+ """
574+
575+ if iface_name == player_iface_name:
576+
577+ if('PlaybackStatus' in changed_props):
578+ self.playback_status_changed_callback(changed_props['PlaybackStatus'])
579+
580+ if('Metadata' in changed_props):
581+ self.song_changed_callback(changed_props['Metadata'])
582+
583+ try:
584+ cover_art = changed_props['Metadata']['mpris:artUrl']
585+ cover_art = cover_art[len('file://'):]
586+
587+ # only if it's a valid file
588+ if os.path.isfile(cover_art):
589+ mtime = os.stat(cover_art).st_mtime
590+
591+ # only if there really was a change
592+ if self.last_cover_art != cover_art or self.last_cover_art_mtime != mtime:
593+ self.last_cover_art = cover_art
594+ self.last_cover_art_mtime = mtime
595+
596+ self.cover_art_changed_callback(self.get_album_art_overlay_path(cover_art))
597+ else:
598+ raise KeyError
599+
600+ except KeyError:
601+ # Clementine is now not playing anything or the current
602+ # song has no cover
603+ self.last_cover_art = None
604+ self.last_cover_art_mtime = None
605+
606+ self.cover_art_changed_callback(None)
607+
608+ def get_path_for_overlayed_cover(self):
609+ """
610+ Overlays the cover of currently playing song and then returns the path
611+ to the overlayed cover file. Returns 'None' if there's nothing playing.
612+ Might throw ClementineTurnedOffError if Clementine's been turned off.
613+ """
614+
615+ metadata = self.get_current_song_metadata()
616+
617+ try:
618+ cover_art = metadata['mpris:artUrl']
619+ cover_art = cover_art[len('file://'):]
620+ return self.get_album_art_overlay_path(cover_art)
621+ except KeyError:
622+ return None
623+
624+ # taken from rhythmbox_control.py
625+ def get_album_art_overlay_path(self, picfile):
626+ """
627+ Adds an overlay to the cover art.
628+ """
629+
630+ if overlay == 0:
631+ return picfile
632+
633+ try:
634+ pb = gtk.gdk.pixbuf_new_from_file(picfile)
635+ except Exception, e:
636+ print e
637+ return picfile
638+
639+ pb_result = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, 250, 250)
640+ pb_result.fill(0x00000000)
641+
642+ if overlay == 1:
643+ overlayfile = os.path.join(RESOURCESDIR, "albumoverlay_jewel.png")
644+ pb.composite(pb_result, 30, 21, 200, 200, 30, 21, 200.0 / pb.get_width(), 200.0 / pb.get_height(), gtk.gdk.INTERP_BILINEAR, 255)
645+ elif overlay == 2:
646+ overlayfile = os.path.join(RESOURCESDIR, "albumoverlay_vinyl.png")
647+ pb.composite(pb_result, 3, 26, 190, 190, 3, 26, 190.0 / pb.get_width(), 190.0 / pb.get_height(), gtk.gdk.INTERP_BILINEAR, 255)
648+ else:
649+ return picfile
650+
651+ pb_overlay = gtk.gdk.pixbuf_new_from_file_at_size(overlayfile, 250, 250)
652+ pb_overlay.composite(pb_result, 0, 0, 250, 250, 0, 0, 1, 1, gtk.gdk.INTERP_BILINEAR, 255)
653+ pb_result.save(album_art_tmpfile, "png", {})
654+
655+ return album_art_tmpfile
656+
657+ def on_dbus_name_change(self, connection_name):
658+ """
659+ This method effectively tracks down the events of Clementine app starting
660+ and shutting down. When the app shuts down, this callback nullifies our
661+ Clementine's proxies and when the app starts, the callback sets the valid
662+ proxies again.
663+ """
664+
665+ if len(connection_name) != 0:
666+ bus_object = self.bus.get_object(connection_name, player_object_path)
667+ self.cl_player = dbus.Interface(bus_object, player_iface_name)
668+
669+ bus_object = self.bus.get_object(connection_name, player_object_path)
670+ self.cl_player.connect_to_signal('Seeked', self.seeked_callback)
671+
672+ bus_object = self.bus.get_object(connection_name, tlist_object_path)
673+ self.cl_tlist = dbus.Interface(bus_object, tlist_iface_name)
674+
675+ try:
676+ cover_path = get_dbus_property(self.cl_player, 'Metadata')['mpris:artUrl']
677+
678+ if os.path.isfile(cover_path):
679+ self.last_cover_art = self.get_album_art_overlay_path(cover_path)
680+ self.last_cover_art_mtime = os.stat(cover_path).st_mtime
681+ else:
682+ raise KeyError
683+
684+ except KeyError, DBusException:
685+ self.last_cover_art = None
686+ self.last_cover_art_mtime = None
687+
688+ connect_to_dbus_properties_changed(self.cl_player, self.on_properties_changed)
689+
690+ self.on_callback()
691+
692+ else:
693+ self.cl_player = None
694+ self.cl_tlist = None
695+
696+ self.off_callback()
697+
698+
699+class ClementineTurnedOffError(Exception):
700+ """
701+ Indicates that user requested an operation that requires Clementine
702+ to be on while it was turned off.
703+ """
704+
705+ pass
706+
707+
708+class ClementineSink(DockManagerSink):
709+
710+ def item_path_found(self, pathtoitem, item):
711+ if item.Get(DOCKITEM_IFACE, "DesktopFile", dbus_interface="org.freedesktop.DBus.Properties").endswith("clementine.desktop"):
712+ self.items[pathtoitem] = ClementineItem(self, pathtoitem)
713+
714+
715+clementine_sink = ClementineSink()
716+
717+def cleanup ():
718+ clementine_sink.dispose ()
719+
720+if __name__ == "__main__":
721+ mainloop = gobject.MainLoop(is_running=True)
722+
723+ atexit.register (cleanup)
724+ signal(SIGTERM, lambda signum, stack_frame: exit(1))
725+
726+ while mainloop.is_running():
727+ mainloop.run()

Subscribers

People subscribed via source and target branches

to status/vote changes: