feat: add API routes /v1/enclosures/{enclosureID}
This commit is contained in:
parent
89ff33ddd0
commit
810b351772
9 changed files with 291 additions and 33 deletions
|
@ -613,6 +613,30 @@ func (c *Client) Icon(iconID int64) (*FeedIcon, error) {
|
||||||
return feedIcon, nil
|
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 {
|
func buildFilterQueryString(path string, filter *Filter) string {
|
||||||
if filter != nil {
|
if filter != nil {
|
||||||
values := url.Values{}
|
values := url.Values{}
|
||||||
|
|
|
@ -242,12 +242,17 @@ type Entries []*Entry
|
||||||
|
|
||||||
// Enclosure represents an attachment.
|
// Enclosure represents an attachment.
|
||||||
type Enclosure struct {
|
type Enclosure struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
EntryID int64 `json:"entry_id"`
|
EntryID int64 `json:"entry_id"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
MimeType string `json:"mime_type"`
|
MimeType string `json:"mime_type"`
|
||||||
Size int `json:"size"`
|
Size int `json:"size"`
|
||||||
|
MediaProgression int64 `json:"media_progression"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnclosureUpdateRequest struct {
|
||||||
|
MediaProgression int64 `json:"media_progression"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enclosures represents a list of attachments.
|
// Enclosures represents a list of attachments.
|
||||||
|
|
|
@ -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("/entries/{entryID}/fetch-content", handler.fetchContent).Methods(http.MethodGet)
|
||||||
sr.HandleFunc("/flush-history", handler.flushHistory).Methods(http.MethodPut, http.MethodDelete)
|
sr.HandleFunc("/flush-history", handler.flushHistory).Methods(http.MethodPut, http.MethodDelete)
|
||||||
sr.HandleFunc("/icons/{iconID}", handler.getIconByIconID).Methods(http.MethodGet)
|
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)
|
sr.HandleFunc("/version", handler.versionHandler).Methods(http.MethodGet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
func TestGetEntryEndpoints(t *testing.T) {
|
||||||
testConfig := newIntegrationTestConfig()
|
testConfig := newIntegrationTestConfig()
|
||||||
if !testConfig.isConfigured() {
|
if !testConfig.isConfigured() {
|
||||||
|
|
80
internal/api/enclosure.go
Normal file
80
internal/api/enclosure.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -8,10 +8,8 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"miniflux.app/v2/internal/config"
|
|
||||||
"miniflux.app/v2/internal/http/request"
|
"miniflux.app/v2/internal/http/request"
|
||||||
"miniflux.app/v2/internal/http/response/json"
|
"miniflux.app/v2/internal/http/response/json"
|
||||||
"miniflux.app/v2/internal/integration"
|
"miniflux.app/v2/internal/integration"
|
||||||
|
@ -20,7 +18,6 @@ import (
|
||||||
"miniflux.app/v2/internal/reader/processor"
|
"miniflux.app/v2/internal/reader/processor"
|
||||||
"miniflux.app/v2/internal/reader/readingtime"
|
"miniflux.app/v2/internal/reader/readingtime"
|
||||||
"miniflux.app/v2/internal/storage"
|
"miniflux.app/v2/internal/storage"
|
||||||
"miniflux.app/v2/internal/urllib"
|
|
||||||
"miniflux.app/v2/internal/validator"
|
"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)
|
entry.Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entry.Content)
|
||||||
proxyOption := config.Opts.MediaProxyMode()
|
|
||||||
|
|
||||||
for i := range entry.Enclosures {
|
entry.Enclosures.ProxifyEnclosureURL(h.router)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
json.OK(w, r, entry)
|
json.OK(w, r, entry)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ import (
|
||||||
mff "miniflux.app/v2/internal/reader/handler"
|
mff "miniflux.app/v2/internal/reader/handler"
|
||||||
mfs "miniflux.app/v2/internal/reader/subscription"
|
mfs "miniflux.app/v2/internal/reader/subscription"
|
||||||
"miniflux.app/v2/internal/storage"
|
"miniflux.app/v2/internal/storage"
|
||||||
"miniflux.app/v2/internal/urllib"
|
|
||||||
"miniflux.app/v2/internal/validator"
|
"miniflux.app/v2/internal/validator"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"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)
|
entry.Content = mediaproxy.RewriteDocumentWithAbsoluteProxyURL(h.router, entry.Content)
|
||||||
proxyOption := config.Opts.MediaProxyMode()
|
|
||||||
|
|
||||||
for i := range entry.Enclosures {
|
entry.Enclosures.ProxifyEnclosureURL(h.router)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItems[i] = contentItem{
|
contentItems[i] = contentItem{
|
||||||
ID: fmt.Sprintf(EntryIDLong, entry.ID),
|
ID: fmt.Sprintf(EntryIDLong, entry.ID),
|
||||||
|
|
|
@ -2,7 +2,14 @@
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
package model // import "miniflux.app/v2/internal/model"
|
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.
|
// Enclosure represents an attachment.
|
||||||
type Enclosure struct {
|
type Enclosure struct {
|
||||||
|
@ -15,6 +22,10 @@ type Enclosure struct {
|
||||||
MediaProgression int64 `json:"media_progression"`
|
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
|
// Html5MimeType will modify the actual MimeType to allow direct playback from HTML5 player for some kind of MimeType
|
||||||
func (e Enclosure) Html5MimeType() string {
|
func (e Enclosure) Html5MimeType() string {
|
||||||
if e.MimeType == "video/m4v" {
|
if e.MimeType == "video/m4v" {
|
||||||
|
@ -34,3 +45,33 @@ func (el EnclosureList) ContainsAudioOrVideo() bool {
|
||||||
}
|
}
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
18
internal/validator/enclosure.go
Normal file
18
internal/validator/enclosure.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Reference in a new issue