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 ){ 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(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 = ConcurrentLinkedQueue() 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" } }