2013-12-09 17:08:58 +01:00
# encoding: utf-8
2014-01-17 03:29:41 +01:00
from __future__ import unicode_literals
2013-06-23 20:57:44 +02:00
import re
2013-09-14 21:41:49 +02:00
import itertools
2013-06-23 20:57:44 +02:00
from . common import InfoExtractor
2014-12-13 12:24:42 +01:00
from . . compat import (
2013-06-23 20:57:44 +02:00
compat_str ,
2013-08-21 17:06:37 +02:00
compat_urlparse ,
2013-09-14 21:41:49 +02:00
compat_urllib_parse ,
2014-12-13 12:24:42 +01:00
)
from . . utils import (
2013-06-23 20:57:44 +02:00
ExtractorError ,
2014-06-07 15:51:01 +02:00
int_or_none ,
2013-06-23 20:57:44 +02:00
unified_strdate ,
)
class SoundcloudIE ( InfoExtractor ) :
""" Information extractor for soundcloud.com
To access the media , the uid of the song and a stream token
must be extracted from the page source and the script must make
a request to media . soundcloud . com / crossdomain . xml . Then
the media can be grabbed by requesting from an url composed
of the stream token and uid
"""
2014-05-05 03:12:41 +02:00
_VALID_URL = r ''' (?x)^(?:https?://)?
2013-12-19 16:39:01 +01:00
( ? : ( ? : ( ? : www \. | m \. ) ? soundcloud \. com /
2013-12-09 19:57:00 +01:00
( ? P < uploader > [ \w \d - ] + ) /
2015-06-24 16:48:57 +02:00
( ? ! sets / | ( ? : likes | tracks ) / ? ( ? : $ | [ ? #]))
2014-08-28 00:58:24 +02:00
( ? P < title > [ \w \d - ] + ) / ?
2013-12-09 17:08:58 +01:00
( ? P < token > [ ^ ? ] + ? ) ? ( ? : [ ? ] . * ) ? $ )
2014-09-18 08:02:03 +02:00
| ( ? : api \. soundcloud \. com / tracks / ( ? P < track_id > \d + )
2014-12-03 11:49:53 +01:00
( ? : / ? \? secret_token = ( ? P < secret_token > [ ^ & ] + ) ) ? )
2014-01-02 16:18:51 +01:00
| ( ? P < player > ( ? : w | player | p . ) \. soundcloud \. com / player / ? . * ? url = . * )
2013-07-24 14:39:21 +02:00
)
'''
2014-01-17 03:29:41 +01:00
IE_NAME = ' soundcloud '
2013-11-09 18:06:09 +01:00
_TESTS = [
{
2014-01-17 03:29:41 +01:00
' url ' : ' http://soundcloud.com/ethmusic/lostin-powers-she-so-heavy ' ,
' md5 ' : ' ebef0a451b909710ed1d7787dddbf0d7 ' ,
' info_dict ' : {
2014-10-25 22:32:01 +02:00
' id ' : ' 62986583 ' ,
' ext ' : ' mp3 ' ,
' upload_date ' : ' 20121011 ' ,
' description ' : ' No Downloads untill we record the finished version this weekend, i was too pumped n i had to post it , earl is prolly gonna b hella p.o \' d ' ,
' uploader ' : ' E.T. ExTerrestrial Music ' ,
' title ' : ' Lostin Powers - She so Heavy (SneakPreview) Adrian Ackers Blueprint 1 ' ,
' duration ' : 143 ,
2013-11-09 18:06:09 +01:00
}
} ,
# not streamable song
{
2014-01-17 03:29:41 +01:00
' url ' : ' https://soundcloud.com/the-concept-band/goldrushed-mastered?in=the-concept-band/sets/the-royal-concept-ep ' ,
' info_dict ' : {
' id ' : ' 47127627 ' ,
' ext ' : ' mp3 ' ,
' title ' : ' Goldrushed ' ,
2014-03-09 12:20:34 +01:00
' description ' : ' From Stockholm Sweden \r \n Povel / Magnus / Filip / David \r \n www.theroyalconcept.com ' ,
2014-01-17 03:29:41 +01:00
' uploader ' : ' The Royal Concept ' ,
' upload_date ' : ' 20120521 ' ,
2014-06-07 15:51:01 +02:00
' duration ' : 227 ,
2013-11-09 18:06:09 +01:00
} ,
2014-01-17 03:29:41 +01:00
' params ' : {
2013-11-09 18:06:09 +01:00
# rtmp
2014-01-17 03:29:41 +01:00
' skip_download ' : True ,
2013-11-09 18:06:09 +01:00
} ,
} ,
2013-12-09 17:08:58 +01:00
# private link
{
2014-01-17 03:29:41 +01:00
' url ' : ' https://soundcloud.com/jaimemf/youtube-dl-test-video-a-y-baw/s-8Pjrp ' ,
' md5 ' : ' aa0dd32bfea9b0c5ef4f02aacd080604 ' ,
' info_dict ' : {
' id ' : ' 123998367 ' ,
' ext ' : ' mp3 ' ,
' title ' : ' Youtube - Dl Test Video \' \' Ä↭ ' ,
' uploader ' : ' jaimeMF ' ,
' description ' : ' test chars: \" \' / \\ ä↭ ' ,
' upload_date ' : ' 20131209 ' ,
2014-06-07 15:51:01 +02:00
' duration ' : 9 ,
2013-12-09 17:08:58 +01:00
} ,
} ,
2014-09-18 08:02:03 +02:00
# private link (alt format)
{
' url ' : ' https://api.soundcloud.com/tracks/123998367?secret_token=s-8Pjrp ' ,
' md5 ' : ' aa0dd32bfea9b0c5ef4f02aacd080604 ' ,
' info_dict ' : {
' id ' : ' 123998367 ' ,
' ext ' : ' mp3 ' ,
' title ' : ' Youtube - Dl Test Video \' \' Ä↭ ' ,
' uploader ' : ' jaimeMF ' ,
' description ' : ' test chars: \" \' / \\ ä↭ ' ,
' upload_date ' : ' 20131209 ' ,
' duration ' : 9 ,
} ,
} ,
2013-12-10 13:04:21 +01:00
# downloadable song
{
2014-07-15 14:18:06 +02:00
' url ' : ' https://soundcloud.com/oddsamples/bus-brakes ' ,
2014-07-23 01:41:44 +02:00
' md5 ' : ' 7624f2351f8a3b2e7cd51522496e7631 ' ,
2014-01-17 03:29:41 +01:00
' info_dict ' : {
2014-07-15 14:18:06 +02:00
' id ' : ' 128590877 ' ,
2014-07-23 01:41:44 +02:00
' ext ' : ' mp3 ' ,
2014-07-15 14:18:06 +02:00
' title ' : ' Bus Brakes ' ,
2014-10-25 22:32:01 +02:00
' description ' : ' md5:0053ca6396e8d2fd7b7e1595ef12ab66 ' ,
2014-07-15 14:18:06 +02:00
' uploader ' : ' oddsamples ' ,
' upload_date ' : ' 20140109 ' ,
' duration ' : 17 ,
2013-12-10 13:04:21 +01:00
} ,
} ,
2013-11-09 18:06:09 +01:00
]
2013-06-23 20:57:44 +02:00
2013-07-24 14:05:14 +02:00
_CLIENT_ID = ' b45b1aa10f1ac2941910a7f0d10f8e28 '
2013-11-21 13:16:19 +01:00
_IPHONE_CLIENT_ID = ' 376f225bf427445fc4bfb6b99b72e0bf '
2013-07-24 14:05:14 +02:00
2013-06-23 20:57:44 +02:00
def report_resolve ( self , video_id ) :
""" Report information extraction. """
2014-03-24 02:15:31 +01:00
self . to_screen ( ' %s : Resolving id ' % video_id )
2013-06-23 20:57:44 +02:00
2013-07-24 14:05:14 +02:00
@classmethod
def _resolv_url ( cls , url ) :
return ' http://api.soundcloud.com/resolve.json?url= ' + url + ' &client_id= ' + cls . _CLIENT_ID
2013-12-09 17:08:58 +01:00
def _extract_info_dict ( self , info , full_title = None , quiet = False , secret_token = None ) :
2013-11-09 18:06:09 +01:00
track_id = compat_str ( info [ ' id ' ] )
name = full_title or track_id
2013-11-25 20:30:41 +01:00
if quiet :
2013-09-14 21:41:49 +02:00
self . report_extraction ( name )
2013-07-24 14:05:14 +02:00
thumbnail = info [ ' artwork_url ' ]
if thumbnail is not None :
thumbnail = thumbnail . replace ( ' -large ' , ' -t500x500 ' )
2014-01-17 03:29:41 +01:00
ext = ' mp3 '
2013-11-09 18:06:09 +01:00
result = {
2013-11-25 20:30:41 +01:00
' id ' : track_id ,
2013-07-24 14:05:14 +02:00
' uploader ' : info [ ' user ' ] [ ' username ' ] ,
' upload_date ' : unified_strdate ( info [ ' created_at ' ] ) ,
2013-11-25 20:30:41 +01:00
' title ' : info [ ' title ' ] ,
2013-07-24 14:05:14 +02:00
' description ' : info [ ' description ' ] ,
' thumbnail ' : thumbnail ,
2014-06-07 15:51:01 +02:00
' duration ' : int_or_none ( info . get ( ' duration ' ) , 1000 ) ,
2014-10-26 19:08:36 +01:00
' webpage_url ' : info . get ( ' permalink_url ' ) ,
2013-07-24 14:05:14 +02:00
}
2014-03-24 02:21:17 +01:00
formats = [ ]
2013-11-09 18:06:09 +01:00
if info . get ( ' downloadable ' , False ) :
2013-11-21 13:16:19 +01:00
# We can build a direct link to the song
2013-11-25 20:30:41 +01:00
format_url = (
2014-01-17 03:29:41 +01:00
' https://api.soundcloud.com/tracks/ {0} /download?client_id= {1} ' . format (
2013-11-25 20:30:41 +01:00
track_id , self . _CLIENT_ID ) )
2014-03-24 02:21:17 +01:00
formats . append ( {
2013-11-25 20:30:41 +01:00
' format_id ' : ' download ' ,
2014-01-17 03:29:41 +01:00
' ext ' : info . get ( ' original_format ' , ' mp3 ' ) ,
2013-11-25 20:30:41 +01:00
' url ' : format_url ,
2013-11-25 22:34:56 +01:00
' vcodec ' : ' none ' ,
2014-03-24 02:21:17 +01:00
' preference ' : 10 ,
} )
# We have to retrieve the url
streams_url = ( ' http://api.soundcloud.com/i1/tracks/ {0} /streams? '
2014-11-23 21:39:15 +01:00
' client_id= {1} &secret_token= {2} ' . format ( track_id , self . _IPHONE_CLIENT_ID , secret_token ) )
2014-05-05 03:12:41 +02:00
format_dict = self . _download_json (
2014-03-24 02:21:17 +01:00
streams_url ,
track_id , ' Downloading track url ' )
for key , stream_url in format_dict . items ( ) :
if key . startswith ( ' http ' ) :
formats . append ( {
' format_id ' : key ,
' ext ' : ext ,
' url ' : stream_url ,
' vcodec ' : ' none ' ,
} )
elif key . startswith ( ' rtmp ' ) :
# The url doesn't have an rtmp app, we have to extract the playpath
url , path = stream_url . split ( ' mp3: ' , 1 )
formats . append ( {
' format_id ' : key ,
' url ' : url ,
' play_path ' : ' mp3: ' + path ,
2015-03-02 17:47:07 +01:00
' ext ' : ' flv ' ,
2014-03-24 02:21:17 +01:00
' vcodec ' : ' none ' ,
} )
2013-11-25 20:30:41 +01:00
if not formats :
2013-11-21 13:16:19 +01:00
# We fallback to the stream_url in the original info, this
# cannot be always used, sometimes it can give an HTTP 404 error
2013-11-25 20:30:41 +01:00
formats . append ( {
2014-01-17 03:29:41 +01:00
' format_id ' : ' fallback ' ,
2013-11-25 20:30:41 +01:00
' url ' : info [ ' stream_url ' ] + ' ?client_id= ' + self . _CLIENT_ID ,
' ext ' : ext ,
2013-11-25 22:34:56 +01:00
' vcodec ' : ' none ' ,
2013-11-25 20:30:41 +01:00
} )
2014-01-17 03:29:41 +01:00
for f in formats :
2013-11-25 20:30:41 +01:00
if f [ ' format_id ' ] . startswith ( ' http ' ) :
2014-01-17 03:29:41 +01:00
f [ ' protocol ' ] = ' http '
2013-11-25 20:30:41 +01:00
if f [ ' format_id ' ] . startswith ( ' rtmp ' ) :
2014-01-17 03:29:41 +01:00
f [ ' protocol ' ] = ' rtmp '
2013-11-25 20:30:41 +01:00
2015-03-02 17:39:32 +01:00
self . _check_formats ( formats , track_id )
self . _sort_formats ( formats )
result [ ' formats ' ] = formats
2013-11-21 13:16:19 +01:00
2013-11-09 18:06:09 +01:00
return result
2013-07-24 14:05:14 +02:00
2013-06-23 20:57:44 +02:00
def _real_extract ( self , url ) :
2013-07-24 14:39:21 +02:00
mobj = re . match ( self . _VALID_URL , url , flags = re . VERBOSE )
2013-06-23 20:57:44 +02:00
if mobj is None :
2014-03-24 02:15:31 +01:00
raise ExtractorError ( ' Invalid URL: %s ' % url )
2013-06-23 20:57:44 +02:00
2013-07-24 14:39:21 +02:00
track_id = mobj . group ( ' track_id ' )
2013-12-09 17:08:58 +01:00
token = None
2013-07-24 14:39:21 +02:00
if track_id is not None :
info_json_url = ' http://api.soundcloud.com/tracks/ ' + track_id + ' .json?client_id= ' + self . _CLIENT_ID
full_title = track_id
2014-09-18 08:02:03 +02:00
token = mobj . group ( ' secret_token ' )
if token :
info_json_url + = " &secret_token= " + token
2014-01-02 16:18:51 +01:00
elif mobj . group ( ' player ' ) :
2013-08-21 17:06:37 +02:00
query = compat_urlparse . parse_qs ( compat_urlparse . urlparse ( url ) . query )
2015-04-17 10:46:25 +02:00
real_url = query [ ' url ' ] [ 0 ]
# If the token is in the query of the original url we have to
# manually add it
if ' secret_token ' in query :
real_url + = ' ?secret_token= ' + query [ ' secret_token ' ] [ 0 ]
return self . url_result ( real_url )
2013-07-24 14:39:21 +02:00
else :
# extract uploader (which is in the url)
2013-12-09 17:08:58 +01:00
uploader = mobj . group ( ' uploader ' )
2013-07-24 14:39:21 +02:00
# extract simple title (uploader + slug of song title)
2014-11-23 21:20:46 +01:00
slug_title = mobj . group ( ' title ' )
2013-12-09 17:08:58 +01:00
token = mobj . group ( ' token ' )
full_title = resolve_title = ' %s / %s ' % ( uploader , slug_title )
if token :
resolve_title + = ' / %s ' % token
2014-11-23 20:41:03 +01:00
2013-07-24 14:39:21 +02:00
self . report_resolve ( full_title )
2014-11-23 20:41:03 +01:00
2013-12-09 17:08:58 +01:00
url = ' http://soundcloud.com/ %s ' % resolve_title
2013-07-24 14:39:21 +02:00
info_json_url = self . _resolv_url ( url )
2014-05-05 03:12:41 +02:00
info = self . _download_json ( info_json_url , full_title , ' Downloading info JSON ' )
2013-06-23 20:57:44 +02:00
2013-12-09 17:08:58 +01:00
return self . _extract_info_dict ( info , full_title , secret_token = token )
2013-06-23 20:57:44 +02:00
2014-05-05 03:12:41 +02:00
2013-07-24 14:05:14 +02:00
class SoundcloudSetIE ( SoundcloudIE ) :
2015-03-30 17:03:38 +02:00
_VALID_URL = r ' https?://(?:(?:www|m) \ .)?soundcloud \ .com/(?P<uploader>[ \ w \ d-]+)/sets/(?P<slug_title>[ \ w \ d-]+)(?:/(?P<token>[^?/]+))? '
2014-01-17 03:29:41 +01:00
IE_NAME = ' soundcloud:set '
2014-08-28 00:58:24 +02:00
_TESTS = [ {
' url ' : ' https://soundcloud.com/the-concept-band/sets/the-royal-concept-ep ' ,
' info_dict ' : {
2015-02-01 15:24:38 +01:00
' id ' : ' 2284613 ' ,
2014-08-28 00:58:24 +02:00
' title ' : ' The Royal Concept EP ' ,
} ,
' playlist_mincount ' : 6 ,
} ]
2013-06-23 20:57:44 +02:00
def _real_extract ( self , url ) :
mobj = re . match ( self . _VALID_URL , url )
# extract uploader (which is in the url)
2014-09-18 11:35:11 +02:00
uploader = mobj . group ( ' uploader ' )
2013-06-23 20:57:44 +02:00
# extract simple title (uploader + slug of song title)
2014-09-18 11:35:11 +02:00
slug_title = mobj . group ( ' slug_title ' )
2013-06-23 20:57:44 +02:00
full_title = ' %s /sets/ %s ' % ( uploader , slug_title )
2014-09-18 11:35:11 +02:00
url = ' http://soundcloud.com/ %s /sets/ %s ' % ( uploader , slug_title )
token = mobj . group ( ' token ' )
if token :
full_title + = ' / ' + token
url + = ' / ' + token
2013-06-23 20:57:44 +02:00
self . report_resolve ( full_title )
2013-07-24 14:05:14 +02:00
resolv_url = self . _resolv_url ( url )
2014-05-05 03:12:41 +02:00
info = self . _download_json ( resolv_url , full_title )
2013-06-23 20:57:44 +02:00
if ' errors ' in info :
2015-04-17 19:24:30 +02:00
msgs = ( compat_str ( err [ ' error_message ' ] ) for err in info [ ' errors ' ] )
raise ExtractorError ( ' unable to download video webpage: %s ' % ' , ' . join ( msgs ) )
2013-06-23 20:57:44 +02:00
2015-07-25 11:47:53 +02:00
entries = [ self . url_result ( track [ ' permalink_url ' ] , ' Soundcloud ' ) for track in info [ ' tracks ' ] ]
2014-08-28 00:58:24 +02:00
return {
' _type ' : ' playlist ' ,
2015-07-25 11:47:53 +02:00
' entries ' : entries ,
2015-02-01 15:24:38 +01:00
' id ' : ' %s ' % info [ ' id ' ] ,
2014-08-28 00:58:24 +02:00
' title ' : info [ ' title ' ] ,
}
2013-09-14 21:41:49 +02:00
class SoundcloudUserIE ( SoundcloudIE ) :
2015-03-30 17:03:38 +02:00
_VALID_URL = r ' https?://(?:(?:www|m) \ .)?soundcloud \ .com/(?P<user>[^/]+)/?((?P<rsrc>tracks|likes)/?)?( \ ?.*)?$ '
2014-01-17 03:29:41 +01:00
IE_NAME = ' soundcloud:user '
2014-08-28 00:58:24 +02:00
_TESTS = [ {
' url ' : ' https://soundcloud.com/the-concept-band ' ,
' info_dict ' : {
' id ' : ' 9615865 ' ,
' title ' : ' The Royal Concept ' ,
} ,
' playlist_mincount ' : 12
} , {
' url ' : ' https://soundcloud.com/the-concept-band/likes ' ,
' info_dict ' : {
' id ' : ' 9615865 ' ,
' title ' : ' The Royal Concept ' ,
} ,
' playlist_mincount ' : 1 ,
2015-06-24 16:49:22 +02:00
} , {
' url ' : ' https://soundcloud.com/the-akashic-chronicler/tracks ' ,
' only_matching ' : True ,
2014-08-28 00:58:24 +02:00
} ]
2013-09-14 21:41:49 +02:00
def _real_extract ( self , url ) :
mobj = re . match ( self . _VALID_URL , url )
uploader = mobj . group ( ' user ' )
2014-07-07 20:21:02 +02:00
resource = mobj . group ( ' rsrc ' )
if resource is None :
resource = ' tracks '
elif resource == ' likes ' :
resource = ' favorites '
2013-09-14 21:41:49 +02:00
url = ' http://soundcloud.com/ %s / ' % uploader
resolv_url = self . _resolv_url ( url )
2014-05-05 03:12:41 +02:00
user = self . _download_json (
resolv_url , uploader , ' Downloading user info ' )
2014-07-07 20:21:02 +02:00
base_url = ' http://api.soundcloud.com/users/ %s / %s .json? ' % ( uploader , resource )
2013-09-14 21:41:49 +02:00
2014-05-05 03:12:41 +02:00
entries = [ ]
2013-09-14 21:41:49 +02:00
for i in itertools . count ( ) :
2014-05-05 03:12:41 +02:00
data = compat_urllib_parse . urlencode ( {
' offset ' : i * 50 ,
2014-07-07 20:21:02 +02:00
' limit ' : 50 ,
2014-05-05 03:12:41 +02:00
' client_id ' : self . _CLIENT_ID ,
} )
new_entries = self . _download_json (
base_url + data , uploader , ' Downloading track page %s ' % ( i + 1 ) )
2014-07-07 20:21:02 +02:00
if len ( new_entries ) == 0 :
self . to_screen ( ' %s : End page received ' % uploader )
2013-09-14 21:41:49 +02:00
break
2015-05-14 18:28:42 +02:00
entries . extend ( self . url_result ( e [ ' permalink_url ' ] , ' Soundcloud ' ) for e in new_entries )
2013-09-14 21:41:49 +02:00
return {
' _type ' : ' playlist ' ,
' id ' : compat_str ( user [ ' id ' ] ) ,
' title ' : user [ ' username ' ] ,
2014-05-05 03:12:41 +02:00
' entries ' : entries ,
}
class SoundcloudPlaylistIE ( SoundcloudIE ) :
2014-09-18 18:57:04 +02:00
_VALID_URL = r ' https?://api \ .soundcloud \ .com/playlists/(?P<id>[0-9]+)(?:/? \ ?secret_token=(?P<token>[^&]+?))?$ '
2014-05-05 03:12:41 +02:00
IE_NAME = ' soundcloud:playlist '
2014-09-18 18:57:04 +02:00
_TESTS = [ {
' url ' : ' http://api.soundcloud.com/playlists/4110309 ' ,
' info_dict ' : {
' id ' : ' 4110309 ' ,
' title ' : ' TILT Brass - Bowery Poetry Club, August \' 03 [Non-Site SCR 02] ' ,
' description ' : ' re:.*?TILT Brass - Bowery Poetry Club ' ,
} ,
' playlist_count ' : 6 ,
} ]
2014-05-05 03:12:41 +02:00
def _real_extract ( self , url ) :
mobj = re . match ( self . _VALID_URL , url )
playlist_id = mobj . group ( ' id ' )
base_url = ' %s //api.soundcloud.com/playlists/ %s .json? ' % ( self . http_scheme ( ) , playlist_id )
2014-09-18 11:35:11 +02:00
data_dict = {
2014-05-05 03:12:41 +02:00
' client_id ' : self . _CLIENT_ID ,
2014-09-18 11:35:11 +02:00
}
token = mobj . group ( ' token ' )
if token :
data_dict [ ' secret_token ' ] = token
data = compat_urllib_parse . urlencode ( data_dict )
2014-05-05 03:12:41 +02:00
data = self . _download_json (
base_url + data , playlist_id , ' Downloading playlist ' )
entries = [
2014-09-18 11:35:11 +02:00
self . _extract_info_dict ( t , quiet = True , secret_token = token )
2014-11-23 22:21:46 +01:00
for t in data [ ' tracks ' ] ]
2014-05-05 03:12:41 +02:00
return {
' _type ' : ' playlist ' ,
' id ' : playlist_id ,
' title ' : data . get ( ' title ' ) ,
' description ' : data . get ( ' description ' ) ,
' entries ' : entries ,
2013-09-14 21:41:49 +02:00
}