transmissionrpc / transmissionrpc / torrent.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
# -*- coding: utf-8 -*-
# Copyright (c) 2008-2013 Erik Svensson <erik.public@gmail.com>
# Licensed under the MIT license.

import sys, datetime

from transmissionrpc.constants import PRIORITY, RATIO_LIMIT, IDLE_LIMIT
from transmissionrpc.utils import Field, format_timedelta

from six import integer_types, string_types, text_type, iteritems


def get_status_old(code):
    """Get the torrent status using old status codes"""
    mapping = {
        (1<<0): 'check pending',
        (1<<1): 'checking',
        (1<<2): 'downloading',
        (1<<3): 'seeding',
        (1<<4): 'stopped',
    }
    return mapping[code]

def get_status_new(code):
    """Get the torrent status using new status codes"""
    mapping = {
        0: 'stopped',
        1: 'check pending',
        2: 'checking',
        3: 'download pending',
        4: 'downloading',
        5: 'seed pending',
        6: 'seeding',
    }
    return mapping[code]

class Torrent(object):
    """
    Torrent is a class holding the data received from Transmission regarding a bittorrent transfer.

    All fetched torrent fields are accessible through this class using attributes.
    This class has a few convenience properties using the torrent data.
    """

    def __init__(self, client, fields):
        if 'id' not in fields:
            raise ValueError('Torrent requires an id')
        self._fields = {}
        self._update_fields(fields)
        self._incoming_pending = False
        self._outgoing_pending = False
        self._client = client

    def _get_name_string(self, codec=None):
        """Get the name"""
        if codec is None:
            codec = sys.getdefaultencoding()
        name = None
        # try to find name
        if 'name' in self._fields:
            name = self._fields['name'].value
        # if name is unicode, try to decode
        if isinstance(name, text_type):
            try:
                name = name.encode(codec)
            except UnicodeError:
                name = None
        return name

    def __repr__(self):
        tid = self._fields['id'].value
        name = self._get_name_string()
        if isinstance(name, str):
            return '<Torrent %d \"%s\">' % (tid, name)
        else:
            return '<Torrent %d>' % (tid)

    def __str__(self):
        name = self._get_name_string()
        if isinstance(name, str):
            return 'Torrent \"%s\"' % (name)
        else:
            return 'Torrent'

    def __copy__(self):
        return Torrent(self._client, self._fields)

    def __getattr__(self, name):
        try:
            return self._fields[name].value
        except KeyError:
            raise AttributeError('No attribute %s' % name)

    def _rpc_version(self):
        """Get the Transmission RPC API version."""
        if self._client:
            return self._client.rpc_version
        return 2

    def _dirty_fields(self):
        """Enumerate changed fields"""
        outgoing_keys = ['bandwidthPriority', 'downloadLimit', 'downloadLimited', 'peer_limit', 'queuePosition'
            , 'seedIdleLimit', 'seedIdleMode', 'seedRatioLimit', 'seedRatioMode', 'uploadLimit', 'uploadLimited']
        fields = []
        for key in outgoing_keys:
            if key in self._fields and self._fields[key].dirty:
                fields.append(key)
        return fields

    def _push(self):
        """Push changed fields to the server"""
        dirty = self._dirty_fields()
        args = {}
        for key in dirty:
            args[key] = self._fields[key].value
            self._fields[key] = self._fields[key]._replace(dirty=False)
        if len(args) > 0:
            self._client.change_torrent(self.id, **args)

    def _update_fields(self, other):
        """
        Update the torrent data from a Transmission JSON-RPC arguments dictionary
        """
        fields = None
        if isinstance(other, dict):
            for key, value in iteritems(other):
                self._fields[key.replace('-', '_')] = Field(value, False)
        elif isinstance(other, Torrent):
            for key in list(other._fields.keys()):
                self._fields[key] = Field(other._fields[key].value, False)
        else:
            raise ValueError('Cannot update with supplied data')
        self._incoming_pending = False
    
    def _status(self):
        """Get the torrent status"""
        code = self._fields['status'].value
        if self._rpc_version() >= 14:
            return get_status_new(code)
        else:
            return get_status_old(code)

    def files(self):
        """
        Get list of files for this torrent.

        This function returns a dictionary with file information for each file.
        The file information is has following fields:
        ::

            {
                <file id>: {
                    'name': <file name>,
                    'size': <file size in bytes>,
                    'completed': <bytes completed>,
                    'priority': <priority ('high'|'normal'|'low')>,
                    'selected': <selected for download>
                }
                ...
            }
        """
        result = {}
        if 'files' in self._fields:
            files = self._fields['files'].value
            indices = range(len(files))
            priorities = self._fields['priorities'].value
            wanted = self._fields['wanted'].value
            for item in zip(indices, files, priorities, wanted):
                selected = True if item[3] else False
                priority = PRIORITY[item[2]]
                result[item[0]] = {
                    'selected': selected,
                    'priority': priority,
                    'size': item[1]['length'],
                    'name': item[1]['name'],
                    'completed': item[1]['bytesCompleted']}
        return result

    @property
    def status(self):
        """
        Returns the torrent status. Is either one of 'check pending', 'checking',
        'downloading', 'seeding' or 'stopped'. The first two is related to
        verification.
        """
        return self._status()

    @property
    def progress(self):
        """Get the download progress in percent."""
        try:
            size = self._fields['sizeWhenDone'].value
            left = self._fields['leftUntilDone'].value
            return 100.0 * (size - left) / float(size)
        except ZeroDivisionError:
            return 0.0

    @property
    def ratio(self):
        """Get the upload/download ratio."""
        return float(self._fields['uploadRatio'].value)

    @property
    def eta(self):
        """Get the "eta" as datetime.timedelta."""
        eta = self._fields['eta'].value
        if eta >= 0:
            return datetime.timedelta(seconds=eta)
        else:
            ValueError('eta not valid')

    @property
    def date_active(self):
        """Get the attribute "activityDate" as datetime.datetime."""
        return datetime.datetime.fromtimestamp(self._fields['activityDate'].value)

    @property
    def date_added(self):
        """Get the attribute "addedDate" as datetime.datetime."""
        return datetime.datetime.fromtimestamp(self._fields['addedDate'].value)

    @property
    def date_started(self):
        """Get the attribute "startDate" as datetime.datetime."""
        return datetime.datetime.fromtimestamp(self._fields['startDate'].value)

    @property
    def date_done(self):
        """Get the attribute "doneDate" as datetime.datetime."""
        return datetime.datetime.fromtimestamp(self._fields['doneDate'].value)

    def format_eta(self):
        """
        Returns the attribute *eta* formatted as a string.

        * If eta is -1 the result is 'not available'
        * If eta is -2 the result is 'unknown'
        * Otherwise eta is formatted as <days> <hours>:<minutes>:<seconds>.
        """
        eta = self._fields['eta'].value
        if eta == -1:
            return 'not available'
        elif eta == -2:
            return 'unknown'
        else:
            return format_timedelta(self.eta)

    def _get_download_limit(self):
        """
        Get the download limit.
        Can be a number or None.
        """
        if self._fields['downloadLimited'].value:
            return self._fields['downloadLimit'].value
        else:
            return None

    def _set_download_limit(self, limit):
        """
        Get the download limit.
        Can be a number, 'session' or None.
        """
        if isinstance(limit, integer_types):
            self._fields['downloadLimited'] = Field(True, True)
            self._fields['downloadLimit'] = Field(limit, True)
            self._push()
        elif limit == None:
            self._fields['downloadLimited'] = Field(False, True)
            self._push()
        else:
            raise ValueError("Not a valid limit")

    download_limit = property(_get_download_limit, _set_download_limit, None, "Download limit in Kbps or None. This is a mutator.")

    def _get_peer_limit(self):
        """
        Get the peer limit.
        """
        return self._fields['peer_limit'].value

    def _set_peer_limit(self, limit):
        """
        Set the peer limit.
        """
        if isinstance(limit, integer_types):
            self._fields['peer_limit'] = Field(limit, True)
            self._push()
        else:
            raise ValueError("Not a valid limit")

    peer_limit = property(_get_peer_limit, _set_peer_limit, None, "Peer limit. This is a mutator.")

    def _get_priority(self):
        """
        Get the priority as string.
        Can be one of 'low', 'normal', 'high'.
        """
        return PRIORITY[self._fields['bandwidthPriority'].value]

    def _set_priority(self, priority):
        """
        Set the priority as string.
        Can be one of 'low', 'normal', 'high'.
        """
        if isinstance(priority, string_types):
            self._fields['bandwidthPriority'] = Field(PRIORITY[priority], True)
            self._push()

    priority = property(_get_priority, _set_priority, None
        , "Bandwidth priority as string. Can be one of 'low', 'normal', 'high'. This is a mutator.")

    def _get_seed_idle_limit(self):
        """
        Get the seed idle limit in minutes.
        """
        return self._fields['seedIdleLimit'].value

    def _set_seed_idle_limit(self, limit):
        """
        Set the seed idle limit in minutes.
        """
        if isinstance(limit, integer_types):
            self._fields['seedIdleLimit'] = Field(limit, True)
            self._push()
        else:
            raise ValueError("Not a valid limit")

    seed_idle_limit = property(_get_seed_idle_limit, _set_seed_idle_limit, None
        , "Torrent seed idle limit in minutes. Also see seed_idle_mode. This is a mutator.")

    def _get_seed_idle_mode(self):
        """
        Get the seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'.
        """
        return IDLE_LIMIT[self._fields['seedIdleMode'].value]

    def _set_seed_idle_mode(self, mode):
        """
        Set the seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'.
        """
        if isinstance(mode, str):
            self._fields['seedIdleMode'] = Field(IDLE_LIMIT[mode], True)
            self._push()
        else:
            raise ValueError("Not a valid limit")

    seed_idle_mode = property(_get_seed_idle_mode, _set_seed_idle_mode, None,
        """
        Seed idle mode as string. Can be one of 'global', 'single' or 'unlimited'.

         * global, use session seed idle limit.
         * single, use torrent seed idle limit. See seed_idle_limit.
         * unlimited, no seed idle limit.

        This is a mutator.
        """
    )

    def _get_seed_ratio_limit(self):
        """
        Get the seed ratio limit as float.
        """
        return float(self._fields['seedRatioLimit'].value)

    def _set_seed_ratio_limit(self, limit):
        """
        Set the seed ratio limit as float.
        """
        if isinstance(limit, (integer_types, float)) and limit >= 0.0:
            self._fields['seedRatioLimit'] = Field(float(limit), True)
            self._push()
        else:
            raise ValueError("Not a valid limit")

    seed_ratio_limit = property(_get_seed_ratio_limit, _set_seed_ratio_limit, None
        , "Torrent seed ratio limit as float. Also see seed_ratio_mode. This is a mutator.")

    def _get_seed_ratio_mode(self):
        """
        Get the seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'.
        """
        return RATIO_LIMIT[self._fields['seedRatioMode'].value]

    def _set_seed_ratio_mode(self, mode):
        """
        Set the seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'.
        """
        if isinstance(mode, str):
            self._fields['seedRatioMode'] = Field(RATIO_LIMIT[mode], True)
            self._push()
        else:
            raise ValueError("Not a valid limit")

    seed_ratio_mode = property(_get_seed_ratio_mode, _set_seed_ratio_mode, None,
        """
        Seed ratio mode as string. Can be one of 'global', 'single' or 'unlimited'.

         * global, use session seed ratio limit.
         * single, use torrent seed ratio limit. See seed_ratio_limit.
         * unlimited, no seed ratio limit.

        This is a mutator.
        """
    )

    def _get_upload_limit(self):
        """
        Get the upload limit.
        Can be a number or None.
        """
        if self._fields['uploadLimited'].value:
            return self._fields['uploadLimit'].value
        else:
            return None

    def _set_upload_limit(self, limit):
        """
        Set the upload limit.
        Can be a number, 'session' or None.
        """
        if isinstance(limit, integer_types):
            self._fields['uploadLimited'] = Field(True, True)
            self._fields['uploadLimit'] = Field(limit, True)
            self._push()
        elif limit == None:
            self._fields['uploadLimited'] = Field(False, True)
            self._push()
        else:
            raise ValueError("Not a valid limit")

    upload_limit = property(_get_upload_limit, _set_upload_limit, None, "Upload limit in Kbps or None. This is a mutator.")

    def _get_queue_position(self):
        """Get the queue position for this torrent."""
        if self._rpc_version() >= 14:
            return self._fields['queuePosition'].value
        else:
            return 0

    def _set_queue_position(self, position):
        """Set the queue position for this torrent."""
        if self._rpc_version() >= 14:
            if isinstance(position, integer_types):
                self._fields['queuePosition'] = Field(position, True)
                self._push()
            else:
                raise ValueError("Not a valid position")
        else:
            pass

    queue_position = property(_get_queue_position, _set_queue_position, None, "Queue position")

    def update(self, timeout=None):
        """Update the torrent information."""
        self._push()
        torrent = self._client.get_torrent(self.id, timeout=timeout)
        self._update_fields(torrent)

    def start(self, bypass_queue=False, timeout=None):
        """
        Start the torrent.
        """
        self._incoming_pending = True
        self._client.start_torrent(self.id, bypass_queue=bypass_queue, timeout=timeout)

    def stop(self, timeout=None):
        """Stop the torrent."""
        self._incoming_pending = True
        self._client.stop_torrent(self.id, timeout=timeout)

    def move_data(self, location, timeout=None):
        """Move torrent data to location."""
        self._incoming_pending = True
        self._client.move_torrent_data(self.id, location, timeout=timeout)

    def locate_data(self, location, timeout=None):
        """Locate torrent data at location."""
        self._incoming_pending = True
        self._client.locate_torrent_data(self.id, location, timeout=timeout)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.