From d1772d410235592b482e3b08b1863f6624d9fe6b Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 19 Feb 2023 15:52:21 +0100 Subject: Adding upstream version 2.0.3. Signed-off-by: Daniel Baumann --- deluge/ui/common.py | 564 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 564 insertions(+) create mode 100644 deluge/ui/common.py (limited to 'deluge/ui/common.py') diff --git a/deluge/ui/common.py b/deluge/ui/common.py new file mode 100644 index 0000000..21bcafd --- /dev/null +++ b/deluge/ui/common.py @@ -0,0 +1,564 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) Damien Churchill 2008-2009 +# Copyright (C) Andrew Resch 2009 +# +# 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 binascii import hexlify +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']}, + 'super_seeding': {'name': _('Super Seeding'), 'status': ['super_seeding']}, +} + +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: 'Skip', + 1: 'Low', + 2: 'Low', + 3: 'Low', + 4: 'Normal', + 5: 'High', + 6: 'High', + 7: 'High', + _('Skip'): 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, optional): The path to the .torrent file. + filetree (int, optional): The version of filetree to create (defaults to 1). + torrent_file (dict, optional): A bdecoded .torrent file contents. + + """ + + def __init__(self, filename='', filetree=1, torrent_file=None): + self._filedata = None + if torrent_file: + self._metainfo = torrent_file + elif filename: + log.debug('Attempting to open %s.', filename) + try: + with open(filename, 'rb') as _file: + self._filedata = _file.read() + except IOError as ex: + log.warning('Unable to open %s: %s', filename, ex) + return + + try: + self._metainfo = bencode.bdecode(self._filedata) + except bencode.BTFailure as ex: + log.warning('Failed to decode %s: %s', filename, ex) + return + else: + log.warning('Requires valid arguments.') + return + + # info_dict with keys decoded to unicode. + info_dict = {k.decode(): v for k, v in self._metainfo[b'info'].items()} + self._info_hash = sha(bencode.bencode(info_dict)).hexdigest() + + # Get encoding from torrent file if available + encoding = info_dict.get('encoding', None) + codepage = info_dict.get('codepage', None) + if not encoding: + encoding = codepage if codepage else b'UTF-8' + if encoding: + encoding = encoding.decode() + + # Decode 'name' with encoding unless 'name.utf-8' found. + if 'name.utf-8' in info_dict: + self._name = decode_bytes(info_dict['name.utf-8']) + else: + self._name = decode_bytes(info_dict['name'], encoding) + + # Get list of files from torrent info + self._files = [] + if 'files' in info_dict: + paths = {} + dirs = {} + prefix = self._name if len(info_dict['files']) > 1 else '' + + for index, f in enumerate(info_dict['files']): + f = {k.decode(): v for k, v in f.items()} + + if 'path.utf-8' in f: + path = decode_bytes(os.path.join(*f['path.utf-8'])) + del f['path.utf-8'] + else: + path = decode_bytes(os.path.join(*f['path']), encoding) + + if prefix: + path = os.path.join(prefix, path) + + self._files.append( + {'path': path, 'size': f['length'], 'download': True} + ) + + f['path'] = path + f['index'] = index + if 'sha1' in f and len(f['sha1']) == 20: + f['sha1'] = hexlify(f['sha1']).decode() + if 'ed2k' in f and len(f['ed2k']) == 16: + f['ed2k'] = hexlify(f['ed2k']).decode() + if 'filehash' in f and len(f['filehash']) == 20: + f['filehash'] = hexlify(f['filehash']).decode() + + paths[path] = f + dirname = os.path.dirname(path) + while dirname: + dirinfo = dirs.setdefault(dirname, {}) + dirinfo['length'] = dirinfo.get('length', 0) + f['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]['index'], paths[path]['length'], True] + + file_tree = FileTree(paths) + file_tree.walk(walk) + self._files_tree = file_tree.get_tree() + else: + self._files.append( + {'path': self._name, 'size': info_dict['length'], 'download': True} + ) + if filetree == 2: + self._files_tree = { + 'contents': { + self._name: { + 'type': 'file', + 'index': 0, + 'length': info_dict['length'], + 'download': True, + } + } + } + else: + self._files_tree = {self._name: (0, info_dict['length'], True)} + + @classmethod + def from_metadata(cls, metadata, trackers=None): + """Create a TorrentInfo from metadata and trackers + + Args: + metadata (dict): A bdecoded info section of torrent file. + trackers (list of lists, optional): The trackers to include. + + """ + if not isinstance(metadata, dict): + return + + metainfo = {b'info': metadata} + if trackers: + metainfo[b'announce'] = trackers[0][0].encode('utf-8') + trackers_utf8 = [[t.encode('utf-8') for t in tier] for tier in trackers] + metainfo[b'announce-list'] = trackers_utf8 + return cls(torrent_file=metainfo) + + 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 metainfo(self): + """Returns the torrent metainfo dictionary. + + This is the bdecoded torrent file contents. + + Returns: + dict: The metainfo dictionary. + + """ + return self._metainfo + + @property + def filedata(self): + """The contents of the .torrent file. + + Returns: + bytes: The bencoded metainfo. + + """ + if not self._filedata: + self._filedata = bencode.bencode(self._metainfo) + return self._filedata + + +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) -- cgit v1.2.3