Fix typos & add more comments

This commit is contained in:
bytedream 2022-04-16 00:17:36 +02:00
parent 2e9ce3cf52
commit 3617955bc5
16 changed files with 82 additions and 68 deletions

View file

@ -73,7 +73,7 @@ _Before reading_: Because of the huge functionality not all cases can be covered
### Login ### Login
Before you can do something, you have to login first. Before you can do something, you have to log in first.
This can be performed via crunchyroll account email and password. This can be performed via crunchyroll account email and password.

View file

@ -728,7 +728,7 @@ func (tc *tarCompress) Close() error {
err = tc.dst.Close() err = tc.dst.Close()
if err != nil && err2 != nil { if err != nil && err2 != nil {
// best way to show double errors at once that i've found // best way to show double errors at once that I've found
return fmt.Errorf("%v\n%v", err, err2) return fmt.Errorf("%v\n%v", err, err2)
} else if err == nil && err2 != nil { } else if err == nil && err2 != nil {
err = err2 err = err2
@ -750,11 +750,11 @@ func (tc *tarCompress) NewFile(information formatInformation) (io.WriteCloser, e
ModTime: time.Now(), ModTime: time.Now(),
Mode: 0644, Mode: 0644,
Typeflag: tar.TypeReg, Typeflag: tar.TypeReg,
// fun fact: i did not set the size for quiet some time because i thought that it isn't // fun fact: I did not set the size for quiet some time because I thought that it isn't
// required. well because of this i debugged this part for multiple hours because without // required. well because of this I debugged this part for multiple hours because without
// proper size information only a tiny amount gets copied into the tar (or zip) writer. // proper size information only a tiny amount gets copied into the tar (or zip) writer.
// this is also the reason why the file content is completely copied into a buffer before // this is also the reason why the file content is completely copied into a buffer before
// writing it to the writer. i could bypass this and save some memory but this requires // writing it to the writer. I could bypass this and save some memory but this requires
// some rewriting and im nearly at the (planned) finish for version 2 so nah in the future // some rewriting and im nearly at the (planned) finish for version 2 so nah in the future
// maybe // maybe
Size: int64(buf.Len()), Size: int64(buf.Len()),

View file

@ -403,7 +403,7 @@ func (dp *downloadProgress) update(msg string, permanent bool) {
pre := fmt.Sprintf("%s%s [", dp.Prefix, msg) pre := fmt.Sprintf("%s%s [", dp.Prefix, msg)
post := fmt.Sprintf("]%4d%% %8d/%d", int(percentage), dp.Current, dp.Total) post := fmt.Sprintf("]%4d%% %8d/%d", int(percentage), dp.Current, dp.Total)
// i don't really know why +2 is needed here but without it the Printf below would not print to the line end // I don't really know why +2 is needed here but without it the Printf below would not print to the line end
progressWidth := terminalWidth() - len(pre) - len(post) + 2 progressWidth := terminalWidth() - len(pre) - len(post) + 2
repeatCount := int(percentage / float32(100) * float32(progressWidth)) repeatCount := int(percentage / float32(100) * float32(progressWidth))
// it can be lower than zero when the terminal is very tiny // it can be lower than zero when the terminal is very tiny

View file

@ -13,7 +13,7 @@ import (
"strconv" "strconv"
) )
// LOCALE represents a locale / language // LOCALE represents a locale / language.
type LOCALE string type LOCALE string
const ( const (
@ -31,16 +31,16 @@ const (
) )
type Crunchyroll struct { type Crunchyroll struct {
// Client is the http.Client to perform all requests over // Client is the http.Client to perform all requests over.
Client *http.Client Client *http.Client
// Context can be used to stop requests with Client and is context.Background by default // Context can be used to stop requests with Client and is context.Background by default.
Context context.Context Context context.Context
// Locale specifies in which language all results should be returned / requested // Locale specifies in which language all results should be returned / requested.
Locale LOCALE Locale LOCALE
// SessionID is the crunchyroll session id which was used for authentication // SessionID is the crunchyroll session id which was used for authentication.
SessionID string SessionID string
// Config stores parameters which are needed by some api calls // Config stores parameters which are needed by some api calls.
Config struct { Config struct {
TokenType string TokenType string
AccessToken string AccessToken string
@ -56,11 +56,11 @@ type Crunchyroll struct {
MaturityRating string MaturityRating string
} }
// If cache is true, internal caching is enabled // If cache is true, internal caching is enabled.
cache bool cache bool
} }
// LoginWithCredentials logs in via crunchyroll username or email and password // LoginWithCredentials logs in via crunchyroll username or email and password.
func LoginWithCredentials(user string, password string, locale LOCALE, client *http.Client) (*Crunchyroll, error) { func LoginWithCredentials(user string, password string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
sessionIDEndpoint := fmt.Sprintf("https://api.crunchyroll.com/start_session.0.json?version=1.0&access_token=%s&device_type=%s&device_id=%s", sessionIDEndpoint := fmt.Sprintf("https://api.crunchyroll.com/start_session.0.json?version=1.0&access_token=%s&device_type=%s&device_id=%s",
"LNDJgOit5yaRIWN", "com.crunchyroll.windows.desktop", "Az2srGnChW65fuxYz2Xxl1GcZQgtGgI") "LNDJgOit5yaRIWN", "com.crunchyroll.windows.desktop", "Az2srGnChW65fuxYz2Xxl1GcZQgtGgI")
@ -87,7 +87,7 @@ func LoginWithCredentials(user string, password string, locale LOCALE, client *h
} }
// LoginWithSessionID logs in via a crunchyroll session id. // LoginWithSessionID logs in via a crunchyroll session id.
// Session ids are automatically generated as a cookie when visiting https://www.crunchyroll.com // Session ids are automatically generated as a cookie when visiting https://www.crunchyroll.com.
func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*Crunchyroll, error) { func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
crunchy := &Crunchyroll{ crunchy := &Crunchyroll{
Client: client, Client: client,
@ -205,7 +205,7 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
return crunchy, nil return crunchy, nil
} }
// request is a base function which handles api requests // request is a base function which handles api requests.
func (c *Crunchyroll) request(endpoint string) (*http.Response, error) { func (c *Crunchyroll) request(endpoint string) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, endpoint, nil) req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil { if err != nil {
@ -241,7 +241,7 @@ func (c *Crunchyroll) request(endpoint string) (*http.Response, error) {
} }
// IsCaching returns if data gets cached or not. // IsCaching returns if data gets cached or not.
// See SetCaching for more information // See SetCaching for more information.
func (c *Crunchyroll) IsCaching() bool { func (c *Crunchyroll) IsCaching() bool {
return c.cache return c.cache
} }
@ -249,12 +249,12 @@ func (c *Crunchyroll) IsCaching() bool {
// SetCaching enables or disables internal caching of requests made. // SetCaching enables or disables internal caching of requests made.
// Caching is enabled by default. // Caching is enabled by default.
// If it is disabled the already cached data still gets called. // If it is disabled the already cached data still gets called.
// The best way to prevent this is to create a complete new Crunchyroll struct // The best way to prevent this is to create a complete new Crunchyroll struct.
func (c *Crunchyroll) SetCaching(caching bool) { func (c *Crunchyroll) SetCaching(caching bool) {
c.cache = caching c.cache = caching
} }
// Search searches a query and returns all found series and movies within the given limit // Search searches a query and returns all found series and movies within the given limit.
func (c *Crunchyroll) Search(query string, limit uint) (s []*Series, m []*Movie, err error) { func (c *Crunchyroll) Search(query string, limit uint) (s []*Series, m []*Movie, err error) {
searchEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/search?q=%s&n=%d&type=&locale=%s", searchEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/search?q=%s&n=%d&type=&locale=%s",
query, limit, c.Locale) query, limit, c.Locale)
@ -397,7 +397,7 @@ func ParseEpisodeURL(url string) (seriesName, title string, episodeNumber int, w
return return
} }
// ParseBetaSeriesURL tries to extract the season id of the given crunchyroll beta url, pointing to a season // ParseBetaSeriesURL tries to extract the season id of the given crunchyroll beta url, pointing to a season.
func ParseBetaSeriesURL(url string) (seasonId string, ok bool) { func ParseBetaSeriesURL(url string) (seasonId string, ok bool) {
pattern := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com/(\w{2}/)?series/(?P<seasonId>\w+).*`) pattern := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com/(\w{2}/)?series/(?P<seasonId>\w+).*`)
if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 { if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
@ -408,7 +408,7 @@ func ParseBetaSeriesURL(url string) (seasonId string, ok bool) {
return return
} }
// ParseBetaEpisodeURL tries to extract the episode id of the given crunchyroll beta url, pointing to an episode // ParseBetaEpisodeURL tries to extract the episode id of the given crunchyroll beta url, pointing to an episode.
func ParseBetaEpisodeURL(url string) (episodeId string, ok bool) { func ParseBetaEpisodeURL(url string) (episodeId string, ok bool) {
pattern := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com/(\w{2}/)?watch/(?P<episodeId>\w+).*`) pattern := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com/(\w{2}/)?watch/(?P<episodeId>\w+).*`)
if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 { if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {

View file

@ -20,7 +20,7 @@ import (
) )
// NewDownloader creates a downloader with default settings which should // NewDownloader creates a downloader with default settings which should
// fit the most needs // fit the most needs.
func NewDownloader(context context.Context, writer io.Writer, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) Downloader { func NewDownloader(context context.Context, writer io.Writer, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) Downloader {
tmp, _ := os.MkdirTemp("", "crunchy_") tmp, _ := os.MkdirTemp("", "crunchy_")
@ -43,12 +43,12 @@ type Downloader struct {
// The files will be placed directly into the root of the directory. // The files will be placed directly into the root of the directory.
// If empty a random temporary directory on the system's default tempdir // If empty a random temporary directory on the system's default tempdir
// will be created. // will be created.
// If the directory does not exist, it will be created // If the directory does not exist, it will be created.
TempDir string TempDir string
// If DeleteTempAfter is true, the temp directory gets deleted afterwards. // If DeleteTempAfter is true, the temp directory gets deleted afterwards.
// Note that in case of a hard signal exit (os.Interrupt, ...) the directory // Note that in case of a hard signal exit (os.Interrupt, ...) the directory
// will NOT be deleted. In such situations try to catch the signal and // will NOT be deleted. In such situations try to catch the signal and
// cancel Context // cancel Context.
DeleteTempAfter bool DeleteTempAfter bool
// Context to control the download process with. // Context to control the download process with.
@ -56,7 +56,7 @@ type Downloader struct {
// process. So it is not recommend stopping the program immediately after calling // process. So it is not recommend stopping the program immediately after calling
// the cancel function. It's better when canceling it and then exit the program // the cancel function. It's better when canceling it and then exit the program
// when Format.Download throws an error. See the signal handling section in // when Format.Download throws an error. See the signal handling section in
// cmd/crunchyroll-go/cmd/download.go for an example // cmd/crunchyroll-go/cmd/download.go for an example.
Context context.Context Context context.Context
// Goroutines is the number of goroutines to download segments with // Goroutines is the number of goroutines to download segments with
@ -65,11 +65,11 @@ type Downloader struct {
// A method to call when a segment was downloaded. // A method to call when a segment was downloaded.
// Note that the segments are downloaded asynchronously (depending on the count of // Note that the segments are downloaded asynchronously (depending on the count of
// Goroutines) and the function gets called asynchronously too, so for example it is // Goroutines) and the function gets called asynchronously too, so for example it is
// first called on segment 1, then segment 254, then segment 3 and so on // first called on segment 1, then segment 254, then segment 3 and so on.
OnSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error OnSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error
// If LockOnSegmentDownload is true, only one OnSegmentDownload function can be called at // If LockOnSegmentDownload is true, only one OnSegmentDownload function can be called at
// once. Normally (because of the use of goroutines while downloading) multiple could get // once. Normally (because of the use of goroutines while downloading) multiple could get
// called simultaneously // called simultaneously.
LockOnSegmentDownload bool LockOnSegmentDownload bool
// If FFmpegOpts is not nil, ffmpeg will be used to merge and convert files. // If FFmpegOpts is not nil, ffmpeg will be used to merge and convert files.
@ -82,7 +82,7 @@ type Downloader struct {
FFmpegOpts []string FFmpegOpts []string
} }
// download's the given format // download's the given format.
func (d Downloader) download(format *Format) error { func (d Downloader) download(format *Format) error {
if err := format.InitVideo(); err != nil { if err := format.InitVideo(); err != nil {
return err return err
@ -109,7 +109,7 @@ func (d Downloader) download(format *Format) error {
} }
// mergeSegments reads every file in tempDir and writes their content to Downloader.Writer. // mergeSegments reads every file in tempDir and writes their content to Downloader.Writer.
// The given output file gets created or overwritten if already existing // The given output file gets created or overwritten if already existing.
func (d Downloader) mergeSegments(files []string) error { func (d Downloader) mergeSegments(files []string) error {
for _, file := range files { for _, file := range files {
select { select {
@ -132,7 +132,7 @@ func (d Downloader) mergeSegments(files []string) error {
// mergeSegmentsFFmpeg reads every file in tempDir and merges their content to the outputFile // mergeSegmentsFFmpeg reads every file in tempDir and merges their content to the outputFile
// with ffmpeg (https://ffmpeg.org/). // with ffmpeg (https://ffmpeg.org/).
// The given output file gets created or overwritten if already existing // The given output file gets created or overwritten if already existing.
func (d Downloader) mergeSegmentsFFmpeg(files []string) error { func (d Downloader) mergeSegmentsFFmpeg(files []string) error {
list, err := os.Create(filepath.Join(d.TempDir, "list.txt")) list, err := os.Create(filepath.Join(d.TempDir, "list.txt"))
if err != nil { if err != nil {
@ -214,13 +214,13 @@ func (d Downloader) mergeSegmentsFFmpeg(files []string) error {
// After every segment download onSegmentDownload will be called with: // After every segment download onSegmentDownload will be called with:
// the downloaded segment, the current position, the total size of segments to download, // the downloaded segment, the current position, the total size of segments to download,
// the file where the segment content was written to an error (if occurred). // the file where the segment content was written to an error (if occurred).
// The filename is always <number of downloaded segment>.ts // The filename is always <number of downloaded segment>.ts.
// //
// Short explanation: // Short explanation:
// The actual crunchyroll video is split up in multiple segments (or video files) which // The actual crunchyroll video is split up in multiple segments (or video files) which
// have to be downloaded and merged after to generate a single video file. // have to be downloaded and merged after to generate a single video file.
// And this function just downloads each of this segment into the given directory. // And this function just downloads each of this segment into the given directory.
// See https://en.wikipedia.org/wiki/MPEG_transport_stream for more information // See https://en.wikipedia.org/wiki/MPEG_transport_stream for more information.
func (d Downloader) downloadSegments(format *Format) ([]string, error) { func (d Downloader) downloadSegments(format *Format) ([]string, error) {
if err := format.InitVideo(); err != nil { if err := format.InitVideo(); err != nil {
return nil, err return nil, err
@ -318,7 +318,7 @@ func (d Downloader) downloadSegments(format *Format) ([]string, error) {
} }
} }
// getCrypt extracts the key and iv of a m3u8 segment and converts it into a cipher.Block and an iv byte sequence // getCrypt extracts the key and iv of a m3u8 segment and converts it into a cipher.Block and an iv byte sequence.
func getCrypt(format *Format, segment *m3u8.MediaSegment) (block cipher.Block, iv []byte, err error) { func getCrypt(format *Format, segment *m3u8.MediaSegment) (block cipher.Block, iv []byte, err error) {
var resp *http.Response var resp *http.Response
@ -341,7 +341,7 @@ func getCrypt(format *Format, segment *m3u8.MediaSegment) (block cipher.Block, i
return block, iv, nil return block, iv, nil
} }
// downloadSegment downloads a segment, decrypts it and names it after the given index // downloadSegment downloads a segment, decrypts it and names it after the given index.
func (d Downloader) downloadSegment(format *Format, segment *m3u8.MediaSegment, filename string, block cipher.Block, iv []byte) (*os.File, error) { func (d Downloader) downloadSegment(format *Format, segment *m3u8.MediaSegment, filename string, block cipher.Block, iv []byte) (*os.File, error) {
// every segment is aes-128 encrypted and has to be decrypted when downloaded // every segment is aes-128 encrypted and has to be decrypted when downloaded
content, err := d.decryptSegment(format.crunchy.Client, segment, block, iv) content, err := d.decryptSegment(format.crunchy.Client, segment, block, iv)
@ -361,7 +361,7 @@ func (d Downloader) downloadSegment(format *Format, segment *m3u8.MediaSegment,
return file, nil return file, nil
} }
// https://github.com/oopsguy/m3u8/blob/4150e93ec8f4f8718875a02973f5d792648ecb97/tool/crypt.go#L25 // https://github.com/oopsguy/m3u8/blob/4150e93ec8f4f8718875a02973f5d792648ecb97/tool/crypt.go#L25.
func (d Downloader) decryptSegment(client *http.Client, segment *m3u8.MediaSegment, block cipher.Block, iv []byte) ([]byte, error) { func (d Downloader) decryptSegment(client *http.Client, segment *m3u8.MediaSegment, block cipher.Block, iv []byte) ([]byte, error) {
req, err := http.NewRequestWithContext(d.Context, http.MethodGet, segment.URI, nil) req, err := http.NewRequestWithContext(d.Context, http.MethodGet, segment.URI, nil)
if err != nil { if err != nil {
@ -387,7 +387,7 @@ func (d Downloader) decryptSegment(client *http.Client, segment *m3u8.MediaSegme
return raw, nil return raw, nil
} }
// https://github.com/oopsguy/m3u8/blob/4150e93ec8f4f8718875a02973f5d792648ecb97/tool/crypt.go#L47 // https://github.com/oopsguy/m3u8/blob/4150e93ec8f4f8718875a02973f5d792648ecb97/tool/crypt.go#L47.
func (d Downloader) pkcs5UnPadding(origData []byte) []byte { func (d Downloader) pkcs5UnPadding(origData []byte) []byte {
length := len(origData) length := len(origData)
unPadding := int(origData[length-1]) unPadding := int(origData[length-1])

View file

@ -9,6 +9,7 @@ import (
"time" "time"
) )
// Episode contains all information about an episode.
type Episode struct { type Episode struct {
crunchy *Crunchyroll crunchy *Crunchyroll
@ -74,7 +75,7 @@ type Episode struct {
StreamID string StreamID string
} }
// EpisodeFromID returns an episode by its api id // EpisodeFromID returns an episode by its api id.
func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) { func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/episodes/%s?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/episodes/%s?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
crunchy.Config.CountryCode, crunchy.Config.CountryCode,
@ -111,7 +112,8 @@ func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
// AudioLocale returns the audio locale of the episode. // AudioLocale returns the audio locale of the episode.
// Every episode in a season (should) have the same audio locale, // Every episode in a season (should) have the same audio locale,
// so if you want to get the audio locale of a season, just call this method on the first episode of the season // so if you want to get the audio locale of a season, just call
// this method on the first episode of the season.
func (e *Episode) AudioLocale() (LOCALE, error) { func (e *Episode) AudioLocale() (LOCALE, error) {
streams, err := e.Streams() streams, err := e.Streams()
if err != nil { if err != nil {
@ -120,7 +122,7 @@ func (e *Episode) AudioLocale() (LOCALE, error) {
return streams[0].AudioLocale, nil return streams[0].AudioLocale, nil
} }
// GetFormat returns the format which matches the given resolution and subtitle locale // GetFormat returns the format which matches the given resolution and subtitle locale.
func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*Format, error) { func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*Format, error) {
streams, err := e.Streams() streams, err := e.Streams()
if err != nil { if err != nil {
@ -186,7 +188,7 @@ func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*
return nil, fmt.Errorf("no matching resolution found") return nil, fmt.Errorf("no matching resolution found")
} }
// Streams returns all streams which are available for the episode // Streams returns all streams which are available for the episode.
func (e *Episode) Streams() ([]*Stream, error) { func (e *Episode) Streams() ([]*Stream, error) {
if e.children != nil { if e.children != nil {
return e.children, nil return e.children, nil

View file

@ -3,7 +3,7 @@ package crunchyroll
import "fmt" import "fmt"
// AccessError is an error which will be returned when some special sort of api request fails. // AccessError is an error which will be returned when some special sort of api request fails.
// See Crunchyroll.request when the error gets used // See Crunchyroll.request when the error gets used.
type AccessError struct { type AccessError struct {
error error

View file

@ -11,11 +11,12 @@ const (
MOVIE = "movies" MOVIE = "movies"
) )
// Format contains detailed information about an episode video stream.
type Format struct { type Format struct {
crunchy *Crunchyroll crunchy *Crunchyroll
ID string ID string
// FormatType represents if the format parent is an episode or a movie // FormatType represents if the format parent is an episode or a movie.
FormatType FormatType FormatType FormatType
Video *m3u8.Variant Video *m3u8.Variant
AudioLocale LOCALE AudioLocale LOCALE
@ -27,7 +28,7 @@ type Format struct {
// The Format.Video.Chunklist pointer is, by default, nil because an additional // The Format.Video.Chunklist pointer is, by default, nil because an additional
// request must be made to receive its content. The request is not made when // request must be made to receive its content. The request is not made when
// initializing a Format struct because it would probably cause an intense overhead // initializing a Format struct because it would probably cause an intense overhead
// since Format.Video.Chunklist is only used sometimes // since Format.Video.Chunklist is only used sometimes.
func (f *Format) InitVideo() error { func (f *Format) InitVideo() error {
if f.Video.Chunklist == nil { if f.Video.Chunklist == nil {
resp, err := f.crunchy.Client.Get(f.Video.URI) resp, err := f.crunchy.Client.Get(f.Video.URI)
@ -45,7 +46,7 @@ func (f *Format) InitVideo() error {
return nil return nil
} }
// Download downloads the Format with the via Downloader specified options // Download downloads the Format with the via Downloader specified options.
func (f *Format) Download(downloader Downloader) error { func (f *Format) Download(downloader Downloader) error {
return downloader.download(f) return downloader.download(f)
} }

View file

@ -5,6 +5,8 @@ import (
"fmt" "fmt"
) )
// MovieListing contains information about something which is called
// movie listing. I don't know what this means thb.
type MovieListing struct { type MovieListing struct {
crunchy *Crunchyroll crunchy *Crunchyroll
@ -36,7 +38,7 @@ type MovieListing struct {
AvailabilityNotes string `json:"availability_notes"` AvailabilityNotes string `json:"availability_notes"`
} }
// MovieListingFromID returns a movie listing by its api id // MovieListingFromID returns a movie listing by its api id.
func MovieListingFromID(crunchy *Crunchyroll, id string) (*MovieListing, error) { func MovieListingFromID(crunchy *Crunchyroll, id string) (*MovieListing, error) {
resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movie_listing/%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movie_listing/%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
crunchy.Config.CountryCode, crunchy.Config.CountryCode,
@ -65,7 +67,7 @@ func MovieListingFromID(crunchy *Crunchyroll, id string) (*MovieListing, error)
return movieListing, nil return movieListing, nil
} }
// AudioLocale is same as Episode.AudioLocale // AudioLocale is same as Episode.AudioLocale.
func (ml *MovieListing) AudioLocale() (LOCALE, error) { func (ml *MovieListing) AudioLocale() (LOCALE, error) {
resp, err := ml.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", resp, err := ml.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
ml.crunchy.Config.CountryCode, ml.crunchy.Config.CountryCode,
@ -86,7 +88,7 @@ func (ml *MovieListing) AudioLocale() (LOCALE, error) {
return LOCALE(jsonBody["audio_locale"].(string)), nil return LOCALE(jsonBody["audio_locale"].(string)), nil
} }
// Streams returns all streams which are available for the movie listing // Streams returns all streams which are available for the movie listing.
func (ml *MovieListing) Streams() ([]*Stream, error) { func (ml *MovieListing) Streams() ([]*Stream, error) {
return fromVideoStreams(ml.crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", return fromVideoStreams(ml.crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
ml.crunchy.Config.CountryCode, ml.crunchy.Config.CountryCode,

View file

@ -6,6 +6,7 @@ import (
"regexp" "regexp"
) )
// Season contains information about an anime season.
type Season struct { type Season struct {
crunchy *Crunchyroll crunchy *Crunchyroll
@ -41,7 +42,7 @@ type Season struct {
SubtitleLocales []LOCALE SubtitleLocales []LOCALE
} }
// SeasonFromID returns a season by its api id // SeasonFromID returns a season by its api id.
func SeasonFromID(crunchy *Crunchyroll, id string) (*Season, error) { func SeasonFromID(crunchy *Crunchyroll, id string) (*Season, error) {
resp, err := crunchy.Client.Get(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/seasons/%s?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", resp, err := crunchy.Client.Get(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/seasons/%s?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
crunchy.Config.CountryCode, crunchy.Config.CountryCode,
@ -70,6 +71,7 @@ func SeasonFromID(crunchy *Crunchyroll, id string) (*Season, error) {
return season, nil return season, nil
} }
// AudioLocale returns the audio locale of the season.
func (s *Season) AudioLocale() (LOCALE, error) { func (s *Season) AudioLocale() (LOCALE, error) {
episodes, err := s.Episodes() episodes, err := s.Episodes()
if err != nil { if err != nil {
@ -78,7 +80,7 @@ func (s *Season) AudioLocale() (LOCALE, error) {
return episodes[0].AudioLocale() return episodes[0].AudioLocale()
} }
// Episodes returns all episodes which are available for the season // Episodes returns all episodes which are available for the season.
func (s *Season) Episodes() (episodes []*Episode, err error) { func (s *Season) Episodes() (episodes []*Episode, err error) {
if s.children != nil { if s.children != nil {
return s.children, nil return s.children, nil

View file

@ -8,6 +8,7 @@ import (
"regexp" "regexp"
) )
// Stream contains information about all available video stream of an episode.
type Stream struct { type Stream struct {
crunchy *Crunchyroll crunchy *Crunchyroll
@ -22,7 +23,7 @@ type Stream struct {
streamURL string streamURL string
} }
// StreamsFromID returns a stream by its api id // StreamsFromID returns a stream by its api id.
func StreamsFromID(crunchy *Crunchyroll, id string) ([]*Stream, error) { func StreamsFromID(crunchy *Crunchyroll, id string) ([]*Stream, error) {
return fromVideoStreams(crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", return fromVideoStreams(crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
crunchy.Config.CountryCode, crunchy.Config.CountryCode,
@ -35,7 +36,7 @@ func StreamsFromID(crunchy *Crunchyroll, id string) ([]*Stream, error) {
crunchy.Config.KeyPairID)) crunchy.Config.KeyPairID))
} }
// Formats returns all formats which are available for the stream // Formats returns all formats which are available for the stream.
func (s *Stream) Formats() ([]*Format, error) { func (s *Stream) Formats() ([]*Format, error) {
if s.children != nil { if s.children != nil {
return s.children, nil return s.children, nil
@ -70,7 +71,7 @@ func (s *Stream) Formats() ([]*Format, error) {
return formats, nil return formats, nil
} }
// fromVideoStreams returns all streams which are accessible via the endpoint // fromVideoStreams returns all streams which are accessible via the endpoint.
func fromVideoStreams(crunchy *Crunchyroll, endpoint string) (streams []*Stream, err error) { func fromVideoStreams(crunchy *Crunchyroll, endpoint string) (streams []*Stream, err error) {
resp, err := crunchy.request(endpoint) resp, err := crunchy.request(endpoint)
if err != nil { if err != nil {

View file

@ -5,6 +5,7 @@ import (
"net/http" "net/http"
) )
// Subtitle contains the information about a video subtitle.
type Subtitle struct { type Subtitle struct {
crunchy *Crunchyroll crunchy *Crunchyroll
@ -13,6 +14,7 @@ type Subtitle struct {
Format string `json:"format"` Format string `json:"format"`
} }
// Save writes the subtitle to the given io.Writer.
func (s Subtitle) Save(writer io.Writer) error { func (s Subtitle) Save(writer io.Writer) error {
req, err := http.NewRequestWithContext(s.crunchy.Context, http.MethodGet, s.URL, nil) req, err := http.NewRequestWithContext(s.crunchy.Context, http.MethodGet, s.URL, nil)
if err != nil { if err != nil {

4
url.go
View file

@ -5,7 +5,7 @@ import (
) )
// ExtractEpisodesFromUrl extracts all episodes from an url. // ExtractEpisodesFromUrl extracts all episodes from an url.
// If audio is not empty, the episodes gets filtered after the given locale // If audio is not empty, the episodes gets filtered after the given locale.
func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Episode, error) { func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Episode, error) {
series, episodes, err := c.ParseUrl(url) series, episodes, err := c.ParseUrl(url)
if err != nil { if err != nil {
@ -78,7 +78,7 @@ func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Ep
} }
// ParseUrl parses the given url into a series or episode. // ParseUrl parses the given url into a series or episode.
// The returning episode is a slice because non-beta urls have the same episode with different languages // The returning episode is a slice because non-beta urls have the same episode with different languages.
func (c *Crunchyroll) ParseUrl(url string) (*Series, []*Episode, error) { func (c *Crunchyroll) ParseUrl(url string) (*Series, []*Episode, error) {
if seriesId, ok := ParseBetaSeriesURL(url); ok { if seriesId, ok := ParseBetaSeriesURL(url); ok {
series, err := SeriesFromID(c, seriesId) series, err := SeriesFromID(c, seriesId)

View file

@ -4,6 +4,7 @@ import (
"github.com/ByteDream/crunchyroll-go" "github.com/ByteDream/crunchyroll-go"
) )
// AllLocales is an array of all available locales.
var AllLocales = []crunchyroll.LOCALE{ var AllLocales = []crunchyroll.LOCALE{
crunchyroll.JP, crunchyroll.JP,
crunchyroll.US, crunchyroll.US,
@ -18,7 +19,7 @@ var AllLocales = []crunchyroll.LOCALE{
crunchyroll.AR, crunchyroll.AR,
} }
// ValidateLocale validates if the given locale actually exist // ValidateLocale validates if the given locale actually exist.
func ValidateLocale(locale crunchyroll.LOCALE) bool { func ValidateLocale(locale crunchyroll.LOCALE) bool {
for _, l := range AllLocales { for _, l := range AllLocales {
if l == locale { if l == locale {
@ -28,7 +29,7 @@ func ValidateLocale(locale crunchyroll.LOCALE) bool {
return false return false
} }
// LocaleLanguage returns the country by its locale // LocaleLanguage returns the country by its locale.
func LocaleLanguage(locale crunchyroll.LOCALE) string { func LocaleLanguage(locale crunchyroll.LOCALE) string {
switch locale { switch locale {
case crunchyroll.JP: case crunchyroll.JP:

View file

@ -9,7 +9,7 @@ import (
) )
// SortEpisodesBySeason sorts the given episodes by their seasons. // SortEpisodesBySeason sorts the given episodes by their seasons.
// Note that the same episodes just with different audio locales will cause problems // Note that the same episodes just with different audio locales will cause problems.
func SortEpisodesBySeason(episodes []*crunchyroll.Episode) [][]*crunchyroll.Episode { func SortEpisodesBySeason(episodes []*crunchyroll.Episode) [][]*crunchyroll.Episode {
sortMap := map[string]map[int][]*crunchyroll.Episode{} sortMap := map[string]map[int][]*crunchyroll.Episode{}
@ -43,7 +43,7 @@ func SortEpisodesBySeason(episodes []*crunchyroll.Episode) [][]*crunchyroll.Epis
return eps return eps
} }
// SortEpisodesByAudio sort the given episodes by their audio locale // SortEpisodesByAudio sort the given episodes by their audio locale.
func SortEpisodesByAudio(episodes []*crunchyroll.Episode) (map[crunchyroll.LOCALE][]*crunchyroll.Episode, error) { func SortEpisodesByAudio(episodes []*crunchyroll.Episode) (map[crunchyroll.LOCALE][]*crunchyroll.Episode, error) {
eps := map[crunchyroll.LOCALE][]*crunchyroll.Episode{} eps := map[crunchyroll.LOCALE][]*crunchyroll.Episode{}
@ -81,7 +81,7 @@ func SortEpisodesByAudio(episodes []*crunchyroll.Episode) (map[crunchyroll.LOCAL
return eps, nil return eps, nil
} }
// MovieListingsByDuration sorts movie listings by their duration // MovieListingsByDuration sorts movie listings by their duration.
type MovieListingsByDuration []*crunchyroll.MovieListing type MovieListingsByDuration []*crunchyroll.MovieListing
func (mlbd MovieListingsByDuration) Len() int { func (mlbd MovieListingsByDuration) Len() int {
@ -94,7 +94,7 @@ func (mlbd MovieListingsByDuration) Less(i, j int) bool {
return mlbd[i].DurationMS < mlbd[j].DurationMS return mlbd[i].DurationMS < mlbd[j].DurationMS
} }
// EpisodesByDuration sorts episodes by their duration // EpisodesByDuration sorts episodes by their duration.
type EpisodesByDuration []*crunchyroll.Episode type EpisodesByDuration []*crunchyroll.Episode
func (ebd EpisodesByDuration) Len() int { func (ebd EpisodesByDuration) Len() int {
@ -107,6 +107,7 @@ func (ebd EpisodesByDuration) Less(i, j int) bool {
return ebd[i].DurationMS < ebd[j].DurationMS return ebd[i].DurationMS < ebd[j].DurationMS
} }
// EpisodesByNumber sorts episodes after their episode number.
type EpisodesByNumber []*crunchyroll.Episode type EpisodesByNumber []*crunchyroll.Episode
func (ebn EpisodesByNumber) Len() int { func (ebn EpisodesByNumber) Len() int {
@ -119,7 +120,7 @@ func (ebn EpisodesByNumber) Less(i, j int) bool {
return ebn[i].EpisodeNumber < ebn[j].EpisodeNumber return ebn[i].EpisodeNumber < ebn[j].EpisodeNumber
} }
// FormatsByResolution sorts formats after their resolution // FormatsByResolution sorts formats after their resolution.
type FormatsByResolution []*crunchyroll.Format type FormatsByResolution []*crunchyroll.Format
func (fbr FormatsByResolution) Len() int { func (fbr FormatsByResolution) Len() int {
@ -140,6 +141,7 @@ func (fbr FormatsByResolution) Less(i, j int) bool {
return iResX+iResY < jResX+jResY return iResX+iResY < jResX+jResY
} }
// SubtitlesByLocale sorts subtitles after their locale.
type SubtitlesByLocale []*crunchyroll.Subtitle type SubtitlesByLocale []*crunchyroll.Subtitle
func (sbl SubtitlesByLocale) Len() int { func (sbl SubtitlesByLocale) Len() int {

View file

@ -30,8 +30,10 @@ type video struct {
} `json:"images"` } `json:"images"`
} }
// Video is the base for Movie and Season.
type Video interface{} type Video interface{}
// Movie contains information about a movie.
type Movie struct { type Movie struct {
video video
Video Video
@ -40,7 +42,7 @@ type Movie struct {
children []*MovieListing children []*MovieListing
// not generated when calling MovieFromID // not generated when calling MovieFromID.
MovieListingMetadata struct { MovieListingMetadata struct {
AvailabilityNotes string `json:"availability_notes"` AvailabilityNotes string `json:"availability_notes"`
AvailableOffline bool `json:"available_offline"` AvailableOffline bool `json:"available_offline"`
@ -65,7 +67,7 @@ type Movie struct {
} }
} }
// MovieFromID returns a movie by its api id // MovieFromID returns a movie by its api id.
func MovieFromID(crunchy *Crunchyroll, id string) (*Movie, error) { func MovieFromID(crunchy *Crunchyroll, id string) (*Movie, error) {
resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movies/%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movies/%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
crunchy.Config.CountryCode, crunchy.Config.CountryCode,
@ -95,8 +97,6 @@ func MovieFromID(crunchy *Crunchyroll, id string) (*Movie, error) {
} }
// MovieListing returns all videos corresponding with the movie. // MovieListing returns all videos corresponding with the movie.
// Beside the normal movie, sometimes movie previews are returned too, but you can try to get the actual movie
// by sorting the returning MovieListing slice with the utils.MovieListingByDuration interface
func (m *Movie) MovieListing() (movieListings []*MovieListing, err error) { func (m *Movie) MovieListing() (movieListings []*MovieListing, err error) {
if m.children != nil { if m.children != nil {
return m.children, nil return m.children, nil
@ -134,6 +134,7 @@ func (m *Movie) MovieListing() (movieListings []*MovieListing, err error) {
return movieListings, nil return movieListings, nil
} }
// Series contains information about an anime series.
type Series struct { type Series struct {
video video
Video Video
@ -156,13 +157,13 @@ type Series struct {
MatureRatings []string `json:"mature_ratings"` MatureRatings []string `json:"mature_ratings"`
SeasonCount int `json:"season_count"` SeasonCount int `json:"season_count"`
// not generated when calling SeriesFromID // not generated when calling SeriesFromID.
SearchMetadata struct { SearchMetadata struct {
Score float64 `json:"score"` Score float64 `json:"score"`
} }
} }
// SeriesFromID returns a series by its api id // SeriesFromID returns a series by its api id.
func SeriesFromID(crunchy *Crunchyroll, id string) (*Series, error) { func SeriesFromID(crunchy *Crunchyroll, id string) (*Series, error) {
resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movies?movie_listing_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movies?movie_listing_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
crunchy.Config.CountryCode, crunchy.Config.CountryCode,
@ -191,7 +192,7 @@ func SeriesFromID(crunchy *Crunchyroll, id string) (*Series, error) {
return series, nil return series, nil
} }
// Seasons returns all seasons of a series // Seasons returns all seasons of a series.
func (s *Series) Seasons() (seasons []*Season, err error) { func (s *Series) Seasons() (seasons []*Season, err error) {
if s.children != nil { if s.children != nil {
return s.children, nil return s.children, nil