Compare commits
	
		
			9 Commits
		
	
	
		
			29b1603886
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						72ab92d6b8
	
				 | 
					
					
						|||
| 
						
						
							
						
						4c2f1d6691
	
				 | 
					
					
						|||
| 
						
						
							
						
						c90e03271e
	
				 | 
					
					
						|||
| 
						
						
							
						
						91db4c7d84
	
				 | 
					
					
						|||
| 
						
						
							
						
						035776c504
	
				 | 
					
					
						|||
| 
						
						
							
						
						1cd87209be
	
				 | 
					
					
						|||
| 
						
						
							
						
						8a087dea4a
	
				 | 
					
					
						|||
| 
						
						
							
						
						08beb029a7
	
				 | 
					
					
						|||
| 
						
						
							
						
						42e06ebf3d
	
				 | 
					
					
						
							
								
								
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							@@ -1,4 +1,4 @@
 | 
				
			|||||||
Copyright (c) 2016 - 2018, Přemysl Janouch <p@janouch.name>
 | 
					Copyright (c) 2016 - 2025, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Permission to use, copy, modify, and/or distribute this software for any
 | 
					Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
purpose with or without fee is hereby granted.
 | 
					purpose with or without fee is hereby granted.
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11
									
								
								README.adoc
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								README.adoc
									
									
									
									
									
								
							@@ -19,14 +19,19 @@ Build dependencies: go
 | 
				
			|||||||
 $ cd bbc-on-ice
 | 
					 $ cd bbc-on-ice
 | 
				
			||||||
 $ go build
 | 
					 $ go build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					or, if you know what you're doing:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 $ go install janouch.name/bbc-on-ice@master
 | 
				
			||||||
 | 
					
 | 
				
			||||||
To run the local server:
 | 
					To run the local server:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 $ ./bbc-on-ice :8000
 | 
					 $ ./bbc-on-ice :8000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Streams have URLs in the following form:
 | 
					Streams have URLs in the following form, derived from
 | 
				
			||||||
 | 
					https://gist.github.com/bpsib/67089b959e4fa898af69fea59ad74bc3[this list]:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 $ mpv http://localhost:8000/nonuk/sbr_low/bbc_radio_one
 | 
					 $ mpv http://localhost:8000/ww/96000/bbc_radio_one
 | 
				
			||||||
 $ mpv http://localhost:8000/uk/sbr_high/bbc_1xtra
 | 
					 $ mpv http://localhost:8000/uk/320000/bbc_1xtra
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Socket activation
 | 
					Socket activation
 | 
				
			||||||
