summaryrefslogtreecommitdiff
path: root/main.go
blob: d440abd6c4c0ee03322bd7521f2ae2f1bbd5fce2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// Copyright 2020 Nick White.
// Use of this source code is governed by the GPLv3
// license that can be found in the LICENSE file.

// bbcschedule lists a day schedule from a BBC radio station (which
// can be in the past, present or future), including IDs that work
// with get_iplayer.
//
// This uses the BBC's broadcasts/schedules api, which is roughly
// documented at https://rms.api.bbc.co.uk/docs and
// https://rms.api.bbc.co.uk/docs/swagger.json
//
// An alternative to these APIs would be doing what get_iplayer does
// in get_links_schedule_mojo and get_links_schedule_json and
// extracting the contents of the <script type="application/ld+json">
// with "@graph" from an HTML page like
// https://www.bbc.co.uk/schedules/p00fzl7j/2020/11/14
// But who wants to parse HTML if it can be avoided, eh?
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"strings"
	"time"
)

const usage = `Usage: bbcschedule [-station stationname] [-date yyyy-mm-dd] [-v]

Lists the schedule for a BBC radio station.
`

const urlBase = "https://rms.api.bbc.co.uk/v2/broadcasts/schedules/%s/%s"

// Broadcasts holds the parts of the JSON response that we care about.
// It is intended to be filled by json.Unmarshal.
type Broadcasts struct {
	Data []struct {
		Urn string
		Start string
		Duration int
		Titles struct {
			Primary string
		}
		Synopses struct {
			Short string
			Medium string
			Long string
		}
	}
}

// fmtDate just reformats an RFC3339 date into a "Kitchen" style time.
func fmtDate(d string) string {
	t, err := time.Parse(time.RFC3339, d)
	if err != nil {
		return d
	}
	return t.Format(time.Kitchen)
}

// idFromUrn gets the ID from an iPlayer URN. This is the appropriate
// ID to give to get_iplayer, so it's what we use.
func idFromUrn(u string) string {
	s := strings.Split(u, ":")
	if len(s) != 5 {
		return u
	}
	return s[4]
}

func main() {
	station := flag.String("station", "bbc_radio_fourfm", "Radio station name (called 'service id' by BBC)")
	date := flag.String("date", time.Now().Format("2006-01-02"), "Date of schedule")
	verbose := flag.Bool("v", false, "Verbose - use long descriptions for programmes")

	flag.Usage = func() {
		fmt.Fprintf(flag.CommandLine.Output(), usage)
		flag.PrintDefaults()
	}
	flag.Parse()

	if flag.NArg() > 0 {
		flag.Usage()
		return
	}

	url := fmt.Sprintf(urlBase, *station, *date)
	resp, err := http.Get(url)
	if err != nil {
		log.Fatalf("Error getting url %s: %v\n", url, err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		log.Fatalf("Error getting url %s, HTTP status code: %d\n", url, resp.StatusCode)
	}

	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("Error reading response for %s: %v\n", url, err)
	}

	var r Broadcasts
	err = json.Unmarshal(b, &r)
	if err != nil {
		log.Fatalf("Error unmarshalling json for %s: %v\n", url, err)
	}

	var longest int
	for _, v := range r.Data {
		l := len(v.Titles.Primary)
		if l > longest {
			longest = l
		}
	}
	for _, v := range r.Data {
		fmtString := fmt.Sprintf("%%7s | %%-%ds | %%3d minutes | %%s | %%s\n", longest)
		var synopsis string
		if *verbose && len(v.Synopses.Long) > 0 {
			synopsis = v.Synopses.Long
		} else if *verbose && len(v.Synopses.Medium) > 0 {
			synopsis = v.Synopses.Medium
		} else {
			synopsis = v.Synopses.Short
		}
		fmt.Printf(fmtString, fmtDate(v.Start), v.Titles.Primary, v.Duration / 60, idFromUrn(v.Urn), synopsis)
	}
}