Skip to content

Commit

Permalink
Add lavalyrics support to YouTube
Browse files Browse the repository at this point in the history
  • Loading branch information
DRSchlaubi committed Jan 6, 2024
1 parent 82a41f7 commit 0a0d94f
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 54 deletions.
41 changes: 0 additions & 41 deletions main/build.gradle

This file was deleted.

52 changes: 52 additions & 0 deletions main/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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:01bf4e7")
compileOnly("dev.arbjerg:lavaplayer:2.0.4")
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
@@ -1,18 +1,24 @@
package com.github.topi314.lavasrc.youtube

import com.github.topi314.lavalyrics.AudioLyricsManager
import com.github.topi314.lavalyrics.lyrics.AudioLyrics

import com.github.topi314.lavasearch.AudioSearchManager
import com.github.topi314.lavasearch.result.AudioSearchResult
import com.github.topi314.lavasearch.result.AudioText
import com.github.topi314.lavasearch.result.BasicAudioSearchResult
import com.github.topi314.lavasearch.result.BasicAudioText
import com.github.topi314.lavasrc.ExtendedAudioPlaylist
import com.github.topi314.lavasrc.youtube.innertube.MusicResponsiveListItemRenderer
import com.github.topi314.lavasrc.youtube.innertube.requestLyrics
import com.github.topi314.lavasrc.youtube.innertube.requestMusicAutoComplete
import com.github.topi314.lavasrc.youtube.innertube.takeFirstSearchResult
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioTrack
import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools
import com.sedmelluq.discord.lavaplayer.track.AudioTrack
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo
import dev.schlaubi.lyrics.LyricsNotFoundException
import org.apache.http.client.methods.HttpGet
import java.net.URLEncoder
import com.github.topi314.lavasrc.youtube.innertube.MusicResponsiveListItemRenderer.NavigationEndpoint.BrowseEndpoint.Configs.Config.Type as PageType
Expand All @@ -29,9 +35,10 @@ private fun MusicResponsiveListItemRenderer.NavigationEndpoint.toUrl() = when {
else -> error("Unknown endpoint: $this")
}

