Episodes now have their own ID

This commit is contained in:
Eduard Urbach 2019-08-28 17:06:42 +09:00
parent 5551dd176e
commit 3a15829831
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
29 changed files with 422 additions and 412 deletions

View File

@ -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 {

View File

@ -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
}

View File

@ -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{},
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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),

164
arn/Episode.go Normal file
View File

@ -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
}

54
arn/EpisodeAPI.go Normal file
View File

@ -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
}

68
arn/EpisodeList.go Normal file
View File

@ -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
}

View File

@ -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.

View File

@ -3,5 +3,5 @@ package arn
// UpcomingEpisode is used in the user schedule.
type UpcomingEpisode struct {
Anime *Anime
Episode *AnimeEpisode
Episode *Episode
}

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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) + "<p class='no-data mountable'>Temporarily disabled.</p>")
}

View File

@ -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))
}

View File

@ -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

View File

@ -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)

View File

@ -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++ {

View File

@ -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))
}

View File

@ -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)

View File

@ -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)

View File

@ -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"))

View File

@ -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

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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
}