Episodes now have their own ID
This commit is contained in:
parent
5551dd176e
commit
3a15829831
@ -144,7 +144,7 @@ func (amv *AMV) MainAnime() *Anime {
|
|||||||
// ExtraAnime returns main anime for the AMV, if available.
|
// ExtraAnime returns main anime for the AMV, if available.
|
||||||
func (amv *AMV) ExtraAnime() []*Anime {
|
func (amv *AMV) ExtraAnime() []*Anime {
|
||||||
objects := DB.GetMany("Anime", amv.ExtraAnimeIDs)
|
objects := DB.GetMany("Anime", amv.ExtraAnimeIDs)
|
||||||
animes := []*Anime{}
|
animes := make([]*Anime, 0, len(amv.ExtraAnimeIDs))
|
||||||
|
|
||||||
for _, obj := range objects {
|
for _, obj := range objects {
|
||||||
if obj == nil {
|
if obj == nil {
|
||||||
|
56
arn/Anime.go
56
arn/Anime.go
@ -83,6 +83,7 @@ type Anime struct {
|
|||||||
Rating *AnimeRating `json:"rating"`
|
Rating *AnimeRating `json:"rating"`
|
||||||
Popularity *AnimePopularity `json:"popularity"`
|
Popularity *AnimePopularity `json:"popularity"`
|
||||||
Trailers []*ExternalMedia `json:"trailers" editable:"true"`
|
Trailers []*ExternalMedia `json:"trailers" editable:"true"`
|
||||||
|
EpisodeIDs []string `json:"episodes" editable:"true"`
|
||||||
|
|
||||||
// Mixins
|
// Mixins
|
||||||
hasMappings
|
hasMappings
|
||||||
@ -356,15 +357,20 @@ func (anime *Anime) EndDateTime() time.Time {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
// Episodes returns the anime episodes wrapper.
|
// Episodes returns the anime episodes.
|
||||||
func (anime *Anime) Episodes() *AnimeEpisodes {
|
func (anime *Anime) Episodes() EpisodeList {
|
||||||
record, err := DB.Get("AnimeEpisodes", anime.ID)
|
objects := DB.GetMany("Episode", anime.EpisodeIDs)
|
||||||
|
episodes := make([]*Episode, 0, len(anime.EpisodeIDs))
|
||||||
|
|
||||||
if err != nil {
|
for _, obj := range objects {
|
||||||
return nil
|
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.
|
// UsersWatchingOrPlanned returns a list of users who are watching the anime right now.
|
||||||
@ -387,13 +393,6 @@ func (anime *Anime) RefreshEpisodes() error {
|
|||||||
// Fetch episodes
|
// Fetch episodes
|
||||||
episodes := anime.Episodes()
|
episodes := anime.Episodes()
|
||||||
|
|
||||||
if episodes == nil {
|
|
||||||
episodes = &AnimeEpisodes{
|
|
||||||
AnimeID: anime.ID,
|
|
||||||
Items: []*AnimeEpisode{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save number of available episodes for comparison later
|
// Save number of available episodes for comparison later
|
||||||
oldAvailableCount := episodes.AvailableCount()
|
oldAvailableCount := episodes.AvailableCount()
|
||||||
|
|
||||||
@ -441,7 +440,7 @@ func (anime *Anime) RefreshEpisodes() error {
|
|||||||
// Number remaining episodes
|
// Number remaining episodes
|
||||||
startNumber := 0
|
startNumber := 0
|
||||||
|
|
||||||
for _, episode := range episodes.Items {
|
for _, episode := range episodes {
|
||||||
if episode.Number != -1 {
|
if episode.Number != -1 {
|
||||||
startNumber = episode.Number
|
startNumber = episode.Number
|
||||||
continue
|
continue
|
||||||
@ -456,7 +455,7 @@ func (anime *Anime) RefreshEpisodes() error {
|
|||||||
lastAiringDate := ""
|
lastAiringDate := ""
|
||||||
timeDifference := oneWeek
|
timeDifference := oneWeek
|
||||||
|
|
||||||
for _, episode := range episodes.Items {
|
for _, episode := range episodes {
|
||||||
if validate.DateTime(episode.AiringDate.Start) {
|
if validate.DateTime(episode.AiringDate.Start) {
|
||||||
if lastAiringDate != "" {
|
if lastAiringDate != "" {
|
||||||
a, _ := time.Parse(time.RFC3339, lastAiringDate)
|
a, _ := time.Parse(time.RFC3339, lastAiringDate)
|
||||||
@ -485,12 +484,20 @@ func (anime *Anime) RefreshEpisodes() error {
|
|||||||
lastAiringDate = episode.AiringDate.Start
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShoboiEpisodes returns a slice of episode info from cal.syoboi.jp.
|
// 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")
|
shoboiID := anime.GetMapping("shoboi/anime")
|
||||||
|
|
||||||
if shoboiID == "" {
|
if shoboiID == "" {
|
||||||
@ -503,7 +510,7 @@ func (anime *Anime) ShoboiEpisodes() ([]*AnimeEpisode, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
arnEpisodes := []*AnimeEpisode{}
|
arnEpisodes := []*Episode{}
|
||||||
shoboiEpisodes := shoboiAnime.Episodes()
|
shoboiEpisodes := shoboiAnime.Episodes()
|
||||||
|
|
||||||
for _, shoboiEpisode := range shoboiEpisodes {
|
for _, shoboiEpisode := range shoboiEpisodes {
|
||||||
@ -529,7 +536,7 @@ func (anime *Anime) ShoboiEpisodes() ([]*AnimeEpisode, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TwistEpisodes returns a slice of episode info from twist.moe.
|
// 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")
|
idList, err := GetIDList("animetwist index")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -566,7 +573,7 @@ func (anime *Anime) TwistEpisodes() ([]*AnimeEpisode, error) {
|
|||||||
return episodes[a].Number < episodes[b].Number
|
return episodes[a].Number < episodes[b].Number
|
||||||
})
|
})
|
||||||
|
|
||||||
arnEpisodes := []*AnimeEpisode{}
|
arnEpisodes := []*Episode{}
|
||||||
|
|
||||||
for _, episode := range episodes {
|
for _, episode := range episodes {
|
||||||
arnEpisode := NewAnimeEpisode()
|
arnEpisode := NewAnimeEpisode()
|
||||||
@ -584,10 +591,9 @@ func (anime *Anime) TwistEpisodes() ([]*AnimeEpisode, error) {
|
|||||||
// UpcomingEpisodes ...
|
// UpcomingEpisodes ...
|
||||||
func (anime *Anime) UpcomingEpisodes() []*UpcomingEpisode {
|
func (anime *Anime) UpcomingEpisodes() []*UpcomingEpisode {
|
||||||
var upcomingEpisodes []*UpcomingEpisode
|
var upcomingEpisodes []*UpcomingEpisode
|
||||||
|
|
||||||
now := time.Now().UTC().Format(time.RFC3339)
|
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) {
|
if episode.AiringDate.Start > now && validate.DateTime(episode.AiringDate.Start) {
|
||||||
upcomingEpisodes = append(upcomingEpisodes, &UpcomingEpisode{
|
upcomingEpisodes = append(upcomingEpisodes, &UpcomingEpisode{
|
||||||
Anime: anime,
|
Anime: anime,
|
||||||
@ -603,7 +609,7 @@ func (anime *Anime) UpcomingEpisodes() []*UpcomingEpisode {
|
|||||||
func (anime *Anime) UpcomingEpisode() *UpcomingEpisode {
|
func (anime *Anime) UpcomingEpisode() *UpcomingEpisode {
|
||||||
now := time.Now().UTC().Format(time.RFC3339)
|
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) {
|
if episode.AiringDate.Start > now && validate.DateTime(episode.AiringDate.Start) {
|
||||||
return &UpcomingEpisode{
|
return &UpcomingEpisode{
|
||||||
Anime: anime,
|
Anime: anime,
|
||||||
@ -719,8 +725,8 @@ func (anime *Anime) CalculatedStatus() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EpisodeByNumber returns the episode with the given number.
|
// EpisodeByNumber returns the episode with the given number.
|
||||||
func (anime *Anime) EpisodeByNumber(number int) *AnimeEpisode {
|
func (anime *Anime) EpisodeByNumber(number int) *Episode {
|
||||||
for _, episode := range anime.Episodes().Items {
|
for _, episode := range anime.Episodes() {
|
||||||
if number == episode.Number {
|
if number == episode.Number {
|
||||||
return episode
|
return episode
|
||||||
}
|
}
|
||||||
|
@ -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{},
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -20,7 +20,6 @@ var DB = Node.Namespace("arn").RegisterTypes(
|
|||||||
(*Analytics)(nil),
|
(*Analytics)(nil),
|
||||||
(*Anime)(nil),
|
(*Anime)(nil),
|
||||||
(*AnimeCharacters)(nil),
|
(*AnimeCharacters)(nil),
|
||||||
(*AnimeEpisodes)(nil),
|
|
||||||
(*AnimeRelations)(nil),
|
(*AnimeRelations)(nil),
|
||||||
(*AnimeList)(nil),
|
(*AnimeList)(nil),
|
||||||
(*Character)(nil),
|
(*Character)(nil),
|
||||||
@ -29,6 +28,7 @@ var DB = Node.Namespace("arn").RegisterTypes(
|
|||||||
(*DraftIndex)(nil),
|
(*DraftIndex)(nil),
|
||||||
(*EditLogEntry)(nil),
|
(*EditLogEntry)(nil),
|
||||||
(*EmailToUser)(nil),
|
(*EmailToUser)(nil),
|
||||||
|
(*Episode)(nil),
|
||||||
(*FacebookToUser)(nil),
|
(*FacebookToUser)(nil),
|
||||||
(*GoogleToUser)(nil),
|
(*GoogleToUser)(nil),
|
||||||
(*Group)(nil),
|
(*Group)(nil),
|
||||||
|
164
arn/Episode.go
Normal file
164
arn/Episode.go
Normal 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
54
arn/EpisodeAPI.go
Normal 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
68
arn/EpisodeList.go
Normal 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
|
||||||
|
}
|
@ -11,7 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewAnimeFromKitsuAnime ...
|
// NewAnimeFromKitsuAnime ...
|
||||||
func NewAnimeFromKitsuAnime(kitsuAnime *kitsu.Anime) (*Anime, *AnimeCharacters, *AnimeRelations, *AnimeEpisodes) {
|
func NewAnimeFromKitsuAnime(kitsuAnime *kitsu.Anime) (*Anime, *AnimeCharacters, *AnimeRelations) {
|
||||||
anime := NewAnime()
|
anime := NewAnime()
|
||||||
attr := kitsuAnime.Attributes
|
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
|
||||||
relations, _ := GetAnimeRelations(anime.ID)
|
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.
|
// StreamKitsuAnime returns a stream of all Kitsu anime.
|
||||||
|
@ -3,5 +3,5 @@ package arn
|
|||||||
// UpcomingEpisode is used in the user schedule.
|
// UpcomingEpisode is used in the user schedule.
|
||||||
type UpcomingEpisode struct {
|
type UpcomingEpisode struct {
|
||||||
Anime *Anime
|
Anime *Anime
|
||||||
Episode *AnimeEpisode
|
Episode *Episode
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,7 @@ func sync(anime *kitsu.Anime) {
|
|||||||
// if err != nil || episodes == nil {
|
// if err != nil || episodes == nil {
|
||||||
// episodes := &arn.AnimeEpisodes{
|
// episodes := &arn.AnimeEpisodes{
|
||||||
// AnimeID: anime.ID,
|
// AnimeID: anime.ID,
|
||||||
// Items: []*arn.AnimeEpisode{},
|
// Items: []*arn.Episode{},
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// arn.DB.Set("AnimeEpisodes", anime.ID, episodes)
|
// arn.DB.Set("AnimeEpisodes", anime.ID, episodes)
|
||||||
|
@ -72,7 +72,7 @@ func refreshQueue(queue []*arn.Anime) {
|
|||||||
func refresh(anime *arn.Anime) {
|
func refresh(anime *arn.Anime) {
|
||||||
fmt.Println(anime.ID, "|", anime.Title.Canonical, "|", anime.GetMapping("shoboi/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()
|
availableEpisodeCount := anime.Episodes().AvailableCount()
|
||||||
|
|
||||||
err := anime.RefreshEpisodes()
|
err := anime.RefreshEpisodes()
|
||||||
@ -87,8 +87,8 @@ func refresh(anime *arn.Anime) {
|
|||||||
faint := color.New(color.Faint).SprintFunc()
|
faint := color.New(color.Faint).SprintFunc()
|
||||||
episodes := anime.Episodes()
|
episodes := anime.Episodes()
|
||||||
|
|
||||||
fmt.Println(faint(episodes.ListString()))
|
fmt.Println(faint(episodes.HumanReadable()))
|
||||||
fmt.Printf("+%d episodes | +%d available (%d total)\n", len(episodes.Items)-episodeCount, episodes.AvailableCount()-availableEpisodeCount, len(episodes.Items))
|
fmt.Printf("+%d episodes | +%d available (%d total)\n", len(episodes)-episodeCount, episodes.AvailableCount()-availableEpisodeCount, len(episodes))
|
||||||
println()
|
println()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ok
|
// 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
|
// Wait for rate limiter
|
||||||
<-rateLimiter.C
|
<-rateLimiter.C
|
||||||
|
@ -37,10 +37,10 @@ func Get(ctx aero.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Episodes
|
// Episodes
|
||||||
episodes := anime.Episodes().Items
|
episodes := anime.Episodes()
|
||||||
|
|
||||||
if len(episodes) > maxEpisodes {
|
if len(episodes) > maxEpisodes {
|
||||||
episodes = anime.Episodes().Last(maxEpisodesLongSeries)
|
episodes = episodes[len(episodes)-maxEpisodesLongSeries:]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Friends watching
|
// Friends watching
|
||||||
|
@ -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
|
||||||
.anime-main-column
|
.anime-main-column
|
||||||
AnimeMainColumn(anime, listItem, tracks, amvs, amvAppearances, episodes, episodeToFriends, user)
|
AnimeMainColumn(anime, listItem, tracks, amvs, amvAppearances, episodes, episodeToFriends, user)
|
||||||
.anime-side-column
|
.anime-side-column
|
||||||
AnimeSideColumn(anime, friends, listItems, user)
|
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)
|
.anime-header(data-id=anime.ID)
|
||||||
a.anime-image-container.mountable(href=anime.ImageLink("original"), target="_blank", rel="noopener", data-mountable-type="header")
|
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")
|
img.anime-cover-image.lazy(data-src=anime.ImageLink("large"), data-webp="true", data-color=anime.AverageColor(), alt=anime.Title.ByUser(user), importance="high")
|
||||||
|
@ -109,11 +109,12 @@ func Episodes(ctx aero.Context) error {
|
|||||||
return ctx.Error(http.StatusNotFound, "Anime not found", err)
|
return ctx.Error(http.StatusNotFound, "Anime not found", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
animeEpisodes, err := arn.GetAnimeEpisodes(id)
|
// episodes := anime.Episodes()
|
||||||
|
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return ctx.Error(http.StatusNotFound, "Anime episodes not found", err)
|
// 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>")
|
||||||
}
|
}
|
||||||
|
@ -38,5 +38,5 @@ func Episodes(ctx aero.Context) error {
|
|||||||
return ctx.Error(http.StatusNotFound, "Anime not found", err)
|
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))
|
||||||
}
|
}
|
||||||
|
@ -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
|
if standAlonePage
|
||||||
h1.mountable
|
h1.mountable
|
||||||
a(href=anime.Link())= anime.Title.ByUser(user)
|
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
|
if len(episodes) > 0
|
||||||
.anime-section.mountable
|
.anime-section.mountable
|
||||||
h3.anime-section-name
|
h3.anime-section-name
|
||||||
a(href=anime.Episodes().Link()) Episodes
|
a(href=fmt.Sprintf("/anime/%s/episodes", anime.ID)) Episodes
|
||||||
|
|
||||||
.episodes
|
.episodes
|
||||||
each episode in 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
|
.episode-number
|
||||||
if episode.Number != -1
|
if episode.Number != -1
|
||||||
span= episode.Number
|
span= episode.Number
|
||||||
|
@ -31,7 +31,7 @@ func Kitsu(ctx aero.Context) error {
|
|||||||
kitsuAnime := kitsuAnimeObj.(*kitsu.Anime)
|
kitsuAnime := kitsuAnimeObj.(*kitsu.Anime)
|
||||||
|
|
||||||
// Convert
|
// Convert
|
||||||
anime, characters, relations, episodes := arn.NewAnimeFromKitsuAnime(kitsuAnime)
|
anime, characters, relations := arn.NewAnimeFromKitsuAnime(kitsuAnime)
|
||||||
|
|
||||||
// Add user ID to the anime
|
// Add user ID to the anime
|
||||||
anime.CreatedBy = user.ID
|
anime.CreatedBy = user.ID
|
||||||
@ -40,7 +40,6 @@ func Kitsu(ctx aero.Context) error {
|
|||||||
anime.Save()
|
anime.Save()
|
||||||
characters.Save()
|
characters.Save()
|
||||||
relations.Save()
|
relations.Save()
|
||||||
episodes.Save()
|
|
||||||
|
|
||||||
// Log
|
// Log
|
||||||
fmt.Println(color.GreenString("✔"), anime.ID, anime.Title.Canonical)
|
fmt.Println(color.GreenString("✔"), anime.ID, anime.Title.Canonical)
|
||||||
|
@ -49,45 +49,43 @@ func Get(ctx aero.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add anime episodes to the days
|
// Add anime episodes to the days
|
||||||
for animeEpisodes := range arn.StreamAnimeEpisodes() {
|
for episode := range arn.StreamEpisodes() {
|
||||||
if animeEpisodes.Anime().Status == "finished" {
|
if episode.Anime().Status == "finished" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, episode := range animeEpisodes.Items {
|
if !validate.DateTime(episode.AiringDate.Start) {
|
||||||
if !validate.DateTime(episode.AiringDate.Start) {
|
continue
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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++ {
|
for i := 0; i < 7; i++ {
|
||||||
|
@ -15,39 +15,34 @@ import (
|
|||||||
func Get(ctx aero.Context) error {
|
func Get(ctx aero.Context) error {
|
||||||
user := utils.GetUser(ctx)
|
user := utils.GetUser(ctx)
|
||||||
id := ctx.Get("id")
|
id := ctx.Get("id")
|
||||||
episodeNumber, err := ctx.GetInt("episode-number")
|
|
||||||
|
// Get episode
|
||||||
|
episode, err := arn.GetEpisode(id)
|
||||||
|
|
||||||
if err != nil {
|
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
|
// 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)
|
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?
|
// Does the episode exist?
|
||||||
uploaded := false
|
uploaded := false
|
||||||
|
|
||||||
if arn.Spaces != nil {
|
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)
|
uploaded = (err == nil) && (stat.Size > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
episode, episodeIndex := animeEpisodes.Find(episodeNumber)
|
_, episodeIndex := anime.Episodes().Find(episode.Number)
|
||||||
|
|
||||||
if episode == nil {
|
if episode == nil {
|
||||||
return ctx.Error(http.StatusNotFound, "Anime episode not found")
|
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))
|
||||||
}
|
}
|
||||||
|
@ -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
|
h1
|
||||||
a(href=anime.Link())= anime.Title.ByUser(user)
|
a(href=anime.Link())= anime.Title.ByUser(user)
|
||||||
|
|
||||||
.episode-navigation-container
|
.episode-navigation-container
|
||||||
if episodeIndex > 0
|
if episodeIndex > 0
|
||||||
.episode-arrow.episode-arrow-previous
|
.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")
|
RawIcon("chevron-left")
|
||||||
|
|
||||||
.episode-video
|
.episode-video
|
||||||
@ -22,9 +22,9 @@ component AnimeEpisode(anime *arn.Anime, episode *arn.AnimeEpisode, episodeIndex
|
|||||||
//- a(href=anime.Link(), title=anime.Title.ByUser(user))
|
//- 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))
|
//- 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
|
.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")
|
RawIcon("chevron-right")
|
||||||
|
|
||||||
h3.episode-view-number= "Episode " + strconv.Itoa(episode.Number)
|
h3.episode-view-number= "Episode " + strconv.Itoa(episode.Number)
|
||||||
|
@ -13,23 +13,25 @@ import (
|
|||||||
func Subtitles(ctx aero.Context) error {
|
func Subtitles(ctx aero.Context) error {
|
||||||
id := ctx.Get("id")
|
id := ctx.Get("id")
|
||||||
language := ctx.Get("language")
|
language := ctx.Get("language")
|
||||||
episodeNumber, err := ctx.GetInt("episode-number")
|
|
||||||
|
// Get episode
|
||||||
|
episode, err := arn.GetEpisode(id)
|
||||||
|
|
||||||
if err != nil {
|
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
|
// 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)
|
return ctx.Error(http.StatusNotFound, "Anime not found", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Response().SetHeader("Access-Control-Allow-Origin", "*")
|
ctx.Response().SetHeader("Access-Control-Allow-Origin", "*")
|
||||||
ctx.Response().SetHeader("Content-Type", "text/vtt; charset=utf-8")
|
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 {
|
if err != nil {
|
||||||
return ctx.Error(http.StatusInternalServerError, err)
|
return ctx.Error(http.StatusInternalServerError, err)
|
||||||
|
@ -19,8 +19,8 @@ func Register(app *aero.Application) {
|
|||||||
page.Get(app, "/anime/:id/tracks", anime.Tracks)
|
page.Get(app, "/anime/:id/tracks", anime.Tracks)
|
||||||
page.Get(app, "/anime/:id/relations", anime.Relations)
|
page.Get(app, "/anime/:id/relations", anime.Relations)
|
||||||
page.Get(app, "/anime/:id/comments", anime.Comments)
|
page.Get(app, "/anime/:id/comments", anime.Comments)
|
||||||
page.Get(app, "/anime/:id/episode/:episode-number", episode.Get)
|
page.Get(app, "/episode/:id", episode.Get)
|
||||||
app.Get("/anime/:id/episode/:episode-number/subtitles/:language", episode.Subtitles)
|
app.Get("/episode/:id/subtitles/:language", episode.Subtitles)
|
||||||
|
|
||||||
// Anime redirects
|
// Anime redirects
|
||||||
page.Get(app, "/kitsu/anime/:id", anime.RedirectByMapping("kitsu/anime"))
|
page.Get(app, "/kitsu/anime/:id", anime.RedirectByMapping("kitsu/anime"))
|
||||||
|
@ -52,7 +52,7 @@ func getAnimeStats() []*arn.PieChart {
|
|||||||
rating[fmt.Sprint(int(anime.Rating.Overall+0.5))]++
|
rating[fmt.Sprint(int(anime.Rating.Overall+0.5))]++
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
for _, episode := range anime.Episodes().Items {
|
for _, episode := range anime.Episodes() {
|
||||||
if episode.Links != nil && episode.Links["twist.moe"] != "" {
|
if episode.Links != nil && episode.Links["twist.moe"] != "" {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
|
22
patches/anime-episodes-upgrade/anime-episodes-upgrade.go
Normal file
22
patches/anime-episodes-upgrade/anime-episodes-upgrade.go
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@ -18,7 +18,7 @@ func main() {
|
|||||||
modified := false
|
modified := false
|
||||||
|
|
||||||
// Try to find incorrect airing dates
|
// Try to find incorrect airing dates
|
||||||
for _, episode := range anime.Episodes().Items {
|
for _, episode := range anime.Episodes() {
|
||||||
if episode.AiringDate.Start == "" {
|
if episode.AiringDate.Start == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,6 @@ type CalendarDay struct {
|
|||||||
// CalendarEntry is a calendar entry.
|
// CalendarEntry is a calendar entry.
|
||||||
type CalendarEntry struct {
|
type CalendarEntry struct {
|
||||||
Anime *arn.Anime
|
Anime *arn.Anime
|
||||||
Episode *arn.AnimeEpisode
|
Episode *arn.Episode
|
||||||
Added bool
|
Added bool
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user