Files
seanime-docker/seanime-2.9.10/internal/onlinestream/repository_actions.go
2025-09-20 14:08:38 +01:00

358 lines
12 KiB
Go

package onlinestream
import (
"errors"
"fmt"
"seanime/internal/api/anilist"
"seanime/internal/extension"
hibikeonlinestream "seanime/internal/extension/hibike/onlinestream"
onlinestream_providers "seanime/internal/onlinestream/providers"
"seanime/internal/util/comparison"
"strings"
)
var (
ErrNoAnimeFound = errors.New("anime not found, try manual matching")
ErrNoEpisodes = errors.New("no episodes found")
errNoEpisodeSourceFound = errors.New("no source found for episode")
)
type (
// episodeContainer contains results of fetching the episodes from the provider.
episodeContainer struct {
Provider string
// List of episode details from the provider.
// It is used to get the episode servers.
ProviderEpisodeList []*hibikeonlinestream.EpisodeDetails
// List of episodes with their servers.
Episodes []*episodeData
}
// episodeData contains some details about a provider episode and all available servers.
episodeData struct {
Provider string
ID string
Number int
Title string
Servers []*hibikeonlinestream.EpisodeServer
}
)
// getEpisodeContainer gets the episode details and servers from the specified provider.
// It takes the media ID, titles in order to fetch the episode details.
// - This function can be used to only get the episode details by setting 'from' and 'to' to 0.
//
// Since the episode details are cached, we can request episode servers multiple times without fetching the episode details again.
func (r *Repository) getEpisodeContainer(provider string, media *anilist.BaseAnime, from int, to int, dubbed bool, year int) (*episodeContainer, error) {
r.logger.Debug().
Str("provider", provider).
Int("mediaId", media.ID).
Int("from", from).
Int("to", to).
Bool("dubbed", dubbed).
Msg("onlinestream: Getting episode container")
// Key identifying the provider episode list in the file cache.
// It includes "dubbed" because Gogoanime has a different entry for dubbed anime.
// e.g. 1$provider$true
providerEpisodeListKey := fmt.Sprintf("%d$%s$%v", media.ID, provider, dubbed)
// Create the episode container
ec := &episodeContainer{
Provider: provider,
Episodes: make([]*episodeData, 0),
ProviderEpisodeList: make([]*hibikeonlinestream.EpisodeDetails, 0),
}
// Get the episode details from the provider.
r.logger.Debug().
Str("key", providerEpisodeListKey).
Msgf("onlinestream: Fetching %s episode list", provider)
// Buckets for caching the episode list and episode data.
fcEpisodeListBucket := r.getFcEpisodeListBucket(provider, media.ID)
fcEpisodeDataBucket := r.getFcEpisodeDataBucket(provider, media.ID)
// Check if the episode list is cached to avoid fetching it again.
var providerEpisodeList []*hibikeonlinestream.EpisodeDetails
if found, _ := r.fileCacher.Get(fcEpisodeListBucket, providerEpisodeListKey, &providerEpisodeList); !found {
var err error
providerEpisodeList, err = r.getProviderEpisodeList(provider, media, dubbed, year)
if err != nil {
r.logger.Error().Err(err).Msg("onlinestream: Failed to get provider episodes")
return nil, err // ErrNoAnimeFound or ErrNoEpisodes
}
_ = r.fileCacher.Set(fcEpisodeListBucket, providerEpisodeListKey, providerEpisodeList)
} else {
r.logger.Debug().
Str("key", providerEpisodeListKey).
Msg("onlinestream: Cache HIT for episode list")
}
ec.ProviderEpisodeList = providerEpisodeList
var lastServerError error
for _, episodeDetails := range providerEpisodeList {
if episodeDetails.Number >= from && episodeDetails.Number <= to {
// Check if the episode is cached to avoid fetching the sources again.
key := fmt.Sprintf("%d$%s$%d$%v", media.ID, provider, episodeDetails.Number, dubbed)
r.logger.Debug().
Str("key", key).
Msgf("onlinestream: Fetching episode '%d' servers", episodeDetails.Number)
// Check episode cache
var cached *episodeData
if found, _ := r.fileCacher.Get(fcEpisodeDataBucket, key, &cached); found {
ec.Episodes = append(ec.Episodes, cached)
r.logger.Debug().
Str("key", key).
Msgf("onlinestream: Cache HIT for episode '%d' servers", episodeDetails.Number)
continue
}
// Zoro dubs
if provider == onlinestream_providers.ZoroProvider && dubbed {
// If the episode details have both sub and dub, we need to get the dub episode.
if !strings.HasSuffix(episodeDetails.ID, string(hibikeonlinestream.SubAndDub)) {
// Skip sub-only episodes
continue
}
// Replace "both" with "dub" so that [getProviderEpisodeServers] can find the dub episode.
episodeDetails.ID = strings.Replace(episodeDetails.ID, string(hibikeonlinestream.SubAndDub), string(hibikeonlinestream.Dub), 1)
}
// Fetch episode servers
servers, err := r.getProviderEpisodeServers(provider, episodeDetails)
if err != nil {
lastServerError = err
r.logger.Error().Err(err).Msgf("onlinestream: failed to get episode '%d' servers", episodeDetails.Number)
continue
}
episode := &episodeData{
ID: episodeDetails.ID,
Number: episodeDetails.Number,
Title: episodeDetails.Title,
Servers: servers,
}
ec.Episodes = append(ec.Episodes, episode)
r.logger.Debug().
Str("key", key).
Msgf("onlinestream: Found %d servers for episode '%d'", len(servers), episodeDetails.Number)
_ = r.fileCacher.Set(fcEpisodeDataBucket, key, episode)
}
}
if from > 0 && to > 0 && len(ec.Episodes) == 0 {
r.logger.Error().Err(lastServerError).Msg("onlinestream: No episode servers found")
return nil, fmt.Errorf("no episode servers found, provider returned: '%w'", lastServerError)
}
if len(ec.ProviderEpisodeList) == 0 {
r.logger.Error().Msg("onlinestream: No episodes found for this anime")
return nil, fmt.Errorf("no episodes found for this anime")
}
return ec, nil
}
// getProviderEpisodeServers gets all the available servers for the episode.
// It returns errNoEpisodeSourceFound if no sources are found.
//
// Example:
//
// episodeDetails, _ := getProviderEpisodeListFromTitles(provider, titles, dubbed)
// episodeServers, err := getProviderEpisodeServers(provider, episodeDetails[0])
func (r *Repository) getProviderEpisodeServers(provider string, episodeDetails *hibikeonlinestream.EpisodeDetails) ([]*hibikeonlinestream.EpisodeServer, error) {
var providerServers []*hibikeonlinestream.EpisodeServer
providerExtension, ok := extension.GetExtension[extension.OnlinestreamProviderExtension](r.providerExtensionBank, provider)
if !ok {
return nil, fmt.Errorf("provider extension '%s' not found", provider)
}
for _, episodeServer := range providerExtension.GetProvider().GetSettings().EpisodeServers {
res, err := providerExtension.GetProvider().FindEpisodeServer(episodeDetails, episodeServer)
if err == nil {
// Add the server to the list for the episode
providerServers = append(providerServers, res)
}
}
if len(providerServers) == 0 {
return nil, errNoEpisodeSourceFound
}
return providerServers, nil
}
// getProviderEpisodeList gets all the hibikeonlinestream.EpisodeDetails from the provider based on the anime's titles.
// It returns ErrNoAnimeFound if the anime is not found or ErrNoEpisodes if no episodes are found.
func (r *Repository) getProviderEpisodeList(provider string, media *anilist.BaseAnime, dubbed bool, year int) ([]*hibikeonlinestream.EpisodeDetails, error) {
var ret []*hibikeonlinestream.EpisodeDetails
// romajiTitle := strings.ReplaceAll(media.GetEnglishTitleSafe(), ":", "")
// englishTitle := strings.ReplaceAll(media.GetRomajiTitleSafe(), ":", "")
romajiTitle := media.GetRomajiTitleSafe()
englishTitle := media.GetEnglishTitleSafe()
providerExtension, ok := extension.GetExtension[extension.OnlinestreamProviderExtension](r.providerExtensionBank, provider)
if !ok {
return nil, fmt.Errorf("provider extension '%s' not found", provider)
}
mId := media.ID
var matchId string
// +---------------------+
// | Database |
// +---------------------+
// Search for the mapping in the database
mapping, found := r.db.GetOnlinestreamMapping(provider, mId)
if found {
r.logger.Debug().Str("animeId", mapping.AnimeID).Msg("onlinestream: Using manual mapping")
matchId = mapping.AnimeID
}
if matchId == "" {
// +---------------------+
// | Search |
// +---------------------+
// Get search results.
var searchResults []*hibikeonlinestream.SearchResult
queryMedia := hibikeonlinestream.Media{
ID: media.ID,
IDMal: media.GetIDMal(),
Status: string(*media.GetStatus()),
Format: string(*media.GetFormat()),
EnglishTitle: media.GetTitle().GetEnglish(),
RomajiTitle: media.GetRomajiTitleSafe(),
EpisodeCount: media.GetTotalEpisodeCount(),
Synonyms: media.GetSynonymsContainingSeason(),
IsAdult: *media.GetIsAdult(),
StartDate: &hibikeonlinestream.FuzzyDate{
Year: *media.GetStartDate().GetYear(),
Month: media.GetStartDate().GetMonth(),
Day: media.GetStartDate().GetDay(),
},
}
added := make(map[string]struct{})
if romajiTitle != "" {
// Search by romaji title
res, err := providerExtension.GetProvider().Search(hibikeonlinestream.SearchOptions{
Media: queryMedia,
Query: romajiTitle,
Dub: dubbed,
Year: year,
})
if err == nil && len(res) > 0 {
searchResults = append(searchResults, res...)
for _, r := range res {
added[r.ID] = struct{}{}
}
}
if err != nil {
r.logger.Error().Err(err).Msg("onlinestream: Failed to search for romaji title")
}
r.logger.Debug().
Int("romajiTitleResults", len(res)).
Msg("onlinestream: Found results for romaji title")
}
if englishTitle != "" {
// Search by english title
res, err := providerExtension.GetProvider().Search(hibikeonlinestream.SearchOptions{
Media: queryMedia,
Query: englishTitle,
Dub: dubbed,
Year: year,
})
if err == nil && len(res) > 0 {
for _, r := range res {
if _, ok := added[r.ID]; !ok {
searchResults = append(searchResults, r)
}
}
}
if err != nil {
r.logger.Error().Err(err).Msg("onlinestream: Failed to search for english title")
}
r.logger.Debug().
Int("englishTitleResults", len(res)).
Msg("onlinestream: Found results for english title")
}
if len(searchResults) == 0 {
return nil, fmt.Errorf("automatic matching returned no results")
}
bestResult, found := GetBestSearchResult(searchResults, media.GetAllTitles())
if !found {
return nil, ErrNoAnimeFound
}
matchId = bestResult.ID
}
// Fetch episodes.
ret, err := providerExtension.GetProvider().FindEpisodes(matchId)
if err != nil {
return nil, fmt.Errorf("provider returned an error: %w", err)
}
if len(ret) == 0 {
return nil, fmt.Errorf("provider returned no episodes")
}
return ret, nil
}
func GetBestSearchResult(searchResults []*hibikeonlinestream.SearchResult, titles []*string) (*hibikeonlinestream.SearchResult, bool) {
// Filter results to get the best match.
compBestResults := make([]*comparison.LevenshteinResult, 0, len(searchResults))
for _, r := range searchResults {
// Compare search result title with all titles.
compBestResult, found := comparison.FindBestMatchWithLevenshtein(&r.Title, titles)
if found {
compBestResults = append(compBestResults, compBestResult)
}
}
if len(compBestResults) == 0 {
return nil, false
}
compBestResult := compBestResults[0]
for _, r := range compBestResults {
if r.Distance < compBestResult.Distance {
compBestResult = r
}
}
// Get most accurate search result.
var bestResult *hibikeonlinestream.SearchResult
for _, r := range searchResults {
if r.Title == *compBestResult.OriginalValue {
bestResult = r
break
}
}
return bestResult, true
}