/
player.py
227 lines (164 loc) · 6.9 KB
/
player.py
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
#!/usr/bin/env python
# encoding: utf-8
"""
player.py
The audio player. A simple wrapper around the MPD client. Uses a lockable version
of the MPD client object, because we're using multiple threads
"""
__version_info__ = (0, 0, 1)
__version__ = '.'.join(map(str, __version_info__))
__author__ = "Willem van der Jagt"
from mpd import MPDClient
from threading import Lock
from book import Book
import config
import re
import time
class LockableMPDClient(MPDClient):
def __init__(self, use_unicode=False):
super(LockableMPDClient, self).__init__()
self.use_unicode = use_unicode
self._lock = Lock()
def acquire(self):
self._lock.acquire()
def release(self):
self._lock.release()
def __enter__(self):
self.acquire()
def __exit__(self, type, value, traceback):
self.release()
class Player(object):
"""The class responsible for playing the audio books"""
def __init__(self, conn_details, status_light):
"""Setup a connection to MPD to be able to play audio.
Also update the MPD database with any new MP3 files that may have been added
and clear any existing playlists.
"""
self.status_light = status_light
self.book = Book()
self.mpd_client = LockableMPDClient()
self.init_mpd(conn_details)
def init_mpd(self, conn_details):
try:
print "Connecting to MPD."
with self.mpd_client:
self.mpd_client.connect(**conn_details)
self.mpd_client.update()
self.mpd_client.clear()
self.mpd_client.setvol(100)
except:
print "Connection to MPD failed. Trying again in 10 seconds."
time.sleep(10)
self.init_mpd(conn_details)
def toggle_pause(self, channel):
"""Toggle playback status between play and pause"""
with self.mpd_client:
state = self.mpd_client.status()['state']
if state == 'play':
self.status_light.action = 'blink_pauze'
self.mpd_client.pause()
elif state == 'pause':
self.status_light.action = 'blink'
self.mpd_client.play()
else:
self.status_light.interrupt('blink_fast', 3)
def rewind(self, channel):
"""Rewind by 20s"""
self.status_light.interrupt('blink_fast', 3)
if self.is_playing():
song_index = int(self.book.part) - 1
elapsed = int(self.book.elapsed)
with self.mpd_client:
if elapsed > 20:
# rewind withing current file if possible
self.mpd_client.seek(song_index, elapsed - 20)
elif song_index > 0:
# rewind to previous file if we're not yet 20 seconds into
# the current file
prev_song = self.mpd_client.playlistinfo(song_index - 1)[0]
prev_song_len = int(prev_song['time'])
# if the previous part is longer than 20 seconds, rewind to 20
# seconds before the end, otherwise rewind to the start of it
if prev_song_len > 20:
self.mpd_client.seek(song_index - 1, prev_song_len - 20)
else:
self.mpd_client.seek(song_index - 1, 0)
else:
# if we're less than 20 seconds into the first part, rewind
# to the start of it
self.mpd_client.seek(0, 0)
def volume_up(self, channel):
volume = int(self.get_status()['volume'])
self.set_volume(min(volume + 10, 100))
def volume_down(self, channel):
volume = int(self.get_status()['volume'])
self.set_volume(max(volume - 10, 0))
def set_volume(self, volume):
"""Set the volume on the MPD client"""
self.status_light.interrupt('blink_fast', 3)
with self.mpd_client:
self.mpd_client.setvol(volume)
print "volume set to %d" % volume
def stop(self):
"""On stopping, reset the current playback and stop and clear the playlist
In contract to pausing, stopping is actually meant to completely stop playing
the current book and start listening to another"""
self.playing = False
self.book.reset()
self.status_light.action = 'on'
with self.mpd_client:
self.mpd_client.stop()
self.mpd_client.clear()
def play(self, book_id, progress=None):
"""Play the book as defined in self.book
1. Get the parts from the current book and add them to the playlsit
2. Start playing the playlist
3. Immediately set the position the last know position to resume playback where
we last left off"""
def sorter(file1, file2):
"""sorting algorithm for files in playlist"""
pattern = '(\d+)(-(\d+))?\.mp3'
try:
file1_index = re.search(pattern, file1).groups()[2] or 0
file2_index = re.search(pattern, file2).groups()[2] or 0
return -1 if int(file1_index) < int(file2_index) else 1
except:
return 0
with self.mpd_client:
parts = self.mpd_client.search('filename', book_id)
if not parts:
self.status_light.interrupt('blink_fast', 3)
return
self.mpd_client.clear()
for part in sorted(parts, cmp=sorter):
self.mpd_client.add(part['file'])
self.book.book_id = book_id
if progress:
# resume at last known position
self.book.set_progress(progress)
self.mpd_client.seek(int(self.book.part) - 1, int(self.book.elapsed))
else:
# start playing from the beginning
self.mpd_client.play()
self.status_light.action = 'blink'
self.book.file_info = self.get_file_info()
def is_playing(self):
return self.get_status()['state'] == 'play'
def finished_book(self):
"""return if a book has finished, in which case we need to delete it from the db
or otherwise we could never listen to that particular book again"""
status = self.get_status()
return self.book.book_id is not None and \
status['state'] == 'stop' and \
self.book.part == int(status['playlistlength']) and \
'time' in self.book.file_info and float(self.book.file_info['time']) - self.book.elapsed < 20
def get_status(self):
with self.mpd_client:
return self.mpd_client.status()
def get_file_info(self):
with self.mpd_client:
return self.mpd_client.currentsong()
def close(self):
self.stop()
self.mpd_client.close()
self.mpd_client.disconnect()