Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LavaLyrics support #177

Merged
merged 1 commit into from
May 6, 2024
Merged
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
32 changes: 23 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
[![](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Fmaven.topi.wtf%2Freleases%2Fcom%2Fgithub%2FTopiSenpai%2FLavaSrc%2Flavasrc%2Fmaven-metadata.xml)](https://maven.topi.wtf/#/releases/com/github/TopiSenpai/LavaSrc/lavasrc)
[![](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Fmaven.topi.wtf%2Freleases%2Fcom%2Fgithub%2Ftopi314%2FLavaSrc%2Flavasrc%2Fmaven-metadata.xml)](https://maven.topi.wtf/#/releases/com/github/topi314/LavaSrc/lavasrc)

# LavaSrc

A collection of additional [Lavaplayer v2](https://github.com/sedmelluq/lavaplayer) & [LavaSearch](https://github.com/topi314/LavaSearch) Audio Source Managers and [Lavalink v4](https://github.com/lavalink-devs/Lavalink) Plugin.
* [Spotify*](https://www.spotify.com) playlists/albums/songs/artists(top tracks)/search results
* [Apple Music*](https://www.apple.com/apple-music/) playlists/albums/songs/artists/search results(Big thx to [ryan5453](https://github.com/ryan5453) for helping me)
* [Deezer](https://www.deezer.com) playlists/albums/songs/artists/search results(Big thx to [ryan5453](https://github.com/ryan5453) and [melike2d](https://github.com/melike2d) for helping me)
A collection of additional [Lavaplayer v2](https://github.com/sedmelluq/lavaplayer), [LavaSearch](https://github.com/topi314/LavaSearch) & [LavaLyrics](https://github.com/topi314/LavaLyrics) Audio Source Managers and [Lavalink v4](https://github.com/lavalink-devs/Lavalink) Plugin.
* [Spotify*](https://www.spotify.com) playlists/albums/songs/artists(top tracks)/search results/[LavaSearch](https://github.com/topi314/LavaSearch)/[LavaLyrics](https://github.com/topi314/LavaLyrics)
* [Apple Music*](https://www.apple.com/apple-music/) playlists/albums/songs/artists/search results/[LavaSearch](https://github.com/topi314/LavaSearch)(Big thx to [ryan5453](https://github.com/ryan5453) for helping me)
* [Deezer](https://www.deezer.com) playlists/albums/songs/artists/search results/[LavaSearch](https://github.com/topi314/LavaSearch)/[LavaLyrics](https://github.com/topi314/LavaLyrics)(Big thx to [ryan5453](https://github.com/ryan5453) and [melike2d](https://github.com/melike2d) for helping me)
* [Yandex Music](https://music.yandex.ru) playlists/albums/songs/artists/podcasts/search results(Thx to [AgutinVBoy](https://github.com/agutinvboy) for implementing it)
* [Flowery TTS](https://flowery.pw/docs/flowery/synthesize-v-1-tts-get) (Thx to [bachtran02](https://github.com/bachtran02) for implementing it)
* [YouTube](https://youtube.com), [YouTubeMusic](https://music.youtube.com/), [Deezer](https://www.deezer.com), [Spotify](https://www.spotify.com) & [AppleMusic](https://www.apple.com/apple-music/) support for [LavaSearch](https://github.com/topi314/LavaSearch) (Thx to [DRSchlaubi](https://github.com/DRSchlaubi) for helping me)
* [YouTube](https://youtube.com) & [YouTubeMusic](https://music.youtube.com/) [LavaSearch](https://github.com/topi314/LavaSearch)/[LavaLyrics](https://github.com/topi314/LavaLyrics) (Thx to [DRSchlaubi](https://github.com/DRSchlaubi) for helping me)

`*tracks are searched & played via YouTube or other configurable sources`

Expand Down Expand Up @@ -87,10 +87,19 @@ To get a Spotify clientId & clientSecret you must go [here](https://developer.sp
```java
AudioPlayerManager playerManager = new DefaultAudioPlayerManager();

// create a new SpotifySourceManager with the default providers, clientId, clientSecret, countryCode and AudioPlayerManager and register it
playerManager.registerSourceManager(new SpotifySourceManager(null, clientId, clientSecret, countryCode, playerManager));
// create a new SpotifySourceManager with the default providers, clientId, clientSecret, spDc, countryCode and AudioPlayerManager and register it
playerManager.registerSourceManager(new SpotifySourceManager(null, clientId, clientSecret, spDc, countryCode, playerManager));
```

<details>
<summary>How to get sp dc cookie</summary>

1. Go to https://open.spotify.com
2. Open DevTools and go to the Application tab
3. Copy the value of the `sp_dc` cookie

</details>

#### Apple Music
```java
AudioPlayerManager playerManager = new DefaultAudioPlayerManager();
Expand Down Expand Up @@ -196,7 +205,9 @@ Snapshot builds are available in https://maven.lavalink.dev/snapshots with the s

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 Spotify clientId, clientSecret go [here](https://developer.spotify.com/dashboard/applications) & then copy them into your `application.yml` like the following.

To get your Spotify spDc cookie go [here](#spotify)

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

Expand All @@ -222,6 +233,7 @@ plugins:
spotify:
clientId: "your client id"
clientSecret: "your client secret"
# spDc: "your sp dc cookie" # the sp dc cookie used for accessing the spotify lyrics api
countryCode: "US" # the country code you want to use for filtering the artists top tracks. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
playlistLoadLimit: 6 # The number of pages at 100 tracks each
albumLoadLimit: 6 # The number of pages at 50 tracks each
Expand All @@ -247,6 +259,8 @@ plugins:
silence: 0 # the silence parameter is in milliseconds. Range is 0 to 10000. The default is 0.
speed: 1.0 # the speed parameter is a float between 0.5 and 10. The default is 1.0. (0.5 is half speed, 2.0 is double speed, etc.)
audioFormat: "mp3" # supported formats are: mp3, ogg_opus, ogg_vorbis, aac, wav, and flac. Default format is mp3
youtube:
countryCode: "US" # the country code you want to use for searching lyrics via ISRC. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
```

### Plugin Info
Expand Down
3 changes: 3 additions & 0 deletions application.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ plugins:
spotify:
clientId: "your client id"
clientSecret: "your client secret"
spDc: "your sp dc cookie" # the sp dc cookie used for accessing the spotify lyrics api
countryCode: "US" # the country code you want to use for filtering the artists top tracks. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
playlistLoadLimit: 6 # The number of pages at 100 tracks each
albumLoadLimit: 6 # The number of pages at 50 tracks each
Expand All @@ -34,6 +35,8 @@ plugins:
silence: 0 # the silence parameter is in milliseconds. Range is 0 to 10000. The default is 0.
speed: 1.0 # the speed parameter is a float between 0.5 and 10. The default is 1.0. (0.5 is half speed, 2.0 is double speed, etc.)
audioFormat: "mp3" # supported formats are: mp3, ogg_opus, ogg_vorbis, aac, wav, and flac. Default format is mp3
youtube:
countryCode: "US" # the country code you want to use for searching lyrics via ISRC. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2

server: # REST and WS server
port: 2333
Expand Down
41 changes: 0 additions & 41 deletions main/build.gradle

This file was deleted.

53 changes: 53 additions & 0 deletions main/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
plugins {
`java-library`
kotlin("jvm")
kotlin("plugin.serialization")
}

base {
archivesName = "lavasrc"
}

java {
withJavadocJar()
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_11
}

dependencies {
api("com.github.topi314.lavasearch:lavasearch:1.0.0")
api("com.github.topi314.lavalyrics:lavalyrics:1.0.0")
compileOnly("dev.arbjerg:lavaplayer:2.0.4")
compileOnly("com.github.lavalink-devs.youtube-source:common:1.0.5")
implementation("org.jsoup:jsoup:1.15.3")
implementation("commons-io:commons-io:2.7")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
implementation("org.jetbrains.kotlin:kotlin-annotations-jvm:1.9.0")
implementation("com.auth0:java-jwt:4.4.0")
compileOnly("org.slf4j:slf4j-api:2.0.7")

lyricsDependency("protocol")
lyricsDependency("client")
}

publishing {
publications {
create<MavenPublication>("maven") {
pom {
artifactId = base.archivesName.get()
from(components["java"])
}
}
}
}

kotlin {
jvmToolchain(11)
}


fun DependencyHandlerScope.lyricsDependency(module: String) {
implementation("dev.schlaubi.lyrics", "$module-jvm", "2.2.2") {
isTransitive = false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public static JsonBrowser fetchResponseAsJson(HttpInterface httpInterface, HttpU
var data = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
log.error("Server responded with not found to '{}': {}", request.getURI(), data);
return null;
} else if (statusCode == HttpStatus.SC_NO_CONTENT) {
log.error("Server responded with not content to '{}'", request.getURI());
return null;
} else if (!HttpClientTools.isSuccessWithContent(statusCode)) {
var data = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
log.error("Server responded with an error to '{}': {}", request.getURI(), data);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.github.topi314.lavasrc.deezer;

import com.github.topi314.lavalyrics.AudioLyricsManager;
import com.github.topi314.lavalyrics.lyrics.AudioLyrics;
import com.github.topi314.lavalyrics.lyrics.BasicAudioLyrics;
import com.github.topi314.lavasearch.AudioSearchManager;
import com.github.topi314.lavasearch.result.AudioSearchResult;
import com.github.topi314.lavasearch.result.BasicAudioSearchResult;
Expand All @@ -15,6 +18,7 @@
import com.sedmelluq.discord.lavaplayer.track.*;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.HttpClientBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand All @@ -25,15 +29,19 @@
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class DeezerAudioSourceManager extends ExtendedAudioSourceManager implements HttpConfigurable, AudioSearchManager {
public class DeezerAudioSourceManager extends ExtendedAudioSourceManager implements HttpConfigurable, AudioSearchManager, AudioLyricsManager {

public static final Pattern URL_PATTERN = Pattern.compile("(https?://)?(www\\.)?deezer\\.com/(?<countrycode>[a-zA-Z]{2}/)?(?<type>track|album|playlist|artist)/(?<identifier>[0-9]+)");
public static final String SEARCH_PREFIX = "dzsearch:";
Expand All @@ -49,6 +57,7 @@ public class DeezerAudioSourceManager extends ExtendedAudioSourceManager impleme

private final String masterDecryptionKey;
private final HttpInterfaceManager httpInterfaceManager;
private Tokens tokens;

public DeezerAudioSourceManager(String masterDecryptionKey) {
if (masterDecryptionKey == null || masterDecryptionKey.isEmpty()) {
Expand All @@ -58,6 +67,43 @@ public DeezerAudioSourceManager(String masterDecryptionKey) {
this.httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager();
}

private void refreshSession() throws IOException {
var getSessionID = new HttpPost(DeezerAudioSourceManager.PRIVATE_API_BASE + "?method=deezer.ping&input=3&api_version=1.0&api_token=");
var json = LavaSrcTools.fetchResponseAsJson(this.getHttpInterface(), getSessionID);

checkResponse(json, "Failed to get session ID: ");
var sessionID = json.get("results").get("SESSION").text();

var getUserToken = new HttpPost(DeezerAudioSourceManager.PRIVATE_API_BASE + "?method=deezer.getUserData&input=3&api_version=1.0&api_token=");
getUserToken.setHeader("Cookie", "sid=" + sessionID);
json = LavaSrcTools.fetchResponseAsJson(this.getHttpInterface(), getUserToken);

checkResponse(json, "Failed to get user token: ");
this.tokens = new Tokens(
json.get("results").get("checkForm").text(),
json.get("results").get("USER").get("OPTIONS").get("license_token").text(),
Instant.now().plus(3600, ChronoUnit.SECONDS)
);
}

public Tokens getTokens() throws IOException {
if (this.tokens == null || Instant.now().isAfter(this.tokens.expireAt)) {
this.refreshSession();
}
return this.tokens;
}

static void checkResponse(JsonBrowser json, String message) throws IllegalStateException {
if (json == null) {
throw new IllegalStateException(message + "No response");
}
var errors = json.get("data").index(0).get("errors").values();
if (!errors.isEmpty()) {
var errorsStr = errors.stream().map(error -> error.get("code").text() + ": " + error.get("message").text()).collect(Collectors.joining(", "));
throw new IllegalStateException(message + errorsStr);
}
}

@NotNull
@Override
public String getSourceName() {
Expand All @@ -78,6 +124,67 @@ public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws
);
}

@Override
@Nullable
public AudioLyrics loadLyrics(@NotNull AudioTrack audioTrack) {
var deezerTackId = "";
if (audioTrack instanceof DeezerAudioTrack) {
deezerTackId = audioTrack.getIdentifier();
}

if (deezerTackId.isEmpty()) {
AudioItem item = AudioReference.NO_TRACK;
try {
if (audioTrack.getInfo().isrc != null && !audioTrack.getInfo().isrc.isEmpty()) {
item = this.getTrackByISRC(audioTrack.getInfo().isrc, false);
}
if (item == AudioReference.NO_TRACK) {
item = this.getSearch(String.format("%s %s", audioTrack.getInfo().title, audioTrack.getInfo().author), false);
}
} catch (IOException e) {
throw new RuntimeException(e);
}

if (item == AudioReference.NO_TRACK) {
return null;
}
if (item instanceof AudioTrack) {
deezerTackId = ((AudioTrack) item).getIdentifier();
} else if (item instanceof AudioPlaylist) {
var playlist = (AudioPlaylist) item;
if (!playlist.getTracks().isEmpty()) {
deezerTackId = playlist.getTracks().get(0).getIdentifier();
}
}
}

try {
return this.getLyrics(deezerTackId);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public AudioLyrics getLyrics(String id) throws IOException {
var json = this.getJson(PRIVATE_API_BASE + "?method=song.getLyrics&api_version=1.0&api_token=" + this.getTokens().api + "&sng_id=" + id);
if (json == null || json.get("results").values().isEmpty()) {
return null;
}

var results = json.get("results");
var lyricsText = results.get("LYRICS_TEXT").text();
var lyrics = new ArrayList<AudioLyrics.Line>();
for (var line : results.get("LYRICS_SYNC_JSON").values()) {
lyrics.add(new BasicAudioLyrics.BasicLine(
Duration.ofMillis(line.get("milliseconds").asLong(0)),
Duration.ofMillis(line.get("duration").asLong(0)),
line.get("line").text()
));
}

return new BasicAudioLyrics("deezer", "LyricFind", lyricsText, lyrics);
}

@Override
@Nullable
public AudioSearchResult loadSearch(@NotNull String query, @NotNull Set<AudioSearchResult.Type> types) {
Expand Down Expand Up @@ -376,4 +483,16 @@ public HttpInterface getHttpInterface() {
return this.httpInterfaceManager.getInterface();
}

public static class Tokens {
public String api;
public String license;
public Instant expireAt;

public Tokens(String api, String license, Instant expireAt) {
this.api = api;
this.license = license;
this.expireAt = expireAt;
}
}

}
Loading
Loading