Skip to content

Commit

Permalink
/ytplay playlist search support
Browse files Browse the repository at this point in the history
  • Loading branch information
ileukocyte committed Jul 22, 2022
1 parent 2efdd73 commit 8d22c8d
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 51 deletions.
3 changes: 3 additions & 0 deletions src/main/kotlin/io/ileukocyte/hibernum/audio/music.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.ileukocyte.hibernum.audio

import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager

import java.util.concurrent.Executors

Expand All @@ -24,6 +25,8 @@ private val musicContextDispatcher = Executors.newFixedThreadPool(3).asCoroutine
object MusicContext : CoroutineContext by musicContextDispatcher, AutoCloseable by musicContextDispatcher

val PLAYER_MANAGER = DefaultAudioPlayerManager().apply {
registerSourceManager(YoutubeAudioSourceManager().apply { setPlaylistPageCount(10) })

AudioSourceManagers.registerLocalSource(this)
AudioSourceManagers.registerRemoteSources(this)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,18 @@ import net.dv8tion.jda.api.interactions.commands.build.OptionData
import net.dv8tion.jda.api.interactions.components.selections.SelectMenu
import net.dv8tion.jda.api.interactions.components.selections.SelectOption

import org.jetbrains.kotlin.utils.addToStdlib.applyIf

class YouTubePlayCommand : TextCommand {
override val name = "ytplay"
override val description = "Plays the specified YouTube video in a voice channel"
override val description = "Searches a YouTube video or playlist by the provided query and plays it in a voice channel"
override val aliases = setOf("yp", "ytp", "youtubeplay", "youtube-play")
override val options = setOf(
OptionData(OptionType.STRING, "query", "A link or a search term", true))
OptionData(OptionType.STRING, "query", "A search term or a YouTube link", true),
OptionData(OptionType.STRING, "kind", "A kind of result (videos or playlists)")
.addChoice("Videos", "videos")
.addChoice("Playlists", "playlists"),
)
override val usages = setOf(setOf("query".toClassicTextUsage()))
override val cooldown = 5L

Expand All @@ -56,6 +62,7 @@ class YouTubePlayCommand : TextCommand {
event.channel as GuildMessageChannel,
event.getOption("query")?.asString ?: return,
deferred,
event.getOption("kind")?.asString ?: "videos",
)
}

Expand All @@ -71,11 +78,12 @@ class YouTubePlayCommand : TextCommand {
return
}

