From 810b3517725b7e1c4f66ec9e40400bf1d1cf24dd Mon Sep 17 00:00:00 2001 From: Pontus Jensen Karlsson Date: Sun, 18 Aug 2024 20:53:19 +0200 Subject: [PATCH] feat: add API routes `/v1/enclosures/{enclosureID}` --- client/client.go | 24 ++++++ client/model.go | 17 ++-- internal/api/api.go | 2 + internal/api/api_integration_test.go | 112 +++++++++++++++++++++++++++ internal/api/enclosure.go | 80 +++++++++++++++++++ internal/api/entry.go | 15 +--- internal/googlereader/handler.go | 13 +--- internal/model/enclosure.go | 43 +++++++++- internal/validator/enclosure.go | 18 +++++ 9 files changed, 291 insertions(+), 33 deletions(-) create mode 100644 internal/api/enclosure.go create mode 100644 internal/validator/enclosure.go diff --git a/client/client.go b/client/client.go index 2840c4e0..71ced695 100644 --- a/client/client.go +++ b/client/client.go @@ -613,6 +613,30 @@ func (c *Client) Icon(iconID int64) (*FeedIcon, error) { return feedIcon, nil } +func (c *Client) Enclosure(enclosureID int64) (*Enclosure, error) { + body, err := c.request.Get(fmt.Sprintf("/v1/enclosures/%d", enclosureID)) + + if err != nil { + return nil, err + } + + defer body.Close() + + var enclosure *Enclosure + + if err := json.NewDecoder(body).Decode(&enclosure); err != nil { + return nil, fmt.Errorf("miniflux: response error(%v)", err) + } + + return enclosure, nil +} + +func (c *Client) UpdateEnclosure(enclosureID int64, enclosureUpdate *EnclosureUpdateRequest) error { + _, err := c.request.Put(fmt.Sprintf("/v1/enclosures/%d", enclosureID), enclosureUpdate) + + return err +} + func buildFilterQueryString(path string, filter *Filter) string { if filter != nil { values := url.Values{} diff --git a/client/model.go b/client/model.go index 57b03d6e..69f2c227 100644 --- a/client/model.go +++ b/client/model.go @@ -242,12 +242,17 @@ type Entries []*Entry // Enclosure represents an attachment. type Enclosure struct { - ID int64 `json:"id"` - UserID int64 `json:"user_id"` - EntryID int64 `json:"entry_id"` - URL string `json:"url"` - MimeType string `json:"mime_type"` - Size int `json:"size"` + ID int64 `json:"id"` + UserID int64 `json:"user_id"` + EntryID int64 `json:"entry_id"` + URL string `json:"url"` + MimeType string `json:"mime_type"` + Size int `json:"size"` + MediaProgression int64 `json:"media_progression"` +} + +type EnclosureUpdateRequest struct { + MediaProgression int64 `json:"media_progression"` } // Enclosures represents a list of attachments. diff --git a/internal/api/api.go b/internal/api/api.go index 2963a990..1109acda 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -72,6 +72,8 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool) { sr.HandleFunc("/entries/{entryID}/fetch-content", handler.fetchContent).Methods(http.MethodGet) sr.HandleFunc("/flush-history", handler.flushHistory).Methods(http.MethodPut, http.MethodDelete) sr.HandleFunc("/icons/{iconID}", handler.getIconByIconID).Methods(http.MethodGet) + sr.HandleFunc("/enclosures/{enclosureID}", handler.getEnclosureById).Methods(http.MethodGet) + sr.HandleFunc("/enclosures/{enclosureID}", handler.updateEnclosureById).Methods(http.MethodPut) sr.HandleFunc("/version", handler.versionHandler).Methods(http.MethodGet) } diff --git a/internal/api/api_integration_test.go b/internal/api/api_integration_test.go index c4bef142..3fd98119 100644 --- a/internal/api/api_integration_test.go +++ b/internal/api/api_integration_test.go @@ -2044,6 +2044,118 @@ func TestGetGlobalEntriesEndpoint(t *testing.T) { } } +func TestUpdateEnclosureEndpoint(t *testing.T) { + testConfig := newIntegrationTestConfig() + if !testConfig.isConfigured() { + t.Skip(skipIntegrationTestsMessage) + } + + adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword) + + regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false) + if err != nil { + t.Fatal(err) + } + defer adminClient.DeleteUser(regularTestUser.ID) + + regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword) + + feedID, err := regularUserClient.CreateFeed(&miniflux.FeedCreationRequest{ + FeedURL: testConfig.testFeedURL, + }) + + if err != nil { + t.Fatal(err) + } + + result, err := regularUserClient.FeedEntries(feedID, nil) + if err != nil { + t.Fatalf(`Failed to get entries: %v`, err) + } + + var enclosure *miniflux.Enclosure + + for _, entry := range result.Entries { + if len(entry.Enclosures) > 0 { + enclosure = entry.Enclosures[0] + break + } + } + + if enclosure == nil { + t.Skip(`Skipping test, missing enclosure in feed.`) + } + + err = regularUserClient.UpdateEnclosure(enclosure.ID, &miniflux.EnclosureUpdateRequest{ + MediaProgression: 20, + }) + + if err != nil { + t.Fatal(err) + } + + updatedEnclosure, err := regularUserClient.Enclosure(enclosure.ID) + + if err != nil { + t.Fatal(err) + } + + if updatedEnclosure.MediaProgression != 20 { + t.Fatalf(`Failed to update media_progression, expected %d but got %d`, 20, updatedEnclosure.MediaProgression) + } +} + +func TestGetEnclosureEndpoint(t *testing.T) { + testConfig := newIntegrationTestConfig() + if !testConfig.isConfigured() { + t.Skip(skipIntegrationTestsMessage) + } + + adminClient := miniflux.NewClient(testConfig.testBaseURL, testConfig.testAdminUsername, testConfig.testAdminPassword) + + regularTestUser, err := adminClient.CreateUser(testConfig.genRandomUsername(), testConfig.testRegularPassword, false) + if err != nil { + t.Fatal(err) + } + defer adminClient.DeleteUser(regularTestUser.ID) + + regularUserClient := miniflux.NewClient(testConfig.testBaseURL, regularTestUser.Username, testConfig.testRegularPassword) + + feedID, err := regularUserClient.CreateFeed(&miniflux.FeedCreationRequest{ + FeedURL: testConfig.testFeedURL, + }) + if err != nil { + t.Fatal(err) + } + + result, err := regularUserClient.FeedEntries(feedID, nil) + if err != nil { + t.Fatalf(`Failed to get entries: %v`, err) + } + + var expectedEnclosure *miniflux.Enclosure + + for _, entry := range result.Entries { + if len(entry.Enclosures) > 0 { + expectedEnclosure = entry.Enclosures[0] + break + } + } + + if expectedEnclosure == nil { + t.Skip(`Skipping test, missing enclosure in feed.`) + } + + enclosure, err := regularUserClient.Enclosure(expectedEnclosure.ID) + + if err != nil { + t.Fatal(err) + } + + if enclosure.ID != expectedEnclosure.ID { + t.Fatalf(`Invalid enclosureID, got %d while expecting %d`, enclosure.ID, expectedEnclosure.ID) + } +} func TestGetEntryEndpoints(t *testing.T) { testConfig := newIntegrationTestConfig() if !testConfig.isConfigured() { diff --git a/internal/api/enclosure.go b/internal/api/enclosure.go new file mode 100644 index 00000000..45222b33 --- /dev/null +++ b/internal/api/enclosure.go @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package api // import "miniflux.app/v2/internal/api" + +import ( + json_parser "encoding/json" + "net/http" + + "miniflux.app/v2/internal/http/request" + "miniflux.app/v2/internal/http/response/json" + "miniflux.app/v2/internal/model" + "miniflux.app/v2/internal/validator" +) + +func (h *handler) getEnclosureById(w http.ResponseWriter, r *http.Request) { + enclosureID := request.RouteInt64Param(r, "enclosureID") + + enclosure, err := h.store.GetEnclosure(enclosureID) + + if err != nil { + json.NotFound(w, r) + return + } + + userID := request.UserID(r) + + if enclosure.UserID != userID { + json.NotFound(w, r) + return + } + + enclosure.ProxifyEnclosureURL(h.router) + + json.OK(w, r, enclosure) +} + +func (h *handler) updateEnclosureById(w http.ResponseWriter, r *http.Request) { + enclosureID := request.RouteInt64Param(r, "enclosureID") + + var enclosureUpdateRequest model.EnclosureUpdateRequest + + if err := json_parser.NewDecoder(r.Body).Decode(&enclosureUpdateRequest); err != nil { + json.BadRequest(w, r, err) + return + } + + if err := validator.ValidateEnclosureUpdateRequest(&enclosureUpdateRequest); err != nil { + json.BadRequest(w, r, err) + return + } + + enclosure, err := h.store.GetEnclosure(enclosureID) + + if err != nil { + json.BadRequest(w, r, err) + return + } + + if enclosure == nil { + json.NotFound(w, r) + return + } + + userID := request.UserID(r) + + if enclosure.UserID != userID { + json.NotFound(w, r) + return + } + + enclosure.MediaProgression = enclosureUpdateRequest.MediaProgression + + if err := h.store.UpdateEnclosure(enclosure); err != nil { + json.ServerError(w, r, err) + return + } + + json.NoContent(w, r) +} diff --git a/internal/api/entry.go b/internal/api/entry.go index 121d2701..508bbee4 100644 --- a/internal/api/entry.go +++ b/internal/api/entry.go @@ -8,10 +8,8 @@ import ( "errors" "net/http" "strconv" - "strings" "time" - "miniflux.app/v2/internal/config" "miniflux.app/v2/internal/http/request" "miniflux.app/v2/internal/http/response/json" "miniflux.app/v2/internal/integration" @@ -20,7 +18,6 @@ import ( "miniflux.app/v2/internal/reader/processor" "miniflux.app/v2/internal/reader/readingtime" "miniflux.app/v2/internal/storage" - "miniflux.app/v2/internal/urllib" "miniflux.app/v2/internal/validator" ) @@ -37,18 +34,8 @@ func (h *handler) getEntryFromBuilder(w http.ResponseWriter, r *http.Request, b } entry.Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entry.Content) - proxyOption := config.Opts.MediaProxyMode() - for i := range entry.Enclosures { - if proxyOption == "all" || proxyOption != "none" && !urllib.IsHTTPS(entry.Enclosures[i].URL) { - for _, mediaType := range config.Opts.MediaProxyResourceTypes() { - if strings.HasPrefix(entry.Enclosures[i].MimeType, mediaType+"/") { - entry.Enclosures[i].URL = mediaproxy.ProxifyAbsoluteURL(h.router, entry.Enclosures[i].URL) - break - } - } - } - } + entry.Enclosures.ProxifyEnclosureURL(h.router) json.OK(w, r, entry) } diff --git a/internal/googlereader/handler.go b/internal/googlereader/handler.go index 7cce675c..c3eb70af 100644 --- a/internal/googlereader/handler.go +++ b/internal/googlereader/handler.go @@ -24,7 +24,6 @@ import ( mff "miniflux.app/v2/internal/reader/handler" mfs "miniflux.app/v2/internal/reader/subscription" "miniflux.app/v2/internal/storage" - "miniflux.app/v2/internal/urllib" "miniflux.app/v2/internal/validator" "github.com/gorilla/mux" @@ -1004,18 +1003,8 @@ func (h *handler) streamItemContentsHandler(w http.ResponseWriter, r *http.Reque } entry.Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entry.Content) - proxyOption := config.Opts.MediaProxyMode() - for i := range entry.Enclosures { - if proxyOption == "all" || proxyOption != "none" && !urllib.IsHTTPS(entry.Enclosures[i].URL) { - for _, mediaType := range config.Opts.MediaProxyResourceTypes() { - if strings.HasPrefix(entry.Enclosures[i].MimeType, mediaType+"/") { - entry.Enclosures[i].URL = mediaproxy.ProxifyAbsoluteURL(h.router, entry.Enclosures[i].URL) - break - } - } - } - } + entry.Enclosures.ProxifyEnclosureURL(h.router) contentItems[i] = contentItem{ ID: fmt.Sprintf(EntryIDLong, entry.ID), diff --git a/internal/model/enclosure.go b/internal/model/enclosure.go index c9ec485f..0e794575 100644 --- a/internal/model/enclosure.go +++ b/internal/model/enclosure.go @@ -2,7 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 package model // import "miniflux.app/v2/internal/model" -import "strings" +import ( + "strings" + + "github.com/gorilla/mux" + "miniflux.app/v2/internal/config" + "miniflux.app/v2/internal/mediaproxy" + "miniflux.app/v2/internal/urllib" +) // Enclosure represents an attachment. type Enclosure struct { @@ -15,6 +22,10 @@ type Enclosure struct { MediaProgression int64 `json:"media_progression"` } +type EnclosureUpdateRequest struct { + MediaProgression int64 `json:"media_progression"` +} + // Html5MimeType will modify the actual MimeType to allow direct playback from HTML5 player for some kind of MimeType func (e Enclosure) Html5MimeType() string { if e.MimeType == "video/m4v" { @@ -34,3 +45,33 @@ func (el EnclosureList) ContainsAudioOrVideo() bool { } return false } + +func (el EnclosureList) ProxifyEnclosureURL(router *mux.Router) { + proxyOption := config.Opts.MediaProxyMode() + + if proxyOption == "all" || proxyOption != "none" { + for i := range el { + if urllib.IsHTTPS(el[i].URL) { + for _, mediaType := range config.Opts.MediaProxyResourceTypes() { + if strings.HasPrefix(el[i].MimeType, mediaType+"/") { + el[i].URL = mediaproxy.ProxifyAbsoluteURL(router, el[i].URL) + break + } + } + } + } + } +} + +func (e *Enclosure) ProxifyEnclosureURL(router *mux.Router) { + proxyOption := config.Opts.MediaProxyMode() + + if proxyOption == "all" || proxyOption != "none" && !urllib.IsHTTPS(e.URL) { + for _, mediaType := range config.Opts.MediaProxyResourceTypes() { + if strings.HasPrefix(e.MimeType, mediaType+"/") { + e.URL = mediaproxy.ProxifyAbsoluteURL(router, e.URL) + break + } + } + } +} diff --git a/internal/validator/enclosure.go b/internal/validator/enclosure.go new file mode 100644 index 00000000..b33ac22e --- /dev/null +++ b/internal/validator/enclosure.go @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package validator + +import ( + "fmt" + + "miniflux.app/v2/internal/model" +) + +func ValidateEnclosureUpdateRequest(request *model.EnclosureUpdateRequest) error { + if request.MediaProgression < 0 { + return fmt.Errorf(`media progression must an positive integer`) + } + + return nil +}