weboob-devel/weboob/applications/radioob/radioob.py
nojhan 22588ab1a6 Add the recordplay command to radioob
Record a stream and serve it, then run a media player on the local stream
(thus no bandwidth is wasted).
The recorder is started in the background but still print on stdout.
Ctr-C ends both the recorder and the player.
2015-09-30 10:53:27 +02:00

646 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
# Copyright(C) 2010-2012 Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from __future__ import print_function
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
from weboob.capabilities.base import empty
from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.media_player import InvalidMediaPlayer, MediaPlayer, MediaPlayerNotFound
from weboob.tools.application.formatters.iformatter import PrettyFormatter
__all__ = ['Radioob']
class RadioListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title')
def get_title(self, obj):
return obj.title
def get_description(self, obj):
result = ''
if hasattr(obj, 'description') and not empty(obj.description):
result += '%-30s' % obj.description
if hasattr(obj, 'current') and not empty(obj.current):
if obj.current.who:
result += ' (Current: %s - %s)' % (obj.current.who, obj.current.what)
else:
result += ' (Current: %s)' % obj.current.what
return result
class SongListFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title')
def get_title(self, obj):
result = obj.title
if hasattr(obj, 'author') and not empty(obj.author):
result += ' (%s)' % obj.author
return result
def get_description(self, obj):
result = ''
if hasattr(obj, 'description') and not empty(obj.description):
result += '%-30s' % obj.description
return result
class AlbumTrackListInfoFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title', 'tracks_list')
def get_title(self, obj):
result = obj.title
if hasattr(obj, 'author') and not empty(obj.author):
result += ' (%s)' % obj.author
return result
def get_description(self, obj):
result = ''
for song in obj.tracks_list:
result += '- %s%-30s%s ' % (self.BOLD, song.title, self.NC)
if hasattr(song, 'duration') and not empty(song.duration):
result += '%-10s ' % song.duration
else:
result += '%-10s ' % ' '
result += '(%s)\r\n\t' % (song.id)
return result
class PlaylistTrackListInfoFormatter(PrettyFormatter):
MANDATORY_FIELDS = ('id', 'title', 'tracks_list')
def get_title(self, obj):
return obj.title
def get_description(self, obj):
result = ''
for song in obj.tracks_list:
result += '- %s%-30s%s ' % (self.BOLD, song.title, self.NC)
if hasattr(song, 'author') and not empty(song.author):
result += '(%-15s) ' % song.author
if hasattr(song, 'duration') and not empty(song.duration):
result += '%-10s ' % song.duration
else:
result += '%-10s ' % ' '
result += '(%s)\r\n\t' % (song.id)
return result
class Radioob(ReplApplication):
APPNAME = 'radioob'
VERSION = '1.1'
COPYRIGHT = 'Copyright(C) 2010-YEAR Romain Bignon\nCopyright(C) YEAR Pierre Maziere'
DESCRIPTION = "Console application allowing to search for web radio stations, listen to them and get information " \
"like the current song."
SHORT_DESCRIPTION = "search, show or listen to radio stations"
CAPS = (CapRadio, CapAudio)
EXTRA_FORMATTERS = {'radio_list': RadioListFormatter,
'song_list': SongListFormatter,
'album_tracks_list_info': AlbumTrackListInfoFormatter,
'playlist_tracks_list_info': PlaylistTrackListInfoFormatter,
}
COMMANDS_FORMATTERS = {'ls': 'radio_list',
'playlist': 'radio_list',
}
COLLECTION_OBJECTS = (Radio, BaseAudio, )
PLAYLIST = []
def __init__(self, *args, **kwargs):
ReplApplication.__init__(self, *args, **kwargs)
self.player = MediaPlayer(self.logger)
def main(self, argv):
self.load_config()
return ReplApplication.main(self, argv)
def complete_download(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
elif len(args) >= 3:
return self.path_completer(args[2])
def do_download(self, line):
"""
download ID [DIRECTORY]
Download an audio file
"""
_id, dest = self.parse_command_args(line, 2, 1)
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, 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
for stream in streams:
self.download_file(stream, dest)
def download_file(self, audio, dest):
_obj = self.get_object(audio.id, 'get_audio', ['url', 'title'])
if not _obj:
print('Audio file not found: %s' % audio.id, file=self.stderr)
return 3
if not _obj.url:
print('Error: the direct URL is not available.', file=self.stderr)
return 4
audio.url = _obj.url
def audio_to_file(_audio):
ext = _audio.ext
if not ext:
ext = 'audiofile'
title = _audio.title if _audio.title else _audio.id
return '%s.%s' % (re.sub('[?:/]', '-', title), ext)
if dest is not None and os.path.isdir(dest):
dest += '/%s' % audio_to_file(audio)
if dest is None:
dest = audio_to_file(audio)
if audio.url.startswith('rtmp'):
if not self.check_exec('rtmpdump'):
return 1
args = ('rtmpdump', '-e', '-r', audio.url, '-o', dest)
elif audio.url.startswith('mms'):
if not self.check_exec('mimms'):
return 1
args = ('mimms', '-r', audio.url, dest)
else:
if self.check_exec('wget'):
args = ('wget', '-c', audio.url, '-O', dest)
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 processs 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:
return self._complete_object()
def do_play(self, line):
"""
play ID [stream_id]
Play a radio or a audio file with a found player (optionnaly specify the wanted stream).
"""
_id, stream_id = self.parse_command_args(line, 2, 1)
if not _id:
print('This command takes an argument: %s' % self.get_command_help('play', short=True), file=self.stderr)
return 2
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
try:
player_name = self.config.get('media_player')
media_player_args = self.config.get('media_player_args')
if not player_name:
self.logger.debug(u'You can set the media_player key to the player you prefer in the radioob '
'configuration file.')
for stream in streams:
if isinstance(stream, BaseAudio) and not stream.url:
stream = self.get_object(stream.id, 'get_audio')
else:
r = requests.get(stream.url, stream=True)
buf = r.iter_content(512).next()
r.close()
playlistFormat = None
for line in buf.split("\n"):
if playlistFormat is None:
if line == "[playlist]":
playlistFormat = "pls"
elif line == "#EXTM3U":
playlistFormat = "m3u"
else:
break
elif playlistFormat == "pls":
if line.startswith('File'):
stream.url = line.split('=', 1).pop(1).strip()
break
elif playlistFormat == "m3u":
if line[0] != "#":
stream.url = line.strip()
break
self.player.play(stream, player_name=player_name, player_args=media_player_args)
except (InvalidMediaPlayer, MediaPlayerNotFound) as e:
print('%s\nRadio URL: %s' % (e, stream.url))
def retrieve_obj(self, _id):
obj = None
if self.interactive:
try:
obj = self.objects[int(_id) - 1]
_id = obj.id
except (IndexError, ValueError):
pass
m = CapAudio.get_object_method(_id)
if m:
obj = self.get_object(_id, m)
return obj if obj is not None else self.get_object(_id, 'get_radio')
def do_playlist(self, line):
"""
playlist cmd [args]
playlist add ID [ID2 ID3 ...]
playlist remove ID [ID2 ID3 ...]
playlist export [FILENAME]
playlist display
"""
if not line:
print('This command takes an argument: %s' % self.get_command_help('playlist'), file=self.stderr)
return 2
cmd, args = self.parse_command_args(line, 2, req_n=1)
if cmd == "add":
_ids = args.strip().split(' ')
for _id in _ids:
audio = self.get_object(_id, 'get_audio')
if not audio:
print('Audio file not found: %s' % _id, file=self.stderr)
return 3
if not audio.url:
print('Error: the direct URL is not available.', file=self.stderr)
return 4
self.PLAYLIST.append(audio)
elif cmd == "remove":
_ids = args.strip().split(' ')
for _id in _ids:
audio_to_remove = self.get_object(_id, 'get_audio')
if not audio_to_remove:
print('Audio file not found: %s' % _id, file=self.stderr)
return 3
if not audio_to_remove.url:
print('Error: the direct URL is not available.', file=self.stderr)
return 4
for audio in self.PLAYLIST:
if audio.id == audio_to_remove.id:
self.PLAYLIST.remove(audio)
break
elif cmd == "export":
filename = "playlist.m3u"
if args:
filename = args
file = open(filename, 'w')
for audio in self.PLAYLIST:
file.write('%s\r\n' % audio.url)
file.close()
elif cmd == "display":
for audio in self.PLAYLIST:
self.cached_format(audio)
else:
print('Playlist command only support "add", "remove", "display" and "export" arguments.', file=self.stderr)
return 2
def complete_info(self, text, line, *ignored):
args = line.split(' ')
if len(args) == 2:
return self._complete_object()
def do_info(self, _id):
"""
info ID
Get information about a radio or an audio file.
"""
if not _id:
print('This command takes an argument: %s' % self.get_command_help('info', short=True), file=self.stderr)
return 2
obj = self.retrieve_obj(_id)
if isinstance(obj, Album):
self.set_formatter('album_tracks_list_info')
elif isinstance(obj, Playlist):
self.set_formatter('playlist_tracks_list_info')
if obj is None:
print('No object matches with this id:', _id, file=self.stderr)
return 3
self.format(obj)
@defaultcount(10)
def do_search(self, pattern=None):
"""
search (radio|song|file|album|playlist) PATTERN
List (radio|song|file|album|playlist) matching a PATTERN.
If PATTERN is not given, this command will list all the (radio|song|album|playlist).
"""
if not pattern:
print('This command takes an argument: %s' % self.get_command_help('playlist'), file=self.stderr)
return 2
cmd, args = self.parse_command_args(pattern, 2, req_n=1)
if not args:
args = ""
self.set_formatter_header(u'Search pattern: %s' % pattern if pattern else u'All radios')
self.change_path([u'search'])
if cmd == "radio":
self.set_formatter('radio_list')
for radio in self.do('iter_radios_search', pattern=args):
self.add_object(radio)
self.format(radio)
elif cmd == "song" or cmd == "file":
self.set_formatter('song_list')
for audio in self.do('search_audio', pattern=args):
self.add_object(audio)
self.format(audio)
elif cmd == "album":
self.set_formatter('song_list')
for album in self.do('search_album', pattern=args):
self.add_object(album)
self.format(album)
elif cmd == "playlist":
self.set_formatter('song_list')
for playlist in self.do('search_playlist', pattern=args):
self.add_object(playlist)
self.format(playlist)
else:
print('Search command only supports "radio", "song", "file", "album" and "playlist" arguments.', file=self.stderr)
return 2
def do_ls(self, line):
"""
ls
List radios
"""
ret = super(Radioob, self).do_ls(line)
return ret