mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-25 02:14:32 +01:00
Dep Updates, Maintenance Tasks Updated
This commit is contained in:
parent
6b10d63675
commit
4b18c099b6
@ -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")
|
||||
|
@ -32,6 +32,7 @@ allprojects {
|
||||
}
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
useIR = true
|
||||
}
|
||||
}
|
||||
|
@ -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}"
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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("/")
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
30
maintenance-tasks/src/main/java/common/ContentUpdation.kt
Normal file
30
maintenance-tasks/src/main/java/common/ContentUpdation.kt
Normal 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()
|
||||
}
|
16
maintenance-tasks/src/main/java/common/Date.kt
Normal file
16
maintenance-tasks/src/main/java/common/Date.kt
Normal 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"
|
||||
}
|
@ -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,
|
@ -1,4 +1,4 @@
|
||||
package analytics_html_img
|
||||
package common
|
||||
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.headers
|
@ -1,4 +1,4 @@
|
||||
package analytics_html_img
|
||||
package common
|
||||
|
||||
import utils.byOptionalProperty
|
||||
import utils.byProperty
|
@ -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="
|
||||
}
|
||||
}
|
@ -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(""", "'")
|
||||
.replace("&", "&")
|
||||
.replace("'", "'")
|
||||
.replace("©", "©")
|
||||
}
|
||||
|
||||
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
|
@ -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,
|
||||
)
|
@ -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
|
||||
)
|
@ -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,
|
||||
)
|
@ -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,
|
||||
)
|
@ -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
|
||||
)
|
@ -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()
|
||||
|
||||
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
|
||||
updateAnalyticsImage()
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
17
maintenance-tasks/src/main/java/models/github/Asset.kt
Normal file
17
maintenance-tasks/src/main/java/models/github/Asset.kt
Normal 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
|
||||
)
|
22
maintenance-tasks/src/main/java/models/github/Author.kt
Normal file
22
maintenance-tasks/src/main/java/models/github/Author.kt
Normal 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
|
||||
)
|
@ -0,0 +1,6 @@
|
||||
package models.github
|
||||
|
||||
data class GithubFileContent(
|
||||
val decryptedContent: String,
|
||||
val sha: String
|
||||
)
|
@ -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
|
||||
)
|
@ -0,0 +1,3 @@
|
||||
package models.github
|
||||
|
||||
class GithubReleasesInfo : ArrayList<GithubReleaseInfoItem>()
|
16
maintenance-tasks/src/main/java/models/github/Reactions.kt
Normal file
16
maintenance-tasks/src/main/java/models/github/Reactions.kt
Normal 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
|
||||
)
|
22
maintenance-tasks/src/main/java/models/github/Uploader.kt
Normal file
22
maintenance-tasks/src/main/java/models/github/Uploader.kt
Normal 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
|
||||
)
|
@ -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
|
||||
}
|
213
maintenance-tasks/src/main/java/scripts/UpdateDownloadCards.kt
Normal file
213
maintenance-tasks/src/main/java/scripts/UpdateDownloadCards.kt
Normal 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()
|
@ -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()
|
||||
}
|
@ -3,5 +3,4 @@ package utils
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
// Test Class- at development Time
|
||||
fun main(): Unit = runBlocking {
|
||||
}
|
||||
fun main(): Unit = runBlocking {}
|
||||
|
@ -77,6 +77,5 @@ kotlin {
|
||||
}
|
||||
}
|
||||
}
|
||||
binaries.executable()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user