MetaTube / engine /actor.go
henry99a's picture
Clean commit for Hugging Face Spaces without binary files
ca7217f
package engine
import (
goerr "errors"
"fmt"
"sort"
"sync"
"golang.org/x/text/language"
"gorm.io/gorm/clause"
"github.com/metatube-community/metatube-sdk-go/collection/sets"
"github.com/metatube-community/metatube-sdk-go/collection/slices"
"github.com/metatube-community/metatube-sdk-go/common/comparer"
"github.com/metatube-community/metatube-sdk-go/common/parser"
"github.com/metatube-community/metatube-sdk-go/engine/providerid"
"github.com/metatube-community/metatube-sdk-go/model"
mt "github.com/metatube-community/metatube-sdk-go/provider"
"github.com/metatube-community/metatube-sdk-go/provider/gfriends"
)
func (e *Engine) searchActorFromDB(keyword string, provider mt.Provider) (results []*model.ActorSearchResult, err error) {
var infos []*model.ActorInfo
if err = e.db.
Where("provider = ? AND name = ? COLLATE NOCASE",
provider.Name(), keyword).
Find(&infos).Error; err == nil {
for _, info := range infos {
if !info.IsValid() {
continue
}
results = append(results, info.ToSearchResult())
}
}
return
}
func (e *Engine) searchActor(keyword string, provider mt.Provider, fallback bool) ([]*model.ActorSearchResult, error) {
innerSearch := func(keyword string) (results []*model.ActorSearchResult, err error) {
if provider.Name() == gfriends.Name {
return provider.(mt.ActorSearcher).SearchActor(keyword)
}
if searcher, ok := provider.(mt.ActorSearcher); ok {
defer func() {
if err != nil || len(results) == 0 {
return // ignore error or empty.
}
const minSimilarity = 0.3
ps := new(slices.WeightedSlice[*model.ActorSearchResult, float64])
for _, result := range results {
if similarity := comparer.Compare(result.Name, keyword); similarity >= minSimilarity {
ps.Append(result, similarity)
}
}
results = ps.SortFunc(sort.Stable).Slice() // replace results.
}()
if fallback {
defer func() {
if innerResults, innerErr := e.searchActorFromDB(keyword, provider);
// ignore DB query error.
innerErr == nil && len(innerResults) > 0 {
// overwrite error.
err = nil
// update results.
asr := sets.NewOrderedSetWithHash(func(v *model.ActorSearchResult) string { return v.Provider + v.ID })
// unlike movie searching, we want search results go first
// than DB data here, so we add results later than DB results.
asr.Add(innerResults...)
asr.Add(results...)
results = asr.AsSlice()
}
}()
}
return searcher.SearchActor(keyword)
}
// All providers should implement the ActorSearcher interface.
return nil, mt.ErrInfoNotFound
}
names := parser.ParseActorNames(keyword)
if len(names) == 0 {
return nil, mt.ErrInvalidKeyword
}
var (
results []*model.ActorSearchResult
errors []error
)
for _, name := range names {
innerResults, innerErr := innerSearch(name)
if innerErr != nil &&
// ignore InfoNotFound error.
!goerr.Is(innerErr, mt.ErrInfoNotFound) {
// add error to chain and handle it later.
errors = append(errors, innerErr)
continue
}
results = append(results, innerResults...)
}
if len(results) == 0 {
if len(errors) > 0 {
return nil, fmt.Errorf("search errors: %v", errors)
}
return nil, mt.ErrInfoNotFound
}
return results, nil
}
func (e *Engine) SearchActor(keyword, name string, fallback bool) ([]*model.ActorSearchResult, error) {
provider, err := e.GetActorProviderByName(name)
if err != nil {
return nil, err
}
return e.searchActor(keyword, provider, fallback)
}
func (e *Engine) SearchActorAll(keyword string, fallback bool) (results []*model.ActorSearchResult, err error) {
var (
mu sync.Mutex
wg sync.WaitGroup
)
for _, provider := range e.actorProviders.Iterator() {
wg.Add(1)
go func(provider mt.ActorProvider) {
defer wg.Done()
if innerResults, innerErr := e.searchActor(keyword, provider, fallback); innerErr == nil {
for _, result := range innerResults {
if result.IsValid() /* validation check */ {
mu.Lock()
results = append(results, result)
mu.Unlock()
}
}
} // ignore error
}(provider)
}
wg.Wait()
sort.SliceStable(results, func(i, j int) bool {
return e.MustGetActorProviderByName(results[i].Provider).Priority() >
e.MustGetActorProviderByName(results[j].Provider).Priority()
})
return
}
func (e *Engine) getActorInfoFromDB(provider mt.ActorProvider, id string) (*model.ActorInfo, error) {
info := &model.ActorInfo{}
err := e.db. // Exact match here.
Where("provider = ?", provider.Name()).
Where("id = ? COLLATE NOCASE", id).
First(info).Error
return info, err
}
func (e *Engine) getActorInfoWithCallback(provider mt.ActorProvider, id string, lazy bool, callback func() (*model.ActorInfo, error)) (info *model.ActorInfo, err error) {
defer func() {
// metadata validation check.
if err == nil && (info == nil || !info.IsValid()) {
err = mt.ErrIncompleteMetadata
}
}()
if provider.Name() == gfriends.Name {
return provider.GetActorInfoByID(id)
}
defer func() {
// gfriends actor image injection for JAV actor providers.
if err == nil && info != nil && provider.Language() == language.Japanese {
if gInfo, gErr := e.MustGetActorProviderByName(gfriends.Name).GetActorInfoByID(info.Name); gErr == nil && len(gInfo.Images) > 0 {
info.Images = append(gInfo.Images, info.Images...)
}
}
}()
// Query DB first (by id).
if lazy {
if info, err = e.getActorInfoFromDB(provider, id); err == nil && info.IsValid() {
return
}
}
// Delayed info auto-save.
defer func() {
if err == nil && info.IsValid() {
// Make sure we save the original info here.
e.db.Clauses(clause.OnConflict{
UpdateAll: true,
}).Create(info) // ignore error
}
}()
return callback()
}
func (e *Engine) getActorInfoByProviderID(provider mt.ActorProvider, id string, lazy bool) (*model.ActorInfo, error) {
if id = provider.NormalizeActorID(id); id == "" {
return nil, mt.ErrInvalidID
}
return e.getActorInfoWithCallback(provider, id, lazy, func() (*model.ActorInfo, error) {
return provider.GetActorInfoByID(id)
})
}
func (e *Engine) GetActorInfoByProviderID(pid providerid.ProviderID, lazy bool) (*model.ActorInfo, error) {
provider, err := e.GetActorProviderByName(pid.Provider)
if err != nil {
return nil, err
}
return e.getActorInfoByProviderID(provider, pid.ID, lazy)
}
func (e *Engine) getActorInfoByProviderURL(provider mt.ActorProvider, rawURL string, lazy bool) (*model.ActorInfo, error) {
id, err := provider.ParseActorIDFromURL(rawURL)
switch {
case err != nil:
return nil, err
case id == "":
return nil, mt.ErrInvalidURL
}
return e.getActorInfoWithCallback(provider, id, lazy, func() (*model.ActorInfo, error) {
return provider.GetActorInfoByURL(rawURL)
})
}
func (e *Engine) GetActorInfoByURL(rawURL string, lazy bool) (*model.ActorInfo, error) {
provider, err := e.GetActorProviderByURL(rawURL)
if err != nil {
return nil, err
}
return e.getActorInfoByProviderURL(provider, rawURL, lazy)
}