File: plugin_guide.rst

package info (click to toggle)
exaile 4.2.1%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 16,452 kB
  • sloc: python: 39,785; javascript: 9,262; makefile: 268; sh: 138
file content (573 lines) | stat: -rw-r--r-- 19,988 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
Plugin Development Guide
========================

.. note:: These instructions always track current Exaile trunk, and may not
          be fully compatible with stable releases.  It is recommended that
          you develop plugins against trunk, so that you can submit patches
          to trunk if need be during the creation of your plugin, and so
          that your plugin can easily be merged into trunk when it is ready.

Style
-----

If you plan to submit your plugin for inclusion in Exaile, please read and
follow the guidelines in the :ref:`code_guidelines`.

Basic plugin structure
----------------------

Plugins in Exaile 3.x+ are handled slightly differently than in the past.
Each plugin has its own directory in ``~/.local/share/exaile/plugins/``. In order
for your plugin to be recognized as valid by Exaile, it needs to have at least
two files in the plugin directory (``~/.local/share/exaile/plugins/myplugin/``):

* ``__init__.py``
* ``PLUGININFO``

The format of the ``PLUGININFO`` is as follows::

    Version='0.0.1'
    Authors=['Your Name <your@email.com>']
    Name=_('Plugin Name')
    Description=_('Something that describes your plugin. Also mention any extra dependencies.')
    Category=_('Development')
    
The following two attributes are optional:

* `Platforms` - A list of the platforms your plugin works on. If you have no
  specific requirements, omitting this argument or using an empty list is
  fine. The values of the list are the sys.platform value.
* `RequiredModules` - A list of additional modules required by your plugin.
  Modules that Exaile already require (e.g. mutagen) don't need to be specified.
  To specify GObject Introspection libraries, prefix it with ``gi:``, e.g.
  ``gi:WebKit2``.

.. note:: Name and Description are what show up in the plugin manager.
          Category is used to list your plugin alongside other plugins.
          Platforms and RequiredModules are used to filter out the plugin
          on inappropriate platforms.

Before Exaile 3.4, ``__init__.py`` was required to define at least two methods,
``enable()`` and ``disable()``. However, Exaile 3.4 introduced a new way to write
plugins which will eliminate a lot of unnecessary boilerplate for plugin
authors. We will use this model below:

.. code-block:: python

    class MyPlugin:
    
        def enable(self, exaile):
            print('You enabled me!')
            
        def disable(self, exaile):
            print('I am being disabled')

    
    plugin_class = MyPlugin

For many types of plugins, this might be enough. However, there are other
optional methods you can define in your plugin object.

* ``on_gui_loaded`` - This will be called when the GUI is ready, or immediately
  if already done
* ``on_exaile_loaded`` - This will be called when exaile has finished loading,
  or immediately if already done
* ``teardown`` - This will be called when exaile is unloading

These methods may be necessary for your plugin because plugins can only
access Exaile’s infrastructure when Exaile itself finishes loading.
The first ``enable()`` method is called when Exaile is partway through
loading. But since we can't do anything until Exaile finishes loading, we
can add ``on_exaile_loaded`` to our object that is called when Exaile finishes
loading. Some plugins need to modify state earlier in the startup process,
hence the need for this separation.

The ``exaile`` object in the above example is an instance of a class called
Exaile, which is defined in ``xl/main.py``. This class is a base for everything
in the program.

You can get a handle on various objects in Exaile by looking at the members
of this class.

Something (slightly) more useful
--------------------------------

Here is an example of a plugin that will, when a track is played, show the
track information in a ``MessageDialog``. It demonstrates a callback on an event,
and getting the Gtk.Window object of Exaile to use as a parent for a MessageBox.

The ``PLUGININFO`` is as follows::

    Version='0.0.1'
    Authors=['Me <me@internets.com>']
    Name='Tutorial Plugin'
    Description='Plugin to demonstrate how to make a plugin.'

and the ``__init__.py`` is as follows

