Merge pull request #27 from dalbodeule/develop

some debugs on Chisu playlist
This commit is contained in:
JinU Choi 2024-08-04 15:32:54 +09:00 committed by GitHub
commit 4f31d87b3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 248 additions and 36 deletions

View File

@ -9,9 +9,7 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.chatbot.chzzk.Connector.chzzk import space.mori.chzzk_bot.chatbot.chzzk.Connector.chzzk
import space.mori.chzzk_bot.chatbot.discord.Discord import space.mori.chzzk_bot.chatbot.discord.Discord
import space.mori.chzzk_bot.common.events.CoroutinesEventBus import space.mori.chzzk_bot.common.events.*
import space.mori.chzzk_bot.common.events.TimerEvent
import space.mori.chzzk_bot.common.events.TimerType
import space.mori.chzzk_bot.common.models.User import space.mori.chzzk_bot.common.models.User
import space.mori.chzzk_bot.common.services.LiveStatusService import space.mori.chzzk_bot.common.services.LiveStatusService
import space.mori.chzzk_bot.common.services.TimerConfigService import space.mori.chzzk_bot.common.services.TimerConfigService
@ -39,6 +37,11 @@ object ChzzkHandler {
UserService.getAllUsers().map { UserService.getAllUsers().map {
chzzk.getChannel(it.token)?.let { token -> addUser(token, it) } chzzk.getChannel(it.token)?.let { token -> addUser(token, it) }
} }
handlers.forEach { handler ->
val streamInfo = getStreamInfo(handler.listener.channelId)
if (streamInfo.content.status == "OPEN") handler.isActive(true, streamInfo)
}
} }
fun disable() { fun disable() {
@ -148,7 +151,6 @@ class UserHandler(
get() = _isActive get() = _isActive
internal fun isActive(value: Boolean, status: IData<IStreamInfo>) { internal fun isActive(value: Boolean, status: IData<IStreamInfo>) {
_isActive = value
if(value) { if(value) {
logger.info("${user.username} is live.") logger.info("${user.username} is live.")
@ -170,23 +172,37 @@ class UserHandler(
"" ""
)) ))
} }
if(!_isActive) {
delay(5000L) delay(5000L)
listener.sendChat("${user.username} 님! 오늘도 열심히 방송하세요!") listener.sendChat("${user.username} 님! 오늘도 열심히 방송하세요!")
Discord.sendDiscord(user, status) Discord.sendDiscord(user, status)
} }
}
} else { } else {
logger.info("${user.username} is offline.") logger.info("${user.username} is offline.")
streamStartTime = null streamStartTime = null
listener.closeAsync() listener.closeAsync()
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
dispatcher.post(TimerEvent( val events = listOf(
TimerEvent(
channel.channelId, channel.channelId,
TimerType.STREAM_OFF, TimerType.STREAM_OFF,
"" null
)) ),
SongEvent(
channel.channelId,
SongType.STREAM_OFF,
null,
null,
null,
null,
null
)
)
events.forEach { dispatcher.post(it) }
} }
} }
_isActive = value
} }
} }

View File

