Dep Updates, Maintenance Tasks Updated

This commit is contained in:
shabinder 2021-06-06 00:13:55 +05:30
parent 6b10d63675
commit 4b18c099b6
38 changed files with 539 additions and 919 deletions

View File

@ -131,7 +131,7 @@ dependencies {
//implementation("com.jakewharton.timber:timber:4.7.1")
implementation("dev.icerock.moko:parcelize:0.6.1")
implementation("com.github.shabinder:storage-chooser:2.0.4.45")
implementation("com.google.accompanist:accompanist-insets:0.9.1")
implementation("com.google.accompanist:accompanist-insets:0.11.1")
// Test
testImplementation("junit:junit:4.13.2")

View File

@ -32,6 +32,7 @@ allprojects {
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "1.8"
useIR = true
}
}

View File

@ -22,7 +22,7 @@ object Versions {
const val versionCode = 20
// Kotlin
const val kotlinVersion = "1.4.32"
const val kotlinVersion = "1.5.10"
const val coroutinesVersion = "1.4.2"
// Code Formatting
@ -51,6 +51,7 @@ object Versions {
const val targetSdkVersion = 29
const val androidLifecycle = "2.3.0"
}
object HostOS {
// Host OS Properties
private val hostOs = System.getProperty("os.name")
@ -58,12 +59,14 @@ object HostOS {
val isMac = hostOs.startsWith("Mac",true)
val isLinux = hostOs.startsWith("Linux",true)
}
object Koin {
val core = "io.insert-koin:koin-core:${Versions.koin}"
val test = "io.insert-koin:koin-test:${Versions.koin}"
val android = "io.insert-koin:koin-android:${Versions.koin}"
val compose = "io.insert-koin:koin-androidx-compose:3.0.1"
}
object Androidx {
const val androidxActivity = "androidx.activity:activity-compose:1.3.0-alpha07"
const val core = "androidx.core:core-ktx:1.3.2"
@ -83,6 +86,7 @@ object Androidx {
const val runtimeLiveData = "androidx.compose.runtime:runtime-livedata:${Versions.compose}"
}*/
}
object JetBrains {
object Kotlin {
const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.kotlinVersion}"
@ -94,17 +98,19 @@ object JetBrains {
object Compose {
// __LATEST_COMPOSE_RELEASE_VERSION__
const val VERSION = "0.4.0-build188"
const val VERSION = "0.4.0"
const val gradlePlugin = "org.jetbrains.compose:compose-gradle-plugin:$VERSION"
}
}
object Decompose {
private const val VERSION = "0.2.3"
private const val VERSION = "0.2.6"
const val decompose = "com.arkivanov.decompose:decompose:$VERSION"
const val decomposeIosX64 = "com.arkivanov.decompose:decompose-iosx64:$VERSION"
const val decomposeIosArm64 = "com.arkivanov.decompose:decompose-iosarm64:$VERSION"
const val extensionsCompose = "com.arkivanov.decompose:extensions-compose-jetbrains:$VERSION"
}
object MVIKotlin {
private const val VERSION = "2.0.3"
const val rx = "com.arkivanov.mvikotlin:rx:$VERSION"
@ -118,6 +124,7 @@ object MVIKotlin {
const val mvikotlinTimeTravel = "com.arkivanov.mvikotlin:mvikotlin-timetravel:$VERSION"
const val mvikotlinExtensionsReaktive = "com.arkivanov.mvikotlin:mvikotlin-extensions-reaktive:$VERSION"
}
object Ktor {
val clientCore = "io.ktor:ktor-client-core:${Versions.ktor}"
val clientJson = "io.ktor:ktor-client-json:${Versions.ktor}"

View File

@ -23,16 +23,8 @@ plugins {
}
kotlin {
jvm("desktop").compilations.all {
kotlinOptions {
useIR = true
}
}
android().compilations.all {
kotlinOptions {
useIR = true
}
}
jvm("desktop")
android()
sourceSets {
named("commonMain") {
dependencies {
@ -51,7 +43,7 @@ kotlin {
implementation(Extras.kermit)
implementation("dev.icerock.moko:parcelize:0.6.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3-native-mt") {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0-native-mt") {
@Suppress("DEPRECATION")
isForce = true
}
@ -69,8 +61,4 @@ kotlin {
}
}
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}
}

View File

@ -31,25 +31,12 @@ kotlin {
}
}
jvm("desktop").compilations.all {
kotlinOptions {
useIR = true
}
}
android().compilations.all {
kotlinOptions {
useIR = true
}
}
jvm("desktop")
android()
js {
/*
* TODO Enable JS IR Compiler
* waiting for Decompose & MVI Kotlin to support same
* */
js(/*BOTH*/) {
browser()
// nodejs()
binaries.executable()
}
sourceSets {
named("commonTest") {
@ -73,8 +60,4 @@ kotlin {
dependencies {}
}
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}
}

View File

@ -34,24 +34,12 @@ kotlin {
}
}
jvm("desktop").compilations.all {
kotlinOptions {
useIR = true
}
}
android().compilations.all {
kotlinOptions {
useIR = true
}
}
js {
/*
* TODO Enable JS IR Compiler
* waiting for Decompose & MVI Kotlin to support same
* */
jvm("desktop")
android()
js(/*BOTH*/) {
browser()
// nodejs()
binaries.executable()
}
sourceSets {
@ -75,10 +63,10 @@ kotlin {
// Extras
implementation(Extras.kermit)
implementation(Serialization.json)
implementation("co.touchlab:stately-common:1.1.7")
implementation("dev.icerock.moko:parcelize:0.6.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3-native-mt") {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0-native-mt") {
@Suppress("DEPRECATION")
isForce = true
}
@ -87,7 +75,7 @@ kotlin {
named("androidMain") {
dependencies {
implementation("androidx.appcompat:appcompat:1.2.0")
implementation("androidx.appcompat:appcompat:1.3.0")
implementation(Androidx.core)
implementation(compose.runtime)
implementation(compose.material)
@ -127,8 +115,4 @@ kotlin {
}
}
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}
}

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=e2774e6fb77c43657decde25542dea710aafd78c4022d19b196e7e78d79d8c6c
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip
distributionSha256Sum=7faa7198769f872826c8ef4f1450f839ec27f0b4d5d1e51bade63667cbccd205
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -18,14 +18,13 @@ application {
}
dependencies {
implementation(Extras.fuzzyWuzzy)
implementation("org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlinVersion}")
implementation("io.ktor:ktor-client-core:1.5.4")
implementation("io.ktor:ktor-client-apache:1.5.4")
implementation("io.ktor:ktor-client-logging:1.5.4")
implementation(Ktor.slf4j)
implementation("io.ktor:ktor-client-serialization:1.5.4")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1")
implementation(Ktor.clientCore)
implementation(Ktor.clientJson)
implementation(Ktor.clientApache)
implementation(Ktor.clientLogging)
implementation(Ktor.clientSerialization)
implementation(Serialization.json)
// testDeps
testImplementation(kotlin("test-junit"))
}
@ -33,10 +32,3 @@ dependencies {
tasks.test {
useJUnit()
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions {
jvmTarget = "1.8"
useIR = true
}
}

View File

@ -1,88 +0,0 @@
package analytics_html_img
import io.ktor.client.features.timeout
import io.ktor.client.request.head
import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.runBlocking
import utils.RETRY_LIMIT_EXHAUSTED
import utils.debug
internal fun updateAnalyticsImage() {
val secrets = Secrets.initSecrets()
// debug("fun main: secrets -> $secrets")
runBlocking {
val oldGithubFile = GithubService.getGithubFileContent(
token = secrets.githubToken,
ownerName = secrets.ownerName,
repoName = secrets.repoName,
branchName = secrets.branchName,
fileName = "README.md"
)
// debug("OLD FILE CONTENT",oldGithubFile)
val imageURL = getAnalyticsImage().also {
debug("Updated IMAGE", it)
}
val replacementText = """
${Common.START_SECTION(secrets.tagName)}
![Today's Analytics]($imageURL)
${Common.END_SECTION(secrets.tagName)}
""".trimIndent()
debug("Updated Text to be Inserted", replacementText)
val regex = """${Common.START_SECTION(secrets.tagName)}(?s)(.*)${Common.END_SECTION(secrets.tagName)}""".toRegex()
val updatedContent = regex.replace(
oldGithubFile.decryptedContent,
replacementText
)
// debug("Updated File Content",updatedContent)
val updationResponse = GithubService.updateGithubFileContent(
token = secrets.githubToken,
ownerName = secrets.ownerName,
repoName = secrets.repoName,
branchName = secrets.branchName,
fileName = secrets.filePath,
commitMessage = secrets.commitMessage,
rawContent = updatedContent,
sha = oldGithubFile.sha
)
debug("File Updation Response", updationResponse.toString())
}
}
internal suspend fun getAnalyticsImage(): String {
var contentLength: Long
var analyticsImage: String
var retryCount = 5
do {
/*
* Get a new Image from Analytics,
* - Use Any Random useless query param ,
* As HCTI Demo, `caches value for a specific Link`
* */
val randomID = (1..100000).random()
analyticsImage = HCTIService.getImageURLFromURL(
url = "https://kind-grasshopper-73.telebit.io/matomo/index.php?module=Widgetize&action=iframe&containerId=VisitOverviewWithGraph&disableLink=0&widget=1&moduleToWidgetize=CoreHome&actionToWidgetize=renderWidgetContainer&idSite=1&period=day&date=yesterday&disableLink=1&widget=$randomID",
delayInMilliSeconds = 5000
)
// Sometimes we get incomplete image, hence verify `content-length`
val req = client.head<HttpResponse>(analyticsImage) {
timeout {
socketTimeoutMillis = 100_000
}
}
contentLength = req.headers["Content-Length"]?.toLong() ?: 0
debug(contentLength.toString())
if(retryCount-- == 0){
// FAIL Gracefully
throw(RETRY_LIMIT_EXHAUSTED())
}
}while (contentLength<1_20_000)
return analyticsImage
}

View File

@ -1,25 +0,0 @@
package audio_conversion
@Suppress("EnumEntryName")
enum class AudioQuality(val kbps: String) {
`128KBPS`("128"),
`160KBPS`("160"),
`192KBPS`("192"),
`224KBPS`("224"),
`256KBPS`("256"),
`320KBPS`("320");
companion object {
fun getQuality(kbps: String): AudioQuality {
return when (kbps) {
"128" -> `128KBPS`
"160" -> `160KBPS`
"192" -> `192KBPS`
"224" -> `224KBPS`
"256" -> `256KBPS`
"320" -> `320KBPS`
else -> `160KBPS`
}
}
}
}

View File

@ -1,101 +0,0 @@
package audio_conversion
import analytics_html_img.client
import io.ktor.client.request.forms.formData
import io.ktor.client.request.forms.submitFormWithBinaryData
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.headers
import io.ktor.client.statement.HttpStatement
import io.ktor.http.isSuccess
import kotlinx.coroutines.delay
import utils.debug
object AudioToMp3 {
suspend fun convertToMp3(
URL: String,
audioQuality: AudioQuality = AudioQuality.getQuality(URL.substringBeforeLast(".").takeLast(3)),
): String? {
val activeHost = getHost() // ex - https://hostveryfast.onlineconverter.com/file/send
val jobLink = convertRequest(URL, activeHost, audioQuality) // ex - https://www.onlineconverter.com/convert/309a0f2bbaeb5687b04f96b6d65b47bfdd
// (jobStatus.contains("d")) == COMPLETION
var jobStatus: String
var retryCount = 40 // Set it to optimal level
do {
jobStatus = try {
client.get(
"${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}"
)
} catch (e: Exception) {
e.printStackTrace()
""
}
retryCount--
debug("Job Status", jobStatus)
if (!jobStatus.contains("d")) delay(400) // Add Delay , to give Server Time to process audio
} while (!jobStatus.contains("d", true) && retryCount != 0)
return if (jobStatus.equals("d", true)) {
// Return MP3 Download Link
"${activeHost.removeSuffix("send")}${jobLink.substringAfterLast("/")}/download"
} else null
}
/*
* Response Link Ex : `https://www.onlineconverter.com/convert/11affb6d88d31861fe5bcd33da7b10a26c`
* - to start the conversion
* */
private suspend fun convertRequest(
URL: String,
host: String? = null,
audioQuality: AudioQuality = AudioQuality.`320KBPS`,
): String {
val activeHost = host ?: getHost()
val res = client.submitFormWithBinaryData<String>(
url = activeHost,
formData = formData {
append("class", "audio")
append("from", "audio")
append("to", "mp3")
append("source", "url")
append("url", URL.replace("https:", "http:"))
append("audio_quality", audioQuality.kbps)
}
) {
headers {
header("Host", activeHost.getHostDomain().also { debug(it) })
header("Origin", "https://www.onlineconverter.com")
header("Referer", "https://www.onlineconverter.com/")
}
}.run {
debug(this)
dropLast(3) // last 3 are useless unicode char
}
val job = client.get<HttpStatement>(res) {
headers {
header("Host", "www.onlineconverter.com")
}
}.execute()
debug("Schedule Job ${job.status.isSuccess()}")
return res
}
// Active Host free to process conversion
// ex - https://hostveryfast.onlineconverter.com/file/send
private suspend fun getHost(): String {
return client.get<String>("https://www.onlineconverter.com/get/host") {
headers {
header("Host", "www.onlineconverter.com")
}
}.also { debug("Active Host", it) }
}
// Extract full Domain from URL
// ex - hostveryfast.onlineconverter.com
private fun String.getHostDomain(): String {
return this.removePrefix("https://").substringBeforeLast(".") + "." + this.substringAfterLast(".").substringBefore("/")
}
}

View File

@ -1,6 +1,6 @@
@file:Suppress("FunctionName")
package analytics_html_img
package common
import io.ktor.client.HttpClient
import io.ktor.client.features.HttpTimeout
@ -18,6 +18,7 @@ internal object Common {
fun END_SECTION(tagName: String = "HTI") = "<!--END_SECTION:$tagName-->"
const val USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:88.0) Gecko/20100101 Firefox/88.0"
}
internal val client = HttpClient {
install(HttpTimeout)
install(JsonFeature) {
@ -33,7 +34,3 @@ internal val client = HttpClient {
level = LogLevel.INFO
}
}
internal data class GithubFileContent(
val decryptedContent: String,
val sha: String
)

View File

@ -0,0 +1,30 @@
package common
/*
* Helper Function to Replace Obsolete Content with new Updated Content
* */
fun getUpdatedContent(
oldContent: String,
newInsertionText: String,
tagName: String
): String{
return getReplaceableRegex(tagName).replace(
oldContent,
getReplacementText(tagName,newInsertionText)
)
}
private fun getReplaceableRegex(tagName: String): Regex {
return """${Common.START_SECTION(tagName)}(?s)(.*)${Common.END_SECTION(tagName)}""".toRegex()
}
private fun getReplacementText(
tagName: String,
newInsertionText: String
): String {
return """
${Common.START_SECTION(tagName)}
$newInsertionText
${Common.END_SECTION(tagName)}
""".trimIndent()
}

View File

@ -0,0 +1,16 @@
package common
import java.util.*
fun getTodayDate(): String {
val c: Calendar = Calendar.getInstance()
val monthName = arrayOf(
"January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November",
"December"
)
val month = monthName[c.get(Calendar.MONTH)]
val year: Int = c.get(Calendar.YEAR)
val date: Int = c.get(Calendar.DATE)
return " $date $month, $year"
}

View File

@ -1,4 +1,4 @@
package analytics_html_img
package common
import io.ktor.client.request.get
import io.ktor.client.request.header
@ -8,15 +8,38 @@ import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.util.InternalAPI
import io.ktor.util.encodeBase64
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import models.github.GithubFileContent
import models.github.GithubReleasesInfo
internal object GithubService {
private const val baseURL = Common.GITHUB_API
suspend fun getGithubRepoReleasesInfo(
ownerName: String,
repoName: String,
): GithubReleasesInfo {
return client.get<GithubReleasesInfo>("$baseURL/repos/$ownerName/$repoName/releases")
}
suspend fun getGithubFileContent(
secrets: Secrets,
fileName: String = "README.md"
): GithubFileContent {
return getGithubFileContent(
token = secrets.githubToken,
ownerName = secrets.ownerName,
repoName = secrets.repoName,
branchName = secrets.branchName,
fileName = fileName
)
}
suspend fun getGithubFileContent(
token: String,
ownerName: String,

View File

@ -1,4 +1,4 @@
package analytics_html_img
package common
import io.ktor.client.request.header
import io.ktor.client.request.headers

View File

@ -1,4 +1,4 @@
package analytics_html_img
package common
import utils.byOptionalProperty
import utils.byProperty

View File

@ -1,289 +0,0 @@
package jiosaavn
import analytics_html_img.client
import audio_conversion.AudioToMp3
import io.github.shabinder.fuzzywuzzy.diffutils.FuzzySearch
import io.ktor.client.request.forms.FormDataContent
import io.ktor.client.request.get
import io.ktor.http.Parameters
import jiosaavn.models.SaavnAlbum
import jiosaavn.models.SaavnPlaylist
import jiosaavn.models.SaavnSearchResult
import jiosaavn.models.SaavnSong
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import utils.debug
val serializer = Json {
ignoreUnknownKeys = true
isLenient = true
}
interface JioSaavnRequests {
suspend fun findSongDownloadURL(
trackName: String,
trackArtists: List<String>,
): String? {
val songs = searchForSong(trackName)
val bestMatches = sortByBestMatch(songs, trackName, trackArtists)
val m4aLink = bestMatches.keys.firstOrNull()?.let {
getSongFromID(it).media_url
}
val mp3Link = m4aLink?.let { AudioToMp3.convertToMp3(it) }
return mp3Link
}
suspend fun searchForSong(
query: String,
includeLyrics: Boolean = false
): List<SaavnSearchResult> {
/*if (query.startsWith("http") && query.contains("saavn.com")) {
return listOf(getSong(query))
}*/
val searchURL = search_base_url + query
val results = mutableListOf<SaavnSearchResult>()
(serializer.parseToJsonElement(client.get(searchURL)) as JsonObject).getJsonObject("songs").getJsonArray("data")?.forEach {
(it as? JsonObject)?.formatData()?.let { jsonObject ->
results.add(serializer.decodeFromJsonElement(SaavnSearchResult.serializer(), jsonObject))
}
}
return results
}
suspend fun getLyrics(ID: String): String? {
return (Json.parseToJsonElement(client.get(lyrics_base_url + ID)) as JsonObject)
.getString("lyrics")
}
suspend fun getSong(
URL: String,
fetchLyrics: Boolean = false
): SaavnSong {
val id = getSongID(URL)
val data = ((serializer.parseToJsonElement(client.get(song_details_base_url + id)) as JsonObject)[id] as JsonObject)
.formatData(fetchLyrics)
return serializer.decodeFromJsonElement(SaavnSong.serializer(), data)
}
suspend fun getSongFromID(
ID: String,
fetchLyrics: Boolean = false
): SaavnSong {
val data = ((serializer.parseToJsonElement(client.get(song_details_base_url + ID)) as JsonObject)[ID] as JsonObject)
.formatData(fetchLyrics)
return serializer.decodeFromJsonElement(SaavnSong.serializer(), data)
}
private suspend fun getSongID(
URL: String,
): String {
val res = client.get<String>(URL) {
body = FormDataContent(
Parameters.build {
append("bitrate", "320")
}
)
}
return try {
res.split("\"song\":{\"type\":\"")[1].split("\",\"image\":")[0].split("\"id\":\"").last()
} catch (e: IndexOutOfBoundsException) {
res.split("\"pid\":\"")[1].split("\",\"").first()
}
}
suspend fun getPlaylist(
URL: String,
includeLyrics: Boolean = false
): SaavnPlaylist? {
return try {
serializer.decodeFromJsonElement(
SaavnPlaylist.serializer(),
(serializer.parseToJsonElement(client.get(playlist_details_base_url + getPlaylistID(URL))) as JsonObject)
.formatData(includeLyrics)
)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
private suspend fun getPlaylistID(
URL: String
): String {
val res = client.get<String>(URL)
return try {
res.split("\"type\":\"playlist\",\"id\":\"")[1].split('"')[0]
} catch (e: IndexOutOfBoundsException) {
res.split("\"page_id\",\"")[1].split("\",\"")[0]
}
}
suspend fun getAlbum(
URL: String,
includeLyrics: Boolean = false
): SaavnAlbum? {
return try {
serializer.decodeFromJsonElement(
SaavnAlbum.serializer(),
(serializer.parseToJsonElement(client.get(album_details_base_url + getAlbumID(URL))) as JsonObject)
.formatData(includeLyrics)
)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
private suspend fun getAlbumID(
URL: String
): String {
val res = client.get<String>(URL)
return try {
res.split("\"album_id\":\"")[1].split('"')[0]
} catch (e: IndexOutOfBoundsException) {
res.split("\"page_id\",\"")[1].split("\",\"")[0]
}
}
private suspend fun JsonObject.formatData(
includeLyrics: Boolean = false
): JsonObject {
return buildJsonObject {
// Accommodate Incoming Json Object Data
// And `Format` everything while iterating
this@formatData.forEach {
if (it.value is JsonPrimitive && it.value.jsonPrimitive.isString) {
put(it.key, it.value.jsonPrimitive.content.format())
} else {
// Format Songs Nested Collection Too
if (it.key == "songs" && it.value is JsonArray) {
put(
it.key,
buildJsonArray {
getJsonArray("songs")?.forEach { song ->
(song as? JsonObject)?.formatData(includeLyrics)?.let { formattedSong ->
add(formattedSong)
}
}
}
)
} else {
put(it.key, it.value)
}
}
}
try {
var url = getString("media_preview_url")!!.replace("preview", "aac") // We Will catch NPE
url = if (getBoolean("320kbps") == true) {
url.replace("_96_p.mp4", "_320.mp4")
} else {
url.replace("_96_p.mp4", "_160.mp4")
}
// Add Media URL to JSON Object
put("media_url", url)
} catch (e: Exception) {
// e.printStackTrace()
// DECRYPT Encrypted Media URL
getString("encrypted_media_url")?.let {
put("media_url", decryptURL(it))
}
// Check if 320 Kbps is available or not
if (getBoolean("320kbps") != true && containsKey("media_url")) {
put("media_url", getString("media_url")?.replace("_320.mp4", "_160.mp4"))
}
}
// Increase Image Resolution
put(
"image",
getString("image")
?.replace("150x150", "500x500")
?.replace("50x50", "500x500")
)
// Fetch Lyrics if Requested
// Lyrics is HTML Based
if (includeLyrics) {
if (getBoolean("has_lyrics") == true) {
put("lyrics", getString("id")?.let { getLyrics(it) })
} else {
put("lyrics", "")
}
}
}
}
fun sortByBestMatch(
tracks: List<SaavnSearchResult>,
trackName: String,
trackArtists: List<String>,
): Map<String, Float> {
/*
* "linksWithMatchValue" is map with Saavn VideoID and its rating/match with 100 as Max Value
**/
val linksWithMatchValue = mutableMapOf<String, Float>()
for (result in tracks) {
var hasCommonWord = false
val resultName = result.title.toLowerCase().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) {
debug("Saavn Removing Common Word: ", 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
// String Containing All Artist Names from JioSaavn Search Result
val artistListString = mutableSetOf<String>().apply {
result.more_info?.singers?.split(",")?.let { addAll(it) }
result.more_info?.primary_artists?.toLowerCase()?.split(",")?.let { addAll(it) }
}.joinToString(" , ")
for (artist in trackArtists) {
if (FuzzySearch.partialRatio(artist.toLowerCase(), artistListString) > 85)
artistMatchNumber++
}
if (artistMatchNumber == 0) {
debug("Artist Match Saavn Removing: $result")
continue
}
val artistMatch: Float = (artistMatchNumber.toFloat() / trackArtists.size) * 100
val nameMatch: Float = FuzzySearch.partialRatio(resultName, trackName).toFloat() / 100
val avgMatch = (artistMatch + nameMatch) / 2
linksWithMatchValue[result.id] = avgMatch
}
return linksWithMatchValue.toList().sortedByDescending { it.second }.toMap().also {
debug("Match Found for $trackName - ${!it.isNullOrEmpty()}")
}
}
companion object {
// EndPoints
const val search_base_url = "https://www.jiosaavn.com/api.php?__call=autocomplete.get&_format=json&_marker=0&cc=in&includeMetaTags=1&query="
const val song_details_base_url = "https://www.jiosaavn.com/api.php?__call=song.getDetails&cc=in&_marker=0%3F_marker%3D0&_format=json&pids="
const val album_details_base_url = "https://www.jiosaavn.com/api.php?__call=content.getAlbumDetails&_format=json&cc=in&_marker=0%3F_marker%3D0&albumid="
const val playlist_details_base_url = "https://www.jiosaavn.com/api.php?__call=playlist.getDetails&_format=json&cc=in&_marker=0%3F_marker%3D0&listid="
const val lyrics_base_url = "https://www.jiosaavn.com/api.php?__call=lyrics.getLyrics&ctx=web6dot0&api_version=4&_format=json&_marker=0%3F_marker%3D0&lyrics_id="
}
}

View File

@ -1,121 +0,0 @@
package jiosaavn
import io.ktor.util.InternalAPI
import io.ktor.util.decodeBase64Bytes
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import utils.unescape
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.DESKeySpec
internal suspend fun JsonObject.formatData(
includeLyrics: Boolean = false
): JsonObject {
return buildJsonObject {
// Accommodate Incoming Json Object Data
// And `Format` everything while iterating
this@formatData.forEach {
if (it.value is JsonPrimitive && it.value.jsonPrimitive.isString) {
put(it.key, it.value.jsonPrimitive.content.format())
} else {
// Format Songs Nested Collection Too
if (it.key == "songs" && it.value is JsonArray) {
put(
it.key,
buildJsonArray {
getJsonArray("songs")?.forEach { song ->
(song as? JsonObject)?.formatData(includeLyrics)?.let { formattedSong ->
add(formattedSong)
}
}
}
)
} else {
put(it.key, it.value)
}
}
}
try {
var url = getString("media_preview_url")!!.replace("preview", "aac") // We Will catch NPE
url = if (getBoolean("320kbps") == true) {
url.replace("_96_p.mp4", "_320.mp4")
} else {
url.replace("_96_p.mp4", "_160.mp4")
}
// Add Media URL to JSON Object
put("media_url", url)
} catch (e: Exception) {
// e.printStackTrace()
// DECRYPT Encrypted Media URL
getString("encrypted_media_url")?.let {
put("media_url", decryptURL(it))
}
// Check if 320 Kbps is available or not
if (getBoolean("320kbps") != true && containsKey("media_url")) {
put("media_url", getString("media_url")?.replace("_320.mp4", "_160.mp4"))
}
}
// Increase Image Resolution
put(
"image",
getString("image")
?.replace("150x150", "500x500")
?.replace("50x50", "500x500")
)
// Fetch Lyrics if Requested
// Lyrics is HTML Based
if (includeLyrics) {
if (getBoolean("has_lyrics") == true) {
put("lyrics", getString("id")?.let { object : JioSaavnRequests {}.getLyrics(it) })
} else {
put("lyrics", "")
}
}
}
}
@Suppress("GetInstance")
@OptIn(InternalAPI::class)
suspend fun decryptURL(url: String): String {
val dks = DESKeySpec("38346591".toByteArray())
val keyFactory = SecretKeyFactory.getInstance("DES")
val key: SecretKey = keyFactory.generateSecret(dks)
val cipher: Cipher = Cipher.getInstance("DES/ECB/PKCS5Padding").apply {
init(Cipher.DECRYPT_MODE, key, SecureRandom())
}
return cipher.doFinal(url.decodeBase64Bytes())
.decodeToString()
.replace("_96.mp4", "_320.mp4")
}
internal fun String.format(): String {
return this.unescape()
.replace("&quot;", "'")
.replace("&amp;", "&")
.replace("&#039;", "'")
.replace("&copy;", "©")
}
fun JsonObject.getString(key: String): String? = this[key]?.jsonPrimitive?.content
fun JsonObject.getLong(key: String): Long = this[key]?.jsonPrimitive?.content?.toLongOrNull() ?: 0
fun JsonObject.getInteger(key: String): Int = this[key]?.jsonPrimitive?.content?.toIntOrNull() ?: 0
fun JsonObject.getBoolean(key: String): Boolean? = this[key]?.jsonPrimitive?.content?.toBoolean()
fun JsonObject.getFloat(key: String): Float? = this[key]?.jsonPrimitive?.content?.toFloatOrNull()
fun JsonObject.getDouble(key: String): Double? = this[key]?.jsonPrimitive?.content?.toDoubleOrNull()
fun JsonObject?.getJsonObject(key: String): JsonObject? = this?.get(key)?.jsonObject
fun JsonArray?.getJsonObject(index: Int): JsonObject? = this?.get(index)?.jsonObject
fun JsonObject?.getJsonArray(key: String): JsonArray? = this?.get(key)?.jsonArray

View File

@ -1,10 +0,0 @@
package jiosaavn.models
import kotlinx.serialization.Serializable
@Serializable
data class MoreInfo(
val language: String,
val primary_artists: String,
val singers: String,
)

View File

@ -1,17 +0,0 @@
package jiosaavn.models
import kotlinx.serialization.Serializable
@Serializable
data class SaavnAlbum(
val albumid: String,
val image: String,
val name: String,
val perma_url: String,
val primary_artists: String,
val primary_artists_id: String,
val release_date: String,
val songs: List<SaavnSong>,
val title: String,
val year: String
)

View File

@ -1,22 +0,0 @@
package jiosaavn.models
import kotlinx.serialization.Serializable
@Serializable
data class SaavnPlaylist(
val fan_count: Int? = 0,
val firstname: String? = null,
val follower_count: Long? = null,
val image: String,
val images: List<String>? = null,
val last_updated: String,
val lastname: String? = null,
val list_count: String? = null,
val listid: String? = null,
val listname: String, // Title
val perma_url: String,
val songs: List<SaavnSong>,
val sub_types: List<String>? = null,
val type: String = "", // chart,etc
val uid: String? = null,
)

View File

@ -1,17 +0,0 @@
package jiosaavn.models
import kotlinx.serialization.Serializable
@Serializable
data class SaavnSearchResult(
val album: String? = "",
val description: String,
val id: String,
val image: String,
val title: String,
val type: String,
val url: String,
val ctr: Int? = 0,
val position: Int? = 0,
val more_info: MoreInfo? = null,
)

View File

@ -1,41 +0,0 @@
package jiosaavn.models
import kotlinx.serialization.Serializable
@Serializable
data class SaavnSong(
val `320kbps`: Boolean,
val album: String,
val album_url: String? = null,
val albumid: String? = null,
val artistMap: Map<String, String>,
val copyright_text: String? = null,
val duration: String,
val encrypted_media_path: String,
val encrypted_media_url: String,
val explicit_content: Int = 0,
val has_lyrics: Boolean = false,
val id: String,
val image: String,
val label: String? = null,
val label_url: String? = null,
val language: String,
val lyrics_snippet: String? = null,
val media_preview_url: String? = null,
val media_url: String? = null, // Downloadable M4A Link
val music: String,
val music_id: String,
val origin: String? = null,
val perma_url: String? = null,
val play_count: Int = 0,
val primary_artists: String,
val primary_artists_id: String,
val release_date: String, // Format - 2021-05-04
val singers: String,
val song: String, // title
val starring: String? = null,
val type: String = "",
val vcode: String? = null,
val vlink: String? = null,
val year: String
)

View File

@ -1,9 +1,46 @@
import analytics_html_img.updateAnalyticsImage
import common.GithubService
import common.Secrets
import kotlinx.coroutines.runBlocking
import scripts.updateAnalyticsImage
import scripts.updateDownloadCards
import utils.debug
fun main(args: Array<String>) {
debug("fun main: args -> ${args.joinToString(";")}")
val secrets = Secrets.initSecrets()
// TASK -> Update Analytics Image in Readme
updateAnalyticsImage()
runBlocking {
val githubFileContent = GithubService.getGithubFileContent(
secrets = secrets,
fileName = "README.md"
)
// Content To be Processed
var updatedGithubContent: String = githubFileContent.decryptedContent
// TASK -> Update Analytics Image in Readme
updatedGithubContent = updateAnalyticsImage(
updatedGithubContent,
secrets
)
// TASK -> Update Total Downloads Card
updatedGithubContent = updateDownloadCards(
updatedGithubContent,
secrets.copy(tagName = "DCI")
)
// Write New Updated README.md
GithubService.updateGithubFileContent(
token = secrets.githubToken,
ownerName = secrets.ownerName,
repoName = secrets.repoName,
branchName = secrets.branchName,
fileName = secrets.filePath,
commitMessage = secrets.commitMessage,
rawContent = updatedGithubContent,
sha = githubFileContent.sha
)
}
}

View File

@ -0,0 +1,17 @@
package models.github
data class Asset(
val browser_download_url: String,
val content_type: String,
val created_at: String,
val download_count: Int,
val id: Int,
val label: Any,
val name: String,
val node_id: String,
val size: Int,
val state: String,
val updated_at: String,
val uploader: Uploader,
val url: String
)

View File

@ -0,0 +1,22 @@
package models.github
data class Author(
val avatar_url: String,
val events_url: String,
val followers_url: String,
val following_url: String,
val gists_url: String,
val gravatar_id: String,
val html_url: String,
val id: Int,
val login: String,
val node_id: String,
val organizations_url: String,
val received_events_url: String,
val repos_url: String,
val site_admin: Boolean,
val starred_url: String,
val subscriptions_url: String,
val type: String,
val url: String
)

View File

@ -0,0 +1,6 @@
package models.github
data class GithubFileContent(
val decryptedContent: String,
val sha: String
)

View File

@ -0,0 +1,23 @@
package models.github
data class GithubReleaseInfoItem(
val assets: List<Asset>,
val assets_url: String,
val author: Author,
val body: String,
val created_at: String,
val draft: Boolean,
val html_url: String,
val id: Int,
val name: String,
val node_id: String,
val prerelease: Boolean,
val published_at: String,
val reactions: Reactions,
val tag_name: String,
val tarball_url: String,
val target_commitish: String,
val upload_url: String,
val url: String,
val zipball_url: String
)

View File

@ -0,0 +1,3 @@
package models.github
class GithubReleasesInfo : ArrayList<GithubReleaseInfoItem>()

View File

@ -0,0 +1,16 @@
package models.github
import kotlinx.serialization.json.JsonNames
data class Reactions(
@JsonNames("+1") val upVotes: Int = 0,
@JsonNames("-1") val downVotes: Int = 0,
val confused: Int = 0,
val eyes: Int = 0,
val heart: Int = 0,
val hooray: Int = 0,
val laugh: Int = 0,
val rocket: Int = 0,
val total_count: Int = 0,
val url: String? = null
)

View File

@ -0,0 +1,22 @@
package models.github
data class Uploader(
val avatar_url: String,
val events_url: String,
val followers_url: String,
val following_url: String,
val gists_url: String,
val gravatar_id: String,
val html_url: String,
val id: Int,
val login: String,
val node_id: String,
val organizations_url: String,
val received_events_url: String,
val repos_url: String,
val site_admin: Boolean,
val starred_url: String,
val subscriptions_url: String,
val type: String,
val url: String
)

View File

@ -0,0 +1,65 @@
package scripts
import common.*
import io.ktor.client.features.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import utils.RETRY_LIMIT_EXHAUSTED
import utils.debug
internal suspend fun updateAnalyticsImage(
fileContent: String? = null,
secrets: Secrets
): String {
// debug("fun main: secrets -> $secrets")
val oldContent = fileContent ?: GithubService.getGithubFileContent(
secrets = secrets,
fileName = "README.md"
).decryptedContent
// debug("OLD FILE CONTENT",oldGithubFile)
val imageURL = getAnalyticsImage().also {
debug("Updated IMAGE", it)
}
return getUpdatedContent(
oldContent,
"![Today's Analytics]($imageURL)",
secrets.tagName
)
}
internal suspend fun getAnalyticsImage(): String {
var contentLength: Long
var analyticsImage: String
var retryCount = 5
do {
/*
* Get a new Image from Analytics,
* - Use Any Random useless query param ,
* As HCTI Demo, `caches value for a specific Link`
* */
val randomID = (1..100000).random()
analyticsImage = HCTIService.getImageURLFromURL(
url = "https://kind-grasshopper-73.telebit.io/matomo/index.php?module=Widgetize&action=iframe&containerId=VisitOverviewWithGraph&disableLink=0&widget=1&moduleToWidgetize=CoreHome&actionToWidgetize=renderWidgetContainer&idSite=1&period=day&date=yesterday&disableLink=1&widget=$randomID",
delayInMilliSeconds = 5000
)
// Sometimes we get incomplete image, hence verify `content-length`
val req = client.head<HttpResponse>(analyticsImage) {
timeout {
socketTimeoutMillis = 100_000
}
}
contentLength = req.headers["Content-Length"]?.toLong() ?: 0
debug(contentLength.toString())
if(retryCount-- == 0){
// FAIL Gracefully
throw(RETRY_LIMIT_EXHAUSTED())
}
}while (contentLength<1_20_000)
return analyticsImage
}

View File

@ -0,0 +1,213 @@
package scripts
import common.*
import io.ktor.client.features.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import utils.RETRY_LIMIT_EXHAUSTED
import utils.debug
internal suspend fun updateDownloadCards(
fileContent: String? = null,
secrets: Secrets
): String {
val oldContent = fileContent ?: GithubService.getGithubFileContent(
secrets = secrets,
fileName = "README.md"
).decryptedContent
val totalDownloads:Int = GithubService.getGithubRepoReleasesInfo(
secrets.ownerName,
secrets.repoName
).let { allReleases ->
var totalCount = 0
for(release in allReleases){
release.assets.forEach {
debug("${it.name}: ${release.tag_name}" ,"Downloads: ${it.download_count}")
totalCount += it.download_count
}
}
debug("Total Download Count: $totalCount")
return@let totalCount
}
return getUpdatedContent(
oldContent,
"""
<a href="https://github.com/Shabinder/SpotiFlyer/releases/latest">
<img src="${getDownloadCard(totalDownloads)}"
height="125" width="280" alt="Total Downloads">
</a>
""".trimIndent(),
secrets.tagName
)
}
private suspend fun getDownloadCard(
count: Int
): String {
var contentLength: Long
var downloadCard: String
var retryCount = 5
do {
downloadCard = HCTIService.getImageURLFromHtml(
html = getDownloadCardHtml(
count = count,
date = getTodayDate()
),
css = downloadCardCSS,
viewPortHeight = "170",
viewPortWidth = "385"
)
// Sometimes we get incomplete image, hence verify `content-length`
val req = client.head<HttpResponse>(downloadCard) {
timeout {
socketTimeoutMillis = 100_000
}
}
contentLength = req.headers["Content-Length"]?.toLong() ?: 0
// debug(contentLength.toString())
if(retryCount-- == 0){
// FAIL Gracefully
throw(RETRY_LIMIT_EXHAUSTED())
}
}while (contentLength<40_000)
return downloadCard
}
fun getDownloadCardHtml(
count: Int,
date: String, // ex: 06 Jun 2021
):String {
return """
<div class="card-container">
<div id="card" class="dark-bg">
<img id="profile-photo" src="https://www.lupusresearch.org/wp-content/uploads/2017/09/resource-downloads-icon.png">
<div class="text-wrapper">
<p id="title">Total Downloads</p>
<p id="source">Github & F-Droid</p>
<div class="contact-wrapper">
<a id="count" href="#">
$count
</a>
<a id="date" href="#">
Updated on: $date
</a>
</div>
</div>
</div>
</div>
""".trimIndent()
}
val downloadCardCSS =
"""
@import url('https://fonts.googleapis.com/css2?family=Poppins&display=swap');
* {
margin: 0;
padding: 0;
}
html,
body {
overflow: hidden;
}
.card-container {
height: 150px;
width: 360px;
padding: 8px 12px;
display: flex;
transition: 0.3s;
}
#card {
display: flex;
align-self: center;
width: fit-content;
background: linear-gradient(120deg, #f0f0f0 20%, #f9f9f9 30%);
border-radius: 22px;
padding: 20px 40px;
margin: 0 auto;
box-shadow: 4px 8px 20px rgba(0, 0, 0, 0.06);
transition: 0.3s;
}
#card:hover {
box-shadow: none;
cursor: pointer;
transform: translateY(2px)
}
#card:hover > #profile-photo {
opacity: 1
}
#profile-photo {
height: 90px;
width: 90px;
border-radius: 100px;
align-self: center;
box-shadow: 0 6px 30px rgba(199, 199, 199, 0.5);
opacity: 0.8;
transition: 0.3s;
}
.text-wrapper {
font-family: 'Poppins', sans-serif;
line-height: 0;
align-self: center;
margin-left: 20px;
}
.text-wrapper p {
margin: 0;
}
.contact-wrapper a {
display: block;
text-decoration: none;
}
#title {
font-size: 20px;
color: #5f5f5f;
margin-bottom: 20px;
}
#source {
font-size: 12px;
color: #9B9B9B;
margin-bottom: 22px;
}
#count {
padding-top: 8px;
font-size: 30px;
color: #615F5F;
margin-top: 15px;
transition: 0.3s;
}
#date {
padding-top: 12px;
font-size: 14px;
color: #615F5F;
margin-top: 15px;
transition: 0.3s;
}
#count:hover,
#date:hover {
color: #9B9B9B;
}
""".trimIndent()

View File

@ -1,92 +0,0 @@
package utils
/*
* JSON UTILS
* */
fun String.escape(): String {
val output = StringBuilder()
for (element in this) {
val chx = element.toInt()
assert(chx != 0)
when {
element == '\n' -> {
output.append("\\n")
}
element == '\t' -> {
output.append("\\t")
}
element == '\r' -> {
output.append("\\r")
}
element == '\\' -> {
output.append("\\\\")
}
element == '"' -> {
output.append("\\\"")
}
element == '\b' -> {
output.append("\\b")
}
chx >= 0x10000 -> {
assert(false) { "Java stores as u16, so it should never give us a character that's bigger than 2 bytes. It literally can't." }
}
chx > 127 -> {
output.append(String.format("\\u%04x", chx))
}
else -> {
output.append(element)
}
}
}
return output.toString()
}
fun String.unescape(): String {
val builder = StringBuilder()
var i = 0
while (i < this.length) {
val delimiter = this[i]
i++ // consume letter or backslash
if (delimiter == '\\' && i < this.length) {
// consume first after backslash
val ch = this[i]
i++
when (ch) {
'\\', '/', '"', '\'' -> {
builder.append(ch)
}
'n' -> builder.append('\n')
'r' -> builder.append('\r')
't' -> builder.append(
'\t'
)
'b' -> builder.append('\b')
'f' -> builder.append("\\f")
'u' -> {
val hex = StringBuilder()
// expect 4 digits
if (i + 4 > this.length) {
throw RuntimeException("Not enough unicode digits! ")
}
for (x in this.substring(i, i + 4).toCharArray()) {
if (!Character.isLetterOrDigit(x)) {
throw RuntimeException("Bad character in unicode escape.")
}
hex.append(Character.toLowerCase(x))
}
i += 4 // consume those four digits.
val code = hex.toString().toInt(16)
builder.append(code.toChar())
}
else -> {
throw RuntimeException("Illegal escape sequence: \\$ch")
}
}
} else { // it's not a backslash, or it's the last character.
builder.append(delimiter)
}
}
return builder.toString()
}

View File

@ -3,5 +3,4 @@ package utils
import kotlinx.coroutines.runBlocking
// Test Class- at development Time
fun main(): Unit = runBlocking {
}
fun main(): Unit = runBlocking {}

View File

@ -49,9 +49,9 @@ dependencies {
implementation(project(":common:dependency-injection"))
implementation("co.touchlab:stately-common:1.1.7")
implementation("dev.icerock.moko:parcelize:0.6.1")
// implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1")
// implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0") {
// https://youtrack.jetbrains.com/issue/KTOR-2670
// https://youtrack.jetbrains.com/issue/KTOR-2670
isForce = true
}
implementation("org.jetbrains:kotlin-react:17.0.1-pre.148-kotlin-1.4.30")
@ -77,6 +77,5 @@ kotlin {
}
}
}
binaries.executable()
}
}