Models and Serializers

This commit is contained in:
Shabinder Singh 2021-10-01 23:22:05 +05:30
parent 13471d4a28
commit de4c84bdda
17 changed files with 570 additions and 30 deletions

View File

@ -7,7 +7,7 @@ koin = "3.1.2"
kermit = "0.1.9"
mokoParcelize = "0.7.1"
ktor = "1.6.3"
kotlinxSerialization = "1.2.2"
kotlinxSerialization = "1.3.0"
sqlDelight = "1.5.1"
sqliteJdbcDriver = "3.34.0"
slf4j = "1.7.31"

View File

@ -0,0 +1,13 @@
package com.shabinder.common.models.soundcloud
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Badges(
val pro: Boolean = false,
@SerialName("pro_unlimited")
val proUnlimited: Boolean = false,
val verified: Boolean = false
)

View File

@ -0,0 +1,10 @@
package com.shabinder.common.models.soundcloud
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CreatorSubscription(
val product: Product = Product()
)

View File

@ -0,0 +1,14 @@
package com.shabinder.common.models.soundcloud
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Format(
@SerialName("mime_type")
val mimeType: String = "",
val protocol: String = ""
) {
val isProgressive get() = protocol == "progressive"
}

View File

@ -0,0 +1,9 @@
package com.shabinder.common.models.soundcloud
import kotlinx.serialization.Serializable
@Serializable
data class Media(
val transcodings: List<Transcoding> = emptyList()
)

View File

@ -0,0 +1,10 @@
package com.shabinder.common.models.soundcloud
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Product(
val id: String = ""
)

View File

@ -0,0 +1,24 @@
package com.shabinder.common.models.soundcloud
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PublisherMetadata(
@SerialName("album_title")
val albumTitle: String = "",
val artist: String = "",
@SerialName("contains_music")
val containsMusic: Boolean = false,
val id: Int = 0,
val isrc: String = "",
val publisher: String = "",
@SerialName("release_title")
val releaseTitle: String = "",
@SerialName("upc_or_ean")
val upcOrEan: String = "",
val urn: String = "",
@SerialName("writer_composer")
val writerComposer: String = ""
)

View File

@ -0,0 +1,95 @@
package com.shabinder.common.models.soundcloud
import com.shabinder.common.models.AudioFormat
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SoundCloudTrack(
@SerialName("artwork_url")
val artworkUrl: String = "",
//val caption: Any = Any(),
@SerialName("comment_count")
val commentCount: Int = 0,
val commentable: Boolean = false,
@SerialName("created_at")
val createdAt: String = "", //2015-05-21T16:36:39Z
val description: String = "",
@SerialName("display_date")
val displayDate: String = "",
@SerialName("download_count")
val downloadCount: Int = 0,
val downloadable: Boolean = false,
val duration: Int = 0, //290116
@SerialName("embeddable_by")
val embeddableBy: String = "",
@SerialName("full_duration")
val fullDuration: Int = 0,
val genre: String = "",
@SerialName("has_downloads_left")
val hasDownloadsLeft: Boolean = false,
val id: Int = 0,
val kind: String = "",
@SerialName("label_name")
val labelName: String = "",
@SerialName("last_modified")
val lastModified: String = "",
val license: String = "",
@SerialName("likes_count")
val likesCount: Int = 0,
val media: Media = Media(), // Important Data
@SerialName("monetization_model")
val monetizationModel: String = "",
val permalink: String = "",
@SerialName("permalink_url")
val permalinkUrl: String = "",
@SerialName("playback_count")
val playbackCount: Int = 0,
val policy: String = "",
val `public`: Boolean = false,
@SerialName("publisher_metadata")
val publisherMetadata: PublisherMetadata = PublisherMetadata(),
//@SerialName("purchase_title")
//val purchaseTitle: Any = Any(),
@SerialName("purchase_url")
val purchaseUrl: String = "", //"http://itunes.apple.com/us/album/sunrise-ep/id993328519"
@SerialName("release_date")
val releaseDate: String = "",
@SerialName("reposts_count")
val repostsCount: Int = 0,
//@SerialName("secret_token")
//val secretToken: Any = Any(),
val sharing: String = "",
val state: String = "",
@SerialName("station_permalink")
val stationPermalink: String = "",
@SerialName("station_urn")
val stationUrn: String = "",
val streamable: Boolean = false,
@SerialName("tag_list")
val tagList: String = "",
val title: String = "",
@SerialName("track_authorization")
val trackAuthorization: String = "",
@SerialName("track_format")
val trackFormat: String = "",
val uri: String = "",
val urn: String = "",
val user: User = User(),
@SerialName("user_id")
val userId: Int = 0,
//val visuals: Any = Any(),
@SerialName("waveform_url")
val waveformUrl: String = ""
) {
fun getDownloadableLink(): Pair<String, AudioFormat>? {
return (media.transcodings.firstOrNull {
it.quality == "hq" && (it.format.isProgressive || it.url.contains("progressive"))
} ?: media.transcodings.firstOrNull {
it.quality == "sq" && (it.format.isProgressive || it.url.contains("progressive"))
})?.let {
it.url to it.audioFormat
}
}
}