class YoutubeSearchManager(
private val sourceManager: () -> YoutubeAudioSourceManager
) : AudioSearchManager {
class YoutubeSourceManager(
private val sourceManager: () -> YoutubeAudioSourceManager,
private val region: String?
) : AudioSearchManager, AudioLyricsManager {
companion object {
const val SEARCH_PREFIX = "ytsearch:"
const val MUSIC_SEARCH_PREFIX = "ytmsearch:"
Expand All @@ -46,6 +53,20 @@ class YoutubeSearchManager(
private val httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager()
override fun getSourceName(): String = "youtube"

override fun loadLyrics(track: AudioTrack): AudioLyrics? = try {
httpInterfaceManager.`interface`.use {
val videoId = when {
track is YoutubeAudioTrack -> track.info.identifier
track.info.isrc != null -> it.takeFirstSearchResult(track.info.isrc, region)
else -> it.takeFirstSearchResult("${track.info.title} - ${track.info.author}", region)
} ?: return@use null

it.requestLyrics(videoId)
}
} catch (e: LyricsNotFoundException) {
null
}

override fun loadSearch(query: String, types: Set<AudioSearchResult.Type>): AudioSearchResult? {
val result = httpInterfaceManager.`interface`.use {
when {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
package com.github.topi314.lavasrc.youtube.innertube

import com.github.topi314.lavalyrics.lyrics.AudioLyrics
import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface
import dev.schlaubi.lyrics.LyricsNotFoundException
import dev.schlaubi.lyrics.internal.model.*
import dev.schlaubi.lyrics.internal.util.*
import dev.schlaubi.lyrics.protocol.Lyrics
import dev.schlaubi.lyrics.protocol.TextLyrics
import dev.schlaubi.lyrics.protocol.TimedLyrics
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.*
import org.apache.http.HttpHeaders
import org.apache.http.client.methods.HttpPost
import org.apache.http.client.utils.URIBuilder
import org.apache.http.entity.ContentType
import org.apache.http.entity.StringEntity
import java.net.URI
import java.time.Duration
import java.util.*

private val json = Json {
ignoreUnknownKeys = true
}

fun HttpInterface.requestMusicAutoComplete(
internal fun HttpInterface.requestMusicAutoComplete(
input: String,
locale: Locale? = null
): InnerTubeBox<SearchSuggestionsSectionRendererContent> =
Expand All @@ -35,7 +44,61 @@ fun HttpInterface.requestMusicAutoComplete(
}
}

private val emptyTrack = Lyrics.Track("", "", "", emptyList())

@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
internal fun HttpInterface.requestLyrics(videoId: String): AudioLyrics {
val browse =
makeRequest<_, JsonObject>(youtubeMusic, "next", body = NextRequest(mobileYoutubeMusicContext, videoId))
val browseId = browse.browseEndpoint ?: throw LyricsNotFoundException()
val browseResult =
makeRequest<_, JsonObject>(youtubeMusic, "browse", body = BrowseRequest(mobileYoutubeMusicContext, browseId))
val lyricsData = browseResult.lyricsData
val data = if (lyricsData != null) {
val source = lyricsData.source
TimedLyrics(emptyTrack, source, lyricsData.lines)
} else {
val renderer = browseResult.musicDescriptionShelfRenderer ?: notFound()
val text = renderer.getRunningText("description")!!
val source = renderer.getRunningText("footer")!!
TextLyrics(emptyTrack, source, text)
}

return WrappedLyrics(data)
}

@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
internal fun HttpInterface.takeFirstSearchResult(query: String, region: String?): String? {
val result = makeRequest<_, JsonObject>(
youtubeMusic,
"search",
body = SearchRequest(mobileYoutubeMusicContext(region), query, onlyTracksSearchParam)
)
val section = result
.getJsonObject("contents")
?.getJsonObject("tabbedSearchResultsRenderer")
?.getJsonArray("tabs")
?.getJsonObject(0)
?.getJsonObject("tabRenderer")
?.getJsonObject("content")
?.getJsonObject("sectionListRenderer")
?.getJsonArray("contents") ?: JsonArray(emptyList())

return section
.firstNotNullOfOrNull {
it.jsonObject.getJsonObject("musicShelfRenderer")
?.getJsonArray("contents")
?.firstNotNullOfOrNull { content ->
content.jsonObject.getJsonObject("musicTwoColumnItemRenderer")
?.getJsonObject("navigationEndpoint")
?.getJsonObject("watchEndpoint")
?.getString("videoId")
}
}
}


@OptIn(ExperimentalSerializationApi::class)
private inline fun <reified B, reified R> HttpInterface.makeRequest(
domain: URI,
vararg endpoint: String,
Expand All @@ -55,7 +118,29 @@ private inline fun <reified B, reified R> HttpInterface.makeRequest(
}

val response = execute(post)
val jsonText = response.entity.content.buffered().readAllBytes().decodeToString()

return json.decodeFromString(jsonText)
return response.entity.content.buffered().use {
json.decodeFromStream(it)
}
}

private class WrappedLyrics(private val lyrics: Lyrics) : AudioLyrics {
override fun getSourceName(): String = "youtube"

override fun getProvider(): String = lyrics.source

override fun getText(): String = lyrics.text

override fun getLines(): MutableList<AudioLyrics.Line>? = (lyrics as? TimedLyrics)?.lines?.map {
Line(it)
}?.toMutableList()

private class Line(private val line: TimedLyrics.Line) : AudioLyrics.Line {
override fun getTimestamp(): Duration = Duration.ofMillis(line.range.first)

override fun getDuration(): Duration = Duration.ofMillis(line.range.last).minus(timestamp)

override fun getLine(): String = line.line

}
}
10 changes: 8 additions & 2 deletions plugin/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
plugins {
id "dev.arbjerg.lavalink.gradle-plugin" version "1.0.15"
id 'org.jetbrains.kotlin.jvm' version '1.9.0'
}

archivesBaseName = "lavasrc-plugin"
Expand All @@ -10,15 +11,20 @@ lavalinkPlugin {
configurePublishing = false
}

sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17

dependencies {
implementation project(":main")
compileOnly "com.github.topi314.lavasearch:lavasearch:1.0.0"
implementation "com.github.topi314.lavasearch:lavasearch-plugin-api:1.0.0"
compileOnly "com.github.topi314.lavalyrics:lavalyrics:01bf4e7"
implementation "com.github.topi314.lavalyrics:lavalyrics-plugin-api:01bf4e7"

// Copy lyrics.kt from main
project(":main").configurations.implementation.dependencies.forEach {
if (it.group == "dev.schlaubi.lyrics") {
add("implementation", it)
}
}
}

publishing {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import com.github.topi314.lavasrc.mirror.DefaultMirroringAudioTrackResolver;
import com.github.topi314.lavasrc.spotify.SpotifySourceManager;
import com.github.topi314.lavasrc.yandexmusic.YandexMusicSourceManager;
import com.github.topi314.lavasrc.youtube.YoutubeSearchManager;
import com.github.topi314.lavasrc.youtube.YoutubeSourceManager;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager;
import dev.arbjerg.lavalink.api.AudioPlayerManagerConfiguration;
Expand All @@ -31,9 +31,9 @@ public class LavaSrcPlugin implements AudioPlayerManagerConfiguration, SearchMan
private DeezerAudioSourceManager deezer;
private YandexMusicSourceManager yandexMusic;
private FloweryTTSSourceManager flowerytts;
private YoutubeSearchManager youtube;
private YoutubeSourceManager youtube;

public LavaSrcPlugin(LavaSrcConfig pluginConfig, SourcesConfig sourcesConfig, SpotifyConfig spotifyConfig, AppleMusicConfig appleMusicConfig, DeezerConfig deezerConfig, YandexMusicConfig yandexMusicConfig, FloweryTTSConfig floweryTTSConfig) {
public LavaSrcPlugin(LavaSrcConfig pluginConfig, SourcesConfig sourcesConfig, SpotifyConfig spotifyConfig, AppleMusicConfig appleMusicConfig, DeezerConfig deezerConfig, YandexMusicConfig yandexMusicConfig, FloweryTTSConfig floweryTTSConfig, YouTubeConfig youTubeConfig) {
log.info("Loading LavaSrc plugin...");

if (sourcesConfig.isSpotify()) {
Expand Down Expand Up @@ -76,7 +76,7 @@ public LavaSrcPlugin(LavaSrcConfig pluginConfig, SourcesConfig sourcesConfig, Sp
}
}
if (sourcesConfig.isYoutube()) {
this.youtube = new YoutubeSearchManager(() -> youtubeAudioSourceManager);
this.youtube = new YoutubeSourceManager(() -> youtubeAudioSourceManager, youTubeConfig.getCountryCode());
}
}

Expand Down Expand Up @@ -147,6 +147,10 @@ public LyricsManager configure(@NotNull LyricsManager manager) {
log.info("Registering Deezer lyrics manager...");
manager.registerLyricsManager(this.deezer);
}
if (this.youtube != null) {
log.info("Registering YouTube lyrics manager...");
manager.registerLyricsManager(this.youtube);
}
return manager;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.github.topi314.lavasrc.plugin;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@ConfigurationProperties(prefix = "plugins.lavasrc.youtube")
@Component
public class YouTubeConfig {

private String countryCode;

public String getCountryCode() {
return countryCode;
}

public void setCountryCode(String countryCode) {
this.countryCode = countryCode;
}
}

0 comments on commit 0a0d94f

Please sign in to comment.