@ -36,6 +36,14 @@ class MessageHandler(
init { init {
reloadCommand() reloadCommand()
dispatcher.subscribe(SongEvent::class) {
if(it.type == SongType.STREAM_OFF) {
val user = UserService.getUser(channel.channelId)
if(! user?.let { usr -> SongListService.getSong(usr) }.isNullOrEmpty()) {
SongListService.deleteUser(user!!)
}
}
}
} }
internal fun reloadCommand() { internal fun reloadCommand() {
@ -199,33 +207,35 @@ class MessageHandler(
val url = parts[1] val url = parts[1]
val songs = SongListService.getSong(user) val songs = SongListService.getSong(user)
if (songs.any { it.url == url }) {
listener.sendChat("같은 노래가 이미 신청되어 있습니다.")
return
}
val video = getYoutubeVideo(url) val video = getYoutubeVideo(url)
if (video == null) { if (video == null) {
listener.sendChat("유튜브에서 찾을 수 없어요!") listener.sendChat("유튜브에서 찾을 수 없어요!")
return return
} }
if (songs.any { it.url == video.url }) {
listener.sendChat("같은 노래가 이미 신청되어 있습니다.")
return
}
SongListService.saveSong( SongListService.saveSong(
user, user,
msg.userId, msg.userId,
video.url, video.url,
video.name, video.name,
video.author, video.author,
video.length video.length,
msg.profile?.nickname ?: ""
) )
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Default).launch {
dispatcher.post(SongEvent( dispatcher.post(SongEvent(
user.token, user.token,
SongType.ADD, SongType.ADD,
msg.userId, msg.userId,
msg.profile?.nickname ?: "",
video.name, video.name,
video.author, video.author,
video.length video.length,
)) ))
} }

View File

@ -11,7 +11,8 @@ enum class SongType(var value: Int) {
class SongEvent( class SongEvent(
val uid: String, val uid: String,
val type: SongType, val type: SongType,
val req_uid: String?, val reqUid: String?,
val reqName: String?,
val name: String?, val name: String?,
val author: String?, val author: String?,
val time: Int?, val time: Int?,

View File

@ -5,15 +5,17 @@ import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.javatime.datetime import org.jetbrains.exposed.sql.javatime.datetime
import java.time.LocalDateTime
object SongLists: IntIdTable("song_list") { object SongLists: IntIdTable("song_list") {
val user = reference("user", Users) val user = reference("user", Users)
val uid = varchar("uid", 64) val uid = varchar("uid", 64)
val url = varchar("url", 128) val url = varchar("url", 128)
val name = text("name") val name = text("name")
val reqName = varchar("req_name", 20)
val author = text("author") val author = text("author")
val time = integer("time") val time = integer("time")
val created_at = datetime("created_at") val created_at = datetime("created_at").default(LocalDateTime.now())
} }
class SongList(id: EntityID<Int>) : IntEntity(id) { class SongList(id: EntityID<Int>) : IntEntity(id) {
@ -27,4 +29,5 @@ class SongList(id: EntityID<Int>) : IntEntity(id) {
var user by User referencedOn SongLists.user var user by User referencedOn SongLists.user
var uid by SongLists.uid var uid by SongLists.uid
var reqName by SongLists.reqName
} }

View File

@ -8,7 +8,7 @@ import space.mori.chzzk_bot.common.models.SongLists
import space.mori.chzzk_bot.common.models.User import space.mori.chzzk_bot.common.models.User
object SongListService { object SongListService {
fun saveSong(user: User, uid: String, url: String, name: String, author: String, time: Int) { fun saveSong(user: User, uid: String, url: String, name: String, author: String, time: Int, reqName: String) {
return transaction { return transaction {
SongList.new { SongList.new {
this.user = user this.user = user
@ -17,6 +17,7 @@ object SongListService {
this.name = name this.name = name
this.author = author this.author = author
this.time = time this.time = time
this.reqName = reqName
} }
} }
} }
@ -32,7 +33,7 @@ object SongListService {
fun getSong(user: User): List<SongList> { fun getSong(user: User): List<SongList> {
return transaction { return transaction {
SongList.find(SongLists.user eq user.id).toList() SongList.find(SongLists.user eq user.id).toList().sortedBy { it.created_at }
} }
} }
@ -50,4 +51,13 @@ object SongListService {
songRow songRow
} }
} }
fun deleteUser(user: User): Boolean {
return transaction {
val songRow = SongList.find(SongLists.user eq user.id).toList()
songRow.forEach { it.delete() }
true
}
}
} }

View File

@ -16,7 +16,7 @@ data class YoutubeVideo(
) )
val regex = ".*(?:youtu.be/|v/|u/\\w/|embed/|watch\\?v=|&v=)([^#&?]*).*".toRegex() val regex = ".*(?:youtu.be/|v/|u/\\w/|embed/|watch\\?v=|&v=)([^#&?]*).*".toRegex()
val durationRegex = """PT(\d+H)?(\d+m)?(\d+S)?""".toRegex() val durationRegex = """PT(\d+H)?(\d+M)?(\d+S)?""".toRegex()
val client = OkHttpClient() val client = OkHttpClient()
val gson = Gson() val gson = Gson()
@ -33,6 +33,7 @@ fun getYoutubeVideoId(url: String): String? {
} }
fun parseDuration(duration: String): Int { fun parseDuration(duration: String): Int {
println(duration)
val matchResult = durationRegex.find(duration) val matchResult = durationRegex.find(duration)
val (hours, minutes, seconds) = matchResult?.destructured ?: return 0 val (hours, minutes, seconds) = matchResult?.destructured ?: return 0
@ -54,7 +55,7 @@ fun getYoutubeVideo(url: String): YoutubeVideo? {
.addPathSegment("videos") .addPathSegment("videos")
.addQueryParameter("id", videoId) .addQueryParameter("id", videoId)
.addQueryParameter("key", dotenv["YOUTUBE_API_KEY"]) .addQueryParameter("key", dotenv["YOUTUBE_API_KEY"])
.addQueryParameter("part", "snippet") .addQueryParameter("part", "snippet,contentDetails,status")
.build() .build()
@ -71,10 +72,12 @@ fun getYoutubeVideo(url: String): YoutubeVideo? {
if (items == null || items.size() == 0) return null if (items == null || items.size() == 0) return null
println(json)
val item = items[0].asJsonObject val item = items[0].asJsonObject
val snippet = item.getAsJsonObject("snippet") val snippet = item.getAsJsonObject("snippet")
val contentDetail = item.asJsonObject.getAsJsonObject("contentDetail") val contentDetail = item.getAsJsonObject("contentDetails")
val status = contentDetail.getAsJsonObject("status") val status = item.getAsJsonObject("status")
if (!status.get("embeddable").asBoolean) return null if (!status.get("embeddable").asBoolean) return null

View File

@ -4,5 +4,6 @@ DB_URL=jdbc:mariadb://localhost:3306/chzzk
DB_USER=chzzk DB_USER=chzzk
DB_PASS=chzzk DB_PASS=chzzk
RUN_AGENT=false RUN_AGENT=false
YOUTUBE_API_KEY=
NID_AUT= NID_AUT=
NID_SES= NID_SES=

View File

@ -13,6 +13,7 @@ import io.ktor.server.routing.*
import io.ktor.server.websocket.* import io.ktor.server.websocket.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import space.mori.chzzk_bot.webserver.routes.apiRoutes import space.mori.chzzk_bot.webserver.routes.apiRoutes
import space.mori.chzzk_bot.webserver.routes.apiSongRoutes
import space.mori.chzzk_bot.webserver.routes.wsSongRoutes import space.mori.chzzk_bot.webserver.routes.wsSongRoutes
import space.mori.chzzk_bot.webserver.routes.wsTimerRoutes import space.mori.chzzk_bot.webserver.routes.wsTimerRoutes
import java.time.Duration import java.time.Duration
@ -38,6 +39,7 @@ val server = embeddedServer(Netty, port = 8080) {
} }
routing { routing {
apiRoutes() apiRoutes()
apiSongRoutes()
wsTimerRoutes() wsTimerRoutes()
wsSongRoutes() wsSongRoutes()
swaggerUI("swagger-ui/index.html", "openapi/documentation.yaml") { swaggerUI("swagger-ui/index.html", "openapi/documentation.yaml") {

View File

@ -4,10 +4,29 @@ import io.ktor.http.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import space.mori.chzzk_bot.common.models.SongList
import space.mori.chzzk_bot.common.services.SongListService import space.mori.chzzk_bot.common.services.SongListService
import space.mori.chzzk_bot.common.services.UserService import space.mori.chzzk_bot.common.services.UserService
fun Routing.songRoutes() { @Serializable
data class SongsDTO(
val url: String,
val name: String,
val author: String,
val time: Int,
val reqName: String
)
fun SongList.toDTO(): SongsDTO = SongsDTO(
this.url,
this.name,
this.author,
this.time,
this.reqName
)
fun Routing.apiSongRoutes() {
route("/songs/{uid}") { route("/songs/{uid}") {
get { get {
val uid = call.parameters["uid"] val uid = call.parameters["uid"]
@ -18,7 +37,7 @@ fun Routing.songRoutes() {
} }
val songs = SongListService.getSong(user) val songs = SongListService.getSong(user)
call.respond(songs) call.respond(HttpStatusCode.OK, songs.map { it.toDTO() })
} }
} }
route("/songs") { route("/songs") {

View File

@ -86,7 +86,7 @@ fun Routing.wsSongRoutes() {
ws.sendSerialized(SongResponse( ws.sendSerialized(SongResponse(
it.type.value, it.type.value,
it.uid, it.uid,
it.req_uid, it.reqUid,
it.name, it.name,
it.author, it.author,
it.time it.time

View File

@ -8,8 +8,7 @@ servers:
paths: paths:
/: /:
get: get:
summary: "Webroot" description: ""
description: "Main page of this api"
responses: responses:
"200": "200":
description: "OK" description: "OK"
@ -22,7 +21,7 @@ paths:
value: "Hello World!" value: "Hello World!"
/health: /health:
get: get:
description: "Health Check endpoint" description: ""
responses: responses:
"200": "200":
description: "OK" description: "OK"
@ -33,3 +32,151 @@ paths:
examples: examples:
Example#1: Example#1:
value: "OK" value: "OK"
/song/{uid}:
get:
description: ""
parameters:
- name: "uid"
in: "path"
required: true
schema:
type: "string"
- name: "Connection"
in: "header"
required: true
description: "Websocket Connection parameter"
schema:
type: "string"
- name: "Upgrade"
in: "header"
required: true
description: "Websocket Upgrade parameter"
schema:
type: "string"
- name: "Sec-WebSocket-Key"
in: "header"
required: true
description: "Websocket Sec-WebSocket-Key parameter"
schema:
type: "string"
responses:
"101":
description: "Switching Protocols"
headers:
Connection:
required: true
schema:
type: "string"
Upgrade:
required: true
schema:
type: "string"
Sec-WebSocket-Accept:
required: true
schema:
type: "string"
/songs:
get:
description: ""
responses:
"400":
description: "Bad Request"
content:
text/plain:
schema:
type: "string"
examples:
Example#1:
value: "Require UID"
/songs/{uid}:
get:
description: ""
parameters:
- name: "uid"
in: "path"
required: true
schema:
type: "string"
responses:
"404":
description: "Not Found"
content:
text/plain:
schema:
type: "string"
examples:
Example#1:
value: "No user found"
"200":
description: "OK"
content:
'*/*':
schema:
type: "array"
items:
$ref: "#/components/schemas/SongList"
/timer/{uid}:
get:
description: ""
parameters:
- name: "uid"
in: "path"
required: true
schema:
type: "string"
- name: "Connection"
in: "header"
required: true
description: "Websocket Connection parameter"
schema:
type: "string"
- name: "Upgrade"
in: "header"
required: true
description: "Websocket Upgrade parameter"
schema:
type: "string"
- name: "Sec-WebSocket-Key"
in: "header"
required: true
description: "Websocket Sec-WebSocket-Key parameter"
schema:
type: "string"
responses:
"101":
description: "Switching Protocols"
headers:
Connection:
required: true
schema:
type: "string"
Upgrade:
required: true
schema:
type: "string"
Sec-WebSocket-Accept:
required: true
schema:
type: "string"
components:
schemas:
Object:
type: "object"
properties: {}
ResultRow:
type: "object"
properties:
fieldIndex:
type: "object"
required:
- "fieldIndex"
SongList:
type: "object"
properties:
writeValues:
$ref: "#/components/schemas/Object"
_readValues:
$ref: "#/components/schemas/ResultRow"
required:
- "id"
- "writeValues"