Skip to content

Commit

Permalink
Merge pull request #4 from appujet/beta-v1
Browse files Browse the repository at this point in the history
Beta v1
  • Loading branch information
appujet authored Oct 12, 2024
2 parents 251c1aa + c391e7c commit c24324e
Show file tree
Hide file tree
Showing 11 changed files with 346 additions and 244 deletions.
18 changes: 4 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# JioSaavn Plugin For Lavalink

[![JitPack](https://jitpack.io/v/appujet/jiosaavn-plugin.svg)](https://jitpack.io/#appujet/jiosaavn-plugin)

- This is a plugin for [Lavalink](https://github.com/lavalink-devs/Lavalink)
- This plugin allows you to play songs from JioSaavn in your discord server.
- This plugin uses the [JioSaavn API](https://github.com/sumitkolhe/jiosaavn-api) to fetch songs.
- NO API KEY REQUIRED
- Special thanks to [topi314](https://github.com/topi314/LavaSrc) and [duncte123](https://github.com/duncte123) because most of the code for this plugin is based on [Lavasrc](https://github.com/topi314/LavaSrc) and [skybot-lavalink-plugin](https://github.com/DuncteBot/skybot-lavalink-plugin).

## Lavalink Usage
Expand Down Expand Up @@ -57,9 +59,8 @@ lavalink:
plugins:
jiosaavn:
apiURL: "https://saavn.dev/api" # JioSaavn API URL
playlistTrackLimit: 50 # The maximum number of tracks to return from given playlist (default 50 tracks)
recommendationsTrackLimit: 20 # The maximum number of track to return from recommendations (default 20 tracks)
recommendationsTrackLimit: 10 # The maximum number of track to return from recommendations (default 10 tracks)
metrics:
prometheus:
Expand Down Expand Up @@ -102,14 +103,3 @@ logging:
- <https://www.jiosaavn.com/album/bhediya/wSM2AOubajk>_
- <https://www.jiosaavn.com/artist/arijit-singh-songs/LlRWpHzy3Hk>_
- <https://www.jiosaavn.com/featured/jai-hanuman/8GIEhrr8clSO0eMLZZxqsA>__

## How to get API URL ?

- You can host the api locally using [this guide](https://github.com/sumitkolhe/jiosaavn-api)

- You can easily deploy your own instance of the API by clicking the button below:

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/sumitkolhe/jiosaavn-api)

> [!TIP]
> To ensure the API provides results in the intended language, configure the [Serverless Function Region](https://vercel.com/docs/concepts/functions/serverless-functions/regions) in Vercel to `Mumbai, India (South) - > bom1`.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version=0.1.9
version=1.0.0
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,18 @@
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.BasicAudioPlaylist;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;


import java.util.List;

public class ExtendedAudioPlaylist extends BasicAudioPlaylist {

@NotNull
protected final Type type;
@Nullable
protected final String url;
@Nullable
protected final String artworkURL;
@Nullable
protected final String author;
@Nullable
protected final Integer totalTracks;
protected final @NotNull String url;
protected final @NotNull String artworkURL;
protected final @NotNull String author;
protected final @NotNull Integer totalTracks;

public ExtendedAudioPlaylist(String name, List<AudioTrack> tracks, @NotNull Type type, @NotNull String url, @NotNull String artworkURL, @NotNull String author, @NotNull Integer totalTracks) {
super(name, tracks, null, false);
Expand All @@ -34,23 +30,19 @@ public Type getType() {
return type;
}

@Nullable
public String getUrl() {
public @NotNull String getUrl() {
return this.url;
}

@Nullable
public String getArtworkURL() {
public @NotNull String getArtworkURL() {
return this.artworkURL;
}

@Nullable
public String getAuthor() {
public @NotNull String getAuthor() {
return this.author;
}

@Nullable
public Integer getTotalTracks() {
public @NotNull Integer getTotalTracks() {
return this.totalTracks;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,38 @@

import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager;
import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools;
import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser;
import com.sedmelluq.discord.lavaplayer.tools.http.HttpContextFilter;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterfaceManager;

import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.CookieStore;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.HttpClientBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.function.Consumer;
import java.util.function.Function;


public abstract class ExtendedAudioSourceManager implements AudioSourceManager, HttpConfigurable {
public static final Logger log = LoggerFactory.getLogger(ExtendedAudioSourceManager.class);
protected final HttpInterfaceManager httpInterfaceManager;

private static final String BASE_API_URL = "https://www.jiosaavn.com/api.php?";
public ExtendedAudioSourceManager () {
this(true);
}
Expand All @@ -42,6 +54,38 @@ public HttpInterface getHttpInterface() {
return httpInterfaceManager.getInterface();
}

public JsonBrowser fetchJson(String endpoint, String[] params, String context) {
try {
URI uri = getUri(endpoint, params, context);
final HttpGet httpGet = new HttpGet(uri);
try (final CloseableHttpResponse response = this.getHttpInterface().execute(httpGet)) {
final String content = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
//log.info("Response from API: {}", content);
return JsonBrowser.parse(content);
}
} catch (Exception e) {
throw new RuntimeException("Error fetching JSON from JioSaavn API", e);
}
}

private static URI getUri(String endpoint, String[] params, String context) throws URISyntaxException {
URIBuilder uriBuilder = new URIBuilder(BASE_API_URL);
uriBuilder.addParameter("__call", endpoint);
uriBuilder.addParameter("_format", "json");
uriBuilder.addParameter("_marker", "0");
uriBuilder.addParameter("api_version", "4");
uriBuilder.addParameter("ctx", context != null ? context : "web6dot0");

if (params != null) {
for (int i = 0; i < params.length; i += 2) {
if (i + 1 < params.length) {
uriBuilder.addParameter(params[i], params[i + 1]);
}
}
}
return uriBuilder.build();
}

@Override
public void shutdown() {
ExceptionTools.closeWithWarnings(httpInterfaceManager);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,19 @@ public void process(LocalAudioTrackExecutor executor) throws Exception {

protected void loadStream(LocalAudioTrackExecutor localExecutor, HttpInterface httpInterface) throws Exception {
final String trackUrl = getPlaybackUrl();
try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(trackUrl), this.getTrackDuration())) {
try (final var stream = this
.wrapStream(new PersistentHttpStream(httpInterface, new URI(trackUrl), this.getTrackDuration()))) {
processDelegate(createAudioTrack(this.trackInfo, stream), localExecutor);
} catch (Exception e) {
log.error("Failed to load track from URL: {}", trackUrl, e);
throw e;
}
}


protected SeekableInputStream wrapStream(SeekableInputStream stream) {
return stream;
}

protected InternalAudioTrack createAudioTrack(AudioTrackInfo trackInfo, SeekableInputStream stream) {
return new MpegAudioTrack(trackInfo, stream);
}
Expand Down
142 changes: 129 additions & 13 deletions main/src/main/java/com/github/appujet/jiosaavn/Utils.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,138 @@
package com.github.appujet.jiosaavn;

import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;

import java.io.IOException;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Utils {

public static JsonBrowser fetchJson(String pageURl, ExtendedAudioSourceManager sourceManager) {
final HttpGet httpGet = new HttpGet(pageURl);
try (final CloseableHttpResponse response = sourceManager.getHttpInterface().execute(httpGet)) {
final String content = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
return JsonBrowser.parse(content);
} catch (IOException e) {
throw new RuntimeException(e);
private static final String KEY = "38346591";
private static final String ALGORITHM = "DES/ECB/NoPadding";
private static final String[] QUALITIES = { "_320", "_160", "_96", "_48", "_12" };

// Regular expressions for extracting tokens
private static final Pattern SONG_PATTERN = Pattern.compile("jiosaavn\\.com/song/[^/]+/([^/]+)$");
private static final Pattern ARTIST_PATTERN = Pattern.compile("jiosaavn\\.com/artist/[^/]+/([^/]+)$");
private static final Pattern ALBUM_PATTERN = Pattern.compile("jiosaavn\\.com/album/[^/]+/([^/]+)$");
private static final Pattern PLAYLIST_PATTERN = Pattern.compile("jiosaavn\\.com/(?:featured|s/playlist)/[^/]+/([^/]+)$");

/**
* Extracts a song token from a JioSaavn song URL.
*
* @param url The song URL.
* @return The song token or null if not found.
*/
public static String extractSongToken(String url) {
return extractToken(url, SONG_PATTERN);
}

/**
* Extracts an artist token from a JioSaavn artist URL.
*
* @param url The artist URL.
* @return The artist token or null if not found.
*/
public static String extractArtistToken(String url) {
return extractToken(url, ARTIST_PATTERN);
}

/**
* Extracts an album token from a JioSaavn album URL.
*
* @param url The album URL.
* @return The album token or null if not found.
*/
public static String extractAlbumToken(String url) {
return extractToken(url, ALBUM_PATTERN);
}

/**
* Extracts a playlist token from a JioSaavn playlist URL.
*
* @param url The playlist URL.
* @return The playlist token or null if not found.
*/
public static String extractPlaylistToken(String url) {
return extractToken(url, PLAYLIST_PATTERN);
}

/**
* Extracts a token using the provided pattern.
*
* @param url The URL to extract the token from.
* @param pattern The regex pattern to match the token.
* @return The token if found, or null if not.
*/
private static String extractToken(String url, Pattern pattern) {
if (url == null || url.isEmpty()) {
return null;
}

Matcher matcher = pattern.matcher(url);
if (matcher.find()) {
return matcher.group(1); // Return the first captured group
}
return null;
}

/**
* Encodes a string using URL encoding and replaces certain characters.
* @param input The string to encode.
* @return The encoded string.
*/
public static String encodeURIComponent(String input) {
String encoded = URLEncoder.encode(input, StandardCharsets.UTF_8);
encoded = encoded.replace("+", "%20");
encoded = encoded.replace("'", "%27");
encoded = encoded.replace("%", "%25");
encoded = encoded.replace("#", "%23");
encoded = encoded.replace("&", "%26");
encoded = encoded.replace("=", "%3D");
encoded = encoded.replace("?", "%3F");

return encoded;

}

/**
* Generates a download link by decrypting the encrypted media URL.
*
* @param encryptedMediaUrl The encrypted media URL.
* @return The download link or null if not valid.
*/
public static String getDownloadLink(String encryptedMediaUrl) {
if (encryptedMediaUrl == null || encryptedMediaUrl.isEmpty())
return null;

try {
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedMediaUrl);
Key key = new SecretKeySpec(KEY.getBytes(), "DES");
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, key);

byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
String decryptedLink = new String(decryptedBytes).trim();

for (String quality : QUALITIES) {
String downloadUrl = decryptedLink.replace("_96", quality);
if (isValidUrl(downloadUrl)) {
return downloadUrl;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

// Helper method to check if a URL is valid
private static boolean isValidUrl(String url) {
return url.startsWith("https://aac.saavncdn.com/") && url.endsWith(".mp4");
}
}
}
Loading

0 comments on commit c24324e

Please sign in to comment.