276 lines
8.5 KiB
Kotlin
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"
|
|
}
|
|
}
|