Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
12bbba7
start soundcloud
Lioncat6 Sep 27, 2025
353a0bb
progress
Lioncat6 Oct 3, 2025
01641e1
sc changes
Lioncat6 Oct 10, 2025
187d4d1
Sc progres
Lioncat6 Oct 10, 2025
99d450d
God I hate deno
Lioncat6 Oct 11, 2025
d9c97e7
Finalize soundcloud support
Lioncat6 Oct 13, 2025
a4701ca
Added soundcloud icon & color
Lioncat6 Oct 13, 2025
bdc0f04
Increase resolution of thumb cover art
Lioncat6 Oct 13, 2025
943a02b
Implement most suggestions
Lioncat6 Oct 14, 2025
c8ae12d
Add test file
Lioncat6 Oct 23, 2025
f339b38
Soundcloud relationship type handling
Lioncat6 Oct 23, 2025
df02363
Added souncloud config
Lioncat6 Oct 23, 2025
1ac64fa
Fixed deno formatting
Lioncat6 Oct 23, 2025
4dd484d
Ran test in download mode and generated snapshot `deno test --allow-n…
Lioncat6 Oct 23, 2025
bf0554b
Revert resolution change
Lioncat6 Oct 23, 2025
f1684f4
Remove time and timezone from parseSoundcloudTimestamp
Lioncat6 Oct 23, 2025
33e8047
Update snapshot
Lioncat6 Oct 23, 2025
6aa35c7
Simplified date logic
Lioncat6 Oct 25, 2025
a6c1ddc
Fixed unions
Lioncat6 Oct 25, 2025
5b604aa
Fix deno formatting
Lioncat6 Oct 25, 2025
ee4b8f0
test(SoundCloud): Stub token retrieval
kellnerd Nov 3, 2025
953182f
fix(SoundCloud): Standardize the used entity type names
kellnerd Nov 3, 2025
29ef636
refactor(SoundCloud): Combine URL patterns for supported releases
kellnerd Nov 3, 2025
4c646d5
fix(SoundCloud): Treat artist subpage URLs as artists and not as tracks
kellnerd Nov 3, 2025
6396fb9
refactor(SoundCloud): Dry & Format code
kellnerd Nov 3, 2025
9020d05
refactor(SoundCloud): Make timestamp parser more robust
kellnerd Nov 3, 2025
2c284c8
fix(SoundCloud): Warn about missing tracks
kellnerd Nov 9, 2025
5d2413f
fix(SoundCloud): Preserve credited track artist & Ignore preview lengths
kellnerd Nov 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@ HARMONY_SPOTIFY_CLIENT_SECRET=
# Tidal app config. See https://developer.tidal.com/reference/web-api
HARMONY_TIDAL_CLIENT_ID=
HARMONY_TIDAL_CLIENT_SECRET=

# Soundcloud app config. See https://developers.soundcloud.com/docs/api/guide
HARMONY_SOUNDCLOUD_CLIENT_ID=
HARMONY_SOUNDCLOUD_CLIENT_SECRET=
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"Beatport",
"brainz",
"Brainz",
"commentable",
"deezer",
"Deezer",
"Deno",
Expand Down Expand Up @@ -37,9 +38,11 @@
"nums",
"preact",
"preorder",
"reposts",
"runtimes",
"secondhandsongs",
"smartradio",
"soundcloud",
"spotify",
"streamable",
"tabler",
Expand Down
3 changes: 3 additions & 0 deletions musicbrainz/seeding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ export function convertLinkType(entityType: EntityType, linkType: LinkType, url?
if (url?.hostname.endsWith('.bandcamp.com')) {
return typeIds.bandcamp;
}
if (url?.hostname.replace('www.', '') == 'soundcloud.com') {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do the URLs ever have a www subdomain? The provider implementation doesn't accept it at least.

return typeIds.soundcloud;
}
return typeIds['discography page'] ?? typeIds['discography entry'];
case 'license':
return typeIds['license'];
Expand Down
102 changes: 102 additions & 0 deletions providers/SoundCloud/__snapshots__/mod.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
export const snapshot = {};

snapshot[`SoundCloud provider > release lookup > track release with downloads enabled 1`] = `
{
artists: [
{
creditedName: "League of Legends",
externalIds: [
{
id: "leagueoflegends",
provider: "soundcloud",
type: "artist",
},
],
name: "League of Legends",
},
],
availableIn: undefined,
externalLinks: [
{
types: [
"free streaming",
"free download",
],
url: "https://soundcloud.com/leagueoflegends/piercing-light-mako-remix",
},
],
images: [
{
provider: "SoundCloud",
thumbUrl: "https://i1.sndcdn.com/artworks-000142912000-f05col-t300x300.jpg",
types: [
"front",
],
url: "https://i1.sndcdn.com/artworks-000142912000-f05col-t500x500.jpg",
},
],
info: {
messages: [],
providers: [
{
apiUrl: "https://api.soundcloud.com/resolve?url=https%3A%2F%2Fsoundcloud.com%2Fleagueoflegends%2Fpiercing-light-mako-remix",
id: "leagueoflegends/track/piercing-light-mako-remix",
internalName: "soundcloud",
lookup: {
method: "id",
value: "leagueoflegends/track/piercing-light-mako-remix",
},
name: "SoundCloud",
url: "https://soundcloud.com/leagueoflegends/piercing-light-mako-remix",
},
],
},
labels: undefined,
media: [
{
format: "Digital Media",
tracklist: [
{
artists: [
{
creditedName: "League of Legends",
externalIds: [
{
id: "leagueoflegends",
provider: "soundcloud",
type: "artist",
},
],
name: "League of Legends",
},
],
availableIn: undefined,
isrc: undefined,
length: 291422,
number: 1,
recording: {
externalIds: [
{
id: "leagueoflegends/piercing-light-mako-remix",
provider: "soundcloud",
type: "track",
},
],
},
title: "Piercing Light (Mako Remix)",
},
],
},
],
packaging: "None",
releaseDate: {
day: 12,
month: 1,
year: 2016,
},
title: "Piercing Light (Mako Remix)",
types: [
"Single",
],
}
`;
203 changes: 203 additions & 0 deletions providers/SoundCloud/api_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// Type definitions from https://github.com/Moebytes/soundcloud.ts
// Copyright (c) 2020 Moebytes. MIT License.

export type SoundcloudImageFormats =
| 't500x500'
| 'crop'
| 't300x300'
| 'large'
| 't67x67'
| 'badge'
| 'small'
| 'tiny'
| 'mini';

export type SoundcloudLicense =
| 'no-rights-reserved'
| 'all-rights-reserved'
| 'cc-by'
| 'cc-by-nc'
| 'cc-by-nd'
| 'cc-by-sa'
| 'cc-by-nc-nd'
| 'cc-by-nc-sa';

export type SoundcloudTrackType =
| 'original'
| 'remix'
| 'live'
| 'recording'
| 'spoken'
| 'podcast'
| 'demo'
| 'in progress'
| 'stem'
| 'loop'
| 'sound effect'
| 'sample'
| 'other';

export interface SoundcloudTrack {
artwork_url: string;
comment_count: number;
commentable: boolean;
created_at: string;
description: string;
display_date: string;
download_count: number;
downloadable: boolean;
duration: number;
embeddable_by: 'all' | 'me' | 'none';
full_duration: number;
genre: string;
has_downloads_left: boolean;
id: number;
kind: 'track';
label_name: string;
last_modified: string;
license: SoundcloudLicense;
likes_count: number;
monetization_model: string;
permalink: string;
permalink_url: string;
playback_count: number;
policy: string;
public: boolean;
purchase_title: string;
purchase_url: string;
reposts_count: number;
secret_token: string;
sharing: 'private' | 'public';
state: 'processing' | 'failed' | 'finished';
streamable: boolean;
tag_list: string;
title: string;
uri: string;
urn: string;
user: SoundcloudUser;
user_id: number;
visuals: string;
waveform_url: string;
release: string | null;
key_signature: string | null;
isrc: string | null;
bpm: number | null;
release_year: number | null;
release_month: number | null;
release_day: number | null;
stream_url: string | null;
download_url: string | null;
available_country_codes: string[] | null;
secret_uri: string | null;
user_favorite: boolean | null;
user_playback_count: number | null;
favoritings_count: number;
access: 'playable' | 'preview' | 'blocked' | string;
/** Artist credit, may contain featured artists. */
metadata_artist: string;
}

export interface SoundcloudPlaylist {
duration: number;
permalink_url: string;
reposts_count: number;
genre: string | null;
permalink: string;
purchase_url: string | null;
description: string | null;
uri: string;
urn: string;
label_name: string | null;
tag_list: string;
set_type: string;
public: boolean;
track_count: number;
user_id: number;
last_modified: string;
license: SoundcloudLicense;
tracks: SoundcloudTrack[];
/** API URL for the tracklist. */
tracks_uri: string;
id: number;
display_date: string;
sharing: 'public' | 'private';
secret_token: string | null;
created_at: string;
likes_count: number;
kind: 'playlist';
title: string;
purchase_title: string | null;
managed_by_feeds: boolean;
artwork_url: string | null;
is_album: boolean;
user: SoundcloudUser;
published_at: string | null;
embeddable_by: 'all' | 'me' | 'none';
release_year: number | null;
release_month: number | null;
release_day: number | null;
type: string | null;
playlist_type: string | null;
}

export interface SoundcloudUser {
avatar_url: string;
city: string;
comments_count: number;
country_code: number | null;
created_at: string;
creator_subscriptions: SoundcloudCreatorSubscription[];
creator_subscription: SoundcloudCreatorSubscription;
description: string;
followers_count: number;
followings_count: number;
first_name: string;
full_name: string;
groups_count: number;
id: number;
kind: string;
last_modified: string;
last_name: string;
likes_count: number;
playlist_likes_count: number;
permalink: string;
permalink_url: string;
playlist_count: number;
reposts_count: number | null;
track_count: number;
uri: string;
urn: string;
username: string;
verified: boolean;
visuals: {
urn: string;
enabled: boolean;
visuals: SoundcloudVisual[];
tracking: null;
};
}

export interface SoundcloudVisual {
urn: string;
entry_time: number;
visual_url: string;
}

export interface SoundcloudCreatorSubscription {
product: {
id: string;
};
}

// Custom type definitions.

export type ApiError = {
code: number;
message: string;
link: string;
status: string;
errors: string[];
error: string | null;
};

export type RawRelease = SoundcloudTrack | SoundcloudPlaylist;
Loading