mirror of
https://github.com/crunchy-labs/crunchy-cli.git
synced 2026-01-21 12:12:00 -06:00
126 lines
3.3 KiB
Go
126 lines
3.3 KiB
Go
package crunchyroll
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/grafov/m3u8"
|
|
"regexp"
|
|
)
|
|
|
|
// Stream contains information about all available video stream of an episode.
|
|
type Stream struct {
|
|
crunchy *Crunchyroll
|
|
|
|
children []*Format
|
|
|
|
HardsubLocale LOCALE
|
|
AudioLocale LOCALE
|
|
Subtitles []*Subtitle
|
|
|
|
formatType FormatType
|
|
id string
|
|
streamURL string
|
|
}
|
|
|
|
// StreamsFromID returns a stream by its api id.
|
|
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",
|
|
crunchy.Config.CountryCode,
|
|
crunchy.Config.MaturityRating,
|
|
crunchy.Config.Channel,
|
|
id,
|
|
crunchy.Locale,
|
|
crunchy.Config.Signature,
|
|
crunchy.Config.Policy,
|
|
crunchy.Config.KeyPairID))
|
|
}
|
|
|
|
// Formats returns all formats which are available for the stream.
|
|
func (s *Stream) Formats() ([]*Format, error) {
|
|
if s.children != nil {
|
|
return s.children, nil
|
|
}
|
|
|
|
resp, err := s.crunchy.Client.Get(s.streamURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
playlist, _, err := m3u8.DecodeFrom(resp.Body, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var formats []*Format
|
|
for _, variant := range playlist.(*m3u8.MasterPlaylist).Variants {
|
|
formats = append(formats, &Format{
|
|
crunchy: s.crunchy,
|
|
ID: s.id,
|
|
FormatType: s.formatType,
|
|
Video: variant,
|
|
AudioLocale: s.AudioLocale,
|
|
Hardsub: s.HardsubLocale,
|
|
Subtitles: s.Subtitles,
|
|
})
|
|
}
|
|
|
|
if s.crunchy.cache {
|
|
s.children = formats
|
|
}
|
|
return formats, nil
|
|
}
|
|
|
|
// fromVideoStreams returns all streams which are accessible via the endpoint.
|
|
func fromVideoStreams(crunchy *Crunchyroll, endpoint string) (streams []*Stream, err error) {
|
|
resp, err := crunchy.request(endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
var jsonBody map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&jsonBody)
|
|
|
|
if len(jsonBody) == 0 {
|
|
// this may get thrown when the crunchyroll account has just a normal account and not one with premium
|
|
return nil, fmt.Errorf("no stream available")
|
|
}
|
|
|
|
audioLocale := jsonBody["audio_locale"].(string)
|
|
|
|
var subtitles []*Subtitle
|
|
for _, rawSubtitle := range jsonBody["subtitles"].(map[string]interface{}) {
|
|
subtitle := &Subtitle{
|
|
crunchy: crunchy,
|
|
}
|
|
decodeMapToStruct(rawSubtitle.(map[string]interface{}), subtitle)
|
|
subtitles = append(subtitles, subtitle)
|
|
}
|
|
|
|
for _, streamData := range jsonBody["streams"].(map[string]interface{})["adaptive_hls"].(map[string]interface{}) {
|
|
streamData := streamData.(map[string]interface{})
|
|
|
|
hardsubLocale := streamData["hardsub_locale"].(string)
|
|
|
|
var id string
|
|
var formatType FormatType
|
|
href := jsonBody["__links__"].(map[string]interface{})["resource"].(map[string]interface{})["href"].(string)
|
|
if match := regexp.MustCompile(`(?sm)^/cms/v2/\S+/crunchyroll/(\w+)/(\w+)$`).FindAllStringSubmatch(href, -1); len(match) > 0 {
|
|
formatType = FormatType(match[0][1])
|
|
id = match[0][2]
|
|
}
|
|
|
|
stream := &Stream{
|
|
crunchy: crunchy,
|
|
HardsubLocale: LOCALE(hardsubLocale),
|
|
formatType: formatType,
|
|
id: id,
|
|
streamURL: streamData["url"].(string),
|
|
AudioLocale: LOCALE(audioLocale),
|
|
Subtitles: subtitles,
|
|
}
|
|
|
|
streams = append(streams, stream)
|
|
}
|
|
|
|
return
|
|
}
|