mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-22 17:14:32 +01:00
Providers/DB implem.
This commit is contained in:
parent
6b024ec430
commit
1f773c3493
@ -64,6 +64,9 @@ dependencies {
|
|||||||
//Compose-Navigation
|
//Compose-Navigation
|
||||||
implementation(Androidx.composeNavigation)
|
implementation(Androidx.composeNavigation)
|
||||||
|
|
||||||
|
implementation(Koin.android)
|
||||||
|
implementation(Koin.androidViewModel)
|
||||||
|
|
||||||
//Lifecycle
|
//Lifecycle
|
||||||
Versions.androidLifecycle.let{
|
Versions.androidLifecycle.let{
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$it")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$it")
|
||||||
|
@ -8,6 +8,8 @@ allprojects {
|
|||||||
jcenter()
|
jcenter()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven(url = "https://jitpack.io")
|
maven(url = "https://jitpack.io")
|
||||||
|
maven(url = "https://dl.bintray.com/ekito/koin")
|
||||||
|
maven(url = "https://kotlin.bintray.com/kotlin-js-wrappers/")
|
||||||
maven(url = "https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
maven(url = "https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,10 @@ object Versions {
|
|||||||
const val coilVersion = "0.4.1"
|
const val coilVersion = "0.4.1"
|
||||||
//DI
|
//DI
|
||||||
const val kodein = "7.2.0"
|
const val kodein = "7.2.0"
|
||||||
|
const val koin = "3.0.0-alpha-4"
|
||||||
|
|
||||||
|
//Logger
|
||||||
|
const val kermit = "0.1.8"
|
||||||
|
|
||||||
//Internet
|
//Internet
|
||||||
const val ktor = "1.5.0"
|
const val ktor = "1.5.0"
|
||||||
@ -27,7 +31,12 @@ object Versions {
|
|||||||
const val targetSdkVersion = 29
|
const val targetSdkVersion = 29
|
||||||
const val androidLifecycle = "2.3.0-rc01"
|
const val androidLifecycle = "2.3.0-rc01"
|
||||||
}
|
}
|
||||||
|
object Koin {
|
||||||
|
val core = "org.koin:koin-core:${Versions.koin}"
|
||||||
|
val test = "org.koin:koin-test:${Versions.koin}"
|
||||||
|
val android = "org.koin:koin-android:${Versions.koin}"
|
||||||
|
val androidViewModel = "org.koin:koin-androidx-viewmodel:${Versions.koin}"
|
||||||
|
}
|
||||||
object Androidx{
|
object Androidx{
|
||||||
const val appCompat = "androidx.appcompat:appcompat:1.2.0"
|
const val appCompat = "androidx.appcompat:appcompat:1.2.0"
|
||||||
const val core = "androidx.core:core-ktx:1.5.0-beta01"
|
const val core = "androidx.core:core-ktx:1.5.0-beta01"
|
||||||
@ -62,7 +71,7 @@ object Ktor {
|
|||||||
|
|
||||||
val auth = "io.ktor:ktor-client-auth:${Versions.ktor}"
|
val auth = "io.ktor:ktor-client-auth:${Versions.ktor}"
|
||||||
val clientAndroid = "io.ktor:ktor-client-android:${Versions.ktor}"
|
val clientAndroid = "io.ktor:ktor-client-android:${Versions.ktor}"
|
||||||
val clientDesktop = "io.ktor:ktor-client-curl:${Versions.ktor}"
|
val clientCurl = "io.ktor:ktor-client-curl:${Versions.ktor}"
|
||||||
val clientApache = "io.ktor:ktor-client-apache:${Versions.ktor}"
|
val clientApache = "io.ktor:ktor-client-apache:${Versions.ktor}"
|
||||||
val slf4j = "org.slf4j:slf4j-simple:${Versions.slf4j}"
|
val slf4j = "org.slf4j:slf4j-simple:${Versions.slf4j}"
|
||||||
val clientIos = "io.ktor:ktor-client-ios:${Versions.ktor}"
|
val clientIos = "io.ktor:ktor-client-ios:${Versions.ktor}"
|
||||||
@ -71,10 +80,11 @@ object Ktor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
object Extras {
|
object Extras {
|
||||||
val youtubeDownloader = "com.github.sealedtx:java-youtube-downloader:2.4.6"
|
const val youtubeDownloader = "com.github.sealedtx:java-youtube-downloader:2.4.6"
|
||||||
val fuzzyWuzzy = "me.xdrop:fuzzywuzzy:1.3.1"
|
const val fuzzyWuzzy = "me.xdrop:fuzzywuzzy:1.3.1"
|
||||||
val mp3agic = "com.mpatric:mp3agic:0.9.1"
|
const val mp3agic = "com.mpatric:mp3agic:0.9.1"
|
||||||
val jsonKlaxon = "com.beust:klaxon:5.4"
|
const val jsonKlaxon = "com.beust:klaxon:5.4"
|
||||||
|
const val kermit = "co.touchlab:kermit:${Versions.kermit}"
|
||||||
object Android {
|
object Android {
|
||||||
val razorpay = "com.razorpay:checkout:1.6.4"
|
val razorpay = "com.razorpay:checkout:1.6.4"
|
||||||
val fetch = "androidx.tonyodev.fetch2:xfetch2:3.1.5"
|
val fetch = "androidx.tonyodev.fetch2:xfetch2:3.1.5"
|
||||||
|
@ -29,7 +29,7 @@ data class TrackDetails(
|
|||||||
var comment:String?=null,
|
var comment:String?=null,
|
||||||
var lyrics:String?=null,
|
var lyrics:String?=null,
|
||||||
var trackUrl:String?=null,
|
var trackUrl:String?=null,
|
||||||
//TODO var albumArt: File,
|
var albumArtPath: String,
|
||||||
var albumArtURL: String,
|
var albumArtURL: String,
|
||||||
var source: Source,
|
var source: Source,
|
||||||
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded,
|
var downloaded: DownloadStatus = DownloadStatus.NotDownloaded,
|
||||||
|
@ -15,19 +15,23 @@ kotlin {
|
|||||||
commonMain {
|
commonMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(Deps.Badoo.Reaktive.reaktive)
|
implementation(Deps.Badoo.Reaktive.reaktive)
|
||||||
|
// SQL Delight
|
||||||
|
implementation(SqlDelight.runtime)
|
||||||
|
implementation(SqlDelight.coroutineExtensions)
|
||||||
|
api(Extras.kermit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(SqlDelight.androidDriver)
|
implementation(SqlDelight.androidDriver)
|
||||||
implementation(SqlDelight.sqliteDriver)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
desktopMain {
|
desktopMain {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(SqlDelight.sqliteDriver)
|
implementation(SqlDelight.sqliteDriver)
|
||||||
|
implementation(SqlDelight.jdbcDriver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.shabinder.common.database
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import co.touchlab.kermit.LogcatLogger
|
||||||
|
import co.touchlab.kermit.Logger
|
||||||
|
import com.shabinder.database.DownloadRecordDatabase
|
||||||
|
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
||||||
|
import com.squareup.sqldelight.db.SqlDriver
|
||||||
|
|
||||||
|
lateinit var appContext: Context
|
||||||
|
|
||||||
|
actual fun createDb(): DownloadRecordDatabase {
|
||||||
|
val driver = AndroidSqliteDriver(DownloadRecordDatabase.Schema, appContext, "DownloadRecordDatabase.db")
|
||||||
|
return DownloadRecordDatabase(driver)
|
||||||
|
}
|
||||||
|
actual fun getLogger(): Logger = LogcatLogger()
|
@ -1,12 +0,0 @@
|
|||||||
package com.shabinder.common.database
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.shabinder.database.DownloadRecordDatabase
|
|
||||||
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
|
||||||
import com.squareup.sqldelight.db.SqlDriver
|
|
||||||
|
|
||||||
actual class DatabaseDriverFactory(private val context: Context) {
|
|
||||||
actual fun createDriver(): SqlDriver {
|
|
||||||
return AndroidSqliteDriver(DownloadRecordDatabase.Schema, context, "DownloadRecordDatabase.db")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package com.shabinder.common.database
|
|
||||||
|
|
||||||
import com.squareup.sqldelight.db.SqlDriver
|
|
||||||
|
|
||||||
expect class DatabaseDriverFactory {
|
|
||||||
fun createDriver(): SqlDriver
|
|
||||||
}
|
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.shabinder.common.database
|
||||||
|
|
||||||
|
import com.shabinder.database.DownloadRecordDatabase
|
||||||
|
import co.touchlab.kermit.Logger
|
||||||
|
|
||||||
|
expect fun createDb() : DownloadRecordDatabase
|
||||||
|
expect fun getLogger(): Logger
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.shabinder.common.database
|
||||||
|
|
||||||
|
import co.touchlab.kermit.CommonLogger
|
||||||
|
import co.touchlab.kermit.Logger
|
||||||
|
import com.shabinder.database.DownloadRecordDatabase
|
||||||
|
import com.squareup.sqldelight.db.SqlDriver
|
||||||
|
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
actual fun createDb(): DownloadRecordDatabase {
|
||||||
|
val databasePath = File(System.getProperty("java.io.tmpdir"), "DownloadRecordDatabase.db")
|
||||||
|
val driver = JdbcSqliteDriver(url = "jdbc:sqlite:${databasePath.absolutePath}")
|
||||||
|
.also { DownloadRecordDatabase.Schema.create(it) }
|
||||||
|
return DownloadRecordDatabase(driver)
|
||||||
|
}
|
||||||
|
actual fun getLogger(): Logger = CommonLogger()
|
@ -1,16 +0,0 @@
|
|||||||
package com.shabinder.common.database
|
|
||||||
|
|
||||||
import com.shabinder.database.DownloadRecordDatabase
|
|
||||||
import com.squareup.sqldelight.db.SqlDriver
|
|
||||||
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
actual class DatabaseDriverFactory {
|
|
||||||
actual fun createDriver(): SqlDriver {
|
|
||||||
val databasePath = File(System.getProperty("java.io.tmpdir"), "DownloadRecordDatabase.db")
|
|
||||||
val driver = JdbcSqliteDriver(url = "jdbc:sqlite:${databasePath.absolutePath}")
|
|
||||||
DownloadRecordDatabase.Schema.create(driver)
|
|
||||||
|
|
||||||
return driver
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,25 +10,32 @@ kotlin {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":common:data-models"))
|
implementation(project(":common:data-models"))
|
||||||
implementation(project(":common:database"))
|
implementation(project(":common:database"))
|
||||||
implementation("org.kodein.di:kodein-di:${Versions.kodein}")
|
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
|
||||||
implementation(Extras.jsonKlaxon)
|
|
||||||
implementation(Ktor.clientCore)
|
implementation(Ktor.clientCore)
|
||||||
implementation(Ktor.clientCio)
|
implementation(Ktor.clientCio)
|
||||||
implementation(Ktor.clientJson)
|
|
||||||
implementation(Ktor.clientSerialization)
|
implementation(Ktor.clientSerialization)
|
||||||
|
implementation(Ktor.clientLogging)
|
||||||
|
implementation(Ktor.clientJson)
|
||||||
implementation(Ktor.auth)
|
implementation(Ktor.auth)
|
||||||
|
// koin
|
||||||
|
api(Koin.core)
|
||||||
|
api(Koin.test)
|
||||||
|
|
||||||
|
api(Extras.kermit)
|
||||||
|
api(Extras.jsonKlaxon)
|
||||||
|
api(Extras.youtubeDownloader)
|
||||||
|
api(Extras.fuzzyWuzzy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
androidMain {
|
androidMain {
|
||||||
dependencies{
|
dependencies{
|
||||||
implementation(Ktor.clientAndroid)
|
implementation(Ktor.clientAndroid)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
desktopMain {
|
desktopMain {
|
||||||
dependencies{
|
dependencies{
|
||||||
//implementation(Ktor.clientDesktop)
|
implementation(Ktor.clientApache)
|
||||||
|
implementation(Ktor.slf4j)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,17 @@ package com.shabinder.common
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import com.shabinder.common.database.appContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
actual open class PlatformDir {
|
actual open class Dir {
|
||||||
|
|
||||||
|
private val context:Context
|
||||||
|
get() = appContext
|
||||||
|
|
||||||
actual fun fileSeparator(): String = File.separator
|
actual fun fileSeparator(): String = File.separator
|
||||||
|
|
||||||
// actual fun imageDir(): String = context.cacheDir.absolutePath + File.separator
|
actual fun imageDir(): String = context.cacheDir.absolutePath + File.separator
|
||||||
actual fun imageDir(): String = defaultDir() + File.separator + ".images" + File.separator
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
actual fun defaultDir(): String =
|
actual fun defaultDir(): String =
|
||||||
@ -17,6 +20,5 @@ actual open class PlatformDir {
|
|||||||
Environment.DIRECTORY_MUSIC + File.separator +
|
Environment.DIRECTORY_MUSIC + File.separator +
|
||||||
"SpotiFlyer"+ File.separator
|
"SpotiFlyer"+ File.separator
|
||||||
|
|
||||||
|
|
||||||
actual fun isPresent(path: String): Boolean = File(path).exists()
|
actual fun isPresent(path: String): Boolean = File(path).exists()
|
||||||
}
|
}
|
@ -0,0 +1,255 @@
|
|||||||
|
package com.shabinder.common
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import co.touchlab.kermit.Logger
|
||||||
|
import com.shabinder.common.YoutubeTrack
|
||||||
|
import com.beust.klaxon.JsonArray
|
||||||
|
import com.beust.klaxon.JsonObject
|
||||||
|
import com.beust.klaxon.Parser
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import kotlinx.serialization.json.putJsonObject
|
||||||
|
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
private const val apiKey = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"
|
||||||
|
|
||||||
|
actual class YoutubeMusic actual constructor(
|
||||||
|
private val logger: Logger,
|
||||||
|
private val httpClient:HttpClient,
|
||||||
|
) {
|
||||||
|
private val tag = "YTMUSIC"
|
||||||
|
actual fun getYTTracks(response: String):List<YoutubeTrack>{
|
||||||
|
val youtubeTracks = mutableListOf<YoutubeTrack>()
|
||||||
|
|
||||||
|
val stringBuilder: StringBuilder = StringBuilder(response)
|
||||||
|
val responseObj: JsonObject = Parser.default().parse(stringBuilder) as JsonObject
|
||||||
|
val contentBlocks = responseObj.obj("contents")?.obj("sectionListRenderer")?.array<JsonObject>("contents")
|
||||||
|
val resultBlocks = mutableListOf<JsonArray<JsonObject>>()
|
||||||
|
if (contentBlocks != null) {
|
||||||
|
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.containsKey("itemSectionRenderer")){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for(contents in cBlock.obj("musicShelfRenderer")?.array<JsonObject>("contents") ?: listOf()){
|
||||||
|
/**
|
||||||
|
* apparently content Blocks without an 'overlay' field don't have linkBlocks
|
||||||
|
* I have no clue what they are and why there even exist
|
||||||
|
*
|
||||||
|
if(!contents.containsKey("overlay")){
|
||||||
|
println(contents)
|
||||||
|
continue
|
||||||
|
TODO check and correct
|
||||||
|
}*/
|
||||||
|
|
||||||
|
val result = contents.obj("musicResponsiveListItemRenderer")
|
||||||
|
?.array<JsonObject>("flexColumns")
|
||||||
|
|
||||||
|
//Add the linkBlock
|
||||||
|
val linkBlock = contents.obj("musicResponsiveListItemRenderer")
|
||||||
|
?.obj("overlay")
|
||||||
|
?.obj("musicItemThumbnailOverlayRenderer")
|
||||||
|
?.obj("content")
|
||||||
|
?.obj("musicPlayButtonRenderer")
|
||||||
|
?.obj("playNavigationEndpoint")
|
||||||
|
|
||||||
|
// detailsBlock is always a list, so we just append the linkBlock to it
|
||||||
|
// instead of carrying along all the other junk from "musicResponsiveListItemRenderer"
|
||||||
|
linkBlock?.let { result?.add(it) }
|
||||||
|
result?.let { resultBlocks.add(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We only need results that are Songs or Videos, so we filter out the rest, since
|
||||||
|
! Songs and Videos are supplied with different details, extracting all details from
|
||||||
|
! both is just carrying on redundant data, so we also have to selectively extract
|
||||||
|
! relevant details. What you need to know to understand how we do that here:
|
||||||
|
!
|
||||||
|
! Songs details are ALWAYS in the following order:
|
||||||
|
! 0 - Name
|
||||||
|
! 1 - Type (Song)
|
||||||
|
! 2 - com.shabinder.spotiflyer.models.gaana.Artist
|
||||||
|
! 3 - Album
|
||||||
|
! 4 - Duration (mm:ss)
|
||||||
|
!
|
||||||
|
! Video details are ALWAYS in the following order:
|
||||||
|
! 0 - Name
|
||||||
|
! 1 - Type (Video)
|
||||||
|
! 2 - Channel
|
||||||
|
! 3 - Viewers
|
||||||
|
! 4 - Duration (hh:mm:ss)
|
||||||
|
!
|
||||||
|
! We blindly gather all the details we get our hands on, then
|
||||||
|
! cherrypick the details we need based on their index numbers,
|
||||||
|
! we do so only if their Type is 'Song' or 'Video
|
||||||
|
*/
|
||||||
|
|
||||||
|
for(result in resultBlocks){
|
||||||
|
|
||||||
|
// Blindly gather available details
|
||||||
|
val availableDetails = mutableListOf<String>()
|
||||||
|
|
||||||
|
/*
|
||||||
|
Filter Out dummies here itself
|
||||||
|
! 'musicResponsiveListItemFlexColumnRenderer' should have more that one
|
||||||
|
! sub-block, if not its a dummy, why does the YTM response contain dummies?
|
||||||
|
! I have no clue. We skip these.
|
||||||
|
|
||||||
|
! Remember that we appended the linkBlock to result, treating that like the
|
||||||
|
! other constituents of a result block will lead to errors, hence the 'in
|
||||||
|
! result[:-1] ,i.e., skip last element in array '
|
||||||
|
*/
|
||||||
|
for(detail in result.subList(0,result.size-1)){
|
||||||
|
if(detail.obj("musicResponsiveListItemFlexColumnRenderer")?.size!! < 2) continue
|
||||||
|
|
||||||
|
// if not a dummy, collect All Variables
|
||||||
|
val details = detail.obj("musicResponsiveListItemFlexColumnRenderer")
|
||||||
|
?.obj("text")
|
||||||
|
?.array<JsonObject>("runs") ?: listOf()
|
||||||
|
for (d in details){
|
||||||
|
d["text"]?.let {
|
||||||
|
if(it.toString() != " • "){
|
||||||
|
availableDetails.add(
|
||||||
|
it.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// log("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") ){
|
||||||
|
|
||||||
|
// skip if result is in hours instead of minutes (no song is that long)
|
||||||
|
if(availableDetails[4].split(':').size != 2) continue
|
||||||
|
|
||||||
|
/*
|
||||||
|
! grab Video ID
|
||||||
|
! this is nested as [playlistEndpoint/watchEndpoint][videoId/playlistId/...]
|
||||||
|
! so hardcoding the dict keys for data look up is an ardours process, since
|
||||||
|
! the sub-block pattern is fixed even though the key isn't, we just
|
||||||
|
! reference the dict keys by index
|
||||||
|
*/
|
||||||
|
|
||||||
|
val videoId:String? = result.last().obj("watchEndpoint")?.get("videoId") as String?
|
||||||
|
val ytTrack = YoutubeTrack(
|
||||||
|
name = availableDetails[0],
|
||||||
|
type = availableDetails[1],
|
||||||
|
artist = availableDetails[2],
|
||||||
|
duration = availableDetails[4],
|
||||||
|
videoId = videoId
|
||||||
|
)
|
||||||
|
youtubeTracks.add(ytTrack)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.i(youtubeTracks.joinToString(" abc \n"),tag)
|
||||||
|
return youtubeTracks
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
|
actual fun sortByBestMatch(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>()
|
||||||
|
|
||||||
|
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 trackNameWords = trackName.toLowerCase().split(" ")
|
||||||
|
|
||||||
|
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())
|
||||||
|
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)
|
||||||
|
artistMatchNumber++
|
||||||
|
}
|
||||||
|
}else{//i.e. is a Video
|
||||||
|
for (artist in trackArtists) {
|
||||||
|
if(FuzzySearch.partialRatio(artist.toLowerCase(),result.name?.toLowerCase()) > 85)
|
||||||
|
artistMatchNumber++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(artistMatchNumber == 0) {
|
||||||
|
//log("YT Api Removing", result.toString())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val artistMatch = (artistMatchNumber / trackArtists.size ) * 100
|
||||||
|
|
||||||
|
// Duration Match
|
||||||
|
/*! time match = 100 - (delta(duration)**2 / original duration * 100)
|
||||||
|
! difference in song duration (delta) is usually of the magnitude of a few
|
||||||
|
! 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)
|
||||||
|
?.minus(trackDurationSec)?.absoluteValue ?: 0
|
||||||
|
val nonMatchValue :Float= ((difference*difference).toFloat()/trackDurationSec.toFloat())
|
||||||
|
val durationMatch = 100 - (nonMatchValue*100)
|
||||||
|
|
||||||
|
val avgMatch = (artistMatch + durationMatch)/2
|
||||||
|
linksWithMatchValue[result.videoId.toString()] = avgMatch.toInt()
|
||||||
|
}
|
||||||
|
//log("YT Api Result", "$trackName - $linksWithMatchValue")
|
||||||
|
return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
actual suspend fun getYoutubeMusicResponse(query: String):String{
|
||||||
|
return httpClient.post("https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey"){
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
headers{
|
||||||
|
//append("Content-Type"," application/json")
|
||||||
|
append("Referer"," https://music.youtube.com/search")
|
||||||
|
}
|
||||||
|
body = buildJsonObject {
|
||||||
|
putJsonObject("context"){
|
||||||
|
putJsonObject("client"){
|
||||||
|
put("clientName" ,"WEB_REMIX")
|
||||||
|
put("clientVersion" ,"0.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
put("query",query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,23 +16,23 @@
|
|||||||
|
|
||||||
package com.shabinder.common
|
package com.shabinder.common
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import co.touchlab.kermit.Kermit
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
import com.github.kiulian.downloader.YoutubeDownloader
|
||||||
import com.shabinder.common.PlatformDir
|
import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
||||||
import com.shabinder.spotiflyer.database.DownloadRecord
|
import com.shabinder.common.spotify.Source
|
||||||
import com.shabinder.spotiflyer.models.DownloadStatus
|
import com.shabinder.database.DownloadRecordDatabase
|
||||||
import com.shabinder.spotiflyer.models.PlatformQueryResult
|
import io.ktor.client.*
|
||||||
import com.shabinder.spotiflyer.models.TrackDetails
|
import kotlinx.coroutines.Dispatchers
|
||||||
import com.shabinder.spotiflyer.models.spotify.Source
|
import kotlinx.coroutines.withContext
|
||||||
import com.shabinder.spotiflyer.utils.log
|
import org.koin.core.KoinComponent
|
||||||
import com.shabinder.spotiflyer.utils.removeIllegalChars
|
|
||||||
import com.shabinder.spotiflyer.utils.showDialog
|
|
||||||
import java.io.File
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
class YoutubeProvider(ytDownloader: YoutubeDownloader): PlatformDir() {
|
|
||||||
|
|
||||||
|
actual class YoutubeProvider actual constructor(
|
||||||
|
private val httpClient: HttpClient,
|
||||||
|
private val database: DownloadRecordDatabase,
|
||||||
|
private val logger: Kermit,
|
||||||
|
private val dir: Dir,
|
||||||
|
){
|
||||||
|
private val ytDownloader: YoutubeDownloader = YoutubeDownloader()
|
||||||
/*
|
/*
|
||||||
* YT Album Art Schema
|
* YT Album Art Schema
|
||||||
* HI-RES Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
|
* HI-RES Url: https://i.ytimg.com/vi/$searchId/maxresdefault.jpg"
|
||||||
@ -42,11 +42,14 @@ class YoutubeProvider(ytDownloader: YoutubeDownloader): PlatformDir() {
|
|||||||
private val sampleDomain2 = "youtube.com"
|
private val sampleDomain2 = "youtube.com"
|
||||||
private val sampleDomain3 = "youtu.be"
|
private val sampleDomain3 = "youtu.be"
|
||||||
|
|
||||||
suspend fun query(fullLink: String): PlatformQueryResult?{
|
private val db: DownloadRecordDatabaseQueries
|
||||||
|
get() = database.downloadRecordDatabaseQueries
|
||||||
|
|
||||||
|
actual suspend fun query(fullLink: String): PlatformQueryResult?{
|
||||||
val link = fullLink.removePrefix("https://").removePrefix("http://")
|
val link = fullLink.removePrefix("https://").removePrefix("http://")
|
||||||
if(link.contains("playlist",true) || link.contains("list",true)){
|
if(link.contains("playlist",true) || link.contains("list",true)){
|
||||||
// Given Link is of a Playlist
|
// Given Link is of a Playlist
|
||||||
log("YT Play",link)
|
logger.i{ link }
|
||||||
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&").substringBefore("?")
|
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&").substringBefore("?")
|
||||||
return getYTPlaylist(
|
return getYTPlaylist(
|
||||||
playlistId
|
playlistId
|
||||||
@ -69,7 +72,7 @@ class YoutubeProvider(ytDownloader: YoutubeDownloader): PlatformDir() {
|
|||||||
searchId
|
searchId
|
||||||
)
|
)
|
||||||
}else{
|
}else{
|
||||||
showDialog("Your Youtube Link is not of a Video!!")
|
logger.d{"Your Youtube Link is not of a Video!!"}
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -86,9 +89,8 @@ class YoutubeProvider(ytDownloader: YoutubeDownloader): PlatformDir() {
|
|||||||
trackList = listOf(),
|
trackList = listOf(),
|
||||||
Source.YouTube
|
Source.YouTube
|
||||||
)
|
)
|
||||||
with(result) {
|
result.apply {
|
||||||
try {
|
try {
|
||||||
log("YT Playlist", searchId)
|
|
||||||
val playlist = ytDownloader.getPlaylist(searchId)
|
val playlist = ytDownloader.getPlaylist(searchId)
|
||||||
val playlistDetails = playlist.details()
|
val playlistDetails = playlist.details()
|
||||||
val name = playlistDetails.title()
|
val name = playlistDetails.title()
|
||||||
@ -105,54 +107,52 @@ class YoutubeProvider(ytDownloader: YoutubeDownloader): PlatformDir() {
|
|||||||
title = it.title(),
|
title = it.title(),
|
||||||
artists = listOf(it.author().toString()),
|
artists = listOf(it.author().toString()),
|
||||||
durationSec = it.lengthSeconds(),
|
durationSec = it.lengthSeconds(),
|
||||||
albumArt = File(imageDir + it.videoId() + ".jpeg"),
|
albumArtPath = dir.imageDir() + it.videoId() + ".jpeg",
|
||||||
source = Source.YouTube,
|
source = Source.YouTube,
|
||||||
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
||||||
downloaded = if (File(
|
downloaded = if (dir.isPresent(
|
||||||
finalOutputDir(
|
dir.finalOutputDir(
|
||||||
itemName = it.title(),
|
itemName = it.title(),
|
||||||
type = folderType,
|
type = folderType,
|
||||||
subFolder = subFolder,
|
subFolder = subFolder,
|
||||||
defaultDir
|
dir.defaultDir()
|
||||||
)
|
)
|
||||||
).exists()
|
)
|
||||||
)
|
)
|
||||||
DownloadStatus.Downloaded
|
DownloadStatus.Downloaded
|
||||||
else {
|
else {
|
||||||
DownloadStatus.NotDownloaded
|
DownloadStatus.NotDownloaded
|
||||||
},
|
},
|
||||||
outputFile = finalOutputDir(it.title(), folderType, subFolder, defaultDir,".m4a"),
|
outputFile = dir.finalOutputDir(it.title(), folderType, subFolder, dir.defaultDir(),".m4a"),
|
||||||
videoID = it.videoId()
|
videoID = it.videoId()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
databaseDAO.insert(
|
db.add(
|
||||||
DownloadRecord(
|
type = "PlayList",
|
||||||
type = "PlayList",
|
name = if (name.length > 17) {
|
||||||
name = if (name.length > 17) {
|
"${name.subSequence(0, 16)}..."
|
||||||
"${name.subSequence(0, 16)}..."
|
} else {
|
||||||
} else {
|
name
|
||||||
name
|
},
|
||||||
},
|
link = "https://www.youtube.com/playlist?list=$searchId",
|
||||||
link = "https://www.youtube.com/playlist?list=$searchId",
|
coverUrl = "https://i.ytimg.com/vi/${
|
||||||
coverUrl = "https://i.ytimg.com/vi/${
|
videos.firstOrNull()?.videoId()
|
||||||
videos.firstOrNull()?.videoId()
|
}/hqdefault.jpg",
|
||||||
}/hqdefault.jpg",
|
totalFiles = videos.size.toLong(),
|
||||||
totalFiles = videos.size,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
showDialog("An Error Occurred While Processing!")
|
logger.d{"An Error Occurred While Processing!"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return if(result.title.isNotBlank()) result
|
return if(result.title.isNotBlank()) result
|
||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@Suppress("DefaultLocale")
|
||||||
private suspend fun getYTTrack(
|
private suspend fun getYTTrack(
|
||||||
searchId:String,
|
searchId:String,
|
||||||
):PlatformQueryResult? {
|
):PlatformQueryResult? {
|
||||||
@ -163,46 +163,44 @@ class YoutubeProvider(ytDownloader: YoutubeDownloader): PlatformDir() {
|
|||||||
coverUrl = "",
|
coverUrl = "",
|
||||||
trackList = listOf(),
|
trackList = listOf(),
|
||||||
Source.YouTube
|
Source.YouTube
|
||||||
)
|
).apply{
|
||||||
with(result) {
|
|
||||||
try {
|
try {
|
||||||
log("YT Video", searchId)
|
logger.i{searchId}
|
||||||
val video = ytDownloader.getVideo(searchId)
|
val video = ytDownloader.getVideo(searchId)
|
||||||
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
||||||
val detail = video?.details()
|
val detail = video?.details()
|
||||||
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(), "", true)
|
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(), "", true)
|
||||||
?: detail?.title() ?: ""
|
?: detail?.title() ?: ""
|
||||||
log("YT View Model", detail.toString())
|
//logger.i{ detail.toString() }
|
||||||
trackList = listOf(
|
trackList = listOf(
|
||||||
TrackDetails(
|
TrackDetails(
|
||||||
title = name,
|
title = name,
|
||||||
artists = listOf(detail?.author().toString()),
|
artists = listOf(detail?.author().toString()),
|
||||||
durationSec = detail?.lengthSeconds() ?: 0,
|
durationSec = detail?.lengthSeconds() ?: 0,
|
||||||
albumArt = File(imageDir, "$searchId.jpeg"),
|
albumArtPath = dir.imageDir() + "$searchId.jpeg",
|
||||||
source = Source.YouTube,
|
source = Source.YouTube,
|
||||||
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||||
downloaded = if (File(
|
downloaded = if (dir.isPresent(
|
||||||
finalOutputDir(
|
dir.finalOutputDir(
|
||||||
itemName = name,
|
itemName = name,
|
||||||
type = folderType,
|
type = folderType,
|
||||||
subFolder = subFolder,
|
subFolder = subFolder,
|
||||||
defaultDir = defaultDir
|
defaultDir = dir.defaultDir()
|
||||||
)
|
)
|
||||||
).exists()
|
)
|
||||||
)
|
)
|
||||||
DownloadStatus.Downloaded
|
DownloadStatus.Downloaded
|
||||||
else {
|
else {
|
||||||
DownloadStatus.NotDownloaded
|
DownloadStatus.NotDownloaded
|
||||||
},
|
},
|
||||||
outputFile = finalOutputDir(name, folderType, subFolder, defaultDir,".m4a"),
|
outputFile = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir(),".m4a"),
|
||||||
videoID = searchId
|
videoID = searchId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
title = name
|
title = name
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
databaseDAO.insert(
|
db.add(
|
||||||
DownloadRecord(
|
|
||||||
type = "Track",
|
type = "Track",
|
||||||
name = if (name.length > 17) {
|
name = if (name.length > 17) {
|
||||||
"${name.subSequence(0, 16)}..."
|
"${name.subSequence(0, 16)}..."
|
||||||
@ -212,12 +210,11 @@ class YoutubeProvider(ytDownloader: YoutubeDownloader): PlatformDir() {
|
|||||||
link = "https://www.youtube.com/watch?v=$searchId",
|
link = "https://www.youtube.com/watch?v=$searchId",
|
||||||
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||||
totalFiles = 1,
|
totalFiles = 1,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
showDialog("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
|
||||||
|
@ -1,2 +1,48 @@
|
|||||||
package com.shabinder.common
|
package com.shabinder.common
|
||||||
|
|
||||||
|
import co.touchlab.kermit.Kermit
|
||||||
|
import com.shabinder.common.database.createDb
|
||||||
|
import com.shabinder.common.database.getLogger
|
||||||
|
import com.shabinder.common.providers.GaanaProvider
|
||||||
|
import com.shabinder.common.providers.SpotifyProvider
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.features.json.*
|
||||||
|
import io.ktor.client.features.json.serializer.*
|
||||||
|
import io.ktor.client.features.logging.*
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.dsl.KoinAppDeclaration
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
fun initKoin(enableNetworkLogs: Boolean = false, appDeclaration: KoinAppDeclaration = {}) =
|
||||||
|
startKoin {
|
||||||
|
appDeclaration()
|
||||||
|
modules(commonModule(enableNetworkLogs = enableNetworkLogs))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun commonModule(enableNetworkLogs: Boolean) = module {
|
||||||
|
single { Dir() }
|
||||||
|
single { createDb() }
|
||||||
|
single { Kermit(getLogger()) }
|
||||||
|
single { SpotifyProvider(get(),get(),get(),get()) }
|
||||||
|
single { GaanaProvider(get(),get(),get(),get()) }
|
||||||
|
single { YoutubeProvider(get(),get(),get(),get()) }
|
||||||
|
single { createHttpClient(enableNetworkLogs = enableNetworkLogs) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val kotlinxSerializer = KotlinxSerializer( Json {
|
||||||
|
isLenient = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
})
|
||||||
|
|
||||||
|
fun createHttpClient(enableNetworkLogs: Boolean,serializer: KotlinxSerializer = kotlinxSerializer) = HttpClient {
|
||||||
|
install(JsonFeature) {
|
||||||
|
this.serializer = serializer
|
||||||
|
}
|
||||||
|
if (enableNetworkLogs) {
|
||||||
|
install(Logging) {
|
||||||
|
logger = Logger.DEFAULT
|
||||||
|
level = LogLevel.INFO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +0,0 @@
|
|||||||
package com.shabinder.common
|
|
||||||
|
|
||||||
import com.shabinder.common.spotify.Token
|
|
||||||
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.features.json.serializer.*
|
|
||||||
import io.ktor.client.request.*
|
|
||||||
import io.ktor.client.request.forms.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
import org.kodein.di.DI
|
|
||||||
import org.kodein.di.bind
|
|
||||||
import org.kodein.di.singleton
|
|
@ -1,13 +1,13 @@
|
|||||||
package com.shabinder.common
|
package com.shabinder.common
|
||||||
|
|
||||||
expect open class PlatformDir() {
|
expect open class Dir() {
|
||||||
fun isPresent(path:String):Boolean
|
fun isPresent(path:String):Boolean
|
||||||
fun fileSeparator(): String
|
fun fileSeparator(): String
|
||||||
fun defaultDir(): String
|
fun defaultDir(): String
|
||||||
fun imageDir(): String
|
fun imageDir(): String
|
||||||
}
|
}
|
||||||
|
|
||||||
fun PlatformDir.finalOutputDir(itemName:String ,type:String, subFolder:String,defaultDir:String,extension:String = ".mp3" ): String =
|
fun Dir.finalOutputDir(itemName:String ,type:String, subFolder:String,defaultDir:String,extension:String = ".mp3" ): String =
|
||||||
defaultDir + removeIllegalChars(type) + this.fileSeparator() +
|
defaultDir + removeIllegalChars(type) + this.fileSeparator() +
|
||||||
if(subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator()} +
|
if(subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator()} +
|
||||||
removeIllegalChars(itemName) + extension
|
removeIllegalChars(itemName) + extension
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
package com.shabinder.common
|
||||||
|
|
||||||
|
import co.touchlab.kermit.Logger
|
||||||
|
import io.ktor.client.*
|
||||||
|
|
||||||
|
expect class YoutubeMusic(
|
||||||
|
logger: Logger,
|
||||||
|
httpClient: HttpClient
|
||||||
|
) {
|
||||||
|
fun getYTTracks(response: String): List<YoutubeTrack>
|
||||||
|
fun sortByBestMatch(
|
||||||
|
ytTracks: List<YoutubeTrack>,
|
||||||
|
trackName: String,
|
||||||
|
trackArtists: List<String>,
|
||||||
|
trackDurationSec: Int
|
||||||
|
): Map<String, Int>
|
||||||
|
|
||||||
|
suspend fun getYoutubeMusicResponse(query: String): String
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package com.shabinder.common
|
||||||
|
|
||||||
|
import co.touchlab.kermit.Kermit
|
||||||
|
import com.shabinder.database.DownloadRecordDatabase
|
||||||
|
import io.ktor.client.*
|
||||||
|
|
||||||
|
expect class YoutubeProvider(
|
||||||
|
httpClient: HttpClient,
|
||||||
|
database: DownloadRecordDatabase,
|
||||||
|
logger: Kermit,
|
||||||
|
dir: Dir
|
||||||
|
) {
|
||||||
|
suspend fun query(fullLink: String): PlatformQueryResult?
|
||||||
|
}
|
@ -1,14 +1,15 @@
|
|||||||
package com.shabinder.common.gaana
|
package com.shabinder.common.gaana
|
||||||
|
|
||||||
import com.shabinder.common.spotify.kotlinxSerializer
|
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.features.json.*
|
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
|
|
||||||
private const val TOKEN = "b2e6d7fbc136547a940516e9b77e5990"
|
private const val TOKEN = "b2e6d7fbc136547a940516e9b77e5990"
|
||||||
private const val BASE_URL = "https://api.gaana.com/"
|
private const val BASE_URL = "https://api.gaana.com/"
|
||||||
|
|
||||||
interface GaanaRequests {
|
interface GaanaRequests {
|
||||||
|
|
||||||
|
val httpClient:HttpClient
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Api Request: http://api.gaana.com/?type=playlist&subtype=playlist_detail&seokey=gaana-dj-hindi-top-50-1&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
|
* Api Request: http://api.gaana.com/?type=playlist&subtype=playlist_detail&seokey=gaana-dj-hindi-top-50-1&token=b2e6d7fbc136547a940516e9b77e5990&format=JSON
|
||||||
*
|
*
|
||||||
@ -91,11 +92,3 @@ interface GaanaRequests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val httpClient by lazy {
|
|
||||||
HttpClient {
|
|
||||||
install(JsonFeature) {
|
|
||||||
serializer = kotlinxSerializer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -16,16 +16,27 @@
|
|||||||
|
|
||||||
package com.shabinder.common.providers
|
package com.shabinder.common.providers
|
||||||
|
|
||||||
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.*
|
import com.shabinder.common.*
|
||||||
|
import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
||||||
import com.shabinder.common.gaana.GaanaRequests
|
import com.shabinder.common.gaana.GaanaRequests
|
||||||
import com.shabinder.common.gaana.GaanaTrack
|
import com.shabinder.common.gaana.GaanaTrack
|
||||||
import com.shabinder.common.spotify.Source
|
import com.shabinder.common.spotify.Source
|
||||||
|
import com.shabinder.database.DownloadRecordDatabase
|
||||||
|
import io.ktor.client.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class GaanaProvider: PlatformDir(),GaanaRequests {
|
class GaanaProvider(
|
||||||
|
override val httpClient: HttpClient,
|
||||||
|
private val database: DownloadRecordDatabase,
|
||||||
|
private val logger: Kermit,
|
||||||
|
private val dir: Dir,
|
||||||
|
): GaanaRequests {
|
||||||
|
|
||||||
private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
|
private val gaanaPlaceholderImageUrl = "https://a10.gaanacdn.com/images/social/gaana_social.jpg"
|
||||||
|
private val db: DownloadRecordDatabaseQueries
|
||||||
|
get() = database.downloadRecordDatabaseQueries
|
||||||
|
|
||||||
suspend fun query(fullLink: String): PlatformQueryResult?{
|
suspend fun query(fullLink: String): PlatformQueryResult?{
|
||||||
//Link Schema: https://gaana.com/type/link
|
//Link Schema: https://gaana.com/type/link
|
||||||
@ -62,12 +73,12 @@ class GaanaProvider: PlatformDir(),GaanaRequests {
|
|||||||
getGaanaSong(seokey = link).tracks.firstOrNull()?.also {
|
getGaanaSong(seokey = link).tracks.firstOrNull()?.also {
|
||||||
folderType = "Tracks"
|
folderType = "Tracks"
|
||||||
subFolder = ""
|
subFolder = ""
|
||||||
if (isPresent(
|
if (dir.isPresent(
|
||||||
finalOutputDir(
|
dir.finalOutputDir(
|
||||||
it.track_title,
|
it.track_title,
|
||||||
folderType,
|
folderType,
|
||||||
subFolder,
|
subFolder,
|
||||||
defaultDir()
|
dir.defaultDir()
|
||||||
)
|
)
|
||||||
)) {//Download Already Present!!
|
)) {//Download Already Present!!
|
||||||
it.downloaded = DownloadStatus.Downloaded
|
it.downloaded = DownloadStatus.Downloaded
|
||||||
@ -76,14 +87,12 @@ class GaanaProvider: PlatformDir(),GaanaRequests {
|
|||||||
title = it.track_title
|
title = it.track_title
|
||||||
coverUrl = it.artworkLink
|
coverUrl = it.artworkLink
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
databaseDAO.insert(
|
db.add(
|
||||||
DownloadRecord(
|
type = "Track",
|
||||||
type = "Track",
|
name = title,
|
||||||
name = title,
|
link = "https://gaana.com/$type/$link",
|
||||||
link = "https://gaana.com/$type/$link",
|
coverUrl = coverUrl,
|
||||||
coverUrl = coverUrl,
|
totalFiles = 1,
|
||||||
totalFiles = 1,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,12 +102,12 @@ class GaanaProvider: PlatformDir(),GaanaRequests {
|
|||||||
folderType = "Albums"
|
folderType = "Albums"
|
||||||
subFolder = link
|
subFolder = link
|
||||||
it.tracks.forEach { track ->
|
it.tracks.forEach { track ->
|
||||||
if (isPresent(
|
if (dir.isPresent(
|
||||||
finalOutputDir(
|
dir.finalOutputDir(
|
||||||
track.track_title,
|
track.track_title,
|
||||||
folderType,
|
folderType,
|
||||||
subFolder,
|
subFolder,
|
||||||
defaultDir()
|
dir.defaultDir()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {//Download Already Present!!
|
) {//Download Already Present!!
|
||||||
@ -109,14 +118,12 @@ class GaanaProvider: PlatformDir(),GaanaRequests {
|
|||||||
title = link
|
title = link
|
||||||
coverUrl = it.custom_artworks.size_480p
|
coverUrl = it.custom_artworks.size_480p
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
databaseDAO.insert(
|
db.add(
|
||||||
DownloadRecord(
|
type = "Album",
|
||||||
type = "Album",
|
name = title,
|
||||||
name = title,
|
link = "https://gaana.com/$type/$link",
|
||||||
link = "https://gaana.com/$type/$link",
|
coverUrl = coverUrl,
|
||||||
coverUrl = coverUrl,
|
totalFiles = trackList.size.toLong(),
|
||||||
totalFiles = trackList.size,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,12 +133,12 @@ class GaanaProvider: PlatformDir(),GaanaRequests {
|
|||||||
folderType = "Playlists"
|
folderType = "Playlists"
|
||||||
subFolder = link
|
subFolder = link
|
||||||
it.tracks.forEach { track ->
|
it.tracks.forEach { track ->
|
||||||
if (isPresent(
|
if (dir.isPresent(
|
||||||
finalOutputDir(
|
dir.finalOutputDir(
|
||||||
track.track_title,
|
track.track_title,
|
||||||
folderType,
|
folderType,
|
||||||
subFolder,
|
subFolder,
|
||||||
defaultDir()
|
dir.defaultDir()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {//Download Already Present!!
|
) {//Download Already Present!!
|
||||||
@ -143,14 +150,12 @@ class GaanaProvider: PlatformDir(),GaanaRequests {
|
|||||||
//coverUrl.value = "TODO"
|
//coverUrl.value = "TODO"
|
||||||
coverUrl = gaanaPlaceholderImageUrl
|
coverUrl = gaanaPlaceholderImageUrl
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
databaseDAO.insert(
|
db.add(
|
||||||
DownloadRecord(
|
type = "Playlist",
|
||||||
type = "Playlist",
|
name = title,
|
||||||
name = title,
|
link = "https://gaana.com/$type/$link",
|
||||||
link = "https://gaana.com/$type/$link",
|
coverUrl = coverUrl,
|
||||||
coverUrl = coverUrl,
|
totalFiles = it.tracks.size.toLong(),
|
||||||
totalFiles = it.tracks.size,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -167,12 +172,12 @@ class GaanaProvider: PlatformDir(),GaanaRequests {
|
|||||||
}
|
}
|
||||||
getGaanaArtistTracks(seokey = link).also {
|
getGaanaArtistTracks(seokey = link).also {
|
||||||
it.tracks.forEach { track ->
|
it.tracks.forEach { track ->
|
||||||
if (isPresent(
|
if (dir.isPresent(
|
||||||
finalOutputDir(
|
dir.finalOutputDir(
|
||||||
track.track_title,
|
track.track_title,
|
||||||
folderType,
|
folderType,
|
||||||
subFolder,
|
subFolder,
|
||||||
defaultDir()
|
dir.defaultDir()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {//Download Already Present!!
|
) {//Download Already Present!!
|
||||||
@ -181,14 +186,12 @@ class GaanaProvider: PlatformDir(),GaanaRequests {
|
|||||||
}
|
}
|
||||||
trackList = it.tracks.toTrackDetailsList(folderType, subFolder)
|
trackList = it.tracks.toTrackDetailsList(folderType, subFolder)
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
databaseDAO.insert(
|
db.add(
|
||||||
DownloadRecord(
|
type = "Artist",
|
||||||
type = "Artist",
|
name = artistDetails?.name ?: link,
|
||||||
name = artistDetails?.name ?: link,
|
link = "https://gaana.com/$type/$link",
|
||||||
link = "https://gaana.com/$type/$link",
|
coverUrl = coverUrl,
|
||||||
coverUrl = coverUrl,
|
totalFiles = trackList.size.toLong(),
|
||||||
totalFiles = trackList.size,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -205,8 +208,7 @@ class GaanaProvider: PlatformDir(),GaanaRequests {
|
|||||||
title = it.track_title,
|
title = it.track_title,
|
||||||
artists = it.artist.map { artist -> artist?.name.toString() },
|
artists = it.artist.map { artist -> artist?.name.toString() },
|
||||||
durationSec = it.duration,
|
durationSec = it.duration,
|
||||||
// albumArt = File(
|
albumArtPath = dir.imageDir() + (it.artworkLink.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg",
|
||||||
// imageDir + (it.artworkLink.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg"),
|
|
||||||
albumName = it.album_title,
|
albumName = it.album_title,
|
||||||
year = it.release_date,
|
year = it.release_date,
|
||||||
comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}",
|
comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}",
|
||||||
@ -214,7 +216,7 @@ class GaanaProvider: PlatformDir(),GaanaRequests {
|
|||||||
downloaded = it.downloaded ?: DownloadStatus.NotDownloaded,
|
downloaded = it.downloaded ?: DownloadStatus.NotDownloaded,
|
||||||
source = Source.Gaana,
|
source = Source.Gaana,
|
||||||
albumArtURL = it.artworkLink,
|
albumArtURL = it.artworkLink,
|
||||||
outputFile = finalOutputDir(it.track_title,type, subFolder,defaultDir(),".m4a")
|
outputFile = dir.finalOutputDir(it.track_title,type, subFolder,dir.defaultDir(),".m4a")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,13 +16,24 @@
|
|||||||
|
|
||||||
package com.shabinder.common.providers
|
package com.shabinder.common.providers
|
||||||
|
|
||||||
|
import co.touchlab.kermit.Kermit
|
||||||
import com.shabinder.common.*
|
import com.shabinder.common.*
|
||||||
|
import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
||||||
import com.shabinder.common.spotify.*
|
import com.shabinder.common.spotify.*
|
||||||
import com.shabinder.spotiflyer.database.DownloadRecord
|
import com.shabinder.database.DownloadRecordDatabase
|
||||||
|
import io.ktor.client.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class SpotifyProvider: PlatformDir(),SpotifyRequests {
|
class SpotifyProvider(
|
||||||
|
override val httpClient: HttpClient,
|
||||||
|
private val database: DownloadRecordDatabase,
|
||||||
|
private val logger: Kermit,
|
||||||
|
private val dir: Dir,
|
||||||
|
) :SpotifyRequests {
|
||||||
|
|
||||||
|
private val db:DownloadRecordDatabaseQueries
|
||||||
|
get() = database.downloadRecordDatabaseQueries
|
||||||
|
|
||||||
suspend fun query(fullLink: String): PlatformQueryResult?{
|
suspend fun query(fullLink: String): PlatformQueryResult?{
|
||||||
var spotifyLink =
|
var spotifyLink =
|
||||||
@ -70,12 +81,12 @@ class SpotifyProvider: PlatformDir(),SpotifyRequests {
|
|||||||
getTrack(link).also {
|
getTrack(link).also {
|
||||||
folderType = "Tracks"
|
folderType = "Tracks"
|
||||||
subFolder = ""
|
subFolder = ""
|
||||||
if (isPresent(
|
if (dir.isPresent(
|
||||||
finalOutputDir(
|
dir.finalOutputDir(
|
||||||
it.name.toString(),
|
it.name.toString(),
|
||||||
folderType,
|
folderType,
|
||||||
subFolder,
|
subFolder,
|
||||||
defaultDir()
|
dir.defaultDir()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {//Download Already Present!!
|
) {//Download Already Present!!
|
||||||
@ -86,14 +97,12 @@ class SpotifyProvider: PlatformDir(),SpotifyRequests {
|
|||||||
coverUrl = (it.album?.images?.elementAtOrNull(1)?.url
|
coverUrl = (it.album?.images?.elementAtOrNull(1)?.url
|
||||||
?: it.album?.images?.elementAtOrNull(0)?.url).toString()
|
?: it.album?.images?.elementAtOrNull(0)?.url).toString()
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
databaseDAO.insert(
|
db.add(
|
||||||
DownloadRecord(
|
|
||||||
type = "Track",
|
type = "Track",
|
||||||
name = title,
|
name = title,
|
||||||
link = "https://open.spotify.com/$type/$link",
|
link = "https://open.spotify.com/$type/$link",
|
||||||
coverUrl = coverUrl,
|
coverUrl = coverUrl,
|
||||||
totalFiles = 1,
|
totalFiles = 1,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,14 +111,14 @@ class SpotifyProvider: PlatformDir(),SpotifyRequests {
|
|||||||
"album" -> {
|
"album" -> {
|
||||||
val albumObject = getAlbum(link)
|
val albumObject = getAlbum(link)
|
||||||
folderType = "Albums"
|
folderType = "Albums"
|
||||||
subFolder = albumObject?.name.toString()
|
subFolder = albumObject.name.toString()
|
||||||
albumObject?.tracks?.items?.forEach {
|
albumObject.tracks?.items?.forEach {
|
||||||
if (isPresent(
|
if (dir.isPresent(
|
||||||
finalOutputDir(
|
dir.finalOutputDir(
|
||||||
it.name.toString(),
|
it.name.toString(),
|
||||||
folderType,
|
folderType,
|
||||||
subFolder,
|
subFolder,
|
||||||
defaultDir()
|
dir.defaultDir()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {//Download Already Present!!
|
) {//Download Already Present!!
|
||||||
@ -124,7 +133,7 @@ class SpotifyProvider: PlatformDir(),SpotifyRequests {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
albumObject.tracks?.items?.toTrackDetailsList(folderType, subFolder).let {it ->
|
albumObject.tracks?.items?.toTrackDetailsList(folderType, subFolder).let {
|
||||||
if (it.isNullOrEmpty()) {
|
if (it.isNullOrEmpty()) {
|
||||||
//TODO Handle Error
|
//TODO Handle Error
|
||||||
} else {
|
} else {
|
||||||
@ -133,14 +142,12 @@ class SpotifyProvider: PlatformDir(),SpotifyRequests {
|
|||||||
coverUrl = (albumObject.images?.elementAtOrNull(1)?.url
|
coverUrl = (albumObject.images?.elementAtOrNull(1)?.url
|
||||||
?: albumObject.images?.elementAtOrNull(0)?.url).toString()
|
?: albumObject.images?.elementAtOrNull(0)?.url).toString()
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
databaseDAO.insert(
|
db.add(
|
||||||
DownloadRecord(
|
type = "Album",
|
||||||
type = "Album",
|
name = title,
|
||||||
name = title,
|
link = "https://open.spotify.com/$type/$link",
|
||||||
link = "https://open.spotify.com/$type/$link",
|
coverUrl = coverUrl,
|
||||||
coverUrl = coverUrl,
|
totalFiles = trackList.size.toLong(),
|
||||||
totalFiles = trackList.size,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,14 +160,14 @@ class SpotifyProvider: PlatformDir(),SpotifyRequests {
|
|||||||
subFolder = playlistObject.name.toString()
|
subFolder = playlistObject.name.toString()
|
||||||
val tempTrackList = mutableListOf<Track>()
|
val tempTrackList = mutableListOf<Track>()
|
||||||
//log("Tracks Fetched", playlistObject.tracks?.items?.size.toString())
|
//log("Tracks Fetched", playlistObject.tracks?.items?.size.toString())
|
||||||
playlistObject?.tracks?.items?.forEach {
|
playlistObject.tracks?.items?.forEach {
|
||||||
it.track?.let { it1 ->
|
it.track?.let { it1 ->
|
||||||
if (isPresent(
|
if (dir.isPresent(
|
||||||
finalOutputDir(
|
dir.finalOutputDir(
|
||||||
it1.name.toString(),
|
it1.name.toString(),
|
||||||
folderType,
|
folderType,
|
||||||
subFolder,
|
subFolder,
|
||||||
defaultDir()
|
dir.defaultDir()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {//Download Already Present!!
|
) {//Download Already Present!!
|
||||||
@ -186,14 +193,12 @@ class SpotifyProvider: PlatformDir(),SpotifyRequests {
|
|||||||
coverUrl = playlistObject.images?.elementAtOrNull(1)?.url
|
coverUrl = playlistObject.images?.elementAtOrNull(1)?.url
|
||||||
?: playlistObject.images?.firstOrNull()?.url.toString()
|
?: playlistObject.images?.firstOrNull()?.url.toString()
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
databaseDAO.insert(
|
db.add(
|
||||||
DownloadRecord(
|
type = "Playlist",
|
||||||
type = "Playlist",
|
name = title,
|
||||||
name = title,
|
link = "https://open.spotify.com/$type/$link",
|
||||||
link = "https://open.spotify.com/$type/$link",
|
coverUrl = coverUrl,
|
||||||
coverUrl = coverUrl,
|
totalFiles = tempTrackList.size.toLong(),
|
||||||
totalFiles = tempTrackList.size,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,8 +231,7 @@ class SpotifyProvider: PlatformDir(),SpotifyRequests {
|
|||||||
title = it.name.toString(),
|
title = it.name.toString(),
|
||||||
artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(),
|
artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(),
|
||||||
durationSec = (it.duration_ms/1000).toInt(),
|
durationSec = (it.duration_ms/1000).toInt(),
|
||||||
// albumArt = File(
|
albumArtPath = dir.imageDir() + (it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast('/') + ".jpeg",
|
||||||
// imageDir + (it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast('/') + ".jpeg"),
|
|
||||||
albumName = it.album?.name,
|
albumName = it.album?.name,
|
||||||
year = it.album?.release_date,
|
year = it.album?.release_date,
|
||||||
comment = "Genres:${it.album?.genres?.joinToString()}",
|
comment = "Genres:${it.album?.genres?.joinToString()}",
|
||||||
@ -235,7 +239,7 @@ class SpotifyProvider: PlatformDir(),SpotifyRequests {
|
|||||||
downloaded = it.downloaded,
|
downloaded = it.downloaded,
|
||||||
source = Source.Spotify,
|
source = Source.Spotify,
|
||||||
albumArtURL = it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString(),
|
albumArtURL = it.album?.images?.elementAtOrNull(1)?.url ?: it.album?.images?.firstOrNull()?.url.toString(),
|
||||||
outputFile = finalOutputDir(it.name.toString(),type, subFolder,defaultDir(),".m4a")
|
outputFile = dir.finalOutputDir(it.name.toString(),type, subFolder,dir.defaultDir(),".m4a")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,241 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2021 Shabinder Singh
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.shabinder.common.providers
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import com.beust.klaxon.JsonArray
|
|
||||||
import com.beust.klaxon.JsonObject
|
|
||||||
import com.beust.klaxon.Parser
|
|
||||||
import com.shabinder.common.YoutubeTrack
|
|
||||||
import com.shabinder.spotiflyer.utils.log
|
|
||||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
|
||||||
import kotlin.math.absoluteValue
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Thanks To https://github.com/spotDL/spotify-downloader
|
|
||||||
* */
|
|
||||||
fun getYTTracks(response: String):List<YoutubeTrack>{
|
|
||||||
val youtubeTracks = mutableListOf<YoutubeTrack>()
|
|
||||||
|
|
||||||
val stringBuilder: StringBuilder = StringBuilder(response)
|
|
||||||
val responseObj: JsonObject = Parser.default().parse(stringBuilder) as JsonObject
|
|
||||||
val contentBlocks = responseObj.obj("contents")?.obj("sectionListRenderer")?.array<JsonObject>("contents")
|
|
||||||
val resultBlocks = mutableListOf<JsonArray<JsonObject>>()
|
|
||||||
if (contentBlocks != null) {
|
|
||||||
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.containsKey("itemSectionRenderer")){
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for(contents in cBlock.obj("musicShelfRenderer")?.array<JsonObject>("contents") ?: listOf()){
|
|
||||||
/**
|
|
||||||
* apparently content Blocks without an 'overlay' field don't have linkBlocks
|
|
||||||
* I have no clue what they are and why there even exist
|
|
||||||
*
|
|
||||||
if(!contents.containsKey("overlay")){
|
|
||||||
println(contents)
|
|
||||||
continue
|
|
||||||
TODO check and correct
|
|
||||||
}*/
|
|
||||||
|
|
||||||
val result = contents.obj("musicResponsiveListItemRenderer")
|
|
||||||
?.array<JsonObject>("flexColumns")
|
|
||||||
|
|
||||||
//Add the linkBlock
|
|
||||||
val linkBlock = contents.obj("musicResponsiveListItemRenderer")
|
|
||||||
?.obj("overlay")
|
|
||||||
?.obj("musicItemThumbnailOverlayRenderer")
|
|
||||||
?.obj("content")
|
|
||||||
?.obj("musicPlayButtonRenderer")
|
|
||||||
?.obj("playNavigationEndpoint")
|
|
||||||
|
|
||||||
// detailsBlock is always a list, so we just append the linkBlock to it
|
|
||||||
// instead of carrying along all the other junk from "musicResponsiveListItemRenderer"
|
|
||||||
linkBlock?.let { result?.add(it) }
|
|
||||||
result?.let { resultBlocks.add(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* We only need results that are Songs or Videos, so we filter out the rest, since
|
|
||||||
! Songs and Videos are supplied with different details, extracting all details from
|
|
||||||
! both is just carrying on redundant data, so we also have to selectively extract
|
|
||||||
! relevant details. What you need to know to understand how we do that here:
|
|
||||||
!
|
|
||||||
! Songs details are ALWAYS in the following order:
|
|
||||||
! 0 - Name
|
|
||||||
! 1 - Type (Song)
|
|
||||||
! 2 - com.shabinder.spotiflyer.models.gaana.Artist
|
|
||||||
! 3 - Album
|
|
||||||
! 4 - Duration (mm:ss)
|
|
||||||
!
|
|
||||||
! Video details are ALWAYS in the following order:
|
|
||||||
! 0 - Name
|
|
||||||
! 1 - Type (Video)
|
|
||||||
! 2 - Channel
|
|
||||||
! 3 - Viewers
|
|
||||||
! 4 - Duration (hh:mm:ss)
|
|
||||||
!
|
|
||||||
! We blindly gather all the details we get our hands on, then
|
|
||||||
! cherrypick the details we need based on their index numbers,
|
|
||||||
! we do so only if their Type is 'Song' or 'Video
|
|
||||||
*/
|
|
||||||
|
|
||||||
for(result in resultBlocks){
|
|
||||||
|
|
||||||
// Blindly gather available details
|
|
||||||
val availableDetails = mutableListOf<String>()
|
|
||||||
|
|
||||||
/*
|
|
||||||
Filter Out dummies here itself
|
|
||||||
! 'musicResponsiveListItemFlexColumnRenderer' should have more that one
|
|
||||||
! sub-block, if not its a dummy, why does the YTM response contain dummies?
|
|
||||||
! I have no clue. We skip these.
|
|
||||||
|
|
||||||
! Remember that we appended the linkBlock to result, treating that like the
|
|
||||||
! other constituents of a result block will lead to errors, hence the 'in
|
|
||||||
! result[:-1] ,i.e., skip last element in array '
|
|
||||||
*/
|
|
||||||
for(detail in result.subList(0,result.size-1)){
|
|
||||||
if(detail.obj("musicResponsiveListItemFlexColumnRenderer")?.size!! < 2) continue
|
|
||||||
|
|
||||||
// if not a dummy, collect All Variables
|
|
||||||
val details = detail.obj("musicResponsiveListItemFlexColumnRenderer")
|
|
||||||
?.obj("text")
|
|
||||||
?.array<JsonObject>("runs") ?: listOf()
|
|
||||||
for (d in details){
|
|
||||||
d["text"]?.let {
|
|
||||||
if(it.toString() != " • "){
|
|
||||||
availableDetails.add(
|
|
||||||
it.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// log("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") ){
|
|
||||||
|
|
||||||
// skip if result is in hours instead of minutes (no song is that long)
|
|
||||||
if(availableDetails[4].split(':').size != 2) continue
|
|
||||||
|
|
||||||
/*
|
|
||||||
! grab Video ID
|
|
||||||
! this is nested as [playlistEndpoint/watchEndpoint][videoId/playlistId/...]
|
|
||||||
! so hardcoding the dict keys for data look up is an ardours process, since
|
|
||||||
! the sub-block pattern is fixed even though the key isn't, we just
|
|
||||||
! reference the dict keys by index
|
|
||||||
*/
|
|
||||||
|
|
||||||
val videoId:String? = result.last().obj("watchEndpoint")?.get("videoId") as String?
|
|
||||||
val ytTrack = YoutubeTrack(
|
|
||||||
name = availableDetails[0],
|
|
||||||
type = availableDetails[1],
|
|
||||||
artist = availableDetails[2],
|
|
||||||
duration = availableDetails[4],
|
|
||||||
videoId = videoId
|
|
||||||
)
|
|
||||||
youtubeTracks.add(ytTrack)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log("YT Search",youtubeTracks.joinToString(" abc \n"))
|
|
||||||
return youtubeTracks
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
|
||||||
fun sortByBestMatch(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>()
|
|
||||||
|
|
||||||
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 trackNameWords = trackName.toLowerCase().split(" ")
|
|
||||||
|
|
||||||
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())
|
|
||||||
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)
|
|
||||||
artistMatchNumber++
|
|
||||||
}
|
|
||||||
}else{//i.e. is a Video
|
|
||||||
for (artist in trackArtists) {
|
|
||||||
if(FuzzySearch.partialRatio(artist.toLowerCase(),result.name?.toLowerCase()) > 85)
|
|
||||||
artistMatchNumber++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(artistMatchNumber == 0) {
|
|
||||||
//log("YT Api Removing", result.toString())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val artistMatch = (artistMatchNumber / trackArtists.size ) * 100
|
|
||||||
|
|
||||||
// Duration Match
|
|
||||||
/*! time match = 100 - (delta(duration)**2 / original duration * 100)
|
|
||||||
! difference in song duration (delta) is usually of the magnitude of a few
|
|
||||||
! 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)
|
|
||||||
?.minus(trackDurationSec)?.absoluteValue ?: 0
|
|
||||||
val nonMatchValue :Float= ((difference*difference).toFloat()/trackDurationSec.toFloat())
|
|
||||||
val durationMatch = 100 - (nonMatchValue*100)
|
|
||||||
|
|
||||||
val avgMatch = (artistMatch + durationMatch)/2
|
|
||||||
linksWithMatchValue[result.videoId.toString()] = avgMatch.toInt()
|
|
||||||
}
|
|
||||||
//log("YT Api Result", "$trackName - $linksWithMatchValue")
|
|
||||||
return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap()
|
|
||||||
}
|
|
@ -1,5 +1,6 @@
|
|||||||
package com.shabinder.common.spotify
|
package com.shabinder.common.spotify
|
||||||
|
|
||||||
|
import com.shabinder.common.kotlinxSerializer
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.features.auth.*
|
import io.ktor.client.features.auth.*
|
||||||
import io.ktor.client.features.auth.providers.*
|
import io.ktor.client.features.auth.providers.*
|
||||||
@ -32,8 +33,3 @@ private val spotifyAuthClient by lazy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val kotlinxSerializer = KotlinxSerializer( kotlinx.serialization.json.Json {
|
|
||||||
isLenient = true
|
|
||||||
ignoreUnknownKeys = true
|
|
||||||
})
|
|
@ -1,23 +1,16 @@
|
|||||||
package com.shabinder.common.spotify
|
package com.shabinder.common.spotify
|
||||||
|
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import io.ktor.client.features.json.*
|
|
||||||
import io.ktor.client.request.*
|
import io.ktor.client.request.*
|
||||||
|
|
||||||
private const val BASE_URL = "https://api.spotify.com/v1/"
|
private const val BASE_URL = "https://api.spotify.com/v1/"
|
||||||
|
|
||||||
private val spotifyRequestsClient by lazy {
|
|
||||||
HttpClient {
|
|
||||||
install(JsonFeature) {
|
|
||||||
serializer = kotlinxSerializer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SpotifyRequests {
|
interface SpotifyRequests {
|
||||||
|
|
||||||
|
val httpClient:HttpClient
|
||||||
|
|
||||||
suspend fun getPlaylist(playlistID: String):Playlist{
|
suspend fun getPlaylist(playlistID: String):Playlist{
|
||||||
return spotifyRequestsClient.get("$BASE_URL/playlists/$playlistID")
|
return httpClient.get("$BASE_URL/playlists/$playlistID")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getPlaylistTracks(
|
suspend fun getPlaylistTracks(
|
||||||
@ -25,26 +18,26 @@ interface SpotifyRequests {
|
|||||||
offset: Int = 0,
|
offset: Int = 0,
|
||||||
limit: Int = 100
|
limit: Int = 100
|
||||||
):PagingObjectPlaylistTrack{
|
):PagingObjectPlaylistTrack{
|
||||||
return spotifyRequestsClient.get("$BASE_URL/playlists/$playlistID/tracks?offset=$offset&limit=$limit")
|
return httpClient.get("$BASE_URL/playlists/$playlistID/tracks?offset=$offset&limit=$limit")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getTrack(id: String?):Track{
|
suspend fun getTrack(id: String?):Track{
|
||||||
return spotifyRequestsClient.get("$BASE_URL/tracks/$id")
|
return httpClient.get("$BASE_URL/tracks/$id")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getEpisode(id: String?) :Track{
|
suspend fun getEpisode(id: String?) :Track{
|
||||||
return spotifyRequestsClient.get("$BASE_URL/episodes/$id")
|
return httpClient.get("$BASE_URL/episodes/$id")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getShow(id: String?): Track{
|
suspend fun getShow(id: String?): Track{
|
||||||
return spotifyRequestsClient.get("$BASE_URL/shows/$id")
|
return httpClient.get("$BASE_URL/shows/$id")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getAlbum(id: String):Album{
|
suspend fun getAlbum(id: String):Album{
|
||||||
return spotifyRequestsClient.get("$BASE_URL/albums/$id")
|
return httpClient.get("$BASE_URL/albums/$id")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getResponse(url:String):String{
|
suspend fun getResponse(url:String):String{
|
||||||
return spotifyRequestsClient.get(url)
|
return httpClient.get(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,48 +0,0 @@
|
|||||||
package com.shabinder.common.youtube
|
|
||||||
|
|
||||||
import com.shabinder.common.gaana.httpClient
|
|
||||||
import io.ktor.client.request.*
|
|
||||||
import io.ktor.client.statement.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
import kotlinx.serialization.json.putJsonObject
|
|
||||||
|
|
||||||
private const val apiKey = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"
|
|
||||||
|
|
||||||
interface YoutubeMusic {
|
|
||||||
|
|
||||||
suspend fun getYoutubeMusicResponse(query: String):String{
|
|
||||||
return httpClient.post("https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey"){
|
|
||||||
contentType(ContentType.Application.Json)
|
|
||||||
headers{
|
|
||||||
//append("Content-Type"," application/json")
|
|
||||||
append("Referer"," https://music.youtube.com/search")
|
|
||||||
}
|
|
||||||
body = buildJsonObject {
|
|
||||||
putJsonObject("context"){
|
|
||||||
putJsonObject("client"){
|
|
||||||
put("clientName" ,"WEB_REMIX")
|
|
||||||
put("clientVersion" ,"0.1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
put("query",query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
fun makeJsonBody(query: String):JsonObject{
|
|
||||||
val client = JsonObject()
|
|
||||||
client["clientName"] = "WEB_REMIX"
|
|
||||||
client["clientVersion"] = "0.1"
|
|
||||||
|
|
||||||
val context = JsonObject()
|
|
||||||
context["client"] = client
|
|
||||||
|
|
||||||
val mainObject = JsonObject()
|
|
||||||
mainObject["context"] = context
|
|
||||||
mainObject["query"] = query
|
|
||||||
|
|
||||||
return mainObject
|
|
||||||
}*/
|
|
@ -2,7 +2,7 @@ package com.shabinder.common
|
|||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
actual open class PlatformDir{
|
actual open class Dir{
|
||||||
|
|
||||||
actual fun fileSeparator(): String = File.separator
|
actual fun fileSeparator(): String = File.separator
|
||||||
|
|
||||||
|
@ -0,0 +1,252 @@
|
|||||||
|
package com.shabinder.common
|
||||||
|
|
||||||
|
import co.touchlab.kermit.Logger
|
||||||
|
import com.beust.klaxon.JsonArray
|
||||||
|
import com.beust.klaxon.JsonObject
|
||||||
|
import com.beust.klaxon.Parser
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
|
import kotlinx.serialization.json.put
|
||||||
|
import kotlinx.serialization.json.putJsonObject
|
||||||
|
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
|
private const val apiKey = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30"
|
||||||
|
|
||||||
|
actual class YoutubeMusic actual constructor(
|
||||||
|
private val logger: Logger,
|
||||||
|
private val httpClient:HttpClient,
|
||||||
|
) {
|
||||||
|
private val tag = "YTMUSIC"
|
||||||
|
actual fun getYTTracks(response: String):List<YoutubeTrack>{
|
||||||
|
val youtubeTracks = mutableListOf<YoutubeTrack>()
|
||||||
|
|
||||||
|
val stringBuilder: StringBuilder = StringBuilder(response)
|
||||||
|
val responseObj: JsonObject = Parser.default().parse(stringBuilder) as JsonObject
|
||||||
|
val contentBlocks = responseObj.obj("contents")?.obj("sectionListRenderer")?.array<JsonObject>("contents")
|
||||||
|
val resultBlocks = mutableListOf<JsonArray<JsonObject>>()
|
||||||
|
if (contentBlocks != null) {
|
||||||
|
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.containsKey("itemSectionRenderer")){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for(contents in cBlock.obj("musicShelfRenderer")?.array<JsonObject>("contents") ?: listOf()){
|
||||||
|
/**
|
||||||
|
* apparently content Blocks without an 'overlay' field don't have linkBlocks
|
||||||
|
* I have no clue what they are and why there even exist
|
||||||
|
*
|
||||||
|
if(!contents.containsKey("overlay")){
|
||||||
|
println(contents)
|
||||||
|
continue
|
||||||
|
TODO check and correct
|
||||||
|
}*/
|
||||||
|
|
||||||
|
val result = contents.obj("musicResponsiveListItemRenderer")
|
||||||
|
?.array<JsonObject>("flexColumns")
|
||||||
|
|
||||||
|
//Add the linkBlock
|
||||||
|
val linkBlock = contents.obj("musicResponsiveListItemRenderer")
|
||||||
|
?.obj("overlay")
|
||||||
|
?.obj("musicItemThumbnailOverlayRenderer")
|
||||||
|
?.obj("content")
|
||||||
|
?.obj("musicPlayButtonRenderer")
|
||||||
|
?.obj("playNavigationEndpoint")
|
||||||
|
|
||||||
|
// detailsBlock is always a list, so we just append the linkBlock to it
|
||||||
|
// instead of carrying along all the other junk from "musicResponsiveListItemRenderer"
|
||||||
|
linkBlock?.let { result?.add(it) }
|
||||||
|
result?.let { resultBlocks.add(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We only need results that are Songs or Videos, so we filter out the rest, since
|
||||||
|
! Songs and Videos are supplied with different details, extracting all details from
|
||||||
|
! both is just carrying on redundant data, so we also have to selectively extract
|
||||||
|
! relevant details. What you need to know to understand how we do that here:
|
||||||
|
!
|
||||||
|
! Songs details are ALWAYS in the following order:
|
||||||
|
! 0 - Name
|
||||||
|
! 1 - Type (Song)
|
||||||
|
! 2 - com.shabinder.spotiflyer.models.gaana.Artist
|
||||||
|
! 3 - Album
|
||||||
|
! 4 - Duration (mm:ss)
|
||||||
|
!
|
||||||
|
! Video details are ALWAYS in the following order:
|
||||||
|
! 0 - Name
|
||||||
|
! 1 - Type (Video)
|
||||||
|
! 2 - Channel
|
||||||
|
! 3 - Viewers
|
||||||
|
! 4 - Duration (hh:mm:ss)
|
||||||
|
!
|
||||||
|
! We blindly gather all the details we get our hands on, then
|
||||||
|
! cherrypick the details we need based on their index numbers,
|
||||||
|
! we do so only if their Type is 'Song' or 'Video
|
||||||
|
*/
|
||||||
|
|
||||||
|
for(result in resultBlocks){
|
||||||
|
|
||||||
|
// Blindly gather available details
|
||||||
|
val availableDetails = mutableListOf<String>()
|
||||||
|
|
||||||
|
/*
|
||||||
|
Filter Out dummies here itself
|
||||||
|
! 'musicResponsiveListItemFlexColumnRenderer' should have more that one
|
||||||
|
! sub-block, if not its a dummy, why does the YTM response contain dummies?
|
||||||
|
! I have no clue. We skip these.
|
||||||
|
|
||||||
|
! Remember that we appended the linkBlock to result, treating that like the
|
||||||
|
! other constituents of a result block will lead to errors, hence the 'in
|
||||||
|
! result[:-1] ,i.e., skip last element in array '
|
||||||
|
*/
|
||||||
|
for(detail in result.subList(0,result.size-1)){
|
||||||
|
if(detail.obj("musicResponsiveListItemFlexColumnRenderer")?.size!! < 2) continue
|
||||||
|
|
||||||
|
// if not a dummy, collect All Variables
|
||||||
|
val details = detail.obj("musicResponsiveListItemFlexColumnRenderer")
|
||||||
|
?.obj("text")
|
||||||
|
?.array<JsonObject>("runs") ?: listOf()
|
||||||
|
for (d in details){
|
||||||
|
d["text"]?.let {
|
||||||
|
if(it.toString() != " • "){
|
||||||
|
availableDetails.add(
|
||||||
|
it.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// log("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") ){
|
||||||
|
|
||||||
|
// skip if result is in hours instead of minutes (no song is that long)
|
||||||
|
if(availableDetails[4].split(':').size != 2) continue
|
||||||
|
|
||||||
|
/*
|
||||||
|
! grab Video ID
|
||||||
|
! this is nested as [playlistEndpoint/watchEndpoint][videoId/playlistId/...]
|
||||||
|
! so hardcoding the dict keys for data look up is an ardours process, since
|
||||||
|
! the sub-block pattern is fixed even though the key isn't, we just
|
||||||
|
! reference the dict keys by index
|
||||||
|
*/
|
||||||
|
|
||||||
|
val videoId:String? = result.last().obj("watchEndpoint")?.get("videoId") as String?
|
||||||
|
val ytTrack = YoutubeTrack(
|
||||||
|
name = availableDetails[0],
|
||||||
|
type = availableDetails[1],
|
||||||
|
artist = availableDetails[2],
|
||||||
|
duration = availableDetails[4],
|
||||||
|
videoId = videoId
|
||||||
|
)
|
||||||
|
youtubeTracks.add(ytTrack)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.i(youtubeTracks.joinToString(" abc \n"),tag)
|
||||||
|
return youtubeTracks
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun sortByBestMatch(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>()
|
||||||
|
|
||||||
|
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 trackNameWords = trackName.toLowerCase().split(" ")
|
||||||
|
|
||||||
|
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())
|
||||||
|
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)
|
||||||
|
artistMatchNumber++
|
||||||
|
}
|
||||||
|
}else{//i.e. is a Video
|
||||||
|
for (artist in trackArtists) {
|
||||||
|
if(FuzzySearch.partialRatio(artist.toLowerCase(),result.name?.toLowerCase()) > 85)
|
||||||
|
artistMatchNumber++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(artistMatchNumber == 0) {
|
||||||
|
//log("YT Api Removing", result.toString())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val artistMatch = (artistMatchNumber / trackArtists.size ) * 100
|
||||||
|
|
||||||
|
// Duration Match
|
||||||
|
/*! time match = 100 - (delta(duration)**2 / original duration * 100)
|
||||||
|
! difference in song duration (delta) is usually of the magnitude of a few
|
||||||
|
! 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)
|
||||||
|
?.minus(trackDurationSec)?.absoluteValue ?: 0
|
||||||
|
val nonMatchValue :Float= ((difference*difference).toFloat()/trackDurationSec.toFloat())
|
||||||
|
val durationMatch = 100 - (nonMatchValue*100)
|
||||||
|
|
||||||
|
val avgMatch = (artistMatch + durationMatch)/2
|
||||||
|
linksWithMatchValue[result.videoId.toString()] = avgMatch.toInt()
|
||||||
|
}
|
||||||
|
//log("YT Api Result", "$trackName - $linksWithMatchValue")
|
||||||
|
return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
actual suspend fun getYoutubeMusicResponse(query: String):String{
|
||||||
|
return httpClient.post("https://music.youtube.com/youtubei/v1/search?alt=json&key=$apiKey"){
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
headers{
|
||||||
|
//append("Content-Type"," application/json")
|
||||||
|
append("Referer"," https://music.youtube.com/search")
|
||||||
|
}
|
||||||
|
body = buildJsonObject {
|
||||||
|
putJsonObject("context"){
|
||||||
|
putJsonObject("client"){
|
||||||
|
put("clientName" ,"WEB_REMIX")
|
||||||
|
put("clientVersion" ,"0.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
put("query",query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,23 +16,22 @@
|
|||||||
|
|
||||||
package com.shabinder.common
|
package com.shabinder.common
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import co.touchlab.kermit.Kermit
|
||||||
import com.github.kiulian.downloader.YoutubeDownloader
|
import com.github.kiulian.downloader.YoutubeDownloader
|
||||||
import com.shabinder.common.PlatformDir
|
import com.shabinder.common.database.DownloadRecordDatabaseQueries
|
||||||
import com.shabinder.common.providers.BaseProvider
|
import com.shabinder.common.spotify.Source
|
||||||
import com.shabinder.spotiflyer.database.DownloadRecord
|
import com.shabinder.database.DownloadRecordDatabase
|
||||||
import com.shabinder.spotiflyer.models.DownloadStatus
|
import io.ktor.client.*
|
||||||
import com.shabinder.spotiflyer.models.PlatformQueryResult
|
import kotlinx.coroutines.Dispatchers
|
||||||
import com.shabinder.spotiflyer.models.TrackDetails
|
import kotlinx.coroutines.withContext
|
||||||
import com.shabinder.spotiflyer.models.spotify.Source
|
|
||||||
import com.shabinder.spotiflyer.utils.log
|
|
||||||
import com.shabinder.spotiflyer.utils.removeIllegalChars
|
|
||||||
import com.shabinder.spotiflyer.utils.showDialog
|
|
||||||
import java.io.File
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
class YoutubeProvider(ytDownloader: YoutubeDownloader): PlatformDir() {
|
actual class YoutubeProvider actual constructor(
|
||||||
|
private val httpClient: HttpClient,
|
||||||
|
private val database: DownloadRecordDatabase,
|
||||||
|
private val logger: Kermit,
|
||||||
|
private val dir: Dir,
|
||||||
|
){
|
||||||
|
private val ytDownloader: YoutubeDownloader = YoutubeDownloader()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* YT Album Art Schema
|
* YT Album Art Schema
|
||||||
@ -43,11 +42,14 @@ class YoutubeProvider(ytDownloader: YoutubeDownloader): PlatformDir() {
|
|||||||
private val sampleDomain2 = "youtube.com"
|
private val sampleDomain2 = "youtube.com"
|
||||||
private val sampleDomain3 = "youtu.be"
|
private val sampleDomain3 = "youtu.be"
|
||||||
|
|
||||||
override suspend fun query(fullLink: String): PlatformQueryResult?{
|
private val db: DownloadRecordDatabaseQueries
|
||||||
|
get() = database.downloadRecordDatabaseQueries
|
||||||
|
|
||||||
|
actual suspend fun query(fullLink: String): PlatformQueryResult?{
|
||||||
val link = fullLink.removePrefix("https://").removePrefix("http://")
|
val link = fullLink.removePrefix("https://").removePrefix("http://")
|
||||||
if(link.contains("playlist",true) || link.contains("list",true)){
|
if(link.contains("playlist",true) || link.contains("list",true)){
|
||||||
// Given Link is of a Playlist
|
// Given Link is of a Playlist
|
||||||
log("YT Play",link)
|
logger.i{ link }
|
||||||
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&").substringBefore("?")
|
val playlistId = link.substringAfter("?list=").substringAfter("&list=").substringBefore("&").substringBefore("?")
|
||||||
return getYTPlaylist(
|
return getYTPlaylist(
|
||||||
playlistId
|
playlistId
|
||||||
@ -70,7 +72,7 @@ class YoutubeProvider(ytDownloader: YoutubeDownloader): PlatformDir() {
|
|||||||
searchId
|
searchId
|
||||||
)
|
)
|
||||||
}else{
|
}else{
|
||||||
showDialog("Your Youtube Link is not of a Video!!")
|
logger.d{"Your Youtube Link is not of a Video!!"}
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,9 +89,8 @@ class YoutubeProvider(ytDownloader: YoutubeDownloader): PlatformDir() {
|
|||||||
trackList = listOf(),
|
trackList = listOf(),
|
||||||
Source.YouTube
|
Source.YouTube
|
||||||
)
|
)
|
||||||
with(result) {
|
result.apply {
|
||||||
try {
|
try {
|
||||||
log("YT Playlist", searchId)
|
|
||||||
val playlist = ytDownloader.getPlaylist(searchId)
|
val playlist = ytDownloader.getPlaylist(searchId)
|
||||||
val playlistDetails = playlist.details()
|
val playlistDetails = playlist.details()
|
||||||
val name = playlistDetails.title()
|
val name = playlistDetails.title()
|
||||||
@ -106,54 +107,52 @@ class YoutubeProvider(ytDownloader: YoutubeDownloader): PlatformDir() {
|
|||||||
title = it.title(),
|
title = it.title(),
|
||||||
artists = listOf(it.author().toString()),
|
artists = listOf(it.author().toString()),
|
||||||
durationSec = it.lengthSeconds(),
|
durationSec = it.lengthSeconds(),
|
||||||
albumArt = File(imageDir + it.videoId() + ".jpeg"),
|
albumArtPath = dir.imageDir() + it.videoId() + ".jpeg",
|
||||||
source = Source.YouTube,
|
source = Source.YouTube,
|
||||||
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
albumArtURL = "https://i.ytimg.com/vi/${it.videoId()}/hqdefault.jpg",
|
||||||
downloaded = if (File(
|
downloaded = if (dir.isPresent(
|
||||||
finalOutputDir(
|
dir.finalOutputDir(
|
||||||
itemName = it.title(),
|
itemName = it.title(),
|
||||||
type = folderType,
|
type = folderType,
|
||||||
subFolder = subFolder,
|
subFolder = subFolder,
|
||||||
defaultDir
|
dir.defaultDir()
|
||||||
)
|
)
|
||||||
).exists()
|
)
|
||||||
)
|
)
|
||||||
DownloadStatus.Downloaded
|
DownloadStatus.Downloaded
|
||||||
else {
|
else {
|
||||||
DownloadStatus.NotDownloaded
|
DownloadStatus.NotDownloaded
|
||||||
},
|
},
|
||||||
outputFile = finalOutputDir(it.title(), folderType, subFolder, defaultDir,".m4a"),
|
outputFile = dir.finalOutputDir(it.title(), folderType, subFolder, dir.defaultDir(),".m4a"),
|
||||||
videoID = it.videoId()
|
videoID = it.videoId()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
databaseDAO.insert(
|
db.add(
|
||||||
DownloadRecord(
|
type = "PlayList",
|
||||||
type = "PlayList",
|
name = if (name.length > 17) {
|
||||||
name = if (name.length > 17) {
|
"${name.subSequence(0, 16)}..."
|
||||||
"${name.subSequence(0, 16)}..."
|
} else {
|
||||||
} else {
|
name
|
||||||
name
|
},
|
||||||
},
|
link = "https://www.youtube.com/playlist?list=$searchId",
|
||||||
link = "https://www.youtube.com/playlist?list=$searchId",
|
coverUrl = "https://i.ytimg.com/vi/${
|
||||||
coverUrl = "https://i.ytimg.com/vi/${
|
videos.firstOrNull()?.videoId()
|
||||||
videos.firstOrNull()?.videoId()
|
}/hqdefault.jpg",
|
||||||
}/hqdefault.jpg",
|
totalFiles = videos.size.toLong(),
|
||||||
totalFiles = videos.size,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
showDialog("An Error Occurred While Processing!")
|
logger.d{"An Error Occurred While Processing!"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return if(result.title.isNotBlank()) result
|
return if(result.title.isNotBlank()) result
|
||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@Suppress("DefaultLocale")
|
||||||
private suspend fun getYTTrack(
|
private suspend fun getYTTrack(
|
||||||
searchId:String,
|
searchId:String,
|
||||||
):PlatformQueryResult? {
|
):PlatformQueryResult? {
|
||||||
@ -164,46 +163,44 @@ class YoutubeProvider(ytDownloader: YoutubeDownloader): PlatformDir() {
|
|||||||
coverUrl = "",
|
coverUrl = "",
|
||||||
trackList = listOf(),
|
trackList = listOf(),
|
||||||
Source.YouTube
|
Source.YouTube
|
||||||
)
|
).apply{
|
||||||
with(result) {
|
|
||||||
try {
|
try {
|
||||||
log("YT Video", searchId)
|
logger.i{searchId}
|
||||||
val video = ytDownloader.getVideo(searchId)
|
val video = ytDownloader.getVideo(searchId)
|
||||||
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg"
|
||||||
val detail = video?.details()
|
val detail = video?.details()
|
||||||
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(), "", true)
|
val name = detail?.title()?.replace(detail.author()!!.toUpperCase(), "", true)
|
||||||
?: detail?.title() ?: ""
|
?: detail?.title() ?: ""
|
||||||
log("YT View Model", detail.toString())
|
//logger.i{ detail.toString() }
|
||||||
trackList = listOf(
|
trackList = listOf(
|
||||||
TrackDetails(
|
TrackDetails(
|
||||||
title = name,
|
title = name,
|
||||||
artists = listOf(detail?.author().toString()),
|
artists = listOf(detail?.author().toString()),
|
||||||
durationSec = detail?.lengthSeconds() ?: 0,
|
durationSec = detail?.lengthSeconds() ?: 0,
|
||||||
albumArt = File(imageDir, "$searchId.jpeg"),
|
albumArtPath = dir.imageDir() + "$searchId.jpeg",
|
||||||
source = Source.YouTube,
|
source = Source.YouTube,
|
||||||
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||||
downloaded = if (File(
|
downloaded = if (dir.isPresent(
|
||||||
finalOutputDir(
|
dir.finalOutputDir(
|
||||||
itemName = name,
|
itemName = name,
|
||||||
type = folderType,
|
type = folderType,
|
||||||
subFolder = subFolder,
|
subFolder = subFolder,
|
||||||
defaultDir = defaultDir
|
defaultDir = dir.defaultDir()
|
||||||
)
|
)
|
||||||
).exists()
|
)
|
||||||
)
|
)
|
||||||
DownloadStatus.Downloaded
|
DownloadStatus.Downloaded
|
||||||
else {
|
else {
|
||||||
DownloadStatus.NotDownloaded
|
DownloadStatus.NotDownloaded
|
||||||
},
|
},
|
||||||
outputFile = finalOutputDir(name, folderType, subFolder, defaultDir,".m4a"),
|
outputFile = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir(),".m4a"),
|
||||||
videoID = searchId
|
videoID = searchId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
title = name
|
title = name
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
databaseDAO.insert(
|
db.add(
|
||||||
DownloadRecord(
|
|
||||||
type = "Track",
|
type = "Track",
|
||||||
name = if (name.length > 17) {
|
name = if (name.length > 17) {
|
||||||
"${name.subSequence(0, 16)}..."
|
"${name.subSequence(0, 16)}..."
|
||||||
@ -213,12 +210,11 @@ class YoutubeProvider(ytDownloader: YoutubeDownloader): PlatformDir() {
|
|||||||
link = "https://www.youtube.com/watch?v=$searchId",
|
link = "https://www.youtube.com/watch?v=$searchId",
|
||||||
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
coverUrl = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg",
|
||||||
totalFiles = 1,
|
totalFiles = 1,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
showDialog("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
|
||||||
|
Loading…
Reference in New Issue
Block a user