View File

@ -0,0 +1,84 @@
package com.shabinder.common.models.soundcloud
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Track(
@SerialName("artwork_url")
val artworkUrl: String = "",
val caption: String = "",
@SerialName("comment_count")
val commentCount: Int = 0,
val commentable: Boolean = false,
@SerialName("created_at")
val createdAt: String = "",
val description: String = "",
@SerialName("display_date")
val displayDate: String = "",
@SerialName("download_count")
val downloadCount: Int = 0,
val downloadable: Boolean = false,
val duration: Int = 0,
@SerialName("embeddable_by")
val embeddableBy: String = "",
@SerialName("full_duration")
val fullDuration: Int = 0,
val genre: String = "",
@SerialName("has_downloads_left")
val hasDownloadsLeft: Boolean = false,
val id: Int = 0,
val kind: String = "",
@SerialName("label_name")
val labelName: String = "",
@SerialName("last_modified")
val lastModified: String = "",
val license: String = "",
@SerialName("likes_count")
val likesCount: Int = 0,
val media: Media = Media(),
@SerialName("monetization_model")
val monetizationModel: String = "",
val permalink: String = "",
@SerialName("permalink_url")
val permalinkUrl: String = "",
@SerialName("playback_count")
val playbackCount: Int = 0,
val policy: String = "",
val `public`: Boolean = false,
@SerialName("publisher_metadata")
val publisherMetadata: PublisherMetadata = PublisherMetadata(),
@SerialName("purchase_title")
val purchaseTitle:String = "",
@SerialName("purchase_url")
val purchaseUrl: String = "",
@SerialName("release_date")
val releaseDate: String = "",
@SerialName("reposts_count")
val repostsCount: Int = 0,
@SerialName("secret_token")
val secretToken: String = "",
val sharing: String = "",
val state: String = "",
@SerialName("station_permalink")
val stationPermalink: String = "",
@SerialName("station_urn")
val stationUrn: String = "",
val streamable: Boolean = false,
@SerialName("tag_list")
val tagList: String = "",
val title: String = "",
@SerialName("track_authorization")
val trackAuthorization: String = "",
@SerialName("track_format")
val trackFormat: String = "",
val uri: String = "",
val urn: String = "",
val user: User = User(),
@SerialName("user_id")
val userId: Int = 0,
val visuals: String = "",
@SerialName("waveform_url")
val waveformUrl: String = ""
)

View File

@ -0,0 +1,22 @@
package com.shabinder.common.models.soundcloud
import com.shabinder.common.models.AudioFormat
import kotlinx.serialization.Serializable
@Serializable
data class Transcoding(
val duration: Int = 0,
val format: Format = Format(),
val preset: String = "",
val quality: String = "", //sq == 128kbps //hq == 256kbps
val snipped: Boolean = false,
val url: String = ""
) {
val audioFormat: AudioFormat = when {
preset.contains("mp3") -> AudioFormat.MP3
preset.contains("aac") || preset.contains("m4a") -> AudioFormat.MP4
preset.contains("flac") -> AudioFormat.FLAC
else -> AudioFormat.UNKNOWN
}
}

View File

