-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
3ce8ffa
commit 4e3a291
Showing
49 changed files
with
7,043 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
.dart_tool/ | ||
build/ | ||
pubspec.lock | ||
doc/api/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Changelog | ||
|
||
## 1.0.0 | ||
|
||
- Initial release. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
# SoundcloudExplodeDart | ||
|
||
SoundcloudExplodeDart utilises SoundCloud's internal V2 API to scrape metadata about users, tracks, playlists, and albums, without requiring an account, API key, or rate-limiting. | ||
|
||
This API was **not** intended to be an exhaustive map of all SoundCloud endpoints, but I will be accepting feature requests, so feel free to suggest any functionality you would like to see by opening a new issue. | ||
|
||
> This project takes inspiration from jerry08's [SoundCloudExplode](https://github.com/jerry08/SoundCloudExplode) library for C#. | ||
## Usage | ||
|
||
### Searching | ||
|
||
Search for users, tracks, playlists, or albums, and apply specific search filters to each query: | ||
|
||
```dart | ||
import 'dart:async'; | ||
import 'soundcloud_explode_dart/soundcloud_explode_dart.dart'; | ||
final client = SoundcloudClient(); | ||
// Most functions return a stream of results in the | ||
// form of Stream<Iterable<E>>. | ||
// The number of results returned in each Iterable<E>, as well as | ||
// the search result offset and search filter are optional parameters. | ||
final stream = client.search( | ||
'Haddaway - What Is Love', | ||
searchFilter: SearchFilter.tracks, | ||
offset: 0, | ||
limit: 50 | ||
); | ||
final streamIterator = StreamIterator(stream); | ||
while (await streamIterator.moveNext()) { | ||
for (final result in streamIterator.current) { | ||
print(result.title); | ||
} | ||
} | ||
``` | ||
|
||
### Querying users | ||
|
||
Retrieve metadata about specific users: | ||
|
||
```dart | ||
import 'soundcloud_explode_dart/soundcloud_explode_dart.dart'; | ||
final client = SoundcloudClient(); | ||
// Users can be retrieved via URL... | ||
final user1 = await client.users.getByUrl('https://www.soundcloud.com/a-user'); | ||
// ...or via their user ID. | ||
final user2 = await client.users.get(123456789); | ||
// Get the tracks/playlists/albums a specific user has uploaded... | ||
final tracks = client.users.getTracks(user1.id); | ||
final playlists = client.users.getPlaylists(user1.id); | ||
final albums = client.users.getAlbums(user1.id); | ||
``` | ||
|
||
### Querying tracks and streams | ||
|
||
Metadata about specific tracks can also be retrieved: | ||
|
||
```dart | ||
import 'soundcloud_explode_dart/soundcloud_explode_dart.dart'; | ||
final client = SoundcloudClient(); | ||
// Tracks can also be retrieved via URL... | ||
final track1 = await client.tracks.getByUrl('https://www.soundcloud.com/a-user/a-track'); | ||
// ...or via their track ID. | ||
final track2 = await client.tracks.get(123456789); | ||
``` | ||
|
||
In order to play a track, you need to resolve the streams available for it: | ||
|
||
```dart | ||
import 'soundcloud_explode_dart/soundcloud_explode_dart.dart'; | ||
import 'some_audio_player/some_audio_player.dart'; | ||
final client = SoundcloudClient(); | ||
final audioPlayer = SomeAudioPlayer(); | ||
final track = await client.tracks.getByUrl('https://www.soundcloud.com/a-user/a-track'); | ||
final streams = await client.tracks.getStreams(track.id); | ||
final stream = streams.firstWhere((s) => s.container == Container.mp3); | ||
await audioPlayer.play(stream.url); | ||
``` | ||
|
||
> Note: some tracks only provide a 30 second snippet and cannot be played in their entirety; those that require a SoundCloud Go subscription are one such example. | ||
> | ||
> To determine whether or not a track is fully playable: | ||
> | ||
> ```dart | ||
> final track = await client.tracks.get(123456789); | ||
> if (track.duration == track.fullDuration) { | ||
> // Track can be played until completion. | ||
> ... | ||
> } | ||
> ``` | ||
### Querying playlists/albums | ||
To retrieve metadata about specific playlists: | ||
```dart | ||
import 'soundcloud_explode_dart/soundcloud_explode_dart.dart'; | ||
final client = SoundcloudClient(); | ||
// Playlists/albums can be retrieved via URL... | ||
final playlist11 = await client.playlists.getByUrl('https://www.soundcloud.com/a-user/sets/a-playlist-or-album'); | ||
// ...or via their playlist ID. | ||
final playlist2 = await client.playlists.get(123456789); | ||
// Indicates if the playlist is identified as an album or not. | ||
final isAlbum = playlist1.isAlbum; | ||
// Get the tracks contained with a playlist/album... | ||
final tracks = client.playlists.getTracks(playlist1.id); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
include: package:lints/recommended.yaml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
targets: | ||
$default: | ||
builders: | ||
json_serializable: | ||
options: | ||
field_rename: snake |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/// Scrape metadata about users, tracks, playlists, and albums from SoundCloud without requiring an account, API key, or rate-limiting. | ||
library; | ||
|
||
export 'src/soundcloud_client.dart'; | ||
export 'src/playlists/playlists.dart'; | ||
export 'src/search/search.dart'; | ||
export 'src/tracks/tracks.dart'; | ||
export 'src/users/users.dart'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import 'package:http/http.dart'; | ||
import '../exceptions/client_unauthorized_exception.dart'; | ||
|
||
/// Interacts with the SoundCloud API. | ||
/// | ||
/// This should only be used internally. | ||
class SoundcloudController { | ||
final Client _http; | ||
String? _clientId; | ||
|
||
/// Creates a new [SoundcloudController] that uses the provided [httpClient]. | ||
SoundcloudController(Client httpClient) | ||
: _http = httpClient; | ||
|
||
/// Gets the client ID for the current session. | ||
/// | ||
/// This parameter is cached so that only one request is made for the duration of this session. | ||
Future<String> getClientId() async { | ||
if (_clientId != null) return _clientId!; | ||
|
||
var response = await _http.get(Uri.https('soundcloud.com', '')); | ||
final scripts = RegExp('<script.*?src="(.*?)"').allMatches(response.body); | ||
|
||
if (scripts.isEmpty) throw ClientUnauthorizedException.clientId(); | ||
|
||
final scriptUrl = scripts.last.group(1); | ||
if (scriptUrl == null) throw ClientUnauthorizedException.clientId(); | ||
|
||
response = await _http.get(Uri.parse(scriptUrl)); | ||
_clientId = response.body | ||
.split(',client_id')[1] | ||
.split('"')[1]; | ||
|
||
return _clientId!; | ||
} | ||
|
||
/// Returns the object represented by a SoundCloud [url]. | ||
/// | ||
/// E.g. https://soundcloud.com/a-user will resolve to a [User] object's JSON representation. | ||
Future<String> resolveUrl(String url) async { | ||
final resolvingUrl = Uri | ||
.parse(url) | ||
.replace(host: 'soundcloud.com') | ||
.toString(); | ||
|
||
final clientId = await getClientId(); | ||
|
||
final uri = Uri.https( | ||
'api-v2.soundcloud.com', | ||
'/resolve', | ||
{ | ||
'url': resolvingUrl, | ||
'client_id': clientId | ||
} | ||
); | ||
|
||
final response = await _http.get(uri); | ||
return response.body; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/// The default number of results to skip when returning from a SoundCloud query. | ||
const defaultOffset = 0; | ||
|
||
/// The default number of results that are returned from a SoundCloud query. | ||
const defaultLimit = 10; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import 'soundcloud_explode_exception.dart'; | ||
|
||
/// Indicates a failure to obtain a valid client ID from the SoundCloud server. | ||
class ClientUnauthorizedException extends SoundcloudExplodeException { | ||
ClientUnauthorizedException(super.message); | ||
|
||
static ClientUnauthorizedException clientId() => ClientUnauthorizedException( | ||
'''Could not resolve a valid client ID for this session. | ||
This may be a bug in the library. If the issue persists, do not hesitate to report it on the | ||
project's GitHub page.''' | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import 'package:soundcloud_explode_dart/src/exceptions/soundcloud_explode_exception.dart'; | ||
|
||
/// Indicates a failure to resolve a [SearchResult] to a specific instance. | ||
class SearchResultException extends SoundcloudExplodeException { | ||
const SearchResultException(super.message); | ||
|
||
static SearchResultException cannotResolve() => SearchResultException( | ||
'Unable to resolve the provided value to a specific [SearchResult].' | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/// Superclass of all exceptions thrown by this library. | ||
class SoundcloudExplodeException implements Exception { | ||
final String? message; | ||
|
||
/// Creates a new [SoundcloudExplodeException] with the provided [message]. | ||
const SoundcloudExplodeException(this.message); | ||
|
||
@override | ||
String toString() => 'SoundcloudExplodeException: \n$message'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import 'soundcloud_explode_exception.dart'; | ||
|
||
/// Indicates a failure in resolving a track or its streams. | ||
class TrackResolutionException extends SoundcloudExplodeException { | ||
const TrackResolutionException(super.message); | ||
|
||
static TrackResolutionException noStreams(int trackId) => TrackResolutionException( | ||
'''Could not resolve any streams for the specified track ID: $trackId | ||
This may be a bug in the library. If the issue persists, do not hesitate to report it on the | ||
project's GitHub page.''' | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// ignore_for_file: invalid_annotation_target | ||
|
||
import 'package:freezed_annotation/freezed_annotation.dart'; | ||
import '../users/mini_user.dart'; | ||
import 'soundcloud_playlist.dart'; | ||
|
||
part 'playlist.freezed.dart'; | ||
part 'playlist.g.dart'; | ||
|
||
/// Metadata about a SoundCloud playlist. | ||
@freezed | ||
class Playlist with _$Playlist implements SoundcloudPlaylist { | ||
const factory Playlist({ | ||
required Uri? artworkUrl, | ||
required DateTime createdAt, | ||
required String? description, | ||
required double duration, | ||
required String? genre, | ||
required int id, | ||
required String? labelName, | ||
required DateTime? lastModified, | ||
@JsonKey(defaultValue: 0) required double likesCount, | ||
required Uri permalinkUrl, | ||
@JsonKey(defaultValue: 0) required double repostsCount, | ||
required String? tagList, | ||
required String title, | ||
required bool isAlbum, | ||
required MiniUser user, | ||
@JsonKey(defaultValue: 0) required double trackCount | ||
}) = _Playlist; | ||
|
||
factory Playlist.fromJson(Map<String, Object?> json) => _$PlaylistFromJson(json); | ||
} |
Oops, something went wrong.