clone

a desktop tv and program guide for over-the-air broadcasts: what's on air, what your tuner gets, live tv, recordings

Be a better Schedules Direct client, and take a subscription's pictures

FelineStateMachine committed a2588d9
From a review against what got other clients banned: a 4003 on anything
but a login is an old token (as Schedules Direct sometimes says), so it
gets one new login rather than stopping the account; pictures are
fetched from where they redirect without the token going along to S3;
programs are asked for 2000 at a time rather than the 5000 allowed; and
up to 2000 pictures an update, of a subscriber's 5000 a day. Saving the
account again lifts a picture pause, as a subscription lifts a trial's
cap.
commit
a2588d9a50700d8c33a35bbe8356f87f04d6af32
parent
9633ca2e7b6a5c1e87223a037d485fc6a0029978
author
FelineStateMachine <226808055+FelineStateMachine@users.noreply.github.com>
kind
fix risk score 1.7 of 3, Jev's estimate
4 files changed, +77 -7
Minternal/sd/art.go18++++++++--
Minternal/sd/client.go7++--
Minternal/sd/sd_test.go55++++++++++++++++++++++++++++++
Minternal/sd/source.go4++-
M internal/sd/art.go +15 -3
M internal/sd/client.go +4 -3
M internal/sd/sd_test.go +55 -0
@@ -25,6 +25,8 @@ type fakeSD struct {
2525 md5 string2626 images []string // pictures downloaded2727 capped bool // pictures answer with the daily cap28+ stale bool // the next request gets 4003 once, as for an old token29+ s3 string // where pictures redirect to, when set2830 }2931 3032 func (f *fakeSD) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -61,6 +63,11 @@ func (f *fakeSD) ServeHTTP(w http.ResponseWriter, r *http.Request) {
6163 fail(403, 4006, "TOKEN_EXPIRED")6264 return6365 }66+ if f.stale {67+ f.stale = false68+ fail(400, 4003, "INVALID_USER")69+ return70+ }6471 today := time.Now().UTC().Format(time.DateOnly)6572 if uri, ok := strings.CutPrefix(path, "image/"); ok {6673 if f.capped {
@@ -68,6 +75,10 @@ func (f *fakeSD) ServeHTTP(w http.ResponseWriter, r *http.Request) {
6875 return6976 }7077 f.images = append(f.images, uri)78+ if f.s3 != "" {79+ http.Redirect(w, r, f.s3+"/"+uri, http.StatusSeeOther)80+ return81+ }7182 w.Header().Set("Content-Type", "image/jpeg")7283 fmt.Fprint(w, "JPEG "+uri)7384 return
@@ -457,3 +468,47 @@ func TestArtworkCap(t *testing.T) {
457468 t.Fatal("a picture without a download")458469 }459470 }471+472+// TestStaleToken: a 4003 outside a login means an old token: one new473+// login, and the update goes on.474+func TestStaleToken(t *testing.T) {475+ s, f := newSource(t)476+ _ = s.SetAccount("viewer", "hunter2")477+ a, _ := LoadAccount(s.accountPath())478+ s.update(context.Background(), a)479+ f.mu.Lock()480+ f.stale, f.md5 = true, "day5"481+ f.mu.Unlock()482+ s.update(context.Background(), a)483+ if st := s.Status(); !st.OK || st.Stopped != "" || f.count("POST token") != 2 {484+ t.Fatalf("status %+v, %d logins", st, f.count("POST token"))485+ }486+}487+488+// TestPictureRedirect: pictures are fetched from where Schedules Direct489+// redirects, without the token.490+func TestPictureRedirect(t *testing.T) {491+ var mu sync.Mutex492+ var tokens []string493+ s3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {494+ mu.Lock()495+ tokens = append(tokens, r.Header.Get("Token"))496+ mu.Unlock()497+ w.Header().Set("Content-Type", "image/jpeg")498+ fmt.Fprint(w, "JPEG from S3")499+ }))500+ defer s3.Close()501+ s, f := newSource(t)502+ f.s3 = strings.Replace(s3.URL, "127.0.0.1", "localhost", 1) // another host503+ _ = s.SetAccount("viewer", "hunter2")504+ a, _ := LoadAccount(s.accountPath())505+ s.update(context.Background(), a)506+ mu.Lock()507+ defer mu.Unlock()508+ if len(tokens) != 2 || tokens[0] != "" || tokens[1] != "" {509+ t.Fatalf("S3 saw tokens %q", tokens)510+ }511+ if s.Guide().Programs["20370"][0].Image == "" {512+ t.Fatal("no picture")513+ }514+}
M internal/sd/source.go +3 -1