Skip to content

Commit

Permalink
LavaLyrics support (#177)
Browse files Browse the repository at this point in the history
  • Loading branch information
topi314 authored May 6, 2024
1 parent 0f0c005 commit 7155441
Show file tree
Hide file tree
Showing 13 changed files with 476 additions and 67 deletions.
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

0 comments on commit 7155441

Please sign in to comment.