1 /******************************************************************************
2 * Copyright (c) Transmission authors and contributors
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a
5 * copy of this software and associated documentation files (the "Software"),
6 * to deal in the Software without restriction, including without limitation
7 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 * and/or sell copies of the Software, and to permit persons to whom the
9 * Software is furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20 * DEALINGS IN THE SOFTWARE.
21 *****************************************************************************/
22
23 #include <math.h> /* pow() */
24 #include <string.h> /* strlen */
25
26 #include <gtk/gtk.h>
27 #include <glib/gi18n.h>
28 #include <gio/gio.h>
29
30 #include <event2/buffer.h>
31
32 #include <libtransmission/transmission.h>
33 #include <libtransmission/log.h>
34 #include <libtransmission/rpcimpl.h>
35 #include <libtransmission/utils.h> /* tr_free */
36 #include <libtransmission/variant.h>
37
38 #include "actions.h"
39 #include "conf.h"
40 #include "notify.h"
41 #include "tr-core.h"
42 #include "tr-prefs.h"
43 #include "util.h"
44
45 /***
46 ****
47 ***/
48
49 enum
50 {
51 ADD_ERROR_SIGNAL,
52 ADD_PROMPT_SIGNAL,
53 BLOCKLIST_SIGNAL,
54 BUSY_SIGNAL,
55 PORT_SIGNAL,
56 PREFS_SIGNAL,
57 LAST_SIGNAL
58 };
59
60 static guint signals[LAST_SIGNAL] = { 0 };
61
62 static void core_maybe_inhibit_hibernation(TrCore* core);
63
64 typedef struct TrCorePrivate
65 {
66 GFileMonitor* monitor;
67 gulong monitor_tag;
68 GFile* monitor_dir;
69 GSList* monitor_files;
70 gulong monitor_idle_tag;
71
72 gboolean adding_from_watch_dir;
73 gboolean inhibit_allowed;
74 gboolean have_inhibit_cookie;
75 gboolean dbus_error;
76 guint inhibit_cookie;
77 gint busy_count;
78 GtkTreeModel* raw_model;
79 GtkTreeModel* sorted_model;
80 tr_session* session;
81 GStringChunk* string_chunk;
82 }
83 TrCorePrivate;
84
core_is_disposed(TrCore const * core)85 static int core_is_disposed(TrCore const* core)
86 {
87 return core == NULL || core->priv->sorted_model == NULL;
88 }
89
90 G_DEFINE_TYPE_WITH_CODE(TrCore, tr_core, G_TYPE_OBJECT, G_ADD_PRIVATE(TrCore));
91
core_dispose(GObject * o)92 static void core_dispose(GObject* o)
93 {
94 TrCore* core = TR_CORE(o);
95
96 if (core->priv->sorted_model != NULL)
97 {
98 g_object_unref(core->priv->sorted_model);
99 core->priv->sorted_model = NULL;
100 core->priv->raw_model = NULL;
101 }
102
103 G_OBJECT_CLASS(tr_core_parent_class)->dispose(o);
104 }
105
core_finalize(GObject * o)106 static void core_finalize(GObject* o)
107 {
108 TrCore* core = TR_CORE(o);
109
110 g_string_chunk_free(core->priv->string_chunk);
111
112 G_OBJECT_CLASS(tr_core_parent_class)->finalize(o);
113 }
114
tr_core_class_init(TrCoreClass * core_class)115 static void tr_core_class_init(TrCoreClass* core_class)
116 {
117 GObjectClass* gobject_class;
118 GType core_type = G_TYPE_FROM_CLASS(core_class);
119
120 gobject_class = G_OBJECT_CLASS(core_class);
121 gobject_class->dispose = core_dispose;
122 gobject_class->finalize = core_finalize;
123
124 signals[ADD_ERROR_SIGNAL] = g_signal_new("add-error", core_type, G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(TrCoreClass, add_error),
125 NULL, NULL, g_cclosure_marshal_VOID__UINT_POINTER, G_TYPE_NONE, 2, G_TYPE_UINT, G_TYPE_POINTER);
126
127 signals[ADD_PROMPT_SIGNAL] = g_signal_new("add-prompt", core_type, G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(TrCoreClass,
128 add_prompt), NULL, NULL, g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1, G_TYPE_POINTER);
129
130 signals[BUSY_SIGNAL] = g_signal_new("busy", core_type, G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET(TrCoreClass, busy), NULL, NULL,
131 g_cclosure_marshal_VOID__BOOLEAN, G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
132
133 signals[BLOCKLIST_SIGNAL] = g_signal_new("blocklist-updated", core_type, G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET(TrCoreClass,
134 blocklist_updated), NULL, NULL, g_cclosure_marshal_VOID__INT, G_TYPE_NONE, 1, G_TYPE_INT);
135
136 signals[PORT_SIGNAL] = g_signal_new("port-tested", core_type, G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(TrCoreClass, port_tested),
137 NULL, NULL, g_cclosure_marshal_VOID__BOOLEAN, G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
138
139 signals[PREFS_SIGNAL] = g_signal_new("prefs-changed", core_type, G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(TrCoreClass,
140 prefs_changed), NULL, NULL, g_cclosure_marshal_VOID__INT, G_TYPE_NONE, 1, G_TYPE_INT);
141 }
142
tr_core_init(TrCore * core)143 static void tr_core_init(TrCore* core)
144 {
145 GtkListStore* store;
146 struct TrCorePrivate* p;
147
148 /* column types for the model used to store torrent information */
149 /* keep this in sync with the enum near the bottom of tr_core.h */
150 GType types[] =
151 {
152 G_TYPE_POINTER, /* collated name */
153 G_TYPE_POINTER, /* tr_torrent* */
154 G_TYPE_INT, /* torrent id */
155 G_TYPE_DOUBLE, /* tr_stat.pieceUploadSpeed_KBps */
156 G_TYPE_DOUBLE, /* tr_stat.pieceDownloadSpeed_KBps */
157 G_TYPE_INT, /* tr_stat.peersGettingFromUs */
158 G_TYPE_INT, /* tr_stat.peersSendingToUs + webseedsSendingToUs */
159 G_TYPE_DOUBLE, /* tr_stat.recheckProgress */
160 G_TYPE_BOOLEAN, /* filter.c:ACTIVITY_FILTER_ACTIVE */
161 G_TYPE_INT, /* tr_stat.activity */
162 G_TYPE_UCHAR, /* tr_stat.finished */
163 G_TYPE_CHAR, /* tr_priority_t */
164 G_TYPE_INT, /* tr_stat.queuePosition */
165 G_TYPE_UINT, /* build_torrent_trackers_hash () */
166 G_TYPE_INT, /* MC_ERROR */
167 G_TYPE_INT /* MC_ACTIVE_PEER_COUNT */
168 };
169
170 #if GLIB_CHECK_VERSION(2, 58, 0)
171 p = core->priv = tr_core_get_instance_private(core);
172 #else
173 p = core->priv = G_TYPE_INSTANCE_GET_PRIVATE(core, TR_CORE_TYPE, struct TrCorePrivate);
174 #endif
175
176 /* create the model used to store torrent data */
177 g_assert(G_N_ELEMENTS(types) == MC_ROW_COUNT);
178 store = gtk_list_store_newv(MC_ROW_COUNT, types);
179
180 p->raw_model = GTK_TREE_MODEL(store);
181 p->sorted_model = gtk_tree_model_sort_new_with_model(p->raw_model);
182 p->string_chunk = g_string_chunk_new(2048);
183 g_object_unref(p->raw_model);
184 }
185
186 /***
187 **** EMIT SIGNALS
188 ***/
189
core_emit_blocklist_udpated(TrCore * core,int ruleCount)190 static inline void core_emit_blocklist_udpated(TrCore* core, int ruleCount)
191 {
192 g_signal_emit(core, signals[BLOCKLIST_SIGNAL], 0, ruleCount);
193 }
194
core_emit_port_tested(TrCore * core,gboolean is_open)195 static inline void core_emit_port_tested(TrCore* core, gboolean is_open)
196 {
197 g_signal_emit(core, signals[PORT_SIGNAL], 0, is_open);
198 }
199
core_emit_err(TrCore * core,enum tr_core_err type,char const * msg)200 static inline void core_emit_err(TrCore* core, enum tr_core_err type, char const* msg)
201 {
202 g_signal_emit(core, signals[ADD_ERROR_SIGNAL], 0, type, msg);
203 }
204
core_emit_busy(TrCore * core,gboolean is_busy)205 static inline void core_emit_busy(TrCore* core, gboolean is_busy)
206 {
207 g_signal_emit(core, signals[BUSY_SIGNAL], 0, is_busy);
208 }
209
gtr_core_pref_changed(TrCore * core,tr_quark const key)210 void gtr_core_pref_changed(TrCore* core, tr_quark const key)
211 {
212 g_signal_emit(core, signals[PREFS_SIGNAL], 0, key);
213 }
214
215 /***
216 ****
217 ***/
218
core_raw_model(TrCore * core)219 static GtkTreeModel* core_raw_model(TrCore* core)
220 {
221 return core_is_disposed(core) ? NULL : core->priv->raw_model;
222 }
223
gtr_core_model(TrCore * core)224 GtkTreeModel* gtr_core_model(TrCore* core)
225 {
226 return core_is_disposed(core) ? NULL : core->priv->sorted_model;
227 }
228
gtr_core_session(TrCore * core)229 tr_session* gtr_core_session(TrCore* core)
230 {
231 return core_is_disposed(core) ? NULL : core->priv->session;
232 }
233
234 /***
235 **** BUSY
236 ***/
237
core_is_busy(TrCore * core)238 static bool core_is_busy(TrCore* core)
239 {
240 return core->priv->busy_count > 0;
241 }
242
core_add_to_busy(TrCore * core,int addMe)243 static void core_add_to_busy(TrCore* core, int addMe)
244 {
245 bool const wasBusy = core_is_busy(core);
246
247 core->priv->busy_count += addMe;
248
249 if (wasBusy != core_is_busy(core))
250 {
251 core_emit_busy(core, core_is_busy(core));
252 }
253 }
254
core_inc_busy(TrCore * core)255 static void core_inc_busy(TrCore* core)
256 {
257 core_add_to_busy(core, 1);
258 }
259
core_dec_busy(TrCore * core)260 static void core_dec_busy(TrCore* core)
261 {
262 core_add_to_busy(core, -1);
263 }
264
265 /***
266 ****
267 **** SORTING THE MODEL
268 ****
269 ***/
270
is_valid_eta(int t)271 static gboolean is_valid_eta(int t)
272 {
273 return t != TR_ETA_NOT_AVAIL && t != TR_ETA_UNKNOWN;
274 }
275
compare_eta(int a,int b)276 static int compare_eta(int a, int b)
277 {
278 int ret;
279
280 gboolean const a_valid = is_valid_eta(a);
281 gboolean const b_valid = is_valid_eta(b);
282
283 if (!a_valid && !b_valid)
284 {
285 ret = 0;
286 }
287 else if (!a_valid)
288 {
289 ret = -1;
290 }
291 else if (!b_valid)
292 {
293 ret = 1;
294 }
295 else
296 {
297 ret = a < b ? 1 : -1;
298 }
299
300 return ret;
301 }
302
compare_double(double a,double b)303 static int compare_double(double a, double b)
304 {
305 int ret;
306
307 if (a < b)
308 {
309 ret = -1;
310 }
311 else if (a > b)
312 {
313 ret = 1;
314 }
315 else
316 {
317 ret = 0;
318 }
319
320 return ret;
321 }
322
compare_uint64(uint64_t a,uint64_t b)323 static int compare_uint64(uint64_t a, uint64_t b)
324 {
325 int ret;
326
327 if (a < b)
328 {
329 ret = -1;
330 }
331 else if (a > b)
332 {
333 ret = 1;
334 }
335 else
336 {
337 ret = 0;
338 }
339
340 return ret;
341 }
342
compare_int(int a,int b)343 static int compare_int(int a, int b)
344 {
345 int ret;
346
347 if (a < b)
348 {
349 ret = -1;
350 }
351 else if (a > b)
352 {
353 ret = 1;
354 }
355 else
356 {
357 ret = 0;
358 }
359
360 return ret;
361 }
362
compare_ratio(double a,double b)363 static int compare_ratio(double a, double b)
364 {
365 int ret;
366
367 if ((int)a == TR_RATIO_INF && (int)b == TR_RATIO_INF)
368 {
369 ret = 0;
370 }
371 else if ((int)a == TR_RATIO_INF)
372 {
373 ret = 1;
374 }
375 else if ((int)b == TR_RATIO_INF)
376 {
377 ret = -1;
378 }
379 else
380 {
381 ret = compare_double(a, b);
382 }
383
384 return ret;
385 }
386
compare_time(time_t a,time_t b)387 static int compare_time(time_t a, time_t b)
388 {
389 int ret;
390
391 if (a < b)
392 {
393 ret = -1;
394 }
395 else if (a > b)
396 {
397 ret = 1;
398 }
399 else
400 {
401 ret = 0;
402 }
403
404 return ret;
405 }
406
compare_by_name(GtkTreeModel * m,GtkTreeIter * a,GtkTreeIter * b,gpointer user_data UNUSED)407 static int compare_by_name(GtkTreeModel* m, GtkTreeIter* a, GtkTreeIter* b, gpointer user_data UNUSED)
408 {
409 char const* ca;
410 char const* cb;
411 gtk_tree_model_get(m, a, MC_NAME_COLLATED, &ca, -1);
412 gtk_tree_model_get(m, b, MC_NAME_COLLATED, &cb, -1);
413 return g_strcmp0(ca, cb);
414 }
415
compare_by_queue(GtkTreeModel * m,GtkTreeIter * a,GtkTreeIter * b,gpointer user_data UNUSED)416 static int compare_by_queue(GtkTreeModel* m, GtkTreeIter* a, GtkTreeIter* b, gpointer user_data UNUSED)
417 {
418 tr_torrent* ta;
419 tr_torrent* tb;
420 tr_stat const* sa;
421 tr_stat const* sb;
422
423 gtk_tree_model_get(m, a, MC_TORRENT, &ta, -1);
424 sa = tr_torrentStatCached(ta);
425 gtk_tree_model_get(m, b, MC_TORRENT, &tb, -1);
426 sb = tr_torrentStatCached(tb);
427
428 return sb->queuePosition - sa->queuePosition;
429 }
430
compare_by_ratio(GtkTreeModel * m,GtkTreeIter * a,GtkTreeIter * b,gpointer user_data)431 static int compare_by_ratio(GtkTreeModel* m, GtkTreeIter* a, GtkTreeIter* b, gpointer user_data)
432 {
433 int ret = 0;
434 tr_torrent* ta;
435 tr_torrent* tb;
436 tr_stat const* sa;
437 tr_stat const* sb;
438
439 gtk_tree_model_get(m, a, MC_TORRENT, &ta, -1);
440 sa = tr_torrentStatCached(ta);
441 gtk_tree_model_get(m, b, MC_TORRENT, &tb, -1);
442 sb = tr_torrentStatCached(tb);
443
444 if (ret == 0)
445 {
446 ret = compare_ratio(sa->ratio, sb->ratio);
447 }
448
449 if (ret == 0)
450 {
451 ret = compare_by_queue(m, a, b, user_data);
452 }
453
454 return ret;
455 }
456
compare_by_activity(GtkTreeModel * m,GtkTreeIter * a,GtkTreeIter * b,gpointer user_data)457 static int compare_by_activity(GtkTreeModel* m, GtkTreeIter* a, GtkTreeIter* b, gpointer user_data)
458 {
459 int ret = 0;
460 tr_torrent* ta;
461 tr_torrent* tb;
462 double aUp;
463 double aDown;
464 double bUp;
465 double bDown;
466
467 gtk_tree_model_get(m, a, MC_SPEED_UP, &aUp, MC_SPEED_DOWN, &aDown, MC_TORRENT, &ta, -1);
468 gtk_tree_model_get(m, b, MC_SPEED_UP, &bUp, MC_SPEED_DOWN, &bDown, MC_TORRENT, &tb, -1);
469
470 ret = compare_double(aUp + aDown, bUp + bDown);
471
472 if (ret == 0)
473 {
474 tr_stat const* const sa = tr_torrentStatCached(ta);
475 tr_stat const* const sb = tr_torrentStatCached(tb);
476 ret = compare_uint64(sa->peersSendingToUs + sa->peersGettingFromUs, sb->peersSendingToUs + sb->peersGettingFromUs);
477 }
478
479 if (ret == 0)
480 {
481 ret = compare_by_queue(m, a, b, user_data);
482 }
483
484 return ret;
485 }
486
compare_by_age(GtkTreeModel * m,GtkTreeIter * a,GtkTreeIter * b,gpointer u)487 static int compare_by_age(GtkTreeModel* m, GtkTreeIter* a, GtkTreeIter* b, gpointer u)
488 {
489 int ret = 0;
490 tr_torrent* ta;
491 tr_torrent* tb;
492
493 gtk_tree_model_get(m, a, MC_TORRENT, &ta, -1);
494 gtk_tree_model_get(m, b, MC_TORRENT, &tb, -1);
495
496 if (ret == 0)
497 {
498 ret = compare_time(tr_torrentStatCached(ta)->addedDate, tr_torrentStatCached(tb)->addedDate);
499 }
500
501 if (ret == 0)
502 {
503 ret = compare_by_name(m, a, b, u);
504 }
505
506 return ret;
507 }
508
compare_by_size(GtkTreeModel * m,GtkTreeIter * a,GtkTreeIter * b,gpointer u)509 static int compare_by_size(GtkTreeModel* m, GtkTreeIter* a, GtkTreeIter* b, gpointer u)
510 {
511 int ret = 0;
512 tr_torrent* t;
513 tr_info const* ia;
514 tr_info const* ib;
515
516 gtk_tree_model_get(m, a, MC_TORRENT, &t, -1);
517 ia = tr_torrentInfo(t);
518 gtk_tree_model_get(m, b, MC_TORRENT, &t, -1);
519 ib = tr_torrentInfo(t);
520
521 if (ret == 0)
522 {
523 ret = compare_uint64(ia->totalSize, ib->totalSize);
524 }
525
526 if (ret == 0)
527 {
528 ret = compare_by_name(m, a, b, u);
529 }
530
531 return ret;
532 }
533
compare_by_progress(GtkTreeModel * m,GtkTreeIter * a,GtkTreeIter * b,gpointer u)534 static int compare_by_progress(GtkTreeModel* m, GtkTreeIter* a, GtkTreeIter* b, gpointer u)
535 {
536 int ret = 0;
537 tr_torrent* t;
538 tr_stat const* sa;
539 tr_stat const* sb;
540
541 gtk_tree_model_get(m, a, MC_TORRENT, &t, -1);
542 sa = tr_torrentStatCached(t);
543 gtk_tree_model_get(m, b, MC_TORRENT, &t, -1);
544 sb = tr_torrentStatCached(t);
545
546 if (ret == 0)
547 {
548 ret = compare_double(sa->percentComplete, sb->percentComplete);
549 }
550
551 if (ret == 0)
552 {
553 ret = compare_double(sa->seedRatioPercentDone, sb->seedRatioPercentDone);
554 }
555
556 if (ret == 0)
557 {
558 ret = compare_by_ratio(m, a, b, u);
559 }
560
561 return ret;
562 }
563
compare_by_eta(GtkTreeModel * m,GtkTreeIter * a,GtkTreeIter * b,gpointer u)564 static int compare_by_eta(GtkTreeModel* m, GtkTreeIter* a, GtkTreeIter* b, gpointer u)
565 {
566 int ret = 0;
567 tr_torrent* ta;
568 tr_torrent* tb;
569
570 gtk_tree_model_get(m, a, MC_TORRENT, &ta, -1);
571 gtk_tree_model_get(m, b, MC_TORRENT, &tb, -1);
572
573 if (ret == 0)
574 {
575 ret = compare_eta(tr_torrentStatCached(ta)->eta, tr_torrentStatCached(tb)->eta);
576 }
577
578 if (ret == 0)
579 {
580 ret = compare_by_name(m, a, b, u);
581 }
582
583 return ret;
584 }
585
compare_by_state(GtkTreeModel * m,GtkTreeIter * a,GtkTreeIter * b,gpointer u)586 static int compare_by_state(GtkTreeModel* m, GtkTreeIter* a, GtkTreeIter* b, gpointer u)
587 {
588 int ret = 0;
589 int sa;
590 int sb;
591 tr_torrent* ta;
592 tr_torrent* tb;
593
594 gtk_tree_model_get(m, a, MC_ACTIVITY, &sa, MC_TORRENT, &ta, -1);
595 gtk_tree_model_get(m, b, MC_ACTIVITY, &sb, MC_TORRENT, &tb, -1);
596
597 if (ret == 0)
598 {
599 ret = compare_int(sa, sb);
600 }
601
602 if (ret == 0)
603 {
604 ret = compare_by_queue(m, a, b, u);
605 }
606
607 return ret;
608 }
609
core_set_sort_mode(TrCore * core,char const * mode,gboolean is_reversed)610 static void core_set_sort_mode(TrCore* core, char const* mode, gboolean is_reversed)
611 {
612 int const col = MC_TORRENT;
613 GtkTreeIterCompareFunc sort_func;
614 GtkSortType type = is_reversed ? GTK_SORT_ASCENDING : GTK_SORT_DESCENDING;
615 GtkTreeSortable* sortable = GTK_TREE_SORTABLE(gtr_core_model(core));
616
617 if (g_strcmp0(mode, "sort-by-activity") == 0)
618 {
619 sort_func = compare_by_activity;
620 }
621 else if (g_strcmp0(mode, "sort-by-age") == 0)
622 {
623 sort_func = compare_by_age;
624 }
625 else if (g_strcmp0(mode, "sort-by-progress") == 0)
626 {
627 sort_func = compare_by_progress;
628 }
629 else if (g_strcmp0(mode, "sort-by-queue") == 0)
630 {
631 sort_func = compare_by_queue;
632 }
633 else if (g_strcmp0(mode, "sort-by-time-left") == 0)
634 {
635 sort_func = compare_by_eta;
636 }
637 else if (g_strcmp0(mode, "sort-by-ratio") == 0)
638 {
639 sort_func = compare_by_ratio;
640 }
641 else if (g_strcmp0(mode, "sort-by-state") == 0)
642 {
643 sort_func = compare_by_state;
644 }
645 else if (g_strcmp0(mode, "sort-by-size") == 0)
646 {
647 sort_func = compare_by_size;
648 }
649 else
650 {
651 sort_func = compare_by_name;
652 type = is_reversed ? GTK_SORT_DESCENDING : GTK_SORT_ASCENDING;
653 }
654
655 gtk_tree_sortable_set_sort_func(sortable, col, sort_func, NULL, NULL);
656 gtk_tree_sortable_set_sort_column_id(sortable, col, type);
657 }
658
659 /***
660 ****
661 **** WATCHDIR
662 ****
663 ***/
664
get_file_mtime(GFile * file)665 static time_t get_file_mtime(GFile* file)
666 {
667 GFileInfo* info;
668 time_t mtime = 0;
669
670 info = g_file_query_info(file, G_FILE_ATTRIBUTE_TIME_MODIFIED, 0, NULL, NULL);
671
672 if (info != NULL)
673 {
674 mtime = g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
675 g_object_unref(G_OBJECT(info));
676 }
677
678 return mtime;
679 }
680
rename_torrent_and_unref_file(GFile * file)681 static void rename_torrent_and_unref_file(GFile* file)
682 {
683 GFileInfo* info;
684
685 info = g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME, 0, NULL, NULL);
686
687 if (info != NULL)
688 {
689 GError* error = NULL;
690 char const* old_name;
691 char* new_name;
692 GFile* new_file;
693
694 old_name = g_file_info_get_attribute_string(info, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME);
695 new_name = g_strdup_printf("%s.added", old_name);
696 new_file = g_file_set_display_name(file, new_name, NULL, &error);
697
698 if (error != NULL)
699 {
700 g_message("Unable to rename \"%s\" as \"%s\": %s", old_name, new_name, error->message);
701 g_error_free(error);
702 }
703
704 if (new_file != NULL)
705 {
706 g_object_unref(G_OBJECT(new_file));
707 }
708
709 g_free(new_name);
710 g_object_unref(G_OBJECT(info));
711 }
712
713 g_object_unref(G_OBJECT(file));
714 }
715
core_watchdir_idle(gpointer gcore)716 static gboolean core_watchdir_idle(gpointer gcore)
717 {
718 GSList* changing = NULL;
719 GSList* unchanging = NULL;
720 TrCore* core = TR_CORE(gcore);
721 time_t const now = tr_time();
722 struct TrCorePrivate* p = core->priv;
723
724 /* separate the files into two lists: changing and unchanging */
725 for (GSList* l = p->monitor_files; l != NULL; l = l->next)
726 {
727 GFile* file = l->data;
728 time_t const mtime = get_file_mtime(file);
729
730 if (mtime + 2 >= now)
731 {
732 changing = g_slist_prepend(changing, file);
733 }
734 else
735 {
736 unchanging = g_slist_prepend(unchanging, file);
737 }
738 }
739
740 /* add the files that have stopped changing */
741 if (unchanging != NULL)
742 {
743 gboolean const do_start = gtr_pref_flag_get(TR_KEY_start_added_torrents);
744 gboolean const do_prompt = gtr_pref_flag_get(TR_KEY_show_options_window);
745
746 core->priv->adding_from_watch_dir = TRUE;
747 gtr_core_add_files(core, unchanging, do_start, do_prompt, TRUE);
748 g_slist_foreach(unchanging, (GFunc)(GCallback)rename_torrent_and_unref_file, NULL);
749 g_slist_free(unchanging);
750 core->priv->adding_from_watch_dir = FALSE;
751 }
752
753 /* keep monitoring the ones that are still changing */
754 g_slist_free(p->monitor_files);
755 p->monitor_files = changing;
756
757 /* if monitor_files is nonempty, keep checking every second */
758 if (core->priv->monitor_files)
759 {
760 return G_SOURCE_CONTINUE;
761 }
762
763 core->priv->monitor_idle_tag = 0;
764 return G_SOURCE_REMOVE;
765 }
766
767 /* If this file is a torrent, add it to our list */
core_watchdir_monitor_file(TrCore * core,GFile * file)768 static void core_watchdir_monitor_file(TrCore* core, GFile* file)
769 {
770 char* filename = g_file_get_path(file);
771 gboolean const is_torrent = g_str_has_suffix(filename, ".torrent");
772
773 if (is_torrent)
774 {
775 struct TrCorePrivate* p = core->priv;
776 bool found = false;
777
778 /* if we're not already watching this file, start watching it now */
779 for (GSList* l = p->monitor_files; !found && l != NULL; l = l->next)
780 {
781 found = g_file_equal(file, l->data);
782 }
783
784 if (!found)
785 {
786 g_object_ref(file);
787 p->monitor_files = g_slist_prepend(p->monitor_files, file);
788
789 if (p->monitor_idle_tag == 0)
790 {
791 p->monitor_idle_tag = gdk_threads_add_timeout_seconds(1, core_watchdir_idle, core);
792 }
793 }
794 }
795
796 g_free(filename);
797 }
798
799 /* GFileMonitor noticed a file was created */
on_file_changed_in_watchdir(GFileMonitor * monitor UNUSED,GFile * file,GFile * other_type UNUSED,GFileMonitorEvent event_type,gpointer core)800 static void on_file_changed_in_watchdir(GFileMonitor* monitor UNUSED, GFile* file, GFile* other_type UNUSED,
801 GFileMonitorEvent event_type, gpointer core)
802 {
803 if (event_type == G_FILE_MONITOR_EVENT_CREATED)
804 {
805 core_watchdir_monitor_file(core, file);
806 }
807 }
808
809 /* walk through the pre-existing files in the watchdir */
core_watchdir_scan(TrCore * core)810 static void core_watchdir_scan(TrCore* core)
811 {
812 char const* dirname = gtr_pref_string_get(TR_KEY_watch_dir);
813 GDir* dir = g_dir_open(dirname, 0, NULL);
814
815 if (dir != NULL)
816 {
817 char const* name;
818
819 while ((name = g_dir_read_name(dir)) != NULL)
820 {
821 char* filename = g_build_filename(dirname, name, NULL);
822 GFile* file = g_file_new_for_path(filename);
823 core_watchdir_monitor_file(core, file);
824 g_object_unref(file);
825 g_free(filename);
826 }
827
828 g_dir_close(dir);
829 }
830 }
831
core_watchdir_update(TrCore * core)832 static void core_watchdir_update(TrCore* core)
833 {
834 gboolean const is_enabled = gtr_pref_flag_get(TR_KEY_watch_dir_enabled);
835 GFile* dir = g_file_new_for_path(gtr_pref_string_get(TR_KEY_watch_dir));
836 struct TrCorePrivate* p = core->priv;
837
838 if (p->monitor != NULL && (!is_enabled || !g_file_equal(dir, p->monitor_dir)))
839 {
840 g_signal_handler_disconnect(p->monitor, p->monitor_tag);
841 g_file_monitor_cancel(p->monitor);
842 g_object_unref(p->monitor);
843 g_object_unref(p->monitor_dir);
844
845 p->monitor_dir = NULL;
846 p->monitor = NULL;
847 p->monitor_tag = 0;
848 }
849
850 if (is_enabled && p->monitor == NULL)
851 {
852 GFileMonitor* m = g_file_monitor_directory(dir, 0, NULL, NULL);
853 core_watchdir_scan(core);
854
855 g_object_ref(dir);
856 p->monitor = m;
857 p->monitor_dir = dir;
858 p->monitor_tag = g_signal_connect(m, "changed", G_CALLBACK(on_file_changed_in_watchdir), core);
859 }
860
861 g_object_unref(dir);
862 }
863
864 /***
865 ****
866 ***/
867
on_pref_changed(TrCore * core,tr_quark const key,gpointer data UNUSED)868 static void on_pref_changed(TrCore* core, tr_quark const key, gpointer data UNUSED)
869 {
870 switch (key)
871 {
872 case TR_KEY_sort_mode:
873 case TR_KEY_sort_reversed:
874 {
875 char const* mode = gtr_pref_string_get(TR_KEY_sort_mode);
876 gboolean const is_reversed = gtr_pref_flag_get(TR_KEY_sort_reversed);
877 core_set_sort_mode(core, mode, is_reversed);
878 break;
879 }
880
881 case TR_KEY_peer_limit_global:
882 tr_sessionSetPeerLimit(gtr_core_session(core), gtr_pref_int_get(key));
883 break;
884
885 case TR_KEY_peer_limit_per_torrent:
886 tr_sessionSetPeerLimitPerTorrent(gtr_core_session(core), gtr_pref_int_get(key));
887 break;
888
889 case TR_KEY_inhibit_desktop_hibernation:
890 core_maybe_inhibit_hibernation(core);
891 break;
892
893 case TR_KEY_watch_dir:
894 case TR_KEY_watch_dir_enabled:
895 core_watchdir_update(core);
896 break;
897
898 default:
899 break;
900 }
901 }
902
903 /**
904 ***
905 **/
906
gtr_core_new(tr_session * session)907 TrCore* gtr_core_new(tr_session* session)
908 {
909 TrCore* core = TR_CORE(g_object_new(TR_CORE_TYPE, NULL));
910
911 core->priv->session = session;
912
913 /* init from prefs & listen to pref changes */
914 on_pref_changed(core, TR_KEY_sort_mode, NULL);
915 on_pref_changed(core, TR_KEY_sort_reversed, NULL);
916 on_pref_changed(core, TR_KEY_watch_dir_enabled, NULL);
917 on_pref_changed(core, TR_KEY_peer_limit_global, NULL);
918 on_pref_changed(core, TR_KEY_inhibit_desktop_hibernation, NULL);
919 g_signal_connect(core, "prefs-changed", G_CALLBACK(on_pref_changed), NULL);
920
921 return core;
922 }
923
gtr_core_close(TrCore * core)924 tr_session* gtr_core_close(TrCore* core)
925 {
926 tr_session* session = gtr_core_session(core);
927
928 if (session != NULL)
929 {
930 core->priv->session = NULL;
931 gtr_pref_save(session);
932 }
933
934 return session;
935 }
936
937 /***
938 **** COMPLETENESS CALLBACK
939 ***/
940
941 struct notify_callback_data
942 {
943 TrCore* core;
944 int torrent_id;
945 };
946
on_torrent_completeness_changed_idle(gpointer gdata)947 static gboolean on_torrent_completeness_changed_idle(gpointer gdata)
948 {
949 struct notify_callback_data* data = gdata;
950 gtr_notify_torrent_completed(data->core, data->torrent_id);
951 g_object_unref(G_OBJECT(data->core));
952 g_free(data);
953 return G_SOURCE_REMOVE;
954 }
955
956 /* this is called in the libtransmission thread, *NOT* the GTK+ thread,
957 so delegate to the GTK+ thread before calling notify's dbus code... */
on_torrent_completeness_changed(tr_torrent * tor,tr_completeness completeness,bool was_running,void * gcore)958 static void on_torrent_completeness_changed(tr_torrent* tor, tr_completeness completeness, bool was_running, void* gcore)
959 {
960 if (was_running && completeness != TR_LEECH && tr_torrentStat(tor)->sizeWhenDone != 0)
961 {
962 struct notify_callback_data* data = g_new(struct notify_callback_data, 1);
963 data->core = gcore;
964 data->torrent_id = tr_torrentId(tor);
965 g_object_ref(G_OBJECT(data->core));
966 gdk_threads_add_idle(on_torrent_completeness_changed_idle, data);
967 }
968 }
969
970 /***
971 **** METADATA CALLBACK
972 ***/
973
get_collated_name(TrCore * core,tr_torrent const * tor)974 static char const* get_collated_name(TrCore* core, tr_torrent const* tor)
975 {
976 char buf[2048];
977 char const* name = tr_torrentName(tor);
978 char* down = g_utf8_strdown(name ? name : "", -1);
979 tr_info const* inf = tr_torrentInfo(tor);
980 g_snprintf(buf, sizeof(buf), "%s\t%s", down, inf->hashString);
981 g_free(down);
982 return g_string_chunk_insert_const(core->priv->string_chunk, buf);
983 }
984
985 struct metadata_callback_data
986 {
987 TrCore* core;
988 int torrent_id;
989 };
990
find_row_from_torrent_id(GtkTreeModel * model,int id,GtkTreeIter * setme)991 static gboolean find_row_from_torrent_id(GtkTreeModel* model, int id, GtkTreeIter* setme)
992 {
993 GtkTreeIter iter;
994 gboolean match = FALSE;
995
996 if (gtk_tree_model_iter_children(model, &iter, NULL))
997 {
998 do
999 {
1000 int row_id;
1001 gtk_tree_model_get(model, &iter, MC_TORRENT_ID, &row_id, -1);
1002 match = id == row_id;
1003 }
1004 while (!match && gtk_tree_model_iter_next(model, &iter));
1005 }
1006
1007 if (match)
1008 {
1009 *setme = iter;
1010 }
1011
1012 return match;
1013 }
1014
on_torrent_metadata_changed_idle(gpointer gdata)1015 static gboolean on_torrent_metadata_changed_idle(gpointer gdata)
1016 {
1017 struct notify_callback_data* data = gdata;
1018 tr_session* session = gtr_core_session(data->core);
1019 tr_torrent* tor = tr_torrentFindFromId(session, data->torrent_id);
1020
1021 /* update the torrent's collated name */
1022 if (tor != NULL)
1023 {
1024 GtkTreeIter iter;
1025 GtkTreeModel* model = core_raw_model(data->core);
1026
1027 if (find_row_from_torrent_id(model, data->torrent_id, &iter))
1028 {
1029 char const* collated = get_collated_name(data->core, tor);
1030 GtkListStore* store = GTK_LIST_STORE(model);
1031 gtk_list_store_set(store, &iter, MC_NAME_COLLATED, collated, -1);
1032 }
1033 }
1034
1035 /* cleanup */
1036 g_object_unref(G_OBJECT(data->core));
1037 g_free(data);
1038 return G_SOURCE_REMOVE;
1039 }
1040
1041 /* this is called in the libtransmission thread, *NOT* the GTK+ thread,
1042 so delegate to the GTK+ thread before changing our list store... */
on_torrent_metadata_changed(tr_torrent * tor,void * gcore)1043 static void on_torrent_metadata_changed(tr_torrent* tor, void* gcore)
1044 {
1045 struct notify_callback_data* data = g_new(struct notify_callback_data, 1);
1046 data->core = gcore;
1047 data->torrent_id = tr_torrentId(tor);
1048 g_object_ref(G_OBJECT(data->core));
1049 gdk_threads_add_idle(on_torrent_metadata_changed_idle, data);
1050 }
1051
1052 /***
1053 ****
1054 **** ADDING TORRENTS
1055 ****
1056 ***/
1057
build_torrent_trackers_hash(tr_torrent * tor)1058 static unsigned int build_torrent_trackers_hash(tr_torrent* tor)
1059 {
1060 uint64_t hash = 0;
1061 tr_info const* const inf = tr_torrentInfo(tor);
1062
1063 for (unsigned int i = 0; i < inf->trackerCount; ++i)
1064 {
1065 for (char const* pch = inf->trackers[i].announce; *pch != '\0'; ++pch)
1066 {
1067 hash = (hash << 4) ^ (hash >> 28) ^ *pch;
1068 }
1069 }
1070
1071 return hash;
1072 }
1073
is_torrent_active(tr_stat const * st)1074 static gboolean is_torrent_active(tr_stat const* st)
1075 {
1076 return st->peersSendingToUs > 0 || st->peersGettingFromUs > 0 || st->activity == TR_STATUS_CHECK;
1077 }
1078
gtr_core_add_torrent(TrCore * core,tr_torrent * tor,gboolean do_notify)1079 void gtr_core_add_torrent(TrCore* core, tr_torrent* tor, gboolean do_notify)
1080 {
1081 if (tor != NULL)
1082 {
1083 GtkTreeIter unused;
1084 tr_stat const* st = tr_torrentStat(tor);
1085 char const* collated = get_collated_name(core, tor);
1086 unsigned int const trackers_hash = build_torrent_trackers_hash(tor);
1087 GtkListStore* store = GTK_LIST_STORE(core_raw_model(core));
1088
1089 gtk_list_store_insert_with_values(store, &unused, 0,
1090 MC_NAME_COLLATED, collated,
1091 MC_TORRENT, tor,
1092 MC_TORRENT_ID, tr_torrentId(tor),
1093 MC_SPEED_UP, st->pieceUploadSpeed_KBps,
1094 MC_SPEED_DOWN, st->pieceDownloadSpeed_KBps,
1095 MC_ACTIVE_PEERS_UP, st->peersGettingFromUs,
1096 MC_ACTIVE_PEERS_DOWN, st->peersSendingToUs + st->webseedsSendingToUs,
1097 MC_RECHECK_PROGRESS, st->recheckProgress,
1098 MC_ACTIVE, is_torrent_active(st),
1099 MC_ACTIVITY, st->activity,
1100 MC_FINISHED, st->finished,
1101 MC_PRIORITY, tr_torrentGetPriority(tor),
1102 MC_QUEUE_POSITION, st->queuePosition,
1103 MC_TRACKERS, trackers_hash,
1104 -1);
1105
1106 if (do_notify)
1107 {
1108 gtr_notify_torrent_added(tr_torrentName(tor));
1109 }
1110
1111 tr_torrentSetMetadataCallback(tor, on_torrent_metadata_changed, core);
1112 tr_torrentSetCompletenessCallback(tor, on_torrent_completeness_changed, core);
1113 }
1114 }
1115
core_create_new_torrent(TrCore * core,tr_ctor * ctor)1116 static tr_torrent* core_create_new_torrent(TrCore* core, tr_ctor* ctor)
1117 {
1118 tr_torrent* tor;
1119 bool do_trash = false;
1120 tr_session* session = gtr_core_session(core);
1121
1122 /* let the gtk client handle the removal, since libT
1123 * doesn't have any concept of the glib trash API */
1124 tr_ctorGetDeleteSource(ctor, &do_trash);
1125 tr_ctorSetDeleteSource(ctor, FALSE);
1126 tor = tr_torrentNew(ctor, NULL, NULL);
1127
1128 if (tor != NULL && do_trash)
1129 {
1130 char const* config = tr_sessionGetConfigDir(session);
1131 char const* source = tr_ctorGetSourceFile(ctor);
1132
1133 if (source != NULL)
1134 {
1135 /* #1294: don't delete the .torrent file if it's our internal copy */
1136 bool const is_internal = strstr(source, config) == source;
1137
1138 if (!is_internal)
1139 {
1140 gtr_file_trash_or_remove(source, NULL);
1141 }
1142 }
1143 }
1144
1145 return tor;
1146 }
1147
core_add_ctor(TrCore * core,tr_ctor * ctor,gboolean do_prompt,gboolean do_notify)1148 static int core_add_ctor(TrCore* core, tr_ctor* ctor, gboolean do_prompt, gboolean do_notify)
1149 {
1150 tr_info inf;
1151 int err = tr_torrentParse(ctor, &inf);
1152
1153 switch (err)
1154 {
1155 case TR_PARSE_ERR:
1156 break;
1157
1158 case TR_PARSE_DUPLICATE:
1159 /* don't complain about .torrent files in the watch directory
1160 * that have already been added... that gets annoying and we
1161 * don't want to be nagging users to clean up their watch dirs */
1162 if (tr_ctorGetSourceFile(ctor) == NULL || !core->priv->adding_from_watch_dir)
1163 {
1164 core_emit_err(core, err, inf.name);
1165 }
1166
1167 tr_metainfoFree(&inf);
1168 tr_ctorFree(ctor);
1169 break;
1170
1171 default:
1172 if (do_prompt)
1173 {
1174 g_signal_emit(core, signals[ADD_PROMPT_SIGNAL], 0, ctor);
1175 }
1176 else
1177 {
1178 gtr_core_add_torrent(core, core_create_new_torrent(core, ctor), do_notify);
1179 tr_ctorFree(ctor);
1180 }
1181
1182 tr_metainfoFree(&inf);
1183 break;
1184 }
1185
1186 return err;
1187 }
1188
core_apply_defaults(tr_ctor * ctor)1189 static void core_apply_defaults(tr_ctor* ctor)
1190 {
1191 if (!tr_ctorGetPaused(ctor, TR_FORCE, NULL))
1192 {
1193 tr_ctorSetPaused(ctor, TR_FORCE, !gtr_pref_flag_get(TR_KEY_start_added_torrents));
1194 }
1195
1196 if (!tr_ctorGetDeleteSource(ctor, NULL))
1197 {
1198 tr_ctorSetDeleteSource(ctor, gtr_pref_flag_get(TR_KEY_trash_original_torrent_files));
1199 }
1200
1201 if (!tr_ctorGetPeerLimit(ctor, TR_FORCE, NULL))
1202 {
1203 tr_ctorSetPeerLimit(ctor, TR_FORCE, gtr_pref_int_get(TR_KEY_peer_limit_per_torrent));
1204 }
1205
1206 if (!tr_ctorGetDownloadDir(ctor, TR_FORCE, NULL))
1207 {
1208 tr_ctorSetDownloadDir(ctor, TR_FORCE, gtr_pref_string_get(TR_KEY_download_dir));
1209 }
1210 }
1211
gtr_core_add_ctor(TrCore * core,tr_ctor * ctor)1212 void gtr_core_add_ctor(TrCore* core, tr_ctor* ctor)
1213 {
1214 gboolean const do_notify = FALSE;
1215 gboolean const do_prompt = gtr_pref_flag_get(TR_KEY_show_options_window);
1216 core_apply_defaults(ctor);
1217 core_add_ctor(core, ctor, do_prompt, do_notify);
1218 }
1219
1220 /***
1221 ****
1222 ***/
1223
1224 struct add_from_url_data
1225 {
1226 TrCore* core;
1227 tr_ctor* ctor;
1228 bool do_prompt;
1229 bool do_notify;
1230 };
1231
add_file_async_callback(GObject * file,GAsyncResult * result,gpointer gdata)1232 static void add_file_async_callback(GObject* file, GAsyncResult* result, gpointer gdata)
1233 {
1234 gsize length;
1235 char* contents;
1236 GError* error = NULL;
1237 struct add_from_url_data* data = gdata;
1238
1239 if (!g_file_load_contents_finish(G_FILE(file), result, &contents, &length, NULL, &error))
1240 {
1241 g_message(_("Couldn't read \"%s\": %s"), g_file_get_parse_name(G_FILE(file)), error->message);
1242 g_error_free(error);
1243 }
1244 else if (tr_ctorSetMetainfo(data->ctor, (uint8_t const*)contents, length) == 0)
1245 {
1246 core_add_ctor(data->core, data->ctor, data->do_prompt, data->do_notify);
1247 }
1248 else
1249 {
1250 tr_ctorFree(data->ctor);
1251 }
1252
1253 core_dec_busy(data->core);
1254 g_free(data);
1255 }
1256
add_file(TrCore * core,GFile * file,gboolean do_start,gboolean do_prompt,gboolean do_notify)1257 static bool add_file(TrCore* core, GFile* file, gboolean do_start, gboolean do_prompt, gboolean do_notify)
1258 {
1259 bool handled = false;
1260 tr_session* session = gtr_core_session(core);
1261
1262 if (session != NULL)
1263 {
1264 tr_ctor* ctor;
1265 bool tried = false;
1266 bool loaded = false;
1267
1268 ctor = tr_ctorNew(session);
1269 core_apply_defaults(ctor);
1270 tr_ctorSetPaused(ctor, TR_FORCE, !do_start);
1271
1272 /* local files... */
1273 if (!tried)
1274 {
1275 char* str = g_file_get_path(file);
1276
1277 if ((tried = (str != NULL) && g_file_test(str, G_FILE_TEST_EXISTS)))
1278 {
1279 loaded = !tr_ctorSetMetainfoFromFile(ctor, str);
1280 }
1281
1282 g_free(str);
1283 }
1284
1285 /* magnet links... */
1286 if (!tried && g_file_has_uri_scheme(file, "magnet"))
1287 {
1288 /* GFile mangles the original string with /// so we have to un-mangle */
1289 char* str = g_file_get_parse_name(file);
1290 char* magnet = g_strdup_printf("magnet:%s", strchr(str, '?'));
1291 tried = true;
1292 loaded = !tr_ctorSetMetainfoFromMagnetLink(ctor, magnet);
1293 g_free(magnet);
1294 g_free(str);
1295 }
1296
1297 /* hashcodes that we can turn into magnet links... */
1298 if (!tried)
1299 {
1300 char* str = g_file_get_basename(file);
1301
1302 if (gtr_is_hex_hashcode(str))
1303 {
1304 char* magnet = g_strdup_printf("magnet:?xt=urn:btih:%s", str);
1305 tried = true;
1306 loaded = !tr_ctorSetMetainfoFromMagnetLink(ctor, magnet);
1307 g_free(magnet);
1308 }
1309
1310 g_free(str);
1311 }
1312
1313 /* if we were able to load the metainfo, add the torrent */
1314 if (loaded)
1315 {
1316 handled = true;
1317 core_add_ctor(core, ctor, do_prompt, do_notify);
1318 }
1319 else if (g_file_has_uri_scheme(file, "http") || g_file_has_uri_scheme(file, "https") ||
1320 g_file_has_uri_scheme(file, "ftp"))
1321 {
1322 struct add_from_url_data* data;
1323
1324 data = g_new0(struct add_from_url_data, 1);
1325 data->core = core;
1326 data->ctor = ctor;
1327 data->do_prompt = do_prompt;
1328 data->do_notify = do_notify;
1329
1330 handled = true;
1331 core_inc_busy(core);
1332 g_file_load_contents_async(file, NULL, add_file_async_callback, data);
1333 }
1334 else
1335 {
1336 tr_ctorFree(ctor);
1337 g_message(_("Skipping unknown torrent \"%s\""), g_file_get_parse_name(file));
1338 }
1339 }
1340
1341 return handled;
1342 }
1343
gtr_core_add_from_url(TrCore * core,char const * uri)1344 bool gtr_core_add_from_url(TrCore* core, char const* uri)
1345 {
1346 bool handled;
1347 bool const do_start = gtr_pref_flag_get(TR_KEY_start_added_torrents);
1348 bool const do_prompt = gtr_pref_flag_get(TR_KEY_show_options_window);
1349 bool const do_notify = false;
1350
1351 GFile* file = g_file_new_for_uri(uri);
1352 handled = add_file(core, file, do_start, do_prompt, do_notify);
1353 g_object_unref(file);
1354 gtr_core_torrents_added(core);
1355
1356 return handled;
1357 }
1358
gtr_core_add_files(TrCore * core,GSList * files,gboolean do_start,gboolean do_prompt,gboolean do_notify)1359 void gtr_core_add_files(TrCore* core, GSList* files, gboolean do_start, gboolean do_prompt, gboolean do_notify)
1360 {
1361 for (GSList* l = files; l != NULL; l = l->next)
1362 {
1363 add_file(core, l->data, do_start, do_prompt, do_notify);
1364 }
1365
1366 gtr_core_torrents_added(core);
1367 }
1368
gtr_core_torrents_added(TrCore * self)1369 void gtr_core_torrents_added(TrCore* self)
1370 {
1371 gtr_core_update(self);
1372 core_emit_err(self, TR_CORE_ERR_NO_MORE_TORRENTS, NULL);
1373 }
1374
gtr_core_torrent_changed(TrCore * self,int id)1375 void gtr_core_torrent_changed(TrCore* self, int id)
1376 {
1377 GtkTreeIter iter;
1378 GtkTreeModel* model = core_raw_model(self);
1379
1380 if (find_row_from_torrent_id(model, id, &iter))
1381 {
1382 GtkTreePath* path = gtk_tree_model_get_path(model, &iter);
1383 gtk_tree_model_row_changed(model, path, &iter);
1384 }
1385 }
1386
gtr_core_remove_torrent(TrCore * core,int id,gboolean delete_local_data)1387 void gtr_core_remove_torrent(TrCore* core, int id, gboolean delete_local_data)
1388 {
1389 tr_torrent* tor = gtr_core_find_torrent(core, id);
1390
1391 if (tor != NULL)
1392 {
1393 /* remove from the gui */
1394 GtkTreeIter iter;
1395 GtkTreeModel* model = core_raw_model(core);
1396
1397 if (find_row_from_torrent_id(model, id, &iter))
1398 {
1399 gtk_list_store_remove(GTK_LIST_STORE(model), &iter);
1400 }
1401
1402 /* remove the torrent */
1403 tr_torrentRemove(tor, delete_local_data, gtr_file_trash_or_remove);
1404 }
1405 }
1406
gtr_core_load(TrCore * self,gboolean forcePaused)1407 void gtr_core_load(TrCore* self, gboolean forcePaused)
1408 {
1409 tr_ctor* ctor;
1410 tr_torrent** torrents;
1411 int count = 0;
1412
1413 ctor = tr_ctorNew(gtr_core_session(self));
1414
1415 if (forcePaused)
1416 {
1417 tr_ctorSetPaused(ctor, TR_FORCE, TRUE);
1418 }
1419
1420 tr_ctorSetPeerLimit(ctor, TR_FALLBACK, gtr_pref_int_get(TR_KEY_peer_limit_per_torrent));
1421
1422 torrents = tr_sessionLoadTorrents(gtr_core_session(self), ctor, &count);
1423
1424 for (int i = 0; i < count; ++i)
1425 {
1426 gtr_core_add_torrent(self, torrents[i], FALSE);
1427 }
1428
1429 tr_free(torrents);
1430 tr_ctorFree(ctor);
1431 }
1432
gtr_core_clear(TrCore * self)1433 void gtr_core_clear(TrCore* self)
1434 {
1435 gtk_list_store_clear(GTK_LIST_STORE(core_raw_model(self)));
1436 }
1437
1438 /***
1439 ****
1440 ***/
1441
gtr_compare_double(double const a,double const b,int decimal_places)1442 static int gtr_compare_double(double const a, double const b, int decimal_places)
1443 {
1444 int ret;
1445 int64_t const ia = (int64_t)(a * pow(10, decimal_places));
1446 int64_t const ib = (int64_t)(b * pow(10, decimal_places));
1447
1448 if (ia < ib)
1449 {
1450 ret = -1;
1451 }
1452 else if (ia > ib)
1453 {
1454 ret = 1;
1455 }
1456 else
1457 {
1458 ret = 0;
1459 }
1460
1461 return ret;
1462 }
1463
update_foreach(GtkTreeModel * model,GtkTreeIter * iter)1464 static void update_foreach(GtkTreeModel* model, GtkTreeIter* iter)
1465 {
1466 int oldActivity;
1467 int newActivity;
1468 int oldActivePeerCount;
1469 int newActivePeerCount;
1470 int oldError;
1471 int newError;
1472 bool oldFinished;
1473 bool newFinished;
1474 int oldQueuePosition;
1475 int newQueuePosition;
1476 int oldDownloadPeerCount;
1477 int newDownloadPeerCount;
1478 int oldUploadPeerCount;
1479 int newUploadPeerCount;
1480 tr_priority_t oldPriority;
1481 tr_priority_t newPriority;
1482 unsigned int oldTrackers;
1483 unsigned int newTrackers;
1484 double oldUpSpeed;
1485 double newUpSpeed;
1486 double oldDownSpeed;
1487 double newDownSpeed;
1488 double oldRecheckProgress;
1489 double newRecheckProgress;
1490 gboolean oldActive;
1491 gboolean newActive;
1492 tr_stat const* st;
1493 tr_torrent* tor;
1494
1495 /* get the old states */
1496 gtk_tree_model_get(model, iter,
1497 MC_TORRENT, &tor,
1498 MC_ACTIVE, &oldActive,
1499 MC_ACTIVE_PEER_COUNT, &oldActivePeerCount,
1500 MC_ACTIVE_PEERS_UP, &oldUploadPeerCount,
1501 MC_ACTIVE_PEERS_DOWN, &oldDownloadPeerCount,
1502 MC_ERROR, &oldError,
1503 MC_ACTIVITY, &oldActivity,
1504 MC_FINISHED, &oldFinished,
1505 MC_PRIORITY, &oldPriority,
1506 MC_QUEUE_POSITION, &oldQueuePosition,
1507 MC_TRACKERS, &oldTrackers,
1508 MC_SPEED_UP, &oldUpSpeed,
1509 MC_RECHECK_PROGRESS, &oldRecheckProgress,
1510 MC_SPEED_DOWN, &oldDownSpeed,
1511 -1);
1512
1513 /* get the new states */
1514 st = tr_torrentStat(tor);
1515 newActive = is_torrent_active(st);
1516 newActivity = st->activity;
1517 newFinished = st->finished;
1518 newPriority = tr_torrentGetPriority(tor);
1519 newQueuePosition = st->queuePosition;
1520 newTrackers = build_torrent_trackers_hash(tor);
1521 newUpSpeed = st->pieceUploadSpeed_KBps;
1522 newDownSpeed = st->pieceDownloadSpeed_KBps;
1523 newRecheckProgress = st->recheckProgress;
1524 newActivePeerCount = st->peersSendingToUs + st->peersGettingFromUs + st->webseedsSendingToUs;
1525 newDownloadPeerCount = st->peersSendingToUs;
1526 newUploadPeerCount = st->peersGettingFromUs + st->webseedsSendingToUs;
1527 newError = st->error;
1528
1529 /* updating the model triggers off resort/refresh,
1530 so don't do it unless something's actually changed... */
1531 if (newActive != oldActive || newActivity != oldActivity || newFinished != oldFinished || newPriority != oldPriority ||
1532 newQueuePosition != oldQueuePosition || newError != oldError || newActivePeerCount != oldActivePeerCount ||
1533 newDownloadPeerCount != oldDownloadPeerCount || newUploadPeerCount != oldUploadPeerCount ||
1534 newTrackers != oldTrackers || gtr_compare_double(newUpSpeed, oldUpSpeed, 2) != 0 ||
1535 gtr_compare_double(newDownSpeed, oldDownSpeed, 2) != 0 ||
1536 gtr_compare_double(newRecheckProgress, oldRecheckProgress, 2) != 0)
1537 {
1538 gtk_list_store_set(GTK_LIST_STORE(model), iter,
1539 MC_ACTIVE, newActive,
1540 MC_ACTIVE_PEER_COUNT, newActivePeerCount,
1541 MC_ACTIVE_PEERS_UP, newUploadPeerCount,
1542 MC_ACTIVE_PEERS_DOWN, newDownloadPeerCount,
1543 MC_ERROR, newError,
1544 MC_ACTIVITY, newActivity,
1545 MC_FINISHED, newFinished,
1546 MC_PRIORITY, newPriority,
1547 MC_QUEUE_POSITION, newQueuePosition,
1548 MC_TRACKERS, newTrackers,
1549 MC_SPEED_UP, newUpSpeed,
1550 MC_SPEED_DOWN, newDownSpeed,
1551 MC_RECHECK_PROGRESS, newRecheckProgress,
1552 -1);
1553 }
1554 }
1555
gtr_core_update(TrCore * core)1556 void gtr_core_update(TrCore* core)
1557 {
1558 GtkTreeIter iter;
1559 GtkTreeModel* model;
1560
1561 /* update the model */
1562 model = core_raw_model(core);
1563
1564 if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
1565 {
1566 do
1567 {
1568 update_foreach(model, &iter);
1569 }
1570 while (gtk_tree_model_iter_next(model, &iter));
1571 }
1572
1573 /* update hibernation */
1574 core_maybe_inhibit_hibernation(core);
1575 }
1576
1577 /**
1578 *** Hibernate
1579 **/
1580
1581 #define SESSION_MANAGER_SERVICE_NAME "org.gnome.SessionManager"
1582 #define SESSION_MANAGER_INTERFACE "org.gnome.SessionManager"
1583 #define SESSION_MANAGER_OBJECT_PATH "/org/gnome/SessionManager"
1584
gtr_inhibit_hibernation(guint * cookie)1585 static gboolean gtr_inhibit_hibernation(guint* cookie)
1586 {
1587 gboolean success;
1588 GVariant* response;
1589 GDBusConnection* connection;
1590 GError* err = NULL;
1591 char const* application = "Transmission BitTorrent Client";
1592 char const* reason = "BitTorrent Activity";
1593 int const toplevel_xid = 0;
1594 int const flags = 4; /* Inhibit suspending the session or computer */
1595
1596 connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &err);
1597
1598 response = g_dbus_connection_call_sync(connection, SESSION_MANAGER_SERVICE_NAME, SESSION_MANAGER_OBJECT_PATH,
1599 SESSION_MANAGER_INTERFACE, "Inhibit", g_variant_new("(susu)", application, toplevel_xid, reason, flags), NULL,
1600 G_DBUS_CALL_FLAGS_NONE, 1000, NULL, &err);
1601
1602 if (response != NULL)
1603 {
1604 *cookie = g_variant_get_uint32(g_variant_get_child_value(response, 0));
1605 }
1606
1607 success = response != NULL && err == NULL;
1608
1609 /* logging */
1610 if (success)
1611 {
1612 tr_logAddInfo("%s", _("Inhibiting desktop hibernation"));
1613 }
1614 else
1615 {
1616 tr_logAddError(_("Couldn't inhibit desktop hibernation: %s"), err->message);
1617 g_error_free(err);
1618 }
1619
1620 /* cleanup */
1621 if (response != NULL)
1622 {
1623 g_variant_unref(response);
1624 }
1625
1626 if (connection != NULL)
1627 {
1628 g_object_unref(connection);
1629 }
1630
1631 return success;
1632 }
1633
gtr_uninhibit_hibernation(guint inhibit_cookie)1634 static void gtr_uninhibit_hibernation(guint inhibit_cookie)
1635 {
1636 GVariant* response;
1637 GDBusConnection* connection;
1638 GError* err = NULL;
1639
1640 connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &err);
1641
1642 response = g_dbus_connection_call_sync(connection, SESSION_MANAGER_SERVICE_NAME, SESSION_MANAGER_OBJECT_PATH,
1643 SESSION_MANAGER_INTERFACE, "Uninhibit", g_variant_new("(u)", inhibit_cookie), NULL, G_DBUS_CALL_FLAGS_NONE, 1000, NULL,
1644 &err);
1645
1646 /* logging */
1647 if (err == NULL)
1648 {
1649 tr_logAddInfo("%s", _("Allowing desktop hibernation"));
1650 }
1651 else
1652 {
1653 g_warning("Couldn't uninhibit desktop hibernation: %s.", err->message);
1654 g_error_free(err);
1655 }
1656
1657 /* cleanup */
1658 g_variant_unref(response);
1659 g_object_unref(connection);
1660 }
1661
gtr_core_set_hibernation_allowed(TrCore * core,gboolean allowed)1662 static void gtr_core_set_hibernation_allowed(TrCore* core, gboolean allowed)
1663 {
1664 g_return_if_fail(core);
1665 g_return_if_fail(core->priv);
1666
1667 core->priv->inhibit_allowed = allowed != 0;
1668
1669 if (allowed && core->priv->have_inhibit_cookie)
1670 {
1671 gtr_uninhibit_hibernation(core->priv->inhibit_cookie);
1672 core->priv->have_inhibit_cookie = FALSE;
1673 }
1674
1675 if (!allowed && !core->priv->have_inhibit_cookie && !core->priv->dbus_error)
1676 {
1677 if (gtr_inhibit_hibernation(&core->priv->inhibit_cookie))
1678 {
1679 core->priv->have_inhibit_cookie = TRUE;
1680 }
1681 else
1682 {
1683 core->priv->dbus_error = TRUE;
1684 }
1685 }
1686 }
1687
core_maybe_inhibit_hibernation(TrCore * core)1688 static void core_maybe_inhibit_hibernation(TrCore* core)
1689 {
1690 /* hibernation is allowed if EITHER
1691 * (a) the "inhibit" pref is turned off OR
1692 * (b) there aren't any active torrents */
1693 gboolean const hibernation_allowed = !gtr_pref_flag_get(TR_KEY_inhibit_desktop_hibernation) ||
1694 !gtr_core_get_active_torrent_count(core);
1695 gtr_core_set_hibernation_allowed(core, hibernation_allowed);
1696 }
1697
1698 /**
1699 *** Prefs
1700 **/
1701
core_commit_prefs_change(TrCore * core,tr_quark const key)1702 static void core_commit_prefs_change(TrCore* core, tr_quark const key)
1703 {
1704 gtr_core_pref_changed(core, key);
1705 gtr_pref_save(gtr_core_session(core));
1706 }
1707
gtr_core_set_pref(TrCore * self,tr_quark const key,char const * newval)1708 void gtr_core_set_pref(TrCore* self, tr_quark const key, char const* newval)
1709 {
1710 if (g_strcmp0(newval, gtr_pref_string_get(key)) != 0)
1711 {
1712 gtr_pref_string_set(key, newval);
1713 core_commit_prefs_change(self, key);
1714 }
1715 }
1716
gtr_core_set_pref_bool(TrCore * self,tr_quark const key,gboolean newval)1717 void gtr_core_set_pref_bool(TrCore* self, tr_quark const key, gboolean newval)
1718 {
1719 if (newval != gtr_pref_flag_get(key))
1720 {
1721 gtr_pref_flag_set(key, newval);
1722 core_commit_prefs_change(self, key);
1723 }
1724 }
1725
gtr_core_set_pref_int(TrCore * self,tr_quark const key,int newval)1726 void gtr_core_set_pref_int(TrCore* self, tr_quark const key, int newval)
1727 {
1728 if (newval != gtr_pref_int_get(key))
1729 {
1730 gtr_pref_int_set(key, newval);
1731 core_commit_prefs_change(self, key);
1732 }
1733 }
1734
gtr_core_set_pref_double(TrCore * self,tr_quark const key,double newval)1735 void gtr_core_set_pref_double(TrCore* self, tr_quark const key, double newval)
1736 {
1737 if (gtr_compare_double(newval, gtr_pref_double_get(key), 4))
1738 {
1739 gtr_pref_double_set(key, newval);
1740 core_commit_prefs_change(self, key);
1741 }
1742 }
1743
1744 /***
1745 ****
1746 **** RPC Interface
1747 ****
1748 ***/
1749
1750 /* #define DEBUG_RPC */
1751
1752 static int nextTag = 1;
1753
1754 typedef void (* server_response_func)(TrCore* core, tr_variant* response, gpointer user_data);
1755
1756 struct pending_request_data
1757 {
1758 TrCore* core;
1759 server_response_func response_func;
1760 gpointer response_func_user_data;
1761 };
1762
1763 static GHashTable* pendingRequests = NULL;
1764
core_read_rpc_response_idle(void * vresponse)1765 static gboolean core_read_rpc_response_idle(void* vresponse)
1766 {
1767 int64_t intVal;
1768 tr_variant* response = vresponse;
1769
1770 if (tr_variantDictFindInt(response, TR_KEY_tag, &intVal))
1771 {
1772 int const tag = (int)intVal;
1773 struct pending_request_data* data = g_hash_table_lookup(pendingRequests, &tag);
1774
1775 if (data != NULL)
1776 {
1777 if (data->response_func != NULL)
1778 {
1779 (*data->response_func)(data->core, response, data->response_func_user_data);
1780 }
1781
1782 g_hash_table_remove(pendingRequests, &tag);
1783 }
1784 }
1785
1786 tr_variantFree(response);
1787 tr_free(response);
1788 return G_SOURCE_REMOVE;
1789 }
1790
core_read_rpc_response(tr_session * session UNUSED,tr_variant * response,void * unused UNUSED)1791 static void core_read_rpc_response(tr_session* session UNUSED, tr_variant* response, void* unused UNUSED)
1792 {
1793 tr_variant* response_copy = tr_new(tr_variant, 1);
1794
1795 *response_copy = *response;
1796 tr_variantInitBool(response, false);
1797
1798 gdk_threads_add_idle(core_read_rpc_response_idle, response_copy);
1799 }
1800
core_send_rpc_request(TrCore * core,tr_variant const * request,int tag,server_response_func response_func,void * response_func_user_data)1801 static void core_send_rpc_request(TrCore* core, tr_variant const* request, int tag, server_response_func response_func,
1802 void* response_func_user_data)
1803 {
1804 tr_session* session = gtr_core_session(core);
1805
1806 if (pendingRequests == NULL)
1807 {
1808 pendingRequests = g_hash_table_new_full(g_int_hash, g_int_equal, g_free, g_free);
1809 }
1810
1811 if (session == NULL)
1812 {
1813 g_error("GTK+ client doesn't support connections to remote servers yet.");
1814 }
1815 else
1816 {
1817 /* remember this request */
1818 struct pending_request_data* data;
1819 data = g_new0(struct pending_request_data, 1);
1820 data->core = core;
1821 data->response_func = response_func;
1822 data->response_func_user_data = response_func_user_data;
1823 g_hash_table_insert(pendingRequests, g_memdup(&tag, sizeof(int)), data);
1824
1825 /* make the request */
1826 #ifdef DEBUG_RPC
1827 {
1828 struct evbuffer* buf = tr_variantToBuf(request, TR_VARIANT_FMT_JSON_LEAN);
1829 size_t const buf_len = evbuffer_get_length(buf);
1830 g_message("request: [%*.*s]", (int)buf_len, (int)buf_len, evbuffer_pullup(buf, -1));
1831 evbuffer_free(buf);
1832 }
1833 #endif
1834
1835 tr_rpc_request_exec_json(session, request, core_read_rpc_response, GINT_TO_POINTER(tag));
1836 }
1837 }
1838
1839 /***
1840 **** Sending a test-port request via RPC
1841 ***/
1842
on_port_test_response(TrCore * core,tr_variant * response,gpointer u UNUSED)1843 static void on_port_test_response(TrCore* core, tr_variant* response, gpointer u UNUSED)
1844 {
1845 tr_variant* args;
1846 bool is_open;
1847
1848 if (!tr_variantDictFindDict(response, TR_KEY_arguments, &args) ||
1849 !tr_variantDictFindBool(args, TR_KEY_port_is_open, &is_open))
1850 {
1851 is_open = false;
1852 }
1853
1854 core_emit_port_tested(core, is_open);
1855 }
1856
gtr_core_port_test(TrCore * core)1857 void gtr_core_port_test(TrCore* core)
1858 {
1859 int const tag = nextTag;
1860 ++nextTag;
1861
1862 tr_variant request;
1863 tr_variantInitDict(&request, 2);
1864 tr_variantDictAddStr(&request, TR_KEY_method, "port-test");
1865 tr_variantDictAddInt(&request, TR_KEY_tag, tag);
1866 core_send_rpc_request(core, &request, tag, on_port_test_response, NULL);
1867 tr_variantFree(&request);
1868 }
1869
1870 /***
1871 **** Updating a blocklist via RPC
1872 ***/
1873
on_blocklist_response(TrCore * core,tr_variant * response,gpointer data UNUSED)1874 static void on_blocklist_response(TrCore* core, tr_variant* response, gpointer data UNUSED)
1875 {
1876 tr_variant* args;
1877 int64_t ruleCount;
1878
1879 if (!tr_variantDictFindDict(response, TR_KEY_arguments, &args) ||
1880 !tr_variantDictFindInt(args, TR_KEY_blocklist_size, &ruleCount))
1881 {
1882 ruleCount = -1;
1883 }
1884
1885 if (ruleCount > 0)
1886 {
1887 gtr_pref_int_set(TR_KEY_blocklist_date, tr_time());
1888 }
1889
1890 core_emit_blocklist_udpated(core, ruleCount);
1891 }
1892
gtr_core_blocklist_update(TrCore * core)1893 void gtr_core_blocklist_update(TrCore* core)
1894 {
1895 int const tag = nextTag;
1896 ++nextTag;
1897
1898 tr_variant request;
1899 tr_variantInitDict(&request, 2);
1900 tr_variantDictAddStr(&request, TR_KEY_method, "blocklist-update");
1901 tr_variantDictAddInt(&request, TR_KEY_tag, tag);
1902 core_send_rpc_request(core, &request, tag, on_blocklist_response, NULL);
1903 tr_variantFree(&request);
1904 }
1905
1906 /***
1907 ****
1908 ***/
1909
gtr_core_exec(TrCore * core,tr_variant const * top)1910 void gtr_core_exec(TrCore* core, tr_variant const* top)
1911 {
1912 int const tag = nextTag;
1913 ++nextTag;
1914
1915 core_send_rpc_request(core, top, tag, NULL, NULL);
1916 }
1917
1918 /***
1919 ****
1920 ***/
1921
gtr_core_get_torrent_count(TrCore * core)1922 size_t gtr_core_get_torrent_count(TrCore* core)
1923 {
1924 return gtk_tree_model_iter_n_children(core_raw_model(core), NULL);
1925 }
1926
gtr_core_get_active_torrent_count(TrCore * core)1927 size_t gtr_core_get_active_torrent_count(TrCore* core)
1928 {
1929 GtkTreeIter iter;
1930 size_t activeCount = 0;
1931 GtkTreeModel* model = core_raw_model(core);
1932
1933 if (gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
1934 {
1935 do
1936 {
1937 int activity;
1938 gtk_tree_model_get(model, &iter, MC_ACTIVITY, &activity, -1);
1939
1940 if (activity != TR_STATUS_STOPPED)
1941 {
1942 ++activeCount;
1943 }
1944 }
1945 while (gtk_tree_model_iter_next(model, &iter));
1946 }
1947
1948 return activeCount;
1949 }
1950
gtr_core_find_torrent(TrCore * core,int id)1951 tr_torrent* gtr_core_find_torrent(TrCore* core, int id)
1952 {
1953 tr_session* session;
1954 tr_torrent* tor = NULL;
1955
1956 if ((session = gtr_core_session(core)) != NULL)
1957 {
1958 tor = tr_torrentFindFromId(session, id);
1959 }
1960
1961 return tor;
1962 }
1963
gtr_core_open_folder(TrCore * core,int torrent_id)1964 void gtr_core_open_folder(TrCore* core, int torrent_id)
1965 {
1966 tr_torrent const* tor = gtr_core_find_torrent(core, torrent_id);
1967
1968 if (tor != NULL)
1969 {
1970 gboolean const single = tr_torrentInfo(tor)->fileCount == 1;
1971 char const* currentDir = tr_torrentGetCurrentDir(tor);
1972
1973 if (single)
1974 {
1975 gtr_open_file(currentDir);
1976 }
1977 else
1978 {
1979 char* path = g_build_filename(currentDir, tr_torrentName(tor), NULL);
1980 gtr_open_file(path);
1981 g_free(path);
1982 }
1983 }
1984 }
1985