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