From 1aa86712be29a0cd9bfebbe27fd46eccda9bf3d0 Mon Sep 17 00:00:00 2001 From: Blusky Date: Wed, 6 Dec 2023 19:18:05 +0100 Subject: [PATCH 1/3] Implement HandleEpisodeAction --- go.mod | 1 + go.sum | 2 ++ pkg/apis/handlers.go | 14 ++++++++++- pkg/data/sqlite.go | 57 ++++++++++++++++++++++++++++++++++++++++++-- pkg/data/types.go | 2 +- 5 files changed, 72 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 76e7c98..95000b5 100644 --- a/go.mod +++ b/go.mod @@ -13,4 +13,5 @@ require ( github.com/spf13/cobra v1.4.0 golang.org/x/text v0.3.8 // indirect modernc.org/sqlite v1.26.0 + github.com/relvacode/iso8601 v1.3.0 ) diff --git a/go.sum b/go.sum index 8431190..7733300 100644 --- a/go.sum +++ b/go.sum @@ -975,6 +975,8 @@ github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/relvacode/iso8601 v1.3.0 h1:HguUjsGpIMh/zsTczGN3DVJFxTU/GX+MMmzcKoMO7ko= +github.com/relvacode/iso8601 v1.3.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= diff --git a/pkg/apis/handlers.go b/pkg/apis/handlers.go index 3b6ac04..b5de0da 100644 --- a/pkg/apis/handlers.go +++ b/pkg/apis/handlers.go @@ -482,8 +482,20 @@ func (e *EpisodeAPI) HandleEpisodeAction(w http.ResponseWriter, r *http.Request) // since (int) optional also, if no actions, then release all // aggregated (bool) + username := chi.URLParam(r, "username") + device := r.URL.Query().Get("device") + since := r.URL.Query().Get("since") + + actions, err := e.Data.RetrieveEpisodeActionHistory(username, device, since) + + if err != nil { + log.Printf("error retrieving episodes actions output: %#v", err) + w.WriteHeader(400) + return + } + episodeActionOutput := &EpisodeActionOutput{ - Actions: []data.EpisodeAction{}, + Actions: actions, Timestamp: timestamp.Now(), } diff --git a/pkg/data/sqlite.go b/pkg/data/sqlite.go index 12e0310..3fb8d2b 100644 --- a/pkg/data/sqlite.go +++ b/pkg/data/sqlite.go @@ -9,6 +9,7 @@ import ( "github.com/oxtyped/go-opml/opml" "github.com/pkg/errors" + "github.com/relvacode/iso8601" _ "modernc.org/sqlite" ) @@ -129,9 +130,61 @@ func (l *SQLite) AddEpisodeActionHistory(username string, e EpisodeAction) error return nil } -func (l *SQLite) RetrieveEpisodeActionHistory(username string, deviceId string, since time.Time) ([]EpisodeAction, error) { +func (l *SQLite) RetrieveEpisodeActionHistory(username string, device string, since string) ([]EpisodeAction, error) { + db := l.db + + actions := []EpisodeAction{} + + query := "SELECT a.podcast, a.episode, d.name, a.action, a.position, a.started, a.total, a.timestamp" + query = query + " FROM episode_actions as a, devices as d, users as u" + query = query + " WHERE a.device_id = d.id AND d.user_id = u.id AND u.name = ?" + var args []interface{} + args = append(args, username) + if device != "" { + query = query + " AND d.name = ?" + args = append(args, device) + } + if since != "" { + parsedTs, err := iso8601.ParseString(since) + if err != nil { + return nil, err + } + query = query + " AND a.timestamp > ?" + args = append(args, parsedTs.Unix()) + } + query = query + " ORDER BY a.id" + rows, err := db.Query(query, args...) + if err != nil { + return nil, err + } + + for rows.Next() { + a := EpisodeAction{} + var ts string + err := rows.Scan( + &a.Podcast, + &a.Episode, + &a.Device, + &a.Action, + &a.Position, + &a.Started, + &a.Total, + &ts, + ) + timestamp := CustomTimestamp{} + g, _ := strconv.ParseInt(ts, 10, 64) + timestamp.Time = time.Unix(g, 0) + a.Timestamp = timestamp + if err != nil { + log.Printf("error scanning: %#v", err) + continue + } + + actions = append(actions, a) + + } - return []EpisodeAction{}, nil + return actions, nil } // GetDevicesFromUsername returns a list of device names that belongs to diff --git a/pkg/data/types.go b/pkg/data/types.go index e4a08d7..c23cfba 100644 --- a/pkg/data/types.go +++ b/pkg/data/types.go @@ -11,7 +11,7 @@ type DataInterface interface { AddSubscriptionHistory(Subscription) error RetrieveSubscriptionHistory(string, string, time.Time) ([]Subscription, error) AddEpisodeActionHistory(username string, e EpisodeAction) error - RetrieveEpisodeActionHistory(username string, deviceId string, since time.Time) ([]EpisodeAction, error) + RetrieveEpisodeActionHistory(username string, device string, since string) ([]EpisodeAction, error) // RetrieveLoginToken(username string, password string) (string, error) RetrieveDevices(username string) ([]Device, error) AddDevice(username string, deviceName string, caption string, deviceType string) error From c2a1b3b80e6ed76b4a9cea53885226986dda0c35 Mon Sep 17 00:00:00 2001 From: Michael Hrivnak Date: Thu, 12 Dec 2024 18:27:08 -0500 Subject: [PATCH 2/3] fixes timestamp filtering of episode actions The timestamp is being stored in the DB as an integer, but in a field of type varchar(255). So the DB cannot be used to sort of filter, because it would sort the integers alphabetically. The workaround is to load all of the results into memory and then throw away any that don't meet the optional `since=` filter. fixes #39 --- README.md | 11 +++++++++-- go.mod | 1 - go.sum | 2 -- pkg/data/sqlite.go | 32 ++++++++++++++++---------------- pkg/data/types.go | 2 +- 5 files changed, 26 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 7f04c6c..152a912 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,16 @@ $ gpodder2go serve --no-auth Alternatively, you can switch to use [Antennapod](https://antennapod.org/) which has implemented the login spec which gpodder2go currently supports. -### Supports +### Supported Clients -- [Antennapod](https://antennapod.org/) +#### [Antennapod](https://antennapod.org/) + +These features are all working with Antennapod: + - Authentication API + - Subscriptions API + - Episode Actions API + - Device API + - Device Synchronization API ### Development diff --git a/go.mod b/go.mod index 3c12660..017e5c9 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,6 @@ require ( github.com/spf13/cobra v1.4.0 k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b modernc.org/sqlite v1.26.0 - github.com/relvacode/iso8601 v1.3.0 ) require ( diff --git a/go.sum b/go.sum index f0f9292..9bcf121 100644 --- a/go.sum +++ b/go.sum @@ -971,8 +971,6 @@ github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/relvacode/iso8601 v1.3.0 h1:HguUjsGpIMh/zsTczGN3DVJFxTU/GX+MMmzcKoMO7ko= -github.com/relvacode/iso8601 v1.3.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= diff --git a/pkg/data/sqlite.go b/pkg/data/sqlite.go index f7126df..e78217a 100644 --- a/pkg/data/sqlite.go +++ b/pkg/data/sqlite.go @@ -140,17 +140,11 @@ func (s *SQLite) RetrieveDevices(username string) ([]Device, error) { func (l *SQLite) AddEpisodeActionHistory(username string, e EpisodeAction) error { db := l.db - tx, err := db.Begin() - if err != nil { - return err - } - _, err = tx.Exec("INSERT INTO episode_actions(device_id, podcast, episode, action, position, started, total, timestamp) VALUES (?,?,?,?,?,?,?,?)", e.Device, e.Podcast, e.Episode, e.Action, e.Position, e.Started, e.Total, e.Timestamp.Unix()) + _, err := db.Exec("INSERT INTO episode_actions(device_id, podcast, episode, action, position, started, total, timestamp) VALUES (?,?,?,?,?,?,?,?)", e.Device, e.Podcast, e.Episode, e.Action, e.Position, e.Started, e.Total, e.Timestamp.Unix()) if err != nil { - tx.Rollback() return err } - tx.Commit() return nil } @@ -161,14 +155,8 @@ func (l *SQLite) RetrieveEpisodeActionHistory(username string, deviceId string, query := "SELECT a.podcast, a.episode, a.device_id, a.action, a.position, a.started, a.total, a.timestamp" query = query + " FROM episode_actions as a, devices as d, users as u" - query = query + " WHERE a.device_id = d.name AND d.user_id = u.id AND u.username = ?" - var args []interface{} - args = append(args, username) - if !since.IsZero() { - query = query + " AND a.timestamp > ?" - args = append(args, since) - } - query = query + " ORDER BY a.id" + query = query + " WHERE a.device_id = d.name AND d.user_id = u.id AND u.username = ? ORDER BY a.id" + args := []interface{}{username} rows, err := db.Query(query, args...) if err != nil { return nil, err @@ -200,8 +188,20 @@ func (l *SQLite) RetrieveEpisodeActionHistory(username string, deviceId string, timestamp.Time = time.Unix(g, 0) a.Timestamp = timestamp - actions = append(actions, a) + // For some reason, the timestamp for episode actions has been stored as + // an integer, but in a field of type varchar(255). (that's why you see + // the ParseInt call above). So we cannot use a DB query to sort or + // filter by timestamp, because the DB would sort the integers + // alphabetically. Someone should probably fix that by making a + // migration to change the timestamp type in the DB, and then let the DB + // handle the filtering. + if !since.IsZero() { + if a.Timestamp.Before(since) { + continue + } + } + actions = append(actions, a) } return actions, nil diff --git a/pkg/data/types.go b/pkg/data/types.go index 394456c..19e686a 100644 --- a/pkg/data/types.go +++ b/pkg/data/types.go @@ -38,7 +38,7 @@ type Subscription struct { Devices []int `json:"devices"` Podcast string `json:"podcast"` Action string `json:"action"` - Timestamp CustomTimestamp `json:"timestamp"` + Timestamp CustomTimestamp `json:"timestamp"` // sqlite stores this as a varchar(255) } type Device struct { From 9b546da1c082983383b4a72f48a476dc6e54f99c Mon Sep 17 00:00:00 2001 From: Michael Hrivnak Date: Fri, 13 Dec 2024 23:06:21 -0500 Subject: [PATCH 3/3] adds docs on how to use with Antennapod --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 152a912..6d2f043 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,18 @@ These features are all working with Antennapod: - Device API - Device Synchronization API +To start using with two devices, especially if you want to transfer state +from old_phone to new_phone: + +#. Log in with old_phone and force a full sync. +#. Log in with new_phone, but select old_phone as the device. Subscriptions will sync. +#. Log out with new_phone. +#. Log in with new_phone and create a new device ID for it. +#. Use the API (such as with curl) to [create a sync group](https://gpoddernet.readthedocs.io/en/latest/api/reference/sync.html#device-synchronization-api) with both devices. + +After that, episode state will sync between them, and a new subscription on +either one will propagate to the other. + ### Development ```