From 3a158298315b46cb21993ec7b1c52e6bf78ac92c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 28 Aug 2019 17:06:42 +0900 Subject: [PATCH] Episodes now have their own ID --- arn/AMV.go | 2 +- arn/Anime.go | 56 +++--- arn/AnimeEpisode.go | 78 --------- arn/AnimeEpisodes.go | 157 ----------------- arn/AnimeEpisodesAPI.go | 54 ------ arn/Database.go | 2 +- arn/Episode.go | 164 ++++++++++++++++++ arn/EpisodeAPI.go | 54 ++++++ arn/EpisodeList.go | 68 ++++++++ arn/KitsuAnime.go | 14 +- arn/UpcomingEpisode.go | 2 +- jobs/kitsu-import-anime/kitsu-import-anime.go | 2 +- jobs/refresh-episodes/refresh-episodes.go | 6 +- jobs/twist/twist.go | 2 +- pages/anime/anime.go | 4 +- pages/anime/anime.pixy | 4 +- pages/anime/editanime/editanime.go | 11 +- pages/anime/episodes.go | 2 +- pages/anime/episodes.pixy | 6 +- pages/animeimport/kitsu.go | 3 +- pages/calendar/calendar.go | 68 ++++---- pages/episode/episode.go | 23 +-- pages/episode/episode.pixy | 8 +- pages/episode/subtitles.go | 12 +- pages/index/animeroutes/animeroutes.go | 4 +- pages/statistics/anime.go | 2 +- .../anime-episodes-upgrade.go | 22 +++ patches/fix-airing-dates/fix-airing-dates.go | 2 +- utils/Calendar.go | 2 +- 29 files changed, 422 insertions(+), 412 deletions(-) delete mode 100644 arn/AnimeEpisode.go delete mode 100644 arn/AnimeEpisodes.go delete mode 100644 arn/AnimeEpisodesAPI.go create mode 100644 arn/Episode.go create mode 100644 arn/EpisodeAPI.go create mode 100644 arn/EpisodeList.go create mode 100644 patches/anime-episodes-upgrade/anime-episodes-upgrade.go diff --git a/arn/AMV.go b/arn/AMV.go index fbcef12c..53997ea2 100644 --- a/arn/AMV.go +++ b/arn/AMV.go @@ -144,7 +144,7 @@ func (amv *AMV) MainAnime() *Anime { // ExtraAnime returns main anime for the AMV, if available. func (amv *AMV) ExtraAnime() []*Anime { objects := DB.GetMany("Anime", amv.ExtraAnimeIDs) - animes := []*Anime{} + animes := make([]*Anime, 0, len(amv.ExtraAnimeIDs)) for _, obj := range objects { if obj == nil { diff --git a/arn/Anime.go b/arn/Anime.go index 49f22424..f24ee877 100644 --- a/arn/Anime.go +++ b/arn/Anime.go @@ -83,6 +83,7 @@ type Anime struct { Rating *AnimeRating `json:"rating"` Popularity *AnimePopularity `json:"popularity"` Trailers []*ExternalMedia `json:"trailers" editable:"true"` + EpisodeIDs []string `json:"episodes" editable:"true"` // Mixins hasMappings @@ -356,15 +357,20 @@ func (anime *Anime) EndDateTime() time.Time { return t } -// Episodes returns the anime episodes wrapper. -func (anime *Anime) Episodes() *AnimeEpisodes { - record, err := DB.Get("AnimeEpisodes", anime.ID) +// Episodes returns the anime episodes. +func (anime *Anime) Episodes() EpisodeList { + objects := DB.GetMany("Episode", anime.EpisodeIDs) + episodes := make([]*Episode, 0, len(anime.EpisodeIDs)) - if err != nil { - return nil + for _, obj := range objects { + if obj == nil { + continue + } + + episodes = append(episodes, obj.(*Episode)) } - return record.(*AnimeEpisodes) + return episodes } // UsersWatchingOrPlanned returns a list of users who are watching the anime right now. @@ -387,13 +393,6 @@ func (anime *Anime) RefreshEpisodes() error { // Fetch episodes episodes := anime.Episodes() - if episodes == nil { - episodes = &AnimeEpisodes{ - AnimeID: anime.ID, - Items: []*AnimeEpisode{}, - } - } - // Save number of available episodes for comparison later oldAvailableCount := episodes.AvailableCount() @@ -441,7 +440,7 @@ func (anime *Anime) RefreshEpisodes() error { // Number remaining episodes startNumber := 0 - for _, episode := range episodes.Items { + for _, episode := range episodes { if episode.Number != -1 { startNumber = episode.Number continue @@ -456,7 +455,7 @@ func (anime *Anime) RefreshEpisodes() error { lastAiringDate := "" timeDifference := oneWeek - for _, episode := range episodes.Items { + for _, episode := range episodes { if validate.DateTime(episode.AiringDate.Start) { if lastAiringDate != "" { a, _ := time.Parse(time.RFC3339, lastAiringDate) @@ -485,12 +484,20 @@ func (anime *Anime) RefreshEpisodes() error { lastAiringDate = episode.AiringDate.Start } - episodes.Save() + // Save new episode ID list + episodeIDs := make([]string, len(episodes)) + + for index := range episodes { + episodeIDs[index] = episodes[index].ID + } + + anime.EpisodeIDs = episodeIDs + anime.Save() return nil } // ShoboiEpisodes returns a slice of episode info from cal.syoboi.jp. -func (anime *Anime) ShoboiEpisodes() ([]*AnimeEpisode, error) { +func (anime *Anime) ShoboiEpisodes() (EpisodeList, error) { shoboiID := anime.GetMapping("shoboi/anime") if shoboiID == "" { @@ -503,7 +510,7 @@ func (anime *Anime) ShoboiEpisodes() ([]*AnimeEpisode, error) { return nil, err } - arnEpisodes := []*AnimeEpisode{} + arnEpisodes := []*Episode{} shoboiEpisodes := shoboiAnime.Episodes() for _, shoboiEpisode := range shoboiEpisodes { @@ -529,7 +536,7 @@ func (anime *Anime) ShoboiEpisodes() ([]*AnimeEpisode, error) { } // TwistEpisodes returns a slice of episode info from twist.moe. -func (anime *Anime) TwistEpisodes() ([]*AnimeEpisode, error) { +func (anime *Anime) TwistEpisodes() (EpisodeList, error) { idList, err := GetIDList("animetwist index") if err != nil { @@ -566,7 +573,7 @@ func (anime *Anime) TwistEpisodes() ([]*AnimeEpisode, error) { return episodes[a].Number < episodes[b].Number }) - arnEpisodes := []*AnimeEpisode{} + arnEpisodes := []*Episode{} for _, episode := range episodes { arnEpisode := NewAnimeEpisode() @@ -584,10 +591,9 @@ func (anime *Anime) TwistEpisodes() ([]*AnimeEpisode, error) { // UpcomingEpisodes ... func (anime *Anime) UpcomingEpisodes() []*UpcomingEpisode { var upcomingEpisodes []*UpcomingEpisode - now := time.Now().UTC().Format(time.RFC3339) - for _, episode := range anime.Episodes().Items { + for _, episode := range anime.Episodes() { if episode.AiringDate.Start > now && validate.DateTime(episode.AiringDate.Start) { upcomingEpisodes = append(upcomingEpisodes, &UpcomingEpisode{ Anime: anime, @@ -603,7 +609,7 @@ func (anime *Anime) UpcomingEpisodes() []*UpcomingEpisode { func (anime *Anime) UpcomingEpisode() *UpcomingEpisode { now := time.Now().UTC().Format(time.RFC3339) - for _, episode := range anime.Episodes().Items { + for _, episode := range anime.Episodes() { if episode.AiringDate.Start > now && validate.DateTime(episode.AiringDate.Start) { return &UpcomingEpisode{ Anime: anime, @@ -719,8 +725,8 @@ func (anime *Anime) CalculatedStatus() string { } // EpisodeByNumber returns the episode with the given number. -func (anime *Anime) EpisodeByNumber(number int) *AnimeEpisode { - for _, episode := range anime.Episodes().Items { +func (anime *Anime) EpisodeByNumber(number int) *Episode { + for _, episode := range anime.Episodes() { if number == episode.Number { return episode } diff --git a/arn/AnimeEpisode.go b/arn/AnimeEpisode.go deleted file mode 100644 index b7627f7c..00000000 --- a/arn/AnimeEpisode.go +++ /dev/null @@ -1,78 +0,0 @@ -package arn - -import "github.com/animenotifier/notify.moe/arn/validate" - -// AnimeEpisode ... -type AnimeEpisode struct { - Number int `json:"number" editable:"true"` - Title EpisodeTitle `json:"title" editable:"true"` - AiringDate AiringDate `json:"airingDate" editable:"true"` - Links map[string]string `json:"links"` -} - -// EpisodeTitle ... -type EpisodeTitle struct { - Romaji string `json:"romaji" editable:"true"` - English string `json:"english" editable:"true"` - Japanese string `json:"japanese" editable:"true"` -} - -// Available tells you whether the episode is available (triggered when it has a link). -func (a *AnimeEpisode) Available() bool { - return len(a.Links) > 0 -} - -// AvailableOn tells you whether the episode is available on a given service. -func (a *AnimeEpisode) AvailableOn(serviceName string) bool { - return a.Links[serviceName] != "" -} - -// Merge combines the data of both episodes to one. -func (a *AnimeEpisode) Merge(b *AnimeEpisode) { - if b == nil { - return - } - - a.Number = b.Number - - // Titles - if b.Title.Romaji != "" { - a.Title.Romaji = b.Title.Romaji - } - - if b.Title.English != "" { - a.Title.English = b.Title.English - } - - if b.Title.Japanese != "" { - a.Title.Japanese = b.Title.Japanese - } - - // Airing date - if validate.DateTime(b.AiringDate.Start) { - a.AiringDate.Start = b.AiringDate.Start - } - - if validate.DateTime(b.AiringDate.End) { - a.AiringDate.End = b.AiringDate.End - } - - // Links - if a.Links == nil { - a.Links = map[string]string{} - } - - for name, link := range b.Links { - a.Links[name] = link - } -} - -// NewAnimeEpisode creates an empty anime episode. -func NewAnimeEpisode() *AnimeEpisode { - return &AnimeEpisode{ - Number: -1, - Title: EpisodeTitle{}, - AiringDate: AiringDate{}, - Links: map[string]string{}, - } -} diff --git a/arn/AnimeEpisodes.go b/arn/AnimeEpisodes.go deleted file mode 100644 index c97f1e00..00000000 --- a/arn/AnimeEpisodes.go +++ /dev/null @@ -1,157 +0,0 @@ -package arn - -import ( - "sort" - "strconv" - "strings" - "sync" - - "github.com/aerogo/nano" -) - -// AnimeEpisodes is a list of episodes for an anime. -type AnimeEpisodes struct { - AnimeID AnimeID `json:"animeId" mainID:"true"` - Items []*AnimeEpisode `json:"items" editable:"true"` - - sync.Mutex -} - -// Link returns the link for that object. -func (episodes *AnimeEpisodes) Link() string { - return "/anime/" + episodes.AnimeID + "/episodes" -} - -// Sort sorts the episodes by episode number. -func (episodes *AnimeEpisodes) Sort() { - episodes.Lock() - defer episodes.Unlock() - - sort.Slice(episodes.Items, func(i, j int) bool { - return episodes.Items[i].Number < episodes.Items[j].Number - }) -} - -// Find finds the given episode number. -func (episodes *AnimeEpisodes) Find(episodeNumber int) (*AnimeEpisode, int) { - episodes.Lock() - defer episodes.Unlock() - - for index, episode := range episodes.Items { - if episode.Number == episodeNumber { - return episode, index - } - } - - return nil, -1 -} - -// Merge combines the data of both episode slices to one. -func (episodes *AnimeEpisodes) Merge(b []*AnimeEpisode) { - if b == nil { - return - } - - episodes.Lock() - defer episodes.Unlock() - - for index, episode := range b { - if index >= len(episodes.Items) { - episodes.Items = append(episodes.Items, episode) - } else { - episodes.Items[index].Merge(episode) - } - } -} - -// Last returns the last n items. -func (episodes *AnimeEpisodes) Last(count int) []*AnimeEpisode { - return episodes.Items[len(episodes.Items)-count:] -} - -// AvailableCount counts the number of available episodes. -func (episodes *AnimeEpisodes) AvailableCount() int { - episodes.Lock() - defer episodes.Unlock() - - available := 0 - - for _, episode := range episodes.Items { - if len(episode.Links) > 0 { - available++ - } - } - - return available -} - -// Anime returns the anime the episodes refer to. -func (episodes *AnimeEpisodes) Anime() *Anime { - anime, _ := GetAnime(episodes.AnimeID) - return anime -} - -// String implements the default string serialization. -func (episodes *AnimeEpisodes) String() string { - return episodes.Anime().String() -} - -// GetID returns the anime ID. -func (episodes *AnimeEpisodes) GetID() string { - return episodes.AnimeID -} - -// TypeName returns the type name. -func (episodes *AnimeEpisodes) TypeName() string { - return "AnimeEpisodes" -} - -// Self returns the object itself. -func (episodes *AnimeEpisodes) Self() Loggable { - return episodes -} - -// ListString returns a text representation of the anime episodes. -func (episodes *AnimeEpisodes) ListString() string { - episodes.Lock() - defer episodes.Unlock() - - b := strings.Builder{} - - for _, episode := range episodes.Items { - b.WriteString(strconv.Itoa(episode.Number)) - b.WriteString(" | ") - b.WriteString(episode.Title.Japanese) - b.WriteString(" | ") - b.WriteString(episode.AiringDate.StartDateHuman()) - b.WriteByte('\n') - } - - return strings.TrimRight(b.String(), "\n") -} - -// StreamAnimeEpisodes returns a stream of all anime episodes. -func StreamAnimeEpisodes() <-chan *AnimeEpisodes { - channel := make(chan *AnimeEpisodes, nano.ChannelBufferSize) - - go func() { - for obj := range DB.All("AnimeEpisodes") { - channel <- obj.(*AnimeEpisodes) - } - - close(channel) - }() - - return channel -} - -// GetAnimeEpisodes ... -func GetAnimeEpisodes(id string) (*AnimeEpisodes, error) { - obj, err := DB.Get("AnimeEpisodes", id) - - if err != nil { - return nil, err - } - - return obj.(*AnimeEpisodes), nil -} diff --git a/arn/AnimeEpisodesAPI.go b/arn/AnimeEpisodesAPI.go deleted file mode 100644 index 642f0eab..00000000 --- a/arn/AnimeEpisodesAPI.go +++ /dev/null @@ -1,54 +0,0 @@ -package arn - -import ( - "errors" - "fmt" - "reflect" - - "github.com/aerogo/aero" - "github.com/aerogo/api" -) - -// Force interface implementations -var ( - _ fmt.Stringer = (*AnimeEpisodes)(nil) - _ api.Editable = (*AnimeEpisodes)(nil) - _ api.ArrayEventListener = (*AnimeEpisodes)(nil) -) - -// Authorize returns an error if the given API POST request is not authorized. -func (episodes *AnimeEpisodes) Authorize(ctx aero.Context, action string) error { - user := GetUserFromContext(ctx) - - if user == nil || (user.Role != "editor" && user.Role != "admin") { - return errors.New("Not logged in or not authorized to edit") - } - - return nil -} - -// Edit creates an edit log entry. -func (episodes *AnimeEpisodes) Edit(ctx aero.Context, key string, value reflect.Value, newValue reflect.Value) (consumed bool, err error) { - return edit(episodes, ctx, key, value, newValue) -} - -// OnAppend saves a log entry. -func (episodes *AnimeEpisodes) OnAppend(ctx aero.Context, key string, index int, obj interface{}) { - onAppend(episodes, ctx, key, index, obj) -} - -// OnRemove saves a log entry. -func (episodes *AnimeEpisodes) OnRemove(ctx aero.Context, key string, index int, obj interface{}) { - onRemove(episodes, ctx, key, index, obj) -} - -// Save saves the episodes in the database. -func (episodes *AnimeEpisodes) Save() { - DB.Set("AnimeEpisodes", episodes.AnimeID, episodes) -} - -// Delete deletes the episode list from the database. -func (episodes *AnimeEpisodes) Delete() error { - DB.Delete("AnimeEpisodes", episodes.AnimeID) - return nil -} diff --git a/arn/Database.go b/arn/Database.go index 003d03a6..ca62fe7e 100644 --- a/arn/Database.go +++ b/arn/Database.go @@ -20,7 +20,6 @@ var DB = Node.Namespace("arn").RegisterTypes( (*Analytics)(nil), (*Anime)(nil), (*AnimeCharacters)(nil), - (*AnimeEpisodes)(nil), (*AnimeRelations)(nil), (*AnimeList)(nil), (*Character)(nil), @@ -29,6 +28,7 @@ var DB = Node.Namespace("arn").RegisterTypes( (*DraftIndex)(nil), (*EditLogEntry)(nil), (*EmailToUser)(nil), + (*Episode)(nil), (*FacebookToUser)(nil), (*GoogleToUser)(nil), (*Group)(nil), diff --git a/arn/Episode.go b/arn/Episode.go new file mode 100644 index 00000000..a63575ae --- /dev/null +++ b/arn/Episode.go @@ -0,0 +1,164 @@ +package arn + +import ( + "fmt" + + "github.com/aerogo/nano" + "github.com/animenotifier/notify.moe/arn/validate" +) + +// Episode represents a single episode for an anime. +type Episode struct { + ID string `json:"id"` + AnimeID AnimeID `json:"animeId"` + Number int `json:"number" editable:"true"` + Title EpisodeTitle `json:"title" editable:"true"` + AiringDate AiringDate `json:"airingDate" editable:"true"` + Links map[string]string `json:"links"` +} + +// EpisodeTitle contains the title information for an anime episode. +type EpisodeTitle struct { + Romaji string `json:"romaji" editable:"true"` + English string `json:"english" editable:"true"` + Japanese string `json:"japanese" editable:"true"` +} + +// NewAnimeEpisode creates a new anime episode. +func NewAnimeEpisode() *Episode { + return &Episode{ + ID: GenerateID("Episode"), + Number: -1, + } +} + +// Anime returns the anime the episode refers to. +func (episode *Episode) Anime() *Anime { + anime, _ := GetAnime(episode.AnimeID) + return anime +} + +// GetID returns the episode ID. +func (episode *Episode) GetID() string { + return episode.ID +} + +// TypeName returns the type name. +func (episode *Episode) TypeName() string { + return "Episode" +} + +// Self returns the object itself. +func (episode *Episode) Self() Loggable { + return episode +} + +// Link returns the permalink to the episode. +func (episode *Episode) Link() string { + return "/episode/" + episode.ID +} + +// Available tells you whether the episode is available (triggered when it has a link). +func (episode *Episode) Available() bool { + return len(episode.Links) > 0 +} + +// AvailableOn tells you whether the episode is available on a given service. +func (episode *Episode) AvailableOn(serviceName string) bool { + return episode.Links[serviceName] != "" +} + +// Previous returns the previous episode, if available. +func (episode *Episode) Previous() *Episode { + episodes := episode.Anime().Episodes() + _, index := episodes.Find(episode.Number) + + if index > 0 { + return episodes[index-1] + } + + return nil +} + +// Next returns the next episode, if available. +func (episode *Episode) Next() *Episode { + episodes := episode.Anime().Episodes() + _, index := episodes.Find(episode.Number) + + if index != -1 && index+1 < len(episodes) { + return episodes[index+1] + } + + return nil +} + +// Merge combines the data of both episodes to one. +func (episode *Episode) Merge(b *Episode) { + if b == nil { + return + } + + episode.Number = b.Number + + // Titles + if b.Title.Romaji != "" { + episode.Title.Romaji = b.Title.Romaji + } + + if b.Title.English != "" { + episode.Title.English = b.Title.English + } + + if b.Title.Japanese != "" { + episode.Title.Japanese = b.Title.Japanese + } + + // Airing date + if validate.DateTime(b.AiringDate.Start) { + episode.AiringDate.Start = b.AiringDate.Start + } + + if validate.DateTime(b.AiringDate.End) { + episode.AiringDate.End = b.AiringDate.End + } + + // Links + if episode.Links == nil { + episode.Links = map[string]string{} + } + + for name, link := range b.Links { + episode.Links[name] = link + } +} + +// String implements the default string serialization. +func (episode *Episode) String() string { + return fmt.Sprintf("%s ep. %d", episode.Anime().TitleByUser(nil), episode.Number) +} + +// StreamEpisodes returns a stream of all episodes. +func StreamEpisodes() <-chan *Episode { + channel := make(chan *Episode, nano.ChannelBufferSize) + + go func() { + for obj := range DB.All("Episode") { + channel <- obj.(*Episode) + } + + close(channel) + }() + + return channel +} + +// GetEpisode returns the episode with the given ID. +func GetEpisode(id string) (*Episode, error) { + obj, err := DB.Get("Episode", id) + + if err != nil { + return nil, err + } + + return obj.(*Episode), nil +} diff --git a/arn/EpisodeAPI.go b/arn/EpisodeAPI.go new file mode 100644 index 00000000..9068efad --- /dev/null +++ b/arn/EpisodeAPI.go @@ -0,0 +1,54 @@ +package arn + +import ( + "errors" + "fmt" + "reflect" + + "github.com/aerogo/aero" + "github.com/aerogo/api" +) + +// Force interface implementations +var ( + _ fmt.Stringer = (*Episode)(nil) + _ api.Editable = (*Episode)(nil) + _ api.ArrayEventListener = (*Episode)(nil) +) + +// Authorize returns an error if the given API POST request is not authorized. +func (episode *Episode) Authorize(ctx aero.Context, action string) error { + user := GetUserFromContext(ctx) + + if user == nil || (user.Role != "editor" && user.Role != "admin") { + return errors.New("Not logged in or not authorized to edit") + } + + return nil +} + +// Edit creates an edit log entry. +func (episode *Episode) Edit(ctx aero.Context, key string, value reflect.Value, newValue reflect.Value) (consumed bool, err error) { + return edit(episode, ctx, key, value, newValue) +} + +// OnAppend saves a log entry. +func (episode *Episode) OnAppend(ctx aero.Context, key string, index int, obj interface{}) { + onAppend(episode, ctx, key, index, obj) +} + +// OnRemove saves a log entry. +func (episode *Episode) OnRemove(ctx aero.Context, key string, index int, obj interface{}) { + onRemove(episode, ctx, key, index, obj) +} + +// Save saves the episode in the database. +func (episode *Episode) Save() { + DB.Set("Episode", episode.ID, episode) +} + +// Delete deletes the episode list from the database. +func (episode *Episode) Delete() error { + DB.Delete("Episode", episode.AnimeID) + return nil +} diff --git a/arn/EpisodeList.go b/arn/EpisodeList.go new file mode 100644 index 00000000..d0d49381 --- /dev/null +++ b/arn/EpisodeList.go @@ -0,0 +1,68 @@ +package arn + +import ( + "sort" + "strconv" + "strings" +) + +// EpisodeList is a list of episodes. +type EpisodeList []*Episode + +// Sort sorts the episodes by episode number. +func (episodes EpisodeList) Sort() { + sort.Slice(episodes, func(i, j int) bool { + return episodes[i].Number < episodes[j].Number + }) +} + +// Find finds the given episode number. +func (episodes EpisodeList) Find(episodeNumber int) (*Episode, int) { + for index, episode := range episodes { + if episode.Number == episodeNumber { + return episode, index + } + } + + return nil, -1 +} + +// Merge combines the data of both episode lists to one. +func (episodes EpisodeList) Merge(b EpisodeList) { + for index, episode := range b { + if index >= len(episodes) { + episodes = append(episodes, episode) + } else { + episodes[index].Merge(episode) + } + } +} + +// HumanReadable returns a text representation of the anime episodes. +func (episodes EpisodeList) HumanReadable() string { + b := strings.Builder{} + + for _, episode := range episodes { + b.WriteString(strconv.Itoa(episode.Number)) + b.WriteString(" | ") + b.WriteString(episode.Title.Japanese) + b.WriteString(" | ") + b.WriteString(episode.AiringDate.StartDateHuman()) + b.WriteByte('\n') + } + + return strings.TrimRight(b.String(), "\n") +} + +// AvailableCount counts the number of available episodes. +func (episodes EpisodeList) AvailableCount() int { + available := 0 + + for _, episode := range episodes { + if episode.Available() { + available++ + } + } + + return available +} diff --git a/arn/KitsuAnime.go b/arn/KitsuAnime.go index 86b7a108..ab283ceb 100644 --- a/arn/KitsuAnime.go +++ b/arn/KitsuAnime.go @@ -11,7 +11,7 @@ import ( ) // NewAnimeFromKitsuAnime ... -func NewAnimeFromKitsuAnime(kitsuAnime *kitsu.Anime) (*Anime, *AnimeCharacters, *AnimeRelations, *AnimeEpisodes) { +func NewAnimeFromKitsuAnime(kitsuAnime *kitsu.Anime) (*Anime, *AnimeCharacters, *AnimeRelations) { anime := NewAnime() attr := kitsuAnime.Attributes @@ -82,16 +82,6 @@ func NewAnimeFromKitsuAnime(kitsuAnime *kitsu.Anime) (*Anime, *AnimeCharacters, } } - // Episodes - episodes, _ := GetAnimeEpisodes(anime.ID) - - if episodes == nil { - episodes = &AnimeEpisodes{ - AnimeID: anime.ID, - Items: []*AnimeEpisode{}, - } - } - // Relations relations, _ := GetAnimeRelations(anime.ID) @@ -102,7 +92,7 @@ func NewAnimeFromKitsuAnime(kitsuAnime *kitsu.Anime) (*Anime, *AnimeCharacters, } } - return anime, characters, relations, episodes + return anime, characters, relations } // StreamKitsuAnime returns a stream of all Kitsu anime. diff --git a/arn/UpcomingEpisode.go b/arn/UpcomingEpisode.go index 07b61aef..f0317dd9 100644 --- a/arn/UpcomingEpisode.go +++ b/arn/UpcomingEpisode.go @@ -3,5 +3,5 @@ package arn // UpcomingEpisode is used in the user schedule. type UpcomingEpisode struct { Anime *Anime - Episode *AnimeEpisode + Episode *Episode } diff --git a/jobs/kitsu-import-anime/kitsu-import-anime.go b/jobs/kitsu-import-anime/kitsu-import-anime.go index 8c417293..105e6b3f 100644 --- a/jobs/kitsu-import-anime/kitsu-import-anime.go +++ b/jobs/kitsu-import-anime/kitsu-import-anime.go @@ -154,7 +154,7 @@ func sync(anime *kitsu.Anime) { // if err != nil || episodes == nil { // episodes := &arn.AnimeEpisodes{ // AnimeID: anime.ID, -// Items: []*arn.AnimeEpisode{}, +// Items: []*arn.Episode{}, // } // arn.DB.Set("AnimeEpisodes", anime.ID, episodes) diff --git a/jobs/refresh-episodes/refresh-episodes.go b/jobs/refresh-episodes/refresh-episodes.go index 12c0b313..1ea4ed59 100644 --- a/jobs/refresh-episodes/refresh-episodes.go +++ b/jobs/refresh-episodes/refresh-episodes.go @@ -72,7 +72,7 @@ func refreshQueue(queue []*arn.Anime) { func refresh(anime *arn.Anime) { fmt.Println(anime.ID, "|", anime.Title.Canonical, "|", anime.GetMapping("shoboi/anime")) - episodeCount := len(anime.Episodes().Items) + episodeCount := len(anime.Episodes()) availableEpisodeCount := anime.Episodes().AvailableCount() err := anime.RefreshEpisodes() @@ -87,8 +87,8 @@ func refresh(anime *arn.Anime) { faint := color.New(color.Faint).SprintFunc() episodes := anime.Episodes() - fmt.Println(faint(episodes.ListString())) - fmt.Printf("+%d episodes | +%d available (%d total)\n", len(episodes.Items)-episodeCount, episodes.AvailableCount()-availableEpisodeCount, len(episodes.Items)) + fmt.Println(faint(episodes.HumanReadable())) + fmt.Printf("+%d episodes | +%d available (%d total)\n", len(episodes)-episodeCount, episodes.AvailableCount()-availableEpisodeCount, len(episodes)) println() } } diff --git a/jobs/twist/twist.go b/jobs/twist/twist.go index 87ac4124..0f072df3 100644 --- a/jobs/twist/twist.go +++ b/jobs/twist/twist.go @@ -54,7 +54,7 @@ func main() { } // Ok - color.Green("Found %d episodes for anime %s (Kitsu: %s)", len(anime.Episodes().Items), anime.ID, kitsuID) + color.Green("Found %d episodes for anime %s (Kitsu: %s)", len(anime.Episodes()), anime.ID, kitsuID) // Wait for rate limiter <-rateLimiter.C diff --git a/pages/anime/anime.go b/pages/anime/anime.go index b791711e..a0202f57 100644 --- a/pages/anime/anime.go +++ b/pages/anime/anime.go @@ -37,10 +37,10 @@ func Get(ctx aero.Context) error { } // Episodes - episodes := anime.Episodes().Items + episodes := anime.Episodes() if len(episodes) > maxEpisodes { - episodes = anime.Episodes().Last(maxEpisodesLongSeries) + episodes = episodes[len(episodes)-maxEpisodesLongSeries:] } // Friends watching diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index 6189cddd..8dbfed58 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -1,11 +1,11 @@ -component Anime(anime *arn.Anime, listItem *arn.AnimeListItem, tracks []*arn.SoundTrack, amvs []*arn.AMV, amvAppearances []*arn.AMV, episodes []*arn.AnimeEpisode, friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem, episodeToFriends map[int][]*arn.User, user *arn.User) +component Anime(anime *arn.Anime, listItem *arn.AnimeListItem, tracks []*arn.SoundTrack, amvs []*arn.AMV, amvAppearances []*arn.AMV, episodes []*arn.Episode, friends []*arn.User, listItems map[*arn.User]*arn.AnimeListItem, episodeToFriends map[int][]*arn.User, user *arn.User) .anime .anime-main-column AnimeMainColumn(anime, listItem, tracks, amvs, amvAppearances, episodes, episodeToFriends, user) .anime-side-column AnimeSideColumn(anime, friends, listItems, user) -component AnimeMainColumn(anime *arn.Anime, listItem *arn.AnimeListItem, tracks []*arn.SoundTrack, amvs []*arn.AMV, amvAppearances []*arn.AMV, episodes []*arn.AnimeEpisode, episodeToFriends map[int][]*arn.User, user *arn.User) +component AnimeMainColumn(anime *arn.Anime, listItem *arn.AnimeListItem, tracks []*arn.SoundTrack, amvs []*arn.AMV, amvAppearances []*arn.AMV, episodes []*arn.Episode, episodeToFriends map[int][]*arn.User, user *arn.User) .anime-header(data-id=anime.ID) a.anime-image-container.mountable(href=anime.ImageLink("original"), target="_blank", rel="noopener", data-mountable-type="header") img.anime-cover-image.lazy(data-src=anime.ImageLink("large"), data-webp="true", data-color=anime.AverageColor(), alt=anime.Title.ByUser(user), importance="high") diff --git a/pages/anime/editanime/editanime.go b/pages/anime/editanime/editanime.go index 28a91fca..3cbc1f11 100644 --- a/pages/anime/editanime/editanime.go +++ b/pages/anime/editanime/editanime.go @@ -109,11 +109,12 @@ func Episodes(ctx aero.Context) error { return ctx.Error(http.StatusNotFound, "Anime not found", err) } - animeEpisodes, err := arn.GetAnimeEpisodes(id) + // episodes := anime.Episodes() - if err != nil { - return ctx.Error(http.StatusNotFound, "Anime episodes not found", err) - } + // if err != nil { + // return ctx.Error(http.StatusNotFound, "Anime episodes not found", err) + // } - return ctx.HTML(components.EditAnimeTabs(anime) + editform.Render(animeEpisodes, "Edit anime episodes", user)) + // editform.Render(episodes, "Edit anime episodes", user) + return ctx.HTML(components.EditAnimeTabs(anime) + "

Temporarily disabled.

") } diff --git a/pages/anime/episodes.go b/pages/anime/episodes.go index e64b3e43..edfca4f3 100644 --- a/pages/anime/episodes.go +++ b/pages/anime/episodes.go @@ -38,5 +38,5 @@ func Episodes(ctx aero.Context) error { return ctx.Error(http.StatusNotFound, "Anime not found", err) } - return ctx.HTML(components.AnimeEpisodes(anime, anime.Episodes().Items, episodeToFriends, user, true)) + return ctx.HTML(components.AnimeEpisodes(anime, anime.Episodes(), episodeToFriends, user, true)) } diff --git a/pages/anime/episodes.pixy b/pages/anime/episodes.pixy index b92bd38d..eaa338aa 100644 --- a/pages/anime/episodes.pixy +++ b/pages/anime/episodes.pixy @@ -1,4 +1,4 @@ -component AnimeEpisodes(anime *arn.Anime, episodes []*arn.AnimeEpisode, episodeToFriends map[int][]*arn.User, user *arn.User, standAlonePage bool) +component AnimeEpisodes(anime *arn.Anime, episodes []*arn.Episode, episodeToFriends map[int][]*arn.User, user *arn.User, standAlonePage bool) if standAlonePage h1.mountable a(href=anime.Link())= anime.Title.ByUser(user) @@ -6,11 +6,11 @@ component AnimeEpisodes(anime *arn.Anime, episodes []*arn.AnimeEpisode, episodeT if len(episodes) > 0 .anime-section.mountable h3.anime-section-name - a(href=anime.Episodes().Link()) Episodes + a(href=fmt.Sprintf("/anime/%s/episodes", anime.ID)) Episodes .episodes each episode in episodes - a.episode.mountable(href=anime.Link() + "/episode/" + strconv.Itoa(episode.Number), data-mountable-type="episode", data-available=episode.Available()) + a.episode.mountable(href=episode.Link(), data-mountable-type="episode", data-available=episode.Available()) .episode-number if episode.Number != -1 span= episode.Number diff --git a/pages/animeimport/kitsu.go b/pages/animeimport/kitsu.go index 98eac556..c2814c90 100644 --- a/pages/animeimport/kitsu.go +++ b/pages/animeimport/kitsu.go @@ -31,7 +31,7 @@ func Kitsu(ctx aero.Context) error { kitsuAnime := kitsuAnimeObj.(*kitsu.Anime) // Convert - anime, characters, relations, episodes := arn.NewAnimeFromKitsuAnime(kitsuAnime) + anime, characters, relations := arn.NewAnimeFromKitsuAnime(kitsuAnime) // Add user ID to the anime anime.CreatedBy = user.ID @@ -40,7 +40,6 @@ func Kitsu(ctx aero.Context) error { anime.Save() characters.Save() relations.Save() - episodes.Save() // Log fmt.Println(color.GreenString("✔"), anime.ID, anime.Title.Canonical) diff --git a/pages/calendar/calendar.go b/pages/calendar/calendar.go index 18e68988..8c1b6fdc 100644 --- a/pages/calendar/calendar.go +++ b/pages/calendar/calendar.go @@ -49,45 +49,43 @@ func Get(ctx aero.Context) error { } // Add anime episodes to the days - for animeEpisodes := range arn.StreamAnimeEpisodes() { - if animeEpisodes.Anime().Status == "finished" { + for episode := range arn.StreamEpisodes() { + if episode.Anime().Status == "finished" { continue } - for _, episode := range animeEpisodes.Items { - if !validate.DateTime(episode.AiringDate.Start) { - continue - } - - // Since we validated the date earlier, we can ignore the error value. - airingDate, _ := time.Parse(time.RFC3339, episode.AiringDate.Start) - - // Subtract from the starting date offset. - since := airingDate.Sub(now) - - // Ignore entries in the past and more than 1 week away. - if since < 0 || since >= oneWeek { - continue - } - - dayIndex := int(since / (24 * time.Hour)) - - entry := &utils.CalendarEntry{ - Anime: animeEpisodes.Anime(), - Episode: episode, - Added: false, - } - - if user != nil { - animeListItem := user.AnimeList().Find(entry.Anime.ID) - - if animeListItem != nil && (animeListItem.Status == arn.AnimeListStatusWatching || animeListItem.Status == arn.AnimeListStatusPlanned) { - entry.Added = true - } - } - - days[dayIndex].Entries = append(days[dayIndex].Entries, entry) + if !validate.DateTime(episode.AiringDate.Start) { + continue } + + // Since we validated the date earlier, we can ignore the error value. + airingDate, _ := time.Parse(time.RFC3339, episode.AiringDate.Start) + + // Subtract from the starting date offset. + since := airingDate.Sub(now) + + // Ignore entries in the past and more than 1 week away. + if since < 0 || since >= oneWeek { + continue + } + + dayIndex := int(since / (24 * time.Hour)) + + entry := &utils.CalendarEntry{ + Anime: episode.Anime(), + Episode: episode, + Added: false, + } + + if user != nil { + animeListItem := user.AnimeList().Find(entry.Anime.ID) + + if animeListItem != nil && (animeListItem.Status == arn.AnimeListStatusWatching || animeListItem.Status == arn.AnimeListStatusPlanned) { + entry.Added = true + } + } + + days[dayIndex].Entries = append(days[dayIndex].Entries, entry) } for i := 0; i < 7; i++ { diff --git a/pages/episode/episode.go b/pages/episode/episode.go index bf605f60..03e98053 100644 --- a/pages/episode/episode.go +++ b/pages/episode/episode.go @@ -15,39 +15,34 @@ import ( func Get(ctx aero.Context) error { user := utils.GetUser(ctx) id := ctx.Get("id") - episodeNumber, err := ctx.GetInt("episode-number") + + // Get episode + episode, err := arn.GetEpisode(id) if err != nil { - return ctx.Error(http.StatusBadRequest, "Episode is not a number", err) + return ctx.Error(http.StatusNotFound, "Episode not found", err) } // Get anime - anime, err := arn.GetAnime(id) + anime := episode.Anime() - if err != nil { + if anime == nil { return ctx.Error(http.StatusNotFound, "Anime not found", err) } - // Get anime episodes - animeEpisodes, err := arn.GetAnimeEpisodes(id) - - if err != nil { - return ctx.Error(http.StatusNotFound, "Anime episodes not found", err) - } - // Does the episode exist? uploaded := false if arn.Spaces != nil { - stat, err := arn.Spaces.StatObject("arn", fmt.Sprintf("videos/anime/%s/%d.webm", anime.ID, episodeNumber), minio.StatObjectOptions{}) + stat, err := arn.Spaces.StatObject("arn", fmt.Sprintf("videos/anime/%s/%d.webm", anime.ID, episode.Number), minio.StatObjectOptions{}) uploaded = (err == nil) && (stat.Size > 0) } - episode, episodeIndex := animeEpisodes.Find(episodeNumber) + _, episodeIndex := anime.Episodes().Find(episode.Number) if episode == nil { return ctx.Error(http.StatusNotFound, "Anime episode not found") } - return ctx.HTML(components.AnimeEpisode(anime, episode, episodeIndex, uploaded, user)) + return ctx.HTML(components.Episode(anime, episode, episodeIndex, uploaded, user)) } diff --git a/pages/episode/episode.pixy b/pages/episode/episode.pixy index bafc016f..b139575d 100644 --- a/pages/episode/episode.pixy +++ b/pages/episode/episode.pixy @@ -1,11 +1,11 @@ -component AnimeEpisode(anime *arn.Anime, episode *arn.AnimeEpisode, episodeIndex int, uploaded bool, user *arn.User) +component Episode(anime *arn.Anime, episode *arn.Episode, episodeIndex int, uploaded bool, user *arn.User) h1 a(href=anime.Link())= anime.Title.ByUser(user) .episode-navigation-container if episodeIndex > 0 .episode-arrow.episode-arrow-previous - a.light-button(href=anime.Link() + "/episode/" + strconv.Itoa(anime.Episodes().Items[episodeIndex - 1].Number), title="Previous episode") + a.light-button(href=episode.Previous().Link(), title="Previous episode") RawIcon("chevron-left") .episode-video @@ -22,9 +22,9 @@ component AnimeEpisode(anime *arn.Anime, episode *arn.AnimeEpisode, episodeIndex //- a(href=anime.Link(), title=anime.Title.ByUser(user)) //- img.anime-cover-image.lazy(data-src=anime.ImageLink("large"), data-webp="true", data-color=anime.AverageColor(), alt=anime.Title.ByUser(user)) - if episodeIndex < len(anime.Episodes().Items) - 1 + if episodeIndex < len(anime.Episodes()) - 1 .episode-arrow.episode-arrow-next - a.light-button(href=anime.Link() + "/episode/" + strconv.Itoa(anime.Episodes().Items[episodeIndex + 1].Number), title="Next episode") + a.light-button(href=episode.Next().Link(), title="Next episode") RawIcon("chevron-right") h3.episode-view-number= "Episode " + strconv.Itoa(episode.Number) diff --git a/pages/episode/subtitles.go b/pages/episode/subtitles.go index a81f75e5..53138860 100644 --- a/pages/episode/subtitles.go +++ b/pages/episode/subtitles.go @@ -13,23 +13,25 @@ import ( func Subtitles(ctx aero.Context) error { id := ctx.Get("id") language := ctx.Get("language") - episodeNumber, err := ctx.GetInt("episode-number") + + // Get episode + episode, err := arn.GetEpisode(id) if err != nil { - return ctx.Error(http.StatusBadRequest, "Episode is not a number", err) + return ctx.Error(http.StatusNotFound, "Episode not found", err) } // Get anime - anime, err := arn.GetAnime(id) + anime := episode.Anime() - if err != nil { + if anime == nil { return ctx.Error(http.StatusNotFound, "Anime not found", err) } ctx.Response().SetHeader("Access-Control-Allow-Origin", "*") ctx.Response().SetHeader("Content-Type", "text/vtt; charset=utf-8") - obj, err := arn.Spaces.GetObject("arn", fmt.Sprintf("videos/anime/%s/%d.%s.vtt", anime.ID, episodeNumber, language), minio.GetObjectOptions{}) + obj, err := arn.Spaces.GetObject("arn", fmt.Sprintf("videos/anime/%s/%d.%s.vtt", anime.ID, episode.Number, language), minio.GetObjectOptions{}) if err != nil { return ctx.Error(http.StatusInternalServerError, err) diff --git a/pages/index/animeroutes/animeroutes.go b/pages/index/animeroutes/animeroutes.go index d9b530bc..20d657e3 100644 --- a/pages/index/animeroutes/animeroutes.go +++ b/pages/index/animeroutes/animeroutes.go @@ -19,8 +19,8 @@ func Register(app *aero.Application) { page.Get(app, "/anime/:id/tracks", anime.Tracks) page.Get(app, "/anime/:id/relations", anime.Relations) page.Get(app, "/anime/:id/comments", anime.Comments) - page.Get(app, "/anime/:id/episode/:episode-number", episode.Get) - app.Get("/anime/:id/episode/:episode-number/subtitles/:language", episode.Subtitles) + page.Get(app, "/episode/:id", episode.Get) + app.Get("/episode/:id/subtitles/:language", episode.Subtitles) // Anime redirects page.Get(app, "/kitsu/anime/:id", anime.RedirectByMapping("kitsu/anime")) diff --git a/pages/statistics/anime.go b/pages/statistics/anime.go index 31a47070..7aa98929 100644 --- a/pages/statistics/anime.go +++ b/pages/statistics/anime.go @@ -52,7 +52,7 @@ func getAnimeStats() []*arn.PieChart { rating[fmt.Sprint(int(anime.Rating.Overall+0.5))]++ found := false - for _, episode := range anime.Episodes().Items { + for _, episode := range anime.Episodes() { if episode.Links != nil && episode.Links["twist.moe"] != "" { found = true break diff --git a/patches/anime-episodes-upgrade/anime-episodes-upgrade.go b/patches/anime-episodes-upgrade/anime-episodes-upgrade.go new file mode 100644 index 00000000..7f9a6462 --- /dev/null +++ b/patches/anime-episodes-upgrade/anime-episodes-upgrade.go @@ -0,0 +1,22 @@ +package main + +import "github.com/animenotifier/notify.moe/arn" + +func main() { + defer arn.Node.Close() + + for episodes := range arn.StreamAnimeEpisodes() { + anime := episodes.Anime() + anime.EpisodeIDs = nil + + for _, episode := range episodes.Items { + episode.ID = arn.GenerateID("Episode") + episode.AnimeID = anime.ID + episode.Save() + + anime.EpisodeIDs = append(anime.EpisodeIDs, episode.ID) + } + + anime.Save() + } +} diff --git a/patches/fix-airing-dates/fix-airing-dates.go b/patches/fix-airing-dates/fix-airing-dates.go index 8fa0cec1..cd616bd0 100644 --- a/patches/fix-airing-dates/fix-airing-dates.go +++ b/patches/fix-airing-dates/fix-airing-dates.go @@ -18,7 +18,7 @@ func main() { modified := false // Try to find incorrect airing dates - for _, episode := range anime.Episodes().Items { + for _, episode := range anime.Episodes() { if episode.AiringDate.Start == "" { continue } diff --git a/utils/Calendar.go b/utils/Calendar.go index f9bac0e1..c923316a 100644 --- a/utils/Calendar.go +++ b/utils/Calendar.go @@ -12,6 +12,6 @@ type CalendarDay struct { // CalendarEntry is a calendar entry. type CalendarEntry struct { Anime *arn.Anime - Episode *arn.AnimeEpisode + Episode *arn.Episode Added bool }