Manage Albums and Playlists in radioob

This commit is contained in:
Bezleputh 2014-02-18 23:04:26 +01:00 committed by Florent
commit e33c177212
2 changed files with 301 additions and 54 deletions

View file

@ -24,7 +24,7 @@ import re
import requests import requests
from weboob.capabilities.radio import ICapRadio, Radio from weboob.capabilities.radio import ICapRadio, Radio
from weboob.capabilities.audio import ICapAudio, BaseAudio from weboob.capabilities.audio import ICapAudio, BaseAudio, Playlist, Album
from weboob.capabilities.base import empty from weboob.capabilities.base import empty
from weboob.tools.application.repl import ReplApplication, defaultcount from weboob.tools.application.repl import ReplApplication, defaultcount
from weboob.tools.application.media_player import InvalidMediaPlayer, MediaPlayer, MediaPlayerNotFound from weboob.tools.application.media_player import InvalidMediaPlayer, MediaPlayer, MediaPlayerNotFound
@ -49,6 +49,75 @@ class RadioListFormatter(PrettyFormatter):
return result 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): class Radioob(ReplApplication):
APPNAME = 'radioob' APPNAME = 'radioob'
VERSION = '0.j' VERSION = '0.j'
@ -57,11 +126,16 @@ class Radioob(ReplApplication):
"like the current song." "like the current song."
SHORT_DESCRIPTION = "search, show or listen to radio stations" SHORT_DESCRIPTION = "search, show or listen to radio stations"
CAPS = (ICapRadio, ICapAudio) CAPS = (ICapRadio, ICapAudio)
EXTRA_FORMATTERS = {'radio_list': RadioListFormatter} EXTRA_FORMATTERS = {'radio_list': RadioListFormatter,
COMMANDS_FORMATTERS = {'ls': 'radio_list', 'song_list': SongListFormatter,
'search': 'radio_list', 'album_tracks_list_info': AlbumTrackListInfoFormatter,
'playlist_tracks_list_info': PlaylistTrackListInfoFormatter,
}
COMMANDS_FORMATTERS = {'ls': 'radio_list',
'playlist': 'radio_list', 'playlist': 'radio_list',
} }
COLLECTION_OBJECTS = (Radio, BaseAudio, ) COLLECTION_OBJECTS = (Radio, BaseAudio, )
PLAYLIST = [] PLAYLIST = []
@ -152,24 +226,30 @@ class Radioob(ReplApplication):
try: try:
stream_id = int(stream_id) stream_id = int(stream_id)
except (ValueError,TypeError): except (ValueError, TypeError):
stream_id = 0 stream_id = 0
radio = self.get_object(_id, 'get_radio') obj = self.retrieve_obj(_id)
audio = self.get_object(_id, 'get_audio')
if radio is None and audio is None: if obj is None:
print >>sys.stderr, 'Radio or Audio file not found:', _id print >>sys.stderr, 'No object matches with this id:', _id
return 3 return 3
if audio is None: if isinstance(obj, Radio):
try: try:
stream = radio.streams[stream_id] streams = [obj.streams[stream_id]]
except IndexError: except IndexError:
print >>sys.stderr, 'Stream #%d not found' % stream_id print >>sys.stderr, 'Stream %d not found' % stream_id
return 1 return 1
elif isinstance(obj, BaseAudio):
streams = [obj]
else: else:
stream = audio streams = obj.tracks_list
if len(streams) == 0:
print >>sys.stderr, 'Radio or Audio file not found:', _id
return 3
try: try:
player_name = self.config.get('media_player') player_name = self.config.get('media_player')
@ -178,31 +258,58 @@ class Radioob(ReplApplication):
self.logger.debug(u'You can set the media_player key to the player you prefer in the radioob ' self.logger.debug(u'You can set the media_player key to the player you prefer in the radioob '
'configuration file.') 'configuration file.')
r = requests.get(stream.url, stream=True) for stream in streams:
buf = r.iter_content(512).next() if isinstance(stream, BaseAudio) and not stream.url:
r.close() stream = self.get_object(stream.id, 'get_audio')
playlistFormat = None else:
for line in buf.split("\n"): r = requests.get(stream.url, stream=True)
if playlistFormat is None: buf = r.iter_content(512).next()
if line == "[playlist]": r.close()
playlistFormat = "pls" playlistFormat = None
elif line == "#EXTM3U": for line in buf.split("\n"):
playlistFormat = "m3u" if playlistFormat is None:
else: if line == "[playlist]":
break playlistFormat = "pls"
elif playlistFormat == "pls": elif line == "#EXTM3U":
if line.startswith('File'): playlistFormat = "m3u"
stream.url = line.split('=', 1).pop(1).strip() else:
break break
elif playlistFormat == "m3u": elif playlistFormat == "pls":
if line[0] != "#": if line.startswith('File'):
stream.url = line.strip() stream.url = line.split('=', 1).pop(1).strip()
break 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)
self.player.play(stream, player_name=player_name, player_args=media_player_args)
except (InvalidMediaPlayer, MediaPlayerNotFound) as e: except (InvalidMediaPlayer, MediaPlayerNotFound) as e:
print '%s\nRadio URL: %s' % (e, stream.url) print '%s\nRadio URL: %s' % (e, stream.url)
def retrieve_obj(self, _id):
if self.interactive:
try:
obj = self.objects[int(_id) - 1]
_id = obj.id
except (IndexError, ValueError):
pass
m = re.match('^(\w+)\.(.*)', _id)
if m:
if m.group(1) == 'album':
return self.get_object(_id, 'get_album')
elif m.group(1) == 'playlist':
return self.get_object(_id, 'get_playlist')
else:
return self.get_object(_id, 'get_audio')
return self.get_object(_id, 'get_radio')
def do_playlist(self, line): def do_playlist(self, line):
""" """
playlist cmd [args] playlist cmd [args]
@ -269,7 +376,6 @@ class Radioob(ReplApplication):
print >>sys.stderr, 'Playlist command only support "add", "remove", "display" and "export" arguments.' print >>sys.stderr, 'Playlist command only support "add", "remove", "display" and "export" arguments.'
return 2 return 2
def complete_info(self, text, line, *ignored): def complete_info(self, text, line, *ignored):
args = line.split(' ') args = line.split(' ')
if len(args) == 2: if len(args) == 2:
@ -285,35 +391,67 @@ class Radioob(ReplApplication):
print >>sys.stderr, 'This command takes an argument: %s' % self.get_command_help('info', short=True) print >>sys.stderr, 'This command takes an argument: %s' % self.get_command_help('info', short=True)
return 2 return 2
radio = self.get_object(_id, 'get_radio') obj = self.retrieve_obj(_id)
audio = self.get_object(_id, 'get_audio')
if radio is None and audio is None: if isinstance(obj, Album):
print >>sys.stderr, 'Radio or Audio file not found:', _id self.set_formatter('album_tracks_list_info')
elif isinstance(obj, Playlist):
self.set_formatter('playlist_tracks_list_info')
if obj is None:
print >>sys.stderr, 'No object matches with this id:', _id
return 3 return 3
if audio is None: self.format(obj)
self.format(radio)
else:
self.format(audio)
@defaultcount(10) @defaultcount(10)
def do_search(self, pattern=None): def do_search(self, pattern=None):
""" """
search PATTERN search (radio|song|album|playlist) PATTERN
List radios matching a PATTERN. List (radio|song|album|playlist) matching a PATTERN.
If PATTERN is not given, this command will list all the radios. If PATTERN is not given, this command will list all the (radio|song|album|playlist).
""" """
if not pattern:
print >>sys.stderr, 'This command takes an argument: %s' % self.get_command_help('playlist')
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.set_formatter_header(u'Search pattern: %s' % pattern if pattern else u'All radios')
self.change_path([u'search']) self.change_path([u'search'])
for backend, radio in self.do('iter_radios_search', pattern=pattern):
self.add_object(radio)
self.format(radio)
for backend, audio in self.do('search_audio', pattern=pattern):
self.add_object(audio)
self.format(audio)
if cmd == "radio":
self.set_formatter('radio_list')
for backend, radio in self.do('iter_radios_search', pattern=args):
self.add_object(radio)
self.format(radio)
elif cmd == "song":
self.set_formatter('song_list')
for backend, audio in self.do('search_audio', pattern=args):
self.add_object(audio)
self.format(audio)
elif cmd == "album":
self.set_formatter('song_list')
for backend, album in self.do('search_album', pattern=args):
self.add_object(album)
self.format(album)
elif cmd == "playlist":
self.set_formatter('song_list')
for backend, playlist in self.do('search_playlist', pattern=args):
self.add_object(playlist)
self.format(playlist)
else:
print >>sys.stderr, 'Search command only supports "radio", "song", "album" and "playlist" arguments.'
return 2
def do_ls(self, line): def do_ls(self, line):
""" """

