1
0
mirror of https://github.com/mxpv/podsync.git synced 2024-05-11 05:55:04 +00:00

303 lines
6.6 KiB
Go
Raw Normal View History

2017-08-13 14:50:59 -07:00
package feeds
import (
"fmt"
"strconv"
2017-08-13 14:50:59 -07:00
"time"
itunes "github.com/mxpv/podcast"
2018-11-24 11:58:08 -08:00
"github.com/pkg/errors"
2019-03-31 20:59:24 -07:00
log "github.com/sirupsen/logrus"
2018-11-24 11:58:08 -08:00
2017-08-19 16:58:23 -07:00
"github.com/mxpv/podsync/pkg/api"
2017-11-03 16:04:33 -07:00
"github.com/mxpv/podsync/pkg/model"
2017-08-13 14:50:59 -07:00
)
2019-03-29 19:18:03 -07:00
type Builder interface {
Build(feed *model.Feed) error
2017-11-02 18:03:44 -07:00
}
2018-11-24 11:58:08 -08:00
type storage interface {
SaveFeed(feed *model.Feed) error
GetFeed(hashID string) (*model.Feed, error)
UpdateFeed(feed *model.Feed) error
2018-11-24 11:58:08 -08:00
GetMetadata(hashID string) (*model.Feed, error)
2019-03-31 20:59:24 -07:00
Downgrade(userID string, featureLevel int) ([]string, error)
2018-11-24 11:58:08 -08:00
}
2019-03-29 19:18:03 -07:00
type cacheService interface {
Set(key, value string, ttl time.Duration) error
Get(key string) (string, error)
2019-03-31 20:59:24 -07:00
Invalidate(key ...string) error
2019-03-29 19:18:03 -07:00
}
2017-11-03 17:19:44 -07:00
type Service struct {
2018-11-24 11:58:08 -08:00
generator IDGen
storage storage
2019-03-29 19:18:03 -07:00
builders map[api.Provider]Builder
cache cacheService
2017-08-13 14:50:59 -07:00
}
func NewFeedService(db storage, cache cacheService, builders map[api.Provider]Builder) (*Service, error) {
idGen, err := NewIDGen()
if err != nil {
return nil, err
}
svc := &Service{
generator: idGen,
storage: db,
builders: builders,
cache: cache,
}
return svc, nil
}
func (s *Service) CreateFeed(req *api.CreateFeedRequest, identity *api.Identity) (string, error) {
feed, err := s.makeFeed(req, identity)
if err != nil {
return "", err
}
// Make sure builder exists for this provider
_, ok := s.builders[feed.Provider]
if !ok {
return "", fmt.Errorf("failed to get builder for URL: %s", req.URL)
}
if err := s.storage.SaveFeed(feed); err != nil {
return "", err
}
return feed.HashID, nil
}
func (s *Service) makeFeed(req *api.CreateFeedRequest, identity *api.Identity) (*model.Feed, error) {
2017-08-13 17:12:35 -07:00
feed, err := parseURL(req.URL)
2017-08-13 14:50:59 -07:00
if err != nil {
2017-11-10 17:13:01 -08:00
return nil, errors.Wrapf(err, "failed to create feed for URL: %s", req.URL)
2017-08-13 14:50:59 -07:00
}
2017-11-03 19:16:15 -07:00
now := time.Now().UTC()
2019-01-06 21:36:42 -08:00
feed.UserID = identity.UserID
2017-11-10 17:13:01 -08:00
feed.FeatureLevel = identity.FeatureLevel
feed.Quality = req.Quality
feed.Format = req.Format
feed.PageSize = req.PageSize
2017-11-03 19:16:15 -07:00
feed.CreatedAt = now
feed.LastAccess = now
2017-08-13 14:50:59 -07:00
2019-01-07 20:47:59 -08:00
switch {
case identity.FeatureLevel >= api.ExtendedPagination:
2017-11-10 17:13:01 -08:00
if feed.PageSize > 600 {
feed.PageSize = 600
}
2019-01-07 20:47:59 -08:00
case identity.FeatureLevel == api.ExtendedFeatures:
2017-11-10 17:13:01 -08:00
if feed.PageSize > 150 {
feed.PageSize = 150
2017-08-20 18:35:47 -07:00
}
2019-01-07 20:47:59 -08:00
default:
2017-11-10 17:13:01 -08:00
feed.Quality = api.QualityHigh
feed.Format = api.FormatVideo
feed.PageSize = 50
2017-08-20 18:35:47 -07:00
}
2017-08-13 14:50:59 -07:00
// Generate short id
2019-01-06 21:36:42 -08:00
hashID, err := s.generator.Generate()
2017-08-13 14:50:59 -07:00
if err != nil {
2017-11-10 17:13:01 -08:00
return nil, errors.Wrap(err, "failed to generate id for feed")
2017-08-13 14:50:59 -07:00
}
2019-01-06 21:36:42 -08:00
feed.HashID = hashID
2017-08-13 14:50:59 -07:00
2017-11-10 17:13:01 -08:00
return feed, nil
}
2017-11-03 19:16:15 -07:00
func (s Service) QueryFeed(hashID string) (*model.Feed, error) {
return s.storage.GetFeed(hashID)
2017-11-03 19:16:15 -07:00
}
func makeEnclosure(feed *model.Feed, id string, lengthInBytes int64) (string, itunes.EnclosureType, int64) {
ext := "mp4"
contentType := itunes.MP4
if feed.Format == api.FormatAudio {
ext = "m4a"
contentType = itunes.M4A
2019-03-31 20:59:24 -07:00
}
url := fmt.Sprintf("http://podsync.net/download/%s/%s.%s", feed.HashID, id, ext)
return url, contentType, lengthInBytes
2019-03-31 20:59:24 -07:00
}
2019-04-06 13:28:36 -07:00
func short(str string, i int) string {
runes := []rune(str)
if len(runes) > i {
return string(runes[:i]) + " ..."
}
return str
}
func (s *Service) BuildFeed(hashID string) ([]byte, error) {
2019-03-31 20:59:24 -07:00
const (
2019-04-06 13:28:36 -07:00
cacheTTL = 30 * time.Minute
maxDescriptionLen = 384
2019-03-31 20:59:24 -07:00
)
cached, err := s.cache.Get(hashID)
2019-03-29 19:18:03 -07:00
if err == nil {
return []byte(cached), nil
2019-03-29 19:18:03 -07:00
}
// Query feed from DynamoDB
2019-03-29 19:18:03 -07:00
2017-11-03 19:16:15 -07:00
feed, err := s.QueryFeed(hashID)
2017-08-13 14:50:59 -07:00
if err != nil {
return nil, err
}
// Rebuild feed using YouTube API
2017-08-13 14:50:59 -07:00
builder, ok := s.builders[feed.Provider]
if !ok {
2017-11-03 19:16:15 -07:00
return nil, errors.Wrapf(err, "failed to get builder for feed: %s", hashID)
2017-08-13 14:50:59 -07:00
}
log.Infof("building new feed %q", hashID)
2019-04-06 12:54:09 -07:00
oldLastID := feed.LastID
if err := builder.Build(feed); err != nil {
2019-04-06 12:28:15 -07:00
log.WithError(err).WithField("feed_id", hashID).Error("failed to build feed")
return nil, err
}
2019-04-06 13:28:36 -07:00
if len(feed.Episodes) > 300 {
for _, episode := range feed.Episodes {
episode.Description = short(episode.Description, maxDescriptionLen)
}
}
2019-04-06 12:54:09 -07:00
if oldLastID != feed.LastID {
if err := s.storage.UpdateFeed(feed); err != nil {
log.WithError(err).WithField("feed_id", hashID).Error("failed to save feed")
}
}
2019-03-31 20:59:24 -07:00
podcast, err := s.buildPodcast(feed)
if err != nil {
return nil, err
}
2019-03-31 20:59:24 -07:00
body := podcast.String()
2019-03-31 20:59:24 -07:00
// Save to cache
2019-03-31 20:59:24 -07:00
if err := s.cache.Set(hashID, body, cacheTTL); err != nil {
log.WithError(err).Errorf("failed to save cache for feed %q", hashID)
return nil, err
2019-03-31 20:59:24 -07:00
}
return []byte(body), nil
}
2019-03-29 19:18:03 -07:00
func (s *Service) buildPodcast(feed *model.Feed) (*itunes.Podcast, error) {
const (
podsyncGenerator = "Podsync generator"
defaultCategory = "TV & Film"
)
now := time.Now().UTC()
p := itunes.New(feed.Title, feed.ItemURL, feed.Description, &feed.PubDate, &now)
p.Generator = podsyncGenerator
p.AddSubTitle(feed.Title)
p.AddCategory(defaultCategory, nil)
p.AddImage(feed.CoverArt)
p.IAuthor = feed.Title
p.AddSummary(feed.Description)
if feed.Explicit {
p.IExplicit = "yes"
} else {
p.IExplicit = "no"
}
if feed.Language != "" {
p.Language = feed.Language
2017-11-04 17:27:01 -07:00
}
for i, episode := range feed.Episodes {
item := itunes.Item{
GUID: episode.ID,
Link: episode.VideoURL,
Title: episode.Title,
Description: episode.Description,
ISubtitle: episode.Title,
IOrder: strconv.Itoa(i),
}
2019-03-29 19:18:03 -07:00
item.AddSummary(episode.Description)
item.AddImage(episode.Thumbnail)
item.AddPubDate(&episode.PubDate)
item.AddDuration(episode.Duration)
item.AddEnclosure(makeEnclosure(feed, episode.ID, episode.Size))
2019-03-29 19:18:03 -07:00
// p.AddItem requires description to be not empty, use workaround
if item.Description == "" {
item.Description = " "
}
2019-03-29 19:18:03 -07:00
if feed.Explicit {
item.IExplicit = "yes"
} else {
item.IExplicit = "no"
}
_, err := p.AddItem(item)
if err != nil {
return nil, errors.Wrapf(err, "failed to add item to podcast (id %q)", episode.ID)
}
2019-03-31 20:59:24 -07:00
}
return &p, nil
2017-08-13 14:50:59 -07:00
}
2017-11-03 19:16:15 -07:00
func (s Service) GetMetadata(hashID string) (*api.Metadata, error) {
feed, err := s.storage.GetMetadata(hashID)
2017-11-03 16:04:33 -07:00
if err != nil {
return nil, err
}
return &api.Metadata{
2019-02-24 14:39:58 -08:00
Provider: feed.Provider,
Format: feed.Format,
Quality: feed.Quality,
2017-11-03 16:04:33 -07:00
}, nil
2017-08-13 14:50:59 -07:00
}
2017-11-03 20:55:58 -07:00
func (s Service) Downgrade(patronID string, featureLevel int) error {
2019-03-31 20:59:24 -07:00
logger := log.WithFields(log.Fields{
"user_id": patronID,
"level": featureLevel,
})
logger.Info("downgrading patron")
ids, err := s.storage.Downgrade(patronID, featureLevel)
2019-03-31 20:59:24 -07:00
if err != nil {
logger.WithError(err).Error("database error while downgrading patron")
return err
}
2017-11-03 20:55:58 -07:00
2019-03-31 20:59:24 -07:00
if s.cache.Invalidate(ids...) != nil {
logger.WithError(err).Error("failed to invalidate cached feeds")
2018-11-24 11:58:08 -08:00
return err
2017-11-03 20:55:58 -07:00
}
2019-03-31 20:59:24 -07:00
logger.Info("successfully updated user")
2018-11-24 11:58:08 -08:00
return nil
2017-11-03 20:55:58 -07:00
}