@ -0,0 +1,38 @@
package com.shabinder.common.models.soundcloud
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class User(
@SerialName("avatar_url")
val avatarUrl: String = "",
val badges: Badges = Badges(),
val city: String = "",
@SerialName("country_code")
val countryCode: String = "",
@SerialName("first_name")
val firstName: String = "",
@SerialName("followers_count")
val followersCount: Int = 0,
@SerialName("full_name")
val fullName: String = "",
val id: Int = 0,
val kind: String = "",
@SerialName("last_modified")
val lastModified: String = "",
@SerialName("last_name")
val lastName: String = "",
val permalink: String = "",
@SerialName("permalink_url")
val permalinkUrl: String = "",
@SerialName("station_permalink")
val stationPermalink: String = "",
@SerialName("station_urn")
val stationUrn: String = "",
val uri: String = "",
val urn: String = "",
val username: String = "",
val verified: Boolean = false
)

View File

@ -0,0 +1,14 @@
package com.shabinder.common.models.soundcloud
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Visual(
@SerialName("entry_time")
val entryTime: Int = 0,
val urn: String = "",
@SerialName("visual_url")
val visualUrl: String = ""
)

View File

@ -0,0 +1,13 @@
package com.shabinder.common.models.soundcloud
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Visuals(
val enabled: Boolean = false,
//val tracking: Any = Any(),
val urn: String = "",
val visuals: List<Visual> = listOf()
)

View File

@ -0,0 +1,159 @@
package com.shabinder.common.models.soundcloud.resolvemodel
import com.shabinder.common.models.soundcloud.Media
import com.shabinder.common.models.soundcloud.PublisherMetadata
import com.shabinder.common.models.soundcloud.Track
import com.shabinder.common.models.soundcloud.User
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.JsonClassDiscriminator
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
@Serializable
@JsonClassDiscriminator("kind")
sealed class SoundCloudResolveResponseBase {
abstract val kind: String
@SerialName("playlist")
@Serializable
data class SoundCloudResolveResponsePlaylist(
@SerialName("artwork_url")
val artworkUrl: String = "",
@SerialName("calculated_artwork_url")
val calculatedArtworkUrl: String = "", //t500x500, t120x120 // "https://i1.sndcdn.com/artworks-pjsabv9w0EXW3lBJ-nvjDYg-large.jpg" // https://i1.sndcdn.com/artworks-pjsabv9w0EXW3lBJ-nvjDYg-t500x500.jpg
@SerialName("created_at")
val createdAt: String = "",
val description: String = "",
@SerialName("display_date")
val displayDate: String = "",
val duration: Int = 0,
override val kind: String = "",
@SerialName("embeddable_by")
val embeddableBy: String = "",
val genre: String = "",
val id: Int = 0,
@SerialName("is_album")
val isAlbum: Boolean = false,
@SerialName("label_name")
val labelName: String = "",
@SerialName("last_modified")
val lastModified: String = "",
val license: String = "",
@SerialName("likes_count")
val likesCount: Int = 0,
@SerialName("managed_by_feeds")
val managedByFeeds: Boolean = false,
val permalink: String = "",
@SerialName("permalink_url")
val permalinkUrl: String = "",
val `public`: Boolean = false,
@SerialName("published_at")
val publishedAt: String = "",
@SerialName("purchase_title")
val purchaseTitle: String = "",
@SerialName("purchase_url")
val purchaseUrl: String = "",
@SerialName("release_date")
val releaseDate: String = "",
@SerialName("reposts_count")
val repostsCount: Int = 0,
@SerialName("secret_token")
val secretToken: String = "",
@SerialName("set_type")
val setType: String = "",
val sharing: String = "",
@SerialName("tag_list")
val tagList: String = "",
val title: String = "", //"Top 50: Hip-hop & Rap"
@SerialName("track_count")
val trackCount: Int = 0,
val tracks: List<Track> = emptyList(),
val uri: String = "",
val user: User = User(),
@SerialName("user_id")
val userId: Int = 0
) : SoundCloudResolveResponseBase()
@SerialName("track")
@Serializable
data class SoundCloudResolveResponseTrack(
@SerialName("artwork_url")
val artworkUrl: String = "",
val caption: String = "",
@SerialName("comment_count")
val commentCount: Int = 0,
val commentable: Boolean = false,
@SerialName("created_at")
val createdAt: String = "",
val description: String = "",
@SerialName("display_date")
val displayDate: String = "",
@SerialName("download_count")
val downloadCount: Int = 0,
val downloadable: Boolean = false,
val duration: Int = 0,
@SerialName("embeddable_by")
val embeddableBy: String = "",
@SerialName("full_duration")
val fullDuration: Int = 0,
val genre: String = "",
@SerialName("has_downloads_left")
val hasDownloadsLeft: Boolean = false,
val id: Int = 0,
override val kind: String = "",
@SerialName("label_name")
val labelName: String = "",
@SerialName("last_modified")
val lastModified: String = "",
val license: String = "",
@SerialName("likes_count")
val likesCount: Int = 0,
val media: Media = Media(),
@SerialName("monetization_model")
val monetizationModel: String = "",
val permalink: String = "",
@SerialName("permalink_url")
val permalinkUrl: String = "",
@SerialName("playback_count")
val playbackCount: Int = 0,
val policy: String = "",
val `public`: Boolean = false,
@SerialName("publisher_metadata")
val publisherMetadata: PublisherMetadata = PublisherMetadata(),
@SerialName("purchase_title")
val purchaseTitle: String = "",
@SerialName("purchase_url")
val purchaseUrl: String = "",
@SerialName("release_date")
val releaseDate: String = "",
@SerialName("reposts_count")
val repostsCount: Int = 0,
@SerialName("secret_token")
val secretToken: String = "",
val sharing: String = "",
val state: String = "",
@SerialName("station_permalink")
val stationPermalink: String = "",
@SerialName("station_urn")
val stationUrn: String = "",
val streamable: Boolean = false,
@SerialName("tag_list")
val tagList: String = "",
val title: String = "",
@SerialName("track_authorization")
val trackAuthorization: String = "",
@SerialName("track_format")
val trackFormat: String = "",
val uri: String = "",
val urn: String = "",
val user: User = User(),
@SerialName("user_id")
val userId: Int = 0,
val visuals: String = "",
@SerialName("waveform_url")
val waveformUrl: String = ""
) : SoundCloudResolveResponseBase()
}