-----------------
 | 
					-----------------
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										232
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										232
									
								
								main.go
									
									
									
									
									
								
							@@ -6,10 +6,10 @@ import (
 | 
				
			|||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"io/ioutil"
 | 
					 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"net"
 | 
						"net"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
@@ -21,79 +21,159 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	targetURI = "http://a.files.bbci.co.uk/media/live/manifesto/" +
 | 
						targetURI = "http://as-hls-%s-live.akamaized.net/pool_%s/live/%s/" +
 | 
				
			||||||
		"audio/simulcast/hls/%s/%s/ak/%s.m3u8"
 | 
							"%s/%s.isml/%s-audio%%3d%s.norewind.m3u8"
 | 
				
			||||||
	metaBaseURI = "http://polling.bbc.co.uk/radio/nhppolling/"
 | 
						networksURI1 = "https://rms.api.bbc.co.uk/radio/networks.json"
 | 
				
			||||||
 | 
						networksURI2 = "https://rms.api.bbc.co.uk/v2/networks/%s"
 | 
				
			||||||
 | 
						metaURI      = "https://rms.api.bbc.co.uk/v2/services/%s/segments/latest"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var client = &http.Client{Transport: &http.Transport{}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func get(url string) (resp *http.Response, err error) {
 | 
				
			||||||
 | 
						req, err := http.NewRequest("GET", url, nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// lsn.lv returned 403 for the default net.http User-Agent.
 | 
				
			||||||
 | 
						req.Header.Set("User-Agent", "bbc-on-ice/1")
 | 
				
			||||||
 | 
						return client.Do(req)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getServiceTitle1(name string) (string, error) {
 | 
				
			||||||
 | 
						resp, err := get(networksURI1)
 | 
				
			||||||
 | 
						if resp != nil {
 | 
				
			||||||
 | 
							defer resp.Body.Close()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return name, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b, err := io.ReadAll(resp.Body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var v struct {
 | 
				
			||||||
 | 
							Results []struct {
 | 
				
			||||||
 | 
								Services []struct {
 | 
				
			||||||
 | 
									ID    string `json:"id"`
 | 
				
			||||||
 | 
									Title string `json:"title"`
 | 
				
			||||||
 | 
								} `json:"services"`
 | 
				
			||||||
 | 
							} `json:"results"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = json.Unmarshal(b, &v)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return name, errors.New("invalid metadata response")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, network := range v.Results {
 | 
				
			||||||
 | 
							for _, service := range network.Services {
 | 
				
			||||||
 | 
								if service.ID == name {
 | 
				
			||||||
 | 
									return service.Title, nil
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return name, errors.New("unknown service")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// getServiceTitle returns a human-friendly identifier for a BBC service ID.
 | 
				
			||||||
 | 
					func getServiceTitle(name string) (string, error) {
 | 
				
			||||||
 | 
						// This endpoint is incomplete,
 | 
				
			||||||
 | 
						// but it contains the kind of service titles we want.
 | 
				
			||||||
 | 
						title, err := getServiceTitle1(name)
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							return title, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Network IDs tend to coincide with service IDs.
 | 
				
			||||||
 | 
						resp, err := get(fmt.Sprintf(networksURI2, name))
 | 
				
			||||||
 | 
						if resp != nil {
 | 
				
			||||||
 | 
							defer resp.Body.Close()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return name, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						b, err := io.ReadAll(resp.Body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var v struct {
 | 
				
			||||||
 | 
							LongTitle string `json:"long_title"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = json.Unmarshal(b, &v)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return name, errors.New("invalid metadata response")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if v.LongTitle == "" {
 | 
				
			||||||
 | 
							return name, errors.New("unknown service")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return v.LongTitle, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type meta struct {
 | 
					type meta struct {
 | 
				
			||||||
	title   string // what's playing right now
 | 
						title   string // what's playing right now
 | 
				
			||||||
	timeout uint   // timeout for the next poll in ms
 | 
						timeout uint   // timeout for the next poll in ms
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var errNoSong = errors.New("no song is playing")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// getMeta retrieves and decodes metadata info from an independent webservice.
 | 
					// getMeta retrieves and decodes metadata info from an independent webservice.
 | 
				
			||||||
func getMeta(name string) (*meta, error) {
 | 
					func getMeta(name string) (*meta, error) {
 | 
				
			||||||
	resp, err := http.Get(metaBaseURI + name)
 | 
						resp, err := get(fmt.Sprintf(metaURI, name))
 | 
				
			||||||
	if resp != nil {
 | 
						if resp != nil {
 | 
				
			||||||
		defer resp.Body.Close()
 | 
							defer resp.Body.Close()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	b, err := ioutil.ReadAll(resp.Body)
 | 
						b, err := io.ReadAll(resp.Body)
 | 
				
			||||||
	if len(b) < 2 {
 | 
						if os.Getenv("DEBUG") != "" {
 | 
				
			||||||
		// There needs to be an enclosing () pair
 | 
							log.Println(string(b))
 | 
				
			||||||
		return nil, errors.New("invalid metadata response")
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	type broadcast struct {
 | 
						// TODO: update more completely for the new OpenAPI
 | 
				
			||||||
		Title      string // title of the broadcast
 | 
						//  - `broadcasts/poll/bbc_radio_one` looks almost useful
 | 
				
			||||||
		Percentage int    // how far we're in
 | 
						//  - https://rms.api.bbc.co.uk/v2/experience/inline/play/${name}
 | 
				
			||||||
	}
 | 
						//    seems to be what we want, even provides timer/polling values
 | 
				
			||||||
	var v struct {
 | 
						var v struct {
 | 
				
			||||||
		Packages struct {
 | 
							Data []struct {
 | 
				
			||||||
			OnAir struct {
 | 
								Titles struct {
 | 
				
			||||||
				Broadcasts        []broadcast
 | 
									Primary   string  `json:"primary"`
 | 
				
			||||||
				BroadcastNowIndex uint
 | 
									Secondary *string `json:"secondary"`
 | 
				
			||||||
			} `json:"on-air"`
 | 
									Tertiary  *string `json:"tertiary"`
 | 
				
			||||||
			Richtracks []struct {
 | 
								} `json:"titles"`
 | 
				
			||||||
				Artist       string
 | 
								Offset struct {
 | 
				
			||||||
				Title        string
 | 
									NowPlaying bool `json:"now_playing"`
 | 
				
			||||||
				IsNowPlaying bool `json:"is_now_playing"`
 | 
								} `json:"offset"`
 | 
				
			||||||
			}
 | 
							} `json:"data"`
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		Timeouts struct {
 | 
					 | 
				
			||||||
			PollingTimeout uint `json:"polling_timeout"`
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	err = json.Unmarshal(b[1:len(b)-1], &v)
 | 
						err = json.Unmarshal(b, &v)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, errors.New("invalid metadata response")
 | 
							return nil, errors.New("invalid metadata response")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	onAir := v.Packages.OnAir
 | 
						if len(v.Data) == 0 || !v.Data[0].Offset.NowPlaying {
 | 
				
			||||||
	if onAir.BroadcastNowIndex >= uint(len(onAir.Broadcasts)) {
 | 
							return nil, errNoSong
 | 
				
			||||||
		return nil, errors.New("no active broadcast")
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	title := onAir.Broadcasts[onAir.BroadcastNowIndex].Title
 | 
					
 | 
				
			||||||
	for _, rt := range v.Packages.Richtracks {
 | 
						titles := v.Data[0].Titles
 | 
				
			||||||
		if rt.IsNowPlaying {
 | 
						parts := []string{titles.Primary}
 | 
				
			||||||
			title = rt.Artist + " - " + rt.Title + " / " + title
 | 
						if titles.Secondary != nil {
 | 
				
			||||||
		}
 | 
							parts = append(parts, *titles.Secondary)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return &meta{timeout: v.Timeouts.PollingTimeout, title: title}, nil
 | 
						if titles.Tertiary != nil {
 | 
				
			||||||
 | 
							parts = append(parts, *titles.Tertiary)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return &meta{timeout: 5000, title: strings.Join(parts, " - ")}, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// resolveM3U8 resolves an M3U8 playlist to the first link that seems to
 | 
					// resolveM3U8 resolves an M3U8 playlist to the first link that seems to
 | 
				
			||||||
// be playable, possibly recursing.
 | 
					// be playable, possibly recursing.
 | 
				
			||||||
func resolveM3U8(target string) (out []string, err error) {
 | 
					func resolveM3U8(target string) (out []string, err error) {
 | 
				
			||||||
	resp, err := http.Get(target)
 | 
						resp, err := get(target)
 | 
				
			||||||
	if resp != nil {
 | 
					 | 
				
			||||||
		defer resp.Body.Close()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	b, err := ioutil.ReadAll(resp.Body)
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b, err := io.ReadAll(resp.Body)
 | 
				
			||||||
 | 
						if resp.StatusCode != http.StatusOK {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("%s: %s", target, resp.Status)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	if !utf8.Valid(b) {
 | 
						if !utf8.Valid(b) {
 | 
				
			||||||
		return nil, errors.New("invalid UTF-8")
 | 
							return nil, errors.New("invalid UTF-8")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -117,6 +197,36 @@ func resolveM3U8(target string) (out []string, err error) {
 | 
				
			|||||||
	return out, nil
 | 
						return out, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const resolveURI = "https://lsn.lv/bbcradio.m3u8?station=%s"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var poolRE = regexp.MustCompile(`/pool_([^/]+)/`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// resolvePool figures out the randomized part of stream URIs.
 | 
				
			||||||
 | 
					func resolvePool(name string) (pool string, err error) {
 | 
				
			||||||
 | 
						target := fmt.Sprintf(resolveURI, url.QueryEscape(name))
 | 
				
			||||||
 | 
						resp, err := get(target)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						b, err := io.ReadAll(resp.Body)
 | 
				
			||||||
 | 
						if resp.StatusCode != http.StatusOK {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("%s: %s", target, resp.Status)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, line := range strings.Split(string(b), "\n") {
 | 
				
			||||||
 | 
							if strings.HasPrefix(line, "#") {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if m := poolRE.FindStringSubmatch(line); m == nil {
 | 
				
			||||||
 | 
								return "", fmt.Errorf("%s: unexpected URI", target)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return m[1], nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return "", fmt.Errorf("%s: no URI", target)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// metaProc periodically polls the sub-URL given by name for titles and sends
 | 
					// metaProc periodically polls the sub-URL given by name for titles and sends
 | 
				
			||||||
// them out the given channel. Never returns prematurely.
 | 
					// them out the given channel. Never returns prematurely.
 | 
				
			||||||
func metaProc(ctx context.Context, name string, out chan<- string) {
 | 
					func metaProc(ctx context.Context, name string, out chan<- string) {
 | 
				
			||||||
@@ -130,9 +240,10 @@ func metaProc(ctx context.Context, name string, out chan<- string) {
 | 
				
			|||||||
	var interval time.Duration
 | 
						var interval time.Duration
 | 
				
			||||||
	for {
 | 
						for {
 | 
				
			||||||
		meta, err := getMeta(name)
 | 
							meta, err := getMeta(name)
 | 
				
			||||||
		if err != nil {
 | 
							if err == errNoSong {
 | 
				
			||||||
			current = name + " - " + err.Error()
 | 
								interval, current = maxInterval, ""
 | 
				
			||||||
			interval = maxInterval
 | 
							} else if err != nil {
 | 
				
			||||||
 | 
								interval, current = maxInterval, err.Error()
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			current = meta.title
 | 
								current = meta.title
 | 
				
			||||||
			interval = time.Duration(meta.timeout) * time.Millisecond
 | 
								interval = time.Duration(meta.timeout) * time.Millisecond
 | 
				
			||||||
@@ -197,7 +308,7 @@ func dataProc(ctx context.Context, playlistURL string, maxChunk int,
 | 
				
			|||||||
	go urlProc(ctx, playlistURL, urls)
 | 
						go urlProc(ctx, playlistURL, urls)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for url := range urls {
 | 
						for url := range urls {
 | 
				
			||||||
		resp, err := http.Get(url)
 | 
							resp, err := get(url)
 | 
				
			||||||
		if resp != nil {
 | 
							if resp != nil {
 | 
				
			||||||
			defer resp.Body.Close()
 | 
								defer resp.Body.Close()
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -239,15 +350,20 @@ func proxy(w http.ResponseWriter, req *http.Request) {
 | 
				
			|||||||
		panic("cannot hijack connection")
 | 
							panic("cannot hijack connection")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// E.g. `nonuk`, `sbr_low` `bbc_radio_one`, or `uk`, `sbr_high`, `bbc_1xtra`
 | 
						// [ww]/[uk], [48000/96000]/[128000/320000], bbc_radio_one/bbc_1xtra/...
 | 
				
			||||||
	region, quality, name := m[1], m[2], m[3]
 | 
						region, quality, name, pool := m[1], m[2], m[3], "904"
 | 
				
			||||||
 | 
						if p, err := resolvePool(name); err != nil {
 | 
				
			||||||
 | 
							log.Printf("failed to resolve pool: %s\n", err)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							pool = p
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// TODO: We probably shouldn't poll the top level playlist.
 | 
						mediaPlaylistURL :=
 | 
				
			||||||
	mainPlaylistURL := fmt.Sprintf(targetURI, region, quality, name)
 | 
							fmt.Sprintf(targetURI, region, pool, region, name, name, name, quality)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// This validates the parameters as a side-effect.
 | 
						// This validates the parameters as a side-effect.
 | 
				
			||||||
	target, err := resolveM3U8(mainPlaylistURL)
 | 
						media, err := resolveM3U8(mediaPlaylistURL)
 | 
				
			||||||
	if err == nil && len(target) == 0 {
 | 
						if err == nil && len(media) == 0 {
 | 
				
			||||||
		err = errors.New("cannot resolve playlist")
 | 
							err = errors.New("cannot resolve playlist")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -256,7 +372,7 @@ func proxy(w http.ResponseWriter, req *http.Request) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	wantMeta := req.Header.Get("Icy-MetaData") == "1"
 | 
						wantMeta := req.Header.Get("Icy-MetaData") == "1"
 | 
				
			||||||
	resp, err := http.Head(target[0])
 | 
						resp, err := http.Head(media[0])
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
							http.Error(w, err.Error(), http.StatusInternalServerError)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -269,11 +385,10 @@ func proxy(w http.ResponseWriter, req *http.Request) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	defer conn.Close()
 | 
						defer conn.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// TODO: Retrieve some general information from somewhere?
 | 
						serviceTitle, _ := getServiceTitle(name)
 | 
				
			||||||
	// There's nothing interesting in the playlist files.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmt.Fprintf(bufrw, "ICY 200 OK\r\n")
 | 
						fmt.Fprintf(bufrw, "ICY 200 OK\r\n")
 | 
				
			||||||
	fmt.Fprintf(bufrw, "icy-name:%s\r\n", name)
 | 
						fmt.Fprintf(bufrw, "icy-name:%s\r\n", serviceTitle)
 | 
				
			||||||
	// BBC marks this as a video type, maybe just force audio/mpeg.
 | 
						// BBC marks this as a video type, maybe just force audio/mpeg.
 | 
				
			||||||
	fmt.Fprintf(bufrw, "content-type:%s\r\n", resp.Header["Content-Type"][0])
 | 
						fmt.Fprintf(bufrw, "content-type:%s\r\n", resp.Header["Content-Type"][0])
 | 
				
			||||||
	fmt.Fprintf(bufrw, "icy-pub:%d\r\n", 0)
 | 
						fmt.Fprintf(bufrw, "icy-pub:%d\r\n", 0)
 | 
				
			||||||
@@ -286,7 +401,7 @@ func proxy(w http.ResponseWriter, req *http.Request) {
 | 
				
			|||||||
	go metaProc(req.Context(), name, metaChan)
 | 
						go metaProc(req.Context(), name, metaChan)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	chunkChan := make(chan []byte)
 | 
						chunkChan := make(chan []byte)
 | 
				
			||||||
	go dataProc(req.Context(), mainPlaylistURL, metaint, chunkChan)
 | 
						go dataProc(req.Context(), mediaPlaylistURL, metaint, chunkChan)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// dataProc may return less data near the end of a subfile, so we give it
 | 
						// dataProc may return less data near the end of a subfile, so we give it
 | 
				
			||||||
	// a maximum count of bytes to return at once and do our own buffering.
 | 
						// a maximum count of bytes to return at once and do our own buffering.
 | 
				
			||||||
@@ -306,6 +421,9 @@ func proxy(w http.ResponseWriter, req *http.Request) {
 | 
				
			|||||||
	for {
 | 
						for {
 | 
				
			||||||
		select {
 | 
							select {
 | 
				
			||||||
		case title := <-metaChan:
 | 
							case title := <-metaChan:
 | 
				
			||||||
 | 
								if title == "" {
 | 
				
			||||||
 | 
									title = serviceTitle
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			queuedMetaUpdate = []byte(fmt.Sprintf("StreamTitle='%s'",
 | 
								queuedMetaUpdate = []byte(fmt.Sprintf("StreamTitle='%s'",
 | 
				
			||||||
				strings.Replace(title, "'", "’", -1)))
 | 
									strings.Replace(title, "'", "’", -1)))
 | 
				
			||||||
		case chunk, ok := <-chunkChan:
 | 
							case chunk, ok := <-chunkChan:
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user