mirror of
https://github.com/mxpv/podsync.git
synced 2024-05-11 05:55:04 +00:00
270 lines
6.0 KiB
Go
270 lines
6.0 KiB
Go
package feeds
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/go-pg/pg"
|
|
itunes "github.com/mxpv/podcast"
|
|
"github.com/mxpv/podsync/pkg/api"
|
|
"github.com/mxpv/podsync/pkg/model"
|
|
"github.com/pkg/errors"
|
|
"github.com/ventu-io/go-shortid"
|
|
)
|
|
|
|
const (
|
|
MetricQueries = "queries"
|
|
MetricDownloads = "downloads"
|
|
)
|
|
|
|
type stats interface {
|
|
Inc(metric, hashID string) (int64, error)
|
|
Get(metric, hashID string) (int64, error)
|
|
}
|
|
|
|
type builder interface {
|
|
Build(feed *model.Feed) (podcast *itunes.Podcast, err error)
|
|
}
|
|
|
|
type Service struct {
|
|
sid *shortid.Shortid
|
|
stats stats
|
|
db *pg.DB
|
|
builders map[api.Provider]builder
|
|
}
|
|
|
|
func (s Service) makeFeed(req *api.CreateFeedRequest, identity *api.Identity) (*model.Feed, error) {
|
|
feed, err := parseURL(req.URL)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to create feed for URL: %s", req.URL)
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
|
|
feed.UserID = identity.UserId
|
|
feed.FeatureLevel = identity.FeatureLevel
|
|
feed.Quality = req.Quality
|
|
feed.Format = req.Format
|
|
feed.PageSize = req.PageSize
|
|
feed.CreatedAt = now
|
|
feed.LastAccess = now
|
|
|
|
if identity.FeatureLevel >= api.ExtendedPagination {
|
|
if feed.PageSize > 600 {
|
|
feed.PageSize = 600
|
|
}
|
|
} else if identity.FeatureLevel == api.ExtendedFeatures {
|
|
if feed.PageSize > 150 {
|
|
feed.PageSize = 150
|
|
}
|
|
} else {
|
|
feed.Quality = api.QualityHigh
|
|
feed.Format = api.FormatVideo
|
|
feed.PageSize = 50
|
|
}
|
|
|
|
// Generate short id
|
|
hashId, err := s.sid.Generate()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to generate id for feed")
|
|
}
|
|
|
|
feed.HashID = hashId
|
|
|
|
return feed, 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)
|
|
}
|
|
|
|
// Save to database
|
|
_, err = s.db.Model(feed).Insert()
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "failed to save feed to database")
|
|
}
|
|
|
|
return feed.HashID, nil
|
|
}
|
|
|
|
func (s Service) QueryFeed(hashID string) (*model.Feed, error) {
|
|
lastAccess := time.Now().UTC()
|
|
|
|
feed := &model.Feed{}
|
|
res, err := s.db.Model(feed).
|
|
Set("last_access = ?", lastAccess).
|
|
Where("hash_id = ?", hashID).
|
|
Returning("*").
|
|
Update()
|
|
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to query feed: %s", hashID)
|
|
}
|
|
|
|
if res.RowsAffected() != 1 {
|
|
return nil, api.ErrNotFound
|
|
}
|
|
|
|
return feed, nil
|
|
}
|
|
|
|
func (s Service) BuildFeed(hashID string) (*itunes.Podcast, error) {
|
|
feed, err := s.QueryFeed(hashID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
count, err := s.stats.Inc(MetricQueries, feed.HashID)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to update metrics for feed: %s", hashID)
|
|
}
|
|
|
|
if feed.PageSize > 150 && count > api.ExtendedPaginationQueryLimit {
|
|
return nil, api.ErrQuotaExceeded
|
|
}
|
|
|
|
builder, ok := s.builders[feed.Provider]
|
|
if !ok {
|
|
return nil, errors.Wrapf(err, "failed to get builder for feed: %s", hashID)
|
|
}
|
|
|
|
podcast, err := builder.Build(feed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return podcast, nil
|
|
}
|
|
|
|
func (s Service) GetMetadata(hashID string) (*api.Metadata, error) {
|
|
feed := &model.Feed{}
|
|
err := s.db.
|
|
Model(feed).
|
|
Where("hash_id = ?", hashID).
|
|
Column("provider", "format", "quality", "user_id").
|
|
Select()
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
downloads, err := s.stats.Inc(MetricDownloads, hashID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &api.Metadata{
|
|
Provider: feed.Provider,
|
|
Format: feed.Format,
|
|
Quality: feed.Quality,
|
|
Downloads: downloads,
|
|
}, nil
|
|
}
|
|
|
|
func (s Service) Downgrade(patronID string, featureLevel int) error {
|
|
log.Printf("Downgrading patron '%s' to feature level %d", patronID, featureLevel)
|
|
|
|
if featureLevel > api.ExtendedFeatures {
|
|
return nil
|
|
}
|
|
|
|
if featureLevel == api.ExtendedFeatures {
|
|
const maxPages = 150
|
|
res, err := s.db.
|
|
Model(&model.Feed{}).
|
|
Set("page_size = ?", maxPages).
|
|
Where("user_id = ? AND page_size > ?", patronID, maxPages).
|
|
Update()
|
|
|
|
if err != nil {
|
|
log.Printf("! failed to reduce page sizes for patron '%s': %v", patronID, err)
|
|
return err
|
|
}
|
|
|
|
res, err = s.db.
|
|
Model(&model.Feed{}).
|
|
Set("feature_level = ?", api.ExtendedFeatures).
|
|
Where("user_id = ?", patronID, maxPages).
|
|
Update()
|
|
|
|
if err != nil {
|
|
log.Printf("! failed to downgrade patron '%s' to feature level %d: %v", patronID, featureLevel, err)
|
|
return err
|
|
}
|
|
|
|
log.Printf("Updated %d feed(s) of user '%s' to feature level %d", res.RowsAffected(), patronID, featureLevel)
|
|
return nil
|
|
}
|
|
|
|
if featureLevel == api.DefaultFeatures {
|
|
res, err := s.db.
|
|
Model(&model.Feed{}).
|
|
Set("page_size = ?", 50).
|
|
Set("feature_level = ?", api.DefaultFeatures).
|
|
Set("format = ?", api.FormatVideo).
|
|
Set("quality = ?", api.QualityHigh).
|
|
Where("user_id = ?", patronID).
|
|
Update()
|
|
|
|
if err != nil {
|
|
log.Printf("! failed to downgrade patron '%s' to feature level %d: %v", patronID, featureLevel, err)
|
|
return err
|
|
}
|
|
|
|
log.Printf("Updated %d feed(s) of user '%s' to feature level %d", res.RowsAffected(), patronID, featureLevel)
|
|
return nil
|
|
}
|
|
|
|
return errors.New("unsupported downgrade type")
|
|
}
|
|
|
|
type feedOption func(*Service)
|
|
|
|
//noinspection GoExportedFuncWithUnexportedType
|
|
func WithPostgres(db *pg.DB) feedOption {
|
|
return func(service *Service) {
|
|
service.db = db
|
|
}
|
|
}
|
|
|
|
//noinspection GoExportedFuncWithUnexportedType
|
|
func WithBuilder(provider api.Provider, builder builder) feedOption {
|
|
return func(service *Service) {
|
|
service.builders[provider] = builder
|
|
}
|
|
}
|
|
|
|
//noinspection GoExportedFuncWithUnexportedType
|
|
func WithStats(m stats) feedOption {
|
|
return func(service *Service) {
|
|
service.stats = m
|
|
}
|
|
}
|
|
|
|
func NewFeedService(opts ...feedOption) (*Service, error) {
|
|
sid, err := shortid.New(1, shortid.DefaultABC, uint64(time.Now().UnixNano()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
svc := &Service{
|
|
sid: sid,
|
|
builders: make(map[api.Provider]builder),
|
|
}
|
|
|
|
for _, fn := range opts {
|
|
fn(svc)
|
|
}
|
|
|
|
return svc, nil
|
|
}
|