1 /*******************************************************************************
2 *                         Goggles Music Manager                                *
3 ********************************************************************************
4 *           Copyright (C) 2007-2021 by Sander Jansen. All Rights Reserved      *
5 *                               ---                                            *
6 * This program is free software: you can redistribute it and/or modify         *
7 * it under the terms of the GNU General Public License as published by         *
8 * the Free Software Foundation, either version 3 of the License, or            *
9 * (at your option) any later version.                                          *
10 *                                                                              *
11 * This program is distributed in the hope that it will be useful,              *
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of               *
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                *
14 * GNU General Public License for more details.                                 *
15 *                                                                              *
16 * You should have received a copy of the GNU General Public License            *
17 * along with this program.  If not, see http://www.gnu.org/licenses.           *
18 ********************************************************************************/
19 #include "gmdefs.h"
20 #include "gmutils.h"
21 #include "GMApp.h"
22 #include "GMTrack.h"
23 #include "GMList.h"
24 #include "GMDatabase.h"
25 #include "GMTrackDatabase.h"
26 #include "GMCover.h"
27 #include "GMCoverCache.h"
28 #include "GMTrackList.h"
29 #include "GMTrackItem.h"
30 #include "GMTrackView.h"
31 #include "GMTaskManager.h"
32 #include "GMSource.h"
33 #include "GMSourceView.h"
34 #include "GMClipboard.h"
35 #include "GMPodcastSource.h"
36 #include "GMPlayerManager.h"
37 #include "GMWindow.h"
38 #include "GMIconTheme.h"
39 #include "GMFilename.h"
40 #include "GMTag.h"
41 #include "GMAlbumList.h"
42 #include "GMAudioPlayer.h"
43 #include "GMCoverLoader.h"
44 
45 
46 #include <fcntl.h> /* Definition of AT_* constants */
47 #include <sys/stat.h>
48 
49 /*
50   Notes:
51 
52 
53     PodcastDownloader
54       - don't update db, but send callback to mainthread to update status
55 */
56 
gm_rfc1123(FXTime time)57 FXString gm_rfc1123(FXTime time) {
58   FXDate date;
59   date.setTime(time);
60   FXString str;
61   str  = FXString::value("%s, %d %s %d ",FXDate::dayNameShort(date.dayOfWeek()),date.day(),FXDate::monthNameShort(date.month()),date.year());
62   str += FXSystem::universalTime(time, "%T GMT");
63   return str;
64   }
65 
66 
gm_is_feed(const FXString & mime)67 static FXbool gm_is_feed(const FXString & mime) {
68   if ( comparecase(mime,"application/rss+xml")==0 ||
69        comparecase(mime,"text/xml")==0 ||
70        comparecase(mime,"application/xml")==0 ) {
71     return true;
72     }
73   else {
74     return false;
75     }
76   }
77 
78 //-----------------------------------------------------------------------------
79 
80 
81 struct FeedLink {
82   FXString description;
83   FXString url;
84   };
85 
86 //-----------------------------------------------------------------------------
87 
88 
unescape_html(FXString & value)89 static void unescape_html(FXString & value) {
90   value.substitute("&#38;","&");
91   }
92 
93 
parse_duration(const FXString & value,FXuint & d)94 static void parse_duration(const FXString & value,FXuint & d) {
95   FXint n = value.contains(":");
96   FXint hh=0,mm=0,ss=0;
97   switch(n) {
98     case 2: value.scan("%d:%d:%d",&hh,&mm,&ss); break;
99     case 1: value.scan("%d:%d",&mm,&ss); break;
100     case 0: value.scan("%d",&ss); break;
101     default: GM_DEBUG_PRINT("[rss] failed to parse duration %s\n",value.text()); d=0; return; break;
102     };
103   d=(hh*3600)+(mm*60)+ss;
104   }
105 
106 
107 
108 struct RssItem {
109   FXString id;
110   FXString url;
111   FXString title;
112   FXString description;
113   FXint    length = 0;
114   FXuint   time = 0;
115   FXTime   date = 0;
116 
RssItemRssItem117   RssItem() {}
118 
119 
trimRssItem120   void trim() {
121     url.trim();
122     title.trim();
123     description.trim();
124     }
125 
clearRssItem126   void clear() {
127     id.clear();
128     url.clear();
129     title.clear();
130     description.clear();
131     length=0;
132     time=0;
133     date=0;
134     }
135 
guidRssItem136   FXString guid() const {
137     if (!id.empty())
138       return id;
139     else
140       return url;
141     }
142 
143   };
144 
145 struct RssFeed {
146 public:
147   FXString         title;
148   FXString         description;
149   FXString         category;
150   FXString         image;
151   FXTime           date = 0;
152   FXArray<RssItem> items;
153 public:
RssFeedRssFeed154   RssFeed() {}
155 
trimRssFeed156   void trim() {
157     title.trim();
158     description.trim();
159     category.trim();
160     for (FXint i=0;i<items.no();i++) {
161       items[i].trim();
162       }
163     }
164 
165 #ifdef DEBUG
debugRssFeed166   void debug() {
167     fxmessage("      title: %s\n",title.text());
168     fxmessage("description: %s\n",description.text());
169     fxmessage("   category: %s\n",category.text());
170     fxmessage("       date: %s\n",FXSystem::universalTime(date).text());
171     for (FXint i=0;i<items.no();i++) {
172       fxmessage("----\n");
173       fxmessage("         url: %s\n",items[i].url.text());
174       fxmessage("      length: %d\n",items[i].length);
175       fxmessage("       title: %s\n",items[i].title.text());
176       fxmessage(" description: %s\n",items[i].description.text());
177       fxmessage("          id: %s\n",items[i].id.text());
178       fxmessage("    duration: %d\n",items[i].time);
179       fxmessage("        date: %s\n",FXSystem::universalTime(items[i].date).text());
180       }
181     }
182 #endif
183 
184   };
185 
186 
187 class RssParser : public XmlParser {
188 public:
189   RssFeed  feed;
190   RssItem  item;
191   FXString value;
192 protected:
begin(const FXchar * element,const FXchar ** attributes)193   FXint begin(const FXchar *element,const FXchar** attributes){
194     switch(node()) {
195       case Elem_None:
196         if (compare(element,"rss")==0)
197           return Elem_RSS;
198         break;
199       case Elem_RSS:
200         if (compare(element,"channel")==0)
201           return Elem_Channel;
202         break;
203       case Elem_Channel:
204         if (compare(element,"item")==0)
205           return Elem_Item;
206         else if (comparecase(element,"title")==0)
207           return Elem_Channel_Title;
208         else if (comparecase(element,"description")==0)
209           return Elem_Channel_Description;
210         else if (comparecase(element,"category")==0)
211           return Elem_Channel_Category;
212         else if (comparecase(element,"itunes:category")==0)
213           parse_itunes_category(attributes);
214         else if (comparecase(element,"itunes:image")==0 && feed.image.empty())
215           parse_itunes_image(attributes);
216         else if (comparecase(element,"image")==0 && feed.image.empty())
217           return Elem_Channel_Image;
218         else if (comparecase(element,"pubdate")==0)
219           return Elem_Channel_Date;
220         break;
221       case Elem_Item:
222         if (comparecase(element,"enclosure")==0) {
223           if (attributes)
224             parse_enclosure(attributes);
225           }
226         else if (comparecase(element,"title")==0)
227           return Elem_Item_Title;
228         else if (comparecase(element,"description")==0)
229           return Elem_Item_Description;
230         else if (comparecase(element,"guid")==0)
231           return Elem_Item_Guid;
232         else if (comparecase(element,"pubdate")==0)
233           return Elem_Item_Date;
234         else if (comparecase(element,"itunes:duration")==0)
235           return Elem_Item_Duration;
236         break;
237       case Elem_Channel_Image:
238         if (comparecase(element,"url")==0)
239           return Elem_Channel_Image_Url;
240       default: break;
241       }
242     return 0;
243     }
244 
data(const FXchar * ptr,FXint len)245   void data(const FXchar * ptr,FXint len){
246     switch(node()) {
247       case Elem_Item_Title         : item.title.append(ptr,len); break;
248       case Elem_Item_Description   : item.description.append(ptr,len); break;
249       case Elem_Item_Guid          : item.id.append(ptr,len); break;
250       case Elem_Item_Date          : value.append(ptr,len); break;
251       case Elem_Item_Duration      : value.append(ptr,len); break;
252       case Elem_Channel_Title      : feed.title.append(ptr,len); break;
253       case Elem_Channel_Description: feed.description.append(ptr,len); break;
254       case Elem_Channel_Category   : feed.category.append(ptr,len); break;
255       case Elem_Channel_Date       : value.append(ptr,len); break;
256       case Elem_Channel_Image_Url  : feed.image.append(ptr,len); break;
257       }
258     }
end(const FXchar *)259   void end(const FXchar*) {
260     switch(node()){
261       case Elem_Item:
262         feed.items.append(item);
263         item.clear();
264         break;
265       case Elem_Item_Date:
266         gm_parse_datetime(value,item.date);
267         if (feed.date==0) feed.date=item.date;
268         value.clear();
269         break;
270       case Elem_Item_Duration:
271         parse_duration(value,item.time);
272         value.clear();
273         break;
274       case Elem_Channel_Date:
275         gm_parse_datetime(value,feed.date);
276         value.clear();
277         break;
278       }
279     }
280 
parse_itunes_image(const FXchar ** attributes)281   void parse_itunes_image(const FXchar** attributes) {
282     for (FXint i=0;attributes[i];i+=2) {
283       if (comparecase(attributes[i],"href")==0){
284         feed.image = attributes[i+1];
285         }
286       }
287     }
288 
parse_itunes_category(const FXchar ** attributes)289   void parse_itunes_category(const FXchar** attributes) {
290     for (FXint i=0;attributes[i];i+=2) {
291       if (comparecase(attributes[i],"text")==0){
292         feed.category = attributes[i+1];
293         unescape_html(feed.category);
294         }
295       }
296     }
297 
parse_enclosure(const FXchar ** attributes)298   void parse_enclosure(const FXchar** attributes) {
299     for (FXint i=0;attributes[i];i+=2) {
300       if (comparecase(attributes[i],"url")==0)
301         item.url = attributes[i+1];
302       else if (comparecase(attributes[i],"length")==0)
303         item.length = FXString(attributes[i+1]).toInt();
304       }
305     }
306 public:
307   enum {
308     Elem_RSS = Elem_Last,
309     Elem_Channel,
310     Elem_Channel_Title,
311     Elem_Channel_Description,
312     Elem_Channel_Category,
313     Elem_Channel_Date,
314     Elem_Channel_Image,
315     Elem_Channel_Image_Url,
316     Elem_Item,
317     Elem_Item_Title,
318     Elem_Item_Description,
319     Elem_Item_Guid,
320     Elem_Item_Date,
321     Elem_Item_Duration,
322     };
323 public:
RssParser()324   RssParser(){}
~RssParser()325   ~RssParser(){}
326   };
327 
328 /*--------------------------------------------------------------------------------------------*/
329 
330 
331 
make_podcast_feed_directory(const FXString & title)332 FXString make_podcast_feed_directory(const FXString & title) {
333   gm::TextConverter filter(gm::TextConverter::NOSPACE);
334   FXString feed = filter.convert(title);
335   FXDir::createDirectories(GMApp::getPodcastDirectory()+PATHSEPSTRING+feed);
336   return feed;
337   }
338 
339 
340 /*--------------------------------------------------------------------------------------------*/
341 
342 class GMImportPodcast : public GMWorker {
343 FXDECLARE(GMImportPodcast)
344 protected:
345   FXString         url;
346   RssParser        rss;
347   GMTrackDatabase* db = nullptr;
348   GMQuery          get_feed;
349   GMQuery          get_tag;
350   GMQuery          add_tag;
351   GMQuery          add_feed;
352 public:
353 protected:
GMImportPodcast()354   GMImportPodcast(){}
355 public:
GMImportPodcast(FXApp * app,GMTrackDatabase * database,const FXString & u)356   GMImportPodcast(FXApp*app,GMTrackDatabase * database,const FXString & u) : GMWorker(app), url(u), db(database) {
357     get_feed = db->compile("SELECT id FROM feeds WHERE url == ?;");
358     get_tag  = db->compile("SELECT id FROM tags WHERE name == ?;");
359     add_tag  = db->compile("INSERT INTO tags VALUES ( NULL, ? );");
360     add_feed = db->compile("INSERT INTO feeds VALUES ( NULL,?,?,?,?,?,?,NULL,NULL,0);");
361     }
362 
insert_feed()363   void insert_feed() {
364     FXint feed=0;
365 
366     get_feed.set(0,url);
367     get_feed.execute(feed);
368     if (feed) return;
369 
370     rss.feed.trim();
371 
372     FXString feed_dir = make_podcast_feed_directory(rss.feed.title);
373 
374     try {
375       GMLockTransaction transaction(db);
376       FXint tag=0;
377 
378       // Allow unset categories, just don't insert empty strings
379       if (!rss.feed.category.empty()) {
380         get_tag.set(0,rss.feed.category);
381         get_tag.execute(tag);
382         if (!tag) {
383           add_tag.set(0,rss.feed.category);
384           tag = add_tag.insert();
385           }
386         }
387 
388       add_feed.set(0,url);
389       add_feed.set(1,rss.feed.title);
390       add_feed.set(2,rss.feed.description);
391       add_feed.set(3,feed_dir);
392       add_feed.set_null(4,tag);
393       add_feed.set(5,rss.feed.date);
394       FXint feed_id = add_feed.insert();
395 
396       GMQuery add_feed_item(db,"INSERT INTO feed_items VALUES ( NULL, ? , ? , ? , NULL, ? , ? , ?, ?, ?, 0)");
397 
398       for (FXint i=0;i<rss.feed.items.no();i++) {
399         add_feed_item.set(0,feed_id);
400         add_feed_item.set(1,rss.feed.items[i].id);
401         add_feed_item.set(2,rss.feed.items[i].url);
402         add_feed_item.set(3,rss.feed.items[i].title);
403         add_feed_item.set(4,rss.feed.items[i].description);
404         add_feed_item.set(5,rss.feed.items[i].length);
405         add_feed_item.set(6,rss.feed.items[i].time);
406         add_feed_item.set(7,rss.feed.items[i].date);
407         add_feed_item.execute();
408         }
409       transaction.commit();
410       }
411     catch(GMDatabaseException&) {
412       }
413     }
414 
onThreadLeave(FXObject *,FXSelector,void *)415   long onThreadLeave(FXObject*,FXSelector,void*) {
416     FXint code=0;
417     if (thread->join(code) && code==0) {
418       insert_feed();
419       GMPlayerManager::instance()->getTrackView()->refresh();
420       }
421     else {
422       GM_DEBUG_PRINT("No feed found code %d\n",code);
423       }
424     delete this;
425     return 1;
426     }
427 
select_feed(const FXArray<FeedLink> & links)428   FXint select_feed(const FXArray<FeedLink> & links){
429     if (links.no()>1) {
430       GMThreadDialog dialog(GMPlayerManager::instance()->getMainWindow(),fxtr("Select Feed"),DECOR_TITLE|DECOR_BORDER|DECOR_RESIZE,0,0,0,0,0,0,0,0,0,0);
431       FXHorizontalFrame *closebox=new FXHorizontalFrame(&dialog,LAYOUT_SIDE_BOTTOM|LAYOUT_FILL_X|PACK_UNIFORM_WIDTH,0,0,0,0);
432       new GMButton(closebox,fxtr("Subscribe"),nullptr,&dialog,FXDialogBox::ID_ACCEPT,BUTTON_INITIAL|BUTTON_DEFAULT|LAYOUT_RIGHT|FRAME_RAISED|FRAME_THICK,0,0,0,0, 15,15);
433       new GMButton(closebox,fxtr("&Cancel"),nullptr,&dialog,FXDialogBox::ID_CANCEL,BUTTON_DEFAULT|LAYOUT_RIGHT|FRAME_RAISED|FRAME_THICK,0,0,0,0, 15,15);
434       FXVerticalFrame * main = new FXVerticalFrame(&dialog,LAYOUT_FILL_X|LAYOUT_FILL_Y,0,0,0,0,10,5,10,10);
435       FXMatrix * matrix = new FXMatrix(main,2,LAYOUT_FILL_X|MATRIX_BY_COLUMNS);
436       new FXLabel(matrix,fxtr("Feed:"),nullptr,LABEL_NORMAL|LAYOUT_RIGHT|LAYOUT_CENTER_Y);
437       GMListBox * feedbox = new GMListBox(matrix,nullptr,0,LAYOUT_FILL_X|FRAME_LINE);
438       for (int i=0;i<links.no();i++){
439         feedbox->appendItem(links[i].description);
440         }
441       feedbox->setNumVisible(FXMIN(feedbox->getNumItems(),9));
442       if (dialog.execute(channel)) {
443         return feedbox->getCurrentItem();
444         }
445       }
446     else if (links.no()==1){
447       return 0;
448       }
449     return -1;
450     }
451 
452 
findFeedLink(const FXString & html,FXArray<FeedLink> & links)453   FXbool findFeedLink(const FXString & html,FXArray<FeedLink> & links) {
454     FXRex link("<link[^>]*>",FXRex::IgnoreCase|FXRex::Normal);
455     FXRex attr("\\s+(\\l\\w*)(?:\\s*=\\s*(?:([\'\"])(.*?)\\2|([^\\s\"\'>]+)))?",FXRex::Capture);
456 
457     FXint b[5],e[5],f=0;
458     while(link.search(html,f,html.length()-1,FXRex::Normal,b,e,1)>=0){
459       f=e[0];
460 
461       FeedLink feed;
462       FXString mimetype;
463       FXString mlink = html.mid(b[0],e[0]-b[0]);
464       GM_DEBUG_PRINT("Link: %s\n",mlink.text());
465 
466       FXint ff=0;
467       while(attr.search(mlink,ff,mlink.length()-1,FXRex::Normal,b,e,5)>=0){
468         if (b[1]>=0) {
469           if (e[1]-b[1]==4) {
470             if (comparecase(&mlink[b[1]],"type",4)==0) {
471               mimetype = (b[2]>0) ? mlink.mid(b[3],e[3]-b[3]) : mlink.mid(b[4],e[4]-b[4]);
472               GM_DEBUG_PRINT("\tmimetype=%s\n",mimetype.text());
473               }
474             else if (comparecase(&mlink[b[1]],"href",4)==0) {
475               feed.url = (b[2]>0) ? mlink.mid(b[3],e[3]-b[3]) : mlink.mid(b[4],e[4]-b[4]);
476               GM_DEBUG_PRINT("\thref=%s\n",feed.url.text());
477               }
478             }
479           else if (e[1]-b[1]==5 && comparecase(&mlink[b[1]],"title",5)==0) {
480             feed.description = (b[2]>0) ? mlink.mid(b[3],e[3]-b[3]) : mlink.mid(b[4],e[4]-b[4]);
481             }
482           }
483         ff=e[0];
484         }
485       if (comparecase(mimetype,"application/rss+xml")==0 && !feed.url.empty()) {
486         links.append(feed);
487         }
488       }
489     return (links.no()>0);
490     }
491 
492 
run()493   FXint run() {
494     HttpClient client;
495 
496     client.setAcceptEncoding(HttpClient::AcceptEncodingGZip);
497 
498     do {
499 
500       if (!client.basic("GET",url))
501         break;
502 
503       HttpMediaType media;
504 
505       if (!client.getContentType(media))
506         break;
507 
508       GM_DEBUG_PRINT("[rss] media.mime %s\n",media.mime.text());
509       if (gm_is_feed(media.mime)) {
510         if (rss.parse(client.body(),media.parameters["charset"]))
511           return 0;
512         else
513           return 1;
514         }
515       else if (comparecase(media.mime,"text/html")==0) {
516         FXArray<FeedLink> links;
517         if (findFeedLink(client.body(),links)) {
518           FXint index = select_feed(links);
519           if (index==-1) return 1;
520           FXString uri = links[index].url;
521           if (uri[0]=='/') {
522             uri = FXURL::scheme(url) + "://" +FXURL::host(url) + uri;
523             }
524           url=uri;
525           continue;
526           }
527         }
528       break;
529       }
530     while(1);
531 
532     return 1;
533     }
534   };
535 
536 FXDEFMAP(GMImportPodcast) GMImportPodcastMap[]={
537   FXMAPFUNC(SEL_COMMAND,GMImportPodcast::ID_THREAD_LEAVE,GMImportPodcast::onThreadLeave),
538   };
539 
540 FXIMPLEMENT(GMImportPodcast,GMWorker,GMImportPodcastMap,ARRAYNUMBER(GMImportPodcastMap));
541 
542 
543 
544 
545 /*--------------------------------------------------------------------------------------------*/
546 
547 
548 class GMDownloader : public GMWorker {
549 FXDECLARE(GMDownloader)
550 protected:
download(const FXString & url,const FXString & filename,FXbool resume=true)551   FXbool download(const FXString & url,const FXString & filename,FXbool resume=true) {
552     HttpClient http;
553     HttpContentRange range;
554     FXFile     file;
555     FXString   headers;
556     FXuint     mode   = FXIO::Writing;
557     FXlong     offset = 0;
558 
559     if (FXStat::exists(filename) && resume) {
560       GM_DEBUG_PRINT("[download] file %s exists trying resume\n",filename.text());
561       mode    = FXIO::ReadWrite|FXIO::Append;
562       offset  = FXStat::size(filename);
563       if (offset>0) {
564         // shutup compiler warnings and just pass string to bytes value
565         headers = FXString::value("Range: bytes=%s-\r\nIf-Range: %s\r\n",FXString::value(offset).text(),gm_rfc1123(FXStat::modified(filename)).text());
566         /// %lld for FOX is FXlong on 32bit and 64bit so ignore any formating warnings.
567         //headers = FXString::value("Range: bytes=%lld-\r\nIf-Range: %s\r\n",offset,gm_rfc1123(FXStat::modified(filename)).text());
568         GM_DEBUG_PRINT("%s\n",headers.text());
569         }
570       }
571 
572     if (!file.open(filename,mode)){
573       GM_DEBUG_PRINT("[download] failed to open local file\n");
574       return false;
575       }
576 
577     if (!http.basic("GET",url,headers)) {
578       GM_DEBUG_PRINT("[download] failed to connect %s\n",url.text());
579       return false;
580       }
581 
582     if (http.status.code == HTTP_PARTIAL_CONTENT) {
583 
584       if (!http.getContentRange(range)) {
585         GM_DEBUG_PRINT("[download] failed to parse content range header\n");
586         return false;
587         }
588 
589       // FIXME make sure range is what we requested...
590       GM_DEBUG_PRINT("[download] http partial content %lld-%lld of %lld\n",range.first,range.last,range.length);
591       }
592     else if (http.status.code == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE) {
593       GM_DEBUG_PRINT("[download] http invalid range. retrying full content\n");
594       http.discard();
595       if (!http.basic("GET",url) || http.status.code!=HTTP_OK)
596         return false;
597       file.truncate(0);
598       }
599     else if (http.status.code == HTTP_OK) {
600       GM_DEBUG_PRINT("[download] get full content\n");
601       file.truncate(0);
602       }
603     else {
604       GM_DEBUG_PRINT("[download] http failed %d\n",http.status.code);
605       return false;
606       }
607 
608     /// Actual transfer
609     FXuchar buffer[4096];
610     FXlong  n,nbytes=0,ntotal=0,ncontent=http.getContentLength();
611     //FXTime  timestamp,last = FXThread::time();
612     //const FXlong seconds   = 1000000000;
613     //const FXlong msample   = 100000000;
614     //const FXlong kilobytes = 1024;
615 
616     while((n=http.readBody(buffer,4096))>0 && processing) {
617       nbytes+=n;
618       ntotal+=n;
619 
620       /* calculate transfer speed
621       timestamp = FXThread::time();
622       if (timestamp-last>msample) {
623         FXuint kbps = (nbytes * seconds ) / (kilobytes*(timestamp-last));
624         FXuint pct = (FXuint)(((double)ntotal/(double)ncontent) * 100.0);
625         nbytes=0;
626         last=timestamp;
627         }
628       */
629 
630       /* Write and check out of disk space */
631       if (file.writeBlock(buffer,n)<n) {
632         GM_DEBUG_PRINT("[download] disk write error\n");
633         return false;
634         }
635       }
636     file.close();
637 
638     /// Set the modtime
639     FXTime modtime=0;
640     if (gm_parse_datetime(http.getHeader("last-modified"),modtime) && modtime!=0) {
641       GM_DEBUG_PRINT("[download] Set Modified to \"%s\"\n",http.getHeader("last-modified").text());
642       FXStat::modified(filename,modtime);
643       }
644 
645     /// Check for partial content
646     if (ncontent!=-1 && ncontent<ntotal){
647       GM_DEBUG_PRINT("[download] Incomplete %ld / %ld\n",ncontent,ntotal);
648       return false;
649       }
650 
651     if (ncontent!=-1)
652       GM_DEBUG_PRINT("[download] Finished %ld / %ld\n",ncontent,ntotal);
653     else
654       GM_DEBUG_PRINT("[download] Finished %ld\n",ncontent);
655 
656     return true;
657     }
658 
GMDownloader()659   GMDownloader(){}
GMDownloader(FXApp * app)660   GMDownloader(FXApp*app) : GMWorker(app) {}
661   };
662 
663 FXIMPLEMENT(GMDownloader,GMWorker,nullptr,0);
664 
665 
666 
667 
668 
669 class GMPodcastDownloader : public GMDownloader {
670 FXDECLARE(GMPodcastDownloader)
671 protected:
672   FXMutex          mutex;
673   FXCondition      condition;
674 
675   FXint            length = 0;
676   FXint            id     = 0;
677   FXString         url;
678   FXString         local;
679   FXString         localdir;
680 
681   GMPodcastSource* src = nullptr;
682   GMTrackDatabase* db  = nullptr;
683 protected:
GMPodcastDownloader()684   GMPodcastDownloader(){}
685 public:
686   enum {
687     ID_DOWNLOAD_COMPLETE = GMWorker::ID_LAST
688     };
689 public:
GMPodcastDownloader(FXApp * app,GMPodcastSource * s)690   GMPodcastDownloader(FXApp*app,GMPodcastSource * s) : GMDownloader(app),src(s), db(s->db) {
691     next_task();
692     }
693 
next_task()694   void next_task() {
695     GMQuery next_download(db,"SELECT feed_items.id, feed_items.url,feeds.local FROM feed_items,feeds WHERE feeds.id == feed_items.feed AND flags&1 ORDER BY feed_items.date LIMIT 1;");
696     id=0;
697     url.clear();
698     local.clear();
699     if (next_download.row()) {
700       next_download.get(0,id);
701       next_download.get(1,url);
702       next_download.get(2,localdir);
703       }
704     }
705 
onDownloadComplete(FXObject *,FXSelector,void *)706   long onDownloadComplete(FXObject*,FXSelector,void*) {
707     mutex.lock();
708 
709     GMQuery update_feed(db,"UPDATE feed_items SET local = ?, time = CASE WHEN time != 0 THEN time ELSE ? END, flags = ((flags&~(1<<?))|(1<<?)) WHERE id == ?;");
710 
711     update_feed.set(0,local);
712     update_feed.set(1,length);
713     update_feed.set(2,ITEM_FLAG_QUEUE);
714     if (!local.empty())
715       update_feed.set(3,ITEM_FLAG_LOCAL);
716     else
717       update_feed.set(3,ITEM_FLAG_DOWNLOAD_FAILED);
718 
719     update_feed.set(4,id);
720     update_feed.execute();
721 
722     GMTrackView * view = GMPlayerManager::instance()->getTrackView();
723     if (view->getSource()==src) {
724       FXint item = view->findTrackIndexById(id);
725       if (item>=0) {
726         GMFeedItem * feeditem = static_cast<GMFeedItem*>(view->getTrackItem(item));
727         FXuint f = feeditem->getFlags();
728         f&=~(1<<ITEM_FLAG_QUEUE);
729         if (!local.empty())
730           f|=(1<<ITEM_FLAG_LOCAL);
731         else
732           f|=(1<<ITEM_FLAG_DOWNLOAD_FAILED);
733         feeditem->setFlags(f);
734         view->updateTrackItem(item);
735         }
736       }
737     next_task();
738     condition.signal();
739     mutex.unlock();
740     return 1;
741     }
742 
onThreadLeave(FXObject *,FXSelector,void *)743   long onThreadLeave(FXObject*,FXSelector,void*) {
744     FXint code=0;
745     if (thread->join(code) && code==0) {
746       }
747     src->downloader = nullptr;
748     delete this;
749     return 1;
750     }
751 
752 
downloadNext()753   void downloadNext() {
754     local  = FXPath::name(FXURL::path(url));
755     length = 0;
756     FXDir::createDirectories(GMApp::getPodcastDirectory()+PATHSEPSTRING+localdir);
757 
758     // Download file
759     if (download(url,GMApp::getPodcastDirectory()+PATHSEPSTRING+localdir+PATHSEPSTRING+local,true)){
760 
761       // Get duration from file
762       GMFileTag tags;
763       if (tags.open(GMApp::getPodcastDirectory()+PATHSEPSTRING+localdir+PATHSEPSTRING+local,FILETAG_AUDIOPROPERTIES)){
764         length = tags.getTime();
765         }
766 
767       }
768     else {
769       local.clear();
770       }
771     }
772 
run()773   FXint run() {
774     while(id && processing) {
775       downloadNext();
776       mutex.lock();
777       send(FXSEL(SEL_COMMAND,ID_DOWNLOAD_COMPLETE));
778       condition.wait(mutex);
779       mutex.unlock();
780       }
781     return 0;
782     }
783   };
784 
785 FXDEFMAP(GMPodcastDownloader) GMPodcastDownloaderMap[]={
786   FXMAPFUNC(SEL_COMMAND,GMPodcastDownloader::ID_THREAD_LEAVE,GMPodcastDownloader::onThreadLeave),
787   FXMAPFUNC(SEL_COMMAND,GMPodcastDownloader::ID_DOWNLOAD_COMPLETE,GMPodcastDownloader::onDownloadComplete),
788   };
789 FXIMPLEMENT(GMPodcastDownloader,GMDownloader,GMPodcastDownloaderMap,ARRAYNUMBER(GMPodcastDownloaderMap));
790 
791 
792 class GMPodcastUpdater : public GMTask {
793 protected:
794   GMTrackDatabase * db;
795 protected:
796   virtual FXint run();
797 public:
798   GMPodcastUpdater(FXObject*tgt,FXSelector sel);
799   virtual ~GMPodcastUpdater();
800   };
801 
802 
803 
GMPodcastUpdater(FXObject * tgt,FXSelector sel)804 GMPodcastUpdater::GMPodcastUpdater(FXObject*tgt,FXSelector sel) : GMTask(tgt,sel) {
805   db = GMPlayerManager::instance()->getTrackDatabase();
806   }
807 
~GMPodcastUpdater()808 GMPodcastUpdater::~GMPodcastUpdater() {
809   }
810 
811 
gm_transfer_file(HttpClient & http,FXFile & file)812 FXbool gm_transfer_file(HttpClient & http,FXFile & file) {
813   FXuchar buffer[4096];
814   FXlong  n;
815   while((n=http.readBody(buffer,4096))>0) {
816     if (file.writeBlock(buffer,n)<n) {
817       return false;
818       }
819     }
820   return true;
821   }
822 
823 
824 
gm_download_cover(const FXString & url,FXString path)825 FXbool gm_download_cover(const FXString & url,FXString path) {
826   FXString filename;
827   HttpClient http;
828   HttpMediaType media;
829   FXString headers;
830   FXString existing;
831 
832   GM_DEBUG_PRINT("[rss] check for updated cover %s\n",url.text());
833 
834   // we'll probably have an existing one
835   if (FXStat::exists(path+PATHSEPSTRING+"cover.jpg")) {
836     existing = path+PATHSEPSTRING+"cover.jpg";
837     }
838   else if (FXStat::exists(path+PATHSEPSTRING+"cover.png")) {
839     existing = path+PATHSEPSTRING+"cover.png";
840     }
841 
842   // conditional request
843   if (!existing.empty())
844     headers=FXString::value("If-Modified-Since: %s\r\n",gm_rfc1123(FXStat::modified(existing)).text());
845 
846   if (http.basic("GET",url,headers)) {
847 
848     if (http.status.code == HTTP_NOT_MODIFIED) {
849       GM_DEBUG_PRINT("[rss] cover not modified\n");
850       return true;
851       }
852 
853     if (http.getContentType(media)) {
854 
855       if (media.mime=="image/jpg" ||  media.mime=="image/jpeg"){
856         filename=path+PATHSEPSTRING+"cover.jpg";
857         }
858       else if (media.mime=="image/png") {
859         filename=path+PATHSEPSTRING+"cover.png";
860         }
861       else {
862         GM_DEBUG_PRINT("[rss] cover unknown mimetype: %s\n",media.mime.text());
863         return false;
864         }
865 
866       FXFile file;
867       if (!file.open(filename,FXIO::Writing))
868         return false;
869 
870       if (!gm_transfer_file(http,file)) {
871         file.close();
872         FXFile::remove(filename);
873         }
874       file.close();
875 
876       // Set the modtime
877       FXTime modtime=0;
878       if (gm_parse_datetime(http.getHeader("last-modified"),modtime) && modtime!=0) {
879         FXStat::modified(filename,modtime);
880         }
881 
882       // Remove previous cover
883       if (existing != filename) {
884         GM_DEBUG_PRINT("[rss] removing old cover %s\n",filename.text());
885         FXFile::remove(existing);
886         }
887 
888       return true;
889       }
890     }
891   return false;
892   }
893 
894 
run()895 FXint GMPodcastUpdater::run() {
896   try {
897 
898     GMQuery all_feeds(db,"SELECT id,url,local,date,autodownload FROM feeds;");
899     GMQuery all_items(db,"SELECT id,guid FROM feed_items WHERE feed = ?;");
900     GMQuery del_items(db,"DELETE FROM feed_items WHERE id == ? AND NOT (flags&2);");
901     GMQuery get_item(db,"SELECT id FROM feed_items WHERE feed == ? AND guid == ?;");
902     GMQuery set_feed(db,"UPDATE feeds SET url = ?, date = ? WHERE id = ?;");
903     GMQuery fix_time(db,"UPDATE feed_items SET time = ? WHERE feed = ? AND guid = ?;");
904     GMQuery add_feed_item(db,"INSERT INTO feed_items VALUES ( NULL, ? , ? , ? , NULL, ? , ? , ?, ?, ?, ?)");
905 
906 
907     taskmanager->setStatus("Syncing Podcasts...");
908     GMTaskTransaction transaction(db);
909 
910     FXTime date;
911     FXString url;
912     FXString guid;
913     FXString feed_dir;
914     FXint id,item_id,autodownload;
915     FXuint flags=0;
916 
917 
918     while(all_feeds.row() && processing) {
919       all_feeds.get(0,id);
920       all_feeds.get(1,url);
921       all_feeds.get(2,feed_dir);
922       all_feeds.get(3,date);
923       all_feeds.get(4,autodownload);
924 
925       if (autodownload)
926         flags|=ITEM_QUEUE;
927       else
928         flags=0;
929 
930       HttpClient    http;
931       HttpMediaType media;
932       FXString      moved;
933 
934       http.setAcceptEncoding(HttpClient::AcceptEncodingGZip);
935 
936       if (!http.basic("GET",url,FXString::null,FXString::null,&moved))
937         continue;
938 
939       if (!http.getContentType(media)) {
940         continue;
941         }
942 
943       if (!gm_is_feed(media.mime)) {
944         GM_DEBUG_PRINT("[rss] \"%s\" not a feed: %s\n",url.text(),media.mime.text());
945         continue;
946         }
947 
948 
949       FXString feed = http.body();
950       RssParser rss;
951 
952       if (rss.parse(feed,media.parameters["charset"])) {
953         if (rss.feed.date!=date) {
954           GM_DEBUG_PRINT("[rss] feed needs updating %s - %s\n",FXSystem::universalTime(rss.feed.date).text(),FXSystem::localTime(rss.feed.date).text());
955 
956           rss.feed.trim();
957 
958           gm_dump_file(GMApp::getPodcastDirectory()+PATHSEPSTRING+feed_dir+PATHSEPSTRING"feed.rss",feed);
959 
960           GM_DEBUG_PRINT("%s - %s\n",url.text(),FXSystem::universalTime(date).text());
961           if (!rss.feed.image.empty()) {
962             gm_download_cover(rss.feed.image,GMApp::getPodcastDirectory()+PATHSEPSTRING+feed_dir);
963             }
964 
965           FXDictionary guids;
966           for (int i=0;i<rss.feed.items.no();i++){
967             if (rss.feed.items[i].guid().empty()) continue;
968             guids.insert(rss.feed.items[i].guid().text(),(void*)(FXival)1);
969             }
970 
971           all_items.set(0,id);
972           while(all_items.row()){
973             all_items.get(0,item_id);
974             all_items.get(1,guid);
975             if (guid.empty() || guids.has(guid)==false){
976               del_items.set(0,item_id);
977               del_items.execute();
978               }
979             }
980           all_items.reset();
981 
982           for (int i=0;i<rss.feed.items.no();i++){
983             item_id=0;
984             get_item.set(0,id);
985             get_item.set(1,rss.feed.items[i].guid());
986             get_item.execute(item_id);
987             if (item_id==0) {
988               add_feed_item.set(0,id);
989               add_feed_item.set(1,rss.feed.items[i].guid());
990               add_feed_item.set(2,rss.feed.items[i].url);
991               add_feed_item.set(3,rss.feed.items[i].title);
992               add_feed_item.set(4,rss.feed.items[i].description);
993               add_feed_item.set(5,rss.feed.items[i].length);
994               add_feed_item.set(6,rss.feed.items[i].time);
995               add_feed_item.set(7,rss.feed.items[i].date);
996               add_feed_item.set(8,flags);
997               add_feed_item.execute();
998               }
999             else {
1000               if (rss.feed.items[i].time) {
1001                 fix_time.set(0,rss.feed.items[i].time);
1002                 fix_time.set(1,id);
1003                 fix_time.set(2,rss.feed.items[i].guid());
1004                 fix_time.execute();
1005                 }
1006               }
1007             }
1008           GM_DEBUG_PRINT("[rss] Update date to %s\n",FXSystem::universalTime(rss.feed.date).text());
1009           if (!moved.empty()) {
1010             GM_DEBUG_PRINT("[rss] feed was moved:\n      from: \"%s\"\n        to: \"%s\"\n",url.text(),moved.text());
1011             set_feed.set(0,moved);
1012             }
1013           else {
1014             set_feed.set(0,url);
1015             }
1016           set_feed.set(1,rss.feed.date);
1017           set_feed.set(2,id);
1018           set_feed.execute();
1019           }
1020         else {
1021           GM_DEBUG_PRINT("[rss] feed is up to date\n");
1022           }
1023         }
1024       else {
1025         GM_DEBUG_PRINT("[rss] failed to parse feed\n");
1026         }
1027       }
1028     transaction.commit();
1029     }
1030   catch(GMDatabaseException&) {
1031     return 1;
1032     }
1033   return 0;
1034   }
1035 
1036 /*--------------------------------------------------------------------------------------------*/
1037 
1038 class GMPodcastFeed : public GMAlbumListItem {
1039   FXDECLARE(GMPodcastFeed)
1040 protected:
GMPodcastFeed()1041   GMPodcastFeed() {}
1042 public:
1043   enum {
1044     AUTODOWNLOAD = 16
1045     };
1046 
1047 public:
GMPodcastFeed(const FXString & feed,FXbool ad,FXint id)1048   GMPodcastFeed(const FXString & feed,FXbool ad,FXint id) : GMAlbumListItem(0,feed,0,id) {
1049     if (ad) state|=AUTODOWNLOAD;
1050     }
1051 
isAutoDownload() const1052   FXbool isAutoDownload() const { return (state&AUTODOWNLOAD)!=0; }
1053 
setAutoDownload(FXbool download)1054   void setAutoDownload(FXbool download) { state^=((0-download)^state)&AUTODOWNLOAD; }
1055 
1056   };
1057 
1058 FXIMPLEMENT(GMPodcastFeed,GMAlbumListItem,nullptr,0);
1059 
1060 
1061 class GMPodcastClipboardData : public GMClipboardData {
1062 public:
1063   GMPodcastSource * src;
1064   FXIntList         ids;
1065 public:
request(FXDragType target,GMClipboard * clipboard)1066   FXbool request(FXDragType target,GMClipboard * clipboard) {
1067     if (target==GMClipboard::urilistType){
1068       FXString uri;
1069       FXStringList filenames;
1070       src->getLocalFiles(ids,filenames);
1071       gm_convert_filenames_to_uri(filenames,uri);
1072       clipboard->setDNDData(FROM_CLIPBOARD,target,uri);
1073       return true;
1074       }
1075     else if (target==GMClipboard::kdeclipboard){
1076       clipboard->setDNDData(FROM_CLIPBOARD,target,"0");
1077       return true;
1078       }
1079     else if (target==GMClipboard::gnomeclipboard){
1080       FXString clipdata;
1081       FXStringList filenames;
1082       src->getLocalFiles(ids,filenames);
1083       gm_convert_filenames_to_gnomeclipboard(filenames,clipdata);
1084       clipboard->setDNDData(FROM_CLIPBOARD,target,clipdata);
1085       return true;
1086       }
1087     return false;
1088     }
1089 
~GMPodcastClipboardData()1090   ~GMPodcastClipboardData() {
1091     src=nullptr;
1092     }
1093   };
1094 
1095 
1096 
1097 FXDEFMAP(GMPodcastSource) GMPodcastSourceMap[]={
1098   FXMAPFUNC(SEL_COMMAND,GMPodcastSource::ID_ADD_FEED,GMPodcastSource::onCmdAddFeed),
1099   FXMAPFUNC(SEL_COMMAND,GMPodcastSource::ID_REFRESH_FEED,GMPodcastSource::onCmdRefreshFeed),
1100   FXMAPFUNC(SEL_TIMEOUT,GMPodcastSource::ID_REFRESH_FEED,GMPodcastSource::onCmdRefreshFeed),
1101   FXMAPFUNC(SEL_COMMAND,GMPodcastSource::ID_DOWNLOAD_FEED,GMPodcastSource::onCmdDownloadFeed),
1102   FXMAPFUNC(SEL_COMMAND,GMPodcastSource::ID_REMOVE_FEED,GMPodcastSource::onCmdRemoveFeed),
1103   FXMAPFUNC(SEL_COMMAND,GMPodcastSource::ID_MARK_NEW,GMPodcastSource::onCmdMarkNew),
1104   FXMAPFUNC(SEL_COMMAND,GMPodcastSource::ID_MARK_PLAYED,GMPodcastSource::onCmdMarkPlayed),
1105   FXMAPFUNC(SEL_COMMAND,GMPodcastSource::ID_DELETE_LOCAL,GMPodcastSource::onCmdDeleteLocal),
1106   FXMAPFUNC(SEL_COMMAND,GMPodcastSource::ID_AUTO_DOWNLOAD,GMPodcastSource::onCmdAutoDownload),
1107   FXMAPFUNC(SEL_TASK_COMPLETED,GMPodcastSource::ID_FEED_UPDATER,GMPodcastSource::onCmdFeedUpdated),
1108   FXMAPFUNC(SEL_TASK_CANCELLED,GMPodcastSource::ID_FEED_UPDATER,GMPodcastSource::onCmdFeedUpdated),
1109   FXMAPFUNC(SEL_TIMEOUT,GMPodcastSource::ID_TRACK_PLAYED,GMPodcastSource::onCmdTrackPlayed),
1110   FXMAPFUNC(SEL_TASK_COMPLETED,GMPodcastSource::ID_LOAD_COVERS,GMPodcastSource::onCmdLoadCovers),
1111   FXMAPFUNC(SEL_TASK_CANCELLED,GMPodcastSource::ID_LOAD_COVERS,GMPodcastSource::onCmdLoadCovers),
1112   FXMAPFUNC(SEL_COMMAND,GMSource::ID_COPY_TRACK,GMPodcastSource::onCmdCopyTrack),
1113   FXMAPFUNC(SEL_DND_REQUEST,GMSource::ID_COPY_TRACK,GMPodcastSource::onCmdRequestTrack)
1114 
1115 
1116   };
1117 FXIMPLEMENT(GMPodcastSource,GMSource,GMPodcastSourceMap,ARRAYNUMBER(GMPodcastSourceMap));
1118 
1119 
GMPodcastSource(GMTrackDatabase * database)1120 GMPodcastSource::GMPodcastSource(GMTrackDatabase * database) : GMSource(), db(database) {
1121   FXASSERT(db);
1122   db->execute("SELECT count(id) FROM feed_items WHERE (flags&4)==0",navailable);
1123   scheduleUpdate();
1124   }
1125 
1126 
~GMPodcastSource()1127 GMPodcastSource::~GMPodcastSource(){
1128   GMApp::instance()->removeTimeout(this,ID_REFRESH_FEED);
1129   if (downloader) {
1130     downloader->stop();
1131     }
1132   delete covercache;
1133   }
1134 
1135 
getLocalFiles(const FXIntList & ids,FXStringList & files)1136 void GMPodcastSource::getLocalFiles(const FXIntList & ids,FXStringList & files) {
1137   GMQuery get_local(db,"SELECT (? || '/' || feeds.local || '/' || feed_items.local)  FROM feed_items,feeds WHERE feeds.id == feed_items.feed AND feed_items.id == ? AND feed_items.flags&2;");
1138   for (FXint i=0;i<ids.no();i++) {
1139     get_local.set(0,GMApp::instance()->getPodcastDirectory());
1140     get_local.set(1,ids[i]);
1141     if (get_local.row()) {
1142       files.no(files.no()+1);
1143       get_local.get(0,files[files.no()-1]);
1144       }
1145     get_local.reset();
1146     }
1147   }
1148 
1149 
1150 
getAlbumIcon() const1151 FXIcon* GMPodcastSource::getAlbumIcon() const {
1152   return GMIconTheme::instance()->icon_podcast;
1153   }
1154 
1155 
loadCovers()1156 void GMPodcastSource::loadCovers() {
1157   if (covercache==nullptr) {
1158     covercache = new GMCoverCache("podcastcovers",GMPlayerManager::instance()->getPreferences().gui_coverdisplay_size);
1159     if (!covercache->load()) {
1160       updateCovers();
1161       }
1162     }
1163   else if (covercache->getSize()!=GMPlayerManager::instance()->getPreferences().gui_coverdisplay_size){
1164     updateCovers();
1165     }
1166   }
1167 
updateCovers()1168 void GMPodcastSource::updateCovers() {
1169   if (covercache) {
1170     GMCoverPathList list;
1171     FXString feed_dir;
1172     FXint feed,n=0;
1173     FXint nfeeds;
1174     db->execute("SELECT COUNT(*) FROM feeds",nfeeds);
1175     if (nfeeds) {
1176       list.no(nfeeds);
1177       GMQuery all_feeds(db,"SELECT id,local FROM feeds");
1178       while(all_feeds.row()){
1179         all_feeds.get(0,feed);
1180         all_feeds.get(1,feed_dir);
1181         list[n].path = GMApp::getPodcastDirectory()+PATHSEPSTRING+feed_dir;
1182         list[n].id   = feed;
1183         n++;
1184         }
1185       GMCoverLoader * loader = new GMCoverLoader(covercache->getTempFilename(),list,GMPlayerManager::instance()->getPreferences().gui_coverdisplay_size,this,ID_LOAD_COVERS);
1186       loader->setFolderOnly(true);
1187       GMPlayerManager::instance()->runTask(loader);
1188       }
1189     }
1190   }
1191 
1192 
1193 
onCmdLoadCovers(FXObject *,FXSelector sel,void * ptr)1194 long GMPodcastSource::onCmdLoadCovers(FXObject*,FXSelector sel,void*ptr) {
1195   GMCoverLoader * loader = *static_cast<GMCoverLoader**>(ptr);
1196   if (FXSELTYPE(sel)==SEL_TASK_COMPLETED) {
1197     covercache->load(loader->getCacheWriter());
1198     GMPlayerManager::instance()->getTrackView()->redrawAlbumList();
1199     }
1200   delete loader;
1201   return 0;
1202   }
1203 
1204 
1205 
updateAvailable()1206 void GMPodcastSource::updateAvailable() {
1207   db->execute("SELECT count(id) FROM feed_items WHERE (flags&4)==0",navailable);
1208   GMPlayerManager::instance()->getSourceView()->refresh(this);
1209   }
1210 
1211 
1212 #define SECONDS 1000000000LL
1213 
1214 
getUpdateInterval() const1215 FXlong GMPodcastSource::getUpdateInterval() const {
1216   return GMApp::instance()->reg().readLongEntry(settingKey(),"update-interval",0);
1217   }
1218 
setUpdateInterval(FXlong interval)1219 void GMPodcastSource::setUpdateInterval(FXlong interval) {
1220   GMApp::instance()->reg().writeLongEntry(settingKey(),"update-interval",interval);
1221   scheduleUpdate();
1222   }
1223 
setLastUpdate()1224 void GMPodcastSource::setLastUpdate() {
1225   GMApp::instance()->reg().writeLongEntry(settingKey(),"last-update",FXThread::time());
1226   scheduleUpdate();
1227   }
1228 
scheduleUpdate()1229 void GMPodcastSource::scheduleUpdate() {
1230   FXlong interval   = getUpdateInterval();
1231   FXTime lastupdate = FXThread::time() - GMApp::instance()->reg().readLongEntry(settingKey(),"last-update",0);
1232   if (interval) {
1233     FXlong next = interval - lastupdate;
1234     GM_DEBUG_PRINT("Podcast schedule %ld %ld %ld\n",interval/SECONDS,lastupdate/SECONDS,next/SECONDS);
1235     if (next<=(10*SECONDS))
1236       GMApp::instance()->addTimeout(this,GMPodcastSource::ID_REFRESH_FEED,1*SECONDS);
1237     else
1238       GMApp::instance()->addTimeout(this,GMPodcastSource::ID_REFRESH_FEED,next);
1239     }
1240   else {
1241     GMApp::instance()->removeTimeout(this,GMPodcastSource::ID_REFRESH_FEED);
1242     }
1243   }
1244 
1245 
1246 
getName() const1247 FXString GMPodcastSource::getName() const {
1248   if (navailable)
1249     return FXString::value(fxtr("Podcasts (%d)"),navailable);
1250   else
1251     return fxtr("Podcasts");
1252   }
1253 
1254 
1255 
removeFeeds(const FXIntList & feeds)1256 void GMPodcastSource::removeFeeds(const FXIntList & feeds) {
1257   GMQuery remove_feed_items(db,"DELETE FROM feed_items WHERE feed = ?;");
1258   GMQuery remove_feed(db,"DELETE FROM feeds WHERE id = ?;");
1259   GMQuery query_feed_dir(db,"SELECT local FROM feeds WHERE id = ?;");
1260   GMQuery query_feed_files(db,"SELECT local FROM feed_items WHERE feed = ? AND flags&2;");
1261 
1262   FXString feed_directory;
1263   FXString file,local;
1264 
1265   for (FXint i=0;i<feeds.no();i++){
1266 
1267     // Get feed directory
1268     query_feed_dir.execute(feeds[i],feed_directory);
1269 
1270     // Get feed files
1271     query_feed_files.set(0,feeds[i]);
1272 
1273     // Construct feed directory
1274     feed_directory.prepend(GMApp::getPodcastDirectory()+PATHSEPSTRING);
1275 
1276     GM_DEBUG_PRINT("feed dir: %s\n",feed_directory.text());
1277 
1278     // Delete all music files
1279     while(query_feed_files.row()) {
1280       query_feed_files.get(0,local);
1281 
1282       if (!local.empty()) {
1283         file = feed_directory+PATHSEPSTRING+local;
1284         GM_DEBUG_PRINT("feed file: %s\n",file.text());
1285         if (FXStat::exists(file))
1286           FXFile::remove(file);
1287         }
1288 
1289       }
1290 
1291     // try removing feed directory
1292     if (FXStat::exists(feed_directory) && !FXDir::remove(feed_directory))
1293       fxwarning("failed to remove feed directory");
1294 
1295     try {
1296       GMLockTransaction transaction(db);
1297 
1298       // Remove feed items
1299       remove_feed_items.update(feeds[i]);
1300 
1301       // Remove feed
1302       remove_feed.update(feeds[i]);
1303 
1304       transaction.commit();
1305       }
1306     catch(GMDatabaseException&) {
1307       }
1308     }
1309   }
1310 
1311 
configure(GMColumnList & list)1312 void GMPodcastSource::configure(GMColumnList& list){
1313   list.no(4);
1314   list[0]=GMColumn(notr("Date"),HEADER_DATE,GMFeedItem::ascendingDate,GMFeedItem::descendingDate,200,true,true,1);
1315   list[1]=GMColumn(notr("Feed"),HEADER_ALBUM,GMFeedItem::ascendingFeed,GMFeedItem::descendingFeed,100,true,true,1);
1316   list[2]=GMColumn(notr("Title"),HEADER_TITLE,GMFeedItem::ascendingTitle,GMFeedItem::descendingTitle,200,true,true,1);
1317   list[3]=GMColumn(notr("Time"),HEADER_TIME,GMFeedItem::ascendingTime,GMFeedItem::descendingTime,200,true,true,1);
1318   }
1319 
1320 
hasCurrentTrack(GMSource * src) const1321 FXbool GMPodcastSource::hasCurrentTrack(GMSource * src) const {
1322   if (src==this) return true;
1323   return false;
1324   }
1325 
setTrack(GMTrack &) const1326 FXbool GMPodcastSource::setTrack(GMTrack&) const {
1327   return false;
1328   }
1329 
getTrack(GMTrack & info) const1330 FXbool GMPodcastSource::getTrack(GMTrack & info) const {
1331   GMQuery q(db,"SELECT feed_items.url,feed_items.local,feeds.local,feeds.title,feed_items.title FROM feed_items,feeds WHERE feeds.id == feed_items.feed AND feed_items.id == ?;");
1332   FXString local;
1333   FXString localdir;
1334   q.set(0,current_track);
1335   if (q.row()) {
1336     q.get(0,info.url);
1337     q.get(1,local);
1338     q.get(2,localdir);
1339     if (!local.empty()){
1340       info.url = GMApp::getPodcastDirectory() + PATHSEPSTRING + localdir + PATHSEPSTRING + local;
1341       }
1342     info.artist       = q.get(3);
1343     info.album_artist = q.get(3);
1344     info.album        = q.get(3);
1345     info.title        = q.get(4);
1346     }
1347   return true;
1348   }
1349 
source_menu(FXMenuPane * pane)1350 FXbool GMPodcastSource::source_menu(FXMenuPane * pane){
1351   new GMMenuCommand(pane,fxtr("Add Podcast…"),nullptr,this,ID_ADD_FEED);
1352   return true;
1353   }
1354 
source_context_menu(FXMenuPane * pane)1355 FXbool GMPodcastSource::source_context_menu(FXMenuPane * pane){
1356   new GMMenuCommand(pane,fxtr("Refresh\t\t"),nullptr,this,ID_REFRESH_FEED);
1357   new GMMenuCommand(pane,fxtr("Add Podcast…"),nullptr,this,ID_ADD_FEED);
1358   return true;
1359   }
1360 
album_context_menu(FXMenuPane * pane)1361 FXbool GMPodcastSource::album_context_menu(FXMenuPane * pane){
1362   GMPodcastFeed * item = static_cast<GMPodcastFeed*>(GMPlayerManager::instance()->getTrackView()->getCurrentAlbumItem());
1363   GMMenuCheck * autodownload = new GMMenuCheck(pane,fxtr("Auto Download"),this,ID_AUTO_DOWNLOAD);
1364   if (item->isAutoDownload())
1365     autodownload->setCheck(true);
1366   new FXMenuSeparator(pane);
1367   new GMMenuCommand(pane,fxtr("Remove Podcast"),nullptr,this,ID_REMOVE_FEED);
1368   return true;
1369   }
1370 
track_context_menu(FXMenuPane * pane)1371 FXbool GMPodcastSource::track_context_menu(FXMenuPane * pane){
1372   new GMMenuCommand(pane,fxtr("Download"),nullptr,this,ID_DOWNLOAD_FEED);
1373   new GMMenuCommand(pane,fxtr("Mark Played"),nullptr,this,ID_MARK_PLAYED);
1374   new GMMenuCommand(pane,fxtr("Mark New"),nullptr,this,ID_MARK_NEW);
1375   new GMMenuCommand(pane,fxtr("Remove Local"),nullptr,this,ID_DELETE_LOCAL);
1376   return true;
1377   }
1378 
listTags(GMList * list,FXIcon * icon)1379 FXbool GMPodcastSource::listTags(GMList * list,FXIcon * icon){
1380   GMQuery q(db,"SELECT id,name FROM tags WHERE id in (SELECT DISTINCT(tag) FROM feeds);");
1381   FXint id;
1382   const FXchar * c_title;
1383   while(q.row()){
1384       q.get(0,id);
1385       c_title = q.get(1);
1386       list->appendItem(c_title,icon,(void*)(FXival)id);
1387       }
1388   return true;
1389   }
1390 
listAlbums(GMAlbumList * list,const FXIntList &,const FXIntList & taglist)1391 FXbool GMPodcastSource::listAlbums(GMAlbumList *list,const FXIntList &,const FXIntList & taglist){
1392   FXString q = "SELECT id,title,autodownload FROM feeds";
1393   if (taglist.no()) {
1394     FXString tagselection;
1395     GMQuery::makeSelection(taglist,tagselection);
1396     q += " WHERE tag " + tagselection;
1397     }
1398 
1399   GMQuery query;
1400   query = db->compile(q);
1401 
1402   const FXchar * c_title;
1403   FXint id,autodownload;
1404   GMPodcastFeed* item;
1405   while(query.row()){
1406       query.get(0,id);
1407       c_title = query.get(1);
1408       query.get(2,autodownload);
1409       item = new GMPodcastFeed(c_title,autodownload,id);
1410       list->appendItem(item);
1411       }
1412 
1413   list->sortItems();
1414   if (list->getNumItems()>1){
1415     FXString all = FXString::value(fxtrformat("All %d Feeds"),list->getNumItems());
1416     list->prependItem(new GMPodcastFeed(all,false,-1));
1417     }
1418   return true;
1419   }
1420 
1421 
listTracks(GMTrackList * tracklist,const FXIntList & albumlist,const FXIntList & taglist)1422 FXbool GMPodcastSource::listTracks(GMTrackList * tracklist,const FXIntList & albumlist,const FXIntList & taglist){
1423   FXString selection,tagselection;
1424   GMQuery::makeSelection(albumlist,selection);
1425   GMQuery::makeSelection(taglist,tagselection);
1426 
1427   FXString query = "SELECT feed_items.id,feeds.title,feed_items.title,time,feed_items.date,flags FROM feed_items, feeds";
1428   query+=" WHERE feeds.id == feed_items.feed";
1429 
1430   if (albumlist.no())
1431     query+=" AND feed " + selection;
1432 
1433   if (taglist.no())
1434     query+=" AND feeds.tag " + tagselection;
1435 
1436   GMQuery q(db,query.text());
1437   const FXchar * c_title;
1438   const FXchar * c_feed;
1439   FXint id;
1440   FXTime date;
1441   FXuint time;
1442   FXuint flags;
1443 
1444   while(q.row()){
1445       q.get(0,id);
1446       c_feed = q.get(1);
1447       c_title = q.get(2);
1448       q.get(3,time);
1449       q.get(4,date);
1450       q.get(5,flags);
1451       GMFeedItem* item = new GMFeedItem(id,c_feed,c_title,date,time,flags);
1452       tracklist->appendItem(item);
1453       }
1454   return true;
1455   }
1456 
1457 
refreshFeeds()1458 void GMPodcastSource::refreshFeeds() {
1459   FXint num_feeds=0;
1460   db->execute("SELECT COUNT(*) FROM feeds;",num_feeds);
1461   if (num_feeds) {
1462     GM_DEBUG_PRINT("Found %d feeds. Running Podcast Updater\n",num_feeds);
1463     GMPlayerManager::instance()->runTask(new GMPodcastUpdater(this,ID_FEED_UPDATER));
1464     }
1465   }
1466 
onCmdRefreshFeed(FXObject *,FXSelector,void *)1467 long GMPodcastSource::onCmdRefreshFeed(FXObject*,FXSelector,void*){
1468   refreshFeeds();
1469   return 1;
1470   }
1471 
1472 
setItemFlags(FXuint add,FXuint remove,FXuint condition)1473 void GMPodcastSource::setItemFlags(FXuint add,FXuint remove,FXuint condition){
1474   GMQuery q(db,"UPDATE feed_items SET flags = (flags&~(?))|? WHERE flags&?");
1475   q.set(0,remove);
1476   q.set(1,add);
1477   q.set(2,condition);
1478   q.execute();
1479   }
1480 
1481 
1482 
onCmdFeedUpdated(FXObject *,FXSelector,void * ptr)1483 long GMPodcastSource::onCmdFeedUpdated(FXObject*,FXSelector,void*ptr){
1484   GMTask * task = *static_cast<GMTask**>(ptr);
1485   db->execute("SELECT count(id) FROM feed_items WHERE (flags&4)==0",navailable);
1486 
1487   // Retry any failed downloads
1488   setItemFlags(ITEM_QUEUE,ITEM_FAILED,ITEM_FAILED);
1489 
1490   if (downloader==nullptr) {
1491     FXint n;
1492     db->execute("SELECT count(id) FROM feed_items WHERE flags&1",n);
1493     if (n)  {
1494       GM_DEBUG_PRINT("Found %d queued. Start fetching\n",n);
1495       downloader = new GMPodcastDownloader(FXApp::instance(),this);
1496       downloader->start();
1497       }
1498     }
1499 
1500   GMPlayerManager::instance()->getSourceView()->refresh(this);
1501   setLastUpdate();
1502   delete task;
1503   return 0;
1504   }
1505 
onCmdDownloadFeed(FXObject *,FXSelector,void *)1506 long GMPodcastSource::onCmdDownloadFeed(FXObject*,FXSelector,void*){
1507   try {
1508     GMLockTransaction transaction(db);
1509     FXIntList tracks;
1510     GMPlayerManager::instance()->getTrackView()->getSelectedTracks(tracks);
1511     GMQuery queue_tracks(db,"UPDATE feed_items SET flags = (flags|1) WHERE id == ?;");
1512     for (FXint i=0;i<tracks.no();i++){
1513       queue_tracks.set(0,tracks[i]);
1514       queue_tracks.execute();
1515       }
1516     transaction.commit();
1517     if (downloader==nullptr) {
1518       downloader = new GMPodcastDownloader(FXApp::instance(),this);
1519       downloader->start();
1520       }
1521     }
1522   catch(GMDatabaseException&) {
1523     }
1524   return 1;
1525   }
1526 
1527 
onCmdDeleteLocal(FXObject *,FXSelector,void *)1528 long GMPodcastSource::onCmdDeleteLocal(FXObject*,FXSelector,void*){
1529   FXIntList tracks;
1530   GMPlayerManager::instance()->getTrackView()->getSelectedTracks(tracks);
1531   GMQuery get_local(db,"SELECT (? || '/' || feeds.local || '/' || feed_items.local)  FROM feed_items,feeds WHERE feeds.id == feed_items.feed AND feed_items.id == ? AND feed_items.flags&2;");
1532   GMQuery clear_local(db,"UPDATE feed_items SET flags = (flags&~(?)) WHERE id == ?");
1533   FXString local;
1534   FXString localdir;
1535   for (FXint i=0;i<tracks.no();i++) {
1536     get_local.set(0,GMApp::getPodcastDirectory());
1537     get_local.set(1,tracks[i]);
1538     if (get_local.row()) {
1539       get_local.get(0,local);
1540       GM_DEBUG_PRINT("delete local: %s\n",local.text());
1541       if (FXFile::remove(local) || !FXStat::exists(local)) {
1542         clear_local.set(0,ITEM_LOCAL);
1543         clear_local.set(1,tracks[i]);
1544         clear_local.execute();
1545         }
1546       else {
1547         GM_DEBUG_PRINT("failed to remove local: %s\n",local.text());
1548         }
1549       get_local.reset();
1550       }
1551     }
1552   return 1;
1553   }
1554 
onCmdAutoDownload(FXObject *,FXSelector,void *)1555 long GMPodcastSource::onCmdAutoDownload(FXObject*,FXSelector,void*){
1556   try {
1557     GMLockTransaction transaction(db);
1558     GMPodcastFeed * item = static_cast<GMPodcastFeed*>(GMPlayerManager::instance()->getTrackView()->getCurrentAlbumItem());
1559     GMQuery update_feed(db,"UPDATE feeds SET autodownload = ? WHERE id == ?;");
1560     update_feed.set(0,!item->isAutoDownload());
1561     update_feed.set(1,item->getId());
1562     update_feed.execute();
1563     transaction.commit();
1564     item->setAutoDownload(!item->isAutoDownload());
1565     }
1566   catch(GMDatabaseException&) {
1567     }
1568   return 1;
1569   }
1570 
1571 
1572 
1573 
1574 
1575 
1576 
1577 
1578 
1579 
1580 
1581 
1582 
1583 
1584 
1585 
onCmdMarkNew(FXObject *,FXSelector,void *)1586 long GMPodcastSource::onCmdMarkNew(FXObject*,FXSelector,void*){
1587   try {
1588     GMLockTransaction transaction(db);
1589     FXIntList tracks;
1590     GMPlayerManager::instance()->getTrackView()->getSelectedTracks(tracks);
1591     GMQuery mark_tracks(db,"UPDATE feed_items SET flags = (flags&~(4)) WHERE id == ?;");
1592     for (FXint i=0;i<tracks.no();i++){
1593       mark_tracks.set(0,tracks[i]);
1594       mark_tracks.execute();
1595       }
1596     transaction.commit();
1597     updateAvailable();
1598     }
1599   catch(GMDatabaseException&) {
1600     }
1601   return 1;
1602   }
1603 
1604 
onCmdMarkPlayed(FXObject *,FXSelector,void *)1605 long GMPodcastSource::onCmdMarkPlayed(FXObject*,FXSelector,void*){
1606   try {
1607     GMLockTransaction transaction(db);
1608     FXIntList tracks;
1609     GMPlayerManager::instance()->getTrackView()->getSelectedTracks(tracks);
1610     GMQuery queue_tracks(db,"UPDATE feed_items SET flags = (flags|4) WHERE id == ?;");
1611     for (FXint i=0;i<tracks.no();i++){
1612       queue_tracks.set(0,tracks[i]);
1613       queue_tracks.execute();
1614       }
1615     transaction.commit();
1616     updateAvailable();
1617     }
1618   catch(GMDatabaseException&) {
1619     }
1620   return 1;
1621   }
1622 
onCmdTrackPlayed(FXObject *,FXSelector,void *)1623 long GMPodcastSource::onCmdTrackPlayed(FXObject*,FXSelector,void*) {
1624   try {
1625     GMLockTransaction transaction(db);
1626     FXTRACE((60,"%s::onCmdTrackPlayed\n",getClassName()));
1627     FXASSERT(current_track>=0);
1628     GMQuery set_played(db,"UPDATE feed_items SET flags = (flags|4) WHERE id == ?;");
1629     set_played.set(0,current_track);
1630     set_played.execute();
1631     transaction.commit();
1632     updateAvailable();
1633     }
1634   catch(GMDatabaseException&) {
1635     }
1636   return 1;
1637   }
1638 
1639 
1640 
1641 
onCmdAddFeed(FXObject *,FXSelector,void *)1642 long GMPodcastSource::onCmdAddFeed(FXObject*,FXSelector,void*){
1643   FXDialogBox dialog(GMPlayerManager::instance()->getMainWindow(),fxtr("Subscribe to Podcast"),DECOR_TITLE|DECOR_BORDER|DECOR_RESIZE,0,0,0,0,0,0,0,0,0,0);
1644   GMPlayerManager::instance()->getMainWindow()->create_dialog_header(&dialog,fxtr("Subscribe to Podcast"),fxtr("Specify url for the rss feed"),nullptr);
1645   FXHorizontalFrame *closebox=new FXHorizontalFrame(&dialog,LAYOUT_SIDE_BOTTOM|LAYOUT_FILL_X|PACK_UNIFORM_WIDTH,0,0,0,0);
1646   new GMButton(closebox,fxtr("Subscribe"),nullptr,&dialog,FXDialogBox::ID_ACCEPT,BUTTON_INITIAL|BUTTON_DEFAULT|LAYOUT_RIGHT|FRAME_RAISED|FRAME_THICK,0,0,0,0, 15,15);
1647   new GMButton(closebox,fxtr("&Cancel"),nullptr,&dialog,FXDialogBox::ID_CANCEL,BUTTON_DEFAULT|LAYOUT_RIGHT|FRAME_RAISED|FRAME_THICK,0,0,0,0, 15,15);
1648   new FXSeparator(&dialog,SEPARATOR_GROOVE|LAYOUT_FILL_X|LAYOUT_SIDE_BOTTOM);
1649   FXVerticalFrame * main = new FXVerticalFrame(&dialog,LAYOUT_FILL_X|LAYOUT_FILL_Y,0,0,0,0,10,5,10,10);
1650   FXMatrix * matrix = new FXMatrix(main,2,LAYOUT_FILL_X|MATRIX_BY_COLUMNS);
1651   new FXLabel(matrix,fxtr("Location"),nullptr,LABEL_NORMAL|LAYOUT_RIGHT|LAYOUT_CENTER_Y);
1652   GMTextField * location_field = new GMTextField(matrix,40,nullptr,0,LAYOUT_FILL_X|LAYOUT_FILL_COLUMN|FRAME_SUNKEN|FRAME_THICK);
1653   if (dialog.execute()) {
1654     FXString url=location_field->getText().trim();
1655     if (!url.empty()) {
1656       GMImportPodcast * podcast = new GMImportPodcast(FXApp::instance(),db,url);
1657       podcast->start();
1658       }
1659     }
1660   return 1;
1661   }
1662 
onCmdRemoveFeed(FXObject *,FXSelector,void *)1663 long GMPodcastSource::onCmdRemoveFeed(FXObject*,FXSelector,void*){
1664   FXIntList feeds;
1665   GMPlayerManager::instance()->getTrackView()->getSelectedAlbums(feeds);
1666   if (FXMessageBox::question(GMPlayerManager::instance()->getMainWindow(),MBOX_YES_NO,fxtr("Remove Feed?"),fxtr("Remove feed and all downloaded episodes?"))==MBOX_CLICKED_YES) {
1667     removeFeeds(feeds);
1668     GMPlayerManager::instance()->getTrackView()->refresh();
1669     }
1670   return 1;
1671   }
1672 
dnd_provides(FXDragType types[])1673 FXuint GMPodcastSource::dnd_provides(FXDragType types[]){
1674   types[0]=GMClipboard::kdeclipboard;
1675   types[1]=GMClipboard::urilistType;
1676   return 2;
1677   }
1678 
onCmdCopyTrack(FXObject *,FXSelector,void *)1679 long GMPodcastSource::onCmdCopyTrack(FXObject*,FXSelector,void*){
1680   FXDragType types[3]={GMClipboard::kdeclipboard,GMClipboard::gnomeclipboard,FXWindow::urilistType};
1681   GMPodcastClipboardData * data = new GMPodcastClipboardData;
1682   if (GMClipboard::instance()->acquire(this,types,3,data)){
1683     FXApp::instance()->beginWaitCursor();
1684     data->src=this;
1685     GMPlayerManager::instance()->getTrackView()->getSelectedTracks(data->ids);
1686     FXApp::instance()->endWaitCursor();
1687     }
1688   else {
1689     delete data;
1690     FXApp::instance()->beep();
1691     }
1692   return 1;
1693   }
1694 
onCmdRequestTrack(FXObject * sender,FXSelector,void * ptr)1695 long GMPodcastSource::onCmdRequestTrack(FXObject*sender,FXSelector,void*ptr){
1696   FXEvent* event=(FXEvent*)ptr;
1697   FXWindow*window=(FXWindow*)sender;
1698   if(event->target==GMClipboard::urilistType){
1699     FXStringList filenames;
1700     FXIntList tracks;
1701     FXString uri;
1702     GMPlayerManager::instance()->getTrackView()->getSelectedTracks(tracks);
1703     getLocalFiles(tracks,filenames);
1704     gm_convert_filenames_to_uri(filenames,uri);
1705     window->setDNDData(FROM_DRAGNDROP,event->target,uri);
1706     return 1;
1707     }
1708   else if (event->target==GMClipboard::kdeclipboard){
1709     window->setDNDData(FROM_DRAGNDROP,event->target,"0"); // copy
1710     return 1;
1711     }
1712   return 0;
1713   }
1714