2012-10-20 23:55:25 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Extension script to add a context menu item for enqueueing episodes in a player
|
|
|
|
# Requirements: gPodder 3.x (or "tres" branch newer than 2011-06-08)
|
|
|
|
# (c) 2011-06-08 Thomas Perl <thp.io/about>
|
|
|
|
# Released under the same license terms as gPodder itself.
|
2019-08-17 16:25:00 +02:00
|
|
|
import functools
|
2018-07-24 11:08:10 +02:00
|
|
|
import logging
|
2012-10-20 23:55:25 +02:00
|
|
|
import subprocess
|
|
|
|
|
|
|
|
import gpodder
|
|
|
|
from gpodder import util
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
_ = gpodder.gettext
|
|
|
|
|
2017-04-01 22:15:38 +02:00
|
|
|
__title__ = _('Enqueue/Resume in media players')
|
|
|
|
__description__ = _('Add a context menu item for enqueueing/resuming playback of episodes in installed media players')
|
2013-03-24 21:13:27 +01:00
|
|
|
__authors__ = 'Thomas Perl <thp@gpodder.org>, Bernd Schlapsi <brot@gmx.info>'
|
2020-05-25 18:39:00 +02:00
|
|
|
__doc__ = 'https://gpodder.github.io/docs/extensions/enqueueinmediaplayer.html'
|
2013-03-24 21:13:27 +01:00
|
|
|
__payment__ = 'https://flattr.com/submit/auto?user_id=BerndSch&url=http://wiki.gpodder.org/wiki/Extensions/EnqueueInMediaplayer'
|
2012-11-18 19:26:02 +01:00
|
|
|
__category__ = 'interface'
|
2012-10-20 23:55:25 +02:00
|
|
|
__only_for__ = 'gtk'
|
|
|
|
|
2014-10-22 13:27:19 +02:00
|
|
|
|
2016-01-15 13:36:03 +01:00
|
|
|
DefaultConfig = {
|
2017-04-01 22:15:38 +02:00
|
|
|
'enqueue_after_download': False, # Set to True to enqueue an episode right after downloading
|
|
|
|
'default_player': '', # Set to the player to be used for auto-enqueueing (otherwise pick first installed)
|
2016-01-15 13:36:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-10-22 13:27:19 +02:00
|
|
|
class Player(object):
|
2016-01-15 13:36:03 +01:00
|
|
|
def __init__(self, slug, application, command):
|
|
|
|
self.slug = slug
|
|
|
|
self.application = application
|
2013-03-09 11:53:07 +01:00
|
|
|
self.title = '/'.join((_('Enqueue in'), application))
|
|
|
|
self.command = command
|
|
|
|
self.gpodder = None
|
2012-10-20 23:55:25 +02:00
|
|
|
|
2013-03-09 11:53:07 +01:00
|
|
|
def is_installed(self):
|
2014-10-22 13:27:19 +02:00
|
|
|
raise NotImplemented('Must be implemented by subclass')
|
|
|
|
|
|
|
|
def open_files(self, filenames):
|
|
|
|
raise NotImplemented('Must be implemented by subclass')
|
2013-02-20 22:54:56 +01:00
|
|
|
|
2019-08-17 16:25:00 +02:00
|
|
|
def enqueue_episodes(self, episodes, config=None):
|
|
|
|
filenames = [episode.get_playback_url(config=config) for episode in episodes]
|
2013-02-20 22:54:56 +01:00
|
|
|
|
2014-10-22 13:27:19 +02:00
|
|
|
self.open_files(filenames)
|
2013-02-20 22:54:56 +01:00
|
|
|
|
|
|
|
for episode in episodes:
|
|
|
|
episode.playback_mark()
|
2014-10-22 13:27:19 +02:00
|
|
|
if self.gpodder is not None:
|
|
|
|
self.gpodder.update_episode_list_icons(selected=True)
|
|
|
|
|
|
|
|
|
|
|
|
class FreeDesktopPlayer(Player):
|
|
|
|
def is_installed(self):
|
|
|
|
return util.find_command(self.command[0]) is not None
|
|
|
|
|
|
|
|
def open_files(self, filenames):
|
2018-05-28 21:13:29 +02:00
|
|
|
util.Popen(self.command + filenames)
|
2014-10-22 13:27:19 +02:00
|
|
|
|
|
|
|
|
|
|
|
class Win32Player(Player):
|
|
|
|
def is_installed(self):
|
|
|
|
if not gpodder.ui.win32:
|
|
|
|
return False
|
|
|
|
|
|
|
|
from gpodder.gtkui.desktopfile import win32_read_registry_key
|
|
|
|
try:
|
|
|
|
self.command = win32_read_registry_key(self.command)
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
2022-04-17 11:07:51 +02:00
|
|
|
logger.warning('Win32 player not found: %s (%s)', self.command, e)
|
2014-10-22 13:27:19 +02:00
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
def open_files(self, filenames):
|
|
|
|
for cmd in util.format_desktop_command(self.command, filenames):
|
2018-05-28 21:13:29 +02:00
|
|
|
util.Popen(cmd, close_fds=True)
|
2013-02-20 22:54:56 +01:00
|
|
|
|
|
|
|
|
2017-04-01 22:15:38 +02:00
|
|
|
class MPRISResumer(FreeDesktopPlayer):
|
|
|
|
"""
|
|
|
|
resume episod playback at saved time
|
|
|
|
"""
|
|
|
|
OBJECT_PLAYER = '/org/mpris/MediaPlayer2'
|
|
|
|
OBJECT_DBUS = '/org/freedesktop/DBus'
|
|
|
|
INTERFACE_PLAYER = 'org.mpris.MediaPlayer2.Player'
|
|
|
|
INTERFACE_PROPS = 'org.freedesktop.DBus.Properties'
|
|
|
|
SIGNAL_PROP_CHANGE = 'PropertiesChanged'
|
|
|
|
NAME_DBUS = 'org.freedesktop.DBus'
|
|
|
|
|
|
|
|
def __init__(self, slug, application, command, bus_name):
|
|
|
|
super(MPRISResumer, self).__init__(slug, application, command)
|
|
|
|
self.title = '/'.join((_('Resume in'), application))
|
|
|
|
self.bus_name = bus_name
|
|
|
|
self.player = None
|
2017-04-17 18:28:29 +02:00
|
|
|
self.position_us = None
|
2017-04-01 22:15:38 +02:00
|
|
|
self.url = None
|
|
|
|
|
|
|
|
def is_installed(self):
|
|
|
|
if gpodder.ui.win32:
|
|
|
|
return False
|
|
|
|
return util.find_command(self.command[0]) is not None
|
|
|
|
|
2019-08-17 16:25:00 +02:00
|
|
|
def enqueue_episodes(self, episodes, config=None):
|
|
|
|
self.do_enqueue(episodes[0].get_playback_url(config=config),
|
2017-04-01 22:15:38 +02:00
|
|
|
episodes[0].current_position)
|
|
|
|
|
|
|
|
for episode in episodes:
|
|
|
|
episode.playback_mark()
|
|
|
|
if self.gpodder is not None:
|
|
|
|
self.gpodder.update_episode_list_icons(selected=True)
|
|
|
|
|
|
|
|
def init_dbus(self):
|
|
|
|
bus = gpodder.dbus_session_bus
|
|
|
|
|
|
|
|
if not bus.name_has_owner(self.bus_name):
|
|
|
|
logger.debug('MPRISResumer %s is not there...', self.bus_name)
|
|
|
|
return False
|
|
|
|
|
|
|
|
self.player = bus.get_object(self.bus_name, self.OBJECT_PLAYER)
|
2017-04-17 18:28:29 +02:00
|
|
|
self.signal_match = self.player.connect_to_signal(self.SIGNAL_PROP_CHANGE,
|
2017-04-01 22:15:38 +02:00
|
|
|
self.on_prop_change,
|
|
|
|
dbus_interface=self.INTERFACE_PROPS)
|
|
|
|
return True
|
|
|
|
|
2017-04-23 13:40:54 +02:00
|
|
|
def enqueue_when_ready(self, filename, pos):
|
2017-04-01 22:15:38 +02:00
|
|
|
def name_owner_changed(name, old_owner, new_owner):
|
|
|
|
logger.debug('name_owner_changed "%s" "%s" "%s"',
|
|
|
|
name, old_owner, new_owner)
|
|
|
|
if name == self.bus_name:
|
|
|
|
logger.debug('MPRISResumer player %s is there', name)
|
|
|
|
cancel.remove()
|
|
|
|
util.idle_add(lambda: self.do_enqueue(filename, pos))
|
|
|
|
|
|
|
|
bus = gpodder.dbus_session_bus
|
|
|
|
obj = bus.get_object(self.NAME_DBUS, self.OBJECT_DBUS)
|
2017-04-17 18:28:29 +02:00
|
|
|
cancel = obj.connect_to_signal('NameOwnerChanged', name_owner_changed, dbus_interface=self.NAME_DBUS)
|
2017-04-01 22:15:38 +02:00
|
|
|
|
|
|
|
def do_enqueue(self, filename, pos):
|
|
|
|
def on_reply():
|
|
|
|
logger.debug('MPRISResumer opened %s', self.url)
|
|
|
|
|
|
|
|
def on_error(exception):
|
|
|
|
logger.error('MPRISResumer error %s', repr(exception))
|
|
|
|
self.signal_match.remove()
|
|
|
|
|
|
|
|
if filename.startswith('/'):
|
|
|
|
try:
|
|
|
|
import pathlib
|
|
|
|
self.url = pathlib.Path(filename).as_uri()
|
2017-04-17 18:28:29 +02:00
|
|
|
except ImportError:
|
2017-04-01 22:15:38 +02:00
|
|
|
self.url = 'file://' + filename
|
2017-04-17 18:28:29 +02:00
|
|
|
self.position_us = pos * 1000 * 1000 # pos in microseconds
|
2017-04-01 22:15:38 +02:00
|
|
|
if self.init_dbus():
|
|
|
|
# async to not freeze the ui waiting for the application to answer
|
|
|
|
self.player.OpenUri(self.url,
|
|
|
|
dbus_interface=self.INTERFACE_PLAYER,
|
|
|
|
reply_handler=on_reply,
|
|
|
|
error_handler=on_error)
|
|
|
|
else:
|
2017-04-23 13:40:54 +02:00
|
|
|
self.enqueue_when_ready(filename, pos)
|
2017-04-01 22:15:38 +02:00
|
|
|
logger.debug('MPRISResumer launching player %s', self.application)
|
|
|
|
super(MPRISResumer, self).open_files([])
|
|
|
|
|
|
|
|
def on_prop_change(self, interface, props, invalidated_props):
|
|
|
|
def on_reply():
|
2017-04-17 18:28:29 +02:00
|
|
|
pass
|
2017-04-01 22:15:38 +02:00
|
|
|
|
|
|
|
def on_error(exception):
|
|
|
|
logger.error('MPRISResumer SetPosition error %s', repr(exception))
|
|
|
|
self.signal_match.remove()
|
|
|
|
|
2017-04-17 18:28:29 +02:00
|
|
|
metadata = props.get('Metadata', {})
|
|
|
|
url = metadata.get('xesam:url')
|
|
|
|
track_id = metadata.get('mpris:trackid')
|
2017-04-01 22:15:38 +02:00
|
|
|
if url is not None and track_id is not None:
|
|
|
|
if url == self.url:
|
|
|
|
logger.info('Enqueue %s setting track %s position=%d',
|
2017-04-17 18:28:29 +02:00
|
|
|
url, track_id, self.position_us)
|
|
|
|
self.player.SetPosition(str(track_id), self.position_us,
|
2017-04-01 22:15:38 +02:00
|
|
|
dbus_interface=self.INTERFACE_PLAYER,
|
|
|
|
reply_handler=on_reply,
|
|
|
|
error_handler=on_error)
|
|
|
|
else:
|
|
|
|
logger.debug('Changed but wrong url: %s, giving up', url)
|
|
|
|
self.signal_match.remove()
|
|
|
|
|
|
|
|
|
2013-03-09 11:53:07 +01:00
|
|
|
PLAYERS = [
|
|
|
|
# Amarok, http://amarok.kde.org/
|
2016-01-15 13:36:03 +01:00
|
|
|
FreeDesktopPlayer('amarok', 'Amarok', ['amarok', '--play', '--append']),
|
2013-03-09 11:53:07 +01:00
|
|
|
|
|
|
|
# VLC, http://videolan.org/
|
2016-01-15 13:36:03 +01:00
|
|
|
FreeDesktopPlayer('vlc', 'VLC', ['vlc', '--started-from-file', '--playlist-enqueue']),
|
2013-03-09 11:53:07 +01:00
|
|
|
|
|
|
|
# Totem, https://live.gnome.org/Totem
|
2016-01-15 13:36:03 +01:00
|
|
|
FreeDesktopPlayer('totem', 'Totem', ['totem', '--enqueue']),
|
2014-04-22 17:20:51 +02:00
|
|
|
|
|
|
|
# DeaDBeeF, http://deadbeef.sourceforge.net/
|
2016-01-15 13:36:03 +01:00
|
|
|
FreeDesktopPlayer('deadbeef', 'DeaDBeeF', ['deadbeef', '--queue']),
|
2014-04-22 17:20:51 +02:00
|
|
|
|
|
|
|
# gmusicbrowser, http://gmusicbrowser.org/
|
2016-01-15 13:36:03 +01:00
|
|
|
FreeDesktopPlayer('gmusicbrowser', 'gmusicbrowser', ['gmusicbrowser', '-enqueue']),
|
2014-04-22 17:20:51 +02:00
|
|
|
|
|
|
|
# Audacious, http://audacious-media-player.org/
|
2016-01-15 13:36:03 +01:00
|
|
|
FreeDesktopPlayer('audacious', 'Audacious', ['audacious', '--enqueue']),
|
2014-04-22 17:20:51 +02:00
|
|
|
|
|
|
|
# Clementine, http://www.clementine-player.org/
|
2016-01-15 13:36:03 +01:00
|
|
|
FreeDesktopPlayer('clementine', 'Clementine', ['clementine', '--append']),
|
2014-10-22 13:27:19 +02:00
|
|
|
|
2021-06-27 12:00:41 +02:00
|
|
|
# Strawberry, https://www.strawberrymusicplayer.org/
|
|
|
|
FreeDesktopPlayer('strawberry', 'Strawberry', ['strawberry', '--append']),
|
|
|
|
|
2014-10-22 13:27:19 +02:00
|
|
|
# Parole, http://docs.xfce.org/apps/parole/start
|
2016-01-15 13:36:03 +01:00
|
|
|
FreeDesktopPlayer('parole', 'Parole', ['parole', '-a']),
|
2014-10-22 13:27:19 +02:00
|
|
|
|
|
|
|
# Winamp 2.x, http://www.oldversion.com/windows/winamp/
|
2016-01-15 13:36:03 +01:00
|
|
|
Win32Player('winamp', 'Winamp', r'HKEY_CLASSES_ROOT\Winamp.File\shell\Enqueue\command'),
|
2014-10-22 13:27:19 +02:00
|
|
|
|
|
|
|
# VLC media player, http://videolan.org/vlc/
|
2016-01-15 13:36:03 +01:00
|
|
|
Win32Player('vlc', 'VLC', r'HKEY_CLASSES_ROOT\VLC.mp3\shell\AddToPlaylistVLC\command'),
|
2014-10-16 14:17:03 +02:00
|
|
|
|
2014-10-22 13:27:19 +02:00
|
|
|
# foobar2000, http://www.foobar2000.org/
|
2016-01-15 13:36:03 +01:00
|
|
|
Win32Player('foobar2000', 'foobar2000', r'HKEY_CLASSES_ROOT\foobar2000.MP3\shell\enqueue\command'),
|
2013-03-09 11:53:07 +01:00
|
|
|
]
|
2012-10-20 23:55:25 +02:00
|
|
|
|
2017-04-01 22:15:38 +02:00
|
|
|
|
|
|
|
RESUMERS = [
|
|
|
|
# doesn't play on my system, but the track is appended.
|
|
|
|
MPRISResumer('amarok', 'Amarok', ['amarok', '--play'], 'org.mpris.MediaPlayer2.amarok'),
|
|
|
|
|
|
|
|
MPRISResumer('vlc', 'VLC', ['vlc', '--started-from-file'], 'org.mpris.MediaPlayer2.vlc'),
|
|
|
|
|
|
|
|
# totem mpris2 plugin is broken for me: it raises AttributeError:
|
|
|
|
# File "/usr/lib/totem/plugins/dbus/dbusservice.py", line 329, in OpenUri
|
|
|
|
# self.totem.add_to_playlist_and_play (uri)
|
|
|
|
# MPRISResumer('totem', 'Totem', ['totem'], 'org.mpris.MediaPlayer2.totem'),
|
|
|
|
|
|
|
|
# with https://github.com/Serranya/deadbeef-mpris2-plugin
|
|
|
|
MPRISResumer('resume in deadbeef', 'DeaDBeeF', ['deadbeef'], 'org.mpris.MediaPlayer2.DeaDBeeF'),
|
|
|
|
|
|
|
|
# the gPodder Dowloads directory must be in gmusicbrowser's library
|
|
|
|
MPRISResumer('resume in gmusicbrowser', 'gmusicbrowser', ['gmusicbrowser'], 'org.mpris.MediaPlayer2.gmusicbrowser'),
|
|
|
|
|
|
|
|
# Audacious doesn't implement MPRIS2.OpenUri
|
|
|
|
# MPRISResumer('audacious', 'resume in Audacious', ['audacious', '--enqueue'], 'org.mpris.MediaPlayer2.audacious'),
|
|
|
|
|
|
|
|
# beware: clementine never exits on my system (even when launched from cmdline)
|
|
|
|
# so the zombie clementine process will get all the bus messages and never answer
|
|
|
|
# resulting in freezes and timeouts!
|
|
|
|
MPRISResumer('clementine', 'Clementine', ['clementine'], 'org.mpris.MediaPlayer2.clementine'),
|
|
|
|
|
|
|
|
# just enable the plugin
|
|
|
|
MPRISResumer('parole', 'Parole', ['parole'], 'org.mpris.MediaPlayer2.parole'),
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2012-10-20 23:55:25 +02:00
|
|
|
class gPodderExtension:
|
|
|
|
def __init__(self, container):
|
|
|
|
self.container = container
|
2016-01-15 13:36:03 +01:00
|
|
|
self.config = container.config
|
2019-08-17 16:25:00 +02:00
|
|
|
self.gpodder_config = self.container.manager.core.config
|
2012-10-20 23:55:25 +02:00
|
|
|
|
2013-03-09 11:53:07 +01:00
|
|
|
# Only display media players that can be found at extension load time
|
2017-03-25 21:41:49 +01:00
|
|
|
self.players = [player for player in PLAYERS if player.is_installed()]
|
2017-04-01 22:15:38 +02:00
|
|
|
self.resumers = [r for r in RESUMERS if r.is_installed()]
|
2012-10-20 23:55:25 +02:00
|
|
|
|
2013-02-16 22:44:55 +01:00
|
|
|
def on_ui_object_available(self, name, ui_object):
|
|
|
|
if name == 'gpodder-gtk':
|
2017-04-01 22:15:38 +02:00
|
|
|
for p in self.players + self.resumers:
|
2013-02-20 22:54:56 +01:00
|
|
|
p.gpodder = ui_object
|
2013-02-16 22:44:55 +01:00
|
|
|
|
2012-10-20 23:55:25 +02:00
|
|
|
def on_episodes_context_menu(self, episodes):
|
2013-03-09 11:53:07 +01:00
|
|
|
if not any(e.file_exists() for e in episodes):
|
2012-10-20 23:55:25 +02:00
|
|
|
return None
|
|
|
|
|
2019-08-17 16:25:00 +02:00
|
|
|
ret = [(p.title, functools.partial(p.enqueue_episodes, config=self.gpodder_config))
|
|
|
|
for p in self.players]
|
2017-04-01 22:15:38 +02:00
|
|
|
|
|
|
|
# needs dbus, doesn't handle more than 1 episode
|
|
|
|
# and no point in using DBus when episode is not played.
|
|
|
|
if not hasattr(gpodder.dbus_session_bus, 'fake') and \
|
|
|
|
len(episodes) == 1 and episodes[0].current_position > 0:
|
2019-08-17 16:25:00 +02:00
|
|
|
ret.extend([(p.title, functools.partial(p.enqueue_episodes, config=self.gpodder_config))
|
|
|
|
for p in self.resumers])
|
2017-04-01 22:15:38 +02:00
|
|
|
|
|
|
|
return ret
|
2013-03-09 11:53:07 +01:00
|
|
|
|
2016-01-15 13:36:03 +01:00
|
|
|
def on_episode_downloaded(self, episode):
|
|
|
|
if self.config.enqueue_after_download:
|
|
|
|
if not self.config.default_player and len(self.players):
|
|
|
|
player = self.players[0]
|
|
|
|
logger.info('Picking first installed player: %s (%s)', player.slug, player.application)
|
|
|
|
else:
|
|
|
|
player = next((player for player in self.players if self.config.default_player == player.slug), None)
|
|
|
|
if player is None:
|
|
|
|
logger.info('No player set, use one of: %r', [player.slug for player in self.players])
|
|
|
|
return
|
|
|
|
|
|
|
|
logger.info('Enqueueing downloaded file in %s', player.application)
|
|
|
|
player.enqueue_episodes([episode])
|