soundcloud
soundcloud.py
Python wrapper for some of the internal v2 SoundCloud API (read/GET only methods). Does not require an API key.
Note: This is NOT the official SoundCloud developer API
SoundCloud is not accepting any more application registration requests 1 so I made this library so developers can use SoundCloud's internal API for their projects.
Installation
pip install soundcloud-v2
Documentation
https://7x11x13.xyz/soundcloud.py
Example
from soundcloud import SoundCloud
sc = SoundCloud(auth_token="auth_token")
assert sc.is_client_id_valid()
assert sc.is_auth_token_valid()
me = sc.get_user_by_username("7x11x13")
assert me.permalink == "7x11x13"
Notes on auth_token
Some methods require authentication in the form of an OAuth2 access token. You can find your token in your browser cookies for SoundCloud under the name "oauth_token". A new token will be generated each time you log out and log back in.
Notes on **kwargs
All API methods have a **kwargs argument which you can use to pass extra, undocumented
arguments to the SoundCloud v2 API in case I missed some parameter which you find useful.
If this is the case, feel free to create an issue or pull request to document the missing
argument.
License
1# ruff: noqa 2""" 3# soundcloud.py 4 5[](https://github.com/7x11x13/soundcloud.py/actions/workflows/ci.yml) 6[](https://coveralls.io/github/7x11x13/soundcloud.py?branch=main) 7[](https://pypi.org/project/soundcloud-v2/) 8[](https://pypi.org/project/soundcloud-v2/) 9[](https://pypi.org/project/soundcloud-v2/) 10[](https://pepy.tech/project/soundcloud-v2) 11 12Python wrapper for some of the internal v2 SoundCloud API (read/GET only methods). Does not require an API key. 13 14### Note: This is NOT the official [SoundCloud developer API](https://developers.soundcloud.com/docs/api/guide) 15 16SoundCloud is not accepting any more application registration requests [^1] so 17I made this library so developers can use SoundCloud's internal API for their projects. 18 19 20[^1]: https://docs.google.com/forms/d/e/1FAIpQLSfNxc82RJuzC0DnISat7n4H-G7IsPQIdaMpe202iiHZEoso9w/closedform 21 22## Installation 23 24```bash 25pip install soundcloud-v2 26``` 27 28## Documentation 29 30https://7x11x13.xyz/soundcloud.py 31 32## Example 33 34```python 35from soundcloud import SoundCloud 36 37sc = SoundCloud(auth_token="auth_token") 38assert sc.is_client_id_valid() 39assert sc.is_auth_token_valid() 40me = sc.get_user_by_username("7x11x13") 41assert me.permalink == "7x11x13" 42``` 43 44## Notes on `auth_token` 45Some methods require authentication in the form of an OAuth2 access token. 46You can find your token in your browser cookies for SoundCloud under the name "oauth_token". 47A new token will be generated each time you log out and log back in. 48 49## Notes on `**kwargs` 50All API methods have a `**kwargs` argument which you can use to pass extra, undocumented 51arguments to the SoundCloud v2 API in case I missed some parameter which you find useful. 52If this is the case, feel free to create an issue or pull request to document the missing 53argument. 54 55## License 56[MIT](https://choosealicense.com/licenses/mit/) 57 58""" 59 60from soundcloud.exceptions import * 61from soundcloud.exceptions import __all__ as ex_all 62from soundcloud.resource import * 63from soundcloud.resource import __all__ as res_all 64from soundcloud.soundcloud import * 65from soundcloud.soundcloud import __all__ as sc_all 66 67__version__ = "1.7.0" 68 69__all__ = sc_all + ex_all + res_all
71class SoundCloud: 72 """ 73 SoundCloud v2 API client 74 """ 75 76 _DEFAULT_USER_AGENT = ( 77 "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0" 78 ) 79 _ASSETS_SCRIPTS_REGEX = re.compile( 80 r"src=\"(https:\/\/a-v2\.sndcdn\.com/assets/.*\.js)\"" 81 ) 82 _CLIENT_ID_REGEX = re.compile(r"client_id:\"([^\"]+)\"") 83 client_id: str 84 """SoundCloud client ID. Needed for all requests.""" 85 _user_agent: str 86 _auth_token: Optional[str] 87 _authorization: Optional[str] 88 89 def __init__( 90 self, 91 client_id: Optional[str] = None, 92 auth_token: Optional[str] = None, 93 user_agent: str = _DEFAULT_USER_AGENT, 94 impersonate: BrowserTypeLiteral = "chrome", 95 ) -> None: 96 self._impersonate = impersonate 97 self._session: requests.Session = requests.Session(impersonate=impersonate) 98 if not client_id: 99 client_id = self.generate_client_id(impersonate=impersonate) 100 101 self.client_id = client_id 102 self._user_agent = user_agent 103 self._auth_token = None 104 self._authorization = None 105 self.auth_token = auth_token 106 107 @property 108 def auth_token(self) -> Optional[str]: 109 """SoundCloud auth token. Only needed for some requests.""" 110 return self._auth_token 111 112 @auth_token.setter 113 def auth_token(self, new_auth_token: Optional[str]) -> None: 114 if new_auth_token is not None: 115 if new_auth_token.startswith("OAuth"): 116 new_auth_token = new_auth_token.split()[-1] 117 self._authorization = f"OAuth {new_auth_token}" if new_auth_token else None 118 self._auth_token = new_auth_token 119 120 @auth_token.deleter 121 def auth_token(self): 122 self.auth_token = None 123 124 def _get_default_headers(self) -> Dict[str, str]: 125 return {"User-Agent": self._user_agent} 126 127 @classmethod 128 def generate_client_id(cls, impersonate: BrowserTypeLiteral = "chrome") -> str: 129 """Generates a SoundCloud client ID 130 131 Raises: 132 ClientIDGenerationError: Client ID could not be generated. 133 134 Returns: 135 str: Valid client ID 136 """ 137 with requests.Session(impersonate=impersonate) as s: 138 r = s.get("https://soundcloud.com") 139 r.raise_for_status() 140 matches = cls._ASSETS_SCRIPTS_REGEX.findall(r.text) 141 if not matches: 142 raise ClientIDGenerationError("No asset scripts found") 143 for url in matches: 144 r = s.get(url) 145 r.raise_for_status() 146 client_id = cls._CLIENT_ID_REGEX.search(r.text) 147 if client_id: 148 return client_id.group(1) 149 raise ClientIDGenerationError(f"Could not find client_id in script '{url}'") 150 151 def is_client_id_valid(self) -> bool: 152 """ 153 Checks if current client_id is valid 154 """ 155 try: 156 TrackRequest(self, track_id=1032303631, use_auth=False) 157 return True 158 except HTTPError as err: 159 if err.response.status_code == 401: 160 return False 161 else: 162 raise 163 164 def is_auth_token_valid(self) -> bool: 165 """ 166 Checks if current auth_token is valid 167 """ 168 try: 169 MeRequest(self) 170 return True 171 except HTTPError as err: 172 if err.response.status_code == 401: 173 return False 174 else: 175 raise 176 177 def get_me(self) -> Optional[User]: 178 """ 179 Gets the user associated with client's auth token 180 """ 181 return MeRequest(self) 182 183 def get_my_history(self, **kwargs) -> Generator[HistoryItem, None, None]: 184 """ 185 Returns the stream of recently listened tracks 186 for the client's auth token 187 """ 188 return MeHistoryRequest(self, **kwargs) 189 190 def get_my_stream(self, **kwargs) -> Generator[StreamItem, None, None]: 191 """ 192 Returns the stream of recent uploads/reposts 193 for the client's auth token 194 """ 195 return MeStreamRequest(self, **kwargs) 196 197 def resolve(self, url: str) -> Optional[SearchItem]: 198 """ 199 Returns the resource at the given URL if it 200 exists, otherwise return None 201 """ 202 return ResolveRequest(self, url=url) 203 204 def search(self, query: str, **kwargs) -> Generator[SearchItem, None, None]: 205 """ 206 Search for users, tracks, and playlists 207 """ 208 return SearchRequest(self, q=query, **kwargs) 209 210 def search_albums( 211 self, query: str, **kwargs 212 ) -> Generator[AlbumPlaylist, None, None]: 213 """ 214 Search for albums (not playlists) 215 """ 216 return SearchAlbumsRequest(self, q=query, **kwargs) 217 218 def search_playlists( 219 self, query: str, **kwargs 220 ) -> Generator[AlbumPlaylist, None, None]: 221 """ 222 Search for playlists 223 """ 224 return SearchPlaylistsRequest(self, q=query, **kwargs) 225 226 def search_tracks(self, query: str, **kwargs) -> Generator[Track, None, None]: 227 """ 228 Search for tracks 229 """ 230 return SearchTracksRequest(self, q=query, **kwargs) 231 232 def search_users(self, query: str, **kwargs) -> Generator[User, None, None]: 233 """ 234 Search for users 235 """ 236 return SearchUsersRequest(self, q=query, **kwargs) 237 238 def get_tag_tracks_recent(self, tag: str, **kwargs) -> Generator[Track, None, None]: 239 """ 240 Get most recent tracks for this tag 241 """ 242 return TagRecentTracksRequest(self, tag=tag, **kwargs) 243 244 def get_playlist(self, playlist_id: int) -> Optional[BasicAlbumPlaylist]: 245 """ 246 Returns the playlist with the given playlist_id. 247 If the ID is invalid, return None 248 """ 249 return PlaylistRequest(self, playlist_id=playlist_id) 250 251 def post_playlist( 252 self, sharing: Literal["private", "public"], title: str, tracks: List[int] 253 ) -> Optional[BasicAlbumPlaylist]: 254 """ 255 Create a new playlist 256 """ 257 body = {"playlist": {"sharing": sharing, "title": title, "tracks": tracks}} 258 return PostPlaylistRequest(self, body=body) 259 260 def delete_playlist(self, playlist_id: int) -> Optional[NoContentResponse]: 261 """ 262 Delete a playlist 263 """ 264 return DeletePlaylistRequest(self, playlist_id=playlist_id) 265 266 def get_playlist_likers( 267 self, playlist_id: int, **kwargs 268 ) -> Generator[User, None, None]: 269 """ 270 Get people who liked this playlist 271 """ 272 return PlaylistLikersRequest(self, playlist_id=playlist_id, **kwargs) 273 274 def get_playlist_reposters( 275 self, playlist_id: int, **kwargs 276 ) -> Generator[User, None, None]: 277 """ 278 Get people who reposted this playlist 279 """ 280 return PlaylistRepostersRequest(self, playlist_id=playlist_id, **kwargs) 281 282 def get_track(self, track_id: int) -> Optional[BasicTrack]: 283 """ 284 Returns the track with the given track_id. 285 If the ID is invalid, return None 286 """ 287 return TrackRequest(self, track_id=track_id) 288 289 def get_tracks( 290 self, 291 track_ids: List[int], 292 playlistId: Optional[int] = None, 293 playlistSecretToken: Optional[str] = None, 294 **kwargs, 295 ) -> List[BasicTrack]: 296 """ 297 Returns the tracks with the given track_ids. 298 Can be used to get track info for hidden tracks in a hidden playlist. 299 """ 300 if playlistId is not None: 301 kwargs["playlistId"] = playlistId 302 if playlistSecretToken is not None: 303 kwargs["playlistSecretToken"] = playlistSecretToken 304 return TracksRequest( 305 self, ids=",".join([str(id) for id in track_ids]), **kwargs 306 ) 307 308 def get_track_albums( 309 self, track_id: int, **kwargs 310 ) -> Generator[BasicAlbumPlaylist, None, None]: 311 """ 312 Get albums that this track is in 313 """ 314 return TrackAlbumsRequest(self, track_id=track_id, **kwargs) 315 316 def get_track_playlists( 317 self, track_id: int, **kwargs 318 ) -> Generator[BasicAlbumPlaylist, None, None]: 319 """ 320 Get playlists that this track is in 321 """ 322 return TrackPlaylistsRequest(self, track_id=track_id, **kwargs) 323 324 def get_track_comments( 325 self, track_id: int, threaded: int = 0, **kwargs 326 ) -> Generator[BasicComment, None, None]: 327 """ 328 Get comments on this track 329 """ 330 return TrackCommentsRequest( 331 self, track_id=track_id, threaded=threaded, **kwargs 332 ) 333 334 def get_track_comments_with_interactions( 335 self, track_id: int, threaded: int = 0, **kwargs 336 ) -> Generator[CommentWithInteractions, None, None]: 337 """ 338 Get comments on this track with interaction data. Requires authentication. 339 """ 340 track = self.get_track(track_id) 341 if not track: 342 return 343 track_urn = track.urn 344 creator_urn = track.user.urn 345 comments = self.get_track_comments(track_id, threaded, **kwargs) 346 while True: 347 chunk = list(itertools.islice(comments, 10)) 348 if not chunk: 349 return 350 comment_urns = [comment.self.urn for comment in chunk] 351 result = UserInteractionsRequest( 352 self, 353 UserInteractionsQueryParams( 354 creator_urn, 355 "sc:interactiontype:reaction", 356 track_urn, 357 comment_urns, 358 ), 359 ) 360 if not result: 361 return 362 comments_with_interactions = [] 363 for comment, user_interactions, creator_interactions in zip( 364 chunk, result.user, result.creator 365 ): 366 assert user_interactions.interactionCounts is not None 367 likes = list( 368 filter( 369 lambda x: ( 370 x.interactionTypeValueUrn == "sc:interactiontypevalue:like" 371 ), 372 user_interactions.interactionCounts, 373 ) 374 ) 375 num_likes = likes[0].count if likes else 0 376 comments_with_interactions.append( 377 CommentWithInteractions( 378 comment=comment, 379 likes=num_likes or 0, 380 liked_by_creator=creator_interactions.userInteraction 381 == "sc:interactiontypevalue:like", 382 liked_by_user=user_interactions.userInteraction 383 == "sc:interactiontypevalue:like", 384 ) 385 ) 386 yield from comments_with_interactions 387 388 def get_track_likers(self, track_id: int, **kwargs) -> Generator[User, None, None]: 389 """ 390 Get users who liked this track 391 """ 392 return TrackLikersRequest(self, track_id=track_id, **kwargs) 393 394 def get_track_related( 395 self, track_id: int, **kwargs 396 ) -> Generator[BasicTrack, None, None]: 397 """ 398 Get related tracks 399 """ 400 return TrackRelatedRequest(self, track_id=track_id, **kwargs) 401 402 def get_track_reposters( 403 self, track_id: int, **kwargs 404 ) -> Generator[User, None, None]: 405 """ 406 Get users who reposted this track 407 """ 408 return TrackRepostersRequest(self, track_id=track_id, **kwargs) 409 410 def get_track_original_download( 411 self, track_id: int, token: Optional[str] = None 412 ) -> Optional[str]: 413 """ 414 Get track original download link. If track is private, 415 requires secret token to be provided (last part of secret URL). 416 Requires authentication. 417 """ 418 if token is not None: 419 download = TrackOriginalDownloadRequest( 420 self, track_id=track_id, secret_token=token 421 ) 422 else: 423 download = TrackOriginalDownloadRequest(self, track_id=track_id) 424 if download is None: 425 return None 426 else: 427 return download.redirectUri 428 429 def get_user(self, user_id: int) -> Optional[User]: 430 """ 431 Returns the user with the given user_id. 432 If the ID is invalid, return None 433 """ 434 return UserRequest(self, user_id=user_id) 435 436 def get_user_by_username(self, username: str) -> Optional[User]: 437 """ 438 Returns the user with the given username. 439 If the username is invalid, return None 440 """ 441 resource = self.resolve(f"https://soundcloud.com/{username}") 442 if resource and isinstance(resource, User): 443 return resource 444 else: 445 return None 446 447 def get_user_comments( 448 self, user_id: int, **kwargs 449 ) -> Generator[Comment, None, None]: 450 """ 451 Get comments by this user 452 """ 453 return UserCommentsRequest(self, user_id=user_id, **kwargs) 454 455 def get_conversation_messages( 456 self, user_id: int, conversation_id: int, **kwargs 457 ) -> Generator[Message, None, None]: 458 """ 459 Get messages in this conversation 460 """ 461 return UserConversationMessagesRequest( 462 self, user_id=user_id, conversation_id=conversation_id, **kwargs 463 ) 464 465 def get_conversations( 466 self, user_id: int, **kwargs 467 ) -> Generator[Conversation, None, None]: 468 """ 469 Get conversations including this user 470 """ 471 return UserConversationsRequest(self, user_id=user_id, **kwargs) 472 473 def get_unread_conversations( 474 self, user_id: int, **kwargs 475 ) -> Generator[Conversation, None, None]: 476 """ 477 Get conversations unread by this user 478 """ 479 return UserConversationsUnreadRequest(self, user_id=user_id, **kwargs) 480 481 def get_user_emails( 482 self, user_id: int, **kwargs 483 ) -> Generator[UserEmail, None, None]: 484 """ 485 Get user's email addresses. Requires authentication. 486 """ 487 return UserEmailsRequest(self, user_id=user_id, **kwargs) 488 489 def get_user_featured_profiles( 490 self, user_id: int, **kwargs 491 ) -> Generator[User, None, None]: 492 """ 493 Get profiles featured by this user 494 """ 495 return UserFeaturedProfilesRequest(self, user_id=user_id, **kwargs) 496 497 def get_user_followers(self, user_id: int, **kwargs) -> Generator[User, None, None]: 498 """ 499 Get user's followers 500 """ 501 return UserFollowersRequest(self, user_id=user_id, **kwargs) 502 503 def get_user_following(self, user_id: int, **kwargs) -> Generator[User, None, None]: 504 """ 505 Get users this user is following 506 """ 507 return UserFollowingsRequest(self, user_id=user_id, **kwargs) 508 509 def get_user_likes(self, user_id: int, **kwargs) -> Generator[Like, None, None]: 510 """ 511 Get likes by this user 512 """ 513 return UserLikesRequest(self, user_id=user_id, **kwargs) 514 515 def get_user_related_artists( 516 self, user_id: int, **kwargs 517 ) -> Generator[User, None, None]: 518 """ 519 Get artists related to this user 520 """ 521 return UserRelatedArtistsRequest(self, user_id=user_id, **kwargs) 522 523 def get_user_reposts( 524 self, user_id: int, **kwargs 525 ) -> Generator[RepostItem, None, None]: 526 """ 527 Get reposts by this user 528 """ 529 return UserRepostsRequest(self, user_id=user_id, **kwargs) 530 531 def get_user_stream( 532 self, user_id: int, **kwargs 533 ) -> Generator[StreamItem, None, None]: 534 """ 535 Returns generator of track uploaded by given user and 536 reposts by this user 537 """ 538 return UserStreamRequest(self, user_id=user_id, **kwargs) 539 540 def get_user_tracks( 541 self, user_id: int, **kwargs 542 ) -> Generator[BasicTrack, None, None]: 543 """ 544 Get tracks uploaded by this user 545 """ 546 return UserTracksRequest(self, user_id=user_id, **kwargs) 547 548 def get_user_popular_tracks( 549 self, user_id: int, **kwargs 550 ) -> Generator[BasicTrack, None, None]: 551 """ 552 Get popular tracks uploaded by this user 553 """ 554 return UserToptracksRequest(self, user_id=user_id, **kwargs) 555 556 def get_user_albums( 557 self, user_id: int, **kwargs 558 ) -> Generator[BasicAlbumPlaylist, None, None]: 559 """ 560 Get albums uploaded by this user 561 """ 562 return UserAlbumsRequest(self, user_id=user_id, **kwargs) 563 564 def get_user_playlists( 565 self, user_id: int, **kwargs 566 ) -> Generator[BasicAlbumPlaylist, None, None]: 567 """ 568 Get playlists uploaded by this user 569 """ 570 return UserPlaylistsRequest(self, user_id=user_id, **kwargs) 571 572 def get_user_links(self, user_urn: str, **kwargs) -> List[WebProfile]: 573 """ 574 Get links in this user's description 575 """ 576 return UserWebProfilesRequest(self, user_urn=user_urn, **kwargs)
SoundCloud v2 API client
89 def __init__( 90 self, 91 client_id: Optional[str] = None, 92 auth_token: Optional[str] = None, 93 user_agent: str = _DEFAULT_USER_AGENT, 94 impersonate: BrowserTypeLiteral = "chrome", 95 ) -> None: 96 self._impersonate = impersonate 97 self._session: requests.Session = requests.Session(impersonate=impersonate) 98 if not client_id: 99 client_id = self.generate_client_id(impersonate=impersonate) 100 101 self.client_id = client_id 102 self._user_agent = user_agent 103 self._auth_token = None 104 self._authorization = None 105 self.auth_token = auth_token
107 @property 108 def auth_token(self) -> Optional[str]: 109 """SoundCloud auth token. Only needed for some requests.""" 110 return self._auth_token
SoundCloud auth token. Only needed for some requests.
127 @classmethod 128 def generate_client_id(cls, impersonate: BrowserTypeLiteral = "chrome") -> str: 129 """Generates a SoundCloud client ID 130 131 Raises: 132 ClientIDGenerationError: Client ID could not be generated. 133 134 Returns: 135 str: Valid client ID 136 """ 137 with requests.Session(impersonate=impersonate) as s: 138 r = s.get("https://soundcloud.com") 139 r.raise_for_status() 140 matches = cls._ASSETS_SCRIPTS_REGEX.findall(r.text) 141 if not matches: 142 raise ClientIDGenerationError("No asset scripts found") 143 for url in matches: 144 r = s.get(url) 145 r.raise_for_status() 146 client_id = cls._CLIENT_ID_REGEX.search(r.text) 147 if client_id: 148 return client_id.group(1) 149 raise ClientIDGenerationError(f"Could not find client_id in script '{url}'")
Generates a SoundCloud client ID
Raises:
- ClientIDGenerationError: Client ID could not be generated.
Returns:
str: Valid client ID
151 def is_client_id_valid(self) -> bool: 152 """ 153 Checks if current client_id is valid 154 """ 155 try: 156 TrackRequest(self, track_id=1032303631, use_auth=False) 157 return True 158 except HTTPError as err: 159 if err.response.status_code == 401: 160 return False 161 else: 162 raise
Checks if current client_id is valid
164 def is_auth_token_valid(self) -> bool: 165 """ 166 Checks if current auth_token is valid 167 """ 168 try: 169 MeRequest(self) 170 return True 171 except HTTPError as err: 172 if err.response.status_code == 401: 173 return False 174 else: 175 raise
Checks if current auth_token is valid
177 def get_me(self) -> Optional[User]: 178 """ 179 Gets the user associated with client's auth token 180 """ 181 return MeRequest(self)
Gets the user associated with client's auth token
183 def get_my_history(self, **kwargs) -> Generator[HistoryItem, None, None]: 184 """ 185 Returns the stream of recently listened tracks 186 for the client's auth token 187 """ 188 return MeHistoryRequest(self, **kwargs)
Returns the stream of recently listened tracks for the client's auth token
190 def get_my_stream(self, **kwargs) -> Generator[StreamItem, None, None]: 191 """ 192 Returns the stream of recent uploads/reposts 193 for the client's auth token 194 """ 195 return MeStreamRequest(self, **kwargs)
Returns the stream of recent uploads/reposts for the client's auth token
197 def resolve(self, url: str) -> Optional[SearchItem]: 198 """ 199 Returns the resource at the given URL if it 200 exists, otherwise return None 201 """ 202 return ResolveRequest(self, url=url)
Returns the resource at the given URL if it exists, otherwise return None
204 def search(self, query: str, **kwargs) -> Generator[SearchItem, None, None]: 205 """ 206 Search for users, tracks, and playlists 207 """ 208 return SearchRequest(self, q=query, **kwargs)
Search for users, tracks, and playlists
210 def search_albums( 211 self, query: str, **kwargs 212 ) -> Generator[AlbumPlaylist, None, None]: 213 """ 214 Search for albums (not playlists) 215 """ 216 return SearchAlbumsRequest(self, q=query, **kwargs)
Search for albums (not playlists)
218 def search_playlists( 219 self, query: str, **kwargs 220 ) -> Generator[AlbumPlaylist, None, None]: 221 """ 222 Search for playlists 223 """ 224 return SearchPlaylistsRequest(self, q=query, **kwargs)
Search for playlists
226 def search_tracks(self, query: str, **kwargs) -> Generator[Track, None, None]: 227 """ 228 Search for tracks 229 """ 230 return SearchTracksRequest(self, q=query, **kwargs)
Search for tracks
232 def search_users(self, query: str, **kwargs) -> Generator[User, None, None]: 233 """ 234 Search for users 235 """ 236 return SearchUsersRequest(self, q=query, **kwargs)
Search for users
238 def get_tag_tracks_recent(self, tag: str, **kwargs) -> Generator[Track, None, None]: 239 """ 240 Get most recent tracks for this tag 241 """ 242 return TagRecentTracksRequest(self, tag=tag, **kwargs)
Get most recent tracks for this tag
244 def get_playlist(self, playlist_id: int) -> Optional[BasicAlbumPlaylist]: 245 """ 246 Returns the playlist with the given playlist_id. 247 If the ID is invalid, return None 248 """ 249 return PlaylistRequest(self, playlist_id=playlist_id)
Returns the playlist with the given playlist_id. If the ID is invalid, return None
251 def post_playlist( 252 self, sharing: Literal["private", "public"], title: str, tracks: List[int] 253 ) -> Optional[BasicAlbumPlaylist]: 254 """ 255 Create a new playlist 256 """ 257 body = {"playlist": {"sharing": sharing, "title": title, "tracks": tracks}} 258 return PostPlaylistRequest(self, body=body)
Create a new playlist
260 def delete_playlist(self, playlist_id: int) -> Optional[NoContentResponse]: 261 """ 262 Delete a playlist 263 """ 264 return DeletePlaylistRequest(self, playlist_id=playlist_id)
Delete a playlist
266 def get_playlist_likers( 267 self, playlist_id: int, **kwargs 268 ) -> Generator[User, None, None]: 269 """ 270 Get people who liked this playlist 271 """ 272 return PlaylistLikersRequest(self, playlist_id=playlist_id, **kwargs)
Get people who liked this playlist
274 def get_playlist_reposters( 275 self, playlist_id: int, **kwargs 276 ) -> Generator[User, None, None]: 277 """ 278 Get people who reposted this playlist 279 """ 280 return PlaylistRepostersRequest(self, playlist_id=playlist_id, **kwargs)
Get people who reposted this playlist
282 def get_track(self, track_id: int) -> Optional[BasicTrack]: 283 """ 284 Returns the track with the given track_id. 285 If the ID is invalid, return None 286 """ 287 return TrackRequest(self, track_id=track_id)
Returns the track with the given track_id. If the ID is invalid, return None
289 def get_tracks( 290 self, 291 track_ids: List[int], 292 playlistId: Optional[int] = None, 293 playlistSecretToken: Optional[str] = None, 294 **kwargs, 295 ) -> List[BasicTrack]: 296 """ 297 Returns the tracks with the given track_ids. 298 Can be used to get track info for hidden tracks in a hidden playlist. 299 """ 300 if playlistId is not None: 301 kwargs["playlistId"] = playlistId 302 if playlistSecretToken is not None: 303 kwargs["playlistSecretToken"] = playlistSecretToken 304 return TracksRequest( 305 self, ids=",".join([str(id) for id in track_ids]), **kwargs 306 )
Returns the tracks with the given track_ids. Can be used to get track info for hidden tracks in a hidden playlist.
308 def get_track_albums( 309 self, track_id: int, **kwargs 310 ) -> Generator[BasicAlbumPlaylist, None, None]: 311 """ 312 Get albums that this track is in 313 """ 314 return TrackAlbumsRequest(self, track_id=track_id, **kwargs)
Get albums that this track is in
316 def get_track_playlists( 317 self, track_id: int, **kwargs 318 ) -> Generator[BasicAlbumPlaylist, None, None]: 319 """ 320 Get playlists that this track is in 321 """ 322 return TrackPlaylistsRequest(self, track_id=track_id, **kwargs)
Get playlists that this track is in
324 def get_track_comments( 325 self, track_id: int, threaded: int = 0, **kwargs 326 ) -> Generator[BasicComment, None, None]: 327 """ 328 Get comments on this track 329 """ 330 return TrackCommentsRequest( 331 self, track_id=track_id, threaded=threaded, **kwargs 332 )
Get comments on this track
334 def get_track_comments_with_interactions( 335 self, track_id: int, threaded: int = 0, **kwargs 336 ) -> Generator[CommentWithInteractions, None, None]: 337 """ 338 Get comments on this track with interaction data. Requires authentication. 339 """ 340 track = self.get_track(track_id) 341 if not track: 342 return 343 track_urn = track.urn 344 creator_urn = track.user.urn 345 comments = self.get_track_comments(track_id, threaded, **kwargs) 346 while True: 347 chunk = list(itertools.islice(comments, 10)) 348 if not chunk: 349 return 350 comment_urns = [comment.self.urn for comment in chunk] 351 result = UserInteractionsRequest( 352 self, 353 UserInteractionsQueryParams( 354 creator_urn, 355 "sc:interactiontype:reaction", 356 track_urn, 357 comment_urns, 358 ), 359 ) 360 if not result: 361 return 362 comments_with_interactions = [] 363 for comment, user_interactions, creator_interactions in zip( 364 chunk, result.user, result.creator 365 ): 366 assert user_interactions.interactionCounts is not None 367 likes = list( 368 filter( 369 lambda x: ( 370 x.interactionTypeValueUrn == "sc:interactiontypevalue:like" 371 ), 372 user_interactions.interactionCounts, 373 ) 374 ) 375 num_likes = likes[0].count if likes else 0 376 comments_with_interactions.append( 377 CommentWithInteractions( 378 comment=comment, 379 likes=num_likes or 0, 380 liked_by_creator=creator_interactions.userInteraction 381 == "sc:interactiontypevalue:like", 382 liked_by_user=user_interactions.userInteraction 383 == "sc:interactiontypevalue:like", 384 ) 385 ) 386 yield from comments_with_interactions
Get comments on this track with interaction data. Requires authentication.
388 def get_track_likers(self, track_id: int, **kwargs) -> Generator[User, None, None]: 389 """ 390 Get users who liked this track 391 """ 392 return TrackLikersRequest(self, track_id=track_id, **kwargs)
Get users who liked this track
402 def get_track_reposters( 403 self, track_id: int, **kwargs 404 ) -> Generator[User, None, None]: 405 """ 406 Get users who reposted this track 407 """ 408 return TrackRepostersRequest(self, track_id=track_id, **kwargs)
Get users who reposted this track
410 def get_track_original_download( 411 self, track_id: int, token: Optional[str] = None 412 ) -> Optional[str]: 413 """ 414 Get track original download link. If track is private, 415 requires secret token to be provided (last part of secret URL). 416 Requires authentication. 417 """ 418 if token is not None: 419 download = TrackOriginalDownloadRequest( 420 self, track_id=track_id, secret_token=token 421 ) 422 else: 423 download = TrackOriginalDownloadRequest(self, track_id=track_id) 424 if download is None: 425 return None 426 else: 427 return download.redirectUri
Get track original download link. If track is private, requires secret token to be provided (last part of secret URL). Requires authentication.
429 def get_user(self, user_id: int) -> Optional[User]: 430 """ 431 Returns the user with the given user_id. 432 If the ID is invalid, return None 433 """ 434 return UserRequest(self, user_id=user_id)
Returns the user with the given user_id. If the ID is invalid, return None
436 def get_user_by_username(self, username: str) -> Optional[User]: 437 """ 438 Returns the user with the given username. 439 If the username is invalid, return None 440 """ 441 resource = self.resolve(f"https://soundcloud.com/{username}") 442 if resource and isinstance(resource, User): 443 return resource 444 else: 445 return None
Returns the user with the given username. If the username is invalid, return None
447 def get_user_comments( 448 self, user_id: int, **kwargs 449 ) -> Generator[Comment, None, None]: 450 """ 451 Get comments by this user 452 """ 453 return UserCommentsRequest(self, user_id=user_id, **kwargs)
Get comments by this user
455 def get_conversation_messages( 456 self, user_id: int, conversation_id: int, **kwargs 457 ) -> Generator[Message, None, None]: 458 """ 459 Get messages in this conversation 460 """ 461 return UserConversationMessagesRequest( 462 self, user_id=user_id, conversation_id=conversation_id, **kwargs 463 )
Get messages in this conversation
465 def get_conversations( 466 self, user_id: int, **kwargs 467 ) -> Generator[Conversation, None, None]: 468 """ 469 Get conversations including this user 470 """ 471 return UserConversationsRequest(self, user_id=user_id, **kwargs)
Get conversations including this user
473 def get_unread_conversations( 474 self, user_id: int, **kwargs 475 ) -> Generator[Conversation, None, None]: 476 """ 477 Get conversations unread by this user 478 """ 479 return UserConversationsUnreadRequest(self, user_id=user_id, **kwargs)
Get conversations unread by this user
481 def get_user_emails( 482 self, user_id: int, **kwargs 483 ) -> Generator[UserEmail, None, None]: 484 """ 485 Get user's email addresses. Requires authentication. 486 """ 487 return UserEmailsRequest(self, user_id=user_id, **kwargs)
Get user's email addresses. Requires authentication.
489 def get_user_featured_profiles( 490 self, user_id: int, **kwargs 491 ) -> Generator[User, None, None]: 492 """ 493 Get profiles featured by this user 494 """ 495 return UserFeaturedProfilesRequest(self, user_id=user_id, **kwargs)
Get profiles featured by this user
497 def get_user_followers(self, user_id: int, **kwargs) -> Generator[User, None, None]: 498 """ 499 Get user's followers 500 """ 501 return UserFollowersRequest(self, user_id=user_id, **kwargs)
Get user's followers
503 def get_user_following(self, user_id: int, **kwargs) -> Generator[User, None, None]: 504 """ 505 Get users this user is following 506 """ 507 return UserFollowingsRequest(self, user_id=user_id, **kwargs)
Get users this user is following
509 def get_user_likes(self, user_id: int, **kwargs) -> Generator[Like, None, None]: 510 """ 511 Get likes by this user 512 """ 513 return UserLikesRequest(self, user_id=user_id, **kwargs)
Get likes by this user
523 def get_user_reposts( 524 self, user_id: int, **kwargs 525 ) -> Generator[RepostItem, None, None]: 526 """ 527 Get reposts by this user 528 """ 529 return UserRepostsRequest(self, user_id=user_id, **kwargs)
Get reposts by this user
531 def get_user_stream( 532 self, user_id: int, **kwargs 533 ) -> Generator[StreamItem, None, None]: 534 """ 535 Returns generator of track uploaded by given user and 536 reposts by this user 537 """ 538 return UserStreamRequest(self, user_id=user_id, **kwargs)
Returns generator of track uploaded by given user and reposts by this user
540 def get_user_tracks( 541 self, user_id: int, **kwargs 542 ) -> Generator[BasicTrack, None, None]: 543 """ 544 Get tracks uploaded by this user 545 """ 546 return UserTracksRequest(self, user_id=user_id, **kwargs)
Get tracks uploaded by this user
548 def get_user_popular_tracks( 549 self, user_id: int, **kwargs 550 ) -> Generator[BasicTrack, None, None]: 551 """ 552 Get popular tracks uploaded by this user 553 """ 554 return UserToptracksRequest(self, user_id=user_id, **kwargs)
Get popular tracks uploaded by this user
556 def get_user_albums( 557 self, user_id: int, **kwargs 558 ) -> Generator[BasicAlbumPlaylist, None, None]: 559 """ 560 Get albums uploaded by this user 561 """ 562 return UserAlbumsRequest(self, user_id=user_id, **kwargs)
Get albums uploaded by this user
564 def get_user_playlists( 565 self, user_id: int, **kwargs 566 ) -> Generator[BasicAlbumPlaylist, None, None]: 567 """ 568 Get playlists uploaded by this user 569 """ 570 return UserPlaylistsRequest(self, user_id=user_id, **kwargs)
Get playlists uploaded by this user
2class ClientIDGenerationError(Exception): 3 """ 4 Raised when a client ID could not be dynamically generated. 5 """
Raised when a client ID could not be dynamically generated.
16@dataclass 17class BasicComment(BaseData): 18 """Comment without a specified track""" 19 20 kind: str 21 id: int 22 body: str 23 created_at: datetime.datetime 24 timestamp: Optional[int] 25 track_id: int 26 user_id: int 27 self: CommentSelf 28 user: BasicUser
Comment without a specified track
31@dataclass 32class Comment(BasicComment): 33 """Comment with a specified track""" 34 35 track: CommentTrack
Comment with a specified track
25@dataclass 26class CommentWithInteractions(BaseData): 27 comment: BasicComment 28 likes: int 29 """Number of likes on comment""" 30 31 liked_by_creator: bool 32 """Whether comment was liked by the track creator""" 33 34 liked_by_user: bool 35 """Whether the current logged in user liked the comment"""
11@dataclass 12class Conversation(BaseData): 13 """DM conversation between two users""" 14 15 id: str 16 last_message: Message 17 read: bool 18 started_at: datetime.datetime 19 summary: str 20 users: Tuple[Union[BasicUser, MissingUser], ...]
DM conversation between two users
7@dataclass 8class OriginalDownload(BaseData): 9 """Contains a download link for a track""" 10 11 redirectUri: str
Contains a download link for a track
8@dataclass 9class HistoryItem(BaseData): 10 """Item in user's listen history""" 11 12 played_at: int 13 track: BasicTrack 14 track_id: int
Item in user's listen history
23@dataclass 24class PlaylistLike(BaseLike): 25 """Like on a playlist""" 26 27 playlist: AlbumPlaylistNoTracks
Like on a playlist
Like on a track
10@dataclass 11class Message(BaseData): 12 """Single DM between two users""" 13 14 content: str 15 conversation_id: str 16 sender: Union[BasicUser, MissingUser] 17 sender_urn: str 18 sender_type: str 19 sent_at: datetime.datetime
Single DM between two users
22@dataclass 23class AlbumPlaylist(BaseAlbumPlaylist): 24 """Playlist or album with full user info""" 25 26 user: User
Playlist or album with full user info
36@dataclass 37class AlbumPlaylistNoTracks(BaseData): 38 """Playlist or album with no track info""" 39 40 artwork_url: Optional[str] 41 created_at: datetime.datetime 42 duration: int 43 id: int 44 kind: str 45 last_modified: datetime.datetime 46 likes_count: Optional[int] 47 managed_by_feeds: bool 48 permalink: str 49 permalink_url: str 50 public: bool 51 reposts_count: Optional[int] 52 secret_token: Optional[str] 53 sharing: str 54 title: str 55 track_count: int 56 uri: str 57 user_id: int 58 set_type: str 59 is_album: bool 60 published_at: Optional[datetime.datetime] 61 release_date: Optional[str] 62 display_date: datetime.datetime 63 user: BasicUser
Playlist or album with no track info
29@dataclass 30class BasicAlbumPlaylist(BaseAlbumPlaylist): 31 """Playlist or album with partial user info""" 32 33 user: BasicUser
Playlist or album with partial user info
47@dataclass 48class PlaylistStreamItem(BaseStreamItem): 49 """Album or playlist post in user's feed""" 50 51 playlist: BasicAlbumPlaylist
Album or playlist post in user's feed
54@dataclass 55class PlaylistStreamRepostItem(BaseStreamRepostItem): 56 """Album or playlist repost in user's feed""" 57 58 playlist: BasicAlbumPlaylist
Album or playlist repost in user's feed
33@dataclass 34class TrackStreamItem(BaseStreamItem): 35 """Track post in user's feed""" 36 37 track: BasicTrack
Track post in user's feed
40@dataclass 41class TrackStreamRepostItem(BaseStreamRepostItem): 42 """Track repost in user's feed""" 43 44 track: BasicTrack
Track repost in user's feed
80@dataclass 81class BasicTrack(BaseTrack): 82 """Track with partial user info""" 83 84 user: BasicUser
Track with partial user info
12@dataclass 13class Format(BaseData): 14 """Track file format""" 15 16 protocol: str 17 mime_type: str
Track file format
32@dataclass 33class Media(BaseData): 34 """List of available transcodings""" 35 36 transcodings: Tuple[Transcoding, ...]
List of available transcodings
87@dataclass 88class MiniTrack(BaseData): 89 """Track with minimal info""" 90 91 id: int 92 kind: str 93 monetization_model: str 94 policy: str
Track with minimal info
39@dataclass 40class PublisherMetadata(BaseData): 41 """Publisher info""" 42 43 id: str 44 urn: str 45 contains_music: bool
Publisher info
Track with full user info
20@dataclass 21class Transcoding(BaseData): 22 """Available transcoding for track""" 23 24 url: str 25 preset: str 26 duration: int 27 snipped: bool 28 format: Format 29 quality: str
Available transcoding for track
97@dataclass 98class CommentTrack(BaseData): 99 """Track with partial info""" 100 101 artwork_url: Optional[str] 102 caption: Optional[str] 103 id: int 104 kind: str 105 last_modified: datetime.datetime 106 permalink: str 107 permalink_url: str 108 public: bool 109 secret_token: Optional[str] 110 sharing: str 111 title: str 112 uri: str 113 urn: str 114 user_id: int 115 full_duration: int 116 duration: int 117 display_date: datetime.datetime 118 media: Media 119 station_urn: Optional[str] 120 station_permalink: Optional[str] 121 track_authorization: str 122 monetization_model: str 123 policy: str 124 user: BasicUser
Track with partial info
20@dataclass 21class Badges(BaseData): 22 """User badges""" 23 24 pro: bool 25 pro_unlimited: bool 26 verified: bool
User badges
29@dataclass 30class BasicUser(BaseData): 31 """User with partial information""" 32 33 avatar_url: str 34 first_name: str 35 followers_count: int 36 full_name: str 37 id: int 38 kind: str 39 last_modified: datetime.datetime 40 last_name: str 41 permalink: str 42 permalink_url: str 43 uri: str 44 urn: str 45 username: str 46 verified: bool 47 city: Optional[str] 48 country_code: Optional[str] 49 badges: Badges 50 station_urn: Optional[str] 51 station_permalink: Optional[str]
User with partial information
54@dataclass 55class User(BasicUser): 56 """User with full information""" 57 58 comments_count: int 59 created_at: Optional[datetime.datetime] 60 creator_subscriptions: Tuple[CreatorSubscription, ...] 61 creator_subscription: CreatorSubscription 62 description: Optional[str] 63 followings_count: int 64 groups_count: int 65 likes_count: Optional[int] 66 playlist_likes_count: Optional[int] 67 playlist_count: int 68 reposts_count: Optional[int] 69 track_count: int 70 visuals: Optional[Visuals]
User with full information
87@dataclass 88class UserEmail(BaseData): 89 """Email address associated with a user""" 90 91 address: str 92 confirmed: bool 93 id: int 94 kind: str 95 last_modified: datetime.datetime 96 primary: bool 97 urn: str 98 user_id: str
Email address associated with a user
7@dataclass 8class NoContentResponse(BaseData): 9 """Response with no content, mainly for DELETE requests""" 10 11 status_code: int
Response with no content, mainly for DELETE requests