mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-22 17:14:32 +01:00
Yt Music Fix
This commit is contained in:
parent
ec89357d4b
commit
1b58bbfcf0
@ -73,6 +73,7 @@ dependencies {
|
|||||||
//DECOMPOSE
|
//DECOMPOSE
|
||||||
implementation(Decompose.decompose)
|
implementation(Decompose.decompose)
|
||||||
implementation(Decompose.extensionsCompose)
|
implementation(Decompose.extensionsCompose)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
//Lifecycle
|
//Lifecycle
|
||||||
Versions.androidLifecycle.let{
|
Versions.androidLifecycle.let{
|
||||||
|
@ -56,6 +56,6 @@
|
|||||||
android:value="rzp_live_3ZQeoFYOxjmXye"
|
android:value="rzp_live_3ZQeoFYOxjmXye"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<service android:name="com.shabinder.spotiflyer.worker.ForegroundService"/>
|
<service android:name="com.shabinder.common.di.worker.ForegroundService"/>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
@ -1,14 +1,18 @@
|
|||||||
package com.shabinder.android
|
package com.shabinder.android
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import android.util.Log
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.arkivanov.decompose.ComponentContext
|
import com.arkivanov.decompose.ComponentContext
|
||||||
import com.arkivanov.decompose.extensions.compose.jetbrains.rootComponent
|
import com.arkivanov.decompose.extensions.compose.jetbrains.rootComponent
|
||||||
import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory
|
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.FetchPlatformQueryResult
|
||||||
import com.shabinder.common.di.createDirectories
|
import com.shabinder.common.di.createDirectories
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.shabinder.common.root.SpotiFlyerRoot
|
import com.shabinder.common.root.SpotiFlyerRoot
|
||||||
import com.shabinder.common.root.SpotiFlyerRootContent
|
import com.shabinder.common.root.SpotiFlyerRootContent
|
||||||
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
|
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
|
||||||
import com.shabinder.common.ui.SpotiFlyerTheme
|
import com.shabinder.common.ui.SpotiFlyerTheme
|
||||||
import com.shabinder.common.ui.colorOffWhite
|
import com.shabinder.common.ui.colorOffWhite
|
||||||
import com.shabinder.database.Database
|
import com.shabinder.database.Database
|
||||||
|
import com.tonyodev.fetch2.Status
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@ -44,6 +50,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
//TODO pass updates from Foreground Service
|
//TODO pass updates from Foreground Service
|
||||||
private val downloadFlow = MutableStateFlow(hashMapOf<String, DownloadStatus>())
|
private val downloadFlow = MutableStateFlow(hashMapOf<String, DownloadStatus>())
|
||||||
|
|
||||||
|
private lateinit var updateUIReceiver: BroadcastReceiver
|
||||||
|
private lateinit var queryReceiver: BroadcastReceiver
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
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?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
handleIntentFromExternalActivity(intent)
|
handleIntentFromExternalActivity(intent)
|
||||||
|
@ -1,7 +1,26 @@
|
|||||||
package com.shabinder.android.di
|
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
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val appModule = 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
|
||||||
|
}
|
||||||
}
|
}
|
@ -12,6 +12,7 @@ import android.provider.Settings
|
|||||||
import com.github.javiersantos.appupdater.AppUpdater
|
import com.github.javiersantos.appupdater.AppUpdater
|
||||||
import com.github.javiersantos.appupdater.enums.Display
|
import com.github.javiersantos.appupdater.enums.Display
|
||||||
import com.github.javiersantos.appupdater.enums.UpdateFrom
|
import com.github.javiersantos.appupdater.enums.UpdateFrom
|
||||||
|
import com.tonyodev.fetch2.Fetch
|
||||||
|
|
||||||
fun Activity.checkIfLatestVersion() {
|
fun Activity.checkIfLatestVersion() {
|
||||||
AppUpdater(this,0).run {
|
AppUpdater(this,0).run {
|
||||||
|
@ -10,6 +10,7 @@ allprojects {
|
|||||||
maven(url = "https://jitpack.io")
|
maven(url = "https://jitpack.io")
|
||||||
maven(url = "https://dl.bintray.com/ekito/koin")
|
maven(url = "https://dl.bintray.com/ekito/koin")
|
||||||
maven(url = "https://kotlin.bintray.com/kotlinx/")
|
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://kotlin.bintray.com/kotlin-js-wrappers/")
|
||||||
maven(url = "https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
maven(url = "https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ object Versions {
|
|||||||
const val minSdkVersion = 24
|
const val minSdkVersion = 24
|
||||||
const val compileSdkVersion = 30
|
const val compileSdkVersion = 30
|
||||||
const val targetSdkVersion = 29
|
const val targetSdkVersion = 29
|
||||||
const val androidLifecycle = "2.3.0-rc01"
|
const val androidLifecycle = "2.3.0"
|
||||||
}
|
}
|
||||||
object Koin {
|
object Koin {
|
||||||
val core = "org.koin:koin-core:${Versions.koin}"
|
val core = "org.koin:koin-core:${Versions.koin}"
|
||||||
@ -123,7 +123,7 @@ object Extras {
|
|||||||
const val kermit = "co.touchlab:kermit:${Versions.kermit}"
|
const val kermit = "co.touchlab:kermit:${Versions.kermit}"
|
||||||
object Android {
|
object Android {
|
||||||
val razorpay = "com.razorpay:checkout:1.6.4"
|
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"
|
val appUpdator = "com.github.amitbd1508:AppUpdater:4.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,14 +70,13 @@ internal class SpotiFlyerListStoreProvider(
|
|||||||
}
|
}
|
||||||
dispatch(Result.UpdateTrackList(list.toMutableList().updateTracksStatuses(downloadProgressFlow.value)))
|
dispatch(Result.UpdateTrackList(list.toMutableList().updateTracksStatuses(downloadProgressFlow.value)))
|
||||||
}
|
}
|
||||||
|
|
||||||
is Intent.StartDownload -> {
|
is Intent.StartDownload -> {
|
||||||
|
downloadTracks(listOf(intent.track),fetchQuery.youtubeMusic::getYTIDBestMatch,dir::saveFileWithMetadata)
|
||||||
dispatch(Result.UpdateTrackItem(intent.track.apply { downloaded = DownloadStatus.Queued }))
|
dispatch(Result.UpdateTrackItem(intent.track.apply { downloaded = DownloadStatus.Queued }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private object ReducerImpl : Reducer<State, Result> {
|
private object ReducerImpl : Reducer<State, Result> {
|
||||||
override fun State.reduce(result: Result): State =
|
override fun State.reduce(result: Result): State =
|
||||||
when (result) {
|
when (result) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("multiplatform-compose-setup")
|
id("multiplatform-compose-setup")
|
||||||
id("android-setup")
|
id("android-setup")
|
||||||
|
id("kotlin-parcelize")
|
||||||
kotlin("plugin.serialization")
|
kotlin("plugin.serialization")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -8,6 +9,7 @@ kotlin {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
|
api("dev.icerock.moko:parcelize:0.6.0")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,8 +17,11 @@
|
|||||||
package com.shabinder.common.models
|
package com.shabinder.common.models
|
||||||
|
|
||||||
import com.shabinder.common.models.spotify.Source
|
import com.shabinder.common.models.spotify.Source
|
||||||
|
import dev.icerock.moko.parcelize.Parcelable
|
||||||
|
import dev.icerock.moko.parcelize.Parcelize
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
@Serializable
|
@Serializable
|
||||||
data class TrackDetails(
|
data class TrackDetails(
|
||||||
var title:String,
|
var title:String,
|
||||||
@ -36,14 +39,15 @@ data class TrackDetails(
|
|||||||
var progress: Int = 2,//2 for visual progress bar hint
|
var progress: Int = 2,//2 for visual progress bar hint
|
||||||
var outputFilePath: String,
|
var outputFilePath: String,
|
||||||
var videoID:String? = null
|
var videoID:String? = null
|
||||||
)
|
):Parcelable
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed class DownloadStatus {
|
sealed class DownloadStatus:Parcelable {
|
||||||
object Downloaded :DownloadStatus()
|
@Parcelize object Downloaded :DownloadStatus()
|
||||||
data class Downloading(val progress: Int = 0):DownloadStatus()
|
@Parcelize data class Downloading(val progress: Int = 2):DownloadStatus()
|
||||||
object Queued :DownloadStatus()
|
@Parcelize object Queued :DownloadStatus()
|
||||||
object NotDownloaded :DownloadStatus()
|
@Parcelize object NotDownloaded :DownloadStatus()
|
||||||
object Converting :DownloadStatus()
|
@Parcelize object Converting :DownloadStatus()
|
||||||
object Failed :DownloadStatus()
|
@Parcelize object Failed :DownloadStatus()
|
||||||
}
|
}
|
@ -32,6 +32,8 @@ kotlin {
|
|||||||
androidMain {
|
androidMain {
|
||||||
dependencies{
|
dependencies{
|
||||||
implementation(Ktor.clientAndroid)
|
implementation(Ktor.clientAndroid)
|
||||||
|
implementation(Extras.Android.fetch)
|
||||||
|
implementation(Koin.android)
|
||||||
api(files("$rootDir/libs/mobile-ffmpeg.aar"))
|
api(files("$rootDir/libs/mobile-ffmpeg.aar"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,12 @@ package com.shabinder.common.di
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
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.database.appContext
|
||||||
|
import com.shabinder.common.di.worker.ForegroundService
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
|
|
||||||
actual fun openPlatform(packageID:String, platformLink:String){
|
actual fun openPlatform(packageID:String, platformLink:String){
|
||||||
@ -41,5 +46,25 @@ actual suspend fun downloadTracks(
|
|||||||
getYTIDBestMatch:suspend (String,TrackDetails)->String?,
|
getYTIDBestMatch:suspend (String,TrackDetails)->String?,
|
||||||
saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -3,6 +3,7 @@ package com.shabinder.common.di
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
|
import android.media.MediaScannerConnection
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
@ -95,6 +96,7 @@ actual class Dir actual constructor(
|
|||||||
.removeAllTags()
|
.removeAllTags()
|
||||||
.setId3v1Tags(trackDetails)
|
.setId3v1Tags(trackDetails)
|
||||||
.setId3v2TagsAndSaveFile(trackDetails)
|
.setId3v2TagsAndSaveFile(trackDetails)
|
||||||
|
addToLibrary(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Config.RETURN_CODE_CANCEL -> {
|
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? {
|
actual suspend fun loadImage(url: String): ImageBitmap? {
|
||||||
val cachePath = imageCacheDir() + getNameURL(url)
|
val cachePath = imageCacheDir() + getNameURL(url)
|
||||||
return (loadCachedImage(cachePath) ?: freshImage(url))?.asImageBitmap()
|
return (loadCachedImage(cachePath) ?: freshImage(url))?.asImageBitmap()
|
||||||
|
@ -34,7 +34,7 @@ actual class YoutubeProvider actual constructor(
|
|||||||
private val logger: Kermit,
|
private val logger: Kermit,
|
||||||
private val dir: Dir,
|
private val dir: Dir,
|
||||||
){
|
){
|
||||||
private val ytDownloader: YoutubeDownloader = YoutubeDownloader()
|
val ytDownloader: YoutubeDownloader = YoutubeDownloader()
|
||||||
/*
|
/*
|
||||||
* YT Album Art Schema
|
* YT Album Art Schema
|
||||||
* HI-RES Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
|
* HI-RES Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* 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.annotation.SuppressLint
|
||||||
import android.app.*
|
import android.app.*
|
||||||
@ -24,42 +24,30 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.media.MediaScannerConnection
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.*
|
import android.os.*
|
||||||
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.arthenica.mobileffmpeg.Config
|
import co.touchlab.kermit.Kermit
|
||||||
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_CANCEL
|
|
||||||
import com.arthenica.mobileffmpeg.Config.RETURN_CODE_SUCCESS
|
|
||||||
import com.arthenica.mobileffmpeg.FFmpeg
|
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
import com.github.kiulian.downloader.YoutubeDownloader
|
||||||
import com.mpatric.mp3agic.Mp3File
|
import com.shabinder.common.di.Dir
|
||||||
import com.shabinder.android.worker.removeAllTags
|
import com.shabinder.common.di.FetchPlatformQueryResult
|
||||||
import com.shabinder.android.worker.setId3v1Tags
|
import com.shabinder.common.di.getData
|
||||||
import com.shabinder.android.worker.setId3v2Tags
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.spotiflyer.di.Directories
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.shabinder.spotiflyer.models.DownloadStatus
|
import com.shabinder.common.ui.R.*
|
||||||
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.tonyodev.fetch2.*
|
import com.tonyodev.fetch2.*
|
||||||
import com.tonyodev.fetch2core.DownloadBlock
|
import com.tonyodev.fetch2core.DownloadBlock
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import retrofit2.Call
|
import org.koin.android.ext.android.inject
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
class ForegroundService : Service(){
|
class ForegroundService : Service(),CoroutineScope{
|
||||||
private val tag = "Foreground Service"
|
private val tag: String = "Foreground Service"
|
||||||
private val channelId = "ForegroundDownloaderService"
|
private val channelId = "ForegroundDownloaderService"
|
||||||
private val notificationId = 101
|
private val notificationId = 101
|
||||||
private var total = 0 //Total Downloads Requested
|
private var total = 0 //Total Downloads Requested
|
||||||
@ -69,29 +57,33 @@ class ForegroundService : Service(){
|
|||||||
private val isFinished: Boolean
|
private val isFinished: Boolean
|
||||||
get() = converted + failed == total
|
get() = converted + failed == total
|
||||||
private var isSingleDownload: Boolean = false
|
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 requestMap = hashMapOf<Request, TrackDetails>()
|
||||||
private val allTracksStatus = hashMapOf<String,DownloadStatus>()
|
private val allTracksStatus = hashMapOf<String, DownloadStatus>()
|
||||||
private var wakeLock: PowerManager.WakeLock? = null
|
private var wakeLock: PowerManager.WakeLock? = null
|
||||||
private var isServiceStarted = false
|
private var isServiceStarted = false
|
||||||
private var messageList = mutableListOf("", "", "", "","")
|
private var messageList = mutableListOf("", "", "", "","")
|
||||||
private lateinit var cancelIntent:PendingIntent
|
private lateinit var cancelIntent:PendingIntent
|
||||||
private lateinit var fetch:Fetch
|
|
||||||
private lateinit var downloadManager : DownloadManager
|
private lateinit var downloadManager : DownloadManager
|
||||||
@Inject lateinit var ytDownloader: YoutubeDownloader
|
|
||||||
@Inject lateinit var youtubeMusicApi: YoutubeMusicApi
|
private val fetcher: FetchPlatformQueryResult by inject()
|
||||||
@Inject lateinit var directories:Directories
|
private val logger: Kermit by inject()
|
||||||
private val defaultDir
|
private val fetch: Fetch by inject()
|
||||||
get() = directories.defaultDir()
|
private val dir: Dir by inject()
|
||||||
private val imageDir
|
private val ytDownloader:YoutubeDownloader
|
||||||
get() = directories.imageDir()
|
get() = fetcher.youtubeProvider.ytDownloader
|
||||||
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? = null
|
override fun onBind(intent: Intent): IBinder? = null
|
||||||
|
|
||||||
@SuppressLint("UnspecifiedImmutableFlag")
|
@SuppressLint("UnspecifiedImmutableFlag")
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
serviceJob = SupervisorJob()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
createNotificationChannel(channelId,"Downloader Service")
|
createNotificationChannel(channelId,"Downloader Service")
|
||||||
}
|
}
|
||||||
@ -101,14 +93,15 @@ class ForegroundService : Service(){
|
|||||||
).apply{action = "kill"}
|
).apply{action = "kill"}
|
||||||
cancelIntent = PendingIntent.getService (this, 0 , intent , FLAG_CANCEL_CURRENT )
|
cancelIntent = PendingIntent.getService (this, 0 , intent , FLAG_CANCEL_CURRENT )
|
||||||
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
initialiseFetch()
|
fetch.removeAllListeners().addListener(fetchListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("WakelockTimeout")
|
@SuppressLint("WakelockTimeout")
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
// Send a notification that service is started
|
// Send a notification that service is started
|
||||||
log(tag, "Service Started.")
|
Log.i(tag,"Foreground Service Started.")
|
||||||
startForeground(notificationId, getNotification())
|
startForeground(notificationId, getNotification())
|
||||||
|
|
||||||
intent?.let{
|
intent?.let{
|
||||||
when (it.action) {
|
when (it.action) {
|
||||||
"kill" -> killService()
|
"kill" -> killService()
|
||||||
@ -126,15 +119,6 @@ class ForegroundService : Service(){
|
|||||||
val downloadObjects: ArrayList<TrackDetails>? = (it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList(
|
val downloadObjects: ArrayList<TrackDetails>? = (it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList(
|
||||||
"object"
|
"object"
|
||||||
))
|
))
|
||||||
val imagesList: ArrayList<String>? = (it.getStringArrayListExtra("imagesList") ?: it.extras?.getStringArrayList(
|
|
||||||
"imagesList"
|
|
||||||
))
|
|
||||||
|
|
||||||
imagesList?.let{ imageList ->
|
|
||||||
serviceScope.launch {
|
|
||||||
downloadAllImages(imageList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadObjects?.let { list ->
|
downloadObjects?.let { list ->
|
||||||
downloadObjects.size.let { size ->
|
downloadObjects.size.let { size ->
|
||||||
@ -153,8 +137,8 @@ class ForegroundService : Service(){
|
|||||||
//Service Already Started
|
//Service Already Started
|
||||||
START_STICKY
|
START_STICKY
|
||||||
} else{
|
} else{
|
||||||
log(tag, "Starting the foreground service task")
|
|
||||||
isServiceStarted = true
|
isServiceStarted = true
|
||||||
|
Log.i(tag,"Starting the foreground service task")
|
||||||
wakeLock =
|
wakeLock =
|
||||||
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
|
||||||
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply {
|
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "EndlessService::lock").apply {
|
||||||
@ -170,74 +154,51 @@ class ForegroundService : Service(){
|
|||||||
**/
|
**/
|
||||||
private fun downloadAllTracks(trackList: List<TrackDetails>) {
|
private fun downloadAllTracks(trackList: List<TrackDetails>) {
|
||||||
trackList.forEach {
|
trackList.forEach {
|
||||||
serviceScope.launch {
|
launch {
|
||||||
if (!it.videoID.isNullOrBlank()) {//Video ID already known!
|
if (!it.videoID.isNullOrBlank()) {//Video ID already known!
|
||||||
downloadTrack(it.videoID!!, it)
|
downloadTrack(it.videoID!!, it)
|
||||||
} else {
|
} else {
|
||||||
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
|
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
|
||||||
val jsonBody = makeJsonBody(searchQuery.trim()).toJsonString()
|
val videoID = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery,it)
|
||||||
youtubeMusicApi.getYoutubeMusicResponse(jsonBody).enqueue(
|
logger.d("Service VideoID") { videoID ?: "Not Found" }
|
||||||
object : Callback<String> {
|
if (videoID.isNullOrBlank()) {
|
||||||
override fun onResponse(
|
sendTrackBroadcast(Status.FAILED.name, it)
|
||||||
call: Call<String>,
|
failed++
|
||||||
response: Response<String>
|
updateNotification()
|
||||||
) {
|
allTracksStatus[it.title] = DownloadStatus.Failed
|
||||||
serviceScope.launch {
|
} else {//Found Youtube Video ID
|
||||||
val videoId = sortByBestMatch(
|
downloadTrack(videoID, it)
|
||||||
getYTTracks(response.body().toString()),
|
}
|
||||||
trackName = it.title,
|
|
||||||
trackArtists = it.artists,
|
|
||||||
trackDurationSec = it.durationSec
|
|
||||||
).keys.firstOrNull()
|
|
||||||
log("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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
val audioData = ytDownloader.getVideo(videoID).getData()
|
val audioData = ytDownloader.getVideo(videoID).getData()
|
||||||
|
|
||||||
audioData?.let {
|
audioData?.let {
|
||||||
val url: String = it.url()
|
val url: String = it.url()
|
||||||
log("DHelper Link Found", url)
|
logger.d("DHelper Link Found") { url }
|
||||||
val request= Request(url, track.outputFile).apply{
|
val request= Request(url, track.outputFilePath).apply{
|
||||||
priority = Priority.NORMAL
|
priority = Priority.NORMAL
|
||||||
networkType = NetworkType.ALL
|
networkType = NetworkType.ALL
|
||||||
}
|
}
|
||||||
fetch.enqueue(request,
|
fetch.enqueue(request,
|
||||||
{ request1 ->
|
{ request1 ->
|
||||||
requestMap[request1] = track
|
requestMap[request1] = track
|
||||||
log(tag, "Enqueuing Download")
|
logger.d(tag){"Enqueuing Download"}
|
||||||
},
|
},
|
||||||
{ error ->
|
{ error ->
|
||||||
log(tag, "Enqueuing Error:${error.throwable.toString()}")
|
logger.d(tag){"Enqueuing Error:${error.throwable.toString()}"}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}catch (e: java.lang.Exception){
|
}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>,
|
downloadBlocks: List<DownloadBlock>,
|
||||||
totalBlocks: Int
|
totalBlocks: Int
|
||||||
) {
|
) {
|
||||||
serviceScope.launch {
|
launch {
|
||||||
val track = requestMap[download.request]
|
val track = requestMap[download.request]
|
||||||
addToNotification("Downloading ${track?.title}")
|
addToNotification("Downloading ${track?.title}")
|
||||||
log(tag, "${track?.title} Download Started")
|
logger.d(tag){"${track?.title} Download Started"}
|
||||||
track?.let{
|
track?.let{
|
||||||
allTracksStatus[it.title] = DownloadStatus.Downloading
|
allTracksStatus[it.title] = DownloadStatus.Downloading()
|
||||||
sendTrackBroadcast(Status.DOWNLOADING.name,track)
|
sendTrackBroadcast(Status.DOWNLOADING.name,track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -290,20 +251,20 @@ class ForegroundService : Service(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCompleted(download: Download) {
|
override fun onCompleted(download: Download) {
|
||||||
serviceScope.launch {
|
launch {
|
||||||
val track = requestMap[download.request]
|
val track = requestMap[download.request]
|
||||||
removeFromNotification("Downloading ${track?.title}")
|
removeFromNotification("Downloading ${track?.title}")
|
||||||
try{
|
try{
|
||||||
track?.let {
|
track?.let {
|
||||||
convertToMp3(download.file, it)
|
dir.saveFileWithMetadata(byteArrayOf(),it)
|
||||||
allTracksStatus[it.title] = DownloadStatus.Converting
|
allTracksStatus[it.title] = DownloadStatus.Converting
|
||||||
}
|
}
|
||||||
log(tag, "${track?.title} Download Completed")
|
logger.d(tag){"${track?.title} Download Completed"}
|
||||||
}catch (
|
}catch (
|
||||||
e: KotlinNullPointerException
|
e: KotlinNullPointerException
|
||||||
){
|
){
|
||||||
log(tag, "${track?.title} Download Failed! Error:Fetch!!!!")
|
logger.d(tag){"${track?.title} Download Failed! Error:Fetch!!!!"}
|
||||||
log(tag, "${track?.title} Requesting Download thru Android DM")
|
logger.d(tag){"${track?.title} Requesting Download thru Android DM"}
|
||||||
downloadUsingDM(download.request.url, download.request.file, track!!)
|
downloadUsingDM(download.request.url, download.request.file, track!!)
|
||||||
downloaded++
|
downloaded++
|
||||||
requestMap.remove(download.request)
|
requestMap.remove(download.request)
|
||||||
@ -324,11 +285,11 @@ class ForegroundService : Service(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(download: Download, error: Error, throwable: Throwable?) {
|
override fun onError(download: Download, error: Error, throwable: Throwable?) {
|
||||||
serviceScope.launch {
|
launch {
|
||||||
val track = requestMap[download.request]
|
val track = requestMap[download.request]
|
||||||
downloaded++
|
downloaded++
|
||||||
log(tag, download.error.throwable.toString())
|
logger.d(tag){download.error.throwable.toString()}
|
||||||
log(tag, "${track?.title} Requesting Download thru Android DM")
|
logger.d(tag){"${track?.title} Requesting Download thru Android DM"}
|
||||||
downloadUsingDM(download.request.url, download.request.file, track!!)
|
downloadUsingDM(download.request.url, download.request.file, track!!)
|
||||||
requestMap.remove(download.request)
|
requestMap.remove(download.request)
|
||||||
removeFromNotification("Downloading ${track.title}")
|
removeFromNotification("Downloading ${track.title}")
|
||||||
@ -345,15 +306,19 @@ class ForegroundService : Service(){
|
|||||||
etaInMilliSeconds: Long,
|
etaInMilliSeconds: Long,
|
||||||
downloadedBytesPerSecond: Long
|
downloadedBytesPerSecond: Long
|
||||||
) {
|
) {
|
||||||
serviceScope.launch {
|
launch {
|
||||||
val track = requestMap[download.request]
|
requestMap[download.request]?.run {
|
||||||
log(tag, "${track?.title} ETA: ${etaInMilliSeconds / 1000} sec")
|
allTracksStatus[title] = DownloadStatus.Downloading(download.progress)
|
||||||
val intent = Intent().apply {
|
logger.d(tag){"${title} ETA: ${etaInMilliSeconds / 1000} sec"}
|
||||||
action = "Progress"
|
|
||||||
putExtra("progress", download.progress)
|
|
||||||
putExtra("track", requestMap[download.request])
|
val intent = Intent().apply {
|
||||||
|
action = "Progress"
|
||||||
|
putExtra("progress", download.progress)
|
||||||
|
putExtra("track", this@run)
|
||||||
|
}
|
||||||
|
sendBroadcast(intent)
|
||||||
}
|
}
|
||||||
sendBroadcast(intent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -362,7 +327,7 @@ class ForegroundService : Service(){
|
|||||||
* If fetch Fails , Android Download Manager To RESCUE!!
|
* If fetch Fails , Android Download Manager To RESCUE!!
|
||||||
**/
|
**/
|
||||||
fun downloadUsingDM(url: String, outputDir: String, track: TrackDetails){
|
fun downloadUsingDM(url: String, outputDir: String, track: TrackDetails){
|
||||||
serviceScope.launch {
|
launch {
|
||||||
val uri = Uri.parse(url)
|
val uri = Uri.parse(url)
|
||||||
val request = DownloadManager.Request(uri).apply {
|
val request = DownloadManager.Request(uri).apply {
|
||||||
setAllowedNetworkTypes(
|
setAllowedNetworkTypes(
|
||||||
@ -378,7 +343,7 @@ class ForegroundService : Service(){
|
|||||||
|
|
||||||
//Start Download
|
//Start Download
|
||||||
val downloadID = downloadManager.enqueue(request)
|
val downloadID = downloadManager.enqueue(request)
|
||||||
log("DownloadManager", "Download Request Sent")
|
logger.d("DownloadManager"){"Download Request Sent"}
|
||||||
|
|
||||||
val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
|
val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
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
|
//Checking if the received broadcast is for our enqueued download by matching download id
|
||||||
if (downloadID == id) {
|
if (downloadID == id) {
|
||||||
allTracksStatus[track.title] = DownloadStatus.Converting
|
allTracksStatus[track.title] = DownloadStatus.Converting
|
||||||
convertToMp3(outputDir, track)
|
launch { dir.saveFileWithMetadata(byteArrayOf(),track);converted++ }
|
||||||
converted++
|
|
||||||
//Unregister this broadcast Receiver
|
//Unregister this broadcast Receiver
|
||||||
this@ForegroundService.unregisterReceiver(this)
|
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
|
* This is the method that can be called to update the Notification
|
||||||
@ -472,7 +374,7 @@ class ForegroundService : Service(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun releaseWakeLock() {
|
private fun releaseWakeLock() {
|
||||||
log(tag, "Releasing Wake Lock")
|
logger.d(tag){"Releasing Wake Lock"}
|
||||||
try {
|
try {
|
||||||
wakeLock?.let {
|
wakeLock?.let {
|
||||||
if (it.isHeld) {
|
if (it.isHeld) {
|
||||||
@ -480,7 +382,7 @@ class ForegroundService : Service(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log(tag, "Service stopped without being started: ${e.message}")
|
logger.d(tag){"Service stopped without being started: ${e.message}"}
|
||||||
}
|
}
|
||||||
isServiceStarted = false
|
isServiceStarted = false
|
||||||
}
|
}
|
||||||
@ -501,7 +403,7 @@ class ForegroundService : Service(){
|
|||||||
* Cleaning All Residual Files except Mp3 Files
|
* Cleaning All Residual Files except Mp3 Files
|
||||||
**/
|
**/
|
||||||
private fun cleanFiles(dir: File) {
|
private fun cleanFiles(dir: File) {
|
||||||
log(tag, "Starting Cleaning in ${dir.path} ")
|
logger.d(tag){"Starting Cleaning in ${dir.path} "}
|
||||||
val fList = dir.listFiles()
|
val fList = dir.listFiles()
|
||||||
fList?.let {
|
fList?.let {
|
||||||
for (file in fList) {
|
for (file in fList) {
|
||||||
@ -509,7 +411,7 @@ class ForegroundService : Service(){
|
|||||||
cleanFiles(file)
|
cleanFiles(file)
|
||||||
} else if(file.isFile) {
|
} else if(file.isFile) {
|
||||||
if(file.path.toString().substringAfterLast(".") != "mp3"){
|
if(file.path.toString().substringAfterLast(".") != "mp3"){
|
||||||
log(tag, "Cleaning ${file.path}")
|
logger.d(tag){ "Cleaning ${file.path}"}
|
||||||
file.delete()
|
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() {
|
private fun killService() {
|
||||||
serviceScope.launch{
|
launch{
|
||||||
log(tag,"Killing Self")
|
logger.d(tag){"Killing Self"}
|
||||||
messageList = mutableListOf("Cleaning And Exiting","","","","")
|
messageList = mutableListOf("Cleaning And Exiting","","","","")
|
||||||
fetch.cancelAll()
|
fetch.cancelAll()
|
||||||
fetch.removeAll()
|
fetch.removeAll()
|
||||||
updateNotification()
|
updateNotification()
|
||||||
cleanFiles(File(defaultDir))
|
cleanFiles(File(dir.defaultDir()))
|
||||||
cleanFiles(File(imageDir))
|
cleanFiles(File(dir.imageCacheDir()))
|
||||||
messageList = mutableListOf("","","","","")
|
messageList = mutableListOf("","","","","")
|
||||||
releaseWakeLock()
|
releaseWakeLock()
|
||||||
|
serviceJob.cancel()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
} else {
|
} 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 {
|
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")
|
setContentTitle("Total: $total Completed:$converted Failed:$failed")
|
||||||
setSilent(true)
|
setSilent(true)
|
||||||
setStyle(
|
setStyle(
|
||||||
@ -639,7 +466,7 @@ class ForegroundService : Service(){
|
|||||||
addLine(messageList[messageList.size - 5])
|
addLine(messageList[messageList.size - 5])
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
addAction(R.drawable.ic_round_cancel_24,"Exit",cancelIntent)
|
addAction(drawable.ic_round_cancel_24,"Exit",cancelIntent)
|
||||||
build()
|
build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -661,3 +488,10 @@ class ForegroundService : Service(){
|
|||||||
this@ForegroundService.sendBroadcast(intent)
|
this@ForegroundService.sendBroadcast(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Fetch.removeAllListeners():Fetch{
|
||||||
|
for (listener in this.getListenerSet()) {
|
||||||
|
this.removeListener(listener)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
@ -25,6 +25,7 @@ expect class Dir(
|
|||||||
suspend fun loadImage(url:String): ImageBitmap?
|
suspend fun loadImage(url:String): ImageBitmap?
|
||||||
suspend fun clearCache()
|
suspend fun clearCache()
|
||||||
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, trackDetails: TrackDetails)
|
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, trackDetails: TrackDetails)
|
||||||
|
fun addToLibrary(path:String)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun downloadFile(url: String): Flow<DownloadResult> {
|
suspend fun downloadFile(url: String): Flow<DownloadResult> {
|
||||||
|
@ -12,7 +12,7 @@ import kotlinx.coroutines.withContext
|
|||||||
class FetchPlatformQueryResult(
|
class FetchPlatformQueryResult(
|
||||||
private val gaanaProvider: GaanaProvider,
|
private val gaanaProvider: GaanaProvider,
|
||||||
private val spotifyProvider: SpotifyProvider,
|
private val spotifyProvider: SpotifyProvider,
|
||||||
private val youtubeProvider: YoutubeProvider,
|
val youtubeProvider: YoutubeProvider,
|
||||||
val youtubeMusic: YoutubeMusic,
|
val youtubeMusic: YoutubeMusic,
|
||||||
private val database: Database
|
private val database: Database
|
||||||
) {
|
) {
|
||||||
|
@ -27,7 +27,7 @@ class YoutubeMusic constructor(
|
|||||||
trackDurationSec = trackDetails.durationSec
|
trackDurationSec = trackDetails.durationSec
|
||||||
).keys.firstOrNull()
|
).keys.firstOrNull()
|
||||||
}
|
}
|
||||||
suspend fun getYTTracks(query: String):List<YoutubeTrack>{
|
private suspend fun getYTTracks(query: String):List<YoutubeTrack>{
|
||||||
val youtubeTracks = mutableListOf<YoutubeTrack>()
|
val youtubeTracks = mutableListOf<YoutubeTrack>()
|
||||||
|
|
||||||
val responseObj = Json.parseToJsonElement(getYoutubeMusicResponse(query))
|
val responseObj = Json.parseToJsonElement(getYoutubeMusicResponse(query))
|
||||||
@ -121,23 +121,25 @@ class YoutubeMusic constructor(
|
|||||||
! other constituents of a result block will lead to errors, hence the 'in
|
! other constituents of a result block will lead to errors, hence the 'in
|
||||||
! result[:-1] ,i.e., skip last element in array '
|
! 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)){
|
||||||
if(detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"]?.jsonObject?.size?:0 < 2) continue
|
for(detail in detailArray.jsonArray){
|
||||||
|
if(detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"]?.jsonObject?.size?:0 < 2) continue
|
||||||
|
|
||||||
// if not a dummy, collect All Variables
|
// if not a dummy, collect All Variables
|
||||||
val details = detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"]
|
val details = detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"]
|
||||||
?.jsonObject?.get("text")
|
?.jsonObject?.get("text")
|
||||||
?.jsonObject?.get("runs")?.jsonArray ?: listOf()
|
?.jsonObject?.get("runs")?.jsonArray ?: listOf()
|
||||||
|
|
||||||
for (d in details){
|
for (d in details){
|
||||||
d.jsonObject["text"]?.jsonPrimitive?.contentOrNull?.let {
|
d.jsonObject["text"]?.jsonPrimitive?.contentOrNull?.let {
|
||||||
if(it != " • "){
|
if(it != " • "){
|
||||||
availableDetails.add(it)
|
availableDetails.add(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// log("YT Music details",availableDetails.toString())
|
//logger.d("YT Music details"){availableDetails.toString()}
|
||||||
/*
|
/*
|
||||||
! Filter Out non-Song/Video results and incomplete results here itself
|
! Filter Out non-Song/Video results and incomplete results here itself
|
||||||
! From what we know about detail order, note that [1] - indicate result type
|
! From what we know about detail order, note that [1] - indicate result type
|
||||||
@ -171,7 +173,7 @@ class YoutubeMusic constructor(
|
|||||||
return youtubeTracks
|
return youtubeTracks
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sortByBestMatch(
|
private fun sortByBestMatch(
|
||||||
ytTracks:List<YoutubeTrack>,
|
ytTracks:List<YoutubeTrack>,
|
||||||
trackName:String,
|
trackName:String,
|
||||||
trackArtists:List<String>,
|
trackArtists:List<String>,
|
||||||
@ -240,7 +242,7 @@ class YoutubeMusic constructor(
|
|||||||
val avgMatch = (artistMatch + durationMatch)/2
|
val avgMatch = (artistMatch + durationMatch)/2
|
||||||
linksWithMatchValue[result.videoId.toString()] = avgMatch.toInt()
|
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()
|
return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ actual class Dir actual constructor(private val logger: Kermit) {
|
|||||||
.setId3v1Tags(trackDetails)
|
.setId3v1Tags(trackDetails)
|
||||||
.setId3v2TagsAndSaveFile(trackDetails)
|
.setId3v2TagsAndSaveFile(trackDetails)
|
||||||
}
|
}
|
||||||
|
actual fun addToLibrary(path:String){}
|
||||||
actual suspend fun loadImage(url: String): ImageBitmap? {
|
actual suspend fun loadImage(url: String): ImageBitmap? {
|
||||||
val cachePath = imageCacheDir() + getNameURL(url)
|
val cachePath = imageCacheDir() + getNameURL(url)
|
||||||
var picture: ImageBitmap? = loadCachedImage(cachePath)
|
var picture: ImageBitmap? = loadCachedImage(cachePath)
|
||||||
|
@ -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>
|
@ -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>
|
Loading…
Reference in New Issue
Block a user