@@ -0,0 +1,114 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getComicsSearchUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/comics/search", APIUrl)
|
||||
}
|
||||
|
||||
func getComicsDetailUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/comics/detail", APIUrl)
|
||||
}
|
||||
|
||||
func ComicsSearch(ctx context.Context, req ComicsSearchListReq) (resp ComicsSearchListResp, err error) {
|
||||
endpoint := getComicsSearchUrl()
|
||||
req.Need_total_info = "y"
|
||||
var data map[string]interface{}
|
||||
data, err = StructToMapViaJSON(req)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = comicsPost(ctx, endpoint, data, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ComicsDetail(ctx context.Context, req ComicsDetailReq) (resp ComicsDetailResp, err error) {
|
||||
endpoint := getComicsDetailUrl()
|
||||
|
||||
var data map[string]interface{}
|
||||
data, err = StructToMapViaJSON(req)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = comicsPost(ctx, endpoint, data, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func comicsPost(ctx context.Context, endpoint string, req map[string]interface{}, response interface{}) (err error) {
|
||||
jsonData, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress", log.E(err))
|
||||
return
|
||||
}
|
||||
encryptedData, err := encryptBase64(string(jsonData), APIKey)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress encryptBase64", log.E(err))
|
||||
return
|
||||
}
|
||||
// 创建 POST 请求
|
||||
httpReq, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer([]byte(encryptedData)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Content-Length", fmt.Sprintf("%d", len(encryptedData)))
|
||||
httpReq.Header.Set("appid", Appid)
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应体
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return
|
||||
}
|
||||
// 解析 JSON
|
||||
var res Response
|
||||
if err = json.Unmarshal(body, &res); err != nil {
|
||||
return
|
||||
}
|
||||
if res.Status != "y" {
|
||||
err = fmt.Errorf("errorCode:%v error:%v", res.ErrorCode, res.Error)
|
||||
return
|
||||
}
|
||||
|
||||
// 进行解密
|
||||
dataStr, err := decryptBase64(res.Data, APIKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal([]byte(dataStr), response)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package laosiji
|
||||
|
||||
type Tags struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ContentInfo struct {
|
||||
W string `json:"w"`
|
||||
H string `json:"h"`
|
||||
F string `json:"f"`
|
||||
}
|
||||
|
||||
type ComicsSearchInfo struct {
|
||||
Id string `json:"id"` // id
|
||||
Name string `json:"name"` // 名称
|
||||
Alias_name string `json:"alias_name"` // 别名
|
||||
Type string `json:"type"` // 类型
|
||||
Img string `json:"img"` // 封面
|
||||
Description string `json:"description"` // 描述
|
||||
Money string `json:"money"` //
|
||||
Category string `json:"category"` // 分类
|
||||
Update_status string `json:"update_status"` // 更新状态 0更新中 1更新完成
|
||||
Update_date string `json:"update_date"` // 更新时间
|
||||
Chapter_count string `json:"chapter_count"` // 章节数量
|
||||
Is_adult string `json:"is_adult"` // 是否有声漫
|
||||
Tags []Tags `json:"tags"` // 标签
|
||||
Sub_title string `json:"sub_title"` // 子标题
|
||||
Chapter []ChapterInfo `json:"chapter"` // 漫画章节
|
||||
IsAdd bool `json:"isAdd"`
|
||||
}
|
||||
|
||||
type ComicsSearchListReq struct {
|
||||
Page string `json:"page"` //
|
||||
Page_size string `json:"page_size"` //
|
||||
Cat_id string `json:"cat_id"` //
|
||||
Start_time string `json:"start_time"` // 更新开始时间
|
||||
End_time string `json:"end_time"` // 更新结束时间
|
||||
Is_end string `json:"is_end"` // 是否完结 y | n
|
||||
Source_site string `json:"source_site"` // 源站 如 www.toptoon.net
|
||||
Source_url string `json:"source_url"` // 源链接 如 https://www.toptoon.net/comic/epList/81181
|
||||
Need_total_info string `json:"need_total_info"` // 分页信息 默认y
|
||||
Keywords string `json:"keywords"` // 关键字
|
||||
}
|
||||
|
||||
type ChapterInfo struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Img string `json:"img"`
|
||||
Content []ContentInfo `json:"content"`
|
||||
}
|
||||
type ComicsSearchListResp struct {
|
||||
Data []ComicsSearchInfo `json:"data"` //
|
||||
Total string `json:"total"` //
|
||||
Current_page string `json:"current_page"` //
|
||||
Page_size string `json:"page_size"` //
|
||||
Last_page string `json:"last_page"` //
|
||||
}
|
||||
|
||||
type ComicsDetailReq struct {
|
||||
Id string `json:"id"` // id
|
||||
}
|
||||
|
||||
type ComicsDetailResp struct {
|
||||
ComicsSearchInfo
|
||||
}
|
||||
|
||||
type ComicsSyncReq struct {
|
||||
Ids []string `json:"ids"`
|
||||
}
|
||||
|
||||
type ComicsSyncResp struct {
|
||||
List []ComicsSearchInfo `json:"list"`
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/web/webg"
|
||||
"bytes"
|
||||
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
Name string
|
||||
Appid string
|
||||
APIKey string
|
||||
APIUrl string
|
||||
IMAGEYUAN string
|
||||
NoticeURL string
|
||||
)
|
||||
|
||||
// Config 是老司机接口配置。所有凭证必须由各服务自己的配置文件注入。
|
||||
type Config struct {
|
||||
AppID string
|
||||
APIKey string
|
||||
APIUrl string
|
||||
ImageYuan string
|
||||
NoticeURL string
|
||||
}
|
||||
|
||||
func Init(lsjCfg webg.GlobalConfig) {
|
||||
InitConfig(Config{
|
||||
AppID: lsjCfg.LSJ.AppID,
|
||||
APIKey: lsjCfg.LSJ.APIKey,
|
||||
APIUrl: lsjCfg.LSJ.APIUrl,
|
||||
ImageYuan: lsjCfg.LSJ.ImageYuan,
|
||||
})
|
||||
}
|
||||
|
||||
// InitConfig 供不依赖 web 配置结构的服务(例如 skd)初始化老司机客户端。
|
||||
func InitConfig(cfg Config) {
|
||||
Name = "老司机"
|
||||
Appid = strings.TrimSpace(cfg.AppID)
|
||||
APIKey = strings.TrimSpace(cfg.APIKey)
|
||||
APIUrl = strings.TrimRight(strings.TrimSpace(cfg.APIUrl), "/")
|
||||
IMAGEYUAN = strings.TrimRight(strings.TrimSpace(cfg.ImageYuan), "/")
|
||||
NoticeURL = strings.TrimSpace(cfg.NoticeURL)
|
||||
}
|
||||
|
||||
// Configured 判断调用老司机接口所需的配置是否完整。
|
||||
func Configured() bool {
|
||||
return Appid != "" && APIKey != "" && APIUrl != ""
|
||||
}
|
||||
|
||||
func ensureConfigured() error {
|
||||
if Configured() {
|
||||
return nil
|
||||
}
|
||||
missing := make([]string, 0, 3)
|
||||
if Appid == "" {
|
||||
missing = append(missing, "appId")
|
||||
}
|
||||
if APIKey == "" {
|
||||
missing = append(missing, "apiKey")
|
||||
}
|
||||
if APIUrl == "" {
|
||||
missing = append(missing, "apiUrl")
|
||||
}
|
||||
return fmt.Errorf("laosiji configuration missing: %s", strings.Join(missing, ","))
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Status string `json:"status"`
|
||||
Data string `json:"data"` // 如果成功,返回的这个数据是加密的
|
||||
Time string `json:"time"`
|
||||
Error string `json:"error"`
|
||||
ErrorCode int `json:"errorCode"`
|
||||
}
|
||||
|
||||
func QueryUndress(ctx context.Context, taskID string) (resp QueryUndressResponse, err error) {
|
||||
endpoint := getQueryUrl()
|
||||
err = post(ctx, endpoint, map[string]interface{}{
|
||||
"task_id": taskID,
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("taskID", taskID), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GenerateUndress(ctx context.Context, req map[string]interface{}) (resp GenerateUndressResponse, err error) {
|
||||
endpoint := getAiUndressGenerateUrl()
|
||||
err = post(ctx, endpoint, req, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("req", req), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func QueryTextToImage(ctx context.Context, taskID string) (resp QueryTextToImageResponse, err error) {
|
||||
endpoint := getQueryUrl()
|
||||
err = post(ctx, endpoint, map[string]interface{}{
|
||||
"task_id": taskID,
|
||||
}, &resp)
|
||||
if err != nil {
|
||||
log.Error("QueryTextToImage post fail", log.Any("taskID", taskID), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GenerateTextToImage(ctx context.Context, req map[string]interface{}) (resp GenerateTextToImageResponse, err error) {
|
||||
endpoint := getTextToImageGenerateUrl()
|
||||
err = post(ctx, endpoint, req, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateTextToImage post fail", log.Any("req", req), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getQueryUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/ai/detail", APIUrl)
|
||||
}
|
||||
|
||||
func getTextToImageGenerateUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/ai/generate", APIUrl)
|
||||
}
|
||||
|
||||
func getAiUndressGenerateUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/ai/undress", APIUrl)
|
||||
}
|
||||
|
||||
func post(ctx context.Context, endpoint string, req map[string]interface{}, response interface{}) (err error) {
|
||||
if err = ensureConfigured(); err != nil {
|
||||
return err
|
||||
}
|
||||
jsonData, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress", log.E(err))
|
||||
return
|
||||
}
|
||||
encryptedData, err := encryptBase64(string(jsonData), APIKey)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress encryptBase64", log.E(err))
|
||||
return
|
||||
}
|
||||
// 创建 POST 请求
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer([]byte(encryptedData)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Content-Length", fmt.Sprintf("%d", len(encryptedData)))
|
||||
httpReq.Header.Set("appid", Appid)
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应体
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return fmt.Errorf("laosiji api http status:%d body:%s", resp.StatusCode, truncateToolBody(body, APIKey, Appid))
|
||||
}
|
||||
|
||||
// 解析 JSON
|
||||
var res Response
|
||||
if err = json.Unmarshal(body, &res); err != nil {
|
||||
return
|
||||
}
|
||||
if res.Status != "y" {
|
||||
err = fmt.Errorf("errorCode:%v error:%s", res.ErrorCode, truncateToolText(res.Error, APIKey, Appid))
|
||||
return
|
||||
}
|
||||
|
||||
// 进行解密
|
||||
dataStr, err := decryptBase64(res.Data, APIKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal([]byte(dataStr), response)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AES-128-ECB encrypt
|
||||
func encryptBase64(input, key string) (string, error) {
|
||||
if len(key) > 16 {
|
||||
key = key[:16]
|
||||
}
|
||||
plainText := []byte(input)
|
||||
keyBytes := []byte(key)
|
||||
block, err := aes.NewCipher(keyBytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
blockSize := block.BlockSize()
|
||||
plainText = pkcs7Padding(plainText, blockSize)
|
||||
|
||||
encrypted := make([]byte, len(plainText))
|
||||
for bs, be := 0, blockSize; bs < len(plainText); bs, be = bs+blockSize, be+blockSize {
|
||||
block.Encrypt(encrypted[bs:be], plainText[bs:be])
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(encrypted), nil
|
||||
}
|
||||
|
||||
// AES-128-ECB decrypt
|
||||
func decryptBase64(cipherText, key string) (string, error) {
|
||||
if len(key) > 16 {
|
||||
key = key[:16]
|
||||
}
|
||||
data, err := base64.StdEncoding.DecodeString(cipherText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher([]byte(key))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
blockSize := block.BlockSize()
|
||||
decrypted := make([]byte, len(data))
|
||||
for bs, be := 0, blockSize; bs < len(data); bs, be = bs+blockSize, be+blockSize {
|
||||
block.Decrypt(decrypted[bs:be], data[bs:be])
|
||||
}
|
||||
|
||||
decrypted = pkcs7UnPadding(decrypted)
|
||||
return string(decrypted), nil
|
||||
}
|
||||
|
||||
// PKCS7Padding pads plaintext for AES ECB
|
||||
func pkcs7Padding(src []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(src)%blockSize
|
||||
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(src, padText...)
|
||||
}
|
||||
|
||||
// PKCS7UnPadding removes padding
|
||||
func pkcs7UnPadding(src []byte) []byte {
|
||||
length := len(src)
|
||||
unpadding := int(src[length-1])
|
||||
return src[:(length - unpadding)]
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getMovieSearchUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/movie/search", APIUrl)
|
||||
}
|
||||
|
||||
func getMovieDetailUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/movie/detail", APIUrl)
|
||||
}
|
||||
|
||||
func getMoviedDetailByMidUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/movie/detailByMid", APIUrl)
|
||||
}
|
||||
|
||||
// StructToMapViaJSON 通过 JSON 转换结构体到 map
|
||||
func StructToMapViaJSON(obj interface{}) (map[string]interface{}, error) {
|
||||
// 将结构体转换为 JSON
|
||||
jsonData, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 将 JSON 解析为 map
|
||||
var result map[string]interface{}
|
||||
err = json.Unmarshal(jsonData, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// MovieSearch 视频查询
|
||||
func MovieSearch(ctx context.Context, req MovieSearchReq) (resp MovieSearchResp, err error) {
|
||||
endpoint := getMovieSearchUrl()
|
||||
req.Need_total_info = "y"
|
||||
|
||||
var data map[string]interface{}
|
||||
data, err = StructToMapViaJSON(req)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = moviePost(ctx, endpoint, data, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MovieDetail 获取视频列表
|
||||
func MovieDetail(ctx context.Context, req MovieDetailReq) (resp MovieDetailResp, err error) {
|
||||
endpoint := getMovieDetailUrl()
|
||||
|
||||
var data map[string]interface{}
|
||||
data, err = StructToMapViaJSON(req)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = moviePost(ctx, endpoint, data, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func moviePost(ctx context.Context, endpoint string, req map[string]interface{}, response interface{}) (err error) {
|
||||
if err = ensureConfigured(); err != nil {
|
||||
return err
|
||||
}
|
||||
jsonData, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress", log.E(err))
|
||||
return
|
||||
}
|
||||
encryptedData, err := encryptBase64(string(jsonData), APIKey)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress encryptBase64", log.E(err))
|
||||
return
|
||||
}
|
||||
// 创建 POST 请求
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer([]byte(encryptedData)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Content-Length", fmt.Sprintf("%d", len(encryptedData)))
|
||||
httpReq.Header.Set("appid", Appid)
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应体
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return fmt.Errorf("laosiji movie api http status:%d body:%s", resp.StatusCode, truncateToolBody(body, APIKey, Appid))
|
||||
}
|
||||
// 解析 JSON
|
||||
var res Response
|
||||
if err = json.Unmarshal(body, &res); err != nil {
|
||||
return
|
||||
}
|
||||
if res.Status != "y" {
|
||||
err = fmt.Errorf("errorCode:%v error:%s", res.ErrorCode, truncateToolText(res.Error, APIKey, Appid))
|
||||
return
|
||||
}
|
||||
|
||||
// 进行解密
|
||||
dataStr, err := decryptBase64(res.Data, APIKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal([]byte(dataStr), response)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package laosiji
|
||||
|
||||
type TagInfo struct {
|
||||
Id string `json:"id"` // 视频编号
|
||||
Name string `json:"name"` // 视频名字
|
||||
}
|
||||
|
||||
type LinkInfo struct {
|
||||
Id string `json:"id"` // 视频编号
|
||||
Name string `json:"name"` // 视频名字
|
||||
Preview_m3u8_url string `json:"preview_m3u8_url"` // 预览地址(部分视频无预览地址)
|
||||
M3u8_url string `json:"m3u8_url"` // 播放地址
|
||||
Hevc_m3u8_url string `json:"hevc_m3u8_url"` // H.265 播放地址
|
||||
}
|
||||
|
||||
type MovieInfo struct {
|
||||
Id string `json:"id"` // 视频编号
|
||||
Name string `json:"name"` // 视频名字
|
||||
Img_x string `json:"img_x"` // 横图封面
|
||||
Img_y string `json:"img_y"` // 竖图封面
|
||||
Img_type string `json:"img_type"` // 封面类型 long 横图 short竖图
|
||||
Cat_id string `json:"cat_id"` // 分类编号
|
||||
Cat_name string `json:"cat_name"` // 分类名字
|
||||
Status string `json:"status"` // 上架状态
|
||||
Status_text string `json:"status_text"` //
|
||||
Show_at string `json:"show_at"` // 上架时间
|
||||
Tags []TagInfo `json:"tags"` // 标签
|
||||
Update_status string `json:"update_status"` // 更新状态0 更新重 1已完结
|
||||
Description string `json:"description"` // 视频描述
|
||||
Language string `json:"language"` // 视频语言
|
||||
Director string `json:"director"` // 视频导演
|
||||
Issue_date string `json:"issue_date"` // 视频上架时间
|
||||
Actor string `json:"actor"` // 演员
|
||||
Up_user string `json:"up_user"` //
|
||||
IsAdd bool `json:"isAdd"`
|
||||
}
|
||||
|
||||
type NovelSearchInfo struct {
|
||||
Id string `json:"id"` // id
|
||||
Name string `json:"name"` // 名称
|
||||
Alias_name string `json:"alias_name"` // 别名
|
||||
Author string `json:"author"` // 作者
|
||||
Type string `json:"type"` // 类型
|
||||
Img string `json:"img"` // 封面
|
||||
Description string `json:"description"` // 描述
|
||||
Money string `json:"money"` //
|
||||
Sub_title string `json:"sub_title"` // 子标题
|
||||
Category string `json:"category"` // 分类
|
||||
Category_name string `json:"category_name"` // 分类名称
|
||||
Update_status string `json:"update_status"` // 更新状态 0更新中 1更新完成
|
||||
Update_date string `json:"update_date"` // 更新时间
|
||||
Chapter_count string `json:"chapter_count"` // 章节数量
|
||||
Is_adult string `json:"is_adult"` // 是否是有声
|
||||
Tags []Tags `json:"tags"` // 标签
|
||||
Last_update string `json:"last_update"` // 最后更新时间
|
||||
Created_at string `json:"created_at"` // 创建时间
|
||||
Updated_at string `json:"updated_at"` // 更新时间
|
||||
Chapter []NovelChapterInfo `json:"chapter"` // 小说章节
|
||||
IsAdd bool `json:"isAdd"`
|
||||
}
|
||||
|
||||
type NovelChapterInfo struct {
|
||||
Id string `json:"id"` // id
|
||||
Name string `json:"name"` // 标题
|
||||
Img string `json:"img"` // 图片
|
||||
Content string `json:"content"` // 内容
|
||||
Is_audio string `json:"is_audio"` // 是否有声
|
||||
}
|
||||
|
||||
type MovieDetailInfo struct {
|
||||
Id string `json:"id"` // 视频编号
|
||||
Name string `json:"name"` // 视频名字
|
||||
Img_x string `json:"img_x"` // 横图封面
|
||||
Img_y string `json:"img_y"` // 竖图封面
|
||||
Img_type string `json:"img_type"` // 封面类型 long 横图 short竖图
|
||||
Cat_id string `json:"cat_id"` // 分类编号
|
||||
Cat_name string `json:"cat_name"` // 分类名字
|
||||
Status string `json:"status"` // 上架状态
|
||||
Status_text string `json:"status_text"` //
|
||||
Show_at string `json:"show_at"` // 上架时间
|
||||
Tags []TagInfo `json:"tags"` // 标签
|
||||
Update_status string `json:"update_status"` // 更新状态0 更新重 1已完结
|
||||
Description string `json:"description"` // 视频描述
|
||||
Language string `json:"language"` // 视频语言
|
||||
Director string `json:"director"` // 视频导演
|
||||
Issue_date string `json:"issue_date"` // 视频上架时间
|
||||
Duration string `json:"duration"` // 视频时长
|
||||
Actor string `json:"actor"` // 演员
|
||||
|
||||
Is_more_link string `json:"is_more_link"` // 是多集还是单集
|
||||
Preview_images []string `json:"preview_images"` // 预览图片
|
||||
Links []LinkInfo `json:"links"` // 链接
|
||||
Series string `json:"series"` // 系列 主要是av
|
||||
Source_tags string `json:"source_tags"` // 采集网站的标签
|
||||
// Source_actor string `json:"source_actor"` // 采集网站的演员 主要是av使用
|
||||
}
|
||||
|
||||
/*
|
||||
| 分类编号(cat_id) | 名称 | 分区(position) |
|
||||
| 13 | 成人短视频 | guochan |
|
||||
| 12 | VR | av |
|
||||
| 11 | 电影解说 | movie |
|
||||
| 10 | 音乐 | movie |
|
||||
| 9 | 短剧 | movie |
|
||||
| 8 | 纪录片 | movie |
|
||||
| 7 | 动漫 | movie |
|
||||
| 6 | 电影 | movie |
|
||||
| 5 | 连续剧 | movie |
|
||||
| 4 | 综艺 | movie |
|
||||
| 3 | GC | guochan |
|
||||
| 2 | DM | guochan |
|
||||
| 1 | AV | av |
|
||||
| | |
|
||||
| position 说明 guochan 是国产成人视频 av 主要是日本和欧美成人视频 movie 是正规影视资源 bl 男同 douyin 短视频 cartoon 动漫 dark 暗网资源 所有资源需要用户具备权限才能获取 全部传递all
|
||||
*/
|
||||
type MovieSearchReq struct {
|
||||
Position string `json:"position"` // position 说明 guochan 是国产成人视频 av 主要是日本和欧美成人视频 movie 是正规影视资源 bl 男同 douyin 短视频 cartoon 动漫 dark 暗网资源
|
||||
Keywords string `json:"keywords"` // 关键字
|
||||
Ids string `json:"ids"` // 视频ID
|
||||
Cat_id string `json:"cat_id"` // 分类ID
|
||||
Update_status string `json:"update_status"` // 1表示已经完结 0表示未完结
|
||||
Hevc_status int `json:"hevc_status"` // 5表示只查询已有 H.265 资源的视频
|
||||
Mid string `json:"mid"` // 一般无需使用 多个用,分开
|
||||
Page string `json:"page"` // 分页
|
||||
Page_size string `json:"page_size"` // 每页数据 建议不要超过500
|
||||
Start_time string `json:"start_time"` // 更新日期开始日期 格式 2022-01-01 12:00:00
|
||||
End_time string `json:"end_time"` // 更新日期结束日期 格式 2022-01-01 12:00:00
|
||||
Home_id string `json:"home_id"` // up主id
|
||||
Need_total_info string `json:"need_total_info"` // 分页信息 默认y
|
||||
}
|
||||
|
||||
// MovieSearchResp 视频查询返回接口
|
||||
type MovieSearchResp struct {
|
||||
// Status string `json:"status"` // 状态 0待处理 -1处理失败 1处理中 2处理成功
|
||||
// Time string `json:"time"` //
|
||||
Data []MovieInfo `json:"data"` //
|
||||
Total string `json:"total"` //
|
||||
Current_page string `json:"current_page"` //
|
||||
Page_size string `json:"page_size"` //
|
||||
Last_page string `json:"last_page"` //
|
||||
}
|
||||
|
||||
// MovieDetailReq 视频详情
|
||||
type MovieDetailReq struct {
|
||||
Id string `json:"id"` // id
|
||||
}
|
||||
|
||||
// MovieSearchResp 视频详情返回接口
|
||||
type MovieDetailResp struct {
|
||||
MovieDetailInfo
|
||||
}
|
||||
|
||||
type MovieAddListReq struct {
|
||||
Ids []string `json:"ids"`
|
||||
Position string `json:"position"`
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getNovelSearchUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/novel/search", APIUrl)
|
||||
}
|
||||
|
||||
func getNovelDetailUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/novel/detail", APIUrl)
|
||||
}
|
||||
|
||||
func NovelSearch(ctx context.Context, req NovelSearchListReq) (resp NovelSearchListResp, err error) {
|
||||
endpoint := getNovelSearchUrl()
|
||||
req.Need_total_info = "y"
|
||||
|
||||
var data map[string]interface{}
|
||||
data, err = StructToMapViaJSON(req)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = novelPost(ctx, endpoint, data, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func NovelDetail(ctx context.Context, req NovelDetailReq) (resp NovelDetailResp, err error) {
|
||||
endpoint := getNovelDetailUrl()
|
||||
|
||||
var data map[string]interface{}
|
||||
data, err = StructToMapViaJSON(req)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = novelPost(ctx, endpoint, data, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func novelPost(ctx context.Context, endpoint string, req map[string]interface{}, response interface{}) (err error) {
|
||||
jsonData, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress", log.E(err))
|
||||
return
|
||||
}
|
||||
encryptedData, err := encryptBase64(string(jsonData), APIKey)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress encryptBase64", log.E(err))
|
||||
return
|
||||
}
|
||||
// 创建 POST 请求
|
||||
httpReq, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer([]byte(encryptedData)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Content-Length", fmt.Sprintf("%d", len(encryptedData)))
|
||||
httpReq.Header.Set("appid", Appid)
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应体
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return
|
||||
}
|
||||
// 解析 JSON
|
||||
var res Response
|
||||
if err = json.Unmarshal(body, &res); err != nil {
|
||||
return
|
||||
}
|
||||
if res.Status != "y" {
|
||||
err = fmt.Errorf("errorCode:%v error:%v", res.ErrorCode, res.Error)
|
||||
return
|
||||
}
|
||||
|
||||
// 进行解密
|
||||
dataStr, err := decryptBase64(res.Data, APIKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal([]byte(dataStr), response)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package laosiji
|
||||
|
||||
type NovelSearchListReq struct {
|
||||
Page string `json:"page"` //
|
||||
Page_size string `json:"page_size"` //
|
||||
Cat_id string `json:"cat_id"` // 分类 audio 18R normal
|
||||
Start_time string `json:"start_time"` // 更新开始时间
|
||||
End_time string `json:"end_time"` // 更新结束时间
|
||||
Is_end string `json:"is_end"` // 是否完结 y | n
|
||||
Need_total_info string `json:"need_total_info"` // 分页信息 默认y
|
||||
Keywords string `json:"keywords"` // 关键字
|
||||
}
|
||||
|
||||
type NovelSearchListResp struct {
|
||||
Data []NovelSearchInfo `json:"data"` //
|
||||
Total string `json:"total"` // 总数
|
||||
Current_page string `json:"current_page"` // 当前页
|
||||
Page_size string `json:"page_size"` // 当前页数
|
||||
Last_page string `json:"last_page"` //
|
||||
}
|
||||
|
||||
type NovelDetailReq struct {
|
||||
Id string `json:"id"` // id
|
||||
}
|
||||
|
||||
type NovelDetailResp struct {
|
||||
NovelSearchInfo
|
||||
}
|
||||
|
||||
type NovelAddListReq struct {
|
||||
Ids []string `json:"ids"`
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"91porn-server/common/log"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getPostSearchUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/post/search", APIUrl)
|
||||
}
|
||||
|
||||
func getPostDetailUrl() string {
|
||||
return fmt.Sprintf("%s/lsjapi/post/detail", APIUrl)
|
||||
}
|
||||
|
||||
func PostSearch(ctx context.Context, req PostSearchListReq) (resp PostSearchListResp, err error) {
|
||||
endpoint := getPostSearchUrl()
|
||||
req.Need_total_info = "y"
|
||||
|
||||
var data map[string]interface{}
|
||||
data, err = StructToMapViaJSON(req)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = postPost(ctx, endpoint, data, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func PostDetail(ctx context.Context, req PostDetailReq) (resp PostDetailResp, err error) {
|
||||
endpoint := getPostDetailUrl()
|
||||
|
||||
var data map[string]interface{}
|
||||
data, err = StructToMapViaJSON(req)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.E(err))
|
||||
return resp, err
|
||||
}
|
||||
|
||||
err = postPost(ctx, endpoint, data, &resp)
|
||||
if err != nil {
|
||||
log.Error("GenerateUndress post fail", log.Any("data", data), log.E(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func postPost(ctx context.Context, endpoint string, req map[string]interface{}, response interface{}) (err error) {
|
||||
jsonData, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress", log.E(err))
|
||||
return
|
||||
}
|
||||
encryptedData, err := encryptBase64(string(jsonData), APIKey)
|
||||
if err != nil {
|
||||
log.Warn("GenerateAiUndress encryptBase64", log.E(err))
|
||||
return
|
||||
}
|
||||
// 创建 POST 请求
|
||||
httpReq, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer([]byte(encryptedData)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Content-Length", fmt.Sprintf("%d", len(encryptedData)))
|
||||
httpReq.Header.Set("appid", Appid)
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应体
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return
|
||||
}
|
||||
// 解析 JSON
|
||||
var res Response
|
||||
if err = json.Unmarshal(body, &res); err != nil {
|
||||
return
|
||||
}
|
||||
if res.Status != "y" {
|
||||
err = fmt.Errorf("errorCode:%v error:%v", res.ErrorCode, res.Error)
|
||||
return
|
||||
}
|
||||
|
||||
// 进行解密
|
||||
dataStr, err := decryptBase64(res.Data, APIKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal([]byte(dataStr), response)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package laosiji
|
||||
|
||||
type Categories struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type PostFiles struct {
|
||||
Image string `json:"image"`
|
||||
Type string `json:"type"`
|
||||
Ico string `json:"ico"`
|
||||
Tips string `json:"tips"`
|
||||
Video_link string `json:"video_link"`
|
||||
}
|
||||
|
||||
type UpUserInfo struct {
|
||||
Id string `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Img string `json:"img"`
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
|
||||
type PostSearchListReq struct {
|
||||
Home_id string `json:"home_id"`
|
||||
Page string `json:"page"`
|
||||
Page_size string `json:"page_size"`
|
||||
Pay_type string `json:"pay_type"`
|
||||
Start_time int `json:"start_time"`
|
||||
End_time int `json:"end_time"`
|
||||
Need_total_info string `json:"need_total_info"`
|
||||
Keywords string `json:"keywords"` // 关键字
|
||||
}
|
||||
|
||||
type PostSearchInfo struct {
|
||||
Id string `json:"id"` // id
|
||||
Title string `json:"title"` // 标题
|
||||
Time string `json:"time"` // 更新时间
|
||||
Money string `json:"money"` //
|
||||
User_id string `json:"user_id"` //
|
||||
Content string `json:"content"` // 文本内容
|
||||
Img string `json:"img"` // 封面
|
||||
Hide_files string `json:"hide_files"` //
|
||||
Position string `json:"position"` // 类型
|
||||
Categories []Categories `json:"categories"` // 分类
|
||||
Img_count string `json:"img_count"` // 图片数量
|
||||
Rich_content []string `json:"up_user"` // 富文本
|
||||
Files []PostFiles `json:"files"` // 内容节点
|
||||
Up_user UpUserInfo `json:"up_user"` // 发布者
|
||||
IsAdd bool `json:"isAdd"`
|
||||
}
|
||||
|
||||
type PostSearchListResp struct {
|
||||
Data []PostSearchInfo `json:"data"` //
|
||||
Total string `json:"total"` //
|
||||
Current_page string `json:"current_page"` //
|
||||
Page_size string `json:"page_size"` //
|
||||
Last_page string `json:"last_page"` //
|
||||
}
|
||||
|
||||
type PostDetailReq struct {
|
||||
Id string `json:"id"` // id
|
||||
}
|
||||
|
||||
type PostDetailResp struct {
|
||||
PostSearchInfo
|
||||
}
|
||||
|
||||
type PostAddListReq struct {
|
||||
Ids []string `json:"ids"`
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package laosiji
|
||||
|
||||
type GenerateTextToImageResponse struct {
|
||||
TaskID string `json:"task_id"` // 任务 ID
|
||||
}
|
||||
|
||||
type QueryTextToImageResponse struct {
|
||||
TaskID string `json:"task_id"` // 任务 ID
|
||||
Bid string `json:"bid"`
|
||||
Fee string `json:"fee"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"` // 状态 0待处理 -1处理失败 1处理中 2处理成功
|
||||
OutData string `json:"out_data"` // 输出数据 不同的ai 处理的数据不一样 查看文档的描述
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/common/hevcpull"
|
||||
"91porn-server/common/log"
|
||||
)
|
||||
|
||||
const (
|
||||
videoTranscodingVersion = "2.0"
|
||||
transcodeNoNeedStatus = 4
|
||||
transcodeSuccessStatus = 5
|
||||
transcodeFailedStatus = -1
|
||||
|
||||
sourceM3u8CheckMaxBytes = 8 << 20
|
||||
)
|
||||
|
||||
var (
|
||||
errTranscodeTaskNotFound = errors.New("laosiji transcode task not found")
|
||||
// ErrTranscodeQueueFull 表示云端 H.265 待处理队列已经达到限制。
|
||||
ErrTranscodeQueueFull = errors.New("laosiji transcode queue full")
|
||||
sourceM3u8HTTPClient = &http.Client{Timeout: 15 * time.Second}
|
||||
uploadHTTPClient = &http.Client{Timeout: 30 * time.Second}
|
||||
)
|
||||
|
||||
// SystemDomainsResp 是老司机 system/domains 接口返回的资源域名和上传配置。
|
||||
type SystemDomainsResp struct {
|
||||
ImgFreeCDN string `json:"img_free_cdn"`
|
||||
MovieFreeCDN string `json:"movie_free_cdn"`
|
||||
MovieSourceCDN string `json:"movie_source_cdn"`
|
||||
UploadURL string `json:"upload_url"`
|
||||
MediaDir string `json:"media_dir"`
|
||||
UploadKey string `json:"upload_key"`
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
type transcodeAPIResp struct {
|
||||
Status string `json:"status"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
Time string `json:"time"`
|
||||
Error string `json:"error"`
|
||||
ErrorCode int `json:"errorCode"`
|
||||
}
|
||||
|
||||
type transcodeTask struct {
|
||||
ID string `json:"id"`
|
||||
FileID string `json:"file_id"`
|
||||
FileURL string `json:"file_url"`
|
||||
Status int `json:"status"`
|
||||
Duration int `json:"duration"`
|
||||
Height int `json:"height"`
|
||||
Width int `json:"width"`
|
||||
TranscodeError string `json:"transcode_error"`
|
||||
TranscodeFile string `json:"transcode_file"`
|
||||
}
|
||||
|
||||
// TranscodeQueueInfo 是云端转码队列概览。
|
||||
type TranscodeQueueInfo struct {
|
||||
Waiting int `json:"waiting"`
|
||||
Done int `json:"done"`
|
||||
Error int `json:"error"`
|
||||
}
|
||||
|
||||
// TranscodeResult 是本地异步任务使用的统一转码结果。
|
||||
type TranscodeResult struct {
|
||||
Status int
|
||||
Done bool
|
||||
NoNeed bool
|
||||
Failed bool
|
||||
HevcURL string
|
||||
FileURL string
|
||||
FileID string
|
||||
ErrorMsg string
|
||||
}
|
||||
|
||||
func getSystemDomainsURL() string {
|
||||
return strings.TrimRight(APIUrl, "/") + "/lsjapi/system/domains"
|
||||
}
|
||||
|
||||
// SystemDomains 获取老司机临时上传、转码配置。
|
||||
func SystemDomains(ctx context.Context) (resp SystemDomainsResp, err error) {
|
||||
if err = ensureConfigured(); err != nil {
|
||||
return resp, err
|
||||
}
|
||||
if err = moviePost(ctx, getSystemDomainsURL(), map[string]interface{}{}, &resp); err != nil {
|
||||
return resp, err
|
||||
}
|
||||
missing := make([]string, 0, 3)
|
||||
if strings.TrimSpace(resp.UploadURL) == "" {
|
||||
missing = append(missing, "upload_url")
|
||||
}
|
||||
if strings.TrimSpace(resp.UploadKey) == "" {
|
||||
missing = append(missing, "upload_key")
|
||||
}
|
||||
if strings.TrimSpace(resp.MovieSourceCDN) == "" {
|
||||
missing = append(missing, "movie_source_cdn")
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
return resp, fmt.Errorf("laosiji system/domains missing: %s", strings.Join(missing, ","))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// SubmitH264ToH265ByFileURL 使用完整 file_url 异步提交转码任务。
|
||||
func SubmitH264ToH265ByFileURL(ctx context.Context, fileURL string, domains SystemDomainsResp, maxRunning int) (TranscodeResult, error) {
|
||||
fileURL = strings.TrimSpace(fileURL)
|
||||
if fileURL == "" {
|
||||
return TranscodeResult{}, errors.New("empty file url")
|
||||
}
|
||||
return submitByTask(ctx, md5Hex(fileURL), fileURL, domains, maxRunning)
|
||||
}
|
||||
|
||||
// SubmitH264ToH265Task decouples the stable cloud task ID from the temporary
|
||||
// signed fetch URL. This keeps polling stable across App domain or signing-key
|
||||
// changes while a task is pending.
|
||||
func SubmitH264ToH265Task(ctx context.Context, fileID, fileURL string, domains SystemDomainsResp, maxRunning int) (TranscodeResult, error) {
|
||||
fileID = strings.TrimSpace(fileID)
|
||||
fileURL = strings.TrimSpace(fileURL)
|
||||
if fileID == "" {
|
||||
return TranscodeResult{}, errors.New("empty file id")
|
||||
}
|
||||
if fileURL == "" {
|
||||
return TranscodeResult{}, errors.New("empty file url")
|
||||
}
|
||||
return submitByTask(ctx, fileID, fileURL, domains, maxRunning)
|
||||
}
|
||||
|
||||
func submitByTask(ctx context.Context, fileID, fileURL string, domains SystemDomainsResp, maxRunning int) (TranscodeResult, error) {
|
||||
task, err := queryVideoTranscoding(ctx, domains, fileID)
|
||||
if err == nil {
|
||||
return transcodeTaskToResult(domains, task, fileURL, fileID), nil
|
||||
}
|
||||
if !errors.Is(err, errTranscodeTaskNotFound) {
|
||||
return TranscodeResult{}, err
|
||||
}
|
||||
if err = checkSourceM3u8(ctx, fileURL); err != nil {
|
||||
return TranscodeResult{}, err
|
||||
}
|
||||
task, err = createVideoTranscoding(ctx, domains, fileID, fileURL, maxRunning)
|
||||
if err != nil {
|
||||
return TranscodeResult{}, err
|
||||
}
|
||||
return transcodeTaskToResult(domains, task, fileURL, fileID), nil
|
||||
}
|
||||
|
||||
func checkSourceM3u8(ctx context.Context, fileURL string) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build source m3u8 request: %s", hevcpull.RedactText(err.Error()))
|
||||
}
|
||||
resp, err := sourceM3u8HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("source m3u8 request failed: %s", hevcpull.RedactText(err.Error()))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return fmt.Errorf("source m3u8 http status:%d url:%s", resp.StatusCode, hevcpull.RedactURL(fileURL))
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, sourceM3u8CheckMaxBytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !looksLikeM3u8(body) {
|
||||
return fmt.Errorf("source m3u8 invalid content url:%s", hevcpull.RedactURL(fileURL))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func looksLikeM3u8(body []byte) bool {
|
||||
body = bytes.TrimPrefix(body, []byte{0xEF, 0xBB, 0xBF})
|
||||
body = bytes.TrimLeft(body, " \t\r\n")
|
||||
return bytes.HasPrefix(body, []byte("#EXTM3U"))
|
||||
}
|
||||
|
||||
// QueryH264ToH265 查询老司机源视频对应的 H.265 云转码结果。
|
||||
func QueryH264ToH265(ctx context.Context, h264URL string) (TranscodeResult, bool, error) {
|
||||
domains, err := SystemDomains(ctx)
|
||||
if err != nil {
|
||||
return TranscodeResult{}, false, err
|
||||
}
|
||||
return QueryH264ToH265WithDomains(ctx, h264URL, domains)
|
||||
}
|
||||
|
||||
// QueryH264ToH265WithDomains 使用已经获取的域名配置查询转码结果。
|
||||
func QueryH264ToH265WithDomains(ctx context.Context, h264URL string, domains SystemDomainsResp) (TranscodeResult, bool, error) {
|
||||
h264URL = strings.TrimSpace(h264URL)
|
||||
if h264URL == "" {
|
||||
return TranscodeResult{}, false, errors.New("empty h264 m3u8 url")
|
||||
}
|
||||
return queryByFileURL(ctx, transcodeSourceURL(h264URL, domains.MovieSourceCDN), domains)
|
||||
}
|
||||
|
||||
// QueryH264ToH265ByFileURL 与 SubmitH264ToH265ByFileURL 使用相同的完整 file_url 查询结果。
|
||||
func QueryH264ToH265ByFileURL(ctx context.Context, fileURL string, domains SystemDomainsResp) (TranscodeResult, bool, error) {
|
||||
fileURL = strings.TrimSpace(fileURL)
|
||||
if fileURL == "" {
|
||||
return TranscodeResult{}, false, errors.New("empty file url")
|
||||
}
|
||||
return queryByTask(ctx, md5Hex(fileURL), fileURL, domains)
|
||||
}
|
||||
|
||||
func queryByFileURL(ctx context.Context, fileURL string, domains SystemDomainsResp) (TranscodeResult, bool, error) {
|
||||
return queryByTask(ctx, md5Hex(fileURL), fileURL, domains)
|
||||
}
|
||||
|
||||
// QueryH264ToH265Task queries by the stable task ID used during submission.
|
||||
// fileURL is diagnostic metadata only and is not sent to the cloud query API.
|
||||
func QueryH264ToH265Task(ctx context.Context, fileID, fileURL string, domains SystemDomainsResp) (TranscodeResult, bool, error) {
|
||||
fileID = strings.TrimSpace(fileID)
|
||||
fileURL = strings.TrimSpace(fileURL)
|
||||
if fileID == "" {
|
||||
return TranscodeResult{}, false, errors.New("empty file id")
|
||||
}
|
||||
return queryByTask(ctx, fileID, fileURL, domains)
|
||||
}
|
||||
|
||||
func queryByTask(ctx context.Context, fileID, fileURL string, domains SystemDomainsResp) (TranscodeResult, bool, error) {
|
||||
task, err := queryVideoTranscoding(ctx, domains, fileID)
|
||||
if err != nil {
|
||||
if errors.Is(err, errTranscodeTaskNotFound) {
|
||||
return TranscodeResult{}, false, nil
|
||||
}
|
||||
return TranscodeResult{}, false, err
|
||||
}
|
||||
return transcodeTaskToResult(domains, task, fileURL, fileID), true, nil
|
||||
}
|
||||
|
||||
func transcodeSourceURL(rawURL, movieSourceCDN string) string {
|
||||
rawURL = strings.TrimSpace(rawURL)
|
||||
movieSourceCDN = strings.TrimSpace(movieSourceCDN)
|
||||
if rawURL == "" || movieSourceCDN == "" {
|
||||
return rawURL
|
||||
}
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err == nil && parsed.Scheme != "" {
|
||||
name := path.Base(parsed.Path)
|
||||
if name != "" && name != "." && strings.Contains(name, ".m3u8") {
|
||||
return joinURLPath(movieSourceCDN, name)
|
||||
}
|
||||
return rawURL
|
||||
}
|
||||
rawURL = strings.TrimPrefix(rawURL, "/")
|
||||
rawURL = strings.TrimPrefix(rawURL, "laosiji/")
|
||||
if idx := strings.Index(rawURL, "m3m/"); idx >= 0 {
|
||||
rawURL = rawURL[idx+len("m3m/"):]
|
||||
}
|
||||
return joinURLPath(movieSourceCDN, rawURL)
|
||||
}
|
||||
|
||||
// TranscodeSourceURL 把本地资源 path 或播放 URL 转换成云转码使用的源站 URL。
|
||||
func TranscodeSourceURL(rawURL, movieSourceCDN string) string {
|
||||
return transcodeSourceURL(rawURL, movieSourceCDN)
|
||||
}
|
||||
|
||||
func createVideoTranscoding(ctx context.Context, domains SystemDomainsResp, fileID, fileURL string, maxRunning int) (transcodeTask, error) {
|
||||
if q, err := getVideoTranscodingQueue(ctx, domains); err != nil {
|
||||
log.Warn("getVideoTranscodingQueue failed", log.E(err))
|
||||
} else if maxRunning > 0 && q.Waiting >= maxRunning {
|
||||
return transcodeTask{}, fmt.Errorf("%w waiting:%d limit:%d", ErrTranscodeQueueFull, q.Waiting, maxRunning)
|
||||
}
|
||||
params := map[string]string{
|
||||
"v": videoTranscodingVersion,
|
||||
"key": domains.UploadKey,
|
||||
"file_id": fileID,
|
||||
"file_url": fileURL,
|
||||
"ext_data": `{"project":"91porn","source":"laosiji","type":"full"}`,
|
||||
}
|
||||
if NoticeURL != "" {
|
||||
params["notice_url"] = NoticeURL
|
||||
}
|
||||
endpoint := joinURLPath(domains.UploadURL, "upload/videoTranscoding")
|
||||
return uploadAPIGet(ctx, endpoint, params)
|
||||
}
|
||||
|
||||
func getVideoTranscodingQueue(ctx context.Context, domains SystemDomainsResp) (TranscodeQueueInfo, error) {
|
||||
endpoint := joinURLPath(domains.UploadURL, "upload/getVideoTranscodingQueueInfo")
|
||||
data, err := uploadAPICall(ctx, endpoint, map[string]string{
|
||||
"v": videoTranscodingVersion,
|
||||
"key": domains.UploadKey,
|
||||
})
|
||||
if err != nil {
|
||||
return TranscodeQueueInfo{}, err
|
||||
}
|
||||
var queue TranscodeQueueInfo
|
||||
if len(data) > 0 {
|
||||
if err = json.Unmarshal(data, &queue); err != nil {
|
||||
return TranscodeQueueInfo{}, err
|
||||
}
|
||||
}
|
||||
return queue, nil
|
||||
}
|
||||
|
||||
func queryVideoTranscoding(ctx context.Context, domains SystemDomainsResp, fileID string) (transcodeTask, error) {
|
||||
endpoint := joinURLPath(domains.UploadURL, "upload/queryVideoTranscoding")
|
||||
return uploadAPIGet(ctx, endpoint, map[string]string{
|
||||
"v": videoTranscodingVersion,
|
||||
"key": domains.UploadKey,
|
||||
"file_id": fileID,
|
||||
})
|
||||
}
|
||||
|
||||
func transcodeTaskToResult(domains SystemDomainsResp, task transcodeTask, fileURL, fileID string) TranscodeResult {
|
||||
result := TranscodeResult{Status: task.Status, FileURL: fileURL, FileID: fileID}
|
||||
switch {
|
||||
case task.Status == transcodeNoNeedStatus:
|
||||
result.Done = true
|
||||
result.NoNeed = true
|
||||
result.HevcURL = task.FileURL
|
||||
case task.Status == transcodeSuccessStatus:
|
||||
if transcodeFile := strings.TrimSpace(task.TranscodeFile); transcodeFile != "" {
|
||||
result.Done = true
|
||||
result.HevcURL = joinURLPath(domains.MovieSourceCDN, transcodeFile)
|
||||
} else {
|
||||
result.Failed = true
|
||||
result.ErrorMsg = "laosiji transcode succeeded without transcode_file"
|
||||
}
|
||||
case task.Status <= transcodeFailedStatus:
|
||||
result.Failed = true
|
||||
result.ErrorMsg = truncateToolText(task.TranscodeError, domains.UploadKey)
|
||||
if result.ErrorMsg == "" {
|
||||
result.ErrorMsg = fmt.Sprintf("laosiji transcode failed status:%d", task.Status)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func uploadAPIGet(ctx context.Context, endpoint string, params map[string]string) (transcodeTask, error) {
|
||||
data, err := uploadAPICall(ctx, endpoint, params)
|
||||
if err != nil {
|
||||
return transcodeTask{}, err
|
||||
}
|
||||
var task transcodeTask
|
||||
if len(data) > 0 {
|
||||
if err = json.Unmarshal(data, &task); err != nil {
|
||||
return transcodeTask{}, err
|
||||
}
|
||||
}
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func uploadAPICall(ctx context.Context, endpoint string, params map[string]string) (json.RawMessage, error) {
|
||||
parsed, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := parsed.Query()
|
||||
for key, value := range params {
|
||||
query.Set(key, value)
|
||||
}
|
||||
parsed.RawQuery = query.Encode()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := uploadHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("upload api request failed: %s", truncateToolText(err.Error(), params["key"]))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
safeBody := truncateToolBody(body, params["key"])
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return nil, fmt.Errorf("upload api http status:%d body:%s", resp.StatusCode, safeBody)
|
||||
}
|
||||
var apiResp transcodeAPIResp
|
||||
if err = json.Unmarshal(body, &apiResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if apiResp.Status != "y" {
|
||||
errMsg := truncateToolText(apiResp.Error, params["key"])
|
||||
if strings.Contains(errMsg, "最多允许待处理") || strings.Contains(errMsg, "排队处理") {
|
||||
return nil, fmt.Errorf("%w errorCode:%d error:%s", ErrTranscodeQueueFull, apiResp.ErrorCode, errMsg)
|
||||
}
|
||||
if apiResp.ErrorCode == 2000 {
|
||||
return nil, fmt.Errorf("%w errorCode:%d error:%s", errTranscodeTaskNotFound, apiResp.ErrorCode, errMsg)
|
||||
}
|
||||
return nil, fmt.Errorf("upload api errorCode:%d error:%s", apiResp.ErrorCode, errMsg)
|
||||
}
|
||||
return apiResp.Data, nil
|
||||
}
|
||||
|
||||
func md5Hex(value string) string {
|
||||
sum := md5.Sum([]byte(value))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// TranscodeFileID 返回源视频 URL 对应的云端幂等键。
|
||||
func TranscodeFileID(fileURL string) string {
|
||||
fileURL = strings.TrimSpace(fileURL)
|
||||
if fileURL == "" {
|
||||
return ""
|
||||
}
|
||||
return md5Hex(fileURL)
|
||||
}
|
||||
|
||||
func joinURLPath(host, uri string) string {
|
||||
host = strings.TrimRight(strings.TrimSpace(host), "/")
|
||||
uri = strings.TrimLeft(strings.TrimSpace(uri), "/")
|
||||
if host == "" {
|
||||
return uri
|
||||
}
|
||||
if uri == "" {
|
||||
return host
|
||||
}
|
||||
return host + "/" + uri
|
||||
}
|
||||
|
||||
// MovieM3u8SourcePath 把完整 URL 或相对路径规范成本地保存的老司机资源 path。
|
||||
func MovieM3u8SourcePath(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
return joinURLPath("laosiji", movieM3u8RelativePath(raw))
|
||||
}
|
||||
|
||||
// MovieM3u8OriginURL 把本地老司机资源 path 还原成源站完整 URL。
|
||||
func MovieM3u8OriginURL(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
if parsed, err := url.Parse(raw); err == nil && parsed.IsAbs() {
|
||||
return raw
|
||||
}
|
||||
return joinURLPath(APIUrl, movieM3u8RelativePath(raw))
|
||||
}
|
||||
|
||||
func movieM3u8RelativePath(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
if parsed, err := url.Parse(raw); err == nil {
|
||||
if parsed.IsAbs() {
|
||||
raw = parsed.Path
|
||||
} else if parsed.Path != "" {
|
||||
raw = parsed.Path
|
||||
}
|
||||
}
|
||||
raw = strings.TrimPrefix(raw, "/")
|
||||
raw = strings.TrimPrefix(raw, "laosiji/")
|
||||
if idx := strings.Index(raw, "m3m/"); idx >= 0 {
|
||||
return raw[idx:]
|
||||
}
|
||||
if strings.Contains(raw, "/") {
|
||||
return raw
|
||||
}
|
||||
return joinURLPath("m3m", raw)
|
||||
}
|
||||
|
||||
func truncateToolBody(body []byte, secrets ...string) string {
|
||||
return truncateToolText(string(body), secrets...)
|
||||
}
|
||||
|
||||
func truncateToolText(text string, secrets ...string) string {
|
||||
const limit = 300
|
||||
text = hevcpull.RedactText(strings.TrimSpace(text))
|
||||
secrets = append(secrets, APIKey, Appid)
|
||||
for _, secret := range secrets {
|
||||
if secret = strings.TrimSpace(secret); secret != "" {
|
||||
text = strings.ReplaceAll(text, secret, "[REDACTED]")
|
||||
text = strings.ReplaceAll(text, url.QueryEscape(secret), "[REDACTED]")
|
||||
}
|
||||
}
|
||||
if len(text) <= limit {
|
||||
return text
|
||||
}
|
||||
return text[:limit] + "..."
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
package laosiji
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return f(req)
|
||||
}
|
||||
|
||||
func preserveConfig(t *testing.T) {
|
||||
t.Helper()
|
||||
old := Config{
|
||||
AppID: Appid,
|
||||
APIKey: APIKey,
|
||||
APIUrl: APIUrl,
|
||||
ImageYuan: IMAGEYUAN,
|
||||
NoticeURL: NoticeURL,
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
InitConfig(old)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfiguredRejectsMissingCredentials(t *testing.T) {
|
||||
preserveConfig(t)
|
||||
InitConfig(Config{})
|
||||
|
||||
if Configured() {
|
||||
t.Fatal("empty configuration must not be ready")
|
||||
}
|
||||
_, err := SystemDomains(context.Background())
|
||||
if err == nil || !strings.Contains(err.Error(), "appId,apiKey,apiUrl") {
|
||||
t.Fatalf("expected a diagnostic configuration error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMovieM3u8PathNormalization(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"": "",
|
||||
"demo.m3u8": "laosiji/m3m/demo.m3u8",
|
||||
"m3m/demo.m3u8": "laosiji/m3m/demo.m3u8",
|
||||
"laosiji/m3m/demo.m3u8": "laosiji/m3m/demo.m3u8",
|
||||
"https://cdn.example.com/m3m/demo.m3u8?token=ignored": "laosiji/m3m/demo.m3u8",
|
||||
"https://cdn.example.com/rk130/hevc/demo/index.m3u8": "laosiji/rk130/hevc/demo/index.m3u8",
|
||||
"laosiji/rk130/hevc/demo/index.m3u8?token=also-ignored": "laosiji/rk130/hevc/demo/index.m3u8",
|
||||
}
|
||||
for input, want := range tests {
|
||||
if got := MovieM3u8SourcePath(input); got != want {
|
||||
t.Errorf("MovieM3u8SourcePath(%q) = %q, want %q", input, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranscodeTaskNoNeedIsExplicit(t *testing.T) {
|
||||
sourceURL := "https://app.example/api/app/vid/h5/light/m3u8/source.m3u8"
|
||||
result := transcodeTaskToResult(SystemDomainsResp{MovieSourceCDN: "https://cdn.example"}, transcodeTask{
|
||||
Status: transcodeNoNeedStatus,
|
||||
FileURL: sourceURL,
|
||||
}, sourceURL, TranscodeFileID(sourceURL))
|
||||
|
||||
if !result.Done || !result.NoNeed {
|
||||
t.Fatalf("status=4 must be done and no-need: %+v", result)
|
||||
}
|
||||
if result.HevcURL != sourceURL {
|
||||
t.Fatalf("no-need diagnostic URL = %q, want %q", result.HevcURL, sourceURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranscodeSuccessWithoutFileIsFailed(t *testing.T) {
|
||||
result := transcodeTaskToResult(SystemDomainsResp{}, transcodeTask{
|
||||
Status: transcodeSuccessStatus,
|
||||
}, "https://app.example/source.m3u8", "file-id")
|
||||
|
||||
if result.Done || !result.Failed {
|
||||
t.Fatalf("empty successful task must fail locally instead of staying pending: %+v", result)
|
||||
}
|
||||
if !strings.Contains(result.ErrorMsg, "without transcode_file") {
|
||||
t.Fatalf("missing diagnostic error: %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTranscodeFailureRedactsCredentials(t *testing.T) {
|
||||
preserveConfig(t)
|
||||
const (
|
||||
appID = "app-id-secret"
|
||||
apiKey = "api-key-secret"
|
||||
uploadKey = "upload key+secret"
|
||||
)
|
||||
InitConfig(Config{
|
||||
AppID: appID,
|
||||
APIKey: apiKey,
|
||||
APIUrl: "https://api.example",
|
||||
})
|
||||
|
||||
result := transcodeTaskToResult(SystemDomainsResp{UploadKey: uploadKey}, transcodeTask{
|
||||
Status: transcodeFailedStatus,
|
||||
TranscodeError: fmt.Sprintf(
|
||||
"failure app=%s api=%s upload=%s encoded=%s",
|
||||
appID,
|
||||
apiKey,
|
||||
uploadKey,
|
||||
url.QueryEscape(uploadKey),
|
||||
),
|
||||
}, "https://app.example/source.m3u8", "file-id")
|
||||
|
||||
if !result.Failed {
|
||||
t.Fatalf("negative cloud status must be failed: %+v", result)
|
||||
}
|
||||
for _, secret := range []string{appID, apiKey, uploadKey, url.QueryEscape(uploadKey)} {
|
||||
if strings.Contains(result.ErrorMsg, secret) {
|
||||
t.Fatalf("transcode error leaked credential %q: %s", secret, result.ErrorMsg)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(result.ErrorMsg, "[REDACTED]") {
|
||||
t.Fatalf("transcode error did not show redaction marker: %s", result.ErrorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadNetworkErrorRedactsKey(t *testing.T) {
|
||||
preserveConfig(t)
|
||||
InitConfig(Config{
|
||||
AppID: "app-id-secret",
|
||||
APIKey: "1234567890abcdef",
|
||||
APIUrl: "https://api.example",
|
||||
})
|
||||
uploadKey := "upload key+secret"
|
||||
oldClient := uploadHTTPClient
|
||||
uploadHTTPClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
return nil, fmt.Errorf("dial failed for %s", req.URL.String())
|
||||
})}
|
||||
t.Cleanup(func() {
|
||||
uploadHTTPClient = oldClient
|
||||
})
|
||||
|
||||
_, err := uploadAPICall(context.Background(), "https://upload.example/query", map[string]string{
|
||||
"key": uploadKey,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected network error")
|
||||
}
|
||||
errText := err.Error()
|
||||
if strings.Contains(errText, uploadKey) || strings.Contains(errText, url.QueryEscape(uploadKey)) {
|
||||
t.Fatalf("network error leaked upload key: %s", errText)
|
||||
}
|
||||
if !strings.Contains(errText, "[REDACTED]") {
|
||||
t.Fatalf("network error did not show redaction marker: %s", errText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoviePostNon2xxIsDiagnosticAndRedacted(t *testing.T) {
|
||||
preserveConfig(t)
|
||||
const (
|
||||
appID = "app-id-secret"
|
||||
apiKey = "1234567890abcdef"
|
||||
)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
_, _ = fmt.Fprintf(w, "failure app=%s key=%s %s", appID, apiKey, strings.Repeat("x", 500))
|
||||
}))
|
||||
defer server.Close()
|
||||
InitConfig(Config{AppID: appID, APIKey: apiKey, APIUrl: server.URL})
|
||||
|
||||
err := moviePost(context.Background(), server.URL, map[string]interface{}{}, &struct{}{})
|
||||
if err == nil || !strings.Contains(err.Error(), "status:502") {
|
||||
t.Fatalf("expected HTTP status diagnostic, got %v", err)
|
||||
}
|
||||
if strings.Contains(err.Error(), appID) || strings.Contains(err.Error(), apiKey) {
|
||||
t.Fatalf("HTTP error leaked credentials: %s", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "[REDACTED]") || !strings.HasSuffix(err.Error(), "...") {
|
||||
t.Fatalf("HTTP error was not safely redacted/truncated: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryH264ToH265TaskUsesExplicitStableFileID(t *testing.T) {
|
||||
const fileID = "stable-file-id"
|
||||
oldClient := uploadHTTPClient
|
||||
uploadHTTPClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||
if got := req.URL.Query().Get("file_id"); got != fileID {
|
||||
t.Fatalf("query file_id = %q, want %q", got, fileID)
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(
|
||||
`{"status":"y","data":{"file_id":"stable-file-id","status":1}}`,
|
||||
)),
|
||||
Request: req,
|
||||
}, nil
|
||||
})}
|
||||
t.Cleanup(func() {
|
||||
uploadHTTPClient = oldClient
|
||||
})
|
||||
|
||||
result, found, err := QueryH264ToH265Task(
|
||||
context.Background(),
|
||||
fileID,
|
||||
"https://app.example/source.m3u8?hevc_exp=1&hevc_sig=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
SystemDomainsResp{UploadURL: "https://upload.example", UploadKey: "upload-key"},
|
||||
)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("explicit task query failed: found=%v err=%v", found, err)
|
||||
}
|
||||
if result.FileID != fileID {
|
||||
t.Fatalf("result file ID = %q, want %q", result.FileID, fileID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package laosiji
|
||||
|
||||
type GenerateUndressResponse struct {
|
||||
TaskID string `json:"task_id"` // 任务 ID
|
||||
}
|
||||
|
||||
type QueryUndressResponse struct {
|
||||
TaskID string `json:"task_id"` // 任务 ID
|
||||
Bid string `json:"bid"`
|
||||
Fee string `json:"fee"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"` // 状态 0待处理 -1处理失败 1处理中 2处理成功
|
||||
OutData string `json:"out_data"` // 输出数据 不同的ai 处理的数据不一样 查看文档的描述
|
||||
}
|
||||
Reference in New Issue
Block a user