Permalink
Browse files

Basic playback control works now.

- Removed mpylayer, controlling mplayer directly via asyncproc.py
  instead.
- Added api functions for playing, skipping and reporting songs.
- Tuned client for playback and volume control.
  • Loading branch information...
dbrgn committed Apr 4, 2013
1 parent d02a240 commit 2be3036d9e95d08d7672eec7d3e5124e5bccf9f0
Showing with 171 additions and 23 deletions.
  1. +89 −17 orochi/api.py
  2. +82 −5 orochi/client.py
  3. +0 −1 requirements.txt
View
@@ -3,10 +3,8 @@
import os
import sys
-import time
import requests
-import mpylayer
def env(key):
@@ -33,7 +31,6 @@ def __init__(self):
'Accept': 'application/json',
})
self.play_token = None
- self.current_track = None
def _get(self, resource, params={}, **kwargs):
"""Do a GET request to the specified API resource.
@@ -119,24 +116,99 @@ def search_mix(self, query, sort='hot', page=1, per_page=20):
})
return data['mixes']
+ def _playback_control(self, mix_id, command):
+ """Used to do play/next/skip requests.
+
+ Args:
+ mix_id:
+ The 8tracks mix id to start playing.
+ command:
+ The command to execute (play/next/skip).
+
+ Returns:
+ Information about the set, including track data.
+
+ """
+ play_token = self._obtain_play_token()
+ resource = 'sets/{token}/{command}.json'.format(token=play_token, command=command)
+ data = self._get(resource, {
+ 'mix_id': mix_id,
+ })
+ return data['set']
+
def play_mix(self, mix_id):
+ """Start a mix playback.
+
+ Args:
+ mix_id:
+ The 8tracks mix id to start playing.
+
+ Returns:
+ Information about the playing set, including track data.
+
+ """
+ return self._playback_control(mix_id, 'play')
+
+ def next_track(self, mix_id):
+ """Request the next track after a track has regularly finished playing.
+
+ If you want to skip a track, use ``skip_track`` instead.
+
+ Args:
+ mix_id:
+ The currently playing 8tracks mix id.
+
+ Returns:
+ New set information, including track data.
+
+ """
+ return self._playback_control(mix_id, 'next')
+
+ def skip_track(self, mix_id):
+ """Skip a track.
+
+ Note that the caller has the responsibility to check whether the user
+ is allowed to skip a track or not.
+
+ Args:
+ mix_id:
+ The currently playing 8tracks mix id.
+
+ Returns:
+ New set information, including track data.
+
+ """
+ return self._playback_control(mix_id, 'skip')
+
+ def report_track(self, mix_id, track_id):
+ """Report a track as played.
+
+ In order to be legal and pay royalties properly, 8tracks must report
+ every performance of every song played to SoundExchange. A
+ "performance" is counted when the 30 second mark of a song is reached.
+ So at 30 seconds, you must call this function.
+
+ Args:
+ mix_id:
+ The currently playing 8tracks mix id.
+ track_id:
+ The id of the track to report.
+
+ Returns:
+ TODO
+
+ Raises:
+ TODO
+
+ """
play_token = self._obtain_play_token()
- data = self._get('sets/{token}/play.json'.format(token=play_token), {
+ data = self._get('sets/{token}/report.json'.format(token=play_token), {
'mix_id': mix_id,
+ 'track_id': track_id,
})
- self.current_track = data['set']['track']
- print('Track url: ' + self.current_track['url'])
- #Track: {u'performer': u'Yukon Blonde', u'name': u'Brides Song', u'url': u'https://dtp6gm33au72i.cloudfront.net/tf/000/796/'
-
-
-## Get song
-#
-#params = {'mix_id': mix['id']}
-#r = requests.get(BASE_URL + 'sets/{token}/play.json'.format(token=play_token), params=params, headers=HEADERS)
-#data_track = r.json()
-#track = data_track['set']['track']
-#
-#print('Now playing "{track[name]}" by "{track[performer]}"...'.format(track=track))
+ import ipdb; ipdb.set_trace()
+
+
#mp = mpylayer.MPlayerControl()
#mp.loadfile(track['url'])
#time.sleep(1)
View
@@ -3,16 +3,26 @@
import os
import cmd
+import time
from string import Template
from textwrap import TextWrapper
+try:
+ from shlex import quote
+except ImportError: # Python < 3.3
+ from pipes import quote
from .api import EightTracksAPI
+from .asyncproc import Process
+
+
+LOADFILE_TIMEOUT = 6
class CmdExitMixin(object):
"""A mixin for a Cmd instance that provides the exit and quit command."""
def do_exit(self, s):
+ print('Goodbye.')
return True
def help_exit(self):
@@ -36,6 +46,7 @@ class Client(CmdExitMixin, cmd.Cmd, object):
def preloop(self):
self.api = EightTracksAPI()
self.mix_ids = {}
+ self.volume = 100
return super(Client, self).preloop()
def precmd(self, line):
@@ -75,12 +86,11 @@ def do_play(self, s):
try:
mix_id = self.mix_ids[int(s)]
except ValueError:
- print('Invalid mix number. Please run a search first and then '
+ print('*** Invalid mix number: Please run a search first and then '
'specify a mix number to play.')
except KeyError:
- print('Mix with number {i} not found. Did you run a search yet?'.format(i=s))
+ print('*** Mix with number {i} not found: Did you run a search yet?'.format(i=s))
else:
- self.api.play_mix(mix_id)
i = PlayCommand(mix_id, self)
i.prompt = '{0}:{1})> '.format(self.prompt[:-3], mix_id)
i.cmdloop()
@@ -94,29 +104,96 @@ class PlayCommand(cmd.Cmd, object):
def __init__(self, mix_id, parent_cmd, *args, **kwargs):
self.mix_id = mix_id
+ self.parent_cmd = parent_cmd
self.api = parent_cmd.api
+
r = super(PlayCommand, self).__init__(*args, **kwargs)
+
+ # Initialize mplayer slave session with line buffer
+ self.p = Process(['mplayer', '-slave', '-quiet', '-idle'], bufsize=1)
+
+ # Play first track at max volume
+ self.status = self.api.play_mix(mix_id)
+ self._play(self.status['track']['url'])
+ self.p.write('volume {} 1\n'.format(self.parent_cmd.volume))
self.do_status('')
+
return r
def emptyline(self):
"""Don't repeat last command on empty line."""
pass
+ def _play(self, url):
+ """Play the specified file using mplayer's ``loadfile`` and wait for
+ the command to finish.
+
+ Args:
+ url:
+ The URL of the file to play.
+
+ Raises:
+ RuntimeError:
+ Raised if the loadfile command didn't return inside
+ `LOADFILE_TIMEOUT` seconds. This is done by checking the
+ subprocess' stdout for `Starting playback...`. The second
+ argument to RuntimeError is mplayer's stdout.
+
+ """
+ if url.startswith('https'):
+ url = 'http' + url[5:]
+ self.p.write('loadfile {}\n'.format(quote(url)))
+
+ # Wait for loadfile command to finish
+ start = time.time()
+ while 1:
+ if self.p.read().endswith('Starting playback...\n'):
+ break
+ if time.time() - start > LOADFILE_TIMEOUT:
+ raise RuntimeError("Playback didn't start inside {}s. ".format(LOADFILE_TIMEOUT) +
+ "Something must have gone wrong.", self.p.readerr())
+ time.sleep(0.05)
+
+ def do_pause(self, s):
+ self.p.write('pause\n')
+
+ def help_pause(self):
+ print('Pause or resume the playback.')
+
def do_stop(self, s):
print('Stopping playback...')
+ self.p.write('stop\n')
+ self.p.terminate()
return True
def help_stop(self):
print('Stop the playback and exit play mode.')
+ def do_volume(self, s):
+ try:
+ vol = int(s)
+ assert 0 <= vol <= 100
+ except (ValueError, AssertionError):
+ print('*** ValueError: Argument must be a number between 0 and 100.')
+ else:
+ self.parent_cmd.volume = vol
+ self.p.write('volume {} 1\n'.format(vol))
+
+ def help_volume(self):
+ print('Syntax: volume <amount>')
+ print('Change playback volume. The argument must be a number between 0 and 100.')
+
def do_status(self, s):
- print('Now playing "{0[name]}" by "{0[performer]}".'.format(self.api.current_track))
+ track = self.status['track']
+ print('Now playing "{0[name]}" by "{0[performer]}".'.format(track))
def help_status(self):
- print('Syntax: status')
print('Show the status of the currently playing song.')
+ do_EOF = do_stop
+ help_EOF = help_stop
+
+
if __name__ == '__main__':
client = Client()
client.cmdloop()
View
@@ -1,2 +1 @@
requests==1.2.0
-mpylayer==0.2a1

0 comments on commit 2be3036

Please sign in to comment.