Code Cleaned & Ktlint Added

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

View File

@ -58,7 +58,6 @@ import com.shabinder.common.models.TrackDetails
import com.shabinder.common.root.SpotiFlyerRoot
import com.shabinder.common.root.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.*

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@
plugins {
id("com.android.library")
id("ktlint-setup")
}
android {
@ -43,5 +44,4 @@ android {
res.srcDirs("src/androidMain/res")
}
}
}

View File

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

View File

@ -18,6 +18,7 @@ plugins {
id("com.android.library")
id("kotlin-multiplatform")
id("org.jetbrains.compose")
id("ktlint-setup")
}
kotlin {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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])
@ -114,8 +142,8 @@ fun HomeTabBar(
},
icon = {
when (category) {
HomeCategory.About -> Icon(Icons.Outlined.Info,"Info Tab")
HomeCategory.History -> Icon(Icons.Outlined.History,"History Tab")
HomeCategory.About -> Icon(Icons.Outlined.Info, "Info Tab")
HomeCategory.History -> Icon(Icons.Outlined.History, "History Tab")
}
}
)
@ -125,31 +153,36 @@ fun HomeTabBar(
@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(
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(
border = BorderStroke(
1.dp,
Brush.horizontalGradient(
listOf(
colorPrimary,
colorAccent
)))
){
Text(text = "Search",style = SpotiFlyerTypography.h6,modifier = Modifier.padding(4.dp))
)
)
)
) {
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 = {
.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
modifier = Modifier.clickable(
onClick = {
// if(!isOnline(ctx)) showDialog("Check Your Internet Connection") else
onItemClicked(item.link)
})
}
)
)
}
}
@Composable
fun HomeCategoryTabIndicator(
modifier: Modifier = Modifier,

View File

@ -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)
@ -66,7 +79,7 @@ fun SpotiFlyerRootContent(component: SpotiFlyerRoot, statusBarHeight:Dp = 0.dp):
if (it == SplashState.Shown && isSplashShown == SplashState.Shown) 100.dp else 0.dp
}
Box{
Box {
Splash(
modifier = Modifier.alpha(splashAlpha),
onTimeout = {
@ -85,7 +98,7 @@ 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)
@ -136,7 +149,7 @@ fun AppBar(
style = appNameStyle
)
}
},/*
}, /*
actions = {
IconButton(
onClick = { *//*TODO: Open Preferences*//* }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,6 +23,6 @@ data class YoutubeTrack(
var name: String? = null,
var type: String? = null, // Song / Video
var artist: String? = null,
var duration:String? = null,
var duration: String? = null,
var videoId: String? = null
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,22 +21,22 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@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
)

View File

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

View File

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

View File

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

View File

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

View File

@ -21,4 +21,5 @@ import kotlinx.serialization.Serializable
@Serializable
data class Copyright(
var text: String? = null,
var type: String? = null)
var type: String? = null
)

View File

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

View File

@ -21,4 +21,5 @@ import kotlinx.serialization.Serializable
@Serializable
data class Followers(
var href: String? = null,
var total: Int? = null)
var total: Int? = null
)

View File

@ -22,4 +22,5 @@ import kotlinx.serialization.Serializable
data class Image(
var width: Int? = null,
var height: Int? = null,
var url: String? = null)
var url: String? = null
)

View File

@ -24,4 +24,5 @@ data class LinkedTrack(
var href: String? = null,
var id: String? = null,
var type: String? = null,
var uri: String? = null)
var uri: String? = null
)

View File

@ -26,4 +26,5 @@ data class PagingObjectPlaylistTrack(
var next: String? = null,
var offset: Int = 0,
var previous: String? = null,
var total: Int = 0)
var total: Int = 0
)

View File

@ -26,4 +26,5 @@ data class PagingObjectTrack(
var next: String? = null,
var offset: Int = 0,
var previous: String? = null,
var total: Int = 0)
var total: Int = 0
)

View File

@ -34,4 +34,5 @@ data class Playlist(
var snapshot_id: String? = null,
var tracks: PagingObjectPlaylistTrack? = null,
var type: String? = null,
var uri: String? = null)
var uri: String? = null
)

View File

@ -23,4 +23,5 @@ data class PlaylistTrack(
var added_at: String? = null,
var added_by: UserPublic? = null,
var track: Track? = null,
var is_local: Boolean? = null)
var is_local: Boolean? = null
)

View File

@ -21,7 +21,7 @@ import kotlinx.serialization.Serializable
@Serializable
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?
)

View File

@ -40,4 +40,3 @@ data class Track(
var popularity: Int? = null,
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
@ -56,32 +55,27 @@ actual class Dir actual constructor(
actual fun defaultDir(): String =
Environment.getExternalStorageDirectory().toString() + File.separator +
Environment.DIRECTORY_MUSIC + File.separator +
"SpotiFlyer"+ 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

View File

@ -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,7 +40,7 @@ const val TAG = "C-Manager"
val internetAvailability by lazy { ConnectionLiveData(appContext) }
@Composable
fun isInternetAvailableState(): State<Boolean?>{
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
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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(
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}" }
}
)
}
@ -237,10 +254,10 @@ class ForegroundService : Service(),CoroutineScope{
launch {
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"
@ -339,7 +355,7 @@ class ForegroundService : Service(),CoroutineScope{
/**
* If fetch Fails , Android Download Manager To RESCUE!!
**/
fun downloadUsingDM(url: String, outputDir: String, track: TrackDetails){
fun downloadUsingDM(url: String, outputDir: String, track: TrackDetails) {
launch {
val uri = Uri.parse(url)
val request = DownloadManager.Request(uri).apply {
@ -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)
}

View File

@ -23,10 +23,13 @@ import com.shabinder.common.di.providers.GaanaProvider
import com.shabinder.common.di.providers.SpotifyProvider
import com.shabinder.common.di.providers.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 {
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
}

View File

@ -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()} +
if (subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator() } +
removeIllegalChars(itemName) + extension

View File

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

View File

@ -22,33 +22,32 @@ import com.shabinder.common.di.providers.SpotifyProvider
import com.shabinder.common.di.providers.YoutubeMp3
import com.shabinder.common.di.providers.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()
)

View File

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

View File

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

View File

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

View File

@ -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,7 +106,6 @@ class GaanaProvider(
folderType = "Artist"
subFolder = link
coverUrl = gaanaPlaceholderImageUrl
val artistDetails =
getGaanaArtistDetails(seokey = link).artist.firstOrNull()
?.also {
title = it.name
@ -142,29 +113,20 @@ class GaanaProvider(
}
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() },
@ -177,7 +139,20 @@ class GaanaProvider(
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
}
}
}

