Skip to content

Commit

Permalink
remove apple music api token auto extraction
Browse files Browse the repository at this point in the history
  • Loading branch information
topi314 committed Jul 23, 2023
1 parent 30ab086 commit 194d62f
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 59 deletions.
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,20 @@ playerManager.registerSourceManager(new SpotifySourceManager(null, clientId, cli
```java
AudioPlayerManager playerManager = new DefaultAudioPlayerManager();

// create a new AppleMusicSourceManager with the standard providers, token(pass null for automatic extraction), countrycode and AudioPlayerManager and register it
// create a new AppleMusicSourceManager with the standard providers, apple music api token, countrycode and AudioPlayerManager and register it
playerManager.registerSourceManager(new AppleMusicSourceManager(null, mediaAPIToken , "us", playerManager));
```

<details>
<summary>How to get media api token with Apple developer account</summary>

1. Go to https://music.apple.com
2. Open DevTools and go to the Debugger tab
3. Search with this regex `const \w{2}="(?<token>(ey[\w-]+)\.([\w-]+)\.([\w-]+))"` in all `index-*.js` files
4. Copy the token from the source code

</details>

#### Deezer
```java
AudioPlayerManager playerManager = new DefaultAudioPlayerManager();
Expand All @@ -100,7 +110,6 @@ playerManager.registerSourceManager(new DeezerSourceManager("...");
<details>
<summary>How to get access token</summary>

## How to get access token
1. (Optional) Open DevTools in your browser and on the Network tab enable trotlining.
2. Go to https://oauth.yandex.ru/authorize?response_type=token&client_id=23cabbbdc6cd418abb4b39c32c41195d
3. Authorize and grant access
Expand Down Expand Up @@ -163,6 +172,8 @@ For all supported urls and queries see [here](#supported-urls-and-queries)

To get your Spotify clientId & clientSecret go [here](https://developer.spotify.com/dashboard/applications) & then copy them into your `application.yml` like the following.

To get your Apple Music api token go [here](#apple-music)

To get your Yandex Music access token go [here](#yandex-music)

(YES `plugins` IS AT ROOT IN THE YAML)
Expand All @@ -188,7 +199,7 @@ plugins:
albumLoadLimit: 6 # The number of pages at 50 tracks each
applemusic:
countryCode: "US" # the country code you want to use for filtering the artists top tracks and language. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
# mediaAPIToken: "..." # Can be used to bypass the auto token fetching which is likely to break again in the future
mediaAPIToken: "your apple music api token" # apple music api token
playlistLoadLimit: 6 # The number of pages at 300 tracks each
albumLoadLimit: 6 # The number of pages at 300 tracks each
deezer:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,15 @@
import com.github.topisenpai.lavasrc.mirror.MirroringAudioTrackResolver;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterfaceManager;
import com.sedmelluq.discord.lavaplayer.track.AudioItem;
import com.sedmelluq.discord.lavaplayer.track.AudioReference;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo;
import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist;
import org.apache.commons.io.IOUtils;
import com.sedmelluq.discord.lavaplayer.track.*;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;
import org.jsoup.Jsoup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -37,7 +32,6 @@
public class AppleMusicSourceManager extends MirroringAudioSourceManager implements HttpConfigurable {

public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?(www\\.)?music\\.apple\\.com/(?<countrycode>[a-zA-Z]{2}/)?(?<type>album|playlist|artist|song)(/[a-zA-Z\\d\\-]+)?/(?<identifier>[a-zA-Z\\d\\-.]+)(\\?i=(?<identifier2>\\d+))?");
public static final Pattern TOKEN_SCRIPT_PATTERN = Pattern.compile("const \\w{2}=\"(?<token>(ey[\\w-]+)\\.([\\w-]+)\\.([\\w-]+))\"");
public static final String SEARCH_PREFIX = "amsearch:";
public static final int MAX_PAGE_ITEMS = 300;
public static final String API_BASE = "https://api.music.apple.com/v1/";
Expand All @@ -46,7 +40,7 @@ public class AppleMusicSourceManager extends MirroringAudioSourceManager impleme
private final String countryCode;
private int playlistPageLimit;
private int albumPageLimit;
private String token;
private final String token;
private String origin;
private Instant tokenExpire;

Expand All @@ -56,12 +50,17 @@ public AppleMusicSourceManager(String[] providers, String mediaAPIToken, String

public AppleMusicSourceManager(String mediaAPIToken, String countryCode, AudioPlayerManager audioPlayerManager, MirroringAudioTrackResolver mirroringAudioTrackResolver) {
super(audioPlayerManager, mirroringAudioTrackResolver);
if (mediaAPIToken == null || mediaAPIToken.isEmpty()) {
throw new RuntimeException("Apple Music API token is empty or null");
}
this.token = mediaAPIToken;

try {
this.parseTokenData();
} catch (IOException e) {
throw new IllegalArgumentException("Cannot parse token for expire date and origin", e);
throw new RuntimeException("Failed to parse Apple Music API token", e);
}

if (countryCode == null || countryCode.isEmpty()) {
this.countryCode = "us";
} else {
Expand All @@ -85,9 +84,9 @@ public String getSourceName() {
@Override
public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException {
return new AppleMusicAudioTrack(trackInfo,
DataFormatTools.readNullableText(input),
DataFormatTools.readNullableText(input),
this
DataFormatTools.readNullableText(input),
DataFormatTools.readNullableText(input),
this
);
}

Expand Down Expand Up @@ -129,44 +128,14 @@ public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference)
}

public void parseTokenData() throws IOException {
if (this.token == null || this.token.isEmpty()) {
return;
}
var json = JsonBrowser.parse(new String(Base64.getDecoder().decode(this.token.split("\\.")[1])));
this.tokenExpire = Instant.ofEpochSecond(json.get("exp").asLong(0));
this.origin = json.get("root_https_origin").index(0).text();
}

public void requestToken() throws IOException {
var request = new HttpGet("https://music.apple.com");
try (var response = this.httpInterfaceManager.getInterface().execute(request)) {
var document = Jsoup.parse(response.getEntity().getContent(), null, "");
var elements = document.select("script[type=module][src~=/assets/index.*.js]");
if (elements.isEmpty()) {
throw new IllegalStateException("Cannot find token script element");
}

for (var element : elements) {
var tokenScriptURL = element.attr("src");
request = new HttpGet("https://music.apple.com" + tokenScriptURL);
try (var indexResponse = this.httpInterfaceManager.getInterface().execute(request)) {
var tokenScript = IOUtils.toString(indexResponse.getEntity().getContent(), StandardCharsets.UTF_8);
var tokenMatcher = TOKEN_SCRIPT_PATTERN.matcher(tokenScript);
if (tokenMatcher.find()) {
this.token = tokenMatcher.group("token");
this.parseTokenData();
return;
}
}

}
}
throw new IllegalStateException("Cannot find token script url");
}

public String getToken() throws IOException {
if (this.token == null || this.tokenExpire == null || this.tokenExpire.isBefore(Instant.now())) {
this.requestToken();
if (this.tokenExpire.isBefore(Instant.now())) {
throw new FriendlyException("Apple Music API token is expired", FriendlyException.Severity.SUSPICIOUS, null);
}
return this.token;
}
Expand Down Expand Up @@ -266,17 +235,17 @@ private AudioTrack parseTrack(JsonBrowser json) {
var attributes = json.get("attributes");
var artwork = attributes.get("artwork");
return new AppleMusicAudioTrack(
new AudioTrackInfo(
attributes.get("name").text(),
attributes.get("artistName").text(),
attributes.get("durationInMillis").asLong(0),
json.get("id").text(),
false,
attributes.get("url").text()
),
attributes.get("isrc").text(),
artwork.get("url").text().replace("{w}", artwork.get("width").text()).replace("{h}", artwork.get("height").text()),
this
new AudioTrackInfo(
attributes.get("name").text(),
attributes.get("artistName").text(),
attributes.get("durationInMillis").asLong(0),
json.get("id").text(),
false,
attributes.get("url").text()
),
attributes.get("isrc").text(),
artwork.get("url").text().replace("{w}", artwork.get("width").text()).replace("{h}", artwork.get("height").text()),
this
);
}

Expand Down

0 comments on commit 194d62f

Please sign in to comment.