View File

@ -8,6 +8,7 @@ val globalJson by lazy {
Json {
isLenient = true
ignoreUnknownKeys = true
coerceInputValues = true
}
}

View File

@ -2,12 +2,15 @@ package com.shabinder.common.providers.sound_cloud.requests
import com.shabinder.common.models.SpotiFlyerException
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.utils.requireNotNull
import com.shabinder.common.models.soundcloud.resolvemodel.SoundCloudResolveResponseBase
import com.shabinder.common.models.soundcloud.resolvemodel.SoundCloudResolveResponseBase.SoundCloudResolveResponsePlaylist
import com.shabinder.common.models.soundcloud.resolvemodel.SoundCloudResolveResponseBase.SoundCloudResolveResponseTrack
import io.github.shabinder.utils.getBoolean
import io.github.shabinder.utils.getString
import io.ktor.client.*
import io.ktor.client.features.*
import io.ktor.client.request.*
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.json.JsonObject
interface SoundCloudRequests {
@ -16,62 +19,58 @@ interface SoundCloudRequests {
suspend fun parseURL(url: String) {
getItem(url).let { item: JsonObject ->
when (item.getString("kind")) {
"track" -> {
}
"playlist" -> {
}
"user" -> {
getResponseObj(url).let { item ->
when (item) {
is SoundCloudResolveResponseTrack -> {
getTrack(item)
}
is SoundCloudResolveResponsePlaylist -> {
}
else -> throw SpotiFlyerException.FeatureNotImplementedYet()
}
}
}
@Suppress("NAME_SHADOWING")
suspend fun getTrack(track: JsonObject): TrackDetails? {
suspend fun getTrack(track: SoundCloudResolveResponseTrack): TrackDetails? {
val track = getTrackInfo(track)
val title = track.getString("title")
if (track.getString("policy") == "BLOCK")
throw SpotiFlyerException.GeoLocationBlocked(extraInfo = "Use VPN to access $title")
if (track.policy == "BLOCK")
throw SpotiFlyerException.GeoLocationBlocked(extraInfo = "Use VPN to access ${track.title}")
if (track.getBoolean("streamable") == false)
throw SpotiFlyerException.LinkInvalid("\nSound Cloud Reports that $title is not streamable !\n")
if (!track.streamable)
throw SpotiFlyerException.LinkInvalid("\nSound Cloud Reports that ${track.title} is not streamable !\n")
return null
}
suspend fun getTrackInfo(track: JsonObject): JsonObject {
if (track.containsKey("media"))
return track
suspend fun getTrackInfo(res: SoundCloudResolveResponseTrack): SoundCloudResolveResponseTrack {
if (res.media.transcodings.isNotEmpty())
return res
val infoURL = URLS.TRACK_INFO.buildURL(track.getString("id").requireNotNull())
val infoURL = URLS.TRACK_INFO.buildURL(res.id.toString())
return httpClient.get(infoURL) {
parameter("client_id", CLIENT_ID)
}
}
suspend fun getItem(url: String, clientID: String = CLIENT_ID): JsonObject {
suspend fun getResponseObj(url: String, clientID: String = CLIENT_ID): SoundCloudResolveResponseBase {
val itemURL = URLS.RESOLVE.buildURL(url)
val resp: JsonObject = try {
val resp: SoundCloudResolveResponseBase = try {
httpClient.get(itemURL) {
parameter("client_id", clientID)
}
} catch (e: ClientRequestException) {
if (clientID != ALT_CLIENT_ID)
return getItem(url, ALT_CLIENT_ID)
return getResponseObj(url, ALT_CLIENT_ID)
throw e
}
val tracksPresent = resp.getString("kind").equals("playlist") && resp.containsKey("tracks")
val tracksPresent = (resp is SoundCloudResolveResponsePlaylist && resp.tracks.isNotEmpty())
if (!tracksPresent && clientID != ALT_CLIENT_ID)
return getItem(ALT_CLIENT_ID)
return getResponseObj(ALT_CLIENT_ID)
return resp
}
@ -90,7 +89,25 @@ interface SoundCloudRequests {
ME({ "https://api-v2.soundcloud.com/me?oauth_token=$it" }),
}
private const val CLIENT_ID = "a3e059563d7fd3372b49b37f00a00bcf"
private const val ALT_CLIENT_ID = "2t9loNQH90kzJcsFCODdigxfp325aq4z"
const val CLIENT_ID = "a3e059563d7fd3372b49b37f00a00bcf"
const val ALT_CLIENT_ID = "2t9loNQH90kzJcsFCODdigxfp325aq4z"
}
}
@OptIn(InternalSerializationApi::class)
suspend inline fun <reified T : Any> SoundCloudRequests.doAuthenticatedRequest(url: String): T {
var clientID: String = SoundCloudRequests.CLIENT_ID
return try {
httpClient.get(url) {
parameter("client_id", clientID)
}
} catch (e: ClientRequestException) {
if (clientID != SoundCloudRequests.ALT_CLIENT_ID) {
clientID = SoundCloudRequests.ALT_CLIENT_ID
return httpClient.get(url) {
parameter("client_id", clientID)
}
}
throw e
}
}

View File

@ -1,10 +1,15 @@
package com.shabinder.common.providers
import com.shabinder.common.models.TrackDetails
import com.shabinder.common.models.soundcloud.SoundCloudTrack
import com.shabinder.common.models.soundcloud.resolvemodel.SoundCloudResolveResponseBase
import com.shabinder.common.providers.utils.CommonUtils
import com.shabinder.common.providers.utils.SpotifyUtils
import com.shabinder.common.providers.utils.SpotifyUtils.toTrackDetailsList
import com.shabinder.common.utils.globalJson
import io.github.shabinder.runBlocking
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.serializer
import kotlin.test.Test
class TestSpotifyTrackMatching {
@ -17,7 +22,19 @@ class TestSpotifyTrackMatching {
private val spotifyToken: String?
get() = null
// get() = "BQB41HqrLcrh5eRYaL97GvaH6tRe-1EktQ8VGTWUQuFnYVWBEoTcF7T_8ogqVn1GHl9HCcMiQ0HBT-ybC74"
@OptIn(InternalSerializationApi::class)
@Test
fun testRandomThing() = runBlocking {
globalJson.decodeFromString(SoundCloudResolveResponseBase.serializer(), """{"artwork_url":null,"trackCount":12,"kind":"playlist"}""")
.also {
println(it)
println(it is SoundCloudResolveResponseBase.SoundCloudResolveResponsePlaylist)
println(it is SoundCloudResolveResponseBase.SoundCloudResolveResponseTrack)
}
}
@Test
fun matchVideo() = runBlocking {