Compare commits

..

No commits in common. "8a087dea4a923254b564d992d15080b11494acf7" and "29b160388678213201460638fcc58f3a7ce1199f" have entirely different histories.

4 changed files with 49 additions and 52 deletions

View File

@ -1,4 +1,4 @@
Copyright (c) 2016 - 2024, Přemysl Eric Janouch <p@janouch.name> Copyright (c) 2016 - 2018, Přemysl 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.

View File

@ -19,19 +19,14 @@ 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, derived from Streams have URLs in the following form:
https://gist.github.com/bpsib/67089b959e4fa898af69fea59ad74bc3[this list]:
$ mpv http://localhost:8000/ww/96000/bbc_radio_one $ mpv http://localhost:8000/nonuk/sbr_low/bbc_radio_one
$ mpv http://localhost:8000/uk/320000/bbc_1xtra $ mpv http://localhost:8000/uk/sbr_high/bbc_1xtra
Socket activation Socket activation
----------------- -----------------

3
go.mod
View File

@ -1,3 +0,0 @@
module janouch.name/bbc-on-ice
go 1.22.0

85
main.go
View File

@ -21,9 +21,9 @@ import (
) )
const ( const (
targetURI = "http://as-hls-%s-live.akamaized.net/pool_904/live/%s/" + targetURI = "http://a.files.bbci.co.uk/media/live/manifesto/" +
"%s/%s.isml/%s-audio%%3d%s.norewind.m3u8" "audio/simulcast/hls/%s/%s/ak/%s.m3u8"
metaURI = "https://rms.api.bbc.co.uk/v2/services/%s/segments/latest" metaBaseURI = "http://polling.bbc.co.uk/radio/nhppolling/"
) )
type meta struct { type meta struct {
@ -33,7 +33,7 @@ type meta struct {
// 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(fmt.Sprintf(metaURI, name)) resp, err := http.Get(metaBaseURI + name)
if resp != nil { if resp != nil {
defer resp.Body.Close() defer resp.Body.Close()
} }
@ -41,41 +41,46 @@ func getMeta(name string) (*meta, error) {
return nil, err return nil, err
} }
b, err := ioutil.ReadAll(resp.Body) b, err := ioutil.ReadAll(resp.Body)
if len(b) < 2 {
// TODO: update more completely for the new OpenAPI // There needs to be an enclosing () pair
// - `broadcasts/poll/bbc_radio_one` looks almost useful return nil, errors.New("invalid metadata response")
// - 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 {
Data []struct {
Titles struct {
Primary string `json:"primary"`
Secondary *string `json:"secondary"`
Tertiary *string `json:"tertiary"`
} `json:"titles"`
Offset struct {
NowPlaying bool `json:"now_playing"`
} `json:"offset"`
} `json:"data"`
} }
err = json.Unmarshal(b, &v)
type broadcast struct {
Title string // title of the broadcast
Percentage int // how far we're in
}
var v struct {
Packages struct {
OnAir struct {
Broadcasts []broadcast
BroadcastNowIndex uint
} `json:"on-air"`
Richtracks []struct {
Artist string
Title string
IsNowPlaying bool `json:"is_now_playing"`
}
}
Timeouts struct {
PollingTimeout uint `json:"polling_timeout"`
}
}
err = json.Unmarshal(b[1:len(b)-1], &v)
if err != nil { if err != nil {
return nil, errors.New("invalid metadata response") return nil, errors.New("invalid metadata response")
} }
if len(v.Data) == 0 || !v.Data[0].Offset.NowPlaying { onAir := v.Packages.OnAir
return nil, errors.New("no song is playing") if onAir.BroadcastNowIndex >= uint(len(onAir.Broadcasts)) {
return nil, errors.New("no active broadcast")
} }
title := onAir.Broadcasts[onAir.BroadcastNowIndex].Title
titles := v.Data[0].Titles for _, rt := range v.Packages.Richtracks {
parts := []string{} if rt.IsNowPlaying {
if titles.Tertiary != nil { title = rt.Artist + " - " + rt.Title + " / " + title
parts = append(parts, *titles.Tertiary) }
} }
if titles.Secondary != nil { return &meta{timeout: v.Timeouts.PollingTimeout, title: title}, nil
parts = append(parts, *titles.Secondary)
}
parts = append(parts, titles.Primary)
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
@ -234,15 +239,15 @@ func proxy(w http.ResponseWriter, req *http.Request) {
panic("cannot hijack connection") panic("cannot hijack connection")
} }
// [ww]/[uk], [48000/96000]/[128000/320000], bbc_radio_one/bbc_1xtra/... // E.g. `nonuk`, `sbr_low` `bbc_radio_one`, or `uk`, `sbr_high`, `bbc_1xtra`
region, quality, name := m[1], m[2], m[3] region, quality, name := m[1], m[2], m[3]
mediaPlaylistURL := // TODO: We probably shouldn't poll the top level playlist.
fmt.Sprintf(targetURI, region, region, name, name, name, quality) mainPlaylistURL := fmt.Sprintf(targetURI, region, quality, name)
// This validates the parameters as a side-effect. // This validates the parameters as a side-effect.
media, err := resolveM3U8(mediaPlaylistURL) target, err := resolveM3U8(mainPlaylistURL)
if err == nil && len(media) == 0 { if err == nil && len(target) == 0 {
err = errors.New("cannot resolve playlist") err = errors.New("cannot resolve playlist")
} }
if err != nil { if err != nil {
@ -251,7 +256,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(media[0]) resp, err := http.Head(target[0])
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@ -281,7 +286,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(), mediaPlaylistURL, metaint, chunkChan) go dataProc(req.Context(), mainPlaylistURL, 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.