cpod

Yet another cron-friendly podcatcher

git clone https://git.8pit.net/cpod.git

  1// Copyright (C) 2013-2015 Sören Tempel
  2//
  3// This program is free software: you can redistribute it and/or modify
  4// it under the terms of the GNU General Public License as published by
  5// the Free Software Foundation, either version 3 of the License, or
  6// (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 of
 10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 11// GNU General Public License for more details.
 12//
 13// You should have received a copy of the GNU General Public License
 14// along with this program. If not, see <http://www.gnu.org/licenses/>.
 15
 16package main
 17
 18import (
 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)
 30
 31const (
 32	appName    = "cpod"
 33	appVersion = "1.9"
 34)
 35
 36var (
 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)
 41
 42var (
 43	logger      = log.New(os.Stderr, fmt.Sprintf("%s: ", appName), 0)
 44	downloadDir = util.EnvDefault("CPOD_DOWNLOAD_DIR", "podcasts")
 45)
 46
 47func main() {
 48	flag.Parse()
 49	if *version {
 50		logger.Fatal(appVersion)
 51	}
 52
 53	storeDir := filepath.Join(util.EnvDefault("XDG_CONFIG_HOME", ".config"), appName)
 54	lockPath := filepath.Join(os.TempDir(), fmt.Sprintf("%s-%s", appName, util.Username()))
 55
 56	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	}
 61
 62	storage, err := store.Load(filepath.Join(storeDir, "urls"))
 63	if err != nil {
 64		logger.Fatal(err)
 65	}
 66
 67	update(storage)
 68	if err := os.Remove(lockPath); err != nil {
 69		logger.Fatal(err)
 70	}
 71}
 72
 73func update(storage *store.Store) {
 74	var wg sync.WaitGroup
 75	var counter int
 76
 77	for cast := range storage.Fetch() {
 78		wg.Add(1)
 79		counter++
 80
 81		go func(p store.Podcast) {
 82			defer func() {
 83				wg.Done()
 84				counter--
 85			}()
 86
 87			feed := p.Feed
 88			if p.Error != nil {
 89				logger.Println(p.Error)
 90				return
 91			}
 92
 93			items, err := newItems(feed)
 94			if err != nil {
 95				logger.Println(err)
 96				return
 97			}
 98
 99			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					break
104				}
105
106				if err := writeMarker(feed.Title, item.PubDate); err != nil {
107					logger.Println(err)
108					break
109				}
110			}
111		}(cast)
112
113		for *limit > 0 && counter >= *limit {
114			time.Sleep(3 * time.Second)
115		}
116	}
117
118	wg.Wait()
119}
120
121func newItems(cast feedparser.Feed) (items []feedparser.Item, err error) {
122	unread, err := readMarker(cast.Title)
123	if os.IsNotExist(err) {
124		err = nil
125	} else if err != nil {
126		return
127	}
128
129	if *recent > 0 && len(cast.Items) >= *recent {
130		cast.Items = cast.Items[0:*recent]
131	}
132
133	for _, item := range cast.Items {
134		if !item.PubDate.After(unread) {
135			break
136		}
137
138		if len(item.Attachment) > 0 {
139			items = append(items, item)
140		}
141	}
142
143	return
144}
145
146func getItem(cast feedparser.Feed, item feedparser.Item) error {
147	title, err := util.Escape(cast.Title)
148	if err != nil {
149		return err
150	}
151
152	target := filepath.Join(downloadDir, title)
153	if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
154		return err
155	}
156
157	fp, err := util.GetFile(item.Attachment, target)
158	if err != nil {
159		return err
160	}
161
162	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 err
167		}
168	}
169
170	return nil
171}
172
173func readMarker(name string) (marker time.Time, err error) {
174	name, err = util.Escape(name)
175	if err != nil {
176		return
177	}
178
179	file, err := os.Open(filepath.Join(downloadDir, name, ".latest"))
180	if err != nil {
181		return
182	}
183
184	defer file.Close()
185	var timestamp int64
186
187	if _, err = fmt.Fscanf(file, "%d\n", &timestamp); err != nil {
188		return
189	}
190
191	marker = time.Unix(timestamp, 0)
192	return
193}
194
195func writeMarker(name string, latest time.Time) error {
196	name, err := util.Escape(name)
197	if err != nil {
198		return err
199	}
200
201	path := filepath.Join(downloadDir, name, ".latest")
202	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
203		return err
204	}
205
206	file, err := os.Create(path)
207	if err != nil {
208		return err
209	}
210
211	defer file.Close()
212	if _, err := fmt.Fprintf(file, "%d\n", latest.Unix()); err != nil {
213		return err
214	}
215
216	return nil
217}