1# Copyright 2013-2017 Nick Boultbee 2# 3# This program is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation; either version 2 of the License, or 6# (at your option) any later version. 7 8from quodlibet import ngettext, _ 9from quodlibet.qltk import get_top_parent, get_menu_item_top_parent 10from quodlibet.qltk.msg import ConfirmationPrompt 11from quodlibet.qltk.x import SeparatorMenuItem 12from quodlibet.util import print_exc, format_int_locale 13from quodlibet.util.dprint import print_d, print_e 14from quodlibet.plugins import PluginHandler, PluginManager 15from quodlibet.plugins.gui import MenuItemPlugin 16 17 18def confirm_multi_playlist_invoke(parent, plugin_name, count): 19 """Dialog to confirm invoking a plugin with X playlists 20 in case X is high 21 """ 22 params = {"name": plugin_name, "count": format_int_locale(count)} 23 title = ngettext("Run the plugin \"%(name)s\" on %(count)s playlist?", 24 "Run the plugin \"%(name)s\" on %(count)s playlists?", 25 count) % params 26 description = "" 27 ok_text = _("_Run Plugin") 28 prompt = ConfirmationPrompt(parent, title, description, ok_text).run() 29 return prompt == ConfirmationPrompt.RESPONSE_INVOKE 30 31 32class PlaylistPlugin(MenuItemPlugin): 33 """ 34 Playlist plugins are much like songsmenu plugins, 35 and provide one or more of the following instance methods: 36 37 self.plugin_single_playlist(playlist) 38 self.plugin_playlist(song) 39 self.plugin_playlists(songs) 40 41 All matching provided callables on a single object are called in the 42 above order if they match until one returns a true value. 43 44 The `single_` variant is only called if a single song/album is selected. 45 46 The singular version is called once for each selected playlist, but the 47 plural version is called with a list of playlists. 48 49 Returning `True` from these signifies a change was made and the UI / 50 library should update; otherwise this isn't guaranteed. 51 52 Currently (01/2016) only the singular forms are actually supported in 53 the UI, but this won't always be the case. 54 55 To make your plugin insensitive if unsupported playlists are selected, 56 a method that takes a list of songs and returns True or False to set 57 the sensitivity of the menu entry: 58 self.plugin_handles(playlists) 59 60 All of this is managed by the constructor, so 61 make sure it gets called if you override it (you shouldn't have to). 62 63 Note: If inheriting both `PlaylistPlugin` and `SongsMenuPlugin`, 64 it (currently) needs to be done in that order. 65 """ 66 plugin_single_playlist = None 67 plugin_playlist = None 68 plugin_playlists = None 69 70 def __init__(self, playlists=None, library=None): 71 super(PlaylistPlugin, self).__init__() 72 self._library = library 73 self._playlists = playlists or [] 74 self.set_sensitive(bool(self.plugin_handles(playlists))) 75 76 def plugin_handles(self, playlists): 77 return True 78 79 80class PlaylistPluginHandler(PluginHandler): 81 """Handles PlaylistPlugins""" 82 83 def init_plugins(self): 84 PluginManager.instance.register_handler(self) 85 86 def __init__(self, confirmer=None): 87 """Takes an optional `confirmer`, mainly for testing""" 88 89 self.__plugins = [] 90 self._confirm_multiple = (confirmer or 91 confirm_multi_playlist_invoke) 92 93 def populate_menu(self, menu, library, browser, playlists): 94 """Appends items onto `menu` for each enabled playlist plugin, 95 separated as necessary. """ 96 97 attrs = ['plugin_playlist', 'plugin_playlists'] 98 99 if len(playlists) == 1: 100 attrs.append('plugin_single_playlist') 101 102 items = [] 103 kinds = self.__plugins 104 kinds.sort(key=lambda plugin: plugin.PLUGIN_ID) 105 print_d("Found %d Playlist plugin(s): %s" % (len(kinds), kinds)) 106 for Kind in kinds: 107 usable = any([callable(getattr(Kind, s)) for s in attrs]) 108 if usable: 109 try: 110 items.append(Kind(playlists=playlists, library=library)) 111 except: 112 print_e("Couldn't initialise playlist plugin %s: " % Kind) 113 print_exc() 114 items = [i for i in items if i.initialized] 115 116 if items: 117 menu.append(SeparatorMenuItem()) 118 for item in items: 119 try: 120 menu.append(item) 121 args = (library, browser, playlists) 122 if item.get_submenu(): 123 for subitem in item.get_submenu().get_children(): 124 subitem.connect( 125 'activate', self.__on_activate, item, *args) 126 else: 127 item.connect( 128 'activate', self.__on_activate, item, *args) 129 except: 130 print_exc() 131 item.destroy() 132 133 def handle(self, plugin_id, library, browser, playlists): 134 """Start a plugin directly without a menu""" 135 136 for plugin in self.__plugins: 137 if plugin.PLUGIN_ID == plugin_id: 138 try: 139 plugin = plugin(playlists, library) 140 except Exception: 141 print_exc() 142 else: 143 parent = get_top_parent(browser) 144 self.__handle(plugin, library, browser, playlists, parent) 145 return 146 147 def __on_activate(self, item, plugin, library, browser, playlists): 148 parent = get_menu_item_top_parent(item) 149 self.__handle(plugin, library, browser, playlists, parent) 150 151 def __handle(self, plugin, library, browser, playlists, parent): 152 if len(playlists) == 0: 153 return 154 155 if (len(playlists) == 1 156 and callable(plugin.plugin_single_playlist)): 157 pl = playlists[0] 158 try: 159 ret = plugin.plugin_single_playlist(pl) 160 except Exception: 161 print_exc() 162 else: 163 if ret: 164 print_d("Updating %s" % pl) 165 browser.changed(pl) 166 browser.activate() 167 return 168 if callable(plugin.plugin_playlist): 169 total = len(playlists) 170 if total > plugin.MAX_INVOCATIONS: 171 if not self._confirm_multiple( 172 parent, plugin.PLUGIN_NAME, total): 173 return 174 175 try: 176 ret = map(plugin.plugin_playlist, playlists) 177 if ret: 178 for update, pl in zip(ret, playlists): 179 if update: 180 print_d("Updating %s" % pl) 181 browser.changed(pl) 182 browser.activate() 183 except Exception: 184 print_exc() 185 else: 186 if any(ret): 187 return 188 if callable(plugin.plugin_playlists): 189 try: 190 if plugin.plugin_playlists(playlists): 191 browser.activate() 192 except Exception: 193 print_exc() 194 for pl in playlists: 195 browser.changed(pl) 196 197 def plugin_handle(self, plugin): 198 return issubclass(plugin.cls, PlaylistPlugin) 199 200 def plugin_enable(self, plugin): 201 self.__plugins.append(plugin.cls) 202 203 def plugin_disable(self, plugin): 204 self.__plugins.remove(plugin.cls) 205 206 207# Single instance 208PLAYLIST_HANDLER = PlaylistPluginHandler() 209