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