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", ×tamp); 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}