.. code-block:: python

    '''
        This plugin will show an obnoxious Gtk.MessageDialog that
        won't disappear, when a track is played. The MessageDialog
        will contain the information of the currently playing track.
    '''
    
    from xl import event
    from gi.repository import Gtk
    
    # The main functionality of each plugin is generally defined in a class
    # This is by convention, and also makes programming easier
    class TutorialPlugin:
    
        def enable(self, exaile):
            '''This method is called when the plugin is loaded by exaile'''
            
            # We need a reference to the main Exaile object in order to set the
            # parent window for our obnoxious MessageDialog
            self.exaile = exaile
            
        def disable(self, exaile):
            '''This method is called when the plugin is disabled. Typically it is used for
               removing any GUI elements that we may have added in _enable()'''
            self.show_messagebox("Byebye!")
        
        def on_exaile_loaded(self):
            '''Called when exaile is ready for us to manipulate it'''
            
            #The reason why we dont use show_messagebox here is it hangs the GUI
            #which means it would hang Exaile as soon as you restart, because all
            #enabled plugins are loaded on start.
            print('You enabled the Tutorial plugin!')  
            
            # Add a callback for the 'playback_track_start' event.
            # See xl/event.py for more details.
            event.add_callback(self.popup_message, 'playback_track_start')
            
           
        def popup_message(self, type, player, track):
            # The Track object (defined in xl/track.py) stores its data in lists
            # Convert the lists into strings for displaying
            title = track.get_tag_display('title')
            artist = track.get_tag_display('artist')
            album = track.get_tag_display('album')
            message = "Started playing %s by %s on %s" % (title, artist, album)
            self.show_messagebox(message)
        
        def show_messagebox(self, message):
            # This is the obnoxious MessageDialog. Due to (something to do with threading?)
            # it will steal, and never relinquish, focus when it is displayed.
            dialog = Gtk.MessageDialog(self.exaile.gui.main.window, 0,
                                       Gtk.MessageType.INFO, Gtk.ButtonsType.OK, message)
            dialog.run()
            dialog.destroy()
          
    
    plugin_class = TutorialPlugin

Have a look in the comments for an explanation of what everything is doing.

Adding a track to the Playlist
------------------------------

This is relatively simple. A Playlist consists of the actual graphical
representation of a playlist (see ``xlgui/playlist.py``) and its underlying
Playlist object (see ``xl/playlist.py``). Any changes made to the underlying
playlist object are shown in the graphical representation. We will be
appending Track objects to this underlying playlist.

First you need to get a handle on the underlying Playlist:

.. code-block:: python

    playlist_handle = exaile.gui.main.get_selected_playlist().playlist

Then, you need to create a Track object (defined in ``xl/track.py``). The
method to do this from a local file versus a URL is slightly different.

For a local source:

.. code-block:: python

    from xl import trax
    path = "/home/user/track.ogg" #basically, just specify an absolute path
    myTrack = trax.Track(path)

For a url:

.. code-block:: python

    from xl import trax
    url = "http://path/to/streaming/source" 
    myTrack = trax.get_tracks_from_uri(url)

You can set the track information like this:

.. code-block:: python

    myTrack.set_tags(title='Cool Track',
                     artist='Cool Person',
                     album='Cool Album')

Once you have a Track object, and a handle on the Playlist you would like
to add the track to, you can proceed to add the track:

.. code-block:: python

    playlist_handle.add(myTrack)

Note that ``get_tracks_from_uri()`` returns a list, so you will need to use the
method for adding multiple tracks if your Track object was created this way.
You can also create your own list of Track objects and add them all in one
go like this too:

.. code-block:: python

    playlist_handle.add_tracks(myTrack)

This is pretty much all you need to do to add a track to the playlist. An
example in a plugin might be:

.. code-block:: python

    from xl import event, trax
    
    class PlaylistExample:
   
        def enable(self, exaile):
            self.exaile = exaile
            
        def disable(self, exaile):
            pass
   
        def on_gui_loaded(self):
            self.playlist_handle = self.exaile.gui.main.get_selected_playlist().playlist
            
            local_tr = self.create_track_from_path('/home/user/track.ogg')
            remote_tr = self.create_track_from_url('http://site.com/track.ogg')
            self.add_single_to_playlist(local_tr)
            self.add_multiple_to_playlist(remote_tr)
        
        def create_track_from_path(self, path):
            return trax.Track(path)

        def create_track_from_url(self, url):
            return trax.get_tracks_from_uri(url)

        def add_single_to_playlist(self, track):
            self.playlist_handle.add(track)

        def add_multiple_to_playlist(self, tracks):
            self.playlist_handle.add_tracks(tracks)
    
    
    plugin_class = PlaylistExample

You can do more things when adding a track than simply specifying a track
object to add, see the methods in the class Playlist (``xl/playlist.py``) for more
details.

Adding another page to the left-hand Notebook
---------------------------------------------

