Code Cleaned & Ktlint Added

This commit is contained in:
shabinder 2021-03-19 20:45:58 +05:30
parent 46e5e89a2e
commit ccea676b77
144 changed files with 1709 additions and 1403 deletions

View File

@ -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.*

View File

@ -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")
} }

View File

@ -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)

View File

@ -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"

View File

@ -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")
} }
} }
} }

View File

@ -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)
}*/
} }

View File

@ -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 {

View File

@ -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 {}
}
} }
} }

View File

@ -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") {

View File

@ -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)
} }

View File

@ -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",
@ -132,9 +137,9 @@ 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()
} }

View File

@ -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()

View File

@ -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(
Modifier.clickable(
onClick = {
downloadTrack() 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
) )

View File

@ -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])
@ -114,8 +142,8 @@ fun HomeTabBar(
}, },
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")
} }
} }
) )
@ -125,31 +153,36 @@ fun HomeTabBar(
@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(
2.dp,
Brush.horizontalGradient(
listOf(
colorPrimary, colorPrimary,
colorAccent 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(
1.dp,
Brush.horizontalGradient(
listOf(
colorPrimary, colorPrimary,
colorAccent colorAccent
))) )
){ )
Text(text = "Search",style = SpotiFlyerTypography.h6,modifier = Modifier.padding(4.dp)) )
) {
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(
onClick = {
shareApp() 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 = {
// if(!isOnline(ctx)) showDialog("Check Your Internet Connection") else
onItemClicked(item.link) onItemClicked(item.link)
}) }
)
) )
} }
} }
@Composable @Composable
fun HomeCategoryTabIndicator( fun HomeCategoryTabIndicator(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,

View File

@ -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)
@ -66,7 +79,7 @@ fun SpotiFlyerRootContent(component: SpotiFlyerRoot, statusBarHeight:Dp = 0.dp):
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,7 +98,7 @@ 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)
@ -136,7 +149,7 @@ fun AppBar(
style = appNameStyle style = appNameStyle
) )
} }
},/* }, /*
actions = { actions = {
IconButton( IconButton(
onClick = { *//*TODO: Open Preferences*//* } onClick = { *//*TODO: Open Preferences*//* }

View File

@ -20,7 +20,7 @@ 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,

View File

@ -23,9 +23,8 @@ 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(

View File

@ -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",

View File

@ -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

View File

@ -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")

View File

@ -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()
} }

View File

@ -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()

View File

@ -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()
} }

View File

@ -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,
) )

View File

@ -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 {

View File

@ -23,6 +23,6 @@ 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
) )

View File

@ -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
) )

View File

@ -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,
) )

View File

@ -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,
) )

View File

@ -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,
) )

View File

@ -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
) )

View File

@ -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>,
) )

View File

@ -20,5 +20,5 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class GaanaSong( data class GaanaSong(
val tracks : List<GaanaTrack> val tracks: List<GaanaTrack>
) )

View File

@ -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
) )

View File

@ -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
) )

View File

@ -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
) )

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
) )

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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?
) )

View File

@ -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
) )

View File

@ -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
)

View File

@ -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
)

View File

@ -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
) )

View File

@ -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>,

View File

@ -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)

View File

@ -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

View File

@ -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))
} }
} }
} }

View File

@ -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) {

View File

@ -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
@ -56,32 +55,27 @@ actual class Dir actual constructor(
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

View File

@ -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,7 +40,7 @@ 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
} }

View File

@ -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?
) )

View File

@ -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()

View File

@ -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

View File

@ -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 = "",
@ -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,
@ -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
} }
} }

View File

@ -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>? = (
it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList(
"object" "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}" }
} }
) )
} }
@ -237,10 +254,10 @@ class ForegroundService : Service(),CoroutineScope{
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"
@ -339,7 +355,7 @@ 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 {
@ -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)
} }

View File

@ -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(
Json {
isLenient = true isLenient = true
ignoreUnknownKeys = 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
} }

View File

@ -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

View File

@ -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

View File

@ -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()
) )

View File

@ -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) } }
} }

View File

@ -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,

View File

@ -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

View File

@ -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,7 +106,6 @@ 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
@ -142,29 +113,20 @@ class GaanaProvider(
} }
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() },
@ -177,7 +139,20 @@ class GaanaProvider(
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
}
}
} }

View File

@ -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&amp;_branch_match_id=862039436205270630 * Fetching Standard Link: https://open.spotify.com/playlist/37i9dQZF1DX9RwfGbeGQwP?si=iWz7B1tETiunDntnDo3lSQ&amp;_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
}
}
} }

View File

@ -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

View File

@ -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,19 +65,21 @@ 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
@ -80,7 +93,7 @@ class YoutubeMusic constructor(
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)
} }
} }
} }

View File

@ -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
} }

View File

@ -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)
} }
} }

View File

@ -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)
} }
} }

View File

@ -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
* **/ * **/

View File

@ -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(
Parameters.build {
append("vid", videoID) append("vid", videoID)
append("k",key) append("k", key)
}) }
)
} }
} }
} }