View File

@ -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&amp;_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
}
}
}

View File

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

View File

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

View File

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

View File

@ -21,16 +21,16 @@ import com.shabinder.common.models.spotify.Album
import com.shabinder.common.models.spotify.PagingObjectPlaylistTrack
import com.shabinder.common.models.spotify.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)
}
}

View File

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

View File

@ -16,8 +16,6 @@
package com.shabinder.common.di.utils
/**
* Removing Illegal Chars from File Name
* **/

View File

@ -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 {
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)
})
append("k", key)
}
)
}
}
}

View File

@ -25,7 +25,7 @@ import com.shabinder.common.models.AllPlatforms
import com.shabinder.common.models.DownloadResult
import com.shabinder.common.models.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) {

View File

@ -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()
@ -57,31 +55,26 @@ actual class Dir actual constructor(
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()
@ -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,13 +107,15 @@ 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 {
@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
@ -130,8 +125,8 @@ actual class Dir actual constructor(
val result: BufferedImage? = ImageIO.read(input)
if (result != null) {
GlobalScope.launch(Dispatchers.IO) { //TODO Refactor
cacheImage(result,imageCacheDir() + getNameURL(url))
GlobalScope.launch(Dispatchers.IO) { // TODO Refactor
cacheImage(result, imageCacheDir() + getNameURL(url))
}
result.toImageBitmap()
} else null
@ -140,6 +135,7 @@ actual class Dir actual constructor(
null
}
}
}
actual val db: Database?
get() = database
@ -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()

View File

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

View File

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

View File

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

View File

@ -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,15 +100,15 @@ actual suspend fun downloadTracks(
}
}
suspend fun downloadTrack(videoID: String, track: TrackDetails, fetcher:FetchPlatformQueryResult,dir:Dir) {
suspend fun downloadTrack(videoID: String, track: TrackDetails, fetcher: FetchPlatformQueryResult, dir: Dir) {
val url = fetcher.youtubeMp3.getMp3DownloadLink(videoID)
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)

View File

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

View File

@ -17,5 +17,5 @@
package com.shabinder.common.di
actual data class Picture(
var imageUrl:String
var imageUrl: String
)

View File

@ -18,8 +18,7 @@ package com.shabinder.common.di
import co.touchlab.kermit.Kermit
import 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,

View File

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

View File

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

View File

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

View File

@ -21,11 +21,11 @@ import com.shabinder.common.list.SpotiFlyerList.State
import com.shabinder.common.list.store.SpotiFlyerListStore.Intent
import com.shabinder.common.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()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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