deluge/deluge/ui/common.py
2018-09-14 16:06:06 +01:00

557 lines
18 KiB
Python

# -*- coding: utf-8 -*-
#
# Copyright (C) Damien Churchill 2008-2009 <damoxc@gmail.com>
# Copyright (C) Andrew Resch 2009 <andrewresch@gmail.com>
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
"""
The ui common module contains methods and classes that are deemed useful for all the interfaces.
"""
from __future__ import unicode_literals
import logging
import os
from hashlib import sha1 as sha
from deluge import bencode
from deluge.common import decode_bytes
log = logging.getLogger(__name__)
# Dummy translation dicts so the text is available for Translators.
#
# All entries in deluge.common.TORRENT_STATE should be added here.
#
# No need to import these, just simply use the `_()` function around a status variable.
def _(message):
return message
STATE_TRANSLATION = {
'All': _('All'),
'Active': _('Active'),
'Allocating': _('Allocating'),
'Checking': _('Checking'),
'Downloading': _('Downloading'),
'Seeding': _('Seeding'),
'Paused': _('Paused'),
'Queued': _('Queued'),
'Error': _('Error'),
}
TORRENT_DATA_FIELD = {
'queue':
{'name': '#', 'status': ['queue']},
'name':
{'name': _('Name'), 'status': ['state', 'name']},
'progress_state':
{'name': _('Progress'), 'status': ['progress', 'state']},
'state':
{'name': _('State'), 'status': ['state']},
'progress':
{'name': _('Progress'), 'status': ['progress']},
'size':
{'name': _('Size'), 'status': ['total_wanted']},
'downloaded':
{'name': _('Downloaded'), 'status': ['all_time_download']},
'uploaded':
{'name': _('Uploaded'), 'status': ['total_uploaded']},
'remaining':
{'name': _('Remaining'), 'status': ['total_remaining']},
'ratio':
{'name': _('Ratio'), 'status': ['ratio']},
'download_speed':
{'name': _('Down Speed'), 'status': ['download_payload_rate']},
'upload_speed':
{'name': _('Up Speed'), 'status': ['upload_payload_rate']},
'max_download_speed':
{'name': _('Down Limit'), 'status': ['max_download_speed']},
'max_upload_speed':
{'name': _('Up Limit'), 'status': ['max_upload_speed']},
'max_connections':
{'name': _('Max Connections'), 'status': ['max_connections']},
'max_upload_slots':
{'name': _('Max Upload Slots'), 'status': ['max_upload_slots']},
'peers':
{'name': _('Peers'), 'status': ['num_peers', 'total_peers']},
'seeds':
{'name': _('Seeds'), 'status': ['num_seeds', 'total_seeds']},
'avail':
{'name': _('Avail'), 'status': ['distributed_copies']},
'seeds_peers_ratio':
{'name': _('Seeds:Peers'), 'status': ['seeds_peers_ratio']},
'time_added':
{'name': _('Added'), 'status': ['time_added']},
'tracker':
{'name': _('Tracker'), 'status': ['tracker_host']},
'download_location':
{'name': _('Download Folder'), 'status': ['download_location']},
'seeding_time':
{'name': _('Seeding Time'), 'status': ['seeding_time']},
'active_time':
{'name': _('Active Time'), 'status': ['active_time']},
'time_since_transfer':
{'name': _('Last Activity'), 'status': ['time_since_transfer']},
'finished_time':
{'name': _('Finished Time'), 'status': ['finished_time']},
'last_seen_complete':
{'name': _('Complete Seen'), 'status': ['last_seen_complete']},
'completed_time':
{'name': _('Completed'), 'status': ['completed_time']},
'eta':
{'name': _('ETA'), 'status': ['eta']},
'shared':
{'name': _('Shared'), 'status': ['shared']},
'prioritize_first_last':
{'name': _('Prioritize First/Last'), 'status': ['prioritize_first_last']},
'sequential_download':
{'name': _('Sequential Download'), 'status': ['sequential_download']},
'is_auto_managed':
{'name': _('Auto Managed'), 'status': ['is_auto_managed']},
'auto_managed':
{'name': _('Auto Managed'), 'status': ['auto_managed']},
'stop_at_ratio':
{'name': _('Stop At Ratio'), 'status': ['stop_at_ratio']},
'stop_ratio':
{'name': _('Stop Ratio'), 'status': ['stop_ratio']},
'remove_at_ratio':
{'name': _('Remove At Ratio'), 'status': ['remove_at_ratio']},
'move_completed':
{'name': _('Move On Completed'), 'status': ['move_completed']},
'move_completed_path':
{'name': _('Move Completed Path'), 'status': ['move_completed_path']},
'move_on_completed':
{'name': _('Move On Completed'), 'status': ['move_on_completed']},
'move_on_completed_path':
{'name': _('Move On Completed Path'), 'status': ['move_on_completed_path']},
'owner':
{'name': _('Owner'), 'status': ['owner']},
'pieces':
{'name': _('Pieces'), 'status': ['num_pieces', 'piece_length']},
'seed_rank':
{'name': _('Seed Rank'), 'status': ['seed_rank']},
}
TRACKER_STATUS_TRANSLATION = [
_('Error'),
_('Warning'),
_('Announce OK'),
_('Announce Sent'),
]
PREFS_CATOG_TRANS = {
'interface': _('Interface'),
'downloads': _('Downloads'),
'bandwidth': _('Bandwidth'),
'queue': _('Queue'),
'network': _('Network'),
'proxy': _('Proxy'),
'cache': _('Cache'),
'other': _('Other'),
'daemon': _('Daemon'),
'plugins': _('Plugins'),
}
FILE_PRIORITY = {
0: 'Ignore',
1: 'Low',
2: 'Low',
3: 'Low',
4: 'Normal',
5: 'High',
6: 'High',
7: 'High',
_('Ignore'): 0,
_('Low'): 1,
_('Normal'): 4,
_('High'): 7,
}
del _
# The keys from session statistics for cache status.
DISK_CACHE_KEYS = [
'disk.num_blocks_read', 'disk.num_blocks_written', 'disk.num_read_ops', 'disk.num_write_ops',
'disk.num_blocks_cache_hits', 'read_hit_ratio', 'write_hit_ratio', 'disk.disk_blocks_in_use',
'disk.read_cache_blocks',
]
class TorrentInfo(object):
"""Collects information about a torrent file.
Args:
filename (str): The path to the .torrent file.
filetree (int, optional): The version of filetree to create (defaults to 1).
metainfo (bytes, optional): A bencoded filedump from a .torrent file.
metadata (bytes, optional): A bencoded metadata info_dict.
"""
def __init__(self, filename='', filetree=1, metainfo=None, metadata=None):
# Get the torrent metainfo from the torrent file
if metadata:
self._metainfo_dict = {b'info': bencode.bdecode(metadata)}
self._metainfo = bencode.bencode(self._metainfo_dict)
else:
self._metainfo = metainfo
if filename and not self._metainfo:
log.debug('Attempting to open %s.', filename)
try:
with open(filename, 'rb') as _file:
self._metainfo = _file.read()
except IOError as ex:
log.warning('Unable to open %s: %s', filename, ex)
return
try:
self._metainfo_dict = bencode.bdecode(self._metainfo)
except bencode.BTFailure as ex:
log.warning('Failed to decode %s: %s', filename, ex)
return
info_dict = self._metainfo_dict[b'info']
self._info_hash = sha(bencode.bencode(info_dict)).hexdigest()
# Get encoding from torrent file if available
encoding = self._metainfo_dict.get(b'encoding', None)
codepage = self._metainfo_dict.get(b'codepage', None)
if not encoding:
encoding = codepage if codepage else b'UTF-8'
# Decode 'name' with encoding unless 'name.utf-8' found.
if b'name.utf-8' in info_dict:
self._name = decode_bytes(info_dict[b'name.utf-8'])
else:
if encoding:
encoding = encoding.decode()
self._name = decode_bytes(info_dict[b'name'], encoding)
# Get list of files from torrent info
if b'files' in info_dict:
paths = {}
dirs = {}
prefix = self._name if len(info_dict[b'files']) > 1 else ''
for index, f in enumerate(info_dict[b'files']):
if b'path.utf-8' in f:
path = decode_bytes(os.path.join(*f[b'path.utf-8']))
del f[b'path.utf-8']
else:
path = decode_bytes(os.path.join(*f[b'path']), encoding)
if prefix:
path = os.path.join(prefix, path)
f[b'path'] = path
f[b'index'] = index
if b'sha1' in f and len(f[b'sha1']) == 20:
f[b'sha1'] = f[b'sha1'].encode(b'hex')
if b'ed2k' in f and len(f[b'ed2k']) == 16:
f[b'ed2k'] = f['ed2k'].encode(b'hex')
if b'filehash' in f and len(f[b'filehash']) == 20:
f[b'filehash'] = f[b'filehash'].encode(b'hex')
paths[path] = f
dirname = os.path.dirname(path)
while dirname:
dirinfo = dirs.setdefault(dirname, {})
dirinfo[b'length'] = dirinfo.get(b'length', 0) + f[b'length']
dirname = os.path.dirname(dirname)
if filetree == 2:
def walk(path, item):
if item['type'] == 'dir':
item.update(dirs[path])
else:
item.update(paths[path])
item['download'] = True
file_tree = FileTree2(list(paths))
file_tree.walk(walk)
else:
def walk(path, item):
if isinstance(item, dict):
return item
return [paths[path][b'index'], paths[path][b'length'], True]
file_tree = FileTree(paths)
file_tree.walk(walk)
self._files_tree = file_tree.get_tree()
else:
if filetree == 2:
self._files_tree = {
'contents': {
self._name: {
'type': 'file',
'index': 0,
'length': info_dict[b'length'],
'download': True,
},
},
}
else:
self._files_tree = {
self._name: (0, info_dict[b'length'], True),
}
self._files = []
if b'files' in info_dict:
prefix = ''
if len(info_dict[b'files']) > 1:
prefix = self._name
for f in info_dict[b'files']:
self._files.append({
'path': f[b'path'],
'size': f[b'length'],
'download': True,
})
else:
self._files.append({
'path': self._name,
'size': info_dict[b'length'],
'download': True,
})
def as_dict(self, *keys):
"""The torrent info as a dictionary, filtered by keys.
Args:
keys (str): A space-separated string of keys.
Returns:
dict: The torrent info dict with specified keys.
"""
return {key: getattr(self, key) for key in keys}
@property
def name(self):
"""The name of the torrent.
Returns:
str: The torrent name.
"""
return self._name
@property
def info_hash(self):
"""The calculated torrent info_hash.
Returns:
str: The torrent info_hash.
"""
return self._info_hash
@property
def files(self):
"""The files that the torrent contains.
Returns:
list: The list of torrent files.
"""
return self._files
@property
def files_tree(self):
"""A tree of the files the torrent contains.
::
{
"some_directory": {
"some_file": (index, size, download)
}
}
Returns:
dict: The tree of files.
"""
return self._files_tree
@property
def metadata(self):
"""The torrents metainfo dictionary.
Returns:
dict: The bdecoded metainfo dictionary.
"""
return self._metainfo_dict
@property
def filedata(self):
"""The contents of the .torrent file.
Returns:
str: The metainfo bencoded dictionary from a torrent file.
"""
return self._metainfo
class FileTree2(object):
"""
Converts a list of paths in to a file tree.
:param paths: The paths to be converted
:type paths: list
"""
def __init__(self, paths):
self.tree = {'contents': {}, 'type': 'dir'}
def get_parent(path):
parent = self.tree
while '/' in path:
directory, path = path.split('/', 1)
child = parent['contents'].get(directory)
if child is None:
parent['contents'][directory] = {
'type': 'dir',
'contents': {},
}
parent = parent['contents'][directory]
return parent, path
for path in paths:
if path[-1] == '/':
path = path[:-1]
parent, path = get_parent(path)
parent['contents'][path] = {
'type': 'dir',
'contents': {},
}
else:
parent, path = get_parent(path)
parent['contents'][path] = {
'type': 'file',
}
def get_tree(self):
"""
Return the tree.
:returns: the file tree.
:rtype: dictionary
"""
return self.tree
def walk(self, callback):
"""
Walk through the file tree calling the callback function on each item
contained.
:param callback: The function to be used as a callback, it should have
the signature func(item, path) where item is a `tuple` for a file
and `dict` for a directory.
:type callback: function
"""
def walk(directory, parent_path):
for path in list(directory['contents']):
full_path = os.path.join(parent_path, path).replace('\\', '/')
if directory['contents'][path]['type'] == 'dir':
directory['contents'][path] = callback(
full_path, directory['contents'][path],
) or directory['contents'][path]
walk(directory['contents'][path], full_path)
else:
directory['contents'][path] = callback(
full_path, directory['contents'][path],
) or directory['contents'][path]
walk(self.tree, '')
def __str__(self):
lines = []
def write(path, item):
depth = path.count('/')
path = os.path.basename(path)
path = path + '/' if item['type'] == 'dir' else path
lines.append(' ' * depth + path)
self.walk(write)
return '\n'.join(lines)
class FileTree(object):
"""
Convert a list of paths in a file tree.
:param paths: The paths to be converted.
:type paths: list
"""
def __init__(self, paths):
self.tree = {}
def get_parent(path):
parent = self.tree
while '/' in path:
directory, path = path.split('/', 1)
child = parent.get(directory)
if child is None:
parent[directory] = {}
parent = parent[directory]
return parent, path
for path in paths:
if path[-1] == '/':
path = path[:-1]
parent, path = get_parent(path)
parent[path] = {}
else:
parent, path = get_parent(path)
parent[path] = []
def get_tree(self):
"""
Return the tree, after first converting all file lists to a tuple.
:returns: the file tree.
:rtype: dictionary
"""
def to_tuple(path, item):
if isinstance(item, dict):
return item
return tuple(item)
self.walk(to_tuple)
return self.tree
def walk(self, callback):
"""
Walk through the file tree calling the callback function on each item
contained.
:param callback: The function to be used as a callback, it should have
the signature func(item, path) where item is a `tuple` for a file
and `dict` for a directory.
:type callback: function
"""
def walk(directory, parent_path):
for path in list(directory):
full_path = os.path.join(parent_path, path)
if isinstance(directory[path], dict):
directory[path] = callback(full_path, directory[path]) or directory[path]
walk(directory[path], full_path)
else:
directory[path] = callback(full_path, directory[path]) or directory[path]
walk(self.tree, '')
def __str__(self):
lines = []
def write(path, item):
depth = path.count('/')
path = os.path.basename(path)
path = isinstance(item, dict) and path + '/' or path
lines.append(' ' * depth + path)
self.walk(write)
return '\n'.join(lines)