This is done pretty easily. Basically, you need to subclass ``xlgui.panel.Panel``
and register a provider advertising your panel.

The subclass needs to have two attributes:

* ``ui_info`` - This defines the location of the .glade file that will be loaded
  into the notebook page (This file must be in Gtk.Builder format, not glade format)
* ``name`` - This is the name that will show on the notebook page, such as "MyPlugin"

.. code-block:: python

    from xl import providers
    from xlgui import panel
    
    # Note: The following uses the exaile object from the enable() method. You
    # might want to call this from the on_gui_loaded function of your plugin.
    page = MyPanel(exaile.gui.main.window)
    providers.register('main-panel', page)
    
    # to remove later:
    providers.unregister('main-panel', page)
       
    class MyPanel(panel.Panel):
        
        #specifies the path to the gladefile (must be in Gtk.Builder format) and the name of the Root Element in the gladefile
        ui_info = (os.path.dirname(__file__) + "mypanel_gladefile.glade", 'NameOfRootElement')    
    
        def __init__(self, parent):
            panel.Panel.__init__(self, parent)
            
            #This is the name that will show up on the tab in Exaile
            self.name = "MyPlugin"
            
            #typically here you'd set up your gui further, eg connect methods to signals etc

That's pretty much all there is to it. To see an actual implementation,
have a look at ``xlgui/panel/collection.py`` or take a look at the Jamendo plugin.

Setting the cover art for a track
---------------------------------

This is done by subclassing ``CoverSearchMethod`` and adding and instance of
the subclass the existing list. When Exaile plays a track with no cover,
it uses all the methods in its ``CoverSearchMethod`` list to try and find a cover.

A ``CoverSearchMethod`` must define:

* ``name`` - The name of the ``CoverSearchMethod``, used for removing it from the list once its been added
* ``type`` - The type of the ``CoverSearchMethod`` (local, remote)
* ``find_covers(self, track, limit=-1)`` - This is the method that is called
  by Exaile when it utilises the ``CoverSearchMethod``. This method must return
  an absolute path to the cover file on the users harddrive.

Here is an example CoverSearchMethod (taken from the Jamendo plugin). It
searches Jamendo for covers, downloads the cover to a local temp directory
and returns the path to the downloaded cover.

.. code-block:: python

    import urllib.request
    import hashlib
    from xl.cover import CoverSearchMethod, NoCoverFoundException
    
    class JamendoCoverSearch(CoverSearchMethod):
        name = 'jamendo'
        type = 'remote'
    
        def __init__(self):
            CoverSearchMethod.__init__(self)
    
        def find_covers(self, track, limit=-1):
            jamendo_url = track.get_loc_for_io()
    
            cache_dir = self.manager.cache_dir
            if (not jamendo_url) or (not ('http://' and 'jamendo' in jamendo_url)):
                raise NoCoverFoundException
    
            #http://stream10.jamendo.com/stream/61541/ogg2/02%20-%20PieRreF%20-%20Hologram.ogg?u=0&h=f2b227d38d
            split=jamendo_url.split('/')
            track_num = split[4]
            image_url = jamapi.get_album_image_url_from_track(track_num)
    
            if not image_url:
                raise NoCoverFoundException
    
            local_name = hashlib.sha1(split[6]).hexdigest() + ".jpg"
            covername = os.path.join(cache_dir, local_name)
            urllib.request.urlretrieve(image_url, covername)
    
            return [covername]

You can then add it to the list of ``CoverSearchMethods`` for Exaile to try like this:

.. code-block:: python

    exaile.covers.add_search_method(JamendoCoverSearch())

And remove it like this:

.. code-block:: python

    exaile.covers.remove_search_method_by_name('jamendo')


Make strings translatable
-------------------------

Every message should be written in English and should be translatable. The
following example shows how you can make a string translatable:

.. code-block:: python

    from xl.nls import gettext as _
    print(_('translatable string'))


Saving/Loading arbitrary settings
---------------------------------

This is quite easy. It's probably quicker to just show some code instead
of trying to explain it:

.. code-block:: python

    from xl import settings
    
    #to save a setting:
    setting_value = 'I am the value for this setting!'
    settings.set_option('plugin/pluginname/settingname', setting_value)
    
    #to get a setting
    default_value = 'If the setting doesn't exist, I am the default value.'
    retrieved_setting = settings.get_option('plugin/pluginname/settingname', default_value)

That's all there is to it. There is a few restrictions as to the
datatypes you can save as settings, see ``xl/settings.py`` for more details.

