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("&","&");
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