-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstreamer.py
More file actions
293 lines (242 loc) · 9.03 KB
/
streamer.py
File metadata and controls
293 lines (242 loc) · 9.03 KB
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
import os
import sys
import errno
import traceback
import thread
import random
import time
from datetime import datetime
import json
from subprocess import call
from threading import Thread, Lock
from Queue import Queue, Empty
import webbrowser
from dropbox import client
# Dropbox application specific data
APP_KEY = 'kpvw9op95082dzj'
APP_SECRET = 'm953vw8r3mz45gq'
TOKEN_FILE = "token.txt"
MUSIC_FOLDER = '/Ambience'
# Caching locations to improve performance/resource utilization
MUSIC_CACHE = os.path.expanduser('./.cache')
INDEX_FILE = "index.json"
MUSIC_BUFFER_SIZE = 3
MUSIC_READ_CHUNK_SIZE = 1024
# Timing constants
MUSIC_LIST_UPDATE_TIME = 60 * 60 # 1 hour
GENERAL_SLEEP_TIME = 15
RECENT_MUSIC_DAYS = 7
class MusicListUpdater(Thread):
def __init__(self, shared):
Thread.__init__(self)
self.shared = shared
def run(self):
# Load in a cached music list
try:
self.shared.MusicListLock.acquire()
with open(INDEX_FILE, 'r') as file:
self.shared.MusicList = json.load(file)
print "[Loaded music list]"
except:
traceback.print_exc()
finally:
self.shared.MusicListLock.release()
if len(self.shared.MusicList) > 0:
time.sleep(MUSIC_LIST_UPDATE_TIME)
while True:
try:
self._perform_update()
except:
traceback.print_exc()
time.sleep(GENERAL_SLEEP_TIME)
# Keep trying if the fetch fails
if len(self.shared.MusicList) <= 0:
continue
time.sleep(MUSIC_LIST_UPDATE_TIME)
def _perform_update(self):
"""
Traverses down two levels into the music folder
and builds a list of all MP3's found.
Replaces MusicList with the new list.
"""
# Initialize the new list
newMusicList = []
newRecentList = []
try:
self.shared.DropboxLock.acquire()
# Fetch all the metadata about the music-containing folder
currentTime = datetime.now()
cursor = None
delta = self.shared.Dropbox.delta(cursor, MUSIC_FOLDER)
while len(delta['entries']) > 0 or delta['has_more']:
for path, metadata in delta['entries']:
if metadata is None:
continue
if metadata.get('mime_type', 'Something Else') != u'audio/mpeg':
continue
# Append the properly capitalized name
path = metadata.get('path', path)
newMusicList.append(path)
# Get the modification date
modified = metadata.get('modified', 'Tue, 19 Jul 2011 21:55:38 +0000')[:-6]
modified = datetime.strptime(modified, '%a, %d %b %Y %H:%M:%S')
if (currentTime - modified).days < RECENT_MUSIC_DAYS:
newRecentList.append(path)
cursor = delta['cursor']
delta = self.shared.Dropbox.delta(cursor, MUSIC_FOLDER)
except:
traceback.print_exc()
finally:
self.shared.DropboxLock.release()
# Copy the list over
try:
self.shared.MusicListLock.acquire()
self.shared.MusicList = newMusicList
self.shared.RecentMusicList = newRecentList
# Save the list in a file
with open(INDEX_FILE, 'w') as file:
json.dump(newMusicList, file)
print '[Music list updated]'
finally:
self.shared.MusicListLock.release()
class MusicBufferer(Thread):
def __init__(self, shared):
Thread.__init__(self)
self.shared = shared
self.queue = Queue()
def run(self):
while True:
if self.shared.MusicQueue.qsize() < MUSIC_BUFFER_SIZE:
try:
if self.queue.qsize() > 0:
self._downloadSong()
else:
self.addSong()
except:
traceback.print_exc()
time.sleep(GENERAL_SLEEP_TIME)
else:
time.sleep(GENERAL_SLEEP_TIME)
def addSong(self, musicSource=None):
"""
Navigates the music list at random and chooses a song to download
There is a bias towards more recent music
If the song is provided, that song is added instead
"""
try:
self.shared.MusicListLock.acquire()
# Choose a song
if musicSource is None:
if len(self.shared.MusicList) <= 0:
return
# Get a random number with a bias towards the newer stuff
musicLength = len(self.shared.MusicList)
recentLength = len(self.shared.RecentMusicList)
idx = random.randrange(musicLength + min(musicLength / 2, recentLength * 10))
if idx < musicLength:
musicSource = self.shared.MusicList[idx]
else:
idx = idx % recentLength
musicSource = self.shared.RecentMusicList[idx]
# Queue the song for download
self.queue.put(musicSource)
finally:
self.shared.MusicListLock.release()
def _downloadSong(self):
"""
Pops a song off the internal queue and downloads it
"""
# Determine where to download the song
try:
musicSource = self.queue.get(False)
except Empty:
return
musicDestPath = os.path.join(MUSIC_CACHE,
os.path.basename(os.path.dirname(musicSource)),
os.path.basename(musicSource))
try: os.makedirs(os.path.dirname(musicDestPath))
except: pass
# Download the song
musicDest = open(musicDestPath, "wb")
try:
self.shared.DropboxLock.acquire()
source, metadata = self.shared.Dropbox.get_file_and_metadata(musicSource)
musicDest.write(source.read())
musicDest.close()
source.close()
# Add it to the playlist
self.shared.MusicQueue.put(musicDestPath)
finally:
self.shared.DropboxLock.release()
class MusicPlayer(Thread):
def __init__(self, shared):
Thread.__init__(self)
self.shared = shared
def run(self):
while True:
try:
if self.shared.MusicQueue.qsize() > 0:
self.shared.CurrentSong = self.shared.MusicQueue.get()
call(["mpg123", "--buffer", "4096", "--smooth", "--quiet", self.shared.CurrentSong])
os.remove(self.shared.CurrentSong)
else:
time.sleep(GENERAL_SLEEP_TIME)
except KeyboardInterrupt:
exit()
except:
traceback.print_exc()
class DropboxAudioStreamer():
def __init__(self):
"""
Initializes the audio streamer and connects to Dropbox
"""
self.Dropbox = None
self.DropboxLock = Lock()
self.MusicList = []
self.RecentMusicList = []
self.MusicListLock = Lock()
self.MusicQueue = Queue()
self.CurrentSong = None
# Connect to Dropbox
try:
# Token is saved locally
token = open(TOKEN_FILE).read()
self.Dropbox = client.DropboxClient(token)
print "[loaded access token]"
except IOError:
# Token not available, so login
flow = client.DropboxOAuth2FlowNoRedirect(APP_KEY, APP_SECRET)
authorize_url = flow.start()
webbrowser.open(authorize_url)
code = raw_input("Enter the authorization code here: ").strip()
try:
access_token, user_id = flow.finish(code)
except ErrorResponse:
handleException()
exit()
with open(TOKEN_FILE, 'w') as f:
f.write(access_token)
self.Dropbox = client.DropboxClient(access_token)
# Set the volume to 100%
call(['amixer', 'sset', "'PCM'", '100%'])
# Empty the cache of old music
call(['rm', '-r', MUSIC_CACHE])
# Make the cache directory
try: os.makedirs(MUSIC_CACHE)
except: pass
# Start the child threads
self.MusicListUpdater = MusicListUpdater(self)
self.MusicListUpdater.daemon = True
self.MusicListUpdater.start()
self.MusicBufferer = MusicBufferer(self)
self.MusicBufferer.daemon = True
self.MusicBufferer.start()
def start(self, async=True):
self.MusicPlayer = MusicPlayer(self)
if async:
self.MusicPlayer.daemon = True
self.MusicPlayer.start()
else:
self.MusicPlayer.run()
if __name__ == '__main__':
DropboxAudioStreamer().start(False)