1// Copyright (C) 2013-2015 Sören Tempel2//3// This program is free software: you can redistribute it and/or modify4// it under the terms of the GNU General Public License as published by5// the Free Software Foundation, either version 3 of the License, or6// (at your option) any later version.7//8// This program is distributed in the hope that it will be useful,9// but WITHOUT ANY WARRANTY; without even the implied warranty of10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the11// GNU General Public License for more details.12//13// You should have received a copy of the GNU General Public License14// along with this program. If not, see <http://www.gnu.org/licenses/>.1516package main1718import (19 "flag"20 "fmt"21 "github.com/nmeum/cpod/store"22 "github.com/nmeum/cpod/util"23 "github.com/nmeum/go-feedparser"24 "log"25 "os"26 "path/filepath"27 "sync"28 "time"29)3031const (32 appName = "cpod"33 appVersion = "1.9"34)3536var (37 limit = flag.Int("p", 5, "number of maximal parallel downloads")38 recent = flag.Int("r", 0, "number of most recent episodes to download")39 version = flag.Bool("v", false, "display version number and exit")40)4142var (43 logger = log.New(os.Stderr, fmt.Sprintf("%s: ", appName), 0)44 downloadDir = util.EnvDefault("CPOD_DOWNLOAD_DIR", "podcasts")45)4647func main() {48 flag.Parse()49 if *version {50 logger.Fatal(appVersion)51 }5253 storeDir := filepath.Join(util.EnvDefault("XDG_CONFIG_HOME", ".config"), appName)54 lockPath := filepath.Join(os.TempDir(), fmt.Sprintf("%s-%s", appName, util.Username()))5556 if err := util.Lock(lockPath); os.IsExist(err) {57 logger.Fatalf("database is locked, remove %q to force unlock\n", lockPath)58 } else if err != nil {59 logger.Fatal(err)60 }6162 storage, err := store.Load(filepath.Join(storeDir, "urls"))63 if err != nil {64 logger.Fatal(err)65 }6667 update(storage)68 if err := os.Remove(lockPath); err != nil {69 logger.Fatal(err)70 }71}7273func update(storage *store.Store) {74 var wg sync.WaitGroup75 var counter int7677 for cast := range storage.Fetch() {78 wg.Add(1)79 counter++8081 go func(p store.Podcast) {82 defer func() {83 wg.Done()84 counter--85 }()8687 feed := p.Feed88 if p.Error != nil {89 logger.Println(p.Error)90 return91 }9293 items, err := newItems(feed)94 if err != nil {95 logger.Println(err)96 return97 }9899 for i := len(items) - 1; i >= 0; i-- {100 item := items[i]101 if err := getItem(feed, item); err != nil {102 logger.Println(err)103 break104 }105106 if err := writeMarker(feed.Title, item.PubDate); err != nil {107 logger.Println(err)108 break109 }110 }111 }(cast)112113 for *limit > 0 && counter >= *limit {114 time.Sleep(3 * time.Second)115 }116 }117118 wg.Wait()119}120121func newItems(cast feedparser.Feed) (items []feedparser.Item, err error) {122 unread, err := readMarker(cast.Title)123 if os.IsNotExist(err) {124 err = nil125 } else if err != nil {126 return127 }128129 if *recent > 0 && len(cast.Items) >= *recent {130 cast.Items = cast.Items[0:*recent]131 }132133 for _, item := range cast.Items {134 if !item.PubDate.After(unread) {135 break136 }137138 if len(item.Attachment) > 0 {139 items = append(items, item)140 }141 }142143 return144}145146func getItem(cast feedparser.Feed, item feedparser.Item) error {147 title, err := util.Escape(cast.Title)148 if err != nil {149 return err150 }151152 target := filepath.Join(downloadDir, title)153 if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {154 return err155 }156157 fp, err := util.GetFile(item.Attachment, target)158 if err != nil {159 return err160 }161162 name, err := util.Escape(item.Title)163 if err == nil {164 newfp := filepath.Join(target, name+filepath.Ext(fp))165 if err = os.Rename(fp, newfp); err != nil {166 return err167 }168 }169170 return nil171}172173func readMarker(name string) (marker time.Time, err error) {174 name, err = util.Escape(name)175 if err != nil {176 return177 }178179 file, err := os.Open(filepath.Join(downloadDir, name, ".latest"))180 if err != nil {181 return182 }183184 defer file.Close()185 var timestamp int64186187 if _, err = fmt.Fscanf(file, "%d\n", ×tamp); err != nil {188 return189 }190191 marker = time.Unix(timestamp, 0)192 return193}194195func writeMarker(name string, latest time.Time) error {196 name, err := util.Escape(name)197 if err != nil {198 return err199 }200201 path := filepath.Join(downloadDir, name, ".latest")202 if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {203 return err204 }205206 file, err := os.Create(path)207 if err != nil {208 return err209 }210211 defer file.Close()212 if _, err := fmt.Fprintf(file, "%d\n", latest.Unix()); err != nil {213 return err214 }215216 return nil217}