diff --git a/weboob/applications/radioob/radioob.py b/weboob/applications/radioob/radioob.py index 9efd5218..4c15a43c 100644 --- a/weboob/applications/radioob/radioob.py +++ b/weboob/applications/radioob/radioob.py @@ -19,10 +19,14 @@ from __future__ import print_function -import subprocess +import subprocess # FIXME use subprocess everywhere instead of os.spawn* import os +import sys import re import requests +import signal +import copy +import time from weboob.capabilities.radio import CapRadio, Radio from weboob.capabilities.audio import CapAudio, BaseAudio, Playlist, Album @@ -198,14 +202,6 @@ class Radioob(ReplApplication): audio.url = _obj.url - def check_exec(executable): - with open(os.devnull, 'w') as devnull: - process = subprocess.Popen(['which', executable], stdout=devnull) - if process.wait() != 0: - print('Please install "%s"' % executable, file=self.stderr) - return False - return True - def audio_to_file(_audio): ext = _audio.ext if not ext: @@ -220,23 +216,188 @@ class Radioob(ReplApplication): dest = audio_to_file(audio) if audio.url.startswith('rtmp'): - if not check_exec('rtmpdump'): + if not self.check_exec('rtmpdump'): return 1 args = ('rtmpdump', '-e', '-r', audio.url, '-o', dest) elif audio.url.startswith('mms'): - if not check_exec('mimms'): + if not self.check_exec('mimms'): return 1 args = ('mimms', '-r', audio.url, dest) else: - if check_exec('wget'): + if self.check_exec('wget'): args = ('wget', '-c', audio.url, '-O', dest) - elif check_exec('curl'): + elif self.check_exec('curl'): args = ('curl', '-C', '-', audio.url, '-o', dest) else: return 1 os.spawnlp(os.P_WAIT, args[0], *args) + + def check_exec(self, executable): + with open(os.devnull, 'w') as devnull: + process = subprocess.Popen(['which', executable], stdout=devnull) + if process.wait() != 0: + print('Please install "%s"' % executable, file=self.stderr) + return False + return True + + + def do_record(self, line): + """ + record ID [STREAM] + + Record each track of an audio stream in a separate file in a directory. + """ + + streams, dest = self.record_parse(line) + if isinstance(streams, int): + return streams + + for stream in streams: + self.record_stream(stream, dest, background = False) + + + def do_recordplay(self, line): + """ + recordplay ID [STREAM] + + Record each track of an audio stream in a separate file AND play it at the same time. + This takes care of not wasting bandwidth: only one connection to the stream is used. + """ + + streams,dest = self.record_parse(line) + if isinstance(streams, int): + return streams + + for stream in streams: + # Start the recorder in the background + # recpid = self.record_stream(stream, dest, background=True) + recpid = self.record_stream(stream, dest, background=True) + time.sleep(1) + self.logger.debug(u'streamripper recorder in background with PID: %i' % recpid) + + # Play the local relay server, not the original stream. + relay_port = self.config.get('relay_port') + if not relay_port: + relay_port = 8000 + self.logger.debug(u'No relay_port in config file, will use default: %i' % relay_port) + # Build a local stream to avoid changing the stream URL. + # localstream = copy.deepcopy(stream) # FIXME reference kept, why? + # localstream.url = u'http://localhost:%i' % relay_port + class Media: + def __init__(self, url, title, _id): + self.url = url + self.title = title + self.id = _id + localstream = Media(u'http://localhost:%i' % relay_port, stream.title, stream.id) + + player_name = self.config.get('media_player') + media_player_args = self.config.get('media_player_args') + if not player_name: + self.logger.debug(u'No media_player in config file, will try to guess.') + try: + self.player.play(localstream, player_name=player_name, player_args=media_player_args) + except KeyboardInterrupt: + self.logger.debug(u'Player closed') + pass + except: + e = sys.exc_info()[0] + self.logger.debug(u'Encountered an error: %s' % e) + raise + finally: + # Once player ends, terminate the recorder too. + self.logger.debug(u'Terminate the recorder') + # os.kill(recpid, signal.SIGTERM) + os.kill(recpid, signal.SIGKILL) + + + def record_parse(self, line): + _id, stream_id = self.parse_command_args(line, 2, 1) + + + try: + stream_id = int(stream_id) + except (ValueError, TypeError): + stream_id = 0 + + obj = self.retrieve_obj(_id) + + if obj is None: + print('No object matches with this id:', _id, file=self.stderr) + return 3 + + if isinstance(obj, Radio): + try: + streams = [obj.streams[stream_id]] + except IndexError: + print('Stream %d not found' % stream_id, file=self.stderr) + return 1 + elif isinstance(obj, BaseAudio): + streams = [obj] + + else: + streams = obj.tracks_list + + + if len(streams) == 0: + print('Radio or Audio file not found:', _id, file=self.stderr) + return 3 + + dest = "records" + return streams, dest + + + def record_stream(self, stream, dest, background = False): + """ + Run streamripper on the given stream and save files in the given directory. + + If background is False, this function returns the process id of the new process; + if background is True, returns the process’s exit code if it exits normally, + or -signal, where signal is the signal that killed the process. + """ + if dest is None: + title = stream.title if stream.title else stream.id + dest = '%s' % re.sub('[?:/]', '-', title) + + if not os.path.exists(dest): + os.makedirs(dest) + + if not os.path.isdir(dest): + print('The destination "%s" is not a directory' % dest, file=self.stderr) + return 5 + + if not self.check_exec('streamripper'): + print('streamripper not found, please install it', file=self.stderr) + return 1 + + if background: + # Start a relay server, but do not try to find a free port. + relay_port = self.config.get('relay_port') + if not relay_port: + relay_port = 8000 + args = ['streamripper', stream.url, '-d', dest, '-z', '-r', str(relay_port)] + spawn = subprocess.Popen + else: + args = ['streamripper', stream.url, '-d', dest] + spawn = subprocess.call + + try: + self.logger.debug(u'Invoking: %s' % (u' '.join([str(a) for a in args]))) + proc = spawn(args) + except OSError: + print(u'Failed to start the recorder') + return 7 + + if proc.returncode: + print('The recorder ended unexpectedly', file=self.stderr) + self.logger.debug(u'streamripper error, pid=%i, code=%i' % (proc.pid, proc.returncode)) + return 8 + + return proc.pid + + + def complete_play(self, text, line, *ignored): args = line.split(' ') if len(args) == 2: