Merge branch 'how_to_remove_music' into 'master'
Remove all music related stuff See merge request jdejeu/ottobotv2!12
This commit is contained in:
commit
891ae1ae0b
|
@ -1,21 +1,14 @@
|
|||
package nl.voidcorp.discord
|
||||
|
||||
import com.sedmelluq.discord.lavaplayer.source.bandcamp.BandcampAudioSourceManager
|
||||
import com.sedmelluq.discord.lavaplayer.source.beam.BeamAudioSourceManager
|
||||
import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager
|
||||
import com.sedmelluq.discord.lavaplayer.source.twitch.TwitchStreamAudioSourceManager
|
||||
import com.sedmelluq.discord.lavaplayer.source.vimeo.VimeoAudioSourceManager
|
||||
import net.dv8tion.jda.api.entities.Activity
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter
|
||||
import net.dv8tion.jda.api.sharding.DefaultShardManagerBuilder
|
||||
import nl.voidcorp.discord.music.CustomYoutubeSourceManager
|
||||
import nl.voidcorp.discord.music.PlayerManager
|
||||
import nl.voidcorp.discord.storage.ConfigStore
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
|
||||
@Service
|
||||
class Loader(listeners: List<ListenerAdapter>, playerManager: PlayerManager, store: ConfigStore) {
|
||||
class Loader(listeners: List<ListenerAdapter>, store: ConfigStore) {
|
||||
init {
|
||||
val token = System.getenv("DISCORD_TOKEN") ?: throw RuntimeException("'DISCORD_TOKEN' not set!")
|
||||
val builder = DefaultShardManagerBuilder(token)
|
||||
|
@ -27,12 +20,5 @@ class Loader(listeners: List<ListenerAdapter>, playerManager: PlayerManager, sto
|
|||
jda.setActivityProvider {
|
||||
Activity.playing("v${store.version} ($it)")
|
||||
}
|
||||
|
||||
playerManager.registerSourceManager(SoundCloudAudioSourceManager())
|
||||
playerManager.registerSourceManager(BandcampAudioSourceManager())
|
||||
playerManager.registerSourceManager(VimeoAudioSourceManager())
|
||||
playerManager.registerSourceManager(TwitchStreamAudioSourceManager())
|
||||
playerManager.registerSourceManager(BeamAudioSourceManager())
|
||||
playerManager.registerSourceManager(CustomYoutubeSourceManager())
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package nl.voidcorp.discord.commands.music
|
||||
|
||||
import nl.voidcorp.discord.command.*
|
||||
import nl.voidcorp.discord.music.PlayerManager
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class ForceLeave(val playerManager: PlayerManager) : Command(
|
||||
"forceleave",
|
||||
group = CommandGroup.MUSIC,
|
||||
location = CommandSource.GUILD,
|
||||
commandLevel = CommandLevel.MODERATOR
|
||||
) {
|
||||
override fun handle(event: CommandMessage): CommandResult {
|
||||
event.guild!!.audioManager.closeAudioConnection()
|
||||
playerManager.delGuildPlayer(event.guild)
|
||||
return CommandResult.SUCCESS
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package nl.voidcorp.discord.commands.music
|
||||
|
||||
import nl.voidcorp.discord.command.*
|
||||
import nl.voidcorp.discord.music.PlayerManager
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class Loop(val playerManager: PlayerManager) :
|
||||
Command("loop", location = CommandSource.GUILD, group = CommandGroup.MUSIC) {
|
||||
override fun handle(event: CommandMessage): CommandResult {
|
||||
val loop = playerManager.getGuildPlayer(event.guild!!).loop()
|
||||
if (loop){
|
||||
event.reply("Now looping!")
|
||||
}else{
|
||||
event.reply("No longer looping...")
|
||||
}
|
||||
return CommandResult.SUCCESS
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package nl.voidcorp.discord.commands.music
|
||||
|
||||
import nl.voidcorp.discord.command.*
|
||||
import nl.voidcorp.discord.music.AudioLoadHandler
|
||||
import nl.voidcorp.discord.music.PlayerManager
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class Play(val playerManager: PlayerManager) :
|
||||
Command("play", location = CommandSource.GUILD, group = CommandGroup.MUSIC, usage = "play song url (or song name prepended with ytsearch:)") {
|
||||
override fun handle(event: CommandMessage): CommandResult {
|
||||
val chan = event.member!!.voiceState!!.channel
|
||||
if (chan == null) {
|
||||
event.reply("Please join a voice channel to play music!")
|
||||
return CommandResult.SUCCESS
|
||||
} else if (event.params.drop(1).isEmpty()) {
|
||||
event.reply("I'm going to need a url or a search term to actually find a song...")
|
||||
return CommandResult.PARAMETERS
|
||||
}
|
||||
|
||||
val am = event.guild!!.audioManager
|
||||
|
||||
|
||||
val ts = playerManager.getGuildPlayer(event.guild)
|
||||
|
||||
if (!ts.playing) {
|
||||
am.openAudioConnection(chan)
|
||||
}
|
||||
|
||||
playerManager.loadItem(event.params.drop(1).joinToString(" "), AudioLoadHandler(ts))
|
||||
|
||||
|
||||
return CommandResult.SUCCESS
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package nl.voidcorp.discord.commands.music
|
||||
|
||||
import nl.voidcorp.discord.command.*
|
||||
import nl.voidcorp.discord.music.PlayerManager
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class PlaylistCommand(val playerManager: PlayerManager) :
|
||||
Command("playlist", location = CommandSource.GUILD, group = CommandGroup.MUSIC) {
|
||||
override fun handle(event: CommandMessage): CommandResult {
|
||||
val player = playerManager.getGuildPlayer(event.guild!!)
|
||||
val list = player.totalList.take(10)
|
||||
.mapIndexed { index, audioTrack -> "${index + 1} - ${audioTrack.info.title}" }
|
||||
.joinToString("\n")
|
||||
if (list.isNotBlank())
|
||||
event.reply(list)
|
||||
else
|
||||
event.reply("The playlist is still empty...")
|
||||
return CommandResult.SUCCESS
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package nl.voidcorp.discord.commands.music
|
||||
|
||||
import nl.voidcorp.discord.command.*
|
||||
import nl.voidcorp.discord.music.PlayerManager
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class Skip(val playerManager: PlayerManager) :
|
||||
Command("skip", location = CommandSource.GUILD, group = CommandGroup.MUSIC) {
|
||||
override fun handle(event: CommandMessage): CommandResult {
|
||||
playerManager.getGuildPlayer(event.guild!!).skip()
|
||||
return CommandResult.SUCCESS
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package nl.voidcorp.discord.music
|
||||
|
||||
import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler
|
||||
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack
|
||||
|
||||
|
||||
class AudioLoadHandler(private val trackScheduler: TrackScheduler) : AudioLoadResultHandler {
|
||||
|
||||
override fun loadFailed(exception: FriendlyException) {
|
||||
throw exception
|
||||
}
|
||||
|
||||
override fun trackLoaded(track: AudioTrack) {
|
||||
trackScheduler.queue(track)
|
||||
}
|
||||
|
||||
override fun noMatches() {
|
||||
|
||||
}
|
||||
|
||||
override fun playlistLoaded(playlist: AudioPlaylist) {
|
||||
if (playlist.isSearchResult) {
|
||||
trackScheduler.queue(playlist.selectedTrack ?: playlist.tracks.first())
|
||||
} else
|
||||
for (t in playlist.tracks) {
|
||||
trackScheduler.queue(t)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package nl.voidcorp.discord.music
|
||||
|
||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer
|
||||
import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame
|
||||
import net.dv8tion.jda.api.audio.AudioSendHandler
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
|
||||
class AudioPlayerSendHandler(private val audioPlayer: AudioPlayer) : AudioSendHandler {
|
||||
private var lastFrame: AudioFrame? = null
|
||||
|
||||
override fun canProvide(): Boolean {
|
||||
lastFrame = audioPlayer.provide()
|
||||
return lastFrame != null
|
||||
}
|
||||
|
||||
override fun provide20MsAudio() = ByteBuffer.wrap(lastFrame!!.data)
|
||||
|
||||
override fun isOpus(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,428 +0,0 @@
|
|||
package nl.voidcorp.discord.music
|
||||
|
||||
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager
|
||||
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager
|
||||
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager
|
||||
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioTrack
|
||||
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeMixProvider
|
||||
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeSearchProvider
|
||||
import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools
|
||||
import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools.convertToMapLayout
|
||||
import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools
|
||||
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException
|
||||
import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser
|
||||
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 com.sedmelluq.discord.lavaplayer.track.*
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.http.client.config.RequestConfig
|
||||
import org.apache.http.client.methods.HttpGet
|
||||
import org.apache.http.client.utils.URIBuilder
|
||||
import org.apache.http.client.utils.URLEncodedUtils
|
||||
import org.apache.http.impl.client.HttpClientBuilder
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.DataInput
|
||||
import java.io.DataOutput
|
||||
import java.io.IOException
|
||||
import java.net.URISyntaxException
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
import java.util.function.Consumer
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* Audio source manager that implements finding Youtube videos or playlists based on an URL or ID.
|
||||
*/
|
||||
class CustomYoutubeSourceManager : YoutubeAudioSourceManager(), AudioSourceManager, HttpConfigurable {
|
||||
|
||||
private val extractors = arrayOf(
|
||||
Extractor(directVideoIdPattern) { id -> loadTrackWithVideoId(id, false) },
|
||||
Extractor(
|
||||
Pattern.compile("^$PLAYLIST_ID_REGEX$")
|
||||
) { id -> loadPlaylistWithId(id, null) },
|
||||
Extractor(
|
||||
Pattern.compile("^$PROTOCOL_REGEX$DOMAIN_REGEX/.*")
|
||||
) {
|
||||
println(it)
|
||||
this.loadFromMainDomain(it) },
|
||||
Extractor(
|
||||
Pattern.compile("^$PROTOCOL_REGEX$SHORT_DOMAIN_REGEX/.*")
|
||||
) { this.loadFromShortDomain(it) }
|
||||
)
|
||||
|
||||
private val httpInterfaceManager: HttpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager()
|
||||
private val searchProvider: YoutubeSearchProvider = YoutubeSearchProvider(this)
|
||||
private val mixProvider: YoutubeMixProvider = YoutubeMixProvider(this)
|
||||
|
||||
|
||||
override fun getSourceName(): String {
|
||||
return "youtube"
|
||||
}
|
||||
|
||||
override fun loadItem(manager: DefaultAudioPlayerManager, reference: AudioReference): AudioItem? {
|
||||
return try {
|
||||
loadItemOnce(reference)
|
||||
} catch (exception: FriendlyException) {
|
||||
// In case of a connection reset exception, try once more.
|
||||
if (HttpClientTools.isRetriableNetworkException(exception.cause)) {
|
||||
loadItemOnce(reference)
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun loadFromShortDomain(identifier: String): AudioItem {
|
||||
val urlInfo = getUrlInfo(identifier, true)
|
||||
return loadFromUrlWithVideoId(urlInfo.path.substring(1), urlInfo)
|
||||
}
|
||||
|
||||
private fun loadFromMainDomain(identifier: String): AudioItem? {
|
||||
val urlInfo = getUrlInfo(identifier, true)
|
||||
println(urlInfo.parameters)
|
||||
if ("/watch" == urlInfo.path) {
|
||||
val videoId = urlInfo.parameters["v"]
|
||||
|
||||
if (videoId != null) {
|
||||
return loadFromUrlWithVideoId(videoId, urlInfo)
|
||||
}
|
||||
} else if ("/playlist" == urlInfo.path) {
|
||||
val playlistId = urlInfo.parameters["list"]
|
||||
|
||||
if (playlistId != null) {
|
||||
return loadPlaylistWithId(playlistId, null)
|
||||
}
|
||||
} else if ("/watch_videos" == urlInfo.path) {
|
||||
val videoIds = urlInfo.parameters["video_ids"]
|
||||
if (videoIds != null) {
|
||||
return loadAnonymous(videoIds)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
override fun isTrackEncodable(track: AudioTrack): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun encodeTrack(track: AudioTrack, output: DataOutput) {
|
||||
// No custom values that need saving
|
||||
}
|
||||
|
||||
override fun decodeTrack(trackInfo: AudioTrackInfo, input: DataInput): AudioTrack {
|
||||
return YoutubeAudioTrack(trackInfo, this)
|
||||
}
|
||||
|
||||
override fun shutdown() {
|
||||
ExceptionTools.closeWithWarnings(httpInterfaceManager)
|
||||
|
||||
mixProvider.shutdown()
|
||||
}
|
||||
|
||||
override fun configureRequests(configurator: java.util.function.Function<RequestConfig, RequestConfig>) {
|
||||
httpInterfaceManager.configureRequests(configurator)
|
||||
searchProvider.configureRequests(configurator)
|
||||
}
|
||||
|
||||
override fun configureBuilder(configurator: Consumer<HttpClientBuilder>) {
|
||||
httpInterfaceManager.configureBuilder(configurator)
|
||||
searchProvider.configureBuilder(configurator)
|
||||
}
|
||||
|
||||
private fun loadItemOnce(reference: AudioReference): AudioItem? {
|
||||
return loadNonSearch(reference.identifier) ?: searchProvider.loadSearchResult(
|
||||
reference.identifier.trim())
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun loadAnonymous(videoIds: String): AudioItem? {
|
||||
try {
|
||||
httpInterface.use { httpInterface ->
|
||||
httpInterface.execute(HttpGet("https://www.youtube.com/watch_videos?video_ids=$videoIds"))
|
||||
.use { response ->
|
||||
val statusCode = response.statusLine.statusCode
|
||||
val context = httpInterface.context
|
||||
if (statusCode != 200) {
|
||||
throw IOException("Invalid status code for playlist response: $statusCode")
|
||||
}
|
||||
// youtube currently transforms watch_video links into a link with a video id and a list id.
|
||||
// because thats what happens, we can simply re-process with the redirected link
|
||||
val redirects = context.redirectLocations
|
||||
return if (redirects != null && !redirects.isEmpty()) {
|
||||
loadNonSearch(redirects[0].toString())
|
||||
} else {
|
||||
throw FriendlyException(
|
||||
"Unable to process youtube watch_videos link", FriendlyException.Severity.SUSPICIOUS,
|
||||
IllegalStateException("Expected youtube to redirect watch_videos link to a watch?v={id}&list={list_id} link, but it did not redirect at all")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw ExceptionTools.wrapUnfriendlyExceptions(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun loadFromUrlWithVideoId(videoId: String, urlInfo: UrlInfo): AudioItem {
|
||||
var videoId = videoId
|
||||
if (videoId.length > 11) {
|
||||
// YouTube allows extra junk in the end, it redirects to the correct video.
|
||||
videoId = videoId.substring(0, 11)
|
||||
}
|
||||
|
||||
if (!directVideoIdPattern.matcher(videoId).matches()) {
|
||||
return AudioReference.NO_TRACK
|
||||
} else if (urlInfo.parameters.containsKey("list")) {
|
||||
val playlistId = urlInfo.parameters["list"]
|
||||
|
||||
return if (playlistId!!.startsWith("RD")) {
|
||||
mixProvider.loadMixWithId(playlistId, videoId)
|
||||
} else {
|
||||
loadLinkedPlaylistWithId(urlInfo.parameters.getValue("list"), videoId)
|
||||
}
|
||||
} else {
|
||||
return loadTrackWithVideoId(videoId, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadNonSearch(identifier: String): AudioItem? {
|
||||
println(identifier)
|
||||
for (extractor in extractors) {
|
||||
if (extractor.pattern.matcher(identifier).matches()) {
|
||||
val item = extractor.loader(identifier)
|
||||
|
||||
if (item != null) {
|
||||
return item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun loadLinkedPlaylistWithId(playlistId: String, videoId: String): AudioItem {
|
||||
val playlist = loadPlaylistWithId(playlistId, videoId)
|
||||
|
||||
return playlist ?: loadTrackWithVideoId(videoId, false)
|
||||
}
|
||||
|
||||
private fun determineFailureReasonFromStatus(status: String?, reason: String?, mustExist: Boolean): Boolean {
|
||||
if ("fail" == status) {
|
||||
if (("This video does not exist." == reason || "This video is unavailable." == reason) && !mustExist) {
|
||||
return true
|
||||
} else if (reason != null) {
|
||||
throw FriendlyException(reason, FriendlyException.Severity.COMMON, null)
|
||||
}
|
||||
} else if ("ok" == status) {
|
||||
return false
|
||||
}
|
||||
|
||||
throw FriendlyException(
|
||||
"Track is unavailable for an unknown reason.", FriendlyException.Severity.SUSPICIOUS,
|
||||
IllegalStateException("Main page had no video, but video info has no error.")
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun getTrackInfoFromEmbedPage(httpInterface: HttpInterface, videoId: String): JsonBrowser {
|
||||
val basicInfo = loadTrackBaseInfoFromEmbedPage(httpInterface, videoId)
|
||||
basicInfo.put("args", loadTrackArgsFromVideoInfoPage(httpInterface, videoId, basicInfo.get("sts").text()))
|
||||
return basicInfo
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun loadTrackBaseInfoFromEmbedPage(httpInterface: HttpInterface, videoId: String): JsonBrowser {
|
||||
httpInterface.execute(HttpGet("https://www.youtube.com/embed/$videoId")).use { response ->
|
||||
val statusCode = response.statusLine.statusCode
|
||||
if (statusCode != 200) {
|
||||
throw IOException("Invalid status code for embed video page response: $statusCode")
|
||||
}
|
||||
|
||||
val html = IOUtils.toString(response.entity.content, Charset.forName(CHARSET))
|
||||
val configJson = DataFormatTools.extractBetween(html, "'PLAYER_CONFIG': ", "});writeEmbed();")
|
||||
|
||||
if (configJson != null) {
|
||||
return JsonBrowser.parse(configJson)
|
||||
}
|
||||
}
|
||||
|
||||
throw FriendlyException(
|
||||
"Track information is unavailable.", FriendlyException.Severity.SUSPICIOUS,
|
||||
IllegalStateException("Expected player config is not present in embed page.")
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun loadTrackArgsFromVideoInfoPage(
|
||||
httpInterface: HttpInterface,
|
||||
videoId: String,
|
||||
sts: String
|
||||
): Map<String, String> {
|
||||
val videoApiUrl = "https://youtube.googleapis.com/v/$videoId"
|
||||
val encodedApiUrl = URLEncoder.encode(videoApiUrl, CHARSET)
|
||||
val url =
|
||||
"https://www.youtube.com/get_video_info?sts=" + sts + "&video_id=" + videoId + "&eurl=" + encodedApiUrl +
|
||||
"hl=en_GB"
|
||||
|
||||
httpInterface.execute(HttpGet(url)).use { response ->
|
||||
val statusCode = response.statusLine.statusCode
|
||||
if (statusCode != 200) {
|
||||
throw IOException("Invalid status code for video info response: $statusCode")
|
||||
}
|
||||
|
||||
return convertToMapLayout(URLEncodedUtils.parse(response.entity))
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPlaylistWithId(playlistId: String, selectedVideoId: String?): AudioPlaylist? {
|
||||
log.debug("Starting to load playlist with ID {}", playlistId)
|
||||
|
||||
try {
|
||||
httpInterface.use { httpInterface ->
|
||||
httpInterface.execute(HttpGet("https://www.youtube.com/playlist?list=$playlistId")).use { response ->
|
||||
val statusCode = response.statusLine.statusCode
|
||||
if (statusCode != 200) {
|
||||
throw IOException("Invalid status code for playlist response: $statusCode")
|
||||
}
|
||||
|
||||
val document = Jsoup.parse(response.entity.content, CHARSET, "")
|
||||
return buildPlaylist(httpInterface, document, selectedVideoId)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
throw ExceptionTools.wrapUnfriendlyExceptions(e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun buildPlaylist(
|
||||
httpInterface: HttpInterface,
|
||||
document: Document,
|
||||
selectedVideoId: String?
|
||||
): AudioPlaylist? {
|
||||
val isAccessible = !document.select("#pl-header").isEmpty()
|
||||
|
||||
if (!isAccessible) {
|
||||
return if (selectedVideoId != null) {
|
||||
null
|
||||
} else {
|
||||
throw FriendlyException("The playlist is private.", FriendlyException.Severity.COMMON, null)
|
||||
}
|
||||
}
|
||||
|
||||
val container = document.select("#pl-header").first().parent()
|
||||
|
||||
val playlistName = container.select(".pl-header-title").first().text()
|
||||
|
||||
val tracks = ArrayList<AudioTrack>()
|
||||
var loadMoreUrl = extractPlaylistTracks(container, container, tracks)
|
||||
var loadCount = 0
|
||||
val pageCount = 6
|
||||
|
||||
// Also load the next pages, each result gives us a JSON with separate values for list html and next page loader html
|
||||
while (loadMoreUrl != null && ++loadCount < pageCount) {
|
||||
httpInterface.execute(HttpGet("https://www.youtube.com$loadMoreUrl")).use { response ->
|
||||
val statusCode = response.statusLine.statusCode
|
||||
if (statusCode != 200) {
|
||||
throw IOException("Invalid status code for playlist response: $statusCode")
|
||||
}
|
||||
|
||||
val json = JsonBrowser.parse(response.entity.content)
|
||||
|
||||
val html = json.get("content_html").text()
|
||||
val videoContainer = Jsoup.parse("<table>$html</table>", "")
|
||||
|
||||
val moreHtml = json.get("load_more_widget_html").text()
|
||||
val moreContainer = if (moreHtml != null) Jsoup.parse(moreHtml) else null
|
||||
|
||||
loadMoreUrl = extractPlaylistTracks(videoContainer, moreContainer, tracks)
|
||||
}
|
||||
}
|
||||
|
||||
return BasicAudioPlaylist(playlistName, tracks, findSelectedTrack(tracks, selectedVideoId), false)
|
||||
}
|
||||
|
||||
private fun extractPlaylistTracks(
|
||||
videoContainer: Element,
|
||||
loadMoreContainer: Element?,
|
||||
tracks: MutableList<AudioTrack>
|
||||
): String? {
|
||||
for (video in videoContainer.select(".pl-video")) {
|
||||
val lengthElements = video.select(".timestamp span")
|
||||
|
||||
// If the timestamp element does not exist, it means the video is private
|
||||
if (!lengthElements.isEmpty()) {
|
||||
val videoId = video.attr("data-video-id").trim { it <= ' ' }
|
||||
val title = video.attr("data-title").trim { it <= ' ' }
|
||||
val author = video.select(".pl-video-owner a").text().trim { it <= ' ' }
|
||||
val duration = DataFormatTools.durationTextToMillis(lengthElements.first().text())
|
||||
|
||||
tracks.add(buildTrackObject(videoId, title, author, false, duration))
|
||||
}
|
||||
}
|
||||
|
||||
if (loadMoreContainer != null) {
|
||||
val more = loadMoreContainer.select(".load-more-button")
|
||||
if (!more.isEmpty()) {
|
||||
return more.first().attr("data-uix-load-more-href")
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
private class UrlInfo constructor(val path: String, val parameters: Map<String, String>)
|
||||
|
||||
private class Extractor constructor(
|
||||
val pattern: Pattern,
|
||||
val loader: (String) -> AudioItem?
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(YoutubeAudioSourceManager::class.java)
|
||||
internal val CHARSET = "UTF-8"
|
||||
|
||||
private const val PROTOCOL_REGEX = "(?:http://|https://|)"
|
||||
private const val DOMAIN_REGEX = "(?:www\\.|m\\.|music\\.|)youtube\\.com"
|
||||
private const val SHORT_DOMAIN_REGEX = "(?:www\\.|)youtu\\.be"
|
||||
private const val VIDEO_ID_REGEX = "(?<v>[a-zA-Z0-9_-]{11})"
|
||||
private const val PLAYLIST_ID_REGEX = "(?<list>(PL|LL|FL|UU)[a-zA-Z0-9_-]+)"
|
||||
|
||||
private val directVideoIdPattern = Pattern.compile("^$VIDEO_ID_REGEX$")
|
||||
|
||||
private fun getUrlInfo(url: String, retryValidPart: Boolean): UrlInfo {
|
||||
var url = url
|
||||
try {
|
||||
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
||||
url = "https://$url"
|
||||
}
|
||||
|
||||
val builder = URIBuilder(url)
|
||||
|
||||
return UrlInfo(builder.path, builder.queryParams.map { it.name to it.value }.toMap())
|
||||
|
||||
} catch (e: URISyntaxException) {
|
||||
return if (retryValidPart) {
|
||||
getUrlInfo(url.substring(0, e.index - 1), false)
|
||||
} else {
|
||||
throw FriendlyException("Not a valid URL: $url", FriendlyException.Severity.COMMON, e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package nl.voidcorp.discord.music
|
||||
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack
|
||||
import net.dv8tion.jda.api.entities.Guild
|
||||
import net.dv8tion.jda.api.entities.TextChannel
|
||||
import org.apache.logging.log4j.LogManager
|
||||
|
||||
abstract class MusicAnnouncer(open val channel: TextChannel?, private val guild: Guild) {
|
||||
|
||||
private val logger = LogManager.getLogger("Music - ${guild.name}")
|
||||
|
||||
fun close() {
|
||||
guild.audioManager.closeAudioConnection()
|
||||
}
|
||||
|
||||
open fun sendPlayTrack(track: AudioTrack) {
|
||||
logger.info("Playing ${track.info.title} (${track.identifier})")
|
||||
}
|
||||
|
||||
open fun sendQueueTrack(track: AudioTrack) {
|
||||
logger.info("Queued ${track.info.title}")
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package nl.voidcorp.discord.music
|
||||
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack
|
||||
import net.dv8tion.jda.api.entities.Guild
|
||||
import net.dv8tion.jda.api.entities.TextChannel
|
||||
|
||||
class MusicAnnouncerImpl(override val channel: TextChannel, guild: Guild) : MusicAnnouncer(channel, guild) {
|
||||
override fun sendPlayTrack(track: AudioTrack) {
|
||||
channel.sendMessage("Now playing ${track.info.title.replace("@here", "@hеre").replace("@everyone", "@еveryone")}").queue()
|
||||
}
|
||||
|
||||
override fun sendQueueTrack(track: AudioTrack) {
|
||||
channel.sendMessage("Queue'd ${track.info.title.replace("@here", "@hеre").replace("@everyone", "@еveryone")}!").queue()
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package nl.voidcorp.discord.music
|
||||
|
||||
import net.dv8tion.jda.api.entities.Guild
|
||||
|
||||
class NullMusicAnnouncer(guild: Guild) : MusicAnnouncer(null, guild)
|
|
@ -1,48 +0,0 @@
|
|||
package nl.voidcorp.discord.music
|
||||
|
||||
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager
|
||||
import net.dv8tion.jda.api.entities.Guild
|
||||
import nl.voidcorp.discord.storage.GuildRepo
|
||||
import nl.voidcorp.discord.storage.GuildStore
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class PlayerManager(val repo: GuildRepo) : DefaultAudioPlayerManager() {
|
||||
val guildPlayMap = mutableMapOf<Long, TrackScheduler>()
|
||||
fun getGuildPlayer(guild: Guild): TrackScheduler {
|
||||
return if (guildPlayMap.containsKey(guild.idLong)) {
|
||||
guildPlayMap[guild.idLong] ?: error("oof?")
|
||||
} else {
|
||||
val player = createPlayer()
|
||||
val store = repo.findByGuildId(guild.idLong) ?: GuildStore(guild.idLong)
|
||||
val channel = store.musicChannels.firstOrNull()
|
||||
?: store.botChannels.firstOrNull()
|
||||
?: guild.textChannels.firstOrNull {
|
||||
it.name.contains("music", true) && it.name.contains(
|
||||
"bot",
|
||||
true
|
||||
)
|
||||
}?.idLong
|
||||
?: guild.textChannels.firstOrNull { it.name.contains("music", true) }?.idLong
|
||||
?: guild.textChannels.firstOrNull { it.name.contains("bot", true) }?.idLong
|
||||
player.volume = 50
|
||||
|
||||
val ts = TrackScheduler(
|
||||
player,
|
||||
if (channel != null) MusicAnnouncerImpl(
|
||||
guild.getTextChannelById(channel)!!,
|
||||
guild
|
||||
) else NullMusicAnnouncer(guild)
|
||||
) {
|
||||
delGuildPlayer(guild)
|
||||
}
|
||||
|
||||
player.addListener(ts)
|
||||
guild.audioManager.sendingHandler = AudioPlayerSendHandler(player)
|
||||
guildPlayMap[guild.idLong] = ts
|
||||
ts
|
||||
}
|
||||
}
|
||||
|
||||
fun delGuildPlayer(guild: Guild) = guildPlayMap.remove(guild.idLong)
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
package nl.voidcorp.discord.music
|
||||
|
||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer
|
||||
import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrack
|
||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason
|
||||
import java.util.*
|
||||
|
||||
class TrackScheduler(
|
||||
private val player: AudioPlayer,
|
||||
private val announcer: MusicAnnouncer,
|
||||
private val delet: () -> Unit
|
||||
) :
|
||||
AudioEventAdapter() {
|
||||
private val queue = ArrayDeque<AudioTrack>()
|
||||
|
||||
private var loop = false
|
||||
|
||||
fun queue(track: AudioTrack) {
|
||||
if (!player.startTrack(track, true)) {
|
||||
queue.addLast(track)
|
||||
announcer.sendQueueTrack(track)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onTrackStart(player: AudioPlayer, track: AudioTrack) {
|
||||
announcer.sendPlayTrack(track)
|
||||
}
|
||||
|
||||
override fun onTrackEnd(player: AudioPlayer, track: AudioTrack, endReason: AudioTrackEndReason) {
|
||||
if (loop && endReason.mayStartNext) {
|
||||
queue.addLast(track.makeClone())
|
||||
}
|
||||
if ((endReason.mayStartNext || endReason == AudioTrackEndReason.STOPPED) && queue.isNotEmpty()) {
|
||||
player.startTrack(queue.pop(), true)
|
||||
} else if (queue.isEmpty()) {
|
||||
announcer.close()
|
||||
delet()
|
||||
}
|
||||
}
|
||||
|
||||
val playing
|
||||
get() = player.playingTrack != null
|
||||
|
||||
val totalList: List<AudioTrack>
|
||||
get() = if (player.playingTrack == null) listOf() else mutableListOf(player.playingTrack).apply { addAll(queue) }
|
||||
|
||||
fun skip() = player.stopTrack()
|
||||
|
||||
fun loop(): Boolean {
|
||||
loop = !loop
|
||||
return loop
|
||||
}
|
||||
}
|
|
@ -8,4 +8,4 @@ spring.jpa.generate-ddl=true
|
|||
spring.jpa.hibernate.ddl-auto=create-drop
|
||||
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
|
||||
|
||||
ottobot.version=1.7
|
||||
ottobot.version=1.8
|
Loading…
Reference in a new issue