From dc81bb09f22c58c90ac552499a6ad75c2a2a1299 Mon Sep 17 00:00:00 2001 From: dalbodeule <11470513+dalbodeule@users.noreply.github.com> Date: Sun, 4 Aug 2024 13:50:05 +0900 Subject: [PATCH] =?UTF-8?q?add=20Chisu=20playlist=20functions=20-=20add=20?= =?UTF-8?q?Playlist,=20Playlist=20settings=20configs.=20-=20add=20"!?= =?UTF-8?q?=EB=85=B8=EB=9E=98=EC=B6=94=EA=B0=80"=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 1 + .../chzzk_bot/chatbot/chzzk/MessageHandler.kt | 62 +++++++++++-- .../chzzk_bot/chatbot/chzzk/SongModule.kt | 11 +++ common/build.gradle.kts | 3 + .../chzzk_bot/common/events/SongEvents.kt | 20 ++++ .../chzzk_bot/common/models/SongConfig.kt | 18 ++++ .../mori/chzzk_bot/common/models/SongList.kt | 30 ++++++ .../common/services/SongListService.kt | 53 +++++++++++ .../mori/chzzk_bot/common/utils/getYoutube.kt | 91 +++++++++++++++++++ 9 files changed, 281 insertions(+), 8 deletions(-) create mode 100644 chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/SongModule.kt create mode 100644 common/src/main/kotlin/space/mori/chzzk_bot/common/events/SongEvents.kt create mode 100644 common/src/main/kotlin/space/mori/chzzk_bot/common/models/SongConfig.kt create mode 100644 common/src/main/kotlin/space/mori/chzzk_bot/common/models/SongList.kt create mode 100644 common/src/main/kotlin/space/mori/chzzk_bot/common/services/SongListService.kt create mode 100644 common/src/main/kotlin/space/mori/chzzk_bot/common/utils/getYoutube.kt diff --git a/.idea/.gitignore b/.idea/.gitignore index c3f502a..cd1cdb7 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -6,3 +6,4 @@ # Datasource local storage ignored files /dataSources/ /dataSources.local.xml +discord.xml \ No newline at end of file diff --git a/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/MessageHandler.kt b/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/MessageHandler.kt index c0ef401..137610d 100644 --- a/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/MessageHandler.kt +++ b/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/MessageHandler.kt @@ -4,15 +4,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.koin.java.KoinJavaComponent.inject -import space.mori.chzzk_bot.common.events.CoroutinesEventBus -import space.mori.chzzk_bot.common.events.TimerEvent -import space.mori.chzzk_bot.common.events.TimerType +import space.mori.chzzk_bot.common.events.* import space.mori.chzzk_bot.common.models.User -import space.mori.chzzk_bot.common.services.CommandService -import space.mori.chzzk_bot.common.services.CounterService -import space.mori.chzzk_bot.common.services.TimerConfigService -import space.mori.chzzk_bot.common.services.UserService +import space.mori.chzzk_bot.common.services.* import space.mori.chzzk_bot.common.utils.getUptime +import space.mori.chzzk_bot.common.utils.getYoutubeVideo import xyz.r2turntrue.chzzk4j.chat.ChatMessage import xyz.r2turntrue.chzzk4j.chat.ChzzkChat import java.time.LocalDateTime @@ -46,7 +42,13 @@ class MessageHandler( val user = UserService.getUser(channel.channelId) ?: throw RuntimeException("User not found. it's bug? ${channel.channelName} - ${channel.channelId}") val commands = CommandService.getCommands(user) - val manageCommands = mapOf("!명령어추가" to this::manageAddCommand, "!명령어삭제" to this::manageRemoveCommand, "!명령어수정" to this::manageUpdateCommand, "!시간" to this::timerCommand) + val manageCommands = mapOf( + "!명령어추가" to this::manageAddCommand, + "!명령어삭제" to this::manageRemoveCommand, + "!명령어수정" to this::manageUpdateCommand, + "!시간" to this::timerCommand, + "!노래추가" to this::songAddCommand + ) manageCommands.forEach { (commandName, command) -> this.commands[commandName] = command @@ -186,6 +188,50 @@ class MessageHandler( } } + // songs + fun songAddCommand(msg: ChatMessage, user: User) { + val parts = msg.content.split(" ", limit = 3) + if (parts.size < 2) { + listener.sendChat("유튜브 URL을 입력해주세요!") + return + } + + val url = parts[1] + val songs = SongListService.getSong(user) + + if (songs.any { it.url == url }) { + listener.sendChat("같은 노래가 이미 신청되어 있습니다.") + return + } + + val video = getYoutubeVideo(url) + if (video == null) { + listener.sendChat("유튜브에서 찾을 수 없어요!") + return + } + + SongListService.saveSong( + user, + msg.userId, + video.url, + video.name, + video.author, + video.length + ) + CoroutineScope(Dispatchers.Main).launch { + dispatcher.post(SongEvent( + user.token, + SongType.ADD, + msg.userId, + video.name, + video.author, + video.length + )) + } + + listener.sendChat("노래가 추가되었습니다.") + } + internal fun handle(msg: ChatMessage, user: User) { val commandKey = msg.content.split(' ')[0] diff --git a/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/SongModule.kt b/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/SongModule.kt new file mode 100644 index 0000000..1782b09 --- /dev/null +++ b/chatbot/src/main/kotlin/space/mori/chzzk_bot/chatbot/chzzk/SongModule.kt @@ -0,0 +1,11 @@ +package space.mori.chzzk_bot.chatbot.chzzk + +import space.mori.chzzk_bot.common.models.User +import space.mori.chzzk_bot.common.services.CommandService +import xyz.r2turntrue.chzzk4j.chat.ChatMessage + +class SongModule { + companion object { + + } +} \ No newline at end of file diff --git a/common/build.gradle.kts b/common/build.gradle.kts index a1f5ed5..06d04af 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -31,6 +31,9 @@ dependencies { // https://mvnrepository.com/artifact/io.github.cdimascio/dotenv-kotlin implementation("io.github.cdimascio:dotenv-kotlin:6.4.1") + // https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp + implementation("com.squareup.okhttp3:okhttp:4.12.0") + testImplementation(kotlin("test")) } diff --git a/common/src/main/kotlin/space/mori/chzzk_bot/common/events/SongEvents.kt b/common/src/main/kotlin/space/mori/chzzk_bot/common/events/SongEvents.kt new file mode 100644 index 0000000..50da08d --- /dev/null +++ b/common/src/main/kotlin/space/mori/chzzk_bot/common/events/SongEvents.kt @@ -0,0 +1,20 @@ +package space.mori.chzzk_bot.common.events + +enum class SongType(var value: Int) { + ADD(0), + REMOVE(1), + NEXT(2), + + STREAM_OFF(50) +} + +class SongEvent( + val uid: String, + val type: SongType, + val req_uid: String?, + val name: String?, + val author: String?, + val time: Int?, +): Event { + var TAG = javaClass.simpleName +} \ No newline at end of file diff --git a/common/src/main/kotlin/space/mori/chzzk_bot/common/models/SongConfig.kt b/common/src/main/kotlin/space/mori/chzzk_bot/common/models/SongConfig.kt new file mode 100644 index 0000000..58a89d5 --- /dev/null +++ b/common/src/main/kotlin/space/mori/chzzk_bot/common/models/SongConfig.kt @@ -0,0 +1,18 @@ +package space.mori.chzzk_bot.common.models + +import org.jetbrains.exposed.dao.IntEntity +import org.jetbrains.exposed.dao.IntEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.ReferenceOption + +object SongConfigs: IntIdTable("song_config") { + val user = reference("user", Users, onDelete = ReferenceOption.CASCADE) + val option = integer("option") +} +class SongConfig(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(TimerConfigs) + + var user by User referencedOn TimerConfigs.user + var option by TimerConfigs.option +} \ No newline at end of file diff --git a/common/src/main/kotlin/space/mori/chzzk_bot/common/models/SongList.kt b/common/src/main/kotlin/space/mori/chzzk_bot/common/models/SongList.kt new file mode 100644 index 0000000..62e0ca8 --- /dev/null +++ b/common/src/main/kotlin/space/mori/chzzk_bot/common/models/SongList.kt @@ -0,0 +1,30 @@ +package space.mori.chzzk_bot.common.models + +import org.jetbrains.exposed.dao.IntEntity +import org.jetbrains.exposed.dao.IntEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.javatime.datetime + +object SongLists: IntIdTable("song_list") { + val user = reference("user", Users) + val uid = varchar("uid", 64) + val url = varchar("url", 128) + val name = text("name") + val author = text("author") + val time = integer("time") + val created_at = datetime("created_at") +} + +class SongList(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(SongLists) + + var url by SongLists.url + var name by SongLists.name + var author by SongLists.author + var time by SongLists.time + var created_at by SongLists.created_at + + var user by User referencedOn SongLists.user + var uid by SongLists.uid +} \ No newline at end of file diff --git a/common/src/main/kotlin/space/mori/chzzk_bot/common/services/SongListService.kt b/common/src/main/kotlin/space/mori/chzzk_bot/common/services/SongListService.kt new file mode 100644 index 0000000..2a330be --- /dev/null +++ b/common/src/main/kotlin/space/mori/chzzk_bot/common/services/SongListService.kt @@ -0,0 +1,53 @@ +package space.mori.chzzk_bot.common.services + +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.transactions.transaction +import space.mori.chzzk_bot.common.models.SongList +import space.mori.chzzk_bot.common.models.SongLists +import space.mori.chzzk_bot.common.models.User + +object SongListService { + fun saveSong(user: User, uid: String, url: String, name: String, author: String, time: Int) { + return transaction { + SongList.new { + this.user = user + this.uid = uid + this.url = url + this.name = name + this.author = author + this.time = time + } + } + } + + fun getSong(user: User, uid: String): List { + return transaction { + SongList.find( + (SongLists.user eq user.id) and + (SongLists.uid eq uid) + ).toList() + } + } + + fun getSong(user: User): List { + return transaction { + SongList.find(SongLists.user eq user.id).toList() + } + } + + fun deleteSong(user: User, uid: String, name: String): SongList { + return transaction { + val songRow = SongList.find( + (SongLists.user eq user.id) and + (SongLists.uid eq uid) and + (SongLists.name eq name) + ).firstOrNull() + + songRow ?: throw RuntimeException("Song not found! ${user.username} / $uid / $name") + + songRow.delete() + songRow + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/space/mori/chzzk_bot/common/utils/getYoutube.kt b/common/src/main/kotlin/space/mori/chzzk_bot/common/utils/getYoutube.kt new file mode 100644 index 0000000..52cade0 --- /dev/null +++ b/common/src/main/kotlin/space/mori/chzzk_bot/common/utils/getYoutube.kt @@ -0,0 +1,91 @@ +package space.mori.chzzk_bot.common.utils + +import com.google.gson.Gson +import com.google.gson.JsonObject +import io.github.cdimascio.dotenv.dotenv +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.IOException + +data class YoutubeVideo( + val url: String, + val name: String, + val author: String, + val length: Int +) + +val regex = ".*(?:youtu.be/|v/|u/\\w/|embed/|watch\\?v=|&v=)([^#&?]*).*".toRegex() +val durationRegex = """PT(\d+H)?(\d+m)?(\d+S)?""".toRegex() + +val client = OkHttpClient() +val gson = Gson() + +val dotenv = dotenv { + ignoreIfMissing = true +} + + +fun getYoutubeVideoId(url: String): String? { + val matchResult = regex.find(url) + + return matchResult?.groups?.get(1)?.value +} + +fun parseDuration(duration: String): Int { + val matchResult = durationRegex.find(duration) + val (hours, minutes, seconds) = matchResult?.destructured ?: return 0 + + val hourInSec = hours.dropLast(1).toIntOrNull()?.times(3600) ?: 0 + val minutesInSec = minutes.dropLast(1).toIntOrNull()?.times(60) ?: 0 + val totalSeconds = seconds.dropLast(1).toIntOrNull() ?: 0 + + return hourInSec + minutesInSec + totalSeconds +} + +fun getYoutubeVideo(url: String): YoutubeVideo? { + val videoId = getYoutubeVideoId(url) + + val api = HttpUrl.Builder() + .scheme("https") + .host("www.googleapis.com") + .addPathSegment("youtube") + .addPathSegment("v3") + .addPathSegment("videos") + .addQueryParameter("id", videoId) + .addQueryParameter("key", dotenv["YOUTUBE_API_KEY"]) + .addQueryParameter("part", "snippet") + .build() + + + val request = Request.Builder() + .url(api) + .build() + + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) throw IOException("Unexpected code $response") + + val responseBody = response.body?.string() + val json = gson.fromJson(responseBody, JsonObject::class.java) + val items = json.getAsJsonArray("items") + + if (items == null || items.size() == 0) return null + + val item = items[0].asJsonObject + val snippet = item.getAsJsonObject("snippet") + val contentDetail = item.asJsonObject.getAsJsonObject("contentDetail") + val status = contentDetail.getAsJsonObject("status") + + if (!status.get("embeddable").asBoolean) return null + + val duration = contentDetail.get("duration").asString + val length = parseDuration(duration) + + return YoutubeVideo( + "https://www.youtube.com/watch?v=$videoId", + snippet.get("title").asString, + snippet.get("channelTitle").asString, + length + ) + } +} \ No newline at end of file