package feed import ( "context" "fmt" "sort" "strconv" "strings" "time" itunes "github.com/eduncan911/podcast" "github.com/pkg/errors" "github.com/mxpv/podsync/pkg/model" ) // sort.Interface implementation type timeSlice []*model.Episode func (p timeSlice) Len() int { return len(p) } // In descending order func (p timeSlice) Less(i, j int) bool { return p[i].PubDate.After(p[j].PubDate) } func (p timeSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } func Build(_ctx context.Context, feed *model.Feed, cfg *Config, hostname string) (*itunes.Podcast, error) { const ( podsyncGenerator = "Podsync generator (support us at https://github.com/mxpv/podsync)" defaultCategory = "TV & Film" ) var ( now = time.Now().UTC() author = feed.Title title = feed.Title description = feed.Description ) if cfg.Custom.Author != "" { author = cfg.Custom.Author } if cfg.Custom.Title != "" { title = cfg.Custom.Title } if cfg.Custom.Description != "" { description = cfg.Custom.Description } p := itunes.New(title, feed.ItemURL, description, &feed.PubDate, &now) p.Generator = podsyncGenerator p.AddSubTitle(title) p.IAuthor = author p.AddSummary(description) if feed.PrivateFeed { p.IBlock = "yes" } if cfg.Custom.OwnerName != "" && cfg.Custom.OwnerEmail != "" { p.IOwner = &itunes.Author{ Name: cfg.Custom.OwnerName, Email: cfg.Custom.OwnerEmail, } } if cfg.Custom.CoverArt != "" { p.AddImage(cfg.Custom.CoverArt) } else { p.AddImage(feed.CoverArt) } if cfg.Custom.Category != "" { p.AddCategory(cfg.Custom.Category, cfg.Custom.Subcategories) } else { p.AddCategory(defaultCategory, cfg.Custom.Subcategories) } if cfg.Custom.Explicit { p.IExplicit = "yes" } else { p.IExplicit = "no" } if cfg.Custom.Language != "" { p.Language = cfg.Custom.Language } for _, episode := range feed.Episodes { if episode.PubDate.IsZero() { episode.PubDate = now } } // Sort all episodes in descending order sort.Sort(timeSlice(feed.Episodes)) for i, episode := range feed.Episodes { if episode.Status != model.EpisodeDownloaded { // Skip episodes that are not yet downloaded continue } item := itunes.Item{ GUID: episode.ID, Link: episode.VideoURL, Title: episode.Title, Description: episode.Description, ISubtitle: episode.Title, // Some app prefer 1-based order IOrder: strconv.Itoa(i + 1), } item.AddPubDate(&episode.PubDate) item.AddSummary(episode.Description) item.AddImage(episode.Thumbnail) item.AddDuration(episode.Duration) enclosureType := itunes.MP4 if feed.Format == model.FormatAudio { enclosureType = itunes.MP3 } var ( episodeName = EpisodeName(cfg, episode) downloadURL = fmt.Sprintf("%s/%s/%s", strings.TrimRight(hostname, "/"), cfg.ID, episodeName) ) item.AddEnclosure(downloadURL, enclosureType, episode.Size) // p.AddItem requires description to be not empty, use workaround if item.Description == "" { item.Description = " " } if cfg.Custom.Explicit { item.IExplicit = "yes" } else { item.IExplicit = "no" } if _, err := p.AddItem(item); err != nil { return nil, errors.Wrapf(err, "failed to add item to podcast (id %q)", episode.ID) } } return &p, nil } func EpisodeName(feedConfig *Config, episode *model.Episode) string { ext := "mp4" if feedConfig.Format == model.FormatAudio { ext = "mp3" } return fmt.Sprintf("%s.%s", episode.ID, ext) }