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("com.jakewharton.timber:timber:4.7.1")
implementation("dev.icerock.moko:parcelize:0.6.1") implementation("dev.icerock.moko:parcelize:0.6.1")
implementation("com.github.shabinder:storage-chooser:2.0.4.45") 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 // Test
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")

View File

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

View File

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

View File

@ -23,16 +23,8 @@ plugins {
} }
kotlin { kotlin {
jvm("desktop").compilations.all { jvm("desktop")
kotlinOptions { android()
useIR = true
}
}
android().compilations.all {
kotlinOptions {
useIR = true
}
}
sourceSets { sourceSets {
named("commonMain") { named("commonMain") {
dependencies { dependencies {
@ -51,7 +43,7 @@ kotlin {
implementation(Extras.kermit) implementation(Extras.kermit)
implementation("dev.icerock.moko:parcelize:0.6.1") 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") @Suppress("DEPRECATION")
isForce = true 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 { jvm("desktop")
kotlinOptions { android()
useIR = true
}
}
android().compilations.all {
kotlinOptions {
useIR = true
}
}
js { js(/*BOTH*/) {
/*
* TODO Enable JS IR Compiler
* waiting for Decompose & MVI Kotlin to support same
* */
browser() browser()
// nodejs() // nodejs()
binaries.executable()
} }
sourceSets { sourceSets {
named("commonTest") { named("commonTest") {
@ -73,8 +60,4 @@ kotlin {
dependencies {} dependencies {}
} }
} }
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}
} }

View File

@ -34,24 +34,12 @@ kotlin {
} }
} }
jvm("desktop").compilations.all { jvm("desktop")
kotlinOptions { android()
useIR = true
} js(/*BOTH*/) {
}
android().compilations.all {
kotlinOptions {
useIR = true
}
}
js {
/*
* TODO Enable JS IR Compiler
* waiting for Decompose & MVI Kotlin to support same
* */
browser() browser()
// nodejs() // nodejs()
binaries.executable()
} }
sourceSets { sourceSets {
@ -75,10 +63,10 @@ kotlin {
// Extras // Extras
implementation(Extras.kermit) implementation(Extras.kermit)
implementation(Serialization.json)
implementation("co.touchlab:stately-common:1.1.7") implementation("co.touchlab:stately-common:1.1.7")
implementation("dev.icerock.moko:parcelize:0.6.1") 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.5.0-native-mt") {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3-native-mt") {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
isForce = true isForce = true
} }
@ -87,7 +75,7 @@ kotlin {
named("androidMain") { named("androidMain") {
dependencies { dependencies {
implementation("androidx.appcompat:appcompat:1.2.0") implementation("androidx.appcompat:appcompat:1.3.0")
implementation(Androidx.core) implementation(Androidx.core)
implementation(compose.runtime) implementation(compose.runtime)
implementation(compose.material) 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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=e2774e6fb77c43657decde25542dea710aafd78c4022d19b196e7e78d79d8c6c distributionSha256Sum=7faa7198769f872826c8ef4f1450f839ec27f0b4d5d1e51bade63667cbccd205
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -18,14 +18,13 @@ application {
} }
dependencies { 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(Ktor.slf4j)
implementation("io.ktor:ktor-client-serialization:1.5.4") implementation(Ktor.clientCore)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1") implementation(Ktor.clientJson)
implementation(Ktor.clientApache)
implementation(Ktor.clientLogging)
implementation(Ktor.clientSerialization)
implementation(Serialization.json)
// testDeps // testDeps
testImplementation(kotlin("test-junit")) testImplementation(kotlin("test-junit"))
} }
@ -33,10 +32,3 @@ dependencies {
tasks.test { tasks.test {
useJUnit() 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") @file:Suppress("FunctionName")
package analytics_html_img package common
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.features.HttpTimeout import io.ktor.client.features.HttpTimeout
@ -18,6 +18,7 @@ internal object Common {
fun END_SECTION(tagName: String = "HTI") = "<!--END_SECTION:$tagName-->" 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" 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 { internal val client = HttpClient {
install(HttpTimeout) install(HttpTimeout)
install(JsonFeature) { install(JsonFeature) {
@ -33,7 +34,3 @@ internal val client = HttpClient {
level = LogLevel.INFO 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.get
import io.ktor.client.request.header import io.ktor.client.request.header
@ -8,15 +8,38 @@ import io.ktor.http.ContentType
import io.ktor.http.contentType import io.ktor.http.contentType
import io.ktor.util.InternalAPI import io.ktor.util.InternalAPI
import io.ktor.util.encodeBase64 import io.ktor.util.encodeBase64
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put import kotlinx.serialization.json.put
import models.github.GithubFileContent
import models.github.GithubReleasesInfo
internal object GithubService { internal object GithubService {
private const val baseURL = Common.GITHUB_API 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( suspend fun getGithubFileContent(
token: String, token: String,
ownerName: 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.header
import io.ktor.client.request.headers import io.ktor.client.request.headers

View File

@ -1,4 +1,4 @@
package analytics_html_img package common
import utils.byOptionalProperty import utils.byOptionalProperty
import utils.byProperty 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 import utils.debug
fun main(args: Array<String>) { fun main(args: Array<String>) {
debug("fun main: args -> ${args.joinToString(";")}") 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 // 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
)
}
} }

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 import kotlinx.coroutines.runBlocking
// Test Class- at development Time // Test Class- at development Time
fun main(): Unit = runBlocking { fun main(): Unit = runBlocking {}
}

View File

@ -77,6 +77,5 @@ kotlin {
} }
} }
} }
binaries.executable()
} }
} }