wbooster-android/app/src/main/java/zoo/wbooster/Services.kt

276 lines
8.5 KiB
Kotlin

package zoo.wbooster
import android.app.*
import android.content.Intent
import android.media.MediaPlayer
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.util.Log
import android.widget.ProgressBar
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationCompat
import com.beust.klaxon.Klaxon
import com.vdurmont.emoji.EmojiParser
import com.yausername.ffmpeg.FFmpeg
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLException
import com.yausername.youtubedl_android.YoutubeDLRequest
import java.lang.Exception
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
class VideoUsefulDetails(
val path: String,
val title: String,
val thumbnail: String
)
class VideoInfo(
val _type: String,
val id: String,
val ie_key: String,
val title: String,
val url: String
)
class PlaylistInfos(
val _type: String,
val extractor: String,
val extractor_key: String,
val id: String,
val title: String,
val uploader: String,
val uploader_id: String,
val uploader_url: String,
val webpage_url: String,
val webpage_url_basename: String,
val entries: Array<VideoInfo>
){
fun getRandomVideo() = this.entries.get(Random().nextInt(this.entries.size))
}
class DownloaderService: Service() {
private var playlist: PlaylistInfos? = null
private val binder = DownloaderBinder()
private var isStarted = false
inner class DownloaderBinder: Binder() {
fun getService(): DownloaderService = this@DownloaderService
}
private fun customInit(){
if (isStarted) return
// Initialize Youtube-dl
// Thanks to https://github.com/yausername/youtubedl-android
try {
YT.init(application)
} catch (e: YoutubeDLException) {
Log.e("failed to initialize youtubedl-android", e.localizedMessage)
}
try {
FFmpeg.getInstance().init(application)
} catch (e: Exception) {
Log.e("failed to initialize FFmpeg", e.localizedMessage)
}
playlist = downloadPlaylistInfo()
isStarted = true
}
private fun downloadPlaylistInfo(): PlaylistInfos?{
val request = YoutubeDLRequest(PLAYLIST_URL)
request.setOption("--dump-single-json")
request.setOption("--flat-playlist")
try {
return Klaxon().parse<PlaylistInfos>(YT.execute(request).out)
} catch (e: Exception) {
println("Error downloading playlist infos")
println(e)
return null
}
}
fun startDownloading(mediaPlayerService: MediaPlayerService, downloadBar: ProgressBar) {
if (playlist == null){
println("No fucking playlist info")
return
}
Thread(Runnable {
while (true) {
val video = playlist?.getRandomVideo()
val url = video?.url ?: ""
val info = try {
YT.getInfo(url)
} catch(e: YoutubeDLException) {
println("Error getting infos for video, skipping")
println(e)
continue
}
val title = EmojiParser.removeAllEmojis(info.title)
if (title == null){
println("Error with music title, skipping")
continue
}
val pathTemplate = "${applicationContext.filesDir}/${title}--${video?.id}"
val filePath = "$pathTemplate.mp3"
val thumnailPath = "$pathTemplate.jpg"
val request = YoutubeDLRequest(url)
request.setOption("-x")
request.setOption("--audio-format", "mp3")
request.setOption("--write-thumbnail")
request.setOption("--cache-dir", "${applicationContext.filesDir}/.cache")
request.setOption("-o", "$pathTemplate.%(ext)s")
try {
YT.execute(request) { progress, etaInSeconds ->
println("$progress% (ETA $etaInSeconds seconds)")
downloadBar.progress = progress.toInt()
}
mediaPlayerService.addNextSong(VideoUsefulDetails(filePath, info.title, thumnailPath))
} catch (e: Exception) {
println(e)
}
}
}).start()
}
override fun onBind(intent: Intent?): IBinder? {
customInit()
return binder
}
companion object {
/**
* URL to the WBooster playlist
*/
private val PLAYLIST_URL =
"https://www.youtube.com/playlist?list=PLQC73oq6JtgyYNOeifrJXXzZ1-F0Kgmbg"
private val YT = YoutubeDL.getInstance()
}
}
class MediaPlayerService: Service(), MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener {
private lateinit var notificationManager: NotificationManager
private lateinit var notificationBuilder: NotificationCompat.Builder
private var playerThread: Thread? = null
private val binder = MediaPlayerBinder()
private var mMediaPlayer: MediaPlayer? = null
private var playlistQueue: Queue<VideoUsefulDetails> = ConcurrentLinkedQueue<VideoUsefulDetails>()
private var currentSong: VideoUsefulDetails? = null
var titleUpdater = {title: String -> }
var thumbnailUpdater = { path: String ->}
inner class MediaPlayerBinder: Binder() {
fun getService(): MediaPlayerService = this@MediaPlayerService
}
fun addNextSong(details: VideoUsefulDetails) {
this.playlistQueue.add(details)
}
/* Get next music path and update interface */
private fun updateNextSong(){
/* poll next song */
currentSong = playlistQueue.poll()
while (currentSong == null){
Thread.sleep(1000) /* Wait for another element */
currentSong = playlistQueue.poll()
}
// Update notification
notificationBuilder.setContentText(currentSong!!.title)
val notification = notificationBuilder.build()
notification.flags = Notification.FLAG_ONGOING_EVENT
notificationManager.notify(1, notificationBuilder.build())
// Update title
titleUpdater(currentSong!!.title)
// Update image
thumbnailUpdater(currentSong!!.thumbnail)
}
fun startPlaying() {
playerThread?.interrupt()
playerThread = Thread(Runnable {
updateNextSong()
/* launch media player */
mMediaPlayer = MediaPlayer().apply {
setDataSource(currentSong!!.path)
setOnPreparedListener(this@MediaPlayerService)
setOnCompletionListener(this@MediaPlayerService)
prepareAsync() // prepare async to not block main thread
}
})
/* Start media player thread */
playerThread?.start()
}
@RequiresApi(Build.VERSION_CODES.O)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Create Notification channel
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"Foreground music channel",
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(serviceChannel)
val notificationIntent = Intent(this, AppCompatActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0)
notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getText(R.string.app_name))
.setContentText("")
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentIntent(pendingIntent)
startForeground(1, notificationBuilder.build())
return START_NOT_STICKY
}
override fun onBind(intent: Intent?) = binder
/** Called when MediaPlayer is ready */
override fun onPrepared(mediaPlayer: MediaPlayer) {
mediaPlayer.start()
}
override fun onCompletion(mp: MediaPlayer?) {
updateNextSong()
mp?.reset()
mp?.setDataSource(currentSong!!.path)
mp?.prepareAsync()
}
override fun onDestroy() {
super.onDestroy()
mMediaPlayer?.release()
playerThread?.interrupt()
stopSelf()
}
companion object {
val CHANNEL_ID = "ForegroundMediaPlayer"
}
}