View file

@ -17,17 +17,74 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>. # along with weboob. If not, see <http://www.gnu.org/licenses/>.
import re
from datetime import timedelta from datetime import timedelta
from .image import BaseImage from .image import BaseImage
from .base import Field, StringField from .base import Field, StringField, IntField, CapBaseObject
from .file import ICapFile, BaseFile from .file import ICapFile, BaseFile
__all__ = ['BaseAudio', 'ICapAudio'] __all__ = ['BaseAudio', 'ICapAudio']
def decode_id(decode_id):
def wrapper(func):
def inner(self, *args, **kwargs):
arg = unicode(args[0])
_id = decode_id(arg)
if _id is None:
return None
new_args = [_id]
new_args.extend(args[1:])
return func(self, *new_args, **kwargs)
return inner
return wrapper
class Album(CapBaseObject):
"""
Represent an album
"""
title = StringField('album name')
author = StringField('artist name')
year = IntField('release year')
thumbnail = Field('Image associated to the album', BaseImage)
tracks_list = Field('list of tracks', list)
def __init__(self, _id):
CapBaseObject.__init__(self, unicode("album.%s" % _id))
@classmethod
def decode_id(cls, _id):
if _id:
m = re.match('^(album)\.(.*)', _id)
if m:
return m.group(2)
return _id
class Playlist(CapBaseObject):
"""
Represent a playlist
"""
title = StringField('playlist name')
tracks_list = Field('list of tracks', list)
def __init__(self, _id):
CapBaseObject.__init__(self, unicode("playlist.%s" % _id))
@classmethod
def decode_id(cls, _id):
if _id:
m = re.match('^(playlist)\.(.*)', _id)
if m:
return m.group(2)
return _id
class BaseAudio(BaseFile): class BaseAudio(BaseFile):
""" """
Represent an audio file Represent an audio file
@ -37,6 +94,17 @@ class BaseAudio(BaseFile):
format = StringField('file format') format = StringField('file format')
thumbnail = Field('Image associated to the file', BaseImage) thumbnail = Field('Image associated to the file', BaseImage)
def __init__(self, _id):
BaseFile.__init__(self, unicode("audio.%s" % _id))
@classmethod
def decode_id(cls, _id):
if _id:
m = re.match('^(audio)\.(.*)', _id)
if m:
return m.group(2)
return _id
class ICapAudio(ICapFile): class ICapAudio(ICapFile):
""" """
@ -53,6 +121,25 @@ class ICapAudio(ICapFile):
""" """
return self.search_file(pattern, sortby) return self.search_file(pattern, sortby)
def search_album(self, pattern, sortby=ICapFile.SEARCH_RELEVANCE):
"""
search for an album
:param pattern: pattern to search on
:type pattern: str
:rtype: iter[:class:`Album`]
"""
raise NotImplementedError()
def search_playlist(self, pattern, sortby=ICapFile.SEARCH_RELEVANCE):
"""
search for an album
:param pattern: pattern to search on
:type pattern: str
:rtype: iter[:class:`Playlist`]
"""
raise NotImplementedError()
@decode_id(BaseAudio.decode_id)
def get_audio(self, _id): def get_audio(self, _id):
""" """
Get an audio file from an ID. Get an audio file from an ID.
@ -62,3 +149,25 @@ class ICapAudio(ICapFile):
:rtype: :class:`BaseAudio`] :rtype: :class:`BaseAudio`]
""" """
return self.get_file(_id) return self.get_file(_id)
@decode_id(Playlist.decode_id)
def get_playlist(self, _id):
"""
Get a playlist from an ID.
:param id: playlist ID
:type id: str
:rtype: :class:`Playlist`]
"""
raise NotImplementedError()
@decode_id(Album.decode_id)
def get_album(self, _id):
"""
Get an album from an ID.
:param id: album ID
:type id: str
:rtype: :class:`Album`]
"""
raise NotImplementedError()