Yt Music Fix

This commit is contained in:
shabinder 2021-02-22 23:08:33 +05:30
parent ec89357d4b
commit 1b58bbfcf0
21 changed files with 319 additions and 294 deletions

View File

@ -73,6 +73,7 @@ dependencies {
//DECOMPOSE
implementation(Decompose.decompose)
implementation(Decompose.extensionsCompose)
/*
//Lifecycle
Versions.androidLifecycle.let{

View File

@ -56,6 +56,6 @@
android:value="rzp_live_3ZQeoFYOxjmXye"
/>
<service android:name="com.shabinder.spotiflyer.worker.ForegroundService"/>
<service android:name="com.shabinder.common.di.worker.ForegroundService"/>
</application>
</manifest>

View File

@ -1,14 +1,18 @@
package com.shabinder.android
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material.Surface
import androidx.lifecycle.lifecycleScope
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.jetbrains.rootComponent
import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory
@ -20,12 +24,14 @@ import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.di.createDirectories
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.common.root.SpotiFlyerRootContent
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
import com.shabinder.common.ui.SpotiFlyerTheme
import com.shabinder.common.ui.colorOffWhite
import com.shabinder.database.Database
import com.tonyodev.fetch2.Status
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -44,6 +50,8 @@ class MainActivity : ComponentActivity() {
//TODO pass updates from Foreground Service
private val downloadFlow = MutableStateFlow(hashMapOf<String, DownloadStatus>())
private lateinit var updateUIReceiver: BroadcastReceiver
private lateinit var queryReceiver: BroadcastReceiver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -95,6 +103,71 @@ class MainActivity : ComponentActivity() {
}
}
private fun initializeBroadcast(){
val intentFilter = IntentFilter().apply {
addAction(Status.QUEUED.name)
addAction(Status.FAILED.name)
addAction(Status.DOWNLOADING.name)
addAction("Progress")
addAction("Converting")
addAction("track_download_completed")
}
updateUIReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
//Update Flow with latest details
if (intent != null) {
val trackDetails = intent.getParcelableExtra<TrackDetails?>("track")
trackDetails?.let { track ->
lifecycleScope.launch {
val latestMap = downloadFlow.value.apply {
this[track.title] = when (intent.action) {
Status.QUEUED.name -> DownloadStatus.Queued
Status.FAILED.name -> DownloadStatus.Failed
Status.DOWNLOADING.name -> DownloadStatus.Downloading()
"Progress" -> DownloadStatus.Downloading(intent.getIntExtra("progress", 0))
"Converting" -> DownloadStatus.Converting
"track_download_completed" -> DownloadStatus.Downloaded
else -> DownloadStatus.NotDownloaded
}
}
downloadFlow.emit(latestMap)
}
}
}
}
}
val queryFilter = IntentFilter().apply { addAction("query_result") }
queryReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
//UI update here
if (intent != null){
@Suppress("UNCHECKED_CAST")
val trackList = intent.getSerializableExtra("tracks") as? HashMap<String, DownloadStatus>?
trackList?.let { list ->
Log.i("Service Response", "${list.size} Tracks Active")
lifecycleScope.launch {
downloadFlow.emit(list)
}
}
}
}
}
registerReceiver(updateUIReceiver, intentFilter)
registerReceiver(queryReceiver, queryFilter)
}
override fun onResume() {
super.onResume()
initializeBroadcast()
}
override fun onPause() {
super.onPause()
unregisterReceiver(updateUIReceiver)
unregisterReceiver(queryReceiver)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
handleIntentFromExternalActivity(intent)

View File

@ -1,7 +1,26 @@
package com.shabinder.android.di
import com.shabinder.common.database.appContext
import com.tonyodev.fetch2.Fetch
import com.tonyodev.fetch2.FetchConfiguration
import org.koin.dsl.module
val appModule = module {
single { createFetchInstance() }
}
private fun createFetchInstance():Fetch{
val fetchConfiguration =
FetchConfiguration.Builder(appContext).run {
setNamespace("ForegroundDownloaderService")
setDownloadConcurrentLimit(4)
build()
}
return Fetch.run {
setDefaultInstanceConfiguration(fetchConfiguration)
getDefaultInstance()
}.apply {
removeAll() //Starting fresh
}
}

View File

@ -12,6 +12,7 @@ import android.provider.Settings
import com.github.javiersantos.appupdater.AppUpdater
import com.github.javiersantos.appupdater.enums.Display
import com.github.javiersantos.appupdater.enums.UpdateFrom
import com.tonyodev.fetch2.Fetch
fun Activity.checkIfLatestVersion() {
AppUpdater(this,0).run {

View File

@ -10,6 +10,7 @@ allprojects {
maven(url = "https://jitpack.io")
maven(url = "https://dl.bintray.com/ekito/koin")
maven(url = "https://kotlin.bintray.com/kotlinx/")
maven(url = "https://dl.bintray.com/icerockdev/moko")
maven(url = "https://kotlin.bintray.com/kotlin-js-wrappers/")
maven(url = "https://maven.pkg.jetbrains.space/public/p/compose/dev")
}

View File

@ -29,7 +29,7 @@ object Versions {
const val minSdkVersion = 24
const val compileSdkVersion = 30
const val targetSdkVersion = 29
const val androidLifecycle = "2.3.0-rc01"
const val androidLifecycle = "2.3.0"
}
object Koin {
val core = "org.koin:koin-core:${Versions.koin}"
@ -123,7 +123,7 @@ object Extras {
const val kermit = "co.touchlab:kermit:${Versions.kermit}"
object Android {
val razorpay = "com.razorpay:checkout:1.6.4"
val fetch = "androidx.tonyodev.fetch2:xfetch2:3.1.5"
val fetch = "androidx.tonyodev.fetch2:xfetch2:3.1.6"
val appUpdator = "com.github.amitbd1508:AppUpdater:4.1.0"
}
}

View File

@ -70,14 +70,13 @@ internal class SpotiFlyerListStoreProvider(
}
dispatch(Result.UpdateTrackList(list.toMutableList().updateTracksStatuses(downloadProgressFlow.value)))
}
is Intent.StartDownload -> {
downloadTracks(listOf(intent.track),fetchQuery.youtubeMusic::getYTIDBestMatch,dir::saveFileWithMetadata)
dispatch(Result.UpdateTrackItem(intent.track.apply { downloaded = DownloadStatus.Queued }))
}
}
}
}
private object ReducerImpl : Reducer<State, Result> {
override fun State.reduce(result: Result): State =
when (result) {

View File

@ -1,6 +1,7 @@
plugins {
id("multiplatform-compose-setup")
id("android-setup")
id("kotlin-parcelize")
kotlin("plugin.serialization")
}
@ -8,6 +9,7 @@ kotlin {
sourceSets {
commonMain {
dependencies {
api("dev.icerock.moko:parcelize:0.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
}
}

View File

@ -17,8 +17,11 @@
package com.shabinder.common.models
import com.shabinder.common.models.spotify.Source
import dev.icerock.moko.parcelize.Parcelable
import dev.icerock.moko.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Parcelize
@Serializable
data class TrackDetails(
var title:String,
@ -36,14 +39,15 @@ data class TrackDetails(
var progress: Int = 2,//2 for visual progress bar hint
var outputFilePath: String,
var videoID:String? = null
)
):Parcelable
@Serializable
sealed class DownloadStatus {
object Downloaded :DownloadStatus()
data class Downloading(val progress: Int = 0):DownloadStatus()
object Queued :DownloadStatus()
object NotDownloaded :DownloadStatus()
object Converting :DownloadStatus()
object Failed :DownloadStatus()
sealed class DownloadStatus:Parcelable {
@Parcelize object Downloaded :DownloadStatus()
@Parcelize data class Downloading(val progress: Int = 2):DownloadStatus()
@Parcelize object Queued :DownloadStatus()
@Parcelize object NotDownloaded :DownloadStatus()
@Parcelize object Converting :DownloadStatus()
@Parcelize object Failed :DownloadStatus()
}

View File

@ -32,6 +32,8 @@ kotlin {
androidMain {
dependencies{
implementation(Ktor.clientAndroid)
implementation(Extras.Android.fetch)
implementation(Koin.android)
api(files("$rootDir/libs/mobile-ffmpeg.aar"))
}
}

View File

@ -3,7 +3,12 @@ package com.shabinder.common.di
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.core.content.ContextCompat
import com.github.kiulian.downloader.model.YoutubeVideo
import com.github.kiulian.downloader.model.formats.Format
import com.github.kiulian.downloader.model.quality.AudioQuality
import com.shabinder.common.database.appContext
import com.shabinder.common.di.worker.ForegroundService
import com.shabinder.common.models.TrackDetails
actual fun openPlatform(packageID:String, platformLink:String){
@ -41,5 +46,25 @@ actual suspend fun downloadTracks(
getYTIDBestMatch:suspend (String,TrackDetails)->String?,
saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit
){
//TODO
if(!list.isNullOrEmpty()){
val serviceIntent = Intent(appContext, ForegroundService::class.java)
serviceIntent.putParcelableArrayListExtra("object",ArrayList<TrackDetails>(list))
appContext.let { ContextCompat.startForegroundService(it, serviceIntent) }
}
}
fun YoutubeVideo.getData(): Format?{
return try {
findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
} catch (e: java.lang.IndexOutOfBoundsException) {
try {
findAudioWithQuality(AudioQuality.high)?.get(0) as Format
} catch (e: java.lang.IndexOutOfBoundsException) {
try {
findAudioWithQuality(AudioQuality.low)?.get(0) as Format
} catch (e: java.lang.IndexOutOfBoundsException) {
null
}
}
}
}

View File

@ -3,6 +3,7 @@ package com.shabinder.common.di
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.MediaScannerConnection
import android.os.Environment
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
@ -95,6 +96,7 @@ actual class Dir actual constructor(
.removeAllTags()
.setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails)
addToLibrary(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3")
}
}
Config.RETURN_CODE_CANCEL -> {
@ -107,6 +109,13 @@ actual class Dir actual constructor(
}
}
actual fun addToLibrary(path:String) {
logger.d{"Scanning File"}
MediaScannerConnection.scanFile(
appContext,
listOf(path).toTypedArray(), null,null)
}
actual suspend fun loadImage(url: String): ImageBitmap? {
val cachePath = imageCacheDir() + getNameURL(url)
return (loadCachedImage(cachePath) ?: freshImage(url))?.asImageBitmap()

View File

@ -34,7 +34,7 @@ actual class YoutubeProvider actual constructor(
private val logger: Kermit,
private val dir: Dir,
){
private val ytDownloader: YoutubeDownloader = YoutubeDownloader()
val ytDownloader: YoutubeDownloader = YoutubeDownloader()
/*
* YT Album Art Schema
* HI-RES Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"

View File

@ -14,7 +14,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.shabinder.android.worker
package com.shabinder.common.di.worker
import android.annotation.SuppressLint
import android.app.*
@ -24,42 +24,30 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.*
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import com.arthenica.mobileffmpeg.Config
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS
import com.arthenica.mobileffmpeg.FFmpeg
import co.touchlab.kermit.Kermit
import com.github.kiulian.downloader.YoutubeDownloader
import com.mpatric.mp3agic.Mp3File
import com.shabinder.android.worker.removeAllTags
import com.shabinder.android.worker.setId3v1Tags
import com.shabinder.android.worker.setId3v2Tags
import com.shabinder.spotiflyer.di.Directories
import com.shabinder.spotiflyer.models.DownloadStatus
import com.shabinder.spotiflyer.models.TrackDetails
import com.shabinder.spotiflyer.models.spotify.Source
import com.shabinder.spotiflyer.networking.YoutubeMusicApi
import com.shabinder.spotiflyer.networking.makeJsonBody
import com.shabinder.spotiflyer.providers.getYTTracks
import com.shabinder.spotiflyer.providers.sortByBestMatch
import com.shabinder.spotiflyer.utils.*
import com.shabinder.common.di.Dir
import com.shabinder.common.di.FetchPlatformQueryResult
import com.shabinder.common.di.getData
import com.shabinder.common.models.DownloadStatus
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.ui.R.*
import com.tonyodev.fetch2.*
import com.tonyodev.fetch2core.DownloadBlock
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import org.koin.android.ext.android.inject
import java.io.File
import java.util.*
import kotlin.coroutines.CoroutineContext
class ForegroundService : Service(){
private val tag = "Foreground Service"
class ForegroundService : Service(),CoroutineScope{
private val tag: String = "Foreground Service"
private val channelId = "ForegroundDownloaderService"
private val notificationId = 101
private var total = 0 //Total Downloads Requested
@ -69,29 +57,33 @@ class ForegroundService : Service(){
private val isFinished: Boolean
get() = converted + failed == total
private var isSingleDownload: Boolean = false
private val serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
private lateinit var serviceJob :Job
override val coroutineContext: CoroutineContext
get() = serviceJob + Dispatchers.IO
private val requestMap = hashMapOf<Request, TrackDetails>()
private val allTracksStatus = hashMapOf<String,DownloadStatus>()
private val allTracksStatus = hashMapOf<String, DownloadStatus>()
private var wakeLock: PowerManager.WakeLock? = null
private var isServiceStarted = false
private var messageList = mutableListOf("", "", "", "","")
private lateinit var cancelIntent:PendingIntent
private lateinit var fetch:Fetch
private lateinit var downloadManager : DownloadManager
@Inject lateinit var ytDownloader: YoutubeDownloader
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi
@Inject lateinit var directories:Directories
private val defaultDir
get() = directories.defaultDir()
private val imageDir
get() = directories.imageDir()
private val fetcher: FetchPlatformQueryResult by inject()
private val logger: Kermit by inject()
private val fetch: Fetch by inject()
private val dir: Dir by inject()
private val ytDownloader:YoutubeDownloader
get() = fetcher.youtubeProvider.ytDownloader
override fun onBind(intent: Intent): IBinder? = null
@SuppressLint("UnspecifiedImmutableFlag")
override fun onCreate() {
super.onCreate()
serviceJob = SupervisorJob()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel(channelId,"Downloader Service")
}
@ -101,14 +93,15 @@ class ForegroundService : Service(){
).apply{action = "kill"}
cancelIntent = PendingIntent.getService (this, 0 , intent , FLAG_CANCEL_CURRENT )
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
initialiseFetch()
fetch.removeAllListeners().addListener(fetchListener)
}
@SuppressLint("WakelockTimeout")
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Send a notification that service is started
log(tag, "Service Started.")
Log.i(tag,"Foreground Service Started.")
startForeground(notificationId, getNotification())
intent?.let{
when (it.action) {
"kill" -> killService()
@ -126,15 +119,6 @@ class ForegroundService : Service(){
val downloadObjects: ArrayList<TrackDetails>? = (it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList(
"object"
))
val imagesList: ArrayList<String>? = (it.getStringArrayListExtra("imagesList") ?: it.extras?.getStringArrayList(
"imagesList"
))
imagesList?.let{ imageList ->
serviceScope.launch {
downloadAllImages(imageList)
}
}
downloadObjects?.let { list ->
downloadObjects.size.let { size ->
@ -153,8 +137,8 @@ class ForegroundService : Service(){
//Service Already Started
START_STICKY
} else{
log(tag, "Starting the foreground service task")
isServiceStarted = true
Log.i(tag,"Starting the foreground service task")
wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply {
@ -170,74 +154,51 @@ class ForegroundService : Service(){
**/
private fun downloadAllTracks(trackList: List<TrackDetails>) {
trackList.forEach {
serviceScope.launch {
launch {
if (!it.videoID.isNullOrBlank()) {//Video ID already known!
downloadTrack(it.videoID!!, it)
} else {
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
val jsonBody = makeJsonBody(searchQuery.trim()).toJsonString()
youtubeMusicApi.getYoutubeMusicResponse(jsonBody).enqueue(
object : Callback<String> {
override fun onResponse(
call: Call<String>,
response: Response<String>
) {
serviceScope.launch {
val videoId = sortByBestMatch(
getYTTracks(response.body().toString()),
trackName = it.title,
trackArtists = it.artists,
trackDurationSec = it.durationSec
).keys.firstOrNull()
log("Service VideoID", videoId ?: "Not Found")
if (videoId.isNullOrBlank()) {
val videoID = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery,it)
logger.d("Service VideoID") { videoID ?: "Not Found" }
if (videoID.isNullOrBlank()) {
sendTrackBroadcast(Status.FAILED.name, it)
failed++
updateNotification()
allTracksStatus[it.title] = DownloadStatus.Failed
} else {//Found Youtube Video ID
downloadTrack(videoId, it)
downloadTrack(videoID, it)
}
}
}
override fun onFailure(call: Call<String>, t: Throwable) {
if (t.message.toString()
.contains("Failed to connect")
) showDialog("Failed, Check Your Internet Connection!")
log("YT API Req. Fail", t.message.toString())
}
}
)
}
}
}
}
fun downloadTrack(videoID:String,track: TrackDetails){
serviceScope.launch(Dispatchers.IO) {
private fun downloadTrack(videoID:String, track: TrackDetails){
launch {
try {
val audioData = ytDownloader.getVideo(videoID).getData()
audioData?.let {
val url: String = it.url()
log("DHelper Link Found", url)
val request= Request(url, track.outputFile).apply{
logger.d("DHelper Link Found") { url }
val request= Request(url, track.outputFilePath).apply{
priority = Priority.NORMAL
networkType = NetworkType.ALL
}
fetch.enqueue(request,
{ request1 ->
requestMap[request1] = track
log(tag, "Enqueuing Download")
logger.d(tag){"Enqueuing Download"}
},
{ error ->
log(tag, "Enqueuing Error:${error.throwable.toString()}")
logger.d(tag){"Enqueuing Error:${error.throwable.toString()}"}
}
)
}
}catch (e: java.lang.Exception){
log("Service YT Error", e.message.toString())
logger.d("Service YT Error"){e.message.toString()}
}
}
}
@ -266,12 +227,12 @@ class ForegroundService : Service(){
downloadBlocks: List<DownloadBlock>,
totalBlocks: Int
) {
serviceScope.launch {
launch {
val track = requestMap[download.request]
addToNotification("Downloading ${track?.title}")
log(tag, "${track?.title} Download Started")
logger.d(tag){"${track?.title} Download Started"}
track?.let{
allTracksStatus[it.title] = DownloadStatus.Downloading
allTracksStatus[it.title] = DownloadStatus.Downloading()
sendTrackBroadcast(Status.DOWNLOADING.name,track)
}
}
@ -290,20 +251,20 @@ class ForegroundService : Service(){
}
override fun onCompleted(download: Download) {
serviceScope.launch {
launch {
val track = requestMap[download.request]
removeFromNotification("Downloading ${track?.title}")
try{
track?.let {
convertToMp3(download.file, it)
dir.saveFileWithMetadata(byteArrayOf(),it)
allTracksStatus[it.title] = DownloadStatus.Converting
}
log(tag, "${track?.title} Download Completed")
logger.d(tag){"${track?.title} Download Completed"}
}catch (
e: KotlinNullPointerException
){
log(tag, "${track?.title} Download Failed! Error:Fetch!!!!")
log(tag, "${track?.title} Requesting Download thru Android DM")
logger.d(tag){"${track?.title} Download Failed! Error:Fetch!!!!"}
logger.d(tag){"${track?.title} Requesting Download thru Android DM"}
downloadUsingDM(download.request.url, download.request.file, track!!)
downloaded++
requestMap.remove(download.request)
@ -324,11 +285,11 @@ class ForegroundService : Service(){
}
override fun onError(download: Download, error: Error, throwable: Throwable?) {
serviceScope.launch {
launch {
val track = requestMap[download.request]
downloaded++
log(tag, download.error.throwable.toString())
log(tag, "${track?.title} Requesting Download thru Android DM")
logger.d(tag){download.error.throwable.toString()}
logger.d(tag){"${track?.title} Requesting Download thru Android DM"}
downloadUsingDM(download.request.url, download.request.file, track!!)
requestMap.remove(download.request)
removeFromNotification("Downloading ${track.title}")
@ -345,24 +306,28 @@ class ForegroundService : Service(){
etaInMilliSeconds: Long,
downloadedBytesPerSecond: Long
) {
serviceScope.launch {
val track = requestMap[download.request]
log(tag, "${track?.title} ETA: ${etaInMilliSeconds / 1000} sec")
launch {
requestMap[download.request]?.run {
allTracksStatus[title] = DownloadStatus.Downloading(download.progress)
logger.d(tag){"${title} ETA: ${etaInMilliSeconds / 1000} sec"}
val intent = Intent().apply {
action = "Progress"
putExtra("progress", download.progress)
putExtra("track", requestMap[download.request])
putExtra("track", this@run)
}
sendBroadcast(intent)
}
}
}
}
/**
* If fetch Fails , Android Download Manager To RESCUE!!
**/
fun downloadUsingDM(url: String, outputDir: String, track: TrackDetails){
serviceScope.launch {
launch {
val uri = Uri.parse(url)
val request = DownloadManager.Request(uri).apply {
setAllowedNetworkTypes(
@ -378,7 +343,7 @@ class ForegroundService : Service(){
//Start Download
val downloadID = downloadManager.enqueue(request)
log("DownloadManager", "Download Request Sent")
logger.d("DownloadManager"){"Download Request Sent"}
val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
@ -387,8 +352,7 @@ class ForegroundService : Service(){
//Checking if the received broadcast is for our enqueued download by matching download id
if (downloadID == id) {
allTracksStatus[track.title] = DownloadStatus.Converting
convertToMp3(outputDir, track)
converted++
launch { dir.saveFileWithMetadata(byteArrayOf(),track);converted++ }
//Unregister this broadcast Receiver
this@ForegroundService.unregisterReceiver(this)
}
@ -398,69 +362,7 @@ class ForegroundService : Service(){
}
}
/**
*Converting Downloaded Audio (m4a) to Mp3.( Also Applying Metadata)
**/
fun convertToMp3(filePath: String, track: TrackDetails){
serviceScope.launch {
sendTrackBroadcast("Converting",track)
val m4aFile = File(filePath)
addToNotification("Processing ${track.title}")
FFmpeg.executeAsync(
"-i $filePath -y -b:a 160k -acodec libmp3lame -vn ${filePath.substringBeforeLast('.') + ".mp3"}"
) { _, returnCode ->
when (returnCode) {
RETURN_CODE_SUCCESS -> {
log(Config.TAG, "Async command execution completed successfully.")
removeFromNotification("Processing ${track.title}")
m4aFile.delete()
writeMp3Tags(filePath.substringBeforeLast('.') + ".mp3", track)
//FFMPEG task Completed
}
RETURN_CODE_CANCEL -> {
log(Config.TAG, "Async command execution cancelled by user.")
}
else -> {
log(
Config.TAG, String.format(
"Async command execution failed with rc=%d.",
returnCode
)
)
}
}
}
}
}
@Suppress("BlockingMethodInNonBlockingContext")
private fun writeMp3Tags(filePath: String, track: TrackDetails){
serviceScope.launch {
var mp3File = Mp3File(filePath)
mp3File = removeAllTags(mp3File)
mp3File = setId3v1Tags(mp3File, track)
mp3File = setId3v2Tags(mp3File, track,this@ForegroundService)
log("Mp3Tags", "saving file")
mp3File.save(filePath.substringBeforeLast('.') + ".new.mp3")
val file = File(filePath)
file.delete()
val newFile = File((filePath.substringBeforeLast('.') + ".new.mp3"))
newFile.renameTo(file)
converted++
updateNotification()
addToLibrary(file.absolutePath)
allTracksStatus.remove(track.title)
//Notify Download Completed
sendTrackBroadcast("track_download_completed",track)
//All tasks completed (REST IN PEACE)
if(isFinished && !isSingleDownload){
delay(5000)
onDestroy()
}
}
}
/**
* This is the method that can be called to update the Notification
@ -472,7 +374,7 @@ class ForegroundService : Service(){
}
private fun releaseWakeLock() {
log(tag, "Releasing Wake Lock")
logger.d(tag){"Releasing Wake Lock"}
try {
wakeLock?.let {
if (it.isHeld) {
@ -480,7 +382,7 @@ class ForegroundService : Service(){
}
}
} catch (e: Exception) {
log(tag, "Service stopped without being started: ${e.message}")
logger.d(tag){"Service stopped without being started: ${e.message}"}
}
isServiceStarted = false
}
@ -501,7 +403,7 @@ class ForegroundService : Service(){
* Cleaning All Residual Files except Mp3 Files
**/
private fun cleanFiles(dir: File) {
log(tag, "Starting Cleaning in ${dir.path} ")
logger.d(tag){"Starting Cleaning in ${dir.path} "}
val fList = dir.listFiles()
fList?.let {
for (file in fList) {
@ -509,7 +411,7 @@ class ForegroundService : Service(){
cleanFiles(file)
} else if(file.isFile) {
if(file.path.toString().substringAfterLast(".") != "mp3"){
log(tag, "Cleaning ${file.path}")
logger.d(tag){ "Cleaning ${file.path}"}
file.delete()
}
}
@ -517,76 +419,18 @@ class ForegroundService : Service(){
}
}
/*
* Add File to Android's Media Library.
* */
private fun addToLibrary(path:String) {
log(tag,"Scanning File")
MediaScannerConnection.scanFile(this,
listOf(path).toTypedArray(), null,null)
}
/**
* Function to fetch all Images for use in mp3 tags.
**/
suspend fun downloadAllImages(urlList: ArrayList<String>, func: ((resource:File) -> Unit)? = null) {
/*
* Last Element of this List defines Its Source
* */
val source = urlList.last()
log("Image","Fetching All ")
for (url in urlList.subList(0, urlList.size - 1)) {
log("Image","Fetching")
val imgUri = url.toUri().buildUpon().scheme("https").build()
val r = ImageRequest.Builder(this@ForegroundService)
.data(imgUri)
.build()
val bitmap = Coil.execute(r).drawable?.toBitmap()
val file = when (source) {
Source.Spotify.name -> {
File(imageDir, url.substringAfterLast('/') + ".jpeg")
}
Source.YouTube.name -> {
File(
imageDir,
url.substringBeforeLast('/', url)
.substringAfterLast(
'/',
url
) + ".jpeg"
)
}
Source.Gaana.name -> {
File(
imageDir,
(url.substringBeforeLast('/').substringAfterLast(
'/'
)) + ".jpeg"
)
}
else -> File(imageDir, url.substringAfterLast('/') + ".jpeg")
}
if (bitmap != null) {
file.writeBitmap(bitmap)
func?.let { it(file) }
log("Image","Saved")
} else log("Foreground Service", "Album Art Could Not be Fetched")
}
}
private fun killService() {
serviceScope.launch{
log(tag,"Killing Self")
launch{
logger.d(tag){"Killing Self"}
messageList = mutableListOf("Cleaning And Exiting","","","","")
fetch.cancelAll()
fetch.removeAll()
updateNotification()
cleanFiles(File(defaultDir))
cleanFiles(File(imageDir))
cleanFiles(File(dir.defaultDir()))
cleanFiles(File(dir.imageCacheDir()))
messageList = mutableListOf("","","","","")
releaseWakeLock()
serviceJob.cancel()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
stopForeground(true)
} else {
@ -609,25 +453,8 @@ class ForegroundService : Service(){
}
}
private fun initialiseFetch() {
val fetchConfiguration =
FetchConfiguration.Builder(this).run {
setNamespace(channelId)
setDownloadConcurrentLimit(4)
build()
}
fetch = Fetch.run {
setDefaultInstanceConfiguration(fetchConfiguration)
getDefaultInstance()
}.apply {
addListener(fetchListener)
removeAll() //Starting fresh
}
}
private fun getNotification():Notification = NotificationCompat.Builder(this, channelId).run {
setSmallIcon(R.drawable.ic_download_arrow)
setSmallIcon(drawable.ic_download_arrow)
setContentTitle("Total: $total Completed:$converted Failed:$failed")
setSilent(true)
setStyle(
@ -639,7 +466,7 @@ class ForegroundService : Service(){
addLine(messageList[messageList.size - 5])
}
)
addAction(R.drawable.ic_round_cancel_24,"Exit",cancelIntent)
addAction(drawable.ic_round_cancel_24,"Exit",cancelIntent)
build()
}
@ -661,3 +488,10 @@ class ForegroundService : Service(){
this@ForegroundService.sendBroadcast(intent)
}
}
private fun Fetch.removeAllListeners():Fetch{
for (listener in this.getListenerSet()) {
this.removeListener(listener)
}
return this
}

View File

@ -25,6 +25,7 @@ expect class Dir(
suspend fun loadImage(url:String): ImageBitmap?
suspend fun clearCache()
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, trackDetails: TrackDetails)
fun addToLibrary(path:String)
}
suspend fun downloadFile(url: String): Flow<DownloadResult> {

View File

@ -12,7 +12,7 @@ import kotlinx.coroutines.withContext
class FetchPlatformQueryResult(
private val gaanaProvider: GaanaProvider,
private val spotifyProvider: SpotifyProvider,
private val youtubeProvider: YoutubeProvider,
val youtubeProvider: YoutubeProvider,
val youtubeMusic: YoutubeMusic,
private val database: Database
) {

View File

@ -27,7 +27,7 @@ class YoutubeMusic constructor(
trackDurationSec = trackDetails.durationSec
).keys.firstOrNull()
}
suspend fun getYTTracks(query: String):List<YoutubeTrack>{
private suspend fun getYTTracks(query: String):List<YoutubeTrack>{
val youtubeTracks = mutableListOf<YoutubeTrack>()
val responseObj = Json.parseToJsonElement(getYoutubeMusicResponse(query))
@ -121,7 +121,8 @@ class YoutubeMusic constructor(
! other constituents of a result block will lead to errors, hence the 'in
! result[:-1] ,i.e., skip last element in array '
*/
for(detail in result.subList(0,result.size-1)){
for(detailArray in result.subList(0,result.size-1)){
for(detail in detailArray.jsonArray){
if(detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"]?.jsonObject?.size?:0 < 2) continue
// if not a dummy, collect All Variables
@ -137,7 +138,8 @@ class YoutubeMusic constructor(
}
}
}
// log("YT Music details",availableDetails.toString())
}
//logger.d("YT Music details"){availableDetails.toString()}
/*
! Filter Out non-Song/Video results and incomplete results here itself
! From what we know about detail order, note that [1] - indicate result type
@ -171,7 +173,7 @@ class YoutubeMusic constructor(
return youtubeTracks
}
fun sortByBestMatch(
private fun sortByBestMatch(
ytTracks:List<YoutubeTrack>,
trackName:String,
trackArtists:List<String>,
@ -240,7 +242,7 @@ class YoutubeMusic constructor(
val avgMatch = (artistMatch + durationMatch)/2
linksWithMatchValue[result.videoId.toString()] = avgMatch.toInt()
}
//log("YT Api Result", "$trackName - $linksWithMatchValue")
logger.d("YT Api Result"){"$trackName - $linksWithMatchValue"}
return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap()
}

View File

@ -75,7 +75,7 @@ actual class Dir actual constructor(private val logger: Kermit) {
.setId3v1Tags(trackDetails)
.setId3v2TagsAndSaveFile(trackDetails)
}
actual fun addToLibrary(path:String){}
actual suspend fun loadImage(url: String): ImageBitmap? {
val cachePath = imageCacheDir() + getNameURL(url)
var picture: ImageBitmap? = loadCachedImage(cachePath)

View File

@ -0,0 +1,26 @@
<!--
~ Copyright (c) 2021 Shabinder Singh
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,4c4.41,0 8,3.59 8,8s-3.59,8 -8,8s-8,-3.59 -8,-8S7.59,4 12,4M12,2C6.48,2 2,6.48 2,12c0,5.52 4.48,10 10,10c5.52,0 10,-4.48 10,-10C22,6.48 17.52,2 12,2L12,2zM13,12l0,-4h-2l0,4H8l4,4l4,-4H13z"/>
</vector>

View File

@ -0,0 +1,26 @@
<!--
~ Copyright (c) 2021 Shabinder Singh
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM16.3,16.3c-0.39,0.39 -1.02,0.39 -1.41,0L12,13.41 9.11,16.3c-0.39,0.39 -1.02,0.39 -1.41,0 -0.39,-0.39 -0.39,-1.02 0,-1.41L10.59,12 7.7,9.11c-0.39,-0.39 -0.39,-1.02 0,-1.41 0.39,-0.39 1.02,-0.39 1.41,0L12,10.59l2.89,-2.89c0.39,-0.39 1.02,-0.39 1.41,0 0.39,0.39 0.39,1.02 0,1.41L13.41,12l2.89,2.89c0.38,0.38 0.38,1.02 0,1.41z"/>
</vector>