mirror of
https://github.com/chris124567/hulu
synced 2024-11-25 17:47:30 +00:00
Delete client.go
This commit is contained in:
parent
9bcd331a50
commit
f7a7e71e3d
224
client/client.go
224
client/client.go
@ -1,224 +0,0 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"lukechampine.com/frand"
|
||||
)
|
||||
|
||||
const (
|
||||
contentTypeJSON = "application/json"
|
||||
contentTypeForm = "application/x-www-form-urlencoded"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
c *http.Client
|
||||
huluSession string
|
||||
huluGUID string
|
||||
}
|
||||
|
||||
// Returns a Client object that will use the provided Hulu session cookie to
|
||||
// interact with the Hulu API.
|
||||
func NewClient(c *http.Client, huluSession, huluGUID string) Client {
|
||||
return Client{c, huluSession, huluGUID}
|
||||
}
|
||||
|
||||
// Returns a Client object using a default HTTP client with a timeout of 10s.
|
||||
func NewDefaultClient(huluSession, huluGUID string) Client {
|
||||
return NewClient(&http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}, huluSession, huluGUID)
|
||||
}
|
||||
|
||||
// Makes an HTTP request to a Hulu API endpoint. The only cookie Hulu validates is
|
||||
// the session cookie so we just provide it alone.
|
||||
func (c Client) request(method string, url string, data io.Reader, contentType string) (*http.Response, error) {
|
||||
request, err := http.NewRequest(method, url, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Close = true
|
||||
request.Header = StandardHeaders()
|
||||
request.Header.Set("Cookie", "_hulu_session="+c.huluSession)
|
||||
if method == http.MethodPost && len(contentType) > 0 {
|
||||
request.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
return c.c.Do(request)
|
||||
}
|
||||
|
||||
// Queries the Hulu entity search API endpoint for shows and movies. This can
|
||||
// return content that you do not have the right subscription for (like stuff
|
||||
// requiring an HBO subscription) so be mindful of that.
|
||||
func (c Client) Search(query string) (s SearchResults, err error) {
|
||||
query = url.QueryEscape(query)
|
||||
response, err := c.request(http.MethodGet, fmt.Sprintf("https://discover.hulu.com/content/v5/search/entity?language=en&device_context_id=2&search_query=%s&limit=64&include_offsite=true&v=26e1061d-68ec-48bf-be5a-b2f704d37256&schema=1&device_info=web:3.29.0&referralHost=production&keywords=%s&type=entity&limit=64", query, query), nil, "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
err = json.NewDecoder(response.Body).Decode(&s)
|
||||
return
|
||||
}
|
||||
|
||||
// Returns the season information containing the episode list in a given season
|
||||
// for a given show.
|
||||
func (c Client) Season(id string, season int) (s Season, err error) {
|
||||
response, err := c.request(http.MethodGet, fmt.Sprintf("https://discover.hulu.com/content/v5/hubs/series/%s/season/%d?limit=999&schema=1&offset=0&device_info=web:3.29.0&referralHost=production", id, season), nil, "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
err = json.NewDecoder(response.Body).Decode(&s)
|
||||
return
|
||||
}
|
||||
|
||||
// The /config endpoint returns a large hex encoded string. This string then
|
||||
// has to be decoded using a hardcoded key from Hulu. The decoded data is JSON
|
||||
// containing a bunch of configuration options for the player. More importantly,
|
||||
// it contains the KeyID field which is needed to call Playlist.
|
||||
func (c Client) ServerConfig() (co Config, err error) {
|
||||
rv := strconv.Itoa(int(frand.Uint64n(1e6)))
|
||||
base := strings.Join([]string{hex.EncodeToString(deejayKey), strconv.Itoa(deejayDeviceID), strconv.Itoa(deejayKeyVersion), rv}, ",")
|
||||
nonce := md5.Sum([]byte(base))
|
||||
|
||||
values := url.Values{}
|
||||
values.Add("app_version", strconv.Itoa(deejayKeyVersion))
|
||||
values.Add("badging", "true")
|
||||
values.Add("device", strconv.Itoa(deejayDeviceID))
|
||||
values.Add("device_id", c.huluGUID)
|
||||
values.Add("encrypted_nonce", hex.EncodeToString(nonce[:]))
|
||||
values.Add("language", "en")
|
||||
values.Add("region", "US")
|
||||
values.Add("rv", rv)
|
||||
values.Add("version", strconv.Itoa(deejayKeyVersion))
|
||||
|
||||
response, err := c.request(http.MethodPost, "https://play.hulu.com/config", strings.NewReader(values.Encode()), contentTypeForm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
ciphertext, err := io.ReadAll(hex.NewDecoder(response.Body))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(deejayKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
dec := cipher.NewCBCDecrypter(block, make([]byte, 16))
|
||||
unpad := func(b []byte) []byte {
|
||||
if len(b) == 0 {
|
||||
return b
|
||||
}
|
||||
// pks padding is designed so that the value of all the padding bytes is
|
||||
// the number of padding bytes repeated so to figure out how many
|
||||
// padding bytes there are we can just look at the value of the last
|
||||
// byte
|
||||
// i.e if there are 6 padding bytes then it will look at like
|
||||
// <data> 0x6 0x6 0x6 0x6 0x6 0x6
|
||||
count := int(b[len(b)-1])
|
||||
return b[0 : len(b)-count]
|
||||
}
|
||||
plaintext := make([]byte, len(ciphertext))
|
||||
dec.CryptBlocks(plaintext, ciphertext)
|
||||
err = json.Unmarshal(unpad(plaintext), &co)
|
||||
return
|
||||
}
|
||||
|
||||
// This allows us to get the EAB ID for a given plain ID. The EAB ID is
|
||||
// necessary to call Playlist.
|
||||
func (c Client) PlaybackInformation(id string) (p PlaybackInformation, err error) {
|
||||
response, err := c.request(http.MethodGet, fmt.Sprintf("https://discover.hulu.com/content/v5/deeplink/playback?namespace=entity&id=%s&schema=1&device_info=web:3.29.0&referralHost=production", id), nil, "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
err = json.NewDecoder(response.Body).Decode(&p)
|
||||
return
|
||||
}
|
||||
|
||||
// Playlist returns information containing the Widevine license endpoint,
|
||||
// the MPD file URL, and information relating to subtitles (Hulu calls them
|
||||
// transcripts).
|
||||
func (c Client) Playlist(sessionKey int, eabID string) (p Playlist, err error) {
|
||||
randUUID := func() string {
|
||||
randChars := func(n int) (s string) {
|
||||
c := []byte("ABCDEF0123456789")
|
||||
for i := 0; i < n; i++ {
|
||||
s += string(c[frand.Intn(len(c))])
|
||||
}
|
||||
return
|
||||
}
|
||||
return strings.Join([]string{randChars(8), randChars(4), randChars(4), randChars(4), randChars(12)}, "-")
|
||||
}
|
||||
|
||||
playlistRequest := PlaylistRequest{
|
||||
DeviceIdentifier: c.huluGUID + ":d40b",
|
||||
DeejayDeviceID: deejayDeviceID,
|
||||
Version: deejayKeyVersion,
|
||||
AllCdn: true,
|
||||
ContentEabID: eabID,
|
||||
Region: "US",
|
||||
XlinkSupport: false,
|
||||
DeviceAdID: randUUID(),
|
||||
LimitAdTracking: false,
|
||||
IgnoreKidsBlock: false,
|
||||
Language: "en",
|
||||
GUID: c.huluGUID,
|
||||
Rv: int(frand.Uint64n(1e7)),
|
||||
Kv: sessionKey,
|
||||
Unencrypted: true,
|
||||
IncludeT2RevenueBeacon: "1",
|
||||
CpSessionID: randUUID(),
|
||||
NetworkMode: "wifi",
|
||||
PlayIntent: "resume",
|
||||
Playback: PlaylistRequestPlayback{
|
||||
Version: 2,
|
||||
Video: PlaylistRequestVideo{Codecs: PlaylistRequestCodecs{Values: []PlaylistRequestValues{{Type: "H264", Profile: "HIGH", Level: "4.1", Framerate: 30}}, SelectionMode: "ONE"}},
|
||||
Audio: PlaylistRequestAudio{Codecs: PlaylistRequestCodecs{Values: []PlaylistRequestValues{{Type: "AAC"}}, SelectionMode: "ONE"}},
|
||||
DRM: PlaylistRequestDRM{Values: []PlaylistRequestValues{{Type: "WIDEVINE", Version: "MODULAR", SecurityLevel: "L3"}}, SelectionMode: "ONE"},
|
||||
Manifest: PlaylistRequestManifest{
|
||||
Type: "DASH",
|
||||
HTTPS: true,
|
||||
MultipleCdns: true,
|
||||
PatchUpdates: true,
|
||||
HuluTypes: true,
|
||||
LiveDai: true,
|
||||
MultiplePeriods: true,
|
||||
Xlink: false,
|
||||
SecondaryAudio: true,
|
||||
LiveFragmentDelay: 3,
|
||||
},
|
||||
Segments: PlaylistRequestSegments{Values: []PlaylistRequestValues{{Type: "FMP4", Encryption: &PlaylistRequestEncryption{Mode: "CENC", Type: "CENC"}, HTTPS: true}}, SelectionMode: "ONE"},
|
||||
},
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err = json.NewEncoder(&buf).Encode(playlistRequest); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
response, err := c.request(http.MethodPost, "https://play.hulu.com/v6/playlist", &buf, contentTypeJSON)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
err = json.NewDecoder(response.Body).Decode(&p)
|
||||
return
|
||||
}
|
Loading…
Reference in New Issue
Block a user