1 /*
2 * This file is part of nzbget. See <http://nzbget.net>.
3 *
4 * Copyright (C) 2007-2017 Andrey Prygunkov <hugbug@users.sourceforge.net>
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 2 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
20
21 #include "nzbget.h"
22 #include "DownloadInfo.h"
23 #include "QueueEditor.h"
24 #include "Options.h"
25 #include "Log.h"
26 #include "Util.h"
27 #include "FileSystem.h"
28 #include "QueueCoordinator.h"
29 #include "PrePostProcessor.h"
30 #include "HistoryCoordinator.h"
31 #include "UrlCoordinator.h"
32 #include "ParParser.h"
33
34 const int MAX_ID = 1000000000;
35
36 class GroupSorter
37 {
38 public:
GroupSorter(NzbList * nzbList,QueueEditor::ItemList * sortItemList)39 GroupSorter(NzbList* nzbList, QueueEditor::ItemList* sortItemList) :
40 m_nzbList(nzbList), m_sortItemList(sortItemList) {}
41 bool Execute(const char* sort);
42 bool operator()(const std::unique_ptr<NzbInfo>& refNzbInfo1, const std::unique_ptr<NzbInfo>& refNzbInfo2) const;
43
44 private:
45 enum ESortCriteria
46 {
47 scName,
48 scSize,
49 scRemainingSize,
50 scAge,
51 scCategory,
52 scPriority
53 };
54
55 enum ESortOrder
56 {
57 soAscending,
58 soDescending,
59 soAuto
60 };
61
62 NzbList* m_nzbList;
63 QueueEditor::ItemList* m_sortItemList;
64 ESortCriteria m_sortCriteria;
65 ESortOrder m_sortOrder;
66 };
67
Execute(const char * sort)68 bool GroupSorter::Execute(const char* sort)
69 {
70 if (!strcasecmp(sort, "name") || !strcasecmp(sort, "name+") || !strcasecmp(sort, "name-"))
71 {
72 m_sortCriteria = scName;
73 }
74 else if (!strcasecmp(sort, "size") || !strcasecmp(sort, "size+") || !strcasecmp(sort, "size-"))
75 {
76 m_sortCriteria = scSize;
77 }
78 else if (!strcasecmp(sort, "left") || !strcasecmp(sort, "left+") || !strcasecmp(sort, "left-"))
79 {
80 m_sortCriteria = scRemainingSize;
81 }
82 else if (!strcasecmp(sort, "age") || !strcasecmp(sort, "age+") || !strcasecmp(sort, "age-"))
83 {
84 m_sortCriteria = scAge;
85 }
86 else if (!strcasecmp(sort, "category") || !strcasecmp(sort, "category+") || !strcasecmp(sort, "category-"))
87 {
88 m_sortCriteria = scCategory;
89 }
90 else if (!strcasecmp(sort, "priority") || !strcasecmp(sort, "priority+") || !strcasecmp(sort, "priority-"))
91 {
92 m_sortCriteria = scPriority;
93 }
94 else
95 {
96 error("Could not sort groups: incorrect sort order (%s)", sort);
97 return false;
98 }
99
100 char lastCh = sort[strlen(sort) - 1];
101 if (lastCh == '+')
102 {
103 m_sortOrder = soAscending;
104 }
105 else if (lastCh == '-')
106 {
107 m_sortOrder = soDescending;
108 }
109 else
110 {
111 m_sortOrder = soAuto;
112 }
113
114 RawNzbList tempList;
115 for (NzbInfo* nzbInfo : m_nzbList)
116 {
117 tempList.push_back(nzbInfo);
118 }
119
120 ESortOrder origSortOrder = m_sortOrder;
121 if (m_sortOrder == soAuto && m_sortCriteria == scPriority)
122 {
123 m_sortOrder = soDescending;
124 }
125
126 std::stable_sort(m_nzbList->begin(), m_nzbList->end(), *this);
127
128 if (origSortOrder == soAuto &&
129 std::equal(tempList.begin(), tempList.end(), m_nzbList->begin(),
130 [](NzbInfo* nzbInfo1, std::unique_ptr<NzbInfo>& nzbInfo2)
131 {
132 return nzbInfo1 == nzbInfo2.get();
133 }))
134 {
135 m_sortOrder = m_sortOrder == soDescending ? soAscending : soDescending;
136 std::stable_sort(m_nzbList->begin(), m_nzbList->end(), *this);
137 }
138
139 return true;
140 }
141
operator ()(const std::unique_ptr<NzbInfo> & refNzbInfo1,const std::unique_ptr<NzbInfo> & refNzbInfo2) const142 bool GroupSorter::operator()(const std::unique_ptr<NzbInfo>& refNzbInfo1, const std::unique_ptr<NzbInfo>& refNzbInfo2) const
143 {
144 NzbInfo* nzbInfo1 = refNzbInfo1.get();
145 NzbInfo* nzbInfo2 = refNzbInfo2.get();
146
147 // if list of ID is empty - sort all items
148 bool sortItem1 = m_sortItemList->empty();
149 bool sortItem2 = m_sortItemList->empty();
150
151 for (QueueEditor::EditItem& item : m_sortItemList)
152 {
153 sortItem1 |= item.m_nzbInfo == nzbInfo1;
154 sortItem2 |= item.m_nzbInfo == nzbInfo2;
155 }
156
157 if (!sortItem1 || !sortItem2)
158 {
159 return false;
160 }
161
162 bool ret = false;
163
164 if (m_sortOrder == soDescending)
165 {
166 std::swap(nzbInfo1, nzbInfo2);
167 }
168
169 switch (m_sortCriteria)
170 {
171 case scName:
172 ret = strcmp(nzbInfo1->GetName(), nzbInfo2->GetName()) < 0;
173 break;
174
175 case scSize:
176 ret = nzbInfo1->GetSize() < nzbInfo2->GetSize();
177 break;
178
179 case scRemainingSize:
180 ret = nzbInfo1->GetRemainingSize() - nzbInfo1->GetPausedSize() <
181 nzbInfo2->GetRemainingSize() - nzbInfo2->GetPausedSize();
182 break;
183
184 case scAge:
185 ret = nzbInfo1->GetMinTime() > nzbInfo2->GetMinTime();
186 break;
187
188 case scCategory:
189 ret = strcmp(nzbInfo1->GetCategory(), nzbInfo2->GetCategory()) < 0;
190 break;
191
192 case scPriority:
193 ret = nzbInfo1->GetPriority() < nzbInfo2->GetPriority();
194 break;
195 }
196
197 return ret;
198 }
199
200
FindFileInfo(int id)201 FileInfo* QueueEditor::FindFileInfo(int id)
202 {
203 for (NzbInfo* nzbInfo : m_downloadQueue->GetQueue())
204 {
205 FileInfo* fileInfo = nzbInfo->GetFileList()->Find(id);
206 if (fileInfo)
207 {
208 return fileInfo;
209 }
210 }
211 return nullptr;
212 }
213
PauseUnpauseEntry(FileInfo * fileInfo,bool pause)214 void QueueEditor::PauseUnpauseEntry(FileInfo* fileInfo, bool pause)
215 {
216 fileInfo->SetPaused(pause);
217 }
218
DeleteEntry(FileInfo * fileInfo)219 void QueueEditor::DeleteEntry(FileInfo* fileInfo)
220 {
221 if (!fileInfo->GetDeleted())
222 {
223 fileInfo->GetNzbInfo()->PrintMessage(
224 fileInfo->GetNzbInfo()->GetDeleting() ? Message::mkDetail : Message::mkInfo,
225 "Deleting file %s from download queue", fileInfo->GetFilename());
226 g_QueueCoordinator->DeleteQueueEntry(m_downloadQueue, fileInfo);
227 }
228 }
229
MoveEntry(FileInfo * fileInfo,int offset)230 void QueueEditor::MoveEntry(FileInfo* fileInfo, int offset)
231 {
232 int entry = 0;
233 for (FileInfo* fileInfo2 : fileInfo->GetNzbInfo()->GetFileList())
234 {
235 if (fileInfo2 == fileInfo)
236 {
237 break;
238 }
239 entry++;
240 }
241
242 int newEntry = entry + offset;
243 int size = (int)fileInfo->GetNzbInfo()->GetFileList()->size();
244
245 if (newEntry < 0)
246 {
247 newEntry = 0;
248 }
249 if (newEntry > size - 1)
250 {
251 newEntry = (int)size - 1;
252 }
253
254 if (newEntry >= 0 && newEntry <= size - 1)
255 {
256 std::unique_ptr<FileInfo> movedFileInfo = std::move(*(fileInfo->GetNzbInfo()->GetFileList()->begin() + entry));
257 fileInfo->GetNzbInfo()->GetFileList()->erase(fileInfo->GetNzbInfo()->GetFileList()->begin() + entry);
258 fileInfo->GetNzbInfo()->GetFileList()->insert(fileInfo->GetNzbInfo()->GetFileList()->begin() + newEntry, std::move(movedFileInfo));
259 }
260 }
261
MoveGroup(NzbInfo * nzbInfo,int offset)262 void QueueEditor::MoveGroup(NzbInfo* nzbInfo, int offset)
263 {
264 int entry = 0;
265 for (NzbInfo* nzbInfo2 : m_downloadQueue->GetQueue())
266 {
267 if (nzbInfo2 == nzbInfo)
268 {
269 break;
270 }
271 entry++;
272 }
273
274 int newEntry = entry + offset;
275 int size = (int)m_downloadQueue->GetQueue()->size();
276
277 if (newEntry < 0)
278 {
279 newEntry = 0;
280 }
281 if (newEntry > size - 1)
282 {
283 newEntry = (int)size - 1;
284 }
285
286 if (newEntry >= 0 && newEntry <= size - 1)
287 {
288 std::unique_ptr<NzbInfo> movedNzbInfo = std::move(*(m_downloadQueue->GetQueue()->begin() + entry));
289 m_downloadQueue->GetQueue()->erase(m_downloadQueue->GetQueue()->begin() + entry);
290 m_downloadQueue->GetQueue()->insert(m_downloadQueue->GetQueue()->begin() + newEntry, std::move(movedNzbInfo));
291 }
292 }
293
EditEntry(DownloadQueue * downloadQueue,int ID,DownloadQueue::EEditAction action,const char * args)294 bool QueueEditor::EditEntry(DownloadQueue* downloadQueue, int ID, DownloadQueue::EEditAction action, const char* args)
295 {
296 m_downloadQueue = downloadQueue;
297 IdList cIdList;
298 cIdList.push_back(ID);
299 return InternEditList(nullptr, &cIdList, action, args);
300 }
301
EditList(DownloadQueue * downloadQueue,IdList * idList,NameList * nameList,DownloadQueue::EMatchMode matchMode,DownloadQueue::EEditAction action,const char * args)302 bool QueueEditor::EditList(DownloadQueue* downloadQueue, IdList* idList, NameList* nameList, DownloadQueue::EMatchMode matchMode,
303 DownloadQueue::EEditAction action, const char* args)
304 {
305 if (action == DownloadQueue::eaPostDelete)
306 {
307 return g_PrePostProcessor->EditList(downloadQueue, idList, action, args);
308 }
309 else if (DownloadQueue::eaHistoryDelete <= action && action <= DownloadQueue::eaHistorySetName)
310 {
311 return g_HistoryCoordinator->EditList(downloadQueue, idList, action, args);
312 }
313
314 m_downloadQueue = downloadQueue;
315 bool ok = true;
316
317 std::unique_ptr<IdList> nameIdList;
318
319 if (nameList)
320 {
321 nameIdList = std::make_unique<IdList>();
322 idList = nameIdList.get();
323 ok = BuildIdListFromNameList(idList, nameList, matchMode, action);
324 }
325
326 ok = ok && (InternEditList(nullptr, idList, action, args) || matchMode == DownloadQueue::mmRegEx);
327
328 m_downloadQueue->Save();
329
330 return ok;
331 }
332
InternEditList(ItemList * itemList,IdList * idList,DownloadQueue::EEditAction action,const char * args)333 bool QueueEditor::InternEditList(ItemList* itemList,
334 IdList* idList, DownloadQueue::EEditAction action, const char* args)
335 {
336 ItemList workItems;
337 if (!itemList)
338 {
339 itemList = &workItems;
340 int offset = args && (action == DownloadQueue::eaFileMoveOffset ||
341 action == DownloadQueue::eaGroupMoveOffset) ? atoi(args) : 0;
342 PrepareList(itemList, idList, action, offset);
343 }
344
345 switch (action)
346 {
347 case DownloadQueue::eaFilePauseAllPars:
348 case DownloadQueue::eaFilePauseExtraPars:
349 PauseParsInGroups(itemList, action == DownloadQueue::eaFilePauseExtraPars);
350 break;
351
352 case DownloadQueue::eaGroupMerge:
353 return MergeGroups(itemList);
354
355 case DownloadQueue::eaGroupSort:
356 return SortGroups(itemList, args);
357
358 case DownloadQueue::eaGroupMoveAfter:
359 case DownloadQueue::eaGroupMoveBefore:
360 return MoveGroupsTo(itemList, idList, action == DownloadQueue::eaGroupMoveBefore, args);
361
362 case DownloadQueue::eaFileSplit:
363 return SplitGroup(itemList, args);
364
365 case DownloadQueue::eaFileReorder:
366 ReorderFiles(itemList);
367 break;
368
369 default:
370 for (EditItem& item : itemList)
371 {
372 switch (action)
373 {
374 case DownloadQueue::eaFilePause:
375 PauseUnpauseEntry(item.m_fileInfo, true);
376 break;
377
378 case DownloadQueue::eaFileResume:
379 PauseUnpauseEntry(item.m_fileInfo, false);
380 break;
381
382 case DownloadQueue::eaFileMoveOffset:
383 case DownloadQueue::eaFileMoveTop:
384 case DownloadQueue::eaFileMoveBottom:
385 MoveEntry(item.m_fileInfo, item.m_offset);
386 break;
387
388 case DownloadQueue::eaFileDelete:
389 DeleteEntry(item.m_fileInfo);
390 break;
391
392 case DownloadQueue::eaGroupSetPriority:
393 SetNzbPriority(item.m_nzbInfo, args);
394 break;
395
396 case DownloadQueue::eaGroupSetCategory:
397 case DownloadQueue::eaGroupApplyCategory:
398 SetNzbCategory(item.m_nzbInfo, args, action == DownloadQueue::eaGroupApplyCategory);
399 break;
400
401 case DownloadQueue::eaGroupSetName:
402 SetNzbName(item.m_nzbInfo, args);
403 break;
404
405 case DownloadQueue::eaGroupSetDupeKey:
406 case DownloadQueue::eaGroupSetDupeScore:
407 case DownloadQueue::eaGroupSetDupeMode:
408 SetNzbDupeParam(item.m_nzbInfo, action, args);
409 break;
410
411 case DownloadQueue::eaGroupSetParameter:
412 SetNzbParameter(item.m_nzbInfo, args);
413 break;
414
415 case DownloadQueue::eaGroupMoveTop:
416 case DownloadQueue::eaGroupMoveBottom:
417 case DownloadQueue::eaGroupMoveOffset:
418 MoveGroup(item.m_nzbInfo, item.m_offset);
419 break;
420
421 case DownloadQueue::eaGroupPause:
422 case DownloadQueue::eaGroupResume:
423 case DownloadQueue::eaGroupPauseAllPars:
424 case DownloadQueue::eaGroupPauseExtraPars:
425 EditGroup(item.m_nzbInfo, action, args);
426 break;
427
428 case DownloadQueue::eaGroupDelete:
429 case DownloadQueue::eaGroupParkDelete:
430 case DownloadQueue::eaGroupDupeDelete:
431 case DownloadQueue::eaGroupFinalDelete:
432 if (item.m_nzbInfo->GetKind() == NzbInfo::nkUrl)
433 {
434 DeleteUrl(item.m_nzbInfo, action);
435 }
436 else
437 {
438 EditGroup(item.m_nzbInfo, action, args);
439 }
440 break;
441
442 case DownloadQueue::eaGroupSortFiles:
443 SortGroupFiles(item.m_nzbInfo);
444 break;
445
446 default:
447 // suppress compiler warning "enumeration not handled in switch"
448 break;
449 }
450 }
451 }
452
453 return itemList->size() > 0;
454 }
455
PrepareList(ItemList * itemList,IdList * idList,DownloadQueue::EEditAction action,int offset)456 void QueueEditor::PrepareList(ItemList* itemList, IdList* idList,
457 DownloadQueue::EEditAction action, int offset)
458 {
459 if (action == DownloadQueue::eaFileMoveTop || action == DownloadQueue::eaGroupMoveTop)
460 {
461 offset = -MAX_ID;
462 }
463 else if (action == DownloadQueue::eaFileMoveBottom || action == DownloadQueue::eaGroupMoveBottom)
464 {
465 offset = MAX_ID;
466 }
467
468 itemList->reserve(idList->size());
469 if ((offset != 0) &&
470 (action == DownloadQueue::eaFileMoveOffset || action == DownloadQueue::eaFileMoveTop || action == DownloadQueue::eaFileMoveBottom))
471 {
472 // add IDs to list in order they currently have in download queue
473 for (NzbInfo* nzbInfo : m_downloadQueue->GetQueue())
474 {
475 int nrEntries = (int)nzbInfo->GetFileList()->size();
476 int lastDestPos = -1;
477 int start, end, step;
478 if (offset < 0)
479 {
480 start = 0;
481 end = nrEntries;
482 step = 1;
483 }
484 else
485 {
486 start = nrEntries - 1;
487 end = -1;
488 step = -1;
489 }
490 for (int index = start; index != end; index += step)
491 {
492 std::unique_ptr<FileInfo>& fileInfo = nzbInfo->GetFileList()->at(index);
493 IdList::iterator it2 = std::find(idList->begin(), idList->end(), fileInfo->GetId());
494 if (it2 != idList->end())
495 {
496 int workOffset = offset;
497 int destPos = index + workOffset;
498 if (lastDestPos == -1)
499 {
500 if (destPos < 0)
501 {
502 workOffset = -index;
503 }
504 else if (destPos > nrEntries - 1)
505 {
506 workOffset = nrEntries - 1 - index;
507 }
508 }
509 else
510 {
511 if (workOffset < 0 && destPos <= lastDestPos)
512 {
513 workOffset = lastDestPos - index + 1;
514 }
515 else if (workOffset > 0 && destPos >= lastDestPos)
516 {
517 workOffset = lastDestPos - index - 1;
518 }
519 }
520 lastDestPos = index + workOffset;
521 itemList->emplace_back(fileInfo.get(), nullptr, workOffset);
522 }
523 }
524 }
525 }
526 else if (((offset != 0) &&
527 (action == DownloadQueue::eaGroupMoveOffset || action == DownloadQueue::eaGroupMoveTop || action == DownloadQueue::eaGroupMoveBottom)) ||
528 action == DownloadQueue::eaGroupMoveBefore || action == DownloadQueue::eaGroupMoveAfter)
529 {
530 // add IDs to list in order they currently have in download queue
531 int nrEntries = (int)m_downloadQueue->GetQueue()->size();
532 int lastDestPos = -1;
533 int start, end, step;
534 if (offset <= 0)
535 {
536 start = 0;
537 end = nrEntries;
538 step = 1;
539 }
540 else
541 {
542 start = nrEntries - 1;
543 end = -1;
544 step = -1;
545 }
546 for (int index = start; index != end; index += step)
547 {
548 std::unique_ptr<NzbInfo>& nzbInfo = m_downloadQueue->GetQueue()->at(index);
549 IdList::iterator it2 = std::find(idList->begin(), idList->end(), nzbInfo->GetId());
550 if (it2 != idList->end())
551 {
552 int workOffset = offset;
553 int destPos = index + workOffset;
554 if (lastDestPos == -1)
555 {
556 if (destPos < 0)
557 {
558 workOffset = -index;
559 }
560 else if (destPos > nrEntries - 1)
561 {
562 workOffset = nrEntries - 1 - index;
563 }
564 }
565 else
566 {
567 if (workOffset < 0 && destPos <= lastDestPos)
568 {
569 workOffset = lastDestPos - index + 1;
570 }
571 else if (workOffset > 0 && destPos >= lastDestPos)
572 {
573 workOffset = lastDestPos - index - 1;
574 }
575 }
576 lastDestPos = index + workOffset;
577 itemList->emplace_back(nullptr, nzbInfo.get(), workOffset);
578 }
579 }
580 }
581 else if (action < DownloadQueue::eaGroupMoveOffset)
582 {
583 // check ID range
584 int maxId = 0;
585 int minId = MAX_ID;
586 for (NzbInfo* nzbInfo : m_downloadQueue->GetQueue())
587 {
588 for (FileInfo* fileInfo : nzbInfo->GetFileList())
589 {
590 int ID = fileInfo->GetId();
591 if (ID > maxId)
592 {
593 maxId = ID;
594 }
595 if (ID < minId)
596 {
597 minId = ID;
598 }
599 }
600 }
601
602 //add IDs to list in order they were transmitted in command
603 for (int id : *idList)
604 {
605 if (minId <= id && id <= maxId)
606 {
607 FileInfo* fileInfo = FindFileInfo(id);
608 if (fileInfo)
609 {
610 itemList->emplace_back(fileInfo, nullptr, offset);
611 }
612 }
613 }
614 }
615 else
616 {
617 // check ID range
618 int maxId = 0;
619 int minId = MAX_ID;
620 for (NzbInfo* nzbInfo : m_downloadQueue->GetQueue())
621 {
622 int ID = nzbInfo->GetId();
623 if (ID > maxId)
624 {
625 maxId = ID;
626 }
627 if (ID < minId)
628 {
629 minId = ID;
630 }
631 }
632
633 //add IDs to list in order they were transmitted in command
634 for (int id : *idList)
635 {
636 if (minId <= id && id <= maxId)
637 {
638 for (NzbInfo* nzbInfo : m_downloadQueue->GetQueue())
639 {
640 if (id == nzbInfo->GetId())
641 {
642 itemList->emplace_back(nullptr, nzbInfo, offset);
643 }
644 }
645 }
646 }
647 }
648 }
649
BuildIdListFromNameList(IdList * idList,NameList * nameList,DownloadQueue::EMatchMode matchMode,DownloadQueue::EEditAction action)650 bool QueueEditor::BuildIdListFromNameList(IdList* idList, NameList* nameList, DownloadQueue::EMatchMode matchMode, DownloadQueue::EEditAction action)
651 {
652 #ifndef HAVE_REGEX_H
653 if (matchMode == DownloadQueue::mmRegEx)
654 {
655 return false;
656 }
657 #endif
658
659 std::set<int> uniqueIds;
660
661 for (CString& name : nameList)
662 {
663 std::unique_ptr<RegEx> regEx;
664 if (matchMode == DownloadQueue::mmRegEx)
665 {
666 regEx = std::make_unique<RegEx>(name);
667 if (!regEx->IsValid())
668 {
669 return false;
670 }
671 }
672
673 bool found = false;
674
675 for (NzbInfo* nzbInfo : m_downloadQueue->GetQueue())
676 {
677 for (FileInfo* fileInfo : nzbInfo->GetFileList())
678 {
679 if (action < DownloadQueue::eaGroupMoveOffset)
680 {
681 // file action
682 BString<1024> filename("%s/%s", fileInfo->GetNzbInfo()->GetName(), FileSystem::BaseFileName(fileInfo->GetFilename()));
683 if (((!regEx && !strcmp(filename, name)) || (regEx && regEx->Match(filename))) &&
684 (uniqueIds.find(fileInfo->GetId()) == uniqueIds.end()))
685 {
686 uniqueIds.insert(fileInfo->GetId());
687 idList->push_back(fileInfo->GetId());
688 found = true;
689 }
690 }
691 }
692
693 if (action >= DownloadQueue::eaGroupMoveOffset)
694 {
695 // group action
696 const char *filename = nzbInfo->GetName();
697 if (((!regEx && !strcmp(filename, name)) || (regEx && regEx->Match(filename))) &&
698 (uniqueIds.find(nzbInfo->GetId()) == uniqueIds.end()))
699 {
700 uniqueIds.insert(nzbInfo->GetId());
701 idList->push_back(nzbInfo->GetId());
702 found = true;
703 }
704 }
705 }
706
707 if (!found && (matchMode == DownloadQueue::mmName))
708 {
709 return false;
710 }
711 }
712
713 return true;
714 }
715
EditGroup(NzbInfo * nzbInfo,DownloadQueue::EEditAction action,const char * args)716 bool QueueEditor::EditGroup(NzbInfo* nzbInfo, DownloadQueue::EEditAction action, const char* args)
717 {
718 ItemList itemList;
719 bool allPaused = true;
720 int id = nzbInfo->GetId();
721
722 // collecting files belonging to group
723 for (FileInfo* fileInfo : nzbInfo->GetFileList())
724 {
725 itemList.emplace_back(fileInfo, nullptr, 0);
726 allPaused &= fileInfo->GetPaused();
727 }
728
729 if (action == DownloadQueue::eaGroupDelete ||
730 action == DownloadQueue::eaGroupParkDelete ||
731 action == DownloadQueue::eaGroupDupeDelete ||
732 action == DownloadQueue::eaGroupFinalDelete)
733 {
734 nzbInfo->SetDeleting(true);
735 nzbInfo->SetParking(action == DownloadQueue::eaGroupParkDelete &&
736 g_Options->GetKeepHistory() > 0 &&
737 !nzbInfo->GetUnpackCleanedUpDisk() &&
738 nzbInfo->GetCurrentSuccessArticles() > 0);
739 nzbInfo->SetAvoidHistory(action == DownloadQueue::eaGroupFinalDelete);
740 nzbInfo->SetDeletePaused(allPaused);
741 if (action == DownloadQueue::eaGroupDupeDelete)
742 {
743 nzbInfo->SetDeleteStatus(NzbInfo::dsDupe);
744 }
745 nzbInfo->SetCleanupDisk(action != DownloadQueue::eaGroupParkDelete);
746 }
747
748 DownloadQueue::EEditAction GroupToFileMap[] = {
749 (DownloadQueue::EEditAction)0,
750 DownloadQueue::eaFileMoveOffset,
751 DownloadQueue::eaFileMoveTop,
752 DownloadQueue::eaFileMoveBottom,
753 DownloadQueue::eaFilePause,
754 DownloadQueue::eaFileResume,
755 DownloadQueue::eaFileDelete,
756 DownloadQueue::eaFilePauseAllPars,
757 DownloadQueue::eaFilePauseExtraPars,
758 DownloadQueue::eaFileReorder,
759 DownloadQueue::eaFileSplit,
760 DownloadQueue::eaFileMoveOffset,
761 DownloadQueue::eaFileMoveTop,
762 DownloadQueue::eaFileMoveBottom,
763 (DownloadQueue::EEditAction)0,
764 (DownloadQueue::EEditAction)0,
765 DownloadQueue::eaFilePause,
766 DownloadQueue::eaFileResume,
767 DownloadQueue::eaFileDelete,
768 DownloadQueue::eaFileDelete,
769 DownloadQueue::eaFileDelete,
770 DownloadQueue::eaFileDelete,
771 DownloadQueue::eaFilePauseAllPars,
772 DownloadQueue::eaFilePauseExtraPars,
773 (DownloadQueue::EEditAction)0,
774 (DownloadQueue::EEditAction)0,
775 (DownloadQueue::EEditAction)0,
776 (DownloadQueue::EEditAction)0 };
777
778 bool ok = InternEditList(&itemList, nullptr, GroupToFileMap[action], args);
779
780 if ((action == DownloadQueue::eaGroupDelete || action == DownloadQueue::eaGroupDupeDelete || action == DownloadQueue::eaGroupFinalDelete) &&
781 // NZBInfo could have been destroyed already
782 m_downloadQueue->GetQueue()->Find(id))
783 {
784 DownloadQueue::Aspect deleteAspect = { DownloadQueue::eaNzbDeleted, m_downloadQueue, nzbInfo, nullptr };
785 m_downloadQueue->Notify(&deleteAspect);
786 }
787
788 return ok;
789 }
790
PauseParsInGroups(ItemList * itemList,bool extraParsOnly)791 void QueueEditor::PauseParsInGroups(ItemList* itemList, bool extraParsOnly)
792 {
793 while (true)
794 {
795 RawFileList GroupFileList;
796 FileInfo* firstFileInfo = nullptr;
797
798 for (ItemList::iterator it = itemList->begin(); it != itemList->end(); )
799 {
800 EditItem& item = *it;
801 if (!firstFileInfo ||
802 (firstFileInfo->GetNzbInfo() == item.m_fileInfo->GetNzbInfo()))
803 {
804 GroupFileList.push_back(item.m_fileInfo);
805 if (!firstFileInfo)
806 {
807 firstFileInfo = item.m_fileInfo;
808 }
809 itemList->erase(it);
810 it = itemList->begin();
811 continue;
812 }
813 it++;
814 }
815
816 if (!GroupFileList.empty())
817 {
818 PausePars(&GroupFileList, extraParsOnly);
819 }
820 else
821 {
822 break;
823 }
824 }
825 }
826
827 /**
828 * If the parameter "bExtraParsOnly" is set to "false", then we pause all par2-files.
829 * If the parameter "bExtraParsOnly" is set to "true", we use the following strategy:
830 * At first we find all par-files, which do not have "vol" in their names, then we pause
831 * all vols and do not affect all just-pars.
832 * In a case, if there are no just-pars, but only vols, we find the smallest vol-file
833 * and do not affect it, but pause all other pars.
834 */
PausePars(RawFileList * fileList,bool extraParsOnly)835 void QueueEditor::PausePars(RawFileList* fileList, bool extraParsOnly)
836 {
837 debug("QueueEditor: Pausing pars");
838
839 RawFileList Pars, Vols;
840
841 for (FileInfo* fileInfo : fileList)
842 {
843 if (fileInfo->GetParFile())
844 {
845 if (!extraParsOnly)
846 {
847 fileInfo->SetPaused(true);
848 }
849 else
850 {
851 BString<1024> loFileName = fileInfo->GetFilename();
852 for (char* p = loFileName; *p; p++) *p = tolower(*p); // convert string to lowercase
853 if (strstr(loFileName, ".vol"))
854 {
855 Vols.push_back(fileInfo);
856 }
857 else
858 {
859 Pars.push_back(fileInfo);
860 }
861 }
862 }
863 }
864
865 if (extraParsOnly)
866 {
867 if (!Pars.empty())
868 {
869 for (FileInfo* fileInfo : Vols)
870 {
871 fileInfo->SetPaused(true);
872 }
873 }
874 else
875 {
876 // pausing all Vol-files except the smallest one
877 FileInfo* smallest = nullptr;
878 for (FileInfo* fileInfo : Vols)
879 {
880 if (!smallest)
881 {
882 smallest = fileInfo;
883 }
884 else if (smallest->GetSize() > fileInfo->GetSize())
885 {
886 smallest->SetPaused(true);
887 smallest = fileInfo;
888 }
889 else
890 {
891 fileInfo->SetPaused(true);
892 }
893 }
894 }
895 }
896 }
897
SetNzbPriority(NzbInfo * nzbInfo,const char * priority)898 void QueueEditor::SetNzbPriority(NzbInfo* nzbInfo, const char* priority)
899 {
900 debug("Setting priority %s for %s", priority, nzbInfo->GetName());
901
902 int priorityVal = atoi(priority);
903 nzbInfo->SetPriority(priorityVal);
904 }
905
SetNzbCategory(NzbInfo * nzbInfo,const char * category,bool applyParams)906 void QueueEditor::SetNzbCategory(NzbInfo* nzbInfo, const char* category, bool applyParams)
907 {
908 debug("QueueEditor: setting category '%s' for '%s'", category, nzbInfo->GetName());
909
910 bool oldUnpack = g_Options->GetUnpack();
911 const char* oldExtensions = g_Options->GetExtensions();
912 if (applyParams && !Util::EmptyStr(nzbInfo->GetCategory()))
913 {
914 Options::Category* categoryObj = g_Options->FindCategory(nzbInfo->GetCategory(), false);
915 if (categoryObj)
916 {
917 oldUnpack = categoryObj->GetUnpack();
918 if (!Util::EmptyStr(categoryObj->GetExtensions()))
919 {
920 oldExtensions = categoryObj->GetExtensions();
921 }
922 }
923 }
924
925 g_QueueCoordinator->SetQueueEntryCategory(m_downloadQueue, nzbInfo, category);
926
927 if (!applyParams)
928 {
929 return;
930 }
931
932 bool newUnpack = g_Options->GetUnpack();
933 const char* newExtensions = g_Options->GetExtensions();
934 if (!Util::EmptyStr(nzbInfo->GetCategory()))
935 {
936 Options::Category* categoryObj = g_Options->FindCategory(nzbInfo->GetCategory(), false);
937 if (categoryObj)
938 {
939 newUnpack = categoryObj->GetUnpack();
940 if (!Util::EmptyStr(categoryObj->GetExtensions()))
941 {
942 newExtensions = categoryObj->GetExtensions();
943 }
944 }
945 }
946
947 if (oldUnpack != newUnpack)
948 {
949 nzbInfo->GetParameters()->SetParameter("*Unpack:", newUnpack ? "yes" : "no");
950 }
951
952 if (strcasecmp(oldExtensions, newExtensions))
953 {
954 // add new params not existed in old category
955 Tokenizer tokNew(newExtensions, ",;");
956 while (const char* newScriptName = tokNew.Next())
957 {
958 bool found = false;
959 const char* oldScriptName;
960 Tokenizer tokOld(oldExtensions, ",;");
961 while ((oldScriptName = tokOld.Next()) && !found)
962 {
963 found = !strcasecmp(newScriptName, oldScriptName);
964 }
965 if (!found)
966 {
967 nzbInfo->GetParameters()->SetParameter(BString<1024>("%s:", newScriptName), "yes");
968 }
969 }
970
971 // remove old params not existed in new category
972 Tokenizer tokOld(oldExtensions, ",;");
973 while (const char* oldScriptName = tokOld.Next())
974 {
975 bool found = false;
976 const char* newScriptName;
977 Tokenizer tokNew(newExtensions, ",;");
978 while ((newScriptName = tokNew.Next()) && !found)
979 {
980 found = !strcasecmp(newScriptName, oldScriptName);
981 }
982 if (!found)
983 {
984 nzbInfo->GetParameters()->SetParameter(BString<1024>("%s:", oldScriptName), "no");
985 }
986 }
987 }
988 }
989
SetNzbName(NzbInfo * nzbInfo,const char * name)990 void QueueEditor::SetNzbName(NzbInfo* nzbInfo, const char* name)
991 {
992 debug("QueueEditor: renaming '%s' to '%s'", nzbInfo->GetName(), name);
993
994 g_QueueCoordinator->SetQueueEntryName(m_downloadQueue, nzbInfo, name);
995 }
996
MergeGroups(ItemList * itemList)997 bool QueueEditor::MergeGroups(ItemList* itemList)
998 {
999 if (itemList->size() == 0)
1000 {
1001 return false;
1002 }
1003
1004 bool ok = true;
1005
1006 EditItem& destItem = itemList->front();
1007
1008 for (EditItem& item : itemList)
1009 {
1010 if (item.m_nzbInfo != destItem.m_nzbInfo)
1011 {
1012 debug("merge %s to %s", item.m_nzbInfo->GetFilename(), destItem.m_nzbInfo->GetFilename());
1013 if (g_QueueCoordinator->MergeQueueEntries(m_downloadQueue, destItem.m_nzbInfo, item.m_nzbInfo))
1014 {
1015 ok = false;
1016 }
1017 }
1018 }
1019
1020 return ok;
1021 }
1022
SplitGroup(ItemList * itemList,const char * name)1023 bool QueueEditor::SplitGroup(ItemList* itemList, const char* name)
1024 {
1025 if (itemList->size() == 0)
1026 {
1027 return false;
1028 }
1029
1030 RawFileList fileList;
1031
1032 for (EditItem& item : itemList)
1033 {
1034 fileList.push_back(item.m_fileInfo);
1035 }
1036
1037 NzbInfo* newNzbInfo = nullptr;
1038 bool ok = g_QueueCoordinator->SplitQueueEntries(m_downloadQueue, &fileList, name, &newNzbInfo);
1039
1040 return ok;
1041 }
1042
SortGroups(ItemList * itemList,const char * sort)1043 bool QueueEditor::SortGroups(ItemList* itemList, const char* sort)
1044 {
1045 AlignGroups(itemList);
1046 GroupSorter sorter(m_downloadQueue->GetQueue(), itemList);
1047 return sorter.Execute(sort);
1048 }
1049
AlignGroups(ItemList * itemList)1050 void QueueEditor::AlignGroups(ItemList* itemList)
1051 {
1052 NzbList* nzbList = m_downloadQueue->GetQueue();
1053 NzbInfo* lastNzbInfo = nullptr;
1054 uint32 lastNum = 0;
1055 uint32 num = 0;
1056 while (num < nzbList->size())
1057 {
1058 std::unique_ptr<NzbInfo>& nzbInfo = nzbList->at(num);
1059
1060 bool selected = false;
1061 for (QueueEditor::EditItem& item : itemList)
1062 {
1063 if (item.m_nzbInfo == nzbInfo.get())
1064 {
1065 selected = true;
1066 break;
1067 }
1068 }
1069
1070 if (selected)
1071 {
1072 if (lastNzbInfo && num - lastNum > 1)
1073 {
1074 std::unique_ptr<NzbInfo> movedNzbInfo = std::move(*(nzbList->begin() + num));
1075 nzbList->erase(nzbList->begin() + num);
1076 nzbList->insert(nzbList->begin() + lastNum + 1, std::move(movedNzbInfo));
1077 lastNum++;
1078 }
1079 else
1080 {
1081 lastNum = num;
1082 }
1083 lastNzbInfo = nzbInfo.get();
1084 }
1085 num++;
1086 }
1087 }
1088
ItemListContainsItem(ItemList * itemList,int id)1089 bool QueueEditor::ItemListContainsItem(ItemList* itemList, int id)
1090 {
1091 return std::find_if(itemList->begin(), itemList->end(),
1092 [id](const EditItem& item)
1093 {
1094 return item.m_nzbInfo->GetId() == id;
1095 }) != itemList->end();
1096 };
1097
MoveGroupsTo(ItemList * itemList,IdList * idList,bool before,const char * args)1098 bool QueueEditor::MoveGroupsTo(ItemList* itemList, IdList* idList, bool before, const char* args)
1099 {
1100 if (itemList->size() == 0 || Util::EmptyStr(args))
1101 {
1102 return false;
1103 }
1104
1105 int targetId = atoi(args);
1106 int offset = 0;
1107
1108 // check if target is in list of moved items
1109 if (ItemListContainsItem(itemList, targetId))
1110 {
1111 // find the next item to use as target-before
1112 bool found = false;
1113 bool targetSet = false;
1114
1115 for (NzbInfo* nzbInfo : m_downloadQueue->GetQueue())
1116 {
1117 if (found)
1118 {
1119 if (!ItemListContainsItem(itemList, nzbInfo->GetId()))
1120 {
1121 targetId = nzbInfo->GetId();
1122 before = true;
1123 targetSet = true;
1124 break;
1125 }
1126 }
1127 else if (targetId == nzbInfo->GetId())
1128 {
1129 found = true;
1130 }
1131 }
1132
1133 if (!targetSet)
1134 {
1135 // there are no next item; move to the bottom then
1136 offset = MAX_ID;
1137 }
1138 }
1139
1140 AlignGroups(itemList);
1141
1142 if (offset == 0)
1143 {
1144 // calculate offset between first moving item and target
1145 int moveId = itemList->at(0).m_nzbInfo->GetId();
1146 bool progress = false;
1147 int step = 0;
1148 for (NzbInfo* nzbInfo : m_downloadQueue->GetQueue())
1149 {
1150 int id = nzbInfo->GetId();
1151 if (id == targetId || id == moveId)
1152 {
1153 if (!progress)
1154 {
1155 step = id == targetId ? -1 : 1;
1156 offset = (before ? 0 : 1) - (step > 0 ? itemList->size() : 0);
1157 progress = true;
1158 }
1159 else
1160 {
1161 break;
1162 }
1163 }
1164
1165 if (progress)
1166 {
1167 offset += step;
1168 }
1169 }
1170 }
1171
1172 return InternEditList(nullptr, idList, DownloadQueue::eaGroupMoveOffset,
1173 CString::FormatStr("%i", offset));
1174 }
1175
ReorderFiles(ItemList * itemList)1176 void QueueEditor::ReorderFiles(ItemList* itemList)
1177 {
1178 if (itemList->size() == 0)
1179 {
1180 return;
1181 }
1182
1183 EditItem& firstItem = itemList->front();
1184 NzbInfo* nzbInfo = firstItem.m_fileInfo->GetNzbInfo();
1185 uint32 insertPos = 0;
1186
1187 // now can reorder
1188 for (EditItem& item : itemList)
1189 {
1190 FileInfo* fileInfo = item.m_fileInfo;
1191
1192 // move file item
1193 FileList::iterator it2 = nzbInfo->GetFileList()->Find(fileInfo);
1194 if (it2 != nzbInfo->GetFileList()->end())
1195 {
1196 std::unique_ptr<FileInfo> movedFileInfo = std::move(*it2);
1197 nzbInfo->GetFileList()->erase(it2);
1198 nzbInfo->GetFileList()->insert(nzbInfo->GetFileList()->begin() + insertPos, std::move(movedFileInfo));
1199 insertPos++;
1200 }
1201 }
1202 }
1203
SetNzbParameter(NzbInfo * nzbInfo,const char * paramString)1204 void QueueEditor::SetNzbParameter(NzbInfo* nzbInfo, const char* paramString)
1205 {
1206 debug("QueueEditor: setting nzb parameter '%s' for '%s'", paramString, FileSystem::BaseFileName(nzbInfo->GetFilename()));
1207
1208 CString str = paramString;
1209 char* value = strchr(str, '=');
1210 if (value)
1211 {
1212 *value = '\0';
1213 value++;
1214 nzbInfo->GetParameters()->SetParameter(str, value);
1215 }
1216 else
1217 {
1218 error("Could not set nzb parameter for %s: invalid argument: %s", nzbInfo->GetName(), paramString);
1219 }
1220 }
1221
SetNzbDupeParam(NzbInfo * nzbInfo,DownloadQueue::EEditAction action,const char * text)1222 void QueueEditor::SetNzbDupeParam(NzbInfo* nzbInfo, DownloadQueue::EEditAction action, const char* text)
1223 {
1224 debug("QueueEditor: setting dupe parameter %i='%s' for '%s'", (int)action, text, nzbInfo->GetName());
1225
1226 switch (action)
1227 {
1228 case DownloadQueue::eaGroupSetDupeKey:
1229 nzbInfo->SetDupeKey(text);
1230 break;
1231
1232 case DownloadQueue::eaGroupSetDupeScore:
1233 nzbInfo->SetDupeScore(atoi(text));
1234 break;
1235
1236 case DownloadQueue::eaGroupSetDupeMode:
1237 {
1238 EDupeMode mode = dmScore;
1239 if (!strcasecmp(text, "SCORE"))
1240 {
1241 mode = dmScore;
1242 }
1243 else if (!strcasecmp(text, "ALL"))
1244 {
1245 mode = dmAll;
1246 }
1247 else if (!strcasecmp(text, "FORCE"))
1248 {
1249 mode = dmForce;
1250 }
1251 else
1252 {
1253 error("Could not set duplicate mode for %s: incorrect mode (%s)", nzbInfo->GetName(), text);
1254 return;
1255 }
1256 nzbInfo->SetDupeMode(mode);
1257 break;
1258 }
1259
1260 default:
1261 // suppress compiler warning
1262 break;
1263 }
1264 }
1265
SortGroupFiles(NzbInfo * nzbInfo)1266 void QueueEditor::SortGroupFiles(NzbInfo* nzbInfo)
1267 {
1268 debug("QueueEditor: sorting inner files for '%s'", nzbInfo->GetName());
1269
1270 std::sort(nzbInfo->GetFileList()->begin(), nzbInfo->GetFileList()->end(),
1271 [](const std::unique_ptr<FileInfo>& fileInfo1, const std::unique_ptr<FileInfo>& fileInfo2)
1272 {
1273 if (!fileInfo1->GetParFile() && !fileInfo2->GetParFile())
1274 {
1275 // ".rar"-files are ordered before ".r01", etc. files
1276 int len1 = strlen(fileInfo1->GetFilename());
1277 int len2 = strlen(fileInfo2->GetFilename());
1278 const char* ext1 = strrchr(fileInfo1->GetFilename(), '.');
1279 const char* ext2 = strrchr(fileInfo2->GetFilename(), '.');
1280 int ext1len = ext1 ? strlen(ext1) : 0;
1281 int ext2len = ext2 ? strlen(ext2) : 0;
1282 bool sameBaseName = len1 == len2 && ext1len == 4 && ext2len == 4 &&
1283 !strncmp(fileInfo1->GetFilename(), fileInfo2->GetFilename(), len1 - 4);
1284 if (sameBaseName && !strcmp(ext1, ".rar") && ext2[1] == 'r' &&
1285 isdigit(ext2[2]) && isdigit(ext2[3]))
1286 {
1287 return true;
1288 }
1289 else if (sameBaseName && !strcmp(ext2, ".rar") && ext1[1] == 'r' &&
1290 isdigit(ext1[2]) && isdigit(ext1[3]))
1291 {
1292 return false;
1293 }
1294 }
1295 else if (fileInfo1->GetParFile() && fileInfo2->GetParFile() &&
1296 ParParser::SameParCollection(fileInfo1->GetFilename(), fileInfo2->GetFilename(),
1297 fileInfo1->GetFilenameConfirmed() && fileInfo2->GetFilenameConfirmed()))
1298 {
1299 return fileInfo1->GetSize() < fileInfo2->GetSize();
1300 }
1301 else if (!fileInfo1->GetParFile() && fileInfo2->GetParFile())
1302 {
1303 return true;
1304 }
1305 else if (fileInfo1->GetParFile() && !fileInfo2->GetParFile())
1306 {
1307 return false;
1308 }
1309
1310 return strcmp(fileInfo1->GetFilename(), fileInfo2->GetFilename()) < 0;
1311 });
1312 }
1313
DeleteUrl(NzbInfo * nzbInfo,DownloadQueue::EEditAction action)1314 bool QueueEditor::DeleteUrl(NzbInfo* nzbInfo, DownloadQueue::EEditAction action)
1315 {
1316 return g_UrlCoordinator->DeleteQueueEntry(m_downloadQueue, nzbInfo, action == DownloadQueue::eaGroupFinalDelete);
1317 }
1318