mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-22 01:04:31 +01:00
Code Cleaned & Ktlint Added
This commit is contained in:
parent
46e5e89a2e
commit
ccea676b77
@ -58,7 +58,6 @@ import com.shabinder.common.models.TrackDetails
|
|||||||
import com.shabinder.common.root.SpotiFlyerRoot
|
import com.shabinder.common.root.SpotiFlyerRoot
|
||||||
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
|
import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks
|
||||||
import com.shabinder.common.uikit.*
|
import com.shabinder.common.uikit.*
|
||||||
import com.shabinder.database.Database
|
|
||||||
import com.shabinder.spotiflyer.utils.*
|
import com.shabinder.spotiflyer.utils.*
|
||||||
import com.tonyodev.fetch2.Status
|
import com.tonyodev.fetch2.Status
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
`kotlin-dsl`
|
`kotlin-dsl`
|
||||||
|
id("org.jlleitschuh.gradle.ktlint")
|
||||||
|
id("org.jlleitschuh.gradle.ktlint-idea")
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
@ -27,7 +29,7 @@ allprojects {
|
|||||||
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://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://dl.bintray.com/kotlin/kotlin-js-wrappers")
|
maven(url = "https://dl.bintray.com/kotlin/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")
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
`kotlin-dsl`
|
`kotlin-dsl`
|
||||||
//`kotlin-dsl-precompiled-script-plugins`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "com.shabinder"
|
group = "com.shabinder"
|
||||||
@ -28,6 +27,7 @@ repositories {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
google()
|
google()
|
||||||
maven(url = "https://jitpack.io")
|
maven(url = "https://jitpack.io")
|
||||||
|
maven(url = "https://plugins.gradle.org/m2/")
|
||||||
maven(url = "https://dl.bintray.com/kotlin/kotlin-js-wrappers")
|
maven(url = "https://dl.bintray.com/kotlin/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")
|
||||||
}
|
}
|
||||||
@ -37,6 +37,7 @@ dependencies {
|
|||||||
implementation("com.google.gms:google-services:4.3.5")
|
implementation("com.google.gms:google-services:4.3.5")
|
||||||
implementation("com.google.firebase:perf-plugin:1.3.5")
|
implementation("com.google.firebase:perf-plugin:1.3.5")
|
||||||
implementation("com.google.firebase:firebase-crashlytics-gradle:2.5.1")
|
implementation("com.google.firebase:firebase-crashlytics-gradle:2.5.1")
|
||||||
|
implementation("org.jlleitschuh.gradle:ktlint-gradle:${Versions.ktLint}")
|
||||||
implementation(JetBrains.Compose.gradlePlugin)
|
implementation(JetBrains.Compose.gradlePlugin)
|
||||||
implementation(JetBrains.Kotlin.gradlePlugin)
|
implementation(JetBrains.Kotlin.gradlePlugin)
|
||||||
implementation(JetBrains.Kotlin.serialization)
|
implementation(JetBrains.Kotlin.serialization)
|
||||||
|
@ -21,26 +21,29 @@ object Versions {
|
|||||||
const val kotlinVersion = "1.4.31"
|
const val kotlinVersion = "1.4.31"
|
||||||
|
|
||||||
const val coroutinesVersion = "1.4.2"
|
const val coroutinesVersion = "1.4.2"
|
||||||
//const val compose = "1.0.0-alpha12"
|
|
||||||
|
|
||||||
const val coilVersion = "0.4.1"
|
const val coilVersion = "0.4.1"
|
||||||
//DI
|
|
||||||
|
// Code Formatting
|
||||||
|
const val ktLint = "10.0.0"
|
||||||
|
|
||||||
|
// DI
|
||||||
const val koin = "3.0.1-beta-1"
|
const val koin = "3.0.1-beta-1"
|
||||||
|
|
||||||
//Logger
|
// Logger
|
||||||
const val kermit = "0.1.8"
|
const val kermit = "0.1.8"
|
||||||
|
|
||||||
//Internet
|
// Internet
|
||||||
const val ktor = "1.5.2"
|
const val ktor = "1.5.2"
|
||||||
|
|
||||||
const val kotlinxSerialization = "1.1.0-RC"
|
const val kotlinxSerialization = "1.1.0-RC"
|
||||||
//Database
|
// Database
|
||||||
const val sqlDelight = "1.4.4"
|
const val sqlDelight = "1.4.4"
|
||||||
|
|
||||||
const val sqliteJdbcDriver = "3.30.1"
|
const val sqliteJdbcDriver = "3.30.1"
|
||||||
const val slf4j = "1.7.30"
|
const val slf4j = "1.7.30"
|
||||||
|
|
||||||
//Android
|
// Android
|
||||||
const val versionCode = 15
|
const val versionCode = 15
|
||||||
const val minSdkVersion = 24
|
const val minSdkVersion = 24
|
||||||
const val compileSdkVersion = 29
|
const val compileSdkVersion = 29
|
||||||
@ -53,7 +56,7 @@ object Koin {
|
|||||||
val android = "io.insert-koin:koin-android:${Versions.koin}"
|
val android = "io.insert-koin:koin-android:${Versions.koin}"
|
||||||
val compose = "io.insert-koin:koin-androidx-compose:${Versions.koin}"
|
val compose = "io.insert-koin:koin-androidx-compose:${Versions.koin}"
|
||||||
}
|
}
|
||||||
object Androidx{
|
object Androidx {
|
||||||
const val androidxActivity = "androidx.activity:activity-compose:1.3.0-alpha02"
|
const val androidxActivity = "androidx.activity:activity-compose:1.3.0-alpha02"
|
||||||
const val core = "androidx.core:core-ktx:1.3.2"
|
const val core = "androidx.core:core-ktx:1.3.2"
|
||||||
const val palette = "androidx.palette:palette-ktx:1.0.0"
|
const val palette = "androidx.palette:palette-ktx:1.0.0"
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.library")
|
id("com.android.library")
|
||||||
|
id("ktlint-setup")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -43,5 +44,4 @@ android {
|
|||||||
res.srcDirs("src/androidMain/res")
|
res.srcDirs("src/androidMain/res")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,31 @@
|
|||||||
* * 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.common.di
|
plugins {
|
||||||
|
id("org.jlleitschuh.gradle.ktlint")
|
||||||
sealed class NetworkResponse<out T> {
|
id("org.jlleitschuh.gradle.ktlint-idea")
|
||||||
data class Success<T>(val value:T):NetworkResponse<T>()
|
}
|
||||||
data class Error(val message:String):NetworkResponse<Nothing>()
|
|
||||||
|
subprojects {
|
||||||
|
apply(plugin = "org.jlleitschuh.gradle.ktlint")
|
||||||
|
apply(plugin = "org.jlleitschuh.gradle.ktlint-idea")
|
||||||
|
repositories {
|
||||||
|
// Required to download KtLint
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
ktlint {
|
||||||
|
android.set(true)
|
||||||
|
outputToConsole.set(true)
|
||||||
|
ignoreFailures.set(true)
|
||||||
|
coloredOutput.set(true)
|
||||||
|
verbose.set(true)
|
||||||
|
filter {
|
||||||
|
exclude("**/generated/**")
|
||||||
|
exclude("**/build/**")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Optionally configure plugin
|
||||||
|
/*configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
|
||||||
|
debug.set(true)
|
||||||
|
}*/
|
||||||
}
|
}
|
@ -18,6 +18,7 @@ plugins {
|
|||||||
id("com.android.library")
|
id("com.android.library")
|
||||||
id("kotlin-multiplatform")
|
id("kotlin-multiplatform")
|
||||||
id("org.jetbrains.compose")
|
id("org.jetbrains.compose")
|
||||||
|
id("ktlint-setup")
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
@ -17,15 +17,16 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id("com.android.library")
|
id("com.android.library")
|
||||||
id("kotlin-multiplatform")
|
id("kotlin-multiplatform")
|
||||||
|
id("ktlint-setup")
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvm("desktop")
|
jvm("desktop")
|
||||||
android()
|
android()
|
||||||
//ios()
|
// ios()
|
||||||
js() {
|
js() {
|
||||||
browser()
|
browser()
|
||||||
//nodejs()
|
// nodejs()
|
||||||
binaries.executable()
|
binaries.executable()
|
||||||
}
|
}
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@ -47,9 +48,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
named("jsTest") {
|
named("jsTest") {
|
||||||
dependencies {
|
dependencies {}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,17 +14,11 @@
|
|||||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import gradle.kotlin.dsl.accessors._2e23d8fadf0ed92ae13e19db3d83f86d.compose
|
|
||||||
import gradle.kotlin.dsl.accessors._2e23d8fadf0ed92ae13e19db3d83f86d.kotlin
|
|
||||||
import gradle.kotlin.dsl.accessors._2e23d8fadf0ed92ae13e19db3d83f86d.sourceSets
|
|
||||||
import org.gradle.kotlin.dsl.withType
|
|
||||||
import org.jetbrains.compose.compose
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
// id("com.android.library")
|
|
||||||
id("android-setup")
|
id("android-setup")
|
||||||
id("kotlin-multiplatform")
|
id("kotlin-multiplatform")
|
||||||
id("org.jetbrains.compose")
|
id("org.jetbrains.compose")
|
||||||
|
id("ktlint-setup")
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
@ -32,14 +26,12 @@ kotlin {
|
|||||||
android()
|
android()
|
||||||
js() {
|
js() {
|
||||||
browser()
|
browser()
|
||||||
//nodejs()
|
// nodejs()
|
||||||
binaries.executable()
|
binaries.executable()
|
||||||
}
|
}
|
||||||
sourceSets {
|
sourceSets {
|
||||||
named("commonMain") {
|
named("commonMain") {
|
||||||
dependencies {
|
dependencies {}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
named("androidMain") {
|
named("androidMain") {
|
||||||
|
@ -33,7 +33,7 @@ kotlin {
|
|||||||
implementation(project(":common:database"))
|
implementation(project(":common:database"))
|
||||||
implementation(project(":common:data-models"))
|
implementation(project(":common:data-models"))
|
||||||
implementation(project(":common:dependency-injection"))
|
implementation(project(":common:dependency-injection"))
|
||||||
//DECOMPOSE
|
// DECOMPOSE
|
||||||
implementation(Decompose.decompose)
|
implementation(Decompose.decompose)
|
||||||
implementation(Decompose.extensionsCompose)
|
implementation(Decompose.extensionsCompose)
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,13 @@ package com.shabinder.common.uikit
|
|||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
@ -39,21 +45,21 @@ import kotlinx.coroutines.withContext
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun ImageLoad(
|
actual fun ImageLoad(
|
||||||
link:String,
|
link: String,
|
||||||
loader:suspend (String) -> Picture,
|
loader: suspend (String) -> Picture,
|
||||||
desc: String,
|
desc: String,
|
||||||
modifier:Modifier,
|
modifier: Modifier,
|
||||||
//placeholder: ImageVector
|
// placeholder: ImageVector
|
||||||
) {
|
) {
|
||||||
var pic by remember(link) { mutableStateOf<ImageBitmap?>(null) }
|
var pic by remember(link) { mutableStateOf<ImageBitmap?>(null) }
|
||||||
LaunchedEffect(link){
|
LaunchedEffect(link) {
|
||||||
withContext(dispatcherIO) {
|
withContext(dispatcherIO) {
|
||||||
pic = loader(link).image
|
pic = loader(link).image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Crossfade(pic){
|
Crossfade(pic) {
|
||||||
if(it == null) Image(PlaceHolderImage(), desc, modifier,contentScale = ContentScale.Crop) else Image(it, desc, modifier,contentScale = ContentScale.Crop)
|
if (it == null) Image(PlaceHolderImage(), desc, modifier, contentScale = ContentScale.Crop) else Image(it, desc, modifier, contentScale = ContentScale.Crop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,9 +74,8 @@ actual fun pristineFont() = FontFamily(
|
|||||||
Font(R.font.pristine_script, FontWeight.Bold)
|
Font(R.font.pristine_script, FontWeight.Bold)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun DownloadImageTick(){
|
actual fun DownloadImageTick() {
|
||||||
Image(
|
Image(
|
||||||
painterResource(R.drawable.ic_tick),
|
painterResource(R.drawable.ic_tick),
|
||||||
"Download Done"
|
"Download Done"
|
||||||
@ -78,7 +83,7 @@ actual fun DownloadImageTick(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun DownloadImageError(){
|
actual fun DownloadImageError() {
|
||||||
Image(
|
Image(
|
||||||
painterResource(R.drawable.ic_error),
|
painterResource(R.drawable.ic_error),
|
||||||
"Error! Cant Download this track"
|
"Error! Cant Download this track"
|
||||||
@ -86,7 +91,7 @@ actual fun DownloadImageError(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun DownloadImageArrow(modifier: Modifier){
|
actual fun DownloadImageArrow(modifier: Modifier) {
|
||||||
Image(
|
Image(
|
||||||
painterResource(R.drawable.ic_arrow),
|
painterResource(R.drawable.ic_arrow),
|
||||||
"Start Download",
|
"Start Download",
|
||||||
@ -125,16 +130,16 @@ actual fun YoutubeMusicLogo() = vectorResource(R.drawable.ic_youtube_music_logo)
|
|||||||
actual fun GithubLogo() = vectorResource(R.drawable.ic_github)
|
actual fun GithubLogo() = vectorResource(R.drawable.ic_github)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun vectorResource(@DrawableRes id: Int) = ImageVector.Companion.vectorResource(id)
|
fun vectorResource(@DrawableRes id: Int) = ImageVector.Companion.vectorResource(id)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun Toast(
|
actual fun Toast(
|
||||||
text: String,
|
text: String,
|
||||||
visibility: MutableState<Boolean>,
|
visibility: MutableState<Boolean>,
|
||||||
duration: ToastDuration
|
duration: ToastDuration
|
||||||
){
|
) {
|
||||||
//We Have Android's Implementation of Toast so its just Empty
|
// We Have Android's Implementation of Toast so its just Empty
|
||||||
}
|
}
|
||||||
actual fun showPopUpMessage(text: String){
|
actual fun showPopUpMessage(text: String) {
|
||||||
android.widget.Toast.makeText(appContext,text, android.widget.Toast.LENGTH_SHORT).show()
|
android.widget.Toast.makeText(appContext, text, android.widget.Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
@ -17,52 +17,52 @@
|
|||||||
@file:Suppress("FunctionName")
|
@file:Suppress("FunctionName")
|
||||||
package com.shabinder.common.uikit
|
package com.shabinder.common.uikit
|
||||||
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import com.shabinder.common.di.Picture
|
import com.shabinder.common.di.Picture
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun ImageLoad(
|
expect fun ImageLoad(
|
||||||
link:String,
|
link: String,
|
||||||
loader:suspend (String) ->Picture,
|
loader: suspend (String) -> Picture,
|
||||||
desc: String = "Album Art",
|
desc: String = "Album Art",
|
||||||
modifier:Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
//placeholder:ImageVector = PlaceHolderImage()
|
// placeholder:ImageVector = PlaceHolderImage()
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun DownloadImageTick()
|
expect fun DownloadImageTick()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun DownloadAllImage():ImageVector
|
expect fun DownloadAllImage(): ImageVector
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun ShareImage():ImageVector
|
expect fun ShareImage(): ImageVector
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun PlaceHolderImage():ImageVector
|
expect fun PlaceHolderImage(): ImageVector
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun SpotiFlyerLogo():ImageVector
|
expect fun SpotiFlyerLogo(): ImageVector
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun SpotifyLogo():ImageVector
|
expect fun SpotifyLogo(): ImageVector
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun YoutubeLogo():ImageVector
|
expect fun YoutubeLogo(): ImageVector
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun GaanaLogo():ImageVector
|
expect fun GaanaLogo(): ImageVector
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun YoutubeMusicLogo():ImageVector
|
expect fun YoutubeMusicLogo(): ImageVector
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun GithubLogo():ImageVector
|
expect fun GithubLogo(): ImageVector
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun HeartIcon():ImageVector
|
expect fun HeartIcon(): ImageVector
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
expect fun DownloadImageError()
|
expect fun DownloadImageError()
|
||||||
|
@ -21,7 +21,7 @@ import androidx.compose.material.Shapes
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
val SpotiFlyerShapes = Shapes(
|
val SpotiFlyerShapes = Shapes(
|
||||||
small = RoundedCornerShape(percent = 50),
|
small = RoundedCornerShape(percent = 50),
|
||||||
medium = RoundedCornerShape(size = 8.dp),
|
medium = RoundedCornerShape(size = 8.dp),
|
||||||
large = RoundedCornerShape(size = 0.dp)
|
large = RoundedCornerShape(size = 0.dp)
|
||||||
)
|
)
|
@ -17,16 +17,30 @@
|
|||||||
package com.shabinder.common.uikit
|
package com.shabinder.common.uikit
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.material.ExtendedFloatingActionButton
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -35,7 +49,6 @@ import com.shabinder.common.di.Picture
|
|||||||
import com.shabinder.common.list.SpotiFlyerList
|
import com.shabinder.common.list.SpotiFlyerList
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SpotiFlyerListContent(
|
fun SpotiFlyerListContent(
|
||||||
@ -44,23 +57,21 @@ fun SpotiFlyerListContent(
|
|||||||
) {
|
) {
|
||||||
val model by component.models.collectAsState(SpotiFlyerList.State())
|
val model by component.models.collectAsState(SpotiFlyerList.State())
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
//TODO Better Null Handling
|
// TODO Better Null Handling
|
||||||
val result = model.queryResult
|
val result = model.queryResult
|
||||||
if(result == null){
|
if (result == null) {
|
||||||
Column(Modifier.fillMaxSize(),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
Spacer(modifier.padding(8.dp))
|
Spacer(modifier.padding(8.dp))
|
||||||
Text("Loading..",style = appNameStyle,color = colorPrimary)
|
Text("Loading..", style = appNameStyle, color = colorPrimary)
|
||||||
}
|
}
|
||||||
}else{
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
content = {
|
content = {
|
||||||
item {
|
item {
|
||||||
CoverImage(result.title, result.coverUrl, coroutineScope,component::loadImage)
|
CoverImage(result.title, result.coverUrl, component::loadImage)
|
||||||
}
|
}
|
||||||
itemsIndexed(model.trackList) { index, item ->
|
itemsIndexed(model.trackList) { index, item ->
|
||||||
TrackCard(
|
TrackCard(
|
||||||
@ -73,7 +84,7 @@ fun SpotiFlyerListContent(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
DownloadAllButton(
|
DownloadAllButton(
|
||||||
onClick = {component.onDownloadAllClicked(model.trackList)},
|
onClick = { component.onDownloadAllClicked(model.trackList) },
|
||||||
modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter)
|
modifier = Modifier.padding(bottom = 24.dp).align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -83,10 +94,10 @@ fun SpotiFlyerListContent(
|
|||||||
@Composable
|
@Composable
|
||||||
fun TrackCard(
|
fun TrackCard(
|
||||||
track: TrackDetails,
|
track: TrackDetails,
|
||||||
downloadTrack:()->Unit,
|
downloadTrack: () -> Unit,
|
||||||
loadImage:suspend (String)-> Picture
|
loadImage: suspend (String) -> Picture
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) {
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)) {
|
||||||
ImageLoad(
|
ImageLoad(
|
||||||
track.albumArtURL,
|
track.albumArtURL,
|
||||||
loadImage,
|
loadImage,
|
||||||
@ -96,18 +107,18 @@ fun TrackCard(
|
|||||||
.height(70.dp)
|
.height(70.dp)
|
||||||
.clip(MaterialTheme.shapes.medium)
|
.clip(MaterialTheme.shapes.medium)
|
||||||
)
|
)
|
||||||
Column(modifier = Modifier.padding(horizontal = 8.dp).height(60.dp).weight(1f),verticalArrangement = Arrangement.SpaceEvenly) {
|
Column(modifier = Modifier.padding(horizontal = 8.dp).height(60.dp).weight(1f), verticalArrangement = Arrangement.SpaceEvenly) {
|
||||||
Text(track.title,maxLines = 1,overflow = TextOverflow.Ellipsis,style = SpotiFlyerTypography.h6,color = colorAccent)
|
Text(track.title, maxLines = 1, overflow = TextOverflow.Ellipsis, style = SpotiFlyerTypography.h6, color = colorAccent)
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.Bottom,
|
verticalAlignment = Alignment.Bottom,
|
||||||
modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize()
|
modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize()
|
||||||
){
|
) {
|
||||||
Text("${track.artists.firstOrNull()}...",fontSize = 12.sp,maxLines = 1)
|
Text("${track.artists.firstOrNull()}...", fontSize = 12.sp, maxLines = 1)
|
||||||
Text("${track.durationSec/60} min, ${track.durationSec%60} sec",fontSize = 12.sp,maxLines = 1,overflow = TextOverflow.Ellipsis)
|
Text("${track.durationSec / 60} min, ${track.durationSec % 60} sec", fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
when(track.downloaded){
|
when (track.downloaded) {
|
||||||
is DownloadStatus.Downloaded -> {
|
is DownloadStatus.Downloaded -> {
|
||||||
DownloadImageTick()
|
DownloadImageTick()
|
||||||
}
|
}
|
||||||
@ -118,15 +129,19 @@ fun TrackCard(
|
|||||||
DownloadImageError()
|
DownloadImageError()
|
||||||
}
|
}
|
||||||
is DownloadStatus.Downloading -> {
|
is DownloadStatus.Downloading -> {
|
||||||
CircularProgressIndicator(progress = (track.downloaded as DownloadStatus.Downloading).progress.toFloat()/100f)
|
CircularProgressIndicator(progress = (track.downloaded as DownloadStatus.Downloading).progress.toFloat() / 100f)
|
||||||
}
|
}
|
||||||
is DownloadStatus.Converting -> {
|
is DownloadStatus.Converting -> {
|
||||||
CircularProgressIndicator(progress = 100f,color = colorAccent)
|
CircularProgressIndicator(progress = 100f, color = colorAccent)
|
||||||
}
|
}
|
||||||
is DownloadStatus.NotDownloaded -> {
|
is DownloadStatus.NotDownloaded -> {
|
||||||
DownloadImageArrow(Modifier.clickable(onClick = {
|
DownloadImageArrow(
|
||||||
downloadTrack()
|
Modifier.clickable(
|
||||||
}))
|
onClick = {
|
||||||
|
downloadTrack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,7 +151,6 @@ fun TrackCard(
|
|||||||
fun CoverImage(
|
fun CoverImage(
|
||||||
title: String,
|
title: String,
|
||||||
coverURL: String,
|
coverURL: String,
|
||||||
scope: CoroutineScope,
|
|
||||||
loadImage: suspend (String) -> Picture,
|
loadImage: suspend (String) -> Picture,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@ -160,7 +174,6 @@ fun CoverImage(
|
|||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
//color = colorAccent,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/*scope.launch {
|
/*scope.launch {
|
||||||
@ -173,7 +186,7 @@ fun DownloadAllButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
text = { Text("Download All") },
|
text = { Text("Download All") },
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
icon = { Icon(imageVector = DownloadAllImage(),"Download All Button",tint = Color(0xFF000000)) },
|
icon = { Icon(imageVector = DownloadAllImage(), "Download All Button", tint = Color(0xFF000000)) },
|
||||||
backgroundColor = colorAccent,
|
backgroundColor = colorAccent,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
|
@ -17,26 +17,54 @@
|
|||||||
package com.shabinder.common.uikit
|
package com.shabinder.common.uikit
|
||||||
|
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.*
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedButton
|
||||||
|
import androidx.compose.material.Tab
|
||||||
|
import androidx.compose.material.TabPosition
|
||||||
|
import androidx.compose.material.TabRow
|
||||||
import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
|
import androidx.compose.material.TabRowDefaults.tabIndicatorOffset
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextField
|
||||||
import androidx.compose.material.TextFieldDefaults.textFieldColors
|
import androidx.compose.material.TextFieldDefaults.textFieldColors
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.History
|
import androidx.compose.material.icons.outlined.History
|
||||||
import androidx.compose.material.icons.outlined.Info
|
import androidx.compose.material.icons.outlined.Info
|
||||||
import androidx.compose.material.icons.rounded.*
|
import androidx.compose.material.icons.rounded.CardGiftcard
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.material.icons.rounded.Edit
|
||||||
|
import androidx.compose.material.icons.rounded.Flag
|
||||||
|
import androidx.compose.material.icons.rounded.Share
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
@ -53,7 +81,7 @@ import com.shabinder.common.main.SpotiFlyerMain.HomeCategory
|
|||||||
import com.shabinder.common.models.DownloadRecord
|
import com.shabinder.common.models.DownloadRecord
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SpotiFlyerMainContent(component: SpotiFlyerMain){
|
fun SpotiFlyerMainContent(component: SpotiFlyerMain) {
|
||||||
val model by component.models.collectAsState(SpotiFlyerMain.State())
|
val model by component.models.collectAsState(SpotiFlyerMain.State())
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@ -69,7 +97,7 @@ fun SpotiFlyerMainContent(component: SpotiFlyerMain){
|
|||||||
component::selectCategory
|
component::selectCategory
|
||||||
)
|
)
|
||||||
|
|
||||||
when(model.selectedCategory){
|
when (model.selectedCategory) {
|
||||||
HomeCategory.About -> AboutColumn()
|
HomeCategory.About -> AboutColumn()
|
||||||
HomeCategory.History -> HistoryColumn(
|
HomeCategory.History -> HistoryColumn(
|
||||||
model.records.sortedByDescending { it.id },
|
model.records.sortedByDescending { it.id },
|
||||||
@ -87,7 +115,7 @@ fun HomeTabBar(
|
|||||||
selectCategory: (HomeCategory) -> Unit,
|
selectCategory: (HomeCategory) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val selectedIndex =categories.indexOfFirst { it == selectedCategory }
|
val selectedIndex = categories.indexOfFirst { it == selectedCategory }
|
||||||
val indicator = @Composable { tabPositions: List<TabPosition> ->
|
val indicator = @Composable { tabPositions: List<TabPosition> ->
|
||||||
HomeCategoryTabIndicator(
|
HomeCategoryTabIndicator(
|
||||||
Modifier.tabIndicatorOffset(tabPositions[selectedIndex])
|
Modifier.tabIndicatorOffset(tabPositions[selectedIndex])
|
||||||
@ -99,57 +127,62 @@ fun HomeTabBar(
|
|||||||
indicator = indicator,
|
indicator = indicator,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
categories.forEachIndexed { index, category ->
|
categories.forEachIndexed { index, category ->
|
||||||
Tab(
|
Tab(
|
||||||
selected = index == selectedIndex,
|
selected = index == selectedIndex,
|
||||||
onClick = { selectCategory(category) },
|
onClick = { selectCategory(category) },
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
text = when (category) {
|
text = when (category) {
|
||||||
HomeCategory.About -> "About"
|
HomeCategory.About -> "About"
|
||||||
HomeCategory.History -> "History"
|
HomeCategory.History -> "History"
|
||||||
},
|
},
|
||||||
style = MaterialTheme.typography.body2
|
style = MaterialTheme.typography.body2
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
icon = {
|
icon = {
|
||||||
when (category) {
|
when (category) {
|
||||||
HomeCategory.About -> Icon(Icons.Outlined.Info,"Info Tab")
|
HomeCategory.About -> Icon(Icons.Outlined.Info, "Info Tab")
|
||||||
HomeCategory.History -> Icon(Icons.Outlined.History,"History Tab")
|
HomeCategory.History -> Icon(Icons.Outlined.History, "History Tab")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchPanel(
|
fun SearchPanel(
|
||||||
link:String,
|
link: String,
|
||||||
updateLink:(String) -> Unit,
|
updateLink: (String) -> Unit,
|
||||||
onSearch:(String) -> Unit,
|
onSearch: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
){
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = modifier.padding(top = 16.dp)
|
modifier = modifier.padding(top = 16.dp)
|
||||||
){
|
) {
|
||||||
TextField(
|
TextField(
|
||||||
value = link,
|
value = link,
|
||||||
onValueChange = updateLink ,
|
onValueChange = updateLink,
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(Icons.Rounded.Edit,"Link Text Box",tint = Color.LightGray)
|
Icon(Icons.Rounded.Edit, "Link Text Box", tint = Color.LightGray)
|
||||||
},
|
},
|
||||||
label = { Text(text = "Paste Link Here...",color = Color.LightGray) },
|
label = { Text(text = "Paste Link Here...", color = Color.LightGray) },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
textStyle = TextStyle.Default.merge(TextStyle(fontSize = 18.sp,color = Color.White)),
|
textStyle = TextStyle.Default.merge(TextStyle(fontSize = 18.sp, color = Color.White)),
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
|
||||||
modifier = modifier.padding(12.dp).fillMaxWidth()
|
modifier = modifier.padding(12.dp).fillMaxWidth()
|
||||||
.border(
|
.border(
|
||||||
BorderStroke(2.dp, Brush.horizontalGradient(listOf(
|
BorderStroke(
|
||||||
colorPrimary,
|
2.dp,
|
||||||
colorAccent
|
Brush.horizontalGradient(
|
||||||
))),
|
listOf(
|
||||||
|
colorPrimary,
|
||||||
|
colorAccent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
RoundedCornerShape(30.dp)
|
RoundedCornerShape(30.dp)
|
||||||
),
|
),
|
||||||
shape = RoundedCornerShape(size = 30.dp),
|
shape = RoundedCornerShape(size = 30.dp),
|
||||||
@ -162,29 +195,34 @@ fun SearchPanel(
|
|||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
modifier = Modifier.padding(12.dp).wrapContentWidth(),
|
modifier = Modifier.padding(12.dp).wrapContentWidth(),
|
||||||
onClick = {
|
onClick = {
|
||||||
if(link.isBlank()) showPopUpMessage("Enter A Link!")
|
if (link.isBlank()) showPopUpMessage("Enter A Link!")
|
||||||
else{
|
else {
|
||||||
//TODO if(!isOnline(ctx)) showPopUpMessage("Check Your Internet Connection") else
|
// TODO if(!isOnline(ctx)) showPopUpMessage("Check Your Internet Connection") else
|
||||||
onSearch(link)
|
onSearch(link)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
border = BorderStroke(1.dp, Brush.horizontalGradient(listOf(
|
border = BorderStroke(
|
||||||
colorPrimary,
|
1.dp,
|
||||||
colorAccent
|
Brush.horizontalGradient(
|
||||||
)))
|
listOf(
|
||||||
){
|
colorPrimary,
|
||||||
Text(text = "Search",style = SpotiFlyerTypography.h6,modifier = Modifier.padding(4.dp))
|
colorAccent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(text = "Search", style = SpotiFlyerTypography.h6, modifier = Modifier.padding(4.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AboutColumn(modifier: Modifier = Modifier) {
|
fun AboutColumn(modifier: Modifier = Modifier) {
|
||||||
//TODO Make Scrollable
|
// TODO Make Scrollable
|
||||||
Column(modifier.fillMaxSize().verticalScroll(rememberScrollState())) {
|
Column(modifier.fillMaxSize().verticalScroll(rememberScrollState())) {
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
border = BorderStroke(1.dp,Color.Gray)
|
border = BorderStroke(1.dp, Color.Gray)
|
||||||
) {
|
) {
|
||||||
Column(modifier.padding(12.dp)) {
|
Column(modifier.padding(12.dp)) {
|
||||||
Text(
|
Text(
|
||||||
@ -193,34 +231,41 @@ fun AboutColumn(modifier: Modifier = Modifier) {
|
|||||||
color = colorAccent
|
color = colorAccent
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.padding(top = 12.dp))
|
Spacer(modifier = Modifier.padding(top = 12.dp))
|
||||||
Row(horizontalArrangement = Arrangement.Center,modifier = modifier.fillMaxWidth()) {
|
Row(horizontalArrangement = Arrangement.Center, modifier = modifier.fillMaxWidth()) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = SpotifyLogo(),
|
imageVector = SpotifyLogo(),
|
||||||
"Open Spotify",
|
"Open Spotify",
|
||||||
tint = Color.Unspecified,
|
tint = Color.Unspecified,
|
||||||
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
||||||
onClick = { openPlatform("com.spotify.music","http://open.spotify.com") })
|
onClick = { openPlatform("com.spotify.music", "http://open.spotify.com") }
|
||||||
|
)
|
||||||
)
|
)
|
||||||
Spacer(modifier = modifier.padding(start = 16.dp))
|
Spacer(modifier = modifier.padding(start = 16.dp))
|
||||||
Icon(imageVector = GaanaLogo(),
|
Icon(
|
||||||
|
imageVector = GaanaLogo(),
|
||||||
"Open Gaana",
|
"Open Gaana",
|
||||||
tint = Color.Unspecified,
|
tint = Color.Unspecified,
|
||||||
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
||||||
onClick = { openPlatform("com.gaana","http://gaana.com") })
|
onClick = { openPlatform("com.gaana", "http://gaana.com") }
|
||||||
|
)
|
||||||
)
|
)
|
||||||
Spacer(modifier = modifier.padding(start = 16.dp))
|
Spacer(modifier = modifier.padding(start = 16.dp))
|
||||||
Icon(imageVector = YoutubeLogo(),
|
Icon(
|
||||||
|
imageVector = YoutubeLogo(),
|
||||||
"Open Youtube",
|
"Open Youtube",
|
||||||
tint = Color.Unspecified,
|
tint = Color.Unspecified,
|
||||||
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
||||||
onClick = { openPlatform("com.google.android.youtube","http://m.youtube.com") })
|
onClick = { openPlatform("com.google.android.youtube", "http://m.youtube.com") }
|
||||||
|
)
|
||||||
)
|
)
|
||||||
Spacer(modifier = modifier.padding(start = 12.dp))
|
Spacer(modifier = modifier.padding(start = 12.dp))
|
||||||
Icon(imageVector = YoutubeMusicLogo(),
|
Icon(
|
||||||
|
imageVector = YoutubeMusicLogo(),
|
||||||
"Open Youtube Music",
|
"Open Youtube Music",
|
||||||
tint = Color.Unspecified,
|
tint = Color.Unspecified,
|
||||||
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
modifier = Modifier.clip(SpotiFlyerShapes.small).clickable(
|
||||||
onClick = { openPlatform("com.google.android.apps.youtube.music","https://music.youtube.com/") })
|
onClick = { openPlatform("com.google.android.apps.youtube.music", "https://music.youtube.com/") }
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,7 +273,7 @@ fun AboutColumn(modifier: Modifier = Modifier) {
|
|||||||
Spacer(modifier = Modifier.padding(top = 8.dp))
|
Spacer(modifier = Modifier.padding(top = 8.dp))
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
border = BorderStroke(1.dp,Color.Gray)//Gray
|
border = BorderStroke(1.dp, Color.Gray) // Gray
|
||||||
) {
|
) {
|
||||||
Column(modifier.padding(12.dp)) {
|
Column(modifier.padding(12.dp)) {
|
||||||
Text(
|
Text(
|
||||||
@ -237,12 +282,14 @@ fun AboutColumn(modifier: Modifier = Modifier) {
|
|||||||
color = colorAccent
|
color = colorAccent
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.padding(top = 6.dp))
|
Spacer(modifier = Modifier.padding(top = 6.dp))
|
||||||
Row(verticalAlignment = Alignment.CenterVertically,
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth().clickable(
|
modifier = Modifier.fillMaxWidth().clickable(
|
||||||
onClick = { openPlatform("","http://github.com/Shabinder/SpotiFlyer") })
|
onClick = { openPlatform("", "http://github.com/Shabinder/SpotiFlyer") }
|
||||||
|
)
|
||||||
.padding(vertical = 6.dp)
|
.padding(vertical = 6.dp)
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = GithubLogo(),"Open Project Repo",tint = Color(0xFFCCCCCC))
|
Icon(imageVector = GithubLogo(), "Open Project Repo", tint = Color(0xFFCCCCCC))
|
||||||
Spacer(modifier = Modifier.padding(start = 16.dp))
|
Spacer(modifier = Modifier.padding(start = 16.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
@ -257,10 +304,10 @@ fun AboutColumn(modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
|
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
|
||||||
.clickable(onClick = { openPlatform("","http://github.com/Shabinder/SpotiFlyer") }),
|
.clickable(onClick = { openPlatform("", "http://github.com/Shabinder/SpotiFlyer") }),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Rounded.Flag,"Help Translate",Modifier.size(32.dp))
|
Icon(Icons.Rounded.Flag, "Help Translate", Modifier.size(32.dp))
|
||||||
Spacer(modifier = Modifier.padding(start = 16.dp))
|
Spacer(modifier = Modifier.padding(start = 16.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
@ -278,7 +325,7 @@ fun AboutColumn(modifier: Modifier = Modifier) {
|
|||||||
.clickable(onClick = { giveDonation() }),
|
.clickable(onClick = { giveDonation() }),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Rounded.CardGiftcard,"Support Developer")
|
Icon(Icons.Rounded.CardGiftcard, "Support Developer")
|
||||||
Spacer(modifier = Modifier.padding(start = 16.dp))
|
Spacer(modifier = Modifier.padding(start = 16.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
@ -293,12 +340,14 @@ fun AboutColumn(modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
|
modifier = modifier.fillMaxWidth().padding(vertical = 6.dp)
|
||||||
.clickable(onClick = {
|
.clickable(
|
||||||
shareApp()
|
onClick = {
|
||||||
}),
|
shareApp()
|
||||||
|
}
|
||||||
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Rounded.Share,"Share SpotiFlyer App")
|
Icon(Icons.Rounded.Share, "Share SpotiFlyer App")
|
||||||
Spacer(modifier = Modifier.padding(start = 16.dp))
|
Spacer(modifier = Modifier.padding(start = 16.dp))
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
@ -319,22 +368,23 @@ fun AboutColumn(modifier: Modifier = Modifier) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun HistoryColumn(
|
fun HistoryColumn(
|
||||||
list: List<DownloadRecord>,
|
list: List<DownloadRecord>,
|
||||||
loadImage:suspend (String)-> Picture,
|
loadImage: suspend (String) -> Picture,
|
||||||
onItemClicked: (String) -> Unit
|
onItemClicked: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
Crossfade(list){
|
Crossfade(list) {
|
||||||
if(it.isEmpty()){
|
if (it.isEmpty()) {
|
||||||
Column(Modifier.padding(bottom = 32.dp).fillMaxSize(),verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(Modifier.padding(bottom = 32.dp).fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
Icon(Icons.Outlined.Info,"No History Available Yet",modifier = Modifier.size(80.dp),
|
Icon(
|
||||||
|
Icons.Outlined.Info, "No History Available Yet", modifier = Modifier.size(80.dp),
|
||||||
colorOffWhite
|
colorOffWhite
|
||||||
)
|
)
|
||||||
Text("No History Available",style = SpotiFlyerTypography.h4.copy(fontWeight = FontWeight.Light),textAlign = TextAlign.Center)
|
Text("No History Available", style = SpotiFlyerTypography.h4.copy(fontWeight = FontWeight.Light), textAlign = TextAlign.Center)
|
||||||
}
|
}
|
||||||
}else{
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
content = {
|
content = {
|
||||||
items(it.distinctBy {record -> record.coverUrl }) { record ->
|
items(it.distinctBy { record -> record.coverUrl }) { record ->
|
||||||
DownloadRecordItem(
|
DownloadRecordItem(
|
||||||
item = record,
|
item = record,
|
||||||
loadImage,
|
loadImage,
|
||||||
@ -351,39 +401,40 @@ fun HistoryColumn(
|
|||||||
@Composable
|
@Composable
|
||||||
fun DownloadRecordItem(
|
fun DownloadRecordItem(
|
||||||
item: DownloadRecord,
|
item: DownloadRecord,
|
||||||
loadImage:suspend (String)-> Picture,
|
loadImage: suspend (String) -> Picture,
|
||||||
onItemClicked:(String)->Unit
|
onItemClicked: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier.fillMaxWidth().padding(end = 8.dp)) {
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(end = 8.dp)) {
|
||||||
ImageLoad(
|
ImageLoad(
|
||||||
item.coverUrl,
|
item.coverUrl,
|
||||||
loadImage,
|
loadImage,
|
||||||
"Album Art",
|
"Album Art",
|
||||||
modifier = Modifier.height(70.dp).width(70.dp).clip(SpotiFlyerShapes.medium)
|
modifier = Modifier.height(70.dp).width(70.dp).clip(SpotiFlyerShapes.medium)
|
||||||
)
|
)
|
||||||
Column(modifier = Modifier.padding(horizontal = 8.dp).height(60.dp).weight(1f),verticalArrangement = Arrangement.SpaceEvenly) {
|
Column(modifier = Modifier.padding(horizontal = 8.dp).height(60.dp).weight(1f), verticalArrangement = Arrangement.SpaceEvenly) {
|
||||||
Text(item.name,maxLines = 1,overflow = TextOverflow.Ellipsis,style = SpotiFlyerTypography.h6,color = colorAccent)
|
Text(item.name, maxLines = 1, overflow = TextOverflow.Ellipsis, style = SpotiFlyerTypography.h6, color = colorAccent)
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.Bottom,
|
verticalAlignment = Alignment.Bottom,
|
||||||
modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize()
|
modifier = Modifier.padding(horizontal = 8.dp).fillMaxSize()
|
||||||
){
|
) {
|
||||||
Text(item.type,fontSize = 13.sp,color = colorOffWhite)
|
Text(item.type, fontSize = 13.sp, color = colorOffWhite)
|
||||||
Text("Tracks: ${item.totalFiles}",fontSize = 13.sp,color = colorOffWhite)
|
Text("Tracks: ${item.totalFiles}", fontSize = 13.sp, color = colorOffWhite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Image(
|
Image(
|
||||||
imageVector = ShareImage(),
|
imageVector = ShareImage(),
|
||||||
"Research",
|
"Research",
|
||||||
modifier = Modifier.clickable(onClick = {
|
modifier = Modifier.clickable(
|
||||||
//if(!isOnline(ctx)) showDialog("Check Your Internet Connection") else
|
onClick = {
|
||||||
onItemClicked(item.link)
|
// if(!isOnline(ctx)) showDialog("Check Your Internet Connection") else
|
||||||
})
|
onItemClicked(item.link)
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeCategoryTabIndicator(
|
fun HomeCategoryTabIndicator(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -16,11 +16,24 @@
|
|||||||
|
|
||||||
package com.shabinder.common.uikit
|
package com.shabinder.common.uikit
|
||||||
|
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.MutableTransitionState
|
||||||
import androidx.compose.animation.core.Spring.StiffnessLow
|
import androidx.compose.animation.core.Spring.StiffnessLow
|
||||||
|
import androidx.compose.animation.core.animateDp
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.TopAppBar
|
import androidx.compose.material.TopAppBar
|
||||||
@ -45,7 +58,7 @@ import com.shabinder.common.uikit.utils.verticalGradientScrim
|
|||||||
private var isSplashShown = SplashState.Shown
|
private var isSplashShown = SplashState.Shown
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SpotiFlyerRootContent(component: SpotiFlyerRoot, statusBarHeight:Dp = 0.dp): SpotiFlyerRoot {
|
fun SpotiFlyerRootContent(component: SpotiFlyerRoot, statusBarHeight: Dp = 0.dp): SpotiFlyerRoot {
|
||||||
|
|
||||||
val transitionState = remember { MutableTransitionState(SplashState.Shown) }
|
val transitionState = remember { MutableTransitionState(SplashState.Shown) }
|
||||||
val transition = updateTransition(transitionState)
|
val transition = updateTransition(transitionState)
|
||||||
@ -63,10 +76,10 @@ fun SpotiFlyerRootContent(component: SpotiFlyerRoot, statusBarHeight:Dp = 0.dp):
|
|||||||
val contentTopPadding by transition.animateDp(
|
val contentTopPadding by transition.animateDp(
|
||||||
transitionSpec = { spring(stiffness = StiffnessLow) }
|
transitionSpec = { spring(stiffness = StiffnessLow) }
|
||||||
) {
|
) {
|
||||||
if (it == SplashState.Shown && isSplashShown == SplashState.Shown) 100.dp else 0.dp
|
if (it == SplashState.Shown && isSplashShown == SplashState.Shown) 100.dp else 0.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
Box{
|
Box {
|
||||||
Splash(
|
Splash(
|
||||||
modifier = Modifier.alpha(splashAlpha),
|
modifier = Modifier.alpha(splashAlpha),
|
||||||
onTimeout = {
|
onTimeout = {
|
||||||
@ -85,17 +98,17 @@ fun SpotiFlyerRootContent(component: SpotiFlyerRoot, statusBarHeight:Dp = 0.dp):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(modifier: Modifier = Modifier, topPadding: Dp = 0.dp,statusBarHeight: Dp = 0.dp,component: SpotiFlyerRoot) {
|
fun MainScreen(modifier: Modifier = Modifier, topPadding: Dp = 0.dp, statusBarHeight: Dp = 0.dp, component: SpotiFlyerRoot) {
|
||||||
|
|
||||||
val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.65f)
|
val appBarColor = MaterialTheme.colors.surface.copy(alpha = 0.65f)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier.fillMaxSize()
|
modifier = modifier.fillMaxSize()
|
||||||
.verticalGradientScrim(
|
.verticalGradientScrim(
|
||||||
color = colorPrimaryDark.copy(alpha = 0.38f),
|
color = colorPrimaryDark.copy(alpha = 0.38f),
|
||||||
startYPercentage = 0.29f,
|
startYPercentage = 0.29f,
|
||||||
endYPercentage = 0f,
|
endYPercentage = 0f,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Spacer(Modifier.background(appBarColor).height(statusBarHeight).fillMaxWidth())
|
Spacer(Modifier.background(appBarColor).height(statusBarHeight).fillMaxWidth())
|
||||||
LocalViewConfiguration.current
|
LocalViewConfiguration.current
|
||||||
@ -136,7 +149,7 @@ fun AppBar(
|
|||||||
style = appNameStyle
|
style = appNameStyle
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},/*
|
}, /*
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { *//*TODO: Open Preferences*//* }
|
onClick = { *//*TODO: Open Preferences*//* }
|
||||||
|
@ -20,11 +20,11 @@ import androidx.compose.material.MaterialTheme
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SpotiFlyerTheme(content: @Composable() () -> Unit) {
|
fun SpotiFlyerTheme(content: @Composable () -> Unit) {
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colors = SpotiFlyerColors,
|
colors = SpotiFlyerColors,
|
||||||
typography = SpotiFlyerTypography,
|
typography = SpotiFlyerTypography,
|
||||||
shapes = SpotiFlyerShapes,
|
shapes = SpotiFlyerShapes,
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -23,107 +23,106 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
expect fun montserratFont(): FontFamily
|
||||||
expect fun montserratFont():FontFamily
|
expect fun pristineFont(): FontFamily
|
||||||
expect fun pristineFont():FontFamily
|
|
||||||
|
|
||||||
val SpotiFlyerTypography = Typography(
|
val SpotiFlyerTypography = Typography(
|
||||||
h1 = TextStyle(
|
h1 = TextStyle(
|
||||||
fontFamily = montserratFont(),
|
fontFamily = montserratFont(),
|
||||||
fontSize = 96.sp,
|
fontSize = 96.sp,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
lineHeight = 117.sp,
|
lineHeight = 117.sp,
|
||||||
letterSpacing = (-1.5).sp
|
letterSpacing = (-1.5).sp
|
||||||
),
|
),
|
||||||
h2 = TextStyle(
|
h2 = TextStyle(
|
||||||
fontFamily = montserratFont(),
|
fontFamily = montserratFont(),
|
||||||
fontSize = 60.sp,
|
fontSize = 60.sp,
|
||||||
fontWeight = FontWeight.Light,
|
fontWeight = FontWeight.Light,
|
||||||
lineHeight = 73.sp,
|
lineHeight = 73.sp,
|
||||||
letterSpacing = (-0.5).sp
|
letterSpacing = (-0.5).sp
|
||||||
),
|
),
|
||||||
h3 = TextStyle(
|
h3 = TextStyle(
|
||||||
fontFamily = montserratFont(),
|
fontFamily = montserratFont(),
|
||||||
fontSize = 48.sp,
|
fontSize = 48.sp,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
lineHeight = 59.sp
|
lineHeight = 59.sp
|
||||||
),
|
),
|
||||||
h4 = TextStyle(
|
h4 = TextStyle(
|
||||||
fontFamily = montserratFont(),
|
fontFamily = montserratFont(),
|
||||||
fontSize = 30.sp,
|
fontSize = 30.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
lineHeight = 37.sp
|
lineHeight = 37.sp
|
||||||
),
|
),
|
||||||
h5 = TextStyle(
|
h5 = TextStyle(
|
||||||
fontFamily = montserratFont(),
|
fontFamily = montserratFont(),
|
||||||
fontSize = 24.sp,
|
fontSize = 24.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
lineHeight = 29.sp
|
lineHeight = 29.sp
|
||||||
),
|
),
|
||||||
h6 = TextStyle(
|
h6 = TextStyle(
|
||||||
fontFamily = montserratFont(),
|
fontFamily = montserratFont(),
|
||||||
fontSize = 18.sp,
|
fontSize = 18.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
lineHeight = 26.sp,
|
lineHeight = 26.sp,
|
||||||
letterSpacing = 0.5.sp
|
letterSpacing = 0.5.sp
|
||||||
|
|
||||||
),
|
),
|
||||||
subtitle1 = TextStyle(
|
subtitle1 = TextStyle(
|
||||||
fontFamily = montserratFont(),
|
fontFamily = montserratFont(),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
lineHeight = 20.sp,
|
lineHeight = 20.sp,
|
||||||
letterSpacing = 0.5.sp
|
letterSpacing = 0.5.sp
|
||||||
),
|
),
|
||||||
subtitle2 = TextStyle(
|
subtitle2 = TextStyle(
|
||||||
fontFamily = montserratFont(),
|
fontFamily = montserratFont(),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
lineHeight = 17.sp,
|
lineHeight = 17.sp,
|
||||||
letterSpacing = 0.1.sp
|
letterSpacing = 0.1.sp
|
||||||
),
|
),
|
||||||
body1 = TextStyle(
|
body1 = TextStyle(
|
||||||
fontFamily = montserratFont(),
|
fontFamily = montserratFont(),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Medium,
|
||||||
lineHeight = 20.sp,
|
lineHeight = 20.sp,
|
||||||
letterSpacing = 0.15.sp,
|
letterSpacing = 0.15.sp,
|
||||||
),
|
),
|
||||||
body2 = TextStyle(
|
body2 = TextStyle(
|
||||||
fontFamily = montserratFont(),
|
fontFamily = montserratFont(),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
lineHeight = 20.sp,
|
lineHeight = 20.sp,
|
||||||
letterSpacing = 0.25.sp
|
letterSpacing = 0.25.sp
|
||||||
),
|
),
|
||||||
button = TextStyle(
|
button = TextStyle(
|
||||||
fontFamily = montserratFont(),
|
fontFamily = montserratFont(),
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
letterSpacing = 1.25.sp
|
letterSpacing = 1.25.sp
|
||||||
),
|
),
|
||||||
caption = TextStyle(
|
caption = TextStyle(
|
||||||
fontFamily = montserratFont(),
|
fontFamily = montserratFont(),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
letterSpacing = 0.sp
|
letterSpacing = 0.sp
|
||||||
),
|
),
|
||||||
overline = TextStyle(
|
overline = TextStyle(
|
||||||
fontFamily = montserratFont(),
|
fontFamily = montserratFont(),
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
letterSpacing = 1.sp
|
letterSpacing = 1.sp
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val appNameStyle = TextStyle(
|
val appNameStyle = TextStyle(
|
||||||
fontFamily = pristineFont(),
|
fontFamily = pristineFont(),
|
||||||
fontSize = 40.sp,
|
fontSize = 40.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
lineHeight = 42.sp,
|
lineHeight = 42.sp,
|
||||||
letterSpacing = (1.5).sp,
|
letterSpacing = (1.5).sp,
|
||||||
color = Color(0xFFECECEC)
|
color = Color(0xFFECECEC)
|
||||||
)
|
)
|
@ -17,7 +17,13 @@
|
|||||||
package com.shabinder.common.uikit.splash
|
package com.shabinder.common.uikit.splash
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -29,7 +35,11 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.shabinder.common.uikit.*
|
import com.shabinder.common.uikit.HeartIcon
|
||||||
|
import com.shabinder.common.uikit.SpotiFlyerLogo
|
||||||
|
import com.shabinder.common.uikit.SpotiFlyerTypography
|
||||||
|
import com.shabinder.common.uikit.colorAccent
|
||||||
|
import com.shabinder.common.uikit.colorPrimary
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
private const val SplashWaitTime: Long = 2000
|
private const val SplashWaitTime: Long = 2000
|
||||||
@ -45,7 +55,7 @@ fun Splash(modifier: Modifier = Modifier, onTimeout: () -> Unit) {
|
|||||||
delay(SplashWaitTime)
|
delay(SplashWaitTime)
|
||||||
currentOnTimeout()
|
currentOnTimeout()
|
||||||
}
|
}
|
||||||
Image(imageVector = SpotiFlyerLogo(),"SpotiFlyer Logo")
|
Image(imageVector = SpotiFlyerLogo(), "SpotiFlyer Logo")
|
||||||
MadeInIndia(Modifier.align(Alignment.BottomCenter))
|
MadeInIndia(Modifier.align(Alignment.BottomCenter))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,7 +63,7 @@ fun Splash(modifier: Modifier = Modifier, onTimeout: () -> Unit) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun MadeInIndia(
|
fun MadeInIndia(
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
){
|
) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = modifier.padding(8.dp)
|
modifier = modifier.padding(8.dp)
|
||||||
@ -68,7 +78,7 @@ fun MadeInIndia(
|
|||||||
fontSize = 22.sp
|
fontSize = 22.sp
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.padding(start = 4.dp))
|
Spacer(modifier = Modifier.padding(start = 4.dp))
|
||||||
Icon(HeartIcon(),"Love",tint = Color.Unspecified)
|
Icon(HeartIcon(), "Love", tint = Color.Unspecified)
|
||||||
Spacer(modifier = Modifier.padding(start = 4.dp))
|
Spacer(modifier = Modifier.padding(start = 4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = " in India",
|
text = " in India",
|
||||||
|
@ -40,8 +40,10 @@ import kotlin.math.pow
|
|||||||
*/
|
*/
|
||||||
fun Modifier.verticalGradientScrim(
|
fun Modifier.verticalGradientScrim(
|
||||||
color: Color,
|
color: Color,
|
||||||
/*@FloatRange(from = 0.0, to = 1.0)*/ startYPercentage: Float = 0f,
|
/*@FloatRange(from = 0.0, to = 1.0)*/
|
||||||
/*@FloatRange(from = 0.0, to = 1.0)*/ endYPercentage: Float = 1f,
|
startYPercentage: Float = 0f,
|
||||||
|
/*@FloatRange(from = 0.0, to = 1.0)*/
|
||||||
|
endYPercentage: Float = 1f,
|
||||||
decay: Float = 1.0f,
|
decay: Float = 1.0f,
|
||||||
numStops: Int = 16,
|
numStops: Int = 16,
|
||||||
fixedHeight: Float? = null
|
fixedHeight: Float? = null
|
||||||
|
@ -19,7 +19,12 @@ package com.shabinder.common.uikit
|
|||||||
|
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
@ -34,26 +39,26 @@ import kotlinx.coroutines.withContext
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun ImageLoad(
|
actual fun ImageLoad(
|
||||||
link:String,
|
link: String,
|
||||||
loader:suspend (String) -> Picture,
|
loader: suspend (String) -> Picture,
|
||||||
desc: String,
|
desc: String,
|
||||||
modifier:Modifier,
|
modifier: Modifier,
|
||||||
//placeholder: ImageVector
|
// placeholder: ImageVector
|
||||||
) {
|
) {
|
||||||
var pic by remember(link) { mutableStateOf<ImageBitmap?>(null) }
|
var pic by remember(link) { mutableStateOf<ImageBitmap?>(null) }
|
||||||
LaunchedEffect(link){
|
LaunchedEffect(link) {
|
||||||
withContext(dispatcherIO) {
|
withContext(dispatcherIO) {
|
||||||
pic = loader(link).image
|
pic = loader(link).image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Crossfade(pic){
|
Crossfade(pic) {
|
||||||
if(it == null) Image(PlaceHolderImage(), desc, modifier,contentScale = ContentScale.Crop) else Image(it, desc, modifier,contentScale = ContentScale.Crop)
|
if (it == null) Image(PlaceHolderImage(), desc, modifier, contentScale = ContentScale.Crop) else Image(it, desc, modifier, contentScale = ContentScale.Crop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun DownloadImageTick(){
|
actual fun DownloadImageTick() {
|
||||||
Image(
|
Image(
|
||||||
vectorXmlResource("drawable/ic_tick.xml"),
|
vectorXmlResource("drawable/ic_tick.xml"),
|
||||||
"Downloaded"
|
"Downloaded"
|
||||||
@ -72,7 +77,7 @@ actual fun pristineFont() = FontFamily(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun DownloadImageError(){
|
actual fun DownloadImageError() {
|
||||||
Image(
|
Image(
|
||||||
vectorXmlResource("drawable/ic_error.xml"),
|
vectorXmlResource("drawable/ic_error.xml"),
|
||||||
"Can't Download"
|
"Can't Download"
|
||||||
@ -80,7 +85,7 @@ actual fun DownloadImageError(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun DownloadImageArrow(modifier: Modifier){
|
actual fun DownloadImageArrow(modifier: Modifier) {
|
||||||
Image(
|
Image(
|
||||||
vectorXmlResource("drawable/ic_arrow.xml"),
|
vectorXmlResource("drawable/ic_arrow.xml"),
|
||||||
"Download",
|
"Download",
|
||||||
@ -89,33 +94,32 @@ actual fun DownloadImageArrow(modifier: Modifier){
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun DownloadAllImage():ImageVector = vectorXmlResource("drawable/ic_download_arrow.xml")
|
actual fun DownloadAllImage(): ImageVector = vectorXmlResource("drawable/ic_download_arrow.xml")
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun ShareImage():ImageVector = vectorXmlResource("drawable/ic_share_open.xml")
|
actual fun ShareImage(): ImageVector = vectorXmlResource("drawable/ic_share_open.xml")
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun PlaceHolderImage():ImageVector = vectorXmlResource("drawable/music.xml")
|
actual fun PlaceHolderImage(): ImageVector = vectorXmlResource("drawable/music.xml")
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun SpotiFlyerLogo():ImageVector =
|
actual fun SpotiFlyerLogo(): ImageVector =
|
||||||
vectorXmlResource("drawable/ic_spotiflyer_logo.xml")
|
vectorXmlResource("drawable/ic_spotiflyer_logo.xml")
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun HeartIcon():ImageVector = vectorXmlResource("drawable/ic_heart.xml")
|
actual fun HeartIcon(): ImageVector = vectorXmlResource("drawable/ic_heart.xml")
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun SpotifyLogo():ImageVector = vectorXmlResource("drawable/ic_spotify_logo.xml")
|
actual fun SpotifyLogo(): ImageVector = vectorXmlResource("drawable/ic_spotify_logo.xml")
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun YoutubeLogo():ImageVector = vectorXmlResource("drawable/ic_youtube.xml")
|
actual fun YoutubeLogo(): ImageVector = vectorXmlResource("drawable/ic_youtube.xml")
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun GaanaLogo():ImageVector = vectorXmlResource("drawable/ic_gaana.xml")
|
actual fun GaanaLogo(): ImageVector = vectorXmlResource("drawable/ic_gaana.xml")
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun YoutubeMusicLogo():ImageVector = vectorXmlResource("drawable/ic_youtube_music_logo.xml")
|
actual fun YoutubeMusicLogo(): ImageVector = vectorXmlResource("drawable/ic_youtube_music_logo.xml")
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun GithubLogo():ImageVector = vectorXmlResource("drawable/ic_github.xml")
|
actual fun GithubLogo(): ImageVector = vectorXmlResource("drawable/ic_github.xml")
|
||||||
|
@ -78,7 +78,7 @@ actual fun Toast(
|
|||||||
isShown = false
|
isShown = false
|
||||||
visibility.value = false
|
visibility.value = false
|
||||||
}
|
}
|
||||||
onDispose { }
|
onDispose { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
|
|
||||||
package com.shabinder.common.models
|
package com.shabinder.common.models
|
||||||
|
|
||||||
sealed class AllPlatforms{
|
sealed class AllPlatforms {
|
||||||
object Js:AllPlatforms()
|
object Js : AllPlatforms()
|
||||||
object Jvm:AllPlatforms()
|
object Jvm : AllPlatforms()
|
||||||
object Native:AllPlatforms()
|
object Native : AllPlatforms()
|
||||||
}
|
}
|
||||||
|
@ -16,24 +16,24 @@
|
|||||||
|
|
||||||
package com.shabinder.common.models
|
package com.shabinder.common.models
|
||||||
|
|
||||||
sealed class CorsProxy(open val url: String){
|
sealed class CorsProxy(open val url: String) {
|
||||||
data class SelfHostedCorsProxy(override val url:String = "https://kind-grasshopper-73.telebit.io/cors/"):CorsProxy(url)
|
data class SelfHostedCorsProxy(override val url: String = "https://kind-grasshopper-73.telebit.io/cors/") : CorsProxy(url)
|
||||||
data class PublicProxyWithExtension(override val url:String = "https://cors.bridged.cc/"):CorsProxy(url)
|
data class PublicProxyWithExtension(override val url: String = "https://cors.bridged.cc/") : CorsProxy(url)
|
||||||
|
|
||||||
fun toggle(mode:CorsProxy? = null):CorsProxy{
|
fun toggle(mode: CorsProxy? = null): CorsProxy {
|
||||||
mode?.let {
|
mode?.let {
|
||||||
corsProxy = mode
|
corsProxy = mode
|
||||||
return corsProxy
|
return corsProxy
|
||||||
}
|
}
|
||||||
corsProxy = when(corsProxy){
|
corsProxy = when (corsProxy) {
|
||||||
is SelfHostedCorsProxy -> PublicProxyWithExtension()
|
is SelfHostedCorsProxy -> PublicProxyWithExtension()
|
||||||
is PublicProxyWithExtension -> SelfHostedCorsProxy()
|
is PublicProxyWithExtension -> SelfHostedCorsProxy()
|
||||||
}
|
}
|
||||||
return corsProxy
|
return corsProxy
|
||||||
}
|
}
|
||||||
|
|
||||||
fun extensionMode():Boolean{
|
fun extensionMode(): Boolean {
|
||||||
return when(corsProxy){
|
return when (corsProxy) {
|
||||||
is SelfHostedCorsProxy -> false
|
is SelfHostedCorsProxy -> false
|
||||||
is PublicProxyWithExtension -> true
|
is PublicProxyWithExtension -> true
|
||||||
}
|
}
|
||||||
@ -44,4 +44,4 @@ sealed class CorsProxy(open val url: String){
|
|||||||
* This Var Keeps Track for Cors Config in JS Platform
|
* This Var Keeps Track for Cors Config in JS Platform
|
||||||
* Default Self Hosted, However ask user to use extension if possible.
|
* Default Self Hosted, However ask user to use extension if possible.
|
||||||
* */
|
* */
|
||||||
var corsProxy:CorsProxy = CorsProxy.SelfHostedCorsProxy()
|
var corsProxy: CorsProxy = CorsProxy.SelfHostedCorsProxy()
|
||||||
|
@ -24,30 +24,29 @@ import kotlinx.serialization.Serializable
|
|||||||
@Parcelize
|
@Parcelize
|
||||||
@Serializable
|
@Serializable
|
||||||
data class TrackDetails(
|
data class TrackDetails(
|
||||||
var title:String,
|
var title: String,
|
||||||
var artists:List<String>,
|
var artists: List<String>,
|
||||||
var durationSec:Int,
|
var durationSec: Int,
|
||||||
var albumName:String?=null,
|
var albumName: String? = null,
|
||||||
var year:String?=null,
|
var year: String? = null,
|
||||||
var comment:String?=null,
|
var comment: String? = null,
|
||||||
var lyrics:String?=null,
|
var lyrics: String? = null,
|
||||||
var trackUrl:String?=null,
|
var trackUrl: String? = null,
|
||||||
var albumArtPath: String,
|
var albumArtPath: String,
|
||||||
var albumArtURL: String,
|
var albumArtURL: String,
|
||||||
var source: Source,
|
var source: Source,
|
||||||
val progress: Int = 2,
|
val progress: Int = 2,
|
||||||
val downloaded: DownloadStatus = DownloadStatus.NotDownloaded,
|
val downloaded: DownloadStatus = DownloadStatus.NotDownloaded,
|
||||||
var outputFilePath: String,
|
var outputFilePath: String,
|
||||||
var videoID:String? = null,
|
var videoID: String? = null,
|
||||||
):Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed class DownloadStatus:Parcelable {
|
sealed class DownloadStatus : Parcelable {
|
||||||
@Parcelize object Downloaded :DownloadStatus()
|
@Parcelize object Downloaded : DownloadStatus()
|
||||||
@Parcelize data class Downloading(val progress: Int = 2):DownloadStatus()
|
@Parcelize data class Downloading(val progress: Int = 2) : DownloadStatus()
|
||||||
@Parcelize object Queued :DownloadStatus()
|
@Parcelize object Queued : DownloadStatus()
|
||||||
@Parcelize object NotDownloaded :DownloadStatus()
|
@Parcelize object NotDownloaded : DownloadStatus()
|
||||||
@Parcelize object Converting :DownloadStatus()
|
@Parcelize object Converting : DownloadStatus()
|
||||||
@Parcelize object Failed :DownloadStatus()
|
@Parcelize object Failed : DownloadStatus()
|
||||||
}
|
}
|
||||||
|
@ -17,10 +17,10 @@
|
|||||||
package com.shabinder.common.models
|
package com.shabinder.common.models
|
||||||
|
|
||||||
data class DownloadRecord(
|
data class DownloadRecord(
|
||||||
var id:Long = 0,
|
var id: Long = 0,
|
||||||
var type:String,
|
var type: String,
|
||||||
var name:String,
|
var name: String,
|
||||||
var link:String,
|
var link: String,
|
||||||
var coverUrl:String,
|
var coverUrl: String,
|
||||||
var totalFiles:Long = 1,
|
var totalFiles: Long = 1,
|
||||||
)
|
)
|
@ -20,7 +20,7 @@ sealed class DownloadResult {
|
|||||||
|
|
||||||
data class Error(val message: String, val cause: Exception? = null) : DownloadResult()
|
data class Error(val message: String, val cause: Exception? = null) : DownloadResult()
|
||||||
|
|
||||||
data class Progress(val progress: Int): DownloadResult()
|
data class Progress(val progress: Int) : DownloadResult()
|
||||||
|
|
||||||
data class Success(val byteArray: ByteArray) : DownloadResult() {
|
data class Success(val byteArray: ByteArray) : DownloadResult() {
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
@ -21,8 +21,8 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class YoutubeTrack(
|
data class YoutubeTrack(
|
||||||
var name: String? = null,
|
var name: String? = null,
|
||||||
var type: String? = null, // Song / Video
|
var type: String? = null, // Song / Video
|
||||||
var artist: String? = null,
|
var artist: String? = null,
|
||||||
var duration:String? = null,
|
var duration: String? = null,
|
||||||
var videoId: String? = null
|
var videoId: String? = null
|
||||||
)
|
)
|
@ -20,9 +20,9 @@ import kotlinx.serialization.SerialName
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Artist (
|
data class Artist(
|
||||||
val popularity : Int,
|
val popularity: Int,
|
||||||
val seokey : String,
|
val seokey: String,
|
||||||
val name : String,
|
val name: String,
|
||||||
@SerialName("artwork_175x175")var artworkLink :String? = null
|
@SerialName("artwork_175x175")var artworkLink: String? = null
|
||||||
)
|
)
|
@ -17,14 +17,13 @@
|
|||||||
package com.shabinder.common.models.gaana
|
package com.shabinder.common.models.gaana
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class CustomArtworks (
|
data class CustomArtworks(
|
||||||
@SerialName("40x40") val size_40p : String,
|
@SerialName("40x40") val size_40p: String,
|
||||||
@SerialName("80x80") val size_80p : String,
|
@SerialName("80x80") val size_80p: String,
|
||||||
@SerialName("110x110")val size_110p : String,
|
@SerialName("110x110")val size_110p: String,
|
||||||
@SerialName("175x175")val size_175p : String,
|
@SerialName("175x175")val size_175p: String,
|
||||||
@SerialName("480x480")val size_480p : String,
|
@SerialName("480x480")val size_480p: String,
|
||||||
)
|
)
|
@ -19,10 +19,10 @@ package com.shabinder.common.models.gaana
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GaanaAlbum (
|
data class GaanaAlbum(
|
||||||
val tracks : List<GaanaTrack>,
|
val tracks: List<GaanaTrack>,
|
||||||
val count : Int,
|
val count: Int,
|
||||||
val custom_artworks : CustomArtworks,
|
val custom_artworks: CustomArtworks,
|
||||||
val release_year : Int,
|
val release_year: Int,
|
||||||
val favorite_count : Int,
|
val favorite_count: Int,
|
||||||
)
|
)
|
@ -20,6 +20,6 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GaanaArtistDetails(
|
data class GaanaArtistDetails(
|
||||||
val artist : List<Artist>,
|
val artist: List<Artist>,
|
||||||
val count : Int,
|
val count: Int,
|
||||||
)
|
)
|
@ -20,6 +20,6 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GaanaArtistTracks(
|
data class GaanaArtistTracks(
|
||||||
val count : Int,
|
val count: Int,
|
||||||
val tracks : List<GaanaTrack>? = null
|
val tracks: List<GaanaTrack>? = null
|
||||||
)
|
)
|
@ -19,10 +19,10 @@ package com.shabinder.common.models.gaana
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GaanaPlaylist (
|
data class GaanaPlaylist(
|
||||||
val modified_on : String,
|
val modified_on: String,
|
||||||
val count : Int,
|
val count: Int,
|
||||||
val created_on : String,
|
val created_on: String,
|
||||||
val favorite_count : Int,
|
val favorite_count: Int,
|
||||||
val tracks : List<GaanaTrack>,
|
val tracks: List<GaanaTrack>,
|
||||||
)
|
)
|
@ -20,5 +20,5 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GaanaSong(
|
data class GaanaSong(
|
||||||
val tracks : List<GaanaTrack>
|
val tracks: List<GaanaTrack>
|
||||||
)
|
)
|
@ -21,22 +21,22 @@ import kotlinx.serialization.SerialName
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GaanaTrack (
|
data class GaanaTrack(
|
||||||
val tags : List<Tags?>? = null,
|
val tags: List<Tags?>? = null,
|
||||||
val seokey : String,
|
val seokey: String,
|
||||||
val albumseokey : String? = null,
|
val albumseokey: String? = null,
|
||||||
val track_title : String,
|
val track_title: String,
|
||||||
val album_title : String? = null,
|
val album_title: String? = null,
|
||||||
val language : String? = null,
|
val language: String? = null,
|
||||||
val duration: Int,
|
val duration: Int,
|
||||||
@SerialName("artwork_large") val artworkLink : String,
|
@SerialName("artwork_large") val artworkLink: String,
|
||||||
val artist : List<Artist?> = emptyList(),
|
val artist: List<Artist?> = emptyList(),
|
||||||
@SerialName("gener") val genre : List<Genre?>? = null,
|
@SerialName("gener") val genre: List<Genre?>? = null,
|
||||||
val lyrics_url : String? = null,
|
val lyrics_url: String? = null,
|
||||||
val youtube_id : String? = null,
|
val youtube_id: String? = null,
|
||||||
val total_favourite_count : Int? = null,
|
val total_favourite_count: Int? = null,
|
||||||
val release_date : String? = null,
|
val release_date: String? = null,
|
||||||
val play_ct : String? = null,
|
val play_ct: String? = null,
|
||||||
val secondary_language : String? = null,
|
val secondary_language: String? = null,
|
||||||
var downloaded: DownloadStatus? = DownloadStatus.NotDownloaded
|
var downloaded: DownloadStatus? = DownloadStatus.NotDownloaded
|
||||||
)
|
)
|
@ -19,7 +19,7 @@ package com.shabinder.common.models.gaana
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Genre (
|
data class Genre(
|
||||||
val genre_id : Int,
|
val genre_id: Int,
|
||||||
val name : String
|
val name: String
|
||||||
)
|
)
|
@ -19,7 +19,7 @@ package com.shabinder.common.models.gaana
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Tags (
|
data class Tags(
|
||||||
val tag_id : Int,
|
val tag_id: Int,
|
||||||
val tag_name : String
|
val tag_name: String
|
||||||
)
|
)
|
@ -30,11 +30,12 @@ data class Album(
|
|||||||
var href: String? = null,
|
var href: String? = null,
|
||||||
var id: String? = null,
|
var id: String? = null,
|
||||||
var images: List<Image?>? = null,
|
var images: List<Image?>? = null,
|
||||||
var label :String? = null,
|
var label: String? = null,
|
||||||
var name: String? = null,
|
var name: String? = null,
|
||||||
var popularity: Int? = null,
|
var popularity: Int? = null,
|
||||||
var release_date: String? = null,
|
var release_date: String? = null,
|
||||||
var release_date_precision: String? = null,
|
var release_date_precision: String? = null,
|
||||||
var tracks: PagingObjectTrack? = null,
|
var tracks: PagingObjectTrack? = null,
|
||||||
var type: String? = null,
|
var type: String? = null,
|
||||||
var uri: String? = null)
|
var uri: String? = null
|
||||||
|
)
|
||||||
|
@ -25,4 +25,5 @@ data class Artist(
|
|||||||
var id: String? = null,
|
var id: String? = null,
|
||||||
var name: String? = null,
|
var name: String? = null,
|
||||||
var type: String? = null,
|
var type: String? = null,
|
||||||
var uri: String? = null)
|
var uri: String? = null
|
||||||
|
)
|
||||||
|
@ -21,4 +21,5 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class Copyright(
|
data class Copyright(
|
||||||
var text: String? = null,
|
var text: String? = null,
|
||||||
var type: String? = null)
|
var type: String? = null
|
||||||
|
)
|
||||||
|
@ -20,21 +20,21 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Episodes(
|
data class Episodes(
|
||||||
var audio_preview_url:String?,
|
var audio_preview_url: String?,
|
||||||
var description:String?,
|
var description: String?,
|
||||||
var duration_ms:Int?,
|
var duration_ms: Int?,
|
||||||
var explicit:Boolean?,
|
var explicit: Boolean?,
|
||||||
var external_urls:Map<String,String>?,
|
var external_urls: Map<String, String>?,
|
||||||
var href:String?,
|
var href: String?,
|
||||||
var id:String?,
|
var id: String?,
|
||||||
var images:List<Image?>?,
|
var images: List<Image?>?,
|
||||||
var is_externally_hosted:Boolean?,
|
var is_externally_hosted: Boolean?,
|
||||||
var is_playable:Boolean?,
|
var is_playable: Boolean?,
|
||||||
var language:String?,
|
var language: String?,
|
||||||
var languages:List<String?>?,
|
var languages: List<String?>?,
|
||||||
var name:String?,
|
var name: String?,
|
||||||
var release_date:String?,
|
var release_date: String?,
|
||||||
var release_date_precision:String?,
|
var release_date_precision: String?,
|
||||||
var type:String?,
|
var type: String?,
|
||||||
var uri:String
|
var uri: String
|
||||||
)
|
)
|
@ -21,4 +21,5 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class Followers(
|
data class Followers(
|
||||||
var href: String? = null,
|
var href: String? = null,
|
||||||
var total: Int? = null)
|
var total: Int? = null
|
||||||
|
)
|
||||||
|
@ -22,4 +22,5 @@ import kotlinx.serialization.Serializable
|
|||||||
data class Image(
|
data class Image(
|
||||||
var width: Int? = null,
|
var width: Int? = null,
|
||||||
var height: Int? = null,
|
var height: Int? = null,
|
||||||
var url: String? = null)
|
var url: String? = null
|
||||||
|
)
|
||||||
|
@ -24,4 +24,5 @@ data class LinkedTrack(
|
|||||||
var href: String? = null,
|
var href: String? = null,
|
||||||
var id: String? = null,
|
var id: String? = null,
|
||||||
var type: String? = null,
|
var type: String? = null,
|
||||||
var uri: String? = null)
|
var uri: String? = null
|
||||||
|
)
|
||||||
|
@ -26,4 +26,5 @@ data class PagingObjectPlaylistTrack(
|
|||||||
var next: String? = null,
|
var next: String? = null,
|
||||||
var offset: Int = 0,
|
var offset: Int = 0,
|
||||||
var previous: String? = null,
|
var previous: String? = null,
|
||||||
var total: Int = 0)
|
var total: Int = 0
|
||||||
|
)
|
||||||
|
@ -26,4 +26,5 @@ data class PagingObjectTrack(
|
|||||||
var next: String? = null,
|
var next: String? = null,
|
||||||
var offset: Int = 0,
|
var offset: Int = 0,
|
||||||
var previous: String? = null,
|
var previous: String? = null,
|
||||||
var total: Int = 0)
|
var total: Int = 0
|
||||||
|
)
|
||||||
|
@ -34,4 +34,5 @@ data class Playlist(
|
|||||||
var snapshot_id: String? = null,
|
var snapshot_id: String? = null,
|
||||||
var tracks: PagingObjectPlaylistTrack? = null,
|
var tracks: PagingObjectPlaylistTrack? = null,
|
||||||
var type: String? = null,
|
var type: String? = null,
|
||||||
var uri: String? = null)
|
var uri: String? = null
|
||||||
|
)
|
||||||
|
@ -23,4 +23,5 @@ data class PlaylistTrack(
|
|||||||
var added_at: String? = null,
|
var added_at: String? = null,
|
||||||
var added_by: UserPublic? = null,
|
var added_by: UserPublic? = null,
|
||||||
var track: Track? = null,
|
var track: Track? = null,
|
||||||
var is_local: Boolean? = null)
|
var is_local: Boolean? = null
|
||||||
|
)
|
||||||
|
@ -21,7 +21,7 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class TokenData(
|
data class TokenData(
|
||||||
var access_token:String?,
|
var access_token: String?,
|
||||||
var token_type:String?,
|
var token_type: String?,
|
||||||
@SerialName("expires_in") var expiry:Long?
|
@SerialName("expires_in") var expiry: Long?
|
||||||
)
|
)
|
@ -40,4 +40,3 @@ data class Track(
|
|||||||
var popularity: Int? = null,
|
var popularity: Int? = null,
|
||||||
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded
|
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,14 +20,15 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UserPrivate(
|
data class UserPrivate(
|
||||||
val country:String,
|
val country: String,
|
||||||
var display_name: String,
|
var display_name: String,
|
||||||
val email:String,
|
val email: String,
|
||||||
var external_urls: Map<String?, String?>? = null,
|
var external_urls: Map<String?, String?>? = null,
|
||||||
var followers: Followers? = null,
|
var followers: Followers? = null,
|
||||||
var href: String? = null,
|
var href: String? = null,
|
||||||
var id: String? = null,
|
var id: String? = null,
|
||||||
var images: List<Image?>? = null,
|
var images: List<Image?>? = null,
|
||||||
var product:String,
|
var product: String,
|
||||||
var type: String? = null,
|
var type: String? = null,
|
||||||
var uri: String? = null)
|
var uri: String? = null
|
||||||
|
)
|
||||||
|
@ -27,4 +27,5 @@ data class UserPublic(
|
|||||||
var id: String? = null,
|
var id: String? = null,
|
||||||
var images: List<Image?>? = null,
|
var images: List<Image?>? = null,
|
||||||
var type: String? = null,
|
var type: String? = null,
|
||||||
var uri: String? = null)
|
var uri: String? = null
|
||||||
|
)
|
||||||
|
@ -27,7 +27,7 @@ data class ItemWynk(
|
|||||||
val cues: List<String>,
|
val cues: List<String>,
|
||||||
val downloadPrice: String,
|
val downloadPrice: String,
|
||||||
val downloadUrl: String,
|
val downloadUrl: String,
|
||||||
val duration: Int, //in Seconds
|
val duration: Int, // in Seconds
|
||||||
val exclusive: Boolean,
|
val exclusive: Boolean,
|
||||||
val formats: List<String>,
|
val formats: List<String>,
|
||||||
val htData: List<HtDataWynk>,
|
val htData: List<HtDataWynk>,
|
||||||
@ -42,11 +42,11 @@ data class ItemWynk(
|
|||||||
val rentUrl: String,
|
val rentUrl: String,
|
||||||
val serverEtag: String,
|
val serverEtag: String,
|
||||||
val shortUrl: String,
|
val shortUrl: String,
|
||||||
val smallImage: String, //Cover Image after Replacing 120x120 with 720x720
|
val smallImage: String, // Cover Image after Replacing 120x120 with 720x720
|
||||||
val subtitle: String, // String : `ArtistName - TrackName`
|
val subtitle: String, // String : `ArtistName - TrackName`
|
||||||
val subtitleId: String, //ARTIST NAME,artist-id , etc //USE SUBTITLE INSTEAD
|
val subtitleId: String, // ARTIST NAME,artist-id , etc //USE SUBTITLE INSTEAD
|
||||||
val subtitleType: String, // ARTIST etc
|
val subtitleType: String, // ARTIST etc
|
||||||
val title: String,
|
val title: String,
|
||||||
val type: String, //Song ,etc
|
val type: String, // Song ,etc
|
||||||
val videoPresent: Boolean
|
val videoPresent: Boolean
|
||||||
)
|
)
|
@ -16,8 +16,6 @@
|
|||||||
|
|
||||||
package com.shabinder.common.models.wynk
|
package com.shabinder.common.models.wynk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Use Kotlinx JSON Parsing as in YT Music
|
// Use Kotlinx JSON Parsing as in YT Music
|
||||||
data class ShortURLWynk(
|
data class ShortURLWynk(
|
||||||
val actualTotal: Int,
|
val actualTotal: Int,
|
||||||
@ -33,15 +31,15 @@ data class ShortURLWynk(
|
|||||||
val isFollowable: Boolean,
|
val isFollowable: Boolean,
|
||||||
val isHt: Boolean,
|
val isHt: Boolean,
|
||||||
val itemIds: List<String>,
|
val itemIds: List<String>,
|
||||||
val itemTypes: List<String>, //Songs , etc
|
val itemTypes: List<String>, // Songs , etc
|
||||||
val items: List<ItemWynk>,
|
val items: List<ItemWynk>,
|
||||||
val lang: String,
|
val lang: String,
|
||||||
val largeImage: String, //Cover Image Alternate
|
val largeImage: String, // Cover Image Alternate
|
||||||
val lastUpdated: Long,
|
val lastUpdated: Long,
|
||||||
val offset: Int,
|
val offset: Int,
|
||||||
val owner: String,
|
val owner: String,
|
||||||
val playIcon: Boolean,
|
val playIcon: Boolean,
|
||||||
val playlistImage: String, //Cover Image
|
val playlistImage: String, // Cover Image
|
||||||
val redesignFeaturedImage: String,
|
val redesignFeaturedImage: String,
|
||||||
val shortUrl: String,
|
val shortUrl: String,
|
||||||
val singers: List<SingerWynk>,
|
val singers: List<SingerWynk>,
|
||||||
|
@ -31,7 +31,7 @@ kotlin {
|
|||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":common:data-models"))
|
implementation(project(":common:data-models"))
|
||||||
//implementation(Badoo.Reaktive.reaktive)
|
// implementation(Badoo.Reaktive.reaktive)
|
||||||
// SQL Delight
|
// SQL Delight
|
||||||
implementation(SqlDelight.runtime)
|
implementation(SqlDelight.runtime)
|
||||||
implementation(SqlDelight.coroutineExtensions)
|
implementation(SqlDelight.coroutineExtensions)
|
||||||
|
@ -19,5 +19,5 @@ package com.shabinder.common.database
|
|||||||
import co.touchlab.kermit.Logger
|
import co.touchlab.kermit.Logger
|
||||||
import com.shabinder.database.Database
|
import com.shabinder.database.Database
|
||||||
|
|
||||||
expect fun createDatabase() : Database?
|
expect fun createDatabase(): Database?
|
||||||
expect fun getLogger(): Logger
|
expect fun getLogger(): Logger
|
@ -44,7 +44,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies{
|
dependencies {
|
||||||
implementation(compose.materialIconsExtended)
|
implementation(compose.materialIconsExtended)
|
||||||
implementation(Koin.android)
|
implementation(Koin.android)
|
||||||
implementation(Ktor.clientAndroid)
|
implementation(Ktor.clientAndroid)
|
||||||
@ -52,11 +52,11 @@ kotlin {
|
|||||||
implementation(Extras.Android.razorpay)
|
implementation(Extras.Android.razorpay)
|
||||||
api(Extras.youtubeDownloader)
|
api(Extras.youtubeDownloader)
|
||||||
api(Extras.mp3agic)
|
api(Extras.mp3agic)
|
||||||
//api(files("$rootDir/libs/mobile-ffmpeg.aar"))
|
// api(files("$rootDir/libs/mobile-ffmpeg.aar"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
desktopMain {
|
desktopMain {
|
||||||
dependencies{
|
dependencies {
|
||||||
implementation(compose.materialIconsExtended)
|
implementation(compose.materialIconsExtended)
|
||||||
implementation(Ktor.clientApache)
|
implementation(Ktor.clientApache)
|
||||||
implementation(Ktor.slf4j)
|
implementation(Ktor.slf4j)
|
||||||
@ -68,9 +68,9 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":common:data-models"))
|
implementation(project(":common:data-models"))
|
||||||
implementation(Ktor.clientJs)
|
implementation(Ktor.clientJs)
|
||||||
implementation(npm("browser-id3-writer","4.4.0"))
|
implementation(npm("browser-id3-writer", "4.4.0"))
|
||||||
implementation(npm("file-saver","2.0.4"))
|
implementation(npm("file-saver", "2.0.4"))
|
||||||
//implementation(npm("@types/file-saver","2.0.1",generateExternals = true))
|
// implementation(npm("@types/file-saver","2.0.1",generateExternals = true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ import com.shabinder.common.models.TrackDetails
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
||||||
actual fun openPlatform(packageID:String, platformLink:String){
|
actual fun openPlatform(packageID: String, platformLink: String) {
|
||||||
val manager: PackageManager = activityContext.packageManager
|
val manager: PackageManager = activityContext.packageManager
|
||||||
try {
|
try {
|
||||||
val intent = manager.getLaunchIntentForPackage(packageID)
|
val intent = manager.getLaunchIntentForPackage(packageID)
|
||||||
@ -49,10 +49,10 @@ actual fun openPlatform(packageID:String, platformLink:String){
|
|||||||
actual val dispatcherIO = Dispatchers.IO
|
actual val dispatcherIO = Dispatchers.IO
|
||||||
actual val currentPlatform: AllPlatforms = AllPlatforms.Jvm
|
actual val currentPlatform: AllPlatforms = AllPlatforms.Jvm
|
||||||
|
|
||||||
actual val isInternetAvailable:Boolean
|
actual val isInternetAvailable: Boolean
|
||||||
get() = internetAvailability.value ?: true
|
get() = internetAvailability.value ?: true
|
||||||
|
|
||||||
actual fun shareApp(){
|
actual fun shareApp() {
|
||||||
val sendIntent: Intent = Intent().apply {
|
val sendIntent: Intent = Intent().apply {
|
||||||
action = Intent.ACTION_SEND
|
action = Intent.ACTION_SEND
|
||||||
putExtra(Intent.EXTRA_TEXT, "Hey, checkout this excellent Music Downloader http://github.com/Shabinder/SpotiFlyer")
|
putExtra(Intent.EXTRA_TEXT, "Hey, checkout this excellent Music Downloader http://github.com/Shabinder/SpotiFlyer")
|
||||||
@ -78,18 +78,18 @@ private fun startPayment(mainActivity: Activity = activityContext as Activity) {
|
|||||||
val preFill = JSONObject()
|
val preFill = JSONObject()
|
||||||
|
|
||||||
val options = JSONObject().apply {
|
val options = JSONObject().apply {
|
||||||
put("name","SpotiFlyer")
|
put("name", "SpotiFlyer")
|
||||||
put("description","Thanks For the Donation!")
|
put("description", "Thanks For the Donation!")
|
||||||
//You can omit the image option to fetch the image from dashboard
|
// You can omit the image option to fetch the image from dashboard
|
||||||
//put("image","https://github.com/Shabinder/SpotiFlyer/raw/master/app/SpotifyDownload.png")
|
// put("image","https://github.com/Shabinder/SpotiFlyer/raw/master/app/SpotifyDownload.png")
|
||||||
put("currency","INR")
|
put("currency", "INR")
|
||||||
put("amount","4900")
|
put("amount", "4900")
|
||||||
put("prefill",preFill)
|
put("prefill", preFill)
|
||||||
}
|
}
|
||||||
|
|
||||||
co.open(mainActivity,options)
|
co.open(mainActivity, options)
|
||||||
}catch (e: Exception){
|
} catch (e: Exception) {
|
||||||
//showPop("Error in payment: "+ e.message)
|
// showPop("Error in payment: "+ e.message)
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,15 +104,15 @@ actual suspend fun downloadTracks(
|
|||||||
list: List<TrackDetails>,
|
list: List<TrackDetails>,
|
||||||
fetcher: FetchPlatformQueryResult,
|
fetcher: FetchPlatformQueryResult,
|
||||||
dir: Dir
|
dir: Dir
|
||||||
){
|
) {
|
||||||
if(!list.isNullOrEmpty()){
|
if (!list.isNullOrEmpty()) {
|
||||||
val serviceIntent = Intent(activityContext, ForegroundService::class.java)
|
val serviceIntent = Intent(activityContext, ForegroundService::class.java)
|
||||||
serviceIntent.putParcelableArrayListExtra("object",ArrayList<TrackDetails>(list))
|
serviceIntent.putParcelableArrayListExtra("object", ArrayList<TrackDetails>(list))
|
||||||
activityContext.let { ContextCompat.startForegroundService(it, serviceIntent) }
|
activityContext.let { ContextCompat.startForegroundService(it, serviceIntent) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun YoutubeVideo.getData(): Format?{
|
fun YoutubeVideo.getData(): Format? {
|
||||||
return try {
|
return try {
|
||||||
findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
|
findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
|
||||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||||
|
@ -21,7 +21,6 @@ import android.graphics.Bitmap
|
|||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.media.MediaScannerConnection
|
import android.media.MediaScannerConnection
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.mpatric.mp3agic.Mp3File
|
import com.mpatric.mp3agic.Mp3File
|
||||||
@ -55,33 +54,28 @@ actual class Dir actual constructor(
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
actual fun defaultDir(): String =
|
actual fun defaultDir(): String =
|
||||||
Environment.getExternalStorageDirectory().toString() + File.separator +
|
Environment.getExternalStorageDirectory().toString() + File.separator +
|
||||||
Environment.DIRECTORY_MUSIC + File.separator +
|
Environment.DIRECTORY_MUSIC + File.separator +
|
||||||
"SpotiFlyer"+ File.separator
|
"SpotiFlyer" + File.separator
|
||||||
|
|
||||||
actual fun isPresent(path: String): Boolean = File(path).exists()
|
actual fun isPresent(path: String): Boolean = File(path).exists()
|
||||||
|
|
||||||
actual fun createDirectory(dirPath: String) {
|
actual fun createDirectory(dirPath: String) {
|
||||||
val yourAppDir = File(dirPath)
|
val yourAppDir = File(dirPath)
|
||||||
|
|
||||||
if(!yourAppDir.exists() && !yourAppDir.isDirectory)
|
if (!yourAppDir.exists() && !yourAppDir.isDirectory) { // create empty directory
|
||||||
{ // create empty directory
|
if (yourAppDir.mkdirs()) { logger.i { "$dirPath created" } } else {
|
||||||
if (yourAppDir.mkdirs())
|
logger.e { "Unable to create Dir: $dirPath!" }
|
||||||
{logger.i{"$dirPath created"}}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
logger.e{"Unable to create Dir: $dirPath!"}
|
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
logger.i { "$dirPath already exists" }
|
logger.i { "$dirPath already exists" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual suspend fun clearCache(){
|
actual suspend fun clearCache() {
|
||||||
File(imageCacheDir()).deleteRecursively()
|
File(imageCacheDir()).deleteRecursively()
|
||||||
}
|
}
|
||||||
|
|
||||||
actual suspend fun cacheImage(image: Any,path:String) {
|
actual suspend fun cacheImage(image: Any, path: String) {
|
||||||
try {
|
try {
|
||||||
FileOutputStream(path).use { out ->
|
FileOutputStream(path).use { out ->
|
||||||
(image as? Bitmap)?.compress(Bitmap.CompressFormat.JPEG, 100, out)
|
(image as? Bitmap)?.compress(Bitmap.CompressFormat.JPEG, 100, out)
|
||||||
@ -100,9 +94,9 @@ actual class Dir actual constructor(
|
|||||||
/*
|
/*
|
||||||
* Check , if Fetch was Used, File is saved Already, else write byteArray we Received
|
* Check , if Fetch was Used, File is saved Already, else write byteArray we Received
|
||||||
* */
|
* */
|
||||||
//if(!m4aFile.exists()) m4aFile.writeBytes(mp3ByteArray)
|
// if(!m4aFile.exists()) m4aFile.writeBytes(mp3ByteArray)
|
||||||
|
|
||||||
when(trackDetails.outputFilePath.substringAfterLast('.')){
|
when (trackDetails.outputFilePath.substringAfterLast('.')) {
|
||||||
".mp3" -> {
|
".mp3" -> {
|
||||||
Mp3File(File(songFile.absolutePath))
|
Mp3File(File(songFile.absolutePath))
|
||||||
.removeAllTags()
|
.removeAllTags()
|
||||||
@ -136,22 +130,23 @@ actual class Dir actual constructor(
|
|||||||
}*/
|
}*/
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
try{
|
try {
|
||||||
Mp3File(File(songFile.absolutePath))
|
Mp3File(File(songFile.absolutePath))
|
||||||
.removeAllTags()
|
.removeAllTags()
|
||||||
.setId3v1Tags(trackDetails)
|
.setId3v1Tags(trackDetails)
|
||||||
.setId3v2TagsAndSaveFile(trackDetails)
|
.setId3v2TagsAndSaveFile(trackDetails)
|
||||||
addToLibrary(songFile.absolutePath)
|
addToLibrary(songFile.absolutePath)
|
||||||
}catch (e:Exception){e.printStackTrace()}
|
} catch (e: Exception) { e.printStackTrace() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun addToLibrary(path:String) {
|
actual fun addToLibrary(path: String) {
|
||||||
logger.d{"Scanning File"}
|
logger.d { "Scanning File" }
|
||||||
MediaScannerConnection.scanFile(
|
MediaScannerConnection.scanFile(
|
||||||
appContext,
|
appContext,
|
||||||
listOf(path).toTypedArray(), null,null)
|
listOf(path).toTypedArray(), null, null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual suspend fun loadImage(url: String): Picture {
|
actual suspend fun loadImage(url: String): Picture {
|
||||||
@ -167,7 +162,7 @@ actual class Dir actual constructor(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private suspend fun freshImage(url:String): Bitmap?{
|
private suspend fun freshImage(url: String): Bitmap? {
|
||||||
return try {
|
return try {
|
||||||
val source = URL(url)
|
val source = URL(url)
|
||||||
val connection: HttpURLConnection = source.openConnection() as HttpURLConnection
|
val connection: HttpURLConnection = source.openConnection() as HttpURLConnection
|
||||||
@ -179,7 +174,7 @@ actual class Dir actual constructor(
|
|||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
cacheImage(result,imageCacheDir() + getNameURL(url))
|
cacheImage(result, imageCacheDir() + getNameURL(url))
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
} else null
|
} else null
|
||||||
|
@ -24,7 +24,6 @@ import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
|
|||||||
import android.net.NetworkRequest
|
import android.net.NetworkRequest
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.shabinder.common.database.appContext
|
import com.shabinder.common.database.appContext
|
||||||
@ -41,8 +40,8 @@ const val TAG = "C-Manager"
|
|||||||
val internetAvailability by lazy { ConnectionLiveData(appContext) }
|
val internetAvailability by lazy { ConnectionLiveData(appContext) }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun isInternetAvailableState(): State<Boolean?>{
|
fun isInternetAvailableState(): State<Boolean?> {
|
||||||
return internetAvailability.observeAsState()
|
return internetAvailability.observeAsState()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,17 +82,17 @@ class ConnectionLiveData(context: Context = appContext) : LiveData<Boolean>() {
|
|||||||
Source: https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback#onAvailable(android.net.Network)
|
Source: https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback#onAvailable(android.net.Network)
|
||||||
*/
|
*/
|
||||||
override fun onAvailable(network: Network) {
|
override fun onAvailable(network: Network) {
|
||||||
Log.d(TAG, "onAvailable: ${network}")
|
Log.d(TAG, "onAvailable: $network")
|
||||||
val networkCapabilities = cm.getNetworkCapabilities(network)
|
val networkCapabilities = cm.getNetworkCapabilities(network)
|
||||||
val hasInternetCapability = networkCapabilities?.hasCapability(NET_CAPABILITY_INTERNET)
|
val hasInternetCapability = networkCapabilities?.hasCapability(NET_CAPABILITY_INTERNET)
|
||||||
Log.d(TAG, "onAvailable: ${network}, $hasInternetCapability")
|
Log.d(TAG, "onAvailable: $network, $hasInternetCapability")
|
||||||
if (hasInternetCapability == true) {
|
if (hasInternetCapability == true) {
|
||||||
// check if this network actually has internet
|
// check if this network actually has internet
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val hasInternet = DoesNetworkHaveInternet.execute(network.socketFactory)
|
val hasInternet = DoesNetworkHaveInternet.execute(network.socketFactory)
|
||||||
if(hasInternet){
|
if (hasInternet) {
|
||||||
withContext(Dispatchers.Main){
|
withContext(Dispatchers.Main) {
|
||||||
Log.d(TAG, "onAvailable: adding network. ${network}")
|
Log.d(TAG, "onAvailable: adding network. $network")
|
||||||
validNetworks.add(network)
|
validNetworks.add(network)
|
||||||
checkValidNetworks()
|
checkValidNetworks()
|
||||||
}
|
}
|
||||||
@ -107,11 +106,10 @@ class ConnectionLiveData(context: Context = appContext) : LiveData<Boolean>() {
|
|||||||
Source: https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback#onLost(android.net.Network)
|
Source: https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback#onLost(android.net.Network)
|
||||||
*/
|
*/
|
||||||
override fun onLost(network: Network) {
|
override fun onLost(network: Network) {
|
||||||
Log.d(TAG, "onLost: ${network}")
|
Log.d(TAG, "onLost: $network")
|
||||||
validNetworks.remove(network)
|
validNetworks.remove(network)
|
||||||
checkValidNetworks()
|
checkValidNetworks()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -122,14 +120,14 @@ class ConnectionLiveData(context: Context = appContext) : LiveData<Boolean>() {
|
|||||||
|
|
||||||
// Make sure to execute this on a background thread.
|
// Make sure to execute this on a background thread.
|
||||||
fun execute(socketFactory: SocketFactory): Boolean {
|
fun execute(socketFactory: SocketFactory): Boolean {
|
||||||
return try{
|
return try {
|
||||||
Log.d(TAG, "PINGING google.")
|
Log.d(TAG, "PINGING google.")
|
||||||
val socket = socketFactory.createSocket() ?: throw IOException("Socket is null.")
|
val socket = socketFactory.createSocket() ?: throw IOException("Socket is null.")
|
||||||
socket.connect(InetSocketAddress("8.8.8.8", 53), 1500)
|
socket.connect(InetSocketAddress("8.8.8.8", 53), 1500)
|
||||||
socket.close()
|
socket.close()
|
||||||
Log.d(TAG, "PING success.")
|
Log.d(TAG, "PING success.")
|
||||||
true
|
true
|
||||||
}catch (e: IOException){
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "No internet connection. $e")
|
Log.e(TAG, "No internet connection. $e")
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,6 @@ package com.shabinder.common.di
|
|||||||
|
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
|
||||||
actual data class Picture (
|
actual data class Picture(
|
||||||
var image: ImageBitmap?
|
var image: ImageBitmap?
|
||||||
)
|
)
|
@ -48,7 +48,7 @@ fun Mp3File.setId3v1Tags(track: TrackDetails): Mp3File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails){
|
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) {
|
||||||
val id3v2Tag = ID3v24Tag().apply {
|
val id3v2Tag = ID3v24Tag().apply {
|
||||||
artist = track.artists.joinToString(",")
|
artist = track.artists.joinToString(",")
|
||||||
title = track.title
|
title = track.title
|
||||||
@ -58,37 +58,37 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails){
|
|||||||
lyrics = "Gonna Implement Soon"
|
lyrics = "Gonna Implement Soon"
|
||||||
url = track.trackUrl
|
url = track.trackUrl
|
||||||
}
|
}
|
||||||
try{
|
try {
|
||||||
val art = File(track.albumArtPath)
|
val art = File(track.albumArtPath)
|
||||||
val bytesArray = ByteArray(art.length().toInt())
|
val bytesArray = ByteArray(art.length().toInt())
|
||||||
val fis = FileInputStream(art)
|
val fis = FileInputStream(art)
|
||||||
fis.read(bytesArray) //read file into bytes[]
|
fis.read(bytesArray) // read file into bytes[]
|
||||||
fis.close()
|
fis.close()
|
||||||
id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
|
id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
|
||||||
this.id3v2Tag = id3v2Tag
|
this.id3v2Tag = id3v2Tag
|
||||||
saveFile(track.outputFilePath)
|
saveFile(track.outputFilePath)
|
||||||
}catch (e: java.io.FileNotFoundException){
|
} catch (e: java.io.FileNotFoundException) {
|
||||||
try {
|
try {
|
||||||
//Image Still Not Downloaded!
|
// Image Still Not Downloaded!
|
||||||
//Lets Download Now and Write it into Album Art
|
// Lets Download Now and Write it into Album Art
|
||||||
downloadFile(track.albumArtURL).collect {
|
downloadFile(track.albumArtURL).collect {
|
||||||
when(it){
|
when (it) {
|
||||||
is DownloadResult.Error -> {}//Error
|
is DownloadResult.Error -> {} // Error
|
||||||
is DownloadResult.Success -> {
|
is DownloadResult.Success -> {
|
||||||
id3v2Tag.setAlbumImage(it.byteArray, "image/jpeg")
|
id3v2Tag.setAlbumImage(it.byteArray, "image/jpeg")
|
||||||
this.id3v2Tag = id3v2Tag
|
this.id3v2Tag = id3v2Tag
|
||||||
saveFile(track.outputFilePath)
|
saveFile(track.outputFilePath)
|
||||||
}
|
}
|
||||||
is DownloadResult.Progress -> {}//Nothing for Now , no progress bar to show
|
is DownloadResult.Progress -> {} // Nothing for Now , no progress bar to show
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}catch (e: Exception){
|
} catch (e: Exception) {
|
||||||
//log("Error", "Couldn't Write Mp3 Album Art, error: ${e.stackTrace}")
|
// log("Error", "Couldn't Write Mp3 Album Art, error: ${e.stackTrace}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Mp3File.saveFile(filePath: String){
|
fun Mp3File.saveFile(filePath: String) {
|
||||||
save(filePath.substringBeforeLast('.') + ".new.mp3")
|
save(filePath.substringBeforeLast('.') + ".new.mp3")
|
||||||
val m4aFile = File(filePath)
|
val m4aFile = File(filePath)
|
||||||
m4aFile.delete()
|
m4aFile.delete()
|
||||||
|
@ -16,7 +16,11 @@
|
|||||||
|
|
||||||
package com.shabinder.common.di
|
package com.shabinder.common.di
|
||||||
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
@ -23,7 +23,7 @@ import com.shabinder.common.models.DownloadStatus
|
|||||||
import com.shabinder.common.models.PlatformQueryResult
|
import com.shabinder.common.models.PlatformQueryResult
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.shabinder.common.models.spotify.Source
|
import com.shabinder.common.models.spotify.Source
|
||||||
import io.ktor.client.*
|
import io.ktor.client.HttpClient
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ actual class YoutubeProvider actual constructor(
|
|||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
private val logger: Kermit,
|
private val logger: Kermit,
|
||||||
private val dir: Dir,
|
private val dir: Dir,
|
||||||
){
|
) {
|
||||||
val ytDownloader: YoutubeDownloader = YoutubeDownloader()
|
val ytDownloader: YoutubeDownloader = YoutubeDownloader()
|
||||||
/*
|
/*
|
||||||
* YT Album Art Schema
|
* YT Album Art Schema
|
||||||
@ -42,38 +42,38 @@ actual class YoutubeProvider actual constructor(
|
|||||||
private val sampleDomain2 = "youtube.com"
|
private val sampleDomain2 = "youtube.com"
|
||||||
private val sampleDomain3 = "youtu.be"
|
private val sampleDomain3 = "youtu.be"
|
||||||
|
|
||||||
actual suspend fun query(fullLink: String): PlatformQueryResult?{
|
actual suspend fun query(fullLink: String): PlatformQueryResult? {
|
||||||
val link = fullLink.removePrefix("https://").removePrefix("http://")
|
val link = fullLink.removePrefix("https://").removePrefix("http://")
|
||||||
if(link.contains("playlist",true) || link.contains("list",true)){
|
if (link.contains("playlist", true) || link.contains("list", true)) {
|
||||||
// Given Link is of a Playlist
|
// Given Link is of a Playlist
|
||||||
logger.i{ link }
|
logger.i { link }
|
||||||
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&").substringBefore("?")
|
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&").substringBefore("?")
|
||||||
return withContext(Dispatchers.IO){
|
return withContext(Dispatchers.IO) {
|
||||||
getYTPlaylist(
|
getYTPlaylist(
|
||||||
playlistId
|
playlistId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}else{//Given Link is of a Video
|
} else { // Given Link is of a Video
|
||||||
var searchId = "error"
|
var searchId = "error"
|
||||||
when{
|
when {
|
||||||
link.contains(sampleDomain1,true) -> {//Youtube Music
|
link.contains(sampleDomain1, true) -> { // Youtube Music
|
||||||
searchId = link.substringAfterLast("/","error").substringBefore("&").substringAfterLast("=")
|
searchId = link.substringAfterLast("/", "error").substringBefore("&").substringAfterLast("=")
|
||||||
}
|
}
|
||||||
link.contains(sampleDomain2,true) -> {//Standard Youtube Link
|
link.contains(sampleDomain2, true) -> { // Standard Youtube Link
|
||||||
searchId = link.substringAfterLast("=","error").substringBefore("&")
|
searchId = link.substringAfterLast("=", "error").substringBefore("&")
|
||||||
}
|
}
|
||||||
link.contains(sampleDomain3,true) -> {//Shortened Youtube Link
|
link.contains(sampleDomain3, true) -> { // Shortened Youtube Link
|
||||||
searchId = link.substringAfterLast("/","error").substringBefore("&")
|
searchId = link.substringAfterLast("/", "error").substringBefore("&")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return if(searchId != "error") {
|
return if (searchId != "error") {
|
||||||
withContext(Dispatchers.IO){
|
withContext(Dispatchers.IO) {
|
||||||
getYTTrack(
|
getYTTrack(
|
||||||
searchId
|
searchId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}else{
|
} else {
|
||||||
logger.d{"Your Youtube Link is not of a Video!!"}
|
logger.d { "Your Youtube Link is not of a Video!!" }
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,7 +81,7 @@ actual class YoutubeProvider actual constructor(
|
|||||||
|
|
||||||
private suspend fun getYTPlaylist(
|
private suspend fun getYTPlaylist(
|
||||||
searchId: String
|
searchId: String
|
||||||
): PlatformQueryResult?{
|
): PlatformQueryResult? {
|
||||||
val result = PlatformQueryResult(
|
val result = PlatformQueryResult(
|
||||||
folderType = "",
|
folderType = "",
|
||||||
subFolder = "",
|
subFolder = "",
|
||||||
@ -99,7 +99,7 @@ actual class YoutubeProvider actual constructor(
|
|||||||
val videos = playlist.videos()
|
val videos = playlist.videos()
|
||||||
|
|
||||||
coverUrl = "https://i.ytimg.com/vi/${
|
coverUrl = "https://i.ytimg.com/vi/${
|
||||||
videos.firstOrNull()?.videoId()
|
videos.firstOrNull()?.videoId()
|
||||||
}/hqdefault.jpg"
|
}/hqdefault.jpg"
|
||||||
title = name
|
title = name
|
||||||
|
|
||||||
@ -113,11 +113,11 @@ actual class YoutubeProvider actual constructor(
|
|||||||
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
||||||
downloaded = if (dir.isPresent(
|
downloaded = if (dir.isPresent(
|
||||||
dir.finalOutputDir(
|
dir.finalOutputDir(
|
||||||
itemName = it.title(),
|
itemName = it.title(),
|
||||||
type = folderType,
|
type = folderType,
|
||||||
subFolder = subFolder,
|
subFolder = subFolder,
|
||||||
dir.defaultDir()
|
dir.defaultDir()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
DownloadStatus.Downloaded
|
DownloadStatus.Downloaded
|
||||||
@ -130,16 +130,16 @@ actual class YoutubeProvider actual constructor(
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
logger.d{"An Error Occurred While Processing!"}
|
logger.d { "An Error Occurred While Processing!" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return if(result.title.isNotBlank()) result
|
return if (result.title.isNotBlank()) result
|
||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DefaultLocale")
|
@Suppress("DefaultLocale")
|
||||||
private suspend fun getYTTrack(
|
private suspend fun getYTTrack(
|
||||||
searchId:String,
|
searchId: String,
|
||||||
): PlatformQueryResult? {
|
): PlatformQueryResult? {
|
||||||
val result = PlatformQueryResult(
|
val result = PlatformQueryResult(
|
||||||
folderType = "",
|
folderType = "",
|
||||||
@ -148,15 +148,15 @@ actual class YoutubeProvider actual constructor(
|
|||||||
coverUrl = "",
|
coverUrl = "",
|
||||||
trackList = listOf(),
|
trackList = listOf(),
|
||||||
Source.YouTube
|
Source.YouTube
|
||||||
).apply{
|
).apply {
|
||||||
try {
|
try {
|
||||||
logger.i{searchId}
|
logger.i { searchId }
|
||||||
val video = ytDownloader.getVideo(searchId)
|
val video = ytDownloader.getVideo(searchId)
|
||||||
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
||||||
val detail = video?.details()
|
val detail = video?.details()
|
||||||
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(), "", true)
|
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(), "", true)
|
||||||
?: detail?.title() ?: ""
|
?: detail?.title() ?: ""
|
||||||
//logger.i{ detail.toString() }
|
// logger.i{ detail.toString() }
|
||||||
trackList = listOf(
|
trackList = listOf(
|
||||||
TrackDetails(
|
TrackDetails(
|
||||||
title = name,
|
title = name,
|
||||||
@ -167,11 +167,11 @@ actual class YoutubeProvider actual constructor(
|
|||||||
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||||
downloaded = if (dir.isPresent(
|
downloaded = if (dir.isPresent(
|
||||||
dir.finalOutputDir(
|
dir.finalOutputDir(
|
||||||
itemName = name,
|
itemName = name,
|
||||||
type = folderType,
|
type = folderType,
|
||||||
subFolder = subFolder,
|
subFolder = subFolder,
|
||||||
defaultDir = dir.defaultDir()
|
defaultDir = dir.defaultDir()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
DownloadStatus.Downloaded
|
DownloadStatus.Downloaded
|
||||||
@ -185,10 +185,10 @@ actual class YoutubeProvider actual constructor(
|
|||||||
title = name
|
title = name
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
logger.e{"An Error Occurred While Processing!,$searchId"}
|
logger.e { "An Error Occurred While Processing!,$searchId" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return if(result.title.isNotBlank()) result
|
return if (result.title.isNotBlank()) result
|
||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -17,15 +17,22 @@
|
|||||||
package com.shabinder.common.di.worker
|
package com.shabinder.common.di.worker
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.*
|
import android.app.DownloadManager
|
||||||
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
|
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.app.PendingIntent.FLAG_CANCEL_CURRENT
|
import android.app.PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
|
import android.app.Service
|
||||||
import android.content.BroadcastReceiver
|
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.net.Uri
|
import android.net.Uri
|
||||||
import android.os.*
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.PowerManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
@ -33,33 +40,43 @@ import androidx.core.net.toUri
|
|||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
import com.github.kiulian.downloader.YoutubeDownloader
|
||||||
import com.github.kiulian.downloader.model.formats.Format
|
import com.github.kiulian.downloader.model.formats.Format
|
||||||
import com.shabinder.common.database.R.*
|
|
||||||
import com.shabinder.common.di.Dir
|
import com.shabinder.common.di.Dir
|
||||||
import com.shabinder.common.di.FetchPlatformQueryResult
|
import com.shabinder.common.di.FetchPlatformQueryResult
|
||||||
|
import com.shabinder.common.di.R
|
||||||
import com.shabinder.common.di.getData
|
import com.shabinder.common.di.getData
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.tonyodev.fetch2.*
|
import com.tonyodev.fetch2.Download
|
||||||
|
import com.tonyodev.fetch2.Error
|
||||||
|
import com.tonyodev.fetch2.Fetch
|
||||||
|
import com.tonyodev.fetch2.FetchListener
|
||||||
|
import com.tonyodev.fetch2.NetworkType
|
||||||
|
import com.tonyodev.fetch2.Priority
|
||||||
|
import com.tonyodev.fetch2.Request
|
||||||
|
import com.tonyodev.fetch2.Status
|
||||||
import com.tonyodev.fetch2core.DownloadBlock
|
import com.tonyodev.fetch2core.DownloadBlock
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.*
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
class ForegroundService : Service(),CoroutineScope{
|
class ForegroundService : Service(), CoroutineScope {
|
||||||
private val tag: String = "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
|
||||||
private var converted = 0//Total Files Converted
|
private var converted = 0 // Total Files Converted
|
||||||
private var downloaded = 0//Total Files downloaded
|
private var downloaded = 0 // Total Files downloaded
|
||||||
private var failed = 0//Total Files failed
|
private var failed = 0 // Total Files failed
|
||||||
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 lateinit var serviceJob :Job
|
private lateinit var serviceJob: Job
|
||||||
override val coroutineContext: CoroutineContext
|
override val coroutineContext: CoroutineContext
|
||||||
get() = serviceJob + Dispatchers.IO
|
get() = serviceJob + Dispatchers.IO
|
||||||
|
|
||||||
@ -67,18 +84,17 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
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 downloadManager : DownloadManager
|
private lateinit var downloadManager: DownloadManager
|
||||||
|
|
||||||
private val fetcher: FetchPlatformQueryResult by inject()
|
private val fetcher: FetchPlatformQueryResult by inject()
|
||||||
private val logger: Kermit by inject()
|
private val logger: Kermit by inject()
|
||||||
private val fetch: Fetch by inject()
|
private val fetch: Fetch by inject()
|
||||||
private val dir: Dir by inject()
|
private val dir: Dir by inject()
|
||||||
private val ytDownloader:YoutubeDownloader
|
private val ytDownloader: YoutubeDownloader
|
||||||
get() = fetcher.youtubeProvider.ytDownloader
|
get() = fetcher.youtubeProvider.ytDownloader
|
||||||
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? = null
|
override fun onBind(intent: Intent): IBinder? = null
|
||||||
|
|
||||||
@SuppressLint("UnspecifiedImmutableFlag")
|
@SuppressLint("UnspecifiedImmutableFlag")
|
||||||
@ -86,13 +102,13 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
serviceJob = SupervisorJob()
|
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")
|
||||||
}
|
}
|
||||||
val intent = Intent(
|
val intent = Intent(
|
||||||
this,
|
this,
|
||||||
ForegroundService::class.java
|
ForegroundService::class.java
|
||||||
).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
|
||||||
fetch.removeAllListeners().addListener(fetchListener)
|
fetch.removeAllListeners().addListener(fetchListener)
|
||||||
}
|
}
|
||||||
@ -100,16 +116,16 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
@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.i(tag,"Foreground 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()
|
||||||
"query" -> {
|
"query" -> {
|
||||||
val response = Intent().apply {
|
val response = Intent().apply {
|
||||||
action = "query_result"
|
action = "query_result"
|
||||||
synchronized(allTracksStatus){
|
synchronized(allTracksStatus) {
|
||||||
putExtra("tracks", allTracksStatus)
|
putExtra("tracks", allTracksStatus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -117,9 +133,11 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloadObjects: ArrayList<TrackDetails>? = (it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList(
|
val downloadObjects: ArrayList<TrackDetails>? = (
|
||||||
"object"
|
it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList(
|
||||||
))
|
"object"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
downloadObjects?.let { list ->
|
downloadObjects?.let { list ->
|
||||||
downloadObjects.size.let { size ->
|
downloadObjects.size.let { size ->
|
||||||
@ -133,13 +151,13 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
downloadAllTracks(list)
|
downloadAllTracks(list)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Wake locks and misc tasks from here :
|
// Wake locks and misc tasks from here :
|
||||||
return if (isServiceStarted){
|
return if (isServiceStarted) {
|
||||||
//Service Already Started
|
// Service Already Started
|
||||||
START_STICKY
|
START_STICKY
|
||||||
} else{
|
} else {
|
||||||
isServiceStarted = true
|
isServiceStarted = true
|
||||||
Log.i(tag,"Starting the foreground service task")
|
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 {
|
||||||
@ -156,18 +174,18 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
private fun downloadAllTracks(trackList: List<TrackDetails>) {
|
private fun downloadAllTracks(trackList: List<TrackDetails>) {
|
||||||
trackList.forEach {
|
trackList.forEach {
|
||||||
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 videoID = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery,it)
|
val videoID = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery, it)
|
||||||
logger.d("Service VideoID") { videoID ?: "Not Found" }
|
logger.d("Service VideoID") { videoID ?: "Not Found" }
|
||||||
if (videoID.isNullOrBlank()) {
|
if (videoID.isNullOrBlank()) {
|
||||||
sendTrackBroadcast(Status.FAILED.name, it)
|
sendTrackBroadcast(Status.FAILED.name, it)
|
||||||
failed++
|
failed++
|
||||||
updateNotification()
|
updateNotification()
|
||||||
allTracksStatus[it.title] = DownloadStatus.Failed
|
allTracksStatus[it.title] = DownloadStatus.Failed
|
||||||
} else {//Found Youtube Video ID
|
} else { // Found Youtube Video ID
|
||||||
downloadTrack(videoID, it)
|
downloadTrack(videoID, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -175,37 +193,36 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun downloadTrack(videoID: String, track: TrackDetails) {
|
||||||
private fun downloadTrack(videoID:String, track: TrackDetails){
|
|
||||||
launch {
|
launch {
|
||||||
try {
|
try {
|
||||||
val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID)
|
val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID)
|
||||||
if (url == null){
|
if (url == null) {
|
||||||
val audioData:Format = ytDownloader.getVideo(videoID).getData() ?: throw Exception("Java YT Dependency Error")
|
val audioData: Format = ytDownloader.getVideo(videoID).getData() ?: throw Exception("Java YT Dependency Error")
|
||||||
val ytUrl: String = audioData.url()
|
val ytUrl: String = audioData.url()
|
||||||
enqueueDownload(ytUrl,track)
|
enqueueDownload(ytUrl, track)
|
||||||
} else enqueueDownload(url,track)
|
} else enqueueDownload(url, track)
|
||||||
}catch (e: Exception){
|
} catch (e: Exception) {
|
||||||
logger.d("Service YT Error"){e.message.toString()}
|
logger.d("Service YT Error") { e.message.toString() }
|
||||||
sendTrackBroadcast(Status.FAILED.name,track)
|
sendTrackBroadcast(Status.FAILED.name, track)
|
||||||
allTracksStatus[track.title] = DownloadStatus.Failed
|
allTracksStatus[track.title] = DownloadStatus.Failed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun enqueueDownload(url: String, track: TrackDetails) {
|
||||||
private fun enqueueDownload(url:String,track:TrackDetails){
|
val request = Request(url, track.outputFilePath).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
|
||||||
logger.d(tag){"Enqueuing Download"}
|
logger.d(tag) { "Enqueuing Download" }
|
||||||
},
|
},
|
||||||
{ error ->
|
{ error ->
|
||||||
logger.d(tag){"Enqueuing Error:${error.throwable.toString()}"}
|
logger.d(tag) { "Enqueuing Error:${error.throwable}" }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -235,12 +252,12 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
totalBlocks: Int
|
totalBlocks: Int
|
||||||
) {
|
) {
|
||||||
launch {
|
launch {
|
||||||
val track = requestMap[download.request]
|
val track = requestMap[download.request]
|
||||||
addToNotification("Downloading ${track?.title}")
|
addToNotification("Downloading ${track?.title}")
|
||||||
logger.d(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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -259,25 +276,25 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
|
|
||||||
override fun onCompleted(download: Download) {
|
override fun onCompleted(download: Download) {
|
||||||
val track = requestMap[download.request]
|
val track = requestMap[download.request]
|
||||||
try{
|
try {
|
||||||
track?.let {
|
track?.let {
|
||||||
val job = launch { dir.saveFileWithMetadata(byteArrayOf(),it) }
|
val job = launch { dir.saveFileWithMetadata(byteArrayOf(), it) }
|
||||||
allTracksStatus[it.title] = DownloadStatus.Converting
|
allTracksStatus[it.title] = DownloadStatus.Converting
|
||||||
sendTrackBroadcast("Converting",it)
|
sendTrackBroadcast("Converting", it)
|
||||||
addToNotification("Processing ${it.title}")
|
addToNotification("Processing ${it.title}")
|
||||||
job.invokeOnCompletion { _ ->
|
job.invokeOnCompletion { _ ->
|
||||||
converted++
|
converted++
|
||||||
allTracksStatus[it.title] = DownloadStatus.Downloaded
|
allTracksStatus[it.title] = DownloadStatus.Downloaded
|
||||||
sendTrackBroadcast(Status.COMPLETED.name,it)
|
sendTrackBroadcast(Status.COMPLETED.name, it)
|
||||||
removeFromNotification("Processing ${it.title}")
|
removeFromNotification("Processing ${it.title}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.d(tag){"${track?.title} Download Completed"}
|
logger.d(tag) { "${track?.title} Download Completed" }
|
||||||
}catch (
|
} catch (
|
||||||
e: KotlinNullPointerException
|
e: KotlinNullPointerException
|
||||||
){
|
) {
|
||||||
logger.d(tag){"${track?.title} Download Failed! Error:Fetch!!!!"}
|
logger.d(tag) { "${track?.title} Download Failed! Error:Fetch!!!!" }
|
||||||
logger.d(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++
|
||||||
@ -301,8 +318,8 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
launch {
|
launch {
|
||||||
val track = requestMap[download.request]
|
val track = requestMap[download.request]
|
||||||
downloaded++
|
downloaded++
|
||||||
logger.d(tag){download.error.throwable.toString()}
|
logger.d(tag) { download.error.throwable.toString() }
|
||||||
logger.d(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}")
|
||||||
@ -322,8 +339,7 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
launch {
|
launch {
|
||||||
requestMap[download.request]?.run {
|
requestMap[download.request]?.run {
|
||||||
allTracksStatus[title] = DownloadStatus.Downloading(download.progress)
|
allTracksStatus[title] = DownloadStatus.Downloading(download.progress)
|
||||||
logger.d(tag){"${title} ETA: ${etaInMilliSeconds / 1000} sec"}
|
logger.d(tag) { "$title ETA: ${etaInMilliSeconds / 1000} sec" }
|
||||||
|
|
||||||
|
|
||||||
val intent = Intent().apply {
|
val intent = Intent().apply {
|
||||||
action = "Progress"
|
action = "Progress"
|
||||||
@ -337,15 +353,15 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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) {
|
||||||
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(
|
||||||
DownloadManager.Request.NETWORK_WIFI or
|
DownloadManager.Request.NETWORK_WIFI or
|
||||||
DownloadManager.Request.NETWORK_MOBILE
|
DownloadManager.Request.NETWORK_MOBILE
|
||||||
)
|
)
|
||||||
setAllowedOverRoaming(false)
|
setAllowedOverRoaming(false)
|
||||||
setTitle(track.title)
|
setTitle(track.title)
|
||||||
@ -354,19 +370,19 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Start Download
|
// Start Download
|
||||||
val downloadID = downloadManager.enqueue(request)
|
val downloadID = downloadManager.enqueue(request)
|
||||||
logger.d("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) {
|
||||||
//Fetching the download id received with the broadcast
|
// Fetching the download id received with the broadcast
|
||||||
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
|
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
|
||||||
//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
|
||||||
launch { dir.saveFileWithMetadata(byteArrayOf(),track);converted++ }
|
launch { dir.saveFileWithMetadata(byteArrayOf(), track); converted++ }
|
||||||
//Unregister this broadcast Receiver
|
// Unregister this broadcast Receiver
|
||||||
this@ForegroundService.unregisterReceiver(this)
|
this@ForegroundService.unregisterReceiver(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -375,8 +391,6 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the method that can be called to update the Notification
|
* This is the method that can be called to update the Notification
|
||||||
*/
|
*/
|
||||||
@ -387,7 +401,7 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun releaseWakeLock() {
|
private fun releaseWakeLock() {
|
||||||
logger.d(tag){"Releasing Wake Lock"}
|
logger.d(tag) { "Releasing Wake Lock" }
|
||||||
try {
|
try {
|
||||||
wakeLock?.let {
|
wakeLock?.let {
|
||||||
if (it.isHeld) {
|
if (it.isHeld) {
|
||||||
@ -395,14 +409,14 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.d(tag){"Service stopped without being started: ${e.message}"}
|
logger.d(tag) { "Service stopped without being started: ${e.message}" }
|
||||||
}
|
}
|
||||||
isServiceStarted = false
|
isServiceStarted = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("SameParameterValue")
|
@Suppress("SameParameterValue")
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
private fun createNotificationChannel(channelId: String, channelName: String){
|
private fun createNotificationChannel(channelId: String, channelName: String) {
|
||||||
val channel = NotificationChannel(
|
val channel = NotificationChannel(
|
||||||
channelId,
|
channelId,
|
||||||
channelName, NotificationManager.IMPORTANCE_DEFAULT
|
channelName, NotificationManager.IMPORTANCE_DEFAULT
|
||||||
@ -416,15 +430,15 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
* Cleaning All Residual Files except Mp3 Files
|
* Cleaning All Residual Files except Mp3 Files
|
||||||
**/
|
**/
|
||||||
private fun cleanFiles(dir: File) {
|
private fun cleanFiles(dir: File) {
|
||||||
logger.d(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) {
|
||||||
if (file.isDirectory) {
|
if (file.isDirectory) {
|
||||||
cleanFiles(file)
|
cleanFiles(file)
|
||||||
} else if(file.isFile) {
|
} else if (file.isFile) {
|
||||||
if(file.path.toString().substringAfterLast(".") != "mp3"){
|
if (file.path.toString().substringAfterLast(".") != "mp3") {
|
||||||
logger.d(tag){ "Cleaning ${file.path}"}
|
logger.d(tag) { "Cleaning ${file.path}" }
|
||||||
file.delete()
|
file.delete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -433,41 +447,41 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun killService() {
|
private fun killService() {
|
||||||
launch{
|
launch {
|
||||||
logger.d(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(dir.defaultDir()))
|
cleanFiles(File(dir.defaultDir()))
|
||||||
//TODO cleanFiles(File(dir.imageCacheDir()))
|
// TODO cleanFiles(File(dir.imageCacheDir()))
|
||||||
messageList = mutableListOf("","","","","")
|
messageList = mutableListOf("", "", "", "", "")
|
||||||
releaseWakeLock()
|
releaseWakeLock()
|
||||||
serviceJob.cancel()
|
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 {
|
||||||
stopSelf()//System will automatically close it
|
stopSelf() // System will automatically close it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
if(isFinished){
|
if (isFinished) {
|
||||||
killService()
|
killService()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
super.onTaskRemoved(rootIntent)
|
super.onTaskRemoved(rootIntent)
|
||||||
if(isFinished){
|
if (isFinished) {
|
||||||
killService()
|
killService()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getNotification():Notification = NotificationCompat.Builder(this, channelId).run {
|
private fun getNotification(): Notification = NotificationCompat.Builder(this, channelId).run {
|
||||||
setSmallIcon(drawable.ic_download_arrow)
|
setSmallIcon(R.drawable.ic_download_arrow)
|
||||||
setContentTitle("Total: $total Completed:$converted Failed:$failed")
|
setContentTitle("Total: $total Completed:$converted Failed:$failed")
|
||||||
setSilent(true)
|
setSilent(true)
|
||||||
setStyle(
|
setStyle(
|
||||||
@ -479,22 +493,22 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
addLine(messageList[messageList.size - 5])
|
addLine(messageList[messageList.size - 5])
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
addAction(drawable.ic_round_cancel_24,"Exit",cancelIntent)
|
addAction(R.drawable.ic_round_cancel_24, "Exit", cancelIntent)
|
||||||
build()
|
build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addToNotification(message:String){
|
private fun addToNotification(message: String) {
|
||||||
messageList.add(message)
|
messageList.add(message)
|
||||||
updateNotification()
|
updateNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeFromNotification(message: String){
|
private fun removeFromNotification(message: String) {
|
||||||
messageList.remove(message)
|
messageList.remove(message)
|
||||||
updateNotification()
|
updateNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendTrackBroadcast(action:String,track:TrackDetails){
|
fun sendTrackBroadcast(action: String, track: TrackDetails) {
|
||||||
val intent = Intent().apply{
|
val intent = Intent().apply {
|
||||||
setAction(action)
|
setAction(action)
|
||||||
putExtra("track", track)
|
putExtra("track", track)
|
||||||
}
|
}
|
||||||
@ -502,7 +516,7 @@ class ForegroundService : Service(),CoroutineScope{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Fetch.removeAllListeners():Fetch{
|
private fun Fetch.removeAllListeners(): Fetch {
|
||||||
for (listener in this.getListenerSet()) {
|
for (listener in this.getListenerSet()) {
|
||||||
this.removeListener(listener)
|
this.removeListener(listener)
|
||||||
}
|
}
|
||||||
|
@ -23,10 +23,13 @@ import com.shabinder.common.di.providers.GaanaProvider
|
|||||||
import com.shabinder.common.di.providers.SpotifyProvider
|
import com.shabinder.common.di.providers.SpotifyProvider
|
||||||
import com.shabinder.common.di.providers.YoutubeMp3
|
import com.shabinder.common.di.providers.YoutubeMp3
|
||||||
import com.shabinder.common.di.providers.YoutubeMusic
|
import com.shabinder.common.di.providers.YoutubeMusic
|
||||||
import io.ktor.client.*
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.features.json.*
|
import io.ktor.client.features.json.JsonFeature
|
||||||
import io.ktor.client.features.json.serializer.*
|
import io.ktor.client.features.json.serializer.KotlinxSerializer
|
||||||
import io.ktor.client.features.logging.*
|
import io.ktor.client.features.logging.DEFAULT
|
||||||
|
import io.ktor.client.features.logging.LogLevel
|
||||||
|
import io.ktor.client.features.logging.Logger
|
||||||
|
import io.ktor.client.features.logging.Logging
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import org.koin.core.context.startKoin
|
import org.koin.core.context.startKoin
|
||||||
import org.koin.dsl.KoinAppDeclaration
|
import org.koin.dsl.KoinAppDeclaration
|
||||||
@ -40,23 +43,25 @@ fun initKoin(enableNetworkLogs: Boolean = false, appDeclaration: KoinAppDeclarat
|
|||||||
|
|
||||||
fun commonModule(enableNetworkLogs: Boolean) = module {
|
fun commonModule(enableNetworkLogs: Boolean) = module {
|
||||||
single { createHttpClient(enableNetworkLogs = enableNetworkLogs) }
|
single { createHttpClient(enableNetworkLogs = enableNetworkLogs) }
|
||||||
single { Dir(get(),createDatabase()) }
|
single { Dir(get(), createDatabase()) }
|
||||||
single { Kermit(getLogger()) }
|
single { Kermit(getLogger()) }
|
||||||
single { TokenStore(get(),get()) }
|
single { TokenStore(get(), get()) }
|
||||||
single { YoutubeMusic(get(),get()) }
|
single { YoutubeMusic(get(), get()) }
|
||||||
single { SpotifyProvider(get(),get(),get()) }
|
single { SpotifyProvider(get(), get(), get()) }
|
||||||
single { GaanaProvider(get(),get(),get()) }
|
single { GaanaProvider(get(), get(), get()) }
|
||||||
single { YoutubeProvider(get(),get(),get()) }
|
single { YoutubeProvider(get(), get(), get()) }
|
||||||
single { YoutubeMp3(get(),get(),get()) }
|
single { YoutubeMp3(get(), get(), get()) }
|
||||||
single { FetchPlatformQueryResult(get(),get(),get(),get(),get(),get()) }
|
single { FetchPlatformQueryResult(get(), get(), get(), get(), get(), get()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
val kotlinxSerializer = KotlinxSerializer( Json {
|
val kotlinxSerializer = KotlinxSerializer(
|
||||||
isLenient = true
|
Json {
|
||||||
ignoreUnknownKeys = true
|
isLenient = true
|
||||||
})
|
ignoreUnknownKeys = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
fun createHttpClient(enableNetworkLogs: Boolean = false,serializer: KotlinxSerializer = kotlinxSerializer) = HttpClient {
|
fun createHttpClient(enableNetworkLogs: Boolean = false, serializer: KotlinxSerializer = kotlinxSerializer) = HttpClient {
|
||||||
install(JsonFeature) {
|
install(JsonFeature) {
|
||||||
this.serializer = serializer
|
this.serializer = serializer
|
||||||
}
|
}
|
||||||
|
@ -22,9 +22,10 @@ import com.shabinder.common.di.utils.removeIllegalChars
|
|||||||
import com.shabinder.common.models.DownloadResult
|
import com.shabinder.common.models.DownloadResult
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.shabinder.database.Database
|
import com.shabinder.database.Database
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.statement.*
|
import io.ktor.client.statement.HttpStatement
|
||||||
import io.ktor.http.*
|
import io.ktor.http.contentLength
|
||||||
|
import io.ktor.http.isSuccess
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@ -33,17 +34,17 @@ expect class Dir(
|
|||||||
logger: Kermit,
|
logger: Kermit,
|
||||||
database: Database? = createDatabase()
|
database: Database? = createDatabase()
|
||||||
) {
|
) {
|
||||||
val db :Database?
|
val db: Database?
|
||||||
fun isPresent(path:String):Boolean
|
fun isPresent(path: String): Boolean
|
||||||
fun fileSeparator(): String
|
fun fileSeparator(): String
|
||||||
fun defaultDir(): String
|
fun defaultDir(): String
|
||||||
fun imageCacheDir(): String
|
fun imageCacheDir(): String
|
||||||
fun createDirectory(dirPath:String)
|
fun createDirectory(dirPath: String)
|
||||||
suspend fun cacheImage(image: Any,path: String) // in Android = ImageBitmap, Desktop = BufferedImage
|
suspend fun cacheImage(image: Any, path: String) // in Android = ImageBitmap, Desktop = BufferedImage
|
||||||
suspend fun loadImage(url:String): Picture
|
suspend fun loadImage(url: String): Picture
|
||||||
suspend fun clearCache()
|
suspend fun clearCache()
|
||||||
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, trackDetails: TrackDetails)
|
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, trackDetails: TrackDetails)
|
||||||
fun addToLibrary(path:String)
|
fun addToLibrary(path: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun downloadFile(url: String): Flow<DownloadResult> {
|
suspend fun downloadFile(url: String): Flow<DownloadResult> {
|
||||||
@ -67,7 +68,7 @@ suspend fun downloadFile(url: String): Flow<DownloadResult> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun getNameURL(url: String): String {
|
fun getNameURL(url: String): String {
|
||||||
return url.substring(url.lastIndexOf('/',url.lastIndexOf('/')-1) + 1, url.length).replace('/','_')
|
return url.substring(url.lastIndexOf('/', url.lastIndexOf('/') - 1) + 1, url.length).replace('/', '_')
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
* Call this function at startup!
|
* Call this function at startup!
|
||||||
@ -80,7 +81,7 @@ fun Dir.createDirectories() {
|
|||||||
createDirectory(defaultDir() + "Playlists/")
|
createDirectory(defaultDir() + "Playlists/")
|
||||||
createDirectory(defaultDir() + "YT_Downloads/")
|
createDirectory(defaultDir() + "YT_Downloads/")
|
||||||
}
|
}
|
||||||
fun Dir.finalOutputDir(itemName:String, type:String, subFolder:String, defaultDir:String, extension:String = ".mp3" ): String =
|
fun Dir.finalOutputDir(itemName: String, type: String, subFolder: String, defaultDir: String, extension: String = ".mp3"): String =
|
||||||
defaultDir + removeIllegalChars(type) + this.fileSeparator() +
|
defaultDir + removeIllegalChars(type) + this.fileSeparator() +
|
||||||
if(subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator()} +
|
if (subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator() } +
|
||||||
removeIllegalChars(itemName) + extension
|
removeIllegalChars(itemName) + extension
|
||||||
|
@ -20,7 +20,7 @@ import com.shabinder.common.models.AllPlatforms
|
|||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
|
||||||
expect fun openPlatform(packageID:String, platformLink:String)
|
expect fun openPlatform(packageID: String, platformLink: String)
|
||||||
|
|
||||||
expect fun shareApp()
|
expect fun shareApp()
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ expect fun giveDonation()
|
|||||||
|
|
||||||
expect val dispatcherIO: CoroutineDispatcher
|
expect val dispatcherIO: CoroutineDispatcher
|
||||||
|
|
||||||
expect val isInternetAvailable:Boolean
|
expect val isInternetAvailable: Boolean
|
||||||
|
|
||||||
expect val currentPlatform: AllPlatforms
|
expect val currentPlatform: AllPlatforms
|
||||||
|
|
||||||
|
@ -22,33 +22,32 @@ import com.shabinder.common.di.providers.SpotifyProvider
|
|||||||
import com.shabinder.common.di.providers.YoutubeMp3
|
import com.shabinder.common.di.providers.YoutubeMp3
|
||||||
import com.shabinder.common.di.providers.YoutubeMusic
|
import com.shabinder.common.di.providers.YoutubeMusic
|
||||||
import com.shabinder.common.models.PlatformQueryResult
|
import com.shabinder.common.models.PlatformQueryResult
|
||||||
import com.shabinder.database.Database
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class FetchPlatformQueryResult(
|
class FetchPlatformQueryResult(
|
||||||
private val gaanaProvider: GaanaProvider,
|
private val gaanaProvider: GaanaProvider,
|
||||||
val spotifyProvider: SpotifyProvider,
|
private val spotifyProvider: SpotifyProvider,
|
||||||
val youtubeProvider: YoutubeProvider,
|
val youtubeProvider: YoutubeProvider,
|
||||||
val youtubeMusic: YoutubeMusic,
|
val youtubeMusic: YoutubeMusic,
|
||||||
val youtubeMp3: YoutubeMp3,
|
val youtubeMp3: YoutubeMp3,
|
||||||
private val dir: Dir
|
private val dir: Dir
|
||||||
) {
|
) {
|
||||||
private val db:DownloadRecordDatabaseQueries?
|
private val db: DownloadRecordDatabaseQueries?
|
||||||
get() = dir.db?.downloadRecordDatabaseQueries
|
get() = dir.db?.downloadRecordDatabaseQueries
|
||||||
|
|
||||||
suspend fun query(link:String): PlatformQueryResult?{
|
suspend fun query(link: String): PlatformQueryResult? {
|
||||||
val result = when{
|
val result = when {
|
||||||
//SPOTIFY
|
// SPOTIFY
|
||||||
link.contains("spotify",true) ->
|
link.contains("spotify", true) ->
|
||||||
spotifyProvider.query(link)
|
spotifyProvider.query(link)
|
||||||
|
|
||||||
//YOUTUBE
|
// YOUTUBE
|
||||||
link.contains("youtube.com",true) || link.contains("youtu.be",true) ->
|
link.contains("youtube.com", true) || link.contains("youtu.be", true) ->
|
||||||
youtubeProvider.query(link)
|
youtubeProvider.query(link)
|
||||||
|
|
||||||
//GAANA
|
// GAANA
|
||||||
link.contains("gaana",true) ->
|
link.contains("gaana", true) ->
|
||||||
gaanaProvider.query(link)
|
gaanaProvider.query(link)
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
@ -56,7 +55,7 @@ class FetchPlatformQueryResult(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
result?.run {
|
result?.run {
|
||||||
withContext(Dispatchers.Default){
|
withContext(Dispatchers.Default) {
|
||||||
db?.add(
|
db?.add(
|
||||||
folderType, title, link, coverUrl, trackList.size.toLong()
|
folderType, title, link, coverUrl, trackList.size.toLong()
|
||||||
)
|
)
|
||||||
|
@ -31,18 +31,18 @@ class TokenStore(
|
|||||||
private val db: TokenDBQueries?
|
private val db: TokenDBQueries?
|
||||||
get() = dir.db?.tokenDBQueries
|
get() = dir.db?.tokenDBQueries
|
||||||
|
|
||||||
private fun save(token: TokenData){
|
private fun save(token: TokenData) {
|
||||||
if(!token.access_token.isNullOrBlank() && token.expiry != null)
|
if (!token.access_token.isNullOrBlank() && token.expiry != null)
|
||||||
db?.add(token.access_token!!, token.expiry!! + Clock.System.now().epochSeconds)
|
db?.add(token.access_token!!, token.expiry!! + Clock.System.now().epochSeconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getToken(): TokenData? {
|
suspend fun getToken(): TokenData? {
|
||||||
var token: TokenData? = db?.select()?.executeAsOneOrNull()?.let {
|
var token: TokenData? = db?.select()?.executeAsOneOrNull()?.let {
|
||||||
TokenData(it.accessToken,null,it.expiry)
|
TokenData(it.accessToken, null, it.expiry)
|
||||||
}
|
}
|
||||||
logger.d{"System Time:${Clock.System.now().epochSeconds} , Token Expiry:${token?.expiry}"}
|
logger.d { "System Time:${Clock.System.now().epochSeconds} , Token Expiry:${token?.expiry}" }
|
||||||
if(Clock.System.now().epochSeconds > token?.expiry ?:0 || token == null){
|
if (Clock.System.now().epochSeconds > token?.expiry ?: 0 || token == null) {
|
||||||
logger.d{"Requesting New Token"}
|
logger.d { "Requesting New Token" }
|
||||||
token = authenticateSpotify()
|
token = authenticateSpotify()
|
||||||
GlobalScope.launch { token?.access_token?.let { save(token) } }
|
GlobalScope.launch { token?.access_token?.let { save(token) } }
|
||||||
}
|
}
|
||||||
|
@ -18,8 +18,7 @@ package com.shabinder.common.di
|
|||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.models.PlatformQueryResult
|
import com.shabinder.common.models.PlatformQueryResult
|
||||||
import com.shabinder.database.Database
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.*
|
|
||||||
|
|
||||||
expect class YoutubeProvider(
|
expect class YoutubeProvider(
|
||||||
httpClient: HttpClient,
|
httpClient: HttpClient,
|
||||||
|
@ -19,11 +19,15 @@ package com.shabinder.common.di.gaana
|
|||||||
import com.shabinder.common.di.currentPlatform
|
import com.shabinder.common.di.currentPlatform
|
||||||
import com.shabinder.common.models.AllPlatforms
|
import com.shabinder.common.models.AllPlatforms
|
||||||
import com.shabinder.common.models.corsProxy
|
import com.shabinder.common.models.corsProxy
|
||||||
import com.shabinder.common.models.gaana.*
|
import com.shabinder.common.models.gaana.GaanaAlbum
|
||||||
import io.ktor.client.*
|
import com.shabinder.common.models.gaana.GaanaArtistDetails
|
||||||
import io.ktor.client.request.*
|
import com.shabinder.common.models.gaana.GaanaArtistTracks
|
||||||
|
import com.shabinder.common.models.gaana.GaanaPlaylist
|
||||||
|
import com.shabinder.common.models.gaana.GaanaSong
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
|
||||||
val corsApi get() = if(currentPlatform is AllPlatforms.Js){
|
val corsApi get() = if (currentPlatform is AllPlatforms.Js) {
|
||||||
corsProxy.url
|
corsProxy.url
|
||||||
} // "https://spotiflyer-cors.azurewebsites.net/" //"https://spotiflyer-cors.herokuapp.com/"//"https://cors.bridged.cc/"
|
} // "https://spotiflyer-cors.azurewebsites.net/" //"https://spotiflyer-cors.herokuapp.com/"//"https://cors.bridged.cc/"
|
||||||
else ""
|
else ""
|
||||||
@ -33,7 +37,7 @@ private val BASE_URL get() = "${corsApi}https://api.gaana.com"
|
|||||||
|
|
||||||
interface GaanaRequests {
|
interface GaanaRequests {
|
||||||
|
|
||||||
val httpClient:HttpClient
|
val httpClient: HttpClient
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Api Request: http://api.gaana.com/?type=playlist&subtype=playlist_detail&seokey=gaana-dj-hindi-top-50-1&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
|
* Api Request: http://api.gaana.com/?type=playlist&subtype=playlist_detail&seokey=gaana-dj-hindi-top-50-1&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
|
||||||
|
@ -25,25 +25,25 @@ import com.shabinder.common.models.PlatformQueryResult
|
|||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.shabinder.common.models.gaana.GaanaTrack
|
import com.shabinder.common.models.gaana.GaanaTrack
|
||||||
import com.shabinder.common.models.spotify.Source
|
import com.shabinder.common.models.spotify.Source
|
||||||
import io.ktor.client.*
|
import io.ktor.client.HttpClient
|
||||||
|
|
||||||
class GaanaProvider(
|
class GaanaProvider(
|
||||||
override val httpClient: HttpClient,
|
override val httpClient: HttpClient,
|
||||||
private val logger: Kermit,
|
private val logger: Kermit,
|
||||||
private val dir: Dir,
|
private val dir: Dir,
|
||||||
): GaanaRequests {
|
) : GaanaRequests {
|
||||||
|
|
||||||
private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
|
private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
|
||||||
|
|
||||||
suspend fun query(fullLink: String): PlatformQueryResult?{
|
suspend fun query(fullLink: String): PlatformQueryResult? {
|
||||||
//Link Schema: https://gaana.com/type/link
|
// Link Schema: https://gaana.com/type/link
|
||||||
val gaanaLink = fullLink.substringAfter("gaana.com/")
|
val gaanaLink = fullLink.substringAfter("gaana.com/")
|
||||||
|
|
||||||
val link = gaanaLink.substringAfterLast('/', "error")
|
val link = gaanaLink.substringAfterLast('/', "error")
|
||||||
val type = gaanaLink.substringBeforeLast('/', "error").substringAfterLast('/')
|
val type = gaanaLink.substringBeforeLast('/', "error").substringAfterLast('/')
|
||||||
|
|
||||||
//Error
|
// Error
|
||||||
if (type == "Error" || link == "Error"){
|
if (type == "Error" || link == "Error") {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return gaanaSearch(
|
return gaanaSearch(
|
||||||
@ -53,8 +53,8 @@ class GaanaProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun gaanaSearch(
|
private suspend fun gaanaSearch(
|
||||||
type:String,
|
type: String,
|
||||||
link:String,
|
link: String,
|
||||||
): PlatformQueryResult {
|
): PlatformQueryResult {
|
||||||
val result = PlatformQueryResult(
|
val result = PlatformQueryResult(
|
||||||
folderType = "",
|
folderType = "",
|
||||||
@ -64,22 +64,14 @@ class GaanaProvider(
|
|||||||
trackList = listOf(),
|
trackList = listOf(),
|
||||||
Source.Gaana
|
Source.Gaana
|
||||||
)
|
)
|
||||||
|
logger.i { "GAANA SEARCH: $type - $link" }
|
||||||
with(result) {
|
with(result) {
|
||||||
when (type) {
|
when (type) {
|
||||||
"song" -> {
|
"song" -> {
|
||||||
getGaanaSong(seokey = link).tracks.firstOrNull()?.also {
|
getGaanaSong(seokey = link).tracks.firstOrNull()?.also {
|
||||||
folderType = "Tracks"
|
folderType = "Tracks"
|
||||||
subFolder = ""
|
subFolder = ""
|
||||||
if (dir.isPresent(
|
it.updateStatusIfPresent(folderType, subFolder)
|
||||||
dir.finalOutputDir(
|
|
||||||
it.track_title,
|
|
||||||
folderType,
|
|
||||||
subFolder,
|
|
||||||
dir.defaultDir()
|
|
||||||
)
|
|
||||||
)) {//Download Already Present!!
|
|
||||||
it.downloaded = DownloadStatus.Downloaded
|
|
||||||
}
|
|
||||||
trackList = listOf(it).toTrackDetailsList(folderType, subFolder)
|
trackList = listOf(it).toTrackDetailsList(folderType, subFolder)
|
||||||
title = it.track_title
|
title = it.track_title
|
||||||
coverUrl = it.artworkLink
|
coverUrl = it.artworkLink
|
||||||
@ -90,17 +82,7 @@ class GaanaProvider(
|
|||||||
folderType = "Albums"
|
folderType = "Albums"
|
||||||
subFolder = link
|
subFolder = link
|
||||||
it.tracks.forEach { track ->
|
it.tracks.forEach { track ->
|
||||||
if (dir.isPresent(
|
track.updateStatusIfPresent(folderType, subFolder)
|
||||||
dir.finalOutputDir(
|
|
||||||
track.track_title,
|
|
||||||
folderType,
|
|
||||||
subFolder,
|
|
||||||
dir.defaultDir()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {//Download Already Present!!
|
|
||||||
track.downloaded = DownloadStatus.Downloaded
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
trackList = it.tracks.toTrackDetailsList(folderType, subFolder)
|
trackList = it.tracks.toTrackDetailsList(folderType, subFolder)
|
||||||
title = link
|
title = link
|
||||||
@ -112,21 +94,11 @@ class GaanaProvider(
|
|||||||
folderType = "Playlists"
|
folderType = "Playlists"
|
||||||
subFolder = link
|
subFolder = link
|
||||||
it.tracks.forEach { track ->
|
it.tracks.forEach { track ->
|
||||||
if (dir.isPresent(
|
track.updateStatusIfPresent(folderType, subFolder)
|
||||||
dir.finalOutputDir(
|
|
||||||
track.track_title,
|
|
||||||
folderType,
|
|
||||||
subFolder,
|
|
||||||
dir.defaultDir()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {//Download Already Present!!
|
|
||||||
track.downloaded = DownloadStatus.Downloaded
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
trackList = it.tracks.toTrackDetailsList(folderType, subFolder)
|
trackList = it.tracks.toTrackDetailsList(folderType, subFolder)
|
||||||
title = link
|
title = link
|
||||||
//coverUrl.value = "TODO"
|
// coverUrl.value = "TODO"
|
||||||
coverUrl = gaanaPlaceholderImageUrl
|
coverUrl = gaanaPlaceholderImageUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -134,37 +106,27 @@ class GaanaProvider(
|
|||||||
folderType = "Artist"
|
folderType = "Artist"
|
||||||
subFolder = link
|
subFolder = link
|
||||||
coverUrl = gaanaPlaceholderImageUrl
|
coverUrl = gaanaPlaceholderImageUrl
|
||||||
val artistDetails =
|
getGaanaArtistDetails(seokey = link).artist.firstOrNull()
|
||||||
getGaanaArtistDetails(seokey = link).artist.firstOrNull()
|
?.also {
|
||||||
?.also {
|
title = it.name
|
||||||
title = it.name
|
coverUrl = it.artworkLink ?: gaanaPlaceholderImageUrl
|
||||||
coverUrl = it.artworkLink ?: gaanaPlaceholderImageUrl
|
}
|
||||||
}
|
|
||||||
getGaanaArtistTracks(seokey = link).also {
|
getGaanaArtistTracks(seokey = link).also {
|
||||||
it.tracks?.forEach { track ->
|
it.tracks?.forEach { track ->
|
||||||
if (dir.isPresent(
|
track.updateStatusIfPresent(folderType, subFolder)
|
||||||
dir.finalOutputDir(
|
|
||||||
track.track_title,
|
|
||||||
folderType,
|
|
||||||
subFolder,
|
|
||||||
dir.defaultDir()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {//Download Already Present!!
|
|
||||||
track.downloaded = DownloadStatus.Downloaded
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
trackList = it.tracks?.toTrackDetailsList(folderType, subFolder) ?: emptyList()
|
trackList = it.tracks?.toTrackDetailsList(folderType, subFolder) ?: emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {//TODO Handle Error}
|
else -> {
|
||||||
|
// TODO Handle Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<GaanaTrack>.toTrackDetailsList(type:String, subFolder:String) = this.map {
|
private fun List<GaanaTrack>.toTrackDetailsList(type: String, subFolder: String) = this.map {
|
||||||
TrackDetails(
|
TrackDetails(
|
||||||
title = it.track_title,
|
title = it.track_title,
|
||||||
artists = it.artist.map { artist -> artist?.name.toString() },
|
artists = it.artist.map { artist -> artist?.name.toString() },
|
||||||
@ -172,12 +134,25 @@ class GaanaProvider(
|
|||||||
albumArtPath = dir.imageCacheDir() + (it.artworkLink.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg",
|
albumArtPath = dir.imageCacheDir() + (it.artworkLink.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg",
|
||||||
albumName = it.album_title,
|
albumName = it.album_title,
|
||||||
year = it.release_date,
|
year = it.release_date,
|
||||||
comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}",
|
comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}",
|
||||||
trackUrl = it.lyrics_url,
|
trackUrl = it.lyrics_url,
|
||||||
downloaded = it.downloaded ?: DownloadStatus.NotDownloaded,
|
downloaded = it.downloaded ?: DownloadStatus.NotDownloaded,
|
||||||
source = Source.Gaana,
|
source = Source.Gaana,
|
||||||
albumArtURL = it.artworkLink,
|
albumArtURL = it.artworkLink,
|
||||||
outputFilePath = dir.finalOutputDir(it.track_title,type, subFolder,dir.defaultDir()/*,".m4a"*/)
|
outputFilePath = dir.finalOutputDir(it.track_title, type, subFolder, dir.defaultDir()/*,".m4a"*/)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
private fun GaanaTrack.updateStatusIfPresent(folderType: String, subFolder: String) {
|
||||||
|
if (dir.isPresent(
|
||||||
|
dir.finalOutputDir(
|
||||||
|
track_title,
|
||||||
|
folderType,
|
||||||
|
subFolder,
|
||||||
|
dir.defaultDir()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) { // Download Already Present!!
|
||||||
|
downloaded = DownloadStatus.Downloaded
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,11 @@
|
|||||||
package com.shabinder.common.di.providers
|
package com.shabinder.common.di.providers
|
||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.di.*
|
import com.shabinder.common.di.Dir
|
||||||
|
import com.shabinder.common.di.TokenStore
|
||||||
|
import com.shabinder.common.di.currentPlatform
|
||||||
|
import com.shabinder.common.di.finalOutputDir
|
||||||
|
import com.shabinder.common.di.kotlinxSerializer
|
||||||
import com.shabinder.common.di.spotify.SpotifyRequests
|
import com.shabinder.common.di.spotify.SpotifyRequests
|
||||||
import com.shabinder.common.di.spotify.authenticateSpotify
|
import com.shabinder.common.di.spotify.authenticateSpotify
|
||||||
import com.shabinder.common.models.AllPlatforms
|
import com.shabinder.common.models.AllPlatforms
|
||||||
@ -27,10 +31,10 @@ import com.shabinder.common.models.spotify.Album
|
|||||||
import com.shabinder.common.models.spotify.Image
|
import com.shabinder.common.models.spotify.Image
|
||||||
import com.shabinder.common.models.spotify.Source
|
import com.shabinder.common.models.spotify.Source
|
||||||
import com.shabinder.common.models.spotify.Track
|
import com.shabinder.common.models.spotify.Track
|
||||||
import io.ktor.client.*
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.features.*
|
import io.ktor.client.features.defaultRequest
|
||||||
import io.ktor.client.features.json.*
|
import io.ktor.client.features.json.JsonFeature
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.header
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -44,23 +48,22 @@ class SpotifyProvider(
|
|||||||
init {
|
init {
|
||||||
logger.d { "Creating Spotify Provider" }
|
logger.d { "Creating Spotify Provider" }
|
||||||
GlobalScope.launch(Dispatchers.Default) {
|
GlobalScope.launch(Dispatchers.Default) {
|
||||||
if(currentPlatform is AllPlatforms.Js){
|
if (currentPlatform is AllPlatforms.Js) {
|
||||||
authenticateSpotifyClient(override = true)
|
authenticateSpotifyClient(override = true)
|
||||||
}else authenticateSpotifyClient()
|
} else authenticateSpotifyClient()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun authenticateSpotifyClient(override:Boolean): HttpClient?{
|
override suspend fun authenticateSpotifyClient(override: Boolean): HttpClient? {
|
||||||
val token = if(override) authenticateSpotify() else tokenStore.getToken()
|
val token = if (override) authenticateSpotify() else tokenStore.getToken()
|
||||||
return if(token == null) {
|
return if (token == null) {
|
||||||
logger.d{ "Please Check your Network Connection" }
|
logger.d { "Please Check your Network Connection" }
|
||||||
null
|
null
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
logger.d { "Spotify Provider Created with $token" }
|
logger.d { "Spotify Provider Created with $token" }
|
||||||
httpClient = HttpClient {
|
httpClient = HttpClient {
|
||||||
defaultRequest {
|
defaultRequest {
|
||||||
header("Authorization","Bearer ${token.access_token}")
|
header("Authorization", "Bearer ${token.access_token}")
|
||||||
}
|
}
|
||||||
install(JsonFeature) {
|
install(JsonFeature) {
|
||||||
serializer = kotlinxSerializer
|
serializer = kotlinxSerializer
|
||||||
@ -72,9 +75,9 @@ class SpotifyProvider(
|
|||||||
|
|
||||||
override lateinit var httpClient: HttpClient
|
override lateinit var httpClient: HttpClient
|
||||||
|
|
||||||
suspend fun query(fullLink: String): PlatformQueryResult?{
|
suspend fun query(fullLink: String): PlatformQueryResult? {
|
||||||
|
|
||||||
if(!this::httpClient.isInitialized){
|
if (!this::httpClient.isInitialized) {
|
||||||
authenticateSpotifyClient()
|
authenticateSpotifyClient()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,20 +85,19 @@ class SpotifyProvider(
|
|||||||
"https://" + fullLink.substringAfterLast("https://").substringBefore(" ").trim()
|
"https://" + fullLink.substringAfterLast("https://").substringBefore(" ").trim()
|
||||||
|
|
||||||
if (!spotifyLink.contains("open.spotify")) {
|
if (!spotifyLink.contains("open.spotify")) {
|
||||||
//Very Rare instance
|
// Very Rare instance
|
||||||
spotifyLink = resolveLink(spotifyLink)
|
spotifyLink = resolveLink(spotifyLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
|
val link = spotifyLink.substringAfterLast('/', "Error").substringBefore('?')
|
||||||
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
|
val type = spotifyLink.substringBeforeLast('/', "Error").substringAfterLast('/')
|
||||||
|
|
||||||
|
|
||||||
if (type == "Error" || link == "Error") {
|
if (type == "Error" || link == "Error") {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type == "episode" || type == "show") {
|
if (type == "episode" || type == "show") {
|
||||||
//TODO Implementation
|
// TODO Implementation
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +108,7 @@ class SpotifyProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun spotifySearch(
|
private suspend fun spotifySearch(
|
||||||
type:String,
|
type: String,
|
||||||
link: String
|
link: String
|
||||||
): PlatformQueryResult {
|
): PlatformQueryResult {
|
||||||
val result = PlatformQueryResult(
|
val result = PlatformQueryResult(
|
||||||
@ -123,21 +125,13 @@ class SpotifyProvider(
|
|||||||
getTrack(link).also {
|
getTrack(link).also {
|
||||||
folderType = "Tracks"
|
folderType = "Tracks"
|
||||||
subFolder = ""
|
subFolder = ""
|
||||||
if (dir.isPresent(
|
it.updateStatusIfPresent(folderType, subFolder)
|
||||||
dir.finalOutputDir(
|
|
||||||
it.name.toString(),
|
|
||||||
folderType,
|
|
||||||
subFolder,
|
|
||||||
dir.defaultDir()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {//Download Already Present!!
|
|
||||||
it.downloaded = com.shabinder.common.models.DownloadStatus.Downloaded
|
|
||||||
}
|
|
||||||
trackList = listOf(it).toTrackDetailsList(folderType, subFolder)
|
trackList = listOf(it).toTrackDetailsList(folderType, subFolder)
|
||||||
title = it.name.toString()
|
title = it.name.toString()
|
||||||
coverUrl = (it.album?.images?.elementAtOrNull(1)?.url
|
coverUrl = (
|
||||||
?: it.album?.images?.elementAtOrNull(0)?.url).toString()
|
it.album?.images?.elementAtOrNull(1)?.url
|
||||||
|
?: it.album?.images?.elementAtOrNull(0)?.url
|
||||||
|
).toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,17 +140,7 @@ class SpotifyProvider(
|
|||||||
folderType = "Albums"
|
folderType = "Albums"
|
||||||
subFolder = albumObject.name.toString()
|
subFolder = albumObject.name.toString()
|
||||||
albumObject.tracks?.items?.forEach {
|
albumObject.tracks?.items?.forEach {
|
||||||
if (dir.isPresent(
|
it.updateStatusIfPresent(folderType, subFolder)
|
||||||
dir.finalOutputDir(
|
|
||||||
it.name.toString(),
|
|
||||||
folderType,
|
|
||||||
subFolder,
|
|
||||||
dir.defaultDir()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {//Download Already Present!!
|
|
||||||
it.downloaded = com.shabinder.common.models.DownloadStatus.Downloaded
|
|
||||||
}
|
|
||||||
it.album = Album(
|
it.album = Album(
|
||||||
images = listOf(
|
images = listOf(
|
||||||
Image(
|
Image(
|
||||||
@ -168,12 +152,14 @@ class SpotifyProvider(
|
|||||||
}
|
}
|
||||||
albumObject.tracks?.items?.toTrackDetailsList(folderType, subFolder).let {
|
albumObject.tracks?.items?.toTrackDetailsList(folderType, subFolder).let {
|
||||||
if (it.isNullOrEmpty()) {
|
if (it.isNullOrEmpty()) {
|
||||||
//TODO Handle Error
|
// TODO Handle Error
|
||||||
} else {
|
} else {
|
||||||
trackList = it
|
trackList = it
|
||||||
title = albumObject.name.toString()
|
title = albumObject.name.toString()
|
||||||
coverUrl = (albumObject.images?.elementAtOrNull(1)?.url
|
coverUrl = (
|
||||||
?: albumObject.images?.elementAtOrNull(0)?.url).toString()
|
albumObject.images?.elementAtOrNull(1)?.url
|
||||||
|
?: albumObject.images?.elementAtOrNull(0)?.url
|
||||||
|
).toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -183,27 +169,17 @@ class SpotifyProvider(
|
|||||||
folderType = "Playlists"
|
folderType = "Playlists"
|
||||||
subFolder = playlistObject.name.toString()
|
subFolder = playlistObject.name.toString()
|
||||||
val tempTrackList = mutableListOf<Track>()
|
val tempTrackList = mutableListOf<Track>()
|
||||||
//log("Tracks Fetched", playlistObject.tracks?.items?.size.toString())
|
// log("Tracks Fetched", playlistObject.tracks?.items?.size.toString())
|
||||||
playlistObject.tracks?.items?.forEach {
|
playlistObject.tracks?.items?.forEach {
|
||||||
it.track?.let { it1 ->
|
it.track?.let { it1 ->
|
||||||
if (dir.isPresent(
|
it1.updateStatusIfPresent(folderType, subFolder)
|
||||||
dir.finalOutputDir(
|
|
||||||
it1.name.toString(),
|
|
||||||
folderType,
|
|
||||||
subFolder,
|
|
||||||
dir.defaultDir()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {//Download Already Present!!
|
|
||||||
it1.downloaded = com.shabinder.common.models.DownloadStatus.Downloaded
|
|
||||||
}
|
|
||||||
tempTrackList.add(it1)
|
tempTrackList.add(it1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var moreTracksAvailable = !playlistObject.tracks?.next.isNullOrBlank()
|
var moreTracksAvailable = !playlistObject.tracks?.next.isNullOrBlank()
|
||||||
|
|
||||||
while (moreTracksAvailable) {
|
while (moreTracksAvailable) {
|
||||||
//Check For More Tracks If available
|
// Check For More Tracks If available
|
||||||
val moreTracks =
|
val moreTracks =
|
||||||
getPlaylistTracks(link, offset = tempTrackList.size)
|
getPlaylistTracks(link, offset = tempTrackList.size)
|
||||||
moreTracks.items?.forEach {
|
moreTracks.items?.forEach {
|
||||||
@ -211,18 +187,18 @@ class SpotifyProvider(
|
|||||||
}
|
}
|
||||||
moreTracksAvailable = !moreTracks.next.isNullOrBlank()
|
moreTracksAvailable = !moreTracks.next.isNullOrBlank()
|
||||||
}
|
}
|
||||||
//log("Total Tracks Fetched", tempTrackList.size.toString())
|
// log("Total Tracks Fetched", tempTrackList.size.toString())
|
||||||
trackList = tempTrackList.toTrackDetailsList(folderType, subFolder)
|
trackList = tempTrackList.toTrackDetailsList(folderType, subFolder)
|
||||||
title = playlistObject.name.toString()
|
title = playlistObject.name.toString()
|
||||||
coverUrl = playlistObject.images?.elementAtOrNull(1)?.url
|
coverUrl = playlistObject.images?.elementAtOrNull(1)?.url
|
||||||
?: playlistObject.images?.firstOrNull()?.url.toString()
|
?: playlistObject.images?.firstOrNull()?.url.toString()
|
||||||
}
|
}
|
||||||
"episode" -> {//TODO
|
"episode" -> { // TODO
|
||||||
}
|
}
|
||||||
"show" -> {//TODO
|
"show" -> { // TODO
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
//TODO Handle Error
|
// TODO Handle Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,18 +210,18 @@ class SpotifyProvider(
|
|||||||
* Fetching Standard Link: https://open.spotify.com/playlist/37i9dQZF1DX9RwfGbeGQwP?si=iWz7B1tETiunDntnDo3lSQ&_branch_match_id=862039436205270630
|
* Fetching Standard Link: https://open.spotify.com/playlist/37i9dQZF1DX9RwfGbeGQwP?si=iWz7B1tETiunDntnDo3lSQ&_branch_match_id=862039436205270630
|
||||||
* */
|
* */
|
||||||
private suspend fun resolveLink(
|
private suspend fun resolveLink(
|
||||||
url:String
|
url: String
|
||||||
):String {
|
): String {
|
||||||
val response = getResponse(url)
|
val response = getResponse(url)
|
||||||
val regex = """https://open\.spotify\.com.+\w""".toRegex()
|
val regex = """https://open\.spotify\.com.+\w""".toRegex()
|
||||||
return regex.find(response)?.value.toString()
|
return regex.find(response)?.value.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<Track>.toTrackDetailsList(type:String, subFolder:String) = this.map {
|
private fun List<Track>.toTrackDetailsList(type: String, subFolder: String) = this.map {
|
||||||
TrackDetails(
|
TrackDetails(
|
||||||
title = it.name.toString(),
|
title = it.name.toString(),
|
||||||
artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(),
|
artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(),
|
||||||
durationSec = (it.duration_ms/1000).toInt(),
|
durationSec = (it.duration_ms / 1000).toInt(),
|
||||||
albumArtPath = dir.imageCacheDir() + (it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast('/') + ".jpeg",
|
albumArtPath = dir.imageCacheDir() + (it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast('/') + ".jpeg",
|
||||||
albumName = it.album?.name,
|
albumName = it.album?.name,
|
||||||
year = it.album?.release_date,
|
year = it.album?.release_date,
|
||||||
@ -254,7 +230,20 @@ class SpotifyProvider(
|
|||||||
downloaded = it.downloaded,
|
downloaded = it.downloaded,
|
||||||
source = Source.Spotify,
|
source = Source.Spotify,
|
||||||
albumArtURL = it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString(),
|
albumArtURL = it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString(),
|
||||||
outputFilePath = dir.finalOutputDir(it.name.toString(),type, subFolder,dir.defaultDir()/*,".m4a"*/)
|
outputFilePath = dir.finalOutputDir(it.name.toString(), type, subFolder, dir.defaultDir()/*,".m4a"*/)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
private fun Track.updateStatusIfPresent(folderType: String, subFolder: String) {
|
||||||
|
if (dir.isPresent(
|
||||||
|
dir.finalOutputDir(
|
||||||
|
name.toString(),
|
||||||
|
folderType,
|
||||||
|
subFolder,
|
||||||
|
dir.defaultDir()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) { // Download Already Present!!
|
||||||
|
downloaded = com.shabinder.common.models.DownloadStatus.Downloaded
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -21,17 +21,14 @@ import com.shabinder.common.di.Dir
|
|||||||
import com.shabinder.common.di.currentPlatform
|
import com.shabinder.common.di.currentPlatform
|
||||||
import com.shabinder.common.di.youtubeMp3.Yt1sMp3
|
import com.shabinder.common.di.youtubeMp3.Yt1sMp3
|
||||||
import com.shabinder.common.models.AllPlatforms
|
import com.shabinder.common.models.AllPlatforms
|
||||||
import com.shabinder.common.models.CorsProxy
|
import io.ktor.client.HttpClient
|
||||||
import com.shabinder.common.models.corsProxy
|
|
||||||
import com.shabinder.database.Database
|
|
||||||
import io.ktor.client.*
|
|
||||||
|
|
||||||
class YoutubeMp3(
|
class YoutubeMp3(
|
||||||
override val httpClient: HttpClient,
|
override val httpClient: HttpClient,
|
||||||
private val logger: Kermit,
|
private val logger: Kermit,
|
||||||
private val dir: Dir,
|
private val dir: Dir,
|
||||||
):Yt1sMp3 {
|
) : Yt1sMp3 {
|
||||||
suspend fun getMp3DownloadLink(videoID:String):String? = getLinkFromYt1sMp3(videoID)?.let{
|
suspend fun getMp3DownloadLink(videoID: String): String? = getLinkFromYt1sMp3(videoID)?.let {
|
||||||
if (currentPlatform is AllPlatforms.Js/* && corsProxy !is CorsProxy.PublicProxyWithExtension*/)
|
if (currentPlatform is AllPlatforms.Js/* && corsProxy !is CorsProxy.PublicProxyWithExtension*/)
|
||||||
"https://kind-grasshopper-73.telebit.io/cors/$it"
|
"https://kind-grasshopper-73.telebit.io/cors/$it"
|
||||||
else it
|
else it
|
||||||
|
@ -21,21 +21,32 @@ import com.shabinder.common.di.gaana.corsApi
|
|||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.shabinder.common.models.YoutubeTrack
|
import com.shabinder.common.models.YoutubeTrack
|
||||||
import com.willowtreeapps.fuzzywuzzy.diffutils.FuzzySearch
|
import com.willowtreeapps.fuzzywuzzy.diffutils.FuzzySearch
|
||||||
import io.ktor.client.*
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.headers
|
||||||
import io.ktor.http.*
|
import io.ktor.client.request.post
|
||||||
import kotlinx.serialization.json.*
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.contentType
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonArray
|
||||||
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.contentOrNull
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import kotlinx.serialization.json.putJsonObject
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
private const val apiKey = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"
|
private const val apiKey = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"
|
||||||
|
|
||||||
class YoutubeMusic constructor(
|
class YoutubeMusic constructor(
|
||||||
private val logger: Kermit,
|
private val logger: Kermit,
|
||||||
private val httpClient:HttpClient,
|
private val httpClient: HttpClient,
|
||||||
) {
|
) {
|
||||||
private val tag = "YT Music"
|
private val tag = "YT Music"
|
||||||
|
|
||||||
suspend fun getYTIDBestMatch(query: String,trackDetails: TrackDetails):String?{
|
suspend fun getYTIDBestMatch(query: String, trackDetails: TrackDetails): String? {
|
||||||
return sortByBestMatch(
|
return sortByBestMatch(
|
||||||
getYTTracks(query),
|
getYTTracks(query),
|
||||||
trackName = trackDetails.title,
|
trackName = trackDetails.title,
|
||||||
@ -43,7 +54,7 @@ class YoutubeMusic constructor(
|
|||||||
trackDurationSec = trackDetails.durationSec
|
trackDurationSec = trackDetails.durationSec
|
||||||
).keys.firstOrNull()
|
).keys.firstOrNull()
|
||||||
}
|
}
|
||||||
private 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))
|
||||||
@ -54,33 +65,35 @@ class YoutubeMusic constructor(
|
|||||||
|
|
||||||
val resultBlocks = mutableListOf<JsonArray>()
|
val resultBlocks = mutableListOf<JsonArray>()
|
||||||
if (contentBlocks != null) {
|
if (contentBlocks != null) {
|
||||||
for (cBlock in contentBlocks){
|
for (cBlock in contentBlocks) {
|
||||||
/**
|
/**
|
||||||
*Ignore user-suggestion
|
*Ignore user-suggestion
|
||||||
*The 'itemSectionRenderer' field is for user notices (stuff like - 'showing
|
*The 'itemSectionRenderer' field is for user notices (stuff like - 'showing
|
||||||
*results for xyz, search for abc instead') we have no use for them, the for
|
*results for xyz, search for abc instead') we have no use for them, the for
|
||||||
*loop below if throw a keyError if we don't ignore them
|
*loop below if throw a keyError if we don't ignore them
|
||||||
*/
|
*/
|
||||||
if(cBlock.jsonObject.containsKey("itemSectionRenderer")){
|
if (cBlock.jsonObject.containsKey("itemSectionRenderer")) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for(contents in cBlock.jsonObject["musicShelfRenderer"]?.jsonObject?.get("contents")?.jsonArray
|
for (
|
||||||
?: listOf()){
|
contents in cBlock.jsonObject["musicShelfRenderer"]?.jsonObject?.get("contents")?.jsonArray
|
||||||
|
?: listOf()
|
||||||
|
) {
|
||||||
/**
|
/**
|
||||||
* apparently content Blocks without an 'overlay' field don't have linkBlocks
|
* apparently content Blocks without an 'overlay' field don't have linkBlocks
|
||||||
* I have no clue what they are and why there even exist
|
* I have no clue what they are and why there even exist
|
||||||
*
|
*
|
||||||
if(!contents.containsKey("overlay")){
|
if(!contents.containsKey("overlay")){
|
||||||
println(contents)
|
println(contents)
|
||||||
continue
|
continue
|
||||||
TODO check and correct
|
TODO check and correct
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
val result = contents.jsonObject["musicResponsiveListItemRenderer"]
|
val result = contents.jsonObject["musicResponsiveListItemRenderer"]
|
||||||
?.jsonObject?.get("flexColumns")?.jsonArray
|
?.jsonObject?.get("flexColumns")?.jsonArray
|
||||||
|
|
||||||
//Add the linkBlock
|
// Add the linkBlock
|
||||||
val linkBlock = contents.jsonObject["musicResponsiveListItemRenderer"]
|
val linkBlock = contents.jsonObject["musicResponsiveListItemRenderer"]
|
||||||
?.jsonObject?.get("overlay")
|
?.jsonObject?.get("overlay")
|
||||||
?.jsonObject?.get("musicItemThumbnailOverlayRenderer")
|
?.jsonObject?.get("musicItemThumbnailOverlayRenderer")
|
||||||
@ -122,7 +135,7 @@ class YoutubeMusic constructor(
|
|||||||
! we do so only if their Type is 'Song' or 'Video
|
! we do so only if their Type is 'Song' or 'Video
|
||||||
*/
|
*/
|
||||||
|
|
||||||
for(result in resultBlocks){
|
for (result in resultBlocks) {
|
||||||
|
|
||||||
// Blindly gather available details
|
// Blindly gather available details
|
||||||
val availableDetails = mutableListOf<String>()
|
val availableDetails = mutableListOf<String>()
|
||||||
@ -137,33 +150,33 @@ 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(detailArray in result.subList(0,result.size-1)){
|
for (detailArray in result.subList(0, result.size - 1)) {
|
||||||
for(detail in detailArray.jsonArray){
|
for (detail in detailArray.jsonArray) {
|
||||||
if(detail.jsonObject["musicResponsiveListItemFlexColumnRenderer"]?.jsonObject?.size?:0 < 2) continue
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//logger.d("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
|
||||||
*/
|
*/
|
||||||
if ( availableDetails.size == 5 && availableDetails[1] in listOf("Song","Video") ){
|
if (availableDetails.size == 5 && availableDetails[1] in listOf("Song", "Video")) {
|
||||||
|
|
||||||
// skip if result is in hours instead of minutes (no song is that long)
|
// skip if result is in hours instead of minutes (no song is that long)
|
||||||
if(availableDetails[4].split(':').size != 2) continue
|
if (availableDetails[4].split(':').size != 2) continue
|
||||||
|
|
||||||
/*
|
/*
|
||||||
! grab Video ID
|
! grab Video ID
|
||||||
@ -173,7 +186,7 @@ class YoutubeMusic constructor(
|
|||||||
! reference the dict keys by index
|
! reference the dict keys by index
|
||||||
*/
|
*/
|
||||||
|
|
||||||
val videoId:String? = result.last().jsonObject["watchEndpoint"]?.jsonObject?.get("videoId")?.jsonPrimitive?.content
|
val videoId: String? = result.last().jsonObject["watchEndpoint"]?.jsonObject?.get("videoId")?.jsonPrimitive?.content
|
||||||
val ytTrack = YoutubeTrack(
|
val ytTrack = YoutubeTrack(
|
||||||
name = availableDetails[0],
|
name = availableDetails[0],
|
||||||
type = availableDetails[1],
|
type = availableDetails[1],
|
||||||
@ -185,64 +198,63 @@ class YoutubeMusic constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//logger.d {youtubeTracks.joinToString("\n")}
|
// logger.d {youtubeTracks.joinToString("\n")}
|
||||||
return youtubeTracks
|
return youtubeTracks
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sortByBestMatch(
|
private fun sortByBestMatch(
|
||||||
ytTracks:List<YoutubeTrack>,
|
ytTracks: List<YoutubeTrack>,
|
||||||
trackName:String,
|
trackName: String,
|
||||||
trackArtists:List<String>,
|
trackArtists: List<String>,
|
||||||
trackDurationSec:Int,
|
trackDurationSec: Int,
|
||||||
):Map<String,Int>{
|
): Map<String, Int> {
|
||||||
/*
|
/*
|
||||||
* "linksWithMatchValue" is map with Youtube VideoID and its rating/match with 100 as Max Value
|
* "linksWithMatchValue" is map with Youtube VideoID and its rating/match with 100 as Max Value
|
||||||
**/
|
**/
|
||||||
val linksWithMatchValue = mutableMapOf<String,Int>()
|
val linksWithMatchValue = mutableMapOf<String, Int>()
|
||||||
|
|
||||||
for (result in ytTracks){
|
for (result in ytTracks) {
|
||||||
|
|
||||||
// LoweCasing Name to match Properly
|
// LoweCasing Name to match Properly
|
||||||
// most song results on youtube go by $artist - $songName or artist1/artist2
|
// most song results on youtube go by $artist - $songName or artist1/artist2
|
||||||
var hasCommonWord = false
|
var hasCommonWord = false
|
||||||
|
|
||||||
val resultName = result.name?.toLowerCase()?.replace("-"," ")?.replace("/"," ") ?: ""
|
val resultName = result.name?.toLowerCase()?.replace("-", " ")?.replace("/", " ") ?: ""
|
||||||
val trackNameWords = trackName.toLowerCase().split(" ")
|
val trackNameWords = trackName.toLowerCase().split(" ")
|
||||||
|
|
||||||
for (nameWord in trackNameWords){
|
for (nameWord in trackNameWords) {
|
||||||
if (nameWord.isNotBlank() && FuzzySearch.partialRatio(nameWord,resultName) > 85) hasCommonWord = true
|
if (nameWord.isNotBlank() && FuzzySearch.partialRatio(nameWord, resultName) > 85) hasCommonWord = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip this Result if No Word is Common in Name
|
// Skip this Result if No Word is Common in Name
|
||||||
if (!hasCommonWord) {
|
if (!hasCommonWord) {
|
||||||
//log("YT Api Removing", result.toString())
|
// log("YT Api Removing", result.toString())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Find artist match
|
// Find artist match
|
||||||
// Will Be Using Fuzzy Search Because YT Spelling might be mucked up
|
// Will Be Using Fuzzy Search Because YT Spelling might be mucked up
|
||||||
// match = (no of artist names in result) / (no. of artist names on spotify) * 100
|
// match = (no of artist names in result) / (no. of artist names on spotify) * 100
|
||||||
var artistMatchNumber = 0
|
var artistMatchNumber = 0
|
||||||
|
|
||||||
if(result.type == "Song"){
|
if (result.type == "Song") {
|
||||||
for (artist in trackArtists){
|
for (artist in trackArtists) {
|
||||||
if(FuzzySearch.ratio(artist.toLowerCase(),result.artist?.toLowerCase() ?: "") > 85)
|
if (FuzzySearch.ratio(artist.toLowerCase(), result.artist?.toLowerCase() ?: "") > 85)
|
||||||
artistMatchNumber++
|
artistMatchNumber++
|
||||||
}
|
}
|
||||||
}else{//i.e. is a Video
|
} else { // i.e. is a Video
|
||||||
for (artist in trackArtists) {
|
for (artist in trackArtists) {
|
||||||
if(FuzzySearch.partialRatio(artist.toLowerCase(),result.name?.toLowerCase() ?: "") > 85)
|
if (FuzzySearch.partialRatio(artist.toLowerCase(), result.name?.toLowerCase() ?: "") > 85)
|
||||||
artistMatchNumber++
|
artistMatchNumber++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(artistMatchNumber == 0) {
|
if (artistMatchNumber == 0) {
|
||||||
//logger.d{ "YT Api Removing: $result" }
|
// logger.d{ "YT Api Removing: $result" }
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
val artistMatch = (artistMatchNumber / trackArtists.size ) * 100
|
val artistMatch = (artistMatchNumber / trackArtists.size) * 100
|
||||||
|
|
||||||
// Duration Match
|
// Duration Match
|
||||||
/*! time match = 100 - (delta(duration)**2 / original duration * 100)
|
/*! time match = 100 - (delta(duration)**2 / original duration * 100)
|
||||||
@ -250,32 +262,34 @@ class YoutubeMusic constructor(
|
|||||||
! seconds, we need to amplify the delta if it is to have any meaningful impact
|
! seconds, we need to amplify the delta if it is to have any meaningful impact
|
||||||
! wen we calculate the avg match value*/
|
! wen we calculate the avg match value*/
|
||||||
val difference = result.duration?.split(":")?.get(0)?.toInt()?.times(60)
|
val difference = result.duration?.split(":")?.get(0)?.toInt()?.times(60)
|
||||||
?.plus(result.duration?.split(":")?.get(1)?.toInt()?:0)
|
?.plus(result.duration?.split(":")?.get(1)?.toInt() ?: 0)
|
||||||
?.minus(trackDurationSec)?.absoluteValue ?: 0
|
?.minus(trackDurationSec)?.absoluteValue ?: 0
|
||||||
val nonMatchValue :Float= ((difference*difference).toFloat()/trackDurationSec.toFloat())
|
val nonMatchValue: Float = ((difference * difference).toFloat() / trackDurationSec.toFloat())
|
||||||
val durationMatch = 100 - (nonMatchValue*100)
|
val durationMatch = 100 - (nonMatchValue * 100)
|
||||||
|
|
||||||
val avgMatch = (artistMatch + durationMatch)/2
|
val avgMatch = (artistMatch + durationMatch) / 2
|
||||||
linksWithMatchValue[result.videoId.toString()] = avgMatch.toInt()
|
linksWithMatchValue[result.videoId.toString()] = avgMatch.toInt()
|
||||||
}
|
}
|
||||||
//logger.d("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().also {
|
||||||
|
logger.d(tag) { "Match Found for $trackName - ${!it.isNullOrEmpty()}" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getYoutubeMusicResponse(query: String):String{
|
private suspend fun getYoutubeMusicResponse(query: String): String {
|
||||||
return httpClient.post("${corsApi}https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey"){
|
return httpClient.post("${corsApi}https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey") {
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
headers{
|
headers {
|
||||||
append("referer","https://music.youtube.com/search")
|
append("referer", "https://music.youtube.com/search")
|
||||||
}
|
}
|
||||||
body = buildJsonObject {
|
body = buildJsonObject {
|
||||||
putJsonObject("context"){
|
putJsonObject("context") {
|
||||||
putJsonObject("client"){
|
putJsonObject("client") {
|
||||||
put("clientName" ,"WEB_REMIX")
|
put("clientName", "WEB_REMIX")
|
||||||
put("clientVersion" ,"0.1")
|
put("clientVersion", "0.1")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
put("query",query)
|
put("query", query)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,17 +19,17 @@ package com.shabinder.common.di.spotify
|
|||||||
import com.shabinder.common.di.isInternetAvailable
|
import com.shabinder.common.di.isInternetAvailable
|
||||||
import com.shabinder.common.di.kotlinxSerializer
|
import com.shabinder.common.di.kotlinxSerializer
|
||||||
import com.shabinder.common.models.spotify.TokenData
|
import com.shabinder.common.models.spotify.TokenData
|
||||||
import io.ktor.client.*
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.features.auth.*
|
import io.ktor.client.features.auth.Auth
|
||||||
import io.ktor.client.features.auth.providers.*
|
import io.ktor.client.features.auth.providers.basic
|
||||||
import io.ktor.client.features.json.*
|
import io.ktor.client.features.json.JsonFeature
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.forms.FormDataContent
|
||||||
import io.ktor.client.request.forms.*
|
import io.ktor.client.request.post
|
||||||
import io.ktor.http.*
|
import io.ktor.http.Parameters
|
||||||
|
|
||||||
suspend fun authenticateSpotify(): TokenData? {
|
suspend fun authenticateSpotify(): TokenData? {
|
||||||
return if(isInternetAvailable) spotifyAuthClient.post("https://accounts.spotify.com/api/token"){
|
return if (isInternetAvailable) spotifyAuthClient.post("https://accounts.spotify.com/api/token") {
|
||||||
body = FormDataContent(Parameters.build { append("grant_type","client_credentials") })
|
body = FormDataContent(Parameters.build { append("grant_type", "client_credentials") })
|
||||||
} else null
|
} else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,16 +21,16 @@ import com.shabinder.common.models.spotify.Album
|
|||||||
import com.shabinder.common.models.spotify.PagingObjectPlaylistTrack
|
import com.shabinder.common.models.spotify.PagingObjectPlaylistTrack
|
||||||
import com.shabinder.common.models.spotify.Playlist
|
import com.shabinder.common.models.spotify.Playlist
|
||||||
import com.shabinder.common.models.spotify.Track
|
import com.shabinder.common.models.spotify.Track
|
||||||
import io.ktor.client.*
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.get
|
||||||
|
|
||||||
private val BASE_URL get() = "${corsApi}https://api.spotify.com/v1"
|
private val BASE_URL get() = "${corsApi}https://api.spotify.com/v1"
|
||||||
|
|
||||||
interface SpotifyRequests {
|
interface SpotifyRequests {
|
||||||
|
|
||||||
val httpClient:HttpClient
|
val httpClient: HttpClient
|
||||||
|
|
||||||
suspend fun authenticateSpotifyClient(override:Boolean = false):HttpClient?
|
suspend fun authenticateSpotifyClient(override: Boolean = false): HttpClient?
|
||||||
|
|
||||||
suspend fun getPlaylist(playlistID: String): Playlist {
|
suspend fun getPlaylist(playlistID: String): Playlist {
|
||||||
return httpClient.get("$BASE_URL/playlists/$playlistID")
|
return httpClient.get("$BASE_URL/playlists/$playlistID")
|
||||||
@ -48,7 +48,7 @@ interface SpotifyRequests {
|
|||||||
return httpClient.get("$BASE_URL/tracks/$id")
|
return httpClient.get("$BASE_URL/tracks/$id")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getEpisode(id: String?) : Track {
|
suspend fun getEpisode(id: String?): Track {
|
||||||
return httpClient.get("$BASE_URL/episodes/$id")
|
return httpClient.get("$BASE_URL/episodes/$id")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ interface SpotifyRequests {
|
|||||||
return httpClient.get("$BASE_URL/albums/$id")
|
return httpClient.get("$BASE_URL/albums/$id")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getResponse(url:String):String{
|
suspend fun getResponse(url: String): String {
|
||||||
return httpClient.get(url)
|
return httpClient.get(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -21,12 +21,19 @@ package com.shabinder.common.di.utils
|
|||||||
// implementation("org.jetbrains.kotlinx:atomicfu:0.14.4")
|
// implementation("org.jetbrains.kotlinx:atomicfu:0.14.4")
|
||||||
// Gist: https://gist.github.com/fluidsonic/ba32de21c156bbe8424c8d5fc20dcd8e
|
// Gist: https://gist.github.com/fluidsonic/ba32de21c156bbe8424c8d5fc20dcd8e
|
||||||
|
|
||||||
import io.ktor.utils.io.core.*
|
import io.ktor.utils.io.core.Closeable
|
||||||
import kotlinx.atomicfu.atomic
|
import kotlinx.atomicfu.atomic
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.channels.*
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.selects.*
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlin.coroutines.*
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.selects.select
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
class ParallelExecutor(
|
class ParallelExecutor(
|
||||||
parentContext: CoroutineContext,
|
parentContext: CoroutineContext,
|
||||||
@ -38,12 +45,10 @@ class ParallelExecutor(
|
|||||||
private val killQueue = Channel<Unit>(Channel.UNLIMITED)
|
private val killQueue = Channel<Unit>(Channel.UNLIMITED)
|
||||||
private val operationQueue = Channel<Operation<*>>(Channel.RENDEZVOUS)
|
private val operationQueue = Channel<Operation<*>>(Channel.RENDEZVOUS)
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
startOrStopProcessors(expectedCount = concurrentOperationLimit.value, actualCount = 0)
|
startOrStopProcessors(expectedCount = concurrentOperationLimit.value, actualCount = 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
if (!isClosed.compareAndSet(expect = false, update = true))
|
if (!isClosed.compareAndSet(expect = false, update = true))
|
||||||
return
|
return
|
||||||
@ -55,7 +60,6 @@ class ParallelExecutor(
|
|||||||
coroutineContext.cancel(cause)
|
coroutineContext.cancel(cause)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun CoroutineScope.launchProcessor() = launch {
|
private fun CoroutineScope.launchProcessor() = launch {
|
||||||
while (true) {
|
while (true) {
|
||||||
val operation = select<Operation<*>?> {
|
val operation = select<Operation<*>?> {
|
||||||
@ -67,7 +71,6 @@ class ParallelExecutor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
suspend fun <Result> execute(block: suspend () -> Result): Result =
|
suspend fun <Result> execute(block: suspend () -> Result): Result =
|
||||||
withContext(coroutineContext) {
|
withContext(coroutineContext) {
|
||||||
val operation = Operation(block)
|
val operation = Operation(block)
|
||||||
@ -76,7 +79,6 @@ class ParallelExecutor(
|
|||||||
operation.result.await()
|
operation.result.await()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TODO This launches all coroutines in advance even if they're never needed. Find a lazy way to do this.
|
// TODO This launches all coroutines in advance even if they're never needed. Find a lazy way to do this.
|
||||||
fun setConcurrentOperationLimit(limit: Int) {
|
fun setConcurrentOperationLimit(limit: Int) {
|
||||||
require(limit >= 1) { "'limit' must be greater than zero: $limit" }
|
require(limit >= 1) { "'limit' must be greater than zero: $limit" }
|
||||||
@ -85,7 +87,6 @@ class ParallelExecutor(
|
|||||||
startOrStopProcessors(expectedCount = limit, actualCount = concurrentOperationLimit.getAndSet(limit))
|
startOrStopProcessors(expectedCount = limit, actualCount = concurrentOperationLimit.getAndSet(limit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun startOrStopProcessors(expectedCount: Int, actualCount: Int) {
|
private fun startOrStopProcessors(expectedCount: Int, actualCount: Int) {
|
||||||
if (expectedCount == actualCount)
|
if (expectedCount == actualCount)
|
||||||
return
|
return
|
||||||
@ -105,7 +106,6 @@ class ParallelExecutor(
|
|||||||
repeat(-change) { killQueue.offer(Unit) }
|
repeat(-change) { killQueue.offer(Unit) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private class Operation<Result>(
|
private class Operation<Result>(
|
||||||
private val block: suspend () -> Result,
|
private val block: suspend () -> Result,
|
||||||
) {
|
) {
|
||||||
@ -114,12 +114,10 @@ class ParallelExecutor(
|
|||||||
|
|
||||||
val result: Deferred<Result> get() = _result
|
val result: Deferred<Result> get() = _result
|
||||||
|
|
||||||
|
|
||||||
suspend fun execute() {
|
suspend fun execute() {
|
||||||
try {
|
try {
|
||||||
_result.complete(block())
|
_result.complete(block())
|
||||||
}
|
} catch (e: Throwable) {
|
||||||
catch (e: Throwable) {
|
|
||||||
_result.completeExceptionally(e)
|
_result.completeExceptionally(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,6 @@
|
|||||||
|
|
||||||
package com.shabinder.common.di.utils
|
package com.shabinder.common.di.utils
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removing Illegal Chars from File Name
|
* Removing Illegal Chars from File Name
|
||||||
* **/
|
* **/
|
||||||
|
@ -17,10 +17,10 @@
|
|||||||
package com.shabinder.common.di.youtubeMp3
|
package com.shabinder.common.di.youtubeMp3
|
||||||
|
|
||||||
import com.shabinder.common.di.gaana.corsApi
|
import com.shabinder.common.di.gaana.corsApi
|
||||||
import io.ktor.client.*
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.forms.FormDataContent
|
||||||
import io.ktor.client.request.forms.*
|
import io.ktor.client.request.post
|
||||||
import io.ktor.http.*
|
import io.ktor.http.Parameters
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
|
||||||
@ -35,29 +35,33 @@ interface Yt1sMp3 {
|
|||||||
/*
|
/*
|
||||||
* Downloadable Mp3 Link for YT videoID.
|
* Downloadable Mp3 Link for YT videoID.
|
||||||
* */
|
* */
|
||||||
suspend fun getLinkFromYt1sMp3(videoID: String):String? =
|
suspend fun getLinkFromYt1sMp3(videoID: String): String? =
|
||||||
getConvertedMp3Link(videoID,getKey(videoID))?.get("dlink")?.jsonPrimitive?.toString()?.replace("\"", "")
|
getConvertedMp3Link(videoID, getKey(videoID))?.get("dlink")?.jsonPrimitive?.toString()?.replace("\"", "")
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* POST:https://yt1s.com/api/ajaxSearch/index
|
* POST:https://yt1s.com/api/ajaxSearch/index
|
||||||
* Body Form= q:yt video link ,vt:format=mp3
|
* Body Form= q:yt video link ,vt:format=mp3
|
||||||
* */
|
* */
|
||||||
private suspend fun getKey(videoID:String):String{
|
private suspend fun getKey(videoID: String): String {
|
||||||
val response:JsonObject? = httpClient.post("${corsApi}https://yt1s.com/api/ajaxSearch/index"){
|
val response: JsonObject? = httpClient.post("${corsApi}https://yt1s.com/api/ajaxSearch/index") {
|
||||||
body = FormDataContent(Parameters.build {
|
body = FormDataContent(
|
||||||
append("q","https://www.youtube.com/watch?v=$videoID")
|
Parameters.build {
|
||||||
append("vt","mp3")
|
append("q", "https://www.youtube.com/watch?v=$videoID")
|
||||||
})
|
append("vt", "mp3")
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return response?.get("kc")?.jsonPrimitive.toString()
|
return response?.get("kc")?.jsonPrimitive.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getConvertedMp3Link(videoID: String,key:String):JsonObject?{
|
private suspend fun getConvertedMp3Link(videoID: String, key: String): JsonObject? {
|
||||||
return httpClient.post("${corsApi}https://yt1s.com/api/ajaxConvert/convert"){
|
return httpClient.post("${corsApi}https://yt1s.com/api/ajaxConvert/convert") {
|
||||||
body = FormDataContent(Parameters.build {
|
body = FormDataContent(
|
||||||
append("vid", videoID)
|
Parameters.build {
|
||||||
append("k",key)
|
append("vid", videoID)
|
||||||
})
|
append("k", key)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -25,7 +25,7 @@ import com.shabinder.common.models.AllPlatforms
|
|||||||
import com.shabinder.common.models.DownloadResult
|
import com.shabinder.common.models.DownloadResult
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.head
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
@ -33,22 +33,22 @@ import kotlinx.coroutines.flow.collect
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
actual fun openPlatform(packageID:String, platformLink:String){
|
actual fun openPlatform(packageID: String, platformLink: String) {
|
||||||
//TODO
|
// TODO
|
||||||
}
|
}
|
||||||
actual val currentPlatform: AllPlatforms = AllPlatforms.Jvm
|
actual val currentPlatform: AllPlatforms = AllPlatforms.Jvm
|
||||||
|
|
||||||
actual val dispatcherIO = Dispatchers.IO
|
actual val dispatcherIO = Dispatchers.IO
|
||||||
|
|
||||||
actual fun shareApp(){
|
actual fun shareApp() {
|
||||||
//TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun giveDonation(){
|
actual fun giveDonation() {
|
||||||
//TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun queryActiveTracks(){}
|
actual fun queryActiveTracks() {}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Refactor This
|
* Refactor This
|
||||||
@ -65,36 +65,39 @@ private suspend fun isInternetAvailable(): Boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual val isInternetAvailable:Boolean
|
actual val isInternetAvailable: Boolean
|
||||||
get(){
|
get() {
|
||||||
var result = false
|
var result = false
|
||||||
val job = GlobalScope.launch { result = isInternetAvailable() }
|
val job = GlobalScope.launch { result = isInternetAvailable() }
|
||||||
while(job.isActive){}
|
while (job.isActive) {}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
val DownloadProgressFlow: MutableSharedFlow<HashMap<String,DownloadStatus>> = MutableSharedFlow(1)
|
val DownloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>> = MutableSharedFlow(1)
|
||||||
|
|
||||||
//Scope Allowing 4 Parallel Downloads
|
// Scope Allowing 4 Parallel Downloads
|
||||||
val DownloadScope = ParallelExecutor(Dispatchers.IO)
|
val DownloadScope = ParallelExecutor(Dispatchers.IO)
|
||||||
|
|
||||||
actual suspend fun downloadTracks(
|
actual suspend fun downloadTracks(
|
||||||
list: List<TrackDetails>,
|
list: List<TrackDetails>,
|
||||||
fetcher: FetchPlatformQueryResult,
|
fetcher: FetchPlatformQueryResult,
|
||||||
dir: Dir
|
dir: Dir
|
||||||
){
|
) {
|
||||||
list.forEach {
|
list.forEach {
|
||||||
DownloadScope.execute { // Send Download to Pool.
|
DownloadScope.execute { // Send Download to Pool.
|
||||||
if (!it.videoID.isNullOrBlank()) {//Video ID already known!
|
if (!it.videoID.isNullOrBlank()) { // Video ID already known!
|
||||||
downloadTrack(it.videoID!!, it,dir::saveFileWithMetadata)
|
downloadTrack(it.videoID!!, it, dir::saveFileWithMetadata)
|
||||||
} else {
|
} else {
|
||||||
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
|
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
|
||||||
val videoId = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery,it)
|
val videoId = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery, it)
|
||||||
if (videoId.isNullOrBlank()) {
|
if (videoId.isNullOrBlank()) {
|
||||||
DownloadProgressFlow.emit(DownloadProgressFlow.replayCache.getOrElse(0
|
DownloadProgressFlow.emit(
|
||||||
) { hashMapOf() }.apply { set(it.title,DownloadStatus.Failed) })
|
DownloadProgressFlow.replayCache.getOrElse(
|
||||||
} else {//Found Youtube Video ID
|
0
|
||||||
downloadTrack(videoId, it,dir::saveFileWithMetadata)
|
) { hashMapOf() }.apply { set(it.title, DownloadStatus.Failed) }
|
||||||
|
)
|
||||||
|
} else { // Found Youtube Video ID
|
||||||
|
downloadTrack(videoId, it, dir::saveFileWithMetadata)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,7 +109,7 @@ private val ytDownloader = YoutubeDownloader()
|
|||||||
suspend fun downloadTrack(
|
suspend fun downloadTrack(
|
||||||
videoID: String,
|
videoID: String,
|
||||||
trackDetails: TrackDetails,
|
trackDetails: TrackDetails,
|
||||||
saveFileWithMetaData:suspend (mp3ByteArray:ByteArray, trackDetails: TrackDetails) -> Unit
|
saveFileWithMetaData: suspend (mp3ByteArray: ByteArray, trackDetails: TrackDetails) -> Unit
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val audioData = ytDownloader.getVideo(videoID).getData()
|
val audioData = ytDownloader.getVideo(videoID).getData()
|
||||||
@ -114,28 +117,37 @@ suspend fun downloadTrack(
|
|||||||
audioData?.let { format ->
|
audioData?.let { format ->
|
||||||
val url: String = format.url()
|
val url: String = format.url()
|
||||||
downloadFile(url).collect {
|
downloadFile(url).collect {
|
||||||
when(it){
|
when (it) {
|
||||||
is DownloadResult.Error -> {
|
is DownloadResult.Error -> {
|
||||||
DownloadProgressFlow.emit(DownloadProgressFlow.replayCache.getOrElse(0
|
DownloadProgressFlow.emit(
|
||||||
) { hashMapOf() }.apply { set(trackDetails.title,DownloadStatus.Failed) })
|
DownloadProgressFlow.replayCache.getOrElse(
|
||||||
|
0
|
||||||
|
) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Failed) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is DownloadResult.Progress -> {
|
is DownloadResult.Progress -> {
|
||||||
DownloadProgressFlow.emit(DownloadProgressFlow.replayCache.getOrElse(0
|
DownloadProgressFlow.emit(
|
||||||
) { hashMapOf() }.apply { set(trackDetails.title,DownloadStatus.Downloading(it.progress)) })
|
DownloadProgressFlow.replayCache.getOrElse(
|
||||||
|
0
|
||||||
|
) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloading(it.progress)) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is DownloadResult.Success -> {//Todo clear map
|
is DownloadResult.Success -> { // Todo clear map
|
||||||
saveFileWithMetaData(it.byteArray,trackDetails)
|
saveFileWithMetaData(it.byteArray, trackDetails)
|
||||||
DownloadProgressFlow.emit(DownloadProgressFlow.replayCache.getOrElse(0
|
DownloadProgressFlow.emit(
|
||||||
) { hashMapOf() }.apply { set(trackDetails.title,DownloadStatus.Downloaded) })
|
DownloadProgressFlow.replayCache.getOrElse(
|
||||||
|
0
|
||||||
|
) { hashMapOf() }.apply { set(trackDetails.title, DownloadStatus.Downloaded) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}catch (e: java.lang.Exception){
|
} catch (e: java.lang.Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun YoutubeVideo.getData(): Format?{
|
fun YoutubeVideo.getData(): Format? {
|
||||||
return try {
|
return try {
|
||||||
findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
|
findAudioWithQuality(AudioQuality.medium)?.get(0) as Format
|
||||||
} catch (e: java.lang.IndexOutOfBoundsException) {
|
} catch (e: java.lang.IndexOutOfBoundsException) {
|
||||||
|
@ -25,6 +25,7 @@ import com.shabinder.database.Database
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.jetbrains.skija.Image
|
import org.jetbrains.skija.Image
|
||||||
import java.awt.image.BufferedImage
|
import java.awt.image.BufferedImage
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
@ -35,13 +36,10 @@ import java.net.HttpURLConnection
|
|||||||
import java.net.URL
|
import java.net.URL
|
||||||
import javax.imageio.ImageIO
|
import javax.imageio.ImageIO
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
actual class Dir actual constructor(
|
actual class Dir actual constructor(
|
||||||
private val logger: Kermit,
|
private val logger: Kermit,
|
||||||
private val database: Database?,
|
private val database: Database?,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
createDirectories()
|
createDirectories()
|
||||||
@ -50,38 +48,33 @@ actual class Dir actual constructor(
|
|||||||
actual fun fileSeparator(): String = File.separator
|
actual fun fileSeparator(): String = File.separator
|
||||||
|
|
||||||
actual fun imageCacheDir(): String = System.getProperty("user.home") +
|
actual fun imageCacheDir(): String = System.getProperty("user.home") +
|
||||||
fileSeparator() + "SpotiFlyer/.images" + fileSeparator()
|
fileSeparator() + "SpotiFlyer/.images" + fileSeparator()
|
||||||
|
|
||||||
actual fun defaultDir(): String = System.getProperty("user.home") + fileSeparator() +
|
actual fun defaultDir(): String = System.getProperty("user.home") + fileSeparator() +
|
||||||
"SpotiFlyer" + fileSeparator()
|
"SpotiFlyer" + fileSeparator()
|
||||||
|
|
||||||
actual fun isPresent(path: String): Boolean = File(path).exists()
|
actual fun isPresent(path: String): Boolean = File(path).exists()
|
||||||
|
|
||||||
actual fun createDirectory(dirPath:String){
|
actual fun createDirectory(dirPath: String) {
|
||||||
val yourAppDir = File(dirPath)
|
val yourAppDir = File(dirPath)
|
||||||
|
|
||||||
if(!yourAppDir.exists() && !yourAppDir.isDirectory)
|
if (!yourAppDir.exists() && !yourAppDir.isDirectory) { // create empty directory
|
||||||
{ // create empty directory
|
if (yourAppDir.mkdirs()) { logger.i { "$dirPath created" } } else {
|
||||||
if (yourAppDir.mkdirs())
|
logger.e { "Unable to create Dir: $dirPath!" }
|
||||||
{logger.i{"$dirPath created"}}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
logger.e{"Unable to create Dir: $dirPath!"}
|
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
logger.i { "$dirPath already exists" }
|
logger.i { "$dirPath already exists" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual suspend fun clearCache() {
|
actual suspend fun clearCache() {
|
||||||
File(imageCacheDir()). deleteRecursively()
|
File(imageCacheDir()).deleteRecursively()
|
||||||
}
|
}
|
||||||
|
|
||||||
actual suspend fun cacheImage(image: Any,path:String) {
|
actual suspend fun cacheImage(image: Any, path: String) {
|
||||||
try {
|
try {
|
||||||
(image as? BufferedImage)?.let {
|
(image as? BufferedImage)?.let {
|
||||||
ImageIO.write(it,"jpeg", File(path))
|
ImageIO.write(it, "jpeg", File(path))
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@ -89,9 +82,9 @@ actual class Dir actual constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
actual suspend fun saveFileWithMetadata(
|
actual suspend fun saveFileWithMetadata(
|
||||||
mp3ByteArray: ByteArray,
|
mp3ByteArray: ByteArray,
|
||||||
trackDetails: TrackDetails
|
trackDetails: TrackDetails
|
||||||
) {
|
) {
|
||||||
val file = File(trackDetails.outputFilePath)
|
val file = File(trackDetails.outputFilePath)
|
||||||
file.writeBytes(mp3ByteArray)
|
file.writeBytes(mp3ByteArray)
|
||||||
@ -101,7 +94,7 @@ actual class Dir actual constructor(
|
|||||||
.setId3v1Tags(trackDetails)
|
.setId3v1Tags(trackDetails)
|
||||||
.setId3v2TagsAndSaveFile(trackDetails)
|
.setId3v2TagsAndSaveFile(trackDetails)
|
||||||
}
|
}
|
||||||
actual fun addToLibrary(path:String){}
|
actual fun addToLibrary(path: String) {}
|
||||||
|
|
||||||
actual suspend fun loadImage(url: String): Picture {
|
actual suspend fun loadImage(url: String): Picture {
|
||||||
val cachePath = imageCacheDir() + getNameURL(url)
|
val cachePath = imageCacheDir() + getNameURL(url)
|
||||||
@ -114,30 +107,33 @@ actual class Dir actual constructor(
|
|||||||
return try {
|
return try {
|
||||||
ImageIO.read(File(cachePath))?.toImageBitmap()
|
ImageIO.read(File(cachePath))?.toImageBitmap()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
//e.printStackTrace()
|
// e.printStackTrace()
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun freshImage(url:String): ImageBitmap?{
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
return try {
|
private suspend fun freshImage(url: String): ImageBitmap? {
|
||||||
val source = URL(url)
|
return withContext(Dispatchers.IO) {
|
||||||
val connection: HttpURLConnection = source.openConnection() as HttpURLConnection
|
try {
|
||||||
connection.connectTimeout = 5000
|
val source = URL(url)
|
||||||
connection.connect()
|
val connection: HttpURLConnection = source.openConnection() as HttpURLConnection
|
||||||
|
connection.connectTimeout = 5000
|
||||||
|
connection.connect()
|
||||||
|
|
||||||
val input: InputStream = connection.inputStream
|
val input: InputStream = connection.inputStream
|
||||||
val result: BufferedImage? = ImageIO.read(input)
|
val result: BufferedImage? = ImageIO.read(input)
|
||||||
|
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
GlobalScope.launch(Dispatchers.IO) { //TODO Refactor
|
GlobalScope.launch(Dispatchers.IO) { // TODO Refactor
|
||||||
cacheImage(result,imageCacheDir() + getNameURL(url))
|
cacheImage(result, imageCacheDir() + getNameURL(url))
|
||||||
}
|
}
|
||||||
result.toImageBitmap()
|
result.toImageBitmap()
|
||||||
} else null
|
} else null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
null
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,7 +144,7 @@ fun BufferedImage.toImageBitmap() = Image.makeFromEncoded(
|
|||||||
toByteArray(this)
|
toByteArray(this)
|
||||||
).asImageBitmap()
|
).asImageBitmap()
|
||||||
|
|
||||||
private fun toByteArray(bitmap: BufferedImage) : ByteArray {
|
private fun toByteArray(bitmap: BufferedImage): ByteArray {
|
||||||
val baOs = ByteArrayOutputStream()
|
val baOs = ByteArrayOutputStream()
|
||||||
ImageIO.write(bitmap, "png", baOs)
|
ImageIO.write(bitmap, "png", baOs)
|
||||||
return baOs.toByteArray()
|
return baOs.toByteArray()
|
||||||
|
@ -32,7 +32,6 @@ fun Mp3File.removeAllTags(): Mp3File {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modifying Mp3 with MetaData!
|
* Modifying Mp3 with MetaData!
|
||||||
**/
|
**/
|
||||||
@ -49,7 +48,7 @@ fun Mp3File.setId3v1Tags(track: TrackDetails): Mp3File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails){
|
suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) {
|
||||||
val id3v2Tag = ID3v24Tag().apply {
|
val id3v2Tag = ID3v24Tag().apply {
|
||||||
artist = track.artists.joinToString(",")
|
artist = track.artists.joinToString(",")
|
||||||
title = track.title
|
title = track.title
|
||||||
@ -59,37 +58,37 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails){
|
|||||||
lyrics = "Gonna Implement Soon"
|
lyrics = "Gonna Implement Soon"
|
||||||
url = track.trackUrl
|
url = track.trackUrl
|
||||||
}
|
}
|
||||||
try{
|
try {
|
||||||
val art = File(track.albumArtPath)
|
val art = File(track.albumArtPath)
|
||||||
val bytesArray = ByteArray(art.length().toInt())
|
val bytesArray = ByteArray(art.length().toInt())
|
||||||
val fis = FileInputStream(art)
|
val fis = FileInputStream(art)
|
||||||
fis.read(bytesArray) //read file into bytes[]
|
fis.read(bytesArray) // read file into bytes[]
|
||||||
fis.close()
|
fis.close()
|
||||||
id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
|
id3v2Tag.setAlbumImage(bytesArray, "image/jpeg")
|
||||||
this.id3v2Tag = id3v2Tag
|
this.id3v2Tag = id3v2Tag
|
||||||
saveFile(track.outputFilePath)
|
saveFile(track.outputFilePath)
|
||||||
}catch (e: java.io.FileNotFoundException){
|
} catch (e: java.io.FileNotFoundException) {
|
||||||
try {
|
try {
|
||||||
//Image Still Not Downloaded!
|
// Image Still Not Downloaded!
|
||||||
//Lets Download Now and Write it into Album Art
|
// Lets Download Now and Write it into Album Art
|
||||||
downloadFile(track.albumArtURL).collect {
|
downloadFile(track.albumArtURL).collect {
|
||||||
when(it){
|
when (it) {
|
||||||
is DownloadResult.Error -> {}//Error
|
is DownloadResult.Error -> {} // Error
|
||||||
is DownloadResult.Success -> {
|
is DownloadResult.Success -> {
|
||||||
id3v2Tag.setAlbumImage(it.byteArray, "image/jpeg")
|
id3v2Tag.setAlbumImage(it.byteArray, "image/jpeg")
|
||||||
this.id3v2Tag = id3v2Tag
|
this.id3v2Tag = id3v2Tag
|
||||||
saveFile(track.outputFilePath)
|
saveFile(track.outputFilePath)
|
||||||
}
|
}
|
||||||
is DownloadResult.Progress -> {}//Nothing for Now , no progress bar to show
|
is DownloadResult.Progress -> {} // Nothing for Now , no progress bar to show
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}catch (e: Exception){
|
} catch (e: Exception) {
|
||||||
//log("Error", "Couldn't Write Mp3 Album Art, error: ${e.stackTrace}")
|
// log("Error", "Couldn't Write Mp3 Album Art, error: ${e.stackTrace}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Mp3File.saveFile(filePath: String){
|
fun Mp3File.saveFile(filePath: String) {
|
||||||
save(filePath.substringBeforeLast('.') + ".new.mp3")
|
save(filePath.substringBeforeLast('.') + ".new.mp3")
|
||||||
val m4aFile = File(filePath)
|
val m4aFile = File(filePath)
|
||||||
m4aFile.delete()
|
m4aFile.delete()
|
||||||
|
@ -23,13 +23,13 @@ import com.shabinder.common.models.DownloadStatus
|
|||||||
import com.shabinder.common.models.PlatformQueryResult
|
import com.shabinder.common.models.PlatformQueryResult
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.shabinder.common.models.spotify.Source
|
import com.shabinder.common.models.spotify.Source
|
||||||
import io.ktor.client.*
|
import io.ktor.client.HttpClient
|
||||||
|
|
||||||
actual class YoutubeProvider actual constructor(
|
actual class YoutubeProvider actual constructor(
|
||||||
private val httpClient: HttpClient,
|
private val httpClient: HttpClient,
|
||||||
private val logger: Kermit,
|
private val logger: Kermit,
|
||||||
private val dir: Dir,
|
private val dir: Dir,
|
||||||
){
|
) {
|
||||||
private val ytDownloader: YoutubeDownloader = YoutubeDownloader()
|
private val ytDownloader: YoutubeDownloader = YoutubeDownloader()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -41,34 +41,34 @@ actual class YoutubeProvider actual constructor(
|
|||||||
private val sampleDomain2 = "youtube.com"
|
private val sampleDomain2 = "youtube.com"
|
||||||
private val sampleDomain3 = "youtu.be"
|
private val sampleDomain3 = "youtu.be"
|
||||||
|
|
||||||
actual suspend fun query(fullLink: String): PlatformQueryResult?{
|
actual suspend fun query(fullLink: String): PlatformQueryResult? {
|
||||||
val link = fullLink.removePrefix("https://").removePrefix("http://")
|
val link = fullLink.removePrefix("https://").removePrefix("http://")
|
||||||
if(link.contains("playlist",true) || link.contains("list",true)){
|
if (link.contains("playlist", true) || link.contains("list", true)) {
|
||||||
// Given Link is of a Playlist
|
// Given Link is of a Playlist
|
||||||
logger.i{ link }
|
logger.i { link }
|
||||||
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&").substringBefore("?")
|
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&").substringBefore("?")
|
||||||
return getYTPlaylist(
|
return getYTPlaylist(
|
||||||
playlistId
|
playlistId
|
||||||
)
|
)
|
||||||
}else{//Given Link is of a Video
|
} else { // Given Link is of a Video
|
||||||
var searchId = "error"
|
var searchId = "error"
|
||||||
when{
|
when {
|
||||||
link.contains(sampleDomain1,true) -> {//Youtube Music
|
link.contains(sampleDomain1, true) -> { // Youtube Music
|
||||||
searchId = link.substringAfterLast("/","error").substringBefore("&").substringAfterLast("=")
|
searchId = link.substringAfterLast("/", "error").substringBefore("&").substringAfterLast("=")
|
||||||
}
|
}
|
||||||
link.contains(sampleDomain2,true) -> {//Standard Youtube Link
|
link.contains(sampleDomain2, true) -> { // Standard Youtube Link
|
||||||
searchId = link.substringAfterLast("=","error").substringBefore("&")
|
searchId = link.substringAfterLast("=", "error").substringBefore("&")
|
||||||
}
|
}
|
||||||
link.contains(sampleDomain3,true) -> {//Shortened Youtube Link
|
link.contains(sampleDomain3, true) -> { // Shortened Youtube Link
|
||||||
searchId = link.substringAfterLast("/","error").substringBefore("&")
|
searchId = link.substringAfterLast("/", "error").substringBefore("&")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return if(searchId != "error") {
|
return if (searchId != "error") {
|
||||||
getYTTrack(
|
getYTTrack(
|
||||||
searchId
|
searchId
|
||||||
)
|
)
|
||||||
}else{
|
} else {
|
||||||
logger.d{"Your Youtube Link is not of a Video!!"}
|
logger.d { "Your Youtube Link is not of a Video!!" }
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -76,7 +76,7 @@ actual class YoutubeProvider actual constructor(
|
|||||||
|
|
||||||
private suspend fun getYTPlaylist(
|
private suspend fun getYTPlaylist(
|
||||||
searchId: String
|
searchId: String
|
||||||
): PlatformQueryResult?{
|
): PlatformQueryResult? {
|
||||||
val result = PlatformQueryResult(
|
val result = PlatformQueryResult(
|
||||||
folderType = "",
|
folderType = "",
|
||||||
subFolder = "",
|
subFolder = "",
|
||||||
@ -94,7 +94,7 @@ actual class YoutubeProvider actual constructor(
|
|||||||
val videos = playlist.videos()
|
val videos = playlist.videos()
|
||||||
|
|
||||||
coverUrl = "https://i.ytimg.com/vi/${
|
coverUrl = "https://i.ytimg.com/vi/${
|
||||||
videos.firstOrNull()?.videoId()
|
videos.firstOrNull()?.videoId()
|
||||||
}/hqdefault.jpg"
|
}/hqdefault.jpg"
|
||||||
title = name
|
title = name
|
||||||
|
|
||||||
@ -108,11 +108,11 @@ actual class YoutubeProvider actual constructor(
|
|||||||
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
||||||
downloaded = if (dir.isPresent(
|
downloaded = if (dir.isPresent(
|
||||||
dir.finalOutputDir(
|
dir.finalOutputDir(
|
||||||
itemName = it.title(),
|
itemName = it.title(),
|
||||||
type = folderType,
|
type = folderType,
|
||||||
subFolder = subFolder,
|
subFolder = subFolder,
|
||||||
dir.defaultDir()
|
dir.defaultDir()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
DownloadStatus.Downloaded
|
DownloadStatus.Downloaded
|
||||||
@ -125,16 +125,16 @@ actual class YoutubeProvider actual constructor(
|
|||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
logger.d{"An Error Occurred While Processing!"}
|
logger.d { "An Error Occurred While Processing!" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return if(result.title.isNotBlank()) result
|
return if (result.title.isNotBlank()) result
|
||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DefaultLocale")
|
@Suppress("DefaultLocale")
|
||||||
private suspend fun getYTTrack(
|
private suspend fun getYTTrack(
|
||||||
searchId:String,
|
searchId: String,
|
||||||
): PlatformQueryResult? {
|
): PlatformQueryResult? {
|
||||||
val result = PlatformQueryResult(
|
val result = PlatformQueryResult(
|
||||||
folderType = "",
|
folderType = "",
|
||||||
@ -143,15 +143,15 @@ actual class YoutubeProvider actual constructor(
|
|||||||
coverUrl = "",
|
coverUrl = "",
|
||||||
trackList = listOf(),
|
trackList = listOf(),
|
||||||
Source.YouTube
|
Source.YouTube
|
||||||
).apply{
|
).apply {
|
||||||
try {
|
try {
|
||||||
logger.i{searchId}
|
logger.i { searchId }
|
||||||
val video = ytDownloader.getVideo(searchId)
|
val video = ytDownloader.getVideo(searchId)
|
||||||
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
||||||
val detail = video?.details()
|
val detail = video?.details()
|
||||||
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(), "", true)
|
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(), "", true)
|
||||||
?: detail?.title() ?: ""
|
?: detail?.title() ?: ""
|
||||||
//logger.i{ detail.toString() }
|
// logger.i{ detail.toString() }
|
||||||
trackList = listOf(
|
trackList = listOf(
|
||||||
TrackDetails(
|
TrackDetails(
|
||||||
title = name,
|
title = name,
|
||||||
@ -162,11 +162,11 @@ actual class YoutubeProvider actual constructor(
|
|||||||
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||||
downloaded = if (dir.isPresent(
|
downloaded = if (dir.isPresent(
|
||||||
dir.finalOutputDir(
|
dir.finalOutputDir(
|
||||||
itemName = name,
|
itemName = name,
|
||||||
type = folderType,
|
type = folderType,
|
||||||
subFolder = subFolder,
|
subFolder = subFolder,
|
||||||
defaultDir = dir.defaultDir()
|
defaultDir = dir.defaultDir()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
DownloadStatus.Downloaded
|
DownloadStatus.Downloaded
|
||||||
@ -180,10 +180,10 @@ actual class YoutubeProvider actual constructor(
|
|||||||
title = name
|
title = name
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
logger.e{"An Error Occurred While Processing!,$searchId"}
|
logger.e { "An Error Occurred While Processing!,$searchId" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return if(result.title.isNotBlank()) result
|
return if (result.title.isNotBlank()) result
|
||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -22,10 +22,10 @@ import org.w3c.files.Blob
|
|||||||
@JsModule("browser-id3-writer")
|
@JsModule("browser-id3-writer")
|
||||||
@JsNonModule
|
@JsNonModule
|
||||||
external class ID3Writer(a: ArrayBuffer) {
|
external class ID3Writer(a: ArrayBuffer) {
|
||||||
fun setFrame(frameName:String,frameValue:Any):ID3Writer
|
fun setFrame(frameName: String, frameValue: Any): ID3Writer
|
||||||
fun removeTag()
|
fun removeTag()
|
||||||
fun addTag():ArrayBuffer
|
fun addTag(): ArrayBuffer
|
||||||
fun getBlob():Blob
|
fun getBlob(): Blob
|
||||||
fun getURL():String
|
fun getURL(): String
|
||||||
fun revokeURL()
|
fun revokeURL()
|
||||||
}
|
}
|
@ -20,26 +20,28 @@ import com.shabinder.common.models.AllPlatforms
|
|||||||
import com.shabinder.common.models.DownloadResult
|
import com.shabinder.common.models.DownloadResult
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.head
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
actual val currentPlatform:AllPlatforms = AllPlatforms.Js
|
actual val currentPlatform: AllPlatforms = AllPlatforms.Js
|
||||||
|
|
||||||
actual fun openPlatform(packageID:String, platformLink:String){
|
actual fun openPlatform(packageID: String, platformLink: String) {
|
||||||
//TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun shareApp(){
|
actual fun shareApp() {
|
||||||
//TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun giveDonation(){
|
actual fun giveDonation() {
|
||||||
//TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun queryActiveTracks(){}
|
actual fun queryActiveTracks() {}
|
||||||
|
|
||||||
actual val dispatcherIO: CoroutineDispatcher = Dispatchers.Default
|
actual val dispatcherIO: CoroutineDispatcher = Dispatchers.Default
|
||||||
|
|
||||||
@ -58,8 +60,8 @@ private suspend fun isInternetAvailable(): Boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual val isInternetAvailable:Boolean
|
actual val isInternetAvailable: Boolean
|
||||||
get(){
|
get() {
|
||||||
return true
|
return true
|
||||||
/*var result = false
|
/*var result = false
|
||||||
val job = GlobalScope.launch { result = isInternetAvailable() }
|
val job = GlobalScope.launch { result = isInternetAvailable() }
|
||||||
@ -68,28 +70,28 @@ actual val isInternetAvailable:Boolean
|
|||||||
}
|
}
|
||||||
|
|
||||||
val DownloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>> = MutableSharedFlow(1)
|
val DownloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>> = MutableSharedFlow(1)
|
||||||
//Error:https://github.com/Kotlin/kotlinx.atomicfu/issues/182
|
// Error:https://github.com/Kotlin/kotlinx.atomicfu/issues/182
|
||||||
//val DownloadScope = ParallelExecutor(Dispatchers.Default) //Download Pool of 4 parallel
|
// val DownloadScope = ParallelExecutor(Dispatchers.Default) //Download Pool of 4 parallel
|
||||||
val allTracksStatus: HashMap<String, DownloadStatus> = hashMapOf()
|
val allTracksStatus: HashMap<String, DownloadStatus> = hashMapOf()
|
||||||
|
|
||||||
actual suspend fun downloadTracks(
|
actual suspend fun downloadTracks(
|
||||||
list: List<TrackDetails>,
|
list: List<TrackDetails>,
|
||||||
fetcher: FetchPlatformQueryResult,
|
fetcher: FetchPlatformQueryResult,
|
||||||
dir: Dir
|
dir: Dir
|
||||||
){
|
) {
|
||||||
list.forEach {
|
list.forEach {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(dispatcherIO) {
|
||||||
allTracksStatus[it.title] = DownloadStatus.Queued
|
allTracksStatus[it.title] = DownloadStatus.Queued
|
||||||
if (!it.videoID.isNullOrBlank()) {//Video ID already known!
|
if (!it.videoID.isNullOrBlank()) { // Video ID already known!
|
||||||
downloadTrack(it.videoID!!, it, fetcher, dir)
|
downloadTrack(it.videoID!!, it, fetcher, dir)
|
||||||
} else {
|
} else {
|
||||||
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
|
val searchQuery = "${it.title} - ${it.artists.joinToString(",")}"
|
||||||
val videoID = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery,it)
|
val videoID = fetcher.youtubeMusic.getYTIDBestMatch(searchQuery, it)
|
||||||
println(videoID+" : "+it.title)
|
println(videoID + " : " + it.title)
|
||||||
if (videoID.isNullOrBlank()) {
|
if (videoID.isNullOrBlank()) {
|
||||||
allTracksStatus[it.title] = DownloadStatus.Failed
|
allTracksStatus[it.title] = DownloadStatus.Failed
|
||||||
DownloadProgressFlow.emit(allTracksStatus)
|
DownloadProgressFlow.emit(allTracksStatus)
|
||||||
} else {//Found Youtube Video ID
|
} else { // Found Youtube Video ID
|
||||||
downloadTrack(videoID, it, fetcher, dir)
|
downloadTrack(videoID, it, fetcher, dir)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,18 +100,18 @@ actual suspend fun downloadTracks(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun downloadTrack(videoID: String, track: TrackDetails, fetcher:FetchPlatformQueryResult,dir:Dir) {
|
suspend fun downloadTrack(videoID: String, track: TrackDetails, fetcher: FetchPlatformQueryResult, dir: Dir) {
|
||||||
val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID)
|
val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID)
|
||||||
if(url == null){
|
if (url == null) {
|
||||||
allTracksStatus[track.title] = DownloadStatus.Failed
|
allTracksStatus[track.title] = DownloadStatus.Failed
|
||||||
DownloadProgressFlow.emit(allTracksStatus)
|
DownloadProgressFlow.emit(allTracksStatus)
|
||||||
println("No URL to Download")
|
println("No URL to Download")
|
||||||
}else {
|
} else {
|
||||||
downloadFile(url).collect {
|
downloadFile(url).collect {
|
||||||
when(it){
|
when (it) {
|
||||||
is DownloadResult.Success -> {
|
is DownloadResult.Success -> {
|
||||||
println("Download Completed")
|
println("Download Completed")
|
||||||
dir.saveFileWithMetadata(it.byteArray, track)
|
dir.saveFileWithMetadata(it.byteArray, track)
|
||||||
}
|
}
|
||||||
is DownloadResult.Error -> {
|
is DownloadResult.Error -> {
|
||||||
allTracksStatus[track.title] = DownloadStatus.Failed
|
allTracksStatus[track.title] = DownloadStatus.Failed
|
||||||
|
@ -27,8 +27,8 @@ import kotlinext.js.Object
|
|||||||
import kotlinext.js.js
|
import kotlinext.js.js
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import org.khronos.webgl.ArrayBuffer
|
import org.khronos.webgl.ArrayBuffer
|
||||||
import org.w3c.dom.ImageBitmap
|
|
||||||
import org.khronos.webgl.Int8Array
|
import org.khronos.webgl.Int8Array
|
||||||
|
import org.w3c.dom.ImageBitmap
|
||||||
|
|
||||||
actual class Dir actual constructor(
|
actual class Dir actual constructor(
|
||||||
private val logger: Kermit,
|
private val logger: Kermit,
|
||||||
@ -45,52 +45,52 @@ actual class Dir actual constructor(
|
|||||||
actual fun fileSeparator(): String = "/"
|
actual fun fileSeparator(): String = "/"
|
||||||
|
|
||||||
actual fun imageCacheDir(): String = "TODO" +
|
actual fun imageCacheDir(): String = "TODO" +
|
||||||
fileSeparator() + "SpotiFlyer/.images" + fileSeparator()
|
fileSeparator() + "SpotiFlyer/.images" + fileSeparator()
|
||||||
|
|
||||||
actual fun defaultDir(): String = "TODO" + fileSeparator() +
|
actual fun defaultDir(): String = "TODO" + fileSeparator() +
|
||||||
"SpotiFlyer" + fileSeparator()
|
"SpotiFlyer" + fileSeparator()
|
||||||
|
|
||||||
actual fun isPresent(path: String): Boolean = false
|
actual fun isPresent(path: String): Boolean = false
|
||||||
|
|
||||||
actual fun createDirectory(dirPath:String){}
|
actual fun createDirectory(dirPath: String) {}
|
||||||
|
|
||||||
actual suspend fun clearCache() {}
|
actual suspend fun clearCache() {}
|
||||||
|
|
||||||
actual suspend fun cacheImage(image: Any,path:String) {}
|
actual suspend fun cacheImage(image: Any, path: String) {}
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
actual suspend fun saveFileWithMetadata(
|
actual suspend fun saveFileWithMetadata(
|
||||||
mp3ByteArray: ByteArray,
|
mp3ByteArray: ByteArray,
|
||||||
trackDetails: TrackDetails
|
trackDetails: TrackDetails
|
||||||
) {
|
) {
|
||||||
val writer = ID3Writer(mp3ByteArray.toArrayBuffer())
|
val writer = ID3Writer(mp3ByteArray.toArrayBuffer())
|
||||||
val albumArt = downloadFile(corsApi+trackDetails.albumArtURL)
|
val albumArt = downloadFile(corsApi + trackDetails.albumArtURL)
|
||||||
albumArt.collect {
|
albumArt.collect {
|
||||||
when(it){
|
when (it) {
|
||||||
is DownloadResult.Success -> {
|
is DownloadResult.Success -> {
|
||||||
logger.d{"Album Art Downloaded Success"}
|
logger.d { "Album Art Downloaded Success" }
|
||||||
val albumArtObj = js {
|
val albumArtObj = js {
|
||||||
this["type"] = 3
|
this["type"] = 3
|
||||||
this["data"] = it.byteArray.toArrayBuffer()
|
this["data"] = it.byteArray.toArrayBuffer()
|
||||||
this["description"] = "Cover Art"
|
this["description"] = "Cover Art"
|
||||||
}
|
}
|
||||||
writeTagsAndSave(writer, albumArtObj as Object,trackDetails)
|
writeTagsAndSave(writer, albumArtObj as Object, trackDetails)
|
||||||
}
|
}
|
||||||
is DownloadResult.Error -> {
|
is DownloadResult.Error -> {
|
||||||
logger.d{"Album Art Downloading Error"}
|
logger.d { "Album Art Downloading Error" }
|
||||||
writeTagsAndSave(writer,null,trackDetails)
|
writeTagsAndSave(writer, null, trackDetails)
|
||||||
}
|
}
|
||||||
is DownloadResult.Progress -> logger.d{"Album Art Downloading: ${it.progress}"}
|
is DownloadResult.Progress -> logger.d { "Album Art Downloading: ${it.progress}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun writeTagsAndSave(writer:ID3Writer, albumArt:Object?, trackDetails: TrackDetails){
|
private suspend fun writeTagsAndSave(writer: ID3Writer, albumArt: Object?, trackDetails: TrackDetails) {
|
||||||
writer.apply {
|
writer.apply {
|
||||||
setFrame("TIT2", trackDetails.title)
|
setFrame("TIT2", trackDetails.title)
|
||||||
setFrame("TPE1", trackDetails.artists.toTypedArray())
|
setFrame("TPE1", trackDetails.artists.toTypedArray())
|
||||||
setFrame("TALB", trackDetails.albumName?:"")
|
setFrame("TALB", trackDetails.albumName ?: "")
|
||||||
try{trackDetails.year?.substring(0,4)?.toInt()?.let { setFrame("TYER", it) }} catch(e:Exception){}
|
try { trackDetails.year?.substring(0, 4)?.toInt()?.let { setFrame("TYER", it) } } catch (e: Exception) {}
|
||||||
setFrame("TPE2", trackDetails.artists.joinToString(","))
|
setFrame("TPE2", trackDetails.artists.joinToString(","))
|
||||||
setFrame("WOAS", trackDetails.source.toString())
|
setFrame("WOAS", trackDetails.source.toString())
|
||||||
setFrame("TLEN", trackDetails.durationSec)
|
setFrame("TLEN", trackDetails.durationSec)
|
||||||
@ -102,7 +102,7 @@ actual class Dir actual constructor(
|
|||||||
saveAs(writer.getBlob(), "${removeIllegalChars(trackDetails.title)}.mp3")
|
saveAs(writer.getBlob(), "${removeIllegalChars(trackDetails.title)}.mp3")
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun addToLibrary(path:String){}
|
actual fun addToLibrary(path: String) {}
|
||||||
|
|
||||||
actual suspend fun loadImage(url: String): Picture {
|
actual suspend fun loadImage(url: String): Picture {
|
||||||
return Picture(url)
|
return Picture(url)
|
||||||
@ -110,12 +110,12 @@ actual class Dir actual constructor(
|
|||||||
|
|
||||||
private fun loadCachedImage(cachePath: String): ImageBitmap? = null
|
private fun loadCachedImage(cachePath: String): ImageBitmap? = null
|
||||||
|
|
||||||
private suspend fun freshImage(url:String): ImageBitmap? = null
|
private suspend fun freshImage(url: String): ImageBitmap? = null
|
||||||
|
|
||||||
actual val db: Database?
|
actual val db: Database?
|
||||||
get() = database
|
get() = database
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ByteArray.toArrayBuffer():ArrayBuffer{
|
fun ByteArray.toArrayBuffer(): ArrayBuffer {
|
||||||
return this.unsafeCast<Int8Array>().buffer
|
return this.unsafeCast<Int8Array>().buffer
|
||||||
}
|
}
|
@ -17,5 +17,5 @@
|
|||||||
package com.shabinder.common.di
|
package com.shabinder.common.di
|
||||||
|
|
||||||
actual data class Picture(
|
actual data class Picture(
|
||||||
var imageUrl:String
|
var imageUrl: String
|
||||||
)
|
)
|
@ -18,8 +18,7 @@ package com.shabinder.common.di
|
|||||||
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.models.PlatformQueryResult
|
import com.shabinder.common.models.PlatformQueryResult
|
||||||
import com.shabinder.database.Database
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.*
|
|
||||||
|
|
||||||
actual class YoutubeProvider actual constructor(
|
actual class YoutubeProvider actual constructor(
|
||||||
httpClient: HttpClient,
|
httpClient: HttpClient,
|
||||||
|
@ -36,12 +36,12 @@ interface SpotiFlyerList {
|
|||||||
/*
|
/*
|
||||||
* Download All Tracks(after filtering already Downloaded)
|
* Download All Tracks(after filtering already Downloaded)
|
||||||
* */
|
* */
|
||||||
fun onDownloadAllClicked(trackList:List<TrackDetails>)
|
fun onDownloadAllClicked(trackList: List<TrackDetails>)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Download All Tracks(after filtering already Downloaded)
|
* Download All Tracks(after filtering already Downloaded)
|
||||||
* */
|
* */
|
||||||
fun onDownloadClicked(track:TrackDetails)
|
fun onDownloadClicked(track: TrackDetails)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* To Pop and return back to Main Screen
|
* To Pop and return back to Main Screen
|
||||||
@ -51,7 +51,7 @@ interface SpotiFlyerList {
|
|||||||
/*
|
/*
|
||||||
* Load Image from cache/Internet and cache it
|
* Load Image from cache/Internet and cache it
|
||||||
* */
|
* */
|
||||||
suspend fun loadImage(url:String): Picture
|
suspend fun loadImage(url: String): Picture
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Sync Tracks Statuses
|
* Sync Tracks Statuses
|
||||||
@ -64,7 +64,7 @@ interface SpotiFlyerList {
|
|||||||
val dir: Dir
|
val dir: Dir
|
||||||
val link: String
|
val link: String
|
||||||
val listOutput: Consumer<Output>
|
val listOutput: Consumer<Output>
|
||||||
val showPopUpMessage:(String)->Unit
|
val showPopUpMessage: (String) -> Unit
|
||||||
val downloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>>
|
val downloadProgressFlow: MutableSharedFlow<HashMap<String, DownloadStatus>>
|
||||||
}
|
}
|
||||||
sealed class Output {
|
sealed class Output {
|
||||||
@ -72,8 +72,8 @@ interface SpotiFlyerList {
|
|||||||
}
|
}
|
||||||
data class State(
|
data class State(
|
||||||
val queryResult: PlatformQueryResult? = null,
|
val queryResult: PlatformQueryResult? = null,
|
||||||
val link:String = "",
|
val link: String = "",
|
||||||
val trackList:List<TrackDetails> = emptyList()
|
val trackList: List<TrackDetails> = emptyList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
internal class SpotiFlyerListImpl(
|
internal class SpotiFlyerListImpl(
|
||||||
componentContext: ComponentContext,
|
componentContext: ComponentContext,
|
||||||
dependencies: Dependencies
|
dependencies: Dependencies
|
||||||
): SpotiFlyerList,ComponentContext by componentContext, Dependencies by dependencies {
|
) : SpotiFlyerList, ComponentContext by componentContext, Dependencies by dependencies {
|
||||||
|
|
||||||
private val store =
|
private val store =
|
||||||
instanceKeeper.getStore {
|
instanceKeeper.getStore {
|
||||||
@ -51,11 +51,11 @@ internal class SpotiFlyerListImpl(
|
|||||||
store.accept(Intent.StartDownloadAll(trackList))
|
store.accept(Intent.StartDownloadAll(trackList))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDownloadClicked(track:TrackDetails) {
|
override fun onDownloadClicked(track: TrackDetails) {
|
||||||
store.accept(Intent.StartDownload(track))
|
store.accept(Intent.StartDownload(track))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed(){
|
override fun onBackPressed() {
|
||||||
listOutput.callback(SpotiFlyerList.Output.Finished)
|
listOutput.callback(SpotiFlyerList.Output.Finished)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,9 +24,9 @@ fun <T : Store<*, *, *>> InstanceKeeper.getStore(key: Any, factory: () -> T): T
|
|||||||
getOrCreate(key) { StoreHolder(factory()) }
|
getOrCreate(key) { StoreHolder(factory()) }
|
||||||
.store
|
.store
|
||||||
|
|
||||||
inline fun <reified T
|
inline fun <reified T :
|
||||||
|
|
||||||
: Store<*, *, *>> InstanceKeeper.getStore(noinline factory: () -> T): T =
|
Store<*, *, *>> InstanceKeeper.getStore(noinline factory: () -> T): T =
|
||||||
getStore(T::class, factory)
|
getStore(T::class, factory)
|
||||||
|
|
||||||
private class StoreHolder<T : Store<*, *, *>>(
|
private class StoreHolder<T : Store<*, *, *>>(
|
||||||
|
@ -21,11 +21,11 @@ import com.shabinder.common.list.SpotiFlyerList.State
|
|||||||
import com.shabinder.common.list.store.SpotiFlyerListStore.Intent
|
import com.shabinder.common.list.store.SpotiFlyerListStore.Intent
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
|
|
||||||
internal interface SpotiFlyerListStore: Store<Intent, State, Nothing> {
|
internal interface SpotiFlyerListStore : Store<Intent, State, Nothing> {
|
||||||
sealed class Intent {
|
sealed class Intent {
|
||||||
data class SearchLink(val link: String): Intent()
|
data class SearchLink(val link: String) : Intent()
|
||||||
data class StartDownload(val track:TrackDetails): Intent()
|
data class StartDownload(val track: TrackDetails) : Intent()
|
||||||
data class StartDownloadAll(val trackList: List<TrackDetails>): Intent()
|
data class StartDownloadAll(val trackList: List<TrackDetails>) : Intent()
|
||||||
object RefreshTracksStatuses: Intent()
|
object RefreshTracksStatuses : Intent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,16 @@
|
|||||||
|
|
||||||
package com.shabinder.common.list.store
|
package com.shabinder.common.list.store
|
||||||
|
|
||||||
import com.arkivanov.mvikotlin.core.store.*
|
import com.arkivanov.mvikotlin.core.store.Reducer
|
||||||
|
import com.arkivanov.mvikotlin.core.store.SimpleBootstrapper
|
||||||
|
import com.arkivanov.mvikotlin.core.store.Store
|
||||||
|
import com.arkivanov.mvikotlin.core.store.StoreFactory
|
||||||
import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor
|
import com.arkivanov.mvikotlin.extensions.coroutines.SuspendExecutor
|
||||||
import com.shabinder.common.database.getLogger
|
import com.shabinder.common.database.getLogger
|
||||||
import com.shabinder.common.di.*
|
import com.shabinder.common.di.Dir
|
||||||
|
import com.shabinder.common.di.FetchPlatformQueryResult
|
||||||
|
import com.shabinder.common.di.downloadTracks
|
||||||
|
import com.shabinder.common.di.queryActiveTracks
|
||||||
import com.shabinder.common.list.SpotiFlyerList.State
|
import com.shabinder.common.list.SpotiFlyerList.State
|
||||||
import com.shabinder.common.list.store.SpotiFlyerListStore.Intent
|
import com.shabinder.common.list.store.SpotiFlyerListStore.Intent
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
@ -38,55 +44,57 @@ internal class SpotiFlyerListStoreProvider(
|
|||||||
) {
|
) {
|
||||||
val logger = getLogger()
|
val logger = getLogger()
|
||||||
fun provide(): SpotiFlyerListStore =
|
fun provide(): SpotiFlyerListStore =
|
||||||
object : SpotiFlyerListStore, Store<Intent, State, Nothing> by storeFactory.create(
|
object :
|
||||||
name = "SpotiFlyerListStore",
|
SpotiFlyerListStore,
|
||||||
initialState = State(),
|
Store<Intent, State, Nothing> by storeFactory.create(
|
||||||
bootstrapper = SimpleBootstrapper(Unit),
|
name = "SpotiFlyerListStore",
|
||||||
executorFactory = ::ExecutorImpl,
|
initialState = State(),
|
||||||
reducer = ReducerImpl
|
bootstrapper = SimpleBootstrapper(Unit),
|
||||||
) {}
|
executorFactory = ::ExecutorImpl,
|
||||||
|
reducer = ReducerImpl
|
||||||
|
) {}
|
||||||
|
|
||||||
private sealed class Result {
|
private sealed class Result {
|
||||||
data class ResultFetched(val result: PlatformQueryResult,val trackList: List<TrackDetails>) : Result()
|
data class ResultFetched(val result: PlatformQueryResult, val trackList: List<TrackDetails>) : Result()
|
||||||
data class UpdateTrackList(val list:List<TrackDetails>): Result()
|
data class UpdateTrackList(val list: List<TrackDetails>) : Result()
|
||||||
data class UpdateTrackItem(val item:TrackDetails): Result()
|
data class UpdateTrackItem(val item: TrackDetails) : Result()
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class ExecutorImpl: SuspendExecutor<Intent, Unit, State, Result, Nothing>() {
|
private inner class ExecutorImpl : SuspendExecutor<Intent, Unit, State, Result, Nothing>() {
|
||||||
|
|
||||||
override suspend fun executeAction(action: Unit, getState: () -> State) {
|
override suspend fun executeAction(action: Unit, getState: () -> State) {
|
||||||
executeIntent(Intent.SearchLink(link),getState)
|
executeIntent(Intent.SearchLink(link), getState)
|
||||||
|
|
||||||
downloadProgressFlow.collectLatest { map ->
|
downloadProgressFlow.collectLatest { map ->
|
||||||
logger.d(map.size.toString(),"ListStore: flow Updated")
|
logger.d(map.size.toString(), "ListStore: flow Updated")
|
||||||
val updatedTrackList = getState().trackList.updateTracksStatuses(map)
|
val updatedTrackList = getState().trackList.updateTracksStatuses(map)
|
||||||
if(updatedTrackList.isNotEmpty()) dispatch(Result.UpdateTrackList(updatedTrackList))
|
if (updatedTrackList.isNotEmpty()) dispatch(Result.UpdateTrackList(updatedTrackList))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun executeIntent(intent: Intent, getState: () -> State) {
|
override suspend fun executeIntent(intent: Intent, getState: () -> State) {
|
||||||
when (intent) {
|
when (intent) {
|
||||||
is Intent.SearchLink -> fetchQuery.query(link)?.let{ result ->
|
is Intent.SearchLink -> fetchQuery.query(link)?.let { result ->
|
||||||
result.trackList = result.trackList.toMutableList()
|
result.trackList = result.trackList.toMutableList()
|
||||||
dispatch((Result.ResultFetched(result,result.trackList.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0){ hashMapOf()}))))
|
dispatch((Result.ResultFetched(result, result.trackList.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() }))))
|
||||||
executeIntent(Intent.RefreshTracksStatuses,getState)
|
executeIntent(Intent.RefreshTracksStatuses, getState)
|
||||||
}
|
}
|
||||||
|
|
||||||
is Intent.StartDownloadAll -> {
|
is Intent.StartDownloadAll -> {
|
||||||
val finalList =
|
val finalList =
|
||||||
intent.trackList.filter { it.downloaded == DownloadStatus.NotDownloaded }
|
intent.trackList.filter { it.downloaded == DownloadStatus.NotDownloaded }
|
||||||
if (finalList.isNullOrEmpty()) showPopUpMessage("All Songs are Processed")
|
if (finalList.isNullOrEmpty()) showPopUpMessage("All Songs are Processed")
|
||||||
else downloadTracks(finalList,fetchQuery,dir)
|
else downloadTracks(finalList, fetchQuery, dir)
|
||||||
|
|
||||||
val list = intent.trackList.map {
|
val list = intent.trackList.map {
|
||||||
if (it.downloaded == DownloadStatus.NotDownloaded)
|
if (it.downloaded == DownloadStatus.NotDownloaded)
|
||||||
return@map it.copy(downloaded = DownloadStatus.Queued)
|
return@map it.copy(downloaded = DownloadStatus.Queued)
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
dispatch(Result.UpdateTrackList(list.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0){ hashMapOf()})))
|
dispatch(Result.UpdateTrackList(list.updateTracksStatuses(downloadProgressFlow.replayCache.getOrElse(0) { hashMapOf() })))
|
||||||
}
|
}
|
||||||
is Intent.StartDownload -> {
|
is Intent.StartDownload -> {
|
||||||
downloadTracks(listOf(intent.track),fetchQuery,dir)
|
downloadTracks(listOf(intent.track), fetchQuery, dir)
|
||||||
dispatch(Result.UpdateTrackItem(intent.track.copy(downloaded = DownloadStatus.Queued)))
|
dispatch(Result.UpdateTrackItem(intent.track.copy(downloaded = DownloadStatus.Queued)))
|
||||||
}
|
}
|
||||||
is Intent.RefreshTracksStatuses -> queryActiveTracks()
|
is Intent.RefreshTracksStatuses -> queryActiveTracks()
|
||||||
@ -95,30 +103,30 @@ internal class SpotiFlyerListStoreProvider(
|
|||||||
}
|
}
|
||||||
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) {
|
||||||
is Result.ResultFetched -> copy(queryResult = result.result, trackList = result.trackList ,link = link)
|
is Result.ResultFetched -> copy(queryResult = result.result, trackList = result.trackList, link = link)
|
||||||
is Result.UpdateTrackList -> copy(trackList = result.list)
|
is Result.UpdateTrackList -> copy(trackList = result.list)
|
||||||
is Result.UpdateTrackItem -> updateTrackItem(result.item)
|
is Result.UpdateTrackItem -> updateTrackItem(result.item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun State.updateTrackItem(item: TrackDetails):State{
|
private fun State.updateTrackItem(item: TrackDetails): State {
|
||||||
val position = this.trackList.map { it.title }.indexOf(item.title)
|
val position = this.trackList.map { it.title }.indexOf(item.title)
|
||||||
if(position != -1){
|
if (position != -1) {
|
||||||
return copy(trackList = trackList.toMutableList().apply { set(position,item) })
|
return copy(trackList = trackList.toMutableList().apply { set(position, item) })
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun List<TrackDetails>.updateTracksStatuses(map:HashMap<String,DownloadStatus>):List<TrackDetails>{
|
private fun List<TrackDetails>.updateTracksStatuses(map: HashMap<String, DownloadStatus>): List<TrackDetails> {
|
||||||
val titleList = this.map { it.title }
|
val titleList = this.map { it.title }
|
||||||
val updatedList = mutableListOf<TrackDetails>().also { it.addAll(this) }
|
val updatedList = mutableListOf<TrackDetails>().also { it.addAll(this) }
|
||||||
|
|
||||||
for(newTrack in map){
|
for (newTrack in map) {
|
||||||
titleList.indexOf(newTrack.key).let { position ->
|
titleList.indexOf(newTrack.key).let { position ->
|
||||||
if(position != -1){
|
if (position != -1) {
|
||||||
updatedList.getOrNull(position)?.copy(downloaded = newTrack.value,progress = (newTrack.value as? DownloadStatus.Downloading)?.progress ?: updatedList[position].progress )?.also { updatedTrack ->
|
updatedList.getOrNull(position)?.copy(downloaded = newTrack.value, progress = (newTrack.value as? DownloadStatus.Downloading)?.progress ?: updatedList[position].progress)?.also { updatedTrack ->
|
||||||
updatedList[position] = updatedTrack
|
updatedList[position] = updatedTrack
|
||||||
//logger.d("$position) ${updatedTrack.downloaded} - ${updatedTrack.title}","List Store Track Update")
|
// logger.d("$position) ${updatedTrack.downloaded} - ${updatedTrack.title}","List Store Track Update")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,6 @@
|
|||||||
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import org.jetbrains.compose.compose
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("multiplatform-setup")
|
id("multiplatform-setup")
|
||||||
id("android-setup")
|
id("android-setup")
|
||||||
|
@ -49,14 +49,14 @@ interface SpotiFlyerMain {
|
|||||||
/*
|
/*
|
||||||
* Load Image from cache/Internet and cache it
|
* Load Image from cache/Internet and cache it
|
||||||
* */
|
* */
|
||||||
suspend fun loadImage(url:String): Picture
|
suspend fun loadImage(url: String): Picture
|
||||||
|
|
||||||
interface Dependencies {
|
interface Dependencies {
|
||||||
val mainOutput: Consumer<Output>
|
val mainOutput: Consumer<Output>
|
||||||
val storeFactory: StoreFactory
|
val storeFactory: StoreFactory
|
||||||
val database: Database?
|
val database: Database?
|
||||||
val dir: Dir
|
val dir: Dir
|
||||||
val showPopUpMessage:(String)->Unit
|
val showPopUpMessage: (String) -> Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Output {
|
sealed class Output {
|
||||||
|
@ -21,7 +21,10 @@ import com.arkivanov.mvikotlin.extensions.coroutines.states
|
|||||||
import com.shabinder.common.di.Picture
|
import com.shabinder.common.di.Picture
|
||||||
import com.shabinder.common.di.isInternetAvailable
|
import com.shabinder.common.di.isInternetAvailable
|
||||||
import com.shabinder.common.main.SpotiFlyerMain
|
import com.shabinder.common.main.SpotiFlyerMain
|
||||||
import com.shabinder.common.main.SpotiFlyerMain.*
|
import com.shabinder.common.main.SpotiFlyerMain.Dependencies
|
||||||
|
import com.shabinder.common.main.SpotiFlyerMain.HomeCategory
|
||||||
|
import com.shabinder.common.main.SpotiFlyerMain.Output
|
||||||
|
import com.shabinder.common.main.SpotiFlyerMain.State
|
||||||
import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent
|
import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent
|
||||||
import com.shabinder.common.main.store.SpotiFlyerMainStoreProvider
|
import com.shabinder.common.main.store.SpotiFlyerMainStoreProvider
|
||||||
import com.shabinder.common.main.store.getStore
|
import com.shabinder.common.main.store.getStore
|
||||||
@ -30,7 +33,7 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
internal class SpotiFlyerMainImpl(
|
internal class SpotiFlyerMainImpl(
|
||||||
componentContext: ComponentContext,
|
componentContext: ComponentContext,
|
||||||
dependencies: Dependencies
|
dependencies: Dependencies
|
||||||
): SpotiFlyerMain,ComponentContext by componentContext, Dependencies by dependencies {
|
) : SpotiFlyerMain, ComponentContext by componentContext, Dependencies by dependencies {
|
||||||
|
|
||||||
private val store =
|
private val store =
|
||||||
instanceKeeper.getStore {
|
instanceKeeper.getStore {
|
||||||
@ -44,7 +47,7 @@ internal class SpotiFlyerMainImpl(
|
|||||||
override val models: Flow<State> = store.states
|
override val models: Flow<State> = store.states
|
||||||
|
|
||||||
override fun onLinkSearch(link: String) {
|
override fun onLinkSearch(link: String) {
|
||||||
if(isInternetAvailable) mainOutput.callback(Output.Search(link = link))
|
if (isInternetAvailable) mainOutput.callback(Output.Search(link = link))
|
||||||
else showPopUpMessage("Check Network Connection Please")
|
else showPopUpMessage("Check Network Connection Please")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,9 +24,9 @@ fun <T : Store<*, *, *>> InstanceKeeper.getStore(key: Any, factory: () -> T): T
|
|||||||
getOrCreate(key) { StoreHolder(factory()) }
|
getOrCreate(key) { StoreHolder(factory()) }
|
||||||
.store
|
.store
|
||||||
|
|
||||||
inline fun <reified T
|
inline fun <reified T :
|
||||||
|
|
||||||
: Store<*, *, *>> InstanceKeeper.getStore(noinline factory: () -> T): T =
|
Store<*, *, *>> InstanceKeeper.getStore(noinline factory: () -> T): T =
|
||||||
getStore(T::class, factory)
|
getStore(T::class, factory)
|
||||||
|
|
||||||
private class StoreHolder<T : Store<*, *, *>>(
|
private class StoreHolder<T : Store<*, *, *>>(
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user