1 /*
2  * This file Copyright (C) 2009-2015 Mnemosyne LLC
3  *
4  * It may be used under the GNU GPL versions 2 or 3
5  * or any future license endorsed by Mnemosyne LLC.
6  *
7  */
8 
9 #include <algorithm>
10 #include <iostream>
11 #include <utility>
12 
13 #include <libtransmission/transmission.h>
14 #include <libtransmission/variant.h>
15 
16 #include "Speed.h"
17 #include "Torrent.h"
18 #include "TorrentDelegate.h"
19 #include "TorrentModel.h"
20 
21 /***
22 ****
23 ***/
24 
25 namespace
26 {
27 
28 struct TorrentIdLessThan
29 {
operator ()__anon15b5ca8b0111::TorrentIdLessThan30     bool operator ()(Torrent* left, Torrent* right) const
31     {
32         return left->id() < right->id();
33     }
34 
operator ()__anon15b5ca8b0111::TorrentIdLessThan35     bool operator ()(int leftId, Torrent* right) const
36     {
37         return leftId < right->id();
38     }
39 
operator ()__anon15b5ca8b0111::TorrentIdLessThan40     bool operator ()(Torrent* left, int rightId) const
41     {
42         return left->id() < rightId;
43     }
44 };
45 
46 template<typename Iter>
getIds(Iter it,Iter end)47 auto getIds(Iter it, Iter end)
48 {
49     torrent_ids_t ids;
50 
51     for ( ; it != end; ++it)
52     {
53         ids.insert((*it)->id());
54     }
55 
56     return ids;
57 }
58 
59 } // namespace
60 
61 /***
62 ****
63 ***/
64 
TorrentModel(Prefs const & prefs)65 TorrentModel::TorrentModel(Prefs const& prefs) :
66     myPrefs(prefs)
67 {
68 }
69 
~TorrentModel()70 TorrentModel::~TorrentModel()
71 {
72     clear();
73 }
74 
clear()75 void TorrentModel::clear()
76 {
77     beginResetModel();
78     qDeleteAll(myTorrents);
79     myTorrents.clear();
80     endResetModel();
81 }
82 
rowCount(QModelIndex const & parent) const83 int TorrentModel::rowCount(QModelIndex const& parent) const
84 {
85     Q_UNUSED(parent)
86 
87     return myTorrents.size();
88 }
89 
data(QModelIndex const & index,int role) const90 QVariant TorrentModel::data(QModelIndex const& index, int role) const
91 {
92     QVariant var;
93 
94     Torrent const* t = myTorrents.value(index.row(), nullptr);
95 
96     if (t != nullptr)
97     {
98         switch (role)
99         {
100         case Qt::DisplayRole:
101             var.setValue(t->name());
102             break;
103 
104         case Qt::DecorationRole:
105             var.setValue(t->getMimeTypeIcon());
106             break;
107 
108         case TorrentRole:
109             var = qVariantFromValue(t);
110             break;
111 
112         default:
113             // std::cerr << "Unhandled role: " << role << std::endl;
114             break;
115         }
116     }
117 
118     return var;
119 }
120 
121 /***
122 ****
123 ***/
124 
removeTorrents(tr_variant * list)125 void TorrentModel::removeTorrents(tr_variant* list)
126 {
127     torrents_t torrents;
128     torrents.reserve(tr_variantListSize(list));
129 
130     int i = 0;
131     tr_variant* child;
132     while ((child = tr_variantListChild(list, i++)) != nullptr)
133     {
134         int64_t id;
135         Torrent* torrent = nullptr;
136 
137         if (tr_variantGetInt(child, &id))
138         {
139             torrent = getTorrentFromId(id);
140         }
141 
142         if (torrent != nullptr)
143         {
144             torrents.push_back(torrent);
145         }
146     }
147 
148     if (!torrents.empty())
149     {
150         rowsRemove(torrents);
151     }
152 }
153 
updateTorrents(tr_variant * torrents,bool isCompleteList)154 void TorrentModel::updateTorrents(tr_variant* torrents, bool isCompleteList)
155 {
156     auto const old = isCompleteList ? myTorrents : torrents_t{};
157     auto added = torrent_ids_t{};
158     auto changed = torrent_ids_t{};
159     auto completed = torrent_ids_t{};
160     auto instantiated = torrents_t{};
161     auto needinfo = torrent_ids_t{};
162     auto processed = torrents_t{};
163 
164     auto const now = time(nullptr);
165     auto const recently_added = [now](auto const& tor)
166         {
167             static auto constexpr max_age = 60;
168             auto const date = tor->dateAdded();
169             return (date != 0) && (difftime(now, date) < max_age);
170         };
171 
172     // build a list of the property keys
173     tr_variant* const firstChild = tr_variantListChild(torrents, 0);
174     bool const table = tr_variantIsList(firstChild);
175     std::vector<tr_quark> keys;
176     if (table)
177     {
178         // In 'table' format, the first entry in 'torrents' is an array of keys.
179         // All the other entries are an array of the values for one torrent.
180         char const* str;
181         size_t len;
182         size_t i = 0;
183         keys.reserve(tr_variantListSize(firstChild));
184         while (tr_variantGetStr(tr_variantListChild(firstChild, i++), &str, &len))
185         {
186             keys.push_back(tr_quark_new(str, len));
187         }
188     }
189     else
190     {
191         // In 'object' format, every entry is an object with the same set of properties
192         size_t i = 0;
193         tr_quark key;
194         tr_variant* value;
195         while (firstChild && tr_variantDictChild(firstChild, i++, &key, &value))
196         {
197             keys.push_back(key);
198         }
199     }
200 
201     // Find the position of TR_KEY_id so we can do torrent lookup
202     auto const id_it = std::find(std::begin(keys), std::end(keys), TR_KEY_id);
203     if (id_it == std::end(keys)) // no ids provided; we can't proceed
204     {
205         return;
206     }
207 
208     auto const id_pos = std::distance(std::begin(keys), id_it);
209 
210     // Loop through the torrent records...
211     std::vector<tr_variant*> values;
212     values.reserve(keys.size());
213     size_t tor_index = table ? 1 : 0;
214     tr_variant* v;
215     processed.reserve(tr_variantListSize(torrents));
216     while ((v = tr_variantListChild(torrents, tor_index++)))
217     {
218         // Build an array of values
219         values.clear();
220         if (table)
221         {
222             // In table mode, v is already a list of values
223             size_t i = 0;
224             tr_variant* val;
225             while ((val = tr_variantListChild(v, i++)))
226             {
227                 values.push_back(val);
228             }
229         }
230         else
231         {
232             // In object mode, v is an object of torrent property key/vals
233             size_t i = 0;
234             tr_quark key;
235             tr_variant* value;
236             while (tr_variantDictChild(v, i++, &key, &value))
237             {
238                 values.push_back(value);
239             }
240         }
241 
242         // Find the torrent id
243         int64_t id;
244         if (!tr_variantGetInt(values[id_pos], &id))
245         {
246             continue;
247         }
248 
249         Torrent* tor = getTorrentFromId(id);
250         std::optional<uint64_t> leftUntilDone;
251         bool is_new = false;
252 
253         if (tor == nullptr)
254         {
255             tor = new Torrent(myPrefs, id);
256             instantiated.push_back(tor);
257             is_new = true;
258         }
259         else
260         {
261             leftUntilDone = tor->leftUntilDone();
262         }
263 
264         if (tor->update(keys.data(), values.data(), keys.size()))
265         {
266             changed.insert(id);
267         }
268 
269         if (is_new && !tor->hasName())
270         {
271             needinfo.insert(id);
272         }
273 
274         if (recently_added(tor) && tor->hasName() && !myAlreadyAdded.count(id))
275         {
276             added.insert(id);
277             myAlreadyAdded.insert(id);
278         }
279 
280         if (leftUntilDone && (*leftUntilDone > 0) && (tor->leftUntilDone() == 0) && (tor->downloadedEver() > 0))
281         {
282             completed.insert(id);
283         }
284 
285         processed.push_back(tor);
286     }
287 
288     // model upkeep
289 
290     if (!instantiated.empty())
291     {
292         rowsAdd(instantiated);
293     }
294 
295     if (!changed.empty())
296     {
297         rowsEmitChanged(changed);
298     }
299 
300     // emit signals
301 
302     if (!added.empty())
303     {
304         emit torrentsAdded(added);
305     }
306 
307     if (!needinfo.empty())
308     {
309         emit torrentsNeedInfo(needinfo);
310     }
311 
312     if (!changed.empty())
313     {
314         emit torrentsChanged(changed);
315     }
316 
317     if (!completed.empty())
318     {
319         emit torrentsCompleted(completed);
320     }
321 
322     // model upkeep
323 
324     if (isCompleteList)
325     {
326         std::sort(processed.begin(), processed.end(), TorrentIdLessThan());
327         torrents_t removed;
328         removed.reserve(old.size());
329         std::set_difference(old.begin(), old.end(), processed.begin(), processed.end(), std::back_inserter(removed));
330         rowsRemove(removed);
331     }
332 }
333 
334 /***
335 ****
336 ***/
337 
getRow(int id) const338 std::optional<int> TorrentModel::getRow(int id) const
339 {
340     std::optional<int> row;
341 
342     auto const it = std::equal_range(myTorrents.begin(), myTorrents.end(), id, TorrentIdLessThan());
343     if (it.first != it.second)
344     {
345         row = std::distance(myTorrents.begin(), it.first);
346         assert(myTorrents[*row]->id() == id);
347     }
348 
349     return row;
350 }
351 
getTorrentFromId(int id)352 Torrent* TorrentModel::getTorrentFromId(int id)
353 {
354     auto const row = getRow(id);
355     return row ? myTorrents[*row] : nullptr;
356 }
357 
getTorrentFromId(int id) const358 Torrent const* TorrentModel::getTorrentFromId(int id) const
359 {
360     auto const row = getRow(id);
361     return row ? myTorrents[*row] : nullptr;
362 }
363 
364 /***
365 ****
366 ***/
367 
getSpans(torrent_ids_t const & ids) const368 std::vector<TorrentModel::span_t> TorrentModel::getSpans(torrent_ids_t const& ids) const
369 {
370     // ids -> rows
371     std::vector<int> rows;
372     rows.reserve(ids.size());
373     for (auto const& id : ids)
374     {
375         auto const row = getRow(id);
376         if (row)
377         {
378             rows.push_back(*row);
379         }
380     }
381 
382     std::sort(rows.begin(), rows.end());
383 
384     // rows -> spans
385     std::vector<span_t> spans;
386     spans.reserve(rows.size());
387     span_t span;
388     bool in_span = false;
389     for (auto const& row : rows)
390     {
391         if (in_span)
392         {
393             if (span.second + 1 == row)
394             {
395                 span.second = row;
396             }
397             else
398             {
399                 spans.push_back(span);
400                 in_span = false;
401             }
402         }
403 
404         if (!in_span)
405         {
406             span.first = span.second = row;
407             in_span = true;
408         }
409     }
410 
411     if (in_span)
412     {
413         spans.push_back(span);
414     }
415 
416     return spans;
417 }
418 
419 /***
420 ****
421 ***/
422 
rowsEmitChanged(torrent_ids_t const & ids)423 void TorrentModel::rowsEmitChanged(torrent_ids_t const& ids)
424 {
425     for (auto const& span : getSpans(ids))
426     {
427         emit dataChanged(index(span.first), index(span.second));
428     }
429 }
430 
rowsAdd(torrents_t const & torrents)431 void TorrentModel::rowsAdd(torrents_t const& torrents)
432 {
433     auto const compare = TorrentIdLessThan();
434 
435     if (myTorrents.empty())
436     {
437         beginInsertRows(QModelIndex(), 0, torrents.size() - 1);
438         myTorrents = torrents;
439         std::sort(myTorrents.begin(), myTorrents.end(), TorrentIdLessThan());
440         endInsertRows();
441     }
442     else
443     {
444         for (auto const& tor : torrents)
445         {
446             auto const it = std::lower_bound(myTorrents.begin(), myTorrents.end(), tor, compare);
447             auto const row = std::distance(myTorrents.begin(), it);
448 
449             beginInsertRows(QModelIndex(), row, row);
450             myTorrents.insert(it, tor);
451             endInsertRows();
452         }
453     }
454 }
455 
rowsRemove(torrents_t const & torrents)456 void TorrentModel::rowsRemove(torrents_t const& torrents)
457 {
458     // must walk in reverse to avoid invalidating row numbers
459     auto const& spans = getSpans(getIds(torrents.begin(), torrents.end()));
460     for (auto it = spans.rbegin(), end = spans.rend(); it != end; ++it)
461     {
462         auto const& span = *it;
463 
464         beginRemoveRows(QModelIndex(), span.first, span.second);
465         auto const n = span.second + 1 - span.first;
466         myTorrents.remove(span.first, n);
467         endRemoveRows();
468     }
469 
470     qDeleteAll(torrents);
471 }
472 
473 /***
474 ****
475 ***/
476 
hasTorrent(QString const & hashString) const477 bool TorrentModel::hasTorrent(QString const& hashString) const
478 {
479     auto test = [hashString](auto const& tor) { return tor->hashString() == hashString; };
480     return std::any_of(myTorrents.cbegin(), myTorrents.cend(), test);
481 }
482