node build fixed
This commit is contained in:
144
seanime-2.9.10/internal/api/metadata/anime.go
Normal file
144
seanime-2.9.10/internal/api/metadata/anime.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/hook"
|
||||
"seanime/internal/util"
|
||||
"seanime/internal/util/filecache"
|
||||
"strconv"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/samber/mo"
|
||||
)
|
||||
|
||||
type (
|
||||
AnimeWrapperImpl struct {
|
||||
metadata mo.Option[*AnimeMetadata]
|
||||
baseAnime *anilist.BaseAnime
|
||||
fileCacher *filecache.Cacher
|
||||
logger *zerolog.Logger
|
||||
}
|
||||
)
|
||||
|
||||
func (aw *AnimeWrapperImpl) GetEpisodeMetadata(epNum int) (ret EpisodeMetadata) {
|
||||
if aw == nil || aw.baseAnime == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ret = EpisodeMetadata{
|
||||
AnidbId: 0,
|
||||
TvdbId: 0,
|
||||
Title: "",
|
||||
Image: "",
|
||||
AirDate: "",
|
||||
Length: 0,
|
||||
Summary: "",
|
||||
Overview: "",
|
||||
EpisodeNumber: epNum,
|
||||
Episode: strconv.Itoa(epNum),
|
||||
SeasonNumber: 0,
|
||||
AbsoluteEpisodeNumber: 0,
|
||||
AnidbEid: 0,
|
||||
}
|
||||
|
||||
defer util.HandlePanicInModuleThen("api/metadata/GetEpisodeMetadata", func() {})
|
||||
|
||||
reqEvent := &AnimeEpisodeMetadataRequestedEvent{}
|
||||
reqEvent.MediaId = aw.baseAnime.GetID()
|
||||
reqEvent.EpisodeNumber = epNum
|
||||
reqEvent.EpisodeMetadata = &ret
|
||||
_ = hook.GlobalHookManager.OnAnimeEpisodeMetadataRequested().Trigger(reqEvent)
|
||||
epNum = reqEvent.EpisodeNumber
|
||||
|
||||
// Default prevented by hook, return the metadata
|
||||
if reqEvent.DefaultPrevented {
|
||||
if reqEvent.EpisodeMetadata == nil {
|
||||
return ret
|
||||
}
|
||||
return *reqEvent.EpisodeMetadata
|
||||
}
|
||||
|
||||
//
|
||||
// Process
|
||||
//
|
||||
|
||||
episode := mo.None[*EpisodeMetadata]()
|
||||
if aw.metadata.IsAbsent() {
|
||||
ret.Image = aw.baseAnime.GetBannerImageSafe()
|
||||
} else {
|
||||
episodeF, found := aw.metadata.MustGet().FindEpisode(strconv.Itoa(epNum))
|
||||
if found {
|
||||
episode = mo.Some(episodeF)
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have Animap metadata, just return the metadata containing the image
|
||||
if episode.IsAbsent() {
|
||||
return ret
|
||||
}
|
||||
|
||||
ret = *episode.MustGet()
|
||||
|
||||
// If TVDB image is not set, use Animap image, if that is not set, use the AniList banner image
|
||||
if ret.Image == "" {
|
||||
// Set Animap image if TVDB image is not set
|
||||
if episode.MustGet().Image != "" {
|
||||
ret.Image = episode.MustGet().Image
|
||||
} else {
|
||||
// If Animap image is not set, use the base media image
|
||||
ret.Image = aw.baseAnime.GetBannerImageSafe()
|
||||
}
|
||||
}
|
||||
|
||||
// Event
|
||||
event := &AnimeEpisodeMetadataEvent{
|
||||
EpisodeMetadata: &ret,
|
||||
EpisodeNumber: epNum,
|
||||
MediaId: aw.baseAnime.GetID(),
|
||||
}
|
||||
_ = hook.GlobalHookManager.OnAnimeEpisodeMetadata().Trigger(event)
|
||||
if event.EpisodeMetadata == nil {
|
||||
return ret
|
||||
}
|
||||
ret = *event.EpisodeMetadata
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func ExtractEpisodeInteger(s string) (int, bool) {
|
||||
pattern := "[0-9]+"
|
||||
regex := regexp.MustCompile(pattern)
|
||||
|
||||
// Find the first match in the input string.
|
||||
match := regex.FindString(s)
|
||||
|
||||
if match != "" {
|
||||
// Convert the matched string to an integer.
|
||||
num, err := strconv.Atoi(match)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return num, true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func OffsetAnidbEpisode(s string, offset int) string {
|
||||
pattern := "([0-9]+)"
|
||||
regex := regexp.MustCompile(pattern)
|
||||
|
||||
// Replace the first matched integer with the incremented value.
|
||||
result := regex.ReplaceAllStringFunc(s, func(matched string) string {
|
||||
num, err := strconv.Atoi(matched)
|
||||
if err == nil {
|
||||
num = num + offset
|
||||
return strconv.Itoa(num)
|
||||
} else {
|
||||
return matched
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
26
seanime-2.9.10/internal/api/metadata/anime_test.go
Normal file
26
seanime-2.9.10/internal/api/metadata/anime_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOffsetEpisode(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"S1", "S2"},
|
||||
{"OP1", "OP2"},
|
||||
{"1", "2"},
|
||||
{"OP", "OP"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
actual := OffsetAnidbEpisode(c.input, 1)
|
||||
if actual != c.expected {
|
||||
t.Errorf("OffsetAnidbEpisode(%s, 1) == %s, expected %s", c.input, actual, c.expected)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
47
seanime-2.9.10/internal/api/metadata/hook_events.go
Normal file
47
seanime-2.9.10/internal/api/metadata/hook_events.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package metadata
|
||||
|
||||
import "seanime/internal/hook_resolver"
|
||||
|
||||
// AnimeMetadataRequestedEvent is triggered when anime metadata is requested and right before the metadata is processed.
|
||||
// This event is followed by [AnimeMetadataEvent] which is triggered when the metadata is available.
|
||||
// Prevent default to skip the default behavior and return the modified metadata.
|
||||
// If the modified metadata is nil, an error will be returned.
|
||||
type AnimeMetadataRequestedEvent struct {
|
||||
hook_resolver.Event
|
||||
MediaId int `json:"mediaId"`
|
||||
// Empty metadata object, will be used if the hook prevents the default behavior
|
||||
AnimeMetadata *AnimeMetadata `json:"animeMetadata"`
|
||||
}
|
||||
|
||||
// AnimeMetadataEvent is triggered when anime metadata is available and is about to be returned.
|
||||
// Anime metadata can be requested in many places, ranging from displaying the anime entry to starting a torrent stream.
|
||||
// This event is triggered after [AnimeMetadataRequestedEvent].
|
||||
// If the modified metadata is nil, an error will be returned.
|
||||
type AnimeMetadataEvent struct {
|
||||
hook_resolver.Event
|
||||
MediaId int `json:"mediaId"`
|
||||
AnimeMetadata *AnimeMetadata `json:"animeMetadata"`
|
||||
}
|
||||
|
||||
// AnimeEpisodeMetadataRequestedEvent is triggered when anime episode metadata is requested.
|
||||
// Prevent default to skip the default behavior and return the overridden metadata.
|
||||
// This event is triggered before [AnimeEpisodeMetadataEvent].
|
||||
// If the modified episode metadata is nil, an empty EpisodeMetadata object will be returned.
|
||||
type AnimeEpisodeMetadataRequestedEvent struct {
|
||||
hook_resolver.Event
|
||||
// Empty metadata object, will be used if the hook prevents the default behavior
|
||||
EpisodeMetadata *EpisodeMetadata `json:"animeEpisodeMetadata"`
|
||||
EpisodeNumber int `json:"episodeNumber"`
|
||||
MediaId int `json:"mediaId"`
|
||||
}
|
||||
|
||||
// AnimeEpisodeMetadataEvent is triggered when anime episode metadata is available and is about to be returned.
|
||||
// In the current implementation, episode metadata is requested for display purposes. It is used to get a more complete metadata object since the original AnimeMetadata object is not complete.
|
||||
// This event is triggered after [AnimeEpisodeMetadataRequestedEvent].
|
||||
// If the modified episode metadata is nil, an empty EpisodeMetadata object will be returned.
|
||||
type AnimeEpisodeMetadataEvent struct {
|
||||
hook_resolver.Event
|
||||
EpisodeMetadata *EpisodeMetadata `json:"animeEpisodeMetadata"`
|
||||
EpisodeNumber int `json:"episodeNumber"`
|
||||
MediaId int `json:"mediaId"`
|
||||
}
|
||||
18
seanime-2.9.10/internal/api/metadata/mock.go
Normal file
18
seanime-2.9.10/internal/api/metadata/mock.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"seanime/internal/util"
|
||||
"seanime/internal/util/filecache"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func GetMockProvider(t *testing.T) Provider {
|
||||
filecacher, err := filecache.NewCacher(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
return NewProvider(&NewProviderImplOptions{
|
||||
Logger: util.NewLogger(),
|
||||
FileCacher: filecacher,
|
||||
})
|
||||
}
|
||||
212
seanime-2.9.10/internal/api/metadata/provider.go
Normal file
212
seanime-2.9.10/internal/api/metadata/provider.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/api/animap"
|
||||
"seanime/internal/hook"
|
||||
"seanime/internal/util/filecache"
|
||||
"seanime/internal/util/result"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/samber/mo"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
type (
|
||||
ProviderImpl struct {
|
||||
logger *zerolog.Logger
|
||||
fileCacher *filecache.Cacher
|
||||
animeMetadataCache *result.BoundedCache[string, *AnimeMetadata]
|
||||
singleflight *singleflight.Group
|
||||
}
|
||||
|
||||
NewProviderImplOptions struct {
|
||||
Logger *zerolog.Logger
|
||||
FileCacher *filecache.Cacher
|
||||
}
|
||||
)
|
||||
|
||||
func GetAnimeMetadataCacheKey(platform Platform, mId int) string {
|
||||
return fmt.Sprintf("%s$%d", platform, mId)
|
||||
}
|
||||
|
||||
// NewProvider creates a new metadata provider.
|
||||
func NewProvider(options *NewProviderImplOptions) Provider {
|
||||
return &ProviderImpl{
|
||||
logger: options.Logger,
|
||||
fileCacher: options.FileCacher,
|
||||
animeMetadataCache: result.NewBoundedCache[string, *AnimeMetadata](100),
|
||||
singleflight: &singleflight.Group{},
|
||||
}
|
||||
}
|
||||
|
||||
// GetCache returns the anime metadata cache.
|
||||
func (p *ProviderImpl) GetCache() *result.BoundedCache[string, *AnimeMetadata] {
|
||||
return p.animeMetadataCache
|
||||
}
|
||||
|
||||
// GetAnimeMetadata fetches anime metadata from api.ani.zip.
|
||||
func (p *ProviderImpl) GetAnimeMetadata(platform Platform, mId int) (ret *AnimeMetadata, err error) {
|
||||
cacheKey := GetAnimeMetadataCacheKey(platform, mId)
|
||||
if cached, ok := p.animeMetadataCache.Get(cacheKey); ok {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
res, err, _ := p.singleflight.Do(cacheKey, func() (interface{}, error) {
|
||||
return p.fetchAnimeMetadata(platform, mId)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res.(*AnimeMetadata), nil
|
||||
}
|
||||
|
||||
func (p *ProviderImpl) fetchAnimeMetadata(platform Platform, mId int) (*AnimeMetadata, error) {
|
||||
ret := &AnimeMetadata{
|
||||
Titles: make(map[string]string),
|
||||
Episodes: make(map[string]*EpisodeMetadata),
|
||||
EpisodeCount: 0,
|
||||
SpecialCount: 0,
|
||||
Mappings: &AnimeMappings{},
|
||||
}
|
||||
|
||||
// Invoke AnimeMetadataRequested hook
|
||||
reqEvent := &AnimeMetadataRequestedEvent{
|
||||
MediaId: mId,
|
||||
AnimeMetadata: ret,
|
||||
}
|
||||
err := hook.GlobalHookManager.OnAnimeMetadataRequested().Trigger(reqEvent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mId = reqEvent.MediaId
|
||||
|
||||
// Default prevented by hook, return the metadata
|
||||
if reqEvent.DefaultPrevented {
|
||||
// Override the metadata
|
||||
ret = reqEvent.AnimeMetadata
|
||||
|
||||
// Trigger the event
|
||||
event := &AnimeMetadataEvent{
|
||||
MediaId: mId,
|
||||
AnimeMetadata: ret,
|
||||
}
|
||||
err = hook.GlobalHookManager.OnAnimeMetadata().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret = event.AnimeMetadata
|
||||
mId = event.MediaId
|
||||
|
||||
if ret == nil {
|
||||
return nil, errors.New("no metadata was returned")
|
||||
}
|
||||
p.animeMetadataCache.SetT(GetAnimeMetadataCacheKey(platform, mId), ret, 1*time.Hour)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
m, err := animap.FetchAnimapMedia(string(platform), mId)
|
||||
if err != nil || m == nil {
|
||||
//return p.AnizipFallback(platform, mId)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret.Titles = m.Titles
|
||||
ret.EpisodeCount = 0
|
||||
ret.SpecialCount = 0
|
||||
ret.Mappings.AnimeplanetId = m.Mappings.AnimePlanetID
|
||||
ret.Mappings.KitsuId = m.Mappings.KitsuID
|
||||
ret.Mappings.MalId = m.Mappings.MalID
|
||||
ret.Mappings.Type = m.Mappings.Type
|
||||
ret.Mappings.AnilistId = m.Mappings.AnilistID
|
||||
ret.Mappings.AnisearchId = m.Mappings.AnisearchID
|
||||
ret.Mappings.AnidbId = m.Mappings.AnidbID
|
||||
ret.Mappings.NotifymoeId = m.Mappings.NotifyMoeID
|
||||
ret.Mappings.LivechartId = m.Mappings.LivechartID
|
||||
ret.Mappings.ThetvdbId = m.Mappings.TheTvdbID
|
||||
ret.Mappings.ImdbId = ""
|
||||
ret.Mappings.ThemoviedbId = m.Mappings.TheMovieDbID
|
||||
|
||||
for key, ep := range m.Episodes {
|
||||
firstChar := key[0]
|
||||
if firstChar == 'S' {
|
||||
ret.SpecialCount++
|
||||
} else {
|
||||
if firstChar >= '0' && firstChar <= '9' {
|
||||
ret.EpisodeCount++
|
||||
}
|
||||
}
|
||||
em := &EpisodeMetadata{
|
||||
AnidbId: ep.AnidbId,
|
||||
TvdbId: ep.TvdbId,
|
||||
Title: ep.AnidbTitle,
|
||||
Image: ep.Image,
|
||||
AirDate: ep.AirDate,
|
||||
Length: ep.Runtime,
|
||||
Summary: strings.ReplaceAll(ep.Overview, "`", "'"),
|
||||
Overview: strings.ReplaceAll(ep.Overview, "`", "'"),
|
||||
EpisodeNumber: ep.Number,
|
||||
Episode: key,
|
||||
SeasonNumber: ep.SeasonNumber,
|
||||
AbsoluteEpisodeNumber: ep.AbsoluteNumber,
|
||||
AnidbEid: ep.AnidbId,
|
||||
HasImage: ep.Image != "",
|
||||
}
|
||||
if em.Length == 0 && ep.Runtime > 0 {
|
||||
em.Length = ep.Runtime
|
||||
}
|
||||
if em.Summary == "" && ep.Overview != "" {
|
||||
em.Summary = ep.Overview
|
||||
}
|
||||
if em.Overview == "" && ep.Overview != "" {
|
||||
em.Overview = ep.Overview
|
||||
}
|
||||
if ep.TvdbTitle != "" && ep.AnidbTitle == "Episode "+ep.AnidbEpisode {
|
||||
em.Title = ep.TvdbTitle
|
||||
|
||||
}
|
||||
ret.Episodes[key] = em
|
||||
}
|
||||
|
||||
// Event
|
||||
event := &AnimeMetadataEvent{
|
||||
MediaId: mId,
|
||||
AnimeMetadata: ret,
|
||||
}
|
||||
err = hook.GlobalHookManager.OnAnimeMetadata().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret = event.AnimeMetadata
|
||||
mId = event.MediaId
|
||||
|
||||
p.animeMetadataCache.SetT(GetAnimeMetadataCacheKey(platform, mId), ret, 1*time.Hour)
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// GetAnimeMetadataWrapper creates a new anime wrapper.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// metadataProvider.GetAnimeMetadataWrapper(media, metadata)
|
||||
// metadataProvider.GetAnimeMetadataWrapper(media, nil)
|
||||
func (p *ProviderImpl) GetAnimeMetadataWrapper(media *anilist.BaseAnime, metadata *AnimeMetadata) AnimeMetadataWrapper {
|
||||
aw := &AnimeWrapperImpl{
|
||||
metadata: mo.None[*AnimeMetadata](),
|
||||
baseAnime: media,
|
||||
fileCacher: p.fileCacher,
|
||||
logger: p.logger,
|
||||
}
|
||||
|
||||
if metadata != nil {
|
||||
aw.metadata = mo.Some(metadata)
|
||||
}
|
||||
|
||||
return aw
|
||||
}
|
||||
165
seanime-2.9.10/internal/api/metadata/types.go
Normal file
165
seanime-2.9.10/internal/api/metadata/types.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/util/result"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
AnilistPlatform Platform = "anilist"
|
||||
MalPlatform Platform = "mal"
|
||||
)
|
||||
|
||||
type (
|
||||
Platform string
|
||||
|
||||
Provider interface {
|
||||
// GetAnimeMetadata fetches anime metadata for the given platform from a source.
|
||||
// In this case, the source is api.ani.zip.
|
||||
GetAnimeMetadata(platform Platform, mId int) (*AnimeMetadata, error)
|
||||
GetCache() *result.BoundedCache[string, *AnimeMetadata]
|
||||
// GetAnimeMetadataWrapper creates a wrapper for anime metadata.
|
||||
GetAnimeMetadataWrapper(anime *anilist.BaseAnime, metadata *AnimeMetadata) AnimeMetadataWrapper
|
||||
}
|
||||
|
||||
// AnimeMetadataWrapper is a container for anime metadata.
|
||||
// This wrapper is used to get a more complete metadata object by getting data from multiple sources in the Provider.
|
||||
// The user can request metadata to be fetched from TVDB as well, which will be stored in the cache.
|
||||
AnimeMetadataWrapper interface {
|
||||
// GetEpisodeMetadata combines metadata from multiple sources to create a single EpisodeMetadata object.
|
||||
GetEpisodeMetadata(episodeNumber int) EpisodeMetadata
|
||||
}
|
||||
)
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type (
|
||||
AnimeMetadata struct {
|
||||
Titles map[string]string `json:"titles"`
|
||||
Episodes map[string]*EpisodeMetadata `json:"episodes"`
|
||||
EpisodeCount int `json:"episodeCount"`
|
||||
SpecialCount int `json:"specialCount"`
|
||||
Mappings *AnimeMappings `json:"mappings"`
|
||||
|
||||
currentEpisodeCount int `json:"-"`
|
||||
}
|
||||
|
||||
AnimeMappings struct {
|
||||
AnimeplanetId string `json:"animeplanetId"`
|
||||
KitsuId int `json:"kitsuId"`
|
||||
MalId int `json:"malId"`
|
||||
Type string `json:"type"`
|
||||
AnilistId int `json:"anilistId"`
|
||||
AnisearchId int `json:"anisearchId"`
|
||||
AnidbId int `json:"anidbId"`
|
||||
NotifymoeId string `json:"notifymoeId"`
|
||||
LivechartId int `json:"livechartId"`
|
||||
ThetvdbId int `json:"thetvdbId"`
|
||||
ImdbId string `json:"imdbId"`
|
||||
ThemoviedbId string `json:"themoviedbId"`
|
||||
}
|
||||
|
||||
EpisodeMetadata struct {
|
||||
AnidbId int `json:"anidbId"`
|
||||
TvdbId int `json:"tvdbId"`
|
||||
Title string `json:"title"`
|
||||
Image string `json:"image"`
|
||||
AirDate string `json:"airDate"`
|
||||
Length int `json:"length"`
|
||||
Summary string `json:"summary"`
|
||||
Overview string `json:"overview"`
|
||||
EpisodeNumber int `json:"episodeNumber"`
|
||||
Episode string `json:"episode"`
|
||||
SeasonNumber int `json:"seasonNumber"`
|
||||
AbsoluteEpisodeNumber int `json:"absoluteEpisodeNumber"`
|
||||
AnidbEid int `json:"anidbEid"`
|
||||
HasImage bool `json:"hasImage"` // Indicates if the episode has a real image
|
||||
}
|
||||
)
|
||||
|
||||
func (m *AnimeMetadata) GetTitle() string {
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
if len(m.Titles["en"]) > 0 {
|
||||
return m.Titles["en"]
|
||||
}
|
||||
return m.Titles["ro"]
|
||||
}
|
||||
|
||||
func (m *AnimeMetadata) GetMappings() *AnimeMappings {
|
||||
if m == nil {
|
||||
return &AnimeMappings{}
|
||||
}
|
||||
return m.Mappings
|
||||
}
|
||||
|
||||
func (m *AnimeMetadata) FindEpisode(ep string) (*EpisodeMetadata, bool) {
|
||||
if m.Episodes == nil {
|
||||
return nil, false
|
||||
}
|
||||
episode, found := m.Episodes[ep]
|
||||
if !found {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return episode, true
|
||||
}
|
||||
|
||||
func (m *AnimeMetadata) GetMainEpisodeCount() int {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
return m.EpisodeCount
|
||||
}
|
||||
|
||||
func (m *AnimeMetadata) GetCurrentEpisodeCount() int {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
if m.currentEpisodeCount > 0 {
|
||||
return m.currentEpisodeCount
|
||||
}
|
||||
count := 0
|
||||
for _, ep := range m.Episodes {
|
||||
firstChar := ep.Episode[0]
|
||||
if firstChar >= '0' && firstChar <= '9' {
|
||||
// Check if aired
|
||||
if ep.AirDate != "" {
|
||||
date, err := time.Parse("2006-01-02", ep.AirDate)
|
||||
if err == nil {
|
||||
if date.Before(time.Now()) || date.Equal(time.Now()) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
m.currentEpisodeCount = count
|
||||
return count
|
||||
}
|
||||
|
||||
// GetOffset returns the offset of the first episode relative to the absolute episode number.
|
||||
// e.g, if the first episode's absolute number is 13, then the offset is 12.
|
||||
func (m *AnimeMetadata) GetOffset() int {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
firstEp, found := m.FindEpisode("1")
|
||||
if !found {
|
||||
return 0
|
||||
}
|
||||
if firstEp.AbsoluteEpisodeNumber == 0 {
|
||||
return 0
|
||||
}
|
||||
return firstEp.AbsoluteEpisodeNumber - 1
|
||||
}
|
||||
|
||||
func (e *EpisodeMetadata) GetTitle() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.ReplaceAll(e.Title, "`", "'")
|
||||
}
|
||||
Reference in New Issue
Block a user