Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

remove entry's file and feed's item from combined feed #138

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion app/api/mocks/store.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 23 additions & 3 deletions app/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type YoutubeSvc interface {
// Store provides access to feed data
type Store interface {
Load(fmFeed string, max int, skipJunk bool) ([]feed.Item, error)
Remove(fmFeed string, item feed.Item) error
}

// YoutubeStore provides access to YouTube channel data
Expand Down Expand Up @@ -320,14 +321,33 @@ func (s *Server) regenerateRSSCtrl(w http.ResponseWriter, r *http.Request) {
rest.RenderJSON(w, rest.JSON{"status": "ok", "feeds": len(s.Conf.YouTube.Channels)})
}

// DELETE /yt/entry/{channel}/{video} - deletes entry from youtube channel and videID
// DELETE /yt/entry/{channel}/{video} - deletes entry from youtube channel by given videoID
func (s *Server) removeEntryCtrl(w http.ResponseWriter, r *http.Request) {
err := s.YoutubeSvc.RemoveEntry(ytfeed.Entry{ChannelID: chi.URLParam(r, "channel"), VideoID: chi.URLParam(r, "video")})
channelID := chi.URLParam(r, "channel")
videoID := chi.URLParam(r, "video")
err := s.YoutubeSvc.RemoveEntry(ytfeed.Entry{ChannelID: channelID, VideoID: videoID})
if err != nil {
rest.SendErrorJSON(w, r, log.Default(), http.StatusInternalServerError, err, "failed to remove entry")
return
}
rest.RenderJSON(w, rest.JSON{"status": "ok", "removed": chi.URLParam(r, "video")})
feeds := s.feeds()
for _, f := range feeds {
items, loadErr := s.Store.Load(f, s.Conf.System.MaxTotal, true)
if loadErr != nil {
continue
}
for _, item := range items {
if item.GUID == fmt.Sprintf("%s::%s", channelID, videoID) {
if storeErr := s.Store.Remove(f, item); storeErr != nil {
rest.SendErrorJSON(w, r, log.Default(), http.StatusInternalServerError, storeErr, "failed to remove entry")
return
}
rest.RenderJSON(w, rest.JSON{"status": "ok", "removed": chi.URLParam(r, "video")})
return
}
}
}
rest.SendErrorJSON(w, r, log.Default(), http.StatusInternalServerError, err, "failed to remove entry")
}

func (s *Server) feeds() []string {
Expand Down
35 changes: 33 additions & 2 deletions app/api/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,14 +328,42 @@ func TestServer_removeEntryCtrl(t *testing.T) {
return nil
},
}
store := &mocks.StoreMock{
LoadFunc: func(string, int, bool) ([]feed.Item, error) {
return []feed.Item{
{
GUID: "chan1::vid1",
Title: "title1",
Link: "http://example.com/link1",
Description: "some description1",
Enclosure: feed.Enclosure{
URL: "http://example.com/enclosure1",
Type: "audio/mpeg",
Length: 12345,
},
},
}, nil
},
RemoveFunc: func(string, feed.Item) error {
return nil
},
}

s := Server{
Version: "1.0",
TemplLocation: "../webapp/templates/*",
YoutubeSvc: yt,
Conf: config.Conf{},
AdminPasswd: "123456",
Store: store,
Conf: config.Conf{
Feeds: map[string]config.Feed{
"feed1": {
Title: "feed1 title",
},
},
},
AdminPasswd: "123456",
}
s.Conf.System.MaxTotal = 2

ts := httptest.NewServer(s.router())
defer ts.Close()
Expand Down Expand Up @@ -363,6 +391,9 @@ func TestServer_removeEntryCtrl(t *testing.T) {
require.Equal(t, 1, len(yt.RemoveEntryCalls()))
require.Equal(t, "chan1", yt.RemoveEntryCalls()[0].Entry.ChannelID)
require.Equal(t, "vid1", yt.RemoveEntryCalls()[0].Entry.VideoID)
require.Equal(t, "feed1", store.LoadCalls()[0].FmFeed)
require.Equal(t, "feed1", store.RemoveCalls()[0].FmFeed)
require.Equal(t, "chan1::vid1", store.RemoveCalls()[0].Item.GUID)
}

func TestServer_configCtrl(t *testing.T) {
Expand Down
31 changes: 31 additions & 0 deletions app/proc/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"fmt"
"time"

"github.com/pkg/errors"

log "github.com/go-pkgz/lgr"
bolt "go.etcd.io/bbolt"

Expand Down Expand Up @@ -116,3 +118,32 @@ func (b BoltDB) removeOld(fmFeed string, keep int) (int, error) {
})
return deleted, err
}

