Initial commit

This commit is contained in:
ByteDream 2021-08-10 00:32:17 +02:00
commit 5f1d811c66
23 changed files with 3612 additions and 0 deletions

70
utils/locale.go Normal file
View file

@ -0,0 +1,70 @@
package utils
import (
"github.com/ByteDream/crunchyroll"
)
var AllLocales = []crunchyroll.LOCALE{
crunchyroll.JP,
crunchyroll.US,
crunchyroll.LA,
crunchyroll.ES,
crunchyroll.FR,
crunchyroll.BR,
crunchyroll.IT,
crunchyroll.DE,
crunchyroll.RU,
crunchyroll.ME,
}
// ValidateLocale validates if the given locale actually exist
func ValidateLocale(locale crunchyroll.LOCALE) bool {
for _, l := range AllLocales {
if l == locale {
return true
}
}
return false
}
// LocaleLanguage returns the country by its locale
func LocaleLanguage(locale crunchyroll.LOCALE) string {
switch locale {
case crunchyroll.JP:
return "Japanese"
case crunchyroll.US:
return "English (US)"
case crunchyroll.LA:
return "Spanish (Latin America)"
case crunchyroll.ES:
return "Spanish (Spain)"
case crunchyroll.FR:
return "French"
case crunchyroll.BR:
return "Portuguese (Brazil)"
case crunchyroll.IT:
return "Italian"
case crunchyroll.DE:
return "German"
case crunchyroll.RU:
return "Russian"
case crunchyroll.ME:
return "Arabic"
default:
return ""
}
}
// SubtitleByLocale returns the subtitle of a crunchyroll.Format by its locale.
// Check the second ok return value if the format has this subtitle
func SubtitleByLocale(format *crunchyroll.Format, locale crunchyroll.LOCALE) (subtitle *crunchyroll.Subtitle, ok bool) {
if format.Subtitles == nil {
return
}
for _, sub := range format.Subtitles {
if sub.Locale == locale {
return sub, true
}
}
return
}

54
utils/sort.go Normal file
View file

@ -0,0 +1,54 @@
package utils
import (
"github.com/ByteDream/crunchyroll"
"strconv"
"strings"
)
// MovieListingsByDuration sorts movie listings by their duration
type MovieListingsByDuration []*crunchyroll.MovieListing
func (mlbd MovieListingsByDuration) Len() int {
return len(mlbd)
}
func (mlbd MovieListingsByDuration) Swap(i, j int) {
mlbd[i], mlbd[j] = mlbd[j], mlbd[i]
}
func (mlbd MovieListingsByDuration) Less(i, j int) bool {
return mlbd[i].DurationMS < mlbd[j].DurationMS
}
// EpisodesByDuration episodes by their duration
type EpisodesByDuration []*crunchyroll.Episode
func (ebd EpisodesByDuration) Len() int {
return len(ebd)
}
func (ebd EpisodesByDuration) Swap(i, j int) {
ebd[i], ebd[j] = ebd[j], ebd[i]
}
func (ebd EpisodesByDuration) Less(i, j int) bool {
return ebd[i].DurationMS < ebd[j].DurationMS
}
// FormatsByResolution sort formats after their resolution
type FormatsByResolution []*crunchyroll.Format
func (fbr FormatsByResolution) Len() int {
return len(fbr)
}
func (fbr FormatsByResolution) Swap(i, j int) {
fbr[i], fbr[j] = fbr[j], fbr[i]
}
func (fbr FormatsByResolution) Less(i, j int) bool {
iSplitRes := strings.Split(fbr[i].Video.Resolution, "x")
iResX, _ := strconv.Atoi(iSplitRes[0])
iResY, _ := strconv.Atoi(iSplitRes[1])
jSplitRes := strings.Split(fbr[j].Video.Resolution, "x")
jResX, _ := strconv.Atoi(jSplitRes[0])
jResY, _ := strconv.Atoi(jSplitRes[1])
return iResX+iResY < jResX+jResY
}

626
utils/structure.go Normal file
View file

@ -0,0 +1,626 @@
package utils
import (
"errors"
"github.com/ByteDream/crunchyroll"
"sort"
"sync"
)
// 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 {
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 {
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 {
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 {
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
}
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)
}
keys := make([]int, 0, len(formatsMap))
for k := range formatsMap {
keys = append(keys, k)
}
sort.Ints(keys)
var orderedFormats [][]*crunchyroll.Format
for _, k := range keys {
orderedFormats = append(orderedFormats, formatsMap[k])
}
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
}