if (id.last() == "videos") {
val videoUrl = event.selectedOptions.firstOrNull()?.value ?: return
val entity = event.selectedOptions.firstOrNull()
?.value
?.applyIf(id.last() == "playlists") { "https://www.youtube.com/playlist?list=$this" }
?: return

play(videoUrl, event.channel as GuildMessageChannel, event.user, true, deferred)
}
play(entity, event.channel as GuildMessageChannel, event.user, id.last() == "videos", deferred)
} else {
throw CommandException("You did not invoke the initial command!")
}
Expand All @@ -86,6 +94,7 @@ class YouTubePlayCommand : TextCommand {
textChannel: GuildMessageChannel,
query: String,
ifFromAnInteraction: InteractionHook? = null,
kind: String = "videos",
) {
member.voiceState?.channel?.let { vc ->
val channel = vc.takeUnless { textChannel.guild.selfMember.voiceState?.channel == vc }
Expand All @@ -98,50 +107,100 @@ class YouTubePlayCommand : TextCommand {
return
}

val videos = withContext(MusicContext) { searchVideos(query) }
if (kind == "videos") {
val videos = withContext(MusicContext) { searchVideos(query) }

if (videos.isEmpty()) {
val error = "No results have been found by the query!"
if (videos.isEmpty()) {
val error = "No results have been found by the query!"

ifFromAnInteraction?.let {
try {
it.editOriginalEmbeds(defaultEmbed(error, EmbedType.FAILURE)).await()
ifFromAnInteraction?.let {
try {
it.editOriginalEmbeds(defaultEmbed(error, EmbedType.FAILURE)).await()

return
} catch (_: ErrorResponseException) {
throw CommandException(error)
return
} catch (_: ErrorResponseException) {
throw CommandException(error)
}
} ?: throw CommandException(error)
}

val menu by lazy {
val options = videos.map {
SelectOption.of(
it.snippet.title.limitTo(SelectOption.LABEL_MAX_LENGTH),
it.id,
).withDescription(
"${it.snippet.channelTitle} \u2022 ${asDuration(it.contentDetails.durationInMillis)}"
.limitTo(SelectOption.DESCRIPTION_MAX_LENGTH)
)
}
} ?: throw CommandException(error)
}

val menu by lazy {
val options = videos.map {
SelectOption.of(
it.snippet.title.limitTo(SelectOption.LABEL_MAX_LENGTH),
it.id,
).withDescription(
"${it.snippet.channelTitle} - ${asDuration(it.contentDetails.durationInMillis)}"
.limitTo(SelectOption.DESCRIPTION_MAX_LENGTH)
)
SelectMenu.create("$name-${member.user.idLong}-videos")
.addOptions(
*options.toTypedArray(),
SelectOption.of("Exit", "exit").withEmoji(Emoji.fromUnicode("\u274C")),
).build()
}

SelectMenu.create("$name-${member.user.idLong}-videos")
.addOptions(
*options.toTypedArray(),
SelectOption.of("Exit", "exit").withEmoji(Emoji.fromUnicode("\u274C")),
).build()
}
val embed = buildEmbed {
color = Immutable.SUCCESS
description = "Select the video you want to play!"
}

val embed = buildEmbed {
color = Immutable.SUCCESS
description = "Select the video you want to play!"
}
ifFromAnInteraction?.let {
it.editOriginalEmbeds(embed).setActionRow(menu).queue(null) {
textChannel.sendMessageEmbeds(embed).setActionRow(menu).queue()
}
} ?: textChannel.sendMessageEmbeds(embed).setActionRow(menu).queue()
} else {
val playlists = withContext(MusicContext) { searchPlaylists(query) }

ifFromAnInteraction?.let {
it.editOriginalEmbeds(embed).setActionRow(menu).queue(null) {
textChannel.sendMessageEmbeds(embed).setActionRow(menu).queue()
if (playlists.isEmpty()) {
val error = "No results have been found by the query!"

ifFromAnInteraction?.let {
try {
it.editOriginalEmbeds(defaultEmbed(error, EmbedType.FAILURE)).await()

return
} catch (_: ErrorResponseException) {
throw CommandException(error)
}
} ?: throw CommandException(error)
}

val menu by lazy {
val options = playlists.map {
val count = it.contentDetails.itemCount
.run { "$this " + "video".singularOrPlural(this) }

SelectOption.of(
it.snippet.title.limitTo(SelectOption.LABEL_MAX_LENGTH),
it.id,
).withDescription(
"${it.snippet.channelTitle} \u2022 $count"
.limitTo(SelectOption.DESCRIPTION_MAX_LENGTH)
)
}

SelectMenu.create("$name-${member.user.idLong}-playlists")
.addOptions(
*options.toTypedArray(),
SelectOption.of("Exit", "exit").withEmoji(Emoji.fromUnicode("\u274C")),
).build()
}

val embed = buildEmbed {
color = Immutable.SUCCESS
description = "Select the playlist you want to play!"
}
} ?: textChannel.sendMessageEmbeds(embed).setActionRow(menu).queue()

ifFromAnInteraction?.let {
it.editOriginalEmbeds(embed).setActionRow(menu).queue(null) {
textChannel.sendMessageEmbeds(embed).setActionRow(menu).queue()
}
} ?: textChannel.sendMessageEmbeds(embed).setActionRow(menu).queue()
}
} ?: ifFromAnInteraction?.let {
try {
it.editOriginalEmbeds(
Expand Down
17 changes: 6 additions & 11 deletions src/main/kotlin/io/ileukocyte/hibernum/extensions/stdlib.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

import org.jetbrains.kotlin.utils.addToStdlib.applyIf

// Java concurrency
suspend fun <T> CompletableFuture<T>.await() = suspendCoroutine<T> { c ->
whenComplete { r, e -> e?.let { c.resumeWithException(it) } ?: c.resume(r) }
Expand Down Expand Up @@ -53,15 +55,9 @@ fun String.containsAll(elements: Collection<CharSequence>) = containsAll(*elemen
fun String.containsAll(vararg args: Char) = toCharArray().toList().containsAll(args.toList())

// Only works with UTF-16 encoding
fun String.limitTo(limit: Int, trim: Boolean = true) = take(limit)
.let {
if (length > limit) {
(it.removeLastChar().takeIf { trim }?.trim()
?: it.removeLastChar()) + '\u2026'
} else {
this
}
}
fun String.limitTo(limit: Int, trim: Boolean = true) = take(limit).applyIf(length > limit) {
(removeLastChar().takeIf { trim }?.trim() ?: removeLastChar()) + '\u2026'
}

fun String.remove(input: String) = replace(input, "")
fun String.remove(regex: Regex) = replace(regex, "")
Expand All @@ -71,8 +67,7 @@ fun String.removeLastChar() = substring(0, length.dec())
fun String.replaceLastChar(charSequence: CharSequence) = removeLastChar() + charSequence
fun String.replaceLastChar(char: Char) = removeLastChar() + char

fun <N : Number> String.singularOrPlural(number: N) =
this + "s".takeUnless { number.toLong() == 1L }.orEmpty()
fun <N : Number> String.singularOrPlural(number: N) = applyIf(number.toLong() != 1L) { "${this}s" }

fun String.surroundWith(charSequence: CharSequence) = "$charSequence$this$charSequence"
fun String.surroundWith(prefix: CharSequence, suffix: CharSequence) = "$prefix$this$suffix"
Expand Down
17 changes: 17 additions & 0 deletions src/main/kotlin/io/ileukocyte/hibernum/utils/youtube.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package io.ileukocyte.hibernum.utils
import com.google.api.client.http.javanet.NetHttpTransport
import com.google.api.client.json.gson.GsonFactory
import com.google.api.services.youtube.YouTube
import com.google.api.services.youtube.model.Playlist
import com.google.api.services.youtube.model.Video
import com.google.api.services.youtube.model.VideoContentDetails

Expand Down Expand Up @@ -38,4 +39,20 @@ suspend fun searchVideos(query: String, maxResults: Long = 15): List<Video> = su
it.resume(videos.execute().items)
}

suspend fun searchPlaylists(query: String, maxResults: Long = 15): List<Playlist> = suspendCoroutine {
val search = YOUTUBE.search().list(listOf("id", "snippet"))

search.key = Immutable.YOUTUBE_API_KEY
search.q = query
search.maxResults = maxResults
search.type = listOf("playlist")

val playlists = YOUTUBE.playlists().list(listOf("id", "snippet", "contentDetails"))

playlists.key = search.key
playlists.id = listOf(search.execute().items.joinToString(",") { p -> p.id.playlistId })

it.resume(playlists.execute().items)
}

val VideoContentDetails.durationInMillis get() = Duration.parse(duration).toMillis()

0 comments on commit 8d22c8d

Please sign in to comment.