// Remove entry matched by item.GUID from given feed
func (b BoltDB) Remove(fmFeed string, item feed.Item) error {

err := b.DB.Update(func(tx *bolt.Tx) (e error) {
bucket := tx.Bucket([]byte(fmFeed))
if bucket == nil {
return fmt.Errorf("no bucket for %s", fmFeed)
}
c := bucket.Cursor()
for k, v := c.Last(); k != nil; k, v = c.Prev() {
var itemDB feed.Item
if err := json.Unmarshal(v, &itemDB); err != nil {
log.Printf("[WARN] failed to unmarshal, %v", err)
continue
}
if itemDB.GUID == item.GUID {
if err := bucket.Delete(k); err != nil {
return errors.Wrapf(err, "failed to delete %s (%s)", string(k), item.GUID)
}
log.Printf("[INFO] delete %s - %s", string(k), item.GUID)
return nil
}
}
return fmt.Errorf("no item found for %s and item.GUID %s", fmFeed, item.GUID)
})

return err
}
27 changes: 27 additions & 0 deletions app/proc/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,30 @@ func TestRemoveOld(t *testing.T) {
})
}
}

func TestStore_Remove(t *testing.T) {
tmpfile, _ := os.CreateTemp("", "")
defer os.Remove(tmpfile.Name())
db, err := bolt.Open(tmpfile.Name(), 0o600, &bolt.Options{Timeout: 1 * time.Second}) // nolint
require.NoError(t, err)
bdb := &BoltDB{DB: db}

item := feed.Item{
PubDate: pubDate,
GUID: "chan1::vid1",
}

created, err := bdb.Save("feed1", item)
require.NoError(t, err)
assert.True(t, created)

err = bdb.Remove("feed1", feed.Item{GUID: "chan2"})
require.Error(t, err)

err = bdb.Remove("feed1", item)
require.NoError(t, err)

items, err := bdb.Load("feed1", 5, false)
require.NoError(t, err)
assert.Equal(t, 0, len(items))
}
3 changes: 3 additions & 0 deletions app/youtube/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,9 @@ func (s *Service) RemoveEntry(entry ytfeed.Entry) error {
if err := s.Store.Remove(entry); err != nil {
return errors.Wrapf(err, "failed to remove entry %s", entry.VideoID)
}
if err := os.Remove(entry.File); err != nil {
return errors.Wrapf(err, "failed to remove file %s", entry.File)
}
return nil
}

Expand Down
46 changes: 46 additions & 0 deletions app/youtube/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,52 @@ func TestService_update(t *testing.T) {

}

func TestService_RemoveEntry(t *testing.T) {
tempDir := t.TempDir()
video := filepath.Join(tempDir, "122b672d10e77708b51c041f852615dc0eedf354.mp3")
_, err := os.Create(video) // nolint
require.NoError(t, err)

storeSvc := &mocks.StoreServiceMock{
LoadFunc: func(string, int) ([]ytfeed.Entry, error) {
res := []ytfeed.Entry{
{ChannelID: "channel1", VideoID: "vid1", Title: "title1", File: video},
{ChannelID: "channel1", VideoID: "vid2", Title: "title2", File: "/tmp/file2.mp3"},
}
return res, nil
},
ResetProcessedFunc: func(_ ytfeed.Entry) error {
return nil
},
RemoveFunc: func(_ ytfeed.Entry) error {
return nil
},
}

svc := Service{
Feeds: []FeedInfo{
{ID: "channel1", Name: "name1", Type: ytfeed.FTChannel},
{ID: "channel2", Name: "name2", Type: ytfeed.FTPlaylist},
},
Store: storeSvc,
RootURL: "http://localhost:8080/yt",
KeepPerChannel: 10,
SkipShorts: time.Second * 60,
}

entry := ytfeed.Entry{
ChannelID: "channel1", VideoID: "vid1", Title: "title1", File: video,
}

err = svc.RemoveEntry(entry)
require.NoError(t, err)

_, err = os.Stat(video)
assert.True(t, os.IsNotExist(err))
assert.Equal(t, entry.ChannelID, storeSvc.RemoveCalls()[0].Entry.ChannelID)
assert.Equal(t, entry.ChannelID, storeSvc.ResetProcessedCalls()[0].Entry.ChannelID)
}

func TestService_totalEntriesToKeep(t *testing.T) {
svc := Service{
Feeds: []FeedInfo{
Expand Down
Loading