mirror of
https://github.com/pierre-emmanuelJ/iptv-proxy.git
synced 2026-03-17 15:34:10 +01:00
Update README.md Update README.md Update README.md Update README.md Update vendor Signed-off-by: Pierre-Emmanuel Jacquier <pierre-emmanuel.jacquier@epitech.eu>
339 lines
9.6 KiB
Go
339 lines
9.6 KiB
Go
// Package xtreamcodes provides a Golang interface to the Xtream-Codes IPTV Server API.
|
|
package xtreamcodes
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
)
|
|
|
|
var defaultUserAgent = "go.xstream-codes (Go-http-client/1.1)"
|
|
|
|
// XtreamClient is the client used to communicate with a Xtream-Codes server.
|
|
type XtreamClient struct {
|
|
Username string
|
|
Password string
|
|
BaseURL string
|
|
UserAgent string
|
|
|
|
ServerInfo ServerInfo
|
|
UserInfo UserInfo
|
|
|
|
// Our HTTP client to communicate with Xtream
|
|
HTTP *http.Client
|
|
Context context.Context
|
|
|
|
// We store an internal map of Streams for use with GetStreamURL
|
|
streams map[int]Stream
|
|
}
|
|
|
|
// NewClient returns an initialized XtreamClient with the given values.
|
|
func NewClient(username, password, baseURL string) (*XtreamClient, error) {
|
|
|
|
_, parseURLErr := url.Parse(baseURL)
|
|
if parseURLErr != nil {
|
|
return nil, fmt.Errorf("error parsing url: %s", parseURLErr.Error())
|
|
}
|
|
|
|
client := &XtreamClient{
|
|
Username: username,
|
|
Password: password,
|
|
BaseURL: baseURL,
|
|
UserAgent: defaultUserAgent,
|
|
|
|
HTTP: http.DefaultClient,
|
|
Context: context.Background(),
|
|
|
|
streams: make(map[int]Stream),
|
|
}
|
|
|
|
authData, authErr := client.sendRequest("", nil)
|
|
if authErr != nil {
|
|
return nil, fmt.Errorf("error sending authentication request: %s", authErr.Error())
|
|
}
|
|
|
|
a := &AuthenticationResponse{}
|
|
|
|
if jsonErr := json.Unmarshal(authData, &a); jsonErr != nil {
|
|
return nil, fmt.Errorf("error unmarshaling json: %s", jsonErr.Error())
|
|
}
|
|
|
|
client.ServerInfo = a.ServerInfo
|
|
client.UserInfo = a.UserInfo
|
|
|
|
return client, nil
|
|
}
|
|
|
|
// NewClientWithContext returns an initialized XtreamClient with the given values.
|
|
func NewClientWithContext(ctx context.Context, username, password, baseURL string) (*XtreamClient, error) {
|
|
c, err := NewClient(username, password, baseURL)
|
|
c.Context = ctx
|
|
return c, err
|
|
}
|
|
|
|
// NewClientWithUserAgent returns an initialized XtreamClient with the given values.
|
|
func NewClientWithUserAgent(ctx context.Context, username, password, baseURL, userAgent string) (*XtreamClient, error) {
|
|
c, err := NewClient(username, password, baseURL)
|
|
c.UserAgent = userAgent
|
|
c.Context = ctx
|
|
return c, err
|
|
}
|
|
|
|
// GetStreamURL will return a stream URL string for the given streamID and wantedFormat.
|
|
func (c *XtreamClient) GetStreamURL(streamID int, wantedFormat string) (string, error) {
|
|
|
|
// For Live Streams the main format is
|
|
// http(s)://domain:port/live/username/password/streamID.ext ( In allowed_output_formats element you have the available ext )
|
|
// For VOD Streams the format is:
|
|
// http(s)://domain:port/movie/username/password/streamID.ext ( In target_container element you have the available ext )
|
|
// For Series Streams the format is
|
|
// http(s)://domain:port/series/username/password/streamID.ext ( In target_container element you have the available ext )
|
|
|
|
validFormat := false
|
|
|
|
for _, allowedFormat := range c.UserInfo.AllowedOutputFormats {
|
|
if wantedFormat == allowedFormat {
|
|
validFormat = true
|
|
}
|
|
}
|
|
|
|
if !validFormat {
|
|
return "", fmt.Errorf("%s is not an allowed output format", wantedFormat)
|
|
}
|
|
|
|
if _, ok := c.streams[streamID]; !ok {
|
|
return "", fmt.Errorf("%d is not a valid stream id", streamID)
|
|
}
|
|
|
|
stream := c.streams[streamID]
|
|
|
|
return fmt.Sprintf("%s/%s/%s/%s/%d.%s", c.BaseURL, stream.Type, c.Username, c.Password, stream.ID, wantedFormat), nil
|
|
}
|
|
|
|
// GetLiveCategories will return a slice of categories for live streams.
|
|
func (c *XtreamClient) GetLiveCategories() ([]Category, error) {
|
|
return c.GetCategories("live")
|
|
}
|
|
|
|
// GetVideoOnDemandCategories will return a slice of categories for VOD streams.
|
|
func (c *XtreamClient) GetVideoOnDemandCategories() ([]Category, error) {
|
|
return c.GetCategories("vod")
|
|
}
|
|
|
|
// GetSeriesCategories will return a slice of categories for series streams.
|
|
func (c *XtreamClient) GetSeriesCategories() ([]Category, error) {
|
|
return c.GetCategories("series")
|
|
}
|
|
|
|
// GetCategories is a helper function used by GetLiveCategories, GetVideoOnDemandCategories and
|
|
// GetSeriesCategories to reduce duplicate code.
|
|
func (c *XtreamClient) GetCategories(catType string) ([]Category, error) {
|
|
catData, catErr := c.sendRequest(fmt.Sprintf("get_%s_categories", catType), nil)
|
|
if catErr != nil {
|
|
return nil, catErr
|
|
}
|
|
|
|
cats := make([]Category, 0)
|
|
|
|
jsonErr := json.Unmarshal(catData, &cats)
|
|
|
|
for idx := range cats {
|
|
cats[idx].Type = catType
|
|
}
|
|
|
|
return cats, jsonErr
|
|
}
|
|
|
|
// GetLiveStreams will return a slice of live streams.
|
|
// You can also optionally provide a categoryID to limit the output to members of that category.
|
|
func (c *XtreamClient) GetLiveStreams(categoryID string) ([]Stream, error) {
|
|
return c.GetStreams("live", categoryID)
|
|
}
|
|
|
|
// GetVideoOnDemandStreams will return a slice of VOD streams.
|
|
// You can also optionally provide a categoryID to limit the output to members of that category.
|
|
func (c *XtreamClient) GetVideoOnDemandStreams(categoryID string) ([]Stream, error) {
|
|
return c.GetStreams("vod", categoryID)
|
|
}
|
|
|
|
// GetStreams is a helper function used by GetLiveStreams and GetVideoOnDemandStreams
|
|
// to reduce duplicate code.
|
|
func (c *XtreamClient) GetStreams(streamAction, categoryID string) ([]Stream, error) {
|
|
var params url.Values
|
|
if categoryID != "" {
|
|
params = url.Values{}
|
|
params.Add("category_id", categoryID)
|
|
}
|
|
|
|
// For whatever reason, unlike live and vod, series streams action doesn't have "_streams".
|
|
if streamAction != "series" {
|
|
streamAction = fmt.Sprintf("%s_streams", streamAction)
|
|
}
|
|
|
|
streamData, streamErr := c.sendRequest(fmt.Sprintf("get_%s", streamAction), params)
|
|
if streamErr != nil {
|
|
return nil, streamErr
|
|
}
|
|
|
|
streams := make([]Stream, 0)
|
|
|
|
if jsonErr := json.Unmarshal(streamData, &streams); jsonErr != nil {
|
|
return nil, jsonErr
|
|
}
|
|
|
|
for _, stream := range streams {
|
|
c.streams[stream.ID] = stream
|
|
}
|
|
|
|
return streams, nil
|
|
}
|
|
|
|
// GetSeries will return a slice of all available Series.
|
|
// You can also optionally provide a categoryID to limit the output to members of that category.
|
|
func (c *XtreamClient) GetSeries(categoryID string) ([]SeriesInfo, error) {
|
|
var params url.Values
|
|
if categoryID != "" {
|
|
params = url.Values{}
|
|
params.Add("category_id", categoryID)
|
|
}
|
|
|
|
seriesData, seriesErr := c.sendRequest("get_series", params)
|
|
if seriesErr != nil {
|
|
return nil, seriesErr
|
|
}
|
|
|
|
seriesInfos := make([]SeriesInfo, 0)
|
|
|
|
if jsonErr := json.Unmarshal(seriesData, &seriesInfos); jsonErr != nil {
|
|
return nil, jsonErr
|
|
}
|
|
|
|
return seriesInfos, nil
|
|
}
|
|
|
|
// GetSeriesInfo will return a series info for the given seriesID.
|
|
func (c *XtreamClient) GetSeriesInfo(seriesID string) (*Series, error) {
|
|
if seriesID == "" {
|
|
return nil, fmt.Errorf("series ID can not be empty")
|
|
}
|
|
|
|
seriesData, seriesErr := c.sendRequest("get_series_info", url.Values{"series_id": []string{seriesID}})
|
|
if seriesErr != nil {
|
|
return nil, seriesErr
|
|
}
|
|
|
|
seriesInfo := &Series{}
|
|
|
|
jsonErr := json.Unmarshal(seriesData, &seriesInfo)
|
|
|
|
return seriesInfo, jsonErr
|
|
}
|
|
|
|
// GetVideoOnDemandInfo will return VOD info for the given vodID.
|
|
func (c *XtreamClient) GetVideoOnDemandInfo(vodID string) (*VideoOnDemandInfo, error) {
|
|
if vodID == "" {
|
|
return nil, fmt.Errorf("vod ID can not be empty")
|
|
}
|
|
|
|
vodData, vodErr := c.sendRequest("get_vod_info", url.Values{"vod_id": []string{vodID}})
|
|
if vodErr != nil {
|
|
return nil, vodErr
|
|
}
|
|
|
|
vodInfo := &VideoOnDemandInfo{}
|
|
|
|
jsonErr := json.Unmarshal(vodData, &vodInfo)
|
|
|
|
return vodInfo, jsonErr
|
|
}
|
|
|
|
// GetShortEPG returns a short version of the EPG for the given streamID. If no limit is provided, the next 4 items in the EPG will be returned.
|
|
func (c *XtreamClient) GetShortEPG(streamID string, limit int) ([]EPGInfo, error) {
|
|
return c.getEPG("get_short_epg", streamID, limit)
|
|
}
|
|
|
|
// GetEPG returns the full EPG for the given streamID.
|
|
func (c *XtreamClient) GetEPG(streamID string) ([]EPGInfo, error) {
|
|
return c.getEPG("get_simple_data_table", streamID, 0)
|
|
}
|
|
|
|
// GetXMLTV will return a slice of bytes for the XMLTV EPG file available from the provider.
|
|
func (c *XtreamClient) GetXMLTV() ([]byte, error) {
|
|
xmlTVData, xmlTVErr := c.sendRequest("xmltv.php", nil)
|
|
if xmlTVErr != nil {
|
|
return nil, xmlTVErr
|
|
}
|
|
|
|
return xmlTVData, xmlTVErr
|
|
}
|
|
|
|
func (c *XtreamClient) getEPG(action, streamID string, limit int) ([]EPGInfo, error) {
|
|
if streamID == "" {
|
|
return nil, fmt.Errorf("stream ID can not be empty")
|
|
}
|
|
|
|
params := url.Values{"stream_id": []string{streamID}}
|
|
if limit > 0 {
|
|
params.Add("limit", strconv.Itoa(limit))
|
|
}
|
|
|
|
epgData, epgErr := c.sendRequest(action, params)
|
|
if epgErr != nil {
|
|
return nil, epgErr
|
|
}
|
|
|
|
epgContainer := &epgContainer{}
|
|
|
|
jsonErr := json.Unmarshal(epgData, &epgContainer)
|
|
|
|
return epgContainer.EPGListings, jsonErr
|
|
}
|
|
|
|
func (c *XtreamClient) sendRequest(action string, parameters url.Values) ([]byte, error) {
|
|
file := "player_api.php"
|
|
if action == "xmltv.php" {
|
|
file = action
|
|
}
|
|
url := fmt.Sprintf("%s/%s?username=%s&password=%s", c.BaseURL, file, c.Username, c.Password)
|
|
if action != "" {
|
|
url = fmt.Sprintf("%s&action=%s", url, action)
|
|
}
|
|
if parameters != nil {
|
|
url = fmt.Sprintf("%s&%s", url, parameters.Encode())
|
|
}
|
|
|
|
request, httpErr := http.NewRequest("GET", url, nil)
|
|
if httpErr != nil {
|
|
return nil, httpErr
|
|
}
|
|
|
|
request.Header.Set("User-Agent", c.UserAgent)
|
|
|
|
request = request.WithContext(c.Context)
|
|
|
|
response, httpErr := c.HTTP.Do(request)
|
|
if httpErr != nil {
|
|
return nil, fmt.Errorf("cannot reach server. %v", httpErr)
|
|
}
|
|
|
|
if response.StatusCode > 399 {
|
|
return nil, fmt.Errorf("status code was %d, expected 2XX-3XX", response.StatusCode)
|
|
}
|
|
|
|
buf := &bytes.Buffer{}
|
|
if _, copyErr := io.Copy(buf, response.Body); copyErr != nil {
|
|
return nil, copyErr
|
|
}
|
|
|
|
if closeErr := response.Body.Close(); closeErr != nil {
|
|
return nil, fmt.Errorf("cannot read response. %v", closeErr)
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|