Source

exaile / xl / player / _base.py

Full commit
  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
# Copyright (C) 2008-2010 Adam Olsen
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
#
# The developers of the Exaile media player hereby grant permission
# for non-GPL compatible GStreamer and Exaile plugins to be used and
# distributed together with GStreamer and Exaile. This permission is
# above and beyond the permissions granted by the GPL license by which
# Exaile is covered. If you modify this code, you may extend this
# exception to your version of the code, but you are not obligated to
# do so. If you do not wish to do so, delete this exception statement
# from your version.

import logging
import time

import pygst
pygst.require('0.10')
import gst

from xl import event, settings, common
from xl.player import pipe
logger = logging.getLogger(__name__)


class ExailePlayer(object):
    """
        Base class all players must inherit from and implement.
    """
    def __init__(self, name, pre_elems=[]):
        self.queue = None
        self._name = name
        self._playtime_stamp = None
        self._last_position = 0

        self._mainbin = pipe.MainBin(self, pre_elems=pre_elems)
        self._pipe = None
        self._bus = None

        self._setup_pipe()
        self._setup_bus()

        self._load_volume()
        event.add_callback(self._on_option_set, '%s_option_set' % self._name)
        event.add_callback(self._on_track_end, 'playback_track_end', self)

    def _on_option_set(self, name, object, data):
        if data == "%s/volume" % self._name:
            self._load_volume()

    def _on_track_end(self, name, obj, track):
        if not track:
            return
        try:
            i = int(track.get_tag_raw('__playcount'))
        except:
            i = 0
        track.set_tag_raw('__playcount', i + 1)
        track.set_tag_raw('__last_played', time.time())

    def _load_volume(self):
        """
            load volume from settings
        """
        volume = settings.get_option("%s/volume" % self._name, 1)
        self._set_volume(volume)

    def _setup_pipe(self):
        """
            Needs to create self._pipe, an instance of gst.Pipeline
            that will control playback.
        """
        raise NotImplementedError

    def _setup_bus(self):
        """
            setup the gstreamer message bus and callbacks
        """
        self._bus = self._pipe.get_bus()
        self._bus.add_signal_watch()
        self._bus.enable_sync_message_emission()
        self._bus.connect('message', self._on_message)

    def _on_message(self, bus, message, reading_tag=False):
        handled = self._handle_message(bus, message, reading_tag)
        if handled:
            pass
        elif message.type == gst.MESSAGE_TAG:
            """ Update track length and optionally metadata from gstreamer's parser.
                Useful for streams and files mutagen doesn't understand. """
            parsed = message.parse_tag()
            event.log_event('tags_parsed', self, (self.current, parsed))
            if self.current and not self.current.get_tag_raw('__length'):
                try:
                    raw_duration = self.playbin.query_duration(gst.FORMAT_TIME, None)[0]
                except gst.QueryError:
                    logger.error("Couldn't query duration")
                    raw_duration = 0
                duration = float(raw_duration)/gst.SECOND
                if duration > 0:
                    self.current.set_tag_raw('__length', duration)
        elif message.type == gst.MESSAGE_EOS and not self.is_paused():
            self._eos_func()
        elif message.type == gst.MESSAGE_ERROR:
            logger.error("%s %s" %(message, dir(message)) )
            a = message.parse_error()[0]
            event.log_event('playback_error', self, message)
            self._error_func()
        return True

    def _handle_message(self, bus, message, reading_tag):
        pass # for overriding

    def _eos_func(self):
        logger.warning("Unhandled EOS message: ", message)

    def _error_func(self):
        self.stop()

    def _set_queue(self, queue):
        self.queue = queue

    def _get_volume(self):
        """
            Gets the current actual volume.  This does not reflect what is
            shown to the user, see the player/volume setting for that.
        """
        return self._mainbin.get_volume()

    def _set_volume(self, volume):
        """
            Sets the volume. This does NOT update the setting value,
            and should be used only internally.
        """
        self._mainbin.set_volume(volume)

    def get_volume(self):
        """
            Gets the current volume

            :returns: the volume percentage
            :type: int
        """
        return (settings.get_option("%s/volume" % self._name, 1) * 100)

    def set_volume(self, volume):
        """
            Sets the current volume

            :param volume: the volume percentage
            :type volume: int
        """
        volume = min(volume, 100)
        volume = max(0, volume)
        settings.set_option("%s/volume" % self._name, volume / 100.0)

    def _get_current(self):
        raise NotImplementedError

    def __get_current(self):
        return self._get_current()
    current = property(__get_current)

    def play(self, track, **kwargs):
        """
            Starts the playback with the provided track
            or stops the playback it immediately if none

            :param track: the track to play
            :type track: :class:`xl.trax.Track`

            .. note:: The following :doc:`events </xl/event>` will be emitted by this method:

                * `playback_player_start`: indicates the start of playback overall
                * `playback_track_start`: indicates playback start of a track
        """
        raise NotImplementedError

    def stop(self, _fire=True, **kwargs):
        """
            Stops the playback

            :param fire: Send the 'playback_player_end' event. Used by engines
                to avoid spurious playback_end events. Not public API.

            .. note:: The following :doc:`events </xl/event>` will be emitted by this method:

                * `playback_player_end`: indicates the end of playback overall
                * `playback_track_end`: indicates playback end of a track
        """
        if self.is_playing() or self.is_paused():
            prev_current = self._stop(**kwargs)

            if _fire:
                event.log_event('playback_player_end', self, prev_current)
            return True
        return False

    def _stop(self, **kwargs):
        raise NotImplementedError

    def pause(self):
        """
            Pauses the playback, does not toggle it

            .. note:: The following :doc:`events </xl/event>` will be emitted by this method:

                * `playback_player_pause`: indicates that the playback has been paused
        """
        if self.is_playing():
            self._pause()
            event.log_event('playback_player_pause', self, self.current)
            return True
        return False

    def _pause(self):
        raise NotImplementedError

    def unpause(self):
        """
            Resumes the playback, does not toggle it

            .. note:: The following :doc:`events </xl/event>` will be emitted by this method:

                * `playback_player_resume`: indicates that the playback has been resumed
        """
        if self.is_paused():
            self._unpause()
            event.log_event('playback_player_resume', self, self.current)
            return True
        return False

    def _unpause():
        raise NotImplementedError

    def toggle_pause(self):
        """
            Toggles between playing and paused state

            .. note:: The following :doc:`events </xl/event>` will be emitted by this method:

                * `playback_toggle_pause`: indicates that the playback has been paused or resumed
        """
        if self.is_paused():
            self.unpause()
        else:
            self.pause()

        event.log_event("playback_toggle_pause", self, self.current)

    def seek(self, value):
        """
            Seek to a position in the currently playing stream

            :param value: the position in seconds
            :type value: int
        """
        raise NotImplementedError

    def get_position(self):
        """
            Gets the current playback position

            :returns: the position in milliseconds
            :rtype: int
        """
        raise NotImplementedError

    def get_time(self):
        """
            Gets the current playback time

            :returns: the playback time in seconds
            :rtype: int
        """
        return self.get_position()/gst.SECOND

    def get_progress(self):
        """
            Gets the current playback progress

            :returns: the playback progress as [0..1]
            :rtype: float
        """
        try:
            progress = self.get_position()/float(
                    self.current.get_tag_raw("__length")*gst.SECOND)
        except TypeError: # track doesnt have duration info
            progress = 0
        except AttributeError: # no current track
            progress = 0
        else:
            if progress < 0:
                progress = 0
            elif progress > 1:
                progress = 1
        return progress

    def set_progress(self, progress):
        """
            Seeks to the progress position

            :param progress: value ranged at [0..1]
            :type progress: float
        """
        seek_position = 0

        try:
            length = self.current.get_tag_raw('__length')
            seek_position = length * progress
        except TypeError, AttributeError:
            pass

        self.seek(seek_position)

    def _get_gst_state(self):
        """
            Returns the raw GStreamer state
        """
        return self._pipe.get_state(timeout=50*gst.MSECOND)[1]

    def get_state(self):
        """
            Gets the player state

            :returns: one of *playing*, *paused* or *stopped*
            :rtype: string
        """
        state = self._get_gst_state()
        if state == gst.STATE_PLAYING:
            return 'playing'
        elif state == gst.STATE_PAUSED:
            return 'paused'
        else:
            return 'stopped'

    def is_playing(self):
        """
            Convenience method to find out if the player is currently playing

            :returns: whether the player is currently playing
            :rtype: bool
        """
        return self._get_gst_state() == gst.STATE_PLAYING

    def is_paused(self):
        """
            Convenience method to find out if the player is currently paused

            :returns: whether the player is currently paused
            :rtype: bool
        """
        return self._get_gst_state() == gst.STATE_PAUSED

    def is_stopped(self):
        """
            Convenience method to find out if the player is currently stopped

            :returns: whether the player is currently stopped
            :rtype: bool
        """
        return self._get_gst_state() == gst.STATE_NULL

    @staticmethod
    def parse_stream_tags(track, tags):
        """
            Called when a tag is found in a stream.
        """
        newsong=False

        for key in tags.keys():
            value = tags[key]
            try:
                value = common.to_unicode(value)
            except UnicodeDecodeError:
                logger.debug('  ' + key + " [can't decode]: " + `str(value)`)
                continue # TODO: What encoding does gst give us?

            value = [value]

            if key == '__bitrate':
                track.set_tag_raw('__bitrate', int(value[0]) / 1000)

            # if there's a comment, but no album, set album to the comment
            elif key == 'comment' and not track.get_tag_raw('album'):
                track.set_tag_raw('album', value)

            elif key == 'album': track.set_tag_raw('album', value)
            elif key == 'artist': track.set_tag_raw('artist', value)
            elif key == 'duration': track.set_tag_raw('__length',
                    float(value[0])/1000000000)
            elif key == 'track-number': track.set_tag_raw('tracknumber', value)
            elif key == 'genre': track.set_tag_raw('genre', value)

            elif key == 'title':
                try:
                    if track.get_tag_raw('__rawtitle') != value:
                        track.set_tag_raw('__rawtitle', value)
                        newsong = True
                except AttributeError:
                    track.set_tag_raw('__rawtitle', value)
                    newsong = True

                title_array = value[0].split(' - ', 1)
                if len(title_array) == 1 or \
                        track.get_loc_for_io().lower().endswith(".mp3"):
                    track.set_tag_raw('title', value)
                else:
                    track.set_tag_raw('artist', [title_array[0]])
                    track.set_tag_raw('title', [title_array[1]])



        return newsong