View File

@ -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) {

View File

@ -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()
@ -57,31 +55,26 @@ actual class Dir actual constructor(
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()
@ -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,13 +107,15 @@ 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? {
return withContext(Dispatchers.IO) {
try {
val source = URL(url) val source = URL(url)
val connection: HttpURLConnection = source.openConnection() as HttpURLConnection val connection: HttpURLConnection = source.openConnection() as HttpURLConnection
connection.connectTimeout = 5000 connection.connectTimeout = 5000
@ -130,8 +125,8 @@ actual class Dir actual constructor(
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
@ -140,6 +135,7 @@ actual class Dir actual constructor(
null null
} }
} }
}
actual val db: Database? actual val db: Database?
get() = database get() = database
@ -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()

View File

@ -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()

View File

@ -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 = "",
@ -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,
@ -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
} }
} }

View File

@ -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()
} }

View File

@ -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,15 +100,15 @@ 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)

View File

@ -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,
@ -52,11 +52,11 @@ actual class Dir actual constructor(
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(
@ -64,33 +64,33 @@ actual class Dir actual constructor(
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
} }

View File

@ -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
) )

View File

@ -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,

View File

@ -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()
) )
} }

View File

@ -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)
} }

View File

@ -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<*, *, *>>(

View File

@ -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()
} }
} }

View File

@ -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,7 +44,9 @@ 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 :
SpotiFlyerListStore,
Store<Intent, State, Nothing> by storeFactory.create(
name = "SpotiFlyerListStore", name = "SpotiFlyerListStore",
initialState = State(), initialState = State(),
bootstrapper = SimpleBootstrapper(Unit), bootstrapper = SimpleBootstrapper(Unit),
@ -47,46 +55,46 @@ internal class SpotiFlyerListStoreProvider(
) {} ) {}
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()
@ -96,29 +104,29 @@ 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")
} }
} }
} }

View File

@ -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")

View File

@ -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 {

View File

@ -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")
} }

View File

@ -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<*, *, *>>(

View File

@ -20,12 +20,12 @@ import com.arkivanov.mvikotlin.core.store.Store
import com.shabinder.common.main.SpotiFlyerMain import com.shabinder.common.main.SpotiFlyerMain
import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent import com.shabinder.common.main.store.SpotiFlyerMainStore.Intent
internal interface SpotiFlyerMainStore: Store<Intent, SpotiFlyerMain.State, Nothing> { internal interface SpotiFlyerMainStore : Store<Intent, SpotiFlyerMain.State, Nothing> {
sealed class Intent { sealed class Intent {
data class OpenPlatform(val platformID:String,val platformLink:String):Intent() data class OpenPlatform(val platformID: String, val platformLink: String) : Intent()
data class SetLink(val link:String):Intent() data class SetLink(val link: String) : Intent()
data class SelectCategory(val category: SpotiFlyerMain.HomeCategory):Intent() data class SelectCategory(val category: SpotiFlyerMain.HomeCategory) : Intent()
object GiveDonation : Intent() object GiveDonation : Intent()
object ShareApp: Intent() object ShareApp : Intent()
} }
} }

View File

@ -38,12 +38,14 @@ import kotlinx.coroutines.flow.map
internal class SpotiFlyerMainStoreProvider( internal class SpotiFlyerMainStoreProvider(
private val storeFactory: StoreFactory, private val storeFactory: StoreFactory,
private val showPopUpMessage: (String)->Unit, private val showPopUpMessage: (String) -> Unit,
private val database: Database? private val database: Database?
) { ) {
fun provide(): SpotiFlyerMainStore = fun provide(): SpotiFlyerMainStore =
object : SpotiFlyerMainStore, Store<Intent, State, Nothing> by storeFactory.create( object :
SpotiFlyerMainStore,
Store<Intent, State, Nothing> by storeFactory.create(
name = "SpotiFlyerHomeStore", name = "SpotiFlyerHomeStore",
initialState = State(), initialState = State(),
bootstrapper = SimpleBootstrapper(Unit), bootstrapper = SimpleBootstrapper(Unit),
@ -58,13 +60,12 @@ internal class SpotiFlyerMainStoreProvider(
?.mapToList(Dispatchers.Default) ?.mapToList(Dispatchers.Default)
?.map { ?.map {
it.map { record -> it.map { record ->
record.run{ record.run {
DownloadRecord(id, type, name, link, coverUrl, totalFiles) DownloadRecord(id, type, name, link, coverUrl, totalFiles)
} }
} }
} }
private sealed class Result { private sealed class Result {
data class ItemsLoaded(val items: List<DownloadRecord>) : Result() data class ItemsLoaded(val items: List<DownloadRecord>) : Result()
data class CategoryChanged(val category: SpotiFlyerMain.HomeCategory) : Result() data class CategoryChanged(val category: SpotiFlyerMain.HomeCategory) : Result()

Some files were not shown because too many files have changed in this diff Show More