Requestor.kt 9.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. package io.r_a_d.radio2.ui.songs.request
  2. import android.util.Log
  3. import androidx.lifecycle.MutableLiveData
  4. import io.r_a_d.radio2.ActionOnError
  5. import io.r_a_d.radio2.Async
  6. import io.r_a_d.radio2.playerstore.Song
  7. import io.r_a_d.radio2.preferenceStore
  8. import io.r_a_d.radio2.tag
  9. import org.json.JSONArray
  10. import org.json.JSONException
  11. import org.json.JSONObject
  12. import java.io.BufferedReader
  13. import java.io.IOException
  14. import java.io.InputStreamReader
  15. import java.net.CookieHandler
  16. import java.net.CookieManager
  17. import java.net.MalformedURLException
  18. import java.net.URL
  19. import java.util.*
  20. import java.util.regex.Pattern
  21. import javax.net.ssl.HttpsURLConnection
  22. import kotlin.collections.ArrayList
  23. import kotlin.random.Random
  24. /**
  25. * Requests a song via the website's API
  26. *
  27. * We scrape the website for a CSRF token and POST it to /request/ endpoint with
  28. * the song id
  29. *
  30. * Created by Kethsar on 1/2/2017.
  31. * Converted to Kotlin and adapted by Yattoz on 05 Nov. 2019
  32. */
  33. class Requestor {
  34. var addRequestMeta: String = ""
  35. private val cookieManager: CookieManager = CookieManager()
  36. private val requestUrl = "https://r-a-d.io/request/%1\$d"
  37. private val searchUrl = "https://r-a-d.io/api/search/%1s?page=%2\$d"
  38. private val favoritesUrl = "https://r-a-d.io/faves/%1s?dl=true"
  39. private val songThresholdStep = 50
  40. private var songThreshold = songThresholdStep
  41. private var localQuery = ""
  42. private var token: String? = null
  43. val snackBarText : MutableLiveData<String?> = MutableLiveData()
  44. private var responseArray : ArrayList<RequestResponse> = ArrayList()
  45. val requestSongArray : ArrayList<Song> = ArrayList()
  46. val favoritesSongArray : ArrayList<Song> = ArrayList()
  47. val isRequestResultUpdated : MutableLiveData<Boolean> = MutableLiveData()
  48. val isFavoritesUpdated : MutableLiveData<Boolean> = MutableLiveData()
  49. var isLoadMoreVisible: Boolean = false
  50. init {
  51. snackBarText.value = ""
  52. isRequestResultUpdated.value = false
  53. isFavoritesUpdated.value = false
  54. isLoadMoreVisible = false
  55. }
  56. fun initFavorites(userName : String? = preferenceStore.getString("userName", null)){
  57. Log.d(tag, "initializing favorites")
  58. favoritesSongArray.clear()
  59. if (userName == null)
  60. {
  61. // Display is done by default in the XML.
  62. Log.d(tag, "no user name set for favorites")
  63. isFavoritesUpdated.value = true
  64. return
  65. }
  66. val favoritesUserUrl = String.format(Locale.getDefault(), favoritesUrl, userName)
  67. val scrapeFavorites : (Any?) -> JSONArray = {
  68. JSONArray(URL(favoritesUserUrl).readText())
  69. }
  70. val postFavorites : (Any?) -> Unit = {
  71. val res = it as JSONArray
  72. for (i in 0 until (res).length())
  73. {
  74. val item = res.getJSONObject(i)
  75. val artistTitle = item.getString("meta")
  76. val id : Int? = if (item.isNull("tracks_id"))
  77. null
  78. else
  79. item.getInt("tracks_id")
  80. val lastRequested : Int? = if (item.isNull("lastrequested")) null else item.getInt("lastrequested")
  81. val lastPlayed : Int? = if (item.isNull("lastplayed")) null else item.getInt("lastplayed")
  82. val requestCount : Int? = if (item.isNull("requestcount")) null else item.getInt("requestcount")
  83. val isRequestable = (coolDown(lastPlayed, lastRequested, requestCount) < 0)
  84. //Log.d(tag, "val : $id")
  85. favoritesSongArray.add(Song(artistTitle, id ?: 0, isRequestable))
  86. }
  87. Log.d(tag, "favorites : $favoritesSongArray")
  88. isFavoritesUpdated.value = true
  89. }
  90. Async(scrapeFavorites, postFavorites, ActionOnError.NOTIFY)
  91. }
  92. fun search(query: String)
  93. {
  94. responseArray.clear()
  95. requestSongArray.clear()
  96. localQuery = query
  97. searchPage(query, 1) // the searchPage function is recursive to get all pages.
  98. }
  99. private fun searchPage(query: String, pageNumber : Int)
  100. {
  101. val searchURL = String.format(Locale.getDefault(), searchUrl, query, pageNumber)
  102. val scrape : (Any?) -> JSONObject = {
  103. val res = URL(searchURL).readText()
  104. val json = JSONObject(res)
  105. json
  106. }
  107. val post : (Any?) -> Unit = {
  108. val response = RequestResponse(it as JSONObject)
  109. responseArray.add(response)
  110. for (i in 0 until response.songs.size)
  111. {
  112. requestSongArray.add(response.songs[i])
  113. }
  114. isRequestResultUpdated.value = true
  115. if (requestSongArray.size >= songThreshold)
  116. {
  117. isLoadMoreVisible = true
  118. } else {
  119. if (response.currentPage < response.lastPage)
  120. searchPage(query, pageNumber + 1) // recursive call to get the next page
  121. else
  122. finishSearch()
  123. }
  124. }
  125. Async(scrape, post, ActionOnError.NOTIFY)
  126. }
  127. private fun finishSearch()
  128. {
  129. isLoadMoreVisible = false
  130. }
  131. fun reset()
  132. {
  133. requestSongArray.clear()
  134. responseArray.clear()
  135. isRequestResultUpdated.value = false
  136. songThreshold = songThresholdStep
  137. }
  138. fun loadMore()
  139. {
  140. songThreshold += songThresholdStep
  141. searchPage(localQuery, responseArray.last().currentPage + 1)
  142. }
  143. /**
  144. * Scrape the website for the CSRF token required for requesting
  145. * scrapeToken and postToken are the two lambas run by the Async() class.
  146. */
  147. private val scrapeToken : (Any?) -> Any? = {
  148. val radioSearchUrl = "https://r-a-d.io/search"
  149. var searchURL: URL? = null
  150. var retVal: String? = null
  151. var reader: BufferedReader? = null
  152. CookieHandler.setDefault(cookieManager) // it[0] ??
  153. try {
  154. searchURL = URL(radioSearchUrl)
  155. } catch (e: MalformedURLException) {
  156. e.printStackTrace()
  157. }
  158. try {
  159. reader = BufferedReader(InputStreamReader(searchURL!!.openStream(), "UTF-8"))
  160. var line: String?
  161. line = reader.readLine()
  162. while (line != null)
  163. {
  164. line = line.trim { it <= ' ' }
  165. val p = Pattern.compile("value=\"(\\w+)\"")
  166. val m = p.matcher(line)
  167. if (line.startsWith("<form")) {
  168. if (m.find()) {
  169. retVal = m.group(1)
  170. break
  171. }
  172. }
  173. line = reader.readLine()
  174. }
  175. } catch (e: IOException) {
  176. e.printStackTrace()
  177. } finally {
  178. if (reader != null) try {
  179. reader.close()
  180. } catch (ignored: IOException) {
  181. }
  182. }
  183. retVal
  184. }
  185. private val postToken : (Any?) -> (Unit) = {
  186. token = it as String?
  187. }
  188. /**
  189. * Request the song with the CSRF token that was scraped
  190. */
  191. private val requestSong: (Any?) -> Any? = {
  192. val reqString = it as String
  193. var response = ""
  194. try {
  195. val reqURL = URL(reqString)
  196. val conn = reqURL.openConnection() as HttpsURLConnection
  197. val tokenObject = JSONObject()
  198. tokenObject.put("_token", token)
  199. val requestBytes = tokenObject.toString().toByteArray()
  200. conn.requestMethod = "POST"
  201. conn.doOutput = true
  202. conn.doInput = true
  203. conn.setChunkedStreamingMode(0)
  204. conn.setRequestProperty("Content-Type", "application/json")
  205. val os = conn.outputStream
  206. os.write(requestBytes)
  207. val responseCode = conn.responseCode
  208. if (responseCode == HttpsURLConnection.HTTP_OK) {
  209. var line: String?
  210. val br = BufferedReader(InputStreamReader(
  211. conn.inputStream))
  212. line = br.readLine()
  213. while (line != null) {
  214. response += line
  215. line = br.readLine()
  216. }
  217. } else {
  218. response += ""
  219. }
  220. } catch (ex: IOException) {
  221. ex.printStackTrace()
  222. } catch (ex: JSONException) {
  223. ex.printStackTrace()
  224. }
  225. response
  226. }
  227. private val postSong : (Any?) -> (Unit) = {
  228. val response = JSONObject(it as String)
  229. val key = response.names()!!.get(0) as String
  230. val value = response.getString(key)
  231. snackBarText.postValue(addRequestMeta + value)
  232. }
  233. fun request(songID: Int?) {
  234. val requestSongUrl = String.format(requestUrl, songID!!)
  235. if (token == null) {
  236. Async(scrapeToken, postToken, ActionOnError.NOTIFY)
  237. }
  238. Async(requestSong, postSong, ActionOnError.NOTIFY, requestSongUrl)
  239. }
  240. fun raF() : Song {
  241. // request a random favorite song. HELL YEAH
  242. val requestableSongArray = ArrayList<Song>()
  243. for (i in 0 until favoritesSongArray.size)
  244. {
  245. if (favoritesSongArray[i].isRequestable && (favoritesSongArray[i].id ?: 0) > 0)
  246. requestableSongArray.add(favoritesSongArray[i])
  247. }
  248. return if (requestableSongArray.isNotEmpty()) {
  249. val songNbr = Random(System.currentTimeMillis()).nextInt(1, requestableSongArray.size)
  250. requestableSongArray[songNbr]
  251. } else {
  252. Song("No song requestable - ")
  253. }
  254. }
  255. companion object {
  256. val instance by lazy {
  257. Requestor()
  258. }
  259. }
  260. }