Searching the collection
-------------------------

The following method returns an list of similar tracks to the current
playing track:

.. code-block:: python

    exaile.dynamic.find_similar_tracks(exaile.player.current, 5) #the second optional argument is the limit

This method returns an list of tuples, which consist of the match rate and the artist's name:

.. code-block:: python

    exaile.dynamic.find_similar_artists(exaile.player.current)

If you would like to search the collection for a specific artist, album or
genre, you can use the following code:

.. code-block:: python

    from xl.trax import search
    
    artist = 'Oasis'
    tracks = [x.track for x in search.search_tracks_from_string(
               exaile.collection, ('artist=="%s"'%artist))]
               
    genre = 'pop'
    tracks = [x.track for x in search.search_tracks_from_string(
               exaile.collection, ('genre=="%s"'%genre))]
               
    album = 'Hefty Fine'
    tracks = [x.track for x in search.search_tracks_from_string(
               exaile.collection, ('album=="%s"'%album))]

You can search the collection also for different assignments, like the last
played tracks, the most recently added tracks or the tracks, which were
played most often. Here you see an example to display the most recently
added tracks:

.. code-block:: python

    from xl.trax import search
    from xl.trax.util import sort_tracks
    
    tracks = [x.track for x in search.search_tracks_from_string(exaile.collection, ('! %s==__null__' % '__last_played'))]
    tracks = sort_tracks(['__last_played'], tracks, True) #sort the tracks by the last playing
   
The other keywords are ``__date_added`` and ``__playcount``

Exaile D-Bus
------------

Here is a simple example how to use the D-Bus object:

.. code-block:: python

    #!/usr/bin/env python3
    
    from io import BytesIO
    import sys

    import dbus
    import Image
    
    def test_dbus():
        bus = dbus.SessionBus()
        try:
            remote_object = bus.get_object("org.exaile.Exaile","/org/exaile/Exaile")
            iface = dbus.Interface(remote_object, "org.exaile.Exaile")
            if iface.IsPlaying():
                title = iface.GetTrackAttr("title")
                print('Title:', title)
                album = iface.GetTrackAttr("album")
                print('Album:', album)
                artist = iface.GetTrackAttr("artist")
                print('Artist:', artist)
                genre = iface.GetTrackAttr("genre")
                print('Genre:', genre)
                dbusArray = iface.GetCoverData()
                coverdata = bytes(dbusArray)
                if coverdata:
                    im = Image.open(BytesIO(coverdata))
                    im.show()
            else:
                print("Exaile is not playing.")
        except dbus.exceptions.DBusException:
            print("Exaile is not running.")
    
    if __name__ == "__main__":
        test_dbus()

Please check out ``xl/xldbus.py`` for further method signatures.

Playback events
---------------

Since playback events can occur far before the main GUI object or even the
``exaile`` object is loaded, connecting to them in advance is required. To 
do this, in your ``__init__`` method:

.. code-block:: python

    event.add_callback(self.on_playback_player_start, 'playback_player_start')


Distributing the Plugin
-----------------------

Create a Plugin Archive
^^^^^^^^^^^^^^^^^^^^^^^

Basically, you just need to tar up your plugin's directory, and rename the
tarfile to <name_of_plugin_directory>.exz

You will need to develop your plugin with a similar hierarchy to the following::

    root --
         \ -- __init__.py
         \ -- PLUGININFO
         \ -- data
           \ -- somefile.glade
           \ -- somefile.dat
         \ -- images
           \ -- somefile.png

The archive should be named with the extension *.exz*. The name of the
plugin.exz file needs to match the name of the plugin directory.

So in the above example, you would need to call your plugin *root.exz* in
order for it to be accepted by Exaile.

exz files can optionally be compressed, using either gzip or bzip2. the
extension remains the same.

This is all you need to do to make a plugin archive.

Exaile API
----------

Now you know the basics about programming plugins for Exaile, but there
are many more useful classes you may need. You can get an overview about
the classes and their use by going through the :ref:`api_docs`.

Building your own version of this documentation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

You can use the Python package manager (`pip <https://pip.pypa.io/en/stable/>`_)
to install sphinx:

.. code-block:: sh
  
    $ pip install sphinx
    
    # or on windows  
    $ py -m pip install sphinx

Then you can run the following command in a terminal:

.. code-block:: sh

    $ cd doc && make html

You'll find the documentation in ``doc/_build/html``.