diff --git a/README.md b/README.md index 7f04c6c..6d2f043 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,28 @@ $ 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 + +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 diff --git a/pkg/apis/handlers.go b/pkg/apis/handlers.go index db9d701..6d412e7 100644 --- a/pkg/apis/handlers.go +++ b/pkg/apis/handlers.go @@ -531,8 +531,31 @@ 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") + + var parsedTs time.Time + if since != "" { + sinceInt, err := strconv.Atoi(since) + if err != nil { + log.Printf("error parsing since: %#v", err) + w.WriteHeader(400) + return + } + parsedTs = time.Unix(int64(sinceInt), 0).UTC() + } + + actions, err := e.Data.RetrieveEpisodeActionHistory(username, device, parsedTs) + + 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(), } @@ -570,6 +593,8 @@ func (e *EpisodeAPI) HandleUploadEpisodeAction(w http.ResponseWriter, r *http.Re err := e.Data.AddEpisodeActionHistory(username, data) if err != nil { log.Printf("error adding episode action into history: %#v", err) + w.WriteHeader(400) + return } pair := Pair{ data.Episode, data.Episode, diff --git a/pkg/data/sqlite.go b/pkg/data/sqlite.go index 83e23b9..e78217a 100644 --- a/pkg/data/sqlite.go +++ b/pkg/data/sqlite.go @@ -139,33 +139,72 @@ func (s *SQLite) RetrieveDevices(username string) ([]Device, error) { } func (l *SQLite) AddEpisodeActionHistory(username string, e EpisodeAction) error { - db := l.db - tx, err := db.Begin() + + _, 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 { return err } - defer tx.Rollback() + return nil +} + +func (l *SQLite) RetrieveEpisodeActionHistory(username string, deviceId string, since time.Time) ([]EpisodeAction, error) { + db := l.db - deviceIds := e.Devices + actions := []EpisodeAction{} - for _, deviceId := range deviceIds { - // deviceId, err := l.GetDeviceIdFromName(v, username) - // if err != nil { - // return err - // } + 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 = ? ORDER BY a.id" + args := []interface{}{username} + rows, err := db.Query(query, args...) + if err != nil { + return nil, err + } - _, err = tx.Exec("INSERT INTO episode_actions(device_id, podcast, episode, action, position, started, total, timestamp) VALUES (?,?,?,?,?,?,?,?)", deviceId, e.Podcast, e.Episode, e.Action, e.Position, e.Started, e.Total, e.Timestamp.Unix()) + 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, + ) if err != nil { - return err + log.Printf("error scanning: %#v", err) + continue + } + timestamp := CustomTimestamp{} + g, err := strconv.ParseInt(ts, 10, 64) + if err != nil { + log.Printf("error scanning: %#v", err) + continue + } + timestamp.Time = time.Unix(g, 0) + a.Timestamp = timestamp + + // 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) } - tx.Commit() - return nil -} -func (l *SQLite) RetrieveEpisodeActionHistory(username string, deviceId string, since time.Time) ([]EpisodeAction, error) { - 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 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 {