feat: 图片上传 API
This commit is contained in:
parent
d63d7a5a13
commit
8f3eddc006
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.huanchengfly.tieba.post.api.models
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UploadPictureResultBean(
|
||||||
|
@SerialName("error_code")
|
||||||
|
@SerializedName("error_code")
|
||||||
|
val errorCode: String,
|
||||||
|
@SerialName("error_msg")
|
||||||
|
@SerializedName("error_msg")
|
||||||
|
val errorMsg: String,
|
||||||
|
val resourceId: String,
|
||||||
|
val chunkNo: String,
|
||||||
|
val picId: String,
|
||||||
|
val picInfo: PicInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PicInfo(
|
||||||
|
val originPic: PicInfoItem,
|
||||||
|
val bigPic: PicInfoItem,
|
||||||
|
val smallPic: PicInfoItem,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PicInfoItem(
|
||||||
|
val width: String,
|
||||||
|
val height: String,
|
||||||
|
val type: String,
|
||||||
|
val picUrl: String
|
||||||
|
)
|
||||||
|
|
@ -12,6 +12,15 @@ import okio.ByteString.Companion.encodeUtf8
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
fun buildMultipartBody(
|
||||||
|
boundary: String = UUID.randomUUID().toString(),
|
||||||
|
builder: MyMultipartBody.Builder.() -> Unit
|
||||||
|
): MyMultipartBody {
|
||||||
|
return MyMultipartBody.Builder(boundary)
|
||||||
|
.apply(builder)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("NAME_SHADOWING")
|
@Suppress("NAME_SHADOWING")
|
||||||
class MyMultipartBody internal constructor(
|
class MyMultipartBody internal constructor(
|
||||||
private val boundaryByteString: ByteString,
|
private val boundaryByteString: ByteString,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import com.huanchengfly.tieba.post.utils.MobileInfoUtil
|
||||||
import com.huanchengfly.tieba.post.utils.UIDUtil
|
import com.huanchengfly.tieba.post.utils.UIDUtil
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import okhttp3.RequestBody
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.http.*
|
import retrofit2.http.*
|
||||||
|
|
||||||
|
|
@ -457,4 +458,16 @@ interface OfficialTiebaApi {
|
||||||
@Field("tbs") tbs: String = AccountUtil.getLoginInfo()!!.tbs,
|
@Field("tbs") tbs: String = AccountUtil.getLoginInfo()!!.tbs,
|
||||||
@Field("stoken") stoken: String = AccountUtil.getSToken()!!
|
@Field("stoken") stoken: String = AccountUtil.getSToken()!!
|
||||||
): Flow<AgreeBean>
|
): Flow<AgreeBean>
|
||||||
|
|
||||||
|
@Headers(
|
||||||
|
"${Header.FORCE_LOGIN}: ${Header.FORCE_LOGIN_TRUE}",
|
||||||
|
"${Header.DROP_HEADERS}: ${Header.CHARSET},${Header.CLIENT_TYPE}",
|
||||||
|
"${Header.NO_COMMON_PARAMS}: ${Param.SWAN_GAME_VER},${Param.SDK_VER}",
|
||||||
|
)
|
||||||
|
@POST("/c/s/uploadPicture")
|
||||||
|
fun uploadPicture(
|
||||||
|
@Body body: RequestBody,
|
||||||
|
@retrofit2.http.Header(Header.COOKIE) cookie: String = "ka=open;BAIDUID=${ClientUtils.baiduId}".takeIf { ClientUtils.baiduId != null }
|
||||||
|
?: "ka=open",
|
||||||
|
): Flow<UploadPictureResultBean>
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
package com.huanchengfly.tieba.post.components
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.util.Log
|
||||||
|
import com.huanchengfly.tieba.post.api.BOUNDARY
|
||||||
|
import com.huanchengfly.tieba.post.api.booleanToString
|
||||||
|
import com.huanchengfly.tieba.post.api.models.UploadPictureResultBean
|
||||||
|
import com.huanchengfly.tieba.post.api.retrofit.RetrofitTiebaApi
|
||||||
|
import com.huanchengfly.tieba.post.api.retrofit.body.MyMultipartBody
|
||||||
|
import com.huanchengfly.tieba.post.api.retrofit.body.buildMultipartBody
|
||||||
|
import com.huanchengfly.tieba.post.api.retrofit.exception.TiebaException
|
||||||
|
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorCode
|
||||||
|
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage
|
||||||
|
import com.huanchengfly.tieba.post.utils.MD5Util
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.flatMapConcat
|
||||||
|
import kotlinx.coroutines.flow.last
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.runningFold
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import java.io.File
|
||||||
|
import java.io.RandomAccessFile
|
||||||
|
|
||||||
|
class ImageUploader(
|
||||||
|
private val forumName: String,
|
||||||
|
private val chunkSize: Int = DEFAULT_CHUNK_SIZE
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val DEFAULT_CHUNK_SIZE = 512000
|
||||||
|
|
||||||
|
const val IMAGE_MAX_SIZE = 5242880
|
||||||
|
const val ORIGIN_IMAGE_MAX_SIZE = 10485760
|
||||||
|
|
||||||
|
const val PIC_WATER_TYPE_NO = 0
|
||||||
|
const val PIC_WATER_TYPE_USER_NAME = 1
|
||||||
|
const val PIC_WATER_TYPE_FORUM_NAME = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uploadImages(
|
||||||
|
filePaths: List<String>,
|
||||||
|
isOriginImage: Boolean = false,
|
||||||
|
): Flow<List<UploadPictureResultBean>> {
|
||||||
|
return filePaths.asFlow()
|
||||||
|
.map { filePath ->
|
||||||
|
uploadSinglePicture(filePath, isOriginImage)
|
||||||
|
}
|
||||||
|
.runningFold<UploadPictureResultBean, MutableList<UploadPictureResultBean>>(initial = mutableListOf()) { list, result ->
|
||||||
|
list.add(result)
|
||||||
|
list
|
||||||
|
}
|
||||||
|
.filter { it.size == filePaths.size }
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
suspend fun uploadSinglePicture(
|
||||||
|
filePath: String,
|
||||||
|
isOriginImage: Boolean = false,
|
||||||
|
): UploadPictureResultBean {
|
||||||
|
val option = BitmapFactory.Options().apply {
|
||||||
|
inJustDecodeBounds = true
|
||||||
|
}
|
||||||
|
BitmapFactory.decodeFile(filePath, option)
|
||||||
|
val width = option.outWidth
|
||||||
|
val height = option.outHeight
|
||||||
|
check(width > 0 && height > 0) { "图片宽高不正确" }
|
||||||
|
val file = File(filePath)
|
||||||
|
val fileLength = file.length()
|
||||||
|
val maxSize = if (isOriginImage) ORIGIN_IMAGE_MAX_SIZE else IMAGE_MAX_SIZE
|
||||||
|
check(fileLength <= maxSize) { "图片大小超过限制" }
|
||||||
|
val fileMd5 = MD5Util.toMd5(file)
|
||||||
|
val isMultipleChunkSize = fileLength % chunkSize == 0L
|
||||||
|
val totalChunkNum = fileLength / chunkSize + if (isMultipleChunkSize) 0 else 1
|
||||||
|
Log.i("ImageUploader", "fileLength=$fileLength, totalChunkNum=$totalChunkNum")
|
||||||
|
val requestBodies = (0 until totalChunkNum).map { chunk ->
|
||||||
|
val isFinish = chunk == totalChunkNum - 1
|
||||||
|
val curChunkSize = if (isFinish) {
|
||||||
|
if (isMultipleChunkSize) {
|
||||||
|
chunkSize
|
||||||
|
} else {
|
||||||
|
fileLength % chunkSize
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chunkSize
|
||||||
|
}.toInt()
|
||||||
|
val chunkBytes = ByteArray(curChunkSize)
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
RandomAccessFile(file, "r").use {
|
||||||
|
it.seek(chunk * chunkSize.toLong())
|
||||||
|
it.read(chunkBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildMultipartBody(BOUNDARY) {
|
||||||
|
setType(MyMultipartBody.FORM)
|
||||||
|
addFormDataPart("alt", "json")
|
||||||
|
addFormDataPart("chunkNo", "${chunk + 1}")
|
||||||
|
if (forumName.isNotEmpty()) addFormDataPart("forum_name", forumName)
|
||||||
|
addFormDataPart("groupId", "1")
|
||||||
|
addFormDataPart("height", "$height")
|
||||||
|
addFormDataPart("isFinish", isFinish.booleanToString())
|
||||||
|
addFormDataPart("is_bjh", "0")
|
||||||
|
addFormDataPart("pic_water_type", "2")
|
||||||
|
addFormDataPart("resourceId", "$fileMd5$chunkSize")
|
||||||
|
addFormDataPart("saveOrigin", isOriginImage.booleanToString())
|
||||||
|
addFormDataPart("size", "$fileLength")
|
||||||
|
if (forumName.isNotEmpty()) addFormDataPart("small_flow_fname", forumName)
|
||||||
|
addFormDataPart("width", "$width")
|
||||||
|
addFormDataPart("chunk", "file", chunkBytes.toRequestBody())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requestBodies.asFlow()
|
||||||
|
.flatMapConcat { RetrofitTiebaApi.OFFICIAL_TIEBA_API.uploadPicture(it) }
|
||||||
|
.catch {
|
||||||
|
throw UploadPictureFailedException(it.getErrorCode(), it.getErrorMessage())
|
||||||
|
}
|
||||||
|
.onEach {
|
||||||
|
Log.i("ImageUploader", "uploadSinglePicture: $it")
|
||||||
|
}
|
||||||
|
.last()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UploadPictureFailedException(
|
||||||
|
override val code: Int = -1,
|
||||||
|
override val message: String = "上传图片失败",
|
||||||
|
) : TiebaException(message)
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
package com.huanchengfly.tieba.post.utils;
|
package com.huanchengfly.tieba.post.utils;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
@ -51,4 +55,27 @@ public class MD5Util {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static String toMd5(@NonNull File file) {
|
||||||
|
if (!file.isFile()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
MessageDigest digest;
|
||||||
|
FileInputStream in;
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int len;
|
||||||
|
try {
|
||||||
|
digest = MessageDigest.getInstance("MD5");
|
||||||
|
in = new FileInputStream(file);
|
||||||
|
while ((len = in.read(buffer, 0, 1024)) != -1) {
|
||||||
|
digest.update(buffer, 0, len);
|
||||||
|
}
|
||||||
|
in.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return toHexString(digest.digest());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue