crunchy-cli/utils/structure.go

668 lines
18 KiB
Go

package utils
import (
"errors"
"github.com/ByteDream/crunchyroll-go"
"sync"
)
// StructureError is the error type which is thrown whenever a structure fails
// to receive information (formats, episodes, ...) from the api endpoint
type StructureError struct {
error
}
func IsStructureError(err error) (ok bool) {
if err != nil {
_, ok = err.(*StructureError)
}
return
}
// FormatStructure is the basic structure which every other structure implements.
// With it, and all other structures the api usage can be simplified magnificent
type FormatStructure struct {
// initState is true if every format, stream, ... in the structure tree is initialized
initState bool
// getFunc specified the function which will be called if crunchyroll.Format is empty / not initialized yet.
// It returns the formats itself, the parent streams (might be nil) and an error if one occurs
getFunc func() ([]*crunchyroll.Format, []*crunchyroll.Stream, error)
// formats holds all formats which were given
formats []*crunchyroll.Format
// parents holds all parents which were given
parents []*crunchyroll.Stream
}
func newFormatStructure(parentStructure *StreamStructure) *FormatStructure {
return &FormatStructure{
getFunc: func() (formats []*crunchyroll.Format, parents []*crunchyroll.Stream, err error) {
streams, err := parentStructure.Streams()
if err != nil {
return
}
var wg sync.WaitGroup
var lock sync.Mutex
for _, stream := range streams {
wg.Add(1)
stream := stream
go func() {
defer wg.Done()
f, err := stream.Formats()
if err != nil {
errors.As(err, &StructureError{})
return
}
lock.Lock()
defer lock.Unlock()
for _, format := range f {
formats = append(formats, format)
parents = append(parents, stream)
}
}()
}
wg.Wait()
return
},
}
}
// NewFormatStructure returns a new FormatStructure, based on the given formats
func NewFormatStructure(formats []*crunchyroll.Format) *FormatStructure {
return &FormatStructure{
getFunc: func() ([]*crunchyroll.Format, []*crunchyroll.Stream, error) {
return formats, nil, nil
},
}
}
// Formats returns all stored formats
func (fs *FormatStructure) Formats() ([]*crunchyroll.Format, error) {
var err error
if fs.formats == nil {
if fs.formats, fs.parents, err = fs.getFunc(); err != nil {
return nil, err
}
fs.initState = true
}
return fs.formats, nil
}
// FormatParent returns the parent stream of a format (if present).
// If the format or parent is not stored, an error will be returned
func (fs *FormatStructure) FormatParent(format *crunchyroll.Format) (*crunchyroll.Stream, error) {
formats, err := fs.Formats()
if err != nil {
return nil, err
}
if fs.parents == nil {
return nil, errors.New("no parents are given")
}
for i, f := range formats {
if f == format {
return fs.parents[i], nil
}
}
return nil, errors.New("given format could not be found")
}
// InitAll recursive requests all given information.
// All functions of FormatStructure or other structs in this file which are executed after this have a much lesser chance to return any error,
// so the error return value of these functions can be pretty safely ignored.
// This function should only be called if you need to the access to any function of FormatStructure which returns a crunchyroll.Format (or an array of it).
// Re-calling this method can lead to heavy problems (believe me, it caused a simple bug and i've tried to fix it for several hours).
// Check FormatStructure.InitAllState if you can call this method without causing bugs
func (fs *FormatStructure) InitAll() error {
var err error
if fs.formats, fs.parents, err = fs.getFunc(); err != nil {
return err
}
fs.initState = true
return nil
}
// InitAllState returns FormatStructure.InitAll or FormatStructure.Formats was called.
// If so, all errors which are returned by functions of structs in this file can be safely ignored
func (fs *FormatStructure) InitAllState() bool {
return fs.initState
}
// AvailableLocales returns all available audio, subtitle and hardsub locales for all formats.
// If includeEmpty is given, locales with no value are included too
func (fs *FormatStructure) AvailableLocales(includeEmpty bool) (audioLocales []crunchyroll.LOCALE, subtitleLocales []crunchyroll.LOCALE, hardsubLocales []crunchyroll.LOCALE, err error) {
var formats []*crunchyroll.Format
if formats, err = fs.Formats(); err != nil {
return
}
audioMap := map[crunchyroll.LOCALE]interface{}{}
subtitleMap := map[crunchyroll.LOCALE]interface{}{}
hardsubMap := map[crunchyroll.LOCALE]interface{}{}
for _, format := range formats {
// audio locale should always have a valid locale
if includeEmpty || !includeEmpty && format.AudioLocale != "" {
audioMap[format.AudioLocale] = nil
}
if format.Subtitles != nil {
for _, subtitle := range format.Subtitles {
if subtitle.Locale == "" && !includeEmpty {
continue
}
subtitleMap[subtitle.Locale] = nil
}
}
if includeEmpty || !includeEmpty && format.Hardsub != "" {
hardsubMap[format.Hardsub] = nil
}
}
for k := range audioMap {
audioLocales = append(audioLocales, k)
}
for k := range subtitleMap {
subtitleLocales = append(subtitleLocales, k)
}
for k := range hardsubMap {
hardsubLocales = append(hardsubLocales, k)
}
return
}
// FilterFormatsByAudio returns all formats which have the given locale as their audio locale
func (fs *FormatStructure) FilterFormatsByAudio(locale crunchyroll.LOCALE) (f []*crunchyroll.Format, err error) {
var formats []*crunchyroll.Format
if formats, err = fs.Formats(); err != nil {
return nil, err
}
for _, format := range formats {
if format.AudioLocale == locale {
f = append(f, format)
}
}
return
}
// FilterFormatsBySubtitle returns all formats which have the given locale as their subtitle locale.
// Hardsub indicates if the subtitle should be shown on the video itself
func (fs *FormatStructure) FilterFormatsBySubtitle(locale crunchyroll.LOCALE, hardsub bool) (f []*crunchyroll.Format, err error) {
var formats []*crunchyroll.Format
if formats, err = fs.Formats(); err != nil {
return nil, err
}
for _, format := range formats {
if hardsub && format.Hardsub == locale {
f = append(f, format)
} else if !hardsub && format.Hardsub == "" {
f = append(f, format)
}
}
return
}
// FilterFormatsByLocales returns all formats which have the given locales as their property.
// Hardsub is the same as in FormatStructure.FilterFormatsBySubtitle
func (fs *FormatStructure) FilterFormatsByLocales(audioLocale, subtitleLocale crunchyroll.LOCALE, hardsub bool) ([]*crunchyroll.Format, error) {
var f []*crunchyroll.Format
formats, err := fs.Formats()
if err != nil {
return nil, err
}
for _, format := range formats {
if format.AudioLocale == audioLocale {
if hardsub && format.Hardsub == subtitleLocale {
f = append(f, format)
} else if !hardsub && format.Hardsub == "" {
f = append(f, format)
}
}
}
if len(f) == 0 {
return nil, errors.New("could not find any matching format")
}
return f, nil
}
// OrderFormatsByID loops through all stored formats and returns a 2d slice
// where a row represents an id and the column all formats which have this id
func (fs *FormatStructure) OrderFormatsByID() ([][]*crunchyroll.Format, error) {
formats, err := fs.Formats()
if err != nil {
return nil, err
}
formatsMap := map[string][]*crunchyroll.Format{}
for _, format := range formats {
if _, ok := formatsMap[format.ID]; !ok {
formatsMap[format.ID] = make([]*crunchyroll.Format, 0)
}
formatsMap[format.ID] = append(formatsMap[format.ID], format)
}
var orderedFormats [][]*crunchyroll.Format
for _, v := range formatsMap {
var f []*crunchyroll.Format
for _, format := range v {
f = append(f, format)
}
orderedFormats = append(orderedFormats, f)
}
return orderedFormats, nil
}
// StreamStructure fields are nearly same as FormatStructure
type StreamStructure struct {
*FormatStructure
getFunc func() ([]*crunchyroll.Stream, []crunchyroll.Video, error)
streams []*crunchyroll.Stream
parents []crunchyroll.Video
}
func newStreamStructure(structure VideoStructure) *StreamStructure {
var getFunc func() (streams []*crunchyroll.Stream, parents []crunchyroll.Video, err error)
switch structure.(type) {
case *EpisodeStructure:
episodeStructure := structure.(*EpisodeStructure)
getFunc = func() (streams []*crunchyroll.Stream, parents []crunchyroll.Video, err error) {
episodes, err := episodeStructure.Episodes()
if err != nil {
return
}
var wg sync.WaitGroup
var lock sync.Mutex
for _, episode := range episodes {
wg.Add(1)
episode := episode
go func() {
defer wg.Done()
s, err := episode.Streams()
if err != nil {
errors.As(err, &StructureError{})
return
}
lock.Lock()
defer lock.Unlock()
for _, stream := range s {
streams = append(streams, stream)
parents = append(parents, episode)
}
}()
}
wg.Wait()
return
}
case *MovieListingStructure:
movieListingStructure := structure.(*MovieListingStructure)
getFunc = func() (streams []*crunchyroll.Stream, parents []crunchyroll.Video, err error) {
movieListings, err := movieListingStructure.MovieListings()
if err != nil {
return
}
var wg sync.WaitGroup
var lock sync.Mutex
for _, movieListing := range movieListings {
wg.Add(1)
movieListing := movieListing
go func() {
defer wg.Done()
s, err := movieListing.Streams()
if err != nil {
errors.As(err, &StructureError{})
return
}
lock.Lock()
defer lock.Unlock()
for _, stream := range s {
streams = append(streams, stream)
parents = append(parents, movieListing)
}
}()
}
wg.Wait()
return
}
}
ss := &StreamStructure{
getFunc: getFunc,
}
ss.FormatStructure = newFormatStructure(ss)
return ss
}
// NewStreamStructure returns a new StreamStructure, based on the given formats
func NewStreamStructure(streams []*crunchyroll.Stream) *StreamStructure {
ss := &StreamStructure{
getFunc: func() ([]*crunchyroll.Stream, []crunchyroll.Video, error) {
return streams, nil, nil
},
}
ss.FormatStructure = newFormatStructure(ss)
return ss
}
// Streams returns all stored streams
func (ss *StreamStructure) Streams() ([]*crunchyroll.Stream, error) {
if ss.streams == nil {
var err error
if ss.streams, ss.parents, err = ss.getFunc(); err != nil {
return nil, err
}
}
return ss.streams, nil
}
// StreamParent returns the parent video (type crunchyroll.Series or crunchyroll.Movie) of a stream (if present).
// If the stream or parent is not stored, an error will be returned
func (ss *StreamStructure) StreamParent(stream *crunchyroll.Stream) (crunchyroll.Video, error) {
streams, err := ss.Streams()
if err != nil {
return nil, err
}
if ss.parents == nil {
return nil, errors.New("no parents are given")
}
for i, s := range streams {
if s == stream {
return ss.parents[i], nil
}
}
return nil, errors.New("given stream could not be found")
}
// VideoStructure is an interface which is implemented by EpisodeStructure and MovieListingStructure
type VideoStructure interface{}
// EpisodeStructure fields are nearly same as FormatStructure
type EpisodeStructure struct {
VideoStructure
*StreamStructure
getFunc func() ([]*crunchyroll.Episode, []*crunchyroll.Season, error)
episodes []*crunchyroll.Episode
parents []*crunchyroll.Season
}
func newEpisodeStructure(structure *SeasonStructure) *EpisodeStructure {
es := &EpisodeStructure{
getFunc: func() (episodes []*crunchyroll.Episode, parents []*crunchyroll.Season, err error) {
seasons, err := structure.Seasons()
if err != nil {
return
}
var wg sync.WaitGroup
var lock sync.Mutex
for _, season := range seasons {
wg.Add(1)
season := season
go func() {
defer wg.Done()
e, err := season.Episodes()
if err != nil {
errors.As(err, &StructureError{})
return
}
lock.Lock()
defer lock.Unlock()
for _, episode := range e {
episodes = append(episodes, episode)
parents = append(parents, season)
}
}()
}
wg.Wait()
return
},
}
es.StreamStructure = newStreamStructure(es)
return es
}
// NewEpisodeStructure returns a new EpisodeStructure, based on the given formats
func NewEpisodeStructure(episodes []*crunchyroll.Episode) *EpisodeStructure {
es := &EpisodeStructure{
getFunc: func() ([]*crunchyroll.Episode, []*crunchyroll.Season, error) {
return episodes, nil, nil
},
}
es.StreamStructure = newStreamStructure(es)
return es
}
// Episodes returns all stored episodes
func (es *EpisodeStructure) Episodes() ([]*crunchyroll.Episode, error) {
if es.episodes == nil {
var err error
if es.episodes, es.parents, err = es.getFunc(); err != nil {
return nil, err
}
}
return es.episodes, nil
}
// EpisodeParent returns the parent season of a stream (if present).
// If the stream or parent is not stored, an error will be returned
func (es *EpisodeStructure) EpisodeParent(episode *crunchyroll.Episode) (*crunchyroll.Season, error) {
episodes, err := es.Episodes()
if err != nil {
return nil, err
}
if es.parents == nil {
return nil, errors.New("no parents are given")
}
for i, e := range episodes {
if e == episode {
return es.parents[i], nil
}
}
return nil, errors.New("given episode could not be found")
}
// GetEpisodeByFormat returns the episode to which the given format belongs to.
// If the format or the parent is not stored, an error will be returned
func (es *EpisodeStructure) GetEpisodeByFormat(format *crunchyroll.Format) (*crunchyroll.Episode, error) {
if !es.initState {
if err := es.InitAll(); err != nil {
return nil, err
}
}
formatParent, err := es.FormatParent(format)
if err != nil {
return nil, err
}
streamParent, err := es.StreamParent(formatParent)
if err != nil {
return nil, err
}
episode, ok := streamParent.(*crunchyroll.Episode)
if !ok {
return nil, errors.New("could not find parent episode")
}
return episode, nil
}
// GetEpisodeByURL returns an episode by its url
func (es *EpisodeStructure) GetEpisodeByURL(url string) (*crunchyroll.Episode, error) {
_, title, ok := crunchyroll.MatchEpisode(url)
if !ok {
return nil, errors.New("invalid url")
}
episodes, err := es.Episodes()
if err != nil {
return nil, err
}
for _, episode := range episodes {
if episode.SlugTitle == title {
return episode, nil
}
}
return nil, errors.New("no episode could be found")
}
func (es *EpisodeStructure) OrderEpisodeByID() ([][]*crunchyroll.Episode, error) {
episodes, err := es.Episodes()
if err != nil {
return nil, err
}
episodesMap := map[string][]*crunchyroll.Episode{}
for _, episode := range episodes {
if _, ok := episodesMap[episode.ID]; !ok {
episodesMap[episode.ID] = make([]*crunchyroll.Episode, 0)
}
episodesMap[episode.ID] = append(episodesMap[episode.ID], episode)
}
var orderedEpisodes [][]*crunchyroll.Episode
for _, v := range episodesMap {
orderedEpisodes = append(orderedEpisodes, v)
}
return orderedEpisodes, nil
}
func (es *EpisodeStructure) OrderFormatsByEpisodeNumber() ([][]*crunchyroll.Format, error) {
formats, err := es.Formats()
if err != nil {
return nil, err
}
formatsMap := map[int][]*crunchyroll.Format{}
for _, format := range formats {
stream, err := es.FormatParent(format)
if err != nil {
return nil, err
}
video, err := es.StreamParent(stream)
if err != nil {
return nil, err
}
episode, ok := video.(*crunchyroll.Episode)
if !ok {
continue
}
if _, ok := formatsMap[episode.EpisodeNumber]; !ok {
formatsMap[episode.EpisodeNumber] = make([]*crunchyroll.Format, 0)
}
formatsMap[episode.EpisodeNumber] = append(formatsMap[episode.EpisodeNumber], format)
}
var highest int
for key := range formatsMap {
if key > highest {
highest = key
}
}
var orderedFormats [][]*crunchyroll.Format
for i := 0; i < highest; i++ {
if formats, ok := formatsMap[i]; ok {
orderedFormats = append(orderedFormats, formats)
} else {
// simply adds nil in case that no episode with number i exists
orderedFormats = append(orderedFormats, nil)
}
}
return orderedFormats, nil
}
// SeasonStructure fields are nearly same as FormatStructure
type SeasonStructure struct {
*EpisodeStructure
getFunc func() ([]*crunchyroll.Season, error)
seasons []*crunchyroll.Season
}
// NewSeasonStructure returns a new SeasonStructure, based on the given formats
func NewSeasonStructure(seasons []*crunchyroll.Season) *SeasonStructure {
ss := &SeasonStructure{
seasons: seasons,
}
ss.EpisodeStructure = newEpisodeStructure(ss)
return ss
}
// Seasons returns all stored seasons
func (ss *SeasonStructure) Seasons() ([]*crunchyroll.Season, error) {
if ss.seasons == nil {
var err error
if ss.seasons, err = ss.getFunc(); err != nil {
return nil, err
}
}
return ss.seasons, nil
}
// MovieListingStructure fields are nearly same as FormatStructure
type MovieListingStructure struct {
VideoStructure
*StreamStructure
getFunc func() ([]*crunchyroll.MovieListing, error)
movieListings []*crunchyroll.MovieListing
}
// NewMovieListingStructure returns a new MovieListingStructure, based on the given formats
func NewMovieListingStructure(movieListings []*crunchyroll.MovieListing) *MovieListingStructure {
ml := &MovieListingStructure{
getFunc: func() ([]*crunchyroll.MovieListing, error) {
return movieListings, nil
},
}
ml.StreamStructure = newStreamStructure(ml)
return ml
}
// MovieListings returns all stored movie listings
func (ml *MovieListingStructure) MovieListings() ([]*crunchyroll.MovieListing, error) {
if ml.movieListings == nil {
var err error
if ml.movieListings, err = ml.getFunc(); err != nil {
return nil, err
}
}
return ml.movieListings, nil
}
// GetMovieListingByFormat returns the movie listing to which the given format belongs to.
// If the format or the parent is not stored, an error will be returned
func (ml *MovieListingStructure) GetMovieListingByFormat(format *crunchyroll.Format) (*crunchyroll.MovieListing, error) {
if !ml.initState {
if err := ml.InitAll(); err != nil {
return nil, err
}
}
formatParent, err := ml.FormatParent(format)
if err != nil {
return nil, err
}
streamParent, err := ml.StreamParent(formatParent)
if err != nil {
return nil, err
}
movieListing, ok := streamParent.(*crunchyroll.MovieListing)
if !ok {
return nil, errors.New("could not find parent movie listing")
}
return movieListing, nil
}