// 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 }