Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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,
Copy link
Owner

Choose a reason for hiding this comment

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

The website says 14 January 2016 when I hover the "9 years ago". Even if there is a timezone offset, it can't be two days?
We should investigate all the available date fields, ideally for a release with a known release date (for example from another platform).

Copy link
Author

@Lioncat6 Lioncat6 Oct 25, 2025

Choose a reason for hiding this comment

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

As it turns out, SoundCloud entities have 4 "release" dates... the most reliable being the dedicated release fields, as those are manually entered by the artist (usually pretty rarely, similar to ISRC and UPC data). The date that appears on the website is the display_date field, which mirrors the last_modified date. The date I'm currently using as a fallback for the actual release date is the created_at date, which isn't what's displayed on the website.

month: 1,
year: 2016,
},
title: "Piercing Light (Mako Remix)",
types: [
"Single",
],
}
`;
206 changes: 206 additions & 0 deletions providers/SoundCloud/api_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// Classes from https://github.com/Moebytes/soundcloud.ts
// 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: string;
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;
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: string;
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[];
id: number;
display_date: string;
sharing: 'public' | 'private';
secret_token: string | null;
created_at: string;
likes_count: number;
kind: string;
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 Classes

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

export type RawReponse = {
/** Raw Data from the soundcloud API */
apiResponse: SoundcloudTrack | SoundcloudPlaylist;
/** The type of release, either 'track' or 'playlist' */
type: 'track' | 'playlist';
/** Url of the release */
href: string;
};
45 changes: 45 additions & 0 deletions providers/SoundCloud/mod.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describeProvider, makeProviderOptions } from '@/providers/test_spec.ts';
import { stubProviderLookups } from '@/providers/test_stubs.ts';
import { assert } from 'std/assert/assert.ts';
import { afterAll, describe } from '@std/testing/bdd';
import { assertSnapshot } from '@std/testing/snapshot';

import SoundCloudProvider from './mod.ts';

describe('SoundCloud provider', () => {
const sc = new SoundCloudProvider(makeProviderOptions());
const lookupStub = stubProviderLookups(sc);

describeProvider(sc, {
urls: [{
description: 'album page',
url: new URL('https://soundcloud.com/ivycomb/sets/crimsongalaxies'),
id: { type: 'playlist', id: 'ivycomb/crimsongalaxies' },
isCanonical: true,
}, {
description: 'standalone track page',
url: new URL('https://soundcloud.com/lonealphamusic/magazines'),
id: { type: 'track', id: 'lonealphamusic/magazines' },
serializedId: 'lonealphamusic/track/magazines',
isCanonical: true,
}, {
description: 'artist page',
url: new URL('https://soundcloud.com/vocalokat'),
id: { type: 'artist', id: 'vocalokat' },
isCanonical: true,
}],
releaseLookup: [{
description: 'track release with downloads enabled',
release: 'leagueoflegends/track/piercing-light-mako-remix',
assert: async (release, ctx) => {
await assertSnapshot(ctx, release);
const isFree = release.externalLinks.some((link) => link.types?.includes('free download'));
assert(isFree, 'Release should be downloadable for free');
},
}],
});

afterAll(() => {
lookupStub.restore();
});
});
Loading