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