/*
* Copyright 2018 Allan Wang
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package com.pitchedapps.frost.facebook.requests
import com.bumptech.glide.Priority
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.signature.ObjectKey
import com.pitchedapps.frost.facebook.FB_IMAGE_ID_MATCHER
import com.pitchedapps.frost.facebook.FB_REDIRECT_URL_MATCHER
import com.pitchedapps.frost.facebook.FB_URL_BASE
import com.pitchedapps.frost.facebook.formattedFbUrl
import com.pitchedapps.frost.facebook.get
import com.pitchedapps.frost.utils.L
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import okhttp3.Call
import java.io.IOException
import java.io.InputStream
/**
* Created by Allan Wang on 29/12/17.
*/
fun RequestAuth.getFullSizedImage(fbid: Long) = frostRequest(::getJsonUrl) {
url("${FB_URL_BASE}photo/view_full_size/?fbid=$fbid&__ajax__=&__user=$userId")
get()
}
/**
* Attempts to get the fbcdn url of the supplied image redirect url
*/
suspend fun String.getFullSizedImageUrl(url: String, timeout: Long = 3000): String? =
withContext(Dispatchers.IO) {
try {
withTimeout(timeout) {
val redirect = requestBuilder().url(url).get().call()
.execute().body()?.string() ?: return@withTimeout null
FB_REDIRECT_URL_MATCHER.find(redirect)[1]?.formattedFbUrl
}
} catch (e: Exception) {
L.e(e) { "Failed to load full size image url" }
null
}
}
/**
* Request loader for a potentially hd version of a url
* In this case, each url may potentially return an id,
* which may potentially be used to fetch a higher res image url
* The following aims to allow such loading while adhering to Glide's lifecycle
*/
data class HdImageMaybe(val url: String, val cookie: String) {
val id: Long by lazy { FB_IMAGE_ID_MATCHER.find(url)[1]?.toLongOrNull() ?: -1 }
val isValid: Boolean by lazy {
id != -1L && cookie.isNotBlank()
}
}
/*
* The following was a test to see if hd image loading would work
*
* It's working and tested, though the improvements aren't really worth the extra data use
* and reload
*/
class HdImageLoadingFactory : ModelLoaderFactory {
override fun build(multiFactory: MultiModelLoaderFactory) = HdImageLoading()
override fun teardown() = Unit
}
fun RequestBuilder.loadWithPotentialHd(model: HdImageMaybe) =
thumbnail(clone().load(model.url))
.load(model)
.apply(RequestOptions().override(Target.SIZE_ORIGINAL))
class HdImageLoading : ModelLoader {
override fun buildLoadData(
model: HdImageMaybe,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData? =
if (!model.isValid) null
else ModelLoader.LoadData(ObjectKey(model), HdImageFetcher(model))
override fun handles(model: HdImageMaybe) = model.isValid
}
class HdImageFetcher(private val model: HdImageMaybe) : DataFetcher {
@Volatile
private var cancelled: Boolean = false
private var urlCall: Call? = null
private var inputStream: InputStream? = null
private fun DataFetcher.DataCallback.fail(msg: String) {
onLoadFailed(RuntimeException(msg))
}
override fun getDataClass(): Class = InputStream::class.java
override fun getDataSource(): DataSource = DataSource.REMOTE
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) {
if (!model.isValid) return callback.fail("Model is invalid")
val result: Result = runCatching {
runBlocking {
withTimeout(20000L) {
val auth = fbAuth.fetch(model.cookie).await()
if (cancelled) throw RuntimeException("Cancelled")
val url = auth.getFullSizedImage(model.id).invoke()
?: throw RuntimeException("Null url")
if (cancelled) throw RuntimeException("Cancelled")
if (!url.contains("png") && !url.contains("jpg")) throw RuntimeException("Invalid format")
urlCall?.execute()?.body()?.byteStream()
}
}
}
if (result.isSuccess)
callback.onDataReady(result.getOrNull())
else
callback.onLoadFailed(
result.exceptionOrNull() as? Exception ?: RuntimeException("Failed")
)
}
override fun cleanup() {
try {
inputStream?.close()
} catch (e: IOException) {
} finally {
inputStream = null
}
}
override fun cancel() {
cancelled = true
urlCall?.cancel()
urlCall = null
cleanup()
}
}