358 lines
12 KiB
Go
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
|
|
}
|