Skip to content

Commit

Permalink
Use Sponsorblock plugin for chapters
Browse files Browse the repository at this point in the history
  • Loading branch information
DRSchlaubi committed Jul 22, 2023
1 parent 27164c0 commit c4afa31
Show file tree
Hide file tree
Showing 12 changed files with 132 additions and 111 deletions.
5 changes: 4 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ kord = "0.10.0-SNAPSHOT"
jjwt = "0.11.5"
api = "3.20.0"
ksp = "1.8.22-1.0.11"
lavakord = "5.0.2"
lavakord = "5.1.0"

[libraries]
kord-common = { group = "dev.kord", name = "kord-common", version.ref = "kord" }
kord-rest = { group = "dev.kord", name = "kord-rest", version.ref = "kord" }
kordex = { group = "com.kotlindiscord.kord.extensions", name = "kord-extensions", version.ref = "kordex" }
kordex-unsafe = { group = "com.kotlindiscord.kord.extensions", name = "unsafe", version.ref = "kordex" }
Expand Down Expand Up @@ -41,6 +42,8 @@ ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version
ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
ktor-client-auth = { group = "io.ktor", name = "ktor-client-auth", version.ref = "ktor" }
ktor-client-resources = { group = "io.ktor", name = "ktor-client-resources", version.ref = "ktor" }
ktor-server-content-negotiation = { group = "io.ktor", name = "ktor-server-content-negotiation", version.ref = "ktor" }
ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" }
ktor-server-resources = { group = "io.ktor", name = "ktor-server-resources", version.ref = "ktor" }
Expand Down
2 changes: 1 addition & 1 deletion music/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
subprojects {
version = "3.0.2-SNAPSHOT"
version = "3.0.3-SNAPSHOT"
}
2 changes: 2 additions & 0 deletions music/player/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ plugins {
group = "dev.schlaubi.mikbot"

dependencies {
implementation(projects.api)
implementation(kotlin("stdlib"))
api(libs.lavakord.kord)
api(libs.lavakord.sponsorblock)
api(libs.lavakord.lavsrc)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import kotlinx.serialization.json.Json
import mu.KotlinLogging

private val youtubeMusic = Url("https://music.youtube.com")
private val youtube = Url("https://www.youtube.com")
const val radioParam = "wAEB" // YouTube sends this parameter starting a song radio


private val webContext = InnerTubeContext(InnerTubeContext.Client("WEB", "2.20220502.01.00"))
Expand Down Expand Up @@ -94,11 +92,6 @@ object InnerTubeClient {
header(HttpHeaders.AcceptLanguage, localeString)
}

suspend fun requestVideoSearch(query: String): InnerTubeSingleBox<TwoColumnSearchResultsRendererContent> =
makeRequest(
youtube, "search", body = SearchRequest(webContext, query)
)

private suspend inline fun <reified B, reified R> makeRequest(
domain: Url,
vararg endpoint: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
package dev.schlaubi.mikmusic.innerttube

import dev.kord.common.Locale
import dev.schlaubi.mikmusic.player.Chapter
import kotlin.math.pow
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

private const val timeSeparator = ':'

suspend fun requestYouTubeAutoComplete(query: String, locale: Locale): List<String> {
val response = InnerTubeClient.requestMusicAutoComplete(query, locale)
Expand All @@ -16,49 +11,3 @@ suspend fun requestYouTubeAutoComplete(query: String, locale: Locale): List<Stri
} ?: emptyList()
}

suspend fun requestVideoRendererById(id: String): VideoRenderer? {
val response = InnerTubeClient.requestVideoSearch("https://youtu.be/$id")

return response
.contents
.twoColumnSearchResultsRenderer
.primaryContents
.sectionListRenderer
.contents
.asSequence()
.flatMap {
it.itemSectionRenderer?.contents?.map(VideoRendererConsent::videoRenderer) ?: emptyList()
}
.filterNotNull()
.firstOrNull {
it.videoId == id
}
}

suspend fun requestVideoChaptersById(id: String): List<Chapter> {
val video = requestVideoRendererById(id) ?: return emptyList()

val content = video.expandableMetadata?.expandableMetadataRenderer?.expandedContent ?: return emptyList()

return content.horizontalCardListRenderer.cards.map {
val renderer = it.macroMarkersListItemRenderer

Chapter(renderer.timeDescription.simpleText.parseDuration(), renderer.title.simpleText)
}
}

private fun String.parseDuration(): Duration {
val units = split(timeSeparator)
val unitCount = units.size - 1
val multiplierOffset = if (unitCount > 2) 1 else 0

val seconds = units.foldRightIndexed(0) { index, input, acc ->
val multiplier = 60.0.pow(multiplierOffset + (unitCount - index)).toInt()
val parsed = input.trimEnd().toInt() * multiplier

acc + parsed
}

return seconds.seconds
}

Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,65 @@ data class SearchSuggestionsSectionRendererContent(
@Serializable
data class SearchSuggestionsRendererContent(
val searchSuggestionRenderer: SearchSuggestionsRenderer? = null,
val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer? = null,
)

@Serializable
data class SearchSuggestionsRenderer(val suggestion: Suggestion) {
@Serializable
data class Suggestion(val runs: List<Run>) {
data class Suggestion(val runs: List<Run> = emptyList()) {
@Serializable
data class Run(val text: String, val bold: Boolean = false)

fun joinRuns() = runs.joinToString("", transform = Run::text)
}
}

@Serializable
data class MusicResponsiveListItemRenderer(
val navigationEndpoint: NavigationEndpoint? = null,
val thumbnail: Thumbnail? = null,
val flexColumns: List<FlexColumn> = emptyList(),
) {
@Serializable
data class NavigationEndpoint(
val browseEndpoint: BrowseEndpoint? = null,
val watchEndpoint: WatchEndpoint? = null,
) {
@Serializable
data class BrowseEndpoint(val browseId: String, val browseEndpointContextSupportedConfigs: Configs) {
@Serializable
data class Configs(val browseEndpointContextMusicConfig: Config) {
@Serializable
data class Config(val pageType: Type) {
@Serializable
enum class Type {
MUSIC_PAGE_TYPE_ALBUM,
MUSIC_PAGE_TYPE_ARTIST
}
}
}
}

@Serializable
data class WatchEndpoint(val videoId: String)
}

@Serializable
data class Thumbnail(val musicThumbnailRenderer: MusicThumbnailRenderer) {
@Serializable
data class MusicThumbnailRenderer(val thumbnail: ThumbnailList) {
@Serializable
data class ThumbnailList(val thumbnails: List<Entry>) {
@Serializable
data class Entry(val url: String)
}
}
}

@Serializable
data class FlexColumn(val musicResponsiveListItemFlexColumnRenderer: MusicResponsiveListItemFlexColumnRenderer) {
@Serializable
data class MusicResponsiveListItemFlexColumnRenderer(val text: SearchSuggestionsRenderer.Suggestion)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package dev.schlaubi.mikmusic.innerttube

import dev.kord.common.Locale
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

@Serializable
data class Entry(
val text: String,
val thumbnail: String? = null,
val url: String? = null,
)

private fun MusicResponsiveListItemRenderer.NavigationEndpoint.toUrl() = when {
browseEndpoint != null -> "https://music.youtube.com/channel/${browseEndpoint.browseId}"
watchEndpoint != null -> "https://music.youtube.com/watch?v${watchEndpoint.videoId}"
else -> error("Unknown endpoint: $this")
}

suspend fun main() {
val results = InnerTubeClient.requestMusicAutoComplete("a", Locale.GERMAN)

val result = results.contents.flatMap {
it.searchSuggestionsSectionRenderer.contents.mapNotNull {
if (it.searchSuggestionRenderer != null) {
Entry(it.searchSuggestionRenderer.suggestion.joinRuns())
} else if (it.musicResponsiveListItemRenderer?.navigationEndpoint != null
&& it.musicResponsiveListItemRenderer.thumbnail != null
) {
val item = it.musicResponsiveListItemRenderer
Entry(
item.flexColumns.first().musicResponsiveListItemFlexColumnRenderer.text.joinRuns(),
item.thumbnail!!.musicThumbnailRenderer.thumbnail.thumbnails.first().url,
item.navigationEndpoint!!.toUrl()
)
} else {
null
}
}
}
println(Json { prettyPrint = true }.encodeToString(result))
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ import dev.schlaubi.lavakord.audio.on
import dev.schlaubi.lavakord.audio.player.Filters
import dev.schlaubi.lavakord.audio.player.applyFilters
import dev.schlaubi.lavakord.plugins.sponsorblock.model.Category
import dev.schlaubi.lavakord.plugins.sponsorblock.model.ChapterStartedEvent
import dev.schlaubi.lavakord.plugins.sponsorblock.model.ChaptersLoadedEvent
import dev.schlaubi.lavakord.plugins.sponsorblock.rest.disableSponsorblock
import dev.schlaubi.lavakord.plugins.sponsorblock.rest.putSponsorblockCategories
import dev.schlaubi.lavakord.rest.updatePlayer
import dev.schlaubi.mikmusic.core.settings.MusicSettingsDatabase
import dev.schlaubi.mikmusic.innerttube.requestVideoChaptersById
import dev.schlaubi.mikmusic.musicchannel.updateMessage
import dev.schlaubi.mikmusic.util.youtubeId
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -72,6 +72,8 @@ class MusicPlayer(val link: Link, private val guild: GuildBehavior) :

link.player.on(consumer = ::onTrackEnd)
link.player.on(consumer = ::onTrackStart)
link.player.on(consumer = ::onChaptersLoaded)
link.player.on(consumer = ::onChapterStarted)
}

suspend fun getChannel() = link.lastChannelId
Expand Down Expand Up @@ -222,20 +224,22 @@ class MusicPlayer(val link: Link, private val guild: GuildBehavior) :
this.filters = SerializableFilters(filters)
}

private suspend fun onTrackStart(event: TrackStartEvent) {
leaveTimeout?.cancel()
private fun onChaptersLoaded(event: ChaptersLoadedEvent) {
val playingTrack = playingTrack ?: return
val queuedBy = playingTrack.queuedBy
this.playingTrack = ChapterQueuedTrack(playingTrack.track, queuedBy, event.chapters)
updateMusicChannelMessage()
}

guild.kord.launch {
val youtubeId = event.track.youtubeId ?: return@launch
val chapters = requestVideoChaptersById(youtubeId)
if (chapters.isNotEmpty()) {
val queuedBy = playingTrack?.queuedBy ?: Snowflake(0)
playingTrack = ChapterQueuedTrack(event.track, queuedBy, chapters)
updateMusicChannelMessage()
restartChapterUpdater()
}
}
private fun onChapterStarted(event: ChapterStartedEvent) {
val chapterTrack = playingTrack as? ChapterQueuedTrack ?: return
chapterTrack.skipTo(event.chapter.start, event.chapter.name)
updateMusicChannelMessage()
}

private fun onTrackStart(@Suppress("UNUSED_PARAMETER") event: TrackStartEvent) {
leaveTimeout?.cancel()
updateMusicChannelMessage()
}

private suspend fun onTrackEnd(event: TrackEndEvent) {
Expand Down Expand Up @@ -309,30 +313,10 @@ class MusicPlayer(val link: Link, private val guild: GuildBehavior) :
suspend fun skipChapter() {
val chapterTrack = (playingTrack as? ChapterQueuedTrack) ?: return
val chapter = chapterTrack.nextChapter()
player.seekTo(chapter.startTime)
restartChapterUpdater(chapter.startTime)
player.seekTo(chapter.start)
updateMusicChannelMessage()
}

private suspend fun restartChapterUpdater(position: Duration? = null) {
chapterUpdater?.cancel()
chapterUpdater = guild.kord.launch {
val chapterTrack = (playingTrack as? ChapterQueuedTrack) ?: return@launch
if (chapterTrack.isOnLast) {
return@launch
}

val nextChapter = chapterTrack.chapters[chapterTrack.chapterIndex + 1]

val diff = nextChapter.startTime - (position ?: player.positionDuration)
delay(diff)

chapterTrack.nextChapter()
updateMusicChannelMessage()
restartChapterUpdater(nextChapter.startTime)
}
}

private suspend fun startNextSong(lastSong: Track? = null, force: Boolean = false, position: Duration? = null) {
val nextTrack = when {
lastSong != null && repeat -> playingTrack!!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ package dev.schlaubi.mikmusic.player

import dev.arbjerg.lavalink.protocol.v4.Track
import dev.kord.common.entity.Snowflake
import dev.schlaubi.lavakord.plugins.sponsorblock.model.YouTubeChapter
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.time.Duration

@Serializable
sealed class QueuedTrack {
Expand All @@ -25,17 +27,21 @@ data class SimpleQueuedTrack(@Contextual override val track: Track, override val
data class ChapterQueuedTrack(
@Contextual override val track: Track,
override val queuedBy: Snowflake,
val chapters: List<Chapter>
val chapters: List<YouTubeChapter>
) : QueuedTrack() {
var chapterIndex: Int = 0
private set
val chapter: Chapter
val chapter: YouTubeChapter
get() = chapters[chapterIndex]

val isOnLast: Boolean
get() = chapterIndex >= chapters.lastIndex

fun nextChapter(): Chapter {
fun skipTo(startTime: Duration, name: String) {
chapterIndex = chapters.indexOfFirst { it.start == startTime && it.name == name }
}

fun nextChapter(): YouTubeChapter {
chapterIndex++
return chapters[chapterIndex]
}
Expand Down
2 changes: 2 additions & 0 deletions runtime/plugins.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
:core:gdpr
:core:redeploy-hook
:music:player
:music:commands
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ if (System.getenv("BUILD_PLUGIN_CI")?.toBoolean() != true) {
":music",
"music:player",
"music:commands",
"clients:discord-oauth",
"clients:haste-client",
"clients:sponsorblock-kt",
"clients:image-color-client",
Expand Down

0 comments on commit c4afa31

Please sign in to comment.