Files
GoFilm/server/model/system/Movies.go
2026-04-05 20:00:40 +08:00

905 lines
31 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package system
import (
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"hash/fnv"
"log"
"regexp"
"server/config"
"server/plugin/common/util"
"server/plugin/db"
"strconv"
"strings"
"time"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
// Movie 影片基本信息
type Movie struct {
Id int64 `json:"id"` // 影片ID
Name string `json:"name"` // 影片名
Cid int64 `json:"cid"` // 所属分类ID
CName string `json:"CName"` // 所属分类名称
EnName string `json:"enName"` // 英文片名
Time string `json:"time"` // 更新时间
Remarks string `json:"remarks"` // 备注 | 清晰度
PlayFrom string `json:"playFrom"` // 播放来源
}
// MovieBasicInfo 影片基本信息
type MovieBasicInfo struct {
Id int64 `json:"id"` //影片Id
Cid int64 `json:"cid"` //分类ID
Pid int64 `json:"pid"` //一级分类ID
Name string `json:"name"` //片名
SubTitle string `json:"subTitle"` //子标题
CName string `json:"cName"` //分类名称
State string `json:"state"` //影片状态 正片|预告...
Picture string `json:"picture"` //简介图片
Actor string `json:"actor"` //主演
Director string `json:"director"` //导演
Blurb string `json:"blurb"` //简介, 不完整
Remarks string `json:"remarks"` // 更新情况
Area string `json:"area"` // 地区
Year string `json:"year"` //年份
}
// PlayItem 影视资源url信息
type PlayItem struct {
Episode string `json:"episode"` // 集数
Link string `json:"link"` // 播放地址
}
// MoviePlayList 播放列表信息, 二维切片
type MoviePlayList [][]PlayItem
// FromList 播放来源切片
type FromList []string
// MovieDetail 影片详情信息
type MovieDetail struct {
Id int64 `json:"id" gorm:"primaryKey"` //影片Id
Mid int64 `json:"mid"` //影片Id
Cid int64 `json:"cid"` //分类ID
Pid int64 `json:"pid"` //一级分类ID
Name string `json:"name"` //片名
Picture string `json:"picture"` //简介图片
SubTitle string `json:"subTitle"` //子标题
CName string `json:"cName"` //分类名称
EnName string `json:"enName"` //英文名
Initial string `json:"initial"` //首字母
ClassTag string `json:"classTag"` //分类标签
Actor string `json:"actor"` //主演
Director string `json:"director"` //导演
Writer string `json:"writer"` //作者
Blurb string `json:"blurb" gorm:"type:text"` //简介, 残缺,不建议使用
Remarks string `json:"remarks"` // 更新情况
ReleaseDate string `json:"releaseDate"` //上映时间
Area string `json:"area"` // 地区
Language string `json:"language"` //语言
Year string `json:"year"` //年份
State string `json:"state"` //影片状态 正片|预告...
UpdateTime string `json:"updateTime"` //更新时间
AddTime int64 `json:"addTime"` //资源添加时间戳
DbId int64 `json:"dbId"` //豆瓣id
DbScore string `json:"dbScore"` // 豆瓣评分
Hits int64 `json:"hits"` //影片热度
Content string `json:"content" gorm:"type:text"` //内容简介
PlayFrom FromList `json:"playFrom" gorm:"type:json"` // 播放来源
DownFrom string `json:"DownFrom"` //下载来源 例: http
//PlaySeparator string `json:"playSeparator"` // 播放信息分隔符
PlayList MoviePlayList `json:"playList" gorm:"type:json"` //播放地址url
DownloadList MoviePlayList `json:"downloadList" gorm:"type:json"` // 下载url地址
}
type SlaveMovieInfo struct {
Id int64 `json:"id" gorm:"primaryKey"` // 自增ID
Sid string `json:"sid"` // 采集站标识ID
//Name string `json:"name"` // 影片名称
Mid string `json:"mid"` // 归一匹配ID
DbId int64 `json:"dbId"` //豆瓣ID 可能为空
PlayList MoviePlayList `json:"playList" gorm:"type:json"`
}
// TableName 设置MovieDetail表的表名
func (m *MovieDetail) TableName() string {
return config.MovieDetailName
}
// TableName 设置slaveMovieInfo 表名
func (m *SlaveMovieInfo) TableName() string {
return config.SlaveMovieInfo
}
// ================================= 数据表处理 =================================
// CreateMovieDetailTable 创建存储检索信息的数据表
func CreateMovieDetailTable() {
// 如果不存在则创建表 并设置自增ID初始值为10000
if !ExistMovieDetailTable() {
err := db.Mdb.AutoMigrate(&MovieDetail{})
if err != nil {
log.Println("Create Table MovieDetailsTable Failed: ", err)
}
}
}
// ExistMovieDetailTable 检测是否存在 MovieDetails表
func ExistMovieDetailTable() bool {
return db.Mdb.Migrator().HasTable(&MovieDetail{})
}
// ResetMovieDetailTable 重置 MovieDetailTable
func ResetMovieDetailTable() {
var m MovieDetail
err := db.Mdb.Exec(fmt.Sprintf("TRUNCATE TABLE %s", m.TableName())).Error
if err != nil {
log.Println("MovieDetailTable Reset Error: ", err)
}
}
// CreateSlaveMovieInfoTable 创建存储检索信息的数据表
func CreateSlaveMovieInfoTable() {
// 如果不存在则创建表 并设置自增ID初始值为10000
if !ExistSlaveMovieInfoTable() {
err := db.Mdb.AutoMigrate(&SlaveMovieInfo{})
if err != nil {
log.Println("Create Table SlaveMovieInfoTable Failed: ", err)
}
}
}
// ExistSlaveMovieInfoTable 检测是否存在 MovieDetails表
func ExistSlaveMovieInfoTable() bool {
return db.Mdb.Migrator().HasTable(&SlaveMovieInfo{})
}
// ResetSlaveMovieInfoTable 重置 SlaveMovieInfoTable (附属站点数据表一般不会单独重置)
func ResetSlaveMovieInfoTable() {
var s SlaveMovieInfo
err := db.Mdb.Exec(fmt.Sprintf("TRUNCATE TABLE %s", s.TableName())).Error
if err != nil {
log.Println("SlaveMovieInfoTable Reset Error: ", err)
}
}
// DelSlaveMovieInfos 删除表中对应站点的数据信息
func DelSlaveMovieInfos(id string) {
// 一次删除过多数据会锁表, 因此直接截断表
//if err := db.Mdb.Where("sid = ?", id).Delete(&SlaveMovieInfo{}).Error; err != nil {
// log.Println("Delete SlaveMovieInfos Error: ", err)
//}
}
// AddMovieDetailIndex 添加详情表索引
func AddMovieDetailIndex() {
var m MovieDetail
tableName := m.TableName()
// 添加索引
db.Mdb.Exec(fmt.Sprintf("CREATE UNIQUE INDEX idx_mid ON %s (mid)", tableName))
}
// AddSlaveMovieInfoIndex 添加附属站点信息表索引
func AddSlaveMovieInfoIndex() {
var s SlaveMovieInfo
tableName := s.TableName()
// 如果不存在索引则创建对应索引
if !db.Mdb.Migrator().HasIndex(&s, "idx_mid") {
// 添加索引
db.Mdb.Exec(fmt.Sprintf("CREATE INDEX idx_sid ON %s (sid DESC)", tableName))
db.Mdb.Exec(fmt.Sprintf("CREATE INDEX idx_mid ON %s (mid DESC)", tableName))
db.Mdb.Exec(fmt.Sprintf("CREATE INDEX idx_dbId ON %s (db_id DESC", tableName))
}
}
// =================================== column序列化 接口========================================================
func (m *MoviePlayList) Scan(value interface{}) error {
if value == nil {
*m = nil
return nil
}
b, ok := value.([]byte)
if !ok {
return errors.New("MoviePlayList serialization failed, value is not []byte")
}
return json.Unmarshal(b, m)
}
func (m MoviePlayList) Value() (driver.Value, error) {
if m == nil {
return nil, nil
}
return json.Marshal(m)
}
func (fl *FromList) Scan(value interface{}) error {
if value == nil {
*fl = nil
return nil
}
b, ok := value.([]byte)
if !ok {
return errors.New("FromList serialization failed, value is not []byte")
}
return json.Unmarshal(b, fl)
}
func (fl FromList) Value() (driver.Value, error) {
if fl == nil {
return nil, nil
}
return json.Marshal(fl)
}
// =================================== Spider数据处理 ========================================================
// SaveDetails 保存影片详情信息到redis中 格式: MovieDetail:Cid?:Id?
func SaveDetails(ml []MovieDetail) (err error) {
// 1. 先将详情信息存入 MovieDetail表中
if err = db.Mdb.Create(&ml).Error; err != nil {
log.Println("影片详情信息保存失败: ", err)
}
// 2. 将详情信息转化为SearchInfo并保存
BatchSaveSearchInfo(ml)
return err
}
// SaveDetail 保存单部影片信息
func SaveDetail(m MovieDetail) (err error) {
// 1. 转换 detail信息 searchInfo
searchInfo := ConvertSearchInfo(m)
// 2. 保存 Search tag 到 redis中 只存储用于检索对应影片的关键字信息
SaveSearchTag(searchInfo)
// 3. 将影片详情信息保存到 MovieDetails表中
// 4. 先查询数据库中是否存在对应记录 ,如果不存在对应记录则 保存当前记录
tx := db.Mdb.Begin()
if !ExistMovieDetailByMid(m.Mid) {
// 执行插入操作
if err := tx.Create(&m).Error; err != nil {
tx.Rollback()
return err
}
} else {
// 只对会变化的字段进行更新
err := tx.Model(&MovieDetail{}).Where("mid", m.Mid).Updates(MovieDetail{PlayList: m.PlayList, DownloadList: m.DownloadList,
Remarks: m.Remarks, State: m.State, UpdateTime: m.UpdateTime, AddTime: m.AddTime, DbScore: m.DbScore, Hits: m.Hits}).Error
if err != nil {
tx.Rollback()
return err
}
}
// 提交事务
tx.Commit()
// 保存影片检索信息到searchTable
err = SaveSearchInfo(searchInfo)
return err
}
// BatchUpdateDetails 保存或更新detail数据
func BatchUpdateDetails(ml []MovieDetail) (err error) {
// 先将details批量保存或更新
for _, m := range ml {
if !ExistMovieDetailByMid(m.Mid) {
// 执行插入操作
if err := db.Mdb.Create(&m).Error; err != nil {
return err
}
} else {
// 只对会变化的字段进行更新
err := db.Mdb.Model(&MovieDetail{}).Where("mid", m.Mid).Updates(MovieDetail{PlayList: m.PlayList, DownloadList: m.DownloadList,
Remarks: m.Remarks, State: m.State, UpdateTime: m.UpdateTime, AddTime: m.AddTime, DbScore: m.DbScore, Hits: m.Hits}).Error
if err != nil {
return err
}
}
// 转化处理searchInfo信息s
s := ConvertSearchInfo(m)
// 保存searchInfo信息
if err := SaveSearchInfo(s); err != nil {
return err
}
}
return err
}
// ExistMovieDetailByMid 通过mid判断是否存在对应信息
func ExistMovieDetailByMid(mid int64) bool {
var count int64
db.Mdb.Model(&MovieDetail{}).Where("mid", mid).Count(&count)
return count > 0
}
// SaveSitePlayList 保存附属站点影片信息
func SaveSitePlayList(sl []SlaveMovieInfo) (err error) {
if len(sl) <= 0 {
return nil
}
if err = db.Mdb.Create(&sl).Error; err != nil {
log.Println("附属站点影片信息保存失败: ", err)
}
return err
}
// UpdateSitePlayList 仅保存播放url列表信息到当前站点
func UpdateSitePlayList(id string, ml []MovieDetail) (err error) {
// 如果ml 为空则直接返回
if len(ml) <= 0 {
return nil
}
var sl []SlaveMovieInfo
for _, m := range ml {
s := SlaveMovieInfo{Sid: id, Mid: GenerateHashKey(m.Name), DbId: m.DbId, PlayList: m.PlayList}
// 查询表中是否已经存在对应的数据记录, 如果有则更新, 无则追加到切片中统一处理, id =-1 表示不存在对应数据
if id := ExistSlaveMovieInfo(s); id > 0 {
if err = db.Mdb.Model(&s).Where("id", id).Updates(s).Error; err != nil {
log.Println("附属站点影片信息更新失败: ", err)
}
continue
}
sl = append(sl, s)
}
// 将处理后的结果存储到 SalveMovieInfo表中
if len(sl) > 0 {
if err = db.Mdb.Create(&sl).Error; err != nil {
log.Println("附属站点影片信息保存失败: ", err)
}
}
return
}
// BatchUpdateSlaveInfo 批量更新SlaveMovieInfo
func BatchUpdateSlaveInfo(sl []SlaveMovieInfo) (err error) {
// 如果ml 为空则直接返回
if len(sl) <= 0 {
return nil
}
//
var rl []SlaveMovieInfo
for _, s := range sl {
if id := ExistSlaveMovieInfo(s); id > 0 {
if err = db.Mdb.Model(&s).Where("id", id).Updates(s).Error; err != nil {
log.Println("附属站点影片信息更新失败: ", err)
}
continue
}
rl = append(rl, s)
}
if len(sl) > 0 {
if err = db.Mdb.Create(&sl).Error; err != nil {
log.Println("附属站点影片信息保存失败: ", err)
}
}
return err
}
// DelSlaveInfoBySid 删除sid对应的采集站的所有数据
func DelSlaveInfoBySid(id string) {
// 查询表中是否存在对应采集站的数据信息
var count int64
db.Mdb.Model(&SlaveMovieInfo{}).Count(&count).Where("sid = ?", id)
// 如果存在对应数据,则进行后续操作
if count > 0 {
for {
res := db.Mdb.Where("sid = ?", id).Limit(5000).Delete(&SlaveMovieInfo{})
if res.Error != nil {
log.Println("Delete SlaveMovieInfo Failed: ", res.Error)
break
}
if res.RowsAffected == 0 {
log.Println("Delete SlaveMovieInfo Over !!!")
break
}
// 短暂休眠, 防止mysql紊乱
time.Sleep(100 * time.Millisecond)
}
}
}
// ExistSlaveMovieInfo 查询对应记录, 如果存在则返还id, 不存在则返还 -1
func ExistSlaveMovieInfo(s SlaveMovieInfo) int64 {
var id int64
if err := db.Mdb.Model(&SlaveMovieInfo{}).Select("id").Where("sid = ? AND (mid = ? OR db_id = ?)", s.Sid, s.Mid, s.DbId).First(&id).Error; err != nil {
// 如果错误类型为gorm.ErrRecordNotFound, 直接返回 0
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0
}
// 如果是其他异常则输出异常信息并返回 -1
log.Println("Find SlaveMovieInfo Failed: ", err)
return -1
}
return id
}
// =================================== Spider数据处理--Redis转存 ========================================================
// MovieDetailCache 主站点数据采集先缓存到redis
func MovieDetailCache(ml []MovieDetail) error {
// 以mid为key将数据存储到redis的hash中
var data = make(map[string]string)
for _, m := range ml {
r, _ := json.Marshal(m)
data[strconv.FormatInt(m.Mid, 10)] = string(r)
}
return db.Rdb.HSet(db.Cxt, config.MovieDetailKey, data).Err()
}
// SlaveDetailCache 附属站点影片信息缓存
func SlaveDetailCache(id string, ml []MovieDetail) error {
// 以mid为key将数据存储到redis的hash中
var data = make(map[string]string)
for _, m := range ml {
// 只执行保存操作, 不考虑更新情况
s := SlaveMovieInfo{Sid: id, Mid: GenerateHashKey(m.Name), DbId: m.DbId, PlayList: m.PlayList}
r, _ := json.Marshal(s)
// redis中的存储key优先db_id
if s.DbId > 0 {
data[fmt.Sprintf("%d", s.DbId)] = string(r)
} else {
data[s.Mid] = string(r)
}
}
// 使用 Sid:Mid为key, 用以区分不同站点数据
return db.Rdb.HSet(db.Cxt, fmt.Sprintf(config.MultipleSiteDetailKey, id), data).Err()
}
// GetSlaveDetailInCache 从redis缓存中获取播放信息
func GetSlaveDetailInCache(sid, mid string) SlaveMovieInfo {
// 初始化返回值
var s SlaveMovieInfo
v, err := db.Rdb.HGet(db.Cxt, fmt.Sprintf(config.MultipleSiteDetailKey, sid), mid).Result()
if err != nil {
// 如果没有获取到对应值, 则直接continue
//log.Println("Get MultipleSiteDetail Failed: ", err)
return s
}
// 如果获取到数据则直接退出本次循环
_ = json.Unmarshal([]byte(v), &s)
return s
}
// SyncMovieDetail 同步redis中的影片数据到mysql中
func SyncMovieDetail(sid string, grade SourceGrade, mode int) {
// 初始化游标
var cursor uint64 = 0
// 根据采集站的类型 Master | Slave 进行不同的处理逻辑
switch grade {
case MasterCollect:
// 循环扫描detail信息, 存储完成后进行删除
for {
vs, nextCursor, err := db.Rdb.HScan(db.Cxt, config.MovieDetailKey, cursor, "", config.FilmScanSize).Result()
if err != nil {
log.Println("ScanMovieDetail Failed: ", err)
}
if len(vs) > 0 {
var ks []string
var ml []MovieDetail
for i := 0; i < len(vs); i += 2 {
ks = append(ks, vs[i])
var m MovieDetail
_ = json.Unmarshal([]byte(vs[i+1]), &m)
ml = append(ml, m)
}
// 批量保存movieDetail
switch mode {
case 0:
// 执行全量保存
if err := SaveDetails(ml); err != nil {
log.Println("SyncMovieDetail AllSave Failed: ", err)
}
case 1:
// 执行更新
if err := BatchUpdateDetails(ml); err != nil {
log.Println("SyncMovieDetail SaveOrUpdate Failed: ", err)
}
default:
log.Println("Synchronization Mode Exception:", mode)
}
// 删除已提取的元素
if err := db.Rdb.HDel(db.Cxt, config.MovieDetailKey, ks...).Err(); err != nil {
log.Println("DeleteMovieDetailCache Failed: ", err)
}
}
// 更新游标
cursor = nextCursor
// 如果游标归零则结束循环同步
if cursor <= 0 {
break
}
}
case SlaveCollect:
// 循环扫描detail信息, 存储完成后进行删除
for {
vs, nextCursor, err := db.Rdb.HScan(db.Cxt, fmt.Sprintf(config.MultipleSiteDetailKey, sid), cursor, "", config.FilmScanSize).Result()
if err != nil {
log.Println("ScanSlaveDetail Failed: ", err)
}
if len(vs) > 0 {
var ks []string
var sl []SlaveMovieInfo
for i := 0; i < len(vs); i += 2 {
ks = append(ks, vs[i])
var s SlaveMovieInfo
_ = json.Unmarshal([]byte(vs[i+1]), &s)
sl = append(sl, s)
}
// 批量保存movieDetail
switch mode {
case 0:
// 执行全量保存
if err := SaveSitePlayList(sl); err != nil {
log.Println("SyncSlaveDetail AllSave Failed: ", err)
}
case 1:
// 执行更新
if err := BatchUpdateSlaveInfo(sl); err != nil {
log.Println("SyncSlaveDetail SaveOrUpdate Failed: ", err)
}
default:
log.Println("Synchronization Mode Exception:", mode)
}
// 删除已提取的元素
if err := db.Rdb.HDel(db.Cxt, fmt.Sprintf(config.MultipleSiteDetailKey, sid), ks...).Err(); err != nil {
log.Println("DeleteSlaveDetailCache Failed: ", err)
}
}
// 更新游标
cursor = nextCursor
// 如果游标归零则结束循环同步
if cursor == 0 {
break
}
}
}
}
// ============================ APi接口 ==================================================
// GetDetailByMid 获取影片对应的详情信息
func GetDetailByMid(mid int64) MovieDetail {
// 初始化返回值
var m MovieDetail
// 从redis获取对应的影片信息
v, err := db.Rdb.HGet(db.Cxt, config.MovieDetailKey, strconv.FormatInt(mid, 10)).Result()
if err != nil {
// 如果没有获取到对应值, 则去mysql中进行查找
if errors.Is(err, redis.Nil) {
if err := db.Mdb.Where("mid = ?", mid).Find(&m).Error; err != nil {
log.Println("Find BasicInfo Failed: ", err)
return m
}
//// 执行本地图片匹配
ReplaceDetailPic(&m)
return m
}
log.Println("Find MovieDetail Failed: ", err)
return m
}
// 如果获取到对应值,则进行反序列化
_ = json.Unmarshal([]byte(v), &m)
return m
//var d MovieDetail
//// 查询mid对应的影片详情信息, 只查询部分字段
//if err := db.Mdb.Model(&MovieDetail{}).Where("mid = ?", mid).First(&d).Error; err != nil {
// log.Println("Find MovieDetail Failed: ", err)
// return d
//}
//// 执行本地图片匹配
//ReplaceDetailPic(&d)
//return d
}
// GetBasicInfoByMid 获取Id对应的影片基本信息
func GetBasicInfoByMid(mid int64) MovieBasicInfo {
// 通过id查询满足条件的影片基本信息
var basic MovieBasicInfo
var d MovieDetail
// 查询mid对应的影片详情信息, 只查询部分字段
if err := db.Mdb.Model(&MovieDetail{}).Select("id, mid, cid, pid, name, sub_title, c_name, state, picture, actor, director,"+
" content, remarks, area, year").Where("mid = ?", mid).First(&d).Error; err != nil {
log.Println("Find MovieDetail Failed: ", err)
return basic
}
// 匹配本地图片
ReplaceDetailPic(&d)
// 将 MovieDetail转化为 BasicInfo
basic = ConvertBasicInfo(d)
return basic
}
// GetBasicInfoByIds 通过searchInfo 获取影片的基本信息
func GetBasicInfoByIds(ids []int64) []MovieBasicInfo {
// 初始化返回值
var l []MovieBasicInfo
// 首先从redis中获取影片的最新信息, 如果没有则转为去mysql表中获取
var ks []string
for _, id := range ids {
ks = append(ks, strconv.FormatInt(id, 10))
}
// 一次性获取所有
vs, err := db.Rdb.HMGet(db.Cxt, config.MovieDetailKey, ks...).Result()
if err != nil {
log.Println("Find MovieDetail Failed: ", err)
return l
}
// 迭代转换 basicInfo, 并将未获取到值的id进行整合
var newIds []int64
var ml []MovieDetail
if len(vs) > 0 {
for i, v := range vs {
if v != nil {
var m MovieDetail
_ = json.Unmarshal([]byte(v.(string)), &m)
ReplaceDetailPic(&m)
l = append(l, ConvertBasicInfo(m))
} else {
newIds = append(newIds, ids[i])
}
}
}
// 如果存在nil值,则去mysql进行补全
if len(newIds) > 0 {
if err := db.Mdb.Model(&MovieDetail{}).Select("id, mid, cid, pid, name, sub_title, c_name, state, picture, actor, director,"+
" content, remarks, area, year").Where("mid IN (?)", newIds).Find(&ml).Error; err != nil {
log.Println("BatchFind BasicInfo Failed: ", err)
return nil
}
for _, m := range ml {
// 执行本地图片匹配
ReplaceDetailPic(&m)
l = append(l, ConvertBasicInfo(m))
}
}
//var ml []MovieDetail
//var l []MovieBasicInfo
// 使用in查询, 一次性拿到满足条件的数据
//if err := db.Mdb.Model(&MovieDetail{}).Select("id, mid, cid, pid, name, sub_title, c_name, state, picture, actor, director,"+
// " content, remarks, area, year").Where("mid IN (?)", ids).Find(&ml).Error; err != nil {
// log.Println("BatchFind BasicInfo Failed: ", err)
// return nil
//}
//// 将查询到的结果批量转化为BasicInfo
//for _, m := range ml {
// // 执行本地图片匹配
// ReplaceDetailPic(&m)
// l = append(l, ConvertBasicInfo(m))
//}
return l
}
// GetMovieListByPid 通过Pid 分类ID 获取对应影片的数据信息
func GetMovieListByPid(pid int64, page *Page) []MovieBasicInfo {
// 返回分页参数
var count int64
db.Mdb.Model(&SearchInfo{}).Where("pid", pid).Count(&count)
page.Total = int(count)
page.PageCount = int((page.Total + page.PageSize - 1) / page.PageSize)
// 通过Search表查询
var ids []int64
if err := db.Mdb.Model(&SearchInfo{}).Limit(page.PageSize).Offset((page.Current-1)*page.PageSize).Select("mid").Where("pid", pid).Order("update_stamp DESC").Find(&ids).Error; err != nil {
log.Println(err)
return nil
}
// 通过ids查询影片基本信息并返回
return GetBasicInfoByIds(ids)
}
// GetMovieListByCid 通过Cid查找对应的影片分页数据, 不适合GetMovieListByPid 糅合
func GetMovieListByCid(cid int64, page *Page) []MovieBasicInfo {
// 返回分页参数
var count int64
db.Mdb.Model(&SearchInfo{}).Where("cid", cid).Count(&count)
page.Total = int(count)
page.PageCount = int((page.Total + page.PageSize - 1) / page.PageSize)
// 进行具体的信息查询
var ids []int64
if err := db.Mdb.Limit(page.PageSize).Offset((page.Current-1)*page.PageSize).Select("mid").Where("cid", cid).Order("update_stamp DESC").Find(&ids).Error; err != nil {
log.Println(err)
return nil
}
// 通过影片ID去redis中获取id对应数据信息
return GetBasicInfoByIds(ids)
}
// GetRelateMovieBasicInfo GetRelateMovie 根据 name, cid, pid, classTag 获取相关影片
func GetRelateMovieBasicInfo(search SearchInfo, page *Page) []MovieBasicInfo {
/*
根据当前影片信息匹配相关的影片
1. 分类Cid,
2. 如果影片名称含有第x季 则根据影片名进行模糊匹配
3. class_tag 剧情内容匹配, 切分后使用 or 进行匹配
4. area 地区
5. 语言 Language
*/
// sql 拼接查询条件
sql := ""
// 优先进行名称相似匹配, 先对影片名称进行精简, 只保留主体用于匹配同系列影片
name := util.CleanFilmName(search.Name)
sql = fmt.Sprintf(`select mid from %s where (name LIKE "%%%s%%" or sub_title LIKE "%%%[2]s%%") AND cid=%d AND search.deleted_at IS NULL union`, search.TableName(), name, search.Cid)
// 添加其他相似匹配规则 同属二级分类
sql = fmt.Sprintf(`%s (select mid from %s where cid=%d AND `, sql, search.TableName(), search.Cid)
// 根据剧情标签查找相似影片, classTag 使用的分隔符为 , | /首先去除 classTag 中包含的所有空格
search.ClassTag = strings.ReplaceAll(search.ClassTag, " ", "")
// 如果 classTag 中包含分割符则进行拆分匹配
cl := strings.Split(util.FormatSpecialChar(search.ClassTag), ",")
if len(cl) > 0 {
s := "("
for _, c := range cl {
s = fmt.Sprintf(`%s class_tag like "%%%s%%" OR`, s, c)
}
sql = fmt.Sprintf("%s %s)", sql, strings.TrimSuffix(s, "OR"))
} else {
sql = fmt.Sprintf(`%s class_tag like "%%%s%%"`, sql, search.ClassTag)
}
// 除名称外的相似影片使用随机排序
//sql = fmt.Sprintf("%s ORDER BY RAND() limit %d,%d)", sql, page.Current, page.PageSize)
sql = fmt.Sprintf("%s AND search.deleted_at IS NULL limit %d,%d)", sql, page.Current, page.PageSize)
// 条件拼接完成后加上limit参数
sql = fmt.Sprintf("(%s) limit %d,%d", sql, page.Current, page.PageSize)
// 执行sql, 获取满足条件的影片mid切片
var ids []int64
db.Mdb.Raw(sql).Scan(&ids)
// 通过 ids 获取影片基本信息,并返回
return GetBasicInfoByIds(ids)
}
// GetMultiplePlay 通过影片名的ID值匹配播放源, 不区分站点
func GetMultiplePlay(mIds []string, dbId int64) []SlaveMovieInfo {
// 初始化返回值
var l []SlaveMovieInfo
// 首先从redis进行匹配
for _, c := range GetCollectSourceListByGrade(SlaveCollect) {
if !c.State {
continue
}
var s SlaveMovieInfo
// 优先使用dbID为key去redis中获取
if s = GetSlaveDetailInCache(c.Id, fmt.Sprintf("%d", dbId)); s.Mid != "" {
l = append(l, s)
continue
}
for _, mid := range mIds {
// 初始化临时变量 SlaveMovieInfo
if s = GetSlaveDetailInCache(c.Id, mid); s.Mid != "" {
l = append(l, s)
break
}
//v, err := db.Rdb.HGet(db.Cxt, fmt.Sprintf(config.MultipleSiteDetailKey, c.Id), mid).Result()
//if err != nil {
// // 如果没有获取到对应值, 则直接continue
// continue
//}
//// 如果获取到数据则直接退出本次循环
//_ = json.Unmarshal([]byte(v), &s)
//l = append(l, s)
//break
}
// 如果迭代完s依旧为空,则去mysql中进行匹配
if s.Mid == "" {
if err := db.Mdb.Model(&SlaveMovieInfo{}).Select("sid, play_list").Where("sid = ? AND (mid IN (?) OR db_id = ?)", c.Id, mIds, dbId).First(&s).Error; err != nil {
log.Println("GetMultiplePlay Failed: ", err)
continue
}
l = append(l, s)
}
}
// 通过siteId, mIds, dbIds 检索满足条件的数据
//if err := db.Mdb.Model(&SlaveMovieInfo{}).Select("sid, play_list").Where("mid IN (?) OR db_id = ?", mIds, dbId).Find(&l).Error; err != nil {
// log.Println("GetMultiplePlay Failed: ", err)
// return nil
//}
return l
}
// ============================ 数据处理 ==================================================
// ConvertSearchInfo 将detail信息处理成 searchInfo
func ConvertSearchInfo(m MovieDetail) SearchInfo {
score, _ := strconv.ParseFloat(m.DbScore, 64)
stamp, _ := time.ParseInLocation(time.DateTime, m.UpdateTime, time.Local)
// detail中的年份信息并不准确, 因此采用 ReleaseDate中的年份
year, err := strconv.ParseInt(regexp.MustCompile(`[1-9][0-9]{3}`).FindString(m.ReleaseDate), 10, 64)
if err != nil {
year = 0
}
return SearchInfo{
Mid: m.Mid,
Cid: m.Cid,
Pid: m.Pid,
Name: m.Name,
SubTitle: m.SubTitle,
CName: m.CName,
ClassTag: m.ClassTag,
Area: m.Area,
Language: m.Language,
Year: year,
Initial: m.Initial,
Score: score,
Hits: m.Hits,
UpdateStamp: stamp.Unix(),
State: m.State,
Remarks: m.Remarks,
// ReleaseDate 部分影片缺失该参数, 所以使用添加时间作为上映时间排序
ReleaseStamp: m.AddTime,
}
}
// ConvertBasicInfo 将Detail信息转化为basic信息
func ConvertBasicInfo(m MovieDetail) MovieBasicInfo {
return MovieBasicInfo{Id: m.Mid, Cid: m.Cid, Pid: m.Pid, Name: m.Name, SubTitle: m.SubTitle,
CName: m.CName, State: m.State, Picture: m.Picture, Actor: m.Actor, Director: m.Director, Blurb: m.Content,
Remarks: m.Remarks, Area: m.Area, Year: m.Year}
}
/*
对附属播放源入库时的name|dbID进行处理,保证唯一性
1. 去除name中的所有空格
2. 去除name中含有的别名.*
3. 去除name首尾的标点符号
4. 将处理完成后的name转化为hash值作为存储时的key
*/
// GenerateHashKey 存储播放源信息时对影片名称进行处理-生成id, 提高各站点间同一影片的匹配度
func GenerateHashKey[K string | ~int | int64](key K) string {
mName := fmt.Sprint(key)
//1. 去除name中的所有空格
mName = regexp.MustCompile(`\s`).ReplaceAllString(mName, "")
//2. 添加常用的名称标准化替换规则
rules := []string{
// 中文季数标签统一
"season", "s", "第", "s", "季", "", "期", "", "画", "",
// --- 3. 剧场版标准化 ---
"剧场版", "ovo", "映画", "ovo", "电影版", "ovo", "The Movie", "ovo", "Movie", "ovo", "(Movie)", "ovo", "〔映画〕", "ovo",
// 特殊数学符号 (用户常用来代替数字,如 ∬ 代表 2)
"", "1", "Ⅱ", "2", "Ⅲ", "3",
"∫", "1", "∬", "2", "∮", "3", "Ⅳ", "4", "", "5", "Ⅵ", "6", "Ⅶ", "7", "Ⅷ", "8", "Ⅸ", "9", "", "10", // 用户可能用积分号代表季数
"一", "1", "二", "2", "三", "3", "四", "4", "五", "5", "六", "6", "七", "7", "八", "8", "九", "9",
// 移除或替换无意义的装饰符号,这些符号在搜索中通常不仅无用还会阻碍匹配
"★", "", "☆", "", "◆", "", "◇", "", "●", "", "○", "",
"【", "", "】", "", "〖", "", "〗", "", "", "", "", "",
"「", "", "」", "", "『", "", "』", "",
"|", "", "", "", // 竖线分隔符
"~", "", "", "", // 波浪号
"...", "", "……", "", // 省略号
"!", "", "", "", "?", "", "", "",
"(", "", ")", "", "", "", "", "",
"[", "", "]", "", "", "", "", "",
"{", "", "}", "", "", "", "", "",
"", "&", "", "+",
"-", "", "", "", "—", "", "", "", // 策略:通常移除所有标点,让 "A-B" 变成 "AB"
"_", "", "_", "",
".", "", "", "", "。", "",
",", "", "", "",
":", "", ":", "", ":", "",
";", "", "", "",
"'", "", "", "", "\"", "", "“", "", "”", "",
"`", "", "", "",
}
mName = strings.NewReplacer(rules...).Replace(mName)
//3. 去除name首尾的标点符号
mName = regexp.MustCompile(`^[[:punct:]]+|[[:punct:]]+$`).ReplaceAllString(mName, "")
//4. 将处理完成后的name转化为hash值作为存储时的key
h := fnv.New32a()
_, err := h.Write([]byte(mName))
if err != nil {
return ""
}
return fmt.Sprint(h.Sum32())
}