Skip to content

Commit

Permalink
move BilibiliAudioSourceManager
Browse files Browse the repository at this point in the history
  • Loading branch information
MagicTeaMC committed Oct 2, 2024
1 parent ce77e12 commit 0278e4e
Show file tree
Hide file tree
Showing 6 changed files with 316 additions and 3 deletions.
7 changes: 5 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
group = "tw.maoyue"
version = "1.2.0"
description = "My Discord music bot, base on JMusicBot"
java.sourceCompatibility = JavaVersion.VERSION_11

plugins {
`java-library`
`maven-publish`
id("com.github.johnrengelman.shadow") version "8.1.1"
kotlin("jvm")
}

repositories {
Expand Down Expand Up @@ -48,9 +48,9 @@ dependencies {
api("org.slf4j:slf4j-nop:2.0.16")
api("org.slf4j:slf4j-api:2.0.16")
api("me.scarsz.jdaappender:jda5:1.2.2")
api(files("./bin/main-0.1.0.jar"))
testImplementation("junit:junit:4.13.2")
testImplementation("org.hamcrest:hamcrest-core:3.0")
implementation(kotlin("stdlib-jdk8"))
}

publishing {
Expand Down Expand Up @@ -87,4 +87,7 @@ tasks.jar {
"Implementation-Vendor-Id" to project.group
)
}
}
kotlin {
jvmToolchain(11)
}
8 changes: 8 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
pluginManagement {
plugins {
kotlin("jvm") version "2.0.0"
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
/*
* This file was generated by the Gradle 'init' task.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@
import dev.lavalink.youtube.YoutubeAudioSourceManager;
import dev.lavalink.youtube.clients.*;
import dev.lavalink.youtube.clients.skeleton.Client;
import me.allvaa.lpsources.bilibili.BilibiliAudioSourceManager;
import net.dv8tion.jda.api.entities.Guild;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tw.maoyue.lavabilibili.BilibiliAudioSourceManager;
import tw.maoyue.lavaodysee.OdyseeAudioSourceManager;

import java.io.IOException;
Expand Down
209 changes: 209 additions & 0 deletions src/main/java/tw/maoyue/lavabilibili/BilibiliAudioSourceManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package tw.maoyue.lavabilibili

import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager
import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools
import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser
import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools
import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface
import com.sedmelluq.discord.lavaplayer.track.*
import org.apache.http.client.methods.HttpGet
import java.io.DataInput
import java.io.DataOutput

class BilibiliAudioSourceManager : AudioSourceManager {
val httpInterface: HttpInterface
private var playlistPageCountConfig: Int = -1

init {
val httpInterfacaManager = HttpClientTools.createDefaultThreadLocalManager()
httpInterfacaManager.setHttpContextFilter(BilibiliHttpContextFilter())
httpInterface = httpInterfacaManager.`interface`
}

override fun getSourceName(): String {
return "bilibili"
}

override fun loadItem(manager: AudioPlayerManager, reference: AudioReference): AudioItem? {
val matcher = URL_PATTERN.matcher(reference.identifier)
if (matcher.find()) {
when (matcher.group("type")) {
"video" -> {
val bvid = matcher.group("id")
val page = (matcher.group("page")?.toInt() ?: 1) - 1

val response = httpInterface.execute(HttpGet("${BASE_URL}x/web-interface/view?bvid=$bvid"))
val responseJson = JsonBrowser.parse(response.entity.content)

val statusCode = responseJson.get("code").`as`(Int::class.java)
if (statusCode != 0) {
return AudioReference.NO_TRACK
}

val trackData = responseJson.get("data")
return if (trackData.get("pages").values().size > 1) {
loadVideoAnthology(trackData, page)
} else {
loadVideo(trackData)
}
}
"audio" -> {
val type = when (matcher.group("audioType")) {
"am" -> "menu"
"au" -> "song"
else -> return AudioReference.NO_TRACK
}
val sid = matcher.group("audioId")

val response = httpInterface.execute(HttpGet("${BASE_URL}audio/music-service-c/web/$type/info?sid=$sid"))
val responseJson = JsonBrowser.parse(response.entity.content)

val statusCode = responseJson.get("code").`as`(Int::class.java)
if (statusCode != 0) {
return AudioReference.NO_TRACK
}

return when (type) {
"song" -> loadAudio(responseJson.get("data"))
"menu" -> loadAudioPlaylist(responseJson.get("data"))
else -> AudioReference.NO_TRACK
}
}
}
}
return null
}

fun setPlaylistPageCount(count: Int): BilibiliAudioSourceManager {
playlistPageCountConfig = count
return this
}

private fun loadVideo(trackData: JsonBrowser): AudioTrack {
val bvid = trackData.get("bvid").`as`(String::class.java)

return BilibiliAudioTrack(
AudioTrackInfo(
trackData.get("title").`as`(String::class.java),
trackData.get("owner").get("name").`as`(String::class.java),
trackData.get("duration").asLong(0) * 1000,
bvid,
false,
getVideoURL(bvid)
),
BilibiliAudioTrack.TrackType.VIDEO,
bvid,
trackData.get("cid").asLong(0),
this
)
}

private fun loadVideoAnthology(trackData: JsonBrowser, page: Int): AudioPlaylist {
val playlistName = trackData.get("title").`as`(String::class.java)
val author = trackData.get("owner").get("name").`as`(String::class.java)
val bvid = trackData.get("bvid").`as`(String::class.java)

val tracks = ArrayList<AudioTrack>()

for (item in trackData.get("pages").values()) {
tracks.add(BilibiliAudioTrack(
AudioTrackInfo(
item.get("part").`as`(String::class.java),
author,
item.get("duration").asLong(0) * 1000,
bvid,
false,
getVideoURL(bvid, item.get("page").`as`(Int::class.java))
),
BilibiliAudioTrack.TrackType.VIDEO,
bvid,
item.get("cid").asLong(0),
this
))
}

return BasicAudioPlaylist(playlistName, tracks, tracks[page], false)
}

private fun loadAudio(trackData: JsonBrowser): AudioTrack {
val sid = trackData.get("statistic").get("sid").asLong(0).toString()

return BilibiliAudioTrack(
AudioTrackInfo(
trackData.get("title").`as`(String::class.java),
trackData.get("uname").`as`(String::class.java),
trackData.get("duration").asLong(0) * 1000,
"au$sid",
false,
getAudioURL("au$sid")
),
BilibiliAudioTrack.TrackType.AUDIO,
sid,
null,
this
)
}

private fun loadAudioPlaylist(playlistData: JsonBrowser): AudioPlaylist {
val playlistName = playlistData.get("title").`as`(String::class.java)
val sid = playlistData.get("statistic").get("sid").asLong(0).toString()

val response = httpInterface.execute(HttpGet("${BASE_URL}audio/music-service-c/web/song/of-menu?sid=$sid&pn=1&ps=100"))
val responseJson = JsonBrowser.parse(response.entity.content)

val tracksData = responseJson.get("data").get("data").values()
val tracks = ArrayList<AudioTrack>()

var curPage = responseJson.get("data").get("curPage").`as`(Int::class.java)
val pageCount = responseJson.get("data").get("pageCount").`as`(Int::class.java).let {
if (playlistPageCountConfig == -1) it
else if (it <= playlistPageCountConfig) it
else playlistPageCountConfig
}

while (curPage <= pageCount) {
val responsePage = httpInterface.execute(HttpGet("${BASE_URL}audio/music-service-c/web/song/of-menu?sid=$sid&pn=${++curPage}&ps=100"))
val responseJsonPage = JsonBrowser.parse(responsePage.entity.content)
tracksData.addAll(responseJsonPage.get("data").get("data").values())
}

for (track in tracksData) {
tracks.add(loadAudio(track))
}

return BasicAudioPlaylist(playlistName, tracks, null, false)
}

override fun isTrackEncodable(track: AudioTrack): Boolean {
return true
}

override fun encodeTrack(track: AudioTrack, output: DataOutput) {
track as BilibiliAudioTrack
DataFormatTools.writeNullableText(output, track.type.toString())
DataFormatTools.writeNullableText(output, track.id)
DataFormatTools.writeNullableText(output, track.cid.toString())
}

override fun decodeTrack(trackInfo: AudioTrackInfo, input: DataInput): AudioTrack {
return BilibiliAudioTrack(trackInfo, DataFormatTools.readNullableText(input).toInt() as BilibiliAudioTrack.TrackType, DataFormatTools.readNullableText(input), DataFormatTools.readNullableText(input).toLong(), this)
}

override fun shutdown() {
//
}

companion object {
const val BASE_URL = "https://api.bilibili.com/"
private val URL_PATTERN = Regex("^https?:\\/\\/(?:(?:www|m)\\.)?bilibili\\.com\\/(?<type>video|audio)\\/(?<id>(?:(?<audioType>am|au)?(?<audioId>[0-9]+))|[A-Za-z0-9]+)\\/?(?:(?:\\?p=(?<page>[\\d]+)(?:&.+)?)?|(?:\\?.*)?)\$").toPattern()

private fun getVideoURL(id: String, page: Int? = null): String {
return "https://www.bilibili.com/video/$id${if (page != null) "?p=$page" else ""}"
}

private fun getAudioURL(id: String): String {
return "https://www.bilibili.com/audio/$id"
}
}
}
60 changes: 60 additions & 0 deletions src/main/java/tw/maoyue/lavabilibili/BilibiliAudioTrack.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package tw.maoyue.lavabilibili

import com.sedmelluq.discord.lavaplayer.container.mpeg.MpegAudioTrack
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager
import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser
import com.sedmelluq.discord.lavaplayer.tools.io.PersistentHttpStream
import com.sedmelluq.discord.lavaplayer.track.AudioTrack
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo
import com.sedmelluq.discord.lavaplayer.track.DelegatedAudioTrack
import com.sedmelluq.discord.lavaplayer.track.playback.LocalAudioTrackExecutor
import org.apache.http.client.methods.HttpGet
import tw.maoyue.lavabilibili.BilibiliAudioSourceManager.Companion.BASE_URL
import java.net.URI

class BilibiliAudioTrack(audioTrackInfo: AudioTrackInfo, val type: TrackType, val /*can be bvid or sid*/ id: String, val cid: Long?, private val sourceManager: BilibiliAudioSourceManager) : DelegatedAudioTrack(audioTrackInfo) {
override fun process(executor: LocalAudioTrackExecutor) {
val stream = PersistentHttpStream(sourceManager.httpInterface, URI(getPlaybackURL()), null)
processDelegate(MpegAudioTrack(trackInfo, stream), executor)
}

private fun getPlaybackURL(): String = when (type) {
TrackType.VIDEO -> {
val response = sourceManager.httpInterface.execute(HttpGet("${BASE_URL}x/player/playurl?bvid=$id&cid=$cid&fnval=16"))
val responseJson = JsonBrowser.parse(response.entity.content)

responseJson
.get("data")
.get("dash")
.get("audio")
.values()
// find the highest quality possible
.sortedByDescending {
it.get("id").`as`(Int::class.java)
}[0]
.get("baseUrl").`as`(String::class.java)
}
TrackType.AUDIO -> {
val response = sourceManager.httpInterface.execute(HttpGet("${BASE_URL}audio/music-service-c/web/url?sid=$id&privilege=2&quality=2"))
val responseJson = JsonBrowser.parse(response.entity.content)

responseJson
.get("data")
.get("cdns")
.values()[0].`as`(String::class.java)
}
}

override fun makeShallowClone(): AudioTrack {
return BilibiliAudioTrack(trackInfo, type, id, cid, sourceManager)
}

override fun getSourceManager(): AudioSourceManager {
return sourceManager
}

enum class TrackType {
VIDEO,
AUDIO
}
}
33 changes: 33 additions & 0 deletions src/main/java/tw/maoyue/lavabilibili/BilibiliHttpContextFilter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package tw.maoyue.lavabilibili

import com.sedmelluq.discord.lavaplayer.tools.http.HttpContextFilter
import org.apache.http.HttpResponse
import org.apache.http.client.methods.HttpUriRequest
import org.apache.http.client.protocol.HttpClientContext

class BilibiliHttpContextFilter : HttpContextFilter {
override fun onContextOpen(context: HttpClientContext) {
//
}

override fun onContextClose(context: HttpClientContext) {
//
}

override fun onRequest(context: HttpClientContext, request: HttpUriRequest, isRepetition: Boolean) {
request.setHeader("Referer", "https://www.bilibili.com/")
request.setHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:130.0) Gecko/20100101 Firefox/130.0")
}

override fun onRequestResponse(
context: HttpClientContext,
request: HttpUriRequest,
response: HttpResponse
): Boolean {
return false
}

override fun onRequestException(context: HttpClientContext?, request: HttpUriRequest, error: Throwable): Boolean {
return false
}
}

0 comments on commit 0278e4e

Please sign in to comment.