diff --git a/modules/grooveshark/__init__.py b/modules/grooveshark/__init__.py new file mode 100644 index 00000000..f7ad83f5 --- /dev/null +++ b/modules/grooveshark/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 Bezleputh +# +# 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 . + + +from .backend import GroovesharkBackend + + +__all__ = ['GroovesharkBackend'] diff --git a/modules/grooveshark/backend.py b/modules/grooveshark/backend.py new file mode 100644 index 00000000..687ad306 --- /dev/null +++ b/modules/grooveshark/backend.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 Bezleputh +# +# 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 . + + +from weboob.tools.backend import BaseBackend +from weboob.capabilities.radio import ICapRadio, Radio, Stream, Emission +from weboob.capabilities.video import ICapVideo, BaseVideo +from weboob.capabilities.collection import ICapCollection +from .browser import GroovesharkBrowser + +__all__ = ['GroovesharkBackend'] + +class GroovesharkBackend(BaseBackend, ICapVideo, ICapCollection): + NAME = 'grooveshark' + DESCRIPTION = u'grooveshark website' + MAINTAINER = u'Bezleputh' + EMAIL = 'carton_ben@yahoo.fr' + VERSION = '0.g' + LICENSE = 'AGPLv3+' + + BROWSER = GroovesharkBrowser + + def fill_video(self, video, fields): + if 'url' in fields: + with self.browser: + video.url = unicode(self.browser.get_stream_url_from_song_id(video.id)) + if 'thumbnail' in fields and video.thumbnail: + with self.browser: + video.thumbnail.data = self.browser.readurl(video.thumbnail.url) + + def search_videos(self, pattern, sortby=ICapVideo.SEARCH_RELEVANCE, nsfw=False, max_results=10): + with self.browser: + for video in self.browser.search_videos(pattern, max_results): + yield video + + def get_video(self, video): + with self.browser: + return self.browser.fill_stream_list(video) + + OBJECTS = {BaseVideo: fill_video} diff --git a/modules/grooveshark/browser.py b/modules/grooveshark/browser.py new file mode 100644 index 00000000..58cc64ab --- /dev/null +++ b/modules/grooveshark/browser.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 Bezleputh +# +# 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 . + +from weboob.tools.browser import BaseBrowser +from weboob.tools.json import json as simplejson +from weboob.capabilities.video import BaseVideo +from weboob.tools.capabilities.thumbnail import Thumbnail +import urllib2 +import hashlib +import uuid +import string +import random +import datetime +from .pages import IndexPage + +__all__ = ['GroovesharkBrowser'] + +class APIError(Exception): + pass + +class GroovesharkBrowser(BaseBrowser): + + PROTOCOL = 'http' + DOMAIN = 'html5.grooveshark.com' + #SAVE_RESPONSE = True + #DEBUG_HTTP = True + #DEBUG_MECHANIZE = True + API_URL = 'https://html5.grooveshark.com/more.php' + + #Setting the static header (country, session and uuid) + HEADER = {} + HEADER['country'] = {} + HEADER['country']['CC1'] = 0 + HEADER['country']['CC2'] = 0 + HEADER['country']['CC3'] = 0 + HEADER['country']['CC4'] = 0 + HEADER['country']['ID'] = 1 + HEADER['country']['IPR'] = 1 + HEADER['privacy'] = 0 + HEADER['session'] = (''.join(random.choice(string.digits + string.letters[:6]) for x in range(32))).lower() + HEADER["uuid"] = str.upper(str(uuid.uuid4())) + + #those values depends on a grooveshark version and may change + GROOVESHARK_CONSTANTS = ('mobileshark', '20120830', 'gooeyFlubber') + COMMUNICATION_TOKEN = None + + PAGES = { + 'http://html5.grooveshark.com/.*': IndexPage, + } + + def home(self): + self.location('/'); + assert self.is_on_page(IndexPage) + self.get_communication_token() + + def search_videos(self, pattern, max_results): + method = 'getResultsFromSearch' + + parameters = {} + parameters['query'] = pattern.encode(self.ENCODING) + parameters['type'] = ['Songs']#['Songs','Playlists','Albums'] + parameters['guts'] = 0 + parameters['ppOverr'] = '' + + response = self.API_post(method, parameters, self.create_token(method)) + + songs = self.create_video_from_songs_result(response['result']['result']['Songs'], max_results) + #playlists = self.create_video_from_playlist_result(response['result']['result']['Playlists']) + #albums = self.create_video_from_albums_result(response['result']['result']['Albums']) + + return songs + + def create_video_from_songs_result(self, songs, max_results): + videos = [] + + if max_results: + songs = songs[0:max_results] + + for song in songs: + video = BaseVideo(song['SongID']) + video.title = u'Song - %s' % song['SongName'].encode('ascii', 'replace') + video.author = u'%s' % song['ArtistName'].encode('ascii', 'replace') + video.description = u'%s - %s - %s'%(video.author, song['AlbumName'].encode('ascii', 'replace'), song['Year'].encode('ascii', 'replace')) + video.thumbnail = Thumbnail(u'http://images.gs-cdn.net/static/albums/40_' + song['CoverArtFilename']) + video.duration = datetime.timedelta(seconds=int(float(song['EstimateDuration']))) + video.rating = float(song['AvgRating']) + video.date = datetime.date(year=int(song['Year']), month=1, day=1) + videos.append(video) + return videos + + def create_video_from_playlist_result(self, playlists): + videos = [] + for playlist in playlists: + video = BaseVideo(playlist['PlaylistID']) + video.title = u'Playlist - %s' %(playlist['Name']) + video.description = playlist['Artists'] + videos.append(video) + return videos + + def create_video_from_albums_result(self, albums): + videos = [] + for album in albums: + video = BaseVideo(album['AlbumID']) + video.title = u'Album - %s' %(album['Name']) + video.description = album['Year'] + videos.append(video) + return videos + + + def get_communication_token(self): + parameters = {'secretKey': hashlib.md5(self.HEADER["session"]).hexdigest()} + result = self.API_post('getCommunicationToken', parameters) + self.COMMUNICATION_TOKEN = result['result'] + + def create_token(self, method): + + if self.COMMUNICATION_TOKEN is None: + self.get_communication_token() + + rnd = (''.join(random.choice(string.hexdigits) for x in range(6))) + return rnd + hashlib.sha1('%s:%s:%s:%s' % (method, self.COMMUNICATION_TOKEN, self.GROOVESHARK_CONSTANTS[2], rnd)).hexdigest() + + def fill_stream_list(self, video): + if isinstance(video, BaseVideo): + video.url = self.get_stream_url_from_song_id(video.id) + + def get_stream_url_from_song_id(self, song_id): + method = 'getStreamKeyFromSongIDEx' + + parameters = {} + parameters['prefetch'] = False + parameters['mobile'] = True + parameters['songID'] = int(song_id) + parameters['country'] = self.HEADER['country'] + + response = self.API_post(method, parameters, self.create_token(method)) + + self.mark_song_downloaded_ex(response['result']) + + return u'http://%s/stream.php?streamKey=%s' %(response['result']['ip'], response['result']['streamKey']) + + # in order to simulate a real browser + def mark_song_downloaded_ex(self, response): + method = 'markSongDownloadedEx' + + parameters = {} + parameters['streamKey'] = response['streamKey'] + parameters['streamServerID'] = response['streamServerID'] + parameters['songID'] = response['SongID'] + + response = self.API_post(method, parameters, self.create_token(method)) + + def check_result(self, result): + if 'fault' in result: + raise APIError('%s' % result['fault']['message']) + if not result['result']: + raise APIError('%s' % "No response found") + + def API_post(self, method, parameters, token=None): + """ + Submit a POST request to the website + The JSON data is parsed and returned as a dictionary + """ + data = self.create_json_data(method, parameters, token) + req = self.create_request(method) + response = urllib2.urlopen(req, data) + return self.parse_response(response) + + def create_json_data(self, method, parameters, token): + data = {} + data['header'] = self.HEADER + data['header']['client'] = self.GROOVESHARK_CONSTANTS[0] + data['header']['clientRevision'] = self.GROOVESHARK_CONSTANTS[1] + if(token is not None): + data['header']['token'] = token + data['method'] = method + data['parameters'] = parameters + return simplejson.dumps(data) + + def create_request(self, method): + req = urllib2.Request('%s?%s' %(self.API_URL, method)) + req.add_header('Content-Type', 'application/json') + return req + + def parse_response(self, response): + result = simplejson.loads(response.read(), self.ENCODING) + self.check_result(result) + return result diff --git a/modules/grooveshark/pages.py b/modules/grooveshark/pages.py new file mode 100644 index 00000000..d685c9e8 --- /dev/null +++ b/modules/grooveshark/pages.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 Bezleputh +# +# 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 . + + +from weboob.tools.browser import BasePage + +__all__ = ['IndexPage'] + +class IndexPage(BasePage): + pass diff --git a/modules/grooveshark/test.py b/modules/grooveshark/test.py new file mode 100644 index 00000000..d9eccfcf --- /dev/null +++ b/modules/grooveshark/test.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +# Copyright(C) 2013 Bezleputh +# +# 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 . + + +from weboob.tools.test import BackendTest + + +class GroovesharkTest(BackendTest): + BACKEND = 'grooveshark' + + def test_grooveshark_video_search(self): + result = list(self.backend.search_videos("Loic Lantoine")) + self.assertTrue(len(result)>0)